简介基于STM32F103的ADS1115驱动代码面向需要高精度模拟量采集的嵌入式开发者适用于工业监测、环境传感、便携仪表等场景既适合初学者快速上手也便于工程师移植到自有项目中。压缩包共189个文件约6.69MB以.c源文件与.h头文件为主同时包含Keil工程配置文件uvprojx/uvoptx、hex烧录文件及编译中间产物头文件对ADS1115引脚功能作了详细注释对照芯片手册连接SCL/SDA即可完成硬件对接。已有2825人学习下载。驱动基于标准外设库实现I2C通信封装了器件初始化、配置PGA增益与采样率、读取16位转换结果等函数充分发挥ADS1115高分辨率、4通道、低功耗的优势。另附“测试ADS1115”示例工程包含初始化ADC、读取输入并显示结果的验证流程可帮助开发者快速确认硬件接线和通信配置是否正确大幅缩短调试时间。 搞嵌入式这几年ADC精度不够用的情况我碰得不少。STM32F103自带12位ADC常规采集电压、电流凑合可用可真遇到高精度测量场景——称重传感器、温度毫伏信号、电池电压监测——12位分辨率就明显不够看了。我最近一个项目里外挂了一片TI的ADS111516位分辨率、内置基准、I2C接口、4路输入小封装低功耗整套驱动写下来也就几百行代码实测效果非常稳。这篇就把我踩过的坑和能直接复用的代码完整整理出来供做采集类项目的朋友参考。ADS1115驱动本身并不复杂但有几个地方容易栽跟头I2C时序细节、寄存器配置字的位含义、单次转换与连续转换模式的选择还有STM32F103硬件I2C的一些历史遗留问题。这篇文章会从方案选型讲到硬件接线再到标准外设库下的完整驱动代码最后把实测数据和典型问题排查表一起放出来。无论是刚入门还是已有基础照着做都能顺利跑起来也能弄明白每一行配置到底在干什么。1. 方案选型为什么最终选了ADS11151.1 这颗ADC芯片解决了什么痛点先聊选型。项目里要采集4路模拟量精度要求到微伏级才有意义STM32F103内置的12位ADC在这个场景下确实力不从心3.3V参考下的量化步长约0.8mV这还只是理论分辨率实际布线和参考电压噪声一叠加有效位数能到10位就已经不错。而对一片ADS1115来说16位线性分辨率、内置低漂移基准、可编程增益放大器PGA这三项是它的核心优势。PGA是很实用的一项功能它允许把输入信号放大到匹配的量程。比如称重传感器输出只有几毫伏直接给ADC会白白浪费分辨率把PGA设到±0.256V量程最小LSB只有7.8uV小信号就能被有效分辨。这一点是MCU内置ADC没法比的。再加上I2C接口只占两根信号线一片就能顶4路模拟输入还能通过ADDR引脚在一条总线上挂4片扩展性非常灵活。1.2 软件I2C和硬件I2C的取舍提到STM32F103和I2C老工程师基本都有一个共识F103的硬件I2C外设行为在不少从机配合下会出现总线忙碌、时序异常等问题这已经是社区里讨论很多年的老话题。ADS1115本身I2C时序不苛刻所以我在这个驱动里直接用GPIO模拟I2C也就是软件I2C跑起来反而更省心。软件I2C在ADS1115这种低速传感器场景下没有任何性能瓶颈100kHz到400kHz随便跑底层代码可控、方便调时序、引脚可以随便复用调试阶段的灵活性比硬件I2C高很多。如果你的项目必须用硬件I2C也可以参照后面的寄存器读写逻辑改一下调用层但个人建议还是软件I2C更稳——至少这几批板子实测下来从未出现总线挂死的情况。2. 硬件连接和地址配置2.1 引脚接线参考我用的是STM32F103C8T6最小系统板SCL接PB6SDA接PB7这俩引脚在初始化时配成推挽输出配合外部上拉电阻使用。ADS1115侧VDD接3.3VGND共地AIN0到AIN3接待测信号ADDR引脚接地。完整接线表整理如下照着接就不容易出错ADS1115引脚接STM32引脚/电平说明VDD3.3V供电范围2.0V~5.5VGNDGND共地SCLPB6I2C时钟加4.7k欧上拉到3.3VSDAPB7I2C数据加4.7k欧上拉到3.3VADDRGND决定I2C地址接法见下方说明AIN0~AIN3模拟输入单端或差分输入电压不能超过VDD2.2 I2C地址选择ADS1115的7位I2C地址由ADDR引脚电平决定常见就这四种接法ADDR接法7位地址8位写地址8位读地址GND0x480x900x91VDD0x490x920x93SDA0x4A0x940x95SCL0x4B0x960x97在单总线上接多片ADS1115时通过不同的ADDR电平可以挂4片也就是16路单端输入这种扩展方式在采集板设计里非常实用。地址宏定义通常写成8位读写地址因为软件I2C发送时是一个字节左移一位后的形式更直观。2.3 硬件设计上的几个注意点第一VDD旁边必须放100nF去耦电容尽量靠近芯片引脚条件允许再并一个10uF电容。ADS1115是内置基准的电源纹波会直接影响转换结果电源处理不好代码层面再怎么优化也白搭。第二输入电压绝对不能超过VDD加0.3V否则内部ESD二极管会导通轻则测量偏差重则芯片损坏。有负压或过压风险时建议加钳位保护。第三差分输入要留意共模电压范围。例如PGA设置为±4.096V量程时共模输入必须落在GND-0.2V到VDD0.2V之间超出范围测量值会异常。3. 驱动代码实现从底层I2C到寄存器读写3.1 GPIO模拟I2C底层软件I2C的核心是时序准确。我习惯用一个简单的延时循环控制翻转频率实测在72MHz主频下大约200kHz以内完全满足ADS1115要求。下面这段是用标准外设库V3.5写的初始化代码// ads1115_i2c.c —— 软件I2C底层 #include ads1115_i2c.h #define I2C_SCL_GPIO GPIOB #define I2C_SCL_PIN GPIO_Pin_6 #define I2C_SDA_GPIO GPIOB #define I2C_SDA_PIN GPIO_Pin_7 static void I2C_Delay(void) { uint8_t i 20; while (i--); } void I2C_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin I2C_SCL_PIN | I2C_SDA_PIN; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(I2C_SCL_GPIO, GPIO_InitStructure); GPIO_SetBits(I2C_SCL_GPIO, I2C_SCL_PIN | I2C_SDA_PIN); }需要说明的是之所以把GPIO配成推挽输出而不是开漏是因为外置上拉已经保证了电平可靠性推挽输出在切换电平时的斜率更陡时序更稳定。SDA在读数据时需要切换成浮空输入读完再切回推挽输出。3.2 I2C Start、Stop和字节收发接下来是协议层三个关键函数起始信号、停止信号、字节读写。这里直接给出平时在用的版本简洁且经过反复验证static void I2C_Start(void) { I2C_SDA_OUT(); GPIO_SetBits(I2C_SDA_GPIO, I2C_SDA_PIN); GPIO_SetBits(I2C_SCL_GPIO, I2C_SCL_PIN); I2C_Delay(); GPIO_ResetBits(I2C_SDA_GPIO, I2C_SDA_PIN); // SCL高电平期间SDA下降沿 I2C_Delay(); GPIO_ResetBits(I2C_SCL_GPIO, I2C_SCL_PIN); } static void I2C_Stop(void) { I2C_SDA_OUT(); GPIO_ResetBits(I2C_SDA_GPIO, I2C_SDA_PIN); GPIO_SetBits(I2C_SCL_GPIO, I2C_SCL_PIN); I2C_Delay(); GPIO_SetBits(I2C_SDA_GPIO, I2C_SDA_PIN); // SCL高电平期间SDA上升沿 }字节收发有几个细节值得强调。发送字节时数据必须在SCL低电平期间准备好从机在SCL上升沿后采样所以每位移位后先拉高SCL再延时再拉低顺序不能反。接收字节时主机在第8个时钟下降沿之后释放SDA然后在第9个时钟周期根据参数决定回ACK还是NAK。ADS1115读转换结果时最后一字节一定发NAK告诉从机不再读了通信才正确结束。static void I2C_WriteByte(uint8_t dat) { uint8_t i; I2C_SDA_OUT(); for (i 0; i 8; i) { if (dat 0x80) GPIO_SetBits(I2C_SDA_GPIO, I2C_SDA_PIN); else GPIO_ResetBits(I2C_SDA_GPIO, I2C_SDA_PIN); dat 1; I2C_Delay(); GPIO_SetBits(I2C_SCL_GPIO, I2C_SCL_PIN); I2C_Delay(); GPIO_ResetBits(I2C_SCL_GPIO, I2C_SCL_PIN); I2C_Delay(); } } static uint8_t I2C_ReadByte(uint8_t ack) { uint8_t i, dat 0; I2C_SDA_IN(); for (i 0; i 8; i) { GPIO_SetBits(I2C_SCL_GPIO, I2C_SCL_PIN); I2C_Delay(); dat 1; if (GPIO_ReadInputDataBit(I2C_SDA_GPIO, I2C_SDA_PIN)) dat | 0x01; GPIO_ResetBits(I2C_SCL_GPIO, I2C_SCL_PIN); I2C_Delay(); } // 第9个时钟处理ACK/NAK I2C_SDA_OUT(); if (ack) GPIO_ResetBits(I2C_SDA_GPIO, I2C_SDA_PIN); else GPIO_SetBits(I2C_SDA_GPIO, I2C_SDA_PIN); I2C_Delay(); GPIO_SetBits(I2C_SCL_GPIO, I2C_SCL_PIN); I2C_Delay(); GPIO_ResetBits(I2C_SCL_GPIO, I2C_SCL_PIN); I2C_SDA_OUT(); GPIO_SetBits(I2C_SDA_GPIO, I2C_SDA_PIN); return dat; }3.3 ADS1115寄存器读写封装搞定了I2C底层ADS1115的访问就简单了。ADS1115内部有4个指针寄存器通过发送指针字节选择0x00是转换结果寄存器0x01是配置寄存器0x02/0x03是高/低阈值寄存器。驱动代码的核心就是“写配置、启动转换、等待完成、读结果”这条链路。地址宏和配置位宏定义如下// ads1115.h #ifndef __ADS1115_H #define __ADS1115_H #include stm32f10x.h // I2C地址ADDR接GND #define ADS1115_WR_ADDR 0x90 #define ADS1115_RD_ADDR 0x91 // 寄存器指针 #define ADS1115_REG_CONVERSION 0x00 #define ADS1115_REG_CONFIG 0x01 #define ADS1115_REG_LO_THRESH 0x02 #define ADS1115_REG_HI_THRESH 0x03 // 配置寄存器常用位段 #define ADS1115_CFG_OS (1 15) // 启动转换/状态 #define ADS1115_CFG_PGA_6144 (0x0 9) // ±6.144V #define ADS1115_CFG_PGA_4096 (0x1 9) // ±4.096V #define ADS1115_CFG_PGA_2048 (0x2 9) // ±2.048V #define ADS1115_CFG_PGA_1024 (0x3 9) // ±1.024V #define ADS1115_CFG_PGA_0512 (0x4 9) // ±0.512V #define ADS1115_CFG_PGA_0256 (0x5 9) // ±0.256V #define ADS1115_CFG_MODE_ONCE (1 8) // 单次转换模式 #define ADS1115_CFG_DR_8 (0x0 5) // 8SPS #define ADS1115_CFG_DR_16 (0x1 5) #define ADS1115_CFG_DR_32 (0x2 5) #define ADS1115_CFG_DR_64 (0x3 5) #define ADS1115_CFG_DR_128 (0x4 5) // 128SPS #define ADS1115_CFG_DR_250 (0x5 5) #define ADS1115_CFG_DR_475 (0x6 5) #define ADS1115_CFG_DR_860 (0x7 5) #define ADS1115_CFG_COMP_QUE_DIS (0x3) // 禁止比较器 #endif寄存器写函数和读函数我保持直观的封装没有引入过重的软件层// ads1115.c #include ads1115.h void ADS1115_WriteReg(uint8_t reg, uint16_t value) { I2C_Start(); I2C_WriteByte(ADS1115_WR_ADDR); I2C_WriteByte(reg); I2C_WriteByte((uint8_t)(value 8)); I2C_WriteByte((uint8_t)(value 0xFF)); I2C_Stop(); } uint16_t ADS1115_ReadReg(uint8_t reg) { uint16_t value; I2C_Start(); I2C_WriteByte(ADS1115_WR_ADDR); I2C_WriteByte(reg); I2C_Start(); // 重复起始信号 I2C_WriteByte(ADS1115_RD_ADDR); value (uint16_t)I2C_ReadByte(1) 8; value | I2C_ReadByte(0); // 最后一字节回NAK I2C_Stop(); return value; }这里有个新手很容易踩的坑读寄存器之前必须先发一个“写指令”把指针指向目标寄存器然后用重复起始信号切换为读模式。如果直接发读地址ADS1115返回的是它上一次操作过的寄存器内容往往不是你现在想要的。3.4 单次转换模式下的通道读取函数我把单次转换模式的流程封装成ADS1115_ReadRaw传入通道号0到3函数内部完成配置写入、等待转换完成、读回原始值三个动作uint16_t ADS1115_ReadRaw(uint8_t channel) { uint16_t cfg; uint16_t timeout 10000; cfg ADS1115_CFG_OS; // bit15置1启动单次转换 cfg | ((uint16_t)(0x4 channel) 12); // 单端通道AIN0-GND100, AIN1-GND101... cfg | ADS1115_CFG_PGA_4096; // ±4.096V量程 cfg | ADS1115_CFG_MODE_ONCE; // 单次转换模式 cfg | ADS1115_CFG_DR_128; // 128SPS噪声更低 cfg | ADS1115_CFG_COMP_QUE_DIS; // 禁用比较器 ADS1115_WriteReg(ADS1115_REG_CONFIG, cfg); // 查询配置寄存器bit15等待转换完成 do { cfg ADS1115_ReadReg(ADS1115_REG_CONFIG); } while ((cfg ADS1115_CFG_OS) 0 --timeout 0); return ADS1115_ReadReg(ADS1115_REG_CONVERSION); }这里的通道宏有个小技巧ADS1115单端模式的MUX编码是连续的AIN0-GND是100、AIN1-GND是101、AIN2-GND是110、AIN3-GND是111所以直接对0x4加通道号再左移12位就能拼出对应配置。我习惯保留这种写法代码简洁后期加通道也只需改一个数字。电压换算单独抽一个函数。PGA为±4.096V时满量程对应原始值-32768到32767LSB等于4.096V除以32768即0.125mVfloat ADS1115_ReadVoltage(uint8_t channel, float fsr) { uint16_t raw ADS1115_ReadRaw(channel); int16_t signedRaw (int16_t)raw; return (float)signedRaw * fsr / 32768.0f; }调用时ADS1115_ReadVoltage(0, 4.096f)就会返回AIN0对地电压单位伏。这里有个强制转换细节必须注意raw必须强转成有符号数否则负数采样值会被当成大正数参与运算电压结果直接翻车这个细节我见不少人踩进去过。3.5 差分输入怎么读如果项目里接的是差分信号比如电桥、电流采样电阻两端那就用差分模式。MUX设为000时正输入是AIN0负输入是AIN1转换结果就是两脚之间的差。实现上把配置字中的MUX段改成0x0 12即可其它流程完全一样。差分模式和单端模式在代码逻辑上唯一的区别就是MUX编码不同不需要额外封装函数写配置时换一下就行。需要提醒的是PGA增益和量程必须和输入信号实际范围匹配。比如输入信号最大约1V建议把量程设在±2.048V而不是±4.096V同样16位下分辨率从0.125mV提升到0.0625mV。量程选得紧凑精度才能吃满但也要留出一点裕量防止信号超量程。4. 实测记录与问题排查4.1 我是怎么验证I2C链路通断的代码写好之后第一步不是直接接传感器而是先验证I2C通信通不通。我的惯用做法是初始化完成后用读配置寄存器的函数读回芯片的默认配置0x8583。如果读出来的值和数据手册里的POR值一致I2C链路基本就通了不一致就先查地址和上拉电阻不要在测量数据上浪费太多时间。实测中使用标准外设库编译后下载到C8T6供电3.3VI2C上拉4.7k欧ADDR接GND。读回0x8583非常稳定。接着接一个2.5V基准电压源到AIN0用±4.096V量程、128SPS配置去采样读取到的原始值在19980到20000之间波动换算电压2.497到2.500V误差在千分之二以内完全满足项目指标。4.2 常见问题速查表实际调试中我整理了几个高频问题的排查方向直接放在表格里方便对照现象可能原因处理办法读回全是0xFFFF地址不对或从机未上电核对ADDR电平测量VDD确认地址宏读回始终0x0000或0x8000输入悬空/短路I2C通信中断检查SDA/SCL波形接一个已知电压源测试数据读数跳变范围大电源纹波、信号线屏蔽差、量程不匹配加强去耦缩短引线检查PGA量程I2C偶尔卡死在等待循环SCL/SDA上拉缺失或SDA电平异常补4.7k欧上拉排查焊点虚焊不建议在没抓波形时盲目改代码。很多I2C问题用逻辑分析仪一看就能定位是电平问题还是时序问题值得在调试阶段就接上能省下大量排查时间。4.3 提高采样精度的三条经验第一采样率不是越高越好。ADS1115最高支持860SPS但高采样率下内部抽取滤波器的平均次数减少等效位数会下降对于慢变化信号8到128SPS才是合理选择读数更稳定、噪声更低。第二如果项目精度要求高建议把电源做干净并考虑用差分输入方式抵消共模噪声。我在一个电池监测项目里就是把电池负极采样用差分接法配合PGA设为±0.256V读数稳定性比单端接法提升明显。第三PCB布线时模拟输入线尽量短避免和数字信号线平行走长距离模拟地和数字地单点连接。这些硬件层面的做法比任何代码优化都有效也往往是测出来的数据是否漂亮的分水岭。5. 扩展HAL库移植与工程优化思路5.1 HAL库下怎么快速移植如果你的工程是用STM32CubeMX生成的移植这套代码也很简单。CubeMX里把PB6、PB7配成GPIO_Output速度选High然后在代码里把底层函数中的GPIO操作换成HAL库API即可。#define SCL_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET) #define SCL_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET) #define SDA_H() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_SET) #define SDA_L() HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET) #define SDA_READ() HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_7)寄存器读写函数、配置解析逻辑、电压换算公式完全不用动只管把最底层的宏替换掉。需要注意HAL_Delay的精度是毫秒级不适合当软件I2C的位延时建议用DWT或者定时器做微秒级延时。5.2 多通道轮询和软件滤波项目里如果4路通道都要采可以做一个简单的轮询调度。每次通道切换后第一次读到的转换结果建议丢弃因为上一通道的残余电压还滞留在采样电容上直接使用会造成数据串扰。实际测试中切换通道后多读一次并忽略该次结果通道间隔离度会好很多。软件滤波方面最实用的还是滑动平均维护一个长度为N的环形缓冲区每次采到新值就覆盖最旧的值取平均作为输出。对于128SPS的采样率N取8到16即可既能滤掉高频毛刺又不会让响应变得太迟钝。// 简单滑动平均N8 #define FILTER_N 8 static int32_t sum 0; static uint16_t buf[FILTER_N] {0}; static uint8_t index 0; uint16_t ADS1115_FilteredRead(uint8_t channel) { uint16_t raw ADS1115_ReadRaw(channel); sum - buf[index]; buf[index] raw; sum raw; index (index 1) % FILTER_N; return (uint16_t)(sum / FILTER_N); }这里注意求和变量要用int32_t因为8个16位数据累加最大是524280uint16_t存不下。这个细节虽然基础但真有人在这上面栽过。这套驱动代码我在几个实际项目的不同批次板卡上都跑过稳定性是经得起验证的。回顾整个过程最花时间的其实不是代码本身而是把量程、PGA、采样率这些配置和实际需求匹配好。如果你刚上手建议先按这里的默认参数跑通再逐步调整到自己项目的量程和采样率——每一步都好验证出问题也好定位。后续想扩展的话可以在这个基础上加环形缓冲、均值滤波甚至移植到RTOS的采集任务里核心的寄存器读写函数基本不用动。本文还有配套的精品资源点击获取