欢迎光临
我们一直在努力

【51单片机实战】温控下位机完整实现:DS18B20采集 + 自定义二进制串口协议 + PWM声光告

文章目录

  • 1. 项目概述
    • 实现功能
    • 硬件环境
  • 2. 核心技术原理
    • 2.1 DS18B20单总线温度采集
      • 2.1.1 单总线通信基础规则
      • 2.1.2 温度读取完整流程
      • 2.1.3 ReadTemp函数逐行解析
    • 2.2 自定义二进制串口通信协议
      • 2.2.1 帧格式定义
      • 2.2.2 功能码定义
    • 2.3 串口中断接收机制
      • 为什么必须用中断?
      • 接收流程
    • 2.4 PWM蜂鸣器驱动与系统定时
      • PWM原理
      • 经典踩坑:蜂鸣器上电就响
      • 1秒定时实现
    • 2.5 自动温控与告警逻辑
  • 3. 完整工程代码(12MHz晶振)
  • 4. 上位机调试指南
    • 常用指令对照表
  • 5. 经典踩坑与解决方案
    • 坑1:串口完全收不到数据
    • 坑2:发了指令没回复
    • 坑3:蜂鸣器上电就响/关不死
    • 坑4:开启自动模式不采集温度
    • 坑5:DS18B20读出来全是0/255
  • 6. 总结

1. 项目概述

本文基于STC89C52RC单片机,实现了一套完整的主从式温控下位机系统,涵盖单总线温度采集、自定义二进制串口协议、PWM蜂鸣器驱动、自动温控告警四大核心模块。项目从硬件时序到底层协议、从中断驱动到业务逻辑完整打通,非常适合作为嵌入式入门的综合实战项目。

实现功能

  • 上位机可下发指令实时读取下位机温度数据
  • 上位机可远程控制LED、蜂鸣器的开关
  • 上位机可设置温度上下限阈值
  • 支持手动/自动工作模式切换
  • 自动模式下每秒采集一次温度,低于下限自动触发光声光告警
  • 全通信采用7字节固定帧二进制协议,带和校验,抗干扰性强
  • 硬件环境

    • 主控:STC89C52RC(12MHz晶振)
    • 传感器:DS18B20数字温度传感器(单总线)
    • 外设:LED指示灯、无源蜂鸣器
    • 通信:USB转TTL模块,波特率2400

    2. 核心技术原理

    2.1 DS18B20单总线温度采集

    单总线(1-Wire)是Dallas半导体的专利总线,所有数据和控制都通过一根线传输,物理层采用开漏输出+上拉电阻架构,任何设备都只能拉低总线,高电平靠上拉电阻实现。

    2.1.1 单总线通信基础规则
  • 主机主导:所有通信必须由主机发起,从机全程被动响应
  • 复位同步:每次发送新指令前,必须先发复位脉冲+等待从机应答
  • 时隙传输:每一位数据都对应一个时隙,每个时隙至少60μs
  • 低位先发:多字节传输时,先传低字节,再传高字节
  • 2.1.2 温度读取完整流程

    DS18B20读取温度必须分两次通信完成:

    • 第一次通信:复位 → 跳过ROM(0xCC) → 启动转换(0x44) → 等待转换完成
    • 第二次通信:复位 → 跳过ROM(0xCC) → 读暂存器(0xBE) → 读取温度字节
    2.1.3 ReadTemp函数逐行解析

    int ReadTemp(void)
    {
    unsigned char LSBVal = 0; // 温度低字节
    unsigned char MSBVal = 0; // 温度高字节
    int Val = 0; // 16位原始温度值
    // 总线就绪:拉高总线1ms,确保传感器处于空闲状态
    DQ = 1;
    Delay480us();
    Delay480us();

    // ========== 第一次通信:启动温度转换 ==========
    DQReset(); // 主机发复位脉冲(拉低480μs)
    ReadDQRespone(); // 等待从机应答脉冲,确认传感器在线
    SendByte(0xCC); // 跳过ROM指令:单设备时跳过地址匹配
    SendByte(0x44); // 启动温度转换指令:传感器开始AD转换
    Delay800ms(); // 等待转换完成:12位分辨率最长750ms

    // ========== 第二次通信:读取温度结果 ==========
    DQReset(); // 重新复位同步
    ReadDQRespone();
    SendByte(0xCC); // 再次跳过ROM
    SendByte(0xBE); // 读暂存器指令:传感器开始输出内部寄存器
    LSBVal = RecvByte();// 先读低字节(低位先发)
    MSBVal = RecvByte();// 后读高字节

    // ========== 数据换算 ==========
    Val = (MSBVal << 8) | LSBVal; // 拼接成16位原始值
    Val >>= 4; // 舍弃低4位小数,得到整数温度
    return Val;
    }

    寄存器格式说明:DS18B20默认12位分辨率,16位寄存器中高5位为符号位,中间7位为整数部分,低4位为小数部分,精度0.0625℃。右移4位直接得到整数摄氏温度,负温以补码存储,int类型会自动识别符号。


    2.2 自定义二进制串口通信协议

    采用类Modbus的固定帧长二进制协议,每帧7字节,相比字符串协议传输效率更高、抗干扰能力更强,适合工业级嵌入式通信。

    2.2.1 帧格式定义
    字节序号字段固定值/含义说明
    0 帧头 0xAA 标记一帧数据的起始,用于同步
    1 从机地址 0x01 总线上可挂载多设备,通过地址区分
    2 功能码 0x01~0x07 标识本条指令的功能
    3 数据位1 参数高字节 传递功能参数
    4 数据位2 参数低字节 传递功能参数
    5 和校验 计算值 帧头+地址+功能码+数据1+数据2+帧尾,取低8位
    6 帧尾 0xBB 标记一帧数据的结束
    2.2.2 功能码定义
    功能码功能参数说明
    0x01 读取当前温度 数据位无意义,应答帧返回温度值
    0x02 LED开关控制 数据位2:0x00关,0x01开
    0x03 蜂鸣器开关控制 数据位2:0x00关,0x01开
    0x05 设置温度上限 数据位1+数据位2:16位阈值
    0x06 设置温度下限 数据位1+数据位2:16位阈值
    0x07 工作模式切换 数据位2:0x00手动,0x01自动

    2.3 串口中断接收机制

    串口接收采用中断接收+主循环处理的经典架构,这是嵌入式串口通信最可靠的实现方式。

    为什么必须用中断?

    上位机的指令是异步随机到达的,如果用主循环查询接收,很容易因为主循环正在执行延时、读温度等操作错过字节,造成丢包。

    • 中断接收:硬件收到一个完整字节就自动触发中断,立刻把数据存入缓冲区,绝对不会丢包
    • 主循环处理:中断只做最精简的数据搬运,复杂的协议解析、逻辑处理放在主循环空闲时执行
    接收流程
  • 串口硬件逐位接收,收齐一个字节后自动置RI标志
  • 触发串口中断,将字节存入接收缓冲区,计数+1
  • 收满7字节后标记「收到完整帧」
  • 主循环检测到完整帧标志,执行协议解析和指令处理

  • 2.4 PWM蜂鸣器驱动与系统定时

    使用定时器0同时实现两个功能:PWM驱动蜂鸣器 + 1秒系统定时,实现定时器资源复用。

    PWM原理

    通过定时器中断周期性翻转引脚电平,控制一个周期内高电平的占比,即可调节蜂鸣器的音量和音调。本项目采用100级占空比,每10次定时器溢出完成一个PWM周期。

    经典踩坑:蜂鸣器上电就响

    很多人写PWM都会遇到Duty=0时蜂鸣器还微响的问题,根源是边界判断错误:

    // 错误写法:curcnt清零为0时,0 <= 0 成立,会输出一个窄脉冲
    Buzzer = (curcnt <= Duty) ? 1 : 0;
    // 正确写法:Duty=0时完全没有输出
    Buzzer = (curcnt < Duty) ? 1 : 0;

    1秒定时实现

    利用定时器中断累计次数实现秒级计时,无需额外定时器。PWM频率为1047Hz时,每秒约产生10470次中断,累计到阈值就置位秒标志。


    2.5 自动温控与告警逻辑

    自动模式下,系统每秒自动采集一次温度,与设定阈值对比:

    • 温度 < 下限:点亮LED + 启动蜂鸣器,声光告警
    • 温度 ≥ 下限:关闭所有告警,恢复正常状态

    注意:自动模式默认只做本地采集和告警,不会主动向上位机上报温度;如需主动上报,可在定时采集逻辑中添加发送帧函数。


    3. 完整工程代码(12MHz晶振)

    #include <reg52.h>
    #include <intrins.h>
    #include <string.h>
    /************************* 硬件引脚定义 *************************/
    sbit DQ = P3^7; // DS18B20单总线
    sbit Led1 = P2^0; // LED指示灯
    sbit Buzzer = P2^1; // 蜂鸣器
    /************************* 全局变量定义 *************************/
    // 串口协议接收
    volatile unsigned char RecvBuff[7]; // 7字节固定帧缓冲区
    volatile unsigned char RecvLen = 0; // 当前接收字节数
    volatile unsigned char IsFrame = 0; // 收到完整帧标志
    // 系统状态
    unsigned char AutoMode = 0; // 0=手动模式,1=自动模式
    int TempHigh = 30; // 温度上限,默认30℃
    int TempLow = 10; // 温度下限,默认10℃
    int CurTemp = 0; // 当前采集温度
    unsigned char SecFlag = 0; // 1秒定时标志
    // PWM蜂鸣器参数
    unsigned char Duty = 0;
    unsigned char ReloadTH0 = 0;
    unsigned char ReloadTL0 = 0;
    #define BEEP_FREQ 1047 // 蜂鸣器音调频率
    #define BEEP_DUTY 50 // 蜂鸣器占空比
    /************************* 软件延时函数 @12MHz *************************/
    void Delay800ms(void)
    {
    unsigned char data i, j, k;
    nop();
    i = 6; j = 143; k = 129;
    do {
    do { while (–k); } while (–j);
    } while (–i);
    }
    void Delay480us(void)
    {
    unsigned char data i;
    nop();
    i = 191;
    while (–i);
    }
    void Delay10us(void)
    {
    unsigned char data i;
    i = 2;
    while (–i);
    }
    /************************* DS18B20温度驱动 *************************/
    void DQReset(void)
    {
    DQ = 0;
    Delay480us();
    DQ = 1;
    }
    void ReadDQRespone(void)
    {
    Delay10us();
    while (DQ);
    while (!DQ);
    }
    void SendByte(unsigned char TmpByte)
    {
    unsigned char n;
    for (n = 0; n < 8; n++)
    {
    if (TmpByte & (0x01 << n))
    {
    DQ = 0; nop(); nop();
    DQ = 1;
    Delay10us();Delay10us();Delay10us();
    Delay10us();Delay10us();Delay10us();
    }
    else
    {
    DQ = 0;
    Delay10us();Delay10us();Delay10us();
    Delay10us();Delay10us();Delay10us();
    DQ = 1; nop(); nop();
    }
    }
    }
    unsigned char RecvByte(void)
    {
    unsigned char TmpByte = 0x00, n;
    for (n = 0; n < 8; n++)
    {
    DQ = 0; nop(); nop();
    DQ = 1;
    Delay10us();
    if (DQ) TmpByte |= (0x01 << n);
    Delay10us();Delay10us();Delay10us();
    Delay10us();Delay10us();Delay10us();
    }
    return TmpByte;
    }
    int ReadTemp(void)
    {
    unsigned char LSB, MSB;
    int Val;
    DQ = 1;
    Delay480us(); Delay480us();

    // 启动温度转换
    DQReset(); ReadDQRespone();
    SendByte(0xCC); SendByte(0x44);
    Delay800ms();

    // 读取温度寄存器
    DQReset(); ReadDQRespone();
    SendByte(0xCC); SendByte(0xBE);
    LSB = RecvByte(); MSB = RecvByte();

    Val = (MSB &lt;&lt; 8) | LSB;
    Val &gt;&gt;= 4;
    return Val;
    }
    /************************* 串口通信模块 *************************/
    void UartInit(void)
    {
    SCON &= ~(0x03 << 6);
    SCON |= (0x01 << 6); // 串口方式1:8位UART
    REN = 1; // 允许接收
    PCON &amp;= ~(0x01 &lt;&lt; 7); // 波特率不加倍

    TMOD &amp;= ~(0x03 &lt;&lt; 4);
    TMOD |= (0x01 &lt;&lt; 5); // 定时器1模式2:8位自动重装
    TH1 = 0xF3; TL1 = 0xF3; // 12MHz下2400波特率
    TR1 = 1;

    ES = 1; EA = 1; // 开启串口中断
    }
    void UartSendByte(unsigned char dat)
    {
    SBUF = dat;
    while (!TI);
    TI = 0;
    }
    void SendFrame(unsigned char cmd, unsigned char data1, unsigned char data2)
    {
    unsigned char buf[7], checksum, i;
    buf[0] = 0xAA;
    buf[1] = 0x01;
    buf[2] = cmd;
    buf[3] = data1;
    buf[4] = data2;
    buf[6] = 0xBB;
    checksum = buf[0] + buf[1] + buf[2] + buf[3] + buf[4] + buf[6];
    buf[5] = checksum;
    for (i = 0; i &lt; 7; i++)
    UartSendByte(buf[i]);
    }
    void UartHandler(void) interrupt 4
    {
    if (RI)
    {
    RecvBuff[RecvLen] = SBUF;
    RI = 0;
    RecvLen++;
    if (RecvLen >= 7) IsFrame = 1;
    }
    }
    /************************* 定时器0 + PWM蜂鸣器 *************************/
    void SetPwmFreq(unsigned long freq, unsigned char TmpDuty)
    {
    unsigned long value;
    value = 65536UL – ((1000000UL / freq) / 10UL);
    ReloadTH0 = value / 256;
    ReloadTL0 = value % 256;
    TH0 = ReloadTH0; TL0 = ReloadTL0;
    Duty = TmpDuty;
    }
    void BeepOn(void)
    {
    Duty = BEEP_DUTY;
    }
    void BeepOff(void)
    {
    Duty = 0;
    Buzzer = 0;
    }
    void Timer0Handler(void) interrupt 1
    {
    static unsigned char curcnt = 0;
    static unsigned int sec_cnt = 0;
    TH0 = ReloadTH0;
    TL0 = ReloadTL0;

    // PWM占空比控制
    curcnt += 10;
    if (curcnt &gt;= 100) curcnt = 0;
    Buzzer = (curcnt &lt; Duty) ? 1 : 0;

    // 1秒计时
    sec_cnt++;
    if (sec_cnt &gt;= 10470)
    {
    sec_cnt = 0;
    SecFlag = 1;
    }
    }
    void Timer0Init(void)
    {
    TMOD &= ~0x03;
    TMOD |= 0x01;
    SetPwmFreq(BEEP_FREQ, 0);
    ET0 = 1; EA = 1;
    TR0 = 1; // 定时器默认常开,保证秒计数正常
    }
    /************************* 协议解析与处理 *************************/
    void ProcProtocol(void)
    {
    unsigned char checksum;
    if (RecvBuff[0] != 0xAA || RecvBuff[6] != 0xBB)
    goto END;

    checksum = RecvBuff[0] + RecvBuff[1] + RecvBuff[2] + RecvBuff[3] + RecvBuff[4] + RecvBuff[6];
    if (checksum != RecvBuff[5])
    goto END;

    switch (RecvBuff[2])
    {
    case 0x01:
    CurTemp = ReadTemp();
    SendFrame(0x01, CurTemp&gt;&gt;8, CurTemp&amp;0xFF);
    break;

    case 0x02:
    Led1 = (RecvBuff[4] == 0x01) ? 1 : 0;
    SendFrame(0x02, 0x00, RecvBuff[4]);
    break;

    case 0x03:
    (RecvBuff[4] == 0x01) ? BeepOn() : BeepOff();
    SendFrame(0x03, 0x00, RecvBuff[4]);
    break;

    case 0x05:
    TempHigh = (RecvBuff[3] &lt;&lt; 8) | RecvBuff[4];
    SendFrame(0x05, RecvBuff[3], RecvBuff[4]);
    break;

    case 0x06:
    TempLow = (RecvBuff[3] &lt;&lt; 8) | RecvBuff[4];
    SendFrame(0x06, RecvBuff[3], RecvBuff[4]);
    break;

    case 0x07:
    AutoMode = RecvBuff[4];
    if (AutoMode) CurTemp = ReadTemp();
    SendFrame(0x07, 0x00, RecvBuff[4]);
    break;

    default: break;
    }
    END:
    memset((void*)RecvBuff, 0, sizeof(RecvBuff));
    RecvLen = 0;
    IsFrame = 0;
    }
    /************************* 自动模式逻辑 *************************/
    void AutoModeProc(void)
    {
    if (!AutoMode || !SecFlag) return;
    SecFlag = 0;
    CurTemp = ReadTemp();

    if (CurTemp &lt; TempLow)
    {
    Led1 = 1;
    BeepOn();
    }
    else
    {
    Led1 = 0;
    BeepOff();
    }
    }
    /************************* 主函数 *************************/
    int main(void)
    {
    UartInit();
    Timer0Init();
    Led1 = 0;
    BeepOff();

    while (1)
    {
    if (IsFrame) ProcProtocol();
    AutoModeProc();
    }
    return 0;
    }


    4. 上位机调试指南

    使用串口调试助手,按以下配置即可测试:

    • 波特率:2400
    • 数据位:8
    • 停止位:1
    • 校验位:无
    • 发送/接收:HEX模式

    常用指令对照表

    功能发送指令(7字节)
    读取当前温度 AA 01 01 00 00 67 BB
    打开LED AA 01 02 00 01 69 BB
    关闭LED AA 01 02 00 00 68 BB
    打开蜂鸣器 AA 01 03 00 01 6A BB
    关闭蜂鸣器 AA 01 03 00 00 69 BB
    设置温度上限30℃ AA 01 05 00 1E 89 BB
    设置温度下限10℃ AA 01 06 00 0A 76 BB
    开启自动模式 AA 01 07 00 01 6E BB
    关闭自动模式 AA 01 07 00 00 6D BB

    5. 经典踩坑与解决方案

    坑1:串口完全收不到数据

    • 排查顺序:先确认TX/RX交叉连接、GND共地 → 回环测试确认串口线正常 → 检查REN、ES、EA三个中断开关是否全开
    • 最常见原因:TXD和RXD接反,或者忘记共地

    坑2:发了指令没回复

    • 排查顺序:先注释掉校验判断,看能不能回复 → 能回复就是校验算法不一致 → 不能回复就是帧解析逻辑问题
    • 注意:校验算法必须收发完全一致,是包含帧尾还是不包含帧尾要提前约定

    坑3:蜂鸣器上电就响/关不死

    • 90%是边界bug:把<=改成<即可解决
    • 剩下10%:蜂鸣器是低电平触发,输出逻辑写反了,翻转一下即可

    坑4:开启自动模式不采集温度

    • 核心原因:定时器0只在蜂鸣器开启时才运行,温度正常时定时器停了,秒计数也就停了
    • 解决方案:定时器默认常开,蜂鸣器只通过占空比控制,不关闭定时器

    坑5:DS18B20读出来全是0/255

    • 时序问题:延时函数和晶振不匹配,单总线时序误差超过10%就会通信失败
    • 硬件问题:DQ引脚没有接上拉电阻,或者接线错误

    6. 总结

    本项目虽然代码量不大,但覆盖了嵌入式开发最核心的基础能力:单总线协议时序、串口中断通信、自定义协议设计、定时器PWM、状态机逻辑。理解了这套代码,就掌握了8位单片机外设驱动和通信开发的核心方法论,后续扩展数码管显示、按键输入、风扇控制等功能都可以快速复用这套框架。

    赞(0)
    未经允许不得转载:171主机测评 » 【51单片机实战】温控下位机完整实现:DS18B20采集 + 自定义二进制串口协议 + PWM声光告
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址