
1. 单总线协议为什么让DS18B20既省引脚又难伺候DS18B20这颗温度传感器在嵌入式圈子里算是老面孔了但每次带新人做实验总有人卡在读出来永远是85℃或者数据全是0xFF这两个坎上。问题不在芯片本身而在于它用的是单总线1-Wire协议——一根数据线既要传时钟又要传数据还要兼顾供电时序容错窗口窄得让人抓狂。先说清楚它到底解决了什么问题。传统温度采集方案里模拟传感器比如LM35需要一路ADC通道每个传感器独占一个引脚I2C传感器比如LM75虽然能挂总线但至少得两根线SCLSDA还得处理地址冲突。DS18B20的杀手锏是所有传感器共用一根数据线理论上一条GPIO可以挂几十个器件每个器件靠出厂烧录的64位ROM ID区分。这对引脚紧张的MCU比如8脚封装的单片机来说价值巨大。但代价也很明显。单总线没有独立的时钟线主机必须靠精确的延时来产生复位脉冲、写时隙、读时隙。以标准速率为例一个写1时隙要求拉低总线1~15μs后释放整个时隙持续60~120μs写0时隙则要拉低60~120μs。读时隙更微妙——主机拉低至少1μs后释放然后在15μs内采样总线电平。这些时间窗口如果被中断打断或者延时函数不准通信直接崩盘。我见过太多人用delay_ms(1)这种粗粒度延时去凑时序结果在72MHz的ARM上跑得好好的换到8MHz的51单片机上就完全读不出数据。根本原因是单总线对延时精度的要求是微秒级而且不同主频下同一条C语句消耗的时间完全不同。所以做这个实验的第一步不是急着写代码而是先搞清楚你手上的平台延时精度够不够。注意DS18B20的时序参数在不同资料里写法略有差异建议以原厂数据手册的AC Electrical Characteristics表格为准不要盲目抄网上的代码。另外还有一个容易被忽略的点总线需要上拉电阻。因为DS18B20的数据引脚是开漏输出没有上拉电阻的话总线永远拉不高。典型值是4.7kΩ但如果总线挂的器件多或者线缆长得适当减小到2.2kΩ甚至1kΩ。这个电阻不是随便选的后面会专门讲怎么算。2. 实验前的硬件清单与接线避坑2.1 元器件选型与封装差异DS18B20常见的有两种封装TO-92直插和防水探头式。TO-92就是那个长得像三极管的小黑块引脚顺序是平面朝向自己从左到右依次是GND、DQ数据、VDD。防水探头则是金属管封装引线一般是红VDD、黑GND、黄或白DQ。两种封装的电气特性完全一样但防水探头内部多了一层导热硅胶热响应时间会慢一些测水温、土壤温度时更合适。供电方式有两种外部供电和寄生供电。外部供电就是VDD接3.3V或5VGND接地DQ接GPIO加上拉电阻。寄生供电则是VDD和GND都接地靠DQ线在总线高电平时通过内部电容取电。寄生供电省一根线但对时序要求更苛刻而且在温度转换期间总线必须保持强上拉否则转换结果会出错。新手建议直接用外部供电稳定压倒一切。参数外部供电寄生供电接线数量3根VDD/GND/DQ2根GND/DQ温度转换期间总线可自由操作必须强上拉适合场景通用实验远距离、引脚极紧张新手推荐度高低2.2 上拉电阻的计算逻辑上拉电阻的取值不是拍脑袋定的。它要满足两个条件一是保证总线能被拉高到VIH以上二是在器件拉低时电流不能超过引脚承受能力。DS18B20的灌电流能力典型值是4mA。假设供电3.3V上拉电阻R则拉低时电流I 3.3 / R。要保证I 4mAR 825Ω。另一方面总线上的漏电流和输入电容会影响上升沿时间R太大则上升沿变缓高速通信时可能采不到高电平。经验公式是R 1 / (2πfC)其中f是通信频率标准速率约16.7kHzC是总线电容每米线缆约50pF。假设线缆1米C50pF则R 190kΩ。综合下来4.7kΩ是最常用的折中值线缆超过3米时降到2.2kΩ。我实际测过在30cm的杜邦线上4.7kΩ和10kΩ都能正常工作但线长拉到2米10kΩ就开始出现偶发读取失败换成2.2kΩ后稳定。所以如果你要做远距离采集上拉电阻一定要往下调。2.3 接线中最容易犯的三个错误第一个错误是把DQ接到带内部上拉的引脚上。有些MCU的GPIO内部有弱上拉比如STM32的输入上拉约40kΩ这个上拉太弱无法替代外部4.7kΩ。正确做法是配置为开漏输出或输入/输出切换模式外部上拉电阻必须接。第二个错误是电源去耦电容缺失。DS18B20在温度转换瞬间电流会跳到1.5mA如果电源走线长、没有去耦电容VDD会瞬间跌落导致复位。建议在VDD和GND之间并一个0.1μF的陶瓷电容位置尽量靠近传感器。第三个错误是热插拔。单总线器件在通电状态下插拔容易因为总线电平突变导致内部状态机锁死。如果非要热插拔先拉低总线再操作。不过实验阶段建议断电接线省心。3. 复位、写时隙、读时隙的时序拆解与代码实现3.1 复位脉冲通信的起手式每次和DS18B20通信前主机必须发一个复位脉冲让所有从机进入等待状态。具体过程是主机拉低总线至少480μs然后释放进入接收模式。如果总线上有DS18B20它会在15~60μs后拉低总线60~240μs作为应答。主机在释放总线后约70μs采样总线如果读到低电平说明有器件在线。这里的关键是拉低时间不能少于480μs但也不能太长否则某些器件会进入掉电复位。我一般用500μs。采样点选在释放后70μs这个时间要卡准太早会读到主机释放后的上升沿太晚会错过应答脉冲。// 以STM32 HAL库为例微秒延时用DWT或TIM实现 uint8_t ds18b20_reset(void) { uint8_t presence; set_pin_output(); // DQ配置为推挽输出 write_pin(0); // 拉低总线 delay_us(500); // 保持500us set_pin_input(); // 释放总线切换为输入 delay_us(70); // 等待70us presence read_pin(); // 读取应答 delay_us(430); // 补足整个时隙 return presence; // 0表示有器件应答 }注意set_pin_input()这一步很多新手忘记切换方向导致主机一直拉着总线永远读不到应答。另外延时函数必须准确如果用HAL_Delay()这种毫秒级延时复位阶段就废了。3.2 写时隙0和1的区别只在拉低时长写时隙分写0和写1。共同点是主机先拉低总线然后在15μs内完成操作整个时隙至少60μs。区别在于写0时拉低60μs以上再释放写1时拉低1~15μs后立即释放。DS18B20会在拉低后的15~60μs窗口内采样总线电平。代码实现上写0和写1可以合并成一个函数void ds18b20_write_bit(uint8_t bit) { set_pin_output(); write_pin(0); if (bit) { delay_us(5); // 写1拉低5us set_pin_input(); // 释放 delay_us(60); // 补足时隙 } else { delay_us(60); // 写0拉低60us set_pin_input(); delay_us(5); } }写字节就是把8个bit从低位到高位依次写出去。这里有个细节DS18B20的数据是LSB优先也就是先写最低位。如果你按MSB优先写读出来的温度会完全错乱。3.3 读时隙主机必须主动喂时钟读时隙和写时隙类似但方向相反。主机拉低总线至少1μs然后释放在释放后的15μs内采样总线。DS18B20会在主机拉低后的15μs内把数据放到总线上。如果主机释放后不及时采样数据就会消失。uint8_t ds18b20_read_bit(void) { uint8_t bit; set_pin_output(); write_pin(0); delay_us(2); // 拉低2us set_pin_input(); // 释放 delay_us(10); // 等待10us让器件输出数据 bit read_pin(); // 采样 delay_us(50); // 补足时隙 return bit; }读字节同样是循环8次每次读一位组合成字节。这里最容易踩的坑是采样点太晚。有些教程写delay_us(30)再采样结果读出来全是1因为DS18B20早就释放总线了。提示如果你用逻辑分析仪抓波形会发现读时隙的采样点最好在拉低后12~14μs之间这个窗口最稳。3.4 温度转换与暂存器读取的完整流程一次完整的温度采集包含四个步骤复位检测应答。跳过ROM0xCC因为总线上只有一个器件时不需要匹配ROM ID。启动转换0x44DS18B20开始ADC最长750ms。再次复位跳过ROM读取暂存器0xBE连续读9个字节。暂存器的前两个字节就是温度值低字节在前高字节在后。温度格式是16位有符号数低4位是小数部分单位0.0625℃。比如读出来0x0191二进制是0000 0001 1001 0001高字节0x01低字节0x91组合成0x0191 401401 × 0.0625 25.0625℃。负温度的处理稍微麻烦一点。DS18B20用补码表示负数比如-25.0625℃对应0xFE6F。判断符号看高字节的最高位如果是1就是负的。计算时先取反加一得到绝对值再乘0.0625最后加负号。float ds18b20_read_temp(void) { uint8_t temp_l, temp_h; int16_t raw; float result; ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0x44); // 等待转换寄生供电时需强上拉 delay_ms(750); ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0xBE); temp_l ds18b20_read_byte(); temp_h ds18b20_read_byte(); raw (temp_h 8) | temp_l; if (raw 0x8000) { result -(float)((~raw 1) 0xFFFF) * 0.0625; } else { result (float)raw * 0.0625; } return result; }这段代码里delay_ms(750)是等转换完成。如果你用寄生供电这里必须把总线强上拉否则转换期间供电不足读出来就是85℃默认上电值。4. 读出来永远是85℃或0xFF的排查链路4.1 85℃的本质上电默认值与转换未完成DS18B20的暂存器第0、1字节在上电复位后的默认值是0x0550换算成温度正好是85℃。如果你读出来一直是85℃说明温度转换根本没有启动或者转换命令没被正确接收。排查顺序是这样的先确认复位应答是否正常。如果ds18b20_reset()返回1无应答说明硬件连接有问题——检查上拉电阻、电源、DQ线是否接对。如果复位正常但读出来还是85℃那就是写时隙有问题0x44命令没写进去。用逻辑分析仪抓一下写0x44时的波形看看拉低时间够不够60μs。还有一种可能是转换等待时间不够。DS18B20在12位分辨率下需要750ms如果你只等了100ms就去读暂存器里还是上次的值或者默认值。解决办法是读暂存器的第6字节配置寄存器确认分辨率或者干脆多等一会儿。4.2 0xFF的成因总线一直被拉高或读时隙采样错误读出来全是0xFF意味着主机在每个读时隙都采到了高电平。可能的原因有三个DQ线没有真正被拉低。检查GPIO配置如果是开漏输出但没加上拉或者推挽输出但引脚映射错了总线永远是高。读时隙拉低时间太短。DS18B20要求主机拉低至少1μs如果你只拉低0.5μs器件可能没检测到起始信号。采样点太晚。前面说过读时隙的采样窗口在拉低后15μs内超过这个时间器件已经释放总线读到的自然是高电平。我遇到过一次特别隐蔽的情况代码里用了HAL_GPIO_WritePin()和HAL_GPIO_ReadPin()但GPIO模式没有在输出和输入之间正确切换。STM32的HAL库在切换模式时需要调用HAL_GPIO_Init()重新配置如果只是改ODR寄存器方向没变读回来的永远是输出寄存器的值。后来改成直接操作BSRR和IDR寄存器问题消失。4.3 用逻辑分析仪定位时序偏差如果你手头有逻辑分析仪哪怕是最便宜的24MHz采样率版本抓一次完整的复位写0xCC写0x44读暂存器波形基本能定位90%的问题。重点看几个地方复位脉冲拉低时间是否≥480μs。写0x44时每个bit的拉低时间是否符合写0≥60μs或写11~15μs。读时隙的采样点是否在拉低后15μs内。总线释放后上升沿是否陡峭如果上升沿超过10μs说明上拉电阻太大或线缆电容太大。没有逻辑分析仪的话可以用示波器看单次触发或者用另一个GPIO做调试输出在关键节点翻转电平用示波器双通道对比。4.4 多器件挂载时的ROM搜索失败如果你挂了好几个DS18B20就不能用跳过ROM命令了必须走ROM搜索算法。这个算法基于单总线的位碰撞检测主机先读一位再读补码如果两位相同说明有冲突记录冲突位下一轮根据分支选择走0还是走1。搜索失败的典型表现是只能找到一个器件或者找到的ROM ID重复。原因通常是读时隙采样点不准导致位判断错误。解决办法是放慢时序把读时隙的采样点往后延一点比如从10μs改成13μs给器件更多时间输出数据。另外多器件时总线电容增大上升沿变缓上拉电阻要相应减小。我实测挂4个TO-92封装、线长1米时4.7kΩ还能用挂8个时就得降到2.2kΩ。5. 从能读到读得准精度优化与抗干扰实战5.1 分辨率配置与转换时间的权衡DS18B20的默认分辨率是12位对应0.0625℃。如果你不需要这么高的精度可以配置成9位0.5℃、10位0.25℃或11位0.125℃。分辨率越低转换时间越短9位只要93.75ms12位要750ms。配置方法是写暂存器的第4字节配置寄存器然后复制到第5字节EEPROM。配置寄存器的bit6和bit5决定分辨率00是9位01是10位10是11位11是12位。上电默认是11位不对默认是12位对应bit61、bit51。void ds18b20_set_resolution(uint8_t res) { ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0x4E); // 写暂存器命令 ds18b20_write_byte(0x00); // TH ds18b20_write_byte(0x00); // TL ds18b20_write_byte(res); // 配置寄存器 ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0x48); // 复制暂存器到EEPROM delay_ms(10); }res的取值0x1F是9位0x3F是10位0x5F是11位0x7F是12位。注意bit7和bit4~0是保留位不要乱写。5.2 电源噪声对温度读数的影响DS18B20的ADC对电源噪声很敏感。我在一个电机驱动的项目里每次电机启动温度读数就会跳变2~3℃。后来在VDD和GND之间并了0.1μF陶瓷电容和10μF钽电容跳变降到0.5℃以内。如果做高精度采集建议电源走线尽量短远离功率器件。在传感器端加RC滤波100Ω0.1μF。软件上做滑动平均滤波连续读5次去掉最大最小值再平均。滑动平均的代码很简单维护一个数组每次新数据进来就移位#define FILTER_LEN 5 float temp_buf[FILTER_LEN]; float temp_filter(float new_val) { float sum 0, max -100, min 200; for (int i FILTER_LEN - 1; i 0; i--) { temp_buf[i] temp_buf[i-1]; } temp_buf[0] new_val; for (int i 0; i FILTER_LEN; i) { sum temp_buf[i]; if (temp_buf[i] max) max temp_buf[i]; if (temp_buf[i] min) min temp_buf[i]; } return (sum - max - min) / (FILTER_LEN - 2); }5.3 长线缆下的信号完整性处理线缆超过5米时分布电容和电感会导致信号反射和衰减。除了减小上拉电阻还可以用双绞线DQ和GND绞在一起减少电磁干扰。在接收端加施密特触发器整形比如74HC14。降低通信速率标准速率不行就改用低速模式更长的时隙。我做过一个20米线缆的土壤温度采集用屏蔽双绞线2.2kΩ上拉74HC14整形连续跑了一周没丢数据。但同样的代码换成普通杜邦线超过3米就开始随机出错。5.4 温度转换期间的强上拉实现寄生供电时温度转换期间总线必须强上拉。强上拉的意思是用一个MOSFET把DQ直接拉到VDD而不是靠4.7kΩ电阻。因为转换瞬间电流达到1.5mA4.7kΩ电阻提供的电流只有0.7mA不够用。强上拉的电路很简单一个P沟道MOSFET源极接VDD漏极接DQ栅极由GPIO控制。转换命令发出后拉低栅极让MOSFET导通转换完成后再释放。如果不想加硬件也可以用推挽输出直接拉高但要注意两个器件同时推挽输出时可能短路所以只适合单器件场景。// 强上拉转换期间把DQ配置为推挽输出并拉高 set_pin_output(); write_pin(1); delay_ms(750); set_pin_input(); // 转换完成后释放6. 从实验到产品几个容易忽略的工程细节6.1 上电后的首次读取要丢弃DS18B20上电后暂存器是默认值85℃第一次转换需要完整750ms。如果你上电后立即读拿到的就是85℃。正确做法是上电后先启动一次转换等750ms后再读或者连续读两次丢弃第一次的结果。我在一个低功耗项目里踩过这个坑MCU休眠唤醒后立即读温度结果每次唤醒第一次都是85℃导致温控逻辑误判。后来改成唤醒后先发转换命令等转换完成再读问题解决。6.2 掉电保存的报警阈值DS18B20内部有TH和TL两个报警寄存器可以设置温度上下限。当温度超过TH或低于TL时报警标志位置位。这个功能在独立温控场景很有用但注意报警标志需要读取暂存器第7字节才能清除而且报警搜索命令0xEC可以快速找到报警的器件。设置阈值的代码void ds18b20_set_alarm(int8_t th, int8_t tl) { ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0x4E); ds18b20_write_byte(th); ds18b20_write_byte(tl); ds18b20_write_byte(0x7F); // 12位分辨率 ds18b20_reset(); ds18b20_write_byte(0xCC); ds18b20_write_byte(0x48); // 保存到EEPROM delay_ms(10); }注意TH和TL是有符号字节范围-55~125℃对应0xC9~0x7D。写进去之后要发0x48命令保存到EEPROM否则掉电丢失。6.3 多器件ROM ID的批量管理挂多个DS18B20时每次读取都要先匹配ROM ID。如果器件数量固定可以在初始化阶段把所有ROM ID读出来存到数组里后续直接遍历数组不用每次都搜索。搜索ROM的算法比较绕但ST和TI都有现成的应用笔记。核心是维护一个冲突位栈每次遇到冲突就压栈下一轮从栈顶取分支。我建议新手先用单器件跑通再尝试多器件否则调试难度指数级上升。6.4 低功耗场景下的采样策略DS18B20的静态电流只有1μA但转换期间会跳到1.5mA。如果做电池供电的温度记录仪可以这样优化用9位分辨率转换时间93.75ms功耗降低8倍。采样间隔拉长到10分钟以上MCU大部分时间休眠。转换期间用强上拉转换完成后立即断电。我算过一笔账一颗2000mAh的CR2032如果每10分钟采样一次每次转换100ms、电流1.5mA平均电流约0.25μA加上MCU休眠电流2μA理论续航超过10年。当然实际受电池自放电限制但撑个两三年没问题。6.5 代码移植时的延时函数适配最后说一个移植性问题。网上大部分DS18B20代码用的是delay_us()但不同平台的实现差异很大。STM32可以用DWT周期计数器51单片机用定时器Arduino用delayMicroseconds()。移植时必须重新校准延时否则时序全乱。校准方法很简单写一个翻转GPIO的循环用示波器测高电平持续时间调整循环次数直到符合要求。比如要500μs先写for(i0;i100;i);测出来是300μs那就改成i167。虽然笨但最可靠。我在实际项目中更倾向于用硬件定时器产生精确延时比如STM32的TIM配置成1μs计数用__HAL_TIM_SET_COUNTER()和轮询方式等待。这样不受编译优化影响移植时只需改定时器配置。提示如果编译器开了-O2优化空的延时循环可能被优化掉记得加volatile修饰循环变量。DS18B20这个实验看起来简单但把时序、硬件、抗干扰、低功耗这几个维度都跑通对嵌入式基本功的提升非常明显。我个人的体会是单总线协议是检验是否真正理解时序的试金石——能把DS18B20读稳的人再去搞DHT11、红外解码、WS2812这类时序敏感的外设基本都是一通百通。