1. 项目概述:为什么选择 MicroPython + ADS1115 这套组合
我最早接触 ADS1115 是因为一个电池电压监测项目。当时用的开发板自带 ADC 只有 12 位,测 3.7V 锂电池时分辨率只能到毫伏级别,勉强够用,但换到微弱信号采集(比如电流互感器输出、电桥传感器)就完全顶不住了。后来换成 ADS1115,16 位分辨率、内置可编程增益放大器(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 协议核心要点回顾
I2C(Inter-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 位) |
|---|---|---|---|
| 接 GND | 0x48 | 0x90 | 0x91 |
| 接 VDD | 0x49 | 0x92 | 0x93 |
| 接 SDA | 0x4A | 0x94 | 0x95 |
| 接 SCL | 0x4B | 0x96 | 0x97 |
MicroPython 的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.3V,5V 主控也能识别,但要注意主控 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 15(OS):单次转换启动位。写入 1 后启动一次转换;在连续模式下,读取时此位表示转换是否完成(1 表示完成)。
- Bits 14-12(MUX):输入多路复用器选择。000 表示 AIN0-AIN1 差分,001 表示 AIN0-AIN3 差分,010 表示 AIN1-AIN3 差分,011 表示 AIN2-AIN3 差分,100 表示 AIN0 对 GND,101 表示 AIN1 对 GND,110 表示 AIN2 对 GND,111 表示 AIN3 对 GND。
- Bits 11-9(PGA):可编程增益放大。000 表示 ±6.144V,001 表示 ±4.096V,010 表示 ±2.048V,011 表示 ±1.024V,100 表示 ±0.512V,101 表示 ±0.256V,110 表示 ±0.256V(同 101),111 表示 ±0.256V(同 101)。
- Bit 8(MODE):数据速率模式下是连续转换(0)还是单次模式(1)。
- Bits 7-5(DR):数据速率。000 表示 8 SPS,001 表示 16 SPS,010 表示 32 SPS,011 表示 64 SPS,100 表示 128 SPS,101 表示 250 SPS,110 表示 475 SPS,111 表示 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, address=0x48): 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_bit=1): 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_bit=1) 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) # 100=AIN0, 101=AIN1, 110=AIN2, 111=AIN3 return self.read_voltage()这段代码有几个值得注意的设计点。read_raw()中我在单次模式下启动转换后,通过轮询配置寄存器的 OS 位来判断转换是否完成,而不是简单地sleep一个固定时间。原因是数据速率不同导致转换时间差异很大(8 SPS 要 125ms,860 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, scl=Pin(22), sda=Pin(21), freq=400000) # ESP32 示例 ads = ADS1115(i2c, address=0x48) # 扫描总线上的设备 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 总线上可以挂多个设备,只要地址不冲突就行。比如同时挂 ADS1115(0x48)和 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_bit=1) 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.096V) |
|---|---|
| 8 SPS | 16 位(无噪声) |
| 16 SPS | 15.5 位 |
| 128 SPS | 14 位 |
| 860 SPS | 12 位 |
也就是说,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。如果信号幅度只有 10mV,PGA 设置为 ±4.096V 时,这个信号只占 80 个 LSB,精度完全浪费。
正确思路是把 PGA 调到信号幅度附近。比如称重传感器输出差分信号约 5mV,PGA 设到 ±0.256V,此时 LSB = 0.256 / 32768 ≈ 7.8μV,5mV 信号对应约 640 个 LSB,精度提升显著。
如果信号超过 PGA 量程怎么办?用分压电阻把信号缩小。分压比的选择要兼顾两个因素:一是保证最大输入信号不超出 PGA 量程,二是尽可能充分利用 ADC 的量程范围。
举个实际例子。我要测一个 0~12V 的电源电压,ADS1115 的 VDD 是 3.3V,PGA 设 ±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_size=10): 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_size=5): 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, alpha=0.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 这个芯片本身只是项目的第一步,但它能帮你把所有采集侧的地基打好。