ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32测频实战:输入捕获与FFT频谱分析详解

STM32测频实战:输入捕获与FFT频谱分析详解 搞STM32的人迟早会遇到“测频率”这个需求调一个电机测速模块、校准信号发生器的输出、判断一个未知PWM波的占空比与频率、甚至在毕业设计里做音频频谱显示。我最早用逻辑分析仪外接后来又用示波器最后发现单片机上直接做输入捕获和FFT测频才是最顺手、也是最能沉淀出方法论的两条路。这篇文章就围绕STM32单片机的输入捕获测频和FFT频谱测频展开把方案选型、底层原理、代码实现、踩坑现场一次说透。这篇文章适合三类人一是刚接触STM32定时器、想看明白输入捕获底层逻辑的新手二是想把测频精度往上推、不想只停留在“能测出大概数”阶段的人三是需要在宽频范围或非正弦信号下做频率识别、想用FFT替代传统过零检测的进阶玩家。内容不挑具体型号我用STM32F407做演示但F1、F3、L4的HAL库流程差别很小换个芯片包就能平移。1. 先想清楚再动手三种测频方案怎么选测频率这件事听着简单本质上就是“数”信号的变化。但“数”的方式不同适用的信号频率段、测量精度、实时性完全不一样。我把常见方案筛了一遍实际工程里真正常用的有三类定时器输入捕获测周法、外部时钟计数测频法、ADC采样加FFT频谱分析法。1.1 输入捕获测周法低频精准适合从几赫兹到几十千赫兹输入捕获测周法的核心思路是用定时器的一个通道去监听引脚上的边沿跳变捕获到上升沿时把定时器计数器的当前值锁存进捕获寄存器下次上升沿再捕获一次两次计数值之差就是信号的一个完整周期换算一下即得频率。这个方案的精度天然和高频信号“过不去”。因为测得的是一个周期内的计数值如果信号频率很高一个周期里计数器只走了几个数误差率就非常高。比如定时器时钟84MHz测一个1MHz的方波一个周期只数84个计数理论上分辨率就不超过1.2%。而且输入捕获还需要引脚上有干净、陡峭的边沿信号但凡带点毛刺捕获到的周期就可能忽长忽短。所以我的个人经验是输入捕获测周法只管低频段从几赫兹到几十千赫兹表现都不错尤其是低频慢速信号它能通过长周期内的计数累加自然滤波稳定性甚至比仪器还好。这种场景像什么编码器测速、电机换相频率检测、PWM遥控信号解析全是它的主场。1.2 外部时钟计数测频法高频计数靠闸门适合几百千赫兹以上既然测周期法在高频段吃亏那换个思路用定时器外部时钟模式直接把被测信号当作定时器的时钟源计数器每来一个上升沿就加一然后在固定的时基里读取计数值。这本质上是“单位时间内的脉冲数”也就是频率。这个方案要用到两个定时器配合一个定时器负责提供精准的时间闸门比如100ms或1s另一个定时器工作在外部时钟模式由外部信号驱动计数。到闸门时间结束时锁存计数值用“脉冲数 / 闸门时间”算出频率。只要闸门够准计数值够大高频测频精度可以做到很高而且不依赖信号边沿质量因为计数器本身就有硬件滤波和触发配置。缺点也很明显分辨率受闸门时间限制。1秒闸门能分辨1Hz但响应太慢100ms闸门能分辨10Hz适合粗测。所以这个方案往往用在需要测高频、但对刷新率要求不高的场景比如测晶振输出、PLL输出、逆变器开关频率。1.3 ADC采样加FFT频谱分析信号不干净也敢说频率适合宽带分析FFT测频和前面两种思路完全不同。它不再“数”脉冲而是先把模拟信号通过ADC连续采一串点然后对这串点做快速傅里叶变换得到频谱图从中找出幅值最大的频率分量那个频率就是信号的基波频率。这个方案的优点非常突出一是能处理非方波信号正弦波、三角波、带谐波的失真波形都行二是能同时分辨多个频率分量一个频谱里可以看到基波、二次谐波、三次谐波甚至能分析干扰信号三是频率覆盖范围广从直流附近到ADC采样率的一半奈奎斯特频率都能测。代价也有需要ADCDSP运算资源对采样率、缓冲区管理、FFT点数和窗函数都有要求频率分辨率受限于采样点数和采样率如果信号比噪声还弱峰值检测容易抓错。所以FFT测频适合的是“这个信号大概长什么样我不知道但我想知道里面有哪些主要频率成分”的场景比如音频频谱分析、振动检测、电力谐波分析。三种方案的适用性我整理成了表格开发前对着看一眼基本就能确定方向方案原理适用频段典型精度响应速度信号要求输入捕获测周法测相邻边沿的时间差1Hz ~ 数十kHz高低频越准快干净方波、边沿陡峭外部时钟计数法固定闸门内数脉冲数百kHz ~ MHz级高高频越准慢依赖闸门任意TTL脉冲ADCFFT采样后频谱分析ADC采样率/2以内由谱分辨率和插值决定中取决于处理时间任意波形抗噪性强2. 输入捕获测周法从底层原理到HAL库落地如果你决定先做输入捕获方案这一章可以直接“抄作业”。我会从定时器时钟结构开始讲为什么参数要那样配再给出实测可用的代码最后补充几个很容易被文档忽略的坑。2.1 定时器配置里的两个“分频”到底在分什么STM32的定时器框图里有两级频率控制一个是PSC预分频器一个是ARR自动重载值。很多人一开始会搞混PSC决定的是定时器计数时钟Tclk而ARR决定的是计数周期。输入捕获场景下PSC的值直接决定测频分辨率ARR则用来兜底防止计数器溢出。举个例子我在F407上配置TIM2定时器时钟来自APB1定时器时钟84MHzTIM_HandleTypeDef htim2; htim2.Instance TIM2; htim2.Init.Prescaler 84 - 1; // 预分频84分频 - 计数时钟1MHz htim2.Init.CounterMode TIM_COUNTERMODE_UP; htim2.Init.Period 0xFFFFFFFF; // 32位定时器ARR拉满防溢出 htim2.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim2.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_DISABLE; if (HAL_TIM_IC_Init(htim2) ! HAL_OK) { Error_Handler(); }这里的PSC设成84-1是为了让计数时钟变成84MHz / 84 1MHz即每个计数脉冲间隔1微秒。这样一来捕获到的计数器差值直接就是信号周期微秒软件里连换算都省了。有人问为什么不用84MHz直接计数因为那样计数器转得太快32位计数器虽然能扛住但1MHz信号一个周期只有84个计数分辨率很差而且高计数时钟下中断里的加减法都更容易出错。1MHz计数时钟测1kHz信号一个周期1000个计数理论上可以分辨0.1%的频率变化这对绝大多数嵌入式场景足够了。2.2 捕获通道初始化边沿触发、滤波和中断缺一不可配置完定时器基础参数还要配置捕获通道。以通道1为例TIM_IC_InitTypeDef sConfigIC; sConfigIC.ICPolarity TIM_INPUTCHANNELPOLARITY_RISING; // 上升沿触发捕获 sConfigIC.ICSelection TIM_ICSELECTION_DIRECTTI; // 直接映射到TI1 sConfigIC.ICPrescaler TIM_ICPSC_DIV1; // 不分频每次边沿都捕获 sConfigIC.ICFilter 0x04; // 输入滤波去毛刺 if (HAL_TIM_IC_ConfigChannel(htim2, sConfigIC, TIM_CHANNEL_1) ! HAL_OK) { Error_Handler(); }ICPolarity决定捕获边沿如果信号反相或者你想测负脉冲宽度可以改成下降沿或者用双沿捕获配合PWM输入模式一次把周期和占空比都拿到。ICSelection保持默认的DirectTI即可这是直接把引脚信号映射到捕获通道边沿检测之后还有一路可选的Prescaler它不是时钟分频而是“每隔N个边沿才触发一次捕获”适合信号频率太高、中断压力大的场景。ICFilter输入滤波是个很容易被忽略的参数。它内部是一个数字滤波器对引脚信号做连续采样只有连续采样到N次有效电平后才认为边沿有效。这个设计非常适合抑制抖动和毛刺。但注意滤波会引入信号延迟频率很高时不要开太大。比如测1MHz方波滤波器延迟如果达到几百纳秒测出来的周期误差就会比较明显。我在实际项目里一般先用0x04这个档位起步如果信号本身来自光耦或施密特触发器输出可以不开滤波直接捕获。2.3 中断与溢出处理32位计数器也必须防溢出输入捕获最怕的就是计数器溢出。假设你测一个10Hz的超低频信号周期是100ms1MHz计数时钟下周期对应100000个计数虽然32位计数器能装下但你根本不知道这100000个计数里是否发生过溢出。即使ARR设成0xFFFFFFFF4294967295在1MHz时钟下从0数满需要4294秒也就是约71分钟看起来很长可如果你换用84MHz直接计数从0数满只需要51秒。低频率、高频时钟、长时间测量组合起来溢出概率并不低。所以正确做法是在捕获中断里处理周期差时先检查有没有发生更新事件溢出中断再用“差值 溢出次数 * (ARR1)”的方式还原真实周期volatile uint32_t g_capture_val 0; volatile uint32_t g_last_val 0; volatile uint32_t g_diff 0; volatile uint32_t g_overflow_cnt 0; volatile uint32_t g_freq_hz 0; void HAL_TIM_IC_CaptureCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2 htim-Channel HAL_TIM_ACTIVE_CHANNEL_1) { uint32_t now HAL_TIM_ReadCapturedValue(htim, TIM_CHANNEL_1); if (now g_last_val) { g_diff now - g_last_val; } else { g_diff (0xFFFFFFFF - g_last_val) now 1; } g_diff g_overflow_cnt * (0xFFFFFFFF 1UL); g_overflow_cnt 0; g_last_val now; if (g_diff 0) { g_freq_hz 1000000UL / g_diff; // 计数时钟1MHz周期单位为us } } } void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim) { if (htim-Instance TIM2) { g_overflow_cnt; } }这段代码我把更新中断和捕获中断分开了更新中断里只做溢出计数捕获中断里做差值计算和溢出补偿。有两点要特别注意第一更新中断里不应该做任何重型逻辑因为定时器溢出频率可能很高尤其是低ARR高时钟下中断里写个变量都嫌慢第二捕获中断里g_overflow_cnt要先清零再取走如果先取走再清零可能丢掉一个刚发生的溢出导致周期偏大。这个先后顺序我用一次“测漂移正弦波频率不准”的教训换来的后面故障排查部分会细说。信号频率的换算用的是1000000 / g_diff因为计数时钟是1MHzg_diff直接就是周期微秒数。注意这里我用整数除法频率低于1kHz时分辨率足够用了如果你测高频信号建议直接用浮点运算或定点放大否则舍入误差会越来越明显。3. FFT频谱测频让单片机“听懂”信号里的频率输入捕获的局限在于它只能处理边沿清晰的方波。一旦信号是正弦波、带谐波或者混着噪声输入捕获基本就废了。这时候FFT才是正解。STM32F4以上型号自带FPU和DSP库FFT运算不是想象中的慢128点实FFT只需要几十微秒级别的时间做实时测频完全没问题。3.1 FFT测频的基本公式采样率、点数和谱分辨率的关系FFT测频前必须搞清楚三个数字的关系采样率fs、FFT点数N、频率分辨率Δf。公式特别简单Δf fs / N比如常用的方案ADC采样率设成25600Hz做256点FFT那么频率分辨率就是25600 / 256 100Hz。这意味着频谱里两个相邻频点之间相差100Hz你只能分辨100Hz级别的频率变化。如果被测信号是1000Hz它能落在精确的频点上读数漂亮但如果是1050HzFFT结果会“糊”在两个相邻频点之间峰值不明显直接找最大值的误差可能到几十赫兹。想要提高分辨率有两条路提高N或者降低fs。提高N意味着更大的缓冲区、更长的采样时间、更慢的实时性降低fs则受奈奎斯特限制——被测信号的最高频率不能超过fs/2。所以实际工程里往往是先估信号的大概范围再定采样率和点数。我的默认组合是fs25600Hz、N256适合测100Hz到10kHz左右的信号频率分辨率100Hz但如果只做显示粗测够用做高精度测量时我会改用fs40960Hz、N1024分辨率40Hz采样时间25ms实时性依然能接受。3.2 用DSP库输出频谱从ADC采样到找峰值的完整流程软件上我用STM32F4的DSP库核心是arm_cfft_f32和arm_cmplx_mag_f32。STM32的DSP库做FFT不是直接吃ADC的原始值而是需要先把实序列构造成复数形式即数组奇数位放实部偶数位放虚部虚部填0然后调用FFT再求模值。一个最简可用的流程如下#include arm_math.h #define FFT_SIZE 256 #define ADC_SAMPLE_RATE 25600.0f float32_t fft_input[FFT_SIZE * 2]; // 复数数组偶数位实部奇数位虚部 float32_t fft_output[FFT_SIZE]; // 模值结果 uint16_t adc_buffer[FFT_SIZE]; // ADC DMA缓冲区 void FFT_Process(void) { // 1. 将ADC原始数据填入FFT输入数组并做加窗 for (int i 0; i FFT_SIZE; i) { fft_input[2 * i] (float32_t)adc_buffer[i] - 2048.0f; // 去掉直流分量 fft_input[2 * i 1] 0.0f; } // 2. 执行256点CFFT arm_cfft_f32(arm_cfft_sR_f32_len256, fft_input, 0, 1); // 3. 计算幅值谱 arm_cmplx_mag_f32(fft_input, fft_output, FFT_SIZE); // 4. 找到最大幅值的频点跳过直流分量索引0 uint32_t max_index 1; float32_t max_value 0.0f; for (int i 1; i FFT_SIZE / 2; i) { if (fft_output[i] max_value) { max_value fft_output[i]; max_index i; } } // 5. 换算频率频点索引 * 分辨率 float32_t freq (float32_t)max_index * ((float32_t)ADC_SAMPLE_RATE / FFT_SIZE); }代码里有几个细节我强调一下ADC原始值是12位的范围0~4095直流偏置大约在2048所以先减2048再做FFT否则直流分量会霸占频谱里的最大幅值导致峰值检测永远找到0Hz遍历找峰时从索引1开始同样是为了跳过直流只用到N/2个频谱点是因为实信号FFT结果是对称的前一半就包含了全部有效信息。3.3 想让FFT测得更准窗函数和频谱插值缺一不可直接取最大幅值频点的做法频率误差很容易超过一个分辨率档位。上面说了如果信号频率不是FFT频点的整数倍能量会泄露到多个相邻频点峰值点附近会出现“平顶山”形状而不是干净的尖峰。这个时候有两个改进手段。第一个是加窗函数最常用的是汉宁窗Hann。窗函数的作用是减小频谱泄露让信号能量尽量集中代价是峰值幅度会降低、主瓣变宽。加窗方法是在FFT之前把每个采样点乘以窗系数窗表可以提前算好存成常量数组也可以用DSP库的arm_win_hann_f32函数现算。实测下来加窗之后找最大频点通常能准确命中真实频率附近。第二个是频谱插值也叫“频率细分”。原理是找到峰值频点index和它相邻两个频点的幅值通过二次抛物线拟合出真实的峰值位置从而恢复出准确频率。抛物线插值的公式不复杂工程上可以直接用float32_t FreqInterpolation(float32_t *mag, uint32_t peak_index, float32_t df) { float32_t y1 mag[peak_index - 1]; float32_t y2 mag[peak_index]; float32_t y3 mag[peak_index 1]; float32_t d (y3 - y1) / (2.0f * (2.0f * y2 - y1 - y3)); return ((float32_t)peak_index d) * df; }这个式子的思路是真实峰值位置相对于离散峰值点的偏移量d可以通过左右幅值的不对称程度估计出来。加窗后用这个公式可以把频率误差从一个分辨率档位比如100Hz压缩到几个Hz以内。我自己在做音频测频时用fs40960、N1024、加汉宁窗抛物线插值实测1000Hz信号的误差能控制在±2Hz内这已经超过很多人对“单片机FFT测频”的预期了。4. 实操中的经典翻车现场与排查速查手册写完代码跑起来大概率不会一次就成功。我把做这个项目时踩过的坑和网友私信里问得最多的问题整理成了一张可查的列表遇到症状先别怀疑人生多数问题下面都能对号入座。4.1 输入捕获常见问题频率跳动、溢出中断卡死、边沿误触发现象一低频测得好好的换到几十kHz方波后频率跳来跳去。这个问题多半出在输入捕获的Prescaler上。TIM_ICPSC_DIV1意味着每个边沿都触发捕获中断几十kHz方波每秒中断几万次CPU可能忙于响应中断主循环被拖垮测量结果也出现节拍错乱。解法是适当增大ICPrescaler到DIV2或DIV4或者改走DMA接收捕获值把中断次数降下来。现象二程序跑一段时间后进HardFault或定时器中断不进。我在用TIM2做32位捕获时踩过一次更新中断里我顺手加了一句打印日志结果每次溢出中断都去执行串口发送发送阻塞导致中断嵌套最后栈爆了进HardFault。中断处理函数里绝对不能做阻塞操作日志、延时、耗时计算都要挪到主循环里做中断里只保存数据和置标志位。现象三信号明明很干净但捕获脉冲宽度偶尔飘一个极端值。这大概率是引脚上的振铃或串扰触发了额外的边沿。排查时可以先用示波器看引脚波形如果边沿有毛刺把ICFilter调到更高档位或者在上游加一个施密特触发器缓冲。ICFilter调到0x0F后对几MHz内的信号仍然足够快还能滤掉高频噪声。4.2 FFT测频失灵直流淹没、峰值误判、缓冲区撕裂现象一信号明明是1kHzFFT结果总是0Hz附近。这是直流分量没去掉。ADC原始值是0~4095均值大概在2048左右FFT之后0Hz处会有一个巨大的直流峰找最大值永远落在第0个频点。解法就是采样值先减2048对12位ADC而言或者减掉实际均值再做FFT。现象二峰值在两个频点之间来回跳。这是频率没有落在整数频点上直接找最大值自然会在相邻频点间摆动。解法是上窗函数加抛物线插值把峰值位置细分到小数位置。如果峰值还是跳可以增大FFT点数提高分辨率。现象三FFT结果不是信号的基波而是它的谐波。如果信号是带失真的方波三次谐波幅值可能有时盖过基波找最大值就会抓错。处理办法是加窗之外再在找峰逻辑里做“基波优先”判断——不一定选最大值而是选某个阈值以上、频率最低的那个峰值也可以在预处理阶段过一遍带通滤波器先滤掉谐波。下面把这个排查过程整理成速查表症状可能原因处理办法捕获值偶尔跳变边沿毛刺/串扰调高ICFilter上游加整形电路高频信号测不准中断频率过高增加ICPrescaler改用DMA捕获运行一段时间死机中断里做了阻塞操作中断只存数据逻辑放主循环FFT找到0Hz直流分量未去除采样值先减均值/直流偏置峰值频点不稳定频率不在整频点上加窗函数抛物线插值抓到谐波而非基波非正弦信号谐波幅值高找最小频率峰或前置滤波ADC数据不连续DMA和FFT共用了缓冲区采用双缓冲DMA交替切换4.3 信号本身带了较大直流偏置FFT和捕获都不准怎么办这个场景经常出现在电机驱动、电源纹波测量中——信号是AC小信号叠加在一个DC偏置上。输入捕获方案里如果直流偏置太高导致电平一直高于触发电平边沿可能根本无法触发FFT方案里直流分量不除掉会压制其他频段的幅值。对FFT除了采样值减均值还有一个更稳的预处理方法用DSP库的arm_mean_f32先算出均值再逐点减去。如果信号里直流分量在缓慢漂移那么对整段数据减去均值会有残留可以改用高通滤波器比如一阶IIR高通截止频率设在信号最低频率的十分之一以下把直流去掉但不伤信号。对输入捕获直流偏置影响了触发电平解决方向不是软件而是硬件。用最简单的阻容耦合电路串联电容偏置电阻把信号搬到触发电平附近或者在信号链上加个比较器把任意波形整形成方波再送进定时器。这也是为什么很多测频模块的硬件接口都带比较器芯片的原因。5. 把测频精度再往上推两个你值得试的扩展方向到这一步输入捕获和FFT两套方案都能稳定工作了。但如果你手头还有时间我强烈建议在下面两个方向上再做一点扩展它们能让这套测频代码从“玩具”变成“工具”。第一个是双缓冲DMA配合FFT连续测频。文章前面的代码里用的是单缓冲ADC采完一轮DMA传输完再在中断里触发FFT。这样带来的后果是采样期间没法同时做FFT等FFT跑完才能开始下一轮采样测频实时性被打折扣。改进方法是让ADC的DMA工作在双缓冲模式DMA在缓冲A和缓冲B之间来回切换ADC数据写入缓冲A时CPU可以对缓冲B的上一轮数据做FFT。这样采样和计算流水线并行刷新率直接翻倍。在STM32的HAL库里开启DMA双缓冲只需要调用HAL_ADCEx_MultiModeStart_DMA并配置好两个缓冲区注意缓冲区大小必须一致且两个缓冲区的地址要对齐到4字节。第二个是把测周法和FFT方案融合成一个自适应测频器。我自己的应用场景是这样的给一个测速装置做升级信号有时候是编码器输出的方波有时候是旋变解码后的正弦波两种来源交替出现单一方案都会翻车。后来干脆做了一层逻辑设一个粗测阶段先用输入捕获快速测一遍如果捕获中断频繁触发且周期稳定说明是干净方波就走测周法输出高精度频率如果捕获异常或者边沿不稳定说明信号可能是正弦波或带谐波于是切换ADC采样路径走FFT测频。这样两种方案的优点都保住了代价只是多占了几个定时器和一段DMA缓冲区换来的是对信号形态的“自适应”能力。这个思路特别适合做毕业设计或者产品原型不用再纠结“被测信号到底是方波还是正弦波”交给算法自己判断。实测中切换判断的阈值我一般设为捕获周期标准差小于平均周期的5%时走测周法否则走FFT。判据简单粗暴但非常有效。提示做FFT测频时ADC的采样率并不是越高越好。采样率越高同样的FFT点数下频率分辨率越差。很多新手拿着1MHz采样率测1kHz信号256点FFT的分辨率接近4kHz什么都测不出来。正确做法是先根据被测信号的频率上限定采样率再根据需要的分辨率定点数。这两个参数永远是联动的。6. 测频之外的事信号调理决定了上限写到最后想额外提一个很多教程不讲的点代码再漂亮信号进不了单片机也是白搭。STM32单片机的PA0~PA15等引脚输入电平范围是0~3.3V部分引脚5V容忍如果你的被测信号是±5V的正弦波或者带很强共模干扰的电机驱动信号直接焊根杜邦线到定时器输入引脚上轻则测不准重则烧引脚。所以正规的做法是在信号进入单片机之前加一级信号调理。对TTL方波加一个施密特触发器比如74HC14既可以整形边沿又可以去毛刺对模拟正弦波先经过运放做电平搬移把信号中心搬到1.65V左右再用一个比较器把它变成方波送进定时器——但既然你都做FFT了模拟信号就别整形成方波了直接做ADC采样就行。对高压信号先经过电阻分压或隔离运放再送ADC。输入捕获和FFT测频的下限很多时候不是由FFT算法决定的而是由你的硬件信号链决定的。我见过一个最典型的案例一位朋友做车载BMS模块的纹波测频FFT程序调了一星期频率结果总是间歇性跳变最后发现是采样输入脚和电机驱动线在同一个排针上电机一转共模噪声全灌进了ADC采样引脚。后来加了一级差分放大和RC低通滤波频率读数立刻稳定。所以说做测频这类的项目软件方案的选择固然重要但硬件信号链的处理同样不能省。输入捕获测周法、外部时钟计数法、FFT频谱分析法本质上是站在不同角度回答同一个问题——信号到底有多快。理解每一种方法在哪一段频率范围内最好用再根据信号形态选好采样链路然后把代码里的中断、DMA、窗函数这些细节逐个打磨到位你手里的STM32就能变成一台不折不扣的便携频率计。这个项目做完以后再遇到测频的需求我不会先想到拿示波器而是会先反问一句这信号是方波还是正弦波是要看单频还是要看频谱回答完这两个问题方案自己就出来了。
返回列表