简介这组4通道AD采样模块ADS1115驱动代码面向使用STM32F103C8T6进行多路模拟信号采集的嵌入式开发者尤其适合需要快速搭建高精度数据采集系统的中高级工程师。资源基于STM32 HAL库编写并附带STM32CubeMX工程配置能够帮助读者快速完成高精度ADC驱动移植与传感器数据采集系统搭建适用于温度、压力、流量、生理信号及机器人多路传感等场景。压缩包共1151个文件以C源码、H头文件、汇编启动文件为主同时包含ICF链接脚本、TXT说明、Keil工程文件、数学库等总计约30.41MB工程结构完整可直接用于编译与调试。目前已有152人学习下载。借助这套代码开发者可以省去底层寄存器配置的繁琐过程集中精力处理应用层逻辑附带的CubeMX工程还便于按需调整引脚和采样参数从而提升开发效率并降低多通道高精度采集系统的搭建门槛。1. 4通道AD采样模块ADS1115驱动代码一块最小系统板加一颗16位ADC能干什么当手头的 STM32F103C8T6 最小系统板需要同时盯着四路模拟量时内置 12 位 ADC 往往不够用分辨率不够、参考电压不稳、多通道切换还会串扰。ADS1115 是一颗带 I2C 接口的 16 位、4 通道 ADC输出可以直接进单片机很多电池电压采集、电流采样和传感器仪表项目都靠它在低成本方案里把精度提上去。这篇笔记会围绕“ADS1115 驱动代码 STM32F103C8T6”讲清楚从接线到寄存器配置、从标准库底层 I2C 到多通道轮询的完整路径也会把最容易翻车的 I2C 死锁、增益误差、参考电压和滤波问题逐个拆开。适合已经会点灯但还没正经调过外部 ADC 的入门开发者也适合想快速移植到现有工程的老手照着抄作业。2. 先把ADS1115和STM32F103C8T6接线想清楚引脚、地址与单端/差分选择ADS1115 的硬件接线比写驱动更值得先花时间。很多人最后不是代码错而是 SCL/SDA 接反、ADDR 悬空、或者模拟输入的地没有和单片机共地导致读出来的数据像黑匣子一样无法解释。先理清引脚功能和地址配置后面驱动编译出来才能一接就通。2.1 STM32F103C8T6引脚功能与I2C地址配置ADS1115 常见封装是 MSOP-10引脚不算多VDD、GND、SCL、SDA、ADDR、ALERT/RDY以及四路模拟输入 AIN0 到 AIN3。它本身没有内置时钟靠 I2C 主机驱动所以只要两根线就能挂在单片机上。对 STM32F103C8T6 来说最省事的接法是利用硬件 I2C1默认引脚是 PB6 的 I2C1_SCL 和 PB7 的 I2C1_SDA。在接杜邦线之前最好对着 STM32F103C8T6 最小系统板原理图确认这两个引脚上有没有其他负载有些开发板会把 PB6/PB7 复用到按键或 LED如果被占用要么换硬件 I2C2要么干脆用 GPIO 软件模拟 I2C。ADDR 引脚决定芯片的 I2C 地址这个地址是 7 位的STM32 标准库发送地址时需要左移一位很多刚上手的人忘记左移结果始终搜不到设备。ADDR 的电平与地址对应关系如下ADDR 接法7 位 I2C 地址接 GND0x48接 VDD0x49接 SDA0x4A接 SCL0x4B同一根 I2C 总线上最多可以挂四个 ADS1115用不同 ADDR 区分这样单颗 4 通道芯片最多能扩到 16 路模拟输入。实际项目中我大多数时候把 ADDR 直接接地只用 0x48 一个地址因为多芯片挂在同一条总线上时线上拉电阻要重新计算反而增加调试成本。2.2 单端4通道与差分2通道的应用场景差异ADS1115 的内部是一个多路选择器加一个 16 位 Σ-Δ ADC。MUX 寄存器位可以把它配成四种单端输入也可以配成差分输入。单端输入测量的是 AINx 对 GND 的电压适合被测量信号本来就有一个明确参考地的场景比如测锂电池电压、电位器分压、普通温度传感器输出。差分输入测量的是 AIN 减 AIN- 的差值还有一个额外好处如果噪声是同时叠加在两条输入线上的共模干扰相减之后会被抵消所以毫伏级小信号优先用差分。选错模式是新手常见坑。举个例子测一个电桥式压力传感器输出只是 10 mV 到 40 mV 的差分信号两条线对地都有大约 2.5 V 的共模电压把其中一条线当 GND 测单端读数会接近 2.5 V小信号变化完全被淹没。这时应该用差分模式同时把 PGA 增益调高。反过来如果只是测一节 3.7 V 电池用单端 AIN0 对 GND 就足够了没必要把电池两端接到差分输入端。下面这个表是我常用的选型参考测量对象推荐模式原因电池电压、0-3.3V 传感器单端参考地明确接线简单桥式传感器、分流电阻压降差分抑制共模噪声小信号分辨率高4-20mA 电流环采样电阻差分采样电阻两端浮地差压小2.3 从STM32F103C8T6最小系统板引线接线表与常见错误具体连线可以直接照抄下表ADS1115 引脚接 STM32F103C8T6 最小系统板VDD3.3VGND系统板 GNDSCLPB6SDAPB7ADDRGNDALERT/RDY悬空或接 PB8可选AIN0-AIN3模拟信号输入需要注意ADS1115 的模拟输入绝对最大范围是 GND-0.3V 到 VDD0.3V超过这个范围会损坏芯片。如果被测信号高于 3.3V必须先电阻分压如果信号是负电压或有大共模电压不能直接接 AIN 引脚。另外模拟信号的 GND 必须和 STM32 系统板的 GND 连在一起否则共模电压不对读回来会一直跳。供电方面我习惯统一用 3.3V 给 ADS1115 供电这样 SCL/SDA 高电平不会超过 3.6V和 STM32 引脚电平匹配省去电平转换。有些模块默认 5V 供电接 3.3V 也能跑但要注意数据手册里 VDD 范围是 2.0V 到 5.5V只要上拉电阻也接到 3.3V就完全兼容。ALERT/RDY 引脚在单次转换模式下可以作转换完成信号输出接到 STM32 的外部中断引脚能节省主循环轮询时间。但初版驱动不建议接先用轮询方式跑通数据链路再考虑中断避免引入新的变量。3. 寄存器配置才是驱动核心配置寄存器与转换寄存器逐位拆解ADS1115 的操作其实很直白写配置寄存器 0x01等待转换完成再读转换寄存器 0x00。只要把这两步做好驱动就通了。难点在于 16 位配置寄存器每一位都影响转换模式、量程和速率错一位数据就完全不对。3.1 配置寄存器(0x01)的每一位增益、速率、连续/单次芯片内部有几个寄存器驱动代码里主要用到两个0x00 是转换结果寄存器16 位只读0x01 是配置寄存器16 位可读可写。剩下的 0x02 和 0x03 是比较器阈值寄存器使用比较器告警功能才需要配普通数据采集先把它们放一边。配置寄存器各位从高到低依次是位名称作用bit15OS单次转换启动位写 1 启动读 1 表示转换完成bit14:12MUX选择输入通道bit11:9PGA增益和满量程范围bit8MODE1 单次0 连续bit7:5DR数据速率bit4COMP_MODE比较器模式默认 0bit3COMP_POL告警极性默认 0bit2COMP_LAT比较器锁存默认 0bit1:0COMP_QUE比较器开关11 表示禁用如果要把 AIN0 对 GND 作为单端输入MUX 就写 000AIN1 对 GND 写 001AIN2 写 010AIN3 写 011。差分模式的 MUX 值是从 100 开始具体可以查数据手册。PGA 位决定满量程000 对应 ±6.144V001 对应 ±4.096V010 对应 ±2.048V011 对应 ±1.024V100 对应 ±0.512V101 对应 ±0.256V。量程越小分辨率越高但输入电压不能超过量程。速率位 DR 也很关键000 是 8SPS100 是 128SPS111 是 860SPS。速率越高噪声越大想稳定读数反而要降速。3.2 单次转换模式与连续模式的代码差异MODE 位设为 1 时是单次转换模式。每次要读数据必须先往配置寄存器写入 OS1 启动一次转换然后等待转换完成。完成后配置寄存器里的 OS 位会由 0 翻回 1可以轮询这一位判断是否完成。连续模式则只需要写一次配置芯片会按设定速率不断更新转换结果寄存器省去重复启动操作。那驱动里到底用哪种多通道轮询场景我强烈建议用单次模式。原因很简单连续模式下切换 MUX 后转换结果寄存器还保留着上一个通道的数据如果时序没配合好容易读到旧值。单次模式每次启动前重新写 MUX 值转换完成再读天然就能做到“切到哪个通道就读哪个通道”逻辑上干净很多。单次模式的代价是每次启动到完成有约 8 ms 等待128SPS 时对大多数采集场景足够。配置寄存器拼装代码里常见做法是定义一批宏避免在代码里直接写魔法数字#define ADS1115_REG_CONVERT 0x00 #define ADS1115_REG_CONFIG 0x01 #define ADS1115_OS_START 0x8000 #define ADS1115_MUX_AIN0_GND 0x0000 #define ADS1115_MUX_AIN1_GND 0x1000 #define ADS1115_MUX_AIN2_GND 0x2000 #define ADS1115_MUX_AIN3_GND 0x3000 #define ADS1115_PGA_4P096 0x0200 #define ADS1115_MODE_SINGLE 0x0100 #define ADS1115_DR_128SPS 0x0080 #define ADS1115_COMP_DISABLE 0x0003宏定义的值不是随便写的MUX_AIN1_GND 等于 0x1000本质是 1 左移 12 位PGA_4P096 是 001 左移 9 位正好 0x0200。写代码时把这些宏按位或起来可读性比直接写 0x8383 好得多也不容易算错位。3.3 读取转换值并换算成电压原始码与满量程的关系ADS1115 转换结果是 16 位有符号数二进制补码格式。满量程正数对应 0x7FFF满量程负数对应 0x80000V 对应 0x0000。把原始值换算成电压不能直接除以 65535而是要看 PGA 设定的满量程。例如 PGA 配置成 ±4.096V 时电压满量程是 4.096V对应 32768 个码所以公式是float ads1115_to_voltage(int16_t raw, float fsr) { return (float)raw * fsr / 32768.0f; }这里 fsr 传入 4.096f。注意 32768 这个数是 2 的 15 次方因为 16 位有符号数正半轴只有 32767 加一个零所以分母用 32768。有的代码写成 65535读出来的电压会整体偏小一倍左右。这个坑很隐蔽单看代码很难发现对着信号源一测就露馅。以 ±4.096V 量程为例一个 LSB 等于 4.096V / 32768算下来是 125 µV。也就是说理论分辨率能到 0.125 mV比 STM32 内置 ADC 在 3.3V 参考下约 0.8 mV 的分辨率高很多。当然这是理想值实际噪声会把最后几位翻毛后面第 6 章会讲怎么用滑动平均把这些毛刺压下去。4. 基于标准库的ADS1115驱动代码从I2C初始化到多通道读取驱动代码我用的是 STM32 标准外设库不涉及 HAL。很多人在 Keil4 里新建基于标准库开发的 STM32F103C8T6 工程模板代码风格和下面这套完全一致。硬件 I2C 在标准库里的坑比 HAL 多一点但摸清时序后照样能稳定跑。4.1 新建基于标准库开发的STM32F103C8T6工程模板与I2C初始化标准库工程的关键是先把时钟和 GPIO 配置好。PB6 和 PB7 要配置成复用开漏输出因为 I2C 总线需要外部上拉电阻把电平拉高。如果配置成推挽输出总线在某些时序下会冲突出现卡死。void i2c_gpio_init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); gpio.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_OD; // 开漏复用 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, gpio); I2C_InitTypeDef i2c; i2c.I2C_Mode I2C_Mode_I2C; i2c.I2C_ClockSpeed 400000; // 400kHz 快速模式 i2c.I2C_DutyCycle I2C_DutyCycle_2; i2c.I2C_Ack I2C_Ack_Enable; i2c.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; i2c.I2C_OwnAddress1 0x00; // STM32 作为主机自身地址无所谓 I2C_Init(I2C1, i2c); I2C_Cmd(I2C1, ENABLE); }这段代码先把 GPIOB 和 I2C1 的时钟打开然后把 PB6/PB7 配成开漏复用。开漏是必须的因为 I2C 协议要求设备只能主动拉低高电平靠上拉电阻实现。I2C 时钟设置为 400kHz但 STM32 的 I2C 外设在 400kHz 下对时序要求较高如果总线较长或线上电容偏大建议改成 100kHz。我调试时一般先用 100kHz 排除信号完整性问题稳定后再提速度。4.2 底层I2C读写函数封装解决标准库的卡死问题标准库 I2C 最大的坑是检查事件标志时容易死循环。如果总线上有设备没上电、地址不对或者时序被打乱状态寄存器的标志永远等不到。所以底层函数里必须加超时这是血泪经验。我习惯写一个带超时的写寄存器函数uint8_t i2c_write_reg(uint8_t dev_addr, uint8_t reg, uint16_t data) { uint8_t buf[3]; buf[0] reg; buf[1] (uint8_t)(data 8); buf[2] (uint8_t)(data 0xFF); uint32_t timeout 10000; I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { if (--timeout 0) return 1; } I2C_Send7bitAddress(I2C1, (dev_addr 1), I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) { if (--timeout 0) return 2; } for (uint8_t i 0; i 3; i) { I2C_SendData(I2C1, buf[i]); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)) { if (--timeout 0) return 3; } } I2C_GenerateSTOP(I2C1, ENABLE); return 0; }这个函数每次循环前都扣次数到 0 就返回错误码。错误码能帮你在调试时快速定位是卡在 START 阶段还是地址阶段。注意 dev_addr 传入的是 0x48 这类 7 位地址发送前要左移一位因为 I2C 硬件在最低位自动补读写方向位。I2C_EVENT_MASTER_BYTE_TRANSMITTING 在标准库中表示数据寄存器已空且移位寄存器也在送数如果换成 BYTE_TRANSMITTED 反而会多等一个周期。读转换结果时需要先发送寄存器地址然后重新产生起始信号将总线切到接收方向。这里给出一个读两个字节的简单实现uint16_t i2c_read_reg16(uint8_t dev_addr, uint8_t reg) { uint8_t buf[2]; uint32_t timeout 10000; I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { if (--timeout 0) return 0; } I2C_Send7bitAddress(I2C1, (dev_addr 1), I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)) { if (--timeout 0) return 0; } I2C_SendData(I2C1, reg); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTING)) { if (--timeout 0) return 0; } I2C_GenerateSTART(I2C1, ENABLE); // 重复起始 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)) { if (--timeout 0) return 0; } I2C_Send7bitAddress(I2C1, (dev_addr 1), I2C_Direction_Receiver); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)) { if (--timeout 0) return 0; } I2C_ReceiveData(I2C1); // 清除 DR I2C_AcknowledgeConfig(I2C1, DISABLE); // 读最后一个字节前关闭 ACK I2C_ReceiveData(I2C1); buf[0] I2C_ReceiveData(I2C1); I2C_GenerateSTOP(I2C1, ENABLE); buf[1] I2C_ReceiveData(I2C1); return (uint16_t)((buf[0] 8) | buf[1]); }这段代码只适合读两个字节但 ADS1115 转换结果正好是两个字节所以够用。最后两行拼接时要注意大小端ADS1115 先发高字节后发低字节所以高字节左移 8 位。如果读回来数值总是跳成 0xFF81 这类多半是字节序拼反了。4.3 ADS1115驱动核心代码单次读取指定通道有了底层 I2C 函数ADS1115 的驱动就只剩下“拼配置、写寄存器、等完成、读结果”四步。下面的函数用单次模式读指定通道通道号 0 到 3 对应 AIN0 到 AIN3int16_t ads1115_read_single(uint8_t channel) { uint16_t mux ADS1115_MUX_AIN0_GND | ((channel 0x03) 12); uint16_t cfg ADS1115_OS_START | mux | ADS1115_PGA_4P096 | ADS1115_MODE_SINGLE | ADS1115_DR_128SPS | ADS1115_COMP_DISABLE; if (i2c_write_reg(0x48, ADS1115_REG_CONFIG, cfg) ! 0) { return 0x8000; // 错误码 } uint32_t timeout 50000; while (1) { uint16_t check read_config_os_bit(); // 读取配置寄存器 OS 位 if ((check 0x8000) ! 0) break; // OS 位为 1转换完成 if (--timeout 0) return 0x8001; delay_us(100); } return (int16_t)i2c_read_reg16(0x48, ADS1115_REG_CONVERT); }channel 左移 12 位是为了顶到 MUX 位channel 必须是 0 到 3。cfg 里同时写了 OS_START 和 MODE_SINGLE这两个不冲突OS 位是启动动作MODE 位决定启动后是单次还是连续。写配置后芯片内部会自动清除 OS 位转换完成再置回 1所以可以用读配置寄存器的方式轮询。实际工程里可以省去 read_config_os_bit 的完整实现直接调用 i2c_read_reg16 读 0x01再判断最高位。但要注意每次读配置寄存器也会占用一条 I2C 总线事务和直接延时到固定时间相比轮询会更加稳妥尤其是用低速率采样时固定延时会浪费 CPU。4.4 主循环里轮询4个通道并打印到串口多通道读取只需在 for 循环里轮流调用 ads1115_read_single。下面这段把 4 个通道的值通过串口打印出来方便肉眼观察#include stdio.h void send_voltage_to_uart(void) { for (uint8_t ch 0; ch 4; ch) { int16_t raw ads1115_read_single(ch); float voltage ads1115_to_voltage(raw, 4.096f); printf(CH%d: %d raw, %.3f mV\r\n, ch, raw, voltage); delay_ms(50); } }printf 能直接输出浮点但会占用更多 flash如果空间紧张可以把毫伏值拆成整数和小数手工拼字符串。主循环里每轮读四个通道之间放一个 delay是因为单次模式每次要等约 8 ms 转换完成四个通道串行读一遍至少 32 ms这个速度对电池电压、温度这类慢变量完全够用。如果要求更高采样率就要改用定时器触发这个放最后一章再讲。5. 驱动移植与调参的常见问题排查4个必踩坑驱动跑通之后真正费时间的是各种“看起来正常但数据不对”的现象。下面这几条是我实际调这块芯片时反复遇到过的坑按现象、原因、解决三步记录方便你直接对着排查。5.1 现象I2C总线卡死SCL被拉低现象是程序第一次读 ADS1115 能成功第二次就死循环或者一上电就卡在某个 while 里。用示波器看 SCL 一直为低SDA 也拉不下来。原因是标准库检查事件标志时没有超时一旦总线上出现异常时序芯片的 I2C 状态机进入错误状态主设备还在等一个永远等不到的标志。还有一个常见诱因是上电顺序ADS1115 的电源还没稳定STM32 就已经开始了 I2C 通信导致芯片没正确响应。解决方法是给所有 I2C 等待循环加上超时并在每次通信前尝试向总线发送一个停止条件。如果卡死发生在连续运行阶段检查模块上的 I2C 上拉电阻是否在 SCL/SDA 上分别接入阻值建议 4.7kΩ 到 10kΩ总线上挂的设备越多上拉电阻就要越小。实在不行就把 I2C 时钟降到 100kHz牺牲一点速度换稳定性。5.2 现象读回数值整体偏大或偏小现象是输入一个已知电压比如用万用表量到 1.000V但 ADS1115 算出来是 1.500V 或者 0.750V而且呈现固定比例关系。原因是换算公式里满量程值用错了。PGA 配置成 ±4.096V但换算时用了 6.144V读值会比实际大 1.5 倍用了 2.048V又会小一半。还有另一种情况是原始值被当成了无符号数导致靠近零点的负电压被算成很大的正电压。解决方法是先用信号源或稳压电源输入一个稳定电压逐字检查配置寄存器实际写入的值和换算公式里的 fsr 是否一致。最简单的方法是写一个寄存器回读函数把 0x01 的内容读出来打印确认 PGA 位是不是预期值。注意换算公式分母要用 32768不是 65535。这个坑我在 3.3 节已经提过但它是出现频率最高的“数据整体不对”的原因值得再强调一次。5.3 现象模拟输入电压接近VDD时读数不稳提到Vbat引脚现象是输入 3.0V 左右的电压时读数在 2.9V 到 3.2V 之间跳而输入 1V 时还算稳定。很多人会怀疑 STM32F103C8T6 的 Vbat 引脚以为它影响 ADC 参考电压。原因是 Vbat 引脚在 STM32F103C8T6 上只给 RTC 和备份寄存器供电和 ADS1115 的参考电压没关系。ADS1115 的参考电压直接来自 VDD 引脚VDD 上的纹波多大测量误差就有多大。开发板供电一般来自 USB 转串口或 DC-DC纹波往往不小输入接近满量程时误差会被放大得特别明显。解决方法是给 ADS1115 的 VDD 和 GND 之间加 10µF 电容靠近芯片处再放一个 100nF 陶瓷电容。如果 ADC 模块和单片机之间用长排线连接最好把模拟电源和数字电源在靠近模块处单点连接避免数字 I2C 信号串到模拟电源上。对精度要求更高的场景可以给 ADS1115 单独用一片低压差线性稳压器供电。5.4 现象切换通道后读到上一通道的数据现象是用连续模式读多个通道A 通道读完马上读 B 通道但 B 通道输出值里还带着 A 通道的残留尤其两个通道电压差很大的时候特别明显。原因是 ADS1115 在连续模式下会持续转换当前 MUX 配置的通道。切换 MUX 后转换结果寄存器不会立即更新为 B 通道可能需要等一个完整的转换周期。如果主程序在切换后立刻读寄存器拿到的还是上一个通道的结果。解决方法是切换到单次模式每次读通道前重新写配置并把 OS 位置 1这样转换结果必然只属于当前 MUX 指向的通道。如果坚持用连续模式切换通道后要把第一次读取结果丢弃取第二次或第三次的结果。这个坑我在第 3 章已经说过单次模式多通道轮询是更省心的做法。5.5 现象数据跳动大16位读数最后几位不停翻现象是输入稳定电压原始值低 4 位到 5 位一直跳算成电压有 1 mV 到 3 mV 的波动和理论分辨率对不上。原因是 ADS1115 内部是 Σ-Δ ADC数据速率越高数字滤波越弱噪声越大。128SPS 时 LSB 是 125µV但有效分辨率可能只有 15 位左右如果信号源内阻又高比如几 kΩA/D 采样的输入电流会在内阻上产生压降进一步加重跳动。解决方法是先降低数据速率到 32SPS 或 16SPS再在 AIN 输入前端加一个 RC 低通滤波器电阻选 1kΩ电容选 100nF截止频率约 1.6kHz不会影响慢信号但能抑制高频噪声。代码层面再加滑动平均这个做法放到下一章专门讲。6. 用定时器触发多通道采样并做滑动平均稳定住16位的最后两位前面的驱动已经有可用的数据链路但主循环轮询有个问题各个通道的采样间隔受循环里 delay 影响不是均匀的而且单次模式等待转换完成期间 CPU 白等。想稳定住 16 位 ADC 的最后两位需要把采样节奏交给定时器再用滑动平均把随机噪声压掉。先开一个定时器中断比如 TIM2 每 50ms 触发一次中断里只置一个采样标志主循环检测到标志后轮询四个通道。这样采样周期不会被 printf 的耗时打乱。滑动平均的代码很简单维护一个长度为 8 的环形缓冲每次把新采样值放进去求平均#define SMOOTH_N 8 float smooth_add(int16_t raw) { static int32_t sum 0; static int16_t buf[SMOOTH_N] {0}; static uint8_t idx 0; sum - buf[idx]; buf[idx] raw; sum buf[idx]; idx (idx 1) % SMOOTH_N; return (float)sum / SMOOTH_N; }这段代码把旧值从累加和里减掉再加入新值避免每次重新遍历数组。注意 sum 是 int32_t不会溢出。使用前先把 buf 清零。滑动平均会让响应变慢但对电池电压和温度这类信号完全没问题如果关心瞬态变化可以把 N 设成 4 或 2。验证方法是把输入接到一个稳定的直流电压源观察串口输出的毫伏值。先调好增益和速率再用滑动平均你会发现跳动幅度从几个毫伏压到 0.2mV 以内16 位最后的两位稳定下来。我最早做这块时没加滤波128SPS 下数据后几位一直跳还以为是 PCB 布线问题后来把速率降到 32SPS 并加了滑动平均问题立刻消失。ADS1115 驱动这件事硬件参数和软件滤波各占一半功夫希望这篇笔记能帮你少走一段弯路。本文还有配套的精品资源点击获取