简介:面向51单片机初学者与课程设计人群,这套温控系统以AT89C52为主控,搭配DS18B20数字温度传感器,完成从温度采集、数据处理到显示与控制的完整闭环。工程从DS18B20初始化、1-Wire单总线读写入手,解析温度寄存器后由六位数码管显示整数与小数,并依据预设上下限触发加热/冷却负载控制,所有逻辑均以C语言实现;随包提供Proteus工程,可在仿真中观察运行效果,再通过hex文件烧录到实体芯片。资源压缩后仅28KB,共3个文件,包含C源码、Proteus仿真工程和编译生成的烧录文件,内容紧凑、便于直接复用或二次开发。已有7000余人参与学习,适合温控类实验、课程设计及入门项目参考,也便于扩展为恒温箱或环境监控方案。
1. 51单片机温控系统:DS18B20单总线方案为什么比模拟传感器省心
用51单片机做温控,第一件事往往是选温度传感器。DS18B20是单总线数字传感器,硬件上只需一根数据线,程序里用GPIO模拟时序把温度读出来,省掉了运放调理、比较器翻转和ADC采样这一整套模拟链路。对恒温箱、孵化器、水族箱加热控制这类场景,它的-55℃到+125℃量程和±0.5℃精度足够用。AT89C52的8KB Flash和32个IO口也刚好扛得住传感器驱动、LCD显示、按键和继电器控制。本文按硬件搭建、单总线时序、控制策略、排错和精度校准五条线展开,把从接线到闭环控制的完整流程盘一遍,适合正在做课设、样机原型,或者想把老51库存用起来的开发者。
2. 硬件选型与电路搭建:AT89C52最小系统与DS18B20四线接线
2.1 主控选型:AT89C52的资源边界
AT89C52是经典8位Flash单片机,8KB Flash、256B RAM、32个IO、3个定时器,没有内置ADC。它有内置UART,方便调试时打印温度;3个定时器可以用来做按键消抖和LCD时序。DS18B20并不要求ADC,数据线就是一根开漏输出的DQ,单片机用一个普通IO模拟时序即可。真正吃资源的是执行机构、显示和交互部分,所以选型时要先盘一遍IO占用,再决定是否换用带更多端口的增强型51。
常见做法是给AT89C52搭配12MHz晶振,一个机器周期1us,写延时函数时按这个基数估算。如果换成STC这类1T增强型51,周期变成约0.5us甚至更快,延时要整体放大,否则单总线时序会翻车。这也是很多新手从AT89C52工程移植到1T单片机后温度死活读不出来的主要原因。我一般会在原理图阶段就把主频标注在延时函数旁边,避免后面调时序时反复猜。
2.2 DS18B20引脚功能与接线约束
DS18B20常见TO-92封装,3个引脚:GND、DQ、VDD。GND接地,DQ是单总线数据线,VDD是电源引脚。它可以外部供电,也可以寄生供电。寄生供电模式下VDD直接接地,工作电流从DQ线上窃取,只有3根线就能跑,但前提是转换电流不大、总线长度短。我的建议是能用外部供电就别省这一根线,寄生供电在继电器动作瞬间经常被电源跌落干扰,导致读回来的温度值跳变。
外部供电时VDD接3.0V到5.5V,DQ经一只4.7k电阻上拉到VCC,GND和单片机共地。如果DS18B20离单片机超过1米,上拉电阻可以降到2.2k,同时用双绞线或者屏蔽线走DQ和GND。单总线本身是开漏结构,漏极开路输出需要外部上拉才能恢复高电平,电阻太小会拉高功耗,太大导致边沿变缓。
2.3 外围电路设计:上拉电阻、去耦与继电器驱动
传感器部分最关键的三个外围元件是上拉电阻R1、去耦电容C1、继电器续流二极管D1。R1选4.7k,C1选0.1uF并尽量靠近DS18B20的VDD和GND引脚,这样能压住数据线高频尖峰。继电器驱动不要用IO直接推线圈,常见做法是P2.1接一只1k电阻到S8050基极,发射极接地,集电极接继电器线圈一端,线圈另一端接电源,线圈两端反向并联一只1N4148。续流二极管缺了的话,继电器断开瞬间的反电动势轻则让单片机复位,重则烧IO口,这是温控项目里最典型的“玄学”故障。
下图接线表可以当原理图自查清单用:
| 信号 | 单片机引脚 | 连接目标 |
|---|---|---|
| 温度数据DQ | P2.0 | DS18B20 DQ,上拉电阻4.7k到VCC |
| 继电器控制 | P2.1 | 1k电阻后接S8050基极 |
| LCD RS | P2.5 | LCD1602 RS |
| LCD EN | P2.7 | LCD1602 E |
| LCD D4-D7 | P0.0-P0.3 | LCD1602数据线(4线模式) |
| 按键1/2/3 | P3.2/P3.3/P3.4 | 按键另一端接GND |
LCD1602用4线模式可以省4个IO。P0口内部没有上拉,接LCD数据线时需要在P0外部加10k排阻上拉,否则高电平输出能力不够。我把按键放在P3口,是因为P3口带内部上拉,按键接GND即可,不需要额外接电阻。整套系统接线完毕后,先用万用表确认VCC和GND没有短路,再上电测试。
3. 单总线时序与底层驱动:从复位脉冲到CRC校验
3.1 单总线协议核心时序参数
DS18B20单总线时序的核心是复位、写时隙、读时隙三件事。主机先拉低DQ至少480us,然后释放,DS18B20会在60到240us内拉低总线作为存在脉冲;主机在释放后约60us采样DQ,读到低电平说明器件在线。写时序时,主机拉低DQ表示一个时隙开始,写0要保持低电平60到120us,写1则拉低1到15us后释放。读时序时,主机拉低DQ约1us后释放,然后在15us内采样,DS18B20靠把总线拉低来表示数据位0。
延时参数决定了时序是否合法,我在12MHz AT89C52下常用这张表做基准:
| 操作 | 最小 | 典型 | 最大 |
|---|---|---|---|
| 复位低电平 | 480us | 600us | 960us |
| 复位释放等待 | 480us | — | — |
| 存在脉冲低电平 | 60us | — | 240us |
| 写0低电平 | 60us | — | 120us |
| 写1低电平 | 1us | 10us | 15us |
| 读采样点 | 1us | 10us | 15us |
这两个时隙之间还必须留至少1us恢复时间,否则连续读写的时候位序会错乱。实际上C51写延时函数很难精确到微秒级,一般用循环加NOP凑,最后用逻辑分析仪或者示波器抓波形确认。保险的做法是让延时落在典型值附近,比如复位低电平做到600us左右,写1低电平做到10us左右,给时序余量留足。
3.2 ROM命令与温度数据格式
单总线上可以挂多个DS18B20,每个芯片有64位ROM编码。单点测温时可以直接发送跳过ROM命令0xCC,省去匹配64位序列号的过程。启动温度转换用0x44,读暂存器用0xBE。常用命令如下:
| 命令 | 代码 | 作用 |
|---|---|---|
| 跳过ROM | 0xCC | 不匹配ROM,直接操作总线上唯一器件 |
| 启动温度转换 | 0x44 | 触发一次温度转换,12位模式下约750ms |
| 读暂存器 | 0xBE | 读取9字节暂存器数据 |
暂存器头两个字节是温度值。16位温度寄存器格式是:高字节的高5位是符号位,接着是整数位,低字节后4位是小数位。12位分辨率下,最低位代表0.0625℃,所以把16位有符号数除以16就得到实际温度。负温度在寄存器里是补码形式,不能直接当有符号数做除法,需要先判断符号位,为1时取反加1再乘以0.0625,最后加负号。
3.3 C语言驱动实现:复位、读写时隙
下面这段是12MHz AT89C52平台上的单总线底层驱动,只保留关键逻辑:
#include <reg52.h> #include <intrins.h> sbit DQ = P2^0; void DelayUs(unsigned char t) { while (t--) { _nop_(); _nop_(); // 12MHz下约等于2us,具体以实测为准 } } unsigned char DS18B20_Reset(void) { unsigned char presence; DQ = 0; DelayUs(250); // 拉低约500us,满足最小480us DQ = 1; DelayUs(30); // 释放后等约60us presence = DQ; // 采样存在脉冲,0表示有器件应答 DelayUs(50); // 等待整个时隙结束 return presence; } void DS18B20_WriteBit(unsigned char bitval) { DQ = 0; // 时隙开始 _nop_(); if (bitval) { DQ = 1; // 写1先释放,再持续到结束 } DelayUs(15); // 低电平维持约30us DQ = 1; DelayUs(15); } unsigned char DS18B20_ReadBit(void) { unsigned char bitval; DQ = 0; // 启动读时隙 _nop_(); DQ = 1; // 释放总线 _nop_(); bitval = DQ; // 在1us到15us窗口内采样 DelayUs(15); return bitval; }逻辑上,复位函数返回的presence变量是个关键判断:读到0说明DS18B20在线,读到1说明总线空或者器件没起来。很多排查工作第一步就是看这个返回值。WriteBit里写0和写1的区别在于低电平是否贯穿整个时隙,写1时低电平只持续很短的几us就释放,让上拉电阻把总线拉高。ReadBit必须在拉低释放后的15us内完成采样,太晚读到的是总线释放后的高电平,会稳定地读出1。
字节级读写和温度读取再往下封装一层:
unsigned char DS18B20_ReadByte(void) { unsigned char i, val = 0; for (i = 0; i < 8; i++) { val >>= 1; if (DS18B20_ReadBit()) { val |= 0x80; } } return val; } void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DS18B20_WriteBit(dat & 0x01); dat >>= 1; } } int DS18B20_ReadTemp(void) { unsigned char low, high; int temp; DS18B20_Reset(); DS18B20_WriteByte(0xCC); // 跳过ROM,单点测温 DS18B20_WriteByte(0x44); // 启动温度转换 DelayMs(750); // 等12位转换完成 DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); // 读暂存器 low = DS18B20_ReadByte(); high = DS18B20_ReadByte(); temp = (high << 8) | low; return temp; }这段代码里,DelayMs(750)是必须的,因为12位分辨率转换时间典型值是750ms。如果读完马上又发读命令,拿到的可能还是上一次的旧值,甚至上电默认的85℃。读暂存器时只读前两个字节就够了,后面7个字节分别是报警阈值、配置寄存器和CRC,单点非工业场景可以暂时忽略。temp是16位有符号数,正负数换算在应用层用浮点做,或者用整数乘10避免浮点库过重。
4. 温控策略与显示交互:滞回控制、LCD1602显示与按键设定
4.1 阈值控制 vs 滞回控制
最简单的温控逻辑是:温度低于下限就开加热,高于上限就关加热。但如果把上下限设成同一个值,比如25℃开、25℃关,继电器的动作频率会非常高,传感器读数只要波动0.1℃就能让继电器咔哒咔哒乱跳。常用做法是滞回控制,也就是把开启点设在目标值减回差,关闭点设在目标值加回差,两者之间有一段缓冲区。目标25℃、回差2℃时,温度降到24℃开加热,升到26℃才关加热,继电器动作次数少很多。
对比一下两种策略:
| 策略 | 开启条件 | 关闭条件 | 继电器动作频率 |
|---|---|---|---|
| 简单阈值 | 温度<25℃ | 温度>=25℃ | 高,容易被噪声干扰 |
| 滞回控制 | 温度<24℃ | 温度>26℃ | 低,回差越大动作越少 |
回差选多大取决于被控对象。水箱加热回差1到2℃够用,空气温箱因为热惯性大,回差建议放到2到5℃。回差太大温度波动明显,太小继电器频繁动作,这个是实际调出来的经验值,不是算出来的。程序里我用一个加热状态位记录当前是否在加热,只有跨越对应边界才翻转状态,逻辑干净也容易加故障保护。
4.2 LCD1602显示与按键设定流程
LCD1602两行显示,第一行显示当前温度,第二行显示设定值和加热状态。4线模式下初始化时序略繁琐,完整代码里一般会带,核心是初始化后每次写数据先判断忙标志,或者干脆加固定延时绕过忙检测。显示格式我习惯做成:
T: 25.3 C SET:25.0 HEAT:ON按键设计三颗:模式切换键、加键、减键。模式切换在运行状态和设定状态之间切换,加键减键只在设定状态下生效。按键扫描放主循环里,10ms消抖一次,避免一次按下被当成多次触发。设定温度以0.1℃为单位,内部用整数保存,比如25.0℃记为250,避免浮点比较误差。
4.3 主循环集成代码
下面给一个控制主循环的骨架,包含温度读取、滞回控制、显示和按键扫描的调用关系:
#define TARGET_DEFAULT 250 // 默认目标25.0℃,单位0.1℃ #define HYSTERESIS_DEFAULT 20 // 默认回差2.0℃ int set_temp = TARGET_DEFAULT; int hyst = HYSTERESIS_DEFAULT; bit heat_on = 0; void ControlLoop(void) { int cur = DS18B20_ReadTempX10(); // 返回0.1℃整数温度 if (heat_on) { if (cur >= set_temp + hyst) { heat_on = 0; RELAY = 0; // 关加热 } } else { if (cur <= set_temp - hyst) { heat_on = 1; RELAY = 1; // 开加热 } } LcdShow(cur, set_temp, heat_on); KeyScan(); // 消抖后更新目标温度和回差 }ReadTempX10是自定义函数,把DS18B20原始值乘10除以16得到0.1℃精度的整数,比如253表示25.3℃。这样比直接用浮点快,也不会出现浮点库占用过多Flash的问题。滞回比较用的是先判断状态再判断边界,温度落在回差带内时状态保持不变,这是滞回控制代码实现的精髓,不要写成每次都独立判断上下限。
主循环里如果每一步都在DelayMs(750)上死等,按键扫描就会被卡住750ms,手感很差。解决思路是用状态机:主循环里先发出温度转换命令,然后不阻塞,每轮扫描按键和刷新显示,等到转换完成的标志位或者定时器溢出后再去读温度。工程包里我按这个思路写了非阻塞版本,逻辑比上面的骨架复杂一些,但实际体验好很多,尤其是加装蜂鸣器和手动开关的时候。
5. 常见问题排查:时序、校验、继电器振荡与温度跳变
5.1 复位应答失败:上拉电阻和总线电容
现象是程序卡在等待存在脉冲那里,或者DS18B20_Reset返回值始终为1,温度永远读不到。常见原因有三个:DQ忘了接上拉电阻,或者上拉电阻用了10k以上导致总线边沿太缓;DS18B20离单片机太远,总线寄生电容过大;VDD和GND接反。
解决方法是先确认VDD和GND顺序,拿到TO-92封装的DS18B20时正面对着引脚朝下,从左到右是GND、DQ、VDD,我在第一次画板时就把引脚顺序看反过。然后换4.7k上拉,总线缩短到30cm以内再试。手头有逻辑分析仪的话,直接抓复位波形看存在脉冲的宽度,比盲改代码快得多。
5.2 温度读出来恒为85℃:上电时序与分辨率
现象是温度读数固定显示85.0℃左右,不随环境变化。这是DS18B20上电时暂存器的默认值,0x0550对应85℃,只能说明寄存器被读出来了,但温度转换根本没完成。原因往往是程序只发了0xCC和0xBE,没有先发0x44启动转换,或者发了0x44之后立即读,没有等750ms。
解决方法是严格按“复位、跳过ROM、启动转换、等待750ms、复位、跳过ROM、读暂存器”的流程走一遍。如果用了9位分辨率,等待时间可以缩短到94ms左右,但工程上不值得为省这600ms去改分辨率。有了这个流程后,85℃这个数就不再是玄学,它反而是识别转换流程有没有执行到位的特征值。
5.3 继电器频繁吸合:滞回区间与滤波
现象是继电器短时间反复通断,咔哒声连成一片。原因很明显:回差设为零或者太小,温度读数又因为DS18B20最低位抖动而上下跳,导致控制逻辑反复翻转。还有个容易被忽略的原因是有加热棒、水泵这类感性负载时,继电器动作瞬间干扰DS18B20的读数,形成“干扰-误判-动作-再干扰”的正反馈。
解决方法是先把回差拉到3℃以上,确认不再抖动后逐步缩小。同时对DS18B20读数做滑动滤波,取最近5次去掉最大最小值后的平均,滤波窗口不要太长,否则温度响应变慢。继电器驱动电路上的续流二极管也必须检查,感性负载不带续流是干扰读数的常见源头。
5.4 温度跳变异常:数据线干扰和接地问题
现象是温度在正常值和-55℃或125℃之间跳,甚至出现正负交替的乱码。DS18B20的测量量程是-55℃到125℃,出现这种边界值说明读到了非法数据。常见原因包括DQ线靠近220V交流走线,继电器线圈反电动势没有吸收干净,以及DS18B20地和单片机地之间电位差过大。
解决方法是数据线远离强电和继电器,必须交叉时走垂直方向;继电器线圈补上续流二极管;单总线上拉电阻从4.7k适当减小到2.2k。如果设备有电机或开关电源,ds18b20供电不要直接从继电器电源取,单独从单片机5V走,地线单点汇接。这个排查顺序我一般是从供电到接地再到干扰源。
6. 进阶验证与精度校准:三个能直接落地的实测技巧
6.1 用串口打印原始寄存器值
调试温度误差时,只看着LCD上的浮点温度不够,我习惯先把16位原始值从串口打出来。串口波特率9600,发送十六进制和换算后的温度:
int raw = DS18B20_ReadTemp(); float t = (raw & 0x8000) ? (float)(((~raw) + 1) & 0xFFFF) / -16.0f : (float)raw / 16.0f; printf("raw=0x%04X temp=%.2f\r\n", raw, t);用这份原始值去对照数据手册的寄存器格式,能区分是传感器问题还是换算问题。符号位处理是负温度最容易写错的地方,正数直接除16,负数必须按补码取反加一后再处理。
6.2 冰水混合物与沸水两点校准
DS18B20出厂精度在-10℃到85℃范围内是±0.5℃,但长期使用后还是值得做一次端点校准。把探头插入冰水混合物,稳定后记录读数t_ice;再插入沸水,记录t_boil。按两点线性修正:
#define CAL_ICE 0.2f #define CAL_BOIL 99.8f float Calibrate(float raw_temp) { return (raw_temp - CAL_ICE) * 100.0f / (CAL_BOIL - CAL_ICE); }修正量一般只有零点几度,别指望把0.0625℃分辨率的芯片硬校准出更高的绝对精度。气温海拔不同沸水温度也有差异,追求精准时要用标准温度计做参照。
6.3 探头脱落与断线保护
温控系统最怕的不是温度偏差,而是探头脱落导致加热失控。DS18B20的DQ断路时读回的数据通常是全1,换算出来接近0℃以下非法值;DQ对地短路则可能稳定读0℃。程序里加一道防线:温度小于-50℃或大于125℃时,直接关断继电器并显示错误码。
从那以后,我每次做温控验证都会强制走一遍三个步骤:先串口看原始值确认时序,再冰水沸水做端点校准,最后拔掉传感器验证保护逻辑真的能触发。这套流程能挡住绝大多数现场翻车的情况,希望帮到你。
本文还有配套的精品资源,点击获取