尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Onewire极简总线协议深度解析:从硬件时序到驱动实战

Onewire极简总线协议深度解析:从硬件时序到驱动实战 1. 为什么叫“极简王者”Onewire的整体设计思路拆解做嵌入式通信这么多年常用协议基本都用过一轮。I2C要两根线SPI要三根起步UART虽然只要两根但得双方约定好波特率Modbus这类工业总线换个从机还得拨地址拨码盘。唯独Onewire一根线就把供电、时钟、数据全干了。第一次接触DS18B20的时候说实话我愣了半天这玩意儿居然只靠一根线就能汇报温度还能量产级稳定。Onewire的“极简”不是表面看上去少接一根线那么简单。它的核心设计哲学是用最少的物理介质实现最大化的信息传递。这一根线上同时承载了三样东西——电源、地参考、双向数据。严格来说主机端只需要接一个IO口外加一颗4.7kΩ左右的上拉电阻从机端就能获得足够的工作电流和数据通信能力。比起I2C的SDA和SCL省掉的这根线在板对板连接器、防爆探头、旋转关节这些场景里直接决定了一个产品能不能做出来。1.1 硬件资源占用对比一线到底省在哪拿实际项目算一笔账就清楚了。一个要采集8路温度的冷链监测设备如果用I2C接8个传感器光从机地址就要折腾半天A0/A1/A2地址引脚跳线跳得让人怀疑人生。如果用SPI每个从机得单独拉一根CS8个传感器就是8根片选线再加上SCK和MISO/MOSI一个10针连接器都不够用。换成Onewire8个DS18B20全部并联在一条总线上主机只需要1个GPIO加1颗电阻。地址每颗芯片出厂时激光烧录好了64位唯一ID不用跳线、不用设地址程序里做一次总线枚举就能把8颗芯片的ROM全部读出来。连接器从10针缩到3针线缆从8根缩到1根这在布线空间寸土寸金的温控器面板里省出来的位置都能多放两个按键了。1.2 极简背后的技术取舍有人会问极简是不是意味着功能弱恰恰相反Onewire把简单和强大结合得很巧妙。它之所以能用一根线工作是因为传输方式用了开漏加外部上拉的结构类似I2C但比I2C的时序更“野”——完全靠主机精确控制高低电平的持续时间来读写字位没有任何硬件时钟同步机制。DS18B20内部其实有一颗小电容靠寄生供电方式从数据线上吸收电荷维持工作所以严格来说这根线连“供电”都省了。这种设计的代价是时序要求极其严格15μs、60μs级别的窗口差一点就可能读回全0xFF。嵌入式圈子里一直流传着一句话“I2C调不通多半是地址错了Onewire调不通多半是延时不对。”这句话虽然糙但真实反映了这个协议的脾气。2. Onewire硬件基础一根线背后藏着哪些电路细节想要用好Onewire先从硬件上把它吃透。很多初学者第一步就栽在硬件上——以为随便找个IO口接上传感器就能用结果要么读不出数据要么数据乱跳。我踩过的坑和总结的经验这部分一次说清楚。2.1 总线结构开漏、上拉和寄生电容从电路结构上看Onewire总线就是一个典型的开漏结构。主机和所有从机的数据引脚内部都是开漏输出外部统一接一颗上拉电阻到VCC然后所有器件的数据引脚直接挂在这根线上。平时总线被上拉电阻拉到高电平谁要发数据就把这根线拉低靠“拉低释放”、“再拉低再释放”的组合来传递0和1。上拉电阻的取值有讲究。官方资料写的是4.7kΩ但实际要结合线上的寄生电容和从机数量来调整。线缆长了、器件多了寄生电容就大上拉电阻若还是4.7kΩ上升沿会变缓导致读时序时主机采样到错误的电平。我做过长线缆的多点测温项目30米屏蔽双绞线挂了12颗DS18B204.7kΩ的电阻上升沿肉眼可见地发圆后来换成2.2kΩ配合上拉电源加强波形才算干净利落。注意上拉电阻不是越小越好。电阻太小从机拉低时电流过大可能损坏从机IO口也可能让总线在从机不该操作时被钳在低电平。1kΩ到4.7kΩ之间的取值都要结合具体场景拿示波器看了波形再定别迷信“标准值”。2.2 寄生供电如何做到不接电源也能工作Onewire支持两种供电方式外部供电和寄生供电。外部供电是VDD和GND都接上数据线只负责通信这是最稳妥的方式。寄生供电则是从机完全靠数据线上的电荷工作——数据线为高电平时内部电容充电数据线为低电平时电容放电维持芯片工作。芯片内部的掉电保持时间大概在几微秒到几十微秒这个量级所以时序上要求主机在总线低电平期间“省着点用”尤其在做温度转换这类耗电操作时主机要在总线上发起一个强上拉信号持续几百微秒以上给芯片充电。寄生供电最大的价值在于传感器只需要两根线。比如做旋转机械上的测温那种地方根本没法走三根线两根线会方便很多。但代价是时序窗口更窄抗干扰能力也变差。我的建议是能外部供电就外部供电只有在物理布线实在不允许时才用寄生模式且必须做好强上拉电路设计否则温度转换时电压被拉低芯片会直接复位。2.3 需要注意的电气细节清单以下是几个容易踩雷的硬件细节是我这几年实际项目中反复确认过的IO口模式选择主机IO口必须支持开漏输出模式或者普通推挽输出配合外部上拉也可以但要注意配置为浮空输入时读电平配置为开漏输出时写电平。如果单片机IO不支持开漏推挽模式也能用但必须在输出高电平时切换为输入模式由外部上拉拉高总线避免推挽强驱动造成的总线冲突。线缆选型短距离1米以内用普通杜邦线就能跑长距离建议用屏蔽双绞线屏蔽层单端接地。Onewire对信号完整性还是比较敏感的劣质线缆会让波形严重畸变。ESD保护室外或工业现场的Onewire接口一定要加TVS管或ESD保护二极管否则雷电感应或静电就能把传感器甚至主控的IO口干掉。我见过一个客户整批温控器返修拆开发现全是IO口损坏就是因为省了这颗TVS。3. 时序协议核心波形就是Onewire的通行语言如果说硬件是骨架时序就是Onewire的灵魂。这个协议没有时钟线所有通信完全靠主机对单根线的电平变化时间进行精确控制。理解Onewire的时序是整个协议学习的核心难点也是“极简”设计中最需要下功夫的地方。3.1 复位脉冲与存在脉冲总线的握手仪式一次完整的Onewire通信第一步永远是复位。主机将总线拉低至少480μs然后释放总线。释放后如果总线上有从机从机会在15μs到60μs之间主动把总线拉低60μs到240μs这就是存在脉冲。主机通过检测这个脉冲就能知道“总线上有设备”。主机释放总线之后总线会通过上拉电阻恢复高电平从机检测到这个上升沿之后开始计时在15μs到60μs窗口内把总线拉低。我调程序时习惯用示波器抓这个波形——如果看不到从机拉低的那一下十有八九是上拉电阻没接、从机供电有问题或者复位低电平时间不够。3.2 写时序的两种电平写1和写0的区别写入一个位主机要拉低总线然后根据要写的数据决定释放时间的长短。写0时主机持续拉低整个时隙至少60μs写1时主机只拉低1μs到15μs就释放剩余时间由外部上拉把总线拉高。从机采样约定是在时隙开始后的15μs到60μs之间读取总线电平。所以写0时只要主机在整个采样窗口内保持低电平从机就认为是0写1时主机拉低时间必须足够短确保从机采样时总线已经恢复高电平。我之前出过的bug是在写1时用了30μs的拉低时间结果从机采样时总线还在低电平写入的数据全变成了0。3.3 读时序的细节为什么不能只看高电平读一个位主机同样要先把总线拉低至少1μs然后释放。从机接到这个信号后如果要发0就会把总线拉低继续保持如果要发1则什么都不做总线被上拉电阻维持在高电平。主机在拉低后约15μs处采样一次总线电平就能得到从机发回的位数据。关键细节在于主机在采样完成后要保证在时隙结束前释放总线并等待直到整个60μs时隙结束后才能开始下一个位操作。如果主机采样完马上又拉低总线去读下一位就会让从机来不及释放总线导致数据错乱。这个问题非常隐蔽程序上稍有疏忽读回来的数据就是全0xFF或间歇性跳变。3.4 关键时序参数速查表为了方便调试我整理了一份常用时序参数表按最小值和推荐值给出基本覆盖了DS18B20和绝大多数Onewire从机参数说明最小值推荐值最大值备注复位低电平时间480μs600μs—太短从机响应不了复位后等待时间—60μs240μs从机存在脉冲窗口写0时隙低电平时间60μs70μs120μs从机采样窗口在15~60μs写1时隙拉低时间1μs5μs15μs超过15μs会被判定为0读时隙拉低时间1μs3μs15μs拉低后切换输入模式采样位时隙总时长60μs70μs120μs相邻位之间需要留间隔转换温度时间—750ms—DS18B20在12位分辨率下注意上面的参数不是绝对的不同从机芯片可能有差异尤其是最大最小值。正规芯片的数据手册里都有完整的时序图我在新项目里拿到一颗不熟悉的Onewire从机时第一件事永远是打开手册找到时序参数表逐项核对千万别拿DS18B20的参数硬套所有芯片。4. 通信流程与ROM命令从枚举到数据交换的完整链路4.1 总线枚举如何用一次扫描找出所有设备Onewire总线上可以挂多个从机每个从机都有自己独立的64位ROM码。主机要跟某一颗从机通信必须先知道它的ROM码。总线枚举的基础算法是“二分法遍历二叉树”——ROM码的每一位就是树上的一个分支主机通过三个层次的命令组合逐位探测总线上的设备。具体流程是这样的主机先发复位脉冲然后发送搜索命令0xF0接着逐位进行读写——每读一位ROM码再读一位它的反码。如果总线上只有一个设备主机读到的这两位互为反码直接写入就行。如果总线上有多个设备且某一位出现了既有0又有1的情况主机读到两次0就知道这一位存在分叉需要根据上一次搜索的方向记录来决定选择哪条分支继续往下走。这个算法听起来不难但实际写起来有个老生常谈的坑罗马不是一天建成的搜索算法也不是一次就能摸清所有设备的。我在项目里一般用现成的“OWFirst OWNext”循环写法原理上就是记录最终搜索路径当路径出现循环时终止。对于初次接触的人来说直接调用成熟库函数比自己从头造轮子更稳妥。4.2 经典ROM操作命令解读Onewire的ROM命令一共就几条非常简单明了0x33读ROM只能用于总线上只有一颗从机的情形。直接读出这颗从机的64位ROM码省了枚举的繁琐。0x55匹配ROM主机发送0x55后紧接着发送64位ROM码只有ROM码匹配的从机才会响应后续的功能命令。0xF0搜索ROM上面说过的枚举命令用于识别总线上所有的从机。0xCC跳过ROM不指定地址让所有从机同时响应后续的功能命令。这个命令在总线上只有一颗设备时非常好用能省下64位ROM的传输时间。在只有单颗从机的绝大多数应用里我都会直接用0xCC跳过ROM寻址把宝贵的通信时间留给真正的数据传输。举个例子每秒钟读一次温度用0xCC跳过ROM后一轮完整操作只发送几个字节如果用0x55匹配ROM光ROM码就要多传8个字节速度差距肉眼可见。4.3 一次完整的数据交换实操示例以DS18B20为例我想读一颗传感器的温度完整流程如下主机发送复位脉冲检测从机的存在脉冲。发送0xCC跳过ROM寻址或0x55匹配指定ROM。发送0x44启动温度转换这是让传感器内部做一次ADC转换。如果是寄生供电模式主机在这之后要把总线拉高并保持750ms以上给传感器充电。再次发送复位脉冲。发送0xCC跳过ROM这次是为了通知传感器“我要读你的寄存器了”。发送0xBE读取暂存器传感器会连续输出9个字节的数据前两个字节就是12位温度值。我实测过在12位分辨率下最快能做的轮询周期大约是1秒因为温度转换就要750ms。如果只是做报警检测不需要这么高的分辨率可以设置成9位分辨率转换时间只要94ms轮询频率能提升不少这个取舍在实时性要求比较高的项目里很关键。5. 驱动实现与实操过程从零手写一个Onewire驱动5.1 延时实现方式的选择Onewire的时序要求微秒级精度这就涉及到延时函数的实现方式。很多人用delay_ms()做延时但Onewire需要的是delay_us()级别的精细控制。不同平台实现微秒延时有不同的讲究单片机裸机用定时器是最稳的方案。初始化一个微秒级定时器在需要延时时读取计数寄存器的值用轮询方式等待。直接空循环的延时受编译器优化影响很大我在GCC下吃过亏——开了-O2优化后原本精确的循环直接被跳过了时序全乱。Linux用户态这是个坑比较多的场景。用户态进程的调度延迟是不确定的普通usleep()可能会睡过头也可能提前醒来。我建议的做法是配合gpiod库操作GPIO再用clock_gettime()做高精度忙等待并且设置实时调度优先级跑起来才能稳定。RTOS环境在临界区里做时序操作进临界区前要把任务调度暂停否则一个高优先级任务抢占打断了时序窗口通信就废了。5.2 核心代码框架复位、读位、写位下面给一套精简但能跑的C语言驱动框架基于标准GPIO操作抽象方便移植// 宏定义IO操作具体实现依平台而定 #define OW_DIR_OUT() // 配置IO为输出模式 #define OW_DIR_IN() // 配置IO为输入模式 #define OW_SET_LOW() // IO输出低电平 #define OW_SET_HIGH() // IO输出高电平 #define OW_READ() // 读取IO电平返回0或1 // 微秒级忙等待实现需依平台修改 void ow_delay_us(unsigned int us) { // 裸机下可用定时器或校准后的空循环 } // 复位并从机是否存在 uint8_t ow_reset(void) { uint8_t presence; OW_DIR_OUT(); OW_SET_LOW(); // 拉低复位脉冲 ow_delay_us(600); // 至少480us OW_SET_HIGH(); // 释放总线 OW_DIR_IN(); // 切换输入等待从机拉低 ow_delay_us(20); // 等待存在脉冲窗口开始 presence OW_READ(); // 读电平0表示有从机 ow_delay_us(60); // 等待整个存在脉冲窗口结束 return (presence 0) ? 1 : 0; } // 写入一个位 void ow_write_bit(uint8_t bit) { OW_DIR_OUT(); OW_SET_LOW(); if (bit) { ow_delay_us(5); // 写1短拉低后释放 OW_SET_HIGH(); ow_delay_us(65); } else { ow_delay_us(65); // 写0全程拉低 OW_SET_HIGH(); ow_delay_us(5); } } // 读取一个位 uint8_t ow_read_bit(void) { uint8_t bit; OW_DIR_OUT(); OW_SET_LOW(); ow_delay_us(3); // 起始拉低脉冲 OW_SET_HIGH(); OW_DIR_IN(); ow_delay_us(10); // 等从机驱动总线 bit OW_READ(); // 采样 ow_delay_us(50); // 等待时隙结束 return bit; }5.3 字节级封装与校验处理有了上面的位操作再往上封装字节读写就很自然了。写一个字节就是循环8次从最低位开始依次写入读一个字节则是循环8次读取并移位累加。这里我特别强调一点读字节时高位和低位的顺序不要搞反。DS18B20的温度寄存器是先低字节后高字节如果读出来顺序搞错温度值就直接翻车了。代码里建议对读回的9字节暂存器数据做一次CRC校验。DS18B20的CRC多项式是X^8X^5X^41即0x8C最后一个字节就是前面8个字节的CRC校验值。我在实际项目里就把CRC校验写进驱动读数据时先校验再使用这样能过滤掉绝大多数由干扰引起的错误读数。5.4 稳定性设计重试机制与超时保护实际嵌入式环境中Onewire总线和任何通信一样都可能受到干扰。一个可靠驱动必须有重试机制。我的做法是读温度时连续尝试3次每次失败后延时10ms再试3次都失败才判定传感器故障。这个策略能容忍偶发的毛刺干扰又不会在传感器真正离线时无限卡死。更关键的是超时保护。在ow_reset()里等待存在脉冲时不能无限等待下去要有超时退出在ow_read_bit()里也不能因为总线异常就一直死循环。我给每个通信阶段都加了超时判断超时立刻返回错误码。没有超时保护的Onewire驱动一旦传感器热插拔或短路整个系统就可能卡死在读取函数里。6. 典型应用场景与示波器验证从理论到实战6.1 官方生态与典型应用Onewire最典型、最普及的用法是环境温度采集DS18B20几乎成了这个协议的代名词。除此之外还有各种Onewire接口的湿度传感器、存储芯片比如DS2431用来存产品序列号和校准参数、电池电量监测芯片等。有一些场景我觉得特别适合Onewire多点温度测量冷链车、冷库、粮仓这类需要均匀分布多个测温点的场景。几十颗DS18B20并联在一根总线上主机轮询读取走线成本极低。防爆/本安环境一根线加一根地线比多根信号线更容易做本安设计能量泄漏路径少本质安全的认证也更好过。旋转部件监测像电机轴承温度线要穿过旋转关节线数越少越好。两根线的寄生供电方案能让传感器做得非常小。6.2 实际项目中的配置与参数选择我做过一个比较典型的项目冷链运输记录仪8路DS18B20测温主控是STM32传感器通过一个4芯航空插头引出实际只用2芯DQ和GND。这里我详细说说同步项目中比较关键的参数选择上拉电阻选的是3.3kΩ因为传感器线长5米寄生电容大约2nF3.3kΩ在上升沿速度和驱动能力之间取了个平衡。主机IO口是开漏模式外部上拉到3.3V。温度转换分辨率设成12位但轮询周期拉到1秒每颗传感器750ms转换8颗串行读取。如果你要更高频的采样就得考虑在转换期间穿插别的任务或者把分辨率降一档。每路传感器的ROM码在出厂前烧录时记下来写入设备配置区运行时直接按ROM匹配读取不做枚举这样启动速度和可靠性都更高。6.3 示波器波形真实记录与判读要点调试Onewire示波器是必备工具。我每次排障第一步永远是抓波形用波形说话不猜。一帧完整的复位波形应该有明显的三段主机拉低的低电平约600μs、释放后的高电平、从机的存在脉冲拉低约120μs。如果看不到存在脉冲检查从机供电和接线如果存在脉冲很浅或者抖动重点检查上拉电阻和线缆。读写位波形主要看拉低时间和采样点的位置。我用双通道示波器通道1接DQ线通道2接一个调试IO代码里在采样点处翻转调试IO。这样能直观地看到采样点落在波形的哪个位置用来验证时序非常方便。7. 常见问题与排查技巧实录7.1 高频问题速查表这是我整理的一份Onewire常见问题排查表基本能覆盖90%以上应用会遇到的问题现象可能原因排查与解决办法读回数据全为0xFF从机未响应时序太快抓波形看存在脉冲把延时调大10%~20%读回温度偶尔跳变干扰或时序临界加CRC校验加强上拉缩短线缆检查电源纹波总线上挂多颗设备但只能读出一颗ROM搜索算法实现有误检查搜索命令0xF0的逻辑重点看分支路径回溯换一颗传感器就读取失败上拉电阻偏大或线缆过长降低上拉电阻检查新传感器的供电方式寄生供电下温度转换经常失败强上拉时间不够检查强上拉电路确保转换期间总线不被拉低长线缆时高速模式下错误率高信号反射和上升沿变缓线缆加终端电阻匹配降低通信速度用屏蔽线断电后再上电读不到传感器存在脉冲时序被初始化代码影响确保复位延时充足检查上电后IO默认状态7.2 独家经验三个最隐蔽的坑第一个坑是主机IO口的默认电平。很多单片机复位后IO口默认是高电平开漏外部上拉电阻又把总线拉高看起来没毛病。但如果IO口默认是推挽输出并输出低电平那么上电瞬间传感器就被强制拉低还没来得及初始化通信就已经进入异常状态。所以写驱动第一步是在复位前明确把IO口配置成正确的输出模式并释放总线。第二个坑是中断对时序的破坏。只要在62μs的位时隙中跑进一个中断服务函数整个时序窗口就完了读回来的数据基本报废。我之前在FreeRTOS上驱动DS18B20温控任务优先级调低之后读取数据就一直偶尔出错。后来把所有位操作放进临界区关闭调度和中断问题立刻消失。如果你用的是RTOS记得给Onewire操作加上临界区保护。第三个坑是“假寄生效”。有些DS18B20的兼容芯片标称支持寄生供电但实际转换期间需要的电流更大如果上拉电阻偏大且强上拉功率不足转换出来温度值就是50℃以上的虚高值。排查方法很简单把传感器改成外部供电如果温度立刻正常就是供电问题。所以在我批量选型时除了原厂DS18B20兼容芯片我都会做完整的供电压力测试不敢只看数据手册。7.3 长线缆抗干扰的高级经验如果传感器线缆超过20米常规的上拉电阻和布线方式就不够用了。我试过几种方案比较实用的是在从机端并联一个0.1μF的旁路电容到地给总线滤高频干扰主机端上拉电阻选1kΩ到2.2kΩ增强驱动能力如果通信速率要求不高把每次通信之间加20ms以上的间隔避免总线持续高频率动作导致发热和反射叠加。更强的方案是使用专门的Onewire总线驱动器比如DS2482或DS2484这类I2C转Onewire的桥接芯片。它们内部有精确的时序发生器不依赖主机软件延时而且驱动能力强专门为长线缆和复杂拓扑优化过。我之前做过一个80米线缆的项目主机用DS2482桥接芯片驱动20多个传感器比直接用GPIO模拟稳定得多。如果你的项目恰好是“单主机、长距离、多从机”的组合直接考虑方案级芯片别死磕GPIO模拟。根据我个人的经验Onewire这个协议说难也难说简单也简单。说简单是因为它的物理层就一根线一个电阻原理清楚、波形直观相比很多无线协议少了很多玄学说难是因为微秒级的时序窗口容不得半点马虎一个延时参数不对整条总线就罢工。如果你刚开始接触它建议先拿示波器把复位、读写位的波形看明白再动手写代码否则容易花很多时间在瞎猜上。等到你亲手把一颗DS18B20稳定跑起来再往总线上挂第二颗、第三颗你会慢慢体会到这种“一根线打天下”的设计确实有种简洁的美感。
返回列表