简介:本资源是一套面向嵌入式开发工程师与STM32进阶学习者的AD7124高精度模数转换芯片驱动代码,解决工业传感、精密测量等场景中ADC芯片与MCU可靠通信的典型问题。压缩包共7个文件(4个头文件、3个源文件),总大小仅9KB,结构精炼:头文件定义寄存器映射与设备结构体,C文件分别实现硬件SPI底层通信、AD7124寄存器读写及状态校验逻辑,包含带设备状态检查与无检查两种操作接口,便于调试与生产环境灵活调用。已有664人学习下载,代码注释清晰、函数接口规范,可直接集成至STM32标准外设库或HAL库工程,显著降低高精度ADC驱动开发门槛。读者可快速掌握AD7124配置流程、SPI时序控制要点及寄存器级调试方法,适用于温度、压力、应变等微弱信号采集系统开发。 前两个月我调一块工业采集板,板载AD7124-8,主控是STM32F103。第一版驱动用GPIO模拟SPI写,能通,但一上系统就出问题:读出来的数据偶尔是0xFFFFFF,偶尔又正常,换硬件SPI才彻底稳住。这次经历让我想通一件事——AD7124这种带24位Σ-Δ核心的精密ADC,虽然SPI速率要求不高,但对时序的确定性要求极高,用硬件SPI是正路。
这篇文章会把整套思路整理出来,包括为什么选硬件SPI、寄存器怎么配、代码怎么分层、以及实测中踩过的几个坑。适合正在调AD7124、或准备用STM32驱动精密ADC的工程师,也适合想搞懂SPI时序和ADC寄存器操作的初学者。
1. 为什么必须用硬件SPI:软件模拟的教训
1.1 软件模拟SPI在RTOS下的隐患
GPIO模拟SPI的驱动代码网上到处都是,原理很简单:拉低CS,翻转SCLK,按位把数据从MOSI送出去,再从MISO读回来。裸机环境下这么干没毛病,但一旦系统跑起来,问题就来了。
我那个项目是采集4路PT100的毫伏信号,AD7124工作在25SPS连续转换模式。裸机测试时读数很稳定,后来把驱动移植到带RTOS的工程里,主任务跑1ms周期控制逻辑,还有几个通信任务在跑。结果AD7124偶尔读出全1,偶尔又是错误值。
原因很直接:GPIO模拟SPI时,SCLK的每个翻转都靠CPU执行指令来完成。如果进程在发送中途被更高优先级任务抢占,SCLK的翻转就会暂停几微秒甚至几十微秒。AD7124的SPI虽然不要求严格连续,但主机命令字节、数据字节之间一旦被插入长时间停顿,芯片很容易把后续字节当成新命令,导致整帧错位。
1.2 硬件SPI的确定性来自外设时钟
换成STM32硬件SPI后,SCLK由外设时钟分频产生,移位寄存器按节拍自动工作,CPU只需要在传输启动前把数据写进DR寄存器,剩下的移位过程完全由硬件完成,不受中断和任务调度影响。这就是决定性差异:SPI时序是否可控,直接决定精密ADC的数据是否正确。
当然硬件SPI也有自己的坑,比如NSS引脚处理、FIFO阈值、传输模式配置,但这些坑是可预期的,不像GPIO模拟那样随机。后面我会专门讲。
1.3 AD7124对SPI时序的硬性要求
AD7124是ADI推出的24位Σ-Δ型ADC,和常见的逐次逼近型ADC不一样。Σ-Δ型内部有数字滤波器,转换结果不是一次量化出来的,而是经过多次采样和滤波积分出来的。这个特性决定了它通常工作在较低输出速率,但精度非常高。
它的SPI接口要求:
- 支持的最高SCLK频率要看数据手册电气特性表,不同供电电压下上限不一样。实际项目别卡上限,降到2MHz左右跑起来稳定很多。
- SPI模式是固定的Mode 1,也就是CPOL=0、CPHA=1,SCLK空闲为低电平,数据在第二个边沿采样。
- 所有寄存器操作都需要先写通信寄存器,告诉芯片接下来是读还是写、操作哪个寄存器。
- DOUT/RDY引脚是转换完成标志,同时兼作数据输出,属于开漏输出,外部必须接上拉电阻。
整套驱动里,SPI模式的配置是一票否决的。模式配错,读什么都是0xFF或0x00,而且没有任何提示。
2. 硬件连接与电路设计:决定调试是否顺利的关键
2.1 引脚连接和软件片选
我用的是STM32F103的SPI1,连接关系如下:
| AD7124引脚 | STM32引脚 | SPI信号 | 说明 |
|---|---|---|---|
| SCLK | PA5 | SPI1_SCK | SPI时钟 |
| DIN | PA7 | SPI1_MOSI | 主机输出到从机输入 |
| DOUT | PB0 | SPI1_MISO | 从机输出到主机输入 |
| CS | PA4 | GPIO输出 | 软件片选 |
| DOUT/RDY | PB1 | GPIO输入 | 转换完成标志,可选 |
这里选PA5/PA6/PA7是因为它们是SPI1的默认映射,而且避开了JTAG引脚,不需要额外做禁用JTAG的操作。
如果你用了其他引脚,比如PB3/PB4这种和JTAG复用的引脚,一定要记得先调用GPIO_PinRemapConfig禁用JTAG,否则引脚电平根本驱动不出来。
CS引脚建议用普通GPIO做软件片选,不要用STM32的硬件NSS。硬件NSS在主模式下的行为受制于SPE位和NSS模式配置,操作起来别扭,而且多个SPI从机挂在同一总线上时需要自己控制CS,软件片选明显更灵活。
DOUT/RDY这个引脚容易被忽略。它是开漏输出,板子上必须加上拉电阻,我习惯用10k,否则读不到低电平,就没法判断转换是否完成。如果不关心转换完成事件,也可以把这个引脚完全空着,轮询状态寄存器,但这样会有几微秒到几十微秒的延迟。
2.2 电源、基准和去耦细节
AD7124是24位分辨率,电源和基准的干净程度直接影响最终读数。
供电上我分了三个域:AVDD、DVDD、IOVDD。AVDD给模拟核心供电,DVDD给数字核心供电,IOVDD给SPI接口供电。这三个脚别直接并在一起从同一个LDO出,虽然芯片内部有隔离,但板级上最好还是分成两个LDO,AVDD一路单独走,DVDD和IOVDD可以共用一路。
去耦电容必须靠近电源引脚放置。我的做法是每个电源引脚旁放一个0.1uF陶瓷电容,然后在电源入口处放一个10uF钽电容。AVDD和AVSS之间还要加一个1uF电容,这个位置离芯片越近越好。
模拟输入走线要尽量短而对称,避开数字信号线,特别是SCLK和MOSI。可以参考一个简单原则:模拟输入区域下方不要走SPI线,否则SCLK的谐波会耦合进ADC输入端,表现为读数低几位跳动。
基准电压源的选择也很重要。AD7124内部有1.25V和2.5V基准,精度和温漂对一般应用够用,但如果做称重或高精度温度测,建议用外部基准。配置寄存器里可以选择基准源,代码里默认用的是内部2.5V基准。
3. 源码模块划分与驱动分层:一套能换芯片复用的结构
3.1 文件结构与各层职责
这套源码不是把所有代码堆在main.c里,而是分成几层,方便以后换主控或者换ADC型号。
Project/ ├─ Core/ │ └─ main.c ├─ Driver/ │ ├─ ad7124_regs.h │ ├─ ad7124_driver.h │ ├─ ad7124_driver.c │ ├─ stm32_spi_port.h │ └─ stm32_spi_port.c └─ BSP/ └─ bsp_spi.c- ad7124_regs.h:定义AD7124所有寄存器的地址和关键位的宏。
- ad7124_driver.c:和硬件无关的AD7124操作逻辑,包括初始化状态机、写寄存器、读数据、通道配置。
- stm32_spi_port.c:封装STM32 HAL库的SPI接口,提供spi_write_read等函数。
- bsp_spi.c:初始化SPI外设、GPIO、中断优先级。
这样分层的意义在于:如果把STM32换成其他厂商的MCU,只需要改stm32_spi_port.c和bsp_spi.c,ad7124_driver.c完全不用动。反过来,如果换ADC型号,只需要替换ad7124_开头的文件,SPI层保持不动。
3.2 写寄存器与读数据的核心函数
AD7124的通信寄存器机制是理解整个驱动的基础。所有寄存器操作都是先写通信寄存器,格式如下:
- bit 6 = 1 表示后续为读操作,0 为写操作
- bit 5 ~ bit 0 为目标寄存器地址
- bit 7 固定为0
所以读数据寄存器0x02时,第一个命令字节是0x40 | 0x02 = 0x42。写ADC控制寄存器0x01时,第一个命令字节是0x00 | 0x01 = 0x01。
写寄存器函数:
void ad7124_write_reg(ad7124_dev_t *dev, uint8_t reg_addr, uint16_t value) { uint8_t tx_buf[3]; tx_buf[0] = reg_addr & 0x3F; /* bit6=0,写操作 */ tx_buf[1] = (uint8_t)(value >> 8); tx_buf[2] = (uint8_t)(value & 0xFF); ad7124_cs_low(dev); HAL_SPI_Transmit(dev->hspi, tx_buf, 3, HAL_MAX_DELAY); ad7124_cs_high(dev); }读转换数据的函数稍微特殊一点,因为数据寄存器是24位,命令字节之后要再接收3个字节:
int32_t ad7124_read_data(ad7124_dev_t *dev, uint32_t *data) { uint8_t tx_buf[4] = {0}; uint8_t rx_buf[4] = {0}; tx_buf[0] = 0x40 | AD7124_REG_DATA; /* 读数据寄存器 */ ad7124_cs_low(dev); HAL_SPI_TransmitReceive(dev->hspi, tx_buf, rx_buf, 4, HAL_MAX_DELAY); ad7124_cs_high(dev); *data = ((uint32_t)rx_buf[1] << 16) | ((uint32_t)rx_buf[2] << 8) | ((uint32_t)rx_buf[3]); return AD7124_OK; }注意读操作时,命令字节之后要发送3个空的0x00字节来产生SCLK时钟,接收到的数据才是完整的24位转换结果。这就是为什么tx_buf长度是4而不是1。
4. AD7124初始化链路与数据采集流程:从寄存器配置到真实电压
4.1 初始化顺序为什么重要
AD7124的初始化顺序是有讲究的,乱序写寄存器容易导致起不来或者配置被意外覆盖。
我的推荐顺序是:
- 硬件复位:拉低CS,DIN保持高电平,发送至少64个SCLK脉冲。这是数据手册明确的复位方式。
- 读ID寄存器,确认SPI通路正常。AD7124-4返回0xE1,AD7124-8返回0xE2,具体以对应型号数据手册为准。如果读不到,先别往下走,回去查硬件。
- 写ADC控制寄存器,把转换模式设为待机或关断,此时芯片不启动转换。
- 配置通道寄存器:选择模拟输入引脚对,并绑定到某个配置寄存器和滤波器寄存器。
- 配置对应的配置寄存器:设置增益PGA、基准源、单端/差分模式。
- 配置对应的滤波器寄存器:设置输出数据速率和滤波器类型。
- 写ADC控制寄存器,启动转换。
这里要解释清楚通道、配置、滤波器三者的关系。AD7124里有16个通道寄存器,但只有8个配置寄存器和8个滤波器寄存器。每个通道寄存器通过位段指定自己使用哪个配置寄存器和哪个滤波器寄存器。这样设计的好处是,不同通道可以共用同一组增益和滤波器配置,也可以各自独立。
初始化完成后,我习惯先读一遍所有配置寄存器,打印出来确认和写入值一致。这一步能排除SPI写入时序问题。
4.2 读取AD值与工程量换算
AD7124的数据寄存器是24位,格式取决于配置寄存器的单极性/双极性设置。
双极性模式下输出是二进制补码:
- 正满量程约0x7FFFFF
- 零输入为0x000000
- 负满量程约0x800000
换算公式:
电压 = (adc_code × Vref) / (增益 × 2^23)其中adc_code需要先转换成有符号数。
单极性模式下:
电压 = (adc_code × Vref) / (增益 × 2^24)这个公式看着简单,但实际项目里最容易栽在符号扩展上。从寄存器读回来的裸数据是uint32_t,如果你直接像无符号数一样参与运算,负电压会算成接近满量程的正数。
正确的做法是先判断bit23,如果为1,就把高8位全部填充为0xFF,转成int32_t再做运算:
int32_t adc_signed; if (raw & 0x800000) adc_signed = (int32_t)(raw | 0xFF000000); else adc_signed = (int32_t)raw; float voltage = (float)adc_signed / 8388608.0f * vref / gain;我最初在这上面吃过亏,读出来电压值一直在±错误范围跳,后来才意识到是符号扩展没做。
4.3 输出速率与滤波窗口的配合
AD7124内置Sinc3或Sinc4数字滤波器,输出速率由滤波器寄存器里的FS位和滤波模式决定。常见配置有25SPS、50SPS、60SPS、250SPS、4800SPS等。
如果做工业测温,50SPS配合50Hz工频干扰抑制效果不错。如果做称重传感器,25SPS再配合应用层20点滑动平均,读数基本能稳定到最后一位。
这里有一个容易被忽视的细节:输出速率设置得越高,噪声越大,有效位数越低。AD7124在5SPS时的有效分辨率可以做到20位以上,但在4800SPS时可能只有16位左右。性能指标在数据手册的RMS噪声表格里,选择速率之前先查一下表,比实际测试更快更准。
5. 实测踩坑记录:SPI通信异常的完整排查思路
5.1 现象:ID寄存器读0xFF
我第一版板子焊接完成后,跑最简单的读ID程序,HAL_SPI_TransmitReceive返回正常,但读回来的ID是0xFF。
这种故障最容易让人怀疑硬件:是不是芯片焊反了?是不是虚焊?是不是买到了翻新片?其实不一定。我花了半天检查供电、焊接、引脚,最后问题出在SPI模式配置上。
5.2 排查链路:从波形到相位
完整的排查思路是这样的:
第一步,用示波器抓CS、SCLK、MOSI三根线。确认CS有拉低动作,SCLK有时钟脉冲,MOSI上出现了命令字节0x42的波形。这一步说明STM32侧基本正常。
第二步,抓MISO线,发现一直保持高电平,没有数据返回。此时有两种可能:AD7124没有识别到命令,或者AD7124根本没上电工作。前者更常见。
第三步,用万用表复查AVDD、DVDD、IOVDD电压,确认都在数据手册范围内。再检查REFIN引脚电压,因为如果基准没起来,ADC也不会正常工作。
第四步,把SPI速率降到最低,排除高速振铃问题。这时仍然读0xFF。
第五步,用示波器对比SCLK上升沿和MOSI数据变化的位置。在Mode 0下,数据通常在上升沿变化,而AD7124要求Mode 1,数据在上升沿变化是错的,会导致芯片采到错误的电平。把CLKPhase从SPI_PHASE_1EDGE改成SPI_PHASE_2EDGE后,ID读回正常。
这个问题本质上就是主机输出数据沿和从机采样沿没有对齐。SPI Mode 0和Mode 1的区别就是采样沿相差半个周期,对某些从机芯片来说,Mode 0也可以用,但AD7124的数据手册明确要求Mode 1。
5.3 高速传输的数据错误与板级处理
另一个实际问题是速率高了之后,MISO数据偶尔跳变。我一开始用SPI1的72MHz时钟直接分频8,也就是9MHz SCLK,超过了数据手册推荐上限,数据偶尔错一位。
降速到分频32,也就是约2.25MHz后,连续跑一晚上再也没出错。这个经验说明,SPI速率不是越高越好,要有针对性地降速。
板级上还可以做的处理是:在SCLK线上串一个22Ω到33Ω的电阻,靠近STM32引脚端放置。这个电阻能抑制信号振铃和过冲,对改善MISO回读质量有帮助。MOSI和MISO线如果走线较长,也可以各串33Ω,但MISO线上的串联电阻会影响上升沿,不能太大。
5.4 DOUT/RDY引脚的两个问题
DOUT/RDY是开漏输出,第一版板子我漏画了上拉电阻,结果GPIO读到的电平一直是高。后来飞线加了一个10k上拉到IOVDD,转换完成标志才正常。
另外注意,如果在连续转换模式下读取数据寄存器,DOUT/RDY会在读操作完成后自动回到高电平,然后在下一个转换周期完成时再次拉低。如果使用中断方式读取,中断服务函数里要尽快完成SPI读取,否则可能错过下一个周期的下降沿,导致丢数据。
6. 这套代码能怎么继续扩展:中断、DMA与多通道轮询
6.1 用RDY下降沿替代轮询
基础版的代码是轮询方式:启动转换后不停读GPIO电平,等到DOUT/RDY变低再去读数据。这种方式逻辑简单,但CPU被占住了。
改进方案是把DOUT/RDY配置为EXTI下降沿触发,中断服务函数里设置标志位,主循环检测到标志后再去读取数据。这样CPU可以在等待转换期间干别的事情。
注意中断服务函数里不要直接做耗时操作,尤其是HAL_SPI_TransmitReceive这类阻塞调用,会导致中断耗时过长。建议在中断里只置标志位,然后回到主循环再做读取。如果系统实时性要求更高,可以配合DMA。
6.2 SPI DMA读取的实现思路
SPI DMA的关键是DMA模式下,主机接收数据时也要同时发送数据产生时钟。所以接收DMA要准备好发送缓冲区,内容全部填充为0x00。
以4字节的数据寄存器读取为例:
uint8_t tx_buf[4] = {0x42, 0x00, 0x00, 0x00}; uint8_t rx_buf[4] = {0}; HAL_SPI_TransmitReceive_DMA(hspi, tx_buf, rx_buf, 4);DMA传输完成后在回调函数里拼接rx_buf[1]到rx_buf[3],得到24位转换结果。这里要注意,SPI DMA的发送和接收是同时启动的,但DMA中断触发时机要配置为传输完成,别只配了半传输。
6.3 多通道自动轮询与状态区分
AD7124支持多个通道使能后自动轮询,每完成一个通道的转换,数据寄存器就更新为该通道的结果。
使用这个功能时,需要把多个通道寄存器都使能,并用状态寄存器的CH_ACTIVE位段来区分当前转换对应的通道。读取流程变成:
- 等待DOUT/RDY变低。
- 读状态寄存器,拿到当前通道号。
- 读数据寄存器,根据通道号换算成对应通道的电压。
- 循环。
调试多通道时,建议先在单通道模式下把每个通道单独调好,再开启多通道轮询。一口气全开,出了问题很难判断是哪个通道的配置错了。
个人经验是,多通道轮询时每个通道的配置寄存器和滤波器寄存器尽量保持一致,除非你明确知道某个通道需要不同的增益或速率。不同通道速率不一致时,轮询周期会受限于最慢的那个通道,这是很多人没料到的坑。
另外,如果几个通道共用同一个滤波器寄存器,改了一个通道的速率,其他通道也会跟着变。所以需要不同速率的场景,一定要把通道绑定到不同的滤波器寄存器上。
最后再分享一个小细节:硬件SPI驱动AD7124,用下来最值得留意的反而不是SPI本身的配置,而是你对自己的工程还有没有隐藏的引脚冲突。我调试过程中遇到过一次死等HAL_SPI_Transmit的卡死,排查半天发现是SPI1的引脚被调试器的功能占用了。这类问题,示波器看不出来,但只要养成“每次初始化外设后回读配置寄存器确认一次”的习惯,就能把问题隔离在很早期。
本文还有配套的精品资源,点击获取