1. 为什么要抠 ADS1115 的底层细节:它不止是一颗“能用的 ADC”
先说个我自己的经历。有次给一个电池管理系统做采样板,用的就是 ADS1115。最开始照着网上的例程一抄,读数确实出来了,电压也对得上,我就很高兴地把它焊进板子。结果到了整机联调,问题来了:采样率稍微一提高,数据就开始跳;有时候连续读几十次,突然冒出来一个完全离谱的数值;更诡异的是,用示波器抓 SDA 波形,发现有几次 ACK 之后又额外多了一个时钟脉冲,然后整条总线就卡死了。那几天我查了驱动代码、查了上拉电阻、甚至怀疑是 PCB 布线问题,最后才发现,问题出在我对 ADS1115 工作模式和 I2C 通信时序的理解不到位——我以为自己在用单次转换模式,实际配置字写下去之后,器件一直在连续转换;我以为读 16 位数据只要发两个字节就行,实际上在 16 位 ADC 的 I2C 读流程里,字节边界和 ACK 的控制是有明确讲究的。
这颗芯片是 TI 的 16 位 ADC,内置 PGA、可编程数据速率、内部参考电压、比较器,还支持连续转换和单次转换两种工作模式。它特别适合用在那些对精度要求中等、对成本敏感、又不希望 MCU 占用太多资源来做信号调理的场景,比如电池电压监测、温度采集、称重传感器信号读取、环境监测传感器节点等。和 STM32 自带的 12 位 ADC 相比,ADS1115 的分辨率更高,而且通过 I2C 接口就能读,不需要额外的模拟引脚,这对引脚紧张的 MCU 来说太友好了。
但 ADS1115 有个特点:它把大量控制逻辑都浓缩在一颗 16 位的配置寄存器里。你只要写一次配置字,器件就按你的要求去执行转换、比较、输出。听起来简单,可一旦你对这个 16 位的每一位理解有偏差,整个工程就会表现得诡异莫测。这篇内容我准备把寄存器、工作模式、I2C 时序这三块彻底拆开,配合实际的波形和经验教训来讲,给正在调板子或者准备用 ADS1115 做项目的朋友一些参考。文章适合有一定单片机基础、正在用或者准备用 ADS1115 的工程师,也适合想深入理解 I2C 通信协议的嵌入式学习者。
2. 寄存器配置的完整地图:16 位配置字是怎么决定一切行为的
2.1 地址字节与 I2C 从机地址的对应关系
ADS1115 的 I2C 地址是 7 位地址,由 ADDR 引脚的电平状态决定。这个引脚可以接 GND、接 VDD、接 SDA、接 SCL 四种状态,对应四个不同的地址。具体关系如下:
| ADDR 引脚电平 | 7 位 I2C 地址 | 写地址字节(8 位) | 读地址字节(8 位) |
|---|---|---|---|
| GND | 0x48 | 0x90 | 0x91 |
| VDD | 0x49 | 0x92 | 0x93 |
| SDA | 0x4A | 0x94 | 0x95 |
| SCL | 0x4B | 0x96 | 0x97 |
这块有个高频坑:很多人的代码里写的是0x90或0x91,但查看数据手册时发现地址是0x48。其实两者是一回事,0x90是 8 位的写地址,0x91是 8 位的读地址,而数据手册里标注的是 7 位地址0x48。你在写驱动时,要么用(0x48 << 1)然后根据读写位加 1,要么直接用现成的0x90/0x91,千万别搞混。
还有一点值得注意:ADDR 接 SDA 和 SCL 的两种接法,在某些 MCU 的 I2C 外设初始化后、总线释放瞬间的电平影响下,可能出现地址识别不稳定的问题。我遇到过一次,PCB 上 ADDR 本来设计为悬空(相当于接 VDD 的默认状态),但焊接时旁边一个电容的漏电把 ADDR 电平拉低了一点点,导致器件时好时坏。后来我改成 ADDR 直接接 GND,问题就消失了。这类问题用万用表量电压是查不出来毛病的,必须用示波器看波形或者用 I2C 扫描程序逐地址探测。
2.2 配置寄存器(0x01)的 16 位位域拆解
这是整颗芯片最核心的寄存器。地址为 0x01,写入一个 16 位配置字后,器件立刻改变行为。位域分布如下:
| 位段 | 位号 | 名称 | 功能 |
|---|---|---|---|
| 15 | Bit 15 | OS | 单次转换启动位 / 转换状态位 |
| 14~12 | Bits 14:12 | MUX | 输入多路复用器选择 |
| 11~9 | Bits 11:9 | PGA | 可编程增益放大器配置 |
| 8 | Bit 8 | MODE | 连续/单次转换模式选择 |
| 7~5 | Bits 7:5 | DR | 数据速率配置 |
| 4 | Bit 4 | COMP_MODE | 比较器模式(迟滞/窗口) |
| 3 | Bit 3 | COMP_POL | 比较器输出极性 |
| 2 | Bit 2 | COMP_LAT | 比较器锁存 |
| 1~0 | Bits 1:0 | COMP_QUE | 比较器触发后禁用比较器次数 |
我逐个说说关键位的作用和选择思路。
OS 位(Bit 15):在单次转换模式下,向该位写 1 启动一次转换;在连续转换模式下,读取该位可以判断转换是否完成——当它从 1 变为 0 时,说明上一次转换结果已经写入转换寄存器。在读配置寄存器时,这个位的状态还可以用来判断器件是否忙碌。一个常见的问法是:启动单次转换后,需要多久才能读结果?这取决于你配置的数据速率 DR。比如 DR 设置为 128 SPS,则单次转换周期大约 8ms;设置为 860 SPS,则大约 1.16ms。你不能一发出启动命令就立刻去读,否则读到的还是上一次的旧数据,甚至可能是上电默认值 0x8000。
MUX 位(Bits 14:12):决定哪个输入通道被采样。有两种基本模式:差分输入和单端输入。差分模式是把两个引脚之间的电压差作为输入,适合桥式传感器这类需要抑制共模干扰的场景;单端模式是把引脚对 GND 的电压作为输入,适合采集电池电压、环境温度这类对地参考信号。配置 MUX 时要注意:如果你要轮流采集多路单端信号,每次切换通道后,最好丢弃第一次转换结果,因为通道切换后输入电容需要充放电,第一次转换结果可能不准。这个说法在很多老工程师那里已经成了标准做法,但具体实践中,它只在中等数据速率下才明显;如果你把速率压到 8 SPS,切换通道后第一笔数据误差也很小。
PGA 位(Bits 11:9):这是 ADS1115 非常好用的一个特性。PGA 可以放大输入信号,使得小信号也能利用满量程的精度。可选增益范围从 ±6.144V 到 ±0.256V。配置它时一定要算清楚:你要采集的电压最大值是多少?如果 PGA 配置过小,信号超出满量程,转换结果会直接饱和在 0x7FFF 或 0x8000,且没有溢出告警;如果 PGA 配置过大,小信号的分辨率就浪费了。做法是:先估算信号动态范围,留出 20% 的余量,再选一个最接近的 PGA 档位。对于最常见的 3.3V 单片机和 0~3.3V 的信号,选 ±4.096V 通常比较合适。
MODE 位(Bit 8):0 表示连续转换模式,1 表示单次转换模式。默认上电是连续模式。这个位太容易被人忽略了,我开头提到的那个“读数据偶尔跳变”的问题,根源之一就是这里。
DR 位(Bits 7:5):配置数据速率。从 8 SPS 到 860 SPS,一共 8 档。选 DR 要考虑的是:信号本身的带宽有多高?你希望多久拿到一个新数据?I2C 总线速度够不够?如果 I2C 时钟只有 100kHz,那么读一次转换结果(包含地址字节、寄存器地址字节、两个数据字节)大概需要 320 微秒以上。你把 DR 配到 860 SPS,每 1.16ms 就要读一次,I2C 总线基本被占满,MCU 可能就没时间做别的了。所以在项目里确定 DR,要同时考虑信号带宽和总线负荷。
COMP_QUE 位(Bits 1:0):控制比较器功能。如果不用比较器,要设为 0b11,表示禁用比较器并置 ALERT/RDY 引脚为高阻态。很多新手配置寄存器时忽略了这两位,结果 ALERT 引脚一直有电平输出,或者比较器在误触发,导致中断不断。
2.3 转换寄存器(0x00)与阈值寄存器的读法
转换寄存器地址是 0x00,16 位,存放最新一次转换结果。读取它不需要配置什么,直接发起 I2C 读请求即可。但要注意:ADS1115 在两种工作模式下读转换寄存器的行为略有不同。连续模式下,你读到的是“最近一次完成的转换结果”;单次模式下,如果转换还没完成,读到的可能是旧值。
阈值寄存器(0x02 和 0x03)是 16 位的比较器阈值。如果你只用最基本的 ADC 采集功能,这两个寄存器完全可以不碰。但如果你要做窗口比较、过压报警、欠压报警,就必须搞清楚它们的格式和比较器工作方式。比较器有两种模式:传统比较器(COMP_MODE=0)和窗口比较器(COMP_MODE=1)。传统模式下,当转换结果超过高阈值(0x02)或低于低阈值(0x03)时触发;窗口模式下,只要结果落在高、低阈值之间就触发。连续转换模式下比较器才工作,单次转换模式下比较器不工作——这个细节在数据手册里写得很清楚,但在实际项目中被忽略的概率极高,我见过有人拿着单次转换模式去测比较器输出,怎么测都不对。
3. 工作模式选择的深层逻辑:连续转换与单次转换到底该怎么选
3.1 连续转换模式的优势与“陷阱”
连续转换模式下,ADS1115 按照 DR 配置的速率不断采样、转换,把结果更新到转换寄存器。你随时发一个读命令,就能拿到最新数据。这种模式最适合需要周期性监控的场景,比如持续测量电池电压、持续监测传感器输出,MCU 只需要隔一段时间来读一次即可,不需要每次去启动转换,省掉了很多 I2C 开销。
但连续模式有个“陷阱”:转换数据更新的时刻和 MCU 发起读的时刻是异步的。假设 DR 配的是 860 SPS,转换结果每 1.16ms 更新一次。MCU 可能在数据正在更新时读了寄存器,此时读到的数据可能是旧的,也可能是新的,也可能是不完整的。这就解释了为什么有些人在连续模式下读到的数据偶尔会跳变。解决方法是:
- 在连续模式下,每次读数据前先读配置寄存器检查 OS 位是否为 0,如果是 0 说明转换已经完成,再去读转换寄存器;
- 或者干脆用单次转换模式,把时序完全掌握在自己手里。
我在实际项目中更倾向于后一种方式,因为它的时序确定性强,不容易出“灵异现象”。
3.2 单次转换模式的完整流程
单次转换模式的核心就是:你往配置寄存器写一个包含 OS=1 的配置字,器件启动一次转换,转换完成后把结果存入转换寄存器,然后器件自动进入低功耗状态,等待下一次命令。这个模式的优点是省电、时序可控、结果稳定。它的流程如下:
- 写配置寄存器:发送器件写地址,寄存器地址 0x01,16 位配置字(OS=1,且按需配置 MUX、PGA、DR、MODE=1)。
- 等待转换完成:等待时间取决于 DR 配置。例如 DR 设为 128 SPS,则等待
1/128秒,约 8ms。为了保险,一般等待1.2 / DR秒。 - 读转换寄存器:发送器件读地址,连续读两个字节,组成 16 位结果。
- 结果换算:根据 PGA 和满量程计算实际电压。
这种模式的缺点也明显:如果你需要高速连续采样,每次都要写配置字 + 发读命令,I2C 总线的开销比连续模式大,数据吞吐率受限。所以,如果采样率要求很高,连续模式更合适;如果对功耗和时序确定性更敏感,单次模式是更好的选择。
3.3 数据速率与分辨率的关系,以及一个常被忽略的取舍
ADS1115 的分辨率固定是 16 位,但这 16 位在不同 PGA 下的有效精度是不一样的。数据手册给出的噪声曲线和有效分辨率表,是很多人从来不看的部分。简单总结:数据速率越高,噪声越大,有效分辨率越低;PGA 增益越大,有效分辨率也越低。举个例子,在 PGA=±4.096V、DR=128 SPS 时,有效分辨率大约在 15 位到 16 位之间;如果 DR 拉到 860 SPS,有效分辨率可能降到 13 位左右。也就是说,你虽然拿到的是 16 位数据,但低几位其实已经是噪声了。
实际工程选速率的思路是:先确定信号最高频率成分,然后按奈奎斯特定理留出至少 5~10 倍余量,再选数据速率。比如采集温度信号,温度变化很慢,用 8 SPS 都够了,还能获得最好的噪声性能;采集振动信号,可能需要 860 SPS,但代价是有效分辨率下降。你要是既想要高采样率又想要高分辨率,要么用更高端的 ADC 芯片,要么进行多次采样取平均来换取等效分辨率。ADS1115 在这个定位下,能做到的就是让你根据需求在两者之间找一个平衡点。
4. I2C 通信时序的细节掌控:从 START 到 STOP 的每一个字节都在传递信息
4.1 I2C 总线的基础时序要素
ADS1115 是 I2C 从机,支持标准模式(100kHz)和快速模式(400kHz)。I2C 通信的基础时序要素包括:START 条件、STOP 条件、数据位、ACK/NACK。每个要素在物理层表现为 SDA 和 SCL 的电平配合。SCL 为高电平时,SDA 出现下降沿,表示 START;SCL 为高电平时,SDA 出现上升沿,表示 STOP。数据传输时,SDA 必须在 SCL 高电平期间保持稳定,在 SCL 低电平期间才能变化。
很多刚接触 I2C 的人,以为只要把代码写对就行了,时序是 MCU 硬件外设自动生成的。但一旦遇到软件模拟 I2C、或者 MCU 的硬件 I2C 不稳定需要换用 GPIO 模拟的情况,这些时序细节就变成了性命攸关的东西。ADS1115 对时序的要求并不苛刻,但它有个细微之处:它在某些条件下需要“总线空闲时间”,即 STOP 和下一次 START 之间要留至少 4.7 微秒(快速模式下是 1.3 微秒)。如果你用 GPIO 模拟 I2C,两次传输之间不加延时,器件容易丢状态。
4.2 写配置寄存器的时序全过程
向 ADS1115 写配置寄存器,发起的完整字节序列是:
START -> 器件写地址(0x90) -> ACK -> 寄存器地址(0x01) -> ACK -> 配置字高字节 -> ACK -> 配置字低字节 -> ACK -> STOP这里最容易出错的是:16 位配置字在 I2C 总线上拆成两个 8 位字节发送,高字节在前。如果你以前接触过一些先发低字节的外设,到这里就要特别小心。比如你想配置单次转换、输入 AIN0 对 GND、PGA=±4.096V、DR=128SPS、禁止比较器,那么 16 位配置字是0b10000011 10000011,也就是0x8383。发送时,先发 0x83,再发 0x83。很多人犯的错是:计算出 16 位值之后,直接按小端习惯先发低字节,结果配置就全乱了,器件表现出的行为完全不是你想要的样子。
还有一个细节:写配置寄存器时,如果 MCU 的 I2C 控制器支持“多字节写”,你可以一次性发完这 5 个字节(地址 + 寄存器地址 + 两个数据字节),再发 STOP。但如果你用的是自己写的软件模拟 I2C,就要注意每一字节后都要等待从机 ACK。ADS1115 在接收到每个字节后,都会在第 9 个时钟脉冲拉低 SDA 表示 ACK。如果你在某个字节后的 ACK 检测上出错,提前发下一个字节,时序就乱了。
4.3 读转换寄存器的两种时序方案
读取转换寄存器(地址 0x00)有两种常见方案。第一种是“先写寄存器地址,再发起读”:先发送 START,然后发送器件写地址 0x90、寄存器地址 0x00、STOP,然后重新发送 START、器件读地址 0x91,然后连读两个字节,最后发 NACK + STOP。第二种是用了 I2C 的“重复起始”特性,在第一个事务结束时不是发 STOP,而是直接再发一个 START,也就是:
START -> 器件写地址(0x90) -> ACK -> 寄存器地址(0x00) -> ACK -> 重复START -> 器件读地址(0x91) -> ACK -> 读高字节 -> ACK(主机回) -> 读低字节 -> NACK(主机回) -> STOP这两种方案在功能上等价,但第二种少了一个 STOP 再 START 的完整总线释放过程,效率更高,也能避免总线被其他器件抢占的问题。不过在实现上,重复起始需要 MCU 的硬件 I2C 外设支持,或者软件模拟时正确地控制 SDA/SCL。用 STM32 的 HAL 库时,HAL_I2C_Master_Transmit和HAL_I2C_Master_Receive这两个函数分开调用,实际上是走了“先 STOP 再 START”的路线;而HAL_I2C_Mem_Read这类函数内部会使用重复起始。两者实测都能稳定工作,但要注意:在 STM32F4 系列上,硬件 I2C 偶发总线锁死的问题,有时通过改用“两次独立调用+足够延时”反而能改善。
读数据时的 ACK 控制是另一个关键点:主机在读最后一个字节前,要回 NACK 而不是 ACK,表示“我只要这两个字节,不要再发下一个了”。如果你在高字节读完后回的是 ACK,器件就会继续把低字节发出来,然后因为你还想读,它还会再发下一字节——但转换寄存器只有 16 位,你再读出来的数据就不确定了。这个问题在软件模拟 I2C 时尤其明显,因为很多人写完读字节函数后,总是默认每次读完都回 ACK,导致最后一个字节读完后,从机的时钟还在继续,总线就卡在那里。
4.4 用逻辑分析仪抓时序:验证你的代码到底发了些什么
我强烈建议每个做 I2C 设备驱动的人,手边备一个逻辑分析仪,哪怕是十几块钱的 8 通道 USB 逻辑分析仪都行。不要光靠眼睛看代码、靠万用表量电平来猜测时序。接入逻辑分析仪后,设置好采样率(建议 4MHz 以上,400kHz 的 I2C 需要至少 1MHz 采样率),接 SDA 和 SCL 两个通道,然后运行代码。你会非常直观地看到:
- START 条件是否正确发出;
- 地址字节后面,从机有没有回 ACK;
- 数据字节的顺序是不是你先发的高字节;
- 读操作最后一字节后,主机有没有回 NACK;
- 是否有多余的时钟脉冲出现在 STOP 之后。
我靠着逻辑分析仪,不止一次找到了“代码看起来没问题但数据就是不对”的病因。有一次问题出在软件模拟 I2C 的 SDA 方向切换上:读数据前需要把 SDA 引脚从输出模式切换为输入模式,但我在切换后没有等一个短延时,结果 SCL 拉高时 SDA 还没稳定,第一个数据位就读错了。这种问题,光靠看代码非常难发现,但逻辑分析仪一抓就原形毕露。
5. 实战代码的完整路线:从硬件连接到 STM32 HAL 库实现
5.1 硬件连接与上拉电阻的选择
ADS1115 的硬件连接非常简单:VDD 接电源、GND 接地、SDA 和 SCL 分别接 MCU 的对应引脚,同时这两根线都要接上拉电阻。I2C 是开漏加外部上拉的架构,上拉电阻的取值很讲究。常见取值在 2.2kΩ 到 10kΩ 之间。取值越小,上升沿越陡峭,但功耗越大,而且可能超出器件允许的灌电流能力;取值越大,功耗越小,但上升沿变缓,总线频率高时可能无法在规定时间达到高电平。对于 3.3V 供电、400kHz 快速模式,4.7kΩ 是个稳妥的选择;如果总线电容较大(比如走线很长、器件很多),可以换成 2.2kΩ。
有个实验数据可以提一下:在 40cm 长的杜邦线连接下,10kΩ 上拉配合 400kHz 通信时,SCL 高电平时间会肉眼可见地变短,甚至出现通信偶发失败。换 4.7kΩ 后就稳定了。所以如果你是面包板实验,线长,建议直接用 4.7kΩ。如果你用 PCB 短走线,10kΩ 也能工作,但没必要冒险。
5.2 STM32 硬件 I2C 的初始化与数据读写
以 STM32F407 和 HAL 库为例。先初始化 I2C1:
I2C_HandleTypeDef hi2c1; 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(); } }写配置寄存器的函数可以这样写:
uint8_t ads1115_write_config(uint16_t config) { uint8_t buf[2]; buf[0] = (config >> 8) & 0xFF; // 高字节在前 buf[1] = config & 0xFF; return HAL_I2C_Mem_Write(&hi2c1, 0x90, 0x01, I2C_MEMADD_SIZE_8BIT, buf, 2, 100); }这里HAL_I2C_Mem_Write的第一个地址参数是 8 位写地址 0x90。如果你更习惯写(0x48 << 1),效果一样,但要注意HAL_I2C_Mem_Write内部会自动把最低位作为读写位,所以这里的地址参数不能是 7 位地址 0x48,而必须是0x48 << 1,也就是 0x90。
读转换寄存器:
uint16_t ads1115_read_raw(void) { uint8_t buf[2]; HAL_I2C_Mem_Read(&hi2c1, 0x90, 0x00, I2C_MEMADD_SIZE_8BIT, buf, 2, 100); return (buf[0] << 8) | buf[1]; }这段代码看起来简单,但有两点要注意:第一,HAL_I2C_Mem_Read会先发送器件写地址 + 寄存器地址,再用重复起始的方式发起读,因此它要求 MCU 的 I2C 外设支持重复起始,且 HAL 库的实现自动处理了这些细节。第二,如果你在读数据后发现总是读到同一个值不更新,先去查一下 MODE 位——器件可能停在连续转换模式但代码的逻辑是单次模式,或者反过来。
5.3 软件模拟 I2C 的实现要点:从 STM32F407 的角度出发
硬件 I2C 偶尔会出现总线锁死的现象,尤其在某些 STM32 系列上。很多人因此转向软件模拟 I2C——也就是用两个 GPIO 手动翻转 SCL、手动控制 SDA 的输入输出方向。这不丢人,反而是一种稳定可控的做法。只是模拟 I2C 对时序的把握要求更高,代码里需要加入恰当的延时。下面给一个最精简的框架:
#define I2C_SCL_PIN GPIO_PIN_6 #define I2C_SDA_PIN GPIO_PIN_7 #define I2C_GPIO_PORT GPIOB #define SCL_HIGH() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_SET) #define SCL_LOW() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SCL_PIN, GPIO_PIN_RESET) #define SDA_HIGH() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_SET) #define SDA_LOW() HAL_GPIO_WritePin(I2C_GPIO_PORT, I2C_SDA_PIN, GPIO_PIN_RESET) #define SDA_READ() HAL_GPIO_ReadPin(I2C_GPIO_PORT, I2C_SDA_PIN) static void i2c_delay(void) { for (volatile int i = 0; i < 50; i++); } static void i2c_start(void) { SDA_HIGH(); SCL_HIGH(); i2c_delay(); SDA_LOW(); i2c_delay(); SCL_LOW(); } static void i2c_stop(void) { SDA_LOW(); SCL_HIGH(); i2c_delay(); SDA_HIGH(); i2c_delay(); } static void i2c_ack(void) { SDA_LOW(); SCL_HIGH(); i2c_delay(); SCL_LOW(); SDA_HIGH(); } static void i2c_nack(void) { SDA_HIGH(); SCL_HIGH(); i2c_delay(); SCL_LOW(); SDA_LOW(); } static int i2c_wait_ack(void) { SDA_HIGH(); // 释放SDA,让从机可以拉低 SCL_HIGH(); i2c_delay(); int ack = SDA_READ(); // 0表示ACK,1表示NACK SCL_LOW(); return ack; } static void i2c_write_byte(uint8_t data) { for (int i = 7; i >= 0; i--) { if (data & (1 << i)) SDA_HIGH(); else SDA_LOW(); SCL_HIGH(); i2c_delay(); SCL_LOW(); i2c_delay(); } } static uint8_t i2c_read_byte(int last) { uint8_t data = 0; SDA_HIGH(); // 释放SDA,由从机驱动 for (int i = 7; i >= 0; i--) { SCL_HIGH(); i2c_delay(); data |= (SDA_READ() << i); SCL_LOW(); i2c_delay(); } if (last) i2c_nack(); else i2c_ack(); return data; }这段代码我在多个项目里用过,稳定性很好。核心经验是:SDA_HIGH()在释放 SDA 之前,必须先把 SDA 引脚配置成输入模式(或开漏输出模式)。如果用推挽输出模式,直接调用SDA_HIGH()确实能把 SDA 拉高,但当从机也试图拉低 SDA 时,它根本拉不动,ACK 就永远读不到,总线直接“焊死”。在 STM32 上正确的做法是:把 SDA 配置为开漏输出,这样输出 1 的时候相当于释放总线,输出 0 的时候拉低总线;读数据时直接把 PIN 配置为输入模式,或者在开漏模式下直接读 IDR 寄存器也行。
5.4 单次模式读电压的实际例程
把以上模块组合起来,一个完整的单次模式读 AIN0 对 GND 电压的例程如下:
#define ADS1115_ADDR_W 0x90 #define ADS1115_ADDR_R 0x91 #define ADS1115_REG_CONV 0x00 #define ADS1115_REG_CFG 0x01 // 配置字:OS=1,MUX=AIN0-GND(000),PGA=4096mV(001),MODE=1(单次), // DR=128SPS(100),COMP_QUE=11(禁用比较器) #define ADS1115_CFG_SINGLE_0_4096_128SPS 0x8383 uint16_t ads1115_single_read_raw(void) { // 写配置字,启动转换 i2c_start(); i2c_write_byte(ADS1115_ADDR_W); i2c_wait_ack(); i2c_write_byte(ADS1115_REG_CFG); i2c_wait_ack(); i2c_write_byte((ADS1115_CFG_SINGLE_0_4096_128SPS >> 8) & 0xFF); i2c_wait_ack(); i2c_write_byte(ADS1115_CFG_SINGLE_0_4096_128SPS & 0xFF); i2c_wait_ack(); i2c_stop(); // 等待转换完成,128SPS 对应约 8ms,这里等 10ms HAL_Delay(10); // 读转换寄存器 i2c_start(); i2c_write_byte(ADS1115_ADDR_W); i2c_wait_ack(); i2c_write_byte(ADS1115_REG_CONV); i2c_wait_ack(); // 重复起始,切换为读模式 i2c_start(); i2c_write_byte(ADS1115_ADDR_R); i2c_wait_ack(); uint8_t hi = i2c_read_byte(0); // 读取高字节,非最后一字节,回ACK uint8_t lo = i2c_read_byte(1); // 读取低字节,最后一字节,回NACK i2c_stop(); return (hi << 8) | lo; } float ads1115_single_read_voltage(void) { uint16_t raw = ads1115_single_read_raw(); // PGA=4096mV 时,满量程为 -4.096V 到 +4.096V // 16位有符号数:0x8000 对应 -4.096V,0x7FFF 对应 +4.096V float voltage = (int16_t)raw * 4.096f / 32768.0f; return voltage; }这个例程几乎是直接可以用的。有一点要说清楚:在ads1115_single_read_voltage里,我把原始值强转成int16_t,是为了让负电压也能正确换算。如果你确认输入永远为正,也可以直接把raw当作无符号数除以 32767,但这样负电压场景下会得到奇怪的结果。养成强转的好习惯,后面项目变成双极性采集时不会踩坑。
6. 常见疑难问题与排查思路:总线锁死、数据跳变、比较器不工作
6.1 I2C 总线锁死与 SCL 被拉低的问题
这是 I2C 通信中出现频率最高的问题之一:总线锁死,SCL 或 SDA 被从机拉住,一直处于低电平,导致后面的通信全部失败。排查步骤我建议按这个顺序来:
- 用示波器抓 SCL 和 SDA 的波形。如果发现 SDA 一直为低且 SCL 也有异常,先判断是不是从机处于异常状态。掉电重启从机,看总线是否恢复。如果恢复,说明从机进入了某种未定义状态。
- 检查 I2C 总线复位机制。很多从机在被中断的传输中(比如主机在传输中途复位了),会一直等待剩余的时钟脉冲。解决办法是:主机侧在每次通信失败后,对 SCL 连续发送 9 个时钟脉冲,让从机完成当前字节的剩余部分,再发送 STOP,释放总线。
- 检查上拉电阻。SDA 或 SCL 若缺少上拉电阻,总线空闲时电平不确定,可能会导致状态机混乱。
- 检查代码里是否有“主机在 STOP 之前就释放了总线”的情况。比如软件模拟 I2C 时,发送 STOP 之前 SDA 的电平状态不对,导致从机没有识别到 STOP,卡在等待状态。
ADS1115 本身对总线锁死的容忍度一般。我实测过:如果在读转换寄存器时,主机在读第二个数据字节后没有回 NACK 而回了 ACK,然后直接发 STOP,ADS1115 偶尔会多产生一个时钟脉冲,之后总线恢复,但下一个读命令的第一次 ACK 会异常。用逻辑分析仪能抓到这个问题。
6.2 数据跳变、读数不稳定的排查思路
数据跳变的原因可以分为三大类:电源噪声、参考电压噪声、时序问题。
- 电源噪声:ADS1115 的内部参考电压由 VDD 供电。如果 VDD 纹波很大,转换结果就会有周期性波动。排查方法:用示波器 AC 耦合看 VDD 波形,如果纹波超过 10mV,就需要加 π 型滤波或者加大去耦电容。ADS1115 的 VDD 引脚旁边至少要放一个 0.1μF 陶瓷电容,并且尽量靠近引脚放置。
- 输入信号源阻抗:ADS1115 的输入端在内部采样时要吸取少量电荷,如果信号源阻抗太高,电荷来不及建立,转换结果就会偏小且不稳定。解决方法是加一个运放跟随器,或者降低信号源阻抗。这是个非常容易被忽略的坑:传感器直接经长线连到 AIN 引脚,阻抗可能达到几十千欧,读数就会飘。
- 时序问题:在连续模式下,主机读取数据和器件更新数据的竞争,会造成尾数跳变。这种跳变通常只影响最低几位,不会出现“满幅跳变”。解决办法是改用单次模式,或者在读数据前先查 OS 位。
- PGA 配置过小:信号接近满量程时,会进入非线性区,转换结果的低几位开始抖动。这时要重新评估输入信号范围,选择更合适的 PGA 档位。
6.3 比较器相关的典型问题与“为什么 ODR 数据看起来不对”
很多人用 ADS1115 的比较器功能来控制欠压报警,但经常会发现:报警电平触发后,信号已经恢复正常了,报警却没有复位;或者反过来,报警一直不复位,直到断电重启。这两个现象背后的原因是配置寄存器的 COMP_LAT 位和 COMP_QUE 位没配对。
COMP_LAT 置 1 时,比较器输出锁存,即使输入信号恢复,ALERT/RDY 引脚也保持触发状态,直到主机读取转换寄存器才解除——这是数据手册里明确写的。如果你想实现“信号越限时报警,恢复后自动解除”,就要把 COMP_LAT 设为 0。COMP_QUE 位设定比较器连续触发多少次后 ALERT 才有效,取值 00 表示触发 1 次就输出,01 表示 2 次,10 表示 4 次,11 表示禁用比较器。如果你把 COMP_QUE 配成 10,而信号恰好是偶发毛刺,报警可能就会延迟多个转换周期才触发,看起来就像“响应慢”甚至“不工作”。
还有一个细节很多人不知道:比较器只在连续转换模式下才工作。你在单次转换模式下配置了比较器阈值,读 ALERT 引脚,发现电平永远不对,就是这个原因。这在数据手册的功能框图里其实画得很清楚,但确实只有踩过坑的人才会去仔细看那页。
6.4 软件模拟 I2C 在 STM32F407 上的特殊注意点
STM32F407 的 GPIO 速度等级会影响模拟 I2C 的边沿。GPIO 速度设置为 LOW 时,输出上升沿和下降沿都比较慢;设置为 VERY_HIGH 时,边沿很陡,但可能引入振铃。模拟 I2C 建议把 SCL 和 SDA 引脚配置为开漏输出(GPIO_MODE_OUTPUT_OD),速度设为 HIGH,同时外部配上拉电阻。原因有两个:一是开漏输出天然支持线与逻辑,所有设备都能安全驱动总线;二是避免某个设备把总线拉低时,另一个设备推挽输出强行拉高,造成短路电流。
另外,STM32F407 的 GPIO 唤醒源等外设配置也可能影响模拟 I2C 的稳定性。我建议在初始化 GPIO 时,把 SCL 和 SDA 的 GPIO 都设为GPIO_NOPULL,不要把内部上拉打开。因为内部上拉电阻阻值约为 40kΩ,和外部上拉并联后等效阻值偏低,对功耗不友好,而且不同芯片的内部上拉一致性一般,可能导致时序边沿不一致。
7. 我的一点实际体会:项目调试中的三个“笨”办法
做了这么多个带 ADC 的项目后,我越来越倾向于在调试阶段采取一些“笨”办法,它们帮我省下的时间远超预期。
第一个办法是写一个 I2C 地址扫描程序,把总线上所有能响应的设备地址都打印出来。在把 ADS1115 接入系统之前,先单独跑一遍扫描程序,确定器件响应地址和你预期的一致。尤其是当 ADDR 引脚不小心悬空或接错时,扫描程序能立刻暴露问题,省去后面所有莫名其妙的故障定位时间。
第二个办法是每隔一段时间就用逻辑分析仪抓一次波形存档。不要等出问题才抓。在代码刚写完、功能正常时抓一次,记录下来。后面如果出现异常,再抓一次对比,往往几秒钟就能定位到问题出现在哪个阶段:是配置字写错了,还是读地址发错了,还是 ACK 阶段出了问题。
第三个办法是在配置寄存器里预留一个“版本号”段位。虽然 ADS1115 的配置寄存器每一位都有特定用途,没有空闲位,但在自己的驱动代码里,我会维护一个宏定义集合,把不同场景的完整配置字命名清楚,比如CFG_SINGLE_AIN0_4096_128SPS,这样每次调用时一看名字就知道当前配置是什么。看似简单,但在多人协作的项目里,这能避免很多因为“改了某个位忘了改另一个位”而产生的低级错误。
ADS1115 是一颗看起来简单、用起来也不复杂、但深挖起来充满细节的芯片。把寄存器配置、工作模式、I2C 时序这三件事吃透之后,它在你的项目中就不会再是一个“黑盒”,而是变成一块你可以完全掌控的积木。希望这篇文章能帮你在调板子的路上少走几步弯路。