有一段时间我一直在琢磨一件事:像STM32H750VBT6这种主频能跑到480MHz、片内RAM又不小的MCU,内部ADC的采样能力却始终够不上“高速采集”的门槛。直到我把AD9226这种12位、65MSPS的并行ADC接到H750的总线上,才真正觉得这套组合能干活了。
这篇内容不是芯片手册翻译,而是我做信号采集与FFT分析项目时的一份比较完整的过程记录。如果你是打算做简易频谱仪、中频信号分析、振动监测或者只是想搞清楚“MCU怎么外挂高速ADC”,这篇文章应该能帮你少走不少弯路。里面会涉及到选型逻辑、硬件连接时的坑、DMA和定时器的配合方式,还有在Cortex-M7上跑FFT时的工程取舍。
1. 为什么非要拿H750去“喂”一颗并行ADC:这个项目真正的起点
1.1 瓶颈其实不在MCU算力,而在ADC采样率
很多人看到STM32H750VBT6的第一反应是“这芯片很强”:Cortex-M7、480MHz、大容量RAM、还带浮点运算单元。照理说,做FFT这种运算密集的事应该很轻松。
但问题在于,信号采集这条链路里,MCU只是“下游”。上游的ADC如果采样率不够,后面MCU再强也白搭。H750内部集成的ADC虽然也是12位,但在实际使用中要做到连续、稳定地采集更高频信号,会受到采样保持电路、触发方式和内部时钟抖动的限制。真要把采样率推到几MSPS以上,内部ADC就非常吃力,更别提还要同时兼顾多通道或长时间连续采集。
所以这个项目的起点很明确:想让MCU做频谱分析,就必须在MCU外面再接一颗更快的ADC。AD9226恰好是一颗很经典的并行ADC,12位分辨率、最高65MSPS采样率、数字输出直接以并行引脚给出。对H750来说,这等于给它配了一个真正意义上的“高速前端”。
1.2 为什么不直接上FPGA
有人可能会说,都到这种采样率了,干脆用FPGA算了。确实,FPGA在高速采集领域很常见,但对应的是更复杂的开发流程和更高的学习成本。FPGA的优势在于并行处理、时序可定制,但并不是每个项目都需要把时序做到那种极致程度。
在这个项目里,采样率我实际跑的是20MSPS到25MSPS,这个量级用MCU加DMA完全可以应付。H750的480MHz主频对FFT计算来说足够,DMA又能把数据从ADC总线搬进内存,几乎不占用CPU。相比之下,FPGA方案的硬件成本、开发周期和调试难度都更高。对于“做一个能实时显示频谱的采集模块”这个目标,MCU方案是我认为更务实的路线。
1.3 这套组合到底能覆盖什么场景
处理器定了、ADC选了,那么这套组合能干什么?我实际把它用在了这样一个场景:采集一段带宽在几MHz以内、频率成分在几百kHz到十几MHz的信号,然后做FFT分析,输出频谱。比如某些传感器输出经过放大和滤波后的中频信号,或者某些设备的窄带电磁辐射特征。
这种场景下,20MSPS采样率意味着理论分析带宽能到10MHz。FFT点数取4096时,频率分辨率大约是4.88kHz。如果需要看更细的频率细节,可以把采样率降到1MSPS,再取8192点,分辨率就能到122Hz。也就是说,同一套硬件,通过改采样率和FFT长度,就能在“宽频扫描”和“窄带细看”两种模式之间切换。
这也是我最终选这套组合的核心原因:硬件只有一块板子,但“带宽”和“分辨率”两个指标可以通过软件灵活调整。
2. 硬件连接:让AD9226的12根数据线不乱飞
2.1 数据线怎么接:端口选择和端接处理
AD9226的输出是12根并行数据线,从D0到D11。这12根线如果随便飞线,在20MSPS以上的速度下很容易出问题。首先是把它们接到H750的哪一组IO口上。我建议不要把数据线分散在不同GPIO引脚上,最好集中在同一个端口的连续引脚上,这样读取的时候一个端口寄存器就能拿到完整数据,也不用来回拼接。
以H750为例,把D0~D11接到GPIOE的PE0~PE11,初始化成输入模式,关闭上下拉,速度开到最高档。注意一点,AD9226的数字输出如果是3.3V电平,跟H750的IO电平匹配;如果上面还挂了5V供电,务必确认输出电平不会超过H750的耐压范围,否则就得加电平转换或者串联电阻分压。
数据线长度也要尽量短。我最初用杜邦线验证,当时只跑5MSPS还能凑合,一上20MSPS,FFT结果立刻出现大量毛刺。后来改成PCB走线并尽量等长,数据线之间短了很多,问题才缓解。
2.2 采样时钟与电源:最容易影响SNR的两个位置
AD9226的采样时钟必须认真对待。这个时钟决定了ADC在什么时刻采样输入信号,时钟的抖动会直接影响采样精度。
我刚开始图省事,想直接用H750的定时器输出PWM当采样时钟。低速验证阶段看起来问题不大,但把采样率拉到20MSPS以上后,频谱底噪明显抬高。后来我把采样时钟换成外部时钟源,同一个信号源同时接入AD9226的CLK和H750定时器的外部时钟输入,让MCU的DMA触发时间与ADC采样沿严格同源,问题才解决。
电源方面,AD9226的模拟电源和数字电源要分开处理。我的做法是模拟电源走独立的LDO,再通过磁珠和电容隔离到数字电源域。ADC的参考电压引脚附近必须放高质量去耦电容,否则参考电压上的噪声会直接进入转换结果。不要小看这几分钱的电容,FFT出来的底噪水平往往就是被这些细节决定的。
2.3 输入端的差分驱动和抗混叠低通
AD9226的模拟输入是差分结构,幅度范围大概2Vpp。如果你要采集的是单端信号,不能直接把信号怼到ADC输入端,需要把单端信号转换成差分信号。
我在这里用了一颗高速运放做单端转差分驱动,同时对信号做了阻抗匹配。运放的带宽要高于信号带宽,否则高频分量会在进入ADC之前就被衰减。还有一点容易被忽略:ADC采样之前必须加抗混叠低通滤波器。因为采样定理只保证fs/2以内的信号不混叠,如果输入信号里存在高于奈奎斯特频率的成分,它会被折叠到低频区间,最终在FFT谱上出现一条“根本不存在的频谱线”。
这个滤波器我用了二阶LC低通,截止频率设置为略低于采样率的一半。如果只做验证,也可以用电阻电容组成一阶RC,但衰减斜率不够,效果差不少。
3. 固件层的“管道”设计:定时器触发、DMA乒乓与12位数据重组
3.1 DMA直接从GPIO_IDR读数据,而不是靠中断
很多人的第一反应是“12位数据线接到GPIO之后,在定时器中断里读端口不就行了”。说实话,采样率在1MSPS以下的时候确实可以,但一到20MSPS,每50ns就要产生一次中断,CPU根本来不及响应,更别提还要做FFT。
所以这里必须走DMA。思路是这样的:把ADC的数据总线看作一个外设,DMA定时从GPIO端口的输入数据寄存器里搬数据到内存缓冲区。搬数据这件事是硬件完成的,不占用CPU。
具体实现时,我让定时器工作在外部时钟输入模式,外部采样时钟每来一个有效沿,DMA就触发一次传输,读取一次GPIOE的IDR寄存器。DMA配置成循环模式,持续往缓冲区里写数据。这样CPU只负责在缓冲区分半时做处理,其余时间可以干别的事。
3.2 关键代码:定时器DMA触发和乒乓缓冲
下面这段是DMA初始化的大致写法:
static DMA_HandleTypeDef hdma_adc; void adc_bus_dma_init(void) { __HAL_RCC_DMA1_CLK_ENABLE(); hdma_adc.Instance = DMA1_Stream0; hdma_adc.Init.Request = DMA_REQUEST_TIM2_UP; hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc.Init.MemInc = DMA_MINC_ENABLE; hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc.Init.Mode = DMA_CIRCULAR; hdma_adc.Init.Priority = DMA_PRIORITY_HIGH; HAL_DMA_Init(&hdma_adc); __HAL_LINKDMA(&htim2, hdma, hdma_adc); }这里外设地址要填GPIOE的IDR地址,目的地址是我们的采集缓冲区。DMA触发源来自定时器更新事件,而定时器又由外部采样时钟驱动,所以每次采样都和ADC的CLK对齐。
缓冲区推荐采用乒乓结构。DMA半传输中断和全传输中断交替触发时,CPU处理前半段数据、DMA往后半段写数据;下一次切换过来。这样采集可以不间断,FFT也能持续更新。
3.3 把偏移二进制转成有符号线性值
DMA搬进来的原始数据并不是直接用,AD9226默认输出的是偏移二进制格式。12位数据范围是0~4095,中间值2048代表模拟零电平。送到FFT之前需要先转成有符号数,否则FFT结果里会有一个巨大的直流分量。
转换代码很简单:
uint16_t raw = capture_buf[i]; int16_t signed_value = (int16_t)(raw & 0x0FFF) - 2048; fft_input[i] = (float32_t)signed_value;这里有一个细节:DMA搬进来的数据是16位宽度,但AD9226只有12位,高4位可能是随机值,必须先做一次掩码。如果不做掩码,后面FFT会莫名多出噪声。
4. 在Cortex-M7上做FFT:窗口、库函数和实时性取舍
4.1 选择FFT配置:长度、采样率与频率分辨率
FFT的第一步不是写库函数,而是算清楚你想要多高的频率分辨率。
频率分辨率公式是fs/N。比如采样率20MSPS,FFT长度4096,分辨率就是20000000/4096≈4883Hz。这是什么概念?如果两个信号频率相差不到4.88kHz,FFT谱上就分不开。而如果你要分析的是几百kHz的低频细节,这个分辨率太粗了。
我通常的做法是先确定目标频段,再反推采样率和FFT长度。看几百kHz以下的窄带信号,就把采样率降到1MSPS左右,FFT长度取8192,分辨率大约122Hz。看宽带信号时,再切回20MSPS加4096点。H750的RAM足够,8192点浮点数据完全放得下。
4.2 CMSIS-DSP里的实际调用方式
Cortex-M7上做FFT,最省事的方案是用CMSIS-DSP库。它针对ARM Cortex-M做了优化,浮点FFT可以直接用FPU算,不需要自己写蝶形运算。
调用流程大致这样:初始化实例,然后把采集数据填入浮点数组,调用一次正向FFT。
arm_rfft_fast_instance_f32 fft_inst; float32_t fft_input[FFT_SIZE]; float32_t fft_output[2 * FFT_SIZE]; arm_rfft_fast_init_f32(&fft_inst, FFT_SIZE); arm_rfft_fast_f32(&fft_inst, fft_input, fft_output, 0);注意输出数组长度是输入数组的两倍,因为FFT输出是复数形式,实部和虚部交错存放。计算幅度时,用对应实部和虚部求模:
float re = fft_output[2 * i]; float im = fft_output[2 * i + 1]; float mag = sqrtf(re * re + im * im);这个库的输入要求是2的整数幂长度,我用的是4096点。如果你要做工程级幅度标定,还需要考虑FFT本身的缩放系数,这个会因库版本不同略有差异。
4.3 窗口函数和幅度校正
对连续采集到的信号做FFT,相当于从时域上截取了一段有限长度的数据。这个截取动作本身会带来频谱泄漏:本来应该是一条细谱线的信号,能量会展宽到旁边好几个频率点。
解决频谱泄漏的办法是加窗。我常用的窗函数有三个,不同场景选择不一样:
| 窗函数 | 主瓣宽度 | 旁瓣抑制 | 适用场景 |
|---|---|---|---|
| 矩形窗 | 最窄 | 差 | 瞬态信号、接近周期的信号 |
| 汉宁窗 | 较窄 | 较好 | 大多数连续频谱监测 |
| 布莱克曼窗 | 较宽 | 很好 | 需要分辨微弱信号时 |
加窗代码很简单,对送入FFT的每个数据点乘一个窗系数。汉宁窗的计算方式如下:
for (int i = 0; i < FFT_SIZE; i++) { float w = 0.5f * (1.0f - cosf(2.0f * 3.1415926f * i / (FFT_SIZE - 1))); fft_input[i] = (float32_t)(signed_value) * w; }加窗之后,信号总能量会变小,所以要测绝对幅度就需要乘一个恢复系数,汉宁窗恢复系数约等于2。若只关心相对频谱形状,这一项可以忽略。
4.4 实时性分析:采4096点要多久,算4096点又要多久
做FFT分析最怕的是数据采集完了,计算半天算不出来,导致下一段数据已经写完,处理程序还在算上一段。
以20MSPS采样率为例,采集4096点只需要4096/20M≈0.2ms。而Cortex-M7跑4096点浮点FFT,如果直接用CMSIS-DSP,大概在1~2ms量级。也就是说,FFT计算比采集慢,无法做到“采一段算一段”的完整流水线实时。
所以我的方案是:不要求每一帧都实时出频谱,而是让DMA连续采样,CPU在缓冲区分半时集中处理。如果采样率比较高,就适当减小FFT长度,或者在两次FFT之间做丢弃和平均。实际做振动监测这类低频场景时,采样率降到1MSPS以下,4096点浮点FFT完全能跟上采集节奏。
5. 调试记录:三个把频谱图搞脏的问题
5.1 混叠出来的“幽灵频率”
第一次把整套系统跑通时,我在信号源上加了一个5MHz正弦波,结果FFT谱上居然在很低的频率位置也出现了一条谱线。这条谱线会随着输入频率变化而反向移动,典型特征就是混叠。
问题出在ADC输入端:5MHz信号虽然低于10MHz奈奎斯特频率,但我没注意到信号源本身还输出了谐波。比如15MHz的二次谐波进入ADC之后,就会混叠到5MHz附近。加抗混叠低通滤波器之前,这条“幽灵频率”一直干扰判断。后来我在信号源后面加了一级截止频率约8MHz的低通,高频谐波被滤掉,频谱立刻干净了。
5.2 高速并行线上的数据毛刺
还有一个很折腾的问题:20MSPS采样时,FFT结果偶尔会出现随机噪声点,幅度不稳定,而且看起来不像是信号本身的问题。
排查下来,是并行数据线太长且没有做端接造成的。AD9226的数据输出沿非常陡,数据线相当于一根天线,信号在线上反射后形成振铃。DMA读取的时刻如果正好落在振铃区间,采回来的数据就是错的。
解决方法是把数据线尽量缩短,并且在靠近MCU端串入33Ω左右的电阻吸收反射。如果你用了FMC方案,还可以考虑利用FMC的时序参数来校正采样点位置。这一步调整之后,误码率明显下降,频谱底噪也恢复了正常。
5.3 时钟抖动抬高噪声底
第三个问题是最隐蔽的:一开始我用MCU定时器输出PWM作为AD9226的采样时钟,低速时频谱底噪看起来还行。一旦把采样率提高,底噪整体上升,尤其是偏离信号频率越远的地方,噪声越明显。
这种噪声不是ADC本身带来的,而是采样时钟抖动造成的。时钟抖动相当于采样时刻每次都有一点点偏差,等效成采样信号上叠加了随机噪声。FFT之后,这部分噪声会均匀抬升整个谱线底部。
我把采样时钟改成外部低抖动时钟源,同时让同一个时钟驱动定时器触发DMA,相位关系固定后,底噪下降非常明显。这也是为什么我在前面章节反复强调时钟问题的原因——它不像数据线那样直观,但影响可能更大。
一点个人经验
做完这套采集系统之后,我的感受是:MCU加外部高速ADC这条路完全走得通,但成败往往不在主频上,而在时序和信号完整性的细节上。数据线等长、时钟抖动、电源去耦、抗混叠滤波,每个环节都不难理解,可每个环节都能毁掉整个FFT结果。
如果你也想复现这套方案,我建议从较低采样率开始,先把DMA链路调通、能在串口打印出FFT幅值,再逐步提高采样率。这样即使出了问题,也容易定位是哪一环引入了噪声。