十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

单总线协议核心拆解:从物理层时序到ROM多设备寻址

单总线协议核心拆解:从物理层时序到ROM多设备寻址 做嵌入式这些年我接触过的通信方式不少但单总线协议1-Wire始终是很特别的一个。它不靠时钟线也不靠差分信号只靠一根数据线配合严格的时间窗口就能完成供电、通信和设备识别。DS18B20 温度传感器、iButton 身份钥匙、电池仓里的存储芯片背后都是这套逻辑。很多朋友第一次照着例程跑通了可一旦换线缆、换芯片、或者挂多个设备就开始各种莫名其妙的问题——十有八九都出在物理层的时序和 ROM 寻址上。下面我就把这根“单行线”从电平标准、上拉电阻、寄生供电一路拆到 64 位 ROM 码和搜索算法把核心逻辑掰开揉碎讲清楚。1. 单总线协议要解决的问题为什么一根线也能通信1.1 省线省到极致从 I2C、UART 到单总线的思路转变正常的通信方式都离不开“时钟”的概念。I2C 有独立的 SCL 时钟线主机和从机跟着时钟沿动作SPI 更夸张MOSI、MISO、SCLK、CS 四条线少一根都不行UART 虽然只有 TX、RX 两根但双方必须事先约定波特率一旦晶振偏差就出乱码。而单总线的思路完全不一样它把时钟信息“塞”进一根数据线的时间窗口里任何一次通信都是从主机主动拉低总线开始的从机靠检测这些低电平的宽度来判断“现在该听什么、该说什么”。这种设计带来的最大好处就是省 GPIO 和布线。对一个温控系统来说如果传感器分布在好几个位置用 I2C 至少要拉两根信号线再加电源和地用单总线只需要一根数据线加公共地传感器本身还能从数据线上取电。对消费类产品来说减少一根线就意味着降低一个连接器引脚、少一道线束成本这在量产阶段是非常实在的收益。单总线还有一个容易被忽视的优点器件可以做成“触点式”。iButton 就是一个典型的例子它的外壳就是一个金属触点人手拿着往读卡器上一碰通信和供电同时建立。这种形态在巡检打卡、身份识别、设备维护记录里非常好用普通协议很难做到这么简单的使用体验。1.2 它能做什么不能做什么从实际项目来看单总线最常见的应用有这么几类温度采集DS18B20、DS18S20 这类单总线温度传感器可以并联在同一根线上主机通过 ROM 寻址区分每颗传感器适合做多测点测温。身份识别iButtonDS1990、DS2401 这类只读序列号器件本质上就是一个出厂激光刻录的 64 位 ID适合做防伪、授权、设备身份绑定。存储与校准DS2431、DS2433 这类 EEPROM 和电池管理芯片可以存放校准系数、产品信息、电池电量和循环次数等数据。安全认证带认证引擎的芯片比如 DS2432可以在单总线上完成密钥校验防止耗材、配件被恶意克隆。不过单总线不是一个万能方案。它的速度非常低标准模式只有 15.4kbps超速模式Overdrive也就是 111.1kbps和 CAN、以太网这类完全不在一个量级。它的传输距离也不算远常规设计建议在几十米以内超过这个长度需要对上拉电阻、线缆和终端芯片做专门的优化。它还是半双工通信主机和从机不能同时收发。所以单总线最适合的场景是“信号量小、节点分散、成本敏感”的控制系统而不是大数据量的骨干网络。1.3 和 CAN 物理层放一起看才算真正看懂“物理层”的含义很多做总线开发的人会习惯性把“协议”和“物理层”混在一起但单总线这点体现得非常清楚。物理层只管电平、阻抗、时序这些物理特性不管数据怎么组织。比如 CAN 的物理层用的是 CANH 和 CANL 两根差分线靠显性位和隐性位表示 0 和 1总线两端还要接 120Ω 终端电阻来消除反射而单总线的物理层就是一根开漏数据线加一个上拉电阻0 和 1 靠高低电平表示。同样是总线物理层完全不同上层即便都叫“寻址”实现方式也天差地别。把单总线、I2C、UART、CAN 拉在一起对比一眼就能看出各自的定位项目单总线(1-Wire)I2CUARTCAN信号线DQ GNDSDA SCL GNDTX RX GNDCANH/CANL 差分时钟来源无靠时隙SCL 时钟线无靠波特率无靠位同步电平方式开漏 上拉电阻开漏 上拉电阻推挽/差分差分 终端电阻寻址方式64位ROM搜索7位/10位地址无报文ID仲裁常见速率15.4k/111.1kbps100k/400k/1M按波特率达1MbpsCAN FD更高典型距离几米到几十米板级板级/短距几百米成本极低低低中顺带回答一个常被问到的问题物理层能不能做加密对单总线、CAN 这类普通总线来说答案是很明确的不行。物理层只负责把 0 和 1 发正确它不区分数据内容更谈不上加密加密这件事必须放在上层协议里做。单总线器件要防克隆靠的不是隐藏总线时序而是使用带安全认证引擎的芯片在应用层完成密钥校验这一点思路一定要转过来。2. 物理层拆解电平、上拉电阻与寄生供电2.1 开漏结构与“总线空闲必须为高”在单总线的物理层里主机和所有从机的数据引脚都是开漏结构。所谓开漏就是引脚内部没有一个主动输出高电平的驱动级只能把引脚拉低或者把引脚“放开”。放开之后总线靠外部接在 VCC 和 DQ 之间的上拉电阻把电平拉回高。这个设计带来的好处是任何一方拉低总线总线就是低所有设备都放开总线才是高。多主机、多从机不会出现两个推挽输出互相打架烧管子的情况这也是单总线能安全并联几十颗器件的基础。所以“总线空闲必须为高”不是随便说说而是整个协议的物理前提。如果上拉电阻缺失总线就会一直飘在低电平或者浮空任何复位、时隙、数据都无法进行如果上拉电阻接错地方或者 VCC 电压不稳定后续解码全是错的。我见过不少“刚上电能读一接长线就失败”的案例最后查下来都是上拉阻值对不上实际线缆电容导致上升沿太慢从机采样窗口内还没越过高电平阈值。2.2 上拉电阻怎么选一个 RC 上升沿的计算实例单总线上拉电阻的典型推荐值是 4.7kΩ这也是 DS18B20 数据手册给的标准值。但在实际工程里这个值不是万能的选型核心要看总线的等效电容。数据线本身有寄生电容每一颗挂载器件也有输入电容二者叠加决定总线释放之后从低到高的上升速度。上升时间可以近似按 RC 一阶电路估算从 0V 上升到 90% VCC 大约需要 2.2 × R × C 的时间这个时间必须远小于从机判断高电平所用的时间窗口否则从机就会把本应的高电平误判成低电平。举个例子假设总线挂 10 颗 DS18B20每颗输入电容按 50pF 估算再加上 2 米线缆约 200pF总电容大约 700pF。用 4.7kΩ 上拉时时间常数 τ 4.7k × 700pF ≈ 3.3µs上升到 90% 大约要 7.2µs。标准模式下读时隙通常在释放后 15µs 内就要采样这个上升沿是来得及的。但如果换成 100 米长线线缆电容按 100pF/米算就有 10nFτ 直接变成 47µs上升 90% 要 100µs早就错过了采样窗口。这种情况要么降低上拉电阻到 2.2kΩ 甚至 1kΩ要么改用带更强驱动力的一线桥接芯片比如 DS2482/DS2480B。板内短线、少于 5 个器件10kΩ 可用但更推荐 4.7kΩ兼容性最好。5 到 20 个器件、线长几米4.7kΩ 是稳妥起点如果波形上升沿偏慢再换 2.2kΩ。长线、大负载、超速模式2.2kΩ 起步必要时加 1.5kΩ 超速专用上拉同时减小线缆电容。注意上拉电阻不是越小越好。阻值太低会让低电平时流过引脚的电流过大既增加功耗也可能超过器件的灌电流能力。实际调参时一定要用示波器看上升沿不要凭感觉换电阻。2.3 寄生供电的秘密数据线就是电源线单总线最让人“眼前一亮”的设计是寄生供电。许多单总线器件只有 DQ 和 GND 两个引脚比如最常见的 TO-92 封装 DS18B20VDD 引脚可以不接外部电源。原理说起来不复杂器件内部有一个整流二极管和储能电容总线为高电平时电流通过 DQ 给内部电容充电总线被拉低时器件靠电容里存的电维持工作。也就是说一根数据线既要传数据又要当电源线这就是“寄生供电”名字的由来。寄生供电的限制也来自这里。既然器件靠电容储能它就不能容忍长时间的低电平否则电容放完电就会复位或者掉电。所以数据手册会明确要求在温度转换、拷贝暂存器等大电流操作期间总线必须保持高电平而且主机要提供“强上拉”——所谓强上拉就是用一个小功率 MOS 管或三极管直接给 DQ 供 5V而不是继续用 4.7kΩ 上拉那么点电流。很多人照着外部供电的例程写代码温度转换期间让总线随便飘着结果 DS18B20 转换出来的温度永远是 85°C 或者乱码多半就是寄生供电下没有拉强上拉。判断一个器件到底是不是寄生供电单总线上有个命令叫 READ POWER SUPPLY0xB4。流程是复位后发这个功能命令再读一个时隙如果总线保持高电平说明器件有外部电源如果器件把总线拉低说明它依靠寄生供电。这样程序就能在运行时自动适配不用在硬件上猜。3. 时序详解时隙世界的微观规则3.1 复位脉冲和存在脉冲每次通信的“握手”单总线上所有通信的第一步都是复位。主机先把总线拉低至少 480µs然后释放。任何挂在总线上的从机在检测到这个足够长的低电平后会等待 15µs 到 60µs然后自己拉低总线 60µs 到 240µs这个由从机发出的低电平就叫“存在脉冲”。主机在释放总线之后的 60µs 到 240µs 窗口内采样如果读到低电平就知道总线上至少有一个从机在线可以开始后续命令。为什么复位低电平要 480µs 这么长因为单总线没有独立的唤醒信号从机可能处在各种低功耗状态必须给足时间让它们苏醒并做好应答准备。同时480µs 这个宽度也远远大于普通数据时隙的低电平宽度从机可以由此区分“这是复位”而不是“一个写 0 时隙”。我在调试时经常用逻辑分析仪先把复位波形拉出来看如果存在脉冲缺失或者位置不对后面所有命令都不可能有结果排查顺序永远是先复位后数据。这里还要注意主机采样点的选择。释放总线后如果采样太早从机的存在脉冲可能还没拉低采样太晚存在脉冲可能已经结束。稳妥的做法是在释放后 60µs 到 70µs 处采样这个位置处于从机应答窗口的中段容错空间最大。我自己实现的复位函数一般释放后延时 70µs 再读一次引脚然后继续等待 410µs让整个复位窗口自然走完。3.2 写时隙写 1 和写 0 的本质区别单总线的数据读写不是靠电平高低而是靠一个固定长度时间窗里的低电平持续时间来区分 0 和 1这个时间窗就叫时隙。每个时隙最短 60µs主机必须保证在时隙起始时拉低总线。写 1 时隙的要求很苛刻主机把总线拉低但只能维持 1µs 到 15µs然后立刻释放让上拉电阻把总线拉回高剩下的时间里总线保持高电平。从机的采样点大概在时隙开始后的 30µs 到 60µs 之间它看到的是“总线已经恢复高”于是判为 1。写 0 时隙则相反主机需要把总线持续拉低最少 60µs让采样点看到的是低电平从而判为 0之后再释放。这段逻辑看起来简单实际翻车最多的地方就是“写 1 的低电平时间过长”。如果主机释放不及时或者上拉电阻太大导致上升沿太慢从机在采样点看到的还是低电平一个好好的 1 就被误判成了 0。我踩过最典型的一个坑同一套代码在不同主频的 MCU 上跑空循环延时函数没校准低电平从原来的 6µs 变成了 40µs所有指令全部错乱。从那以后我在任何单总线项目里都会明确写出每个延时对应的实际微秒数并单独用示波器核对。3.3 读时隙主机让位从机说话读时隙和写时隙在起始阶段是一样的主机先把总线拉低 1µs 到 15µs然后释放。区别在于释放之后总线控制权交给从机。如果从机要发送 0它会继续把总线拉低直到时隙结束如果从机要发送 1它什么都不做让上拉电阻把总线拉回高。主机必须在释放总线后的 15µs 内完成采样过早没等从机接管过晚则可能读到下一个时隙的状态。这里有个很容易犯的错主机读位时为了保险把采样点放在时隙末尾结果读回来全是 1。原因是单总线器件在发送完 0 之后会在时隙结束前释放总线如果主机采样太晚总线已经被上拉电阻拉回高等于白读。正确做法是宁可采样点在 15µs 以内偏早一点也不要贪图“稳”而拖到后面。稳定的实现通常是拉低 2µs、释放、再延时 8µs 左右采样这个时间点既给从机留了接管总线的机会又没超过 15µs 的采样上限。另外读写时隙之间建议留 1µs 以上的恢复时间。很多人写字节时一个时隙接一个时隙中间没有任何空隙短距离小负载下可能没问题但一旦总线电容变大恢复时间不足就会让电平还没稳定就进入下一个时隙累积起来就变成偶发错误。规范的做法是在每个位操作末尾补足 60µs 的时隙总长让采样和切换都有稳定的窗口。4. ROM 寻址64 位身份码与多设备搜索算法4.1 64 位 ROM 码家族码、序列号和 CRC 各管什么每一颗单总线器件在出厂时都有一个全球唯一的 64 位 ROM 码这个码是在激光刻录阶段写死的用户不能修改。它的布局是最低字节是家族码中间 6 个字节是唯一的 48 位序列号最高字节是 CRC 校验码。家族码用来区分器件类型比如 DS18B20 是 0x28DS18S20 是 0x10DS1990 iButton 是 0x01DS2431 EEPROM 是 0x2D。主机拿到 8 个字节之后先看家族码就知道接下来该执行哪一套功能命令。CRC 字节的作用是校验前面 7 个字节有没有被读错。单总线的 CRC-8 多项式是 x^8 x^5 x^4 1初始化值为 0采用 LSB-first 的位处理方式在代码里常用 0x8C 这个反射多项式来做异或运算。校验通过不代表 ROM 码一定正确但校验失败几乎可以断定是时序问题或者总线干扰。我的习惯是所有从总线上读回来的多字节数据ROM 码、暂存器内容先跑一遍 CRC再决定要不要使用宁可在软件里多花几微秒也不要让脏数据进入业务逻辑。下面是单总线 CRC-8 的一个常见实现这段代码也可以用在校验 DS18B20 暂存器的 9 个字节上uint8_t owCrc8(const uint8_t *data, uint8_t len) { uint8_t crc 0; while (len--) { uint8_t in *data; for (uint8_t i 0; i 8; i) { uint8_t mix (crc ^ in) 0x01; crc 1; if (mix) crc ^ 0x8C; in 1; } } return crc; }4.2 四类基本 ROM 指令什么时候该用哪一条主机在和器件通信之前必须先发一条 ROM 指令用来选择“跟谁说话”。单总线最基本的 ROM 指令有四条指令名称功能使用限制0x33READ ROM直接读取 64 位 ROM 码总线上只能有 1 个设备0x55MATCH ROM按 64 位 ROM 码精确匹配设备后面必须跟 8 字节 ROM 码0xCCSKIP ROM跳过寻址直接访问所有设备常用于总线单设备广播功能命令需谨慎0xF0SEARCH ROM搜索总线上所有设备的 ROM 码适合多设备自动发现0xECALARM SEARCH只搜索满足报警条件的设备适合温控报警巡检用 SKIP ROM 的时候要格外小心。如果总线上挂了多颗 DS18B20主机发 0xCC 再发 CONVERT T所有传感器会同时开始转换、同时尝试把结果放到同一根总线上读回来的数据必然互相打架。正确做法是先用 SEARCH ROM 把所有 ROM 码枚举出来再用 MATCH ROM 逐个点名访问。如果系统固定只挂一个设备SKIP ROM 才能安全使用。4.3 SEARCH ROM 搜索算法为什么每位要读两次多设备识别是单总线最精彩的部分。SEARCH ROM 的思路是“按位分叉”主机把每一位都读两次第一次读到的是所有设备在该位的实际值第二次读到的是实际值的取反。如果总线上只有一位设备两次读回的结果必然互补要么是 0 和 1要么是 1 和 0如果总线上有多位设备且某些设备在该位上是 0、另一些是 1两次读回都会是 0这一位就发生了“冲突”也就是总线同时出现了 0 和 1。主机在冲突位必须做出选择写回 0 还是写回 1这个写回动作会把选择广播给所有设备只有 ROM 码与写回方向一致的设备继续参与后续位的比较另一分支的设备进入“旁观”状态。在一遍搜索结束后主机就拿到了一个完整 ROM 码。要想拿到下一个 ROM 码主机需要回到上一个冲突位改写方向再搜一遍。这个过程本质上就是在一棵二叉树上做深度优先遍历只是一切都发生在逐位读写里。SEARCH ROM 算法里最核心的状态变量是“上一个冲突位”。第一遍搜索时遇到冲突一律选 0并记录最后一个被选 0 的冲突位下一遍搜索时在那个记录的位选 1之前的冲突位沿用上一轮的路径之后的冲突位再继续选 0。如此往复直到某遍搜索全程没有遇到冲突位说明所有设备都枚举完了。4.4 SEARCH ROM 代码实现一份可直接改用的参考下面这份实现改编自广泛使用的 OneWire 公开库搜索逻辑去掉了硬件平台相关内容只保留算法骨架。它每调用一次返回一个 ROM 码配合外部循环不断调用直到返回 0 表示搜索结束。static uint8_t searchLastDiscrepancy 0; uint8_t owSearchRom(uint8_t *romId) { uint8_t byteIdx 0, mask 0x01; uint8_t bitIdx 0; uint8_t idBit, cmpBit, dir; uint8_t lastZero 0; if (owReset() 0) return 0; // 总线无设备 owWriteByte(0xF0); // SEARCH ROM do { idBit owReadBit(); cmpBit owReadBit(); if (idBit cmpBit) break; // 读回 1,1总线异常 if (idBit ! cmpBit) { dir idBit; // 所有设备这一位一致 } else { // 00冲突位需要决策 if (bitIdx searchLastDiscrepancy) { dir (romId[byteIdx] mask) ? 1 : 0; // 沿用之前方向 } else { dir (bitIdx searchLastDiscrepancy); } if (dir 0) lastZero bitIdx; // first search时 searchLastDiscrepancy0冲突位默认选1 // 后续根据 lastZero 回溯另一分支 } owWriteBit(dir); if (dir) romId[byteIdx] | mask; else romId[byteIdx] ~mask; bitIdx; mask 1; if (mask 0) { mask 1; byteIdx; } } while (byteIdx 8); if (bitIdx 64) return 0; // 异常中断 searchLastDiscrepancy lastZero; return 1; // 找到一个设备 }调用方式很简单先把所有 ROM 码清零循环调用owSearchRom每成功一次就保存一份地址直到返回 0。第一次调用前要把全局searchLastDiscrepancy初始化为 0。这段代码把搜索逻辑压缩到了最短生产项目可以以此为基础再补上超时保护、异常恢复和“无更多设备”标志就能应对绝大多数多设备采集场景。5. 完整实操DS18B20 多设备读取的最小工程5.1 硬件准备与接线动手之前先把硬件理清楚。一个典型的多点测温系统只需要四样东西MCU 开发板、一个 4.7kΩ 电阻、若干 DS18B20、以及连接线。MCU 的 GPIO 口接到 DQ 总线4.7kΩ 电阻一端接 VCC一般 3.3V 或 5V另一端接 DQ所有 DS18B20 的 DQ 引脚并联到同一条线GND 全部共地。如果传感器离主机比较远优先用双绞线并且每米线缆的电容越小越好。软件上最需要注意的是 GPIO 的方向切换。读总线时要让引脚变成输入模式让从机可以把总线拉低写总线时要变成输出模式主动拉低或释放。很多 MCU 的库函数切换模式有几十微秒的开销这在单总线位操作里是非常致命的所以生产代码建议直接操作寄存器绕过库层抽象。如果开发平台的库函数实在绕不开也要用开关中断的方式把每次位操作的临界区保护起来防止切换模式过程中调度器插队。5.2 核心位操作与时序函数下面给出一套完整的最小实现基于宏抽象 GPIO方便移植到 STM32、GD32、ESP32 等平台。使用时只需要把OW_LOW、OW_HIGH、OW_RELEASE、OW_RD四个宏对接到底层引脚操作上即可。#define OW_LOW() do { ow_dir_out(); ow_write(0); } while (0) #define OW_HIGH() do { ow_dir_out(); ow_write(1); } while (0) #define OW_RELEASE() do { ow_dir_in(); } while (0) #define OW_RD() ow_read() void delay_us(uint32_t us) { // 必须校准建议用 SysTick 或 DWT不要用简单的空循环 } uint8_t owReset(void) { uint8_t presence; OW_LOW(); delay_us(480); // 主机拉低至少 480us OW_RELEASE(); delay_us(70); // 等待存在脉冲 presence (OW_RD() 0); // 采样存在脉冲 delay_us(410); // 让复位窗口完整结束 return presence; } void owWriteBit(uint8_t bit) { if (bit) { OW_LOW(); delay_us(6); // 低电平 1~15us OW_RELEASE(); delay_us(64); // 补足时隙 } else { OW_LOW(); delay_us(60); // 低电平至少 60us OW_RELEASE(); delay_us(10); } } uint8_t owReadBit(void) { uint8_t val; OW_LOW(); delay_us(2); // 起始低电平 1~15us OW_RELEASE(); delay_us(8); // 释放后 15us 内采样 val OW_RD(); delay_us(50); // 补足时隙 return val ? 1 : 0; } void owWriteByte(uint8_t byte) { for (uint8_t i 0; i 8; i) { owWriteBit(byte 0x01); byte 1; } } uint8_t owReadByte(void) { uint8_t val 0; for (uint8_t i 0; i 8; i) { if (owReadBit()) val | (1 i); } return val; }注意不同 MCU 的 GPIO 翻转速度、库函数延时精度差异很大上面代码里的 6µs、8µs、60µs 这些数值不是死参数。第一次移植时务必用示波器确认实际低电平宽度和采样点位置再按实测微调。5.3 单设备温度读取从 ROM 跳过到暂存器解析单设备场景直接用 SKIP ROM 最省事。复位后发 0xCC 跳过寻址再发 0x44 启动温度转换等待转换完成12 位分辨率大约 750ms再次复位后发 0xCC 和 0xBE 读取暂存器。暂存器一共 9 个字节前两个字节是温度值的低字节和高字节第九字节是 CRC。温度数据是 16 位有符号数低 4 位是小数部分实际温度等于raw / 16.0。float readTempSingle(void) { uint8_t sp[9]; if (owReset() 0) return -999; owWriteByte(0xCC); // SKIP ROM owWriteByte(0x44); // CONVERT T delay_ms(750); // 外部供电模式直接延时 if (owReset() 0) return -999; owWriteByte(0xCC); owWriteByte(0xBE); // READ SCRATCHPAD for (uint8_t i 0; i 9; i) sp[i] owReadByte(); if (owCrc8(sp, 8) ! sp[8]) return -999; int16_t raw (int16_t)((sp[1] 8) | sp[0]); return raw / 16.0f; }负温度的处理很多人会栽跟头。DS18B20 的负温度用二进制补码表示比如 -0.5°C 对应的 16 位值是 0xFFF8直接把两个字节拼成uint16_t再除以 16会得到 4095.5 这种离谱结果。正确做法是先把两个字节组合成int16_t利用 C 语言有符号整数的类型转换自动得到带符号的补码值然后再除以 16.0。5.4 多设备采集编排搜索、匹配、逐个点名多设备流程比单设备多一步“点名”。完整流程分成三个阶段先复位并发 SEARCH ROM把所有传感器的 ROM 码收集到一个数组里然后对每个 ROM 码依次执行复位、发 MATCH ROM、把 8 字节 ROM 码逐个发送、发 CONVERT T最后再复位、发 MATCH ROM、读暂存器并解析温度。这样每颗传感器被独立点名互不干扰。需要特别提醒的是温度转换命令发出后如果使用外部供电可以通过读取总线电平判断转换是否完成DS18B20 在转换期间把总线拉低转换结束释放总线。用while (owReadBit() 0);等待可以省去固定延时提升效率。但如果是寄生供电模式这个等待策略不能直接用因为转换期间总线必须被强上拉维持电源此时要切换强上拉电路等到转换结束后再恢复普通上拉否则传感器会因为“饿电”而转换失败。6. 常见问题与排查技巧实录6.1 故障速查表现象、原因、对策单总线开发的问题有很强的共性我整理了这几年遇到频率最高的几类现象可能原因排查与对策复位检测不到存在脉冲上拉电阻缺失或没接 VCC检查 DQ 到 VCC 之间是否有 4.7kΩ测量空闲电平应为高读回全是 0xFF总线没上拉或线序接反用万用表确认 DQ/GND/VCC 三根线重点看 DQ 空闲是否接近电源电压单设备正常多设备全乱用 SKIP ROM 执行了广播功能命令改用 SEARCH ROM 枚举地址再用 MATCH ROM 点名偶发数据错误延时函数未校准或中断打扰关闭位操作期间的中断用 SysTick/DWT 校准延时温度固定 85°C 或乱码寄生供电下转换期间缺少强上拉检查外部供电或增加强上拉电路换主频后全部失效空循环延时假设错误不要依赖编译器优化行为实测每个延时点的微秒数长线时波形上升沿很缓上拉电阻偏大、线缆电容大换 2.2kΩ必要时用桥接芯片排查这类问题我强烈建议先用万用表确认“总线空闲电平”再看“复位波形”最后才看数据时隙。很多人一上来就盯数据位结果查半天才发现是上拉没接白耽误时间。6.2 示波器验证三件事电平、时序、采样点如果你有一台示波器单总线的调试会快速很多。拿到一块新板子我做的第一件事是复位后测量 DQ 的空闲电平确认它在高电平范围。第二件事是抓一次复位波形看主机拉低是否够 480µs从机的存在脉冲是否在释放后 15µs 到 60µs 内出现宽度是否在 60µs 到 240µs。第三件事是抓写 1 时隙重点看主机释放之后总线能否快速回升到高如果在采样点附近还在爬坡就要考虑减小上拉电阻。逻辑分析仪也很好用。把采样率调到 1MHz 以上抓一段复位和几个时隙用光标量每个低电平宽度。我通常会抓一条完整的读暂存器命令对照数据手册逐位核对写地址位、读数据位每一位的电平宽度对不对。只要这段波形对了上层数据解析出的温度基本不会错。6.3 最容易翻车的三个低级错误第一个低级错误是 GPIO 模式配错。很多 MCU 的引脚默认是模拟输入或推挽输出推挽输出模式下如果主机一直输出高电平从机根本没有机会把总线拉低通信一开始就废了。正确做法是写时序时把引脚切成输出模式读时序和释放时切成输入模式而且这个切换必须在每个位操作里严丝合缝地完成。第二个低级错误是编译优化改变空循环延时。同一段for循环在 -O0 和 -O2 下的实际延时可能差好几倍。我见过一个项目在调试版本里一切正常切到 release 版本后单总线完全认不到设备最后查出来就是优化后空循环被编译器“聪明”地省略或压缩了。解决方法是使用硬件定时器延时、volatile变量计数或者干脆用汇编级别的 NOP 来保证时序稳定。第三个低级错误是中断处理造成位操作中途被打断。单总线的一个时隙才几十微秒如果刚好在写 1 的低电平持续期间来了一个高优先级中断中断处理几十微秒总线状态就被破坏了。正确做法是在一个字节或一个完整位操作的临界区内关中断退出后再开。像温度转换这种毫秒级等待不需要关中断但位操作期间一定不能被中断插队。最后再分享一个小技巧如果系统对温度采集速度有要求可以把 DS18B20 的分辨率从默认的 12 位降到 10 位甚至 9 位。12 位分辨率转换时间最长 750ms9 位只要 93.75ms速度差距接近 8 倍。写入配置寄存器的操作是发 0x4E 写暂存器把配置字节对应分辨率位改掉再做一次 0x48 拷贝暂存器让它保存到 EEPROM。代价是分辨率从 0.0625°C 降到 0.5°C但对大多数恒温、温升监控场景来说完全够用。开发后期我一般会把分辨率参数做成可配置项量产固件里预留这个开关调试和交付两不误。
返回列表