AT89C51双路DS18B20测温与1602显示:单总线时序与寻址避坑 简介面向电子信息、自动化等专业学生及单片机初学者的课程设计参考资料围绕AT89C51单片机与DS18B20数字温度传感器展开实现两点温度采集并通过1602LCD循环显示可用于课程设计、毕业设计选题与基础测温方案复现。压缩包内为1个doc文档约199KB将总体方案、硬件设计、软件流程、系统仿真与源程序代码集中整理在同一文档中。文档详细讨论主控制器选型、LM016L显示模块、DS18B20内部结构、高速暂存存储器、测温原理与单总线连接并给出原理框图和单片机最小系统电路图便于读者对照理解温度数据格式、分辨率配置及显示驱动过程。目前已有1652人学习下载适合需要快速搭建多点温度检测实验、撰写设计报告或查漏补缺的读者参考。1. 一根 DQ 线上挂两颗 DS18B20读出来却是同一个数有次看一块两点测温板两颗 DS18B20 的 DQ 全并在 P1.7 上程序里用的是「跳过 ROM」那条命令结果 1602 上下两行数字永远一模一样用手捏住其中一颗另一颗的读数也跟着往下掉。这套基于单片机多点温度检测系统设计的资料正好把这类问题的来龙去脉交代清楚了主控是 AT89C51两路 DS18B20 分别走 P1.6 和 P1.7显示用 1602 字符液晶配套 Keil C 源码和 Proteus 仿真原理图。它适合三类人——课程设计和毕设要交实物的、想把单总线时序真正吃透的、以及手上有块 51 最小系统板想找项目练手的。整套东西不复杂坑却集中在时序和寻址两个地方下面按硬件、时序、软件、排查、改造的顺序往下拆。2. 系统架构拆解AT89C51 如何同时调度两路温度与一块 1602这套方案的分工非常干脆AT89C51 只做三件事按单总线协议去两颗 DS18B20 里取数、把取回来的 16 位补码换算成能显示的字符、把字符按指定坐标写进 1602。没有 RTOS没有中断驱动的采集连定时器都没用上全靠主循环里几个延时函数把节拍撑起来。这种「裸奔式」结构在两点测温这个量级上是合理的代码量小、逻辑线性、出了问题好定位代价是采样节拍靠软件延时一旦后续要加按键、加串口延时块就会变成阻塞源头。2.1 从原理框图看信号的三段式走向原理框图从左到右是三段温度采集、主控处理、液晶显示。左段是两颗 DS18B20各自占用一根 I/O 做单总线通信一根线既传命令又传数据不需要 ADC也不需要额外的信号调理电路中段是 AT89C51 最小系统负责产生时序、执行命令序列、做数值换算右段是 1602 液晶8 位并行数据总线加 RS/RW/EN 三根控制线把字符一个个写进 DDRAM。这个结构有一个容易被忽略的细节两路传感器是并行挂在不同 I/O 口上的独立总线不是挂在同一根线上的多器件总线。前者靠「跳过 ROM」命令就能分别操作因为每次通信的对象从头到尾只有一颗后者必须先用「读 ROM」拿到 64 位序列号再用「匹配 ROM」点名否则所有器件会同时应答数据线出现线与读回来的是几个器件电平的叠加结果。整套资料里两路分别接 P1.6 和 P1.7走的正是前一种省事路线理解这一点后面看代码里的0xCC才不会觉得突兀。主循环的调度顺序是液晶初始化 → 两路 DS18B20 初始化 → 显示开机画面 → 延时 → 采集第一路并显示 → 延时 → 采集第二路并显示 → 回到采集第一路。两级循环之间没有任何状态机靠的是「采一次、刷一次、等一会」的朴素节奏。这种写法在仿真里跑得通实物上也够用但如果哪天要把两秒改成两百毫秒就得把Delay1ms换成定时器计数否则按键和串口都会跟着卡。2.2 主控最小系统晶振、复位、EA 与 P0 上拉AT89C51 是 8 位 CMOS 微处理器片内 4KB Flash 程序存储器、128 字节 RAM、32 根可编程 I/O、两个 16 位定时器、6 个中断源全静态工作范围 0Hz 到 24MHz。这套设计取 12MHz 晶振机器周期正好 1 微秒所有软件延时都按这个基准写死换晶振就得重算延时循环次数这一点后面在避坑章节还要提。最小系统四件套的接法晶振12MHz 石英晶体跨接在 XTAL1第 19 脚和 XTAL2第 18 脚之间两端各挂一只 22pF 瓷片电容到地做负载电容匹配。复位RST第 9 脚接 20uF 电解电容正极电容负极接 5VRST 再经 1K 电阻到地构成上电复位并联一只轻触开关到 5V 做手动复位。RC 时间常数决定复位脉冲宽度20uF × 1K 约 20ms远高于芯片要求的 2 个机器周期。EA第 31 脚接 5V表示从片内 4KB Flash 取指令不外部扩展程序存储器。P0 口上拉P0 内部是开漏结构输出高电平时处于高阻态驱动 1602 的数据总线必须外接 10K 排阻到 5V否则高电平建立不起来液晶收到的全是低电平屏幕上会出现一片黑块。引脚定义在代码里是这样落地的改板子的时候改这几行就够了#include REG51.H #include intrins.h #define uint unsigned int #define uchar unsigned char sbit LcdRs P2^0; /* 1602 寄存器选择0 命令1 数据 */ sbit LcdRw P2^1; /* 1602 读写选择0 写1 读 */ sbit LcdEn P2^2; /* 1602 使能下降沿锁存 */ #define DBPort P0 /* 8 位数据总线接 P0外部必须有 10K 上拉 */ sbit DQ P1^7; /* 第一路 DS18B20 数据线 */ sbit DQ1 P1^6; /* 第二路 DS18B20 数据线 */DBPort用宏定义而不是sfr好处是换端口时只改一行sbit定义的位变量必须指向可位寻址区P1、P2 都满足如果哪天挪到 P3 的某几位也照样能用。晶振频率没有写进代码而是体现在delay_18B20的循环次数里这属于典型的隐式依赖——代码里看不到 12MHz 这个数但每一处延时都按它算好了。2.3 接线约定表两路 DQ、1602 控制线和对比度原理图上的连线散落在各个图块里实际动手焊或者画板时按下面这张表对一遍最省事信号器件引脚连接到备注第一路 DQDS18B20 第 3 脚P1.7需 4.7K 上拉到 5V第二路 DQDS18B20 第 3 脚P1.6同样 4.7K 上拉传感器电源DS18B20 第 1、2 脚GND、5V三线制外部供电LCD 数据D0~D7P0.0~P0.710K 排阻上拉LCD 控制RS、RW、EP2.0、P2.1、P2.2直接驱动无需上拉LCD 对比度VEE 第 3 脚10K 电位器中间抽头两端接 5V 与 GND晶振XTAL1、XTAL212MHz 2×22pF电容另一端接地复位RST 第 9 脚20uF 1K并联手动复位按键程序存储选择EA 第 31 脚5V使用片内 Flash表格里最容易被跳过的是 VEE 那一行。1602 的对比度是靠 VEE 脚上的电压决定的悬空时液晶分子偏转不足屏幕上只会看到一排排方块或者整行黑带很多人第一反应是「程序没跑起来」其实是硬件偏压没调好。上电之后先拧电位器看到字符由淡变清晰地显现出来再回头查程序能省掉一半时间。2.4 供电方式的选择外部供电与寄生供电DS18B20 支持两种供电三线制外部供电VDD 接 5V、GND 接地、DQ 上拉和两线制寄生供电VDD 与 GND 一起接地靠 DQ 线在总线高电平时给内部电容充电。原设计用的是三线制理由很实际——寄生供电在温度转换期间电流需求大总线被拉低时器件没有能量来源转换结果的可靠性依赖上拉电阻的强弱和总线空闲时间的比例调试阶段很容易出现「有时候读得到、有时候读回 85 度」的玄学现象。如果确实需要省线寄生供电也能用代价是上拉电阻从 4.7K 降到 1K 到 2K 之间、DQ 线在转换期间必须保持强上拉、并且不能在这段时间做其他总线操作。对于两块传感器的课程设计场景三线制的两根地线一根电源线并不算什么负担我一般直接按三线制走把调试时间留给时序。3. 单总线时序落地从 480us 复位脉冲到 0.0625℃ 的补码换算DS18B20 的全部通信都压在 DQ 这一根线上靠的是不同宽度的低电平脉冲来区分「0」和「1」所以时序不是可选项是硬指标。整个流程固定为五步主机拉低总线发出复位脉冲、器件回一个存在脉冲表示在线、主机发一条 ROM 命令确定通信对象、发功能命令启动转换或读暂存器、按位读写数据。任何一步的脉宽超出窗口后面的数据就全是乱的。3.1 复位与存在脉冲一个 480us 的拉低复位是每次通信的起点。主机把 DQ 拉低至少 480us然后释放DS18B20 检测到这个下降沿后等待 15 到 60us主动把总线拉低 60 到 240us 作为应答这个低电平就叫存在脉冲。主机在释放总线后的 15us 到 60us 之间采样读到 0 说明器件在线读到 1 说明没接好或者线断了。/* 12MHz 晶振下的粗略延时i 每减一次约 2 个机器周期 */ void delay_18B20(unsigned int i) { while (i--); } /* 复位并检测存在脉冲返回 0 表示器件应答正常 */ unsigned char Init_DS18B20(void) { unsigned char x 0; DQ 1; /* 先释放总线避免上一轮残留低电平 */ delay_18B20(8); DQ 0; /* 拉低复位脉冲开始 */ delay_18B20(80); /* 手册要求 ≥480us这里的循环次数偏少见第 5 章 */ DQ 1; /* 释放总线等器件应答 */ delay_18B20(14); /* 延时到 15~60us 采样窗口内 */ x DQ; /* 读到 0 即检测到存在脉冲 */ delay_18B20(20); /* 等存在脉冲结束总线回到空闲高电平 */ return x; }函数返回值虽然定义了但原代码的调用处并没有检查这是后面排查章节要展开的一个隐患复位失败还继续往下发命令读回来的就是随机值表现出来是温度乱跳而不是报错。3.2 读写的时隙窗口为什么读要放在 15us 内单总线的每一位都靠一个低电平脉冲开启。写「1」时主机拉低 1 到 15us 就释放器件在采样窗口内读到高电平写「0」时主机拉低至少 60us器件读到低电平。读操作反过来主机拉低至少 1us 后释放然后在 15us 内采样总线电平1 表示该位为 10 表示该位为 0采样完还要把时隙补足到 60us让器件完成这一位的处理。/* 读一个字节低位先出 */ unsigned char ReadOneChar(void) { unsigned char i 0; unsigned char dat 0; for (i 8; i 0; i--) { DQ 0; /* 拉低产生读时隙起点保持 ≥1us */ dat 1; /* 先右移为最高位腾位置 */ DQ 1; /* 释放总线把电平决定权交给器件 */ if (DQ) dat | 0x80; /* 15us 内采样读到 1 就置最高位 */ delay_18B20(4); /* 补足时隙长度给器件留处理时间 */ } return dat; } /* 写一个字节低位先出 */ void WriteOneChar(uchar dat) { unsigned char i 0; for (i 8; i 0; i--) { DQ 0; /* 每个时隙都以低电平开始 */ DQ dat 0x01; /* 写 1 则立即释放写 0 则继续拉低 */ delay_18B20(5); /* 写 0 的低电平需保持 60us 左右 */ DQ 1; /* 时隙之间总线回到高电平 */ dat 1; } }这两段是整个项目里最不能改魔数的地方。dat 1和dat | 0x80配合说明数据是低位在前DQ dat 0x01之后立刻delay_18B20(5)写 1 的时候实际也拉低了几个微秒靠的是「短低电平 释放」被器件识别为 1写 0 时因为dat 0x01结果为 0DQ 保持低电平直到延时结束。读函数里if (DQ)这一句的采样时刻就是前面说的 15us 窗口如果晶振换成 24MHz 而延时没重算这个窗口会被压缩读回的数据就开始出错。3.3 从 16 位补码到 1602 上的 ASCII 小数温度转换完成后结果存在高速暂存存储器的第 0 和第 1 字节第 0 字节是低位、先读出来第 1 字节是高位。12 位分辨率下低字节的低 4 位是小数部分高 4 位是整数部分的低 4 位高字节的低 4 位是整数部分的高 4 位高 5 位是符号位扩展。一个最低有效位代表 0.0625℃。/* 读一次温度整数部分放 temp_value一位小数放 xiaoshu */ void ReadTemp(void) { unsigned char a 0, b 0; char t; long tt; Init_DS18B20(); WriteOneChar(0xCC); /* 跳过 ROM这条总线上只有一颗器件 */ WriteOneChar(0x44); /* 启动温度转换 */ delay_18B20(100); /* 原文写法12 位分辨率实际需要 750ms */ Init_DS18B20(); WriteOneChar(0xCC); WriteOneChar(0xBE); /* 读暂存器前两字节即温度值 */ delay_18B20(100); a ReadOneChar(); /* 低字节先出 */ b ReadOneChar(); /* 高字节后出 */ temp_value (b 4); /* 整数高 4 位挪到高位 */ temp_value ((a 0xF0) 4); /* 拼上低字节里的整数低 4 位 */ t a 0x0F; /* 小数部分4 位共 16 个刻度 */ tt t * 625; /* 0.0625 625/10000先放大避免浮点 */ xiaoshu tt / 1000; /* 取一位小数四舍五入留给显示层 */ }tt * 625这一步是整个换算里最关键的设计。51 单片机做浮点乘除要拉进几百字节的浮点库速度和空间都不划算把 0.0625 放大成 625 再用长整型做整数乘除结果精度一样而开销小得多。tt / 1000取的是第一位小数因为t最大 1515 × 625 9375除以 1000 得 9不会溢出。如果显示层要两位小数把除数换成 100 就行但要注意 1602 一行只有 16 个字符格式得重新排。小数部分按 4 位刻度换算是因为配置寄存器默认 R0R11工作在 12 位分辨率。这个寄存器是暂存器的第 5 字节改它就能在 9 到 12 位之间切分辨率位数越低转换越快9 位最长 93.75ms10 位 187.5ms11 位 375ms12 位 750ms。如果觉得两秒刷新一次太慢与其压缩延时不如先把分辨率降下来。3.4 一次完整采集的函数调用顺序把上面的片段串起来一次采集的实际调用链是show_time()→ReadTemp()→Init_DS18B20()→WriteOneChar(0xCC)→WriteOneChar(0x44)→ 延时 →Init_DS18B20()→WriteOneChar(0xCC)→WriteOneChar(0xBE)→ 两次ReadOneChar()→ 数值换算 →temp_to_str()→GotoXY()Print()。这个顺序不能乱换。比如先发 0xBE 再发 0x44读到的是上一次转换的旧值两次Init_DS18B20()之间如果忘了释放总线第二次复位会被器件的存在脉冲顶住检测直接失败读字节的时候如果中途跳出去做别的事时隙被打断读回的字节高位会错乱。这些都不是理论风险是实际调试时最先撞上的几堵墙。4. 软件分层实现1602 驱动、字符化缓冲区与主循环节拍软件结构没有严格分层但逻辑上明显分成四块最底下是单总线时序函数往上一层是温度读取与换算再往上是数值到字符的转换最外面是 1602 驱动和主循环。四块之间的接口很窄——温度层只往外吐temp_value和xiaoshu两个全局变量显示层只认以\0结尾的字符数组中间靠temp_to_str()焊接。这种「全局变量当接口」的做法在几十行代码的项目里够用扩展到多路时就会变成负担。4.1 1602 的写时序与忙等待1602 内部有自己的一套控制器接收一条命令或一个字节需要几十微秒到几毫秒不等清屏命令最长要 1.64ms。主机写完之后如果不等它做完就塞下一个字节会被直接丢弃。两种等待方式读忙标志位读 DB7 判断是否忙或者干脆死等一个足够长的固定延时。原代码两种都用了——写完先调LCD_Wait()读忙标志初始化里又保留了几处Delay1ms兜底。/* 读忙标志返回最高位为 1 表示控制器正忙 */ unsigned char LCD_Wait(void) { LcdRs 0; /* 读状态时 RS 必须拉低 */ LcdRw 1; /* 读方向 */ _nop_(); LcdEn 1; _nop_(); LcdEn 0; return DBPort; /* 高 7 位是地址计数器最高位 D7 是忙标志 */ } /* 写命令或写数据style 传 0 写命令、传 1 写数据 */ void LCD_Write(bit style, unsigned char input) { LcdEn 0; LcdRs style; LcdRw 0; /* 写方向 */ _nop_(); DBPort input; /* 数据先摆上总线 */ _nop_(); LcdEn 1; /* 上升沿准备锁存 */ _nop_(); LcdEn 0; /* 下降沿真正写入 */ _nop_(); LCD_Wait(); /* 等控制器内部操作结束 */ }LCD_Wait()里的return DBPort有一个前提P0 口作为输入使用之前必须先往端口写 0xFF 把内部场效应管关掉否则读回来的永远是上次写进去的值。原代码漏了这一步在 Proteus 里因为仿真模型不模拟端口锁存可能侥幸正常焊到实物上读忙标志会一直读到 0LCD_Wait()变成空操作紧接着就是显示花屏或者丢字符。这是我建议大家第一个动手改的地方在LcdRw 1;之后补一句DBPort 0xFF;。初始化序列是 1602 上电后的标准动作顺序错一步后面全是乱码void LCD_Initial(void) { LcdEn 0; LCD_Write(LCD_COMMAND, 0x38); /* 8 位数据口、2 行显示、5×7 点阵 */ LCD_Write(LCD_COMMAND, 0x38); /* 连发两次确保上电状态被拉回 8 位模式 */ LCD_SetDisplay(LCD_SHOW | LCD_NO_CURSOR); /* 开显示不要光标 */ LCD_Write(LCD_COMMAND, 0x01); /* 清屏执行时间最长 */ LCD_SetInput(LCD_AC_UP | LCD_NO_MOVE); /* 地址自增画面不滚动 */ }0x38发两次是行规因为上电瞬间控制器可能停在 4 位模式第一条指令被当作半个字节吃掉必须补一条才是完整的。清屏之后一定要留足时间LCD_Wait()读忙能等到位如果换成立即返回的写法就得补Delay1ms(2)。4.2 温度数值到显示缓冲区的字符化1602 认的是 ASCIItemp_value里放的是二进制数中间必须转换。个位十位分别除以 10 和模 10 再加工0小数部分同理度符号用0xDF——这是 1602 内置字符表里的「°」编码不是标准 ASCII如果换成 OLED 或者别的屏这个字节要重新查表。uchar TempBuffer[7]; /* 全局缓冲区靠初值为 0 保证结尾有 \0 */ char xiaoshu 0; /* 一位小数 */ 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; /* 摄氏度符号 */ TempBuffer[5] C; TempBuffer[6] \0; /* 原文漏写靠全局零初值兜底 */ }Print()是靠while (*str ! \0)判断结束的缓冲区里没有结束符就会一直往后读把 RAM 里的其他变量当字符打到屏幕上表现为温度后面跟着一串乱码。第二路用的TempBuffer1[7]在原文里只写了TempBuffer1[6] \0前 6 个字节交给另一个函数填如果那个函数少填一位屏幕上就会缺字符——这种问题看起来像液晶坏了实际是字符串没闭合。还有一处值得注意temp_value声明成uchar只放得下 0 到 255而且没处理符号位。正温度在 -30 到 99 的范围内没问题一旦探头放到零下去高字节的符号位被b 4截断显示出来会是二百多度。要支持负温temp_value得换成int读取后先判断b 0xF8是否为 0xF8是则按补码取反加一处理。4.3 主循环的 2 秒轮询与双路切换主循环的逻辑只有四行但它是整个系统的节拍器void main(void) { LCD_Initial(); /* 液晶初始化 */ GotoXY(0, 0); Print(Temp Monitor); /* 开机欢迎画面第一行 */ GotoXY(0, 1); Print( Starting... ); Delay1ms(2000); /* 画面停留 2 秒 */ while (1) { show_time(); /* 采第一路、换算、显示 */ Delay1ms(2000); show_time1(); /* 采第二路、换算、显示 */ Delay1ms(2000); } } void show_time(void) { ReadTemp(); /* 一次完整的单总线采集流程 */ temp_to_str(); /* 数值转字符 */ GotoXY(0, 0); Print(first ); /* 第一行标出通道号 */ GotoXY(0, 1); Print(TempBuffer); /* 第二行显示温度 */ }Delay1ms是双重 for 循环内层 120 次按 12MHz、每次约 8 个机器周期估算接近 1ms但编译器优化等级一变就不准了。原文设计文档里写的是「两秒间隔循环显示」代码里给的是Delay1ms(2000)两边对得上有些流传的版本改成了 400ms文档没同步读的时候以代码为准别照着文档改参数。GotoXY的第二行地址用的是0x80 | (x - 0x40)写法反直觉靠unsigned char的溢出绕回 0xC0功能上等价于0x80 | (0x40 x)但如果哪天把参数类型改成int这个减法会算出负数整条显示就废了建议直接改成加法形式。5. 避坑与排查仿真一次过、上板就翻车的五处高频问题Proteus 的器件模型对时序的容忍度比真实芯片宽得多很多在仿真里跑得漂漂亮亮的代码一焊到洞洞板上就开始出问题。下面五条是我在这个项目上反复撞过、也见过别人撞的坑按出现频率排序。5.1 上电第一读数永远是 85.0℃现象程序跑起来两行温度都显示 85.0过几秒才跳成正常值或者一直卡在 85.0 不动。原因85℃ 是 DS18B20 上电复位后暂存器里的默认值代表「还没转换过」。原代码在发完0x44启动转换之后只调了delay_18B20(100)按 12MHz 每条循环两个机器周期估算大约两百微秒而 12 位分辨率的转换最长要 750ms差了三四个数量级。仿真里器件模型瞬间出结果所以看不出来实物上读到的是上一次的旧值或者默认值。解决把转换后的等待改成实打实的毫秒级延时Delay1ms(750)起步保守一点给 800ms。更好的做法是把分辨率降到 11 位或 10 位转换时间缩短到 375ms 和 187.5ms两秒刷新的节拍也不至于被占掉太多。如果坚持要 12 位又不想干等就在转换期间用DQ 1强上拉供电寄生供电模式必须这么做然后去处理别的事情回来再读——不过这就得改成状态机了。5.2 两路读数完全同步捏一颗另一颗跟着变现象1602 上两行数字一模一样用手捏住第一颗传感器两行同时上升断开第二颗两行还在正常显示。原因两颗传感器的 DQ 被并在同一根 I/O 上程序里又用的是0xCC跳过 ROM。这条命令的语义是「接下来所有器件一起听」于是两颗 DS18B20 同时响应转换命令、同时驱动数据线读的时候电平是两颗器件输出的线与结果表现出来就是两个数永远相等。同理如果只焊了一颗另一路读到的是悬空引脚的电平通常也是乱值。解决要么保持硬件上一颗一个 I/O这套原理图就是这么接的检查DQ和DQ1是不是真的分别接到了 P1.7 和 P1.6有没有在排线时短到一起要么改成一线多点方案用0x33读出各自的 64 位 ROM 码、再用0x55点名操作具体做法放在第 6 章。中间还有一种情况两颗分别接两个口但代码里第二路的sbit忘了改两个函数都操作 P1.7现象和并线一模一样查代码时先看这里。5.3 显示时好时坏字符缺笔画或者整行黑块现象有时正常显示有时第二行全是黑方块有时字符只显示一半复位一下又好了。原因三种可能按概率排——一是 P0 口外部的 10K 排阻没焊或者虚焊开漏输出拉不出高电平数据线的高 4 位一直是低二是 VEE 对比度电压没调好看着像乱码其实是全黑或者全白三是LCD_Wait()读忙标志那句return DBPort之前没有先写 0xFF读回的值恒为 0忙等待形同虚设后续字节被控制器丢弃。解决先拿万用表量 P0 各脚在空闲时是不是 5V不是就查排阻再拧电位器看对比度有没有变化区间最后在LcdRw 1;后面补DBPort 0xFF;再读。三点都排除之后如果还偶发就把LCD_Wait()换成固定延时Delay1ms(2)先让系统稳定跑起来再回头优化等待策略。5.4 零下温度显示成 200 多度现象把探头丢进冰水混合物或者冰箱冷冻层屏幕上的数字不是负数而是 200 以上的怪值。原因temp_value声明为ucharb 4在整型提升后赋值回uchar时被截断高字节里的符号位扩展全丢了。DS18B20 输出的是 16 位补码负温度时高字节高 5 位全是 1正确做法是先判断符号、再决定是直接换算还是取反加一。解决把中间变量换成int读取后先拼成 16 位再判断int raw (b 8) | a; /* 拼成完整 16 位补码 */ int temp_x10; if (raw 0x8000) { /* 最高位为 1说明是负温 */ raw (~raw 1) 0xFFFF; /* 取反加一还原绝对值 */ temp_x10 -(raw * 10 / 16); /* 12 位分辨率1 LSB 0.0625℃×10 后取整 */ } else { temp_x10 raw * 10 / 16; }raw * 10 / 16是把 0.0625 的换算合并进整数运算避免浮点。显示的时候再按符号位决定要不要补负号1602 里-就是普通 ASCII直接放进缓冲区即可。5.5 换了晶振或者改了编译优化等级通信全乱现象原来的代码在 12MHz 板上正常换到 11.0592MHz 的板子上或者 Keil 里把优化等级从 8 调到 9温度就读不出来了。原因单总线的所有时序都压在delay_18B20()的循环次数上这个函数没有任何硬件基准全靠晶振频率和编译器生成的指令条数决定。优化等级一高while (i--)可能被优化成更紧凑的形式实际延时缩短晶振一换机器周期变了所有延时同比缩放。解决短期办法是按新频率重算循环次数用示波器抓 DQ 波形看复位低电平是不是够 480us、读时隙采样点是不是落在 15us 内。长期办法是换成定时器做基准用 T0 工作在模式 2 的 8 位自动重装每 1us 中断一次累加计数把delay_18B20换成基于计数的delay_us(n)这样晶振换了只改重装值代码逻辑不动。我现在的习惯是凡是跟外部器件打交道的延时一律走定时器不再信任while(i--)。6. 二次开发把两点测温扩成一线多点再加一路报警联动这套代码最值得动手改的地方是把「两个 I/O 各带一颗」升级成「一根总线挂多颗」。前者是课程设计的交差写法后者才是单总线真正的价值所在——一根线挂八颗、十颗布线成本几乎不变这在机柜测温、冷链车厢多点布控这类场景里是实打实的优势。改造的核心是两条命令0x33读 ROM和0x55匹配 ROM。每颗 DS18B20 出厂时都烧了一个 64 位激光 ROM前 8 位是家族码0x28中间 48 位是唯一序列号最后 8 位是 CRC 校验。挂多颗时先单独接一颗、用0x33把 ROM 码读出来存进数组全部登记完之后再统一用0x55加 ROM 码点名操作。/* 读当前总线上唯一一颗器件的 64 位 ROM 码多颗时必须逐颗单独接 */ void Read_ROM(unsigned char *rom) { unsigned char i; Init_DS18B20(); WriteOneChar(0x33); /* READ ROM总线上只能有一颗时才安全 */ for (i 0; i 8; i) rom[i] ReadOneChar(); /* 低字节在前rom[0] 恒为家族码 0x28 */ } /* 点名启动转换只让 ROM 码匹配的那一颗开始测温 */ void Convert_By_ROM(unsigned char *rom) { unsigned char i; Init_DS18B20(); WriteOneChar(0x55); /* MATCH ROM */ for (i 0; i 8; i) WriteOneChar(rom[i]); WriteOneChar(0x44); /* 只有被选中的那颗响应转换命令 */ }rom[0]读出来不是0x28基本可以断定接线有问题或者时序已经跑偏可以用它做一次快速自检。另外 CRC 那一位建议校一下八颗器件挂在同一根线上偶尔会有一位翻转不校验的话会拿到一个不存在的 ROM 码后面点名永远没人应答。再往下走还有三处可以按需加改造点涉及函数需要注意温度上下限报警WriteOneChar(0x4E)写 TH/TL报警值存在非易失寄存器掉电不丢超限驱动继电器新增Alarm_Check()51 的 I/O 灌电流有限继电器要加三极管串口上传温度定时器 1 做波特率、SBUF发数据11.0592MHz 晶振波特率更准12MHz 有误差分辨率降到 10 位写配置寄存器第 5 字节转换时间从 750ms 降到 187.5ms显示刷新能提上来改的时候有个顺序建议先把采样周期从死延时换成定时器节拍再把 ROM 寻址接进去最后才加报警逻辑。反过来做的话报警阈值刚调好一改节拍又全乱了白测一轮。我第一次做这个改造时图省事直接在主循环里塞了八次ReadTemp()每次转换后干等 750ms结果一轮扫完六秒多屏幕上第一路的温度已经是六秒前的数据了。后来改成「广播启动转换 → 等 750ms → 逐个点名读暂存器」一轮下来还是一秒出头数据新鲜度立刻不一样。从那以后我每次碰单总线都先把「命令广播」和「数据读取」这两个阶段拆开写再谈别的优化。这套源码和原理图都在包里照着第 2 章的引脚表接一遍比盯着文档看十页都管用时序上卡住了就翻回第 5 章对现象。希望帮到你。本文还有配套的精品资源点击获取