ARTICLE DETAIL

资讯详情

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

单总线温度传感器MY18E20驱动开发:从时序到MicroPython实现

单总线温度传感器MY18E20驱动开发:从时序到MicroPython实现 做嵌入式时间长了你会发现很多“看起来不起眼”的传感器反而最考功底。MY18E20就是典型代表它是一颗单总线数字温度传感器外观、引脚、命令字和你熟悉的DS18B20几乎一样但价格更友好不少量产项目都在用。别以为它简单单总线协议的精髓全在时序里一根数据线上要完成复位、应答、写0、写1、读数据全靠几十微秒的窗口卡出来的稍不留神就会读到85.0或者干脆识别不到设备。这篇文章我打算从MY18E20的底层协议讲起手把手用MicroPython写一套驱动既是给新人做协议教学也是给老工程师一份可以直接用的代码。适合正在做温控、环境监测、IoT数据采集的朋友也适合想彻底搞懂单总线原理的嵌入式爱好者。1. 项目整体设计与思路拆解1.1 MY18E20是什么为什么值得自己写驱动MY18E20是一颗支持1-Wire单总线接口的数字温度传感器测温范围-55℃到125℃默认12位分辨率测量精度标称±0.5℃内部有唯一的64位ROM编码所以理论上可以在一根总线上挂多个传感器分别寻址。从驱动开发的角度看它和DS18B20的寄存器、命令字、时序参数都高度兼容买到手基本可以当作DS18B20来用。很多人图省事直接用现成的库但我更建议自己写一遍驱动。原因很简单第一现成库把时序细节都隐藏了出了问题你只能瞎猜第二不同开发板、不同MicroPython固件的延时精度不一样库里的默认参数未必适合你的硬件第三单总线是软件时序协议掌握了底层逻辑之后遇到任何兼容芯片都能快速移植。我手写这套驱动的过程本质上是把“照抄代码”变成“理解协议”排障时会轻松很多。另外一个现实问题是价格。MY18E20在批量采购时通常比同名进口型号便宜性能也够用很多做环境监测、冷链运输、智能家居的项目都在换这颗料。但便宜不意味着可以放松单总线三个字看着简单真正要在MicroPython这种解释型环境里跑稳还是有不少细节需要抠。1.2 为什么选择单总线协议自己写驱动而不是直接调现成库单总线的核心优势是省引脚。一个GPIO就能读温度特别适合MCU资源紧张的场景。而且1-Wire总线支持多点挂载用一条总线拉几十米挂上十几个传感器做分布测温非常方便。这也是它在农业大棚、机房温控、工业设备监测里长盛不衰的原因。那为什么不直接调MicroPython自带的OneWire库呢官方库确实是能跑的但它有几个问题一它是一个通用驱动围绕DS18B20、DS18S20设计对MY18E20这类兼容芯片来说可调参数很少二它内部帮你做了ROM搜索但如果只是单点测温这些额外逻辑反而增加了出错概率三也是最关键的你很难在现有库之上做深度排障。比如读回来温度是85.0你根本不知道是复位失败、命令没发对还是时序偏差。自己写驱动之后每个时隙都掌握在手里逻辑分析仪一抓波形哪个步骤有问题一目了然。1.3 技术栈选型MicroPython做底层时序行不行很多工程师一听到“MicroPython写时序驱动”就摇头觉得解释型语言速度太慢不适合做微秒级操作。这个观念要分情况看。1-Wire协议的单bit时隙是60到120微秒比I2C、SPI慢得多MicroPython的GPIO操作加延时函数在绝大多数现代MCU上都能覆盖这个时间尺度。实测在ESP32、RP2040、STM32F103这几个常用平台上只要关掉中断干扰复位和读写时序都能稳定满足协议要求。不过也要承认边界如果你做的是几万台的量产设备对时序余量要求极高或者还要同时跑无线协议栈那最终驱动肯定要用C或者直接用硬件1-Wire外设。但作为原型验证、小批量产品、教学实验MicroPython完全够用而且开发速度快得多。这里多说一句如果项目还想用U盘记录温度日志可以找支持USB Host的MicroPython固件在一些开发板上直接插U盘写CSV非常舒服。后面扩展部分我会再提。2. 单总线协议核心细节解析与实操要点2.1 开漏结构与上拉为什么不需要时钟线理解单总线首先得理解它的物理层。1-Wire总线只有一根数据线没有时钟线所有设备都是开漏输出外部需要接一个上拉电阻把总线拉高到供电电压。开漏的意思是设备只能主动把总线拉低不能主动推高需要输出高电平时就释放总线让上拉电阻把电平拉上去。你可能觉得这有点绕但好处很明显多个设备可以同时挂在一条线上谁想发送0就拉低不想发送就释放不会出现推挽输出那种“一个推高一个拉低”的短路问题。上拉电阻阻值对时序影响很大。典型值4.7kΩ短距离、少设备时1kΩ到10kΩ都能工作但阻值太大会让总线上升沿变慢读时隙的采样点可能错过太小则会增加功耗而且设备拉低时电流过大。我一般先上4.7kΩ如果线长超过2米再考虑降阻值或者屏蔽线。还有一个细节是如果MCU引脚内置了弱上拉可以暂时不接外部电阻做验证但正式电路一定要加。2.2 复位脉冲与存在脉冲握手的开始单总线每一次完整通信都必须先由主机发送一个复位脉冲从机回应一个存在脉冲这相当于双方握手。具体过程是主机把总线拉低至少480微秒然后释放。释放后从机会等待15到60微秒再把总线拉低60到240微秒表示“我在”。主机在释放总线后大约60到70微秒时读取电平如果读到低电平说明总线上有设备应答如果读到高电平说明没有设备或者接线有问题。这个握手在MicroPython里的实现很直接拉低、延时、释放、延时、读引脚。需要注意两点第一拉低时间要留够余量我习惯用500微秒而不是480微秒避免编译器或者GPIO切换开销把时间吃掉第二存在脉冲的窗口很窄MicroPython的延时函数本身有误差所以我常把读取点放在60微秒附近这个位置在存在脉冲的有效范围内成功率最高。附上我用的一段复位函数def _reset(self): self.data Pin(self.gpio, Pin.OUT) self.data.value(0) time.sleep_us(500) # 主机拉低复位脉冲 self.data Pin(self.gpio, Pin.IN, Pin.PULL_UP) time.sleep_us(60) # 等待存在脉冲 presence self.data.value() 0 time.sleep_us(440) # 等存在脉冲结束 return presence这个函数返回True表示检测到设备False表示无应答。调试时先单独调用它比直接读温度靠谱得多。2.3 写时隙与读时隙用时间窗口传递0和1握手之后所有数据都是通过时隙Time Slot来传输的。每个时隙至少持续60微秒写数据和读数据都从主机拉低总线开始。区别在于拉低之后的行为。写1时主机先拉低总线但必须在一个比较短的时间窗口内释放让上拉电阻把总线拉高。DS18B20/ MY18E20的写时序规定从写时隙开始到从机采样低电平时间不能超过15微秒否则从机会认为收到0。写0时主机必须把总线一直拉低60到120微秒整个时隙基本都保持低。用一句话记忆1是高所以要快速松手0是低所以要压住不放。读时隙稍微隐蔽一些。主机拉低总线至少1微秒后释放从机会在释放后的15微秒内决定总线状态如果要从机发0从机就把总线拉低如果从机发1从机就保持释放。主机必须在时隙开始后约15微秒处采样总线电平太早可能读到上一态太晚可能读到下一个时隙的起始低电平。我把采样点放在释放后8微秒左右因为在MicroPython里从释放到读取还会经过几微秒的代码开销实际采样点大致落在12到15微秒之间刚好卡进窗口。这里有个参数表方便你对照调整操作主机拉低时间从机采样/动作推荐MicroPython参数写11~15微秒采样窗口15~60微秒拉低8us后释放写060~120微秒无需采样持续低65us后释放读01微秒从机15us内拉低拉低3us后释放8us后读读11微秒从机15us内释放同上2.4 ROM命令与暂存器布局一次温度读取的完整流程单总线的通信流程有两层命令第一层是ROM命令用来选择总线上的某一个设备第二层是功能命令用来操作被选中的设备。单设备场景最常用的是跳过ROM命令0xCC意思是“不用管地址所有设备都执行后面的命令”。多设备场景就需要读ROM命令0x33只适合总线上只有一个设备、匹配ROM命令0x55、搜索ROM命令0xF0。接下去是功能命令0x44启动温度转换0xBE读暂存器0x4E写暂存器0x48复制暂存器等。MY18E20上电后暂存器里默认温度值是85.0℃这个很关键因为它是一个典型故障码。如果你读到的温度一直卡在85.0大概率不是温度真这么高而是根本没有成功启动转换。完整的单次温度读取流程可以拆成这几步复位等待存在脉冲。发送0xCC跳过ROM。发送0x44启动温度转换。等待转换完成。12位分辨率需要约750毫秒分辨率越低越快9位约94毫秒10位约188毫秒11位约375毫秒。再次复位。发送0xCC跳过ROM。发送0xBE读暂存器。连续读取9个字节其中前两个字节是温度值。按分辨率计算实际温度。暂存器的布局是字节0为温度LSB字节1为温度MSB字节2和3是报警阈值字节4是配置寄存器字节5到7保留字节8是CRC校验。温度计算时把LSB和MSB拼成一个16位整数如果是12位分辨率低4位是小数部分对应0.0625℃的步进。负数温度需要用符号扩展处理。后面驱动代码里我会详细演示。3. 实操过程与核心环节实现MicroPython驱动编写3.1 开发环境与硬件接线准备我用的开发板是ESP32和树莓派Pico各测了一遍代码完全通用只需要换GPIO号。元器件清单很简单一块开发板、一颗MY18E20、一个4.7kΩ电阻、几根杜邦线。接线方式MY18E20的VDD接3.3VGND接GNDDQ数据线接到GPIO同时DQ通过4.7kΩ电阻上拉到3.3V。注意如果是面包板电阻要尽量靠近传感器一侧减少寄生电容。固件方面直接从micropython官网下载对应板卡的bin或uf2刷进去就行。如果你后面想用U盘或者USB键盘这类外设就要找支持USB Host的MicroPython固件。这个需求不是必须的单跑温度传感器不影响。MicroPython里GPIO的编号要注意ESP32大部分开发板使用GPIO数字编号比如GPIO4就是Pin(4)树莓派Pico使用GPIO编号例如Pin(16)。我建议在初始化前先用万用表确认引脚避免因为丝印含义不同接错线。接线完成后先试一下GPIO能否置高置低再连传感器这是最基础的排错习惯。3.2 从bit级函数开始把协议变成代码写驱动最底层的是三个函数复位、写一个bit、读一个bit。这三个函数直接决定上层能否工作所以参数要反复调。为了避免不同MicroPython平台对Pin.OUT的推挽/开漏支持不一致我采用动态切换输入输出来模拟开漏需要拉低时设为输出并写0需要释放时设为输入并开启内部上拉。即使有外部4.7kΩ上拉内部上拉打开也没坏处能提高边沿速度。下面是读写bit的核心代码def _write_bit(self, bit): self.data Pin(self.gpio, Pin.OUT) self.data.value(0) if bit: time.sleep_us(8) # 写1短暂拉低后释放 self.data Pin(self.gpio, Pin.IN, Pin.PULL_UP) time.sleep_us(60) # 保证写时隙长度 else: time.sleep_us(65) # 写0持续拉低 self.data Pin(self.gpio, Pin.IN, Pin.PULL_UP) time.sleep_us(5) def _read_bit(self): self.data Pin(self.gpio, Pin.OUT) self.data.value(0) time.sleep_us(3) # 启动读时隙 self.data Pin(self.gpio, Pin.IN, Pin.PULL_UP) time.sleep_us(8) # 让从机有时间控制总线 bit self.data.value() time.sleep_us(50) # 时隙余量 return bit注意写1时拉低8微秒后释放这个8微秒是关键。我在ESP32上测试时如果把拉低时间加到15微秒以上读回来的数据会整片变成0。写0时拉低65微秒刚好在60到120微秒的范围内然后释放5微秒给总线恢复。读bit时拉低3微秒释放后等8微秒再采样实测能稳定读到正确的0和1。3.3 字节级封装与温度转换命令有了bit函数字节级操作就很简单了。1-Wire协议规定字节传输是低有效位在前LSB first所以读字节就是把8个bit依次按位移进一个整数写字节就是把每个bit按位移出def _read_byte(self): value 0 for i in range(8): value | self._read_bit() i return value def _write_byte(self, value): for i in range(8): self._write_bit((value i) 1)接着就可以封装命令了。启动温度转换的完整动作是复位、跳过ROM、发0x44、等待750毫秒。读暂存器的动作是复位、跳过ROM、发0xBE、连续读9个字节。这里我建议把转换和读取分开不要揉在一个函数里这样调试时能分别验证。温度解析部分先拼raw值raw data[0] | (data[1] 8)如果最高位是1说明是负温度需要做符号扩展。最稳妥的写法是if raw 0x8000: raw - 0x10000 temp raw * 0.0625比如12位精度下-0.5℃对应的raw值是0xFFF8也就是-8乘以0.0625就是-0.5℃。如果直接用无符号数换算会得到4095.75℃所以这一步不能省。3.4 完整驱动类一套可直接复用的MY18E20类下面给出我整理后的完整类包含CRC8校验直接复制到项目里就能用。CRC8虽然会增加一点点计算时间但能有效识别总线干扰值得加上。from machine import Pin import time class MY18E20: def __init__(self, gpio): self.gpio gpio self.data Pin(gpio, Pin.OUT) self.data.value(1) time.sleep_ms(10) def _reset(self): self.data Pin(self.gpio, Pin.OUT) self.data.value(0) time.sleep_us(500) self.data Pin(self.gpio, Pin.IN, Pin.PULL_UP) time.sleep_us(60) presence self.data.value() 0 time.sleep_us(440) return presence def _write_bit(self, bit): self.data Pin(self.gpio, Pin.OUT) self.data.value(0) if bit: time.sleep_us(8) self.data Pin(self.gpio, Pin.IN, Pin.PULL_UP) time.sleep_us(60) else: time.sleep_us(65) self.data Pin(self.gpio, Pin.IN, Pin.PULL_UP) time.sleep_us(5) def _read_bit(self): self.data Pin(self.gpio, Pin.OUT) self.data.value(0) time.sleep_us(3) self.data Pin(self.gpio, Pin.IN, Pin.PULL_UP) time.sleep_us(8) bit self.data.value() time.sleep_us(50) return bit def _read_byte(self): value 0 for i in range(8): value | self._read_bit() i return value def _write_byte(self, value): for i in range(8): self._write_bit((value i) 1) def _crc8(self, 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 def convert(self): self._reset() self._write_byte(0xCC) # 跳过ROM self._write_byte(0x44) # 启动温度转换 time.sleep_ms(750) def read_scratchpad(self): self._reset() self._write_byte(0xCC) # 跳过ROM self._write_byte(0xBE) # 读暂存器 data bytearray(9) for i in range(9): data[i] self._read_byte() return data def read_temp(self): self.convert() data self.read_scratchpad() if self._crc8(data) ! 0: return None raw data[0] | (data[1] 8) if raw 0x8000: raw - 0x10000 return raw * 0.0625调用示例sensor MY18E20(4) while True: t sensor.read_temp() if t is not None: print(当前温度{}℃.format(t)) else: print(读取失败) time.sleep_ms(1000)这个类在ESP32和树莓派Pico上都能跑唯一需要调整的是可能的时序参数。如果读回数据总是错优先检查所有time.sleep_us的值尤其是写1时那个8微秒。3.5 调用示例与REPL调试技巧MicroPython最方便的就是REPL。接好传感器后可以先在REPL里手动执行底层函数排查s MY18E20(4) s._reset() # 返回True说明检测到设备 s._write_byte(0xCC) s._write_byte(0x44) # 启动转换然后等几秒 s._reset() s._write_byte(0xCC) s._write_byte(0xBE) s._read_byte() # 返回温度LSB s._read_byte() # 返回温度MSB一段段敲完基本能定位是复位失败还是读写时序问题。如果_reset()返回False先查接线、上拉电阻和供电不要再往下走。如果_read_byte()读到的全是0或255说明bit时序需要调。4. 常见问题与排查技巧实录4.1 MicroPython时序不准怎么办很多新人拿到驱动跑不起来第一反应是“MicroPython不行”。实际上绝大多数问题是延时函数受中断影响。MicroPython的time.sleep_us在调用时如果有系统tick中断或者WiFi任务抢占实际延时可能多出几十微秒这在复位和读时隙里是致命伤。解决办法有两个一是关键时隙关中断。在复位和读bit时用machine.disable_irq()和machine.enable_irq()把中断临时关掉等时序操作完成再打开。注意关闭时间要短否则会影响系统心跳但单总线的几个bit操作加起来也就几百微秒问题不大。二是用忙等替代sleep。在某些平台上time.ticks_us()配合while循环实际效果反而稳定。下面是一个通用忙等函数def delay_us(n): start time.ticks_us() while time.ticks_diff(time.ticks_us(), start) n: pass用这个函数替换所有time.sleep_us时序误差会小一些。缺点是CPU空转但对短延时无所谓。4.2 读回85.0或0xFF的排查85.0这个值太有迷惑性了第一次遇到的人都以为传感器坏了。其实它是MY18E20上电时暂存器的默认温度值也就是说芯片根本没有执行温度转换或者转换结果没被正确读回来。常见原因有三个一是启动转换命令没发出去比如跳过ROM那一步失败二是等待时间不足12位分辨率需要750毫秒你只等了100毫秒三是每次读取前必须重新发一次转换命令不能只复位后直接读暂存器。0xFF则通常表示读时序出了偏差。我需要强调读bit时采样点太早或者太晚都会读到1。太早从机还没来得及拉低太晚可能已经进入下一个时隙。如果读到的字节固定为0xFF说明采样点整体偏晚或偏早试着把释放后的等待时间从8微秒改成5微秒或者改成10微秒观察结果。4.3 多设备场景ROM搜索与匹配单设备用0xCC跳过ROM没问题但总线上挂了多个MY18E20时0xCC会让所有传感器同时响应数据冲突。正确做法是先扫描出每个传感器的64位ROM地址然后针对指定地址发送匹配ROM命令0x55再接功能命令。MicroPython的onewire模块里已经有地址扫描函数可以直接借用。先扫描from machine import Pin from onewire import OneWire ow OneWire(Pin(4)) roms ow.scan() for rom in roms: print(rom.hex())拿到地址后在读取该设备温度时发送0x55匹配ROM然后逐个发送ROM地址字节。基于我们前面的驱动可以扩展一个带地址参数的读取函数核心是把跳过ROM替换成匹配ROMdef read_temp_by_rom(self, rom): self._reset() self._write_byte(0x55) # MATCH ROM for b in rom: self._write_byte(b) self._write_byte(0x44) time.sleep_ms(750) self._reset() self._write_byte(0x55) for b in rom: self._write_byte(b) self._write_byte(0xBE) data bytearray(9) for i in range(9): data[i] self._read_byte() # 后续解析与read_temp一致这样一条总线上挂多少设备都能分别读唯一要注意的是扫描ROM时不可以同时有多个设备在初始化否则搜索算法会出错。4.4 硬件问题速查表故障现象可能原因处理方式_reset()一直返回False接线错误、DQ接触不良、没上拉电阻检查VDD/GND/DQ加上4.7kΩ上拉温度固定85.0转换未启动、等待时间不足、命令顺序错误确认走到0x44命令并等待750ms温度总是0xFF或异常大读写时序偏早偏晚、上拉电阻过大调读时隙采样点上拉改用4.7kΩ偶发CRC错误总线干扰、线缆过长、电源纹波缩短导线加0.1uF去耦电容提高采样次数温度跳变、不连续接触不良、传感器供电不稳定重新插拔排查面包板接触必要时焊接4.5 几条实战经验第一不要把read_temp()放在一个无限循环里以最高频率跑。转换本身需要750毫秒频繁复位反而容易把总线状态搞乱还会增加传感器自发热影响精度。建议轮询周期至少1秒。第二逻辑分析仪是单总线调试的神器。一个几十块的24MHz逻辑分析仪就能把复位脉冲、存在脉冲、每个时隙的低电平时长看得清清楚楚。拿着波形和协议手册对比10分钟就能定位问题比盲调参数高效太多。第三MicroPython的报错不是万能的。时序错误不会抛出异常只会返回错误数据所以驱动里要有“读不到就返回None”的兜底逻辑宁可在应用层多查几次也不要让一个错误温度控制继电器动作。5. 工程化经验与扩展方向5.1 从驱动到产品调度与功耗设计驱动跑通只是第一步真正做成产品还要考虑调度和功耗。电池供电的场景推荐给MY18E20的VDD做一个MOS管开关单片机休眠时彻底断电需要采集时再上电。但注意上电后传感器需要几十毫秒稳定不要立刻发命令。我通常的做法是上电后等待50毫秒再复位然后启动转换读取完成后立刻断电。这样平均功耗能压到很低。如果系统里还有WiFi、蓝牙等任务建议用定时器触发采集不要在应用循环里用while True占住CPU。MicroPython的machine.Timer可以在后台周期执行温度数据放到队列里再由网络任务上报这样不会因为温度采集阻塞其他功能。5.2 可靠性与数据滤波工业场景里一次偶发的CRC错误不能直接当作故障处理。比较稳的写法是连续读三次取中间值或者平均值。这里提供一个简单滑动滤波思路def read_stable_temp(sensor, times3): vals [] for _ in range(times): t sensor.read_temp() if t is not None: vals.append(t) time.sleep_ms(100) if not vals: return None vals.sort() return vals[len(vals) // 2]限幅滤波也可以如果当前读数跟上次读数差超过5℃先不要更新连续两次超差再认为是真实跳变。这种做法在设备自发热、空调风直吹的场景下特别有效。5.3 扩展想法多路采集、日志记录与USB Host驱动基础打好之后扩展方向很多。一条总线上挂多个MY18E20做分布式测温配合前面的ROM匹配函数很容易扩展成多路采集。远程上报走MQTT本地记录可以写文件到SD卡甚至接U盘。如果你用的是支持USB Host的MicroPython固件可以直接把温度数据写成CSV存到U盘这样既不用SD卡模块也方便插到电脑上看曲线。对数据采集类项目来说这个方案成本低、可维护性好。需要提醒的是支持USB Host的固件选择要看具体板卡不是每个开发板都有使用前先确认外设功能和占用引脚。5.4 最后一点体会我真正把整套驱动跑稳是在连续踩了“85.0温度”和“0xFF数据”两个坑之后。回头总结经验最核心的不是代码本身而是对时序窗口的理解。单总线协议没有时钟线一切全靠主机的节奏控制所以写驱动时要多看协议手册上的时间参数别照搬别人的代码就完事。你在自己板子上遇到的时序问题很可能就是某个延时差了十几微秒。用逻辑分析仪抓一圈波形把每个时隙的宽度调到位这套驱动就能稳定陪你跑很久。
返回列表