STM32 I2C驱动ADS1110高精度ADC详解与实战

发布时间:2026/8/31 19:18:23
STM32 I2C驱动ADS1110高精度ADC详解与实战 简介本资源是面向嵌入式初学者与STM32开发者的ADS1110高精度ADC驱动实战例程专为解决I²C接口16位模数转换芯片在STM32平台上的快速集成与稳定采样问题而设计。资源包共151个文件涵盖32个头文件.h定义寄存器与接口、30个源文件.c实现I²C底层通信、ADS1110初始化与读取逻辑含完整阻塞式状态轮询与应答配置以及编译中间文件.o/.d/.crf等和Keil工程核心文件.uvproj/.uvopt/.axf整体压缩后仅1.64MB轻量易导入。已有1100人学习下载说明其在传感器数据采集、电池电压监测、精密信号调理等典型应用场景中具备较强实用性。读者可直接复用Read_ads1110()等关键函数结合预览中列出的stm32f10x_i2c.__i、ads1110_driver.__i等依赖模块快速构建可靠AD采集功能无需从零调试I²C时序与器件寄存器配置。 我最近帮朋友调一块板子需要检测一路高精度电压信号单片机自带的12位ADC压根不够用最后选了TI的ADS1110。这芯片挺有意思的一颗小小的SOT23-6封装I2C接口内部集成基准源和可编程增益放大器16位分辨率直接挂在STM32的I2C总线上就能用。断断续续踩了几个坑把驱动调通了现在把整个过程写下来包括硬件连接、寄存器配置、代码实现和排错经验给后面要用这颗芯片的朋友省点时间。1. ADS1110是什么芯片为什么项目里会选它1.1 这颗芯片的核心规格和定位ADS1110是TI推出的精密连续自校准模数转换器本质上是一颗ΔΣDelta-Sigma型ADC内部集成了时钟发生器、基准电压源、放大器和I2C接口属于那种外围电路极其精简的芯片。供电范围2.7V到5.5V正常工作时电流大概240μA待机模式下更低非常适合电池供电的便携设备。核心参数有这么几个分辨率16位无丢码内部基准电压2.048V可编程增益放大器PGA增益倍率1、2、4、8可选转换速率15SPS、30SPS、60SPS、240SPS四档差分输入通道也可以配置成单端输入I2C接口地址可通过引脚配置这芯片最吸引人的地方就是它把基准源、时钟、ADC核心全部集成在内部不需要外部提供基准电压芯片也不需要外部晶振只要接上电源和I2C两根线就能工作。就像你买了一个带内置电源和时钟的精密仪表接上就能直接读数。1.2 和单片机内置ADC相比它到底强在哪可能有人会问STM32内部也有ADC为什么还要外接一颗16位的这得看具体使用场景。STM32的ADC一般是12位分辨率参考电压通常是VDD也就是3.3V那么最低有效位对应的电压就是3.3V/4096 ≈ 0.8mV。如果测量一个0V到2.048V范围的信号12位ADC的最低位分辨率大概是0.5mV。而ADS1110在16位分辨率下满量程按内部基准2.048V计算最低有效位是2.048V/65536 ≈ 31.25μV换算一下就是0.03125mV。对一个微弱信号检测场景来说这个分辨率差距就是能看出波动和看不出变化的差别。另外还有一点非常关键ADS1110的基准源精确度和温漂性能都比单片机内部的参考源好很多。单片机内置ADC的参考电压受温度和电源噪声影响比较明显ADS1110内部基准的温漂指标在数据手册里给了明确的规格稳定性远好于大多数MCU内置ADC。所以如果项目里要求的是电池电压精确监测传感器微弱信号放大采集称重传感器桥式信号读取这类场景外置高精度ADC几乎是必须的。1.3 什么时候不适合用ADS1110这不是一个万能芯片有几个场景我不推荐用需要高速采样。最高240SPS意味着每个转换周期约4.17ms想在控制环里做高速采样不现实。如果要几千Hz以上的采样率得去看ADS1115仍然偏慢或者干脆上并行接口的高速ADC。需要多通道同时采集。ADS1110只有一路输入虽然是差分结构但不是多通道。如果项目需要同时采集多路信号又不想加模拟开关可以考虑ADS11154通道或者用多颗ADS1110挂在同一条I2C总线上——芯片地址可以通过A0、A1引脚配置最多挂4颗。输入信号幅度超过基准范围。2.048V的基准决定了满量程大约±2.048V如果被测信号本身是0到5V必须先在外部做分压或者用运放调理电路。2. 硬件连接和I2C通信链路搭建2.1 引脚连接与I2C上拉电阻的选型ADS1110的引脚功能如下引脚号名称功能1VDD电源正极2.7V~5.5V2VIN-差分输入负极3VIN差分输入正极4SDAI2C数据线5SCLI2C时钟线6GND电源地7ADDR地址配置引脚SOT23-6虽然只有6个脚但DSBGA封装是另一种ADS1110是SOT23-6封装上面这个表格是按照芯片手册的引脚排列写的实际焊接时要仔细对照丝印和手册方向搞反了上电必烧。说到I2C上拉电阻这是初学者最容易忽略的。I2C总线是开漏结构SCL和SDA线必须通过上拉电阻接到VDD才能输出高电平。ADS1110内部集成了弱上拉但是内部上拉阻值通常在几百kΩ级别实际通信时完全不够用必须外部加上拉电阻。我习惯用4.7kΩ上拉到3.3V这在400kHz速率以下都很稳定。如果总线上挂的设备多、走线长可以换成2.2kΩ增加驱动能力。实际接法很简单VDD接3.3VGND接地在VDD和GND之间放一个100nF去耦电容靠近芯片引脚放置。SCL接STM32的PB6SDA接STM32的PB7以STM32F103的I2C1为例。SCL和SDA分别通过4.7kΩ电阻上拉到3.3V。VIN-接信号地VIN接被测信号如果只做单端测量VIN-直接接GND就行。ADDR引脚接地或者接VDD用于配置I2C地址后面会详细说。2.2 I2C地址是如何确定的能不能挂多颗芯片ADS1110的I2C器件地址是固定7位加上一位读写位。7位地址的高4位固定为1001低3位由ADDR引脚的接法决定。ADDR引脚接GND时低3位是000接VDD时是001接SDA时是010接SCL时是011。再加上一种高阻状态如果芯片有可以配出更多组合让一条总线上最多挂4颗ADS1110每颗地址不同互不干扰。默认情况下ADDR接地那么7位地址就是0b1001000换算成十六进制就是0x48这是最常见的地址值。在代码里配置I2C设备地址时填0x48就行。注意I2C地址有7位和8位两种写法有些驱动库写地址时需要左移一位比如0x48左移一位变成0x90才是带写标志的完整字节。使用STM32标准库时I2C_SendData7bitAddress函数接收的是7位地址直接传0x48即可这点和HAL库一致HAL库的I2C_Transmit是传7位地址。但有些自己封装的软件模拟I2C函数里需要的是8位地址特别容易搞混建议在代码注释里明确写清7位地址0x48或者8位地址0x90。2.3 STM32的I2C时钟配置要点STM32的I2C外设时钟源来自APB1在标准库中配置I2C时钟需要知道APB1的频率。比如系统时钟72MHz时APB1通常是36MHzI2C外设的输入时钟就是36MHz。配置I2C速率时代码里要写I2C_InitTypeDef I2C_InitStructure; I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress 0x30; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed 100000; // 100kHz标准模式ADS1110支持标准模式100kHz和快速模式400kHz。实测下来100kHz最稳妥兼容性最好400kHz虽然规格支持但实际布线和上拉电阻不好的时候容易出问题。我的建议没有特殊速度需求就按100kHz来稳定压倒一切。3. 寄存器配置和数据解析的底层逻辑3.1 配置寄存器的每一位意味着什么ADS1110只有一个配置寄存器8位。上电默认值是0x8C二进制是10001100。每次启动转换前需要向这个寄存器写入合适的配置值。它的位定义如下位序号名称功能7ST/DR1启动单次转换0连续转换模式6-5SC转换速率选择4-3PGA增益设置2-1MUX输入多路复用配置0保留读时返回0ST/DR位是整个芯片工作模式的总开关。如果忽略配置直接上电芯片默认是连续转换模式不断采样更新转换寄存器代码只需要读取转换结果寄存器就行。但如果想要省电或者需要精确控制采样时刻就把ST/DR置1启动一次转换等转换完成后读取结果然后芯片会回到低功耗状态。SC位决定转换速率0015SPS0130SPS1060SPS11240SPSPGA位决定增益00增益1满量程±2.048V01增益2满量程±1.024V10增益4满量程±0.512V11增益8满量程±0.256VMUX位决定输入方式00AIN0相对AIN1的差分输入01AIN0相对AIN1的差分输入同00历史原因保留10AIN0相对GND的单端输入11AIN1相对GND的单端输入这里有个关键点容易踩坑PGA增益和输入范围直接相关。如果输入信号接近2V把增益配成8倍那么输入超过±0.256V就会导致输出饱和读回来的数据一直是0x7FFF正向满量程或者0x8000负向满量程。我一开始接的是0到3.3V的电位器分压信号默认配置增益1读数正常后来想提高灵敏度改成增益8结果读数一直顶着满量程排查了半天才发现是输入信号太大了。3.2 转换数据的格式与拼接方法ADS1110的转换结果存放在16位数据寄存器中二进制补码格式。读取I2C数据时先读到高字节再读到低字节拼接时需要左移8位再按位或uint16_t raw_data (uint8_t)(buf[0] 8) | buf[1];但这里要特别注意符号位。如果读取结果是负值直接把16位数据转成int16_t就能正确还原负数。比如差分输入下VIN比VIN-低时转换结果是负数对应二进制补码0x8000到0xFFFF这个区间。计算实际电压时公式如下float voltage (float)((int16_t)raw_data) * 2.048f / 32768.0f;为什么除以32768而不是65536因为补码格式下16位有符号数范围是-32768到32767满量程对应的正峰值是32767负峰值是-32768。如果按无符号数除以65536再乘基准电压会把负值算成很大的正数这我在实际调试中遇到过看到读数变成3V多才知道符号处理错了。4. STM32驱动代码实现与逐句注解4.1 基于标准库的底层I2C读写函数封装我在项目中用的是STM32F103C8T6标准外设库Standard Peripheral LibraryI2C1主模式。直接把工程里能用的代码贴出来顺便把关键点说透。首先初始化I2Cvoid I2C_Init_ADS1110(void) { GPIO_InitTypeDef GPIO_InitStructure; I2C_InitTypeDef I2C_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_OD; // 开漏复用输出 GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); I2C_InitStructure.I2C_Mode I2C_Mode_I2C; I2C_InitStructure.I2C_DutyCycle I2C_DutyCycle_2; I2C_InitStructure.I2C_OwnAddress 0x30; I2C_InitStructure.I2C_Ack I2C_Ack_Enable; I2C_InitStructure.I2C_AcknowledgedAddress I2C_AcknowledgedAddress_7bit; I2C_InitStructure.I2C_ClockSpeed 100000; I2C_Init(I2C1, I2C_InitStructure); I2C_Cmd(I2C1, ENABLE); }GPIO模式一定要配置成开漏复用输出GPIO_Mode_AF_OD不能配置成推挽输出。I2C协议本身要求开漏结构如果配成推挽外部上拉电阻和内部推挽会打架轻则通信异常重则损坏引脚。这个我第一版代码就写错了调了两天才发现。4.2 启动转换并读取完整流程ADS1110没有独立的读转换结果命令寄存器。芯片在连续转换模式下主设备发起I2C读操作芯片就会把当前的转换结果发出来但不是直接发数据而是先发配置寄存器的内容。具体读时序是发送设备地址读方向读第1个字节配置寄存器读第2个字节转换结果高字节读第3个字节转换结果低字节非应答NACK后停止我封装了一个函数读3个字节然后解析uint8_t ADS1110_ReadConversion(int16_t *adc_value, float *voltage) { uint8_t buf[3]; I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, ADS1110_ADDR, I2C_Direction_Transmitter); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED)); // 写配置寄存器启动单次转换 uint8_t config 0x8C; // 10001100: 单次转换15SPS增益1AIN0-AIN1差分 I2C_SendData(I2C1, config); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED)); I2C_GenerateSTOP(I2C1, ENABLE); // 等待转换完成15SPS时约66ms delay_ms(80); // 发起读操作 I2C_GenerateSTART(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT)); I2C_Send7bitAddress(I2C1, ADS1110_ADDR, I2C_Direction_Receiver); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED)); // 读配置寄存器忽略 I2C_AcknowledgeConfig(I2C1, ENABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); buf[0] I2C_ReceiveData(I2C1); // 读高字节 while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); buf[1] I2C_ReceiveData(I2C1); // 读低字节最后一个字节前要发NACK I2C_AcknowledgeConfig(I2C1, DISABLE); while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_RECEIVED)); buf[2] I2C_ReceiveData(I2C1); I2C_GenerateSTOP(I2C1, ENABLE); I2C_AcknowledgeConfig(I2C1, ENABLE); *adc_value (int16_t)((buf[1] 8) | buf[2]); *voltage (float)(*adc_value) * 2.048f / 32768.0f; return 0; }4.3 连续采样时的数据稳定性处理上面的代码是单次转换模式。如果项目需要持续监测电压更常见的方案是配置成连续转换模式然后周期性读取结果寄存器。连续模式的配置很简单把配置寄存器最高位ST/DR置0uint8_t config 0x0C; // 00001100: 连续转换15SPS增益1AIN0-AIN1差分然后读函数可以简化成每次都读最后的转换结果不需要等待。实测下15SPS速率每次读取间隔约66.7ms即可数据稳定。如果读取间隔太短读到的是上一次的结果这本身没问题只是数据看起来更新不够快。为了提升稳定性我还会在软件上做一个简单的中位值滤波连续读5次去掉最大值和最小值取中间3次的平均值。ADS1110本身噪声已经很小了但外部环境的工频干扰有时还是会耦合进来滤波后数据非常平滑。5. 实测排坑记录和几个容易被忽视的细节5.1 读不到数据或者读到0xFF——先查地址再查总线上拉我调试时遇到过的最大坑是用逻辑分析仪抓I2C波形能明显看到SCL正常翻转但SDA上设备就是不应答ACK没有拉低。后来排查发现是芯片地址搞错了——我用了0x90作为7位地址传给I2C库库内部又把地址左移了一位结果实际发到总线上的地址是0x20设备自然不会应答。标准库的I2C_Send7bitAddress明确要求传7位地址所以传0x48就好不用自己移位。如果地址没问题但仍然读不到下一步检查SDA和SCL有没有上拉电阻。我用示波器量过没有上拉电阻时SDA线一直是低电平根本没有办法拉高I2C协议直接瘫痪。这时候最直观的表现是I2C事件标志一直等不到程序卡死在while循环。接上4.7kΩ上拉电阻到3.3V后一切正常。还有一个比较隐蔽的问题STM32的I2C1对应PB6/PB7但很多开发板把PB6/PB7同时接到了其他外设上比如有些板子的PB6/PB7默认接了LED或者按键这些外部器件会影响I2C时序。发现问题的方法是量一下SCL/SDA引脚在空闲时的电压正常情况下应该是高电平因为上拉如果被拉低或者只有零点几伏那多半是引脚上挂了不该挂的东西。5.2 数据跳变异常——从硬件和软件两方面排查数据跳变一般分两种第一种是数值跳动范围非常大比如从0跳到30000再跳回来。这种情况优先怀疑接触问题特别是用杜邦线连接的时候。I2C本身是低速总线杜邦线连接几十厘米问题不大但如果信号源是从一个内阻很高的传感器直接接进来的没有经过阻抗匹配那么ADC输入端的电荷注入会引起信号波动需要在对地并联一个100nF电容做滤波。第二种是数值在小范围内波动比如稳定在15000附近但±10个数跳。这是正常的量化噪声特别是15SPS低速率下已经算很好了。想要更平滑就做软件滤波或者降低增益使信号幅度在量程范围内尽量大提高信噪比。我在实际项目里还遇到过一次差分输入接反的情况VIN接到了信号地VIN-接到了信号正极读数一直是负的。代码里把int16_t直接打印出来看到负数我当时还以为是芯片坏了后来才发现是输入线接反了。这也是为什么推荐先用单端输入模式调试信号源正极接VINVIN-接地先把链路调通再切换差分模式。5.3 硬件I2C和软件模拟I2C怎么选STM32的硬件I2C有时候会让人头疼特别是标准库下用EV5、EV6这些事件标志时状态机比较绕。如果调试时间紧张软件模拟I2C也是一种完全可行的方案。ADS1110速率最高400kHz软件模拟I2C在72MHz主频下完全可以跑出稳定的100kHz时序。软件模拟的核心代码就三件事起始信号、字节传输、停止信号。我自己的经验是如果项目已经用了标准库对I2C很熟就用硬件I2C效率高。如果是刚上手想快速验证ADS1110的功能直接用软件模拟I2C更省心代码量不大排查问题也更直观。下面给一个精简的软件I2C写字节函数void I2C_Soft_Start(void) { SDA_H(); SCL_H(); delay_us(5); SDA_L(); delay_us(5); SCL_L(); } void I2C_Soft_Stop(void) { SCL_L(); SDA_L(); delay_us(5); SCL_H(); delay_us(5); SDA_H(); } uint8_t I2C_Soft_WriteByte(uint8_t data) { uint8_t i; for (i 0; i 8; i) { if (data 0x80) SDA_H(); else SDA_L(); data 1; delay_us(2); SCL_H(); delay_us(4); SCL_L(); delay_us(2); } // 释放SDA等待ACK SDA_IN(); delay_us(2); SCL_H(); delay_us(4); uint8_t ack !SDA_READ(); SCL_L(); SDA_OUT(); return ack; }5.4 小信号测量时PGA增益的选择规律选择增益有个基本逻辑尽量让输入信号接近满量程这样分辨率利用率最高但绝不能超过满量程。比如信号范围是0到250mV用增益8倍最合适满量程±256mV250mV的信号映射到接近满量程精度最高。如果用增益1倍满量程±2.048V250mV只占了约12%的量程有效分辨率大打折扣。不过增益越高输入偏置电流和噪声的影响也越明显。ADS1110在增益8时等效输入噪声会稍微大一点但这个噪声是指芯片本身的噪声贡献对于250mV级别的信号来说仍然可以接受。我实际测量的结果是增益8、15SPS下输入短路时读数在±2个LSB内波动也就是大约±0.5μV这个精度对大多数传感器信号来说已经非常理想了。6. 把驱动封装成通用模块方便后续项目复用调通之后我把代码整理成了一个独立的驱动模块包含ads1110.h和ads1110.c两个文件把硬件相关的部分做了宏定义隔离换芯片平台时只需要改引脚定义和延时函数核心逻辑完全不用动。这也是我在做驱动时的一个习惯——把硬件相关的东西全部收敛到文件头部方便移植。// ads1110.h #ifndef __ADS1110_H #define __ADS1110_H #include stm32f10x.h #define ADS1110_ADDR 0x48 #define ADS1110_GAIN_1 0x00 #define ADS1110_GAIN_2 0x08 #define ADS1110_GAIN_4 0x10 #define ADS1110_GAIN_8 0x18 #define ADS1110_RATE_15SPS 0x00 #define ADS1110_RATE_30SPS 0x20 #define ADS1110_RATE_60SPS 0x40 #define ADS1110_RATE_240SPS 0x60 #define ADS1110_MUX_AIN0_AIN1 0x00 #define ADS1110_MUX_AIN0_GND 0x04 #define ADS1110_MUX_AIN1_GND 0x0C void ADS1110_Init(void); uint8_t ADS1110_ReadSingle(int16_t *value, float *voltage); uint8_t ADS1110_ReadContinuous(int16_t *value, float *voltage); #endif这样封装好之后在应用层调用就非常简洁了int16_t adc_val; float volt; if (ADS1110_ReadSingle(adc_val, volt) 0) { printf(ADC: %d, Voltage: %.4f V\r\n, adc_val, volt); }封装函数时还有一个细节返回值的约定。我统一用0表示成功非0表示失败失败时通过一个全局错误码变量说明原因这样在主程序中可以针对不同错误做提示或者重试。比如返回值1表示I2C总线忙返回值2表示设备无应答返回值3表示转换超时。这个习惯在调试多设备系统时非常有用能快速定位是哪一环出了问题。如果你打算以后多个项目复用建议把配置寄存器的构造也封装成一个函数传入增益、速率、输入方式三个参数返回配置字节这样改配置的时候不需要去翻寄存器手册uint8_t ADS1110_BuildConfig(uint8_t mode, uint8_t rate, uint8_t gain, uint8_t mux) { return (mode 0x80) | (rate 0x60) | (gain 0x18) | (mux 0x0C); }调用时就很直观uint8_t config ADS1110_BuildConfig(1, ADS1110_RATE_15SPS, ADS1110_GAIN_1, ADS1110_MUX_AIN0_GND);7. 最后聊几点实际使用中的体会ADS1110这颗芯片给我最大的印象就是小巧但功能完整在精密测量场景下是个省心的选择。回头总结几个关键经验希望能帮后来者少走弯路。第一I2C通信出了问题一定要先查硬件再查软件。我用逻辑分析仪抓波形很快就能判断是地址问题、上拉问题还是时序问题。大多数人一上来就怀疑代码逻辑实际上大部分I2C问题出在硬件连接。第二ADS1110的默认配置可能不是你想用的配置。上电默认是连续转换、15SPS、增益1、差分输入。如果你需要其他增益或者输入模式一定要主动写配置寄存器不要依赖默认值。第三软件滤波不是万能的。如果发现数据有周期性跳变先查电源纹波和工频干扰再考虑滤波算法。我在一块开关电源供电的板子上测50Hz的纹波会明显耦合到ADC数据里滤波后虽然平滑了但真实信号也被抹掉了一部分。后来换了线性电源供电问题直接消失。这次调通之后我又把这套驱动移植到了HAL库的STM32F407板子上底层I2C读写函数换了一下上面的配置和解析逻辑一行没改就能跑。这也说明把驱动按硬件无关层和应用层分开设计确实能省下不少重复劳动。如果你也需要在项目里做高精度模拟量采集ADS1110值得一试。本文还有配套的精品资源点击获取