
1. 单总线协议下DS18B20的时序本质DS18B20这颗传感器在嵌入式圈子里算是老面孔了但真正能把它的时序吃透的人并不多。很多人拿到代码烧进去读出来温度是85℃或者干脆读出来全是0然后就卡在那里了。问题的根子往往不在代码逻辑而在单总线协议的时序理解上。单总线顾名思义就是一根线完成所有通信。这根线既要传数据又要传命令还要给传感器供电寄生供电模式下。DS18B20的通信协议完全靠主机拉低和释放总线的持续时间来编码信息。也就是说没有独立的时钟线时钟信号是隐含在数据线的电平变化里的。这就对延时精度提出了硬性要求。我见过不少初学者直接用delay_ms(1)这种粗粒度的延时函数去凑时序结果在11.0592MHz和12MHz两种晶振下表现完全不一样。为什么因为DS18B20的时序窗口是按微秒算的比如写0需要拉低总线60微秒左右写1只需要拉低1到15微秒。你用毫秒级的延时根本控制不住这个窗口。正确的做法是写一个微秒级的延时函数而且这个函数要根据你的MCU主频来校准。以51单片机为例如果晶振是12MHz一个机器周期是1微秒那么_nop_()函数执行一次差不多就是1微秒。但如果你用了1T模式的增强型51一个机器周期可能只有1/12微秒这时候_nop_()的延时就不准了需要重新计算循环次数。我一般会这样写微秒延时void Delay_us(unsigned int us) { while(us--) { _nop_(); _nop_(); _nop_(); _nop_(); } }然后在实际调试时用示波器抓一下波形看看拉低时间是不是符合数据手册的要求。没有示波器的话可以用逻辑分析仪现在几十块钱的USB逻辑分析仪就能看单总线波形非常方便。DS18B20的完整通信流程分三步复位、写命令、读数据。复位脉冲要求主机拉低总线至少480微秒然后释放等待15到60微秒后如果传感器在线它会拉低总线60到240微秒作为应答。这个应答脉冲是判断传感器是否存在的关键。很多人的代码读不到温度第一步就卡在复位没成功。写时隙分写0和写1。写0时主机拉低总线保持60微秒以上然后释放。写1时主机拉低总线保持1到15微秒然后释放剩下的时间由外部上拉电阻把总线拉高。注意写时隙之间至少要有1微秒的恢复时间。读时隙稍微复杂一点。主机先拉低总线至少1微秒然后释放在15微秒内采样总线的电平。如果读到高电平就是1低电平就是0。整个读时隙必须在60微秒内完成而且两次读之间也要有恢复时间。这些时间参数看起来琐碎但每一个都有它的道理。比如为什么写0要拉低60微秒以上因为传感器需要足够的时间来识别这是一个写0操作。为什么读时隙要在15微秒内采样因为超过这个时间传感器可能已经释放总线了你读到的就是上拉电阻带来的高电平会误判成1。理解了这些底层逻辑你再看任何一份DS18B20的代码都能一眼看出它靠不靠谱。2. 从复位到温度转换的完整代码链路写DS18B20的驱动代码我习惯把它拆成几个独立的函数每个函数只做一件事。这样调试的时候可以逐个验证出了问题也容易定位。2.1 复位与应答检测的实现细节复位函数的核心就是拉低、延时、释放、检测。但这里有个容易被忽略的点释放总线之后要先把IO口拉高再切换成输入模式去检测应答。如果你用的是准双向IO口比如51的P1口写1就是释放但读之前最好先写1确保内部场效应管截止外部上拉电阻能把电平拉高。bit DS18B20_Reset(void) { bit presence; DS18B20_DQ 0; Delay_us(480); DS18B20_DQ 1; Delay_us(60); presence DS18B20_DQ; Delay_us(420); return presence; }这个函数返回0表示传感器有应答返回1表示传感器不在线。注意最后的420微秒延时不能省这是为了让总线恢复到空闲状态否则后续的读写时隙会乱掉。2.2 字节读写函数的时序校准写字节的时候低位先写。每个bit的写入都要严格按照写0或写1的时序来。我见过有人把写0和写1的延时搞反了结果写进去的数据全是反的。void DS18B20_WriteByte(unsigned char dat) { unsigned char i; for(i 0; i 8; i) { DS18B20_DQ 0; _nop_(); DS18B20_DQ dat 0x01; Delay_us(60); DS18B20_DQ 1; dat 1; } }这里有个技巧在拉低总线之后先给一个极短的延时一个_nop_就够了然后再把数据位写到总线上。这样做的目的是让传感器能检测到一个明确的下降沿同时数据位在拉低期间就已经稳定了。读字节函数更需要注意采样时机。拉低总线后要尽快释放然后在15微秒内完成采样。unsigned char DS18B20_ReadByte(void) { unsigned char i, dat 0; for(i 0; i 8; i) { DS18B20_DQ 0; _nop_(); DS18B20_DQ 1; _nop_(); _nop_(); dat 1; if(DS18B20_DQ) dat | 0x80; Delay_us(60); } return dat; }读的时候先右移再判断这样第一次读到的bit会最终移到最高位。这个顺序不能错否则读出来的字节是位反转的。2.3 温度转换与暂存器读取的完整流程DS18B20上电默认是12位分辨率温度转换需要750毫秒。这个时间对于大多数应用来说太长了如果你需要快速采样可以在配置寄存器里改成9位分辨率转换时间降到93.75毫秒但精度只有0.5℃。完整的温度读取流程是这样的复位、跳过ROM单传感器时、发送温度转换命令0x44、等待转换完成、复位、跳过ROM、发送读取暂存器命令0xBE、连续读取两个字节的温度值。float DS18B20_GetTemp(void) { unsigned char tempL, tempH; int temp; float result; DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0x44); DS18B20_Reset(); DS18B20_WriteByte(0xCC); DS18B20_WriteByte(0xBE); tempL DS18B20_ReadByte(); tempH DS18B20_ReadByte(); temp (tempH 8) | tempL; if(temp 0x8000) { temp ~temp 1; result -(temp * 0.0625); } else { result temp * 0.0625; } return result; }这里0.0625是12位分辨率下的温度分辨率也就是1/16。如果你改成了9位分辨率这个系数要相应调整。负温度的处理用的是补码取反加一这是DS18B20数据格式决定的。3. 调试中那些让人抓狂的异常现象3.1 温度恒定85℃的根因排查85℃是DS18B20上电后的默认值如果你读出来一直是85℃说明温度转换命令根本没执行成功或者你读的是上电初始值。排查思路是这样的先用示波器或者逻辑分析仪抓一下复位波形看看有没有应答脉冲。如果没有应答检查硬件连接DQ线有没有接上拉电阻通常4.7kΩ到10kΩ电源电压是不是在3.0V到5.5V之间传感器是不是接反了。如果有应答但温度还是85℃那就检查写时隙。写0和写1的延时是不是搞反了写0需要拉低60微秒写1只需要拉低1到15微秒。如果你把写1的延时也搞成60微秒那写进去的数据就全乱了。还有一种可能是转换等待时间不够。12位分辨率需要750毫秒你如果只等了100毫秒就去读读到的还是上一次的转换结果而上一次可能根本没转换。3.2 读数为0或固定值的硬件排查读数为0或者一个固定不变的值通常指向硬件问题。我遇到过好几次最后发现是上拉电阻没接。单总线在没有主机驱动的时候全靠上拉电阻把电平拉高。没有上拉电阻总线就是浮空的读出来的数据完全随机。上拉电阻的阻值也有讲究。4.7kΩ是最常用的但如果总线比较长超过1米可以适当减小到2.2kΩ增强驱动能力。但也不能太小否则传感器拉低总线时电流太大可能损坏IO口。电源问题也容易被忽略。DS18B20可以用寄生供电模式也就是不给VDD供电只靠DQ线供电。但这种模式下温度转换期间总线必须保持高电平否则传感器没电。如果你在转换期间去操作总线读出来的数据肯定不对。我一般建议直接用外部供电VDD接3.3V或5V这样最稳定。3.3 多传感器挂载时的ROM搜索策略一根总线上挂多个DS18B20是完全可行的但这时候就不能用跳过ROM命令了必须用搜索ROM命令0xF0来逐个读取每个传感器的64位ROM地址。搜索ROM的算法比较复杂涉及到二叉树遍历和冲突检测。简单说就是主机发送搜索命令后每个传感器把自己的ROM地址一位一位地发出来。如果多个传感器同时发0或同时发1主机就能确定这一位如果一个发0一个发1总线上就会出现冲突主机读到的是0因为线与逻辑然后主机选择一个分支继续搜索。这个过程需要反复执行直到找到所有传感器。每次搜索前要复位搜索过程中要记录上一次的冲突位置下次从那个位置开始走另一条分支。实际项目中如果传感器数量不多比如少于8个我建议直接用多个IO口分别接每个IO口挂一个传感器。这样代码简单读取速度快也不用处理复杂的搜索算法。多花几个IO口省下来的调试时间绝对值得。4. 提升稳定性的工程化实践4.1 电源去耦与总线保护DS18B20在温度转换的瞬间电流会突然增大如果电源去耦没做好电压会跌落导致转换失败或者读数异常。我一般会在每个传感器的VDD和GND之间并一个0.1μF的陶瓷电容位置尽量靠近传感器引脚。总线保护方面如果传感器离MCU比较远超过2米建议在DQ线上串一个100Ω左右的电阻再并一个TVS二极管到地。这样可以防止长线引入的干扰和静电损坏IO口。4.2 软件滤波与异常值剔除即使硬件做得再好偶尔也会读到异常值。我通常会在软件里加一层滤波连续读三次去掉最大值和最小值取中间值。如果三次读数的偏差超过2℃就认为这次采样无效重新读取。float GetTemp_Filtered(void) { float t1, t2, t3, temp; t1 DS18B20_GetTemp(); Delay_ms(10); t2 DS18B20_GetTemp(); Delay_ms(10); t3 DS18B20_GetTemp(); if(t1 t2) { temp t1; t1 t2; t2 temp; } if(t2 t3) { temp t2; t2 t3; t3 temp; } if(t1 t2) { temp t1; t1 t2; t2 temp; } if((t3 - t1) 2.0) return -100.0; return t2; }返回-100.0表示这次采样无效上层逻辑可以决定是丢弃还是重试。4.3 低功耗场景下的采样策略电池供电的项目里DS18B20的功耗必须考虑。虽然它待机电流只有1μA左右但温度转换时的电流能达到1.5mA。如果每秒采样一次平均电流会显著增加。我的做法是需要采样时才给传感器供电采样完成后立刻断电。具体电路可以用一个MOS管控制VDDMCU的IO口控制MOS管的栅极。采样流程变成上电、延时10毫秒等电源稳定、复位、转换、读取、断电。这样平均电流可以降到微安级别。但要注意断电后DQ线也要拉低否则传感器可能通过DQ线寄生取电导致断电不彻底。5. 从裸机驱动到实际项目的适配经验5.1 在RTOS环境下的时序保护如果你把DS18B20的驱动放到RTOS任务里最大的风险是任务切换打断时序。比如你正在写一个时隙突然被高优先级任务抢占了等回来的时候时序窗口早就过了。解决办法有两个一是把整个读写过程放在临界区里关中断执行。但DS18B20的转换等待有750毫秒关中断这么久肯定不行。所以只能把复位、读写字节这些短时序操作放在临界区转换等待用延时让出CPU。二是用硬件定时器来产生精确延时不依赖软件循环。但51单片机一般没有这么灵活的定时器资源所以还是临界区加短延时的方案最实用。5.2 与上位机通信时的数据打包温度数据传到上位机通常需要打包成一定的协议格式。我一般用简单的帧结构帧头0xAA、温度高字节、温度低字节、校验和、帧尾0x55。校验和用前面几个字节的异或。void SendTempFrame(float temp) { unsigned char buf[5]; int t (int)(temp * 100); buf[0] 0xAA; buf[1] (t 8) 0xFF; buf[2] t 0xFF; buf[3] buf[0] ^ buf[1] ^ buf[2]; buf[4] 0x55; UART_SendBytes(buf, 5); }上位机收到后先找帧头再验校验和都通过了才解析温度值。这样即使偶尔丢几个字节也不会解析出错误的数据。5.3 长期运行中的自恢复机制嵌入式设备经常需要连续运行几个月甚至几年。DS18B20虽然稳定但偶尔也会因为干扰而挂死表现为一直不应答。我的做法是在主循环里加一个计数器如果连续10次复位都没有应答就认为传感器异常尝试重新初始化把DQ线拉低一段时间比如1秒然后释放再重新复位。这个拉低1秒的操作相当于给传感器一个强制复位信号大多数情况下能让它恢复正常。如果还是不行就只能报故障了。if(reset_fail_count 10) { DS18B20_DQ 0; Delay_ms(1000); DS18B20_DQ 1; Delay_ms(100); reset_fail_count 0; }这个自恢复机制在实际项目中救过我好几次尤其是工业现场干扰比较大的场合。6. 关于DS18B20选型与替代方案的思考DS18B20最大的优势是单总线、数字输出、不需要外部ADC。但它的缺点也很明显转换速度慢、价格相对较高、单总线时序对MCU主频敏感。如果你需要更快的采样速度可以考虑DS18B20的升级型号DS18B20-PAR或者换用I2C接口的温度传感器比如LM75、TMP102。I2C的时序比单总线简单得多而且速度可以到400kHz采样率轻松做到每秒几十次。如果成本敏感热敏电阻加ADC的方案更便宜但需要校准精度也不如DS18B20。NTC热敏电阻的阻值随温度变化通过分压电路送到ADC再查表或者用Steinhart-Hart公式计算温度。这个方案适合对精度要求不高的场合。如果测量范围要求宽比如要测几百摄氏度那DS18B20就不合适了它的量程只有-55℃到125℃。这时候得用热电偶加专用放大器的方案。选型这件事没有最好的只有最合适的。先明确你的需求精度、速度、成本、接口资源、环境温度范围然后对着这些指标去挑基本不会错。我在实际项目中用DS18B20最多的场景是室温监测、水温检测、机箱温度监控这些。这些场景对采样速度要求不高精度0.5℃就够用单总线省IO口的优势就很明显。但如果是要做快速温度闭环控制我肯定不会选它响应速度跟不上。最后分享一个小技巧DS18B20的64位ROM地址是全球唯一的可以用它来做设备身份识别。在多传感器系统中你可以先搜索所有ROM地址然后给每个地址分配一个逻辑编号这样即使传感器位置换了也能通过ROM地址识别出是哪一个。这个特性在资产追踪和防拆检测场景里很有用。