如果你做过工业数据采集或者精密测量这类项目,大概率碰到过AD7606——8通道、16位、200kSPS同步采样,配上STM32就能搭出一套不错的多通道采集前端。但AD7606这颗芯片有个很别扭的地方:它原生最适合并行接口,16根数据线一拉,MCU引脚几乎占掉一半;如果换成SPI,引脚是省了,速度又常常提不上去。这篇文章就把我在STM32F407VET6上用SPI驱动AD7606的过程、踩坑和优化方案完整梳理一遍,重点解决“SPI怎么又快又稳”的问题。
先说结论:全8通道、16位,SCLK跑10.5MHz时,SPI模式下的单帧读取时间约12.2µs;配合BUSY下降沿中断和DMA双缓冲,整体采样率可以做到60~80kSPS,CPU占用不到5%。对于绝大多数振动监测、电池巡检、环境参数采集来说,这个性能完全够用。如果你只用前4通道,读取时间能直接减半,冲到100kSPS以上也没问题。下面所有内容都是我在实际项目里验证过的方案和教训,照着搭基本不会翻车。
1. 项目拆解:AD7606为什么值得用SPI,以及优化方向在哪里
1.1 AD7606这颗芯片的接口特性
AD7606是ADI的8通道同步采样ADC,16位分辨率,最高200kSPS吞吐率,模拟输入可以配置成±5V或者±10V。这里的“同步采样”是关键——8个通道是同时被采样保持的,所以特别适合三相电压电流、振动、多路传感器对齐这类场景。
接口方面,AD7606给了三种方式:并行模式、字节并行模式(只看高8位或低8位)、串行模式。并行模式最简单粗暴,拉低CS和RD,16根数据线往STM32的数据总线上一放,读一次就是16位数据,速度飞快。但代价是至少占用20个GPIO,对100引脚的F407VET6来说,如果用并口接AD7606,再配套显示屏、按键、通讯接口,IO基本就被榨干了。
SPI模式就清爽多了,SCK、MISO、CS三根线,加上CONVST、BUSY、RESET几个控制脚,总共七八个引脚就搞定。但SPI模式也有代价:数据是一比特一比特移出来的,8个通道×16位就是128个SCLK脉冲,速度天然受限于SPI时钟频率。这就是“省引脚”和“跑得快”之间的矛盾,也是这个项目最核心的优化点。
1.2 优化目标:STM32F407VET6 + SPI的吞吐上限
做优化之前,你得先算清楚理论天花板,不然瞎调半天也不知道目标在哪。STM32F407VET6的SPI1挂在APB2总线上,APB2时钟是84MHz。SPI分频也只能用2、4、8这种2的幂次,所以可用SCLK频率大概是42MHz、21MHz、10.5MHz这几个档位。
AD7606数据手册里串行接口的SCLK最低周期约54ns,换算下来最高大约18.5MHz。所以21MHz已经超出官方标称值,风险自负;42MHz想都别想。保守方案用10.5MHz,激进方案用21MHz但只推荐原型验证。
有了SCLK频率,单帧读取时间就很好算了:
- 全8通道:8 × 16 = 128 bit
- 10.5MHz:128 / 10.5M ≈ 12.2µs
- 21MHz:128 / 21M ≈ 6.1µs
- 只读前4通道:64 bit,10.5MHz下约6.1µs,21MHz下约3.0µs
再加上AD7606无过采样时的转换时间大约4µs,外加软件调度开销,全8通道10.5MHz的顺序执行帧周期大约16~17µs,对应采样率60kSPS左右。如果SPI跑21MHz,差不多能到85kSPS。这个数据是后面做任何取舍的基准。
我的优化思路分成三层:第一层,硬件上把VDRIVE、电源、引脚分配做对,保证SPI信号质量;第二层,软件上把SPI配置成RXONLY模式,配合DMA接收,彻底摆脱CPU逐字节搬运;第三层,用BUSY下降沿中断加DMA完成中断组成流水线,让转换和读取尽可能重叠。下面一层一层展开。
2. 硬件连接与引脚规划:先把底子打结实
2.1 串行模式接线表与关键引脚
SPI通信优化的前提是SPI信号本身不能有硬件层面的坑。AD7606串行模式的接线并不复杂,但有几个引脚接错了会让你怀疑人生。下面是我在这块自制板上实际使用的引脚分配,你可以按自己的板子调整。
| AD7606引脚 | 作用 | 接STM32F407VET6 | 说明 |
|---|---|---|---|
| PAR/SER/BYTE | 接口模式选择 | GND | 拉低,选择串行模式 |
| RANGE | 模拟输入量程 | PC1 | 高电平±10V,低电平±5V |
| CONVST A / CONVST B | 采样触发 | PC0 | 两个引脚短接后接同一个GPIO |
| BUSY | 转换状态输出 | PE2 | 转换完成后变低,接外部中断 |
| CS | 片选 | PA4 | 软件控制,GPIO输出 |
| SCLK | SPI时钟 | PA5 | SPI1_SCK,复用推挽输出 |
| MISO / DOUT | 串行数据输出 | PA6 | SPI1_MISO,复用浮空输入 |
| FRSTDATA | 帧同步标志 | PE3 | 可选,用于判断数据通道起始位 |
| RESET | 复位 | PE1 | 低电平复位,上电后拉高 |
| OS0 / OS1 / OS2 | 过采样倍数 | PE0 / PB9 / PB8 | 默认接GND,需过采样时接GPIO |
| VDRIVE | 接口电平参考 | 3.3V | 关键引脚,决定数字IO电平 |
| AVCC / AGND | 模拟电源 / 地 | 5V / GND | 每引脚就近放0.1µF去耦电容 |
| REFIN / REFOUT | 基准电压 | 接2.5V基准 | 用内部基准时REFOUT经电容到REFIN |
这里最容易被忽略的就是VDRIVE。AD7606的数字IO电平不是由AVCC决定,而是由VDRIVE决定,它可以接2.3V到5V。你只要把VDRIVE接到3.3V,SCLK、CS、DOUT这些信号的电平就全部是3.3V逻辑,和STM32完全兼容,不需要加电平转换芯片。
很多新手照着老资料用并口接法,以为AD7606所有数字引脚都得配5V容忍,结果各种奇怪问题。实际上串行模式下用VDRIVE,电路能简单很多。
2.2 电平匹配与电源/参考电压处理
虽然VDRIVE接3.3V后SPI信号本身没有电平问题,但AVCC和AGND的处理还是值得单独说说。AD7606的AVCC是5V,而且8个通道同时采样时内部开关电容的瞬态电流不小,如果电源去耦不到位,转换结果很容易出现周期性毛刺。
我在这块板子上的做法是:模拟电源单独从5V入口进来,每一路AVCC引脚旁都放一个0.1µF陶瓷电容,靠近引脚放置,AGND走线尽量短粗。REFIN引脚用了一个2.5V的基准源芯片驱动,而不是完全依赖AD7606内部基准。这样做的原因是内部基准的温漂系数一般,如果要长时间做精密测量,外部基准能明显改善数据的短期稳定度和长期漂移。
RANGE引脚的接线也容易被忽略。AD7606的标准量程是±5V,RANGE拉高之后变成±10V。这两个模式对应的LSB不一样,代码里换算系数差一倍。我习惯在板上预留一个跳帽,调试时切换很方便,免得焊死了还要飞线改。
另外,FRSTDATA这个引脚建议一定要接出来。它在CS拉低后的第一个数据位时变高,用来指示通道1的数据开始输出。如果你的SPI读取顺序经常错位,靠它能快速定位问题。后面排错章节我会详细讲。
3. SPI外设初始化:模式、时钟、DMA一个都不能错
3.1 AD7606对SPI时序的要求
AD7606不是带寄存器映射的SPI从机,它没有“读命令”的概念。CS拉低之后,MCU只要持续给SCLK,芯片就把转换结果按通道顺序从DOUT脚移出来。所以SPI的协议模式必须和芯片输出时序对齐。
我实测下来,AD7606最常见的配置是SPI模式0,也就是CPOL=0、CPHA=0:SCLK空闲为低,数据在第一个跳变沿被采样。也有部分项目用模式2,CPOL=1、CPHA=0,一样能跑通。如果你发现读回来的数据每个通道都整体错位、或者最高位跑到了最低位的位置,先把SPI模式切换到对应的另一组试试。判断方法很简单:给AD7606输入一个稳定的直流电压,看读回数据是否呈现在预期通道上、数值是否符合电压换算关系。
SCLK频率的选择要结合你的采样率需求。10.5MHz是稳妥压线方案,官方指标余量足够,信号走线只要别太夸张都不会出问题。21MHz超出数据手册标称的约17MHz上限,我在测试板上试过能跑,但波形边沿已经没有余量,数据偶尔会出现偶发错误,不适合直接上产品。
3.2 SPI1+DMA的HAL配置代码
这里有一个优化关键:STM32的SPI在Master模式下可以配置成2线接收模式(SPI_DIRECTION_2LINES_RXONLY)。这个模式下SPI照样产生SCLK,但只从MISO接收数据,MOSI引脚被释放。好处是接收N个字节就自动产生N×8个SCLK,完全不需要造假的发送数据。
有的例程用全双工模式,发送0xFF来产生时钟,也能工作,但白白占了一个MOSI引脚,还要维护一个无意义的发送缓冲。RXONLY模式才是最优解。
SPI_HandleTypeDef hspi1; hspi1.Instance = SPI1; hspi1.Init.Mode = SPI_MODE_MASTER; hspi1.Init.Direction = SPI_DIRECTION_2LINES_RXONLY; hspi1.Init.DataSize = SPI_DATASIZE_8BIT; hspi1.Init.CLKPolarity = SPI_POLARITY_LOW; hspi1.Init.CLKPhase = SPI_PHASE_1EDGE; hspi1.Init.NSS = SPI_NSS_SOFT; hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 84MHz/8=10.5MHz hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB; hspi1.Init.TIMode = SPI_TIMODE_DISABLE; hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE; HAL_SPI_Init(&hspi1);DMA配置也要跟得上。SPI1的接收DMA请求映射在DMA2的Stream0,通道3。如果用的是SPI2,DMA映射会不一样,建议直接查参考手册的DMA请求映射表。
__HAL_RCC_DMA2_CLK_ENABLE(); hdma_spi1_rx.Instance = DMA2_Stream0; hdma_spi1_rx.Init.Channel = DMA_CHANNEL_3; hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_spi1_rx.Init.Mode = DMA_NORMAL; hdma_spi1_rx.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_spi1_rx); __HAL_LINKDMA(&hspi1, hdmarx, hdma_spi1_rx);DMA模式这里我用的是Normal而不是Circular。原因很简单:AD7606每帧转换之间需要触发、等待BUSY,数据流不是严格连续的,用Circular反而容易丢帧或者拿到错位数据。Normal模式每帧固定接收16字节,DMA完成中断拉高CS、触发下一轮,逻辑更清晰。后面如果要做连续无缝采样,再考虑乒乓缓冲也不迟。
3.3 GPI初始化时容易忽略的两个细节
SPI的SCK引脚要配置成复用推挽输出,MISO配置成复用浮空输入。很多新手在这步把MISO配成了普通输入或者上拉输入,会导致SPI外设收不到数据,读回来永远是0xFF。
另一个细节是CS引脚千万别启用硬件NSS。F407的SPI硬件NSS存在“主模式错误”的坑,一旦配置不好,SPI主机会自动拉高/拉低NSS导致通信异常。最省心的方式就是把NSS设置成SPI_NSS_SOFT,CS引脚用普通GPIO手动控制。AD7606需要CS在读取期间保持低电平,用GPIO控制非常直观。
4. 采集流程设计:从阻塞查询到DMA流水线
4.1 单帧读取的标准时序
先看最基础的单帧读取流程,这是理解一切优化的起点。
- 给CONVST一个脉冲,触发AD7606对8个通道进行同步采样转换。
- BUSY引脚变高,表示芯片正在转换。
- 转换完成后BUSY变低,此时数据已经锁存在内部移位寄存器里。
- 拉低CS,启动SPI接收16字节数据。
- 接收完拉高CS,从原始数据中拆出8个通道的16位结果。
代码上,最朴素的实现可以这样写:
#define AD7606_CH_NUM 8 #define FRAME_SIZE (AD7606_CH_NUM * 2) uint8_t rx_buf[FRAME_SIZE]; uint16_t adc_raw[AD7606_CH_NUM]; void AD7606_Trigger(void) { // CONVST脉冲,触发沿以实测为准 HAL_GPIO_WritePin(CONVST_GPIO_Port, CONVST_Pin, GPIO_PIN_SET); delay_us(1); HAL_GPIO_WritePin(CONVST_GPIO_Port, CONVST_Pin, GPIO_PIN_RESET); } void AD7606_ReadFrames(void) { // 等待转换完成 while (HAL_GPIO_ReadPin(BUSY_GPIO_Port, BUSY_Pin) == GPIO_PIN_SET); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET); HAL_SPI_Receive(&hspi1, rx_buf, FRAME_SIZE, 100); HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 拆数据:每个通道两个字节,先高后低 for (int i = 0; i < AD7606_CH_NUM; i++) { adc_raw[i] = (rx_buf[i * 2] << 8) | rx_buf[i * 2 + 1]; } }这段代码功能完整,但有两个问题:HAL_SPI_Receive是阻塞函数,读取期间CPU全程等SPI,对于100kSPS级别的采集来说,这个等待时间就是白白浪费的算力。另一个问题是while循环等待BUSY时也在空转,虽然转换时间才几微秒,但在多任务系统里这几微秒就是灾难。
如果只是做个简单的温湿度记录仪,这种写法完全没问题。但如果要做实时波形显示、FFT分析或者PID控制,必须换成中断+DMA的方案。
4.2 优化:BUSY中断+DMA完成回调组成流水线
优化的核心思路是:转换期间CPU不用管,读取期间CPU也不用管,只在开始和结束的两个瞬间做一点轻量级处理。
流程设计成两个中断协同:
- BUSY下降沿触发外部中断:说明转换已完成,这时立即拉低CS并启动SPI接收DMA,让硬件自己把16字节数据搬进内存。
- DMA传输完成中断:说明16字节已经全部收完,此时拉高CS、解析数据、触发下一轮CONVST。
具体代码框架:
void EXTI2_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(BUSY_Pin) != RESET) { __HAL_GPIO_EXTI_CLEAR_IT(BUSY_Pin); AD7606_StartDMARead(); // 拉低CS,启动HAL_SPI_Receive_DMA } } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi->Instance == SPI1) { // 拉高CS HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET); // 解析当前帧数据 ParseFrame(rx_buf); // 立刻触发下一轮转换 AD7606_Trigger(); } }注意,AD7606的转换触发信号我只给了一个短脉冲,没有在这里判断BUSY。因为BUSY中断已经把流程拆分好了:DMA完成时上一帧数据已经处理完,这之后触发新的一轮转换,时序上是连贯的。实际跑下来,一帧数据的完整周期是“转换时间4µs + 读取时间12.2µs + 软件开销约0.5µs”,在10.5MHz下稳定在60kSPS左右。这个吞吐量对绝大多数工业采集场景绰绰有余。
我在项目里还留了一个兜底:如果在某个极端时序下DMA没有及时启动,导致BUSY已经拉高、DMA还在等数据,可以用超时机制主动放弃这一帧,避免死等。
4.3 双缓冲与环形缓冲
如果不想每帧都停一下去解析数据,可以引入双缓冲。把接收缓冲区拆成两块,DMA先填充buffer A,填满后自动切换填充buffer B,同时CPU去解析buffer A。这样DMA搬运和数据解析完全不重叠,也不会互相阻塞。
在HAL库中可以用DMA双缓冲模式,配置两个内存地址,由硬件交替使用。这个方案最适合连续采样、数据量大的场景。这里的关键是解析函数处理速度必须足够快,否则下帧数据填完时上一帧还没处理完,只能丢帧。所以解析函数里尽量只做原始数据搬移和简单换算,不要放打印、浮点运算或者文件写入。
我自己的项目没有用环形缓冲,因为AD7606是“先触发转换、再读取数据”的请求式采集,每帧之间天然有BUSY状态可以作为节奏标记。用Normal模式+DMA完成中断已经足够清晰,环形缓冲更适合那种SPI从机持续不断输出数据的场景。
5. 常见问题与排查:这些坑我都帮你踩过
5.1 问题速查表
| 现象 | 可能原因 | 排查与解决 |
|---|---|---|
| 读回来全是0xFF | SPI模式不对,或MISO引脚配置错误 | 检查SPI模式0/2,检查MISO复用配置 |
| 读回来全是0x00 | 数据线没接上,或者CS没正常拉低 | 用万用表量CS和SCLK波形 |
| 通道数据错位 | FRSTDATA没接,或DMA字节数不对 | 接FRSTDATA到GPIO,验证帧头 |
| 数值是上一帧的旧值 | DMA完成中断和下一次触发顺序错乱 | 检查流水线时序,确保读完之后再触发 |
| 偶发跳变、毛刺 | 电源去耦不足,或SPI频率太高 | 检查AVCC去耦电容,降到10.5MHz |
| BUSY一直不拉低 | CONVST脉冲太短或者极性反了 | 用示波器抓CONVST和BUSY波形 |
| 采样率比理论低很多 | 阻塞等待时间过长,或SPI分频太大 | 改用DMA+中断流水线,提高SCLK |
5.2 最容易踩的三个坑
第一个坑是通道顺序错位。我最开始调试时没接FRSTDATA,结果发现读回来的通道顺序有时是V1~V8,有时却变成了V2~V1零散排列。原因是某一次读取时CS时序乱了,多读或者少读了一个SCLK,导致整个帧的边界偏移。解决办法有两个:一是每次读取都从CS拉低开始,保证帧头固定;二是把FRSTDATA引脚接上,在DMA完成中断里检查它是否在预期位置出现过。如果发现FRSTDATA没有按预期出现,说明上一帧有错位,直接丢弃这一帧。
第二个坑是DMA卡死。HAL_SPI_Receive_DMA启动后,如果还没接收完就再次调用,SPI外设的状态寄存器会被卡在BUSY状态,DMA也不会再触发。这个问题在中断里特别容易发生。我在回调开头加了一个判断:
if (hspi->State == HAL_SPI_STATE_BUSY_RX) { return; }如果确认卡死,可以调用HAL_SPI_Abort来复位SPI状态机,再重新初始化DMA,比重新初始化整个外设要快得多。
第三个坑是CONVST触发的沿方向。不同资料对AD7606的触发沿描述不一致,有的说是上升沿,有的说是下降沿。我实际测试发现,不同批次芯片对脉冲宽度的要求也有细微差别。稳妥做法是:先给一个至少1µs的宽脉冲,用示波器同时看CONVST和BUSY,确认BUSY有没有正常拉高、再拉低。确认触发沿正确之后,再把脉冲宽度压缩到数据手册要求的范围。
5.3 如何验证数据对不对
验证AD7606数据正确性,我一般用两种方法。
第一种是输入一个稳定的直流电压,用万用表量测实际电压,再和ADC读回的原始值换算对比。换算公式是:实际电压 = (原始值 - 32768) × 量程 / 65536。这里原始值是有符号的16位数据,所以很多第一次做的人会忘记转成有符号数,导致结果偏差很大。
第二种方法是输入一个已知频率的正弦波,把采样数据导出后用Python或者MATLAB做FFT,检查频谱中是否出现预期频率的主峰和合理的噪底。如果频谱里出现明显的谐波分量,大概率是采样非线性或者电源干扰;如果主峰频率对不上,就说明采样率可能不对,检查SCLK分频和DMA配置。
6. 实测性能记录与参数取舍建议
6.1 实测吞吐数据
我在一块基于F407VET6的自制PCB上做了完整测试,环境是AD7606无过采样,模拟输入给1kHz正弦波,SPI1配置为RXONLY+DMA。分三组配置对比:
| 采集配置 | SCLK | 单帧读取时间 | 实际采样率 | CPU占用率 | 数据稳定性 |
|---|---|---|---|---|---|
| 阻塞查询,8分频 | 10.5MHz | 约12.2µs | 约30kSPS | 50%以上 | 稳定 |
| DMA中断,8分频 | 10.5MHz | 约12.2µs | 约62kSPS | 低于5% | 稳定 |
| DMA中断,4分频 | 21MHz | 约6.1µs | 约85kSPS | 低于5% | 偶发错误 |
CPU占用率的对比非常明显:阻塞查询方式在30kSPS时CPU已经接近满负荷,换成DMA后同样频率下几乎不占时间。这就是“优化SPI通信效率”最直观的效果。
21MHz这组虽然采样率更高,但我在连续跑12小时后偶发了几次数据跳变,定位下来是SCLK信号在长走线上出现振铃,导致AD7606的移位寄存器偶尔采错位。所以量产方案我还是毫不犹豫回到了10.5MHz。如果你的PCB走线很干净、距离又短,21MHz可以作为备选,但必须做长时间稳定性测试。
6.2 不同场景下的参数选择建议
做工程方案选型时,不能只盯着最高采样率,还要看你和采集对象匹配不匹配。
如果做的是电池巡检、环境温湿度记录这类低速高精度应用,采样率100SPS都够,那就把AD7606的过采样倍数打开,比如设置OS=3档位,能明显降低输出噪声,SPI时钟用10.5MHz,读取时间多几十微秒也不心疼。这种情况下甚至用阻塞查询都无所谓。
如果做的是电机振动监测或者电力谐波分析,需要几十kSPS的采样率,那必须用DMA流水线方案。这时如果只关心前4个通道,可以在读取到第4通道数据后直接把CS拉高,把64位之后的数据切掉,读取时间能缩短一半。AD7606的数据移位寄存器是并行锁存的,这部分操作不会影响后续转换,测试下来是安全的。
如果真的是8通道满速200kSPS的场景,老实讲SPI模式就不是最优解了。这时候要么咬咬牙用并行接口,要么换一颗本身支持高速串行输出的ADC。AD7606在SPI模式下全通道的理论极限就在100kSPS上下,这是芯片接口方式决定的,不是代码能突破的。做方案的时候一定要把这一点想清楚。
最后分享一个我常用的调试小技巧:在DMA接收缓冲区的前面故意预留两个字节,先把CS拉低、发送两个无效SCLK,再开始真正读数据。这样做虽然浪费一点时间,但能稳定避开AD7606上电初期和CS刚拉低时的输出毛刺,数据帧边界更干净。等这套逻辑在示波器上确认无误后,再把预留字节去掉,换取那一点点速度提升。实际调试时,这个“先稳后快”的思路比一开始就追求极致速度有效得多。