STM32驱动ADS1115:16位高精度电压采集实战
发布时间:2026/10/14 15:29:13 锦皓数字建站

简介这是一份面向嵌入式开发者的STM32与ADS1115高精度电压采集驱动代码适合需要16位ADC检测、负电压读取及精确采样场景的工程师学习参考。资源包共3个文件包含2个C源文件与1个H头文件分别承担ADS1115驱动实现、寄存器配置与主程序调用示例压缩包整体约3KB结构精简便于直接移植到现有工程。已有6735人学习下载说明其在同类驱动方案中具有较高参考价值。代码围绕IIC接口通信展开涵盖初始化、通道选择、采样速率配置、命令发送与数据读取等关键环节并针对IIC时序、中断触发读取、电源模式调整及误差校准等实际问题给出实现思路。经测试每秒可读取860个数据点兼顾速度与精度最小分辨率约9.8uV可帮助读者快速搭建稳定可靠的电压测量系统适用于电力监控、环境监测与工业自动化等需要精准采样的项目。1. 基于STM32的ADS1115高精度电压读取从跳变到稳定的那条路如果你用STM32的ADC直接采过毫伏级信号大概率见过这种场面万用表显示1.245V串口打印出来的值在1.238到1.252之间来回蹦偶尔还给你来个1.300的尖峰。这不是玄学是STM32内置12位ADC在量程、参考源和噪声上的综合妥协。换成基于STM32的ADS1115高精度电压读取方案情况会明显不同——ADS1115是一颗16位、I2C接口、内置PGA和基准的ΔΣ型ADC分辨率比STM32内置ADC高16倍差分输入还能直接干掉共模干扰。这套组合适合做电池电压监测、传感器微弱信号采集、电桥输出读取这类对精度有真实要求的场景。下面把我从选型、接线、配置到排错的全过程拆开讲新手能照着跑通熟手能看到参数边界和踩坑点。2. 为什么ADS1115值得挂在STM32上分辨率、PGA与I2C的取舍2.1 16位ΔΣ和12位SAR的本质差距STM32内置ADC多数是12位逐次逼近型SAR单端输入参考电压通常直接取VDDA。假设VDDA是3.3V1个LSB就是3.3V/4096≈0.8mV。你要测的1.245V信号理论量化误差就有±0.4mV再加上电源纹波、地弹和内部参考的温漂实际有效位数ENOB往往只有9到10位。ADS1115是16位ΔΣ架构1个LSB在±4.096V量程下是4.096V/327680.125mV在±0.256V量程下更是低到0.0078mV。ΔΣ靠过采样和噪声整形把量化噪声推到高频再数字滤波掉所以低频段的有效分辨率远高于同位数SAR。这不是纸面参数好看是实测串口跳动从几十个码值降到几个码值的直接原因。2.2 PGA把微小信号“抬”到ADC面前ADS1115内部有一个可编程增益放大器PGA支持±6.144V、±4.096V、±2.048V、±1.024V、±0.512V、±0.256V六档。PGA的作用不是放大后输出而是把输入信号在进入调制器前调整到合适的动态范围。比如你测一个电桥输出满量程只有±20mV如果直接进±4.096V档信号只占满量程的0.5%有效位数浪费严重。切到±0.256V档信号占比提升到7.8%分辨率利用充分。选档原则很简单预估最大输入电压选最接近但不会超的档位。注意PGA的输入范围是相对于GND的绝对限制差分模式下VIN和VIN-都不能超出VDD0.3V或低于GND-0.3V否则内部ESD二极管导通读数直接废掉。2.3 I2C速率与数据率的匹配关系ADS1115支持标准模式100kHz和快速模式400kHz的I2C时钟。STM32的硬件I2C在400kHz下读写ADS1115的转换结果寄存器16位大约需要60到80μs。ADS1115的数据率可配为8、16、32、64、128、250、475、860SPS。如果你设860SPS转换周期约1.16msI2C传输时间远小于转换时间不会成为瓶颈。但如果你用软件模拟I2C400kHz下实际波形可能只有100到200kHz读一次结果要几百微秒这时如果数据率设475SPS以上就可能出现读到的还是上一次结果的情况。常见做法是硬件I2C用400kHz数据率设128SPS或250SPS留足余量软件I2C降到100kHz数据率不超过128SPS。2.4 单端与差分不是随便接两根线ADS1115有4个单端输入或2个差分输入。单端模式下每个通道测的是对GND的电压但GND线上的任何压降都会叠加到结果里。差分模式测的是VIN减VIN-共模干扰和地线压降被抵消。测电池电压时如果电池负极和STM32的GND不是同一点接地单端读数会随负载电流变化而漂移差分接法能压住这个问题。配置寄存器里MUX[2:0]位决定输入方式100是AIN0对GND单端000是AIN0对AIN1差分。差分模式下PGA的输入范围是VIN和VIN-各自相对于GND的绝对范围不是差值范围这点在选档时容易翻车。3. 用STM32 HAL库把ADS1115跑起来接线、配置与读取3.1 硬件连接与上拉电阻的取值ADS1115的SDA和SCL是开漏输出必须接上拉电阻。阻值不是随便选400kHz下总线电容走线引脚按100pF估算上升时间要求小于300ns上拉电阻最大约2.2kΩ如果总线电容到200pF最大约1.1kΩ。我一般用2.2kΩ到4.7kΩ之间3.3V系统下4.7kΩ能跑400kHz但波形上升沿偏缓2.2kΩ更稳但静态电流大一点。ADDR引脚接GND时地址是0x48接VDD是0x49接SDA是0x4A接SCL是0x4B。电源去耦必须做VDD和GND之间放一个0.1μF陶瓷电容紧贴芯片引脚否则内部基准的噪声会直接调制到输出码上。// ads1115.h #ifndef ADS1115_H #define ADS1115_H #include stm32f1xx_hal.h // 按实际芯片系列改 #define ADS1115_ADDR_GND 0x48 #define ADS1115_ADDR_VDD 0x49 #define ADS1115_ADDR_SDA 0x4A #define ADS1115_ADDR_SCL 0x4B // 寄存器指针 #define ADS1115_REG_CONV 0x00 #define ADS1115_REG_CONF 0x01 // 配置寄存器位定义部分 #define ADS1115_OS_SINGLE 0x8000 #define ADS1115_MUX_DIFF_0_1 0x0000 #define ADS1115_MUX_SINGLE_0 0x4000 #define ADS1115_PGA_4_096 0x0000 #define ADS1115_PGA_2_048 0x0200 #define ADS1115_PGA_1_024 0x0400 #define ADS1115_PGA_0_512 0x0600 #define ADS1115_PGA_0_256 0x0800 #define ADS1115_DR_128SPS 0x0080 #define ADS1115_MODE_SINGLE 0x0100 #define ADS1115_COMP_DISABLE 0x0003 void ADS1115_Init(I2C_HandleTypeDef *hi2c); int16_t ADS1115_ReadRaw(uint16_t mux, uint16_t pga, uint16_t dr); float ADS1115_ReadVoltage(uint16_t mux, uint16_t pga, uint16_t dr); #endif上面头文件把地址、寄存器指针和常用配置位都定义好了。注意PGA档位对应的宏值PGA_4_096是0x0000PGA_2_048是0x0200依次递增这是配置寄存器bit11到bit9的编码。数据率宏值对应bit7到bit5128SPS是0x0080。这些宏拼装时直接按位或就行不用再移位。3.2 配置寄存器写入与转换启动ADS1115每次单次转换都要写配置寄存器启动转换然后等OS位bit15变1表示转换完成再读转换寄存器。等待方式有两种轮询OS位或延时。轮询更可靠因为数据率设不同值时转换时间不同延时容易短了或长了。// ads1115.c #include ads1115.h static I2C_HandleTypeDef *ads_i2c; void ADS1115_Init(I2C_HandleTypeDef *hi2c) { ads_i2c hi2c; // 上电后先写一次配置确保进入已知状态 uint8_t cfg[3] {ADS1115_REG_CONF, 0x00, 0x00}; HAL_I2C_Master_Transmit(ads_i2c, ADS1115_ADDR_GND 1, cfg, 3, 100); } int16_t ADS1115_ReadRaw(uint16_t mux, uint16_t pga, uint16_t dr) { uint16_t config ADS1115_OS_SINGLE | mux | pga | dr | ADS1115_MODE_SINGLE | ADS1115_COMP_DISABLE; uint8_t tx[3]; tx[0] ADS1115_REG_CONF; tx[1] (config 8) 0xFF; tx[2] config 0xFF; // 写配置启动单次转换 if (HAL_I2C_Master_Transmit(ads_i2c, ADS1115_ADDR_GND 1, tx, 3, 100) ! HAL_OK) return 0x7FFF; // 错误标志实际项目应做更细处理 // 轮询OS位等待转换完成 uint8_t rx[2]; uint16_t status; uint32_t tickstart HAL_GetTick(); do { HAL_I2C_Master_Transmit(ads_i2c, ADS1115_ADDR_GND 1, tx[0], 1, 100); HAL_I2C_Master_Receive(ads_i2c, (ADS1115_ADDR_GND 1) | 1, rx, 2, 100); status (rx[0] 8) | rx[1]; if (HAL_GetTick() - tickstart 200) return 0x7FFF; // 超时保护 } while (!(status 0x8000)); // 读转换结果 uint8_t reg ADS1115_REG_CONV; HAL_I2C_Master_Transmit(ads_i2c, ADS1115_ADDR_GND 1, reg, 1, 100); HAL_I2C_Master_Receive(ads_i2c, (ADS1115_ADDR_GND 1) | 1, rx, 2, 100); return (int16_t)((rx[0] 8) | rx[1]); } float ADS1115_ReadVoltage(uint16_t mux, uint16_t pga, uint16_t dr) { int16_t raw ADS1115_ReadRaw(mux, pga, dr); float fsr; switch (pga) { case ADS1115_PGA_4_096: fsr 4.096f; break; case ADS1115_PGA_2_048: fsr 2.048f; break; case ADS1115_PGA_1_024: fsr 1.024f; break; case ADS1115_PGA_0_512: fsr 0.512f; break; case ADS1115_PGA_0_256: fsr 0.256f; break; default: fsr 4.096f; break; } return (float)raw * fsr / 32768.0f; }这段代码的关键点写配置时先发寄存器指针0x01再发两个字节配置值。轮询OS位时每次都要重新发指针0x01再读两个字节不能连续读因为ADS1115的I2C接口没有自动地址递增。读转换结果时发指针0x00再读两个字节拼成int16_t。注意返回值是有符号的因为差分模式下结果可能是负的。满量程对应32768所以电压换算用raw * fsr / 32768.0f。如果PGA档位和实际输入不匹配比如输入2V却选了±0.256V档结果会饱和在0x7FFF换算出来接近0.256V但实际已经削顶了。3.3 单端与差分读取的调用差异单端读AIN0对GNDmux传ADS1115_MUX_SINGLE_0pga按输入范围选。差分读AIN0和AIN1mux传ADS1115_MUX_DIFF_0_1。注意差分模式下如果VIN-接地VIN接信号结果和单端类似但共模抑制更好。如果VIN和VIN-都接在电桥两端读数就是桥臂压差。调用时数据率建议先用128SPS稳定后再往上调。// main.c 片段 float v_single ADS1115_ReadVoltage(ADS1115_MUX_SINGLE_0, ADS1115_PGA_4_096, ADS1115_DR_128SPS); float v_diff ADS1115_ReadVoltage(ADS1115_MUX_DIFF_0_1, ADS1115_PGA_0_512, ADS1115_DR_128SPS); printf(single: %.4f V, diff: %.4f V\r\n, v_single, v_diff);单端读电池电压时如果电池和STM32共地且走线短v_single够用。差分读传感器时v_diff能压掉地线上的共模波动。实际项目里我一般先单端粗测确定信号范围后再切差分和合适PGA档。4. 精度上不去的五个血泪坑从现象到解决4.1 读数总在最后两位跳电源和地去耦没做对现象串口打印的电压值稳定在1.245V附近但最后两位在±3个码值内随机跳。原因ADS1115的VDD直接取自STM32板上的3.3V而3.3V上挂着MCU、显示屏和无线模块开关噪声通过电源耦合进ADC内部基准。解决在ADS1115的VDD引脚就近放0.1μF加1μF电容必要时用磁珠或独立LDO供电。地线要单点接地ADS1115的GND和信号源地接在一起不要和电机、继电器共地。4.2 差分读数出现负值但实际是正电压PGA档位选错现象测一个0到100mV的差分信号选了±4.096V档读数在0附近跳偶尔负几十个码值。原因信号只占满量程的2.4%量化噪声和失调电压把真实信号淹没了。解决切到±0.256V档信号占比提升到39%读数立刻稳定。注意切档后要重新校准零点因为PGA的输入失调随增益变化。4.3 I2C通信偶尔失败上拉电阻和总线电容不匹配现象连续读几百次后HAL_I2C返回HAL_ERROR或HAL_TIMEOUT重启后恢复。原因上拉电阻4.7kΩ偏大加上杜邦线较长总线电容超过200pF400kHz下上升沿太缓ADS1115认不到起始条件。解决换2.2kΩ上拉缩短走线或者把I2C时钟降到100kHz。用逻辑分析仪抓波形看上升时间超过300ns就降速或换电阻。4.4 读到的值总是上一次的转换未完成就读取现象快速连续调用读取函数发现电压值变化滞后一个周期。原因轮询OS位时如果配置写完后立刻读转换寄存器OS位还没置1读到的是上一次结果。解决严格按3.2的轮询逻辑每次读之前重新发配置寄存器指针并检查bit15。如果数据率设860SPS转换时间1.16ms轮询间隔要小于这个时间否则会多等。4.5 单端多通道切换后第一读不准输入多路切换建立时间现象从AIN0切到AIN1第一次读数偏差大第二次恢复正常。原因ADS1115内部多路开关切换后输入采样电容需要建立时间尤其在高阻抗信号源下。解决切换通道后丢弃第一次读数或者降低数据率给建立时间留余量。信号源阻抗超过10kΩ时在输入端加电压跟随器缓冲。5. 把精度再压榨一点校准、滤波与差分桥接的进阶手法5.1 两点校准消掉增益和失调误差ADS1115出厂有失调和增益误差虽然不大但做精密测量时值得校准。方法用高精度源给两个已知电压比如0.100V和2.000V读回原始码值算斜率和截距。代码里存两个系数每次换算时先乘斜率再加截距。注意校准要在最终PGA档位和数据率下做换档后系数不通用。typedef struct { float slope; float offset; } ADS_Calib; ADS_Calib cal {1.0f, 0.0f}; void ADS_Calibrate(float v1, int16_t raw1, float v2, int16_t raw2) { cal.slope (v2 - v1) / (float)(raw2 - raw1); cal.offset v1 - cal.slope * raw1; } float ADS_ReadCalibrated(uint16_t mux, uint16_t pga, uint16_t dr) { int16_t raw ADS1115_ReadRaw(mux, pga, dr); return cal.slope * raw cal.offset; }校准系数存在MCU的Flash里上电加载。两点校准能把增益误差从0.1%压到0.01%以内代价是需要一个可信的参考源。没有高精度源时可以用STM32的VREFINT校准内部参考但精度有限。5.2 滑动平均与中值滤波的组合ADS1115在128SPS下噪声已经很小但工频干扰和电源纹波仍会贡献几个码值的跳动。滑动平均取8到16个点能把随机噪声压到1个码值以内代价是响应变慢。中值滤波对尖峰脉冲特别有效取5个点排序取中间值能干掉偶发的I2C误码或电源毛刺。我一般先中值再平均顺序不能反否则尖峰会被平均摊到多个结果里。#define FILTER_LEN 8 static int16_t buf[FILTER_LEN]; static uint8_t idx 0; int16_t ADS_Filter(int16_t new_val) { buf[idx] new_val; if (idx FILTER_LEN) idx 0; int32_t sum 0; for (int i 0; i FILTER_LEN; i) sum buf[i]; return (int16_t)(sum / FILTER_LEN); }这个滑动平均没有做溢出保护FILTER_LEN和码值范围要匹配。16位有符号数最大327678个相加最大262136int32_t够用。如果数据率设860SPS8点平均的等效输出率约107Hz对大多数电池监测和温度采集够用。5.3 差分桥接测电阻把ADS1115当比例式ADC用测RTD或应变片时常见做法是电桥加ADS1115差分输入。电桥激励和ADS1115的基准同源时激励的漂移会被比例式测量抵消。具体接法电桥两端接激励电压中点接AIN0和AIN1PGA选±0.256V或±0.512V。读数换算成电阻时用比例式公式而不是绝对电压能消掉激励源的温漂。这个手法在称重和温度测量里很实用代价是需要匹配电桥电阻否则共模电压可能超出PGA范围。5.4 用STM32定时器触发单次转换需要固定采样率时用定时器中断触发ADS1115单次转换而不是在main循环里轮询。定时器设成1ms周期中断里启动转换并读结果主循环只处理数据。这样采样间隔均匀做FFT或数字滤波时频谱干净。注意中断里I2C传输时间不能超过定时周期400kHz下读一次约80μs1ms周期绰绰有余。如果数据率设860SPS转换时间1.16ms定时周期要大于这个值否则会丢转换。5.5 验证精度的一个土办法没有六位半万用表时用一节新干电池加一个多圈电位器分压输出接ADS1115和一块已知精度的手持表同时读几十个点算最大偏差和标准差。如果偏差在±1mV以内标准差小于0.5mV这套方案就算调通了。我习惯在代码里留一个原始码值打印开关排错时看码值比看电压更直接因为电压换算会掩盖PGA档位和符号问题。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。