单总线协议与MY18E20温度传感器MicroPython驱动实战

📅 发布时间:2026/9/9 0:41:35
单总线协议与MY18E20温度传感器MicroPython驱动实战
搞嵌入式或者玩ESP32、RP2040这些开发板的朋友对温度采集肯定不陌生。市面上便宜好用的数字温度传感器里DS18B20那个生态最有名而MY18E20就是国产兼容方案里用得非常多的一颗芯片封装、命令、时序都和DS18B20高度兼容三根线甚至两根线就能把温度数据送出来。单总线协议的精髓在于只用一根DQ数据线完成双向通信省引脚、省IO一条总线上还能挂多个器件。今天这篇就围绕MY18E20把单总线协议的时序细节拆开揉碎再跟着思路用MicroPython写一个可以直接跑起来的驱动最后聊一聊实际调试中踩过的坑。适合手上有开发板、想低成本做温度采集或者一直调现成库但不知道底层怎么工作的朋友往下看。1. 先搞清楚MY18E20是什么型号1.1 芯片规格与引脚定义MY18E20是国产单总线数字温度传感器协议层和Maxim的DS18B20基本一致在很多方案里直接被当作DS18B20的替代料使用。它的测温范围一般在-55℃到125℃在-10℃到85℃区间精度能做到±0.5℃12位分辨率下温度转换时间约为750ms这些参数都和DS18B20看齐。因为引脚定义也基本兼容常见的TO-92直插封装和防水不锈钢探头封装都有手头有DS18B20的库或者旧项目迁移成本很低。三个引脚里GND接地VDD接3.0~5.5V电源DQ就是数据线负责发命令、读温度、回数据所有通信都在这根线上完成。如果做寄生供电VDD甚至可以悬空由DQ线上的信号给芯片供电不过那样在温度转换期间需要主机强制拉高DQ提供电流实际项目里大多数还是老老实实接三根线简单可靠。MY18E20的待机电流非常小既可以用3.3V逻辑的开发板直连也能在5V单片机上工作电平兼容性比很多I2C传感器更宽。除了默认的12位分辨率MY18E20也支持通过配置寄存器把分辨率降到11位、10位或9位对应转换时间从375ms一路降到93.75ms。分辨率越高温度数据LSB代表的物理量越小12位时是0.0625℃9位时是0.5℃。如果你只是测环境温度12位分辨率性价比最高如果要做快速响应或者电池供电的低功耗场景可以适当降分辨率换转换速度。这一点在驱动里虽然没有显式暴露但了解之后遇到“为什么必须等750ms”就不会困惑了。1.2 为什么选单总线和MicroPython单总线协议1-Wire最大的价值是省引脚。一颗温度传感器只占用一个GPIO一个GPIO理论上可以并联几十颗器件靠每颗芯片出厂烧录的64位ROM码区分身份。相比之下I2C需要两根线加设备地址SPI需要三根线加片选在引脚紧张的板子上单总线的优势非常明显。缺点也明显时序要求精确到微秒级别通信速率不高实际有效波特率也就是几十kbps但拿它传温度这种低频数据绰绰有余。选MicroPython写驱动是因为ESP32、RP2040这些主流开发板都有现成的MicroPython固件下载刷入之后就能用解释型语法控制GPIO。MicroPython里操作IO非常简单用machine.Pin定义引脚、用time.sleep_us做微秒延时就能把单总线协议的底层时序完整模拟出来。相比C驱动MicroPython版本更容易读、容易改跑在ESP32这类主频较高的芯片上时序余量也够用特别适合快速验证传感器逻辑。不需要为了测个温度就去啃寄存器手册和编译工具链看懂时序图就能写驱动这也是这个方向最吸引人的地方。不过MicroPython毕竟不是实时系统Python解释器执行每条语句都有固定开销后面我们会专门讨论这个坑该怎么补。2. 单总线协议底层时序拆解2.1 复位与存在脉冲握手的两个关键电平单总线上所有的通信都由主机发起从机不会主动说话。主机想和MY18E20通信第一件事就是发复位脉冲。主机先把DQ拉低480~960μs然后释放总线让上拉电阻把电平抬高。从机在检测到下降沿后会等待15~60μs再把DQ拉低60~240μs这就是存在脉冲。主机在释放总线后大概70μs时读取引脚电平如果读到低电平说明总线上有从机应答通信链路是通的。这个过程就像两个人碰头先对暗号主机喊“有人在吗”从机回一句“我在”两边对上之后才开始传输具体内容。很多驱动初始化失败问题就出在这个握手阶段要么上拉电阻没接总线释放后一直悬空要么延时太短主机采样太早从机还没来得及拉低要么总线上有干扰存在脉冲的宽度不够。遇到“读不到温度”别急着怀疑芯片坏了先看复位函数返回的布尔值对不对。2.2 写时序主机如何往总线上“塞”数据写时序的本质不是发送高低电平本身而是用低电平的持续时间来表达0和1。主机要写0时拉低DQ并保持60~120μs然后释放要写1时拉低1~15μs就马上释放剩下的时间让上拉电阻把总线拉高。从机在每个时隙开始后的15~60μs窗口内采样根据总线被拉低的时间长短判断收到的是0还是1。这个设计很有意思明明是同一条线、同一个引脚写0和写1的起始动作完全相同区别只在于低电平保持多久。数据手册里特别强调写1的拉低时间不能超过15μs否则从机会误判成0写0的低电平时间最好在60~120μs之间太短了从机采不到太长了占用时隙影响后续时序。实际操作时我习惯写0保持约65μs写1保持约5μs后释放既留足Python解释器的执行余量又不越界。2.3 读时序从机如何把数据交回给主机读时序同样是主机发起。主机把DQ拉低至少1μs然后释放紧接着在释放后约15μs的时间内去采样引脚电平。注意释放瞬间因为上拉电阻的存在总线默认是高电平但从机如果此时想传0它会在主机释放后继续把自己的输出端拉低一直拉到整个时隙结束如果从机传1它就什么都不做总线被上拉保持高电平。所以读时序的关键是采样点要卡得准。采样太早可能会在从机还没来得及输出有效数据时读到高电平采样太晚读0场景下没问题但读1场景下如果总线上有其他容性负载电平可能拖着上不来也会发生误判。MicroPython里每执行一条Python代码都要好几微秒所以我在驱动里把采样时间设在释放后约8~10μs然后不等时隙完全结束就继续跑下一位实测下来比数据手册给出的15μs采样窗口更稳。2.4 一个容易踩的坑MicroPython的微秒延时前面反复提到时隙和延时这里必须说透一个MicroPython特有的坑。time.sleep_us(10)在C语言里可能真的等10μs但在MicroPython里Python解释器解析函数调用、进出函数栈、执行time模块的C接口都需要额外时间。实测在ESP32上一段写了sleep_us(5)的代码实际低电平时间可能是8~15μs。所以驱动参数不能照搬数据手册的极限值要给解释器开销留余量。我用的原则是凡是要短延时的时序写1的拉低、读时序前的拉低尽量把目标时间设在安全区间下限附近而不是卡着下限凡是要长延时的时序复位、写0则在区间内取偏中值。另外如果项目对时序要求很苛刻可以在关键收发函数前后暂时关闭中断比如调用machine.disable_irq()读完或写完再enable_irq()。我在ESP32上试过正常跑不关中断也没问题但如果你同时用着WiFi、Timer这类频繁触发中断的外设建议把关键收发操作包在关中断里面。3. 驱动框架设计与命令集3.1 总线层与设备层的分层思路写驱动前先想清楚结构。我的做法是把代码拆成两层下面一层叫OneWire总线驱动只负责最底层的复位、读写bit、读写byte不关心温度数据长什么样上面一层叫MY18E20设备驱动负责发跳过ROM、温度转换、读暂存器、解析温度值。底层越独立越好以后换一颗同样走单总线的芯片底层代码直接复用只改上层命令和解析逻辑。MicroPython里面向对象很顺手所以我把设备层封装成一个类MY18E20构造函数传入GPIO编号对外暴露convert_temp()和read_temp()两个方法。使用方只需要new一个对象先调用convert_temp()触发转换再调read_temp()读结果不需要接触任何寄存器细节。这种“库式”设计对初学者友好对后续集成也方便这是我在多个项目里迭代之后觉得最舒服的结构。3.2 MY18E20命令集梳理MY18E20继承DS18B20的命令集常用的有这么几条命令代码作用读ROM0x33读取64位ROM码单设备时可用匹配ROM0x55后面跟64位ROM码只与指定设备通信跳过ROM0xCC不指定设备直接操作总线上的所有设备温度转换0x44启动一次温度转换结果存入内部暂存器读暂存器0xBE从第0字节开始读9字节数据写暂存器0x4E写入报警上下限和分辨率配置搜索ROM0xF0多设备时遍历查找所有从机的ROM码单设备场景最省事的流程是复位、发跳过ROM、发温度转换等待转换完成再复位、发跳过ROM、发读暂存器然后连续读9字节。多设备场景必须用0x55匹配ROM或者0xF0搜索ROM否则所有挂在总线上的传感器会同时响应数据就乱套了。命令发送对字节顺序有要求所有字节都是低位在前这点在驱动里要格外小心写代码时如果从MSB开始发读回来的数据会完全对不上。3.3 暂存器与CRC校验读暂存器返回的9字节里前两个字节是温度值本身。温度寄存器是16位有符号数默认12位分辨率下LSB代表0.0625℃。比如温度寄存器里的数值是0x0191换算成十进制是401除以16就得到25.0625℃这个计算在解析函数里直接套用。负温度用二进制补码表示符号位为1时要把16位数值先减65536再除以16否则会得到一个明显不对的大正数。第2、3字节是报警阈值TH和TL第4字节是配置寄存器用来设置9到12位分辨率。第5到7字节是保留字节固定为0xFF第8字节是CRC。CRC校验对工业现场采集特别重要总线长了以后容易有干扰数据跳变在温度显示上往往只是零点几度不太会让你察觉异常。我每次读完9字节都会用CRC-8校验一遍校验不过就丢弃这次结果宁可不刷新也不能把坏数据采进统计表。4. 从零写一个可用的MicroPython驱动4.1 引脚初始化与底层位操作先定义引脚和类。我用的是开漏思路需要拉低总线时把引脚配置为输出并写0需要释放总线时把引脚切换为输入由外部上拉电阻负责把电平抬高。这里不建议直接用输出模式写1来“释放”因为如果从机正在拉低总线你用强推挽输出写1会造成总线冲突长期可能会损伤引脚。GPIO模式切换的成本不高单总线速率也低所以切换方式是安全的。在代码实现里我把引脚编号存在self.pin_num里每次操作前重新创建Pin对象。这是因为MicroPython里直接改Pin的模式在某些板子上会有缓存问题重新构造最稳妥。底层函数分别是_reset、_write_bit、_read_bit函数注释里写清楚了每个延时的设计意图。需要说明的是所有时序参数都是针对ESP32官方MicroPython固件实测调过的如果你换其他板子跑适当把sleep_us里的数字上下调几微秒就能适配。from machine import Pin import time class MY18E20: CMD_SKIP_ROM 0xCC CMD_CONVERT 0x44 CMD_READ_SCRATCHPAD 0xBE def __init__(self, pin_num): self.pin_num pin_num self.dq Pin(pin_num, Pin.OUT) self.dq.value(1) def _reset(self): self.dq Pin(self.pin_num, Pin.OUT) self.dq.value(0) time.sleep_us(480) self.dq Pin(self.pin_num, Pin.IN) time.sleep_us(70) present self.dq.value() 0 time.sleep_us(410) return present def _write_bit(self, bit): self.dq Pin(self.pin_num, Pin.OUT) self.dq.value(0) if bit: time.sleep_us(5) self.dq Pin(self.pin_num, Pin.IN) time.sleep_us(65) else: time.sleep_us(65) self.dq Pin(self.pin_num, Pin.IN) time.sleep_us(5) def _read_bit(self): self.dq Pin(self.pin_num, Pin.OUT) self.dq.value(0) time.sleep_us(2) self.dq Pin(self.pin_num, Pin.IN) time.sleep_us(8) v 1 if self.dq.value() else 0 time.sleep_us(50) return v_reset函数的480μs拉低是复位脉冲的时长70μs时读取存在脉冲刚好落在从机拉低窗口的中段。_write_bit里写1时拉低5μs后马上释放就算Python解释器拖到10μs也不会超过15μs上限写0时拉低65μs算上开销大约70μs依然在60~120μs窗口内。_read_bit里拉低2μs是为了制造一个下降沿释放后等8μs采样再补足剩余位周期保证整个时隙长度接近60μs和协议要求的时序形状一致。4.2 字节读写与CRC8实现字节级读写就是循环调用位函数注意先发低位这一点很容易被忽略。读取字节时把每一位的结果累加到一个整数里bit0是LSB所以用左移操作把高位放上去。CRC-8算法本身不复杂初值0多项式是0x31的反转形式0x8C对每一位做异或判断若异或结果为1就继续异或多项式移位循环8轮。有了CRC校验函数read_temp里就能判断暂存器的最后一字节是否是前面8字节的校验值。def _write_byte(self, data): for i in range(8): self._write_bit((data i) 1) def _read_byte(self): data 0 for i in range(8): if self._read_bit(): data | (1 i) return data staticmethod 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这里顺便给个建议如果你只是自己玩玩不做CRC校验也能跑但一旦传感器线超过30厘米或者旁边有继电器、电机这类干扰源CRC错误会时不时出现。做产品或者做长时间数据采集CRC校验这一行必须写。好多开源例程为了精简把CRC省了实际项目里害人不浅温度偶尔跳一下你根本不知道是真实温度变了还是数据错了。4.3 温度转换和读取的主流程设备层的主流程分两步。convert_temp方法先复位总线发跳过ROM命令再发转换命令然后等待一个转换周期。12位分辨率下转换时间是750ms为了简单我固定sleep 750ms如果你在配置寄存器里改过分辨率这里要相应调整。read_temp方法先复位发跳过ROM发读暂存器然后连续读9字节最后做CRC校验和温度值换算。为了减少外部调用时的出错概率我在read_temp里加了保护逻辑CRC校验失败时返回None而不是返回一个0或者-999之类的占位值。调用方只需要判断一下结果是否为空再决定下一步代码更干净。温度值用float返回保留两位小数打印就够了。def convert_temp(self): self._reset() self._write_byte(self.CMD_SKIP_ROM) self._write_byte(self.CMD_CONVERT) time.sleep_ms(750) def read_temp(self): self._reset() self._write_byte(self.CMD_SKIP_ROM) self._write_byte(self.CMD_READ_SCRATCHPAD) data [self._read_byte() for _ in range(9)] if self._crc8(data[:8]) ! data[8]: return None raw data[0] | (data[1] 8) if raw 0x8000: raw - 0x10000 return raw / 16.0调用方式很简单先定义对象再走“转换-读取”流程sensor MY18E20(4) # 假设DQ接在GPIO4 sensor.convert_temp() t sensor.read_temp() if t is not None: print(temperature %.2f C % t) else: print(CRC error or no sensor)注意上面这个例子只读一次温度。实际项目里通常放在主循环里每次循环先convert_temp再read_temp。如果你在主循环里忘了先转换就直接读读到的永远是上一次转换的旧结果这也是很多初学者“温度不更新”的常见原因。4.4 多设备ROM匹配扩展一总线上挂多个MY18E20是单总线的经典应用但代码复杂度会上一台阶。常规做法是先用0xF0搜索ROM把总线上所有设备的64位ROM码列出来之后每次通信都用0x55匹配ROM并附上目标设备的ROM码。搜索ROM的算法比较绕核心思路是对每一位做两次读第一次读所有设备在该位的输出第二次读反码。根据两次读到的组合判断方向“11”表示无设备“00”表示这一位存在冲突“01/10”表示只有唯一分支然后按规则逐位回溯最终得到完整ROM码。如果不想啃搜索算法还有一个工程上更简单的方案一个GPIO只挂一个传感器代码里创建多个MY18E20实例每个实例指向不同引脚。这种方案牺牲了GPIO数量但完全绕开搜索ROM的复杂度代码稳定性更高。我自己做项目时不超过四路温度采集基本都用“一对一”接法只有需要挂七八个传感器时才会去写搜索算法。毕竟一条总线上设备多一旦某个器件应答时序稍微异常整条总线都会被拖住排查起来比多占几个GPIO头疼得多。5. 常见问题与调试心得5.1 上拉电阻取值单总线在空闲时靠上拉电阻维持高电平没有上拉电阻总线一释放电平就悬空时序全是乱的。实践经验是3.3V供电、短线几十厘米以内用4.7kΩ上拉到VCC5V供电可以继续用4.7kΩ总线上设备较多或线缆超过2米上拉电阻换成1kΩ到2.2kΩ。上拉电阻太小会让总线上升沿变陡但会增加从机导通时的电流上拉电阻太大总线电容充电慢读1时采样到的电平可能还没爬升到位。我自己测试时发现1米屏蔽线加4.7kΩ上拉读时序偶尔出错降到2.2kΩ后连续读几千次都没有CRC错误。如果你用的是现成的MY18E20模块而不是裸芯片绝大多数模块上已经焊好了4.7kΩ上拉电阻直接接开发板就行。只有自己用铁壳探头或者裸芯片搭电路时才需要专门检查上拉电阻。还有一个容易忽略的点开发板内部的GPIO上拉电阻通常只有几十kΩ不能替代外部4.7kΩ上拉尤其长线场景千万不要依赖内部上拉。5.2 读回0xFF、复位失败怎么办读温度一直都是0xFF通常是总线根本没应答。按这个顺序排查先量DQ引脚的静态电平确认有没有上拉或者上拉异常再检查GPIO编号对不对很多开发板的丝印和实际GPIO号不一致然后用万用表量传感器VDD和GNDMY18E20供电异常时也会无响应。如果复位函数始终返回False还有可能是DQ引脚被配置成了推挽输出而不是开漏/输入切换模式导致从机根本没机会拉低总线。我把常见现象和排查方向整理成一个速查表调试时直接按表操作现象可能原因优先排查项复位始终失败上拉电阻缺失 / 接线错误用万用表量DQ静态电平看是否接近VCC读回固定0xFF设备无回应 / 引脚错误单独测复位返回值确认GPIO映射偶尔CRC错误线过长 / 干扰减小上拉电阻缩短导线加屏蔽温度固定不变没有重新发转换命令确认循环里先调用convert_temp再read_temp第一路正常第二路乱多个设备未处理ROM一对一连线或实现搜索ROM匹配5.3 时序参数补偿与固件差异不同开发板、不同MicroPython固件之间解释器执行速度差异很大。比如ESP32的官方固件跑160MHz一次空Pin操作大概几微秒RP2040的MicroPython解释器也跑在MHz级别但C接口实现不一样延时会略有不同。我的驱动里写死了几个时间参数换平台后建议逐项验证复位拉低时间是否在480μs以上写1的低电平时长是否小于15μs读时序采样点是否在15μs内。条件允许的话用逻辑分析仪抓一次DQ线上的波形和手册时序图对照这是最直观的调试方法。如果没有逻辑分析仪就靠功能验证调小写1延时如果读出来的值开始随机变0说明实际拉低时间逼近15μs上限了慢慢加大复位等待时间直到复位稳定返回True再留一点余量作为最终参数。这种试凑法听起来土但在没有示波器的场景下很管用。另外不同MicroPython固件对time.sleep_us的实现精度也不同有些第三方固件基于ESP-IDF的esp_timer精度反而比官方固件更高遇到驱动不稳定可以换固件版本交叉验证。5.4 实际项目中的稳定运行经验写到这里分享几个长期跑数据采集项目攒下来的经验。第一温度和别的传感器不同物理量变化慢不需要高频刷新两次转换之间至少间隔1秒既能降低总线占用也能让传感器自身温度稳定第二每次从read_temp拿到NoneCRC失败时直接丢弃这轮数据不要用上一次的值去填充否则异常会被掩盖第三如果做的是多点采集尽量避免在温度转换期间同时操作WiFi或者刷屏幕单总线时序对中断敏感这些操作抢走CPU时间后容易出现偶发CRC错误。我把这些都实现到代码里后连续跑了两个月两千多个温度点没有一次异常跳变这对一个用MicroPython写的驱动来说已经很可靠了。另外一个小技巧MY18E20这类芯片重新上电后内部暂存器通常已经准备好上一次的温度结果但如果刚上电就立刻读取而没触发转换读出来的值可能不是当前温度。所以我的初始化流程固定是“先转换再读取再进入主循环”避免把残留值当成实时值用。这个坑在各种开源库的讲解帖里很少有人提实际却非常常见。