
前阵子做一套基于STM32F407VET6的多通道同步数据采集板核心就是AD7606这块8通道16位ADC。最初的方案是打算直接走并行总线毕竟AD7606的并行方式最直接数据线一拉RD信号一给数据就出来了。但布线的时候越看越头疼16根数据线加上控制线整块板子一大半引脚都被占走后面还想挂一个SPI Flash和一块LCDIO根本不够分。后来咬咬牙把接口方式改成SPI串行读取这才把板子救回来。真正做完才发现SPI方式不是不行关键看你怎么优化调好了采集效率完全够用而且省下来的IO能让你做很多原本不敢想的事。这篇就把我在STM32F407上通过SPI方式驱动AD7606、并一步步把采集效率拉高的完整思路写出来覆盖接口选型、时序原理、CubeMX配置、DMA双缓冲、以及调试时碰到的几个坑。主要面向准备用AD7606做高精度采集、又不想被并行接口拖累引脚资源的朋友。1. AD7606的接口选型为什么SPI值得花力气优化1.1 三种接口模式与硬件成本对比AD7606这颗芯片给了我三种读取方式并行模式、并行字节模式、串行模式。很多人拿到芯片后习惯性按官方框图里的并行接法来画板因为Datasheet上大量示例电路都是并行方式看起来最省心。但实际做产品或者做功能较多的板子时接口占用的引脚资源往往比采样速率更关键。先看一个对比表格接口方式所需数据引脚典型最快吞吐适合场景并行16位接口DB0~DB15共16根加RD/CS/BS等控制线可达200kSPS满速追求极限采样率、IO资源充足的板卡并行字节接口DB0~DB7共8根分高低字节两次读取需要两次RD时序速率略低于16位并行IO相对紧张但仍能接受串行SPI接口DOUTA/DOUTB/SCLK/CS最少3根数据线受SCLK频率限制通常50~90kSPSIO紧张、希望缩短走线的多通道系统这里有个容易被忽略的点串行接口并不一定只用一个DOUTA引脚。芯片的硬件上还提供了一个DOUTB引脚如果你同时使用DOUTA和DOUTB那么SCLK脉冲可以少一半。因为DOUTA负责输出通道1~4的数据DOUTB负责输出通道5~8的数据两根线各拿半帧一次SCLK跳变就能移出两位。这在实际布线和提升读取速度时非常有用后面会单独说。我这次选的是单DOUTA方式因为板子对采样率要求没有满到200kSPS60kSPS左右已经足够覆盖要采集的电压纹波信号。单线方案硬件上最简洁SCLK、CS、DOUTA三根线就能完成数据回传剩下的引脚全部释放出来给其他外设。1.2 从项目需求推导接口方案做接口选型不应该只看ADC支持什么而是先想明白自己需要什么。我的需求是这样的8通道同步采集有效分辨率尽量逼近16位单通道采样率不需要顶满200kSPS但希望所有通道同时采样、同时输出并且CPU在采集过程中还能处理其他任务比如实时刷新LCD、解析传感器数据。如果换成并行接口数据读取本身确实最快但代价是16位数据总线占用的IO几乎和MCU的另一组GPIO数量相当。而且AD7606的数据输出引脚是双向的虽然读取方向固定但布线干扰、信号翻转时的串扰都是麻烦事。更现实的问题是一片STM32F407一共才100多个引脚时钟、调试、电源、ADC、DAC、串口一安排留给并行数据总线的位置非常紧张。SPI方式的价值就在于用时间换空间。你只需要三根到四根线哪怕跑不到200kSPS但只要满16位精度稳定很多测试场景的底层指标已经足够。关键是SPI是STM32的强项硬件SPI加DMA能省掉绝大部分CPU开销这和并行方式一次一次读数据、不断触发中断的玩法比起来反而在高吞吐率的情况下更省心。2. 想优化SPI首先要读透AD7606的串行时序2.1 时钟极性与相位模式0还是模式1SPI通信优化的前提是先把时序搞对。AD7606的串行接口在Datasheet上给出的做法是SCLK空闲时为低电平数据在SCLK的驱动下从DOUT引脚移出。真正操作时最稳妥的办法是用SPI模式0CPOL0、CPHA0先跑起来。也就是SCLK空闲低电平第一个时钟沿采样数据。这个配置在很多参考工程里都能看到实测下来也最稳定。为什么我会特别强调这一点因为SPI的四种模式有时候会让新手绕晕。模式0是数据在上升沿采样这在STM32CubeMX里对应的是CPOL设为Low、CPHA设为1 Edge。模式1则是在下降沿采样对应CPOLLow、CPHA2 Edge。AD7606的频率特性决定了它并不挑传输极性的相位关键是数据建立保持时间要留够。如果你的SPI配置选错相位读出来的数据可能整体偏移一位甚至全部乱掉。我调试时是这样验证的固定输入一个已知电压比如2.5V然后用不同的SPI模式去读看读回来的原始码是否稳定在0x4000附近。反复对比后发现模式0确实最可靠模式1虽然也能出数但在较高SCLK频率下偶尔会出现偶发的错码。所以我的建议是优先模式0如果你的实际波形和我的不同再用模式1验证不要盲目抄配置。2.2 BUSY、CS、FRSTDATA三者的协作关系AD7606的转换流程可以简单理解成三步CONVST引脚收到一个触发电平芯片开始对8个模拟输入通道同步采样并转换在此期间BUSY引脚被拉高转换完成后BUSY被拉低此时你才能通过CS和SCLK把数据读出来。这里BUSY引脚是判断“是否可以读取”的核心信号。如果在BUSY还是高电平的时候就拉低CS并开始送SCLK数据是不成立的。因为没有转换完输出锁存器里还是上一轮的旧数据甚至可能是混乱状态。CS和SCLK的关系也很有讲究。AD7606不是那种CS拉低后就开始自动送数据的芯片它要求CS拉低后配合SCLK边沿一位一位把数据移出。读完整帧前CS不能提前拉高否则这帧数据就废了。FRSTDATA这个引脚则用来标识整帧数据的起点。串行方式下FRSTDATA在V1通道数据被读出时是高电平之后读V2~V8时恢复低电平。如果只是闭环自用不接FRSTDATA也能跑但如果你想在出错时快速定位问题或是对接一些需要帧同步的DSP/FPGA系统FRSTDATA可以帮你确认读取起始位置是否正确。2.3 128个SCLK周期背后的转换时间约束这是整个优化方案里最核心的计算很多人没搞明白为什么AD7606用SPI方式跑不满200kSPS。原因很简单转换时间和读取时间是串行叠加的。AD7606无过采样时典型转换时间是4微秒也就是从CONVST触发到BUSY拉低大约需要4微秒。这4微秒是硬性的不管什么接口方式都省不掉。但在串行模式下读取8个通道的数据需要送满足够的SCLK时钟数。单通道16位数据8个通道就是128个SCLK脉冲。如果SCLK跑10MHz读取时间就是128除以10MHz约12.8微秒。加上转换的4微秒一个完整的采样周期最快也要16.8微秒对应采样率约59.5kSPS。这就是为什么说SPI方式跑不到200kSPS的硬性约束。如果把SCLK拉高到17MHz读取时间缩到约7.5微秒采样周期约11.5微秒采样率大概能到87kSPS。但17MHz已经是AD7606串行接口的极限附近对板级布线要求很高普通杜邦线连接根本扛不住这么高的时钟频率很容易出现码间串扰。这也是为什么优化通信方案时SCLK频率不是唯一决定因素信号完整性同样重要。我后面做的实测对比里这部分体现得很明显。3. 基于STM32F407的软硬件配置实现3.1 引脚分配与最小硬件连接我用的MCU是STM32F407VET6AD7606的串行模式引脚连接如下AD7606引脚连接目标说明PAR/SER/BYTE SEL3.3V拉高选择串行接口模式DOUTAPA6SPI1_MISO串行数据输出单线方式只接这一根SCLKPA5SPI1_SCKSPI时钟输入由STM32产生CSPD7普通GPIO软件控制片选按帧拉低/拉高BUSYPE8外部中断输入下降沿通知MCU转换完成可读取数据CONVST_A/CONVST_BPE9定时器PWM输出两引脚短接由定时器PWM上升沿触发转换RESETPD6普通GPIO复位上电后给一个高脉冲完成复位RANGEPC4普通GPIO配置模拟输入量程高电平均为±10V低电平均为±5VOS0/OS1/OS2GND过采样关闭追求最高采样率VDRIVE3.3V接口电平参考与STM32的3.3V IO电平匹配AVCC5V模拟电源注意去耦为什么CS用普通GPIO而不是SPI硬件NSS原因很简单AD7606的CS需要在整个读取帧期间保持低电平且需要和DMA传输配合CS拉低后启动DMADMA完成后拉高CS。如果用硬件NSS在SPI传输前由外设自动控制虽然也能实现但时序控制不如软件GPIO灵活特别是接收方向的数据传输你并不能确定硬件NSS拉高的精确时刻是否正好匹配AD7606的帧结束点。软控最直观也好排查。3.2 STM32CubeMX中的SPIDMA配置步骤软件部分用STM32CubeMX生成基础工程减少手写初始化代码的出错概率。打开SPI1设置为Full-Duplex Master。硬件参数里数据大小选择16 BitsCPOL选择LowCPHA选择1 Edge对应SPI Mode 0。波特率分频器选择DIV8。STM32F407的SPI1挂载在APB2总线上时钟84MHz分频8之后得到约10.5MHz的SCLK。这个频率在AD7606支持范围内布线和普通短走线下也能保持较稳定。使能SPI1的RX DMA请求在DMA设置里添加SPI1_RX通道。DMA模式选择Circular循环模式数据宽度Half Word内存地址递增。打开PE8的GPIO外部中断模式选下降沿触发。这个外部中断会作为“转换完成”信号处理器。打开TIM2配置为PWM输出通道连接到PE9频率按你需要的采样率设置。例如采样率10kSPS就把TIM2频率设为10kHz占空比先设20%。PWM的上升沿就是CONVST的触发信号。这里有一个关键细节SPI1的16位数据帧格式决定了DMA每次搬运的是16位数据那DMA缓冲区元素类型必须是uint16_t。AD7606一共8个通道一个完整读取帧16个字节换算成16位就是8个半字。DMA缓冲长度设置为8个uint16_t一次传输完成就是完整的一帧数据。3.3 驱动代码触发转换、等待BUSY、DMA读取基本流程代码可以这样写#define AD7606_FRAME_SIZE 8 uint16_t adc_raw_buf[AD7606_FRAME_SIZE]; volatile uint8_t data_ready 0; void AD7606_Init(void) { // AD7606_RESET_PIN PD6 HAL_GPIO_WritePin(GPIOD, GPIO_PIN_6, GPIO_PIN_SET); HAL_Delay(1); HAL_GPIO_WritePin(GPIOD, GPIO_PIN_6, GPIO_PIN_RESET); } void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin BUSY_PIN) { AD7606_CS_LOW(); // PD7 0 HAL_SPI_Receive_DMA(hspi1, (uint8_t *)adc_raw_buf, AD7606_FRAME_SIZE); } } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { AD7606_CS_HIGH(); // PD7 1 data_ready 1; } }代码看起来很简单核心就是“BUSY下降沿触发读取”。当定时器PWM输出上升沿到CONVST引脚后AD7606开始转换BUSY拉高。约4微秒后转换完成BUSY下降沿触发外部中断中断里拉低CS、启动DMA接收SPI外设自动产生SCLK时钟把8个通道的数据全部搬走。DMA传输完成中断里拉高CS并置位数据就绪标志主循环只需要检查标志然后做后续处理。电压换算也很直接float AD7606_ConvertVoltage(uint16_t raw, float range) { // AD7606输出为二进制补码格式 int16_t val (int16_t)raw; // range对应±10V时传20.0f±5V时传10.0f return (float)val * range / 32768.0f; }需要注意AD7606的数据格式是二进制补码不是普通无符号格式。直接用uint16_t去运算会被当成正数所以先强转成int16_t再换算。这一步很多人会忽略结果就是负电压怎么算都不对。4. 从“能用”到“高效”三个实测有效的优化手法4.1 用BUSY边沿中断消除等待浪费早期的版本我是在主循环里轮询BUSY引脚直到BUSY变低才启动读取。写起来简单但性能很差。原因在于从CONVST触发到BUSY变低中间整整4微秒CPU都在空转而且轮询周期受主循环里其他任务的影响可能你某次循环里正好被LCD刷新拖住了就不能及时响应BUSY下降沿导致采样率不稳定。改成外部中断之后BUSY下降沿一来MCU立刻暂停当前的普通任务跳进中断服务函数里把读取操作启动起来。这样CPU在工作间隙能去处理显示、按键、通信等其他任务等到转换完成的瞬间才被拉回来。这个优化对提升整体系统效率非常明显同时也让采样周期更稳定。如果你觉得外部中断还不够“硬实时”可以考虑用定时器的输入捕获通道连接到BUSY引脚。这样BUSY下降沿会被定时器硬件捕获触发一次DMA突发传输连CPU中断都不需要参与。不过这种玩法配置比较绕建议先用外部中断把功能跑通再根据需求去进阶。4.2 双缓冲DMA与定时器触发采样前面版本是单缓冲DMA每次收完一整帧后要把数据从DMA缓冲区拷贝到业务缓冲区避免下一次DMA传输把还没处理完的数据覆盖掉。这套逻辑在低速采集时没有问题但采样率往上提后拷贝动作和主循环的处理时间会产生冲突。双缓冲的配置其实非常简单把DMA模式设成Circular循环模式缓冲区长度设成16个uint16_t前半部分8个元素存储第一帧后半部分8个元素存储第二帧。DMA在半传输中断和全传输完成中断里分别置标志主循环处理当前完整的半区数据而DMA正在往另一个半区写新数据。这样数据搬运与数据处理在时间上完全错开缓冲区永远不会被覆盖。采样触发的稳定性也是一个优化点。之前用GPIO往CONVST引脚送软件脉冲脉冲间隔受主循环调度影响jitter比较大。改用定时器PWM输出固定频率到CONVST之后采样间隔由硬件决定精确稳定。AD7606是同步采样芯片8个通道的采样时刻完全一致这个特性在高精度电力监测场景里特别宝贵定时器触发能确保间隔匀称减少周期性抖动造成的影响。整个链路的效率关键点在于定时器PWM负责精准触发AD7606内部硬件负责同步转换BUSY中断负责交接DMA负责无CPU参与的数据搬运。四部分各管一段没有谁需要等谁整条流水线就顺畅了。4.3 提高SCLK时钟频率时的信号完整性权衡SCLK频率不是越高越好。我用三种方式实测过不同频率下的数据稳定性SCLK频率连接方式传输帧长采样率上限估算稳定性2.625MHz杜邦线48.8us约18.9kSPS稳定无错码10.5MHzPCB短走线12.2us约61.7kSPS稳定无错码21MHzPCB短走线6.1us约99kSPS接近极限偶发错码从表里能看出来SCLK高到21MHz时虽然读取时间大幅缩短但已经逼近AD7606的内部极限传输线效应开始出现。这个时候单独优化固件已经没有意义要从硬件层面入手缩短SCLK走线长度、加33欧姆串联电阻做阻抗匹配、减少SCLK与数据线之间的并行距离。我在实际板子上选择10.5MHz作为最终工作频率原因有两个一是这个频率下读取一帧只需要12.2微秒配合4微秒转换时间理论采样率可以达到60kSPS以上满足项目指标二是这个频率下数据线即使受到一定干扰仍能保证16位数据完全无错码。追求极限数值并不意味着系统更好能在满足指标的前提下留出余量才是工程上的合理选择。4.4 效率提升实测优化前后数据对比我把优化前后的性能变化整理成了表格方便直观感受方案CPU占用实际最高采样率数据连续性主循环轮询BUSY 软件模拟SPI时序85%~95%约15kSPS较差易受任务调度影响硬件SPI 单缓冲DMA 轮询BUSY40%~50%约35kSPS中等有拷贝开销硬件SPI DMA BUSY外部中断20%左右约55kSPS较好受主循环影响小定时器PWM触发 循环DMA双缓冲10%左右约60kSPS优秀采样间隔均匀这个对比是在STM32F407主频168MHz配置下实测得到的。前两版方案我都实际跑过尤其是第一版软件模拟SPICPU几乎被拖死只能干采集这一件事。改成硬件SPI加DMA后CPU负载明显下降整个系统的可扩展性也上来了。双缓冲的最后一步优化虽然采样率只提升了约9%但数据连续性和系统稳定性质的飞跃尤其在长时间连续采集时不会再出现丢帧。5. 调试过程中踩过的坑及完整排查链路5.1 数据错位或首通道不固定的解决办法调试初期遇到最头疼的问题是每次上电读到的数据第一个数组元素对应的不一定是V1通道。有时是V1输出有时往左偏一两个通道导致实际波形显示乱序。一开始怀疑是芯片损坏后来意识到这是帧同步问题。排查链路是这样的先用示波器同时抓BUSY和CS确认CS是否在BUSY下降沿之后才拉低。如果CS拉低的时刻偏早AD7606输出数据的位序就会乱因为转换还没完全结束输出移位寄存器里的数据不是稳定的整帧。接着我检查了SPI时钟数量。HAL_SPI_Receive_DMA的第三个参数是数据长度这里很容易写错。如果填了16意味着DMA会读16个半字相当于256个SCLK周期超出AD7606一帧的128个周期。多余的SCLK会继续移出无效数据DMA缓冲区里数据就出现错位。还有一个隐患是FRSTDATA引脚。虽然不接也能工作但如果你需要确认首通道位置建议把它接到一个空闲GPIO上。每次读取帧开始前判断FRSTDATA是否为高不是高就说明本次读取的起始位置不对可以主动丢弃或者重新读取。这样能从根源上避免错位数据进入上层算法。5.2 采样值跳变的元凶CS时序与命令帧冲突另一个典型问题是采集正弦波信号时波形明显出现离散跳点跳点幅度不是噪底级别而是整段电压突变。排查过程我排除了ADC本身问题因为用并行方式测试同一片AD7606完全没有这个现象。后来用逻辑分析仪抓SPI总线发现一个问题CS拉低的时机偶尔和SPI_SCK的第一个边沿几乎同时发生甚至CS还没完全稳定SCLK就开始跳变。这会让AD7606的第一个数据位建立时间不足最终读回的首位数据是错的。解决方式是调整顺序先把CS拉低保持一小段时间我加了几个空指令做延时再启动SPI接收。同时把SPI的通信节奏和DMA接收强绑定保证SCLK只在CS完全稳定后才产生。现实中我加了等待CS稳定和SCLK边沿之间的几个NOP延时问题就消失了。实践中如果抓波形不方便也可以用最笨的办法验证把采样率降下来在CS拉低和启动DMA之间插入一个通用定时器延时例如延时2微秒。如果跳变现象消失基本可以锁定是CS建立时间不够。5.3 DMA传输完成标志与BUSY脉冲的时序竞争把采样率往60kSPS以上推的时候又出现一个隐蔽问题偶发丢帧。现象是DMA传输完成中断不是每次都触发或者某次采样读回来的数据是上一帧的旧数据。分析下来这是采样周期和读取周期的竞争问题。AD7606的转换时间为4微秒读取时间在10.5MHz SCLK下约12.2微秒两者加起来约16.2微秒对应采样率上限约61.7kSPS。但如果定时器PWM触发的采样率过于接近甚至超过这个上限下一次BUSY下降沿到来时上一次的DMA可能还没传输完外部中断又触发了一次新的SPI接收DMA请求发生冲突。解决方案是在BUSY中断里加一个忙碌标志void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin BUSY_PIN) { if (spi_busy_flag 0) { spi_busy_flag 1; AD7606_CS_LOW(); HAL_SPI_Receive_DMA(hspi1, (uint8_t *)adc_raw_buf, AD7606_FRAME_SIZE); } // 如果上一帧还没收完这一帧直接丢弃 } } void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) { if (hspi-Instance SPI1) { AD7606_CS_HIGH(); spi_busy_flag 0; data_ready 1; } }这个标志的作用是防止DMA请求重入。如果上次传输还没完成新的BUSY下降沿到来就说明当前设置的采样率已经超出了SPI读取能力的极限必须降低采样率或者提高SCLK。合理做法是把触发采样率限制在理论值的80%以内我在最终方案里把采样率设置在48kSPS左右留出足够余量。在实际操作中这套SPI优化方案最终稳定跑在了48kSPS8通道同步采集CPU负载不到15%同时还能驱动一块LCD显示实时波形。后面如果再想往上突破可行的方向是把DOUTB也接上用双线输出方式把SCLK周期减半或者直接换并口模式。但对于绝大多数中低速高精度采集场景今天这套方案已经足够可靠而且它把STM32的IO资源释放得很彻底给系统整体设计留出了非常大的余地。