简介面向STM32开发者的AD7124多通道读取改进版工程基于STM32F103C8T6硬件SPI2接口实现解决原版采样率不足、PGA1时大于2V或低于-2V信号采集失真等问题。通过优化寄存器设置提升每秒采样次数并将AD7124读取速率最高提升至1.125MHz同时精简main.c架构便于二次开发与维护。压缩包共145个文件包含35个.h头文件、34个.c源文件以及Keil工程配置、编译输出等辅助文件整体约3MB可直接用Keil MDK5打开。目前已有1218人下载学习适合需要高精度多通道模拟前端采集、熟悉硬件SPI通信的嵌入式开发者参考。工程未使用AD7124同步模式并对PGA1下的信号调理做了细致调试可作为传感器信号采集与工业控制场景的实用范例。1. 硬件SPI比软件模拟更值得先想清楚AD7124多通道读取到底难在哪把 AD7124 这种 24 位 Σ-Δ ADC 接到 MCU 上用硬件 SPI 控制、一次读回多个通道表面看是个很普通的传感器驱动实际写起来却比预想中更容易翻车。最典型的场景是某开发者做了一个 4 路电压采集板芯片选 AD7124-4代码里用软件模拟 SPI 先把单通道读通了结果扩展成多通道后发现通道之间的数据互相串、切换后第一次读数总是跳甚至 DOUT/RDY 脚偶尔卡死。这些问题的根源大多不在芯片本身而在硬件 SPI 外设的时序理解、寄存器配置顺序和通道切换时对 Σ-Δ 滤波器建立时间的处理。这篇笔记就把「硬件 SPI 控制 AD7124 多通道读取」这套驱动拆开讲清楚时序怎么配、通道怎么切、数据怎么转以及哪些坑值得你提前躲开。适合正在做多路高精度电压、称重、RTD 采集的嵌入式开发者。2. AD7124 的通道本质寄存器结构、硬件SPI选型与接线注意点2.1 为什么不用软件模拟SPI三线操作与连续读数的差别软件模拟 SPI 看起来简单把 CLK、MOSI、MISO 三个引脚用 GPIO 拉来拉去就行单通道、低速、每次只读一个数据时确实够用。但一旦进入多通道连续读取就很吃力了。AD7124 的每次寄存器读写都要先发一个通信字节指定后续操作的目标寄存器读 24 位数据还要同时发出 24 个时钟脉冲。软件模拟下每个 bit 都要翻转 GPIO一个字节过去通常要几十微秒四个通道扫描一圈延迟明显。更大的风险在于时序抖动GPIO 翻转受中断和任务调度影响CLK 高电平宽窄不一致片选和时钟沿之间的建立保持时间不好保证偶发出现 bit 错位很难排查。硬件 SPI 外设则是另一套逻辑你把要发的数据往发送寄存器一写时钟、移位、接收都由外设完成速率可到几 MHz还能配合 DMA 自动收发。AD7124 这类 ADC 在转换完成后数据寄存器的读取是单次性的DOUT/RDY 信号边沿会告诉你“数据准备好了”硬件 SPI 下你可以在中断里启动一次接收或者用 DMA 连续读回多通道结果。对驱动稳定性来说硬件 SPI 不只是快更重要的是时序是硬件生成的不会因为系统负载变化而抖动。我一般会在项目一开始就把硬件 SPI 作为默认方案软件模拟 SPI 只作为调试期的临时手段。2.2 AD7124的寄存器骨架通道选择、数据寄存器与ID校验的关系AD7124 的控制模型比普通 ADC 复杂一些但抓住几个关键寄存器就能理清。通信寄存器是每笔 SPI 事务的起点它决定了本次是读还是写、目标寄存器是哪一个数据寄存器存放转换结果通道寄存器负责把模拟输入端映射到某个通道同时使能该通道滤波寄存器决定输出数据速率和滤波器模式ADC 控制寄存器选择基准源、PGA 增益和操作模式。多通道读取的本质就是不断改通道寄存器、等转换完成、从数据寄存器把结果读走。这里面最容易忽略的是 ID 校验。AD7124-4 的 ID 寄存器固定在 0x56 地址上电复位后读到的值可以确认芯片型号和通信链路是否正常。写完整的驱动前建议先只做两件事软件复位、读 ID。如果读到的 ID 不对后面配置什么寄存器都没有意义。很多“芯片不工作”的问题最后都发现是 SPI 模式和时序根本没对上。所以要写一个可靠的多通道驱动第一步不是急着配置通道而是把通信层的读写函数做对然后用 ID 校验证明整个链路是通的。2.3 硬件SPI参数设置与引脚/电源注意点硬件 SPI 的参数首先要对着 AD7124 数据手册的时序图确认。实际项目中最常用的是 CPOL0、CPHA1Mode 1或 CPOL1、CPHA1Mode 3这两个模式都能把数据收发放在合适的边沿。我的做法是先挑一个 Mode 试上电读 IDID 正确就说明时钟极性相位对了。时钟频率方面我的起点是 1~2 MHz跑通之后再逐步往上升直到数据错位为止再回退一档。AD7124 的 SCLK 上限很高但那是在理想 PCB 布局下的数字真实产线上走线长一点、地不干净4 MHz 以上就可能偶发错位所以别一上来就拉满。接线和电源上最容易踩的是模拟地和数字地的处理。AD7124 的 AVDD、DVDD 如果来自不同电源中间的滤波电容要靠近引脚放基准电压输入引脚如果走线过长会直接把工频干扰耦合进去。多通道采集时通道之间的串扰往往不是芯片自身问题而是输入信号线在板子上并行走线太长。此外AD7124 的 SPI 从机引脚属于数字域和模拟输入之间要保持距离。之前做过一块 8 通道小板通道 1 和通道 2 的 PCB 走线平行了 3 厘米通道 2 读数就跟着通道 1 的信号轻微波动割线飞线后才解决。接线这种事板子回来再改成本高布局阶段就要想着。3. 驱动初始化代码从复位和ID校验开始配置通道与滤波器3.1 SPI底层初始化与寄存器读写封装不管用哪家 MCU 的 HAL 库硬件 SPI 初始化后先别急着写业务逻辑先把 AD7124 的寄存器读写封装好。下面是一段常见做法用两段缓存实现全双工收发// ad7124_drv.c #include ad7124_drv.h #include main.h extern SPI_HandleTypeDef hspi1; #define AD7124_CS_LOW() HAL_GPIO_WritePin(ADC_CS_GPIO_Port, ADC_CS_Pin, GPIO_PIN_RESET) #define AD7124_CS_HIGH() HAL_GPIO_WritePin(ADC_CS_GPIO_Port, ADC_CS_Pin, GPIO_PIN_SET) static void ad7124_write_reg8(uint8_t reg, uint8_t val) { uint8_t tx[2]; tx[0] reg; // bit70 表示写操作 tx[1] val; AD7124_CS_LOW(); HAL_SPI_Transmit(hspi1, tx, 2, 10); AD7124_CS_HIGH(); } static void ad7124_write_reg16(uint8_t reg, uint16_t val) { uint8_t tx[3]; tx[0] reg; // 通信寄存器写命令 tx[1] (val 8) 0xFF; tx[2] val 0xFF; AD7124_CS_LOW(); HAL_SPI_Transmit(hspi1, tx, 3, 10); AD7124_CS_HIGH(); } static uint8_t ad7124_read_reg8(uint8_t reg) { uint8_t tx[2] { (uint8_t)(0x80 | reg), 0x00 }; uint8_t rx[2] {0, 0}; AD7124_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, tx, rx, 2, 10); AD7124_CS_HIGH(); return rx[1]; }这段代码的逻辑核心是把通信字节放在每笔事务的第一帧。写寄存器时tx[0] 直接放寄存器地址bit7 为 0读寄存器时tx[0] 置 bit7 为 1后续空闲字节 0x00 只是用来产生时钟让 ADC 把数据移到 MISO 上。参数上HAL_SPI_Transmit 的超时设 10ms对 SPI 这种短事务绰绰有余。CS 的拉低和拉高围绕整笔事务保证传输期间不被其他 SPI 设备干扰。注意这段代码默认选用了 8 位帧传输发送 2 字节是通信字节加一个数据字节发送 3 字节则是通信字节加一个 16 位数据的高低位。实践上这些细节都能正常工作但我还是建议移植时对着你自己的数据手册确认一遍通信字节位定义。3.2 复位与ID校验先证明SPI时序是对的寄存器读写函数封装好后第一步要做的是让 AD7124 进入一个已知状态。AD7124 的软件复位是把 DIN 持续拉高至少 64 个 SCLK 周期常见做法是连续发送多字节 0xFF。复位之后要等待一段时间再读 ID因为芯片内部上电和复位需要稳定时间。#define AD7124_ID_REG 0x56 #define AD7124_ID_AD7124_4 0xE1 #define AD7124_ID_AD7124_8 0xE2 uint8_t ad7124_reset_and_check_id(void) { uint8_t tx_buf[8]; memset(tx_buf, 0xFF, sizeof(tx_buf)); AD7124_CS_LOW(); HAL_SPI_Transmit(hspi1, tx_buf, sizeof(tx_buf), 10); AD7124_CS_HIGH(); HAL_Delay(10); // 等待复位完成 uint8_t id ad7124_read_reg8(AD7124_ID_REG); if ((id AD7124_ID_AD7124_4) || (id AD7124_ID_AD7124_8)) { return 1; // ID 匹配SPI 链路正常 } return 0; }这里连发 8 个 0xFF也就是 64 个时钟周期的持续高电平正好满足复位要求。复位后延时 10ms 是经验值给内部 LDO 和基准电路一个稳定窗口。ID 校验如果失败不要急着查 ADC 配置先回头检查 SPI 的模式、时钟频率和接线顺序。我调试时遇到过好几次读回 0xFF 的情况最后都是 SPI 模式配错或者 MISO 虚焊导致的。ID 校验通过之后后续配置才有讨论的基础。3.3 通道与滤波器配置建立多通道扫描表多通道读取前要把通道映射表建立好。AD7124 的通道寄存器负责把某个模拟输入对映射到配置通道同时设置该通道是否启用。下面的代码以 4 个差分通道为例把通道寄存器 0~3 分别配置为 AIN0/AIN1、AIN2/AIN3、AIN4/AIN5、AIN6/AIN7并启用通道 0 到 3。#define AD7124_REG_CH0 0x09 #define AD7124_CH_EN 0x8000 // 通道使能位 #define AD7124_CH_SETUP0 0x0010 // 使用 Setup 0 #define AD7124_CH_AINP0 0x0000 // AIN0 作为正输入 #define AD7124_CH_AINM1 0x0002 // AIN1 作为负输入 static void ad7124_setup_channels(void) { // 通道0: AIN0() / AIN1(-) ad7124_write_reg16(AD7124_REG_CH0, AD7124_CH_EN | AD7124_CH_SETUP0 | (AD7124_CH_AINP0 0) | AD7124_CH_AINM1); // 逐个配置通道1~3正负输入分别对应后续引脚 ad7124_write_reg16(AD7124_REG_CH0 1, AD7124_CH_EN | AD7124_CH_SETUP0 | (0x02 0) | (0x03 1)); // AIN2 / AIN3 ad7124_write_reg16(AD7124_REG_CH0 2, AD7124_CH_EN | AD7124_CH_SETUP0 | (0x04 0) | (0x05 1)); // AIN4 / AIN5 ad7124_write_reg16(AD7124_REG_CH0 3, AD7124_CH_EN | AD7124_CH_SETUP0 | (0x06 0) | (0x07 1)); // AIN6 / AIN7 }这段配置比较直观每个通道寄存器的高位 BIT15 是使能位setup 字段指向后面要讲的一组滤波和增益配置。AINP 和 AINM 的编码对应模拟输入引脚序号。注意我在第二个通道开始直接写了位的组合值实际工程里建议用宏定义把每个输入引脚的编码值列清楚否则半年后回来看代码很难读懂。每次写 16 位寄存器都是三字节事务由前面封装的 write_reg16 完成。通道配置完成后接着配置滤波器的输出速率和增益这些参数要根据实际传感器需求来定比如 RTD 测温常用 50SPS 配合 50Hz 工频抑制。4. 多通道读取实现通道切换策略与数据转换4.1 单次转换与连续转换轮询场景选哪种AD7124 支持连续转换模式和单次转换模式。连续转换模式下芯片一直在转换当前选中的通道DOUT/RDY 引脚会在每个转换周期结束变低表示新数据已经准备好。单次转换模式下配置后只转一次读出结果后停止适合功耗敏感的场景。多通道轮询读取大多数驱动选择连续转换模式因为每次切换通道后只要等待 RDY 变低就能读出数据不需要反复启动。我自己的选择是如果系统里只跑这一个 ADC且对时序要求不苛刻就用连续转换如果多个 SPI 设备共享总线或者对功耗要求高就切到单次转换模式。无论哪种模式通道切换的核心步骤都是相同的写通道寄存器选择当前通道等 RDY 信号读数据再切下一个通道。要注意的是连续转换模式下改了通道寄存器ADC 不会立刻输出新通道的结果需要等待当前转换周期结束之后才是新通道的转换数据。第一次读到的数据极可能是上一个通道的残留结果后面会专门讲怎么处理。4.2 通道切换时序建立时间与第一次读数丢弃Σ-Δ ADC 的数字滤波器决定了它和逐次逼近型 ADC 不一样通道切换后滤波器里还存着旧通道的历史数据。如果滤波器的建立时间很长比如 Sinc4 滤波器搭配很低的输出速率切换通道后立刻读数结果里会混着旧通道的信号。正确做法是给滤波器足够时间建立或者在读取序列里把第一次转换结果丢弃。#define AD7124_NUM_CH 4 #define AD7124_CH_DISCARD 1 // 丢弃第一次转换防止旧通道数据残留 int32_t ad7124_read_all_channels(int32_t *out_raw) { for (int ch 0; ch AD7124_NUM_CH; ch) { ad7124_select_channel(ch); // 写通道寄存器切换当前通道 // 等当前转换周期完成DOUT/RDY 由高变低表示新数据就绪 for (int wait 0; wait 1000; wait) { if (HAL_GPIO_ReadPin(ADC_RDY_GPIO_Port, ADC_RDY_Pin) GPIO_PIN_RESET) break; HAL_Delay(1); } // 丢弃第一次转换结果内部滤波器尚未完全建立 (void)ad7124_read_data(); // 再等一次 RDY读取可用数据 for (int wait 0; wait 1000; wait) { if (HAL_GPIO_ReadPin(ADC_RDY_GPIO_Port, ADC_RDY_Pin) GPIO_PIN_RESET) break; HAL_Delay(1); } out_raw[ch] ad7124_read_data(); } return 0; }这段轮询逻辑是整个驱动的骨架。选中第 N 个通道后第一个 RDY 下降沿出现时数据寄存器里可能还是上一个通道的值因此读出来直接丢弃随后的第二个 RDY 下降沿对应的才是当前通道的有效转换结果。超时计数 1000 次配合 1ms 延时意味着如果滤波器建立时间远远超过 1 秒这个驱动会超时。实际使用中要根据你设置的数据速率调整等待时间。有一个更简洁的做法写完通道寄存器后不做第一次读取直接延时约等于滤波器建立时间的数倍但这种方式会把时间浪费在无意义的等待上不如读一次丢弃来得干脆。4.3 数据寄存器读取与24位补码转电压AD7124 的数据寄存器是 24 位以二进制补码形式存放在连续三个字节里。读取时发出命令字节 0x82读数据寄存器然后接收三个字节。把三个字节拼成 32 位有符号数再根据基准电压、增益和输出速率换算成实际的电压值。#define AD7124_REG_DATA 0x02 #define AD7124_REF_VOLTAGE 2500.0f // 外部基准 mV #define AD7124_PGA_GAIN 1.0f int32_t ad7124_read_data(void) { uint8_t buf[4]; int32_t raw 0; buf[0] 0x80 | AD7124_REG_DATA; // 命令字节读数据寄存器 buf[1] 0x00; // 后续字节用于产生接收时钟 buf[2] 0x00; buf[3] 0x00; AD7124_CS_LOW(); HAL_SPI_TransmitReceive(hspi1, buf, buf, 4, 10); AD7124_CS_HIGH(); raw ((int32_t)buf[1] 16) | ((int32_t)buf[2] 8) | (int32_t)buf[3]; // 24 位补码转 32 位有符号数 if (raw 0x800000) raw - 0x1000000; return raw; } float ad7124_raw_to_voltage(int32_t raw, float gain) { // 满量程对应 ±基准电压 / 增益 return (float)raw * AD7124_REF_VOLTAGE / (gain * 8388608.0f); }数据拼接时要注意类型转换的顺序。buf[1] 是最高字节如果直接做左移char 类型的符号扩展会污染高 16 位所以先把它强转成 int32_t 再移位。补码转换那行代码判断最高位为 1 时减掉 0x1000000相当于把 24 位有符号扩展成 32 位有符号。电压换算是把 raw 相对于满量程 2^23 的比例映射到基准电压范围里。这个换算公式里的 8388608 就是 2^23别手误写成 2^24否则所有通道的电压值都会偏大一倍。增益参数是从当前 setup 配置里读出来的多通道不同增益的情况下每个通道要单独传入对应的 gain不能共用同一个值。5. AD7124多通道驱动的5个坑从数据错位到RDY卡死5.1 现象每个通道读到的都是上一个通道的值某开发者的双通道采集板通道 1 接 1.5V通道 2 接 0.5V读出来的结果却是通道 1 始终 1.5V通道 2 也 1.5V好像通道切换根本没生效。原因在于通道切换后没有等待当前转换周期结束就去读数据。AD7124 在连续转换模式下修改通道寄存器不会打断正在进行的转换数据寄存器里还是切换前的转换结果。解决办法就是前面代码里演示的切通道后等一个 RDY 下降沿丢弃一次数据再等下一个 RDY 读取有效值。如果两个 RDY 间隔太短还可以在切换通道后主动延时覆盖滤波器的建立时间用时间换稳定。5.2 现象上电后第一组多通道数据跳变上电后第一次读四个通道或者某几个通道的第一轮数据明显异常后续轮次恢复正常。根因有两类一是 AD7124 复位后内部基准和滤波器还没稳定二是 MCU 的 SPI 外设上电后的初始状态不确定第一笔事务的 CS 时序可能不完整。解决方法是驱动初始化结束后主动做一次完整的空扫描把第一轮数据全部读出后丢弃再做正常读取。更稳妥的做法是复位后加一个较长的延时比如 50 到 100ms等内部 LDO 和基准彻底稳定后再配置寄存器。如果系统对启动时间有要求可以把 ADC 的电源域和 MCU 分开控制先上电 MCU再上电 ADC这样 ADC 的复位时序是确定的。5.3 现象SPI频率调到满速后偶发读到 0xFFXXXX某个批次板子SPI 时钟设为 10MHz 时读数据偶发出现 0xFFFFFF 或 0xFFFF00 之类的异常值降频到 2MHz 就完全正常。这通常是信号完整性问题SCLK 频率过高MISO 线上的上升沿被 PCB 寄生电容拖慢MCU 在错误的采样点采到了电平未稳定的数据。另一个隐蔽原因是 CS 释放和最后一帧时钟之间的保持时间不够ADC 还没把最后一位数据稳定地送到 MISOCS 就被拉高了。处理方法是先用示波器看 MISO 和 SCLK 的沿对齐情况然后把时钟频率降一半再测完整扫描。如果必须跑高速就要优化 PCB 走线缩短 SCLK 到 ADC 的距离并在 MISO 线上并联小电容抑制振铃。我自己的习惯是量产固件里把 SPI 时钟固定到 4MHz 以下跑高速只出现在实验室调试验证阶段。5.4 现象DOUT/RDY脚长时间不拉低驱动像卡死多通道扫描运行一段时间后某一次读取卡在等待 RDY 的超时循环里ADC 好像“死”了。这个坑的根源多半是 SPI 事务被中途打断比如在 HAL_SPI_Transmit 执行期间被高优先级中断抢占导致通信寄存器中的命令没有完整发送AD7124 内部状态机停留在半途后续任何操作都无法正常响应。解决思路分两层软件上把多通道读取放进临界区确保每笔 SPI 事务不被其他中断打断硬件上给 DOUT/RDY 加上拉电阻防止引脚悬空导致电平不确定。如果已经卡死可以强制拉低 CS 并在 SCLK 上空转几个字节或者直接做一次软件复位。给驱动里留一个“复位并重新初始化”的恢复路径比在停机后人工断电重启要省心得多。5.5 现象电压读数总是偏小或有一个固定偏移多通道全部读数正常但把输入电压和万用表对比时总是固定偏小几十毫伏或者有一个相对稳定的偏移。常见原因是基准电压的实际值和代码里的配置值不一致。如果用了内部基准其实际精度有限不同温度下会有百分之几的偏差如果用了外部基准PCB 上基准源到 ADC 基准引脚的走线压降会产生固定偏移。另一个原因是输入共模电压超出规格导致 PGA 前端出现微小的失调。解决方法是对每个通道做两点校准输入端短路测得零点偏移输入精密电压源测得增益误差然后把校准系数存到非易失区。AD7124 的通道偏移寄存器和增益寄存器可以用来做硬件校准写进去后结果会直接修正。我一般会在量产前对每个通道做一次两点校准比单纯靠芯片指标更可靠。6. 进阶用单周期建立把扫描时间压到极限顺便说校验验证6.1 单周期建立模式让每个通道都只等一个输出周期常规的 Sinc4 滤波器在低数据速率下建立时间接近四个输出周期多通道扫描时每个通道都要等这段时间效率很低。AD7124 的滤波寄存器里提供了单周期建立模式开启后滤波器的建立时间压缩到一个输出周期内。这个模式很适合多通道轮流读取因为切换通道后只需等待一个输出周期即可读回有效数据扫描总时间大幅缩短。代价是对噪声的抑制能力比完整 Sinc4 模式弱一些适合信号本身比较干净的场合。如果测温或者称重这类对低频噪声有要求的场景还是建议回归完整 Sinc4。6.2 用已知电压源和ID做驱动验证驱动写完不要急着接真实传感器先用一个精密电压源直接给每个通道灌入已知电压从低到高扫几个点验证读回值的线性度和通道隔离度。验证时把 ID 校验结果、每个通道的初值、校准系数一起打印出来方便定位是哪个环节出问题。之前调一块 4 通道板时靠这个习惯发现通道 2 的输入引脚虚焊因为所有通道里只有通道 2 输出电压源灌入的值时有大幅度跳变。如果没有电压源也可以用干电池加电阻分压关键是电压要稳定且经过万用表实测。最后提醒一件事这套驱动里 SPI 引脚和 DOUT/RDY 引脚在不同板子上的映射可能完全不同移植到新板子第一个要改的就是引脚定义和 SPI 实例的选择。我早年吃过一次亏换了板子直接编译烧录结果 ID 读出来全是 0xFF排查了整整一个下午才发现是 CS 引脚接错了。那次之后养成了习惯任何驱动移植第一步先把 GPIO 对照表核对一遍再谈代码逻辑。这个习惯确实帮我省了不少时间希望对你也一样希望帮到你。本文还有配套的精品资源点击获取