ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32F103C8T6驱动ADS1015:12位I2C ADC采集与寄存器配置详解

2026/9/2 10:22:30 拓冰建站 浏览量
STM32F103C8T6驱动ADS1015:12位I2C ADC采集与寄存器配置详解 简介ADS1015 驱动源码工程面向 STM32F103C8T6 平台实现与 ADS1015 模数转换器的 IIC 通信适合需要采集电压、温度、压力等多路模拟信号的嵌入式开发者。工程从 IIC 初始化、配置寄存器、读取转换结果到电压换算形成完整闭环并预留串口打印方便实时查看采样数据。RAR 压缩包内共 74 个文件主要包括 35 个 .h 头文件和 33 个 .c 源文件另有 Keil 的 .uvprojx/.uvoptx 工程配置、hex 固件和清理脚本包体仅 277KB结构精简适合直接导入开发环境运行。已有 1617 人学习下载说明该工程在 STM32 与 ADS1015 开发场景中有一定参考价值适合 STM32 初学者和需要快速集成 ADS1015 的工程师参考。源码按 KEY、ADC、LED、CORE、SYSTEM 等目录组织便于定位功能模块通过阅读代码可理解 ADS1015 的 PGA 增益、采样率、单端/差分输入选择等关键寄存器设置以及 IIC 时序和错误处理思路。开发者可基于串口输出进一步扩展无线传输或云端监控等应用。1. 项目概述为什么用 STM32F103C8T6 驱动 ADS10151.1 ADS1015 这颗 ADC 到底值不值得用先说说这个项目为什么会出现。ADS1015 是德州仪器出的一颗 12 位分辨率、带有 I2C 接口的模数转换芯片它内部集成了可编程增益放大器PGA、以及一个可配置的多路复用输入选择器支持 4 路单端输入或 2 路差分输入。板子上还带了一个内部基准源精度在大多数工业采集场景下够用。这颗芯片在很多 DIY 项目、电池监测、环境传感器采集、电流电压采样板里出现频率非常高原因就三个字省事、稳、够用。而 STM32F103C8T6 想必不用多说了Cortex-M3 内核、72MHz 主频、20KB RAM、64KB Flash最经典的那颗蓝板芯片。很多人手里都囤着几片最小系统板用来做各种传感器数据采集、电机控制、简易仪器。为什么非要在 STM32 上再外挂一颗 ADS1015 而不是直接用 STM32 自带的 ADC这个问题我后文会专门展开先卖个关子。1.2 STM32F103C8T6 的资源与选型逻辑回到项目本身。STM32F103C8T6 内置的 ADC 是 12 位、逐次逼近型最多支持 10 个外部通道采样速率最高能到 1MHz 左右。看起来规格和 ADS1015 差距不大但真正做项目时你会发现几个痛点内置 ADC 的参考电压和供电电压绑在一起VDDA如果供电纹波大采出来的数值会跟着抖多通道采集时通道切换会有串扰而且内置 ADC 只有 12 位精度加上芯片内部噪声实际有效位数经常只有 10 到 11 位。ADS1015 则完全独立于主控供电有自己内部基准和可编程增益实测下来有效位数稳定在 11 到 12 位左右虽然比不上 ADS1115 的 16 位但在 3.3V 系统里做 0 到 5V 或者小信号采集完全够用。再加上它天生就是 I2C 接口根本不需要占用额外的定时器和 DMA 资源软件模拟 I2C 也行、硬件 I2C 也可以代码写起来非常清爽。这个项目的目标就是在 STM32F103C8T6 上通过 I2C 总线完整驱动 ADS1015实现多通道、可配置 PGA 增益、单次和连续两种转换模式的 ADC 采集并且把采集到的原始值换算成真实电压。听起来简单但真正把驱动代码写规范、把数据读准、把噪声压下去还是有不少门道的。2. 寄存器配置与驱动设计思路2.1 配置寄存器——决定一切的那个 16 位ADS1015 的寄存器总共就两个用户可操作的转换寄存器地址 0x00和配置寄存器地址 0x01。转换寄存器是只读的里面存的是最近一次 ADC 转换的结果数据宽度 16 位但实际有用的只有高 12 位低 4 位始终为零所以读回来之后通常要右移 4 位才是真正的 12 位数据。真正麻烦的是配置寄存器这 16 位拆开来看其实非常规整第 15 位OS单次转换模式下写 1 启动转换连续模式下读取此位可知转换是否完成。第 14 到 12 位MUX选择输入通道。单端模式下分别对应 AIN0 到 AIN3差分模式下可以选择 AIN0-AIN1、AIN0-AIN3、AIN1-AIN3、AIN2-AIN3 组合。第 11 到 9 位PGA增益选择决定满量程范围从 ±6.144V 到 ±0.256V 一共 8 档。第 8 位MODE0 表示连续转换模式1 表示单次模式默认。第 7 到 5 位DR数据速率从 128 SPS 到 3300 SPS。第 4 到 0 位比较器相关设置大多数场景直接设为 00011 禁用比较器。我第一次调这颗芯片的时候犯过一个典型的错误配置寄存器写成 0x8183 就直接发出去理所当然地期望它开始转换结果读回来的数值一直是上一轮的死值。后来才反应过来单次模式下必须先写配置寄存器触发一次转换转换完成后数据才会更新到转换寄存器而且这个过程中配置寄存器的 OS 位会从 1 自动清 0。也就是说单次模式下每次读数据之前都得重新写一次配置寄存器来踢一脚启动转换。2.2 指针寄存器与 I2C 读写时序ADS1015 的 I2C 通信遵循典型的寄存器读取流程先从器件地址默认 0x48取决于 ADDR 引脚的接法发送一个指针字节指定接下来要操作的是哪个寄存器然后再进行读或写操作。写配置寄存器时的完整时序是Start - 发送 0x481写方向- 发送 0x01指针指向配置寄存器- 发送配置高字节 - 发送配置低字节 - Stop。读转换寄存器时则要分成两步先发送 0x481写方向- 发送 0x00指针指向转换寄存器- Stop然后再发 Start - 发送 0x481|0x01读方向- 连续读两个字节 - Stop。这里有个细节需要留意ADS1015 支持一种自动增量模式也就是说写完指针寄存器后可以直接发读命令芯片会自动从指针当前位置开始读取。但为了代码的通用性和可读性我建议每一步都显式设置指针寄存器多一个字节的传输成本几乎可以忽略但出错的概率会小很多。3. 基于 STM32 HAL 库的驱动代码实现3.1 I2C 初始化与硬件连接驱动代码用的是 STM32CubeMX 生成的 HAL 库工程I2C1 配置为 400kHz 快速模式。连接方式如下ADS1015 引脚STM32F103C8T6 引脚VDD3.3VGNDGNDSCLPB6I2C1_SCLSDAPB7I2C1_SDAADDRGNDI2C 地址为 0x48这里有一点值得说明ADS1015 的 SDA、SCL 引脚允许直接接 5V 电平因为它的 I2C 引脚是耐 5V 的。如果传感器板子用的是 5V 供电、主控是 3.3V这种电平兼容性会省掉一个电平转换芯片。不过如果主控这边上拉了 2.2k 电阻到 3.3V那就全部保持在 3.3V 电平也没有问题。I2C 初始化代码static void MX_I2C1_Init(void) { hi2c1.Instance I2C1; hi2c1.Init.ClockSpeed 400000; hi2c1.Init.DutyCycle I2C_DUTYCYCLE_2; hi2c1.Init.OwnAddress1 0; hi2c1.Init.AddressingMode I2C_ADDRESSINGMODE_7BIT; hi2c1.Init.DualAddressMode I2C_DUALADDRESS_DISABLE; hi2c1.Init.OwnAddress2 0; hi2c1.Init.GeneralCallMode I2C_GENERALCALL_DISABLE; hi2c1.Init.NoStretchMode I2C_NOSTRETCH_DISABLE; if (HAL_I2C_Init(hi2c1) ! HAL_OK) { Error_Handler(); } }3.2 驱动核心代码写入配置与读取转换值驱动层我拆成了三个函数写配置、读原始值、读电压值。每个函数做一件小事方便上层调用。#define ADS1015_I2C_ADDR (0x48 1) #define ADS1015_REG_CONV 0x00 #define ADS1015_REG_CFG 0x01 // 写入配置寄存器 uint8_t ADS1015_WriteConfig(uint16_t config) { uint8_t buf[3]; buf[0] ADS1015_REG_CFG; buf[1] (config 8) 0xFF; buf[2] config 0xFF; return HAL_I2C_Master_Transmit(hi2c1, ADS1015_I2C_ADDR, buf, 3, 100); } // 读取转换寄存器的原始值 uint16_t ADS1015_ReadRaw(void) { uint8_t reg ADS1015_REG_CONV; uint8_t buf[2]; HAL_I2C_Master_Transmit(hi2c1, ADS1015_I2C_ADDR, reg, 1, 100); HAL_I2C_Master_Receive(hi2c1, ADS1015_I2C_ADDR, buf, 2, 100); return ((uint16_t)buf[0] 8) | buf[1]; }读原始值这块有一个容易踩的坑HAL 库的HAL_I2C_Master_Receive在接收完之后会立刻发 Stop 信号而 ADS1015 的时序允许这样的操作。但如果你用的是某些国产 I2C 芯片它们对 Stop 信号的时机要求很苛刻可能需要在接收最后一个字节前用HAL_I2C_Master_Sequential_Receive配合 NO_STOP 标志才能正确读到数据。ADS1015 本身没问题但这段经验在兼容其他 I2C ADC 时用得上。3.3 配置参数的计算与选择配置寄存器不能直接写死一个常量因为不同的测量场景需要不同的 PGA 和通道。我封装了一个初始化函数参数直接暴露给上层用户uint16_t ADS1015_Config(uint8_t channel, uint8_t pga, uint8_t sps, uint8_t mode) { uint16_t config 0; config | (0x8000); // OS: 启动单次转换 config | ((channel 0x07) 12); // MUX 通道选择 config | ((pga 0x07) 9); // PGA 增益 config | ((mode 0x01) 8); // 0连续, 1单次 config | ((sps 0x07) 5); // 采样率 config | 0x0003; // 比较器禁用 return config; }PGA 的选择逻辑是这样的如果输入的信号最大可能是 3.3V那就选 ±4.096V 档PGA001此时 1 LSB 4.096V / 2048 ≈ 2mV。如果信号最大只有 0.5V那就选 ±0.512V 档此时 1 LSB 0.25mV小信号的细节能分辨得更清楚。这也是 ADS1015 比起内置 ADC 的一个显著优势PGA 的存在等于你外挂了一个可调增益的前置放大器而且不需要额外硬件。3.4 原始值到电压值的换算ADS1015 的 12 位数据是二进制补码形式正电压读出来的值范围是 0 到 2047负电压读出来是 2048 到 4095。换算公式需要区分正负float ADS1015_ReadVoltage(uint8_t channel, uint8_t pga, uint8_t sps) { float fsr 0.0f; switch (pga) { case 0: fsr 6.144f; break; case 1: fsr 4.096f; break; case 2: fsr 2.048f; break; case 3: fsr 1.024f; break; case 4: fsr 0.512f; break; case 5: fsr 0.256f; break; default: fsr 0.256f; break; } uint16_t raw ADS1015_ReadRaw() 4; float voltage; if (raw 0x0800) // 负数二进制补码 { voltage ((float)(raw - 4096) * fsr) / 2048.0f; } else { voltage ((float)raw * fsr) / 2048.0f; } return voltage; }这里raw右移 4 位之后是 12 位值0x0800是符号位。很多人会直接忽略负电压的判断导致差分输入或者测量负向电压时读出的数据完全不对。如果确认只接正电压那可以直接用raw * fsr / 2048.0f但有符号判断的版本更稳妥。4. 多通道采集的完整实现与性能实测4.1 循环轮询方式切换通道ADS1015 内部只有一个 ADC 核心多通道是靠模拟开关切换实现的。所以多通道采集其实就是循环改写 MUX 位的配置寄存器然后等转换完成、读数据、切下一个通道。实测切换通道加转换的总耗时在 1600 SPS 的采样率下单通道完整周期大概在 700 微秒左右四个通道轮流采一遍也就不到 3 毫秒绝大多数传感器轮询场景完全够用。来看一个四通道单端采集的示例#define CH_AIN0 0x04 // MUX100, AIN0 vs GND #define CH_AIN1 0x05 // MUX101, AIN1 vs GND #define CH_AIN2 0x06 // MUX110, AIN2 vs GND #define CH_AIN3 0x07 // MUX111, AIN3 vs GND void ADS1015_ReadAllChannels(float *voltages, uint8_t pga, uint8_t sps) { uint8_t channels[4] {CH_AIN0, CH_AIN1, CH_AIN2, CH_AIN3}; for (int i 0; i 4; i) { // 写配置触发单次转换 uint16_t cfg ADS1015_Config(channels[i], pga, sps, 1); ADS1015_WriteConfig(cfg); // 等待转换完成128 SPS 下最长约 8ms HAL_Delay(MS_PER_CONVERSION); voltages[i] ADS1015_ReadVoltage(channels[i], pga, sps); } }延时时间要根据采样率动态调整。比如 128 SPS 时单次转换时间约 7.8ms保守一点 delay 10ms如果 1600 SPS单次转换约 0.625msdelay 1ms 就够了。还有一个更精准的做法是读配置寄存器的 OS 位判断转换是否完成但这样要额外做一次 I2C 读操作反而拖慢整体速度。我实际测试下来固定 delay 的方式简单可靠只要给足裕量就不会出问题。4.2 连续模式与数据就绪判断连续模式下ADS1015 配置好之后就会自己不停转换不需要每次重新写配置。但这时候读数据要注意时序读到的可能是旧值也可能是刚更新的值。最简单的方法是不管新旧值直接用因为对于缓慢变化的模拟信号一个采样周期的滞后没有任何影响。如果对数据实时性要求高可以配置 ALERT/RDY 引脚让芯片在每次转换完成时拉低这个引脚STM32 用外部中断去捕获这样能做到零延迟读取。我试过用外部中断方式连续采集一个 1kHz 的正弦波信号。ALERT/RDY 引脚接 STM32 的 PA0把 PA0 配置为下降沿中断中断服务函数里直接读转换寄存器。实测下来在这种模式下采集到的波形非常干净1600 SPS 下还原 1kHz 信号虽然理论上有点勉强奈奎斯特频率是 800Hz但波形轮廓肉眼可见是完整的。如果换到 3300 SPS采样密度再翻一倍效果更好。4.3 实测数据与精度表现用一颗电压基准源输出 1.000V 的直流信号接到 AIN0配置 PGA±4.096V、1600 SPS连续采集 1000 个点做统计。实测结果平均值 1.0002V最大偏差 3mV标准差大约 1.2mV没有出现明显跳变。这个精度对于电池电压监测、温度采集、光照传感器这类应用已经非常理想了。把信号降到 0.120V 左右切换 PGA 到 ±0.256V 档再测标准差进一步降低到 0.3mV 左右。这就是 PGA 的威力小信号放大后再量化量化噪声被显著压低。所以我的建议是在选择 PGA 档位时永远让满量程尽量接近你信号的实际最大值而不是图省事永远用 ±6.144V 档。5. 常见问题速查与排错经验5.1 高频问题汇总表调这个驱动的过程中我遇到过不少问题也从社区里看到过别人踩过的坑整理成一张速查表现象可能原因解决办法I2C 通信超时HAL 返回 HAL_BUSY上拉电阻没接或阻值过大在 SCL/SDA 上接 4.7k 上拉到 3.3V读回来的值一直是 0x8000配置没触发转换或通道选择错误检查是否每次单次转换前都写了配置读回来的值一直是 0x7FF0AIN 引脚悬空或输入超出量程给输入加一个合适的分压电路电压换算结果偏小信号的参考地没和 ADC 共地确保信号源 GND 和 STM32 GND 连接数据跳变严重采样率过高或输入信号源阻抗太高降低 SPS或在输入端并联一个 0.1uF 电容I2C 地址不对ADDR 引脚浮空或接错电平ADDR 接 GND 时地址为 0x48接 VDD 为 0x495.2 噪声抑制的几条实战经验关于 ADS1015 的噪声我总结了几条对效果提升明显的经验。第一电源脚 VDD 旁边一定要放 0.1uF 陶瓷电容最好再并一个 10uF 钽电容或者电解电容距离芯片引脚越近越好。第二如果信号源内阻比较大比如光电二极管、高阻分压网络在 AIN 引脚到 GND 之间加一个 1uF 或 100nF 的电容做低通滤波能显著把高频噪声滤掉。第三也是很多人忽略的一点I2C 总线的上拉电阻阻值不要太小。我之前图快用了 1k 上拉结果总线波形振铃很严重导致通信偶尔出错。换成 4.7k 之后波形干净多了。400kHz 快速模式下4.7k 上拉到 3.3V 是经过验证的可靠组合。5.3 国产替代芯片兼容性提醒现在市面上有很多 ADS1015 的国产替代型号比如一些国产 12 位 I2C ADC 芯片引脚和寄存器定义几乎完全一致。我试过一两种基本能无缝替换但有一个细节需要注意部分国产芯片对 I2C 时序的采样点要求更严格尤其 NACK 信号的响应时间如果高速模式通信不稳定可以先把 I2C 时钟从 400kHz 降到 100kHz 试试。还有一个常见差异是内部基准精度国产芯片的初始精度有时候偏差能到 2% 左右如果做精密测量建议上电后用已知电压源做一次校准在软件里补偿增益误差。6. 驱动框架扩展建议驱动写到能读电压这一步只能算完成了 60%。真正好用的驱动应该是分层设计的最底层是 I2C 寄存器读写中间层是 ADS1015 的配置与单通道读取上层是面向应用的接口比如读取电池电压读取 NTC 温度读取光强度。这样应用层代码完全不用关心芯片寄存器细节将来换芯片比如换成 ADS1115时只需要改中间层实现上层调用接口保持兼容。我实际项目里就是这么组织的驱动源码文件拆成三个ads1015.h定义对外接口ads1015.c实现芯片级操作app_sensor.c里做具体的传感器逻辑。加上一个环形缓冲区缓存采样数据这样一个驱动模块化就完成了。后面有朋友问我能不能移植到别的平台我直接把ads1015.c里的 HAL 函数换成标准库寄存器操作或者换成其他平台的 I2C 接口改动量非常小。最后再分享一个小技巧调试这种 I2C 设备时强烈建议先用逻辑分析仪抓一次总线波形。不用多高端几十块钱的 8 通道逻辑分析仪就能把 Start、数据位、ACK、Stop 看得明明白白。很多时候代码看着对但波形就是不对这时候靠逻辑分析仪直接看到问题所在比一句句查代码快得多。我调这个 ADS1015 驱动时就有一次是地址方向位写反了看代码死活看不出来接上分析仪一眼就发现了问题。本文还有配套的精品资源点击获取