ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

STM32测频实战:输入捕获与FFT方案详解与对比

STM32测频实战:输入捕获与FFT方案详解与对比 做信号测量类项目的时候“测频率”这三个字永远是绕不开的需求。无论是做一个简易示波器、音频频谱显示器还是给电机测速、检测振动信号最终都要落到“这个信号到底有多快”这个问题上。而STM32平台最常用的两种测频路线就是定时器输入捕获和FFT频谱分析。这两条路各管一段踩过的坑也各自不同这篇博文把我的实际使用经验完整拆开讲。先说清楚这两个方案到底是什么定位输入捕获解决的是“单一频率信号的精准测量”典型场景是测一个PWM波、一个光电编码器输出、或者经过整形后的正弦波频率FFT解决的是“复杂信号里有哪些频率成分”典型场景是音频分析、振动故障诊断、电力谐波检测它能把一堆混在一起的信号切片给你看。两者不是替代关系而是互补关系。我会从原理、选型、配置、代码、排障五个维度把整条链路讲完整适合正在做课程设计、毕业设计或者刚入门嵌入式信号处理的读者直接参考。1. 方案选型先搞清楚你的信号长什么样1.1 输入捕获的适用边界输入捕获的核心思路很简单定时器内部有一个自由运行的计数器我让它在信号的上升沿或下降沿把当前计数值“抓住”存到捕获寄存器里然后拿相邻两次捕获的计数值做差就能得到信号周期取倒数就是频率。这个方案强在哪在一个周期内就能出结果响应速度快、精度可以做得非常高取决于定时器时钟频率。我举个实际例子STM32F407的主频168MHz定时器挂到APB1总线上就是84MHz用它去捕获一个1kHz方波一个周期内计数器走84000个Tick计数误差只有±1个Tick折算下来测频误差大概千分之一这个精度在很多工业现场都够用了。但输入捕获有两个致命限制。第一它只能测“单一主导频率”的信号。如果被测信号是个方波、正弦波、或者经过施密特整形后的脉冲没问题但如果信号本身就是多个频率叠加的复合信号比如音频信号、电机振动信号输入捕获拿到的只是某个过阈值的边沿结果会乱跳。第二它本质上测的是周期所以需要一个足够强的上跳沿如果信号幅值太小、噪声又大引脚上抖动会带来大量误触发频率读数会跟抽风一样跳变。所以用到输入捕获的场景多半要先用比较器或整形电路把信号变成干净的方波。1.2 FFT能干什么、不能干什么FFT快速傅里叶变换解决的完全是另一个问题它在频域里把信号拆开告诉你每个频率成分的幅值有多大。STM32F4系列自带FPU和DSP库做实数FFT的速度非常可观1024点FFT在168MHz频率下大概几百微秒就能跑完实时性完全够。FFT测频最典型的需求就是嵌入式频谱分析仪之类的东西。你给麦克风输入一段音频FFT能把基频和各个谐波都显示出来你做电机故障诊断FFT能看到转频、倍频、轴承故障特征频率你测一个带有大量谐波畸变的工频信号FFT能帮你同时分离出50Hz和它的整数倍谐波。这些场景输入捕获是彻底没戏的。但FFT也有它天生别扭的地方它需要等一整段采样数据攒够才能算一次所以刷新率天然比输入捕获低它的频率分辨率取决于采样率和FFT点数不是想测多细就有多细它还特别吃内存做2048点FFT光存放采样缓冲区和运算结果就要十几KB的RAM小容量的芯片可能会直接卡死。选型的时候一定要先想清楚你的信号到底是“单一频率”还是“多频率成分”这决定了整个方案的方向。1.3 我的选型判断标准拿我自己的习惯来说接到一个测频需求我第一件事就是问三个问题信号是不是只有单一主频频率范围大概是多少需要多快的刷新率如果答案是“单一频率几十Hz到几百kHz刷新越快越好”闭眼选输入捕获成本最低、实现最快、精度也最靠谱。如果答案是“信号很复杂我想知道里面有什么频率成分”那就必须上FFT没得商量。还有一种情况比较特殊就是既需要精准的单频测量又需要知道波形质量。这种情况下我会把两种方案同时做进去用输入捕获测基频用FFT看谐波分量和总畸变率。STM32F407的定时器资源和DSP运算能力都够工程里两个模块并存完全没问题。2. 输入捕获测频的完整实现2.1 定时器配置的几个关键点用STM32CubeMX配置输入捕获比我早年纯手写寄存器快太多了但即使这样也有几个地方特别容易配错。首先是时基设置。定时器的时钟源、预分频系数、自动重装载值必须想清楚。我习惯的做法是把定时器时钟配到84MHzF4或72MHzF1然后把预分频设置为83F4或者71F1这样计数器的Tick就是1MHz计数值直接就是微秒数调试的时候读数值非常直观不用做除法。自动重装载值ARR要看最大被测周期来定16位定时器最大65535个Tick倒推一下1MHz计数下最长能测65535微秒也就是大概15Hz的信号。如果要测更低频率只能降低计数频率比如把Tick降到100kHz代价就是低频时精度下降。然后是输入捕获通道配置。有一点必须说清楚STM32的捕获通道和引脚不是死绑的比如TIM2的通道1可以是PA0也可以是PA5。用CubeMX的时候你只需要在Pinout视图里点选引脚它会自动帮你分配定时器通道看起来很方便新手容易因此忽略定时器与引脚的复用关系。如果后面要改PCB别忘了重新检查引脚复用表别改完硬件代码跑不通还在那瞎查。第三个是边沿选择。捕获上升沿还是下降沿取决于你的信号形态。如果信号不是严格的50%占空比方波测量上升沿间隔比测量高电平脉宽更不容易出错因为脉宽测量受占空比影响大上升沿到上升沿的时间才是完整的周期。我的习惯是设成上升沿捕获然后通过软件判断是否超时。2.2 捕获中断里的关键代码思路配置完中断后重点就在中断处理函数里。这里我强调一个非常容易踩的坑只读捕获寄存器是不够的还要处理定时器的溢出Update事件。设想一个场景被测频率比较低比如20Hz周期50ms而定时器计数到65535就回零了那么一个周期内计数器会回绕好几次。如果没有把溢出次数记录下来你算出来的周期就是错的。正确的做法是在Update中断里给一个变量加1记录完整溢出次数在捕获中断里读出当前CCR值再读当前溢出计数器用完整时间戳相减。我给出一个比较典型的处理流程volatile uint32_t overflow_cnt 0; volatile uint32_t last_timestamp 0; volatile uint32_t period_tick 0; void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); overflow_cnt; // 记录定时器回绕次数 } if (TIM_GetITStatus(TIM2, TIM_IT_CC1) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_CC1); uint32_t cur_timestamp TIM_GetCapture1(TIM2) (overflow_cnt 16); period_tick cur_timestamp - last_timestamp; last_timestamp cur_timestamp; // 此时 period_tick 就是信号周期对应的Timer Tick数 } }用“当前捕获值溢出次数×65536”拼出一个完整时间戳这种方式比单纯用捕获值可靠得多。注意overflow_cnt要保证在读取的那一刻和CCR寄存器是配套的否则临界区会出问题。处理方法是先关定时器中断读取溢出计数和捕获值再开中断或者用代码里“先读SR再读CCR”的次序来规避。实操中我一般直接用临界区保护逻辑简单不容易出错。2.3 软件滤波和测量稳定性硬件上信号再怎么干净到了单片机引脚这里也难免有些毛刺。更别提有些场景直接把电机编码器的输出接进来干扰是真的不小。我的习惯是测频结果出来后一定要做软件滤波。最简单有效的策略是“中值平均”连续采集5个周期值去掉最大最小剩下3个取平均。这个组合能同时抗单点毛刺和短时抖动代码量也就十几行。如果对实时性要求高可以改成滑动平均窗口取4或8移位操作即可不用做复杂除法。另外说一个很实用的细节不要更新完频率值马上清零重测最好让测量窗口重叠。比如用环形缓冲维护最近N个周期数据每次新数据进来就把最旧的数据顶掉更新频率的时候滚动计算平均。这样测频结果的刷新率更高显示也不会有明显的间断感。2.4 把信号整形干净的硬件建议前面我说过输入捕获需要干净的边沿。实测中如果直接把话筒输出、或者未经整形的正弦波接到单片机引脚捕获结果会非常不稳定。因为引脚的电平阈值附近有噪声时一次上升沿会被触发好几次。解决的办法常规是加一个比较器或者施密特触发器。最简单的方案是用一个LM393比较器把参考电压设在信号中点附近输出端加上拉电阻再接单片机引脚。如果信号本身幅值足够也可以用单片机的内部施密特触发输入来改善但抗噪能力依然有限外部整形还是更稳一些。我做过一个测速小车项目直接用光电编码器的A/B输出接STM32的输入捕获引脚编码器本身输出波形是方波基本不需要额外整形。但如果是自己用红外对管码盘搭的测速装置接收端波形往往带着很长的上升/下降沿那就必须在比较器里处理一下否则速度显示一定会出乱子。3. FFT测频从采样到频谱输出的完整链路3.1 采样率、FFT点数和频率分辨率的计算关系FFT测频第一个要搞明白的参数是频率分辨率Δf。它的计算公式很硬核Δf fs / N其中fs是采样率N是FFT点数。举个例子采样率设为10240Hz做1024点FFT分辨率就是10Hz。这意味着如果你信号里有50.0Hz和55.0Hz两个频率分量在频谱上它们只隔了0.5格几乎无法区分。这里还要注意采样定理的约束采样率必须大于被测信号最高频率的两倍。比如你要分析20kHz以内的音频那么采样率至少40kHz保险起见我会留出20%的余量配48kHz或51.2kHz。采样率一旦定了FFT能分析的频率范围上限也基本定了就是fs/2也就是奈奎斯特频率。我的习惯是先把目标频率范围确定下来再反推采样率和点数。比如工程里需要监测0~5kHz的振动信号分辨率希望做到5Hz以内那么Nfs/Δf若fs10kHzN至少2000点实际取2048点分辨率约4.88Hz完全满足要求。如果用1024点分辨率约9.77Hz就偏粗了。表格整理一下常见组合采样率fs(Hz)FFT点数N分辨率Δf(Hz)最高分析频率(Hz)内存占用float缓冲1024010241051208KB1024020485512016KB48000102446.9240008KB48000204823.42400016KB内存占用这个数据大家心里要有数老款STM32F103的RAM只有20KB1024点FFT勉强能跑2048点就有点吃紧F4系列一般都有128KB以上基本不用担心。3.2 ADC采样用定时器触发DMA别用阻塞采样FFT对采样数据的等间隔要求非常严格。如果靠while循环里调用ADC采样中间任何一条中断指令插入都会导致采样点时间不均匀反映到频谱上就是噪声底抬高、杂散分量变多。正确做法是用定时器输出触发ADC采样再用DMA自动搬运结果整个过程CPU全程不参与数据的时间均匀性由硬件保证。以STM32F4为例我用的是TIM3的TRGO事件触发ADC1ADC1规则通道序列采集PWM口对应的引脚DMA循环模式把结果搬运到内存数组。CubeMX里的配置思路大概是TIM3配成PWM模式输出比较频率就是采样率ADC1开启定时器触发DMA配置成循环模式数据宽度半字12位ADC结果。采样缓冲区的组织也要注意。做实数FFT有两种数据排布方式一种是直接用arm_rfft_fast_f32输入是一个float数组实数序列按顺序填充另一种是用arm_cfft_f32输入是复数数组实部放采样值虚部填0。我个人的经验是F4系列用CMSIS-DSP库的arm_rfft_fast_f32最方便它内部做了Radix-4优化性能和代码简洁度都很理想。ADC采样值一般需要做一下电平搬移。很多信号源是单极性的比如麦克风模块输出电压在0~3.3V之间摆动中心点在1.65V附近FFT对直流分量不敏感直接算就行不用额外处理。但如果想把频谱显示得更干净可以先在软件里减掉直流均值再送FFT这样0Hz处的巨大直流分量不会压得旁边的低频分量看不清。3.3 DSP库FFT调用和幅值谱计算CMSIS-DSP库让FFT代码变得非常薄。以STM32F407、Keil环境为例首先在工程选项里勾选使用FPU并把arm_cortexM4lf_math.lib加进来。然后调用方式如下#define FFT_SIZE 2048 float32_t input[FFT_SIZE]; float32_t output[FFT_SIZE]; float32_t mag_output[FFT_SIZE/2]; arm_rfft_fast_instance_f32 fft_handler; // 初始化 arm_rfft_fast_init_f32(fft_handler, FFT_SIZE); // 假设input已经填好ADC采样数据 arm_rfft_fast_f32(fft_handler, input, output, 0); // 0表示正变换 // 计算幅值 arm_cmplx_mag_f32(output, mag_output, FFT_SIZE/2);计算完幅值谱之后找出幅值最大谱线对应的频率就简单了循环搜索最大幅值的下标k频率就是k×fs/N。但要提醒一点直接取最大值谱线会存在栅栏效应单根谱线的频率和真实频率可能有偏差。如果只是显示个大概频率够用但如果你要精确到0.1Hz就要做频谱插值。3.4 提高测频精度的抛物线插值法当信号频率不是FFT分辨率的整数倍时真实频率不会正好落在某根谱线上而是“骑”在相邻两根谱线之间。这时频率估计会带系统偏差最大可能有半个分辨率。解决办法是取幅值最大的谱线以及它左右两根谱线用抛物线拟合峰值位置。公式很朴素假设最大谱线在k幅值分别为y[k-1]、y[k]、y[k1]则修正后的峰值位置delta为delta (y[k-1] - y[k1]) / (2 * (y[k-1] - 2*y[k] y[k1]))真实频率 f (k delta) * fs / N我在实际工程中用这个抛物线插值50Hz工频信号的测量误差能从1Hz左右降到0.1Hz以内。对于一般测试显示完全够了。如果想更进一步可以用加窗FFT双谱线插值算法但代价是计算量增加普通项目没必要追求到这个程度。3.5 加窗还是不加大有讲究FFT有一个逃不掉的问题叫频谱泄漏如果采样窗口内信号不是整数个周期能量会“漏”到旁边的谱线上导致频谱看起来糊成一片。解决思路是给数据加窗函数常用的有汉宁窗Hanning和布莱克曼窗Blackman。但加窗有代价主瓣宽度变大有效分辨率会变差。所以做测频的时候我的取舍是这样的如果被测的是稳定的周期信号、频谱本身就比较干净不加窗也没问题如果是音频这类复杂信号必须加汉宁窗不然谐波之间的旁瓣会互相干扰如果是分析瞬态冲击信号用矩形窗不加窗反而更能保留时间特性。CMSIS-DSP库里没有直接的加窗函数需要自己写个循环。比如汉宁窗的系数计算方式为for (int i 0; i FFT_SIZE; i) { float32_t w 0.5f * (1.0f - arm_cos_f32(2.0f * PI * i / (FFT_SIZE - 1))); input[i] * w; }加窗之后幅值谱的数值会变小因为窗函数把边缘数据削弱了。要恢复真实幅值需要做幅度校正通常除以窗函数的相干增益。汉宁窗的相干增益是0.5所以加窗后幅值要乘以2才能对应真实正弦波的幅值。这个细节很容易被忽略我早期调试时就遇到过明明输入1V的正弦波频谱幅值却只有0.5V排查半天才发现是没做幅度校正。4. 实操中的高频问题与排查经验4.1 输入捕获频率值乱跳我在测速小车项目里遇到过最头疼的问题就是速度显示值隔几秒就跳到几倍数值然后又掉回来。排查过程分三步走最后定位到是编码器A相波形毛刺太多在边沿附近来回触发。解决方法是把“捕获上升沿”改成“捕获上升沿软件消抖”。具体实现第一次捕获触发后关掉捕获中断软件延时2ms再重新开启如果2ms内没有再次触发就认为这个边沿是有效的如果2ms内再次触发说明上一个边沿是毛刺丢弃。这个策略虽然会增加一些测量盲区但对低速测速来说完全能接受。4.2 FFT结果出现明显镜像分量如果FFT出来的频谱在某个频率f位置有峰同时在fs-f附近也出现了一个对称的峰这通常不是真实信号而是采样过程中混入了高频噪声导致的混叠Aliasing。最有效的解决方案是硬件上加RC低通滤波器把高于fs/2的成分在进ADC之前滤掉。很多初学者忽略了这个抗混叠滤波器结果频谱上总有一些说不清道不明的杂峰改软件改到天亮也解决不了最后发现是硬件问题。如果不想动硬件软件上可以把采样率尽量提高让混叠频率落到分析频带之外然后通过数字滤波处理。但总归不如硬件抗混叠来得干净。4.3 浮点运算慢或者HardFaultFFT运算本身是浮点密集型的。如果你用的是不带FPU的F1系列跑2048点实数FFT可能要几百毫秒实时性就很差。解决办法有三个换成F4或H7系列带硬件FPU用定点FFT库替代浮点库降低FFT点数。另外一个常见问题是HardFault多半是内存越界。浮点FFT的临时缓冲是输入缓冲的2倍因为复数的实部和虚部如果只给output分配了FFT_SIZE个float但arm_rfft_fast_f32在运算时会同时写实部和虚部就会越界直接HardFault。我用一个简单规则规避所有FFT相关缓冲统一按2 * FFT_SIZE的大小去分配宁可浪费一点内存绝不踩越界。4.4 上位机显示频谱的串口瓶颈如果你的项目需要把频谱数据发到上位机显示会遇到一个现实问题1024点FFT每点按浮点发出去一个频率点动辄4字节全发一遍要4KB数据。如果用115200波特率1秒最多传大约11.5KB意味着刷新率极限也就2~3帧每秒而且CPU大部分时间都在等串口发送。我的处理办法是对频谱做峰值提取和降采样。比如只在频谱里挑出幅值最大的10个频率点把它们作为“特征频率”发送数据量瞬间降到几十字节或者对频谱做分段最大保持把1024点压缩成128个“包络点”上位机画出来的频谱形状完全够看传输压力却小了一个数量级。这样刷新率能轻松做到10帧以上上位机显示也流畅很多。4.5 常见问题速查现象可能原因处理方向输入捕获频率是实际值N倍引脚毛刺触发多次边沿加施密特整形或软件消抖输入捕获低频测不准定时器溢出未处理补全溢出次数用完整时间戳频率值为0捕获中断没进入检查引脚复用、NVIC、时钟使能FFT频谱杂散多采样时间不均匀改用定时器触发DMAFFT幅度值偏小未做窗函数幅度校正按相干增益补偿高频段出现镜像峰混叠加抗混叠低通滤波器或提升采样率程序HardFaultFFT缓冲越界缓冲按2倍FFT_SIZE分配串口显示刷新慢数据量过大峰值提取或分段降采样5. 两种测频方案的性能对比与扩展建议拿一个“信号测量模式切换”的思路收个尾。我在做嵌入式系统的时候常把输入捕获和FFT做成两个可切换的模块通过串口指令或按键切换模式。模式一是“精准频率计”适合单一频率信号刷新率能做到每周期更新一次模式二是“频谱分析仪”适合复杂信号查看频率成分刷新率取决于FFT点数和采样时间。两个方向后续可以扩展的地方挺多。输入捕获可以扩展出周期测量、脉宽测量、占空比测量甚至可以接正交编码器做电机转速方向检测这些其实都是同一个底层机制在支持。FFT方向则可以做加窗种类切换、频谱峰值自动跟踪、功率谱密度计算配合DAC还能做简单的音频频闪显示或音乐节奏灯。仔细想想这两个模块几乎能覆盖信号测量领域80%的基础需求。我个人在实际项目里最大的体会是无关方案高低真正决定项目成败的是对信号本身的理解。先别急着堆代码拿示波器看看待测信号的形态再决定用输入捕获还是FFT整个工程推进会顺畅得多。如果你正准备搭一个测频相关的小项目建议先从定时器输入捕获练手因为代码短、调试直观、出成果快等熟练了再上FFT去啃复杂信号。这条路走下来你对STM32定时器和信号处理的理解都会上一个台阶。
返回列表