DS18B20在ESP32上的单总线时序实现与优化
2026/9/11 16:22:18 网站建设 项目流程

简介:面向ESP32开发者的DS18B20数字温度传感器驱动源码包,适用于智能家居、农业环境监测、工业过程控制等物联网测温场景。DS18B20支持高精度单总线通信,仅需一根数据线即可与ESP32的GPIO连接,电源与数据线供电方式灵活,很适合快速搭建温度采集节点。压缩包共2个文件,包含一个C源文件和一个头文件,整体仅2KB,C源文件实现单总线时序、温度转换和设备搜索逻辑,头文件则负责声明对外接口,方便直接集成。目前已有1253人学习/下载。源码整合了常用单总线库与温度传感器库的典型调用流程,覆盖传感器初始化、总线设备识别、温度读取与浮点结果返回,并可通过设备唯一识别码区分多个传感器;同时预留了浮点转字符串、多设备管理上的扩展空间,便于开发者借助ESP32的联网能力实现本地显示或云端上报,从而灵活改造为多点温度监控方案。

1. DS18B20 在 ESP32 上真正难的不是读温度,而是时序

DS18B20 是单总线数字温度传感器里最普及的一颗,硬件上只需一根数据线外加一个上拉电阻,12 位分辨率下精度可达 ±0.5°C,在智能温控、农业环境监测和工业过程控制里随处可见。难点在于它的通信完全依赖微秒级时序:复位要拉低 480μs,写 0 时隙要持续 60μs,读采样点在释放后 15μs 内完成。ESP32 跑着 FreeRTOS,GPIO 要经过 IO MUX 和 GPIO 矩阵两级映射,再加上 Wi-Fi 协议栈的中断抢占,直接套用 Arduino 库的 delayMicroseconds 方案经常出现随机 CRC 错误。DS18B20.c 和 DS18B20.h 这对文件把初始化、ROM 搜索、CRC 校验、温度转换封装成 ESP-IDF 组件,关键时序操作放入临界区保护,实测在一条总线上挂多探头仍然稳定。适合从 Arduino 迁移到 ESP-IDF 的开发者,也适合要把多个 DS18B20 接入生产环境的嵌入式工程师。

2. DS18B20.c/.h 中单总线协议的时序骨架

单总线协议由三类基本操作构成:复位脉冲、写时隙、读时隙。DS18B20.c 的核心工作就是在 ESP32 的 GPIO 上精确复现这三类操作,再把设备寻址、温度转换命令、数据校验封装成上层可直接调用的接口。

2.1 复位时序与存在检测的实现逻辑

总线空闲时,外部上拉电阻把数据线维持在 3.3V 高电平。主机发起通信的第一步是把总线拉低至少 480μs 再释放,DS18B20 检测到这个长低电平窗口后,延迟 15~60μs 把总线拉低 60~240μs,作为存在脉冲回应主机。这个握手过程决定了后续所有命令是否有效,如果存在脉冲没抓到,后面的读写时隙全部是空操作。

ESP32 上实现复位,关键是采样存在脉冲的窗口和对 GPIO 方向的切换。典型实现如下:

// ds18b20.c — 复位总线并检测设备存在 int ds18b20_reset(ds18b20_dev_t *dev) { gpio_set_direction(dev->gpio_num, GPIO_MODE_OUTPUT_OPEN_DRAIN); gpio_set_level(dev->gpio_num, 0); ets_delay_us(500); // 拉低 500us,覆盖最小 480us 要求 gpio_set_level(dev->gpio_num, 1); gpio_set_direction(dev->gpio_num, GPIO_MODE_INPUT); ets_delay_us(70); // 等待 DS18B20 拉低总线 int presence = gpio_get_level(dev->gpio_num); ets_delay_us(500); // 等待存在脉冲窗口结束 return presence == 0 ? 0 : -1; }

延时用ets_delay_us而非vTaskDelay,是因为 FreeRTOS 的 tick 默认是 10ms 粒度,无法表达微秒级等待。ets_delay_us是 ROM 层忙等函数,期间不会让出 CPU。gpio_set_direction在每次操作前重复调用,是为了确保引脚在开漏输出和输入两个状态之间正确切换,漏掉这一步会导致引脚方向停留在输出模式,存在脉冲被输出寄存器电平钳住而读不到。

存在脉冲采样点选在释放总线后 70μs,落在手册规定的 15~60μs 起始窗口之后、240μs 结束窗口之前。如果此处采到高电平,优先查三处:传感器是否供电、外部上拉是否开路、GPIO 是否被配置成了推挽输出而不是开漏模式。

2.2 读写时隙的时间参数与容差分析

写时序分写 0 和写 1。主机都要先把总线拉低,区别只在释放时机:写 0 要持续拉低 60~120μs,写 1 只保持 1~15μs 就释放。DS18B20 从下降沿开始计时,在 15~60μs 窗口内读取总线电平,所以写 0 必须低到采样窗口结束,写 1 则要在采样前让总线恢复高电平。

读时隙类似写 1 的启动,主机拉低 1~6μs 后释放,并在 15μs 内完成采样。传感器要返回 0 时从释放时刻起拉低总线,返回 1 时保持高电平。读写单个 bit 的典型实现:

// 写一个 bit static void ds18b20_write_bit(ds18b20_dev_t *dev, int bit) { portENTER_CRITICAL(&dev->spinlock); gpio_set_direction(dev->gpio_num, GPIO_MODE_OUTPUT_OPEN_DRAIN); gpio_set_level(dev->gpio_num, 0); if (bit) { ets_delay_us(6); // 写 1:拉低 6us 后释放 gpio_set_level(dev->gpio_num, 1); ets_delay_us(60); } else { ets_delay_us(60); // 写 0:持续拉低 60us gpio_set_level(dev->gpio_num, 1); ets_delay_us(6); } portEXIT_CRITICAL(&dev->spinlock); } // 读一个 bit static int ds18b20_read_bit(ds18b20_dev_t *dev) { int level; portENTER_CRITICAL(&dev->spinlock); gpio_set_direction(dev->gpio_num, GPIO_MODE_OUTPUT_OPEN_DRAIN); gpio_set_level(dev->gpio_num, 0); ets_delay_us(2); // 拉低 2us 启动读时隙 gpio_set_direction(dev->gpio_num, GPIO_MODE_INPUT); ets_delay_us(8); // 在 15us 窗口内完成采样 level = gpio_get_level(dev->gpio_num); ets_delay_us(50); // 等待读时隙结束 portEXIT_CRITICAL(&dev->spinlock); return level; }

portENTER_CRITICAL进入当前 CPU 的临界区,屏蔽中断防止 Wi-Fi 协议栈或定时器回调打断时序窗口。临界区内不能调用任何可能阻塞或触发调度的 API,vTaskDelayprintfESP_LOGI都不能出现,否则触发断言或产生不可预期的总线状态。

字节操作建立在 bit 操作之上,DS18B20 的数据一律低位在前:

// 写一个字节,低位先出 void ds18b20_write_byte(ds18b20_dev_t *dev, uint8_t byte) { for (int i = 0; i < 8; i++) { ds18b20_write_bit(dev, byte & 0x01); byte >>= 1; } } // 读一个字节,低位先出 uint8_t ds18b20_read_byte(ds18b20_dev_t *dev) { uint8_t byte = 0; for (int i = 0; i < 8; i++) { byte >>= 1; if (ds18b20_read_bit(dev)) { byte |= 0x80; } } return byte; }

时隙参数没有取极端值,而是卡在手册区间的中间:写 1 拉低 6μs,写 0 拉低 60μs,读采样点选在释放后 8μs。这样给 PCB 走线电容、引脚容差和温漂留了余量。量产时如果个别的设备通信失败,可以微调这几个延时,但不要低于手册下限,否则高温环境下会出现间歇性失败。

2.3 CRC 校验与地址完整性的验证

DS18B20 的 ROM 共 8 字节,最后 1 字节是 CRC8。读暂存器命令0xBE会连续返回 9 字节,最后 1 字节同样是 CRC8,用于校验前 8 字节在传输中是否发生位翻转。总线挂多个设备或线缆较长时,CRC 错误率会明显上升,驱动层不做校验的话,读到的是错误温度值而不自知。

// CRC8 校验,多项式 x^8 + x^5 + x^4 + 1 uint8_t ds18b20_crc8(const uint8_t *data, int len) { uint8_t crc = 0; for (int i = 0; i < len; i++) { uint8_t byte = data[i]; for (int j = 0; j < 8; j++) { uint8_t mix = (crc ^ byte) & 0x01; crc >>= 1; if (mix) { crc ^= 0x8C; // 低位优先形式的 CRC 寄存器值 } byte >>= 1; } } return crc; }

0x8C对应X^8+X^5+X^4+1在 LSB First 算法下的归约值。把 9 字节传入后结果为 0 表示校验通过。如果频繁非零,优先检查上拉电阻阻值范围,以及数据线是否靠近开关电源或电机驱动线。

3. 从 Arduino 迁移到 ESP-IDF:GPIO 驱动与任务设计

3.1 GPIO 矩阵映射与 IO MUX 的选型差异

Arduino 环境的digitalWrite经过框架的引脚映射层,包含多层条件判断和位运算。在 240MHz 主频下虽然只多几百纳秒,但对微秒级协议来说是实质性延迟——写 1 时隙被拖慢超过 15μs,DS18B20 就会把它误判成写 0。

ESP-IDF 提供 IO MUX 和 GPIO 矩阵两条路径。IO MUX 是芯片内固定引脚到外设的直连,延迟最低但功能固定;GPIO 矩阵是任意 GPIO 到任意外设的交叉开关,灵活但多一级路由。单总线协议完全软件模拟,不经过任何外设信号路径,直接用gpio_set_levelgpio_get_level操作寄存器即可。DS18B20.c 里的实现正是在开漏输出和输入之间切换 GPIO 方向,绕开 Arduino 的引脚映射开销。

选引脚时注意避开 Strapping 引脚。GPIO 12、GPIO 0、GPIO 2 在芯片复位时有特殊上电时序要求,DS18B20 挂在 GPIO 12 且被外部拉低,可能导致 ESP32 无法正常启动。常见做法是选 GPIO 16、GPIO 17、GPIO 4 这类普通 IO,其中 GPIO 16 和 GPIO 17 还支持 RTC 域,深度睡眠唤醒后能快速恢复 IO 状态。

3.2 GPIO 初始化配置与上拉电阻选型

数据线在 ESP-IDF 里的推荐配置是开漏输入输出双向模式:

gpio_config_t io_conf = { .pin_bit_mask = (1ULL << dev->gpio_num), .mode = GPIO_MODE_INPUT_OUTPUT_OD, // 开漏模式,输入输出双向 .pull_up_en = GPIO_PULLUP_ENABLE, // 内部弱上拉,45k 级别 .pull_down_en = GPIO_PULLDOWN_DISABLE, .intr_type = GPIO_INTR_DISABLE, }; gpio_config(&io_conf);

开漏模式下输出低电平时主动拉低总线,输出高电平时引脚呈高阻态,由外部电阻把电平拉回高。GPIO_PULLUP_ENABLE启用的内部上拉约 45kΩ,短距离实验够用,线缆一长电平上升沿被 RC 延迟拖得很慢,读时隙采到的电平还没恢复到高。

量产建议在 ESP32 引脚附近外接一颗 2.2kΩ~4.7kΩ 电阻到 3.3V。注意数据线上拉只能接 3.3V,不能接 5V。如果 DS18B20 是外部 5V 供电,高电平已是 5V 级别,直接进 ESP32 的 3.3V IO 会超耐压,需要加 MOS 管双向电平转换电路。

数据线缆长度上拉电阻布局建议
< 15cm4.7kΩ板载直连,无需屏蔽
15cm ~ 3m3.3kΩ双绞线或屏蔽线,远离电源线
> 3m2.2kΩ屏蔽线单端接地,主机端上拉

总线上挂多个 DS18B20 时,上拉电阻只需一颗,放在离 ESP32 最近的位置。每颗传感器各加上拉会让等效并联电阻降到几百欧,低电平采样窗口被破坏,还会白白增加功耗。

提示:数据线对地并联 100pF 电容可以滤掉一部分高频干扰,但电容不能太大,否则电平上升沿变缓,反而拉长时序。线长超过 1 米时建议放弃电容滤波,改用屏蔽线。

3.3 FreeRTOS 任务划分与看门狗的影响

温度采集是周期性任务,时序操作必须在临界区内完成,而临界区内不能做浮点运算、不能打印日志、不能发送队列消息。正确做法是临界区只包住读写时隙部分,CRC 校验、温度换算、日志输出都放到临界区外:

void temp_collect_task(void *arg) { ds18b20_dev_t *dev = (ds18b20_dev_t *)arg; float temp_c; while (1) { ds18b20_start_convert(dev); // 发送转换命令 vTaskDelay(pdMS_TO_TICKS(200)); // 等待转换完成 if (ds18b20_read_temp(dev, &temp_c) == 0) { ESP_LOGI("temp", "%.2f°C", temp_c); } else { ESP_LOGW("temp", "read failed / crc error"); } vTaskDelay(pdMS_TO_TICKS(2000)); // 2 秒采样周期 } }

任务栈建议给 4096 字节。ds18b20_read_temp内部涉及浮点格式化和 CRC 计算,栈太小会溢出,表现为任务运行一段时间后系统随机重启。任务优先级给 5 即可,不要用最高优先级。读时序需要短时间屏蔽中断,优先级太高会把 Wi-Fi 协议栈的低优先级任务饿死。

温度采集任务只负责数据获取,上报交给网络任务。常见做法是数据采集任务把 float 温度值通过xQueueSend发给 MQTT 或 HTTP 任务,两边用队列解耦,避免采集任务因 Wi-Fi 重连阻塞影响时序。队列深度 4 就够,采集频率高于上报频率时直接丢旧报新。

深度睡眠场景下,DS18B20 掉电后暂存器内容不保留,唤醒后必须先复位、再搜索 ROM,不能直接用静态保存的地址跳过枚举。ESP32 的 RTC GPIO 在睡眠期间保持电平,但 DS18B20 内部状态还是要重新初始化。

4. 多设备挂载、ROM 搜索与电源模式实战

4.1 单总线上多设备的搜索与寻址

DS18B20 同一根总线可挂多颗传感器,每颗有 64 位 ROM 唯一标识,第 1 字节是家族代码0x28,最后 1 字节是 CRC。主机上电后第一步是枚举总线上存在的设备,通过搜索命令0xF0完成。搜索算法的核心是位冲突检测:主机逐位输出地址位,DS18B20 分两次回传该位的真实值和反码,主机根据组合判断当前位是 0、1 还是两颗设备同时存在。

// 搜索总线上所有设备,rom_ids 由调用方分配 int ds18b20_search_all(ds18b20_dev_t *dev, uint64_t rom_ids[], int max_devs) { int dev_count = 0; uint64_t rom = 0; int last_branch = -1; // 位冲突分支回溯位置 while (dev_count < max_devs) { if (ds18b20_rom_search(dev, &rom, &last_branch) < 0) { break; } rom_ids[dev_count++] = rom; } return dev_count; }

last_branch是容易出错的地方。它记录上一次搜索遇到的冲突位,下一次搜索从这个位置回溯到另一分支继续。每次调用都赋为 -1,搜索只会返回第一个设备,形成死循环。把这个字段封装进结构体长期保存,比用函数参数传递可靠。

单设备场景不必走搜索。发送0x33读 ROM 命令后直接读 8 字节即可,但这条命令在多设备挂载时会因为总线冲突读到一堆0xFF。工程经验是先从设备数量上判断,明确只有一颗时用0x33,否则一律走0xF0搜索。

4.2 匹配 ROM 与跳过 ROM 的使用边界

拿到 ROM 后,对指定传感器发送命令需要先发0x55(匹配 ROM),再跟 8 字节 ROM 地址。总线上其他设备收到不匹配地址后进入休眠,只有地址匹配的那颗响应后续命令。完整读取流程:

// 读取指定 ROM 地址的设备温度 float ds18b20_read_temp_by_rom(ds18b20_dev_t *dev, uint64_t rom) { ds18b20_reset(dev); ds18b20_write_byte(dev, 0x55); // 匹配 ROM for (int i = 0; i < 8; i++) { uint8_t byte = (rom >> (8 * i)) & 0xFF; ds18b20_write_byte(dev, byte); // 低位优先发送 } ds18b20_write_byte(dev, 0xBE); // 读暂存器 uint8_t data[9]; for (int i = 0; i < 9; i++) { data[i] = ds18b20_read_byte(dev); } if (ds18b20_crc8(data, 9) != 0) { return NAN; } int16_t raw = (int16_t)(data[1] << 8 | data[0]); return raw * 0.0625f; // 12 位分辨率转换 }

温度寄存器是 16 位二进制补码,data[1] << 8 | data[0]合并后是实际温度值的 16 倍,乘以 0.0625 得到摄氏温度。返回 NAN 标记读取失败,调用端要判断isnan

总线上只有单颗设备时,直接发0xCC(跳过 ROM)省去 8 字节地址传输。但两条命令不能混用:对多设备总线发0xCC+0x44让所有设备同时开始温度转换,这是预期行为;如果对多设备总线发0xCC+0xBE,所有传感器会同时往总线上推数据,波形直接冲突,读回来是乱码。

4.3 寄生供电与外部供电的选择依据

寄生供电模式下,VCC 引脚直接接地,传感器通过数据线低电平期间的电流给内部电容充电,高电平期间用储存的能量驱动采样电路。这个模式省一根走线,但温度转换期间数据线必须被主机持续拉高 750ms,否则转换中断。ESP32 上实现时,发送0x44后要强制把 GPIO 置为高电平输出:

void ds18b20_start_convert_parasite(ds18b20_dev_t *dev) { ds18b20_send_command(dev, 0x44); // 转换命令 gpio_set_direction(dev->gpio_num, GPIO_MODE_OUTPUT_OPEN_DRAIN); gpio_set_level(dev->gpio_num, 1); // 总线保持高电平,给传感器供电 }

保持期间不能依赖弱上拉,必须由主机主动驱动高电平,否则线上电流不足以支持 ADC 采样。外部供电模式则简单得多,VCC 接 3.3V 或 5V,数据线只做通信。凡是数据线超过半米的场景都建议选外部供电,寄生供电对线缆电容太敏感,线长后充电不足导致转换结果漂移。

4.4 批量转换与统一读取的时序优化

多设备巡检场景,逐个发转换命令再等待,总周期等于设备数量乘以单次转换时间,10 颗设备就是 7.5 秒。优化做法是先发一次0xCC+0x44让所有设备同时转换,等待 750ms 之后逐个匹配 ROM 读温度:

void collect_all_temps(ds18b20_dev_t *dev, uint64_t rom_ids[], int n) { ds18b20_reset(dev); ds18b20_write_byte(dev, 0xCC); // 跳过 ROM,所有设备同时转换 ds18b20_write_byte(dev, 0x44); // 温度转换命令 vTaskDelay(pdMS_TO_TICKS(800)); for (int i = 0; i < n; i++) { ds18b20_reset(dev); ds18b20_write_byte(dev, 0x55); for (int j = 0; j < 8; j++) { ds18b20_write_byte(dev, (rom_ids[i] >> (j * 8)) & 0xFF); } ds18b20_write_byte(dev, 0xBE); uint8_t data[9]; for (int j = 0; j < 9; j++) { data[j] = ds18b20_read_byte(dev); } if (ds18b20_crc8(data, 9) == 0) { int16_t raw = (int16_t)(data[1] << 8 | data[0]); ESP_LOGI("temp", "dev[%d]: %.2f°C", i, raw * 0.0625f); } } }

批量转换后每读一颗都要重新复位并发一次0x55,因为读取过程中其他设备保持休眠,总线空闲后所有设备回到初始状态。时间预算:批量转换 800ms,之后每读一颗约 5ms,挂 10 颗探头时一轮完整采集周期约 850ms,比逐个转换的 7.5 秒压缩了近 9 倍,适合冷链仓储这类高频巡检场景。

5. 逻辑分析仪抓时序与 CRC 失败的快速定位

排查 DS18B20 通信问题,最直接的手段是接逻辑分析仪抓数据线波形。10MHz 采样率即可,通道夹在数据线上,GND 与 ESP32 共地,触发方式设成下降沿。正常波形具备三段结构:复位阶段是 480μs 以上的长低电平窗口,随后有一个 60~240μs 的负向存在脉冲;写时隙连续出现 60μs 宽方波说明写 0,短下拉后快速回高是写 1;读时隙是一个约 6μs 的窄负脉冲,后面跟着 15μs 内的采样窗口。

三种典型异常波形对应不同根因。第一种波形完全消失且总线一直低电平,多半是 GPIO 配置成纯输出模式持续拉低,或者外部上拉电阻漏焊、内部上拉没打开。第二种存在脉冲正常,但读写时隙电平恢复呈明显斜坡,是数据线分布电容大、上拉电阻偏大的典型症状,把 4.7kΩ 换成 2.2kΩ 波形后沿会变陡。第三种波形看起来正常但 CRC 频繁失败,优先怀疑数据线紧挨 PWM 输出或电机驱动线,高频干扰把采样点前的电平翻转了。

定位具体板卡时,在驱动里临时打开诊断模式,CRC 失败打印原始数据:

if (ds18b20_crc8(data, 9) != 0) { ESP_LOGE("ds18b20", "crc fail rom=%08x%08x", (uint32_t)(rom >> 32), (uint32_t)rom); ESP_LOG_BUFFER_HEX("ds18b20", data, 9); }

ESP_LOG_BUFFER_HEX把收到的 9 字节按十六进制打印出来,与逻辑分析仪逐位对照,能立刻定位是哪一位翻转。再对比关闭 Wi-Fi 和蓝牙后的 CRC 错误率,可以判断射频中断是否干扰了时序临界区。确认是干扰后,把 DS18B20 任务绑定到空闲 CPU 核心运行,并缩短临界区长度。

长线场景的三项加固措施:传感器端加 100nF 退耦电容,数据线串联 100Ω 电阻抑制反射,屏蔽层只在 ESP32 端单点接地。线缆超过 5 米时,单总线的容性负载已经接近 DS18B20 手册上限,这时与其加大上拉电阻,不如把总线拆成两路,分别用两个 GPIO 驱动,每路挂的设备不超过 5 颗。

本文还有配套的精品资源,点击获取

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

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

立即咨询