
如果只看手册AD7606的200kSPS吞吐率听起来很香但真正把8个通道同时采起来传统SPI接口很容易卡在100kSPS以下。我第一次用STM32F103的SPI读取时就碰到过这个问题程序跑得再快数据总是一帧错位、偶发毛刺后来换成STM32H7的FMC大家习惯叫FSMC并行总线同一套信号源直接冲到200kSPS。这篇实战记录就把我为什么换总线、FSMC怎么配、对比SPI到底强在哪、以及DMA双缓冲怎么配合都整理出来给要压榨AD7606的朋友参考。1. 为什么SPI模式撑不起高采样率先算清账再动手1.1 AD7606的真实工作节奏转换时间和读出时间是两笔账AD7606是8通道同步采样的16位ADC每次在CONVST引脚给出一个上升沿8个通道会同时进入采样保持并开始转换。转换期间BUSY引脚拉高转换完成之后BUSY拉低这时输出寄存器里的数据才算稳定可以读取。这个流程里最容易被忽略的是时间分配AD7606的典型转换时间大约是4us而200kSPS采样率意味着采样周期只有5us。也就是说留给“读完8个通道数据”的时间窗口只有大约1us。如果读取路径不够快下一秒CONVST又来触发下一次转换程序还在总线上去读上一帧数据光是想就知道要出事。所以看AD7606的“最高200kSPS”并不是说随便拿哪一种接口都能跑到。转换时间那4us省不掉能优化的其实只有“把8个通道的16位结果取回来”这一段。这一段从SPI换成并行总线往往就是采样率能不能翻倍的分水岭。1.2 SPI模式为什么卡在100kSPS附近SPI模式下读取8个通道意味着要连续移出8个16位数据一共128位。假如SPI时钟用20MHz那纯粹数据传输就需要128 bit / 20 MHz 6.4us加上CS拉低建立时间、SPI FIFO空余等待、中断进入退出的开销实际上一帧下来至少也要7us左右。把转换的4us和读出的6.4us加起来4us 6.4us 10.4us 理论最高采样率 1 / 10.4us ≈ 96kSPS这是理想值实测稳定值只会更低。我把SPI配到20MHz、用DMA接收最终稳定在92kSPS左右再往上调就开始出现帧错位和偶发的0x0000。就算SPI时钟能拉到25MHz理论也只有109kSPS左右还是离200kSPS差一大截。这个结论很直接SPI模式真正的瓶颈不是CPU主频而是串行传输的位宽。AD7606一次转换产生128位数据用SPI一位一位移出来时间基本不可压缩。1.3 并行总线为什么是解药STM32H7的FMC把外部SRAM一类设备映射到内存地址空间访问外部设备就像访问数组一样。对AD7606这种16位并行接口的ADC读一个通道只需要在并行数据总线上产生一次16位读事务。如果FMC配置得当一次16位读周期大约在35~40ns左右8个通道读一遍也就是300ns上下。同样一帧数据从SPI的6.4us压到0.3us留给转换的时间就非常宽裕4us 0.3us 开销 ≈ 4.4us 理论最高采样率 ≈ 227kSPS再被AD7606自身的200kSPS上限压住刚好够跑到手册标称值。这也是“采样率翻倍秘籍”最核心的账不是ADC变快了而是把被SPI浪费掉的读出时间拿回来了。2. STM32H7 FSMC总线驱动方案把AD7606变成一段外部SRAM2.1 先分清FSMC与FMC的叫法严格来说STM32H7系列上用到的控制器叫FMC全称是Flexible Memory Controller它是F1系列FSMCFlexible Static Memory Controller的延续和强化。网上很多资料说“H7的FSMC”其实指的就是FMC对外部NOR/SRAM/PSRAM的静态存储控制能力。在CubeMX里配置的时候路径是FMC - NOR/PSRAM/SRAM Controller不是叫FSMC。大家约定俗成叫FSMC也没问题但查手册时别搜错关键词。文章标题沿用FSMC是为了方便检索实际代码和初始化都是走H7的FMC外设。2.2 硬件接线不是把所有引脚都接到一起就完事AD7606并行接口脚位不少但真正参与总线读的只有三组16位数据线、3位地址线、RD和CS控制线。我的接法整理如下AD7606引脚STM32H7引脚说明DB0~DB15FMC_D0~D1516位并行数据总线A0~A2FMC_A0~A2通道地址选择RDFMC_NOEFMC读使能对应AD7606的RDCSFMC_NE1片选映射到Bank1的NE1区域CONVST定时器PWM输出产生采样触发脉冲BUSYGPIO中断引脚转换完成下降沿触发读取OS2~OS0GND关闭内部过采样跑最高速度PAR/SER/BYTE SEL并行模式电平让芯片工作在并行接口需要注意AD7606没有写时序FMC_NWE可以悬空不需要接。还有一点如果只用NE1这一个片选区域读取地址会落在0x60000000开始的Bank1区间很方便调试。我把CS接到FMC_NE1而不是直接接地是为了让地址译码参与片选。否则只要有总线访问哪怕访问的是另一个外部设备也可能把AD7606拉出来响应逻辑上容易出错。2.3 CubeMX配置步骤与几个关键参数FMC的CubeMX配置不复杂但参数填错很容易让人陷入“读出来全是0”的困境。我建议按下面顺序设置打开FMC - NOR/PSRAM/SRAM Controller。选择Bank 1使用NE1片选。Memory Type选SRAMMemory Data Width选16 bits。关闭Burst Access Mode关闭Wait Signal。计划好初始时序参数。生成代码后用调试器先读一次地址0x60000000确认RD脚有脉冲。时序参数是重点。我的内核跑480MHzHCLK配到240MHzFMC时钟也来自HCLK一个周期约4.17ns。AD7606的RD低电平脉宽手册要求至少21ns所以我最早把参数设置成FMC时序参数初始值说明AddressSetupTime2地址建立时间2个FMC时钟AddressHoldTime1地址保持时间DataSetupTime6读数据和地址稳定窗口约25nsBusTurnAroundDuration0连续读之间不需要额外总线周转这样DataSetupTime折算下来大约25ns能覆盖AD7606的最小RD脉宽同时留了一点余量。先把这一组参数跑通再慢慢往小调不要一上来就追求极限否则时序余量不足时排查起来非常痛苦。2.4 读数据代码像读变量一样读ADC配置好FMC后AD7606在内存里就是一段16位的外部SRAM。我用下面的方式定义基地址和读取函数#define AD7606_BASE_ADDR 0x60000000u #define AD7606_CHANNELS 8u static __IO uint16_t * const AD7606 (__IO uint16_t *)AD7606_BASE_ADDR; void AD7606_ReadAll(uint16_t *dst) { for (uint32_t i 0; i AD7606_CHANNELS; i) { dst[i] AD7606[i]; } }这里必须用__IO也就是volatile修饰指针否则编译器可能认为同一块地址的多次读取结果不变直接优化成只读一次后面看到“通道数据全一样”就是这么来的。地址映射上FMC的Bank1 NE1区域从0x60000000开始。16位总线模式下外部地址A0对应到内部地址加2的偏移量所以AD7606[0]读通道0AD7606[1]读通道1以此类推。不要在这个循环里做任何浮点转换先把16位原始值拿回来主循环里再统一处理。3. 对比测试怎么搭同一个信号源两条不同读取路径3.1 硬件平台与信号源测试平台我用的是一块STM32H743ZI开发板主频480MHzHCLK 240MHz外接AD7606模块。信号发生器给CH1输入1kHz、2Vpp的正弦波其余通道接GND量程设在±5V。为了方便观察采集点数设成8192点分别做时域和FFT分析。采样率发生我用定时器PWM直接输出到CONVST这样比在中断里翻转GPIO更稳定不会因为代码路径不同导致采样周期抖动。SPI方案和FSMC方案都用同一套定时器触发唯一不同的是BUSY下降沿之后的数据读取路径。3.2 SPI基线怎么测DMA接收 定时器触发SPI测试时我把SCLK拉到20MHzSPI配置成接收模式BUSY下降沿触发EXTI中断。中断回调里启动SPI的DMA接收一次收8个16位数据void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin BUSY_Pin) { HAL_SPI_Receive_DMA(hspi1, (uint8_t *)spiBuf, 8); } }这里8是指8个半字不是8个字节。HAL里Size的单位是“数据项个数”配置成16位数据宽度后就按半字算。实测下来这套SPI方案在90kSPS附近很稳定超过95kSPS就开始碰运气。为了验证我特意把采样率设到200kSPS跑一次。结果很典型BUSY下降沿到来后还剩1us的时间窗口但SPI读128位需要6.4us于是下一次CONVST已经触发上一帧还没读完输出的数据从某个通道开始错位波形上能看到明显的断裂和毛刺。3.3 FSMC测试流程用同一套中断/DMA框架换总线FSMC方案里我把SPI的接收替换成AD7606_ReadAll同样在BUSY下降沿触发。为了测量真正的读取耗时我在读取函数前后各翻转一次GPIO用逻辑分析仪看这个GPIO的脉宽void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin BUSY_Pin) { TEST_PIN_GPIO_PORT-BSRR TEST_PIN_PIN; AD7606_ReadAll(fsmcBuf); TEST_PIN_GPIO_PORT-BRR TEST_PIN_PIN; } }在FSMC初始参数为“AddressSetupTime2DataSetupTime6”时测得8个通道读取总耗时约0.35us。相比SPI的6.4us差不多只有原来二十分之一。就算加上中断进出的开销200kSPS的5us采样周期也完全足够。有一个测试纪律必须遵守中断回调里绝对不要做printf、浮点运算、FFT这类重活。哪怕只是加一个printf耗时都可能超过几毫秒采样周期全被打乱对比结果也彻底失真。数据采集和数据处理一定要分层。4. 实测数据采样率到底提升多少4.1 最大稳定采样率对比我分别用SPI和FSMC跑了多组测试取能稳定连续采集且不丢不偏的最大采样率作为结果对比项传统SPI方案FSMC并行方案总线参数SCLK20MHzFMC DataSetup6 cycles8通道数据读取耗时约6.4us约0.35us理论最大采样率约96kSPS高于200kSPS实测稳定采样率92kSPS200kSPS强行跑到200kSPS失败帧错位正常“翻倍”这个说法是成立的从SPI稳定值92kSPS左右到FSMC稳定值200kSPS采样率刚好翻了一倍多。但要说清楚这个翻倍是有效吞吐率的提升不是模拟带宽的直接提升。AD7606本身能采多快还是由芯片决定的只是FSMC终于把它的上限释放出来了。4.2 1kHz正弦波的波形质量与FFT表现在200kSPS采样率下1kHz正弦波每个周期依然有200个采样点还原出来的波形非常平滑没有被“读出太慢”这种问题破坏。做FFT观察频谱基频分量清晰底噪水平和手册给出的典型SINAD在同一量级。为了做对照我也把SPI方案强行开到200kSPS。结果频谱里底噪明显抬高并且出现了一个低频包络分量。原因是串行读取来不及完成导致不同通道的转换结果混在不同帧里同一帧的8个通道其实不是同一个时刻采到的数据。这在同步采集场景里是很严重的问题。测试配置观察结果SPI 90kSPS 1kHz正弦波形正常SINAD约77dBFSMC 200kSPS 1kHz正弦波形正常SINAD约77dB与前者基本一致SPI 200kSPS 1kHz正弦波形毛刺明显底噪抬升约15dB出现包络FSMC没有引入额外噪声这一点很关键。我原本担心并行总线读会增加数字噪声但实际测试中SINAD并没有显著变差。只要电源和地处理得好16位数据线同时翻转带进来的噪声和SPI串行读相比并没有想象中严重。4.3 CPU占用和实时性收益除了采样率FSMC方案的另一个优势是CPU占用率大幅下降。以阻塞读取方式计算SPI在90kSPS时一帧读取耗时6.4us采样周期11.1usCPU光是读数据就占了约58%。FSMC在200kSPS时一帧读取耗时0.35us采样周期5us读取占比只有7%。这个数据说明FSMC方案不仅让采样率翻倍还让CPU多出了大量时间去做数据处理、存储或者人机交互。如果项目里还需要做FFT、PID或者图形UI这一项优势会直接决定系统是否流畅。5. 让200kSPS真正落地DMA双缓冲与BUSY信号配合5.1 为什么读得快还不够用BUSY下降沿当采样完成信号FSMC速度快不代表可以乱读。AD7606是8通道同步采样转换结束后8个通道的输出寄存器才同时锁存有效数据。正确流程是先用CONVST触发转换再等BUSY下降沿之后一次性把8个通道读回来。如果不看BUSY就往地址总线发读请求很可能读到的是上一次转换的旧数据甚至是转换过程中的中间值。我在测试时把BUSY下降沿配置成外部中断所有读取动作都从这个中断回调发起。这样能保证每次读取的都是一次完整的、同步的采样帧。在200kSPS下采样周期5usBUSY下降沿一般发生在CONVST后约4us处留给读取的时间只有1us左右。虽然FSMC读8个通道只要0.35us但这段窗口依然很窄任何中断里串入的延时都可能让下一次CONVST提前到来。所以到了这个阶段喂给CPU的活越少越好DMA就该上场了。5.2 DMA memory-to-memory搬运FMC区域当源地址STM32H7的DMA支持memory-to-memory传输。FMC设备在内存里有了地址DMA完全可以把0x60000000当作源地址把8个半字搬运到普通SRAM数组里。每次源地址递增一个半字FMC总线上就会产生一次外部读时序。从效果上看和CPU循环读一模一样但CPU只需要在传输完成时收到一个中断。DMA配置要点有三条源地址要开递增目的地址要开递增数据宽度设成半字。源地址加递增这个点最容易被漏掉。如果源地址固定不变DMA会在同一个地址连续读8次也就是只会反复读通道0。启动DMA的代码放在BUSY中断回调里#define AD7606_FRAME_SIZE 8u static volatile uint16_t dmaBuf[2][AD7606_FRAME_SIZE]; static volatile uint8_t dmaReadIdx 0; void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) { if (GPIO_Pin BUSY_Pin) { dmaReadIdx ^ 1U; HAL_DMA_Start_IT(hdma_memtomem_dma2_stream0, (uint32_t)AD7606_BASE_ADDR, (uint32_t)dmaBuf[dmaReadIdx], AD7606_FRAME_SIZE); } } void DMA2_Stream0_IRQHandler(void) { HAL_DMA_IRQHandler(hdma_memtomem_dma2_stream0); }CubeMX里配置DMA2的Stream0时选择Memory-to-Memory模式把Source Address Increment和Destination Address Increment都打开Data Width设成Half Word。传输完成后主循环里处理dmaBuf当前帧而下一帧已经开始采集互不干扰。5.3 双缓冲实测效果零丢点双缓冲的价值在于“读写分离”DMA写入其中一个缓冲区时CPU可以去处理另一个缓冲区。我跑连续采集100万点时没有出现丢帧和数据交叉。DMA传输本身只需要0.35us余下的时间足够做通道换算、滤波甚至实时FFT的一部分。如果想进一步降低中断频率STM32H7的DMAMUX还能把定时器更新事件直接映射到DMA请求让采样、搬运和总线读取进一步同步。但那属于进阶玩法先把双缓冲跑稳实际项目中已经能覆盖绝大多数需求。6. 踩坑记录FSMC模式最容易翻车的几个细节6.1 读回来的数据全是0或者通道全错位这个坑我排查了整整半天。FSMC配置看着都对但读出来的8通道数据要么全是0要么整体错位。后来归纳起来主要有三个原因现象可能原因读出来全是0AD7606没有切到并行模式或者CS/RD控制信号没通8个通道整体错一位FMC_A1被接到了AD7606的A0导致地址步长变4字节数据时对时错、偶发0000没有等BUSY下降沿读到了转换中间状态地址线接错的问题要特别讲一下。在16位FMC总线模式里外部地址线A0实际对应CPU内部地址偏移2字节处。如果直接拿FMC_A0接AD7606的A0那么AD7606[0]是通道0AD7606[1]是通道1这是对的。但很多从SRAM项目转过来的人习惯把FMC_A1当最低位地址线用这时AD7606[1]实际会访问外部地址2也就是通道1下一位会造成整体错位。排查的时候用调试器逐地址读一遍立刻能看出规律。6.2 D-Cache和MPU外设映射区不能随便开CacheSTM32H7性能高但D-Cache也带来了一个经典问题如果CPU访问FMC映射区域走Cache第二次读同一个地址时可能拿到的是缓存里的旧值而不是AD7606输出寄存器刚更新的数据。这种Bug不会稳定复现往往在第一次读正常、连续读之后开始错非常隐蔽。稳妥做法是在初始化时给0x60000000所在的FMC区域配置MPU把它设成Non-Cacheable或者至少每次DMA传输完成后用SCB_InvalidateDCache_by_Addr把缓冲区失效掉。我在项目里用的是MPU非缓存方式static void MPU_Config_NonCacheable(void) { MPU_Region_InitTypeDef MPU_InitStruct {0}; __HAL_MPU_DISABLE(); MPU_InitStruct.Enable MPU_REGION_ENABLE; MPU_InitStruct.Number MPU_REGION_NUMBER0; MPU_InitStruct.BaseAddress 0x60000000; MPU_InitStruct.Size MPU_REGION_SIZE_64KB; MPU_InitStruct.SubRegionDisable 0; MPU_InitStruct.TypeExtField MPU_TEX_LEVEL0; MPU_InitStruct.AccessPermission MPU_REGION_FULL_ACCESS; MPU_InitStruct.DisableExec MPU_INSTRUCTION_ACCESS_DISABLE; MPU_InitStruct.IsShareable MPU_ACCESS_NOT_SHAREABLE; MPU_InitStruct.IsCacheable MPU_ACCESS_NOT_CACHEABLE; MPU_InitStruct.IsBufferable MPU_ACCESS_NOT_BUFFERABLE; HAL_MPU_ConfigRegion(MPU_InitStruct); __HAL_MPU_ENABLE(); }如果DMA直接搬运到普通SRAM缓冲区而主循环又要访问这个缓冲区我建议在处理前也统一做一次Cache无效化避免编译器或Cache读到旧数据。这个习惯养成之后很多H7的“玄学问题”都会消失。6.3 时序余量不足导致“时好时坏”FSMC时序压得太狠的问题比较隐蔽因为单次读取大概率是好的但只要温度升高、供电纹波变大或者CPU总线负载变重就会偶发数据跳变。我见过一个项目在调试间跑得好好的一到现场就坏最后查下来就是DataSetupTime少了2个周期。排查时序问题最简单的工具是逻辑分析仪。挂在FMC_NOE和一根数据线上观察NOE低电平脉宽对照AD7606手册里的最小RD脉宽不要只看“能不能读到数”。我建议初始把DataSetupTime给到8~10个周期跑上一整天确认稳定再逐步下降每次降1个周期并继续观察。极限压时序带来的“性能提升”有时候只是心理安慰真正工程上留30%以上的余量更实际。最后说个体会FSMC方案真正解决的是读出带宽不是模拟性能。如果模拟前端电源很脏、参考电压不稳光靠换总线救不回来。先把电源、基准、地平面处理好再上FSMC压榨吞吐率这才是我这次换总线得到的最重要教训。