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(i0; i8; 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的最低位是0DQ保持0如果是1DQ被置1。读时隙稍微复杂一点unsigned char DS18B20_ReadByte(void) { unsigned char i, dat 0; for(i0; i8; 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读温度的标准流程是复位跳过ROM0xCC启动温度转换0x44等待转换完成12位分辨率需要750ms复位跳过ROM0xCC读取暂存器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°C0xFFF通常表示读到的全是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的代码我都会先用一个简单的测试程序只读温度并通过串口打印出来确认底层没问题了再整合到完整的项目里。这样排查问题的时候能快速定位是底层驱动的问题还是上层逻辑的问题。这个习惯帮我省了很多调试时间你也可以试试。