ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

AT89C51双路DS18B20测温与1602显示:单总线时序与寻址避坑

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 电解电容正极,电容负极接 +5V,RST 再经 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 表示该位为 1,0 表示该位为 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结果为 0,DQ 保持低电平直到延时结束。读函数里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最大 15,15 × 625 = 9375,除以 1000 得 9,不会溢出。如果显示层要两位小数,把除数换成 100 就行,但要注意 1602 一行只有 16 个字符,格式得重新排。

小数部分按 4 位刻度换算,是因为配置寄存器默认 R0=R1=1,工作在 12 位分辨率。这个寄存器是暂存器的第 5 字节,改它就能在 9 到 12 位之间切分辨率,位数越低转换越快:9 位最长 93.75ms,10 位 187.5ms,11 位 375ms,12 位 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 里因为仿真模型不模拟端口锁存,可能侥幸正常;焊到实物上,读忙标志会一直读到 0,LCD_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 认的是 ASCII,temp_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声明为uchar,b << 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 章对现象。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表