1. 为什么CT107D上的DS18B20值得单独拿出来讲
蓝桥杯单片机组的板子——CT107D,几乎是每个参赛选手绕不开的硬件平台。这块板子上集成了不少外设,数码管、独立按键、矩阵键盘、蜂鸣器、继电器、DS1302、AT24C02、DS18B20、超声波、红外接收等等,基本上把常见嵌入式外设都塞进去了。其中DS18B20这个温度传感器,看起来简单,一根线就能读温度,但实际在CT107D上跑通它,踩坑的人不在少数。
我自己第一次在CT107D上写DS18B20驱动的时候,读出来的温度要么是85,要么是0,要么干脆一直是-127。折腾了大半天才发现,问题出在时序延时上——51单片机的机器周期和DS18B20的微秒级时序要求之间,需要非常精确的配合。后来把延时函数调好了,温度一下子就出来了。
这篇文章就是把我自己在CT107D上调试DS18B20的完整过程整理出来,从单总线协议的底层原理,到具体的延时计算,再到完整的代码实现和常见问题排查,都会讲到。不管你是刚接触蓝桥杯单片机的新手,还是已经写过几遍DS18B20但总是读不准的老手,应该都能从里面找到有用的东西。
DS18B20的核心价值在于它用一根数据线就完成了供电、复位、读写全部通信,这在引脚资源紧张的单片机系统里非常实用。CT107D上把它接在P1.4口附近(具体引脚后面会详细说),通过单总线协议和单片机通信。理解了这个协议,你以后遇到DHT11、AM2302这类单总线传感器,上手会快很多。
2. DS18B20和单总线协议的核心原理拆解
2.1 DS18B20到底是怎么工作的
DS18B20是Dallas Semiconductor(现在归Maxim)出的数字温度传感器,测温范围-55°C到+125°C,在-10°C到+85°C范围内精度是±0.5°C。它内部有三个主要部件:温度敏感元件、ADC转换器和单总线接口逻辑。温度敏感元件把温度转换成电压信号,ADC把它变成数字量,然后通过单总线接口发出去。
它的核心寄存器有两个需要重点关注:温度寄存器和配置寄存器。温度寄存器是16位的,高5位是符号位,低4位是小数位。默认分辨率是12位,对应0.0625°C的分度值。配置寄存器用来设置分辨率,可以选9到12位,分辨率越低转换越快,但精度也越低。
DS18B20的供电方式有两种:寄生电源模式和外部电源模式。寄生电源模式下,DS18B20从数据线上"偷电"——当总线为高电平时,内部电容充电;当总线为低电平时,电容放电维持工作。CT107D上一般用的是外部电源模式,VDD直接接3.3V或5V,这样通信更稳定,也不需要在温度转换期间强拉总线。
2.2 单总线协议的时序本质
单总线协议(1-Wire)的核心思想是:用一根线,通过不同的拉低时长来区分不同的命令。所有通信都由主机(单片机)发起,DS18B20作为从机响应。
协议里定义了六种基本信号:
| 信号类型 | 主机动作 | 从机响应 | 典型时长 |
|---|---|---|---|
| 复位脉冲 | 拉低总线 | 等待 | 480-960μs |
| 存在脉冲 | 释放总线 | 拉低总线 | 60-240μs |
| 写0 | 拉低总线 | 无 | 60-120μs |
| 写1 | 拉低总线后快速释放 | 无 | 1-15μs拉低+释放 |
| 读0 | 拉低总线后释放 | 保持低电平 | 整个时隙 |
| 读1 | 拉低总线后释放 | 释放总线 | 整个时隙 |
关键点在于:所有的时隙(time slot)都必须由主机拉低总线来启动。写时隙和读时隙的区别在于,写时隙主机在整个时隙内控制总线电平,读时隙主机只在开始时拉低,然后释放,由从机决定总线电平。
这里最容易出错的地方是读时隙。主机拉低总线至少1μs,然后释放,必须在15μs内采样总线电平。如果采样太晚,DS18B20可能已经把数据发完了,读到的一直是1。我见过不少人读温度一直是0xFFFF,就是因为采样时机不对。
2.3 CT107D上的硬件连接细节
CT107D开发板上,DS18B20的数据引脚(DQ)连接到单片机的P1.4(不同批次的板子可能略有差异,但绝大多数是P1.4)。VDD接电源,GND接地。板上通常还有一个跳线帽,用来选择DS18B20是否接入总线。
这里有个细节需要注意:CT107D上的P1口还复用了其他功能,比如数码管段选、蜂鸣器、继电器等。P1.4除了接DS18B20,还可能和别的外设共享。所以在写代码的时候,操作P1口要特别小心,不能直接对整个P1赋值,否则会干扰其他外设。
正确的做法是位操作:用sbit DQ = P1^4;定义数据引脚,然后只对DQ进行读写,不影响P1口其他位。这一点在蓝桥杯比赛里非常关键,因为题目往往要求同时使用多个外设,一个不小心就会互相干扰。
3. 在CT107D上实现DS18B20驱动的完整过程
3.1 延时函数的精确计算
51单片机的时序完全依赖软件延时,所以延时函数的精度直接决定了DS18B20能不能正常工作。CT107D上用的晶振通常是12MHz(有些板子是11.0592MHz,但蓝桥杯官方板一般是12MHz)。12MHz晶振下,一个机器周期是1μs,也就是说一个NOP指令正好是1μs。
但C语言写的延时函数,实际执行时间还受到编译器优化、函数调用开销等因素影响。所以最可靠的方法是用示波器或者逻辑分析仪实测,或者根据经验值反复调整。
我常用的微秒级延时函数是这样的:
void Delay_us(unsigned int us) { while(us--) { _nop_(); _nop_(); } }这个函数在12MHz晶振下,每次循环大约2μs(两个NOP加循环开销)。实际用的时候需要根据DS18B20的时序要求来调整。比如复位脉冲需要拉低480μs以上,那就调用Delay_us(240)左右。
更精确的做法是用STC-ISP软件里的延时计算器,或者直接写汇编。但在蓝桥杯比赛里,用C语言加NOP的方式足够了,关键是实测调整。
3.2 复位和存在脉冲的检测
复位脉冲是每次通信的开始。主机拉低总线至少480μs,然后释放,等待DS18B20拉低总线60-240μs作为存在脉冲。
bit DS18B20_Reset(void) { bit presence; DQ = 0; Delay_us(240); // 拉低约480μs DQ = 1; Delay_us(30); // 等待15-60μs presence = DQ; // 采样存在脉冲 Delay_us(200); // 等待存在脉冲结束 return presence; }这段代码里,presence为0表示DS18B20有响应,为1表示没有检测到器件。如果一直返回1,说明硬件连接有问题,或者延时不对。
注意:存在脉冲的采样时机很关键。拉高总线后等待15-60μs再采样,太早或太晚都可能读错。我一般用30μs,实测比较稳。
3.3 写时隙和读时隙的实现
写一个字节需要8个写时隙,每个时隙至少60μs,时隙之间至少恢复1μs。
void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for(i=0; i<8; i++) { DQ = 0; _nop_(); _nop_(); DQ = dat & 0x01; Delay_us(30); // 保持60μs左右 DQ = 1; dat >>= 1; } }写0和写1的区别在于:写0时,DQ拉低后保持60μs;写1时,DQ拉低后很快释放(1-15μs内)。上面的代码里,DQ = dat & 0x01;这一句决定了是写0还是写1。如果dat的最低位是0,DQ保持0;如果是1,DQ被置1。
读时隙稍微复杂一点:
unsigned char DS18B20_ReadByte(void) { unsigned char i, dat = 0; for(i=0; i<8; i++) { dat >>= 1; DQ = 0; _nop_(); _nop_(); DQ = 1; _nop_(); _nop_(); if(DQ) dat |= 0x80; Delay_us(30); } return dat; }读时隙的关键是:主机拉低至少1μs,然后释放,在15μs内采样。上面的代码里,拉低后两个NOP(约2μs),然后释放,再两个NOP后采样。这个时机需要根据实际晶振调整。
3.4 温度转换和读取的完整流程
DS18B20读温度的标准流程是:
- 复位
- 跳过ROM(0xCC)
- 启动温度转换(0x44)
- 等待转换完成(12位分辨率需要750ms)
- 复位
- 跳过ROM(0xCC)
- 读取暂存器(0xBE)
- 读两个字节的温度数据
- 转换成实际温度值
float DS18B20_GetTemp(void) { unsigned char L, H; int temp; float result; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); // 等待转换,可以用延时或者检查总线 Delay_ms(750); DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); L = DS18B20_ReadByte(); H = DS18B20_ReadByte(); temp = (H << 8) | L; // 处理负温度 if(temp & 0x8000) { temp = ~temp + 1; result = -(temp * 0.0625); } else { result = temp * 0.0625; } return result; }这里有个细节:负温度的处理。DS18B20输出的是补码,如果最高位是1,表示负温度。需要先取反加一得到绝对值,再乘以0.0625,最后加上负号。
另外,12位分辨率下转换时间是750ms,如果比赛时时间紧张,可以把分辨率降到9位(转换时间93.75ms),但精度会降到0.5°C。具体怎么选,看题目要求。
4. 常见问题排查与实战避坑指南
4.1 温度一直读85或者-127怎么办
这是最经典的问题。85°C是DS18B20上电后的默认值,如果你读到的一直是85,说明温度转换根本没有启动,或者转换命令没有发出去。
排查步骤:
- 检查复位函数是否返回0(有存在脉冲)
- 检查写时隙的延时是否足够
- 检查是否发送了0x44命令
- 检查等待转换的时间是否足够
-127°C(0xFFF)通常表示读到的全是1,也就是总线一直是高电平。可能的原因:
- DQ引脚定义错了
- 硬件连接松动
- 读时隙采样太晚,DS18B20已经释放总线
我遇到过一次,DQ定义成了P1^3,结果怎么读都是-127。后来改成P1^4就正常了。所以引脚定义一定要对照板子原理图确认。
4.2 温度跳动很大怎么处理
有时候读到的温度会在两个值之间跳,比如25.5和25.6来回变。这通常是分辨率设置和显示处理的问题。
12位分辨率下,温度的最小分度是0.0625°C。如果你直接显示两位小数,就会看到跳动。解决办法有两种:
- 降低分辨率到11位或10位,减少跳动
- 在显示前做滑动平均滤波,取最近几次读数的平均值
我在比赛里一般用滑动平均,取5次读数去掉最高最低再平均,效果不错。
4.3 和其他外设冲突怎么解决
CT107D上P1口复用严重,DS18B20用P1.4,数码管段选也用P1口,蜂鸣器用P1.5,继电器用P1.4(有些板子)。如果同时使用这些外设,很容易冲突。
解决办法:
- 操作DS18B20时,先保存P1口状态,操作完恢复
- 或者用位操作,只改变DQ位,不影响其他位
- 数码管动态扫描和DS18B20读取分时进行,避免同时操作
我一般的做法是:DS18B20读取放在主循环里,数码管扫描放在定时器中断里,两者互不干扰。但要注意,DS18B20的延时函数不能被中断打断,否则时序会乱。所以读取DS18B20的时候,最好关一下总中断,读完再开。
4.4 常见问题速查表
| 现象 | 可能原因 | 解决方法 |
|---|---|---|
| 一直读85 | 转换未启动 | 检查0x44命令和复位 |
| 一直读-127 | 总线一直高 | 检查DQ定义和硬件连接 |
| 温度跳动大 | 分辨率过高 | 降低分辨率或滑动平均 |
| 读数为0 | 读时隙采样太晚 | 调整采样时机 |
| 偶尔读错 | 中断干扰时序 | 读取时关中断 |
| 负温度显示错误 | 补码处理不对 | 取反加一再加负号 |
提示:蓝桥杯比赛里,DS18B20的题目通常要求显示整数温度,这时候可以直接取温度值的高字节和低字节的高4位,拼成一个整数,省去浮点运算,速度更快。
5. 代码优化与比赛实战建议
5.1 用整数运算代替浮点
51单片机没有硬件浮点单元,浮点运算很慢。如果只需要显示整数温度,可以这样处理:
int temp_int; temp_int = ((H << 8) | L) >> 4; // 取整数部分这样得到的就是整数温度,省去了浮点乘法和类型转换。如果题目要求一位小数,可以这样:
int temp_int, temp_dec; temp_int = ((H << 8) | L) >> 4; temp_dec = (((H & 0x0F) << 4) | (L >> 4)) * 10 / 16;这样temp_int是整数部分,temp_dec是小数部分(0-9),显示的时候分开显示就行。
5.2 状态机方式读取温度
在比赛里,主循环往往要处理按键、显示、通信等多个任务。如果DS18B20用阻塞式延时750ms,整个系统会卡住。更好的方式是状态机:
- 状态0:发送转换命令
- 状态1:等待转换(用定时器计时)
- 状态2:读取温度
- 状态3:处理数据
这样主循环不会被阻塞,其他任务也能正常运行。我在省赛的时候就是用这种方式,按键响应和温度显示都很流畅。
5.3 比赛中的时间分配建议
蓝桥杯单片机组的比赛时间通常是5个小时,DS18B20相关的题目一般占10-15分。建议的时间分配:
- 硬件确认和引脚定义:10分钟
- 复位和读写函数调试:30分钟
- 温度转换和显示:20分钟
- 与其他外设整合:30分钟
- 预留调试时间:30分钟
如果DS18B20调了超过1小时还没出来,先放一放,把其他部分做完再回来调。有时候换个思路,问题反而迎刃而解。
5.4 一个容易被忽略的细节:总线释放时间
每次通信结束后,总线需要至少1μs的恢复时间。如果连续操作DS18B20,比如写完命令马上读,中间没有足够的恢复时间,可能会导致通信失败。我在写读函数的时候,每个时隙结束后都会加一个短延时,确保总线恢复。
另外,DS18B20在温度转换期间,总线应该保持高电平。如果这时候有其他外设拉低总线,转换可能会中断。所以转换期间最好不要操作P1口其他位。
6. 从DS18B20延伸出去的单总线思维
DS18B20只是单总线协议的一个应用。理解了它的时序本质,你会发现很多传感器都用了类似的思想——用一根线,通过不同的电平持续时间来编码信息。比如DHT11用一根线传输40位数据,红外接收头用一根线输出NEC协议编码,超声波模块用两根线(触发和回响)完成测距。
单总线协议的精髓在于时间换空间:用精确的时序控制代替多根数据线,节省了引脚资源,但增加了软件复杂度。在引脚紧张的嵌入式系统里,这种取舍非常常见。
我在实际项目中用过DS18B20做多点测温,一条总线上挂8个传感器,每个传感器有唯一的64位ROM地址。通过匹配ROM命令,可以单独读取每个传感器的温度。这种方式在分布式测温系统里非常实用,比如机房温度监控、农业大棚测温等。
回到蓝桥杯,DS18B20的题目万变不离其宗:读温度、显示温度、温度控制(比如超过阈值报警)。把底层驱动写稳了,上层逻辑就是搭积木。我建议你在练习的时候,把DS18B20的驱动单独封装成两个文件(.c和.h),以后遇到类似的题目直接拿来用,省时省力。
最后分享一个我自己的小习惯:每次写完DS18B20的代码,我都会先用一个简单的测试程序,只读温度并通过串口打印出来,确认底层没问题了,再整合到完整的项目里。这样排查问题的时候,能快速定位是底层驱动的问题还是上层逻辑的问题。这个习惯帮我省了很多调试时间,你也可以试试。