DS18B20多点测温嵌入式实战:单总线时序、ROM搜索与CRC校验

发布时间:2026/9/16 2:34:09
DS18B20多点测温嵌入式实战:单总线时序、ROM搜索与CRC校验 简介针对DS18B20多点测温需求的C语言示例源码面向嵌入式开发者、单片机学习者和相关项目调试人员利用DS18B20在-55℃~125℃量程内、最高约±0.5℃精度的测量能力解决多个传感器共用一根数据线时的地址识别与顺序测温问题适用于环境监控、农业温室、机房散热等多点部署场景。压缩包共1个文件为单个C语言源程序rar总大小约2KB结构紧凑核心逻辑集中已有902人学习/下载。源码围绕1-Wire单总线协议实现涵盖传感器初始化、ROM序列号匹配、温度转换命令下发与数据回读等关键环节并给出总线冲突处理思路可在此基础上扩展多路轮询、报警阈值判断或显示模块对想快速理解Dallas单总线时序、或着手搭建小型多点温度监测系统的开发者这份示例具有直接的参考价值。尤其适合初学者从零理解Dallas单总线通信流程也可为项目中的实时温度监测提供可靠参考。1. 拿到 ds18b20 多点测温压缩包之后先别急着改 main 函数从资源站下载的 ds18b20_多点测温 压缩包打开之后通常是 Keil 工程、原理图和几篇 datasheet 的混合体。很多人栽在一个思维定势上以为多点就是把几颗 DS18B20 的 DQ 引脚并在一起然后在 main 里 for 循环读三遍。真实情况是单总线上并了第二个从机之后跳过 ROM0xCC的读法会直接造成总线竞争读回来的数据一会儿是乱码一会儿固定在 85.0℃。这个 85.0 不是环境温度而是 DS18B20 上电复位后暂存器里的默认值也是多点接错线之后最常见的假读数。这篇文章按单总线时序 → ROM 搜索与匹配 → STM32 工程实现 → 现场排错的顺序展开目标是让你拿到这类压缩包后知道哪个文件才是真正要读的以及自己重写一套多点测温代码时需要守住哪些边界条件。适合正在做多路温度采集的嵌入式工程师和单片机开发者以下内容不依赖某个特定压缩包内的源码全部按 DS18B20 数据手册和常见工程实践来写。2. DS18B20 单总线时序多点测温的地基是微秒级窗口2.1 总线上并了多少颗 DS18B20决定你能用哪条命令DS18B20 的所有通信都发生在一条 DQ 数据线上这条线既是时钟又是数据主机通过精确控制电平翻转的时间窗口来区分0和1。协议层只有三种操作初始化复位脉冲 存在脉冲、写时隙、读时隙。而多点测温能成立的前提是每一颗 DS18B20 出厂时都固化了一个 64 位 ROM 码低 8 位是家族码 0x28中间 48 位是激光刻录的序列号高 8 位是 CRC 校验。这个 ROM 码就是传感器在总线上的身份证。单总线命令里0xCCSkip ROM对所有从机生效0x55Match ROM只对 ROM 码匹配的那一颗生效0xF0Search ROM用于枚举总线上全部设备。多点测温的难点在于0xCC 在只有一颗传感器时很好用一旦总线上有两颗以上所有设备会同时向总线发送数据位而单总线是线与逻辑只要有一颗设备输出 0总线就被拉低其他设备输出的 1 就被吞掉读回来的位流完全不具语义。所以多点系统必须走搜索枚举 - 匹配寻址这条路径这也是压缩包里真正值钱的代码所在。2.2 读时隙 15μs 采样窗这些参数要背下来几乎所有时好时坏的多点测温故障最后都能回溯到时序参数贴着边界跑。DS18B20 的时序规范是一组窗口值不是点值这意味着你的延时只要落在窗口内就行但绝对不能追求刚刚好。时序阶段电平行为合法参数区间常见实现取值复位主机拉低 DQ480μs ~ 960μs拉低 500μs存在脉冲采样释放 DQ 后等待从机拉低15μs ~ 60μs 开始采样延时 20μs 后读引脚写 0 时隙拉低 DQ60μs ~ 120μs拉低 65μs写 1 时隙拉低 DQ 后释放低电平 1μs ~ 15μs拉低 6μs 后释放读时隙拉低 DQ 后释放主机采样释放后 15μs 内必须读拉低 4μs释放后 8μs 采样位间恢复任意两个时隙之间至少 1μs预留 2μs读时隙是整个时序里最容易写错的地方因为主机在读数据之前必须自己先拉低总线再释放以产生一个读时隙的下降沿然后 DS18B20 会在之后的 15μs 内决定把总线拉低输出 0还是保持释放输出 1。主机必须在这个 15μs 窗口结束前完成引脚采样采样晚了读到的一律是高电平现象就是所有温度值都异常偏高。我一般把采样点放在释放后 8μs 左右前有 4μs 的低电平驱动后有 7μs 的余量既不早也不晚。2.3 为什么延时函数比时序表更容易坑你时序表是死的延时函数是活的。在很多从网上下载的工程里作者用软件循环或者裸的 for 空转来实现微秒延时这类延时在 -O0 编译时可能正确一旦开优化循环体可能被编译器优化掉延时直接变成几纳秒时序全部失效。更隐蔽的问题是中断如果在写时隙或读时隙过程中触发了一个耗时 50μs 的中断写 1 时隙就被硬生生拉成了写 0 时隙总线上的数据就错了。多点测温对时序延时的要求比单点更高因为多机总线上只要有一个位被干扰整个 ROM 匹配过程就失败了。常见做法是在进入单总线读写函数时关中断出来之后再开对 STM32 平台的延时用 DWT 计数或者 SysTick 微秒延时代替软件空循环。下面是一个基于 DWT 的微秒延时能做到 1μs 量级且不受编译器优化影响static volatile uint32_t *DWT_CYCCNT (uint32_t *)0xE0001004; static volatile uint32_t *DWT_CONTROL (uint32_t *)0xE0001000; static volatile uint32_t *SCB_DEMCR (uint32_t *)0xE000EDFC; void dwt_delay_us(uint32_t us) { uint32_t start *DWT_CYCCNT; uint32_t ticks us * (SystemCoreClock / 1000000UL); while ((*DWT_CYCCNT - start) ticks); } void dwt_init(void) { *SCB_DEMCR | 0x01000000; // 使能 DWT 访问 *DWT_CONTROL | 1; // 使能 CYCCNT 计数器 }这段代码的逻辑是DWT_CYCCNT 是 Cortex-M 内核里的周期计数器每个内核时钟周期加 1用 SystemCoreClock 换算出 1μs 对应的周期数然后循环等待差值到达目标。注意减法用的是无符号回绕特性所以不会有计数清零的边界问题。调用前先 dwt_init 一次之后所有单总线时序延时都用它。3. 多点测温的 ROM 管理搜索、匹配与 CRC8 校验3.1 0xCC 在多机总线上不可用那怎么知道总线连了谁在多点测温系统里第一步不是读温度而是枚举——把总线上每一颗 DS18B20 的 64 位 ROM 码完整读出来存到一个数组里。这一步用 0xF0Search ROM命令实现。搜索的核心原理是主机对 64 位 ROM 码逐位进行两次读时隙第一次读所有设备该位的值第二次读所有设备该位的反码。如果总线上所有设备在这一位上都是 0第一次读到 0、第二次读到 1都是 1 则相反如果既存在 0 也存在 1两次都会读到 0这个位置就是一个分叉点。遇到分叉点时主机决定本次搜索走哪一条路径然后写入一个写时隙把选择结果广播给所有从机。所有 ROM 码这一位与选择不一致的设备会在本次搜索中自动退出不再参与后续位的应答。这样一整轮 64 次读/写之后就得到了一颗设备的完整 ROM 码。下一次搜索从上一个分叉点改走另一条路径就能枚举出下一颗设备直到总线上的设备全部被找出来。整个过程可以用下面这个带分支回溯的骨架实现uint8_t last_disc 0; // 上一次搜索中最后一个走0的分叉位置 uint8_t ds18b20_search(uint8_t *rom) { memset(rom, 0, 8); if (ds18b20_reset() ! 0) { // 没有存在脉冲总线无设备 return 0; } uint8_t disc 0; ds18b20_write_byte(0xF0); // Search ROM for (uint8_t bit 0; bit 64; bit) { uint8_t b1 ds18b20_read_bit(); uint8_t b2 ds18b20_read_bit(); if (b1 1 b2 1) { // 两个设备位都是1总线异常 return 0; } uint8_t dir; if (b1 0 b2 0) { // 分叉点 if (bit last_disc) { dir 1; // 在记录的分叉位置改走1 } else if (bit last_disc) { dir (rom[bit / 8] (bit % 8)) 1; // 沿用上次路径 } else { dir 0; // 新分叉点先走0 } } else { dir b1; // 该位固定无分叉 } if (dir) rom[bit / 8] | (1 (bit % 8)); else rom[bit / 8] ~(1 (bit % 8)); ds18b20_write_bit(dir); // 把选择写回总线 if (b1 0 b2 0 dir 0) { disc bit; // 记录最后一个走0的分叉点 } } last_disc disc; return 1; // 成功枚举出一颗设备 }调用时循环执行 ds18b20_search直到返回 0 表示枚举完毕返回的每一组 8 字节就是一颗 DS18B20 的完整 ROM 码。这段代码第一次运行时从第一个分叉点开始全部走 0第二次运行时会在上次记录的最后一个分叉点强制走 1相当于把一棵二叉树完整遍历一遍。unsigned char 数组里 rom[0] 最低位是最先读到的位直接发给 DS18B20 用即可不需要再做字节序转换。3.2 用 0x55 匹配 ROM 进行单点寻址枚举完成之后真正读取多点温度时不能再用 0xCC必须对每一颗设备单独执行复位 - 0x55 - 8 字节 ROM - 功能命令的完整序列。0x55 后面的 8 字节 ROM 码从低位开始逐字节发送总线上的所有设备都会收到这串匹配码但只有 ROM 码完全一致的设备会响应后续的功能命令其他设备保持静默。这样就把一颗设备从总线上剥离出来了。下面是逐颗读取温度的标准流程包含了启动转换、等待完成、读取 9 字节暂存器和 CRC 校验float ds18b20_read_rom_temp(uint8_t *rom) { uint8_t data[9]; ds18b20_reset(); ds18b20_write_byte(0x55); // Match ROM for (int i 0; i 8; i) { ds18b20_write_byte(rom[i]); } ds18b20_write_byte(0x44); // 启动温度转换 // 外部供电模式下可等待总线释放寄生供电模式请改为固定延时 while (ds18b20_read_bit() 0); // 从机转换期间拉低总线完成释放 ds18b20_reset(); ds18b20_write_byte(0x55); // 再次匹配同一颗设备 for (int i 0; i 8; i) { ds18b20_write_byte(rom[i]); } ds18b20_write_byte(0xBE); // 读暂存器 for (int i 0; i 9; i) { data[i] ds18b20_read_byte(); } if (crc8(data, 8) ! data[8]) { // 第9字节是CRC校验值 return -999.0f; // 校验失败返回一个无效值 } int16_t raw (data[1] 8) | data[0]; return raw * 0.0625f; // 12位分辨率LSB0.0625℃ }0x44 是启动转换命令转换完成后结果存放在暂存器的第 1、2 字节。0xBE 是读暂存器命令连续读 9 个字节前 2 字节是温度值最后 1 字节是 CRC。需要注意 while 等待循环在寄生供电模式下不可用因为转换期间数据线被电容放电驱动无法反映转换状态只有外部供电时数据线才会被从机主动拉低。如果你的板子用的是三线制外部供电这个等待方式完全没问题否则就改成固定的延时等待。3.3 CRC8 校验多点测温里唯一能信的错误检测手段单总线通信没有应答机制主机发出的每一位也不存在回读确认。在多个设备共线的情况下某个时隙被中断打乱或上拉电阻偏弱导致边沿变缓都可能让主机读到一个错误位。判断这一帧数据是否可信的唯一手段就是 CRC8。DS18B20 的 CRC 多项式是 x^8 x^5 x^4 1对应十六进制 0x31右移计算时异或值取反转为 0x8C。uint8_t crc8(const uint8_t *data, uint8_t len) { uint8_t crc 0; while (len--) { crc ^ *data; for (uint8_t i 0; i 8; i) { if (crc 0x01) { crc (crc 1) ^ 0x8C; // 右移法多项式0x8C } else { crc 1; } } } return crc; }在搜索 ROM 阶段读到的 ROM 码第 8 字节本身就是前 7 字节的 CRC可以用同一函数验证这颗设备的 ROM 码是否完好。在读取温度阶段用 data[0] 到 data[7] 计算 CRC与 data[8] 比对。只要 CRC 不过这一帧直接丢弃不要参与任何均值或滤波计算。多点测温因为总线上存在多颗设备的电容负载波形边沿比单点更缓CRC 误码率会明显上升所以真实工程里不存在不写 CRC 也能稳定跑的说法只是偶尔出错还没被察觉而已。4. STM32 上把多点 DS18B20 跑起来引脚、上拉电阻与分辨率4.1 GPIO 开漏模式与 4.7kΩ 上拉电阻的选择依据DS18B20 数据手册里给出的应用电路很明确DQ 引脚需要接一个上拉电阻到 VDD。在单点应用里4.7kΩ 上拉通常没问题但多点测温时要重新审视这个值。总线上每增加一颗设备就会增加约 5pF ~ 15pF 的引脚电容和一小段 PCB 走线电容上拉电阻和总线电容构成一个 RC 低通网络电阻越大上升沿越缓。当上升沿超过 15μs 时设备在写时隙释放总线后主机可能无法在采样窗口内读到稳定的高电平。上拉电阻的选择策略是设备数量少、线缆短用 4.7kΩ超过 5 颗设备或者线长超过 1 米换 2.2kΩ极端情况用 1kΩ。但注意电阻不能太小DS18B20 的 DQ 灌电流能力有限1kΩ 已经是常见下限。还有一个更实用的做法给所有 DS18B20 的 VDD 并联一个 0.1μF 去耦电容每颗一颗靠近引脚放置能明显减少转换瞬间对数据线的干扰。压缩包里如果只有原理图没有 PCB这个电容位置反而是你最先要在自己板子上补的东西。STM32 的 GPIO 配置要使用开漏输出模式下面是 HAL 库的初始化代码GPIO_InitTypeDef gpio {0}; gpio.Pin GPIO_PIN_1; gpio.Mode GPIO_MODE_OUTPUT_OD; // 开漏输出 gpio.Pull GPIO_NOPULL; // 外部已有上拉内部不用 gpio.Speed GPIO_SPEED_FREQ_HIGH; // 提高压摆率下降沿更陡 HAL_GPIO_Init(GPIOB, gpio);开漏模式下GPIO 输出寄存器写 0 时引脚被拉低写 1 时引脚处于高阻态等效于释放总线由外部上拉电阻把电平拉高。这正好匹配单总线的线与逻辑任何设备都可以把总线拉低任何设备也都可以通过释放来输出逻辑 1。读引脚时即使引脚仍处于输出模式只要输出寄存器写 1 释放总线输入数据寄存器依然能正确读到外部电平所以不需要在读写之间切换模式。这一点对从 51 平台迁过来的工程师尤其重要STM32 的 GPIO 没有那么强的方向位概念省掉了每次读写都切换方向的麻烦。4.2 用 DWT 延时的单总线驱动替换工程里那些软延时很多下载来的工程里微秒延时长这样for (i 0; i 10; i);换个编译器、换个优化等级时序就废了。这里给出的驱动延时完全基于 DWT 周期计数器与优化等级无关只依赖 SystemCoreClock 的配置值。#define DQ_WRITE_LOW() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET) #define DQ_RELEASE() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_SET) #define DQ_READ() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) void ds18b20_write_bit(uint8_t bit) { DQ_WRITE_LOW(); if (bit) { dwt_delay_us(6); DQ_RELEASE(); // 写1低电平保持6μs后释放 dwt_delay_us(60); } else { dwt_delay_us(60); // 写0低电平保持60μs DQ_RELEASE(); dwt_delay_us(10); } } uint8_t ds18b20_read_bit(void) { uint8_t val; DQ_WRITE_LOW(); dwt_delay_us(4); // 读时隙起始低电平 DQ_RELEASE(); // 释放总线 dwt_delay_us(8); // 8μs后采样落在15μs窗口内 val DQ_READ(); dwt_delay_us(60); return val; }写 1 和写 0 的本质区别只在于低电平持续的时间。写 0 必须让总线保持低电平超过 60μs写 1 则只需 1μs ~ 15μs 的低电平然后释放。上面代码里写 1 时低电平保持 6μs留了足够余量写 0 时保持 60μs 后释放正好落在规范区间。读时隙的采样点在释放后 8μs此时从机的数据输出已经稳定而且还没到 15μs 的截止线这个时间点对温度变化不太敏感是实际调试中比较省心的位置。如果你换用更高主频的芯片只需要保证 SystemCoreClock 宏正确DWT 延时就能自动适配。4.3 完整的多点温度采集流程与分辨率配置假设已经通过 ds18b20_search 获得了一个 ROM 码数组 devices数量为 device_count那么一轮采集就是用一个 for 循环对每个 ROM 码调用一次匹配读取。这里有三个工程上常见的注意点转换命令和读取命令之间需要匹配同一颗设备所以每次读温度都要发起两次完整的匹配序列多颗设备不要同时发 0x44因为总线只有一个0x44 是广播命令时间上只能逐颗发起逐颗启动再逐颗等待总耗时是每颗设备的转换时间之和而不是单颗转换时间。分辨率配置直接影响转换时间和温度精度。DS18B20 上电默认 12 位分辨率转换时间 750ms这个值对于多点系统来说太慢了。温度值每一位对应 0.0625℃如果把分辨率降到 10 位量化步长变成 0.25℃对绝大多数环境监测场景完全够用而转换时间降到 187.5ms四路采集总时间从 3 秒变成 0.75 秒效果显著。配置方式是通过 0x4E 写暂存器void ds18b20_set_resolution(uint8_t *rom, uint8_t cfg) { ds18b20_reset(); ds18b20_write_byte(0x55); // Match ROM for (int i 0; i 8; i) { ds18b20_write_byte(rom[i]); } ds18b20_write_byte(0x4E); // 写暂存器 ds18b20_write_byte(0x00); // TH 上限 ds18b20_write_byte(0x00); // TL 下限 ds18b20_write_byte(cfg); // 配置寄存器 ds18b20_reset(); ds18b20_write_byte(0x55); for (int i 0; i 8; i) { ds18b20_write_byte(rom[i]); } ds18b20_write_byte(0x48); // 复制到EEPROM掉电不丢失 }cfg 取值9 位分辨率对应 0x1F10 位对应 0x3F11 位对应 0x5F12 位对应 0x7F。0x4E 命令之后必须连续写入 TH、TL 和配置寄存器三个字节顺序不能换写完配置之后还要发 0x48 把暂存器内容复制到 EEPROM否则断电再上电分辨率又跳回默认的 12 位。这个 0x48 步骤是很多压缩包源码里缺失的也是我明明写了配置重启怎么又变慢的唯一原因。5. 多点 DS18B20 组装完之后的排错顺序和一招帧间滤波5.1 从五个现场症状反推故障点多点测温的排错和单点完全不同单点出问题通常是时序或供电多点出问题还多了一个设备间互扰。下面按排查优先级排序先测上拉电平断电状态下用万用表量 DQ 对地电阻应该在 1kΩ ~ 4.7kΩ 之间量出来无穷大说明上拉没焊量出来接近 0 说明 DQ 对地短路。这两种情况在多点板上都常见因为 DQ 引脚间距小焊锡桥接的概率随设备数量上升。再测存在脉冲只接一颗设备复位后示波器抓 DQ应该在释放后 60μs 内看到一个约 120μs 宽的低电平脉冲。这个脉冲没有就检查 VDD 和 GND 是否接反DS18B20 引脚顺序是 GND、DQ、VDD三根线的封装方向错了都很正常。存在脉冲正常后接第二颗设备再次抓复位波形。如果第二颗接上后第一颗的存在脉冲消失或变窄说明上拉电阻太大总线电容把上升沿拖得过缓换 2.2kΩ 上拉通常能解决。接着跑一遍 ds18b20_search确认枚举数量等于实际设备数。枚举少一颗就去查那一颗的 DQ 焊点和 VDD 供电枚举多一颗说明某颗设备存在虚焊靠总线电容偶尔能通信。最后验证 CRC 丢帧率在循环读取程序里统计 crc8 校验失败的次数占总读取次数的比例超过 1%就要检查是不是读时隙采样点太靠后或者中断抢占导致的时序撕裂。STM32 上把单总线读写包进临界区Linux 或 RTOS 环境则关调度器是压掉这类随机 CRC 错误最直接的手段。5.2 连续帧校验比失败就重读更稳的过滤技巧CRC 校验失败后直接重读遇到总线受到周期性干扰时会陷入持续重读到超时的循环反而把采集周期拉长。更实用的做法是给每颗设备维护一个长度为 3 的滑动窗口当前帧 CRC 通过就更新温度值CRC 失败就沿用上一帧的有效值同时失败计数器加一。连续失败超过 3 帧才判定这颗设备离线触发告警。温度信号本身是慢变量相邻两帧的差值在 10 位分辨率下通常不超过 1℃所以这种猜测式的帧间滤波不会引人误差但能极大提高上层逻辑的稳定性。实现上只需要在原来 read_rom_temp 外面包一层状态typedef struct { uint8_t rom[8]; float temp; uint8_t fail_cnt; } ds18b20_dev_t; void ds18b20_poll(ds18b20_dev_t *dev) { float t ds18b20_read_rom_temp(dev-rom); if (t -500.0f) { // CRC通过 dev-temp t; dev-fail_cnt 0; } else { // CRC失败 if (dev-fail_cnt 3) { dev-fail_cnt; } } }这个技巧在多机总线上作用尤为明显它能稳定滤掉因某个时隙被中断打断而产生的单帧坏数据不会因为一次 CRC 失败就把整块告警逻辑打飞。把连续失败阈值设为 3再配合一次总线复位后重发当前设备的匹配序列基本能把误报率压到接近零而如果阈值设成 1每次干扰都重读反而会在多设备轮询时积累出肉眼可见的卡顿。本文还有配套的精品资源点击获取