MicroPython+ADS1115实战:从I2C驱动到滤波算法
2026/9/6 10:38:15 网站建设 项目流程

直到今天,我抽屉里还躺着一块被我换下来的ESP32开发板。当时想做一个12V电池电压监测,用分压电阻把电压压到3.3V以内,再用MicroPython直接读内置ADC,结果读数来回跳,波动能到±80mV。后来换上外置16位ADC芯片ADS1115,走I2C总线读取,同一路信号采回来稳定得让我怀疑自己是不是读错了变量。这篇笔记就是我当时边翻数据手册边调驱动,把I2C通信、驱动封装、采样触发和滤波处理全走通之后攒下来的一份MicroPython + ADS1115实战手册。

它适合谁?用MicroPython做传感器采集、觉得内置ADC不够用的开发者,或者想在ESP32、ESP8266、RP2040这类板子上稳定采集多路模拟信号的玩家。ADS1115这颗芯片很便宜,常见模块十几块钱,四路单端输入、一路差分输入,16位分辨率,八百多种采样速率可选,还能靠I2C一条总线上挂四个,拿来当通用模拟量采集前端非常合适。整篇文章不绕弯子,从硬件接线一路讲到驱动代码、触发方式和滤波算法,你跟着做基本能直接抄作业。

1. 为什么我最终还是绕不开 ADS1115:选型逻辑与芯片关键参数

1.1 ESP32 内置 ADC 不够用吗

不是不能用,是分场景。ESP32 内置 ADC 是 12 位,听起来还行,但实际有效位数通常打折扣,尤其在接近量程两端时非线性非常明显。做电池电压监测这种需要一定绝对精度的场景,你会发现同一个电压,昨天读出来 3.26V,今天换了一个供电方式变成 3.31V,数据本身就是飘的。而且 ESP32 的 ADC 参考电压在不同批次芯片之间存在差异,手册里也写得比较保守,你要是想做校准,得逐个板子标定,麻烦。

另外 ESP32 内置 ADC 的输入阻抗有限,直接接高内阻的信号源会分压,读数偏低。最典型的就是你拿一个 10k 电位器分压去测,采集端一接上去,电压就掉一截,怎么算都不对。ADS1115 的输入阻抗则高得多,对信号源的影响小,这点在测电池电压、电化学传感器这类信号时特别关键。

当然不是说内置 ADC 没用,做简单的按键检测、粗略的电量显示,它完全够。但当你需要可重复、可标定、多通道、稳定采集时,外置 ADC 是更省心的选择。

1.2 ADS1115 在看家本领上到底强在哪里

ADS1115 是德州仪器的 16 位 ΔΣ ADC,带内部基准电压源、可编程增益放大器(PGA)和 I2C 接口。几个关键特性直接决定了它能干多少活:

  • 16 位分辨率,双极性输出,输出格式是二进制补码,范围 -32768 到 +32767。
  • 可编程增益 PGA 共 8 档,从 ±6.144V 到 ±0.256V,小信号可以直接放大,不用外部再加运放。
  • 数据速率 8 SPS 到 860 SPS 可调,低速率下内部数字滤波器能压制更多噪声。
  • 四路模拟输入 AIN0 ~ AIN3,支持四路单端或者两路差分,用寄存器配置切换。
  • 内置比较器,可以设置高/低阈值,超过阈值时 ALERT/RDY 引脚输出信号,适合做低功耗报警。
  • 工作电压 2.0V ~ 5.5V,连续转换模式电流约 150µA,单次模式更低,电池供电也能接受。
  • I2C 地址由 ADDR 引脚决定,0x48 到 0x4B 四个可选地址,一条 I2C 总线上最多挂四个。

不同 PGA 档位对应的量程和每个 LSB 代表的电压如下表:

PGA 设置量程(双极)1 LSB 电压
2/3±6.144V187.5µV
1±4.096V125µV
2±2.048V62.5µV
4±1.024V31.25µV
8±0.512V15.625µV
16±0.256V7.8125µV

注意量程标注的是双极范围,也就是说输入允许正负电压。测单端信号时更关心的是正向满量程,比如 PGA=1 时,单端输入范围实际是 0 到 4.096V。

1.3 引脚接线和最小系统

ADS1115 本身有 TSSOP 或 QFN 封装,市面上买到的大多是带引脚的小模块,模块上一般已经把上拉电阻、电源滤波电容都画好了,直接插面包板就行。但如果你是用的裸片,接线要仔细。

标准接法是这样的:

  • VDD 接 3.3V,如果你的主控是 5V 逻辑,VDD 接 5V 也行,但要注意 SDA/SCL 电平匹配,最好还是让主控和 ADC 供电一致。
  • GND 接 GND,注意模拟地和数字地最终要共地,信号源的地也要和 ADC 共地。
  • SCL 接主控的 I2C 时钟引脚,SDA 接主控的 I2C 数据引脚。
  • ADDR 接地,地址就是 0x48。如果接 VDD 是 0x49,接 SDA 是 0x4A,接 SCL 是 0x4B。
  • AIN0 ~ AIN3 为模拟输入,接被测信号。
  • SDA、SCL 各接一个 4.7k 上拉电阻到 3.3V。模块上一般已经有,裸片必须自己加,否则 I2C 通信不稳定甚至完全不通。
  • ALERT/RDY 引脚可选,做比较器报警或转换完成指示时用。

硬件上最容易踩的坑是 SDA 和 SCL 接反、忘记共地、以及上拉电阻缺失。这三件事占了 I2C 通信失败的绝大多数原因。

2. I2C 通信的第一性原理与 MicroPython 寄存器读写

2.1 I2C 为什么不需要片选引脚

I2C 最妙的设计是省掉了片选。SPI 每个设备要一根 CS 线,设备多了引脚不够用,而 I2C 总共只要两根线:SCL 时钟线、SDA 数据线。所有设备都挂在同一条总线上,靠 7 位地址来点名通信。

它的工作原理可以类比成一条走廊里大家共用一部电话,SCL 是振铃节奏,SDA 是说话内容。总线空闲时 SDA 和 SCL 都是高电平,主机发送一个起始条件(SCL 为高时 SDA 从高变低),然后发出设备地址加读写位。地址匹配的从设备会拉低 SDA 回应一个 ACK,表示"我在,请继续"。之后每个字节的传输,SDA 数据在 SCL 高电平期间必须保持稳定,SCL 低电平期间才允许改变。通信结束时主机发送停止条件(SCL 为高时 SDA 从低变高)。

这里有个反直觉的点:地址是 7 位,比如 ADS1115 的地址 0x48,其实是 7 位的二进制 1001000,左移一位加上读写位之后才是线上传输的那个字节。你在 MicroPython 里写地址时直接写 0x48 就行,库帮你做了移位,不需要自己处理。

2.2 MicroPython 的 I2C API 和总线初始化

MicroPython 的machine.I2C把上面这些时序细节全部封装了,你只需要告诉它用哪个 GPIO 当 SCL、哪个当 SDA、频率多少。

以 ESP32 为例:

from machine import Pin, I2C i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000) print(i2c.scan())

i2c.scan()会扫描总线上所有设备,返回一个地址列表。如果 ADS1115 地址是 0x48,这里会输出[72],因为 0x48 的十进制就是 72。

有几个细节需要注意:

  • freq=400000是快速模式,实测稳定。但如果你的连接线比较长、或者上拉电阻偏大,把频率降到 100000 会更稳。
  • ESP32-S3 上 I2C 引脚不固定,不同开发板的默认引脚可能不一样,最好先看板子丝印或者原理图确认。
  • 如果你的 MicroPython 版本不支持I2C(0, ...)这种写法,可以试试SoftI2C(scl=..., sda=..., freq=...)
  • 总线上如果同时挂了 OLED、温湿度传感器和 ADS1115,scan 会把所有地址都列出来,这是排查连线问题的最快方式。

2.3 用寄存器原语直接读写 ADS1115

ADS1115 的寄存器就那么几个,用 MicroPython 的writeto_memreadfrom_mem就可以直接操作:

  • REG_CONVERSION = 0x00:转换结果寄存器,16 位,只读。
  • REG_CONFIG = 0x01:配置寄存器,16 位,可读可写。
  • REG_LO_THRESH = 0x02:低阈值寄存器,供比较器用。
  • REG_HI_THRESH = 0x03:高阈值寄存器,供比较器用。

先读一次配置寄存器看看默认值:

cfg_raw = i2c.readfrom_mem(0x48, 0x01, 2) cfg = int.from_bytes(cfg_raw, 'big') print(hex(cfg))

再写配置寄存器触发一次单次转换,比如设成:单次模式、PGA=±4.096V、128SPS、AIN0 对 GND、禁用比较器。对应配置值 0x8583:

i2c.writeto_mem(0x48, 0x01, (0x8583).to_bytes(2, 'big'))

写完之后等一段时间,再读转换结果:

raw = i2c.readfrom_mem(0x48, 0x00, 2) value = int.from_bytes(raw, 'big') if value & 0x8000: value -= 0x10000 # 转成带符号数 print(value)

这一步的带符号处理很重要。ADS1115 输出的是 16 位二进制补码,比如0xFFFF表示 -1,如果不做符号扩展会变成 65535。判断最高位是否为 1,是的话减去 65536,就得到了正确的负数。

读到的value要乘上 1 LSB 对应的电压,才是真正的电压值。PGA=±4.096V 时 LSB 是 125µV,所以voltage = value * 0.000125

2.4 设备扫不到怎么办

i2c.scan()后返回空列表,这是新手最常遇到的问题。按顺序排查:

  1. 确认 ADDR 引脚的接法,确定地址。ADDR 悬空时内部有下拉,一般默认还是 0x48,但建议明确接 GND。
  2. SDA 和 SCL 是不是接反了,这是最高频的失误。
  3. 有没有上拉电阻。模块上有,面包板上裸片必须自己加 4.7k 上拉到 VDD。
  4. 供电是否正常,用万用表量 VDD 和 GND 之间电压,芯片不发烫。
  5. 把 I2C 频率降到 100kHz 再试,线长或者干扰大时 400kHz 可能不稳定。
  6. 换一对 GPIO 引脚做软件 I2C,排除硬件引脚损坏或者引脚被占用的可能。
  7. 多设备总线冲突时,逐个断开其余设备,只留 ADS1115 再 scan。

3. 驱动代码怎么写:从裸寄存器到可复用的类

3.1 配置寄存器的 16 个 Bit 逐一拆解

ADS1115 的配置寄存器只有 16 位,但每一位都有明确含义,完全搞懂它,驱动代码就是填数游戏。位分布如下:

Bit字段作用
15OS单次转换启动/状态:写 1 启动一次转换,读置 1 表示转换中,读 0 表示转换完成
14-12MUX输入多路复用器选择,决定用哪一路通道
11-9PGA可编程增益放大器的增益设置
8MODE0 为连续转换模式,1 为单次模式
7-5DR数据速率选择
4COMP_MODE比较器模式选择,默认 0
3COMP_POL比较器输出极性,默认 0
2COMP_LAT比较器锁存功能,默认 0
1-0COMP_QUE比较器触发次数设置,写 11 表示禁用比较器

MUX 的取值对照:

MUX 设置输入通道测量方式
000AIN0-AIN1差分
001AIN0-AIN3差分
010AIN1-AIN3差分
011AIN2-AIN3差分
100AIN0-GND单端
101AIN1-GND单端
110AIN2-GND单端
111AIN3-GND单端

PGA 和 DR 的取值在前面已经列过,这里不再重复。综合起来,一个完整的配置值就是把这些字段按位或起来。比如:单端 AIN0、PGA=4.096、单次模式、128SPS、禁比较器,就是0x4000 | 0x0200 | 0x0100 | 0x0080 | 0x0003 = 0x4583,如果要启动转换再加0x8000,得到0xC583。这个值就是给配置寄存器写入的内容。

3.2 把寄存器操作封装成类

直接用writeto_mem裸操作没问题,但工程上还是封装成类更舒服,尤其多通道、多速率切换的时候。下面这个类我实测过,可以直接复用:

from machine import I2C, Pin import time class ADS1115: REG_CONV = 0x00 REG_CONFIG = 0x01 # 通道选择 MUX_01 = 0x0000 # 差分 AIN0-AIN1 MUX_03 = 0x1000 MUX_13 = 0x2000 MUX_23 = 0x3000 MUX_0G = 0x4000 # 单端 AIN0-GND MUX_1G = 0x5000 MUX_2G = 0x6000 MUX_3G = 0x7000 # 增益 PGA_6144 = 0x0000 PGA_4096 = 0x0200 PGA_2048 = 0x0400 PGA_1024 = 0x0600 PGA_0512 = 0x0800 PGA_0256 = 0x0A00 # 数据速率 RATE_8 = 0x0000 RATE_16 = 0x0020 RATE_32 = 0x0040 RATE_64 = 0x0060 RATE_128 = 0x0080 RATE_250 = 0x00A0 RATE_475 = 0x00C0 RATE_860 = 0x00E0 MODE_CONT = 0x0000 # 连续转换 MODE_SINGLE = 0x0100 # 单次模式 COMP_DIS = 0x0003 # 禁用比较器 def __init__(self, i2c, addr=0x48, pga=PGA_4096, rate=RATE_128, mode=MODE_SINGLE, mux=MUX_0G): self.i2c = i2c self.addr = addr self._mux = mux self._pga = pga self._rate = rate self._mode = mode # 1 LSB 对应的电压 = 满量程 / 32768 full_scale = { self.PGA_6144: 6.144, self.PGA_4096: 4.096, self.PGA_2048: 2.048, self.PGA_1024: 1.024, self.PGA_0512: 0.512, self.PGA_0256: 0.256, }[pga] self._lsb = full_scale / 32768.0 self._base_cfg = mux | pga | rate | mode | self.COMP_DIS self._write_config(self._base_cfg) def _write_config(self, cfg): self.i2c.writeto_mem(self.addr, self.REG_CONFIG, cfg.to_bytes(2, 'big')) def _read_config(self): raw = self.i2c.readfrom_mem(self.addr, self.REG_CONFIG, 2) return int.from_bytes(raw, 'big') def start_conversion(self): # 单次模式下写 OS 位启动转换 self._write_config(self._base_cfg | 0x8000) def is_ready(self): # OS 位为 0 表示转换完成 return (self._read_config() & 0x8000) == 0 def read_raw(self): raw = self.i2c.readfrom_mem(self.addr, self.REG_CONV, 2) value = int.from_bytes(raw, 'big') if value & 0x8000: value -= 0x10000 return value def read_voltage(self): return self.read_raw() * self._lsb def read_single(self): self.start_conversion() while not self.is_ready(): time.sleep_ms(1) return self.read_voltage() def set_mux(self, mux): self._mux = mux self._base_cfg = mux | self._pga | self._rate | self._mode | self.COMP_DIS if self._mode == self.MODE_SINGLE: self._write_config(self._base_cfg | 0x8000) else: self._write_config(self._base_cfg) def set_rate(self, rate): self._rate = rate self._base_cfg = self._mux | self._pga | rate | self._mode | self.COMP_DIS self._write_config(self._base_cfg)

使用起来非常直观:

i2c = I2C(0, scl=Pin(22), sda=Pin(21), freq=400000) adc = ADS1115(i2c) while True: print(adc.read_single()) time.sleep_ms(500)

3.3 为什么 LSB 要用满量程除以 32768,而不是 65536

这是很多人第一次接触时会困惑的点。ADS1115 是双极性输出,16 位补码里,正数范围是 0 到 +32767,负数范围是 -1 到 -32768。整个量程跨了 65536 个码,但如果我们用满量程电压 4.096V 除以 32768,得到的 125µV 才是每个码代表的电压。

验证一下:正满量程 32767 乘 125µV 是 4.095875V,接近 4.096V;负满量程 -32768 乘 125µV 是 -4.096V。所以用满量程电压除以 32768 是对的。千万不要除以 65536,那样算出来电压会差一倍。

3.4 多通道采集的正确打开方式

四路单端输入时会做通道巡检。核心要点是:切换 MUX 后必须重新触发转换,然后等待转换完成再读。

def read_all_channels(adc): channels = { 'AIN0': ADS1115.MUX_0G, 'AIN1': ADS1115.MUX_1G, 'AIN2': ADS1115.MUX_2G, 'AIN3': ADS1115.MUX_3G, } result = {} for name, mux in channels.items(): adc.set_mux(mux) result[name] = adc.read_single() return result

这里有个容易忽略的坑:写配置寄存器本身就是一次 I2C 操作,写入后如果立刻去读转换结果,芯片可能还没开始转换,读到的是旧值。所以要么用is_ready()轮询 OS 位,要么 sleep 到转换时间以上。128SPS 下转换周期约 7.8125ms,我习惯等 10ms,稳妥。

4. 采样触发:单次模式和连续模式到底怎么选

4.1 单次模式完整流程

单次模式的英文叫 single-shot,意思是每次只转换一次,转换完自动进入低功耗状态。流程是这样的:

  1. 配置寄存器里 MODE 位置 1,OS 位置 1,写入配置寄存器。
  2. ADC 开始转换,此时读配置寄存器,OS 位为 1。
  3. 转换完成,OS 位自动清 0,转换结果写入转换寄存器。
  4. 读转换寄存器,得到结果。
  5. 下一次转换需要重新写 OS 位触发,否则读到的一直是上一次的值。

代码里对应的就是start_conversion()is_ready()轮询:

adc.start_conversion() while not adc.is_ready(): time.sleep_ms(1) voltage = adc.read_voltage()

单次模式最大的优势是省电。8SPS 下,每 125ms 只工作几毫秒,其余时间芯片电流极低。这对电池供电的采集节点很重要。但是需要注意,MicroPython 在等待转换的时候如果只是time.sleep_ms,主控并没有休眠,整体功耗还要看你主控的策略。

4.2 连续模式:数据就绪判断与轮询策略

连续模式下,配置寄存器 MODE 位置 0,芯片上电或配置完成后一直循环转换,每隔 1/rate 秒刷新一次转换寄存器。读取时直接读转换寄存器就行,不用等 OS 位。

adc = ADS1115(i2c, mode=ADS1115.MODE_CONT, rate=ADS1115.RATE_128) while True: voltage = adc.read_voltage() print(voltage) time.sleep_ms(10)

连续模式要注意数据同步问题。如果读的速度跟不上转换速度,你会跳着读数。比如 860SPS 下,约 1.16ms 就更新一次,MicroPython 里一次 I2C 读取加上 print 打印可能就要几十毫秒,这期间转换寄存器已经被刷新很多次了,你读到的只是某个瞬间的值。

还有一种情况是读到的值和上次完全一样,这不是芯片坏了,而是转换太快,你两次读取之间芯片已经完成了一轮转换,但结果恰好相同,或者你读寄存器时正好赶在芯片写寄存器之前。

判断新数据最可靠的方式是用 ALERT/RDY 引脚。ADS1115 可以配置成在每次转换完成时,在 ALERT/RDY 引脚上输出一个低电平脉冲。把这个引脚接到主控 GPIO,用外部中断或轮询 GPIO 判断数据就绪,就能精确采到每个点。

4.3 工程选型建议

不同场景用什么模式,我列一张表:

场景推荐模式原因
电池供电的温湿度采集节点单次功耗最低,每秒采一次足够
连续波形监测,如振动信号连续 + ALERT/RDY 中断不丢点,时序准确
多通道巡检单次 + 切换 MUX连续模式切通道也会打断转换,不如单次灵活
超限报警连续 + 比较器阈值ALERT/RDY 引脚硬件输出,主控可以睡觉
低速环境监测单次 + 长时间睡眠配合主控 deep sleep 可以把功耗做到极低

ADS1115 的数据速率上限是 860SPS,做音频、振动这类需要上 kHz 采样的项目不适合它,应该换 ADS1015(3.3kSPS)或者更高速的 ADC。但做温度、压力、电池电压、光照强度这些缓变信号,它的性能完全够用。

4.4 ALERT/RDY 引脚的"准中断"读法

想彻底摆脱轮询等待,可以把 ALERT/RDY 配置成转换完成指示。做法是在配置寄存器里把比较器相关位设置为特定模式,让 ALERT/RDY 引脚在每次转换完成后输出低电平脉冲,主控 GPIO 检测到下降沿时再发起 I2C 读取。这样:

  • 主控不需要反复读配置寄存器查 OS 位。
  • 转换完成的时刻和读取操作之间的延迟最小。
  • 连续模式下不漏点。

MicroPython 里可以用Pin的外部中断来响应:

from machine import Pin rdy = Pin(25, Pin.IN, Pin.PULL_UP) def on_rdy(pin): voltage = adc.read_voltage() print(voltage) rdy.irq(trigger=Pin.IRQ_FALLING, handler=on_rdy)

注意:中断回调里执行 I2C 读取是有讲究的,有些平台的 I2C 在中断上下文里可能不稳定,如果遇到问题,建议在中断里只置标志位,主循环里处理读取。

5. 滤波处理:没有好信号,后期全是白忙

5.1 芯片自带的数字滤波器:数据速率本身就是低通滤波器

ADS1115 内部有一个数字滤波器,数据速率越低,滤波效果越明显。很多人以为 860SPS 就比 8SPS 高级,实际上对慢变信号来说,8SPS 的输出往往比 860SPS 干净得多。原因是低数据速率下,芯片会对内部过采样结果做更多平均,等效带宽更窄,带外噪声被削掉更多。

所以调 ADS1115 的第一条建议是:根据信号的带宽选择数据速率。工业变送器输出、电池电压、环境温度,这些信号变化很慢,64SPS 或 128SPS 就是很好的平衡点。如果你需要 860SPS 去捕捉快速变化的信号,那就要接受它的噪声水平。

一个简单的对应关系:数据速率 128SPS,理论可测信号带宽上限是 64Hz,实际还要打折扣。对大多数传感器应用来说,这个带宽足够了。

5.2 软件滤波三种选型

信号进了主控之后,软件还可以再做一道防护。三种最常用的滤波方式:

滑动平均对随机噪声抑制效果好,实现简单,适合温度等缓变信号。缺点是会引入延迟,窗口越长延迟越大:

def moving_average(samples): return sum(samples) / len(samples)

中值滤波对尖峰脉冲非常有效,比如继电器动作、电机启动产生的毛刺干扰。缺点是会丢失一些细节,对连续变化快的信号不合适:

def median_filter(samples): s = sorted(samples) return s[len(s) // 2]

实际使用中,我建议中值和滑动平均结合:先取 5 个点做中值,滤掉尖峰,再对这 5 个中值结果做滑动平均,平滑输出。这个组合在工业现场能顶很大用。

一阶低通滤波是递归的,内存占用极小,实时性好,适合对每个采样点做处理:

def low_pass(new_value, last_value, alpha=0.2): return last_value + alpha * (new_value - last_value)

alpha 怎么定?如果采样率是 128SPS,想要截止频率大概 10Hz,alpha 大约取 0.2。如果你更偏向平滑,就取 0.1;更跟手,就取 0.3。这个值可以根据实际波形调。

5.3 实测:50Hz 工频干扰怎么压

有次我把 ADS1115 接到一个开关电源供电的传感器输出上,读数里明显带着 50Hz 工频纹波,峰峰值大约 ±20 LSB。不算大,但做精确计量就不合格。

我当时做了两步:

用 100ms 滑动平均。50Hz 工频周期是 20ms,100ms 正好是 5 个完整周期。滑动平均窗口取工频周期的整数倍,理论上能把 50Hz 及其整数倍谐波陷波掉,效果立竿见影。如果你用的采样率是 128SPS,10 个点的滑动平均窗口约 78ms,不是 20ms 整数倍,效果就没那么理想。更好的做法是取 100ms 窗口,也就是采样率设为 100SPS 或 50SPS,让 10 个点正好覆盖 100ms,或者干脆用 20ms 的整数倍来设计窗口。

再用一阶低通把截止频率降到 10Hz 左右。工频是 50Hz,截止频率 10Hz 对它衰减超过 20dB,加上前面的滑动平均,读数基本稳定在 ±1 LSB 以内。

这两步都是软件措施,更重要的一步是硬件上把信号线的屏蔽层单独接地、在 ADC 的 VDD 和 GND 之间加一个 100nF 退耦电容、尽量缩短模拟信号线长度。软件滤波是补救,硬件设计才是根本。

5.4 过采样提升等效分辨率:4 倍采样换来 1 位精度

16 位 ADC 在低速率下已经不错了,可如果你想要更高的有效分辨率,可以用过采样技术。原理不复杂:假设输入信号叠加了随机白噪声,把 4 个连续采样点平均,噪声功率降为原来的 1/4,相当于信噪比提升 6dB,也就是有效分辨率提升 1 位。每做 4 倍过采样,等效分辨率多 1 位。

比如 128SPS 下采集 8 个点求平均,等效采样率降到 32SPS,但在满足条件的情况下有效位数大约从 16 位提升到 17 位。代码很简单:

def read_oversampled(adc, n=8): total = 0 for _ in range(n): total += adc.read_single() return total / n

但有一条必须强调:过采样的前提是噪声是白噪声,且噪声幅度至少要覆盖一个 LSB。如果信号本身很干净,采样结果一直是一个固定数字,平均一万次也提不了分辨率。此时真正限制你的是系统失调和参考电压精度,要从校准和硬件上下功夫。

6. 实测校准与常见问题排查

6.1 一台万用表搞定校准

ADS1115 芯片本身精度很高,但实际测量值还会受分压电阻精度、模块上参考电压、供电电压等因素影响。尤其是某些模块上用的板载参考电压芯片,精度可能只有 1% 到 2%,这时候必须校准。

我的校准步骤很简单:

  1. 用一个已知电压源,或者精密稳压器输出一个标准电压,接到 AIN0。
  2. 用 ADS1115 读一次原始码,记为 raw。
  3. 用万用表实际测量这个电压,记为 v_real。
  4. 计算实际 LSB:lsb_actual = v_real / raw
  5. 记下这个值和理论 LSB 的偏差,后续代码里做线性修正。

更通用的是做两点校准,接两个已知电压,拟合一条直线:

def read_calibrated(adc, scale=1.0, offset=0.0): return adc.read_voltage() * scale + offset

scaleoffset用最小二乘法或直接两点法算出来。校准后整体精度能提升一个数量级,尤其当你关注的是绝对电压而不是相对变化时,这一步省不了。

6.2 电源质量、共地与输入保护

ADS1115 的模拟性能和它的供电质量强相关。VDD 上的纹波会直接耦合到采样结果里,尤其是连续模式、高数据速率下更明显。给 ADC 单独供电,或者在 VDD 引脚就近加 100nF 和 10µF 电容,是成本最低且效果最明显的改善。

共地问题也很基础。信号源是外部独立供电的设备时,信号源的 GND 必须和 ADC 的 GND 连在一起,否则测量的是一个浮空电压,读数毫无意义。

输入保护方面,ADS1115 输入电压不能超过 VDD + 0.3V,也不能低于 GND - 0.3V。做工业采集时,如果信号线可能短路到 12V 或 24V,前端一定要加限流电阻和钳位二极管,否则芯片很容易烧。

6.3 常见错误与修复对照表

现象可能原因解决方式
scan()返回空列表接线错误 / 地址不对 / 无上拉按 2.4 节逐项排查
读数固定为满量程PGA 档位太小,输入超量程换更大的 PGA 档位
读数一直为 0输入通道没选对 / 输入悬空确认 MUX 设置,短接输入到 GND 测试
单次模式读数不更新忘了重新触发转换每次读之前调用start_conversion()
多通道读数串扰MUX 切换后没等转换完成切换后先等一个转换周期再读
读数波动大电源纹波 / 信号线太长 / 接地不好加退耦电容、用屏蔽线、软件滤波
高数据速率下数据跳变读的速度跟不上转换速度用 ALERT/RDY 引脚同步读取
I2C 总线死锁SDA 被从设备拉低复位从设备 / 检查协议时序

6.4 从 I2C 到成品固件的调度建议

到了这一步,你可能已经拿到了稳定数据,但的项目还在裸循环里跑。MicroPython 的能力有限,但做简单的调度还是够的。

建议用定时器或者asyncio来驱动采集,而不是在主循环里一次次sleep。原因很简单:主循环里一旦有网络操作、文件写入、LCD 刷新这类耗时操作,采样节奏就被打乱了。我常用的做法是将采集函数放进machine.Timer回调,采集结果放进一个环形队列,主循环只负责消费这个队列:

from machine import Timer sample_queue = [] def sample_cb(timer): voltage = adc.read_single() if len(sample_queue) < 100: sample_queue.append(voltage) timer = Timer(0) timer.init(period=10, mode=Timer.PERIODIC, callback=sample_cb)

这样 I2C 只集中在定时器回调里执行,主循环可以做显示、上报等逻辑,不会被采集阻塞。需要注意 MicroPython 的定时器回调尽量保持短,不要在回调里做文件写入或长时间打印。

最后补一句我的实操体会。ADS1115 难的不是采集,而是"你以为你采到了真值"。我踩过的最大一个坑,是在单次模式下忘了重新触发转换,结果所有读数都是第一轮的缓存值,排查了两个小时才发现问题。拿到芯片的第一件事,不是调滤波,也不是写花里胡哨的类,而是先把"触发 → 等待 → 读取 → 再触发"这条链路跑通,确认每一个读数都是新的,然后再去做任何高级处理。这个顺序对了,后面的路就顺了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询