ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

STM32硬件SPI驱动AD7124多通道热电偶采集实战解析

STM32硬件SPI驱动AD7124多通道热电偶采集实战解析 简介面向STM32与ADI AD7124开发者的硬件SPI多通道采集驱动工程适用于工业控制、医疗器械、精密测量等需要高精度多路模拟前端采集的场景。工程基于Keil MDK5开发主控为STM32F103C8T6采用SPI2接口未启用AD7124同步模式相比旧版重点优化了采样频率、PGA为一时正负两伏附近信号失真问题、主程序架构以及时钟速率使AD7124最大读取速率提升至一点一二五兆赫兹并支持多个通道同时读取数据。压缩包共一百四十五个文件大小约三兆字节以C源文件、H头文件及Keil工程配置文件为主另有编译生成的AXF、HEX等固件输出可直接打开工程或烧录验证便于对照源码进行学习。代码模块划分清晰main.c文件精简AD7124寄存器配置、SPI读写、多通道轮询与PGA增益调整逻辑均经过调试并针对大信号采集失真给出了有效的处理思路方便二次开发和移植。目前已有1201人学习适合具备一定嵌入式驱动基础、希望基于硬件SPI快速搭建AD7124多通道采集系统的工程师参考也可作为高精度模拟前端驱动开发的排错范本。 前阵子调一款四路热电偶采集板主控是STM32F103ADC选了ADI的AD7124-4通信方式直接上硬件SPI。这个芯片的寄存器逻辑和普通SPI ADC不太一样尤其是多通道读取如果只照搬单通道例程大概率会遇到读回来的数据永远停留在某一通道、轮询顺序错乱、甚至整个SPI总线上设备都无法访问等问题。这篇文章就把我调通的完整思路整理出来重点放在硬件SPI底层的读写字序、AD7124多通道配置和实际调试中踩过的坑。1. 项目方案为什么用硬件SPI控制AD7124多通道读取1.1 AD7124解决哪些采集痛点项目原始需求是采集四路K型热电偶环境温度变化不快但对分辨率有一定要求希望温度显示到0.1℃。选型时对比了半天最后落到AD7124-4上原因很简单它把24位Σ-Δ ADC、内部PGA、基准源、激励电流都集成在一颗芯片里外围只加几个电容和电阻就能跑起来。AD7124-4支持4路差分输入或者7路单端输入AD7124-8支持到8路差分/15路单端。做多路温度、压力、应变片这类传感器采集非常合适。内部PGA可以放大1/2/4/8/16/32/64/128倍意味着小信号不需要额外加运放。内部基准温漂能做到很低虽然比不上外部基准但常规工业采集完全够用。我手里这块板子正好还留了一路预留输入切到单端模式也能扩展所以灵活性对我来说很重要。还有一个实际优势是输出速率可编程从1.17 SPS一直到9600 SPS配合不同的数字滤波模式可以平衡速度和噪声。热电偶本身属于慢变信号我最后设置在25 SPS左右噪声表现已经很理想。1.2 硬件SPI和软件片选怎么搭配这个项目的关键词就是“硬件SPI”不是拿GPIO模拟SCLK那种软件SPI。软件SPI最大的问题是占用CPU太狠尤其是多通道连续采样时一边要翻转时钟一边还要处理ADC数据中间一旦被中断打断时序就会抖动严重时数据错位。硬件SPI靠外设自动移位移位CPU只要把要发的字节塞给SPI数据寄存器就行再配合DMA几乎不占CPU资源。不过要注意一点硬件SPI不等于硬件CS也必须用SPI外设的NSS引脚。我实测下来AD7124的CS信号必须在一个完整通信帧期间全程拉低命令字节和数据字节之间不能断开。STM32的硬件NSS在自动模式下经常发生CS跳变时机不好控的情况所以我直接把CS配成普通GPIO输出软件拉高拉低。这样看起来多写了一行但时序可控性高很多。方案优点缺点软件SPI引脚随便选代码简单占用CPU速度慢易受中断干扰硬件SPI GPIO片选时钟又快又稳片选可控需要多占用一个GPIO口硬件SPI 硬件NSS少占用GPIO片选时序经常不匹配不推荐接口上用STM32的SPI2SPI模式是Mode 3也就是CPOL1、CPHA1。AD7124数据手册里兼容两种模式用Mode 1也可以我习惯统一用Mode 3这样和很多SPI Flash、SD卡模块都能共用一套初始化代码。2. AD7124多通道读取的寄存器准备2.1 通信命令帧所有寄存器访问都从这一字节开始AD7124和普通SPI ADC不一样它不是直接读某个寄存器地址就能返回数据每次访问都得先发一个命令字节告诉芯片接下来是要写还是要读访问哪个寄存器传多少个字节。这个命令字节格式我总结成一句话[7] 位R/W1表示读0表示写 [6:2] 位寄存器地址5位地址 [1:0] 位数据长度编码001字节012字节103字节114字节所以写一个寄存器的命令就是cmd (reg 2) | (len - 1);读一个寄存器的命令就是cmd 0x80 | (reg 2) | (len - 1);比如读DATA寄存器寄存器地址是0x03读3字节命令就是0x80 | (0x03 2) | 2 0x8E。这段对很多人来说是比较容易懵的地方因为大部分寄存器的“地址”并不是直接放到一个地址总线上而是嵌在命令字节里的。我建议把写寄存器和读寄存器封装成独立函数不要在业务代码里到处拼命令字节否则很容易因为一个字节位错导致整个通信崩掉。后面第3节会给完整代码。2.2 多通道需要配置的关键寄存器表AD7124看起来寄存器很多但做多通道读取时真正要打交道的就下面这几类。寄存器地址作用STATUS0x01转换状态、通道号、错误标志ADC_CTRL0x02操作模式、内部基准、数据状态输出设置DATA0x0324位转换结果CONFIG0~CONFIG30x08~0x0B每个setup的PGA、基准源、单/双极性FILTER0~FILTER30x0C~0x0F滤波方式、输出速率CH0~CH150x14起通道使能、正负输入选择、关联setup重点说下通道寄存器。AD7124的每个通道寄存器都是16位里面主要三块内容使能位、正负模拟输入选择、还有关联到哪个setup。正负输入的选择不是简单写个数字就行它内部是用位段表示的。我项目里做了这样的宏定义#define AD7124_CH_ENABLE (1U 15) #define AD7124_CH_SETUP(n) (((n) 0x3U) 13) #define AD7124_CH_AINP(n) (((n) 0x0FU) 5) #define AD7124_CH_AINM(n) (((n) 0x0FU) 1)比如通道0配置成AIN0正输入、AIN1负输入并绑定setup 0那么通道寄存器值就是uint16_t ch0 AD7124_CH_ENABLE | AD7124_CH_SETUP(0) | AD7124_CH_AINP(0) | AD7124_CH_AINM(1);这段宏在不同版本的数据手册里位偏移可能略有差异用之前一定对着手册把位表核对一遍尤其如果你是从老项目移植过来的代码。2.3 单次转换和连续扫描多通道到底选哪个AD7124的多通道操作有两种典型用法这个决定驱动怎么设计。第一种是连续转换模式。ADC开启后会一直对使能的所有通道轮询转换每次转换完成结果刷新到DATA寄存器里并拉低DOUT/RDY脚。问题在于芯片只有一个DATA寄存器CS拉低读数据时你并不知道读到的究竟是哪一路除非在ADC_CTRL里开启“数据状态”功能让读出来的数据后面带一个状态字节里面包含当前通道号。第二种是单次转换模式。每次写控制寄存器触发转换ADC对使能通道完成后会进入待机。如果只使能一个通道结果很干净但想读多路就得通道切换、触发、等待、读取循环执行。我在这块采集板里最终采用“单次转换模式 逐通道轮询”因为热电偶信号变化慢每通道25 SPS已经足够轮询方式逻辑最简单不会出现通道错位。如果项目要求比较高的多通道吞吐再考虑连续转换模式配合数据状态功能。3. 硬件SPI驱动实现3.1 SPI初始化与引脚分配我用STM32CubeMX生成基础代码SPI2配置如下void MX_SPI2_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_32; hspi2.Init.FirstBit SPI_FIRSTBIT_MSB; HAL_SPI_Init(hspi2); }时钟分频选了32分频。STM32F103的APB1时钟是36 MHz32分频后SPI时钟大概是1.125 MHzAD7124完全承受得住而且这个速度下信号完整性也容易保证。如果板子走线很短可以加速到64分频或128分频但我更看重稳定。GPIO部分要注意SCK、MISO、MOSI三个引脚要复用成SPI功能CS引脚配成普通推挽输出。GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin GPIO_PIN_13 | GPIO_PIN_14 | GPIO_PIN_15; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); GPIO_InitStruct.Pin GPIO_PIN_12; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET);3.2 SPI读写函数封装底层的读写函数是整个驱动的地基。写寄存器时先拉低CS发命令字节再发数据字节最后拉高CS。读寄存器时先拉低CS发命令字节然后用全双工收发的方式收数据字节。static void ad7124_spi_write_reg(uint8_t reg, uint8_t len, const uint8_t *data) { uint8_t cmd (reg 2) | (len - 1); AD7124_CS_LOW(); HAL_SPI_Transmit(hspi2, cmd, 1, 100); if (len 0) { HAL_SPI_Transmit(hspi2, (uint8_t *)data, len, 100); } AD7124_CS_HIGH(); } static void ad7124_spi_read_reg(uint8_t reg, uint8_t len, uint8_t *data) { uint8_t cmd 0x80U | (reg 2) | (len - 1); uint8_t dummy[4] {0}; AD7124_CS_LOW(); HAL_SPI_Transmit(hspi2, cmd, 1, 100); HAL_SPI_TransmitReceive(hspi2, dummy, data, len, 100); AD7124_CS_HIGH(); }读数据时我用HAL_SPI_TransmitReceive而不是HAL_SPI_Receive因为前者连续产生SCLK读取过程中MOSI一直发送dummy字节实际AD7124只关心SCLK采样这个操作最稳定。这组函数有个小坑传入的len不能为0否则len-1会变成0xFF命令字节直接错了。所以我在调用层保证len最小是1。3.3 上电复位和写读校验AD7124上电后最好先复位一次避免寄存器处于未知状态。复位的方式是连续给64个SCLK高电平也就是向SPI发8个0xFF字节。void ad7124_reset(void) { uint8_t reset_seq[8] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; AD7124_CS_LOW(); HAL_SPI_Transmit(hspi2, reset_seq, 8, 100); AD7124_CS_HIGH(); HAL_Delay(10); }复位后第一件事就是读设备ID寄存器确认SPI通路正常。AD7124的ID寄存器地址不是固定的不同版本手册略有差异拿到芯片后先看数据手册确认。如果ID读出来完全不对大概率不是芯片坏了而是命令字节拼错或者SPI模式不对。4. 多通道读取的完整代码与数据换算4.1 逐通道轮询读取实现下面这套代码是我的多通道读取版核心逻辑。先把所有通道寄存器清掉保证每次只有一个通道处于使能状态然后在循环里逐个配置、触发、等待、读取。#define CH_COUNT 4 static uint16_t ch_config[CH_COUNT] { AD7124_CH_ENABLE | AD7124_CH_SETUP(0) | AD7124_CH_AINP(0) | AD7124_CH_AINM(1), AD7124_CH_ENABLE | AD7124_CH_SETUP(0) | AD7124_CH_AINP(2) | AD7124_CH_AINM(3), AD7124_CH_ENABLE | AD7124_CH_SETUP(0) | AD7124_CH_AINP(4) | AD7124_CH_AINM(5), AD7124_CH_ENABLE | AD7124_CH_SETUP(0) | AD7124_CH_AINP(6) | AD7124_CH_AINM(7), }; void ad7124_read_all_channels(uint32_t *values) { uint8_t off[2] {0x00, 0x00}; uint8_t ch_buf[2]; uint8_t data[3]; /* 先全部关闭通道 */ for (int i 0; i CH_COUNT; i) { ad7124_spi_write_reg(AD7124_REG_CH0 i, 2, off); } for (int i 0; i CH_COUNT; i) { ch_buf[0] ch_config[i] 8; ch_buf[1] ch_config[i] 0xFF; ad7124_spi_write_reg(AD7124_REG_CH0 i, 2, ch_buf); /* 写ADC_CTRL触发单次转换具体模式位根据手册配置 */ uint16_t adc_ctrl AD7124_SINGLE_CONVERSION_MODE; uint8_t ctrl_buf[2] {adc_ctrl 8, adc_ctrl 0xFF}; ad7124_spi_write_reg(AD7124_REG_ADC_CTRL, 2, ctrl_buf); /* 等待DOUT/RDY拉低 */ while (HAL_GPIO_ReadPin(AD7124_DOUT_GPIO_Port, AD7124_DOUT_Pin) ! GPIO_PIN_RESET) { } ad7124_spi_read_reg(AD7124_REG_DATA, 3, data); values[i] ((uint32_t)data[0] 16) | ((uint32_t)data[1] 8) | (uint32_t)data[2]; } }这里有两个很容易漏的细节。第一循环前必须先关闭所有通道寄存器不然上一轮开启的通道还是使能状态再写当前通道时就会有多通道同时转换读出来的数据顺序完全乱掉。第二触发单次转换后必须等DOUT/RDY脚拉低千万不能立刻读DATA寄存器否则转换还没结束。4.2 连续扫描加数据状态位的进阶玩法如果你不想逐通道切换想直接在连续转换模式下扫描多个通道那一定要开启ADC_CTRL的数据状态功能。开启之后读DATA寄存器时读到的就不只是24位数据而是4个字节前3字节是转换结果最后1字节是状态信息里面包含通道号和错误标志。/* 使能所有需要扫描的通道后开启连续转换模式 */ for (int i 0; i CH_COUNT; i) { /* 写对应通道寄存器 */ } /* 等DOUT/RDY下降沿然后读4字节 */ uint8_t buf[4]; ad7124_spi_read_reg(AD7124_REG_DATA, 4, buf); uint8_t channel_id buf[3] 0x0F; /* 具体位段以手册为准 */这样做的好处是CPU不必在转换过程中频繁切换通道寄存器只要每次DOUT/RDY拉低就进来收一次数据。但你必须通过状态字节判断当前是哪一路否则高速采样时很容易出现通道错位。多通道连续扫描还有一种隐藏问题每个通道的转换结果速率会被平分如果单个通道配置25 SPS4个通道扫描时每路实际只有6.25 SPS设计时要把这个因素算进去。4.3 数据换算和采样率损失读取出来的24位原始值需要转换成实际电压。如果配置的是双极性模式数据格式是二进制补码代码要处理负数int32_t raw_signed; if (data[0] 0x80) { raw_signed (int32_t)(((uint32_t)data[0] 16) | ((uint32_t)data[1] 8) | (uint32_t)data[2]) - (1L 24); } else { raw_signed (int32_t)(((uint32_t)data[0] 16) | ((uint32_t)data[1] 8) | (uint32_t)data[2]); }双极性信号的实际电压计算公式是voltage raw_signed * (Vref / PGA) / 8388608.0;Vref是基准电压我板子上用的是内部基准2.5V增益设为1所以满量程就是±2.5V。换算后如果要给热电偶查表再把电压乘上冷端补偿系数就可以了。如果是单极性模式原始值从0到16777216对应0到Vref/PGA直接线性换算。多通道轮询时每通道的采样率等于你触发一次循环的周期除以通道数比如固定每100ms触发一轮4通道就是每路25 SPS这个节奏对慢变传感器很合适。5. 多通道调试踩坑实录5.1 寄存器读写失败先查这几个地方我把调试中遇到最多的现象整理成了速查表现象排查方向上电后读ID全FFSPI模式错、CS电平反了、SCK/MOSI/MISO接线反读ID全00MISO线没通、VDD没上、芯片处于某种复位状态写寄存器后读回还是旧值命令字节长度编码错误或CS在命令和传输之间被拉高DOUT/RDY一直高没有触发转换或触发后还在转换就进循环等待尤其要注意CS信号。有些逻辑分析仪看着CS波形没问题但实际命令字节和数据字节之间断开了AD7124内部通信状态机会错乱。解决办法是写寄存器时在同一个CS低电平窗口内连续发完所有字节不要在中间调用任何可能耗时的函数。5.2 数据错乱和通道串扰的处理多通道读取最常见的坑就是读到两个通道的数据重复。我之前调试时发现CH0和CH2读出来完全一样查了半天发现是通道寄存器没有及时关闭两个通道同时在转换数据寄存器被后转换的通道刷新了。后来改成每通道只在需要时使能读完后立即关闭问题立刻消失。还有一个容易被忽略的是PGA和基准源设置。多个通道绑定同一个setup没问题但如果你给不同通道绑定了不同setup一定要把每个setup的PGA、基准、极性都配置正确否则切换到某个通道时会读到一个异常倍率。比如A通道PGA1B通道PGA8切换后忘记更新B通道的PGA数据会差8倍。5.3 调试辅助技巧调这种SPI ADC我强烈建议用逻辑分析仪看SPI波形不要只看软件打印结果。一条MOSI命令字节跟几字节数据一眼就能看出命令拼接问题。如果只是临时验证也可以把读出来的ID寄存器值和手册比对。AD7124看起来寄存器很多但驱动只要把底层读写封装好剩下的其实就是按位填channel、config、filter这几个表。另外一点建议多通道轮询不要在主循环里用阻塞方式等DOUT/RDY最好放到定时器中断或者用外部中断触发读取否则主任务会被ADC拖死。我后来的做法是开一个50ms软件定时器到点后启动一次多通道轮询数据放入环形缓冲区主任务只做滤波和协议上报整体稳定很多。写完这段话的时候板子上四路采集已经连续跑了两个星期每通道数据都很稳定偶发毛刺也都是传感器本身质量问题。AD7124这个驱动底层SPI框架搭好了后面换成AD7124-8或者扩展更多通道只是改通道寄存器配置表的问题不用再动通信层。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进