简介:基于STM32F103C8T6与硬件SPI2的AD7124多通道读取驱动程序,是作者对早期单通道版本的改进升级,适合需要高精度、多路模拟信号采集的嵌入式开发者,也适合具有一定STM32和SPI基础的中级开发者学习参考。AD7124是高精度、低噪声的多通道模拟前端(AFE),在数字信号处理和模拟系统集成中应用广泛;该驱动优化了内部寄存器与时钟设置,提升每秒采样次数,修复PGA=1时对大于+2V和小于-2V信号的采集失真,并将最大读取速率提升至1.125MHz,同时精简main.c架构,便于维护和移植,从而为传感器信号调理提供稳定通路。资源包共145个文件,以h/C源文件、编译生成的o/d/crf中间文件、uvprojx工程配置及hex/axf烧录文件为主,压缩包仅3MB,基于Keil MDK5开发。已有1201人学习下载。借助多通道读取能力,可同时采集多路传感器信号,适用于医疗设备、工业控制及精密测量等对模拟前端精度与可靠性要求较高的场景;工程未启用同步模式,文件结构完整,可直接移植或二次开发。 做过多通道高精度采集的人大概都有这种感觉:芯片选型时看的是分辨率、噪声和通道数,等高高兴兴把板子画好、开始写驱动,真正的考验才算开始。这阵子我在一块工业数据采集板上用硬件SPI驱动AD7124做多通道读取,芯片本身是24位Σ-Δ ADC,通道多、内置PGA和数字滤波,听起来不算复杂,但真要把MCU的硬件SPI外设和它稳定跑起来、再保证多通道数据不错位,中间踩了不少坑。这篇文章把完整的寄存器配置链路、驱动骨架和实测排查过程记录下来,适合用STM32或其他带硬件SPI的MCU接AD7124做4到16路模拟量采集、又不想在软件模拟SPI上浪费时间的开发者参考。
1. 为什么最终方案锁定了硬件SPI
1.1 AD7124的通信时序决定了软件模拟不是好选择
AD7124是典型的Σ-Δ ADC从机,SPI时序要求看起来不算苛刻,但通信帧比较长:任何操作都要先发一个命令字节,然后才是寄存器数据。读一次24位数据寄存器,需要32个SCLK,也就是8位命令加24位数据。软件模拟SPI完全能把这一帧发完,我在早期原型板上也这么跑通过,但通道数量一上去,问题就来了。
主循环里同时要处理显示、通信、存储,GPIO翻转的时序抖动会直观地反映在DRDY握手波形上。更麻烦的是,如果中断打断命令字节和数据字节之间的发送,SCLK间隔被拉得太长,AD7124内部状态机可能已经推进,整个帧就废了。硬件SPI把字节发送交给外设处理,CPU只在整帧开始前拉低CS、结束后拉高CS,中断里只判断DRDY并读取数据,时序稳定性完全不一样。所以我后来坚持用MCU的硬件SPI外设,不再用软件模拟。
1.2 多通道读取的核心诉求:让CPU尽量少参与
AD7124-4提供4路差分或7路单端输入,AD7124-8提供8路差分或15路单端输入,对大多数变送器、称重传感器、热电偶采集板来说已经非常够用。多通道读取最笨的办法,是软件每路都去改通道寄存器、启动转换、等DRDY、读数据,然后再切下一路。这样每一轮转换都要CPU全流程参与,转换间隔里CPU基本被绑死,系统其他任务全部靠后。
硬件SPI配合AD7124的序列器(Sequencer)模式后,通道切换由ADC自己完成:使能多个通道,配置完成后启动连续转换,每完成一路转换DRDY就拉低一次,CPU只负责等中断、读DATA寄存器,ADC会自动切到下一个通道。这种方案下,CPU的负载降了一个数量级,整个系统的实时性和可靠性都上来了。多通道版驱动,本质上就是围绕“序列器如何跟硬件SPI配合”来展开的。
2. 硬件SPI下AD7124的寄存器配置链路
2.1 三个关键寄存器必须串起来理解
AD7124的寄存器数量看着多,但多通道场景下真正决定成败的只有三个:
| 寄存器 | 地址范围 | 作用 |
|---|---|---|
| 通道寄存器 CH0~CH15 | 0x10~0x1F | 通道使能、正负输入引脚选择、绑定哪个CONFIG |
| 配置寄存器 CONFIG0~CONFIG7 | 0x21~0x28 | 增益、单双极性、基准源、PGA等模拟参数 |
| 滤波器寄存器 FILTER0~FILTER7 | 0x29~0x30 | 滤波类型、输出速率、陷波频率等 |
这三者的关系是:通道寄存器的CONFIG位必须指向一个配置好的CONFIG寄存器,而这个CONFIG又必须对应一个同编号的FILTER寄存器。比如通道CH0要采集AIN0和AIN1的差分信号,增益1,内部基准,100 SPS输出速率,就需要在CH0寄存器里把CONFIG指向CONFIG0,然后在CONFIG0里设置增益和基准,在FILTER0里设置输出速率。
这套绑定关系错乱,是配置完成后转换结果异常的头号原因。很多新手一上来直接改通道寄存器,发现数据完全不对,翻手册半天才发现CONFIG和FILTER根本没配。我的习惯是先列一张通道分配表,把每一路的物理引脚、增益、输出速率、参考源固定下来,再去填寄存器,这样不会漏。
2.2 序列器方式配置多通道的完整顺序
多通道配置建议按固定顺序做,减少漏项:
- 上电后先复位,确保芯片状态可控。
- 关闭所有通道,避免上电默认值干扰后续配置。
- 按通道分配表依次配置CH寄存器,使能需要采样的通道,关联对应的CONFIG。
- 配置CONFIG寄存器,设置增益、单双极性、参考源。
- 配置FILTER寄存器,设置输出速率和滤波方式。
- 最后配置ADC控制寄存器,开启序列器,选择连续转换模式。
- 等待DRDY,开始逐通道读数据。
这套顺序里,最后一步特别重要:ADC控制寄存器的配置必须在通道、配置、滤波都就绪之后再进行,否则序列器可能在没有完整配置的情况下提前启动,导致转换结果不稳定。我调试时习惯把每一步配置后都回读确认,确认无误再进行下一步,后面会详细说回读验证的方法。
3. 驱动骨架与核心函数实现
3.1 SPI初始化细节:硬件片选还是软件片选
我用的MCU是STM32系列,以HAL库为例,硬件SPI初始化如下:
SPI_HandleTypeDef hspi2; void AD7124_SPI_Init(void) { hspi2.Instance = SPI2; hspi2.Init.Mode = SPI_MODE_MASTER; hspi2.Init.Direction = SPI_DIRECTION_2LINES; hspi2.Init.DataSize = SPI_DATASIZE_8BIT; hspi2.Init.CLKPolarity = SPI_POLARITY_HIGH; hspi2.Init.CLKPhase = SPI_PHASE_2EDGE; hspi2.Init.NSS = SPI_NSS_SOFT; hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB; HAL_SPI_Init(&hspi2); }这里有两个实际项目中很容易踩的细节。第一个是SPI极性和相位,必须按AD7124数据手册的时序图来配,不同MCU的CPOL/CPHA定义略有差异,配错的结果就是寄存器读写全部失败。第二个是片选,我强烈建议用普通GPIO手动控制CS,不要用硬件NSS自动片选。硬件NSS虽然方便,但在多设备共用SPI总线、或者需要连续发送命令加数据的高位宽寄存器帧时,硬件NSS的自动翻转时机很难控制,容易把一个完整的读操作拆成两个总线事务。AD7124一次读操作是“命令字节加3个数据字节”,中间CS被拉高再拉低,数据就作废了。所以我在驱动里统一用AD7124_CS_LOW和AD7124_CS_HIGH两个宏控制片选。
3.2 寄存器读写与复位函数
SPI通信的基础是命令字节加数据字节。AD7124命令字节的最高位是写使能位,为1表示写寄存器,为0表示读寄存器。读写函数如下:
void ad7124_write_reg(uint8_t reg_addr, uint8_t *data, uint8_t len) { AD7124_CS_LOW(); uint8_t cmd = 0x80 | reg_addr; HAL_SPI_Transmit(&hspi2, &cmd, 1, 100); HAL_SPI_Transmit(&hspi2, data, len, 100); AD7124_CS_HIGH(); } void ad7124_read_reg(uint8_t reg_addr, uint8_t *data, uint8_t len) { AD7124_CS_LOW(); uint8_t cmd = 0x40 | reg_addr; HAL_SPI_Transmit(&hspi2, &cmd, 1, 100); HAL_SPI_Receive(&hspi2, data, len, 100); AD7124_CS_HIGH(); } void ad7124_reset(void) { AD7124_CS_LOW(); uint8_t reset_cmd[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; HAL_SPI_Transmit(&hspi2, reset_cmd, 6, 100); AD7124_CS_HIGH(); HAL_Delay(10); }CS在整个帧过程中必须保持低电平,包含命令字节和数据字节,这是硬件SPI驱动AD7124的基本前提。另外,复位指令是CS拉低后连续发送至少64个SCLK的高电平,也就是6个0xFF字节,发送期间不能有其他SPI操作。复位后必须延时足够时间等待芯片内部上电稳定,我一般延时10毫秒,再开始读写寄存器。很多看起来莫名其妙的“写不进去”问题,根源就是复位后没有等待就立即操作。
3.3 多通道配置与数据读取
通道配置以“写入原始寄存器值”的方式封装,具体位域定义以数据手册Channel Register章节为准。工程上建议先把通道分配表做成数组,再统一转换:
typedef struct { uint8_t ch_index; uint8_t config_sel; uint16_t reg_value; } ad7124_ch_cfg_t; const ad7124_ch_cfg_t channel_configs[] = { {0, 0, 0x8001}, // CH0: AIN0+/AIN1-,CONFIG0,使能 {1, 1, 0x8001}, // CH1: AIN2+/AIN3-,CONFIG1,使能 {2, 2, 0x8001}, // CH2: AIN4+/AIN5-,CONFIG2,使能 {3, 3, 0x8001}, // CH3: AIN6+/AIN7-,CONFIG3,使能 }; void ad7124_config_multi_channel(void) { // 1. 关闭所有通道 for (int i = 0; i < 16; i++) { uint8_t data[2] = {0x00, 0x00}; ad7124_write_reg(0x10 + i, data, 2); } // 2. 按表配置通道 for (int i = 0; i < 4; i++) { uint8_t data[2]; data[0] = (channel_configs[i].reg_value >> 8) & 0xFF; data[1] = channel_configs[i].reg_value & 0xFF; ad7124_write_reg(0x10 + channel_configs[i].ch_index, data, 2); } // 3. 配置CONFIG0~3和FILTER0~3,这里以100SPS、增益1为例 for (int i = 0; i < 4; i++) { uint8_t config_data[2] = {0x00, 0x00}; ad7124_write_reg(0x21 + i, config_data, 2); uint8_t filter_data[2] = {0x06, 0x00}; // 具体输出速率由位域决定 ad7124_write_reg(0x29 + i, filter_data, 2); } }读数据的流程非常固定:等待DOUT/RDY拉低,然后读取3个字节的数据寄存器。我用的是DOUT引脚下降沿触发中断,这样CPU占用极低。
volatile uint32_t adc_raw_values[16]; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin == DOUT_PIN_Pin) { uint8_t data[3]; uint32_t raw; ad7124_read_reg(0x02, data, 3); raw = ((uint32_t)data[0] << 16) | ((uint32_t)data[1] << 8) | data[2]; if (raw & 0x800000) { raw |= 0xFF000000; } adc_raw_values[current_channel] = raw; current_channel = (current_channel + 1) % 4; } }这段代码有几个值得注意的地方。一是24位ADC的结果需要做符号扩展,否则负半轴数据会显示成很大的无符号数。二是current_channel必须在读取之后递增,因为序列器模式下读完DATA寄存器后ADC才会切换到下一个通道。三是中断里尽量只做读取和缓存,不要在中断里去做浮点换算或者滤波运算,容易把时序打断。
4. 实测中的坑:从乱码数据到稳定多通道输出
4.1 读回全0xFF:先复位,再查SPI极性
这个坑我踩过不止一次,而且每次都很有迷惑性。上电之后如果不对AD7124做复位,芯片状态完全不可控,直接往寄存器里写配置大概率失败,回读寄存器全部是0xFF。排查链路是这样的:
第一步,用逻辑分析仪抓CS、SCLK、MOSI、MISO四根线,确认命令字节确实发出去了,并且MISO上有响应。如果MISO一直不动,先查焊接和引脚定义。第二步,确认复位时序真的执行了。AD7124的复位是CS拉低后连续发送至少64个SCLK的1,我遇到过代码里写了一行HAL_SPI_Transmit发送0xFF,但因为发送长度设置成了8位,实际只发了8个SCLK,远不够触发复位。第三步,才是SPI极性和相位的问题。如果CS和SCLK波形都对,但数据就是不对,检查CPOL和CPHA两个参数,手动切换一下再回读ID寄存器验证。
4.2 序列器模式数据错位:DRDY握手顺序
如果你使能了CH0到CH3四个通道,却发现读到的第2、3、4组数据明显不符合物理输入,大概率是DRDY握手顺序出了问题。AD7124的序列器按通道编号顺序依次转换,每转换完一个通道,DRDY就拉低一次,主控收到低电平就读一个DATA,读完后DRDY自动变高,直到下一通道完成再次拉低。
最容易犯的错是:用定时器轮询DRDY,轮询间隔和转换周期之间形成竞争,一次SPI读事务覆盖了两个转换结果。表现就是数据错位、通道和结果对不上。我最后改成DOUT引脚的外部中断下降沿触发,每次下降沿只读一次,读完后清除标志,同时加超时保护防止中断丢失导致的死循环。这个方案跑了几个星期没有出现过错位。严格来说,DRDY的下降沿才是“数据有效”的唯一可靠凭据,不要自己推测转换完成时间。
4.3 高增益下跳变:稳定时间比滤波阶数更重要
AD7124内置PGA虽然方便,但增益高了之后,通道间充放电、传感器内阻和PCB走线漏电都会被放大。多通道切换时,前一通道的残余电荷会通过输入网络影响下一通道的第一个结果,这个现象在高输出阻抗传感器上尤其明显。
解决办法分两层。第一层是硬件上把传感器输出阻抗压下来,运放缓冲或者加大输入电容。第二层是软件上对每个通道的前N个采样点标记为“预热样本”,丢弃不用。同时把滤波器寄存器的稳定时间考虑进切换节奏。以SINC4滤波器为例,在100 SPS输出速率下,建立时间大约要40毫秒,也就是说切换通道之后至少要等40毫秒才能读到稳定数据。连续转换模式下如果发现数据突然跳变,别急着调高输出速率,先看看稳定时间够不够。这是多通道采集项目里最常见的“假故障”,也是纯工程经验的部分。
5. 多通道扩展:从8路到16路的工程考量
5.1 芯片级扩展与模拟开关扩展的取舍
如果AD7124-8的15路单端还不够用,常见加法是外扩模拟开关,比如CD4051、CD4067,把输入节点扩展出去,AD7124只接其中一到两路。这样做成本低,但代价也很明显:模拟开关的导通电阻一般有几百欧姆,对低内阻传感器的影响不可忽略;通道切换后,开关电容和线缆电容的建立时间也会被引入。高精度场景下,建议选低导通电阻、低漏电的开关,同时在软件里给足稳定时间,也就是上面说的预热样本机制。模拟开关接进去之后,系统的自校准和系统校准都要重新做一遍,否则增益误差和偏移误差都会变大。
5.2 多芯片SPI总线复用的注意事项
比模拟开关更稳的扩展方式是多块AD7124共用同一条SPI总线,SCLK、MOSI、MISO并联,每一片的CS独立由GPIO控制。读取时先拉目标芯片的CS,再发命令,整帧结束后拉高CS。这样做的好处是每片ADC仍然保持完整的模拟性能和通道数,坏处是PCB走线和驱动逻辑稍复杂。
这里有个总线层面的坑:同一帧SPI事务中间绝对不能插入其他设备的访问。如果项目里又要接Flash又要接ADC,共享总线的访问函数应该做成原子操作,要么关闭中断,要么加互斥信号量。我之前在一个项目里遇到过SPI Flash写操作把ADC的读数据帧打断,结果偶发读到0xFFFFFF,查了好久才定位到是总线竞争问题。
5.3 后续优化方向
多通道读取稳定跑通之后,还可以继续优化。数据缓存可以用DMA配合乒乓buffer,DRDY中断只置位标志,SPI读取和数据搬运交给DMA,CPU基本零负担。多通道的校准数据可以存到片外Flash,上电自动恢复,省去每次开机重新校准的麻烦。硬件设计上,VIO引脚的电平决定了SPI接口电平,和MCU供电不一致时记得加电平转换;参考电压去耦和模拟地分割对24位精度的影响,远大于驱动代码本身。
最后分享一个调试习惯:把所有寄存器读写都封装到write_reg和read_reg之后,先在main里把关键寄存器逐个回读,用串口打印出来,确认每一步配置都真实生效了,再开始跑中断读数据。我之前跳过这一步直接去调数据流,结果花了整整一天才发现是复位后没有等待就配置,配置根本没写进去。AD7124的寄存器链路并不复杂,只要把复位时序、CONFIG绑定关系、DRDY握手这三件事做对,硬件SPI多通道读取就能非常稳定。
本文还有配套的精品资源,点击获取