十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

用MicroPython驱动ADS1115:16位ADC的I2C配置与滤波实践

用MicroPython驱动ADS1115:16位ADC的I2C配置与滤波实践 做嵌入式开发的人迟早会碰到片上ADC不够用的时候。无论是做电池电压监测、环境传感器采集还是搞一个需要精度的工装仪表单片机自带的12位ADC往往在零漂和分辨率上让人头疼。我前阵子调试一个四路电池巡检和两路温度采集的小系统ESP32-S3自带的ADC一接分压电阻就飘最后把ADS1115请了进来。配合MicroPython整个开发和调试过程比想象中顺畅得多I2C总线挂上就能读16位分辨率直接解决了精度焦虑。这篇文章就是我基于这一轮实操整理的硬件与代码手册从I2C通信细节到驱动实现再从采样触发策略到滤波处理把能沉淀的经验都写清楚。适合谁看呢用MicroPython做传感器采集的爱好者、实验室里临时搭采集系统的工程师以及所有不想啃几十页数据手册又想用好ADS1115的人。我会把寄存器配置拆开讲也会分享很多文档里不会写的弯路和排障方法。1. 先搞清楚 ADS1115 能干到什么程度硬件特性与选型逻辑1.1 16位ADC、四个通道、内部PGA到底意味着什么ADS1115是一颗16位、四通道、带I2C接口的Δ-Σ型ADC。这里的“16位”是转换器的分辨率不是最终的测量精度但已经足够让大多数MCU片上ADC汗颜了。比如3.3V系统下12位ADC的1 LSB大约是0.8mV而ADS1115在默认±4.096V量程下1 LSB只有0.125mV整整小6倍多。如果切到±0.256V量程1 LSB约7.8µV做到微伏级别的分辨没有问题。除了分辨率ADS1115真正的杀器是内部可编程增益放大器。PGA允许你量程从±6.144V一路调到±0.256V也就是说一颗芯片既能测3.7V锂电池也能测几百微伏的电桥输出。选量程的原则很简单宁可让信号幅度占到量程的50%以上也别贪大。很多新手直接用默认±6.144V量程去读一个0~0.5V的信号结果就是读数跳得厉害其实不是ADC坏了而是有效分辨率被量程浪费掉了。还有一个经常被忽略的点ADS1115输入阻抗不是恒定的。它内部是开关电容结构等效输入阻抗和采样速率、PGA设置有关典型值在几百千欧到几兆欧之间浮动。这意味着如果你的信号源是兆欧级分压电阻网络直接接上去会有明显偏差。我习惯在AIN引脚对地并联一个0.1µF~1µF的电容由电容提供采样瞬间的电荷这对高内阻信号源很有效。1.2 引脚接线与电平匹配很多人第一步就踩坑ADS1115的引脚不多VDD、GND、SCL、SDA、ADDR外加AIN0~AIN3。先说电源VDD支持2.0V~5.5V但在MicroPython生态里绝大多数场景是3.3V供电此时SCL和SDA也是3.3V逻辑。如果你用5V给芯片供电那么I2C引脚上拉电阻会把它拉到5V这时候再接ESP32的3.3V引脚就有风险。最简单的做法VDD接3.3VSCL/SDA分别上拉4.7kΩ到3.3V。大多数ESP32开发板已经把I2C上拉焊好了但独立模块不一定上拉了但阻值过大时高速通信仍然会出问题。我一般用4.7kΩ如果总线上设备多、走线长会换到2.2kΩ。ADDR引脚决定I2C地址。接地就是0x48接VDD是0x49接SDA是0x4A接SCL是0x4B。这块芯片设计得挺友好同一条I2C总线上最多能挂四颗ADS1115地址不冲突等于最多16路模拟输入。接线时注意AIN引脚输入范围不能超出VDD和GND也不能低于GND否则芯片内部ESD二极管会导通轻则读数错误重则损坏芯片。2. I2C 通信从协议细节到 MicroPython 驱动前置2.1 ADS1115 眼中的 I2C 时序I2C是个两根线的总线协议SCL负责时钟SDA负责数据。所有设备都挂在同一条总线上靠地址区分。ADS1115作为从机只响应主机比如ESP32发起的通信。MicroPython里我们不需要关心每个起始位和应答位但有两个细节必须明白否则后面写驱动会糊。第一个细节ADS1115内部的寄存器是通过“指针”来寻址的。你要读转换结果必须先告诉芯片“我要读0x00寄存器”你要写配置就要先写寄存器地址0x01。MicroPython里的i2c.readfrom_mem(addr, memaddr, n)和i2c.writeto_mem(addr, memaddr, buf)就是封装好的“先发指针、再传数据”接口很顺手。第二个细节MicroPython的I2C地址是7位地址。ADS1115数据手册里写的默认地址是0x48这是7位地址。Arduino常用的Wire库有时用8位地址写0x90不少人移植代码时把0x90直接搬过来结果扫描不到设备。记住在MicroPython的i2c.scan()返回的地址列表中正常只会有0x48、0x49、0x4A、0x4B这类值。I2C的SCL频率也要说一下。标准模式100kHz快速模式400kHz。ADS1115完全支持400kHz我初始化ESP32-S3时直接设freq400_000。不过总线频率越高对上拉电阻和走线质量越敏感如果设备多了或者杜邦线特别长偶尔出现通信错误就先把频率降到100kHz试一下。这算是排查I2C问题最快的验证手段。2.2 MicroPython 初始化与地址扫描脚本在ESP32-S3上初始化I2C非常简单。注意ESP32-S3的GPIO功能比较灵活SCL和SDA可以接到大部分引脚但优先选没有SPI Flash、没有USB等复用功能的引脚避免干扰。我这里用GPIO8和GPIO9举例你可以按自己的板子调整。from machine import Pin, I2C import time i2c I2C(0, sclPin(8), sdaPin(9), freq400_000) # 扫描总线上所有I2C设备 devices i2c.scan() if devices: print(Found I2C devices:, [hex(d) for d in devices]) else: print(No I2C device found)跑完这段如果ADS1115的ADDR接地你会看到[0x48]。如果列表里什么都没有先检查供电、SDA/SCL是否接反、上拉电阻是否工作。如果在ESP32常见开发板上跑还要确认开发板自带的I2C引脚是否被你误用了有的板子虽然引脚引出但已经被板载外设占用。2.3 一个 I2C 总线上挂多颗 ADS1115 与 OLED 的地址规划很多项目不是只挂一颗ADC还会挂OLED、温湿度传感器、光照传感器。比如你手头有SSD1306 OLED地址一般是0x3C、BH1750光照传感器地址是0x23或0x5C再加上默认0x48的ADS1115这几个地址天然不冲突直接挂同一条I2C总线就行。真正容易出问题的是总线电容和上拉电阻。I2C规范里设备数多了、走线长了总线电容变大信号边沿变缓导致误码。如果挂的设备超过三四个或者使用杜邦线连接我会先把多个设备的电源和地都接到同一个点上尽量避免形成长的环形回路上拉电阻维持4.7kΩ压不住再换2.2kΩ。用i2c.scan()在挂载所有设备后跑一遍如果扫描结果时有时无那就是信号质量问题不是地址冲突。3. 驱动实现寄存器映射与配置位逐项拆解3.1 四个寄存器搞懂谁负责什么ADS1115本质上就是几个寄存器在来回倒数据最常用的四个寄存器如下寄存器名称地址作用转换寄存器0x00存放最近的转换结果16位有符号数配置寄存器0x01设置通道、PGA、数据率、触发方式低阈值寄存器0x02比较器模式下的低阈值高阈值寄存器0x03比较器模式下的高阈值如果你不用内置比较器功能0x02和0x03基本不用管。日常开发90%的精力都花在“写配置寄存器启动转换再读转换寄存器拿数据”这个循环上。理解配置寄存器是写好驱动的分水岭。3.2 配置寄存器逐位拆解MUX、PGA、DR、OS配置寄存器是16位每个位段的含义可以通过下面这张表快速理解。我用的是从高位到低位的顺序。位段名称说明bit15OS单次模式写1启动转换读1表示空闲读0表示正在转换bit14~12MUX输入通道选择bit11~9PGA可编程增益决定满量程范围bit8MODE0连续模式1单次模式power-downbit7~5DR数据率决定每秒采样次数bit4COMP_MODE比较器模式一般不用bit3COMP_POL比较器极性一般不用bit2COMP_LAT比较器锁存一般不用bit1~0COMP_QUE比较器触发次数写0b11可禁用比较器MUX段的选择比较重要。写0b100表示AIN0对GND的单端输入0b101表示AIN1对GND0b110表示AIN20b111表示AIN3。如果你需要差分测量0b000表示AIN0正、AIN1负0b011表示AIN2正、AIN3负。下面的表是常用组合MUX值输入配置0b000AIN0 - AIN1 差分0b001AIN0 - AIN3 差分0b010AIN1 - AIN3 差分0b011AIN2 - AIN3 差分0b100AIN0 单端0b101AIN1 单端0b110AIN2 单端0b111AIN3 单端PGA段对应满量程我整理成了下表。注意满量程是正负对称的比如±4.096V意味着可测范围是-4.096V到4.096V。PGA值满量程0b000±6.144V0b001±4.096V0b010±2.048V0b011±1.024V0b100±0.512V0b101±0.256V数据率段直接决定转换速度低速率下内部数字滤波器作用更明显噪声更低。DR值采样率0b0008 SPS0b00116 SPS0b01032 SPS0b01164 SPS0b100128 SPS0b101250 SPS0b110475 SPS0b111860 SPS单次触发模式下写OS1启动转换。转换完成后芯片自动回到空闲状态。连续模式下转换不断进行OS位虽然在写时忽略但读配置寄存器时仍然能反映当前状态。3.3 可直接复用的单次触发驱动代码我基于MicroPython写了一个最常用的单次触发读取函数。它每次调用只启动一次转换读完自动回到低功耗空闲状态非常适合电池供电的采集节点。import time from machine import Pin, I2C i2c I2C(0, sclPin(8), sdaPin(9), freq400_000) ADDR 0x48 REG_CONFIG 0x01 REG_CONV 0x00 MUX_SINGLE { 0: 0x4000, 1: 0x5000, 2: 0x6000, 3: 0x7000, } PGA { 6.144: 0x0000, 4.096: 0x0200, 2.048: 0x0400, 1.024: 0x0600, 0.512: 0x0800, 0.256: 0x0A00, } DR { 8: 0x0000, 16: 0x0020, 32: 0x0040, 64: 0x0060, 128: 0x0080, 250: 0x00A0, 475: 0x00C0, 860: 0x00E0, } def read_adc_single(ch0, pga_key4.096, sps128): mux MUX_SINGLE[ch] config (1 15) | mux | PGA[pga_key] | (1 3) | DR[sps] config_bytes config.to_bytes(2, big) i2c.writeto(ADDR, bytes([REG_CONFIG, config_bytes[0], config_bytes[1]])) # 等待转换完成 time.sleep_ms(int(1000 / sps * 1.5) 1) data i2c.readfrom_mem(ADDR, REG_CONV, 2) raw (data[0] 8) | data[1] if raw 0x8000: raw - 0x10000 fsr float(pga_key) voltage raw * fsr / 32768.0 return raw, voltage for ch in range(4): raw, volt read_adc_single(chch, pga_key4.096, sps128) print(fAIN{ch}: raw{raw}, voltage{volt:.4f}V)这段代码把配置寄存器写为OS1启动转换、MUX选中对应通道、PGA选±4.096V、MODE单次模式、DR128SPS、比较器禁用。MODE位我写了1 3正好是单次模式。转换结果换算成电压时用raw * FSR / 32768。因为ADS1115输出的是二进制补码0x7FFF代表接近正满量程0x8000代表接近负满量程。3.4 用轮询 OS 位替代固定延时更稳的判断方法上面代码里的time.sleep_ms是固定延时按数据率估算简单但不够严谨。如果系统的I2C总线速度慢、调度抖动大延时不够就会读到上一次的旧数据延时太长又浪费。更稳妥的做法是轮询配置寄存器的OS位读回配置寄存器检查bit15看到1就说明转换完成可以读结果。def read_adc_single_poll(ch0, pga_key4.096, sps128): mux MUX_SINGLE[ch] config (1 15) | mux | PGA[pga_key] | (1 3) | DR[sps] config_bytes config.to_bytes(2, big) i2c.writeto(ADDR, bytes([REG_CONFIG, config_bytes[0], config_bytes[1]])) while True: cfg i2c.readfrom_mem(ADDR, REG_CONFIG, 2) os_bit (cfg[0] 7) 0x01 if os_bit 1: break time.sleep_ms(1) data i2c.readfrom_mem(ADDR, REG_CONV, 2) raw (data[0] 8) | data[1] if raw 0x8000: raw - 0x10000 fsr float(pga_key) voltage raw * fsr / 32768.0 return raw, voltage注意读配置寄存器时如果芯片正在转换中OS位读出来是0转换完成回到空闲状态后OS位变回1。这个方式的优势是不管数据率设置成8SPS还是860SPS代码都自适应等待时间不会因为延时不够出现“看起来正常偶尔读重复值”的隐性bug。在我的实测里这种方式也更适合将来把采样率做成可配置项。4. 采样触发策略单次、连续、定时到底怎么选4.1 单次触发适合哪些场景单次触发是最省电、也最容易控制的方式。写入OS1启动一次转换转换完成后自动掉电这正好适合电池供电的传感器节点。比如一个环境监测终端每分钟唤醒一次读一下电池电压和温湿度然后继续睡。如果纯靠连续模式跑即便不读数芯片也一直维持在毫安级实际ADS1115连续模式电流在几百微安量级对低功耗应用不友好。另一个适合单次触发的场景是“不同通道轮流采样”。你需要在四个通道之间切换每次切换通道后又希望精确掌握“这一笔数据是哪一路的”单次模式就非常适合。每次调用读函数时重新配置一次MUX顺序切换AIN0到AIN3逻辑清晰也不存在连续模式下切换通道导致读出混合数据的问题。手动触发还有一个好处数据的“时间戳”很明确。你在写配置寄存器的那个时刻启动转换转换完成时间基本上等于1/SPS所以可以比较准确地推算采样的绝对时间。对于不需要高精度同步的应用这个精度已经足够。4.2 连续模式与数据率上限连续模式需要把配置寄存器的MODE位设为0OS位写0然后芯片就自己一直转。主机只需要按自己的节奏去读转换寄存器。连续模式适合高频数据流比如振动监测、音频特征采集、或者你需要用250SPS以上速率持续记录波形的时候。要注意连续模式下数据率受限于I2C读取速度和主循环的处理能力。ADS1115最高支持860SPS也就是每1.16ms产生一笔数据。虽然I2C读一次结果只需要几十微秒但MicroPython的解释执行开销和主循环调度可能会成为瓶颈。我实测在ESP32-S3上如果只做读取和打印128SPS轻轻松松跑到475SPS就开始感觉到主循环被占用860SPS下如果还要做滤波、显示、存储很可能出现处理不过来。另外连续模式“停止”的方式不如单次直接。我常用的方法写完配置进入连续模式后要停的时候再写一次配置寄存器把MODE设回1并保证OS位不置1或者干脆写一份新的配置覆盖它。这个动作会让芯片回到空闲状态。如果只是暂时不读数让它继续转换也没问题反正它会持续更新转换寄存器你读到的总是“最近一次”的数据。4.3 用定时器实现定时采集与 ADS1115 无硬件触发的妥协ADS1115没有外部硬件触发引脚这是它和DSP内部ADC“EPWM触发ADC采样”这类硬件级同步方案最大的区别。你只能通过I2C命令软件触发或者在MCU端用定时器来实现周期性采样。MicroPython里可以用machine.Timer我写了一个简单的定时采样框架from machine import Timer buffer [] def sample_cb(timer): raw, volt read_adc_single(ch0, pga_key4.096, sps128) buffer.append((time.ticks_ms(), raw, volt)) timer Timer(0) timer.init(period20, modeTimer.PERIODIC, callbacksample_cb)这段代码每20ms读取一次。需要注意的是MicroPython定时器回调是在软件层面执行的回调里如果做I2C传输、文件写入这类耗时操作会阻塞主循环。如果转换时间或者I2C通信时间超过定时周期回调就可能重入导致数据时序错乱。我在实操中更倾向用主循环轮询的方式定时器只置一个标志位主循环检测到标志位后再执行读取这样更安全。还有一个更实际的方案如果你只需要固定间隔采集而单个ADC的转换时间远小于采样间隔直接在循环里调用单次读取配合sleep_ms控制节奏反而比定时器更可控。只有在需要和其他任务并行执行时才上定时器或者把读取放到收包线程里。5. 滤波处理把 ADC 读数从“能看”变为“能用”5.1 噪声从哪来先知道敌人长什么样ADS1115的16位分辨率是理论上限实际读数末尾几位往往在跳动。很多人拿到板子第一反应是“是不是买到假芯片了”其实绝大多数情况下是噪声。噪声主要来自三个方向。第一是电源噪声。给ADS1115供电的3.3V如果不干净基准噪声会直接耦合到转换结果里。ESP32-S3的Wi-Fi射频开启时实测对ADC读数有明显干扰这不是芯片问题而是系统级设计问题。第二是信号源内阻。前面说过高内阻信号源会让开关电容ADC采样瞬间产生电荷不足导致每次采样的电压都有一定随机误差。第三是电磁干扰。杜邦线就像天线特别是接入高阻分压网络时50Hz工频和周围开关电源的噪声都容易被拾取。滤波的本质是“用时间换精度”。ADS1115数据手册里的噪声曲线也说明了这一点数据率越低内部数字滤波越强有效分辨率越高。比如8SPS下能得到接近无噪声16位的效果而860SPS下有效位数可能降到12~13位。所以如果应用对实时性要求不高优先降低数据率这比任何软件滤波都有效。5.2 四种常用滤波在 MicroPython 里的实现我在这类项目里最常用的滤波有四种限幅滤波、中值滤波、滑动平均滤波、一阶低通滤波。它们各自擅长处理不同类型的噪声用一个统一的类包装起来方便组合调用。class LimitFilter: def __init__(self, span0.05): self.span span self.last None def update(self, value): if self.last is None: self.last value elif abs(value - self.last) self.span: return self.last else: self.last value return self.last class MedianFilter: def __init__(self, size5): self.buf [] self.size size def update(self, value): self.buf.append(value) if len(self.buf) self.size: self.buf.pop(0) sorted_buf sorted(self.buf) return sorted_buf[len(sorted_buf) // 2] class MovingAverage: def __init__(self, size10): self.buf [] self.size size def update(self, value): self.buf.append(value) if len(self.buf) self.size: self.buf.pop(0) return sum(self.buf) / len(self.buf) class LowPassFilter: def __init__(self, alpha0.3): self.alpha alpha self.y None def update(self, x): if self.y is None: self.y x else: self.y self.alpha * x (1 - self.alpha) * self.y return self.y限幅滤波适合剔除明显的尖峰脉冲比如电机启停瞬间耦合进来的干扰。中值滤波抗孤点噪声很有效处理突发干扰比滑动平均强。滑动平均让曲线变平滑但窗口越大滞后越明显。一阶低通滤波实现最简单调节alpha可以控制平滑度和响应速度alpha越小越平滑但越迟钝。5.3 滤波组合与参数选择的实测经验滤波不是越复杂越好参数选错甚至会让数据“看起来漂亮但不可信”。我的经验是先做限幅剔除肉眼可见的异常跳变再做一次中值或滑动平均把高频毛刺压下来。关于滑动平均的窗口有个细节。如果你的干扰是50Hz工频希望用滑动平均滤除窗口对应的总时间最好是20ms的整数倍。比如采样率250SPS窗口取5正好覆盖一个50Hz周期如果采样率128SPS窗口取整数无法恰好对齐50Hz就不要再较真改用一中值滤波加一阶低通效果也够用。alpha怎么选测量缓慢变化的电池电压时我常用0.1~0.2平滑度优先。采集温度、液位这类本身变化慢的信号时可以更低。但采集需要跟踪快速变化的电流或转速信号时alpha不要低于0.5否则会引入严重滞后系统控制环容易不稳定。另外要记住滤波不能解决真实的测量错误。如果硬件接线本身有问题比如PGA量程选错导致信号削顶滤波只会让你看到一个圆润的错误值。我调试时总是先不滤波把原始读数打印出来确认数据形态“正常跳”而不是“异常漂”之后再加入滤波处理。6. 实战问题排查我在项目里踩过的几个坑6.1 扫描不到设备按这个顺序查遇到i2c.scan()返回空列表大多数人第一反应是“芯片坏了”。我踩过几次之后总结了一套排查顺序基本能解决九成问题。先是供电和接线。用万用表量一下VDD对GND的电压确定芯片真的通电了SCL和SDA是不是被对调了以及有没有连到正确的GPIO。然后是上拉电阻。没有上拉或者上拉电阻虚焊I2C信号永远拉不高扫描结果必然为空。最后是地址问题。确认ADDR引脚的接法如果ADDR悬空有些芯片内部默认是GND有些模块则已经接了固定电平所以别想当然。还有一类特殊情况总线上挂了其他设备某个设备将SDA一直拉低导致整个总线卡死。这时候把可疑设备逐个拔掉再跑扫描就能定位是谁在捣乱。6.2 数据异常跳变从读数现象反推原因读到的值一直在0x7FFF或0xFFFF附近这说明输入信号已经超出当前PGA量程或者AIN引脚悬空。此时先查PGA设置如果信号是0~1V而你选了±0.256V量程读数就会顶到满量程。再把输入对地短接一次看读数是否回落到接近0能快速区分是接线问题还是设置问题。读数稳定但数值偏大或偏小多半是高内阻信号源导致的分压效应。比如用1MΩ和1MΩ电阻分压理论输出是VDD/2但ADS1115等效输入阻抗不够高接入后分压比被改变。解决办法是降低分压电阻阻值或者在ADC输入端并联电容必要时加一级运放缓冲。读数在某个固定值附近来回跳比如末位跳几十甚至上百先看数据率把SPS降下来再测再看供电关掉ESP32-S3的Wi-Fi或者换成线性稳压供电测试往往能立竿见影。如果仍不行就在模拟输入端并联一个0.1µF电容滤掉射频干扰。6.3 采样率、精度、功耗三者的取舍最后聊一个规划层面的问题。ADS1115不是一只万能的ADC采样率、精度和功耗三者很难兼得。低数据率能发挥芯片最强大的低噪声性能但实时性差高数据率适合波形捕捉但噪声大、主控负担重单次触发省电可频繁触发时功耗并不会比连续模式低太多。我在实际项目中通常这样取舍需要长期监测缓慢信号电池电压、温度、液位时用8~16SPS单次触发精度优先功耗最低。需要记录动态信号振动包络、电机电流时用128~250SPS连续模式配合寄存器读取和简单滤波。如果项目既要求高精度又要求高频那ADS1115可能不是最佳选择应该考虑带硬件FIFO、可外部触发的高速ADC芯片。这个取舍背后有个可以量化的点ADS1115在低数据率下无噪声分辨率接近16位但在860SPS下有效位数下降明显。也就是说真正的瓶颈往往不是ADC位宽而是数据率和噪声的权衡。把这个关系想清楚后面选型和调参都会顺利很多。我个人在反复调试中的体会是ADS1115搭配MicroPython最舒服的工作区间是8到250SPS。低于8SPS即使读取本身很快采样周期也太长交互反馈不跟手高于250SPSMicroPython脚本层的数据处理很容易成为瓶颈。如果你要做高速连续采集建议把滤波和存储都放到C扩展里去实现或者干脆换一个带DMA和硬件触发功能的MCU方案。另外每次焊接完新模块我都会先跑一遍i2c.scan()并连续读取100次把最大最小值打印出来确认硬件状态稳定才开始写业务逻辑。这个小习惯帮我省下了不少排查时间也推荐你试试。
返回列表