资讯动态

硬件SPI驱动AD7124多通道高精度ADC完整方案

发布时间:2026/10/20 5:02:59 来源:尧图企业网站定制
简介面向AD7124高精度模拟前端提供一套基于STM32F103C8T6微控制器的硬件SPI多通道读取驱动改进工程适合需处理多路模拟信号的嵌入式开发者参考可直接用于传感器数据采集与变送器应用。改进版重点解决了PGA增益为1时采集2V与-2V信号出现的失真通过优化寄存器设置与时钟配置将AD7124读取速率提升至1.125MHz每秒采样次数明显提高同时精简了主程序架构使main.c代码更简洁便于阅读和二次开发。该工程支持多通道同时读取适用于医疗设备、工业控制及精密测量等多路信号采集场景。工程基于Keil MDK5环境使用SPI2接口未启用同步模式。压缩包共145个文件以C语言源码、头文件、Keil工程文件及编译生成的axf、hex固件为主整体约3MB导入MDK5即可编译工程。目前已有1224人学习下载对正在调试AD7124驱动或想要优化采集速率与精度的工程师具有直接的代码参考与排错价值。1. 硬件SPI驱动AD7124多通道高精度ADC的一次性打通方案做工业数据采集或者精密仪表的朋友大概率都绕不开高精度ADC选型这个话题。AD7124这颗24位Sigma-Delta ADC凭借内置可编程增益放大器、参考源和多种数字滤波选项在称重、温度、压力采集项目里出镜率极高。但真正上手时很多人会卡在同一个地方用模拟GPIO去模拟SPI时序速率上不去时序抖动还直接影响采样精度。这个项目源码的核心思路很直接——直接用STM32的硬件SPI外设去驱动AD7124把多通道扫描读取做成一套完整可复用的驱动层不依赖具体业务。主控只负责发命令、收数据剩下的配置和通道切换由驱动自动完成。适合正在做传感器调理电路、工业采集板、或者打算把AD7124引入现有MCU工程的开发者。2. 方案总览为什么选硬件SPI加AD7124多通道2.1 硬件SPI和GPIO模拟SPI的真正差距很多初学者习惯用软件模拟SPI因为代码直观、引脚随意调试时也方便打日志。但放到AD7124这种高精度ADC上模拟SPI有几个实际痛点首先是速率上不去GPIO翻转一次需要若干个CPU周期哪怕主频跑到72MHz模拟出来的SCLK撑死也就几百kHz其次是时序不确定如果中断或者任务调度插入SCLK和CS之间的边沿关系会抖动而AD7124对SCLK上升沿采样、下降沿更新的时序窗口是有要求的。抖动严重时偶发的数据错位会直接反映在读取的ADC码值上你很难分辨是传感器本身噪声还是通信时序引入的误差。硬件SPI的优势在于SCLK的极性、相位、速率完全由外设硬件生成不占用CPU翻转引脚的开销时序稳定性和传输速率是两个量级。项目里主频按常规STM32F1/F4选型来设计SPI速率设置在2.25MHz左右这个值是考虑AD7124官方手册SCLK最高50MHz下的余量设计同时留足了PCB走线和隔离器件的裕度。对于24位数据单次读取大约需要32个时钟周期含命令字节在2.25MHz下耗时约14微秒多通道扫描也完全可控。2.2 AD7124的优势和选型理由AD7124-4和AD7124-8是两颗常见的变体区别主要在输入通道数量前者4个差分或8个单端输入后者翻倍。项目代码按照AD7124-4来配置但寄存器地址和命名保持向下兼容换用-8时只需要扩展通道寄存器映射即可。选这颗芯片还有几个更实际的理由内置低噪声可编程增益放大器PGA增益档位从1到128可以直接接毫伏级的桥式传感器内置1.17V或2.5V参考源同时也支持外部差分参考输入数字滤波支持Sinc3和Sinc4对50Hz/60Hz工频干扰有天然的抑制能力。这些特性叠加下来外围电路可以做得非常精简——少了一堆独立仪表放大器和精密参考源芯片BOM成本直接降一个台阶。项目里默认采用内部2.5V参考、PGA增益设为1后续改增益只需改寄存器映射表里的配置值不用动接线。2.3 工程目录结构和文件职责拿到源码包后建议先看目录结构再动手编译。整个工程按「硬件抽象-芯片驱动-应用示例」三层拆分不是一坨代码怼到底├── drivers/ │ ├── ad7124_regs.h // 寄存器地址和Bit定义 │ ├── ad7124_driver.c // 芯片操作核心配置、读数据、通道切换 │ └── ad7124_driver.h ├── bsp/ │ ├── bsp_spi.c // STM32硬件SPI初始化针对某一系列MCU │ └── bsp_gpio.c // CS片选、复位引脚控制 ├── examples/ │ └── main.c // 演示多通道轮询读取 └── README.md // 引脚连接说明和编译指引这种分层的做法好处是换主控的时候只需要动bsp层AD7124的寄存器配置逻辑完全不用改。如果读者手里的MCU不是同一型号对照bsp_spi.c里的注释改成自己主控的SPI初始化即可。下一章就逐个文件拆核心代码。3. 初始化链路时钟、SPI外设和AD7124寄存器配置3.1 GPIO与SPI外设初始化先把最底层的bsp层讲清楚。STM32的SPI外设初始化其实就几件事打开时钟、配置引脚复用功能、设定SPI参数。void bsp_spi_init(void) { GPIO_InitTypeDef gpio; SPI_InitTypeDef spi; // 1. 使能SPI1和GPIOA的时钟PA5SCK, PA6MISO, PA7MOSI RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_GPIOA, ENABLE); // 2. CS引脚手动控制配置为通用推挽输出 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin GPIO_Pin_4; // CS片选PA4 gpio.GPIO_Mode GPIO_Mode_Out_PP; // 推挽输出 gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); // 3. SCK/MOSI为复用推挽输出MISO为浮空输入 gpio.GPIO_Pin GPIO_Pin_5 | GPIO_Pin_7; gpio.GPIO_Mode GPIO_Mode_AF_PP; gpio.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, gpio); gpio.GPIO_Pin GPIO_Pin_6; gpio.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, gpio); // 4. SPI1 主机模式2.25MHz模式0CPOL0, CPHA1 spi.SPI_Direction SPI_Direction_2Lines_FullDuplex; spi.SPI_Mode SPI_Mode_Master; spi.SPI_DataSize SPI_DataSize_8b; spi.SPI_CPOL SPI_CPOL_Low; spi.SPI_CPHA SPI_CPHA_2Edge; spi.SPI_NSS SPI_NSS_Soft; spi.SPI_BaudRatePrescaler SPI_BaudRatePrescaler_32; spi.SPI_FirstBit SPI_FirstBit_MSB; SPI_Init(SPI1, spi); SPI_Cmd(SPI1, ENABLE); }代码里有几个容易出错的点。CPHA1第二个边沿采样是AD7124手册明确规定的用错会出现读回的数据整体移位一个时钟周期现象是所有通道读到的值都在随机跳变在下一章的避坑清单里会单独展开。SPI_BaudRatePrescaler_32这个分频值是按72MHz主频算出来的如果读者的板子主频不同得重新计算不然SCLK超出手册上限会把芯片的通信逻辑搞到异常状态。CS引脚不要用SPI硬件的NSS因为AD7124要求每次通信时CS拉低、结束后拉高用软件控制更灵活方便插入复位时序。GPIO初始化完成后暂时不操作CS。片选逻辑封装在驱动层由每次读写函数统一管理这样应用层永远不需要关心CS的电平状态。3.2 AD7124寄存器读写时序的实现看芯片的SPI协议文档第一眼会被一串寄存器绕晕。其实核心就一个通信寄存器0x00所有读写命令都要先写这个寄存器告诉芯片接下来你要读哪个地址、还是写哪个地址。驱动里封装好了两个底层函数uint8_t ad7124_read_byte(uint8_t reg_addr) { uint8_t cmd 0; uint8_t rx_data 0; // 高三位表示读操作低五位是寄存器地址0x00-0x1F cmd (1 7) | (0 6) | (0 5) | (reg_addr 0x1F); AD7124_CS_LOW(); spi_write_read_byte(cmd); // 发送命令字节收到的是无效字节 rx_data spi_write_read_byte(0xFF); // 发送任意字节同时芯片输出寄存器内容 AD7124_CS_HIGH(); return rx_data; } void ad7124_write_byte(uint8_t reg_addr, uint8_t data) { uint8_t cmd 0; // 写命令最高位为0表示写 cmd (0 7) | (0 6) | (0 5) | (reg_addr 0x1F); AD7124_CS_LOW(); spi_write_read_byte(cmd); spi_write_read_byte(data); AD7124_CS_HIGH(); }注意这里的spi_write_read_byte是个对称函数发送一个字节的同时收到一个字节。因为SPI是Full DuplexMISO和MOSI同时工作读寄存器时第二个字节才是芯片真正返回的数据第一个字节是命令字节的来回通常忽略。命令字节里高三位R/W位和寄存器地址的对应关系十六进制换算下来就是读0x04数据寄存器发送0x84写0x10通道0寄存器发送0x10。这个写法和芯片数据手册里的通信时序图完全一致。3.3 上电复位和配置序列芯片上电后默认处于空闲状态但寄存器的默认值未必符合实际应用。稳妥做法是先发一个复位脉冲然后按顺序配置。AD7124的复位时序比较特殊不是拉RESET引脚而是把SCLK连续拉低64个周期。驱动里的实现方式是把MOSI拉低然后调用SPI连续发送64个0x00字节void ad7124_reset(void) { uint8_t i; AD7124_CS_LOW(); for (i 0; i 8; i) { // 发送8字节全0累计64个SCLK触发芯片复位 spi_write_read_byte(0x00); } AD7124_CS_HIGH(); // 复位后需要等待芯片内部上电完成约500ms delay_ms(500); }这里有个血泪经验很多开发者在初始化时没有执行复位操作直接读ID寄存器偶尔能读对偶尔读到0x00。原因是芯片在上电瞬间电源还没稳定内部状态机没有进入正常模式。发一次复位脉冲相当于给它一个强制的确定性起点之后所有寄存器配置才能确保落盘。项目里复位完成后紧接着执行初始化配置将PGA增益、输出速率、通道使能一次性写入。整个配置序列如下void ad7124_init(void) { // 复位并等待 ad7124_reset(); // 配置通道0为AIN0-AIN1差分输入PGA1使能 // 通道寄存器地址0x10位[7:5]为AIN位[3:1]为AIN-位[0]为使能 ad7124_write_byte(0x10, (0x00 5) | (0x01 1) | 0x01); // 配置通道1为AIN2-AIN3差分输入使能 ad7124_write_byte(0x11, (0x02 5) | (0x03 1) | 0x01); // 配置设置寄存器0: 输出速率和滤波 // 地址0x19默认写入0x0600对应的Sinc3滤波、输出数据速率约50SPS ad7124_write_byte(0x19, 0x06); ad7124_write_byte(0x1A, 0x00); // ADC控制寄存器内部参考使能正常工作模式 // 地址0x01写0x00开启内部参考 ad7124_write_byte(0x01, 0x00); }通道寄存器的位域映射在头文件里有完整宏定义比如AD7124_CH_MAP_AIN0_AIN1、AD7124_PGA_1等项目里直接用宏不会写裸数字。这样做的好处是别人维护代码时不需要频繁翻手册对上每一位的含义。4. 多通道轮询读取从检查数据就绪到拼装24位码值4.1 数据就绪状态判断AD7124内部有状态寄存器地址0x00最低位是DRDY标志。每次配置完通道或者转换完成后芯片会把这一位置为0表示数据已就绪读取数据寄存器后自动清回1。驱动里必须按这个流程操作不然很容易读到转换到一半的不完整数据。uint8_t ad7124_wait_ready(uint32_t timeout_ms) { uint32_t tick 0; uint8_t status 0; while (tick timeout_ms) { status ad7124_read_byte(0x00); // 状态寄存器 if ((status 0x01) 0) { return 1; // 数据就绪 } delay_ms(1); tick; } return 0; // 超时 }这个等待函数要放在通道切换之后调用。想想看切换通道后芯片需要经历一个完整的转换周期输出速率如果设为50SPS等待时间就是20毫秒起步。如果在数据还没就绪时就读数据寄存器通常拿到的值会偏大因为寄存器的内容是上一次转换结果容易让调试者误以为「读数一直不变」或「读数跳变」。4.2 24位数据寄存器的读取与符号扩展数据寄存器地址0x04是24位宽但SPI按字节操作所以需要连续读3个字节再手动拼装。这部分的实现有讲究尤其是符号位处理int32_t ad7124_read_data(void) { uint8_t buf[3] {0}; int32_t raw 0; // 连续读取三个字节先高字节后低字节 buf[0] ad7124_read_byte(0x04); buf[1] ad7124_read_byte(0x04); buf[2] ad7124_read_byte(0x04); // 24位数据拼装 raw ((int32_t)buf[0] 16) | ((int32_t)buf[1] 8) | (int32_t)buf[2]; // 符号扩展如果是负数把高8位补成0xFF if (raw 0x800000) { raw | 0xFF000000; } return raw; }这段代码里最容易翻车的是符号位处理。AD7124输出的是二进制补码格式单极性工作模式下最大值是0xFFFFFF对应的满量程双极性模式下0x7FFFFF是正满量程0x800000是负满量程。如果直接把3个字节塞进int32_t前24位不做符号扩展那么负的半周期读数会变成一个巨大的正数看起来像「传感器输出怎么飘上天了」。所以读完之后必须判断最高位把第24位扩展成完整的32位有符号数。4.3 多通道轮询扫描的实现多通道读取的核心逻辑是先设置当前活动通道等待数据就绪读取结果然后切换到下一个通道。项目示例代码采用顺序遍历的方式#define NUM_CHANNELS 2 // 当前使用两个通道 void demo_multi_channel_scan(void) { int32_t ch_data[NUM_CHANNELS] {0}; uint8_t ch 0; while (1) { // 依次切换通道并读取 for (ch 0; ch NUM_CHANNELS; ch) { ad7124_set_channel(ch); // 写通道选择寄存器 if (ad7124_wait_ready(100)) { // 等待转换完成最多100ms ch_data[ch] ad7124_read_data(); // 读取并保存码值 } } // 这里可以加上校准、滤波或者通过串口输出的代码 // 比如把ch_data[0]和ch_data[1]通过UART发送到上位机 } }通道选择寄存器的写操作封装在ad7124_set_channel函数里实现是将通道号和通道使能位重新写入通道0对应的寄存器。贴一下这个关键实现void ad7124_set_channel(uint8_t ch) { // 通道选择寄存器地址0x10 ch使能位默认置1 ad7124_write_byte(0x10 ch, (ch 5) | 0x01); }这里把通道号当作AIN引脚正端AIN-固定接下一引脚AIN0-AIN1、AIN2-AIN3这种差分配置。如果读者需要更灵活的通道映射比如AIN0接正、AIN3接负那么这里就不能偷懒用ch直接计算而是要查一张预定义的通道映射表。示例代码为了可读性做了简化实际项目建议把这部分改成查表方式后面改造章节会详细说。5. 避坑手册AD7124调试中的高频翻车现场5.1 配置寄存器和通道寄存器混为一谈现象初始化之后读取ID寄存器是对的但通道切换后读到的数据始终是同一个值。原因通道寄存器和设置寄存器是两个独立地址空间。很多人把写配置寄存器的值误写到了通道寄存器里或者反过来。通道寄存器管的是输入引脚选择和使能设置寄存器管的是输出速率、滤波类型和PGA增益。写错位置的话芯片根本不会切换输入源所有通道读到的都是同一个模拟引脚的转换结果。解决对照手册把两个地址表打出来。通道寄存器地址是0x10到0x17设置寄存器是0x19到0x1C每次写寄存器之前确认地址落在哪个范围。项目里建议在驱动层把读写接口的参数从裸地址改成枚举类型编译期就能发现搞混的情况。5.2 SPI时序的回读值整体位移一位现象读到的数据永远是真实值的一半或者两倍而且高位恒定为0或1看起来像触发了一个线性增益误差。原因这是典型的CPOL/CPHA配置错误。AD7124要求CPOL0、CPHA1也就是SCLK空闲为低、第二个边沿采样。如果误配成CPHA0主机在第一个边沿采样恰好采到的是上一个比特的数据整个字节的位序错开一位。解决可以在初始化SPI后先读一次状态寄存器如果发现返回的值有一位固定的错位比如规律性的0x7F或者0xFE立即检查CPHA配置。这类问题短期内不影响通信但会在数据处理阶段彻底带偏计算让人误以为是传感器漂移。5.3 复位时序被片选拉高打断现象复位后芯片没有任何响应读什么寄存器都返回0xFF。原因复位需要连续64个SCLK低电平期间CS必须一直保持低电平。有些实现图省事字节之间拉高再拉低CS这样每8个SCLK就被CS中断一次复位时序永远无法完成。AD7124的复位时序是连续的不允许中间片选跳变。解决复位函数里确保for循环执行期间CS一直为低循环结束后再拉高。这个细节在代码注释里已经标红但很多人从旧项目移植SPI代码时会把CS操作放到每个字节发送函数里就触发了这个问题。5.4 首次读取数据寄存器时返回0xFFFFFF现象上电后第一次读数据返回全1之后正常。原因上电瞬间芯片内部寄存器处于未知状态数据寄存器可能被初始化成最大值0xFFFFFF。而0xFFFFFF恰好又是负满量程表示极易被误读为「传感器信号为负满量程」。解决初始化序列完成后主动执行一次伪读取把数据寄存器的初始值清空。也就是在初始化函数末尾加一行ad7124_read_data()丢弃结果。同理应该丢弃复位后第一次转换结果因为PGA和滤波器需要几个转换周期才能稳定。5.5 接地和参考电压导致的通道间串扰现象两路差分输入信号完全独立但通道1的信号变化会引起通道0读数微小波动尤其在PGA增益设得比较大时。原因PCB布局上模拟地、数字地没有做单点连接或者ADR121参考源的位置离ADC太远导致共阻抗耦合。AD7124的PGA在高增益下对电源和地噪声非常敏感通道切换瞬间内部采样电容的电荷注入会通过输入引脚耦合。解决PCB上把模拟电源、参考源和AD7124的AGND放在同一个区域数字接口走线远离模拟输入。PGA增益设定超过32时建议每路输入对地加一个100nF电容滤掉高频噪声。这是硬件问题不是软件能完全弥补的。6. 二次开发把固定通道扫描改成灵活可配置的驱动层做了这几个项目后我逐渐意识到驱动代码最怕写死。第一版示例里的通道选择直接拿通道号当作AIN正端和AIN负端这在通道数量少、差分对固定时够用但一旦传感器接线方式变了就得改驱动源码重新编译非常不方便。一个靠谱的AD7124驱动应该支持运行时配置通道映射平台侧传入「正端引脚号、负端引脚号、PGA增益」三元组驱动根据查表生成对应的通道寄存器值。改造思路是引入一个通道配置表typedef struct { uint8_t ain_pos; // 正输入端号0-15 uint8_t ain_neg; // 负输入端号0-15 uint8_t pga_gain; // 增益档位对应AD7124_PGA_1至AD7124_PGA_128 uint8_t enable; // 是否使能 } ad7124_ch_cfg_t; static const ad7124_ch_cfg_t g_ch_cfg[AD7124_MAX_CH] { {0, 1, AD7124_PGA_1, 1}, // 通道0AIN0-AIN1增益1 {2, 3, AD7124_PGA_128, 1}, // 通道1AIN2-AIN3增益128例如接热电偶 // 新增通道只需在这里加条目 };初始化时遍历这张表把每条配置转换成对应的寄存器写入通道切换时用查表代替直接算。这样做的好处是更换传感器或者调整量程时只改配置表不改驱动逻辑。如果你熟悉设备树或者配置文件的思路这就是一个最简单的驱动配置化改造。另一个高频需求是加入均值滤波或滑动滤波。ADC原始码值在工业现场多少有噪声尤其是PGA增益开到128时LSB级别的跳动很常见。我一般会在驱动层放两个可选函数接口一个是快速滑动窗口取平均窗口大小做成可配置另一个是一阶滞后滤波参数alpha可调。放驱动层而不是应用层的原因是滤波必须在物理量换算之前做否则中间过程的浮点计算会把噪声放得更大。改造后的驱动接口建议保持一致——初始化、等待就绪、读数据、读多通道这四个函数不变。新增的功能通过配置结构体和附加的滤波参数传递别改已有函数的签名否则所有调用方代码都得跟着改。如果你在前一个版本上接过自己的业务代码这个习惯能帮你省掉大量重构工作。关于验证方法我的习惯是拿到新板子先不做完整应用只跑一段最小验证代码初始化后用万用表量一个已知电压源对比读回来的码值是否落在理论误差范围内。比如给AIN0-AIN1接一个恒定的1.25V电压PGA1、参考2.5V理论上码值应该是约8388608的中间位置附近。如果偏差超过0.5%先查接线和参考电压再查SPI时序最后才怀疑PCB信号完整性。这套排查顺序从第一次调试AD7124到现在没有一次让问题逃过这一轮检查。从那以后我每做一款新的ADC驱动都强制走一遍同样的流程先打通最小通信链路再验证固定通道读数最后才做多通道扫描和滤波处理。按这个节奏来你踩坑的概率会小很多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑