☰
DS18B20单总线时序避坑指南:从85℃到精准测温的实战解析
2026/10/16 1:11:09 网站建设 项目流程

1. 单总线协议为什么让DS18B20既省引脚又难伺候

DS18B20这颗温度传感器在嵌入式圈子里算是老面孔了,但每次带新人做实验,总有人卡在"读出来永远是85℃"或者"数据全是0xFF"这两个坎上。问题不在芯片本身,而在于它用的是单总线(1-Wire)协议——一根数据线既要传时钟又要传数据,还要兼顾供电,时序容错窗口窄得让人抓狂。

先说清楚它到底解决了什么问题。传统温度采集方案里,模拟传感器(比如LM35)需要一路ADC通道,每个传感器独占一个引脚;I2C传感器(比如LM75)虽然能挂总线,但至少得两根线(SCL+SDA),还得处理地址冲突。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或5V,GND接地,DQ接GPIO加上拉电阻。寄生供电则是VDD和GND都接地,靠DQ线在总线高电平时通过内部电容取电。寄生供电省一根线,但对时序要求更苛刻,而且在温度转换期间总线必须保持强上拉,否则转换结果会出错。新手建议直接用外部供电,稳定压倒一切。

参数外部供电寄生供电
接线数量3根(VDD/GND/DQ)2根(GND/DQ)
温度转换期间总线可自由操作必须强上拉
适合场景通用实验远距离、引脚极紧张
新手推荐度高低

2.2 上拉电阻的计算逻辑

上拉电阻的取值不是拍脑袋定的。它要满足两个条件:一是保证总线能被拉高到VIH以上,二是在器件拉低时电流不能超过引脚承受能力。

DS18B20的灌电流能力典型值是4mA。假设供电3.3V,上拉电阻R,则拉低时电流I = 3.3 / R。要保证I < 4mA,R > 825Ω。另一方面,总线上的漏电流和输入电容会影响上升沿时间,R太大则上升沿变缓,高速通信时可能采不到高电平。经验公式是R < 1 / (2πfC),其中f是通信频率(标准速率约16.7kHz),C是总线电容(每米线缆约50pF)。假设线缆1米,C=50pF,则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 温度转换与暂存器读取的完整流程

一次完整的温度采集包含四个步骤:

  1. 复位,检测应答。
  2. 跳过ROM(0xCC),因为总线上只有一个器件时不需要匹配ROM ID。
  3. 启动转换(0x44),DS18B20开始ADC,最长750ms。
  4. 再次复位,跳过ROM,读取暂存器(0xBE),连续读9个字节。

暂存器的前两个字节就是温度值,低字节在前,高字节在后。温度格式是16位有符号数,低4位是小数部分,单位0.0625℃。比如读出来0x0191,二进制是0000 0001 1001 0001,高字节0x01,低字节0x91,组合成0x0191 = 401,401 × 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)或写1(1~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.75ms,12位要750ms。

配置方法是写暂存器的第4字节(配置寄存器),然后复制到第5字节(EEPROM)。配置寄存器的bit6和bit5决定分辨率:00是9位,01是10位,10是11位,11是12位。上电默认是11位?不对,默认是12位,对应bit6=1、bit5=1。

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.5mA,4.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(i=0;i<100;i++);,测出来是300μs,那就改成i<167。虽然笨,但最可靠。

我在实际项目中更倾向于用硬件定时器产生精确延时,比如STM32的TIM,配置成1μs计数,用__HAL_TIM_SET_COUNTER()和轮询方式等待。这样不受编译优化影响,移植时只需改定时器配置。

提示:如果编译器开了-O2优化,空的延时循环可能被优化掉,记得加volatile修饰循环变量。

DS18B20这个实验看起来简单,但把时序、硬件、抗干扰、低功耗这几个维度都跑通,对嵌入式基本功的提升非常明显。我个人的体会是,单总线协议是检验"是否真正理解时序"的试金石——能把DS18B20读稳的人,再去搞DHT11、红外解码、WS2812这类时序敏感的外设,基本都是一通百通。

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

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

立即咨询