拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MicroPython驱动MY18E20:单总线温度传感器时序解析与可靠实现

前阵子做一块多点温度采集板手头批了一批国产单总线温度传感器型号MY18E20。查了一圈资料官方手册写得含糊网上能找到的MicroPython驱动又大多是照着外语教程改的读取逻辑单薄、异常处理基本没有。与其在别人的代码上打补丁不如把协议吃透自己撸一套驱动——顺带也把单总线协议的细枝末节彻底理了一遍。这篇文章适合谁如果你手里正好有MY18E20或者任何DS18B20系列的兼容芯片想搞懂它到底怎么工作、为什么有时候读数诡异如果你是MicroPython玩家不想只会import现成库想自己写一个可靠的驱动——那这篇正好是你的菜。即使你用的是其他平台前面两章对单总线协议的拆解也完全照用。后面我会把完整可跑的驱动代码贴出来并把实测中踩过的坑一条条拆开讲。1. 选型逻辑为什么我选了MY18E20而不是“大牌原装”1.1 从DS18B20到MY18E20兼容芯片到底靠不靠谱先交代一下背景。DS18B20在温度采集领域几乎可以说是“事实标准”但这两年原厂芯片价格波动大、交期不稳定国产兼容型号开始大量出现在市场上。MY18E20就是其中之一封装常见的有TO-92、SOP-8、甚至部分贴片封装外观和引脚定义跟DS18B20基本一致。我最初也犹豫过兼容芯片会不会在时序上有细微差别导致老驱动跑不动实测下来MY18E20的指令集、暂存器布局、温度数据格式都跟DS18B20同族协议保持一致。换句话说凡是写DS18B20的驱动框架在MY18E20上基本都能直接跑。但这里有一个前提不能拿着别人的库盲用必须自己把协议关键点确认一遍。因为兼容芯片的批次差异是真实存在的手册里一般会写“兼容DS1820/DS18B20协议”但具体到时序边界的余量、复位拉低的最短时间不同批次可能有细微差别。如果你的项目用量大建议先拿几片回来实测复位波形和读写时序确认没问题再批量用。另外我选择MY18E20还有一个现实原因单价便宜而且供货相对稳定。做产品不是搞收藏在满足精度和可靠性要求的前提下国产兼容芯片是控制成本的好路子。这颗芯片在-10°C到85°C范围内的典型精度是±0.5°C量程覆盖-55°C到125°C常规环境监测、设备温度保护、冷链记录完全够用。1.2 单总线协议的“一根线哲学”单总线1-Wire这个名字听起来简单实际上它解决的问题很巧妙用一根数据线完成供电部分场景和双向通信。你想想看常规的I2C需要两根线SPI要四根UART要两根还得分收发而单总线只要一根线加上公共地。这意味着MCU一个GPIO就能挂一堆传感器布线成本大幅下降板子面积也能省一点。但它也有代价时序要求严格、通信速率不高典型场景下位速率在15.4kbps左右、抗干扰能力需要外部电路配合。这就是为什么很多人在MicroPython里直接调现成驱动时经常出现读不到数据、温度跳变、偶尔返回-127这类怪问题。根因往往是不理解它底层那些微妙的时间窗口。单总线的工作机制可以类比成“一根线上多人轮流发言”主机是主持人每个从机有唯一的64位ROM编号作为身份标识。主机通过拉低总线产生复位脉冲来宣布“开始一轮通信”从机用拉低总线回应存在脉冲表示“我在”。然后双方按照约定的时序一位一位地交换数据。2. 单总线时序拆解读0读1是怎么区分出来的2.1 电气基础开漏、上拉和寄生供电单总线的物理层本质是开漏输出结构。主机和从机都不能主动输出高电平只能拉低总线或者释放总线释放时由外部上拉电阻把电平拉回高。理解这一点特别重要因为所有时序都是围绕“谁在什么时候拉低、谁在什么时候释放”来设计的。标准做法是在数据线和VCC之间接一个4.7kΩ上拉电阻。如果系统是3.3V供电4.7k是安全的起点如果总线拉得比较长超过50cm可以换成2.2kΩ甚至1kΩ来增强驱动能力但电阻太小会增加功耗影响寄生供电设备的充电效率。MicroPython的GPIO内部上拉一般是30kΩ到50kΩ只适合极短距离调试正式项目一定要外部上拉。这里展开讲一下寄生供电模式因为很多人在这上面翻车。DS18B20系列芯片有两种供电方式外部供电VCC接电源和寄生供电VCC和GND都接地靠数据线在高电平期间给内部电容充电。启用寄生供电后把数据线拉低来驱动电路对时序余量要求极高尤其是在转换温度内部ADC工作的750ms窗口内如果数据线没有一个稳定的高电平状态芯片内部电容存的那点电可能撑不住导致转换失败。我的建议很简单能用外部供电就用外部供电寄生供电留给那些实在拉不了线的场景。单总线相关参数我给一个实测参考表参数最小值典型值最大值说明复位低电平时间480us500us960us主机拉低总线从机存在脉冲60us100us240us主机释放后从机拉低写0低电平时间60us70us120us低电平保持写1低电平时间1us5us15us短暂拉低后释放读采样窗口-释放后10us左右15us主机采样总线位周期-70us120us每位传输总时间12位转换时间-750ms-发0x44后的等待时间2.2 复位脉冲与存在检测通信的握手环节每次与MY18E20通信第一件事永远是复位。主机把总线拉低至少480us然后释放。从机检测到这个下降沿后内部会开始计时并在主机释放后的60us到240us窗口内主动把总线拉低产生一个60us到240us的低电平“存在脉冲”。主机在这个窗口内读到低电平说明总线上有设备在线。这里有一个细节很多人会忽略复位低电平时间不能太短也不能过长。太短从机检测不到过长会影响后续时序。MicroPython里time.sleep_us的精度其实没有想象的准解释执行本身有开销我的做法是写500us让余量往里收。存在脉冲采样点的选择也讲究。你不能在释放后立刻采样因为总线电容放电需要时间电平可能还处于过渡状态也不能等太久因为存在脉冲最短只有60us。实测下来释放后等70us再采样是比较稳妥的折中方案。2.3 读时序与写时序的时间窗口单总线数据通信的基本单位是“位”。每个位用一个完整的时序槽time slot来传输典型周期内主机需要控制几个关键时间点。先看写时序。主机要写0时拉低总线并保持60us到120us然后释放主机要写1时拉低总线1us到15us后立即释放剩下的时间交给上拉电阻恢复高电平。从机在每个位周期的起始点采样总线电平通过采样到低电平时间的长短来判断是0还是1。写成代码就是GPIO拉低、延时、释放、延时。再看读时序。主机发起一个读时序时先主动拉低总线1us到15us然后释放。注意接下来是重点从机如果要发0它会继续把总线拉低至少60us如果要发1它就在主机释放后什么也不做总线被上拉到高。主机必须在释放总线后的15us窗口内采样总线电平。采样太早可能读到主机自己拉低造成的假低电平采样太晚从机已经释放总线读到的一律是高电平那所有位都会被误判成1。下面这段是对时序窗口的小结方便对照主机拉低小于15us是写1或读时序的发起信号。主机拉低大于60us是写0或复位信号。从机在主机释放后15us内决定拉低或保持高主机在释放后约10us处采样。理解了这些你就明白为什么很多MicroPython驱动在读取时偶尔出错了。解释执行语言存在不确定的指令延迟如果每个GPIO翻转之间夹着垃圾回收、中断处理时序就会漂移。后面我会给出一个相对稳的实现方式。3. 一次完整温度读取的三个阶段3.1 ROM命令总线上的点名机制一根总线上可以挂多个传感器每个器件出厂时都有一个唯一的64位ROM编码其中第1字节是家族代码DS18B20系列为0x28最后1字节是CRC校验中间6字节是序列号。主机发命令时先用ROM命令来选择要跟哪个设备通信。常用的ROM命令有几种ROM命令命令字作用Search ROM0xF0识别总线上所有设备的ROM编号Read ROM0x33读取总线上唯一设备的ROM多设备时会产生数据冲突Match ROM0x55匹配指定ROM只跟目标设备通信Skip ROM0xCC跳过ROM匹配直接对总线上所有设备广播Alarm Search0xEC查找温度越限的设备在只有一颗传感器的场景下Skip ROM0xCC是最常用的省掉了发64位ROM编码的开销。但如果你挂了多个设备就必须用Search ROM做一次枚举把每个设备的ROM编号存起来之后再通过Match ROM点名通信。枚举算法本质是二叉树搜索主机逐位读取ROM编码同时利用“线与”特性判断总线上是否存在0和1的冲突再通过回写命令引导搜索方向。3.2 功能命令启动转换与读暂存器跟MY18E20通信的完整流程分两步先发启动温度转换命令等待转换完成再发读暂存器命令把9字节数据读回来。启动转换的命令是0x44。跳过ROM后直接广播这个命令总线上所有设备会同时开始温度转换。转换时间跟配置的分辨率有关9位分辨率约93.75ms10位约187.5ms11位约375ms12位约750ms。出厂默认通常就是12位如果你不关心0.0625°C的细粒度可以把分辨率降到10位甚至9位来换取更快的转换速度。读取暂存器的命令是0xBE。暂存器一共9个字节含义如下字节偏移内容0温度低字节1温度高字节2报警上限TH3报警下限TL4配置寄存器5保留6保留7计数器剩余值8CRC校验值一个常见的坑很多新手发完0x44之后立刻发0xBE开始读结果读到的是上一次转换的旧值甚至上电默认值。因为12位转换要750ms你如果不等它转换完就读读到的是暂存器里还没更新的旧数据。正确做法是等待足够时间后再读或者循环读第7字节的忙标志位bit5为1表示还在转换中。3.3 温度数据格式与符号处理温度值是由暂存器第0字节低字节和第1字节高字节拼出来的16位数据而且是左对齐的。前5位是符号扩展位如果温度为正高5位为0如果温度为负高5位为1。第12位分辨率下bit3的权重是2^-4 0.0625°C。举个例子读到的原始值是0x0191二进制就是0000 0001 1001 0001低4位是1恢复成温度就是 0x0191 4 25.0625°C。如果原始值是0xFC90高5位为全1说明是负数。先把16位转成有符号数0xFC90 - 0x10000 -880然后乘以0.0625得到-55.0°C。代码里处理这个转换时最稳妥的方式是判断第15位raw data[0] | (data[1] 8) if raw 0x8000: raw - 0x10000 temp raw * 0.0625千万不要直接把raw右移4位当无符号数用负温度会算出一堆完全错误的正值。4. MicroPython驱动实现从底层函数到可用的类4.1 环境准备引脚选择与固件版本写驱动之前先说环境。MicroPython版本建议1.19以上不同移植版的machine模块对GPIO开漏模式的支持有差异。ESP32和RP2040上Pin(pin_id, Pin.OPEN_DRAIN, Pin.PULL_UP)的写法都能用但ESP32的GPIO内部上拉比较弱不适合长线。接线方式很简单传感器VCC接3.3V或5V看芯片规格GND接GNDDQ数据线接一个GPIO同时在DQ和VCC之间接一颗4.7kΩ电阻。微控制器选择方面我用的是ESP32和RP2040两个平台做验证结论是RP2040的解释执行速度略快时序抖动稍小但ESP32外设丰富更适合做产品原型。你手上有什么板子就用什么跑下面的代码不会有平台兼容问题。4.2 底层读写函数用GPIO模拟1-Wire时序这是整个驱动的核心。MicroPython没法像C语言那样精确到微秒怎么办我的思路是接受一定程度的时序抖动但保证关键窗口不出界。具体策略有三个在每次位操作前后插入小延时给解释器一个“缓冲”。使用machine.disable_irq()在关键时序段关闭中断防止垃圾回收或系统滴答打断。时序参数取中间值不卡上下限。下面先定义引脚和基础操作函数from machine import Pin, disable_irq, enable_irq import time class MY18E20: def __init__(self, pin_id): self.dq Pin(pin_id, Pin.OPEN_DRAIN, Pin.PULL_UP) self.dq.value(1)写位的函数这样实现def _write_bit(self, bit): disable_irq() if bit: # 写1短暂拉低后释放 self.dq.value(0) time.sleep_us(6) self.dq.value(1) time.sleep_us(64) else: # 写0持续拉低 self.dq.value(0) time.sleep_us(60) self.dq.value(1) time.sleep_us(10) enable_irq()注意写1时拉低6us这是为了让从机明确检测到位时序的起始边沿。写0时拉低60us保证从机在采样窗口内稳稳读到低。读位的函数更依赖时机def _read_bit(self): disable_irq() self.dq.value(0) time.sleep_us(2) self.dq.value(1) time.sleep_us(4) val self.dq.value() # 释放后约4us采样 time.sleep_us(60) enable_irq() return val为什么释放后只等4us就采样因为从机要在15us内决定总线电平MicroPython从执行value(1)到执行value()之间本身有解释开销实际到达采样点可能已经过了6-8us仍在15us窗口内。如果你把延时调成10us解释开销叠加后很可能超过15us读到的全是1。这个参数我建议根据你的运行平台微调判断标准很简单能稳定读到85度这个默认值说明时序基本对了。读写字节就是把位操作做8次低位在前def _write_byte(self, data): for i in range(8): self._write_bit((data i) 0x01) def _read_byte(self): byte 0 for i in range(8): byte | self._read_bit() i return byte4.3 复位命令层reset、skip、match、读写字节复位函数直接关系能不能发现设备。我在前面讲过释放后要等70us再采样存在脉冲def reset(self): self.dq.value(1) time.sleep_us(10) self.dq.value(0) time.sleep_us(500) self.dq.value(1) disable_irq() time.sleep_us(70) presence self.dq.value() enable_irq() time.sleep_us(400) return presence 0presence 0表示检测到了存在脉冲——总线上有设备。如果返回False说明设备没接好、上拉电阻有问题或者引脚选错了。然后是命令层封装。在单设备场景下Skip ROM可以直接封装成固定宏def _skip_rom(self): self._write_byte(0xCC)多设备场景下需要Match ROM这里给出一个可复用的点名函数def _match_rom(self, rom): self._write_byte(0x55) for byte in rom: self._write_byte(byte)先读某个设备的ROM编号只在总线上有一个设备时可用def read_rom(self): if not self.reset(): return None self._write_byte(0x33) rom [self._read_byte() for _ in range(8)] return rom读回来的8字节中第8字节是CRC可以用它验证通信是否正确。4.4 温度读取与CRC校验现在串起完整流程。核心函数是read_tempdef read_temp(self, wait_for_conversionTrue, timeout_ms900): if not self.reset(): return None self._skip_rom() self._write_byte(0x44) # 启动温度转换 if wait_for_conversion: start time.ticks_ms() while time.ticks_diff(time.ticks_ms(), start) 750: time.sleep_ms(5) if not self.reset(): return None self._skip_rom() self._write_byte(0xBE) # 读取暂存器 data [self._read_byte() for _ in range(9)] if not self._crc8_check(data): return None raw data[0] | (data[1] 8) if raw 0x8000: raw - 0x10000 return raw * 0.0625CRC校验的实现多项式是x^8 x^5 x^4 1对应0x31逐位算法里通常用0x8C做反向移位def _crc8_check(self, data): crc 0 for byte in data[:8]: for _ in range(8): mix (crc ^ byte) 0x01 crc 1 if mix: crc ^ 0x8C byte 1 return crc data[8]这个CRC用处很大。单总线协议没有应答机制主机发完命令后怎么知道从机有没有完整收到靠的就是最后的CRC。如果CRC校验失败直接丢弃这帧数据返回None比返回一个可能出错的值安全得多。在温度采集系统里宁可少一个点也不要记一个假数据。完整的类定义还应该包含一个read_temp_blocking方法和一个带重试机制的读取函数。我实际用的重试逻辑是连续读3次返回成功的那次如果3次都返回None就上报传感器异常。def read_temp_with_retry(self, retries3): for _ in range(retries): result self.read_temp() if result is not None: return result time.sleep_ms(20) return None5. 实测中的常见故障与完整排查链路5.1 现象一读出来的温度永远是85度这个现象我见过不止一次几乎所有刚开始接触单总线的人都会遇到。明明接线没问题驱动也跑起来了温度就是稳定在85.0°C。85度的来历不是运气而是芯片上电后温度寄存器里的默认值0x0550换算下来正好是85°C。所以你读到85度说明时序、复位、命令传输都成功了问题出在“启动转换”这个环节没有真正完成。常见原因有两个。第一个0x44命令根本没发出去。可能是Skip ROM发送阶段写位时序偏差太大从机没正确解析命令自然也不会启动转换。第二个等待时间不够。如果你用的是12位分辨率转换要750ms但代码里只等了200ms这时候读暂存器读到的是旧值或者默认值。排查方法在read_temp里加入忙标志查询不要死等固定时间。读取暂存器第7字节检查bit5是否为1为1表示还在忙。更简单的验证方式把分辨率配置改到9位等待时间降到100ms左右如果读取恢复正常说明问题就出在转换等待时间上。5.2 现象二偶尔读到-127或者CRC校验失败如果说85度是“大概率误解”那-127就是“真出问题”的信号。DS18B20系列在通信异常时可能出现-127本质是数据线上读到的位多为高电平最终拼出全1的有符号数。我遇到过的根因有三类按概率排第一复位时序不稳定。如果复位后存在脉冲没被正确采样之后的命令全部无效。建议把复位函数里的延时参数打出来用逻辑分析仪或示波器测量实际波形确认低电平时间和采样点位置。第二总线电平被干扰。单总线布线太长又没有合适的上拉电阻信号边沿变缓从机识别不了。解决办法缩短线缆把上拉从4.7k降到2.2k或者在线缆末端加一个100pF左右的电容整形。注意不要加太大否则边沿更慢。第三MicroPython解释执行的时序抖动。你可以在读取位序的采样窗口里把time.sleep_us(4)适当缩短或加长用二分法试出本地平台的稳定参数。我实测过ESP32上延时2us到6us都能工作超过8us就开始随机出错。5.3 现象三总线上挂多个设备互相干扰单总线的“线”是共享的多个设备引发的干扰往往表现为某个设备温度偶尔异常、某个设备读不到、甚至整个总线复位失败。很多人以为只要发Skip ROM广播就能让所有设备同时工作。Skip ROM确实会广播给所有设备但读暂存器的时候如果总线上有多个设备同时响应它们会同时驱动总线数据自然乱套。正确做法是启动转换时可以用Skip ROM广播让所有设备同时转换读温度时必须用Match ROM逐个点名读取。多设备驱动的另一个坑是ROM枚举。总线上有多颗设备时直接发0x33读ROM会产生数据冲突你必须先做一次Search ROM枚举。在MicroPython里实现完整搜索算法不是不行但代码量不小。我的方案是产品装配阶段用单设备状态把每颗传感器的ROM读出来写进配置文件里运行时直接加载ROM表用Match ROM点名。这样既绕开了搜索算法的复杂度又保证了运行时的可靠性。5.4 排查顺序清单这几类问题混在一起时我建议按以下顺序排查步骤检查项验证方法1供电电压和接地万用表测VCC、GND之间电压2上拉电阻数据线对VCC阻值是否为外部上拉内部上拉不可靠3复位和存在脉冲写一个循环调reset观察是否稳定返回True4单设备基础读取用4.1节代码确认能读到元器件ID和默认85度5时序参数细调用二分法调整_read_bit里的延时找到稳定区间6转换等待确认转换等待时间超过分辨率对应时长7CRC判断打开CRC校验观察错误帧出现的频率这一套走下来90%的单总线通信问题都能定位。6. 驱动性能优化与后续扩展6.1 减少等待损耗批量转换和忙查询上一节的read_temp是同步阻塞的每次读取要等750ms。如果采集系统只挂一个传感器这个速度还能接受但要是挂四五个传感器逐一点名读取的耗时就是4倍非常不划算。批量读取可以这样设计先复位发Skip ROM加0x44让所有传感器同时启动转换等待时间不需要完全阻塞可以做些别的事比如读取其他传感器状态、刷新屏幕等到时间差不多后再逐个Match ROM读取温度。这样总耗时几乎等同单颗传感器的转换时间而不是设备数乘以750ms。另外忙查询替代固定延时也是个好办法。启动转换后循环读取暂存器第7字节等忙标志清零再读温度。这样做的好处是自动适配不同分辨率响应也更及时。6.2 让MicroPython时序更稳的三个小技巧第一不同平台对GPIO开漏模式的支持不一样。ESP32上用Pin.OPEN_DRAIN没问题RP2040上需要确认固件版本某些旧固件开漏模式存在bug。建议先写一个简单的翻转测试不断调用_write_bit并在引脚上挂LED观察或者用逻辑分析仪看波形。第二避免在时序关键段使用print。print调用会占用大量CPU时间还会触发文件系统和USB协议栈严重干扰时序。调试的时候可以打印量产代码里务必去掉。第三把中断尽量关掉。我提供的代码在关键位操作前后用了disable_irq和enable_irq注意成对出现否则整个系统会卡死。如果你用的平台没有这个函数可以用machine.disable_irq()的try/finally结构保证恢复。6.3 管理多设备ROM表缓存与动态点名当系统里已经有一份ROM表读多设备温度的核心逻辑是这个样子def read_all_temps(self, rom_list): temps [] if not self.reset(): return temps self._write_byte(0xCC) self._write_byte(0x44) # 广播启动转换 time.sleep_ms(50) # 给传感器一点准备时间 start time.ticks_ms() while time.ticks_diff(time.ticks_ms(), start) 750: time.sleep_ms(10) for rom in rom_list: if not self.reset(): continue self._match_rom(rom) self._write_byte(0xBE) data [self._read_byte() for _ in range(9)] if self._crc8_check(data): raw data[0] | (data[1] 8) if raw 0x8000: raw - 0x10000 temps.append(raw * 0.0625) else: temps.append(None) return temps这段代码在多点采集场景下很实用。广播启动转换只需要一次之后每个设备读取走独立通道互不影响。6.4 扩展思路数据记录、断线检测与告警阈值驱动跑通之后可以往上叠功能。温度数据记录是常见诉求MicroPython的JSON序列化很简单可以定时把温度写入本地文件攒一批后上报。断线检测也容易reset返回False时就可以认为总线断开了记录断线时间戳。告警阈值可以直接写在业务逻辑里比芯片自带的TH/TL寄存器灵活得多。另外一个我比较看好的方向是把这套驱动封装成asyncio的高层接口让温度读取、数据处理、网络上报各跑各的任务不互相阻塞。MicroPython的asyncio在ESP32上已经很成熟了读取温度改成异步等待转换完成整个采集系统的吞吐量能上一个台阶。最后提醒一句驱动没有问题的时候不要为了“优化”去乱调时序参数。单总线最忌讳的就是在能跑的代码上反复折腾那些微秒级延时要么不动动一次就必须用逻辑分析仪验证一遍。我自己就因为手痒把一个参数从4us改成3us结果整块板子出现间歇性读不到数据排查了半天才想起来是我改过时序。后来所有关键参数都写了注释标注了原始测试环境免得下次再踩。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门