H38 数码管 DS18B20 温度计只用三位共阳数码管,12 位分辨率 0.0625℃,量程 -55~125℃。排版按负号、整数、小数点、小数的顺序逐级牺牲;小数用整数乘法换算,绝不链接浮点库。T0 每 50ms 采集、T1 每 5ms 扫描,连续 4 次读数不同才更新显示。

01 设计任务
功能介绍:
使用51单片机 + DS18B20 做一个数字温度计,三位共阳数码管实时显示,要求如下:
1、DS18B20 单总线测温:DQ 接 P3.4,4.7K 上拉到 +5V,12 位分辨率 0.0625℃,测量范围 -55~125℃;
2、三位数码管实时显示:正温度显示"33.5"这种一位整数+一位小数的格式,个位后带小数点;
3、自适应排版:温度上了百位(100℃以上)自动砍掉小数显示"125",零下显示"-8.5"——同一块三位屏,装下 -55~125℃ 全量程,这是本篇的核心;
4、不链接浮点库:小数部分用整数乘法换算(x*6.3 近似 0.0625℃/LSB),整个程序 code 只有 1299 字节;
5、温度稳定性过滤:连续 4 次读到不同值才更新显示,读数不抖不跳;
6、双定时器分工:T0 每 50ms 采集并换算,T1 每 5ms 扫一位数码管,两位各干各的。
特别注意:DS18B20 在前面 H22(家用烟雾报警器)、H34(火焰烟雾温度报警器)、H35(烟雾温湿度报警器)都出现过,但那三篇都是 LCD1602 显示 + 报警逻辑,温度只是报警的一个条件。这一篇是纯显示,把 DS18B20 单独拎出来讲透——尤其是"只有三位数码管"这个约束逼出来的排版策略,比 LCD 那种"想写几行写几行"有意思得多。



02 三位数码管怎么装下 -55~125℃
先算笔账:DS18B20 的量程是 -55~125℃。最长的显示形式是"-55.2"——五个字符。可硬件只有三位。
电路是硬约束:SEG1/SEG2/SEG3 接 P2.0~P2.2,就三个位选,多一位都没有。要么扩硬件,要么让显示内容随温度大小自适应变化。这份程序选了后者,Display_data() 里每一轮扫描都要先判断"现在该显示哪三位":
正温度、没有百位(0~99.9℃)——最常用的区间,精度优先:
| 位 | 内容 | 例(33.5℃) |
| 第 1 位 | 十位 | 3 |
| 第 2 位 | 个位 + 小数点 | 3. |
| 第 3 位 | 十分位 | 5 |
正温度、有百位(100~125℃)——量程优先,砍掉小数:
| 位 | 内容 | 例(125℃) |
| 第 1 位 | 百位 | 1 |
| 第 2 位 | 十位 | 2 |
| 第 3 位 | 个位 | 5 |
负温度、没有十位(-9.9~0℃):
| 位 | 内容 | 例(-8.5℃) |
| 第 1 位 | 负号 | - |
| 第 2 位 | 个位 + 小数点 | 8. |
| 第 3 位 | 十分位 | 5 |
负温度、有十位(-55~-10℃):负号占一位,十位占一位,第三位放个位——小数被挤掉了,显示"-25"。
看出来了吧,这是一个"谁先被牺牲"的优先级排序:负号 > 整数部分 > 小数点 > 小数部分。符号必须留(否认知意义都变了),整数必须留(不然读数错),所以一旦温度绝对值上了两位,第一个被砍的就是小数。
程序里就是三层判断嵌套:外层看有没有负号(TempBuffer1[0]!=16),中层看百位(TempBuffer1[1])或十位(TempBuffer1[2])有没有值,内层决定这一位送整数还是送小数。虽然写出来有点长,但每一屏都在做"当前信息量 vs 显示面积"的最优解——这种思路搬到任何资源受限的显示场景都成立:段码屏、点阵屏、甚至 OLED 上的小字号区域。
03 三位一体:数据换算的三个动作
covert_data() 把 DS18B20 的 16 位原始值翻译成 6 个显示下标,干三件事:
第一件:处理负温。 DS18B20 输出的是补码。判定方法看最高位:if(data_new & 0x8000)——为 1 就是负温。还原绝对值也是标准操作:
负温判定看数据最高位,为 1 就把高、低两个字节分别取反,低字节加一后写回;如果低字节加一超过 255,说明进位溢出,再把高字节补一。
取反加一,再处理低字节加一溢出到高字节的进位——这个写法在 H22 讲过,这里再确认一遍:补码转绝对值不是"去掉符号位"那么简单。
第二件:拼整数部分。 DS18B20 的数据格式是:高 5 位符号、中间 7 位整数、低 4 位小数。所以要各取一段拼起来:
高字节左移四位后与 0x70 相与,只留下 bit6~4 这段整数位;低字节右移四位再与 0x0f,取出整数的低四位,两段或起来拼成完整整数。最后用除和求余一次拆出百位、十位、个位,分别送进显示缓冲的第 1、2、3 位。
拆百十个位用"除 + 求余"老配方,一次拆三个(H30 讲过 BCD 进位链,这里简单粗暴但够用)。
第三件:小数换算——本篇最巧的一行。
取原始值的低四位,乘以 6.3 换算成实际的小数,再除 10 送十分位、对 10 求余送百分位。
DS18B20 的低 4 位是 1/16℃ 的计数(0~15),每格 0.0625℃。要变成"百分位整数",就乘以 6.25——程序里写成 6.3,误差可以忽略(15×6.3=94.5,本该 93.75,最多差 1 个百分位,反正只显示到十分位)。
为什么不直接算 x*625/100?因为编译器会为除法生成一长串代码;也不查表?文件里其实定义了一张表:
程序里另外定义了一张四元素的常量表,按 0、25、50、75 依次存放 12 位分辨率下低四位四种取值对应的小数换算结果。
这是"12 位分辨率下低 4 位只有 0/4/8/12 四种取值"的查表方案——但它一行都没被调用(残留代码)。原因是:查表要先 t[0]>>2 再查,还得处理 9/10/11 位分辨率的情况,反而不如一次乘法来得通用。两种方案留一行注释、选更简单的那个,这也是工程习惯。
顺带一个细节:代码里 x = t[0]; y = x*6.3; ——x 在前面已经被 t[1]|=x 用过,这里重新赋值接着用。同一变量承担两次中间运算,在 RAM 只有 128 字节的 51 上是刻意的节约(整份程序 data 才用 26 字节)。
04 显示扫描:消隐顺序和 5ms 节拍
Display_data() 挂在 T1 中断上,5ms 一次,三位轮流点亮——15ms 扫完一轮,约 66.7Hz。这个频率刚好在"人眼不觉得闪"的下限之上(一般要求 >50Hz),又不会因为扫太快导致每位亮度不足。
每一轮扫描都先把三位位选全部关掉,再把这一位该亮的段码送到 P0,最后才打开本位的位选。
先关位选、再改段码、最后开位选——顺序反了(先改段码再关位选)就会在切换瞬间把上一位的段码"照"到新一位上,出现鬼影(俗称拖影)。这个坑在 H23、H26、H30 都踩过,这里再钉一遍:动态扫描三步骤,消隐永远在第一步。
小数点是靠段码最高位实现的:共阳段码 & 0x7f 把 DP 位清 0(共阳低电平点亮),小数点就亮了。所以"个位带小数点"不是额外加一个器件,而是改一下这一位送出去的字节。
段码表尾部还编进了两个特殊下标:下标 16 是 0xbf——二进制 1011 1111,只有 bit6 为 0,而 bit6 正是中间的 g 段,所以它显示出来就是一根横杠,当负号用;下标 17 是 0xff,八段全灭。用"下标 16/17"当负号和熄灭,和 H37 的 Error 字母一个套路:把"特殊符号"混编进段码表,显示逻辑里就不用写特殊分支。
05 温度稳定性过滤:为什么读数不跳
中断里读温度这一段,藏着一个和 H34 同源但参数不同的过滤:
每轮采集先读一次温度,和当前显示值比较:不一样就把计数加一,超过三次时清零计数并接受新值;一旦读回和显示值相同的数据,计数立刻清零。
DS18B20 是 0.0625℃ 分辨率,温度在临界点附近时,最后一位会在两个相邻码之间来回跳(比如 33.5 和 33.5625 之间)。如果每 50ms 都直接刷新,屏幕上的最后一位就会不停抖。
这段代码的策略是:新值要连续 4 次(200ms)都和当前显示值不同,才承认变化。中途只要读回一次旧值,计数立即清零重来。效果是——真实温度缓慢变化时,显示每 200ms 更新一次,平滑;噪声抖动时,因为跳来跳去凑不满 4 次连续不同,屏幕纹丝不动。
H34 用的是"连续 10 次",这里是"连续 4 次",本质是同一个去抖滤波器:阈值越大越稳但越迟钝。选 4 是因为 50ms 的采样周期本身够快,200ms 的响应延迟人感觉不出来;H34 那边是报警场景,宁可迟钝也不能误报。
06 双定时器分工,以及一处值得商榷的架构
这份程序用了两个定时器,职责划得很清楚:
· T0(50ms,方式 1):读 DS18B20 → 转换 → 更新显示缓冲 TempBuffer1;
· T1(5ms,方式 1):只做数码管扫描,读现成的显示缓冲。
采集与显示解耦,这是正规做法:显示刷新要稳(5ms 一节拍雷打不动),采集慢一点无所谓。缓冲区当"邮箱",T0 往里放、T1 从里取,互不阻塞。
但有一处值得商榷:ReadTemperature() 是在 T0 中断里调的。翻一下 DS18B20.h 就知道这个函数有多"重":
· Init_DS18B20() 里 delay_18B20(80) —— 拉低总线等 480us 以上;
· ReadOneChar() 每位有 delay_18B20(4),8 位 = 32 个循环;
· 整个函数里有 3 次初始化 + 3 次写命令 + 2 字节读。
这些"循环延时"在 12MHz 晶振下单个循环约 8us,delay_18B20(80) 就是 640us 左右。加起来整个读温度过程要几毫秒,全都压在 50ms 的中断服务程序里。后果是:每次采集,T0 中断会长时间占用 CPU,与 T1 的 5ms 显示扫描抢时间,极端情况下会让数码管某一位亮度变暗或者轻微抖动。
工程上更稳的写法是:中断只管定时标志,读温度放到主循环里做——
改成定时器中断里只累加计时、置一个读温度标志,具体读温度的动作放到主循环,看到标志才调用读温度函数、更新显示缓冲。
这样中断永远是"短平快",几十微秒就退出,显示扫描绝不会被采集打断。这份程序能正常跑(仿真里 12MHz + 三位显示,余量还够),但"中断里不做长时序操作"这条纪律值得记住——DS18B20、LCD1602 忙等待、EEPROM 写周期(H31 的 5ms)都属于"重活",都该放主循环。
还有一个隐藏细节:代码里转换后的等待只有 delay_18B20(80)(几百微秒),而 12 位分辨率的转换时间需要 750ms。为什么还能读到数据?因为这是在 50ms 周期的循环里连续跑——每次读到的是上一轮(甚至更早)已经转换完成的结果,数据一直在更新,只是滞后。单次上电第一次读就容易拿到 85℃(DS18B20 上电默认值 0x0550),这也是为什么很多人的仿真一启动屏幕先闪一下 85——不是坏了,是第一次转换还没完成。
07 排错清单
1、数码管显示 85 不动:上电默认值,等一轮转换就好;如果一直 85,查 DQ 有没有接 P3.4、4.7K 上拉有没有接 +5V——单总线没有上拉电阻,DS18B20 根本不会应答;
2、读数一直是 0 或 127:ReadTemperature() 里初始化失败的表现。Init_DS18B20() 里读了 x=DQ 却没判断——应答位是 1 说明器件没响应。排错时先把 x 打印/点灯出来看看;
3、温度正负颠倒或数值离谱:重点查补码处理和整数拼接那几行,t[1] <<= 4; t[1] &= 0x70; 这两句的掩码写错(比如写成 0x7f)就会把符号位混进整数;
4、显示有鬼影/拖影:扫描顺序错了,必须"先关位选、再送段码、最后开位选";
5、某一位特别暗:5ms 一位导致占空比 1/3,本来就是暗的;想亮一点就把 T1 初值改小(比如 2ms),但一轮扫描不能低于 10ms(否则会闪);
6、小数位不对:y = x * 6.3 里的 x 要先 & 0x0f 取低四位,漏了就会把整数部分也算进小数;
7、源码几处小瑕疵,心里有数就行:① Time_1s++ 在 50ms 中断里累加,名字叫 1s 实际是 50ms,而且全文没用过;② dotcode[4] 查表数组定义了没调用(见 03 节);③ Init_DS18B20() 的返回值丢掉了,初始化失败无感知。
08 总结
DS18B20 这个器件前面出现过三次,但都是"配角"(报警条件之一)。这一篇让它当主角,值得记住的其实是约束下的设计:只有三位显示位,就得在"符号、整数、小数"之间排优先级;RAM 只有 128 字节,就得让变量复用、用乘法代替查表、坚决不碰浮点(code 1299 字节就是证明)。
到这里,数码管这条线(H23、H26、H27、H30、H37、H38)和温度传感器这条线(H22、H34、H35、H38)都算走通了。下一篇换个器件,继续。
欢迎关注,持续更新!
09 文件日期修改
如果你想把文件修改为最新的日期,又不想新建工程,那么你可以参考下面的文章。
"如何不打开程序的情况下更改proteus和keil程序的日期?"
工具和仿真文件在一起,见下面的下载链接。
10 资料下载
全套资料包含 Keil 工程(C 语言源码 + hex 文件)、Proteus 仿真工程、设计报告、程序流程图、讲解视频,以及编译截图和仿真截图。下载后可直接打开运行,也可用于课程设计、毕业设计参考。
也可以关注公众号「单片机仿真」,回复 H38 获取下载入口。公众号内还有 A / T / H 全系列 400 多个单片机仿真实例,回复「目录」可查看完整清单。

