ARTICLE DETAIL

资讯详情

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

MicroPython + ADS1115 高精度ADC驱动实战:从I2C协议到滤波算法

MicroPython + ADS1115 高精度ADC驱动实战:从I2C协议到滤波算法 1. 项目概述为什么选择 MicroPython ADS1115 这套组合我最早接触 ADS1115 是因为一个电池电压监测项目。当时用的开发板自带 ADC 只有 12 位测 3.7V 锂电池时分辨率只能到毫伏级别勉强够用但换到微弱信号采集比如电流互感器输出、电桥传感器就完全顶不住了。后来换成 ADS111516 位分辨率、内置可编程增益放大器PGA、4 路单端或 2 路差分输入还走 I2C 总线从 STM32 到 ESP32 再到树莓派 Pico 全都能接配合 MicroPython 这种“开箱即用”的解释型语言基本就是传感器采集场景里的万金油。先说这套组合能解决什么问题。MicroPython 本身不是为高性能采集设计的但它的优势在于开发效率极高——I2C 扫描、寄存器读写、异常调试全都可以在交互式终端里即时验证不需要编译烧录的循环。ADS1115 恰恰是一款“智能外设”风格的 ADC数据手册里给好了寄存器地址和位定义不需要自己跟时序较劲只要按 I2C 协议把配置字写进去等转换完成再读结果就行。两者搭配适合做低速、高精度的数据采集前端典型场景包括电池/电源电压监测单端模式PGA 设到 ±4.096V 或根据分压比调整称重传感器或压力传感器的差分信号采集PGA 设到 ±256mV配合 HX711 之外的第二方案环境监测光照、温湿度传感器的模拟输出多通道数据记录仪4 路单端输入轮询采样适合谁来参考如果你已经会用 MicroPython 点个灯、读个温湿度但对 I2C 协议底层不熟、不知道怎么从数据手册摸出驱动代码或者被噪声和抖动问题折磨过这篇文章就是给你写的。我会从协议层开始讲到驱动代码、采样触发、滤波处理最后附上调试踩坑记录整套内容可以直接抄作业。2. I2C 通信与硬件设计先把底子打好2.1 I2C 协议核心要点回顾I2CInter-Integrated Circuit是 Philips 在 1982 年推出的串行通信协议只需要两根线SCL时钟线和 SDA数据线。它的设计哲学是“多主多从”所有设备挂在同一条总线上通过地址寻址区分设备。对 ADS1115 来说它永远是从机角色由主控比如 ESP32、Pico发起通信。你需要掌握的关键细节有几个起始条件与停止条件SCL 为高电平时SDA 从高跳到低表示起始SCL 为高电平时SDA 从低跳到高表示停止。所有通信都以这两个条件为边界。地址字节第一个字节的高 7 位是从机地址最低位是读/写标志位0 表示写1 表示读。应答机制每传输完 8 位数据接收方要在第 9 个时钟周期拉低 SDA 表示 ACK不拉低则视为 NACK。主控在读数据时如果不再需要更多字节会在最后发 NACK 然后发停止条件。时序参数标准模式100kHz和快速模式400kHz下信号上升沿、建立时间、保持时间都有严格限制。但 MicroPython 的 I2C 驱动会自动处理这些时序你不必手动卡 ns 级别的时间。我见过不少刚接触 I2C 的朋友看到时序图就犯怵其实对使用 MicroPython 的开发者来说I2C 时序是个“黑盒”——你在代码里调用i2c.readfrom_mem()底层驱动已经帮你处理好了 START、地址、寄存器地址、STOP 等所有信号。真正需要你手动模拟 I2C 的场合大概只有两类一是某些特殊引脚不支持硬件 I2C二是想追求极低的 GPIO 占用成本。网上流传的“软件 I2C”思路本质就是用 GPIO 模拟时序MicroPython 里可以用machine.Pin按数据手册的时序图逐 bit 翻转电平实现但这个方法只在不得已时用后面我会详细讲。2.2 ADS1115 硬件接线地址、上拉电阻与电平匹配ADS1115 的地址由 ADDR 引脚决定这是新手最容易忽略的一个环节。ADDR 引脚可以接 GND、VDD、SDA、SCL对应 4 种不同地址ADDR 引脚接法7 位地址写地址8 位读地址8 位接 GND0x480x900x91接 VDD0x490x920x93接 SDA0x4A0x940x95接 SCL0x4B0x960x97MicroPython 的I2C.scan()返回的是 7 位地址所以你在代码里看到的通常是 0x48 这类值。地址位ADDR 引脚可以支持最多 4 片 ADS1115 挂同一条总线但要注意如果 ADDR 接 SDA 或 SCL线上的逻辑电平会影响地址识别设计时最好优先用接地或接 VDD 的方式省心。上拉电阻是 I2C 总线的“标配”。I2C 协议的 SDA 和 SCL 是开漏输出必须外接上拉电阻才能输出高电平。电阻阻值怎么选主要看总线电容和通信速率100kHz 标准模式上拉电阻常见 4.7kΩ ~ 10kΩ400kHz 快速模式上拉电阻建议 1kΩ ~ 4.7kΩ如果上拉电阻太大信号上升沿会变缓高速通信时可能出错太小了灌电流变大设备可能驱动不了。我实测过 ESP32 的 GPIO 内部上拉不可靠因为内部上拉阻值通常在 30kΩ 到 50kΩ 之间驱动能力不够总线容易受干扰或通信失败。保险做法是外接 2.2kΩ 或 4.7kΩ 电阻到 VDD。电平匹配是另一个大坑。很多传感器模块是 3.3V 逻辑而主控是 5V比如 Arduino UNO。如果直接把 5V 主控的 SCL/SDA 接到 3.3V 的 ADS1115 上长期运行可能烧毁模块。解决方案有几种使用电平转换模块如 TXS0108E、逻辑电平转换板让上拉电阻接到 3.3V这样总线高电平被钳位在 3.3V5V 主控也能识别但要注意主控 GPIO 是否容忍 3.3V“高压输入”被反灌如果是 3.3V 主控接 3.3V 模块直接共地、共用 3.3V 电源、外接上拉就行我习惯的接线方式是ADS1115 的 VDD 和主控逻辑电源接同一路SDA、SCL 各接一个 4.7kΩ 上拉电阻到同一路电源AGND 与主控 GND 共地。如果采集的是高阻抗信号源模拟输入引脚与地之间建议加一个 0.1μF 的滤波电容可以明显降低串入的射频干扰。2.3 四种设备接法与信号源注意事项ADS1115 支持 4 路单端输入AIN0~AIN3 分别对 GND或 2 路差分输入AIN0-AIN1、AIN2-AIN3。接线前先想清楚你要测什么因为单端和差分的接线方式差别很大。单端接法传感器输出信号线接 AIN0传感器 GND 与 ADS1115 的 GND 共地。适合信号源参考地与 ADC 参考地一致且距离近的场景比如板载电源电压监测。差分接法传感器正输出接 AIN0负输出接 AIN1。差分模式能抵消共模噪声适合桥式传感器、热电偶等信号幅度小且噪声大的场景。还有一点容易被忽略ADC 输入引脚不能超过 VDD 0.3V也不能低于 GND - 0.3V。如果输入信号可能超出这个范围必须在外面加分压电阻网络或钳位二极管。我在做电池监测时锂电池电压是 4.2V 满电超出了 ADS1115 的电源电压 3.3V这时用 100kΩ 和 47kΩ 电阻分压把电压缩到 1.35V 以内再送入 AIN0程序里再把读数乘回分压比。3. 驱动实现从数据手册到 MicroPython 代码3.1 ADS1115 内部结构与寄存器地图ADS1115 的内部控制核心是 4 个 16 位寄存器I2C 通信的本质就是读写这些寄存器。数据手册给出的寄存器地图如下寄存器地址读写功能转换寄存器0x00只读存放最新的 ADC 转换结果配置寄存器0x01读写设置 PGA、数据速率、操作模式、通道选择等比较阈值寄存器低0x02读写比较器模式下低阈值比较阈值寄存器高0x03读写比较器模式下高阈值大多数场景下你用到的就是配置寄存器和转换寄存器。配置寄存器 16 位每位都有特定含义我把位定义列一下Bit 15OS单次转换启动位。写入 1 后启动一次转换在连续模式下读取时此位表示转换是否完成1 表示完成。Bits 14-12MUX输入多路复用器选择。000 表示 AIN0-AIN1 差分001 表示 AIN0-AIN3 差分010 表示 AIN1-AIN3 差分011 表示 AIN2-AIN3 差分100 表示 AIN0 对 GND101 表示 AIN1 对 GND110 表示 AIN2 对 GND111 表示 AIN3 对 GND。Bits 11-9PGA可编程增益放大。000 表示 ±6.144V001 表示 ±4.096V010 表示 ±2.048V011 表示 ±1.024V100 表示 ±0.512V101 表示 ±0.256V110 表示 ±0.256V同 101111 表示 ±0.256V同 101。Bit 8MODE数据速率模式下是连续转换0还是单次模式1。Bits 7-5DR数据速率。000 表示 8 SPS001 表示 16 SPS010 表示 32 SPS011 表示 64 SPS100 表示 128 SPS101 表示 250 SPS110 表示 475 SPS111 表示 860 SPS。Bits 4-0比较器相关配置如果不用比较器模式保持默认即可。这里有个初学者常犯的错把 PGA 和输入电压范围搞混。PGA 设置的是满量程范围FSR不是分辨率。不管你设多少分辨率都是 FSR ÷ 32768。3.2 MicroPython 驱动代码最小实现与逐行注释下面给出一份我实际在用的 ADS1115 驱动骨架基于 MicroPython 的machine.I2C兼容 ESP32、树莓派 Pico 等常见平台。from machine import Pin, I2C import time class ADS1115: # 寄存器地址 REG_CONVERSION 0x00 REG_CONFIG 0x01 REG_LO_THRESH 0x02 REG_HI_THRESH 0x03 # 增益对应的满量程电压 PGA_VALUES { 0: 6.144, 1: 4.096, 2: 2.048, 3: 1.024, 4: 0.512, 5: 0.256, 6: 0.256, 7: 0.256, } def __init__(self, i2c, address0x48): self.i2c i2c self.addr address self.gain 1 # 默认 PGA 1对应 ±4.096V self.data_rate 0b100 # 默认 128 SPS self.mux 0b100 # 默认 AIN0 单端 def set_gain(self, pga_code): if pga_code 0 or pga_code 7: raise ValueError(PGA code must be 0-7) self.gain pga_code def set_data_rate(self, dr_code): if dr_code 0 or dr_code 7: raise ValueError(DR code must be 0-7) self.data_rate dr_code def set_mux(self, mux_code): if mux_code 0 or mux_code 7: raise ValueError(MUX code must be 0-7) self.mux mux_code def _build_config(self, os_bit1): config 0 config | (os_bit 15) config | (self.mux 12) config | (self.gain 9) config | (1 8) # 单次模式 config | (self.data_rate 5) # bits 4-0 设为默认禁用比较器 return config def _write_config(self, config): data [(config 8) 0xFF, config 0xFF] self.i2c.writeto_mem(self.addr, self.REG_CONFIG, bytes(data)) def read_raw(self): config self._build_config(os_bit1) self._write_config(config) # 等待转换完成最坏情况为 1/8 SPS ≈ 125ms这里轮询 OS 位 while True: config self._read_config() if (config 15) 0x01: break time.sleep_ms(1) data self.i2c.readfrom_mem(self.addr, self.REG_CONVERSION, 2) raw (data[0] 8) | data[1] # 转换结果为 16 位有符号数 if raw 0x8000: raw - 0x10000 return raw def _read_config(self): data self.i2c.readfrom_mem(self.addr, self.REG_CONFIG, 2) return (data[0] 8) | data[1] def read_voltage(self): raw self.read_raw() fsr self.PGA_VALUES[self.gain] return raw * fsr / 32768.0 def read_channel(self, channel): if channel 0 or channel 3: raise ValueError(Channel must be 0-3) self.set_mux(0b100 channel) # 100AIN0, 101AIN1, 110AIN2, 111AIN3 return self.read_voltage()这段代码有几个值得注意的设计点。read_raw()中我在单次模式下启动转换后通过轮询配置寄存器的 OS 位来判断转换是否完成而不是简单地sleep一个固定时间。原因是数据速率不同导致转换时间差异很大8 SPS 要 125ms860 SPS 只要 1.16ms轮询能保证在最快时间内读到数据又不会误读旧数据。read_voltage()的换算公式是raw * FSR / 32768因为 16 位 ADC 的码值范围是 -32768 到 32767。例如PGA 设为 ±4.096V 时1 LSB 4.096 / 32768 ≈ 125μV你可以用这个值验证电压读数的精度。初始化 I2C 和 ADS1115 的用法i2c I2C(0, sclPin(22), sdaPin(21), freq400000) # ESP32 示例 ads ADS1115(i2c, address0x48) # 扫描总线上的设备 print(i2c.scan()) # 应该包含 0x48 # 读取 AIN0 电压 voltage ads.read_channel(0) print(AIN0 voltage:, voltage)还能再精简吗很多人喜欢把驱动压缩成十来行直接拼配置字节。但我觉得保留寄存器映射和增益字典是有价值的——以后你想换通道、换量程不用翻数据手册查位定义改代码就能搞定。代码可读性和可维护性在嵌入式项目里同样重要。3.3 软件模拟 I2C 的取舍与实现思路有时候你会遇到硬件 I2C 引脚被占用或者某个引脚上没有硬件 I2C 外设的情况。这时需要软件模拟 I2C也就是用普通 GPIO time.sleep_us()来模拟 I2C 时序。MicroPython 中通过machine.Pin控制电平实现方式很简单但要注意几点必须在 SCL 和 SDA 上外接上拉电阻否则信号线无法主动拉高。时序精度受限建议通信速率降到 100kHz 左右。当时 STM32F407 上模拟 I2C 读 ADS1115我在 400kHz 下偶尔丢数据降到 100kHz 后稳定得一批。实现 START、STOP、发送字节、接收字节几个函数按数据手册的时序图来写。一个最简软件 I2C 发送字节的示意from machine import Pin import time SCL Pin(15, Pin.OUT, Pin.PULL_UP) SDA Pin(16, Pin.OUT, Pin.PULL_UP) def i2c_start(): SDA.value(1) SCL.value(1) SDA.value(0) SCL.value(0) def i2c_stop(): SDA.value(0) SCL.value(1) SDA.value(1) def i2c_write_byte(byte): for i in range(7, -1, -1): SDA.value((byte i) 1) SCL.value(1) time.sleep_us(5) SCL.value(0) time.sleep_us(5) # 释放 SDA读取 ACK SDA Pin(16, Pin.IN, Pin.PULL_UP) SCL.value(1) time.sleep_us(5) ack not SDA.value() SCL.value(0) SDA Pin(16, Pin.OUT, Pin.PULL_UP) return ack注意看代码里SDA模式的切换——发送数据时是输出模式读取 ACK 时要切换为输入模式。这个细节在硬件 I2C 里完全不用操心模拟 I2C 就必须手动处理。软件 I2C 的优点是可以把任何 GPIO 变成 I2C 引脚缺点是 CPU 开销高、时序稳定性差。我的建议是能用硬件 I2C 就绝不用软件模拟只有在引脚资源极度受限或硬件外设故障时才考虑这条路。3.4 多设备总线与 I2C 扩展技巧一条 I2C 总线上可以挂多个设备只要地址不冲突就行。比如同时挂 ADS11150x48和 BH1750 光照传感器0x23扫描结果就是[0x23, 0x48]。但如果你要挂 4 片 ADS1115地址 0x48~0x4B又不确定每片的 ADDR 引脚怎么接最好的验证方法是先用i2c.scan()打印出所有地址确认无误再操作。当总线上的设备超过 2~3 个时注意总线上拉电阻要适当减小。因为每个设备的引脚都有寄生电容设备多了总线电容增大4.7kΩ 上拉可能拉不上去。总线比较长或挂载多时我会把上拉电阻换到 2.2kΩ如果故障依旧再检查时序。如果总线设备太多比如超过 8 个或者有不同逻辑电平的设备混挂可以用 TCA9548A 这类 I2C 多路复用器把总线分成多个通道。但这属于进阶玩法正常情况下挂 2~3 个设备一条总线完全够用不需要 I2C 扩展。4. 采样触发与数据精度别让 ADC 白白浪费4.1 单次模式还是连续模式ADS1115 支持两种转换模式单次模式one-shot和连续模式continuous。理解两者的区别对你的功耗和实时性设计很关键。单次模式下你写配置寄存器时把 OS 位置 1芯片转换一次就进入掉电状态转换结果保存在寄存器里直到下次读或者重新配置。这个模式的优势是功耗低——ADS1115 单次转换时的电流大约几十微安转换完就睡了。适合电池供电的设备比如无线传感器节点。连续模式下芯片按照设定的数据速率反复转换不需要你每次触发适合需要持续监控的场景。但功耗要高一些因为转换电路一直在工作。在 MicroPython 里实现连续模式的代码更简单def read_continuous(self): # 配置寄存器MODE 位设为 0 config self._build_config(os_bit1) config ~(1 8) # 清除 MODE 位进入连续模式 self._write_config(config) time.sleep_ms(10) # 等前几个转换稳定 data self.i2c.readfrom_mem(self.addr, self.REG_CONVERSION, 2) raw (data[0] 8) | data[1] if raw 0x8000: raw - 0x10000 return raw连续模式要注意一个坑转换结果寄存器在每次转换完成后自动更新如果你读取的速度跟不上数据速率读到的可能是旧数据或者半新半旧的数据在更新过程中读。我的做法是先读配置寄存器的 OS 位确认新转换完成再读结果。单次模式天然没这个问题因为你每次读到的都是刚触发的那一次结果。4.2 采样率选择别被数据速率忽悠了ADS1115 的数据速率DR寄存器支持 8 SPS 到 860 SPS。听起来很美好但要注意数据速率设得越高有效分辨率越低因为噪声会变大。TI 数据手册里给出的有效分辨率ENOB数值我摘几个典型值数据速率有效分辨率PGA ±4.096V8 SPS16 位无噪声16 SPS15.5 位128 SPS14 位860 SPS12 位也就是说860 SPS 下你只是名义上有 16 位实际有效位只有 12 位。如果你的信号带宽不需要那么高用 8 SPS 或 16 SPS 能获得更干净的数据。选择数据速率的原则是满足信号最高频率成分的 2 倍以上奈奎斯特采样率即可不要盲目追求高 SPS。做温度监测8 SPS 完全够了处理 50Hz 工频干扰128 SPS 可以采样后做数字滤波如果是音频信号那别用 ADS1115换专用的音频 ADC 吧。4.3 PGA 增益配置与分压电阻的数学计算PGA 的配置决定了 ADC 的满量程输入范围同时也决定了 LSB 的电压分辨率。假设你接的是 ±4.096V 量程那么 LSB 4.096 / 32768 ≈ 125μV。如果信号幅度只有 10mVPGA 设置为 ±4.096V 时这个信号只占 80 个 LSB精度完全浪费。正确思路是把 PGA 调到信号幅度附近。比如称重传感器输出差分信号约 5mVPGA 设到 ±0.256V此时 LSB 0.256 / 32768 ≈ 7.8μV5mV 信号对应约 640 个 LSB精度提升显著。如果信号超过 PGA 量程怎么办用分压电阻把信号缩小。分压比的选择要兼顾两个因素一是保证最大输入信号不超出 PGA 量程二是尽可能充分利用 ADC 的量程范围。举个实际例子。我要测一个 0~12V 的电源电压ADS1115 的 VDD 是 3.3VPGA 设 ±4.096V。分压比选多少如果直接电阻分压最大 12V 对应输出不能超过 4.096V所以分压比至少为 4.096 / 12 ≈ 0.341。为了留余量我取分压比 0.25即上臂 75kΩ、下臂 25kΩ实际用 75kΩ 和 25kΩ 标准阻值或 100kΩ 和 33kΩ 近似。这样 12V 输入对应 3V 输出在 ±4.096V 量程内还有 26% 的余量。程序里换算时乘以 4 就是实际电压。还有个细节分压电阻的阻值不能太大否则源阻抗过高加上 ADS1115 的内阻和采样电容会引入误差。数据手册建议源阻抗小于几百欧姆最好但实际应用中分压电阻往往就是这个阻抗的来源。我常用的折中是 10kΩ 级别——信号传输线上的压降几乎可忽略采样误差也控制在 1% 以内。4.4 电压参考与校准你以为的精确其实不精确ADS1115 片内有一个基准电压源标称精度在 ±0.5% 左右具体看数据手册。这意味着你读数换算出来的电压值和实际电压之间可能差 0.5%。对大多数传感器采集场景这个误差可以接受但如果做高精度测量你需要校准。最简单的校准方法是用一个已知精度的万用表或电压源测一个稳定电压然后记录 ADS1115 的读数得到一个修正系数。校准公式为实际电压 ADS1115读数 × 修正系数 修正系数 标准表读数 / ADS1115读数在代码里维护一个calibration_factor变量换算电压时乘上即可。如果温度变化范围大最好在接近工作温度的条件下校准。有些项目甚至会在板载设计一个精准的参考通道比如用 TL431 或外部电压基准芯片接到一路输入程序启动时自动读取并计算修正系数这属于模拟电路设计的进阶技巧了。5. 滤波处理让数据从“能看”到“能用”5.1 噪声来源分析ADS1115 的噪声到底从哪里来提到滤波先得搞清楚噪声从哪来。我用示波器看过接 ADS1115 读取一个稳定电压时的输出码值在小范围内跳动是正常的。噪声来源主要有三类内部噪声ADC 内部的采样电容充放电、参考电压的噪声、开关电容滤波器带来的噪声这部分跟芯片本身的设计有关数据手册里会给出Noise vs Input Impedance的曲线。外部电磁干扰电源纹波、附近开关电源的高频噪声、长导线的天线效应这些都能耦合到模拟输入引脚上。混叠噪声如果输入信号中有频率高于采样率一半的成分比如开关电源的 300kHz 噪声会混叠到低频段变成你看到的随机抖动。针对内部噪声优化手段是降低数据速率或者用 PGA 的低增益档。针对外部干扰重点是硬件层面输入加 RC 低通滤波器、线缆用双绞线、PCB 布局时模拟地和数字地单点连接。软件滤波是最后一道防线下面分享几种实用的数字滤波方法。5.2 滑动平均滤波最基础但最实用滑动平均滤波的本质是取 N 次采样的平均值。优点是算法简单、响应速度快缺点是对脉冲型噪声比如偶发的尖峰干扰不敏感因为它会把尖峰平均掉但也会让尖峰影响扩散到 N 个点里。class MovingAverageFilter: def __init__(self, window_size10): self.window [] self.window_size window_size def update(self, value): self.window.append(value) if len(self.window) self.window_size: self.window.pop(0) return sum(self.window) / len(self.window)窗口大小怎么选如果采样率是 128 SPS你想要平滑 1 秒内的数据窗口就是 128。但窗口太大信号的瞬态变化会被抹平——比如监测电压突升滑动平均会让响应滞后。我的经验是对低频缓慢变化的信号温度、电池电压窗口取 10~50 即可如果信号本身有快速波动的需求窗口不要超过 5。5.3 中值滤波杀尖峰神器中值滤波的原理是取一段时间窗口内数据的中位数它对付脉冲噪声特别有效。比如采集电路中偶尔出现的开关噪声尖峰平均值会被拉偏但中位数不受少数异常点影响。class MedianFilter: def __init__(self, window_size5): self.window [] self.window_size window_size def update(self, value): self.window.append(value) if len(self.window) self.window_size: self.window.pop(0) sorted_window sorted(self.window) return sorted_window[len(sorted_window) // 2]窗口大小选奇数方便取中位数。窗口越大滤波越强但延迟也越大。我用中值滤波处理过一个温度探头的数据原本偶尔出现 -40°C 的假读数继电器吸合瞬间产生的干扰窗口设 5 后完全消除。5.4 一阶低通滤波指数加权平均延迟与平滑的平衡一阶低通滤波是很多嵌入式项目的首选因为它只需要一个小数参数 α计算开销极低适合在 MicroPython 这种解释型语言里做高频循环滤波。公式是滤波结果 α * 新采样值 (1 - α) * 上次滤波结果α 越大响应越快平滑效果越弱α 越小响应越慢平滑效果越强。α 的选择跟采样周期和截止频率有关。粗略的换算关系是α ≈ 采样周期 / (采样周期 截止时间常数)做电池电压监测采样周期是 100ms想要 10 秒的时间常数那 α ≈ 0.1 / (0.1 10) ≈ 0.01。注意这个公式的截止频率对应 -3dB 点实际效果可以先用这个值再根据波形微调。class LowPassFilter: def __init__(self, alpha0.1): self.alpha alpha self.last_value None def update(self, value): if self.last_value is None: self.last_value value else: self.last_value self.alpha * value (1 - self.alpha) * self.last_value return self.last_value5.5 组合滤波策略现实项目中我到底怎么配单种滤波都有缺陷实际项目中我一般用两级滤波组合先做中值滤波窗口 5剔除偶发尖峰。再做滑动平均或一阶低通抑制高斯白噪声和纹波。这个组合的内存开销很小计算量也低在 MicroPython 里跑 128 SPS 完全没有压力。如果采集的信号是温度这种超低频信号我会把采样率降到 8 SPS然后只做滑动平均窗口 8效果非常好——8 个点平均已经能压掉大部分噪声而且响应延迟只有 1 秒。需要强调的是软件滤波永远不能替代硬件滤波。如果一个信号的噪声大到超出 ADC 量程或者噪声频率极高比如 MHz 级别数字滤波是救不回来的必须在模拟前端加 RC 滤波或运放滤波。我的原则是硬件上做 80% 的努力软件滤波只负责收拾剩下 20% 的残局。6. 常见问题与排查技巧实录6.1 I2C 扫描不到设备这是最多人问的问题。i2c.scan()返回空列表第一步检查接线用万用表量 SCL 和 SDA 的对地电压正常应该在 3.3V 左右。如果量到 0V大概率是没接上拉电阻、接线松动或芯片没有供电。第二步检查地址。很多 ADS1115 模块默认 ADDR 引脚悬空但数据手册要求 ADDR 不能悬空悬空时内部下拉可能让你读到 0x48但有些模块设计有问题悬空时读到的是别的地址。我遇到过 ADDR 悬空扫描不到设备把 ADDR 焊接到 GND 后就正常的情况。第三步检查 I2C 频率。有些模块或杜邦线不靠谱400kHz 下通信失败把freq参数降到 100000 试试。如果 100kHz 下能工作说明是接线质量或上拉电阻的问题。还有一个容易被忽略的问题MicroPython 的 I2C 引脚定义必须是你硬件上真实连接的那两个引脚。ESP32 多数 GPIO 都能映射到 I2C但 Pico 的某些引脚没有 I2C 功能。查询你的开发板引脚复用表确认用的 SCL/SDA 支持 I2C 外设。6.2 读到的电压值误差过大如果你读到的电压和万用表实测差很多先确认 PGA 设置是否和输入信号幅度匹配。我调试时把 PGA 设成了 ±0.256V结果接了一个 1V 信号读数全部溢出还以为是芯片坏了。其次是分压电路问题。分压电阻的精度直接决定测量精度1% 精度的电阻引入的误差约 1%5% 的电阻误差更大。要求高的话用 0.1% 精度的电阻或者做软件校准。再有就是参考电压问题。ADS1115 内置参考电压的温漂大约在 10~20 ppm/°C 级别如果环境温度变化大读数漂移是正常的。要求极高的场景选带外部基准引脚的 ADC 或用参考电压芯片。6.3 读数抖动大读数抖动的排查思路按“外-内-软”的顺序来。先检查模拟输入线是否过长、是否与电源线并行走线输入端口是否加了 RC 滤波然后看看数据速率是不是设高了尝试降到 8 SPS最后在软件里加滤波。有一个我踩过的坑ESP32 开发板的 ADC 引脚和 I2C 引脚的电源是同一个 LDO 供电的当板载 WiFi 开启时电源噪声明显增大ADS1115 的读数也跟着跳。解决办法是给 ADS1115 单独用线性稳压供电或者在 WiFi 不工作的时间窗口采样。6.4 连续读取时偶尔读到 0xFFFF 或异常值这个现象通常是读取时序问题。在连续模式下如果主控读取转换寄存器时恰好芯片正在更新数据可能读到不稳定的值。解决方法是读取前先检查配置寄存器的 OS 位确认转换完成或者改用单次转换模式从根本上规避并发更新问题。另一个可能性是总线竞争。如果总线上还有其他设备且那个设备驱动 PEC 或重复起始条件时行为异常可能导致连续读失败。用示波器抓 SDA/SCL 波形能快速定位但我一般在代码层面先用i2c.scan()确认总线状态。6.5 总线锁死SDA 一直被拉低I2C 总线锁死是很多工程师的噩梦。现象是 SDA 一直为低所有通信全部失败。原因通常是某个从设备异常后一直占用总线。解决办法给 I2C 总线加复位机制在主控端周期性检测 SDA 状态如果检测到 SDA 在一段时间内持续为低就循环发送 9 个时钟脉冲SCL 翻转 9 次让从设备释放总线。MicroPython 里可以这样实现def recover_bus(scl_pin, sda_pin): # 检测 SDA 是否一直为低 scl Pin(scl_pin, Pin.OUT) sda Pin(sda_pin, Pin.IN) if sda.value() 0: # 发送 9 个时钟脉冲 for _ in range(9): scl.value(0) time.sleep_us(5) scl.value(1) time.sleep_us(5) scl.value(0)这个方法对应 I2C 标准的“总线清理”流程。实测下来90% 的总线锁死都能恢复。如果还不行就考虑是不是有设备硬件故障断电重启吧。7. 经验总结与扩展建议写到这里回头看看这个项目的核心价值MicroPython 让 ADS1115 的驱动开发门槛降到极低但精度和稳定性还是要靠对 I2C 协议、寄存器配置、采样策略和滤波算法的综合理解。这些知识不只在 ADS1115 上有用——你换了其他 I2C ADC比如 MCP3421、INA226思路完全一样就算换成 SPI 接口的 ADC比如 ADS1256滤波和校准的方法论依然成立。根据我的实操经验有几个点值得反复强调。第一硬件永远比软件重要特别是信号链路的布线和电源质量软件滤波只能雪中送炭不能锦上添花。第二单次转换模式 OS 位轮询是最稳的读取方式它在代码复杂度和功耗之间取得了最好的平衡。第三滤波参数的选取要跟信号的特征匹配千万不要套一组参数用到底。最后保存驱动代码时不只要保存源码最好把寄存器定义、计算公式、校准系数都写成注释因为三个月后的你一定会忘记当初为什么把 α 设为 0.1。如果你用这套方案做数据记录仪后续还可以加 SD 卡存储、Web 配置页面、甚至 MQTT 上报到物联网平台。MicroPython 生态里这些都是现成模块组合起来就能搭一个完整的监测系统。ADS1115 这个芯片本身只是项目的第一步但它能帮你把所有采集侧的地基打好。
返回列表