STC89C52RC驱动DS18B20温度传感器数码管显示实战解析
2026/9/1 3:03:32 网站建设 项目流程

简介:本资源是一套面向单片机初学者与课程设计学生的完整嵌入式实践项目,基于STC89C52RC单片机实现DS18B20数字温度采集与四位数码管实时显示功能,覆盖硬件驱动、单总线通信、动态扫描显示及KEIL C51工程构建等核心技能点。压缩包共26个文件,包含3个核心C源文件(main.c、18b20.c、delay.c)、2个头文件(18b20.h、delay.h)、1个KEIL工程文件(.uvproj)、1个PDF开发板原理图、1个可直接烧录的.hex文件,以及编译生成的.obj、.lst、.m51等辅助文件,完整呈现从代码编写、编译调试到硬件验证的全流程,包体仅635KB,结构清晰、即开即用。已有342人学习下载,适合单片机原理课设、电子实训或自学进阶使用——读者可快速理解DS18B20单线协议时序实现、数码管段码/位码控制逻辑,并通过原理图反向梳理IO口分配与外围电路连接关系,显著提升软硬协同开发能力。 前两天整理移动硬盘,翻出一个很老的压缩包,名字是“STC89C52RC单片机DS18B20 温度传感器 数码管显示 软件源码KEIL C51工程文件+开发板PDF原理图.zip”。这种资料包在单片机学习圈里几乎人手一份,问题也几乎一样:烧进去之后温度永远显示85℃,或者数码管乱跳,甚至屏幕完全不亮。很多人折腾两三天没结果,就把锅甩给“资料是坏的”。其实大部分情况下工程包是好的,只是你还没搞清楚STC89C52RC、DS18B20和数码管这三样东西是怎么配合的。这篇文章就是我基于这类工程包从复现到改造的完整记录,适合正在学51单片机、想用DS18B20做温度采集显示,或者手里有类似源码但没跑通的朋友。我会把KEIL C51工程怎么打开、DS18B20时序怎么理解、数码管扫描怎么写、原理图怎么对照,以及调试时怎么一步步定位问题,全部讲清楚。

1. 工程包到手之后先看什么:文件构成、编译器版本和烧录前的核对清单

1.1 压缩包里通常有哪些文件

这类工程包解压后,一般不会只有一个孤零零的文件,而是会有一堆看起来杂乱无章的内容。以我手里这份为例,解压后常见的是这几类:

  • .uvproj.uvprojx:KEIL 的工程文件,双击可以直接打开。如果是老版本项目,可能是.uv2
  • .c.h源码文件:核心的驱动代码,例如main.cds18b20.cdelay.csmg.c(数码管)。
  • .hex编译好的烧录文件:如果不想重新编译,可以直接用 STC-ISP 烧录。
  • PDF 原理图:开发板的完整硬件连接图,这个是排查问题时的“宪法”。
  • 可能还有运行说明文档、Proteus 仿真文件、取模软件截图等。

很多人下载后直接双击.uvproj,然后发现 KEIL 报错,或者打开后显示一堆乱码、找不到源文件,就开始怀疑资料有问题。实际上,很可能是编译器版本和工程不匹配。

1.2 KEIL C51 工程和 MDK 工程的区别

这里必须先说清楚一个新手最容易踩的坑:STC89C52RC 是 51 单片机,内核是 8051,必须使用 KEIL C51 编译器,而不是 KEIL MDK(也就是现在通常说的 KEIL 5 的 MDK-ARM 版本)。如果你电脑里只装了 KEIL MDK,直接打开.uvproj大概率会提示“没有匹配的编译器”或者编译报出一大堆跟 ARM 有关的错误。

正确做法是:

  • 安装 KEIL C51 版本,旧一点的像 C51V9.54、C51V9.60 都可以。
  • 打开工程后,进入Project -> Options for Target -> Device,确认芯片型号选择为 STC89C52RC 或者 AT89C52。如果没有 STC 选项,就用 STC-ISP 软件内置的器件库导入,或者直接选 AT89C52,两者寄存器完全兼容,时序也一样。
  • Output选项卡里勾选Create HEX File,这样编译之后才能生成.hex文件。

如果工程文件被加密或者损坏,最简单的方法是自己重新新建一个空工程,然后把源码文件添加进去,重新写启动配置。51 单片机不比 Cortex-M 复杂,新建工程花不了几分钟。

1.3 烧录前的核对清单

很多工程包作者用的开发板和你手里的板子不一定一样。烧录之前,先花两分钟核对下面这几点:

  • 系统时钟:代码里的延时函数是基于多少 MHz 晶振写的?常见 11.0592MHz 和 12MHz。STC89C52RC 板子上通常配 11.0592MHz,但也有不少板子用 12MHz。如果代码时钟参数和实际晶振不一致,DS18B20 时序就会出问题。
  • DS18B20 接在哪个引脚:代码里可能定义sbit DQ = P3^2;,你的板子上 DS18B20 插针是否真的连到了 P3.2?
  • 数码管类型:共阴还是共阳?段选、位选分别接在哪个 IO?有没有锁存器?
  • DQ 上拉电阻:DS18B20 的数据线必须接 4.7k 上拉到 VCC,很多学习板预留了孔位但没焊。
  • 烧录软件:STC-ISP 里选择芯片型号为 STC89C52RC,串口号正确,频率选择尽量和开发板一致,否则串口下载可能失败。

我见过太多人“先烧一下试试”,结果卡在串口识别上,然后又去折腾驱动,其实这些都是小事。真正麻烦的是程序能跑但温度不正常,这时候才需要进入下一层,去理解 DS18B20 的时序。

2. DS18B20 三段时序逐条拆:初始化、写位、读位的边界条件

2.1 单总线器件为什么难调

DS18B20 是 DALLAS(现在归 Maxim)出的单总线数字温度传感器,所谓单总线,就是数据和控制都通过一根 DQ 线来传输。主机既可以给这根线灌高低电平去控制传感器,又可以释放总线去读取传感器拉低的电平。这种设计省 IO,但代价就是时序要求非常严格,所有操作都是一条一条的微小时间窗口。

很多工程包的源码里,一眼看过去就是几个 delay 函数和 DQ 引脚操作,缺少注释。如果直接复制到别的板子上,晶振不同、延时精度不同,温度就会变成乱码或者固定值。所以读懂时序比背代码重要得多。

2.2 初始化时序:拉低至少 480us,然后释放,等存在脉冲

DS18B20 每一次通信前都要先复位。复位时序大概是这样:

  • 主机把 DQ 拉低,持续 480us 到 960us。注意,这个时间宁可长一点,不要短于 480us。
  • 主机释放总线(DQ = 1),然后等待 15us 到 60us。
  • DS18B20 检测到总线上的上升沿后,会等待一会,然后主动把 DQ 拉低 60us 到 240us,作为“存在脉冲”,告诉主机我在这里。
  • 主机读取这个低电平,确认传感器在线。

一个常见的复位函数可以这样写:

sbit DQ = P3^2; unsigned char ds18b20_reset(void) { unsigned char presence; DQ = 0; // 主机拉低总线 delay_us(500); // 至少 480us DQ = 1; // 释放总线 delay_us(60); // 等待 15~60us presence = DQ; // 读取存在脉冲 delay_us(400); // 等待剩余复位时间结束 return presence; // 返回 0 表示传感器在位 }

这里的delay_us(500)在实际 12MHz 晶振、12T 模式下,可能实际延时不是 500us,而是更长。后面我会专门说延时精度的坑。但如果初始化返回的 presence 是 1(高电平),说明 DQ 上没有传感器拉低,要么传感器没接好,要么上拉电阻失效,要么时序时间太短。

2.3 写时序:写 0 和写 1 的区别,关键在于释放时机

DS18B20 的写时序是把每一个“位”分成一个时间槽(slot),每个槽至少 60us,两个槽之间要有至少 1us 的恢复时间。写 0 和写 1 的区别如下:

  • 写 1:主机拉低总线,然后必须在 15us 内释放总线,让上拉电阻把总线拉高,一直保持到槽结束。
  • 写 0:主机拉低总线,并且在整个 60us 槽内保持低电平,不要释放。

所以写字节的程序思路就是逐位判断,如果是 0 就拉低一整段时间,如果是 1 就拉低后马上释放:

void ds18b20_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DQ = 0; _nop_(); // 保证拉低时间 if (dat & 0x01) { DQ = 1; // 写 1:在 15us 内释放 } delay_us(60); // 保持时隙 DQ = 1; // 恢复总线 dat >>= 1; } }

很多代码里会把if (dat & 0x01)改成if (dat & 0x80)然后左移,只要保证位序正确即可。关键点是:写 1 时从拉低到释放之间的时间不能太长,如果超过了 15us,传感器会认为你在写 0。我在调试中见过不少程序,因为用了低效的 delay 函数,实际释放时机拖到了 30us 以上,导致写入的指令全错。

2.4 读时序:主机拉低后快速释放,在 15us 内采样

读时序和写时序几乎对称。主机先拉低总线,持续至少 1us,然后释放总线,之后 DS18B20 会决定把总线拉低或保持高电平,分别对应读回 0 和 1。主机必须在释放总线后的 15us 内完成采样,否则会读到后面无意义的电平。

unsigned char ds18b20_read_byte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; DQ = 0; _nop_(); // 拉低 1us 以上 DQ = 1; // 释放总线 _nop_(); if (DQ) // 在 15us 内采样 { dat |= 0x80; } delay_us(60); // 等待时隙结束 } return dat; }

读时序最容易犯的错误是采样点太晚。比如在释放总线后延时了 30us 再去读,这时候 DS18B20 可能已经把总线拉高或者进入下一个状态,读回来的数据就乱了。如果你用示波器观察,标准读时序在释放后会产生一个很窄的低电平窗口,必须在窗口内采样。

2.5 延时函数精度:why 不能照搬

前面所有时序操作都依赖 delay_us,但 C51 里最常见、也最坑的 delay_us 是这样写的:

void delay_us(unsigned int us) { while (us--); }

在 STC89C52RC 的 12T 模式、12MHz 晶振下,一个机器周期是 1us。但是while (us--)这个循环本身包含比较、自减、跳转指令,每执行一次大约消耗 4 到 5 个机器周期,所以这个函数实际延时大约是 4 到 5 倍参数值。如果原作者基于经验设的参数,换一个编译器优化等级后时间还会变。

更稳妥的做法是直接用 STC-ISP 软件里的“软件延时计算器”,让它生成对应频率下的精确延时函数。生成的代码通常长这样:

void delay_us(unsigned int us) //@12.000MHz { unsigned char i; while(us--) { _nop_(); i = 10; while (i--); } }

不同晶振生成结果不同,但至少这套代码是作者实测过的。如果你要复制别人的工程,第一步就是把所有延时函数替换成自己开发板频率对应的版本。

3. 数码管动态扫描:共阴共阳、消影顺序和温度数字拼装

3.1 共阴共阳和段码表

数码管显示看起来简单,但引脚定义搞错会显示成乱码。数码管有共阴和共阳两种,共阴就是把所有 LED 的阴极接到公共端,共阳就是所有阳极接到公共端。对于单个数码管,段选码决定了显示什么数字。常见的共阴段码表如下:

显示共阴段码共阳段码
00x3F0xC0
10x060xF9
20x5B0xA4
30x4F0xB0
40x660x99
50x6D0x92
60x7D0x82
70x070xF8
80x7F0x80
90x6F0x90
负号0x400xBF
熄灭0x000xFF

如果你的板子是共阳,却用了共阴段码,那么显示出来的数字会变成“补码”一样的效果,比如想显示 0,结果显示 8 或者全灭。代码里unsigned char code seg_table[] = {0x3F, ...};这种数组,必须根据硬件原理图改。

3.2 动态扫描原理:用视觉暂留骗过人眼

任何时刻只能点量一个数码管,通过快速轮流点亮不同位,利用人眼视觉暂留,看起来就像同时亮着。扫描频率至少要 50Hz,也就是 20ms 内完成一轮四位数码管的扫描,通常每一位点亮 1ms 到 2ms。如果每位的点亮时间太长,会看到亮度不均;太短则会变暗。

最简单的扫描代码:

void display_scan(unsigned char *buf) // buf[0]~buf[3] 存放段码 { unsigned char i; P0 = 0x00; // 先消隐 for (i = 0; i < 4; i++) { P0 = buf[i]; // 段选数据 P2 = (1 << i); // 位选,假设 P2.0~P2.3 控制位 delay_ms(2); P0 = 0x00; // 消隐,防止鬼影 } }

这里有两个关键点。

第一,位选和段选的配合顺序。通常先送段码,再送位选,或者先送位选,再送段码,取决于锁存器电路。但无论顺序如何,在切换位选之前,一定要先把当前位的段码清零,否则会看到前一个数字的残影。

第二,消隐不是可选项。我见过不少新手的代码没有消隐,结果数字边缘拖着一点点虚影,虽然不大,但看着很别扭。如果在高速扫描下消隐时间太长,亮度又会下降。一般消隐就是几条P0 = 0x00;,成本很低。

3.3 把 DS18B20 温度值拼成数码管数据

DS18B20 返回的是两个字节,LSB 和 MSB。12 位分辨率下,温度寄存器低 4 位是小数部分,有效温度等于:

temp = (MSB << 8 | LSB) * 0.0625

C51 里不擅长浮点运算,更常见的是用整数处理。比如:

int temp_raw; unsigned char int_part; unsigned char dec_part; temp_raw = (MSB << 8) | LSB; // 注意 MSB 是带符号扩展的高字节 if (temp_raw & 0x8000) { temp_raw = ~temp_raw + 1; // 负数取补码 negative_flag = 1; } int_part = temp_raw >> 4; dec_part = ((temp_raw & 0x0F) * 625 + 50) / 1000; // 四舍五入到 0.1℃

然后显示时,把int_part的十位、个位分别放到对应位,再把dec_part放到最后一位并点亮小数点。如果温度是负数,就在最前面显示一个负号,用段码 0x40 或 0xBF,看共阴共阳。

这里要提醒一点:temp_raw = (MSB << 8) | LSB;在 C51 里,如果 MSB 是有符号 char,左移后整型提升可能会产生符号位问题。更保险的写法是把两个字节先转成 unsigned char,再拼成 int,然后用减法处理补码:

int temp_raw = ((int)(unsigned char)MSB << 8) | (unsigned char)LSB; if (temp_raw & 0x8000) { temp_raw = (int)(0x10000 - (unsigned int)temp_raw); }

注意 51 的 int 是 16 位,0x10000已经超过 int 范围,所以直接用temp_raw = ~temp_raw + 1;更符合 C51 的习惯。

4. KEIL C51 工程主循环设计:定时、刷新、状态标志和优化陷阱

4.1 main 函数结构:别在温度转换时卡死屏幕

DS18B20 发起温度转换后,需要等待最长 750ms 才能读取结果。如果主循环里直接执行“发转换 -> 延时 750ms -> 读温度 -> 显示”,那么这 750ms 内数码管是完全不刷新的,你会看到数码管明暗闪烁,而且按键响应也会卡顿。

更好的做法是把温度读取和显示拆开,在循环里快速扫描显示,每隔一段时间才去读一次温度。比如:

void main(void) { unsigned char i; unsigned char buf[4]; while (1) { // 每循环扫描显示约 50 次,约 100ms 后刷新一次温度 for (i = 0; i < 50; i++) { display_scan(buf); delay_ms(2); } read_temperature_from_ds18b20(buf); // 更新 buf } }

这样温度刷新频率是 10Hz,足够日常使用,而且数码管不会闪。

4.2 温度转换命令和执行流程

一次完整的温度读取流程是:

  1. 复位,检测存在脉冲。
  2. 发送 0xCC(跳过 ROM),再发 0x44(启动温度转换)。
  3. 等待转换完成。如果你不想硬等 750ms,可以在转换命令之后不立即读,而是先扫描一遍数码管,等时间差不多了再去读温度。
  4. 复位,再发 0xCC,再发 0xBE(读暂存器)。
  5. 读取 LSB 和 MSB。

这个流程在源码里一定要写清楚,很多工程包的问题在于作者把启动转换和读取写在同一个函数里,中间还加了一个大延时,导致整个程序看起来“卡了很久”。

实际代码示例:

unsigned char ds18b20_read_temp(void) { unsigned char lsb, msb; ds18b20_reset(); ds18b20_write_byte(0xCC); // skip ROM ds18b20_write_byte(0x44); // start conversion delay_ms(750); // 等待转换 ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); // read scratchpad lsb = ds18b20_read_byte(); msb = ds18b20_read_byte(); return (msb << 8) | lsb; // 实际要返回 int }

如果你用 12MHz 晶振,750ms 延时可以用delay_ms(750);,但精度要求不高,因为传感器提前完成也不会导致错误。

4.3 存储位置:code、data、xdata 和段码表

51 单片机内部 RAM 只有 256 字节,扩展 RAM 也不大。STC89C52RC 的 Flash 有 8KB,因此常量数组应该放到 code 区:

unsigned char code seg_table[] = {0x3F, 0x06, ...};

如果把段码表定义成普通数组,就会占用宝贵的内部 RAM,数据多一点之后很容易出现“变量空间溢出”的报错。同理,延时函数里的临时变量可以用 unsigned char,只要够用,不用 unsigned int 就不用。

4.4 编译优化陷阱:volatile 与延时函数被吃掉

这是 KEIL C51 下非常经典的问题。前面提到过,很多同学的delay_us是这样写的:

void delay_us(unsigned int t) { while (t--); }

如果 KEIL 的优化级别比较高,编译器发现这个循环没有任何输出,只是修改一个局部变量,就会把这个循环直接优化成空,或者大幅减少循环次数。结果就是程序里调用了delay_us(500),实际可能只执行了几条 NOP。DS18B20 初始化时序完全乱套,自然读不到正常温度。

解决方法是:

  • 把延时函数里的循环变量声明为volatile
void delay_us(unsigned int t) { volatile unsigned int i = t; while (i--); }
  • 或者把优化级别调低,比如在Options for Target -> C51里面选择 Optimization Level 0 或 1 先调通,确认功能正常后再尝试优化。

我个人的习惯是:所有用于特定外设时序的延时函数,一律用 STC-ISP 生成的版本,并且把相关变量定义成 volatile。这样更换板子和晶振时,只需要重新生成一次延时函数,不会因为优化等级变化而翻车。

5. 开发板PDF原理图与代码引脚对照:上拉、锁存器、供电一个都不能错

5.1 DS18B20 三根线的接法

DS18B20 通常是一个 TO-92 封装的三脚元件,正面朝自己时,引脚排列是:1 脚 GND,2 脚 DQ,3 脚 VCC。千万不要接反,接反大概率会发烫甚至烧毁。

在原理图上,DQ 引脚通常会画一条线连到单片机某个 IO,同时会有一个电阻从 DQ 拉到 VCC,这个电阻阻值一般是 4.7k。如果你的开发板原理图里看不到这个电阻,那就要自己在外接传感器时加一个。没有上拉电阻,单总线空闲状态就是一个悬空电平,复位时传感器无法产生稳定的存在脉冲。

检查方法:用万用表量 DQ 对地电压。正常空闲时应该是接近 VCC 的高电平,比如 4.8V 左右。如果测到 0V 或者飘忽不定,先查上拉。

5.2 数码管的锁存器和限流电阻

很多开发板为了让 IO 口复用,数码管会用 74HC573 或 74HC245 这类锁存器隔离。原理图里会看到 P0 口既连锁存器输入,又连其他外设。这时,代码里控制数码管的步骤就不是简单的P0 = seg_code,而是:

  1. 把段码送到 P0。
  2. 把段选锁存器的 LE(锁存使能)引脚置 1,然后置 0,数据被锁存。
  3. 再送位选码到 P0。
  4. 把位选锁存器的 LE 引脚拉高再拉低。

比如常见开发板用 P2.6 控制段选锁存器,P2.7 控制位选锁存器:

void display_scan(unsigned char *buf) { P0 = 0x00; // 消隐 LE_SEG = 1; LE_SEG = 0; P0 = buf[0]; // 第一个数码管的段码 LE_SEG = 1; LE_SEG = 0; P0 = 0xFE; // 位选:低电平稳第一个数码管 LE_BIT = 1; LE_BIT = 0; delay_ms(2); }

如果你的代码里直接把P0当段码输出,却发现数码管不亮,大概率是漏掉了锁存器操作。看 PDF 原理图时,搜索“74HC573”或者“SEG”“BIT”这些网络标签,就能知道引脚对应关系。

5.3 引脚定义不一致:代码里的 sbit 必须和原理图一一对应

这类工程包里,DS18B20 的 DQ 可能定义在sbit DQ = P3^2;,你的开发板却把传感器插到了 P1.0。如果不去改,程序当然读不到温度。数码管也一样,段选、位选可能不同。

所以拿到工程后,第一步就是用 PDF 阅读器搜索原理图中的传感器接口,找到 DQ 连到单片机的哪个引脚,然后统一修改代码里的 sbit 定义。这个排查非常机械,却非常有效。

5.4 供电问题:5V 和 3.3V 别混用

STC89C52RC 标准工作电压是 5V,DS18B20 在 3V 到 5.5V 都能工作,但如果你用 3.3V 给 STC89C52RC 供电,跑偏了之后很多型号工作不稳定。给 DS18B20 供电时,最好直接用开发板的 5V 电源轨,不要从某个 IO 口反灌电源,也不要和电机这类大电流设备并联。

另外,DS18B20 虽然号称“寄生供电”可以只用两根线,但在实际开发板上,我更推荐 VCC 和 GND 都接好,独立供电,数据线上拉也一样不能少。寄生供电在交流电干扰大、线长超过 30cm 的情况下,容易读取失败。

6. 一次“温度永远85℃”的完整排查过程与修复

6.1 85℃这个数字是从哪来的

DS18B20 上电复位后,温度寄存器的默认值是 0x0550,换算成温度正好是 85℃。如果你的数码管稳定显示 85.0℃或者 85.1℃,基本说明程序里读到了传感器复位后的默认值,但是后续的温度转换没有成功,或者读取的数据有问题,程序没有把错误识别出来。

换句话说,传感器可能在线,但通信不完整。常见原因有:

  • DQ 上拉电阻缺失。
  • 晶振频率和延时函数不匹配。
  • 延时函数被优化成空。
  • 读取时序采样点太晚。
  • 引脚定义错误,程序访问的引脚根本没接 DS18B20。

6.2 排查过程的完整链路

我当时拿到的是一个显示 85.0℃的工程包,数码管能正常显示,按键也没问题,说明数码管部分没问题。于是按照下面顺序排查:

第一步,测 DQ 静止电压。万用表红笔接 DQ,黑笔接 GND,静态显示 4.96V,正常。说明上拉电阻没问题,传感器也接了电源。

第二步,在程序里临时把 DS18B20 复位函数的返回值用数码管显示出来。如果返回 0,说明传感器有回应;如果不为 0,说明初始化失败。当时显示的是 0,说明复位成功了。

第三步,怀疑是读取时序问题。没有示波器,我就写了一个测试程序:连续发送读出暂存器命令,把 LSB 和 MSB 读到数码管上显示。结果发现读出来的数据一直是 0x50 和 0x05,也就是默认值。这说明写入指令可能没有正确执行,或者读时序采样有问题。

第四步,检查写入指令的时序。我用 GPIO 翻转和逻辑分析仪观察 DQ 波形,发现写 1 时序中,拉低到释放的时间已经超过了 15us。正常应该是 5us 左右,但实际到了 40us。原因就是延时函数被优化了——不是变快,而是变慢了?这里要说明:因为while循环没有被优化掉,但编译器可能把局部变量 t 放在了不同的寄存器,循环体执行时间变长,导致每个时隙都超了。

更关键的是,我在Options for Target里看到优化级别被设成了 Level 8(最快)。虽然这不一定必然导致错误,但在某些 C51 编译器下,它会把“空循环”改成更短的指令序列。我把延时函数改成 volatile 后重新编译,问题依旧。接着把优化级别改成 Level 0,重新下载,温度立刻变成正常室温 27.5℃。

6.3 为什么优化级别会影响延时函数

C51 的优化会尝试减少循环跳转的开销,比如把多次空循环合并,或者把仅修改变量的语句识别为“无副作用”从而删除。DS18B20 的时序要求拉低时间必须大于 480us,如果一个delay_us(500)实际只执行了 10us,那传感器复位就永远不会成功。

解决后的经验是:

  • 所有延时函数,尤其是微秒级延时,不要依赖编译器优化。用volatile是第一步,但最可靠的还是用 STC-ISP 生成的精确延时函数。
  • 调通前,把优化级别调到 Level 0(Optimization Level 0)。确认跑通后,再逐步升高优化等级,每升一档都重新测试温度读取是否正常。
  • 如果开了优化后温度偶尔变成 85℃或者乱跳,不要犹豫,直接关掉优化,或者把关键延时函数单独放在一个.c文件里,对该文件选择“不优化”。

6.4 修复后的校准与验证

温度能正常读出来之后,还要做精度验证。DS18B20 的出厂精度是 ±0.5℃,在室温下通常够用。为了验证,我做了两个测试:

  • 冰水混合物:把传感器探头放在 0℃ 冰水混合物里,等待 2 分钟,显示 0.1℃左右,可以接受。
  • 沸水:水温 100℃,实测 99.6℃。这里要注意探头不能直接触底,否则测量的是锅底温度,会超过 100℃。

如果发现整体偏差很大,比如显示 30℃实际 25℃,可以检查供电电压和线路电阻,也可以查看是否使用了寄生供电。有一个更简单的方法:跑完温度转换后,再连续读两次暂存器,取平均值,能滤掉一部分读数抖动。但不要频繁读,因为 DS18B20 在转换期间读出来的值是上一轮的旧值。

7. 从固定温度计到可扩展的温控系统:改造思路与常见扩展

7.1 增加上下限报警和继电器控制

数码管温度计跑通之后,下一步自然想到的就是“超过某个温度就报警”。这个改造很容易,不需要改 DS18B20 驱动,只在主循环里加一个比较逻辑:

if (negative_flag == 0 && int_part >= 30) // 大于 30℃ { beep = 0; // 蜂鸣器响,低电平有效取决于电路 } else { beep = 1; }

如果要控制继电器,驱动方式类似,但要注意继电器线圈需要三极管或 ULN2003 驱动,STC89C52RC 的 IO 口直接拉继电器很容易烧片子。用继电器控制风扇、加热棒,就是一个简易温控系统。

7.2 总线上挂多个 DS18B20:跳过 ROM 命令就不够用了

单总线的价值之一是可以在同一根 DQ 上挂多个 DS18B20,每个 DS18B20 都有唯一的 64 位 ROM 编码。如果只用一个传感器,可以用 0xCC 跳过 ROM;挂多个时,需要先用 0x33(读 ROM)或 0xF0(搜索 ROM)获取每个传感器的编码,再通过 0x55 命令匹配对应编码来读取。

这在 51 单片机上完全可行,但要注意:

  • 总线长度不要超过 50cm,否则信号反射严重。
  • 多个 DS18B20 的总线上拉电阻可能需要减小为 2.2k。
  • 搜索 ROM 的算法比较长,网上有现成代码,直接移植时要小心位序和延时。

如果你只是想做一两个温度点的监控,也可以直接用两个不同的 IO 口接两个 DS18B20,各自独立读取,代码反而更简单。STC89C52RC 的 IO 足够多,没必要为难自己去搞单总线寻址。

7.3 升级显示器件:从数码管到 1602 LCD 或 OLED

数码管显示温度直观,但能显示的信息量太少了。如果想显示温度曲线、报警状态、历史极值,可以考虑换成 1602 字符液晶点阵屏,或者 0.96 寸 OLED 屏。OLED 用 I2C 接口,51 上软件模拟 I2C 也不难。

改造时最省力的方式就是保留现有的 DS18B20 驱动,只把display_scan换成 LCD 的显示函数。我实际做的时候,把温度值格式化成一个字符串:

sprintf(buf, "Temp: %d.%d C", int_part, dec_part);

然后调用 OLED 显示字符串,数码管那部分代码全部注释掉。整个移植过程大概一个晚上就能搞定,前提是 DS18B20 驱动已经稳定。

7.4 用串口把温度数据发给上位机

如果还想做数据分析,串口是很方便的通道。STC89C52RC 有 UART,通过 STC-ISP 下载线的 USB 转 TTL 接口连接电脑,把温度数据不断发出去。C51 的 printf 默认会把数据发到串口 1,但要先初始化串口波特率,并且printf对浮点数支持有限,最好用整数分别发送:

printf("T=%d.%d\n", int_part, dec_part);

这样电脑端用串口助手或者自己写的 Python 脚本就能记录温度曲线。这也是很多人拿到 DS18B20 工程后做的第一个“数据采集系统”。

我个人这几年调试下来的最大感受是:DS18B20 这类单总线器件,最关键的不是代码本身,而是“时序图 + 延时准确”。如果你也拿到了类似的工程包但没跑通,不要急着怀疑资料,先把原理图里 DS18B20 所在引脚的上拉电阻确认好,再用 STC-ISP 生成一套精确延时函数,成功概率会高很多。等你把它跑通,再去扩展成温控风扇或者智能鱼缸温度计,就会非常得心应手。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询