基于AT89C51的DS18B20多点温度检测系统设计 简介面向单片机课程设计与毕业设计场景这份基于AT89C51的多点温度检测系统设计资料完整解决了双点温度采集、LCD显示与系统搭建等问题。方案选用Dallas DS18B20单总线数字温度传感器结合AT89C51单片机进行数据采集并通过1602LCD液晶循环显示两路温度文档对DS18B20内部结构、温度数据格式、配置寄存器分辨率设置、高速暂存存储器及与单片机的单总线连接方式都有详细说明涵盖复位、跳过ROM、启动温度变换与读取温度等关键时序。资源为1个doc文件容量仅199KB内含完整设计报告、系统原理框图以及附源程序代码从绪论、硬件设计、软件设计、系统仿真到设计总结与参考文献结构完整便于直接参考和二次修改。已有1650人学习适合需要完成单片机温度检测课程设计或入门单总线测温应用的读者。1. 多点温度检测系统到底是什么两路DS18B20、一片1602和51的经典组合做多点温度检测系统最顺手的路子不是拿 NTC 接一堆 ADC 通道而是用带单总线接口的数字温度传感器直接读。这套基于 AT89C51 的多点温度检测系统设计就是用一颗 51 单片机同时挂两个 DS18B20再通过 1602LCD 循环显示两路温度资源包里给了完整源程序、原理图和 Proteus 仿真工程。它解决的是“两个点各是多少度、还要让用户直接看到数值”这一类需求比如冰箱冷藏室和冷冻室的温度、机柜内部多测点温度巡查。适合课程设计复现、单片机入门练手也适合给工业温控系统做前期功能验证。真正劝退新手的不是接线而是单总线时序和温度数据的拆分处理这两块我会把参数和坑都拆开讲。2. 硬件选型与单总线机理为什么两点测温没走 ADC 老路2.1 主控选型AT89C51 的资源够不够用这套设计选 AT89C51 做主控制器最直接的原因是它和工业标准 MCS-51 指令集、引脚完全兼容外围电路非常省事。芯片内部带 4K 字节可编程闪速程序存储器循环写擦次数约 1000 次写程序调试完全够用128×8 位内部数据存储器32 条可编程 I/O 线两个 16 位定时器/计数器六个中断源还有可编程串行通道和低功耗闲置、掉电模式。对“温度采集LCD 显示”这个场景来说I/O 数量和 Flash 容量都富余。从资源占用上看这套设计实际用掉的资源很少两个 DS18B20 各占一个 I/O 口P1.7 和 P1.61602LCD 数据总线占 P0 口 8 位控制线 RS、RW、EN 占 P2.0、P2.1、P2.2剩余的 I/O 还能继续扩展按键、蜂鸣器或者报警灯。12MHz 晶振提供 1us 级机器周期做单总线延时和 LCD 时序控制都比较顺手。相比用 NTC 热敏电阻加 ADC 的方式DS18B20 直接把温度转成数字量省掉了信号调理电路和校准环节这也是整个系统能保持“单片机最小系统传感器LCD”结构精简的根本原因。2.2 DS18B20 单总线工作原理与读取流程DS18B20 是一线器件支持“单总线协议”即数据线和控制线共用一根 I/O 口线。测温范围 -55℃125℃在 -10℃85℃ 范围内精度 ±0.5℃本设计把测温范围限定在 -30℃99℃显示精度 0.1℃落在传感器的最佳精度区间内。芯片内部有四个主要数据部件64 位激光 ROM、温度灵敏元件、非易失性温度报警触发器 TH 和 TL、配置寄存器。64 位激光 ROM 从高位到低位依次是 8 位 CRC 校验、48 位序列号、8 位家族代码0x28H出厂时写好每个芯片都不同。配置寄存器是高速暂存存储器里的第五个字节其中 R0、R1 两位决定温度分辨率对应关系如下表R0R1分辨率/bit最大转换时间00993.75us0110187.5us1011375us1112750us出厂默认 R01、R11 即 12 位分辨率本设计直接用默认值。12 位分辨率下温度最小单位是 0.0625℃这意味着读到的原始数值每增加 1对应温度上升 0.0625℃。高速暂存存储器由 9 个字节组成第 0 字节是温度低位第 1 字节是温度高位第 2、3 字节是 TH、TL第 4 字节是配置寄存器第 5 至 7 字节保留第 8 字节是 CRC。单片机通过单线接口发布温度转换命令后转换结果以两字节补码形式存放在第 0 和第 1 字节读取时低位在前、高位在后。整套操作流程固定为发送复位脉冲使总线上所有 DS18B20 复位 → 发跳过 ROM 命令0xCC→ 发启动温度转换命令0x44→ 等待转换完成 → 再次复位 → 发跳过 ROM 命令 → 发读取暂存器命令0xBE→ 连续读取第 0、1 字节。之所以用跳过 ROM是因为本设计一个 I/O 口上只挂一个传感器不需要做序列号匹配。2.3 显示模块与整体电路连接显示部分用的是 LM016L2 行 16 列液晶可显示 2 行 16 列英文字符有 8 位数据总线 D0-D7 和 RS、RW、EN 三个控制端口工作电压 5V和常用的 1602B 功能和引脚一致区别只是少了背光调节的两个线脚。实际上工程里直接当 1602 用就行。整体电路连接关系可以用一张表说清楚模块引脚连接目标AT89C51P0.0-P0.7LCD 数据总线 D0-D7AT89C51P2.0LCD RSAT89C51P2.1LCD RWAT89C51P2.2LCD ENAT89C51P1.7第一路 DS18B20 的 DQAT89C51P1.6第二路 DS18B20 的 DQLCDVEE10k 电位器 RP1 滑臂用于调对比度复位电路RSTR110k 上拉、C320uF 到地配合按键复位两个 DS18B20 的 VCC 接 5VGND 接地DQ 分别接 P1.7 和 P1.6。单片机最小系统用 12MHz 晶振两个 22pF 负载电容接在晶振两端。LCD 对比度通过 10k 电位器调节这个电位器没调好会出现“有显示但看不见字”的情况后面避坑部分会再提。3. 硬件电路与温度采集时序从复位脉冲到寄存器解析3.1 最小系统与传感器接线整个系统的硬件核心是 AT89C51 最小系统复位电路用 10k 电阻上拉 RST20uF 电容到地上电瞬间电容充电形成高电平复位脉冲时钟电路用 12MHz 晶振配合两个 22pF 电容接到 XTAL1、XTAL2。EA 引脚直接接高电平让单片机从内部 Flash 取指令。P0 口作为 LCD 数据总线必须要外接上拉电阻因为 51 的 P0 口是开漏结构不能直接输出高电平驱动 LCD 数据线。很多人在 Proteus 里仿真时漏掉这一步结果 LCD 花屏或乱码。完整的 Proteus 仿真图里LCD 的 14 个引脚按序连接两个 DS18B20 分别标 FIRST 和 SECOND人机交互路径非常直观。3.2 单总线时序初始化、写时序、读时序的参数细节单总线协议的所有操作都由主机产生时隙信号DS18B20 只是被动响应。三个核心时序必须按参数来差一点就会初始化失败或者读出乱值。复位时序主机先把 DQ 拉低 480us 以上一般做到 500~600us然后释放总线等待 15~60usDS18B20 会拉低总线约 60~240us 作为存在脉冲。主机在这个窗口内读 DQ读到 0 说明传感器在线读到 1 说明复位失败。写时序主机拉低 DQ 产生写起始在 15us 内决定写 0 还是写 1。写 0 时整个写时隙保持低电平 60~120us写 1 时先拉低 1~15us然后释放靠上拉电阻把总线拉高。写完一个字节后需要留出至少 1us 的恢复时间。读时序主机拉低 DQ 至少 1us 产生读起始然后释放总线在 15us 内采样 DQ 电平。如果 DS18B20 输出 0它会继续拉低总线如果输出 1总线保持高电平。采样点放太后容易读到下一时隙的起始沿导致数据错位。下面这段是典型的 51 驱动代码初学时按这个框架写就行/***********ds18b20 初始化函数**********************/ void Init_DS18B20(void) { unsigned char x 0; DQ 1; // 先释放总线 delay_18B20(8); // 总线空闲稳定 DQ 0; // 主机拉低总线 delay_18B20(80); // 保持低电平约 480us完成复位脉冲 DQ 1; // 释放总线 delay_18B20(14); // 等待 15~60us 窗口让传感器发出存在脉冲 x DQ; // 读总线0 表示传感器在线1 表示不在线 delay_18B20(20); // 等待存在脉冲结束 }这里 delay_18B20 的参数是基于 12MHz 晶振用软件循环算出来的。delay_18B20(80) 大致对应几百微秒具体数值受 C 编译器优化级别影响。实际工程里如果发现初始化经常失败不要纠结循环次数改用定时器延时或者直接数nop() 更可靠。写一个字节和读一个字节的代码逻辑也要吃透/***********ds18b20 写一个字节********************/ void WriteOneChar(unsigned char dat) { unsigned char i 0; for (i 8; i 0; i--) { DQ 0; // 拉低产生写时隙起始 DQ dat 0x01; // 低位为 0 则保持低电平写 0为 1 则释放总线写 1 delay_18B20(5); // 保持写时隙电平 DQ 1; // 结束写时隙释放总线 dat 1; // 准备下一位 } } /***********ds18b20 读一个字节********************/ unsigned char ReadOneChar(void) { unsigned char i 0; unsigned char dat 0; for (i 8; i 0; i--) { DQ 0; // 拉低产生读时隙起始 dat 1; // 低位先有空位 DQ 1; // 释放总线等待传感器驱动电平 if (DQ) dat | 0x80; // 15us 内采样读到 1 则最高位置 1 delay_18B20(4); // 等待读时隙结束总时长约 60us } return dat; }写字节时dat 0x01 是移出最低位一个 for 循环发完 8 位。读字节时dat 先右移再判断 DQ所以先读到的低位会慢慢被推到高位去等 8 个位读完dat 就是完整的一字节。这两个函数是单总线通信的基石后续所有命令都靠它们发出。3.3 温度寄存器解析符号位、补码与四位小数读取温度时命令序列是固定的复位 → 0xCC 跳过 ROM → 0x44 启动转换 → 等待 → 复位 → 0xCC → 0xBE 读取暂存器 → 连读两个字节。0x44 之后一定要给足转换时间12 位分辨率最大 750us我一般直接延时 1ms 以上否则会读到上一次的旧值或者出现 85℃ 这个标志性错误值。/**************读取 ds18b20 当前温度************/ void ReadTemp(void) { unsigned char a 0; unsigned char b 0; char t; long tt; Init_DS18B20(); WriteOneChar(0xCC); // 跳过读序列号操作 WriteOneChar(0x44); // 启动温度转换 delay_18B20(100); // 等待转换完成12 位分辨率约 750us这里留了余量 Init_DS18B20(); WriteOneChar(0xCC); // 跳过读序列号操作 WriteOneChar(0xBE); // 读取温度寄存器前两个字节就是温度值 delay_18B20(100); a ReadOneChar(); // 读温度低位 b ReadOneChar(); // 读温度高位 temp_value b 4; // 高位去掉低 4 位 temp_value (a 0xf0) 4; // 低位去掉小数位只取整数部分 t a 0x0f; // 低四位是小数部分 tt t * 625; // 0.0625℃ * 1000 62.5放大后用整数计算 xiaoshu tt / 1000; // 得到小数点后第一位的数值 }温度值在寄存器里的布局我再补一张表方便对照条件二进制示例十六进制温度125℃0000 0111 1101 000007D0H12525.0625℃0000 0001 1001 00010191H25.062510.125℃0000 0000 1010 001000A2H10.1250.5℃0000 0000 0000 10000008H0.50℃0000 0000 0000 00000000H0-0.5℃1111 1111 1111 1000FFF8H-0.5-10.125℃1111 1111 0101 1110FF5EH-10.125-25.0625℃1111 1110 0110 1111FE6FH-25.0625-55℃1111 1100 1001 0000FC90H-55符号位 S 为 0 时是正温度直接把二进制位转为十进制再乘以 0.0625 就是实际温度。符号位 S 为 1 时是负温度寄存器里存的是补码要先取反加 1 算出绝对值再在前面加负号。本设计温度范围下限是 -30℃所以严格说负数场景躲不开避坑章节里会专门说这个问题。4. 软件主流程与 1602 驱动两秒一换的温度巡显4.1 主程序流程一个开机画面加三段时间片主程序整体结构非常清晰调用几个子程序液晶初始化、第一路 DS18B20 初始化、第二路 DS18B20 初始化、液晶显示数据另外加一个开机欢迎画面的程序。运行节奏是上电显示开机画面保持 2 秒然后显示第一个温度保持 2 秒再显示第二个温度保持 2 秒之后不断循环。void main(void) { LCD_Initial(); // 液晶初始化8 位数据2 行显示5*7 点阵 Init_DS18B20(); // 第一路传感器初始化 DQ1 1; delay_18B20(8); DQ1 0; delay_18B20(80); // 第二路传感器复用同一套时序 DQ1 1; delay_18B20(14); GotoXY(0, 0); Print(Multi-Temp Test); // 开机画面 Delay1ms(2000); while (1) { GotoXY(0, 0); Print(first :); show_time(); // 读第一路温度并显示 Delay1ms(2000); GotoXY(0, 0); Print(second:); show_time1(); // 读第二路温度并显示 Delay1ms(2000); } }这段代码的节奏就是两秒一换开机画面 2 秒第一路 2 秒第二路 2 秒无限循环。Delay1ms 是一个用嵌套循环实现的软件延时以 12MHz 晶振为前提。要注意的是整个循环里没有任何按键中断参与纯粹靠延时驱动逻辑简单但 CPU 全程空转这在小系统里完全能接受。4.2 1602 初始化与字符定位四条命令必须按顺序1602 是字符型液晶内部有字符发生器收到 ASCII 码后直接显示对应字符但数字不能直接送显示必须先把数值转成字符。初始化时序固定为四条命令功能设置 0x388 位数据端口、2 行显示、5*7 点阵、开显示无光标 0x0C、清屏 0x01、输入模式 0x06AC 递增画面不动。这四条命令之间要有延时不能连续猛写。void LCD_Initial(void) { LcdEn 0; LCD_Write(LCD_COMMAND, 0x38); // 8 位数据端口2 行显示5*7 点阵 LCD_Write(LCD_COMMAND, 0x38); // 重复一次确保液晶稳定进入 8 位模式 LCD_Write(LCD_COMMAND, 0x0C); // 开显示无光标不闪烁 LCD_Write(LCD_COMMAND, 0x01); // 清屏 LCD_Write(LCD_COMMAND, 0x06); // 写入一个字符后地址自动加 1 } void GotoXY(unsigned char x, unsigned char y) { if (y 0) LCD_Write(LCD_COMMAND, 0x80 | x); // 第一行 DDRAM 首地址 0x80 if (y 1) LCD_Write(LCD_COMMAND, 0x80 | (x - 0x40)); // 第二行 DDRAM 首地址 0xC0 } void Print(unsigned char *str) { while (*str ! \0) { LCD_Write(LCD_DATA, *str); // 数据模式写入字符 str; } }GotoXY 是往指定行列写字符的关键函数。1602 第一行 DDRAM 地址从 0x00 开始第二行从 0x40 开始命令里要加 0x80 才表示“设置地址”。所以 x0、y1 时实际写入的是 0x80(0-0x40)即 0x40 的地址位再配合 0x80 位就是 0xC0。LCD_Wait 是忙标志检测函数在 Proteus 仿真里有时候会因为端口映射问题读取异常很多人直接改用固定延时绕过两者都能跑。LCD_Write 的写入顺序是先置 RS/RW再送数据最后给使能脉冲这个顺序不能反反了液晶会锁存到错误电平。4.3 温度转字符整数部分和小数部分分开拼温度读取完成后温度值是一个数值显示到 LCD 上必须拼成字符串。本设计的做法是把整数部分和小数部分分开处理每一段拼上对应的 ASCII 字符。void temp_to_str(void) { TempBuffer[0] temp_value / 10 0; // 十位数字转字符 TempBuffer[1] temp_value % 10 0; // 个位数字转字符 TempBuffer[2] .; // 小数点 TempBuffer[3] xiaoshu 0; // 小数第一位转字符 TempBuffer[4] 0xdf; // 温度符号度 °1602 字库内置 TempBuffer[5] C; // 摄氏标识 TempBuffer[6] \0; // 字符串结束符 }这段代码利用了 1602 内置字库里 0xdf 对应的就是度数符号省去了自定义字模的麻烦。整数部分是温度值除以 10 取十位数字取余得到个位数字各自加 0 转成 ASCII。小数部分就是 ReadTemp 里算好的 xiaoshu 变量加 0 后直接显示小数点后第一位。为了保住显示精度 0.1℃这里把 0.0625℃ 分辨率的小数部分四舍五入到了十分位。显示时先定位到第一行显示固定标签再定位到第二行显示温度字符串整体信息布局每次循环保持一致。5. 单片机多点测温避坑与常见问题排查五个高频踩坑记录5.1 温度转换命令后立即读读出 85℃ 这个经典错误值现象程序在发送 0x44 转换命令后马上发送 0xBE 读温度读回来的数据显示 85.0℃而且两个传感器都这样。原因0x44 是启动温度转换的命令DS18B20 需要时间完成转换12 位分辨率下的最大转换时间是 750us。如果主机在转换完成前就发读命令传感器返回的是暂存器里的旧值上电复位后暂存器的默认值对应的正好是 85℃。解决0x44 命令之后必须延时等待转换完成工程上至少留 1ms 余量不要卡着 750us 临界值。如果是 9 位分辨率模式转换时间是 93.75us但代码里统一按最长的 12 位模式等待是最省心的做法。从那以后我每次写完 ReadTemp 都会先自查0x44 后面有没有给足延时。5.2 小数部分用浮点乘法编译出来显示错乱现象想用 0.0625 直接乘小数位代码写 xiaoshu t * 0.0625; 但 51 单片机是 8 位机浮点运算会引入很大的额外代码量和误差显示出来的小数位经常是乱码或者对不上。原因51 的数学协处理器是没有的float 运算全靠编译器生成软浮点库处理 t * 0.0625 这种运算慢且容易出差而且浮点结果再转整型还会有截断误差。解决把小数运算全部整数化。0.0625 乘以 1000 是 62.5所以 t * 625 / 1000 就能得到小数点后第一位全程用整数运算一次乘法一次除法又快又准。前提是 t 是 0~15 的小数位值625*t 最大 9375long 类型足够装。5.3 负温度显示乱码uchar 装不下补码负数现象把测温范围放到 -30℃ 后零下温度在 LCD 上显示成一段乱码或者无效字符正温度正常。原因温度寄存器里负温度是补码形式高位字节可能是 0xFF 或 0xFE。temp_value 定义成 unsigned charb4 会把高位字节的高 4 位移出去剩下的值完全不是真实温度显示自然就错了。解决处理温度时先把 16 位原始数据合并成 int再判断符号位。符号位为 1 时先对 16 位值取反加 1 得到绝对值然后计算整数和小数拼字符串时在最前面加一个 - 字符。注意同时要把负号字符放到 TempBuffer 里显示长度会比正温度多一位GotoXY 的定位坐标要相应调整。5.4 P0 口没接上拉电阻LCD 花屏且命令无效现象Proteus 仿真时 LCD 显示一团方块字符清屏命令发出去没反应或者字符随机跳动。原因AT89C51 的 P0 口是开漏结构内部没有上拉电阻不能主动输出高电平。LCD 数据总线接在 P0 口上时如果没有外部上拉数据位的电平取决于寄生电容和负载逻辑完全不可靠。解决P0.0~P0.7 各接一个 10k 上拉电阻到 VCC。Proteus 里直接放 8 个 RES 组件或者用一个排阻 RP1 替代功能一样。另外 LCD 的 VEE 对比度电位器也要调调到字符清晰但不出现满屏方块为止。这个坑在电路图上很容易被忽略因为 P1、P2、P3 口不需要上拉也能工作唯独 P0 例外。5.5 把 0xC0 当显示开关命令光标位置和显示状态全乱现象想配置“开显示、无光标”参考网上的代码写 LCD_Write(LCD_COMMAND, 0xC0); 结果清屏后字符从第二行中间开始显示光标状态也不对。原因0xC0 不是显示开关控制命令它是 1602 第二行首地址 0x40 加上 DDRAM 设置位 0x80 的组合作用是“把光标定位到第二行开头”。显示开关控制命令是 0x0C二进制 0000 1100其中最高位固定为 0000 1 是控制命令的标志位模式。两个值在十六进制上看着像功能完全是两回事。解决查阅 1602 指令表把常用命令整理成宏定义写代码时直接引用宏名而不是裸数字。比如 LCD_CLEAR_SCREEN 0x01、LCD_SHOW 0x04组合时用 0x08|DisplayMode 来构造显示开关命令。这样即使换 LCD 型号也只需要改宏定义一个地方。这个坑基本是抄代码不看数据手册造成的我自己早年也翻过车。5.6 两个 DS18B20 初始化失败但单个传感器都正常现象单独接任何一个传感器都能读到温度两个一起接到系统里初始化函数返回失败温度读出来全是一个固定值。原因两个传感器的 DQ 分别接 P1.7 和 P1.6如果代码里复用了同一个初始化函数但 DQ 引脚定义没有切换或者 Proteus 里两个传感器的 DQ 引脚意外短路会导致总线冲突。解决先核对原理图连接确认 FIRST 的 DQ 只接 P1.7SECOND 的 DQ 只接 P1.6。然后检查代码里第二路传感器是否也用到了 DQ1 的引脚定义初始化、读写字节每个函数都要分开写或者把引脚定义改成宏通过传参方式复用同一套时序函数。更稳妥的做法是先把两路分别测试逐路确认能读数后再组合到主循环里避免一出问题就怀疑时序浪费大量排查时间。6. 同一根总线挂两个 DS18B20ROM 匹配点名怎么做本设计的两个 DS18B20 各占一个 I/O 口编程简单但代价是每多一个传感器就要多占一个引脚。DS18B20 真正的价值在单总线多挂载能力一条 DQ 线上可以同时挂多个传感器靠 64 位激光 ROM 里的序列号区分彼此。这个方案常见做法是先逐个获取序列号再通过匹配 ROM 命令点名读取。单总线上多个传感器共存的关键命令有三个0x33 读 ROM、0x55 匹配 ROM、0xCC 跳过 ROM。上电时只能有一个传感器挂在总线上发 0x33 读出它的 8 字节序列号并存进数组然后换下一个传感器重复操作。正常工作阶段复位后发 0x55跟上目标传感器的 8 字节序列号此时只有序列号匹配的那个传感器会响应后续命令其余全部保持沉默。void MatchROM(unsigned char *rom) { unsigned char i; Init_DS18B20(); // 统一复位总线 WriteOneChar(0x55); // 发匹配 ROM 命令 for (i 0; i 8; i) { WriteOneChar(rom[i]); // 逐字节发送序列号低位在前 } }匹配成功后发 0x44 启动转换再发 0xBE 读回温度所有时序和单传感器一模一样。还有一种更快的方式先发跳过 ROM 加 0x44让总线上所有传感器同时开始转换等 750us 后逐个匹配读取这样每轮只等一次转换时间多路采集效率更高。0xCC 会造成总线上所有传感器同时响应所以只能用于广播命令不能用于点名读数据。从这套设计往多路扩展时我一般先把基础驱动放到独立文件再封装 SensorRead(idx) 这类接口上层代码完全不用关心底层是哪个引脚还是哪条总线。从那以后我每次写单总线程序都强制自己先画一遍时序图再动手写延时确认每个时间窗口落在数据手册指定的区间里。希望帮到你。本文还有配套的精品资源点击获取