news 2026/9/6 10:30:47

基于MicroPython的ADS1115高精度ADC驱动与滤波实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于MicroPython的ADS1115高精度ADC驱动与滤波实践

如果你手里正好有一块ADC模块,又刚好在MicroPython环境里做数据采集,那这篇内容应该能帮你少走不少弯路。我这次整理的是ADS1115这颗16位ADC从硬件接线、I2C通信、寄存器驱动,到采样触发和滤波处理的完整实践记录。文章里所有代码我都在ESP32上实际跑过,遇到的问题也都会写在对应的小节里,方便你直接对照排查。

ADS1115是一颗四通道、16位分辨率、自带可编程增益放大器(PGA)的I2C接口ADC,量程和采样率都能软件配置,非常适合替代单片机内置ADC做高精度测量。MicroPython环境下,它的驱动逻辑不复杂,核心就是搞懂I2C读写、配置寄存器的位定义,以及如何根据信号特征选择合适的采样触发方式和滤波算法。这篇文章适合正在用ESP32、ESP8266、树莓派Pico做传感器采集、电池监测、电压测量或者任何需要稳定ADC读数的开发者,也适合刚入门I2C通信的朋友当作一份上手手册。

1. 方案选型与整体设计思路

1.1 ADS1115这颗芯片到底强在哪

先说结论:在很多场景下,MCU内置ADC被ADS1115替代,不是因为内置ADC不能用,而是因为精度和稳定性差距太明显。

拿ESP32内置的12位ADC举例,实测下来在3.3V供电环境下,采样电池电压或电位器分压时,即使信号很稳定,转换结果仍然会有一两个LSB的跳动,而且ESP32的ADC还受衰减档位、参考电压温漂和引脚串扰影响,在低电压区间存在比较明显的非线性。而ADS1115是16位、内置PGA、内部参考电压的精密ADC,在低速采样模式下有效分辨率能到15到16位,拿来测电压、电流传感器输出、称重传感器信号这类慢变信号,读数稳定很多。

ADS1115另一个实用特性是PGA可编程增益。板载PGA支持±6.144V、±4.096V、±2.048V、±1.024V、±0.512V、±0.256V六档量程。这意味着小信号不需要额外搭运放放大,直接配置寄存器把量程调低,精度就能提上来。比如测0到1V的信号,用±1.024V档位,LSB只有31.25微伏;如果傻乎乎用±6.144V档位,LSB是187.5微伏,精度差距六倍。这个特性对传感器信号采集特别关键。

再加上ADS1115的I2C接口只占两根线,数据速率最高支持3.4MHz(实际我一般跑100k到400k),地址可以通过ADDR引脚配置A0到A3四个地址,一条I2C总线上最多挂四个。低功耗模式待机电流不到1微安,非常适合电池供电的数据采集节点。

1.2 为什么选MicroPython而不是C

选MicroPython主要图两件事:开发效率和交互调试体验。

ESP32跑MicroPython,固件烧好后直接进REPL,可以一行一行测试I2C扫描、看寄存器返回值,不用每次都走编译烧录流程。我在开发采集模块的时候,先用MicroPython脚本验证硬件和寄存器配置,确认逻辑没问题,再决定要不要把同样算法移植到C或者Arduino里。MicroPython虽然运行速度不如C,但ADC采样本身由ADS1115芯片完成,MicroPython只是负责定时发起转换和读取结果。128SPS的采样率下,就算用Python写滤波算法,性能也完全够用。

MicroPython的machine库把I2C操作封装得很简洁。扫描线路上有哪些I2C设备,只需执行一句i2c.scan(),就能拿到所有七位地址。对新手来说,这种交互式排查方式比直接写固件然后上逻辑分析仪友好得多。

1.3 从传感器到数据的完整链路

ADS1115是一个ΔΣ(德尔塔-西格玛)型ADC,它的内部架构决定了采样率和噪声特性。信号从AIN引脚进入后,先经过输入多路复用器(MUX)选择通道,再进PGA放大器,然后被ΔΣ调制器转换成高频率的1比特流,最后经过片内数字抽取滤波器输出16位结果。这个数字抽取滤波器本质上是低通滤波,采样率配置越低,抽取倍数越高,有效分辨率越高。

所以整个信号链可以画成:模拟输入 → MUX选择 → PGA增益 → ΔΣ调制 → 数字抽取滤波 → I2C寄存器 → MCU软件处理。理解了这条链路,后面很多决策就清楚了:PGA决定量程和分辨率,数据速率(DR)决定噪声和响应速度,I2C寄存器负责数据传输,软件滤波负责处理残余噪声。这就是整篇内容的技术骨架。

2. I2C通信基础与硬件接线

2.1 I2C协议速览:地址、时序与上拉电阻

I2C是Philips(现在的NXP)发明的两线制串行总线,只有两根信号线:SCL(时钟)和SDA(数据)。设备分主设备和从设备,ADS1115永远是从设备,ESP32作为主设备发起通信。I2C的特点是所有设备共享两条线,靠地址区分设备,因此所有设备的SDA和SCL都必须工作在开漏模式,通过上拉电阻把线路拉到高电平。

一个完整的I2C传输包含起始条件(SCL高电平期间SDA拉低)、地址字节(7位地址加1位读写标志)、从设备ACK应答、数据字节、停止条件(SCL高电平期间SDA拉高)。地址字节中,最高位MSB先发送。ADS1115的七位地址由ADDR引脚电平决定。

上拉电阻阻值也值得说。常见做法是4.7kΩ上拉到VDD,如果总线上的设备数量和线缆长度增加,可以适当减小到2.2kΩ。上拉电阻过大会导致信号上升沿太慢,影响高速通信;过小则会增加功耗和灌电流。MicroPython初始化I2C时,freq=100000就是100kHz标准模式,freq=400000是快速模式。ADS1115在400kHz下完全没问题,但如果你同时挂了其他I2C传感器,建议先用100kHz排查稳定性,再提频率。

2.2 硬件接线与ADDR地址配置

接线其实就四根线,但很多人第一次就栽在这里。看我的接线表:

ADS1115引脚连接到
VDD3.3V(也可以接5V,芯片支持2.0~5.5V)
GND电源地
SCLMCU的SCL引脚
SDAMCU的SDA引脚
ADDRGND、VDD、SDA或SCL,决定I2C地址

很多人忽略ADDR引脚,模块默认贴片焊盘可能已经把它接GND了,所以地址一般是0x48。完整地址对照如下:

ADDR引脚连接到七位I2C地址(十六进制)
GND0x48
VDD0x49
SDA0x4A
SCL0x4B

如果一条总线上要接两个ADS1115,注意SDA和SCL不能同时作为ADDR电平来源,会冲突,一般第二片把ADDR接VDD,地址设为0x49。

接线时还要注意,ADS1115的模拟输入范围不能超过VDD + 0.3V。如果测量对象电压接近或超过供电电压,必须先做分压,否则芯片会损坏。另外,输入信号如果是双极性(比如交流信号),必须使用差分输入模式,而且负输入不能低于GND过多,否则ADC内部ESD保护二极管会导通。

2.3 MicroPython中I2C的初始化与地址扫描

MicroPython里初始化I2C有两种方式,一个是machine.I2C,这是硬件I2C外设,由芯片内部硬件电路控制时序,速度更快,不占用CPU。另一个是SoftI2C,也就是用GPIO模拟时序,可以结合任意两个引脚使用。我推荐优先使用硬件I2C,因为时序稳定,不容易出错。

ESP32开发板上,不同板卡的默认I2C引脚可能不一样。常见的是SCL=22、SDA=21,但ESP32-S3的板子有些默认是SCL=8、SDA=9,甚至有些合宙的板子引脚定义都不一样。所以别照抄别人的程序,先跑一下扫描脚本确认实际引脚。

from machine import Pin, I2C # 根据你的开发板实际情况修改引脚号 i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=100000) devices = i2c.scan() if devices: for addr in devices: print("I2C设备地址:", hex(addr)) else: print("未扫描到任何I2C设备")

如果这里返回0x48,说明硬件链路没问题。返回空列表,重点检查四根线、上拉电阻和ADDR引脚电平。这个脚本是最基础的诊断工具,我建议遇到任何I2C问题,第一步都先跑一遍。

2.4 硬件I2C与软件I2C如何选

machine.I2C对应硬件外设,支持中断、时钟拉伸等底层特性,速率精度高。SoftI2C通过GPIO模拟时序,优点是可以指定任意两个GPIO,缺点是时序精度依赖CPU负载,如果主循环里有大量重计算,软件I2C的时序可能被拉长,虽然不会出错,但实时性会变差。

我实际使用中还有个体验:软件I2C在某些涉及高频GPIO翻转的场合,和中断服务程序共用引脚时容易偶发时序错乱,而硬件I2C基本没遇到过。所以如果引脚够用,建议直接用machine.I2C。只有在硬件I2C外设被其他设备如OLED占用,而且你不想软件切换总线的时候,才考虑用SoftI2C跑ADS1115。

3. ADS1115寄存器级驱动实现

3.1 寄存器地图与字节序

ADS1115一共有四个寄存器,都是16位宽度。I2C读写在芯片内部按大端字节序传输,也就是高字节在前,Motorola字节序。四个寄存器如下:

寄存器地址读写属性作用
Conversion0x00只读存储最新ADC转换结果,16位有符号数
Config0x01读写配置MUX、PGA、模式、数据速率等
Lo_thresh0x02读写低阈值,用于比较器
Hi_thresh0x03读写高阈值

日常使用中,核心操作集中在Conversion和Config两个寄存器。转换结果寄存器读出的是16位有符号数,表示当前输入相对满量程的比例。Lo_thresh和Hi_thresh只有用到比较器功能时才需要设置。

因为MicroPython的int.from_bytes(data, 'big')默认返回无符号整数,所以读取转换结果后需要判断符号位。如果结果大于32767,说明是负数,需要减去65536做符号扩展。这是新手最容易漏掉的一步。

3.2 Config寄存器逐位拆解:OS、MUX、PGA、MODE、DR

Config寄存器是ADS1115的控制中枢,16个位各管一件事。逐个解释比背手册有用。

最高位OS(Bit15)是操作状态/启动转换位。在单次模式下,往这个位写1会启动一次转换,转换完成后硬件自动将该位清零。在连续模式下,OS位读出总是0。所以单次模式下轮询这个位,就能知道转换是否结束。

Bit14到Bit12是MUX输入多路选择字段,决定哪个引脚作为正负输入。最常用的是000,即AIN0对GND单端输入。差分模式下,100表示AIN0和AIN1差分,101表示AIN0和AIN3差分,110表示AIN1和AIN3差分。

Bit11到Bit9是PGA增益字段,决定满量程电压范围。这里给一个常用配置值:

PGA字段满量程FSR对应LSB电压(@16位)
000±6.144V187.5µV
001±4.096V125µV
010±2.048V62.5µV
011±1.024V31.25µV
100±0.512V15.625µV
101/110/111±0.256V7.8125µV

Bit8是MODE位,1表示单次模式,0表示连续模式。这个位容易记混,我刚用的时候也把1当成连续模式,结果程序一直不更新数据,排查半天。

Bit7到Bit5是数据速率(DR)字段,决定ADC每秒转换次数。它同时影响内部数字滤波器的截止频率和有效分辨率。低速率的平均效果更好,噪声更低,响应也更慢。

3.3 单次转换的完整驱动代码

单次转换流程很清晰:写Config触发一次转换,等待转换完成,读取Conversion寄存器。这种模式适合电池供电的场合,因为ADC在两次转换之间自动进入低功耗状态。

from machine import Pin, I2C import time ADS1115_ADDR = 0x48 REG_CONVERSION = 0x00 REG_CONFIG = 0x01 i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000) # AIN0对GND,PGA=±4.096V,单次模式,128SPS # 0x0200 = 0b0000 0010 0000 0000 # Bit14-12: 000,MUX=AIN0-GND # Bit11-9: 001,PGA=±4.096V # Bit8: 1,单次模式 # Bit7-5: 100,128SPS config = 0x0200 | (1 << 8) | (0b100 << 5) # 写配置寄存器,OS位 = 1 启动转换 config |= 0x8000 i2c.writeto_mem(ADS1115_ADDR, REG_CONFIG, config.to_bytes(2, 'big')) # 等待转换完成:轮询OS位 while True: cur = i2c.readfrom_mem(ADS1115_ADDR, REG_CONFIG, 2) cur_value = int.from_bytes(cur, 'big') # OS位为1表示转换完成 if cur_value & 0x8000: break time.sleep_ms(5) # 读取转换结果 data = i2c.readfrom_mem(ADS1115_ADDR, REG_CONVERSION, 2) raw = int.from_bytes(data, 'big') if raw > 32767: raw -= 65536 # 计算实际电压,FSR取4.096V voltage = raw * 4.096 / 32768.0 print("原始值:", raw, "电压:", voltage, "V")

注意这段代码里,我先把0x0200赋给config,然后单独写0x8000来置OS位。为什么不用一次写0x8200?其实两者效果相同,但拆开写更直观,方便你理解OS位的作用。

等待转换完成有两种方式,轮询OS位和固定延时。固定延时可以用time.sleep_ms(1000 // rate)估算,但轮询OS位更精确,既不会太早读数,也不会空等太久。128SPS下,一次转换大约需要7.8ms,轮询间隔5ms循环顶多两次就出结果。

3.4 连续模式与多通道轮询

单次模式适合低功耗场景,但如果你要连续监控电压变化,每次等轮询就有点浪费时间。连续模式下,ADS1115按照DR字段自动连续转换,主设备随时可以读取最新数据。

# AIN0对GND,PGA=±4.096V,连续模式,250SPS # 这里把MODE位(Bit8)设为0 config = 0x0200 | (0 << 8) | (0b101 << 5) i2c.writeto_mem(ADS1115_ADDR, REG_CONFIG, config.to_bytes(2, 'big')) while True: data = i2c.readfrom_mem(ADS1115_ADDR, REG_CONVERSION, 2) raw = int.from_bytes(data, 'big') if raw > 32767: raw -= 65536 voltage = raw * 4.096 / 32768.0 print("V:", voltage) time.sleep_ms(100)

连续模式启用的同时,传感器输入引脚可能存在多路复用的问题。如果要读取AIN0、AIN1、AIN2、AIN3四个通道,连续模式下切换MUX字段后,需要等待至少一个转换周期让新通道的转换结果稳定,否则读到的可能是上一通道的残余数据。

多通道采集更常见的做法是单次模式逐个通道采样:先切MUX到通道0,启动转换,读取;再切MUX到通道1,启动转换,读取。这样逻辑清晰,也不会串通道。缺点是整体吞吐率降低,四个通道轮询一遍的时间大约是单通道的四倍。

3.5 更工程化的驱动类封装

写了很多次ADC逻辑后发现,裸代码虽然易懂,但每次都要算一遍raw * FSR / 32768.0,而且增益字段和量程的映射关系经常记错。所以我建议封装一个简单的驱动类,把寄存器位的计算放到一个地方,调用处只传电压值习惯的接口。

class ADS1115: def __init__(self, i2c, addr=0x48): self.i2c = i2c self.addr = addr self.gain = 1 # 默认PGA=1,对应±4.096V def set_gain(self, gain): """gain: 0~6,对应±6.144V 到 ±0.256V""" self.gain = gain def read_adc(self, channel=0): """单次读取指定通道电压,channel: 0~3""" fsr_map = [6.144, 4.096, 2.048, 1.024, 0.512, 0.256, 0.256] fsr = fsr_map[self.gain] mux = 0x4000 if channel == 0 else 0x5000 | (channel << 12) # 这里只是示意,实际MUX字段映射见我上面表格 config = 0x8000 | (1 << 8) | (self.gain << 9) | (0b100 << 5) self.i2c.writeto_mem(self.addr, 0x01, config.to_bytes(2, 'big')) while True: cur = int.from_bytes(self.i2c.readfrom_mem(self.addr, 0x01, 2), 'big') if cur & 0x8000: break raw = int.from_bytes(self.i2c.readfrom_mem(self.addr, 0x00, 2), 'big') if raw > 32767: raw -= 65536 return raw * fsr / 32768.0

封装类的核心价值是把寄存器映射集中起来。后续加滤波、加定时采样,只需要在类外面组合即可,不破坏核心读取逻辑。

4. 采样触发设计:单次、定时中断与ALERT引脚

4.1 采样触发为什么值得单独设计

很多人做ADC采样,主循环里直接while True: read_adc(),表面看也能跑,但仔细想有两个问题。第一,读取频率完全取决于主循环执行速度,如果主循环里还有显示刷新、文件写入、网络请求,采样间隔会忽长忽短,对于需要固定采样间隔的频谱分析或波形重建就是灾难。第二,如果使用连续模式,不断更新的数据会被不规律地读取,无法保证与真实信号的时间关联性。

ADS1115没有外部硬件触发引脚,它的采样触发完全靠I2C寄存器操作和外部MCU的定时配合。所以设计采样触发时,要先明确需求:是低速定点采样(每秒读一次温度),还是中速连续波形采集(250SPS记录电压曲线),还是事件驱动(只在有转换完成信号时才去读)。三种需求对应三种不同触发器设计。

4.2 单次模式:软件触发写OS位

单次模式本身就是一个软触发机制。通过把OS位置1,芯片被唤醒并完成一次转换,转换完成后自动断电。这种模式天然适合低功耗采集节点。代码层面就是在第3.3节里那样,每次启动转换前重新写一次Config寄存器。

这种触发方式的好处是采样时刻可控,主设备说什么时候采就什么时候采。缺点是等待转换完成这段时间MCU不能干别的,除非你采用轮询加延时的方式,否则纯while循环会阻塞主程序。所以单次模式一般配合定时器中断来用。

4.3 定时器中断实现等间隔采样

如果希望采样间隔严格均匀,MicroPython的定时器是首选。以ESP32为例,用machine.Timer创建周期定时器,中断回调里只设置一个标志位,读取和滤波放到主循环里做。

from machine import Timer sample_ready = False def timer_cb(t): global sample_ready sample_ready = True tim = Timer(0) tim.init(period=100, mode=Timer.PERIODIC, callback=timer_cb) while True: if sample_ready: sample_ready = False # 这里读取ADS1115、滤波、显示等耗时操作 v = adc1115.read_voltage() process(v)

注意这里的关键设计:回调函数里不要直接执行I2C读取。因为MicroPython的定时器回调运行在中断上下文,I2C操作涉及时钟时序和等待应答,在中断里做会阻塞其他中断,严重时还会导致系统崩溃。正确做法是中断回调里只置标志位,主循环检测到标志后再执行耗时的I2C操作。这样虽然定时精度会受主循环耗时的轻微影响,但对绝大多数传感器采集场景已经足够。

如果系统需要更高精度的固定间隔采样,可以考虑用DMA方式或者直接在中断里做I2C读取。但这需要更底层的开发,MicroPython环境通常不建议,我也没有在实际项目中采用过。

4.4 用ALERT/RDY引脚做硬件触发

ADS1115的ALERT/RDY引脚是个容易被忽略的功能。默认情况下它作为比较器输出使用,但通过配置寄存器,可以让它在每次转换完成时输出一个短暂的低脉冲。把这个引脚接到MCU的外部中断输入,就能实现真正的转换完成事件驱动读取,MCU完全不用轮询等待。

配置方式是把Config寄存器的COMP_QUE字段(Bit1-0)设为00,即禁用比较器,此时ALERT/RDY脚会变为数据就绪输出。每次转换完成,它拉低一个周期,然后恢复高电平。外部中断设置为下降沿触发,在中断回调里读取转换结果。

from machine import Pin aler_pin = Pin(4, Pin.IN, Pin.PULL_UP) def alert_isr(pin): # 快速读取ADS1115,只做数据保存 data = i2c.readfrom_mem(0x48, 0x00, 2) raw = int.from_bytes(data, 'big') if raw > 32767: raw -= 65536 # 将结果放入队列或环形缓冲 buffer.append(raw) aler_pin.irq(trigger=Pin.IRQ_FALLING, handler=alert_isr)

中断回调里执行I2C读取虽然和定时器中断的建议相悖,但区别在于这个回调只做一次短暂的读取操作,并且ADS1115的转换结果在ALERT引脚拉低时已经稳定,不会等待时钟拉伸太久。实测在100kHz I2C频率下,读两个字节大约耗时几十微秒,在ESP32上可以接受。但我仍然建议只做数据入队,真正的滤波和显示放主循环。

三种触发方式对比:

触发方式优点缺点适用场景
单次模式软触发功耗低,实现简单阻塞等待,需要轮询低功耗定点采集
定时器中断触发采样间隔可控定时器回调需轻量化周期性数据采集
ALERT/RDY事件触发省CPU,事件驱动需要额外接线和中断支持高速连续采样或低延迟响应

5. 滤波处理:硬件降噪与软件算法

5.1 ADC噪声从哪里来

ADS1115毕竟是精密ADC,但噪声不可能完全消失。噪声来源主要有几类:一是电源纹波,如果VDD由MCU的3.3V LDO直接提供,而MCU又在驱动WiFi或LED,电源上会叠加周期性纹波。二是参考电压噪声,ADS1115内部参考电压已经做得不错,但外部电源质量仍会影响实际精度。三是输入引脚的电磁干扰,长导线像天线一样接收环境噪声。四是ΔΣ转换器固有的量化噪声和抖动。

解决噪声的思路是从源头到软件处理做多层过滤。硬件上尽可能减少电源纹波、合理布局布线;软件上根据信号特征选择滤波算法。这就像音频系统先做好电源和屏蔽,再用EQ调音,而不是靠后期拉高音量掩盖底噪。

5.2 利用ADS1115自身特性降噪:PGA与DR的配合

很多人开着默认的128SPS就采集,其实DR选多少直接影响噪声性能。ADS1115的数据手册里给出不同DR下的有效分辨率表格,DR越低,内部数字抽取滤波器的平均次数越多,输出噪声越小。8SPS时有效分辨率可以超过16位,而860SPS时有效分辨率大约只有14位。所以如果信号本身变化很慢,比如温度、电池电压、液位,用8SPS或16SPS就能获得更稳定的读数。

PGA档位的选择同样影响噪声。输入信号接近满量程时,量化噪声占比越小,信噪比越高。比如测量0到100mV的微弱信号,用±0.256V档位,信号可以占满约40%的ADC量程,远比用±6.144V档位得到的信号幅度大得多。所以选择PGA的原则是让目标信号尽量接近满量程,但不能超过满量程,否则输出会饱和。

给电路输入端加一个简单的RC低通滤波器也是性价比很高的方案。比如在AIN引脚串联1kΩ电阻,对地并联100nF电容,构成约1.6kHz的低通截止频率,能有效滤除高频噪声。注意R值不宜过大,ADS1115的输入阻抗虽然高,但输入电流和电阻相乘会引入误差。

5.3 软件滤波算法实战:滑动平均、中值滤波与一阶低通

软件滤波最常用的三种:滑动平均、中值滤波、一阶低通。下面给出可直接使用的MicroPython实现。

滑动平均是N个点取平均,能平滑随机噪声,但会引入滞后。窗口越大越平滑,滞后越大。

class MovingAverage: def __init__(self, size=8): self.size = size self.buf = [] def add(self, value): self.buf.append(value) if len(self.buf) > self.size: self.buf.pop(0) return sum(self.buf) / len(self.buf)

中值滤波是对窗口内数据排序取中间值,对尖峰脉冲干扰非常有效,适合剔除传感器偶发毛刺。

def median_filter(values): tmp = sorted(values) return tmp[len(tmp) // 2]

一阶低通滤波是递推算法,公式是y = alpha * x + (1 - alpha) * y_prev。alpha越小滤波越强,但响应越慢。这个滤波器实现简单,内存占用小,适合噪声比较均匀的场景。

class LowPassFilter: def __init__(self, alpha=0.2): self.alpha = alpha self.prev = None def add(self, value): if self.prev is None: self.prev = value else: self.prev = self.alpha * value + (1 - self.alpha) * self.prev return self.prev

实际使用中,我倾向于把滑动平均和一阶低通组合使用。ADC原始数据先过中值滤掉毛刺,再进滑动平均或低通平滑,效果比单一滤波器好很多。代价是延迟增加,不适合快速动态响应,但对大部分传感采集没影响。

5.4 滤波参数怎么选:结合采样率、信号频率与噪声特征

滤波参数没有万能答案,要根据信号和采样率来定。其中一个重要原则是滤波器截止频率要高于目标信号最高频率,同时低于噪声频率。比如采样率128SPS,信号本身是0.5Hz的慢变电压,那滑动平均窗口可以设16甚至32,因为信号周期只有0.5Hz,窗口32个点覆盖0.25秒,对信号不至于过度平滑。而如果信号是10Hz的振动波形,128SPS下每个周期只有12.8个采样点,滑动平均窗口就不能超过4,否则波形被压扁。

信号特征推荐滤波方式参数建议
直流慢信号(温度、电压)滑动平均 + 低通窗口16-32,alpha 0.1~0.2
低频动态波形(1-10Hz)滑动平均窗口4-8
有尖峰脉冲干扰中值滤波窗口3-5
均匀随机噪声一阶低通alpha 0.2~0.4

如果你是刚开始调,我建议先把原始数据的波动范围和噪声幅度打出来,再决定滤波手段。不要上来就写高级滤波器,先看噪声是白噪声还是脉冲,脉冲用中值,白噪声用平均。这是最实用的经验。

6. 常见问题排查与实战心得

6.1 地址扫描不到设备:先查线再查地址

扫描不到设备是最常见的问题。我的排查顺序是:第一,确认VDD和GND有电,万用表量一下ADS1115的VDD引脚电压。第二,确认SCL和SDA没有接反,这是新手最容易犯的错误。第三,确认ADDR引脚电平是否和扫描地址一致,如果ADDR悬空,有些模块内部有弱下拉会表现为0x48,但如果悬空状态受干扰,地址也可能不稳定,最好明确接GND或VDD。第四,检查总线上拉电阻是否正常,部分模块需要自己外接4.7kΩ上拉。

如果这些都没问题,可以降低I2C频率试试,我遇到过某些模块在400kHz下不稳定,降到100kHz就正常了。

6.2 读数恒为0或电压值明显偏低

读数恒为0,先看通道配置是否对。特别是用连续模式的时候,如果Config寄存器里MUX字段写错,实际读取的通道和接线不是同一个,自然读不到信号。另一个可能原因是输入信号为负电压而模式是单端,单端模式无法处理负压,输出会钳位到0。如果输入信号超过PGA量程,输出会饱和到0x7FFF,不会显示0,但换算出来的电压会比实际偏低,比如2.5V信号跑到±2.048V档位,就会显示2.048V满量程而不是实际电压。

检查PGA档位是否匹配信号范围,这是偏低问题的关键。尽量先降低PGA增益(增大量程)测量真实信号幅度,再选择合适的档位。

6.3 噪声大、数据跳变:从硬件滤波和DR入手

数据跳动的排查顺序是从硬件到软件。先看VDD电源,如果MCU驱动WiFi或电机,电源纹波会直接串到ADS1115的输入。可以在VDD引脚加一个10µF电解电容和100nF陶瓷电容去耦。再看输入信号线,尽量用屏蔽线或双绞线,避免长距离平行走线。然后把DR调低,比如从250SPS降到16SPS,有效分辨率立刻提升,噪声肉眼可见减少。最后才用软件滤波。

6.4 I2C总线挂死:怎么恢复和预防

MicroPython在调试时经常遇到I2C总线挂死,表现是sda.scan()返回空,或者程序卡在某次读写上。原因常常是某个从设备在通信中途掉线,或者主设备的I2C状态机没复位。解决办法是先让总线复位:把SCL引脚连续输出9个时钟脉冲,很多挂在半途的从设备会被唤醒并释放SDA。在MicroPython里可以手动操作GPIO模拟这个脉冲,或者直接把MCU复位,最省事。

预防挂死的方法是:确保所有I2C设备的VDD先上电再通信,不要在设备没上电的时候发数据;把I2C频率保持在稳定的范围内;不要在设备通信中途插拔线路。另外,如果总线上挂了多个设备,ADS1115地址冲突也会导致通信异常,先用扫描脚本确认所有地址都唯一。

6.5 我个人在实际操作中的体会

最后分享几个零散但实用的经验。第一,千万别在主循环里无条件一直读ADS1115连续模式的数据,至少加个短暂的延时或定时器控制采样节奏,否则MCU可能被I2C读取占满,导致WiFi和定时任务卡顿。第二,用MicroPython做ADC数据采集时,建议给每个采集结果加上时间戳,使用time.ticks_us()获取微秒级时间,后续做曲线分析和数据对比会方便很多。第三,如果你后续想把这套方案扩展成多通道数据记录仪,可以把ADS1115读取逻辑和滤波逻辑彻底分离,读取类只管拿数据,滤波类只做信号处理,这样加OLED显示、SD卡存储都不会互相纠缠。

这套组合我已经在好几个项目里用了,最近还把它扩展成了ESP32-S3加OLED的迷你电压记录仪,实时画曲线并写入CSV文件,核心逻辑仍然是这里介绍的寄存器操作和滤波框架。ADS1115这颗芯片加MicroPython,做中低速精密采样是成本最低、见效最快的方案,值得动手试一次。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/6 10:30:35

用嵌入式开发DIY美人鱼主题智能手链:硬件拆解与制作指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 10:25:47

深入CMSIS-DSP:从源码审计到工业固件落地的完整实践指南

1. 这套库到底解决什么问题&#xff1a;从项目背景和架构全景说起如果你在 Cortex-M 内核上做过任何形式的数字信号处理&#xff0c;比如 FIR/IIR 滤波、FFT 频谱分析、矩阵求逆&#xff0c;或者 PID 控制器里的微分项平滑&#xff0c;那你大概率已经听说过 CMSIS-DSP。这是 AR…

作者头像 李华
网站建设 2026/9/6 10:25:03

AMS2471J铝合金硫酸阳极氧化标准解读:工艺要求与实用要点

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 10:23:01

UL 510A胶带选型全解析:从认证标准到产线品控实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 10:16:54

RK3588边缘AI推理帧率优化:从5fps到30fps的完整链路实战

做边缘端视觉开发的人&#xff0c;大概都经历过这种"帧率幻觉"&#xff1a;拿到一块RK3588开发板&#xff0c;兴冲冲把YOLOv8的模型导成RKNN格式&#xff0c;对着官方标注的6 TOPS NPU算力&#xff0c;觉得跑个轻量检测模型怎么着也得50帧往上。但真把摄像头接上、加…

作者头像 李华
网站建设 2026/9/6 10:15:12

稻百年研学感悟|深耕文化自信,看见中国企业的未来出路

受访人&#xff1a;朱新军 山东好吉诺机械科技有限公司&#xff0c;山东高密分塾理事长 此次跟随稻百年走进胖东来研学学习&#xff0c;下车伊始、身临其境&#xff0c;我内心就深受触动&#xff0c;也对中国民营企业经营、传统文化赋能企业发展&#xff0c;有了更深刻、更笃定…

作者头像 李华