STM32驱动ADS1110实现16位高精度ADC采集完整指南
简介本资源是一套面向STM32初学者与嵌入式开发者的ADS1110高精度ADC驱动实战代码包专为解决I²C接口16位模数转换芯片在STM32平台上的快速集成与稳定采样问题而设计适用于传感器数据采集、工业测控及教学实验等场景。压缩包共151个文件包含32个头文件.h定义寄存器与接口30个源码文件.c实现I²C底层通信、ADS1110初始化与16位AD值读取逻辑另有编译中间文件.o/.d/.crf、工程配置.uvproj/.uvopt、链接脚本.sct及调试输出.map/.axf等完整构建要素总大小1.64MB。已有1100人学习下载代码结构清晰含典型轮询式I²C忙等待检测、地址写入与读取时序控制并预编译了stm32f10x系列外设及ads1110_driver等关键模块依赖项可直接导入Keil MDK编译运行大幅降低硬件适配门槛。 最近做一个小型工业变送器项目需要把0到5V的模拟量采进来做实时监控单片机选的是STM32F103C8T6需求精度要求0.1%左右MCU内部12位ADC不论分辨率还是稳定性都有点勉强于是翻出ADS1110这颗16位I2C接口ADC芯片。折腾了几天驱动例程已经调通连接、配置、读数和滤波都走了一遍把过程整理成这篇笔记给同样在用ADS1110配STM32的朋友省点弯路。这篇文章适合三类人刚学STM32想外扩高精度ADC的入门者手头有ADS1110但I2C老是读不出数据、或者读出来数值乱跳的开发者以及在硬件I2C和软件模拟I2C之间拿不定主意的选型纠结户。内容覆盖芯片原理、I2C时序、驱动代码、实测噪声表现和常见坑位核心代码我按标准库风格写好放在后面可以直接抄进工程。1. ADS1110这颗16位ADC芯片到底强在哪1.1 一颗自带基准源和PGA的完整采集前端ADS1110是德州仪器出的单通道差分输入ADC16位分辨率I2C接口封装是SOT23-6整个芯片只有六只引脚不占PCB面积。和单片机自带的12位ADC不同ADS1110内部集成了2.048V基准电压源和可编程增益放大器输入范围可以在正负2.048V以内做多档调节这个集成度对小板子来说非常友好。芯片内部的关键路径是这样的模拟信号从VIN和VIN-差分输入先经过PGA放大再进入Delta-Sigma调制器数字滤波器把比量流变成16位二进制补码的结果最后通过I2C接口输出。用户能配置的东西主要是转换速率和PGA倍数全写在配置寄存器里写一次长期有效断电后恢复默认值。这里的“16位分辨率”要客观看待。ADS1110的差分输入当PGA为1倍时满量程是正负2.048V也就是从-2.048V到2.048V整个量程约4.096V量化到16位后每个LSB代表的电压约为0.0625mV这个精度对大多数传感器采集场景已经相当够用。但注意噪声和有效位数是两回事后面实测部分会专门讲。1.2 与STM32内置ADC、外部并口ADC的横向比较STM32F103内部ADC是12位虽然是逐次逼近型速度可以跑到1Mbps但实际做高精度采集时有两个短板一是参考电压VREF默认接在VDDA上VDDA受数字电路波动影响叠加上去基准噪声很难做到千分之一以上稳定度二是STM32的ADC输入阻抗不高直接接高内阻传感器时采样值会明显偏低需要加运放做缓冲。常见的并口ADC比如ADS1115或ADS1256是另外一条路线位数和精度更高但引脚多、接线复杂对大部分项目来说属于性能过剩。ADS1110走的是中间路线精度够用、引脚极少、带基准、带PGA、I2C单总线唯一妥协是速度很慢最高只有240SPS做低频传感器采集完全够用做音频或高速波形采集就别想了。对比项STM32内置12位ADCADS1110并口16位ADC分辨率12位16位16位起参考电压依赖VDDA精度一般内置2.048V精度高外置参考最准输入阻抗较低需缓冲高适合高内阻源高通信接口内部直接读取I2C并口/SPI引脚数量无外部成本6脚SOT23通常20脚以上采样速率最快约1MHz最高240SPS几十到上千SPS1.3 典型应用场景与选型边界单通道、低速率、高分辨率、小体积这四个词基本框定了ADS1110的用武之地。我做过的项目中用它采集过PT100铂电阻经电桥转换后的毫伏级差分信号配合2倍或4倍PGA放大省掉了独立的仪表放大器也用它做过电池电压监测把1.8V到3.3V的电压范围通过电阻分压后接入理论上分的精度完全够。还有一个典型场景是称重传感器。应变片电桥输出满量程只有几毫伏到十几毫伏STM32内部ADC根本没法直接采ADS1110配合PGA可以做到8倍甚至16倍放大把微弱信号直接拉起来省掉一级模拟放大电路。之前有人问过能不能用ADS1110做交流采样、做负压采样这里做一个声明ADS1110适合直流和慢变信号输入绝对电压必须在VDD和GND之间如果要测真正意义上的负压必须把参考点抬高这个后面在硬件连接里细说。2. I2C通信时序与ADS1110的寄存器读写细节2.1 从拨号到挂断I2C总线的通俗理解搞懂ADS1110之前得先把I2C这条总线的规矩理清楚。I2C只有两根线SCL负责时钟节拍SDA负责数据搬运所有设备都挂在这两根线上靠不同的地址区分彼此。这就像一个办公室主机是领导SCL是领导敲桌子的节奏SDA是传话的话筒每个员工有不同的分机号。起始条件主机在SCL保持高电平期间把SDA从高拉到低这就是“各位注意我要开始说话了”停止条件SCL为高期间SDA从低拉到高“散会”。之后每传一个字节都要等接收方拉低SDA回一个应答位相当于说“收到”如果没收到应答要么是设备没上电、地址不对要么是总线被卡死了。ADS1110的7位地址构成是1001加上芯片的A0引脚状态。A0接地时地址是0x90写、0x91读A0接VDD时是0x92写、0x93读A0接SCL时是0x94写、0x95读A0接SDA时是0x96写、0x97读。A0不能悬空我见过的很多“读不出数据”的坑八成是A0浮空导致地址随机漂移。2.2 配置寄存器决定速度、增益和自校准ADS1110内部只有一个配置寄存器8位。这个寄存器的功能拆开来看高两位ST控制采样速率中间两位SC保留不用低三位PGA控制增益第4位是自校准。默认值是0x8C也就是采样速率30SPSPGA倍数为4。配置位默认值功能可选值ST11采样速率选择见下表ST00采样速率选择见下表SC0自校准置1时开始校准PGA21增益选择见下表PGA10增益选择见下表PGA00增益选择见下表ST两位对应速率00是240SPS01是60SPS10是30SPS11是15SPS。PGA三位对应增益000到100对应1、2、4、8、16倍。注意PGA倍数越高有效输入范围越小以1倍增益满量程为基准2倍增益下满量程变成正负1.024V4倍变成正负512mV以此类推。SC自校准位一般不建议置1因为校准过程需要一个相对稳定的输入容易引入混乱我们就用默认的连续转换模式不需要额外的自校准操作。2.3 写入配置和读取转换结果的两套时序初始化时如果需要修改配置要写一次配置寄存器。地址是0x90发送地址后再发送配置值最后发停止位。代码上就是一次标准的I2C写操作没什么花活。真正的注意点在读数据的时序。ADS1110的转换结果存储在输出寄存器一共两个字节格式是16位二进制补码高字节在前。读取时直接发读地址0x91连续读两个字节就够了。关键是如果不手动写配置寄存器芯片会按照默认配置持续转换每次读到的都是上一次转换完成的结果。如果项目需要修改PGA或速率可以每次读之前先写配置寄存器再读数据。但实际使用中更推荐的做法是初始化时把配置寄存器写一次之后就一直保持默认读取路径避免每次读都插入写操作带来的时序复杂度。还有一个细节读第三个字节得到的是当前配置寄存器的值可以用来确认芯片是否按照预期工作调试时候很好用。3. 硬件I2C还是软件模拟I2C如何取舍3.1 两种驱动方式的本质区别网上关于STM32硬件I2C的吐槽很多尤其是标准库时期F1系列的硬件I2C被戏称“九死一生”事件标志位逻辑复杂、容易卡死在中断里很多人调了两天最后全换成GPIO软件模拟。HAL库时代硬件I2C稍微稳了一些但底层状态机的复杂性依旧存在。软件模拟I2C的本质是“用GPIO的高低电平变化拼出I2C时序”完全不依赖芯片内部I2C外设。好处是随便选两个空闲GPIO都能用时序完全由代码控制对于速率为几百KHz的慢速通信来说非常可靠坏处是高速场景扛不住理论上模拟I2C最高只能到几百KHz而硬件I2C可以达到400KHz甚至1MHz。ADS1110最高I2C速率是100KHz标准模式正好在软件模拟的舒适区内。我的建议是如果你用的标准库、代码规模不大用软件模拟省心且移植性强如果你用HAL库、习惯用中断或DMA配合外设可以尝试硬件I2C但务必把HAL库的ErrorCallback和超时机制写好。3.2 硬件连接和上拉电阻的选择ADS1110的引脚定义是VDD、GND、SCL、SDA、VIN、VIN-。VDD接3.3VGND与STM32共地SCL和SDA分别接STM32的PB6和PB7或者任意两个空闲GPIO两根线需要接上拉电阻到VDD阻值一般是4.7k到10k。上拉电阻是不可省略的I2C总线靠的是“线与”机制没有上拉电阻设备无法拉高电平通信必然失败。输入信号的接法要看信号是单端还是差分。如果是单端信号VIN接信号VIN-接模拟地如果是差分信号两根线分别接VIN和VIN-。无论哪种都要确保输入电压在VDD和GND之间超过就会使内部ESD二极管导通芯片发热甚至烧毁。测量0到5V信号时必须用电阻分压把最高电压降到2.048V以内我常用的分压比是2:1用两个1k电阻串联中间抽头接VIN。上拉电阻具体怎么算I2C要求上升时间不超过1微秒总线上电容一般按几十pF估算公式是上升时间约等于0.7乘以电阻乘以电容。用4.7k电阻配47pF电容算下来约0.15微秒妥妥达标。如果总线接了多个设备、线又长电容会变大上拉电阻适当降低到2.2k也是合理的但也不能太小否则总线功耗偏高。4. STM32驱动源码实现软件模拟I2C完整例程4.1 模拟I2C底层从Start到Stop的完整实现我用的是STM32标准库GPIO选用PB6做SCL、PB7做SDA全部配置为开漏输出带上拉模式。开漏输出的好处是代码中只需要控制拉低和释放两种状态即可模拟I2C电平不必担心引脚方向切换的问题。// I2C_Soft.h #ifndef __I2C_SOFT_H #define __I2C_SOFT_H #include stm32f10x.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 #define SCL_H() GPIO_SetBits(I2C_SCL_GPIO, I2C_SCL_PIN) #define SCL_L() GPIO_ResetBits(I2C_SCL_GPIO, I2C_SCL_PIN) #define SDA_H() GPIO_SetBits(I2C_SDA_GPIO, I2C_SDA_PIN) #define SDA_L() GPIO_ResetBits(I2C_SDA_GPIO, I2C_SDA_PIN) #define SDA_READ() GPIO_ReadInputDataBit(I2C_SDA_GPIO, I2C_SDA_PIN) void I2C_Soft_Init(void); void I2C_Start(void); void I2C_Stop(void); void I2C_SendByte(uint8_t byte); uint8_t I2C_RecvByte(void); uint8_t I2C_WaitAck(void); void I2C_SendAck(void); void I2C_SendNack(void); #endif对应的实现文件把每一段时序写得很直白。发送一个字节时从高位到低位逐位送SDASCL拉高一次表示一位数据有效。读取时同样逐位读SCL低电平期间读SDA的内容即可。// I2C_Soft.c #include I2C_Soft.h static void I2C_Delay(void) { uint8_t i 10; while (i--); } void I2C_Soft_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_OD; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); SCL_H(); SDA_H(); } void I2C_Start(void) { SDA_H(); SCL_H(); I2C_Delay(); SDA_L(); I2C_Delay(); SCL_L(); } void I2C_Stop(void) { SDA_L(); SCL_H(); I2C_Delay(); SDA_H(); I2C_Delay(); } void I2C_SendByte(uint8_t byte) { uint8_t i; for (i 0; i 8; i) { if (byte 0x80) SDA_H(); else SDA_L(); byte 1; I2C_Delay(); SCL_H(); I2C_Delay(); SCL_L(); I2C_Delay(); } } uint8_t I2C_RecvByte(void) { uint8_t i, val 0; SDA_H(); for (i 0; i 8; i) { SCL_H(); I2C_Delay(); val 1; if (SDA_READ()) val | 0x01; SCL_L(); I2C_Delay(); } return val; } uint8_t I2C_WaitAck(void) { uint8_t timeout 200; SDA_H(); I2C_Delay(); SCL_H(); I2C_Delay(); while (SDA_READ()) { if (--timeout 0) return 1; I2C_Delay(); } SCL_L(); I2C_Delay(); return 0; } void I2C_SendAck(void) { SDA_L(); I2C_Delay(); SCL_H(); I2C_Delay(); SCL_L(); I2C_Delay(); SDA_H(); } void I2C_SendNack(void) { SDA_H(); I2C_Delay(); SCL_H(); I2C_Delay(); SCL_L(); I2C_Delay(); }4.2 ADS1110驱动封装初始化、写配置、读电压有了I2C底层ADS1110的驱动就简单了。封装三个函数初始化配置、读取原始ADC值、读取转换成电压后的浮点值。使用默认配置时只需要在初始化里写一次0x8C或者干脆不写但为了后面要调整增益方便还是保留一个配置接口。// ADS1110.h #ifndef __ADS1110_H #define __ADS1110_H #include stm32f10x.h #define ADS1110_Addr_Write 0x90 #define ADS1110_Addr_Read 0x91 #define ADS1110_Config_15SPS_GAIN1 0xBC #define ADS1110_Config_30SPS_GAIN1 0xAC #define ADS1110_Config_240SPS_GAIN1 0x8C uint8_t ADS1110_Init(uint8_t config); int16_t ADS1110_ReadRaw(void); float ADS1110_ReadVoltage(float refV, float pga); #endif实际读取电压时需要把16位有符号数除以32768再乘以2倍基准电压再除以PGA倍数这个公式对应的是差分输入模式下面代码里写得很清楚。// ADS1110.c #include ADS1110.h #include I2C_Soft.h uint8_t ADS1110_Init(uint8_t config) { I2C_Soft_Init(); I2C_Start(); I2C_SendByte(ADS1110_Addr_Write); if (I2C_WaitAck()) { I2C_Stop(); return 1; } I2C_SendByte(config); if (I2C_WaitAck()) { I2C_Stop(); return 1; } I2C_Stop(); return 0; } int16_t ADS1110_ReadRaw(void) { uint8_t msb, lsb; int16_t value; I2C_Start(); I2C_SendByte(ADS1110_Addr_Read); if (I2C_WaitAck()) { I2C_Stop(); return 0; } msb I2C_RecvByte(); I2C_SendAck(); lsb I2C_RecvByte(); I2C_SendNack(); I2C_Stop(); value (int16_t)((msb 8) | lsb); return value; } float ADS1110_ReadVoltage(float refV, float pga) { int16_t raw ADS1110_ReadRaw(); float volt (float)raw * 2.0f * refV / 32768.0f / pga; return volt; }计算里的2.0倍来自正负满量程跨度。例如refV为2.048V、pga为1raw为32767时volt约等于2.048Vraw为-32768时volt约等于-2.048V折算公式一致。如果你的信号是单端输入、VIN-固定接GND那测出来0到正2.048V直接看volt就行负值不会出现。4.3 在main.c中调用并测试// main.c 片段 #include stm32f10x.h #include ADS1110.h #include usart.h float voltage; int16_t raw; int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); USART1_Init(115200); if (ADS1110_Init(ADS1110_Config_30SPS_GAIN1) 0) printf(ADS1110 init ok\r\n); else printf(ADS1110 init fail\r\n); while (1) { raw ADS1110_ReadRaw(); voltage ADS1110_ReadVoltage(2.048f, 1.0f); printf(raw:%d, voltage:%.4f V\r\n, raw, voltage); delay_ms(200); } }如果初始化时返回失败优先查硬件连接和上拉电阻再看A0引脚是否浮空。如果读取返回固定数值或一直为0多半是读写地址搞混用逻辑分析仪抓一下SDA波形确认主机发出的第一个字节是0x90还是0x91常规排查手法百试百灵。5. 实测读数与滤波处理有效位数比纸面16位更现实5.1 输入0V和输入2.048V的实际读数我用一个高精度直流稳压源给ADS1110供电同时用它给VIN提供稳定的直流电压VIN-接GNDPGA设为1倍速率30SPS串口打印100组数据观察稳定性。输入0V时原始码值在零附近小幅波动范围大概在正负3个LSB左右换算成电压就是正负0.2mV以内。输入2.000V时原始码值稳定在31900到32100之间换算电压在1.995V左右线性度和期望值基本吻合。这说明1倍增益下有效位数大约在14位到15位之间剩下的低1到2位被噪声吃掉属于正常现象不用恐慌。这里想强调一点数据手册标称的16位是“无丢码分辨率”实际能做到的有噪声有效位数取决于PCB布局、电源质量和参考源稳定度。如果你在开发板上飞线或者电源纹波很大实测噪声会翻倍这时候加滤波比继续抠硬件更划算。5.2 均值滤波与滑动平均的效果对比面对低速率ADC最常用的滤波策略是均值滤波。在30SPS采样率下连续取8次求平均等效速率降到约3.75SPS但标准差可以减少到原来的三分之一左右。因为随机噪声服从正态分布均值滤波对高频白噪声的抑制作用接近根号N的关系8次平均理论上能把噪声降到约35%。另一个做法是滑动平均每次读一个新值滑动窗口内取平均适合需要连续输出的场景。我给电池电压监测用的就是16点滑动平均效果很好实时性损失不大代码实现也就几行。在实际项目中把原始16位值和滤波后的16位值同时打印出来对比会非常直观。如果原始噪声在正负3个LSB以内滤波后稳定在正负1个LSB那这个精度对大多数应用已经足够。6. 踩坑记录I2C初始化失败、读数跳变、地址异常的排查方法6.1 最常见的五个问题与解决表格故障现象可能原因排查与解决初始化返回失败I2C_WaitAck超时上拉电阻缺失或阻值过大用万用表量SCL、SDA对地电压应为高电平接4.7k上拉读取值固定为0或固定一个数字读地址错误或A0悬空检查A0接法确认地址和代码一致用逻辑分析仪看地址字节读出的数值满量程正负32768附近输入超过PGA允许范围检查VIN、VIN-电压差确认没有超出满量程需要调分压比读数跳变很大噪声达到几十个LSB电源纹波大、信号线太长、布线不佳电源加10uF瓷片电容、输入信号用屏蔽线、缩短I2C线写入配置寄存器后仍然按默认参数运行写配置后立即读芯片还没刷新写配置后延时50ms再开始读数据或先启动转换再等完成6.2 用逻辑分析仪和串口打印快速定位排查I2C问题逻辑分析仪是效率最高的工具。把CH0接SCL、CH1接SDA抓一次读写过程能一眼看到起始条件、地址字节、应答位和数据位。最直观的判断起始条件后第一个字节是不是0x90或者0x91如果不是地址配置就有问题如果是但后面没有ACK位拉低说明芯片没有响应问题在上拉电阻或者芯片没正常上电。没有逻辑分析仪也可以只靠串口打印定位。初始化失败时可以在I2C_WaitAck里面加一个变量记录超时位置打印出来就能判断是地址阶段超时还是数据阶段超时。另外读出的原始值如果一直是一个固定值不变基本可以判定是I2C读到了错误的寄存器或者数据错位重新检查高字节、低字节的拼接顺序别把高低字节反了。6.3 三个来自实操的额外提醒第一ADS1110的VDD最好单独加一个100nF和10uF的电容靠近电源引脚模拟部分对电源噪声敏感这个电容能在很大程度上降低转换结果的低频抖动。第二如果输入信号来自长线传感器建议在接口处加一个RC低通滤波器比如1k电阻串联、100nF电容并联到地截止频率约1.6kHz可以有效滤除空间耦合进来的高频噪声。第三I2C两根线要尽量短不要超过20厘米转接板如果用了杜邦线长度越短越稳实测超过30厘米就偶尔出现通信错误。7. 直接把硬件I2C也聊一下HAL库下的快速配置思路如果你用的是STM32CubeMX生成的HAL库工程配置硬件I2C也很简单。在CubeMX里把I2C1设为I2C标准模式速率100kHz使能内部上拉或者外接上拉然后生成代码。之后调用HAL_I2C_Mem_Write和HAL_I2C_Mem_Read接口就可以完成对ADS1110的读写。写配置寄存器的HAL库调用uint8_t config 0x8C; HAL_I2C_Mem_Write(hi2c1, ADS1110_Addr_Write, 0x00, I2C_MEMADD_SIZE_8BIT, config, 1, 100);注意HAL库的Mem_Write接口会自动发送寄存器地址ADS1110没有真正的寄存器地址概念所以这里寄存器地址写0x00一次写一个字节即可。读数据时用Master_Receive接口先发读地址再连续收两个字节uint8_t buf[2]; HAL_I2C_Master_Receive(hi2c1, ADS1110_Addr_Read, buf, 2, 100); int16_t raw (int16_t)((buf[0] 8) | buf[1]);HAL库的硬件I2C在F1系列上最大的坑是总线繁忙标志如果设备掉线或者总线被拉死调用会超时需要调用HAL_I2C_DeInit再重新初始化。代码里加一个错误处理超时后复位I2C外设比一直卡在等待里要稳当得多。8. 完整驱动例程的扩展思路ADS1110驱动本身不难难在和其他模块协同工作。我这次的项目里最后把ADC读取任务放进了定时器中断每100ms读一次读到的电压值经过滑动平均滤波后存入全局变量主循环只负责显示和通信。这种方式比主循环里轮询更稳定不会因为串口打印或者其他任务阻塞导致采样间隔抖动。在驱动基础上还可以扩展几个功能一是增加多通道扩展ADS1110只有一个通道但要测多个点的时候可以外接模拟开关比如CD4051用两三个GPIO控制通道选择逻辑上做成一个“伪多通道ADC”二是增加零点校准和满量程校准软件里保存两组offset和scale系数把每次转换结果套上去可以消除电阻分压误差和基准偏差三是加入超量程判断当raw值接近正负32767时主动报“输入超量程”这个在工业现场很重要能提前暴露传感器故障。最后想说的是ADS1110虽然看起来只是一个小芯片但把它调通的过程把I2C协议、ADC精度、模拟布线、数据滤波这些嵌入式基本功都过了一遍。以后再去用ADS1115、ADS1256这些更复杂的ADC芯片思路完全相通无非是寄存器多几个、时序复杂一点底层的I2C通信逻辑是一样的。本文还有配套的精品资源点击获取