
STM32做高精度数据采集ADC选型上AD7606基本是绕不开的一个器件。8通道、16bit、同步采样、±5V/±10V可切换输入再加上1MΩ输入阻抗无论是做电力监测、振动采集还是电池检测都能一版搞定。很多人在F407VET6这类板卡上第一反应是用并口去读AD7606因为网上资料一搜就是“GPIO高速并口读取”的教程。但实际项目里并口接线多、引脚冲突大、抗干扰差SPI通信优化之后不仅4根线就能搞定DMA加上中断效率反而更高。这篇就把我在工程里用SPI模式驱动AD7606、优化采集效率的过程完整拆开从时序、配置、代码到排查踩坑一步一步说清楚。1. 整体设计与方案选择为什么放弃并口改用SPI1.1 并口的痛点是引脚和布局不是速度先聊一个很多人没想清楚的问题AD7606原本就是为并口设计的并行读取一次能拿两个字节速度上确实有先天优势。但你要看使用场景。高精度数据采集项目里ADC之后往往还要接SD卡存储、屏幕显示、上位机通信、按键输入F407VET6虽然有100个引脚真到了画板子的时候16根数据线DB0~DB15加上片选、读信号、复位、转换触发30个脚就出去了。画完布局你就发现这板子的模拟区和数字区基本没法分开布线数据线走线稍微长一点串扰和地弹就够你调一晚上。如果你只是做毕设或者学习验证并口确实粗暴直接代码也简单。可一旦进入实际产品或者长时间采集的项目引脚就是成本布线就是可靠性。SPI模式把数据线压缩到一根DOUTA加上SCK、CS、CONVST、BUSY5根线解决全部通信整整省下一大片IO区域留给其他外设去用。1.2 SPI模式的真实能力够用而且比想象中强很多资料说SPI慢、并口快这句话放在AD7606上得打折扣。AD7606最大吞吐率在200kSPS级别也就是8个通道同步采样时整个芯片每秒最多完成20万次转换。并行接口的优势在于一次读取能搬16位数据出去但前提是你的MCU总线时钟跟得上。STM32F407主频168MHzGPIO读一个16位并口数据要几条指令换算下来读取本身的延迟其实非常小。问题出在软件上你用轮询读并口每读一个字节都要判断状态、给RD信号打脉冲这些操作全部占用CPU。SPI看起来是串行读一个通道16个bit要16个时钟但在F407上SPI1挂在APB2总线最高42MHz我用21MHz跑读8通道总共128bit理论上一次完整读取只需要6.1微秒左右。AD7606转换时间约4微秒也就是说从触发转换到拿完8个通道的数据整体周期在10微秒量级对应100kSPS的采样率是完全够的。这个速度对于绝大多数工业采集场景已经是余量充足了。1.3 方案对比不同读取方式的实际差异项目并行接口阻塞式SPISPI DMA引脚占用约25~30个GPIOSCK/MISO/CS/CONVST/BUSY共5个同左8通道读取耗时极短但软件开销高128bit / 21MHz ≈ 6.1us同左但DMA搬运不占用CPUCPU参与程度每个字节都要软件参与读取期间全程阻塞等待启动后CPU干别的完成中断再处理布线难度16根线需要处理等长和串扰4根信号线随便走同左调试难度逻辑分析仪要抓十几根线抓4根线一目了然多一个DMA中断流程要理清2. AD7606串行时序与STM32 SPI外设配置2.1 串行模式先看硬件引脚配置先说硬件层。AD7606支持并行、串行、字节接口三种模式具体由硬件引脚决定不是软件寄存器切换的。串行模式下PAR/SEL/BYTE这几个模式选择脚要按数据手册接成固定电平然后你只需要把DCLK接到STM32的SPI SCKDOUTA接到MISO片选CS接到普通GPIOBUSY接到外部中断输入CONVST接定时器的PWM输出或者普通GPIO触发。我遇到过有同学说“我配置了SPI怎么读出来全是0”排查半天最后发现模式选择引脚电平接错AD7606根本没工作在串行模式。这一点务必在画原理图的时候就确认清楚别等到调软件再怀疑人生。2.2 串行读取时序的关键节奏AD7606串行模式读取流程不算复杂但有几个节奏点必须卡准。第一个节奏是CONVST上升沿这个边沿触发所有通道同步采样随后BUSY引脚拉高表示芯片正在转换。第二个节奏是BUSY下降沿代表转换完成数据已经锁存到输出寄存器。第三个节奏是RD也就是SPI读操作拉低CS然后在DCLK的驱动下每个通道按顺序移出16bit数据V1到V8依次排好8个通道全部读完再把CS拉高。有个容易犯的错是CS必须等BUSY拉低之后再拉低并且CS在整个8通道读取过程中要一直保持低电平中间不能抬起来。你如果拿STM32硬件NSS去接CS配置不好容易在每个16bit传输结束就自动释放片选AD7606输出中断数据立刻错位。我的做法是CS用普通GPIO软件拉低后连续读8个16位读完全部再拉高稳得很。2.3 极性相位的选择CPOL0、CPHA1才是正解这一步是无数人踩坑的重灾区。AD7606的数据手册时序图里DCLK空闲时是低电平数据在每个时钟的下降沿从DOUTA输出然后主机在时钟上升沿去采样。对应STM32 SPI外设的参数就是hspi1.Init.CLKPolarity SPI_POLARITY_LOW; // CPOL 0空闲低 hspi1.Init.CLKPhase SPI_PHASE_2EDGE; // CPHA 1第二边沿采样为什么不是CPHA0因为CPHA0模式下主机在第一个时钟边沿采样而AD7606的数据严格来说是在DCLK下降沿之后才稳定输出采样点如果落在数据刚切换的窗口里高速时钟下很容易采到不稳定电平表现出来就是偶发错数、通道串数据。CPHA1把采样点放到第二个边沿数据已经稳定输出了一段时间裕量充足。这个组合我实测在21MHz下非常可靠。2.4 时钟频率不是越高越好SPI时钟频率的选择要结合PCB布线质量来定。F407的SPI1最高可以到42MHzAD7606手册给的DCLK上限也足够高但工程上不能只看芯片极限。DCLK频率越高对走线长度、回路面积、上下拉阻抗就越敏感。我一开始图快直接上42MHz结果数据偶发错误用逻辑分析仪一抓发现MISO上的边沿已经出现振铃。降到21MHz之后整条链路余量立刻大了很多。从时间预算上看21MHz读完整帧需要6.1微秒配合4微秒转换时间依然能做到10微秒一个周期完全满足100kSPS的采样需求。如果非要上200kSPS再考虑42MHz但建议提前做信号完整性测试。3. 实操代码中断 DMA 的高效采集实现3.1 初始化顺序和关键配置我使用STM32CubeMX加HAL库做底层初始化实际工程里配置内容如下。GPIO部分要保证SCK、MISO复用为AF5CS、CONVST设为推挽输出BUSY设为下降沿中断。// 伪代码具体引脚按你的板子调整 GPIO_InitTypeDef GPIO_Init {0}; // SCK PA5, MISO PA6 GPIO_Init.Pin GPIO_PIN_5 | GPIO_PIN_6; GPIO_Init.Mode GPIO_MODE_AF_PP; GPIO_Init.Pull GPIO_NOPULL; GPIO_Init.Speed GPIO_SPEED_FREQ_VERY_HIGH; GPIO_Init.Alternate GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, GPIO_Init); // CS PB0, CONVST PB1 GPIO_Init.Pin GPIO_PIN_0 | GPIO_PIN_1; GPIO_Init.Mode GPIO_MODE_OUTPUT_PP; GPIO_Init.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_Init); // BUSY PA0下降沿中断 GPIO_Init.Pin GPIO_PIN_0; GPIO_Init.Mode GPIO_MODE_IT_FALLING; GPIO_Init.Pull GPIO_NOPULL; HAL_GPIO_Init(GPIOA, GPIO_Init); HAL_NVIC_SetPriority(EXTI0_IRQn, 5, 0); HAL_NVIC_EnableIRQ(EXTI0_IRQn);SPI1初始化关键参数就是上一节说的时钟极性和相位另外要把NSS设成软件管理数据长度设16位MSB先行。DMA方面F407的SPI1接收通道对应DMA2 Stream0 Channel3发送通道对应DMA2 Stream3 Channel3这块务必对着参考手册查不同型号的DMA映射表差异很大。3.2 单次采集模式作为兜底方案优化之前最好先写一版简单阻塞读取用来验证硬件链路。流程就是拉低CONVST再拉高等待BUSY从高变低然后拉低CS用HAL_SPI_Receive读8个uint16_t读完拉高CS。void AD7606_ReadOnce(uint16_t *buf) { // 触发一次转换 HAL_GPIO_WritePin(CONVST_PORT, CONVST_PIN, GPIO_PIN_RESET); delay_us(1); HAL_GPIO_WritePin(CONVST_PORT, CONVST_PIN, GPIO_PIN_SET); while (HAL_GPIO_ReadPin(BUSY_PORT, BUSY_PIN) ! GPIO_PIN_RESET); HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); uint16_t dummy[8] {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF}; HAL_SPI_TransmitReceive(hspi1, dummy, buf, 8, 100); HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET); }注意HAL_SPI_TransmitReceive的size参数当SPI数据宽度配置为16位时这里的8代表8个uint16_t不是8个字节。很多人就是在这里把size写成16导致实际读出了16个通道后面的数据全是重复或错位的。阻塞版本跑通之后再上DMA优化这样做的好处是万一后续DMA数据不对你能快速确认是DMA配置问题还是数据时序问题。3.3 提升效率的核心BUSY中断启动DMA读取高效采集的核心思路是把CPU从“等待”和“搬运”中解放出来。BUSY引脚接外部中断转换完成之后硬件自动进入中断在中断里启动一次SPI接收DMA读取8个通道读完以后进入DMA完成中断在那里处理数据。这样CPU在转换等待和SPI搬运期间完全不需要空转。直接看代码uint16_t adc_rx_buf[8]; uint16_t adc_tx_buf[8] {0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF}; void EXTI0_IRQHandler(void) { if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) ! RESET) { __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0); // CS拉低然后启动DMA搬运主机通过发送0xFFFF产生DCLK时钟 HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET); HAL_SPI_TransmitReceive_DMA(hspi1, adc_tx_buf, adc_rx_buf, 8); } } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET); // 在这里处理 adc_rx_buf例如换算电压、送环形缓冲、上传上位机 DataReadyFlag 1; } }这里有一个细节使用HAL_SPI_TransmitReceive_DMA时发送方向必须给一个数据缓冲内容随意这里赋值0xFFFF目的是让主机持续产生DCLK时钟从机AD7606才会在MISO上把数据移出来。全双工模式下你不会真的用到发送数据但这个DMA必须开启否则主模式不产生时钟。DMA正常模式下每次传输完CS拉高下一轮BUSY中断再触发一次。这种模式由于读取期间完全不占用CPU实际一帧数据从BUSY下降沿到DMA完成时间就是6.1微秒左右CPU只在完成中断里做一点点数据处理。3.4 双缓冲机制应对连续采样如果采样率要求比较高或者数据处理耗时比较长建议做双缓冲。也就是准备两个接收数组DMA交替使用避免出现“上一帧还没处理完下一帧数据已经把缓冲覆盖了”的问题。利用DMA的半传输中断和传输完成中断可以完美实现双缓冲。前提是把DMA配置成循环模式并开启半传输中断。当DMA传输完前半部分数据时进入HalfCplt回调处理缓冲区A当传输完整个buff时进入Cplt回调处理缓冲区B。两个缓冲区交替被CPU读取SPI那边永远不会停。uint16_t adc_rx_buf_A[8]; uint16_t adc_rx_buf_B[8]; void HAL_SPI_TxRxHalfCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { ProcessADCBuffer(adc_rx_buf_A); } } void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { ProcessADCBuffer(adc_rx_buf_B); } }不过要提醒一句AD7606不是那种能连续输出数据流的移位寄存器它每完成一次转换数据在输出寄存器里等你去读读完CS拉高它又等在那边。所以如果你的采集需求是“连续不断采样”正确的做法是让定时器PWM按要求触发CONVSTBUSY中断打开DMA去读DMA完成后再让定时器触发下一轮。这样一环扣一环数据才是真正连续有效的。3.5 电压换算与基本滤波读出来的原始数据是uint16_t但AD7606输出的是二进制补码要先把类型强转成int16_t再做电压换算。以±10V量程为例满量程16位有符号数范围是-32768到32767对应-10V到10V所以换算公式是float voltage (float)((int16_t)raw_value) * 10.0f / 32768.0f;如果选了±5V量程把10.0f换成5.0f就行。这个换算看起来简单但很多人把uint16_t直接乘系数结果负电压全部变成大正数找半天找不到原因。数据处理这一块如果只是关心幅值可以加一个简单的滑动平均。但如果你要拿数据做FFT或者控制环路滤波要慎重相位延迟和幅值衰减都会影响结果。工业现场采集我一般先不加数字滤波先把原始数据可靠采上来再说滤波是后端的事情。4. 实测对比与性能数据4.1 我在实际项目里的测试条件测试平台是STM32F407VET6SPI1时钟21MHzAD7606工作在串行模式量程±10V8通道全开BUSY接PA0CS和CONVST用PB0、PB1输入信号由信号发生器提供。对比对象是同一块板子上的软件模拟SPI读取和阻塞式硬件SPI读取。软件模拟SPI我写的是GPIO翻转DCLK、逐bit读MISO的版本时钟频率大概跑到2MHz就不稳定了再快就有丢数据风险。阻塞式硬件SPI就是前面提到的HAL_SPI_TransmitReceive不启用DMACPU全程等待。优化后版本是BUSY中断DMA读取。4.2 采样率和CPU占用对比实现方式稳定采样率上限CPU占用情况8通道数据完整性GPIO软件模拟SPI约5kSPS极高几乎全程在翻转IO长时间运行偶发丢帧阻塞式硬件SPI约30kSPS读取期间CPU完全等待基本稳定硬件SPI DMA约100kSPS每帧只在中处理数据连续运行2小时无丢帧软件模拟SPI之所以慢不只是GPIO翻转速度的问题每次读bit之间还要判断电平、移位、存储一个bit就要好几条指令和硬件SPI完全不是一个量级。阻塞式硬件SPI快很多但读取期间CPU没法干别的一旦你还要同时刷屏、存储、通信采样率就上不去。用了DMA以后我从BUSY下降沿触发到DMA完成实测时间约6到7微秒主循环里几乎感觉不到采集的存在。做FFT采集时把采样率定在51.2kSPS连续跑了两小时采了370多万帧数据一帧都没有丢。这个稳定性对振动监测和电网录波项目来说已经非常够用了。4.3 优化前后波形质量和稳定性优化之后不仅效率提升了信号质量也有改善。软件模拟SPI受中断影响比较大主循环里一旦有UART发送或者SD卡操作挤占了时间DCLK波形就会出现毛刺采样间隔不均匀FFT频谱上会出现明显的杂散。改用定时器触发CONVST、DMA读取之后采样间隔完全由硬件定时器决定抖动降到微秒以内频谱分析结果干净很多。我之前调试一个电力项目需要对工频信号做高次谐波分析采样率要求每个周期采128点也就是50Hz基波对应6.4kSPS按理说不高。但60多次谐波算下来对采样时刻的精准度要求极高。软件模拟SPI那版做出来的频谱图上谐波旁边总是有一圈旁瓣换了SPIDMA方案之后旁瓣直接下去了十几个dB。不夸张地说通信方式的选择直接决定了这套采集系统的上限。5. 踩坑实录与排查技巧5.1 常见问题速查表现象大概率原因解决方法读回来全是0x0000MISO接错、AD7606没进入串行模式、SPI未使能查模式引脚电平查MISO先跑阻塞版验证读回来全是0xFFFFSPI时钟极性和相位不对、MISO被外部拉高检查CPOL/CPHA用逻辑分析仪抓边沿通道数据串位CS中途被释放、size参数以字节为单位CS用GPIO拉低保持整个读取过程size写8第1个通道数据偶尔错CS拉低后第一个DCLK来得太快CS拉低后加一小段延时再启动DMABUSY中断偶尔丢中断里做了耗时操作、优先级不够中断里只启动DMA数据全部放完成回调采样率提升后偶发乱码DCLK太快信号完整性差降频到21MHz检查布线长度和地回路负电压读数变成大正数uint16_t没有转int16_t换算前先强转int16_t5.2 丢BUSY中断的排查思路这类问题最容易让人心态爆炸因为不是每帧都丢而是几百帧丢一次数据偶尔跳一个点。我处理过一次比较典型的案例最后定位到原因是我在BUSY中断里顺手做了一个浮点运算把上一帧数据换算成了电压值并保存。浮点运算在Cortex-M4核上虽然快但涉及寄存器保存和流水线刷新中断服务时间从几百纳秒涨到几微秒一旦和下一帧BUSY下降沿撞上中断就会被覆盖。排查这种偶发问题建议先用一个GPIO翻转来看实际中断响应时间然后在BUSY中断入口加一个计数器正常情况和异常情况对比。如果计数器显示中断事件确实存在但没进处理函数说明中断被优先级更高的事务打断或者响应太慢。解决办法就是把中断服务时间压缩到极致采集完成回调里只置标志位和启动DMA浮点换算放到主循环或者低优先级回调里做。5.3 数据错位问题的经典场景通道数据错位也就是V1的数据跑到V3、V4的位置这类问题在SPI读取AD7606时太常见了。最典型的场景就是CS提前拉高了。AD7606要求CS在整个读取期间保持低电平但如果你用硬件NSS并且SPI配置成每次传输结束自动释放NSS那读一个通道就释放一次CS数据自然全乱。解决办法我已经强调过CS用软件GPIO控制读取前拉低整个8通道全部DMA完成后再拉高。另外还有一种错位是逻辑时序看起来都对但第一个通道的16bit数据少了一拍。原因是CS拉低后立刻开始给DCLKAD7606的第一位数据还没准备好。你可以在CS拉低后加一个极短的延比如几条NOP指令或者一个空循环再启动SPI传输问题就会消失。这个延时不需要很长几百纳秒就够但对可靠性影响很大。5.4 高精度采集的供电和基准别忽略通信优化做得再好模拟前端不行数据精度一样崩。AD7606是单5V供电但引脚设计很讲究模拟供电AVCC和数字接口供电VDRIVE可以分开VDRIVE电压决定了输出逻辑电平接STM32的3.3V时VDRIVE也要接3.3V别直接把VDRIVE接到5V否则GPIO可能过压。基准电压这一块AD7606内部有基准REFIN/REFOUT引脚上的退耦电容要靠近芯片放置我习惯用2.2uF陶瓷电容加0.1uF高频电容并联。如果项目里用了外部基准外部基准的噪声直接影响最后几位的稳定性别省这个成本。电源建议用低噪声LDO单独供电至少别让ADC和电机驱动、继电器共用一路电源。5.5 排查工具怎么用效率最高调SPI通信问题示波器和逻辑分析仪是必需品。调试流程我建议按这个顺序来先看CONVST有没有上升沿再看BUSY有没有高低变化然后用逻辑分析仪抓CS、SCK、MISO三条线确认CS低电平期间MISO上有8组16bit数据。如果MISO上根本没有数据波形多半是AD7606模式脚配错了如果数据波形存在但读出来不对再去看SCK的边沿对应关系。抓数据波形的时候注意逻辑分析仪采样率至少要比DCLK高10倍21MHz的DCLK建议用200MHz以上的逻辑分析仪否则恢复出来的波形边沿位置不准。我遇到过有人拿24MHz采样率的逻辑分析仪去抓21MHz的SPI出来的波形全是锯齿找不到问题也正常。6. 我最后的建议高精度数据采集做久了你会发现通信优化这件事本质上不是把某一段程序改得多快而是让硬件外设去承担最机械的操作。CPU真正值钱的地方是处理算法和逻辑不是在那里翻转GPIO等数据。STM32F407的SPI和DMA能力其实很强AD7606的串行模式也不是妥协方案两者配合好了效率和稳定性都能超出预期。如果你是从并口迁移到SPI方案一开始会有点不适应总觉得串行读数据不踏实。但我的实际体会是只要把时序核对清楚把CS和BUSY两个关键信号控制好SPIDMA这套方案完全可以作为长期稳定的主力采集通道用。我手头好几个采集项目都是这个架构跑了好几年也没出过通信层面的问题。你按这个思路调一次基本就能感受到硬件协作带来的收益了。