资讯详情

单总线协议1-Wire深度解析:从物理层时序到ROM寻址与DS18B20驱动

📅 2026/9/16 7:28:39 | 华诺云谱 👁 阅读
单总线协议1-Wire深度解析:从物理层时序到ROM寻址与DS18B20驱动
做嵌入式这些年各种通信协议接触过不少但要说“极简主义”的程度单总线协议1-Wire绝对排得上号。一根数据线加一根地线就把物理层、链路层甚至供电一起解决了而设备寻址又依赖一套 64 位 ROM 码机制。很多朋友第一次接触单总线多半是因为 DS18B20 这颗温度传感器调通时序容易但真要把物理层设计、复位时序、ROM 搜索这些底层逻辑讲清楚很多人其实是一知半解的状态。这篇文章我想把单总线从物理层到 ROM 寻址完整拆开聊一遍。不管你是刚入门单片机想要搞懂时序还是已经在项目里挂了十几个传感器想排查偶发通信问题这篇文章都能给你一套可以直接用的思路。我会把每一段时序的参数为什么这么定、每一个寻址命令背后是怎么工作的都用实际项目里的语言讲明白。1. 一根数据线怎么搞定通信和供电先从物理层设计说起1.1 开漏输出和上拉电阻到底解决了什么问题单总线协议最反直觉的地方就是所有设备共用一根数据线而且这根线在空闲时是高电平通信时靠拉低来产生时序。这背后其实是开漏输出结构在起作用。开漏输出的特点很简单芯片内部只有一个下拉 MOS 管能把总线拉低但没有主动推高的能力。所以总线必须外接一个上拉电阻空闲时靠电阻把电平拉回高。这样的好处是极大的——任何设备都只能把总线拉低不能主动拉高于是多个设备挂在同一条线上时不会出现一个设备输出高、另一个设备输出低导致的短路问题。这跟 I2C 的总线设计思路是一脉相承的。我在实际项目里见过不少人把单总线当作普通 GPIO 推挽输出来用结果通信成功率时好时坏。原因就是推挽输出会让总线在高低电平之间来回驱动一旦两个设备同时输出相反电平轻则通信错误重则烧毁引脚。所以硬件设计上单总线节点必须开漏外接上拉电阻的阻值一般取 4.7kΩ这个值是通用推荐值短距离一对一通信时很稳定。上拉电阻的选型需要根据线缆长度和设备数量调整。短距离、单设备的场景用 4.7kΩ 没问题如果总线拉长到几十米或者挂了十几个设备总线电容变大就要把上拉电阻降到 2.2kΩ 甚至 1kΩ否则上升沿太慢会让时序窗口错位。当然电阻也不能太小太小会增加总线低电平时的灌电流对从设备的驱动能力要求就高了。1.2 寄生供电省一根线的关键设计单总线还有一个很“抠”的设计叫寄生供电。传统传感器需要 VCC 和 GND 两根供电线单总线的从设备却可以只靠数据线在空闲时的高电平来“偷电”。数据线为高时从设备内部通过一个二极管给电容充电数据线被拉低时就靠电容里存储的电量维持工作。这个设计在 DS18B20 这类低功耗器件上非常实用三根线变成两根接头少一路防呆压力也小。但寄生供电并不是没有代价的——温度转换这类大电流操作期间总线必须保持高电平否则从设备会瞬间掉电复位。所以在做温度转换时主机需要启动强上拉直接把数据线拉到高电平并保持几百毫秒转换完成后再恢复普通上拉模式。我建议新手在做实验时优先使用外部供电也就是给 DS18B20 的 VDD 引脚接 3.3V 或 5V数据线只负责通信。因为寄生供电模式对时序和电源质量更敏感一旦代码里强上拉时间不够传感器表现就是读出 85℃ 或者干脆无应答排查起来非常折腾。等整个驱动调稳定了再考虑寄生供电来做两线方案。1.3 为什么单总线能带几十个设备器件挂载与负载计算正常情况下单总线协议允许在一条总线上挂载几十个从设备因为每个设备都有独立的 64 位 ROM 码主机可以通过寻址命令来点名通信。但是“允许挂载那么多”和“能稳定挂载那么多”是两回事。限制主要来自两个方面。第一是总线电容每增加一个设备、每一米线缆都会增加总线上的等效电容电容太大直接拖慢上升沿破坏时序要求。第二是寄生供电时的电流预算如果所有设备都靠寄生供电总线上同时充电的电容会非常大主机那点上拉能力根本喂不饱。我在一个项目里试过单总线挂 24 个 DS18B20线缆总长约 15 米用的 4.7kΩ 上拉电阻结果扫描 ROM 时偶发找不到设备后来把上拉改成 2.2kΩ并且每个传感器的供电线独立走 5V问题才稳定解决。所以如果你要挂很多设备记住一个原则优先外部供电其次根据总线长度和挂载数量调整上拉电阻必要时用总线驱动器芯片做电平中继。2. 读懂三种时隙复位、写、读的时序细节与参数要求2.1 复位和存在脉冲主从设备之间的第一次握手单总线的每一次通信事务都是从复位脉冲开始的。主机把总线拉低至少 480μs然后释放上拉电阻把总线拉高。从设备检测到这个低电平脉冲后会等待 15μs 到 60μs然后主动把总线拉低 60μs 到 240μs这个响应就是存在脉冲。这一握手的意义在于主机需要确认总线上到底有没有设备在线。如果主机发出复位脉冲后在 480μs 内没有检测到存在脉冲说明总线上没有设备或者设备已经挂了。注意这里的时间参数是宽松但不随意的——复位脉冲太短从设备可能没识别太长则会占用总线时间影响后续时隙。实际代码里我习惯把复位低电平时间设为 600μs释放后再等 100μs 再开始读存在脉冲这样余量更大。存在脉冲的检测窗口也是一个容易踩坑的地方。有些代码在释放总线后立刻开始采样结果读到的是上拉电阻充电过程中的高电平错过了从设备拉低的时间窗口。正确做法是释放总线后等待约 30μs再持续采样总线状态直到捕捉到低电平或者超时。2.2 写时隙用脉宽来编码 0 和 1复位握手完成后主机就开始给从设备发送命令这就要用到写时隙。单总线的编码方式很有意思不是靠电平高低而是靠低电平持续的时间长短来区分 0 和 1。写 0 时隙主机需要拉低总线并保持 60μs 到 120μs然后释放。写 1 时隙主机只需要拉低总线 1μs 到 15μs然后释放让总线回到高电平。从设备在主机产生下降沿后的 15μs 到 60μs 窗口内采样总线状态如果采样到低电平就认为是 0采样到高电平就认为是 1。这就是为什么写时隙的核心在于控制拉低的时间而不是电平本身。我见过很多人写出的代码是直接GPIO_WriteLow然后延时但他们忽略了一个关键点两个相邻时隙之间必须保留至少 1μs 的恢复时间。如果上一个时隙的 0 拉低了 120μs下一个时隙立刻开始总线还没来得及被上拉电阻拉高就可能把下一个 1 误判成 0。写时隙还有一个容易忽视的细节主机在写 1 时拉低的那 1μs 到 15μs 非常短如果使用的是带延时的 GPIO 操作库这个延时可能被系统调度放大变成几十微秒那从设备就会把 1 判断成 0。所以底层驱动最好采用寄存器直接操作加精确的忙等待延时不要在中断频繁的环境里做单总线时序。2.3 读时隙主机为什么必须主动拉低总线单总线的读时隙是另一个容易让人困惑的地方。很多人会想既然是主机读从设备的数据那主机只要释放总线然后采样不就行了但实际上单总线的读操作也是由主机主动产生一个下降沿开始的。主机的标准读时隙是这样拉低总线至少 1μs然后释放总线。如果从设备要发送 0它会在主机释放总线后继续把总线拉低并保持到读时隙结束如果从设备要发送 1它就不做任何动作让上拉电阻把总线拉回高。主机在下降沿产生后的 15μs 窗口内采样总线电平就能读到从设备发送的比特。这个机制跟写时隙共享同一个基本框架区别在于写时隙的拉低时间决定数据读时隙的拉低时间只是提供一个同步时钟真正的数据由从设备在主机释放后的行为决定。在实际实现中读时隙的采样点非常关键我通常把采样点设在拉低释放后 10μs 左右既能避开下降沿噪声又能在从设备释放总线之前完成采样。读时隙还有一个常见问题主机在进行连续多位读取时如果每读一位之后没有给总线足够的恢复时间电容上残留的低电平会导致误读。每个读时隙之间至少留出 5μs 以上的间隔实际驱动里我会把一位的读时隙总周期控制在 70μs 左右读取一位后延时 20μs 再开始下一位。2.4 时序参数的常见坑严格窗口与容差设计把上面的时序放在一起看你会发现单总线的每个时间参数其实都有比较大的容差范围。比如写 0 时隙允许 60μs 到 120μs写 1 时隙允许 1μs 到 15μs。但真正严格的是采样点主机释放总线后的 15μs 左右这个采样窗口直接决定读到的比特是否正确。我在调 DS18B20 驱动时遇到过最典型的问题就是“时隙总长度足够但高低电平的划分点不对”。很多人写代码时只关注拉低延时却没有确保每个时隙的总周期一致。如果写 1 时隙被一些额外的函数调用拖到 30μs写 0 时隙又刚好卡在 60μs两者之间只差 30μs边缘窗口太小很容易受干扰。所以我的习惯是让写 1 和写 0 的时隙总周期都保持在大约 70μs 到 100μs让数据位之间有一个稳定的边界。另外要注意的是不同的从设备对时序的敏感度不一样。DS18B20 算是对时序容忍度比较好的但有些型号的 EEPROM 和温度传感器对时序更挑剔。所以在正式项目里最好用逻辑分析仪把波形抓下来核对每个时隙的实际长度不要只依赖代码里写的延时数值。延时的实际时间跟主频、编译器优化级别都有关系不同环境下同样的延时函数可能差出好几微秒。3. ROM 寻址原理64 位序列号如何决定“叫谁谁答应”3.1 64 位 ROM 码的组成家族码、序列号、CRC单总线之所以能在一根线上挂多个设备靠的就是每个设备出厂时固化的 64 位 ROM 码。这 64 位分成三部分最低 8 位是家族码比如 DS18B20 的家族码是 0x28DS2431 EEPROM 的家族码是 0x2D中间 48 位是设备唯一的序列号最高 8 位是前 56 位的 CRC 校验码。主机在访问任何一个设备之前都要先通过 ROM 相关命令来确定当前要对哪个设备通信。由于每颗芯片的 48 位序列号都是激光刻录的全球唯一所以理论上你可以在同一条总线上识别出任意数量的设备只要它们的 ROM 码不冲突。CRC 校验码的存在也很关键它让主机在读完全部 64 位之后可以自行计算校验确认读到的序列号没有被干扰。实际项目中如果扫描设备时偶尔扫到错误的 ROM 码多半就是通信过程中某一位被干扰翻转而你没有做 CRC 校验导致后续寻址全部失效。所以任何严谨的单总线驱动读取 ROM 码后都应该做 CRC 校验再做后续操作。3.2 匹配 ROM、跳过 ROM、搜索 ROM 的区别单总线协议定义了若干 ROM 命令最常用的有三个0x33 读 ROM、0x55 匹配 ROM、0xCC 跳过 ROM。这里的关键是理解它们在什么场景下使用。读 ROM 命令只能用在总线上只有一个从设备时直接从设备读取 64 位 ROM 码。如果总线上挂了多个设备它们会同时向总线发送各自的位流结果就是数据冲突读到的内容毫无意义。跳过 ROM 命令则是明确的“我不关心你是谁所有设备都听命令”适合总线上确实只有一个设备或者你想给所有设备广播一个命令的场景。匹配 ROM 命令是每次通信的核心主机先发送 0x55然后紧接着发送一个 64 位的 ROM 码总线上每个设备都会把这个 64 位数据跟自己的 ROM 码逐位比较只有完全匹配的设备才会响应后续的命令其他设备则保持静默。这个过程就像点名一样喊到名字的人应答没喊到的一动不动。3.3 搜索 ROM 算法的核心思想按位二分法定位设备搜索 ROM 命令0xF0是最有意思的一条命令。它的作用是在总线上有多个设备的情况下让主机逐个找出所有设备的 ROM 码。很多人在这一步直接卡住因为不知道该怎样在同一根线上区分多个同时应答的设备。其实搜索 ROM 的底层原理我习惯把它理解成“每一位都进行两次读”。第一次读总线上的所有设备会把自己该位的 ROM 码值放到总线上第二次读所有设备会把自己该位的反码放到总线上。如果总线上所有设备在这一位都是 0那么第一次读到 0、第二次读到 1如果所有设备在这一位都是 1那么第一次读到 1、第二次读到 0如果这一位上既有 0 又有 1那么两次读到的都是 0说明这里出现了一个分叉点。主机记录下所有分叉点的位置然后用类似深度优先搜索的方式在每次分叉时选择一条分支继续深入回溯时再走另一条分支。每完整走完一轮就能得到一个设备的完整 ROM 码。这个过程本质上就是在 64 位寻址空间里做二分遍历唯一的代价是需要多读一位反码来识别分叉位置。我最初实现搜索 ROM 时逻辑绕了半天后来想通一个诀窍把搜索过程想象成走迷宫ROM 码的每一位就是迷宫里的一个岔路口分叉点就是“这一位既可以走 0 也可以走 1”的位置。你只需要维护一个数组记录每个分叉点是走 0 还是走 1以及一个栈用来回溯就能不重不漏地找完所有设备。4. 手写单总线驱动从 GPIO 模拟到完整通信流程4.1 环境准备与引脚选择硬件上的几个注意点在动手写驱动之前先把硬件环境准备好。我通常用 STM32 或者 ESP32 这类单片机来演示因为 GPIO 翻转速度足够快而且可以方便地接逻辑分析仪调试。选择一个支持开漏输出的引脚外部接一个 4.7kΩ 上拉电阻到 3.3V 或 5V。这里有个容易忽略的点单总线的电平标准并不一定是 5V。DS18B20 的数据手册支持 3.0V 到 5.5V 供电所以 3.3V 逻辑也是可以工作的。不过实际使用中3.3V 供电时总线的噪声容限更低如果线缆较长或者干扰较大建议还是用 5V 供电并配上合适的电平转换。ESP32 的 GPIO 是 3.3V 电压域直接驱动 DS18B20 没问题但如果你用的是 5V 单片机的单向电平转换器要特别注意转换器的方向别把从设备的存在脉冲吃掉。接线方式上数据线尽量短一些不要跟电机驱动线、电源线绑在一起走线。单总线对干扰的敏感程度不算低长线走线时我吃过不少亏后面会专门讲排查方法。4.2 关键代码实现时隙、复位、读写字节下面我给出一份基于 GPIO 模拟的单总线驱动核心代码语言用 C方便移植到各种平台。重点看时序的延时方式和采样点的位置。#define BUS_PIN GPIO_PIN_5 #define BUS_PORT GPIOB #define BUS_HIGH() HAL_GPIO_WritePin(BUS_PORT, BUS_PIN, GPIO_PIN_SET) #define BUS_LOW() HAL_GPIO_WritePin(BUS_PORT, BUS_PIN, GPIO_PIN_RESET) #define BUS_READ() HAL_GPIO_ReadPin(BUS_PORT, BUS_PIN) #define DELAY_US(us) delay_us(us) void onewire_reset(void) { BUS_LOW(); DELAY_US(600); // 复位脉冲拉低至少 480us这里放 600us BUS_HIGH(); DELAY_US(60); // 等待从设备响应 } int onewire_read_bit(void) { int bit; BUS_LOW(); DELAY_US(5); // 读时隙起始拉低 5us BUS_HIGH(); DELAY_US(8); // 释放后等约 8us在 15us 窗口内采样 bit BUS_READ(); DELAY_US(50); // 读完一位后等待时隙结束 return bit; } void onewire_write_bit(int bit) { if (bit) { BUS_LOW(); DELAY_US(5); // 写 1只拉低很小时间 BUS_HIGH(); DELAY_US(65); // 时隙剩余时间 } else { BUS_LOW(); DELAY_US(65); // 写 0保持拉低足够时间 BUS_HIGH(); DELAY_US(5); } } void onewire_write_byte(unsigned char data) { for (int i 0; i 8; i) { onewire_write_bit((data i) 0x01); } } unsigned char onewire_read_byte(void) { unsigned char data 0; for (int i 0; i 8; i) { if (onewire_read_bit()) { data | (0x01 i); } } return data; }这份代码的核心就是保证每个时隙的总体长度均匀。读位的时候采样点放在拉低释放后的 8μs 左右这个位置在 15μs 窗口内而且避开了紧贴下降沿的噪声。写位的时候写 1 和写 0 的总周期都在 70μs 上下既满足时序要求又让相邻位之间的边界稳定。延时函数本身也要注意。普通的HAL_Delay是以毫秒为单位的完全不能用在这种微秒级时序里。STM32 上我一般用 DWT 或者定时器来做微秒延时ESP32 上可以用ets_delay_us或者esp_rom_delay_us。在 Linux 用户空间做单总线模拟则要格外小心系统调度的不确定性会让时序完全不可控不建议这么做。4.3 完整的温度采集流程从复位到 CRC 校验有了上面的底层函数完整的 DS18B20 温度采集流程就很好理解了。标准的步骤是复位、写跳过 ROM 命令 0xCC、写温度转换命令 0x44、等待转换完成、再复位、写跳过 ROM 命令、写读暂存器命令 0xBE、连续读取 9 个字节。void ds18b20_start_convert(void) { onewire_reset(); onewire_write_byte(0xCC); // 跳过 ROM假定总线上只有一个设备 onewire_write_byte(0x44); // 启动温度转换 } float ds18b20_read_temperature(void) { unsigned char data[9]; int temp_raw; onewire_reset(); onewire_write_byte(0xCC); // 跳过 ROM onewire_write_byte(0xBE); // 读暂存器 for (int i 0; i 9; i) { data[i] onewire_read_byte(); } temp_raw data[0] | (data[1] 8); return (float)temp_raw * 0.0625f; }注意第 9 个字节就是 CRC你可以用查表法或者逐位法对前 8 个字节算一遍 CRC和读到的第 9 个字节比对不一致就说明这次传输出错应该丢弃数据重读。DS18B20 的 CRC 生成多项式是x^8 x^5 x^4 1网上有很多现成实现强烈建议在正式项目里加上这一步。温度转换命令之后如果是寄生供电模式要把数据线完全拉高并保持至少 750ms让传感器内部完成 ADC 转换。如果是外部供电只要简单延时 750ms 即可。转换完成后传感器会把自己内部的状态标志位置位但读取这个标志位需要额外的读命令所以我一般直接固定延时 750ms 或者根据分辨率调整延时省去状态查询的复杂度。5. 单总线项目实战中的高频问题与排查实录5.1 读不到设备优先检查时序和上拉电阻单总线最常见的故障就是设备无应答。排查时我有一套固定的顺序先看硬件再看时序最后看逻辑。硬件上先量数据线空闲电平正常应该是高电平。如果量出来是 0V检查上拉电阻是不是虚焊、接错或者单片机引脚被复用成其他功能了。如果在数据线上量到 1.2V 左右的电压多半是总线被某个设备拉住了常见原因是设备进入了奇怪的闩锁状态需要断电重新上电。时序上用逻辑分析仪抓复位波形确认复位低电平时间是否足够存在脉冲是否出现。如果存在脉冲太窄或者没有先检查代码里的延时是否真的达到微秒级很多开发板的延时函数在优化后会变成几毫秒直接把通信搞死。5.2 数据偶发错误长线传输和寄生供电的干扰如果设备能读到但数据偶尔错误问题通常出在信号完整性和供电上。长线传输时总线上的分布电容会让上升沿变缓导致从设备采样到错误电平。我的经验是单总线超过 3 米后上拉电阻就应该从 4.7kΩ 降到 2.2kΩ如果超过 10 米最好在总线末端加一个 74HC04 之类的缓冲器做信号整形。我自己在一个项目里线缆长度大概 20 米用 4.7kΩ 上拉时每读 100 次大概有 3 到 5 次 CRC 错误换成 1kΩ 上拉后错误率降到千分之一以下。这个优化很简单但效果好得惊人。寄生供电带来的问题则是另一套思路。如果设备采用寄生供电模式温度转换期间主机必须输出强上拉否则传感器会中途掉电转换结果永远是 85℃ 或者干脆无应答。最简单的解决办法就是改成外部供电数据线只负责通信这是我在正式产品里最推荐的方案。5.3 多设备挂载序列号冲突与总线长度控制挂载多个设备时最典型的故障是搜索 ROM 时设备数量不对或者同一行扫描出重复的序列号。出现这个现象首先要怀疑的是某些传感器的 ROM 码在读取时发生了错误导致搜出来的序列号是错的后续又把错误序列号当作真实设备来访问。解决办法是扫描后立即做 CRC 校验跳过校验失败的序列号。另外如果总线上有设备损坏它可能会让总线一直处于拉低状态导致整个总线瘫痪。排查时需要逐段断开设备确定是哪颗传感器把总线拖死了。多设备的总线长度控制也很关键。因为所有设备共享一根数据线线缆越长回波和反射越严重。如果项目要求传感器分布在不同位置我更建议把传感器分成几组每组一条总线分别接到单片机的不同引脚而不是把所有传感器都串在一条超长总线上。5.4 调试利器用逻辑分析仪看波形最后强烈建议所有做单总线开发的朋友手边备一台逻辑分析仪。国产的十几块钱逻辑分析器配合开源软件就能抓取几十兆采样率的波形对单总线这种微秒级时序来说绰绰有余。抓波形的方法很简单把探针夹在数据线上地线接 GND然后运行你的代码抓一段包含复位、命令、数据读写的完整波形。通过波形可以直观看到每个时隙的长度是否超标、采样点位置是否正确、上拉电阻是否合适。很多代码里调了半天延时都不稳定的问题拿到波形图上一眼就能看出来。我见过不少人凭感觉调参数调一整天都调不好结果用逻辑分析仪一抓发现是写 1 时隙的拉低时间超过了 20μs从设备已经把 1 误判成 0 了。这种问题不看波形基本没法定位。6. 写在最后我对单总线协议的几点使用心得单总线协议看起来简单实际项目里能挖的坑不少但也有规律可循。这么多年用下来我最深的体会是时序类协议的第一原则是稳定而不是最快。单总线的通信速率本身也就几十 kbps把每个时隙做得宽裕一些把采样点放在窗口中间远比把时序压到极限更能保证长期可靠性。尤其是产品要过温湿度循环、振动测试的时候时序余量就是你最后的救命稻草。另外单总线的官方规范文档里有大量时序参数表格读起来枯燥但排查问题时非常管用。我建议把 15μs 采样窗口、480μs 复位脉冲、60μs 写 0 时隙这些核心数字打印出来贴在工位上调代码时随时对照能省不少时间。最后分享一个小技巧如果你的系统里有多条单总线每条线上设备不多驱动代码可以只维护一套底层时序函数把总线号和引脚作为参数传递。这样单片机上有几个 GPIO 就能接几路单总线组网灵活性提高不少。我自己做环境监测系统时就是用 4 路单总线分别接不同区域的传感器每路串 6 到 8 个节点比全部挂一条总线省了无数排查烦恼。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。