ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

单总线协议与MicroPython:从MY18E20驱动看1-Wire时序细节

2026/9/8 23:06:51 拓冰建站 浏览量
单总线协议与MicroPython:从MY18E20驱动看1-Wire时序细节 前阵子给一套分布式温度采集节点改 BOM把主控板上的温度传感器从 DS18B20 换成了价格更友好的 MY18E20。原以为这是颗完全兼容的芯片直接把旧驱动搬过来就行结果在 MicroPython 环境下一测数据一会儿是 25.3一会儿读出 0xFFFFFFFF偶尔还会整条总线上设备全部失联。排查到最后才意识到问题不在芯片本身而是单总线协议的时序窗口非常苛刻而 MicroPython 这种靠解释器跑代码的环境执行时机和 C 语言完全不是一回事。这篇内容把 MY18E20 背后的单总线协议从头到尾拆一遍再给出一个从零编写的 MicroPython 驱动适合想在嵌入式环境里真正吃透 1-Wire 时序、不再被现成库包着走的开发者也适合那些正准备把 DS18B20 系传感器换成兼容物料、担心驱动能不能直接复用的朋友。1. MY18E20 的身世先搞清楚这颗兼容芯片到底兼容什么1.1 一颗改名不改命的温度传感器先说结论MY18E20 是一颗与广为人知的 DS18B20 高度兼容的单总线数字温度传感器最常见的是 TO-92 三脚直插封装引脚定义基本一致。DS18B20 在测温场景里几乎成了单总线温度传感器的代名词测温范围 -55°C 到 125°C标称精度在 -10°C 到 85°C 区间内为 ±0.5°C分辨率可以配置成 9~12 位。MY18E20 这类兼容型号之所以大量出现在现货市场说白了就是成本低、供货稳定很多消费级和工业级项目会用它在不影响功能的前提下压低整机物料成本。芯片名称里的 18E20 暗示了和 DS18B20 的兼容关系但兼容到什么程度不同批次、不同封装可能会有差异。我的建议是动手批量采购之前一定先找供应商要目标批次的 datasheet确认 ROM 族代码是不是 0x28、暂存器布局是否和标准 DS18B20 一致。这个动作花不了十分钟却能省掉后面一整天的排查时间。1.2 引脚、电气接口与基本特征MY18E20 的三根引脚分别为GND电源负极。DQ开漏结构的数据线工作时需要外部上拉电阻。VDD电源正极工作电压范围一般标 3.0V~5.5V。单总线接口最大的特征是所有读写时序都由主机发起从机只在主机拉低总线的间隙里响应。转换结果通过暂存器读出低字节在前。MY18E20 同样宣称支持寄生供电模式——把 VDD 和 GND 短接靠 DQ 引脚在特定时序窗口里给片内电容充电。但我要给一个非常实际的建议如果线长超过 30cm或者环境里电磁噪声比较大老老实实把 VDD 接上。省下的那根线会在调试的时候以十倍的时间代价还回来。2. 单总线时序的硬实时细节复位、写时隙、读时隙逐个拆2.1 开漏总线模型为什么不能两个设备互相推高1-Wire 之所以叫单总线是因为地线之外只需要一根数据线。这根线上的所有设备包括主机都必须遵守开漏规则只能把总线拉低不能主动推高。总线空闲时靠外部上拉电阻保持在 VDD 电平。这个设计有个隐藏的好处只要所有设备都遵守开漏总线上永远不会出现一个设备拼命拉高、另一个设备拼命拉低导致的短路。读数据时从机拉低表示 0保持高表示 1主机靠采样时隙来判断电平。理解了这个模型后面看代码里的Pin.OPEN_DRAIN配置就不会觉得奇怪了。2.2 复位与存在脉冲每次会话的握手每次通信会话都是从主机拉低总线开始的持续时间最低 480μs上限 960μs然后释放总线。释放后主机等待 15~60μs如果总线上有从机从机会主动把总线拉低 60~240μs形成一个存在脉冲。这一步有两个作用确认总线上有没有设备顺便让从机的内部状态机复位。如果存在脉冲没出现后面所有命令都是对空气说的所以这是驱动里第一个必须检查的点。操作参数最小典型最大复位低电平主机拉低480μs—960μs释放后等待主机等待15μs—60μs存在脉冲从机拉低60μs—240μs2.3 写时隙与读时隙60μs 里怎么传一个比特单总线的每个比特都占用一个时隙标准时隙长度是 60μs时隙之间至少留 1μs 的恢复时间。写 1主机拉低 1~15μs通用做法 6μs 左右然后释放总线剩余时间回到高电平。写 0主机拉低 60~120μs整个时隙基本保持低电平。读时隙主机拉低至少 1μs释放后从机会在一个很短的窗口内决定是否把总线拉低。主机必须在时隙开始后的 15μs 内完成采样。这里最关键的是写 1的 1~15μs 窗口拉得太短从机来不及采样拉得太长会被从机当成写 0。如果是在 C 语言里用 GPIO 寄存器直接操作这个窗口非常从容但在 MicroPython 里每一条语句都有解释执行的开销pin.value(0)到pin.value(1)之间如果恰好赶上解释器切换或者系统中断几个微秒的偏差很容易就把时序搞坏。操作主机动作从机响应采样/判定点写 1拉低 1~15μs释放不动作从机在 15~60μs 窗口读高电平写 0拉低 60~120μs不动作从机在 15~60μs 窗口读低电平读 1/0拉低 ≥1μs释放数据 0 时拉低主机在时隙开始后 15μs 内采样3. 一次温度读取的完整会话ROM 寻址、功能命令与 CRC 校验3.1 64 位 ROM 码与寻址机制每颗单总线温度传感器出厂时都有一个 64 位 ROM 码结构是8 位族代码 48 位序列号 8 位 CRC 校验。DS18B20 系的族代码是 0x28兼容芯片一般也沿用它但最好用 Read ROM 命令实测确认。这个 ROM 码让一条总线上可以挂很多颗传感器主机通过 ROM 命令选择跟谁通信。常用的 ROM 命令有命令代码作用Read ROM0x33读取总线上唯一从机的 ROM 码Match ROM0x55匹配指定 ROM 码的从机后续命令只对它生效Skip ROM0xCC跳过 ROM 匹配面向总线上所有从机Search ROM0xF0搜索总线上所有从机的 ROM 码Alarm Search0xEC搜索触发报警条件的设备单颗设备挂载时用 Skip ROM 最省事多颗设备挂载时必须先 Search ROM 拿到每个设备的 64 位 ROM 码之后用 Match ROM 逐个操作。3.2 功能命令与暂存器布局ROM 命令之后紧跟的是功能命令最常用的几个命令代码作用Convert T0x44启动温度转换Read Scratchpad0xBE读暂存器 9 个字节Write Scratchpad0x4E写 TH、TL、配置寄存器Copy Scratchpad0x48把暂存器内容拷到 EEPROMRecall EEPROM0xB8把 EEPROM 内容恢复到暂存器Read Power Supply0xB4查询供电模式暂存器一共 9 个字节温度低字节、温度高字节、TH、TL、配置寄存器、3 个保留字节通常 0xFF最后一个字节是 CRC 校验值。温度数据是 16 位有符号数默认 12 位分辨率下每个 LSB 代表 0.0625°C。比如读回原始值 0x0191换算是(0x0191) / 16 25.0625负温度则要把原始值先按补码转成负数再除以 16。3.3 完整读取流程的状态编排一次完整的温度读取按顺序做这几件事复位确认存在脉冲。发 Skip ROM单设备场景。发 Convert T0x44启动转换。等待转换完成12 位分辨率下最大 750ms9 位只需要 93.75ms。复位再次确认存在脉冲。发 Skip ROM。发 Read Scratchpad0xBE。连续读 9 个字节。对前 8 个字节做 CRC-8 校验匹配最后一位 CRC 字节再解析温度。这里有个很容易忽略的坑如果等待转换完成的期间总线上出现一次意外的低电平脉冲比如主程序里某个中断服务函数碰巧操作了同一个 GPIO从机的转换很可能被中断。MicroPython 环境下建议把等待期间的干扰源先排查干净。3.4 CRC 校验最后的防线1-Wire 的 CRC-8 多项式是 x^8 x^5 x^4 1用位运算实现非常短def crc8(data): crc 0 for byte in data: for _ in range(8): mix (crc ^ byte) 0x01 crc 1 if mix: crc ^ 0x8C byte 1 return crc校验逻辑很简单把暂存器前 8 个字节喂进去算出来的 CRC 必须等于第 9 个字节。对不上这批数据就不要信。我在实际项目里发现很多偶发读错温度的案例追根溯源都是跳过校验直接用了坏数据。4. 从零写 MicroPython 驱动总线层、协议层与内置模块对比4.1 为什么不能用普通数字 IO 直接驱动单总线要求开漏输出而开发板默认的Pin.OUT推挽模式会把引脚主动拉高或拉低。如果主机用推挽输出把 DQ 拉高同时从机又在拉低两个输出级会直接顶上轻则读到错误电平重则损伤引脚。所以驱动第一步就是把引脚配置成开漏模式from machine import Pin dq Pin(4, Pin.OPEN_DRAIN, Pin.PULL_UP)外部还要再接一个 4.7kΩ 上拉电阻。内部上拉一般只是辅助不能完全依赖它去驱动长线。4.2 总线层实现复位、读写一个比特总线层只做四件事复位、写一个比特、读一个比特以及基于它们拼出读/写一个字节。from machine import Pin import utime class MY18E20: def __init__(self, pin_id): self.dq Pin(pin_id, Pin.OPEN_DRAIN, Pin.PULL_UP) def reset(self): # 主机拉低至少 480us释放后检测存在脉冲 self.dq.value(0) utime.sleep_us(480) self.dq.value(1) utime.sleep_us(70) presence (self.dq.value() 0) utime.sleep_us(410) return presence def write_bit(self, bit): if bit: # 写 1拉低 6us 后释放 self.dq.value(0) utime.sleep_us(6) self.dq.value(1) utime.sleep_us(64) else: # 写 0拉低 60us self.dq.value(0) utime.sleep_us(60) self.dq.value(1) utime.sleep_us(10) def read_bit(self): # 拉低 6us 释放9us 后采样 self.dq.value(0) utime.sleep_us(6) self.dq.value(1) utime.sleep_us(9) value self.dq.value() utime.sleep_us(55) return value def write_byte(self, value): for i in range(8): self.write_bit((value i) 0x01) def read_byte(self): value 0 for i in range(8): value | (self.read_bit() i) return value写一个字节时最低位先发这正是单总线的 LSB-first 规则。如果你把write_byte写成高位先发会发现所有数据都莫名其妙地移位了这是初学者最容易踩的坑。read_bit里的采样时机特别关键协议规定主机必须在时隙开始后 15μs 内采样。我的代码在释放后等 9μs 采样正好落在这个窗口内。如果某些平台上sleep_us(9)的实际误差偏大可以把等待时间压缩到 6~8μs但不要低于 5μs否则从机还没来得及把总线拉低。4.3 协议层实现跳过 ROM、启动转换、读温度有了总线层协议层就只是按命令流程拼装def skip_rom(self): self.write_byte(0xCC) def match_rom(self, rom): self.write_byte(0x55) for b in rom: self.write_byte(b) def convert(self): self.reset() self.skip_rom() self.write_byte(0x44) def read_scratchpad(self): self.reset() self.skip_rom() self.write_byte(0xBE) return [self.read_byte() for _ in range(9)] def read_temp(self): self.convert() utime.sleep_ms(750) # 12 位分辨率最大转换时间 data self.read_scratchpad() if crc8(data[:8]) ! data[8]: return None raw data[1] 8 | data[0] if raw 0x8000: raw - 65536 return raw / 16.0实际调用就三行sensor MY18E20(4) temp sensor.read_temp() print(temp)如果你的应用对功耗敏感希望转换完成立刻读取而不是傻等 750ms可以把sleep_ms(750)换成轮询 Convert T 状态。但 DS18B20 系没有专门的转换完成寄存器最实用的做法是改成 9~11 位分辨率把最大转换时间压到 93.75ms~375ms。4.4 与内置 onewire、ds18x20 模块的取舍MicroPython 标准固件里其实已经带了onewire和ds18x20两个模块用起来很省事from machine import Pin from onewire import OneWire import utime ow OneWire(Pin(4, Pin.OPEN_DRAIN, Pin.PULL_UP)) ow.reset() ow.skip_rom() ow.writebyte(0x44) utime.sleep_ms(750) ow.reset() ow.skip_rom() ow.writebyte(0xBE) data ow.readbytes(9) raw data[1] 8 | data[0] print(raw / 16.0)更进一步的ds18x20模块甚至封装了 Search ROM 和温度解析from ds18x20 import DS18X20 ds DS18X20(OneWire(Pin(4))) roms ds.scan() ds.convert_temp() utime.sleep_ms(750) for rom in roms: print(ds.read_temp(rom))那为什么还要自己写一遍驱动我的观点是内置模块适用于标准设备、标准环境、快速出数的场景而自己写驱动是为了在兼容芯片批次差异、长线干扰、多设备总线竞争、极寒/极热现场等真实场景里保留逐位排查的能力。比如一旦ds.scan()偶尔扫不出设备如果你不懂 Search ROM 的底层逻辑面对0xFFFFFFFF的设备码只能干瞪眼。方案优点缺点适用场景内置 ds18x20 模块代码量极小含搜索和解析时序黑盒出问题难排查快速原型、标准器件内置 onewire 模块已封装好位操作仍需自己拼命令流程需要一定控制力完全自写驱动每个时序都可调可查代码量多需理解协议兼容芯片、批量生产、疑难排障5. 实测中的硬骨头时序漂移、供电方式与总线排障5.1 不同平台 sleep_us 的精度与解释器开销MicroPython 跑在不同芯片上utime.sleep_us的底层实现和精度差异很大。比如有的平台基于硬件定时器微秒级休眠比较可靠有的平台内部把sleep_us换算成系统 tick实际误差可能到几十微秒。更要命的是解释执行的开销self.dq.value(1)这一条语句从解释到 GPIO 生效少说也要吃掉几微秒。所以我的建议是不要死记代码里的延时数值把每个时隙的总时间当成延时 解释开销 GPIO 翻转时间的合计来理解。如果你在 ESP32 上跑写 1 时隙里的 6μs 拉低一般没问题换到某些国产开发板或者 RTOS 版本固件可能就得把 6μs 调成 2~4μs给后面的语句开销留出余量。实测中比较稳妥的排查方法拿逻辑分析仪抓 DQ 波形看每个时隙的高/低电平宽度是否落在协议范围内。24MHz 采样的逻辑分析仪就够用了重点是量写 1 的低电平宽度和读时隙的采样位置。5.2 寄生供电与外置供电不是所有场景都能省那根线寄生供电模式看起来诱人两根线就能工作省线缆、省连接器。但它有个硬约束温度转换需要较大电流转换期间 DQ 线必须被强上拉比如用 MOSFET 在转换期间把 DQ 拉到 VDD否则片内电容电量不够转换结果就是错的。我在实际项目里的经验是单颗设备、短线、3.3V 供电、室温环境下寄生供电偶尔也能正常工作但一旦线长超过 1m或者环境温度剧烈变化转换失败率立刻上升。量产项目老老实实接三根线这钱不能省。另外用 5V 给 VDD 供电时DQ 的上拉也要接到 5V而主控 GPIO 通常只能承受 3.3V这时候要么用电平转换要么让上拉电阻接 3.3V 并把 VDD 也降到 3.3V。MY18E20 在 3.3V 下完全能正常工作。5.3 上拉电阻、线缆长度与多设备挂载总线挂的设备越多、线缆越长寄生电容越大上拉电阻就要相应调小。短线30cm单设备用 4.7kΩ线长 1m 左右建议换 2.2kΩ如果挂 10 颗以上设备可能要用到 1kΩ。但电阻太小会让总线低电平时间变长反过来影响时序所以不要盲目往小了调。多设备挂载还有一个隐藏问题Search ROM 命令依赖每个设备 ROM 码的唯一性。兼容芯片的序列号如果烧录不规范出现重复 ROM 码总线搜索就会陷入死循环。批量贴片前可以写一个小脚本把每颗器件的 64 位 ROM 码打出来人工扫一遍重复项。这个工作做一次能避免后续整个产线偶发找不到设备的玄学问题。5.4 没有示波器时怎么自测总线状态不是每个人都随手有示波器但至少你有 LED 和串口。我的排障顺序是这样的第一先确认存在脉冲。单独调reset()如果始终返回False检查 VDD、GND、上拉电阻和 DQ 是否接对。很多读不到温度的案例最终定位都是 DQ 和 VDD 插反了。第二读回暂存器原始字节看数据模式。全部是 0xFF说明设备没有响应命令全部是 0x00可能是 DQ 对地短路CRC 校验经常失败基本可以断定时序偏移或者噪声干扰。第三用Read Power Supply0xB4命令确认供电模式。读到高电平表示外置供电低电平表示寄生供电。如果代码逻辑假设外置供电而实际上处于寄生模式转换数据就是废的。第四把gc.disable()加到关键通信代码前后。MicroPython 的垃圾回收一旦在时隙中间触发停顿可能达到毫秒级整个时序直接报废。通信结束后再gc.enable()恢复。做完整套驱动之后我最大的体会是单总线协议本身不复杂复杂的是它在微秒级时序上的刚性要求。你要是只想让温度数据能出来直接用内置ds18x20模块就够了但如果你要在低成本、多设备、长线的实际项目里不被动花一个下午把时序吃透绝对值。最后再分享一个小技巧驱动里所有时隙延时都定义成模块级常量万一换平台或者换批次芯片需要微调只改一个地方就行别让魔法数字散落在代码里。