1. 项目概述为什么在STM32上同时用输入捕获和FFT测频不是“多此一举”而是“刚性需求”你手头有个STM32F407开发板想测一个正弦波信号的频率——第一反应肯定是打开定时器的输入捕获功能测周期再倒数简单直接。我当年也是这么干的直到客户把一台变频器输出端的电流波形甩到我面前基波50Hz但叠加了大量6k、12k、18kHz的开关谐波还带着随机毛刺。用传统输入捕获一测显示“49.8Hz”可示波器上明明能看到明显的高频振荡。问题出在哪输入捕获只认“边沿”它不管信号是不是纯正弦只要边沿够陡、间隔够稳就给你报个“平均周期”。可现实中的工业信号、电机电流、音频采样、电源纹波从来不是教科书里的理想波形。这就是为什么标题里要并列写上“输入捕获”和“FFT”——它们根本不是替代关系而是分层协作的搭档。输入捕获是“守门员”负责在毫秒级时间尺度上快速锁定信号是否存在、主频是否落在合理区间比如判断电机是否启动、传感器是否掉线而FFT是“显微镜”在微秒级采样窗口内把一段时域波形拆解成几十甚至上百个频率分量告诉你50Hz旁边那个-25dB的11.3kHz峰到底是IGBT开关噪声还是轴承早期磨损的特征频率。我在做一款智能电表的谐波分析模块时就靠这套组合拳把误报率从17%压到了0.3%输入捕获先确认工频在49~51Hz之间才触发ADC连续采样1024点送进FFT引擎否则直接报“信号异常”省下宝贵的CPU周期去处理其他任务。关键词“STM32”、“输入捕获”、“FFT”、“测频”在这里不是孤立标签而是构成了一条完整的嵌入式信号分析链路硬件触发输入捕获→ 高精度采样ADCDMA→ 实时计算FFT→ 多维度判据幅值/相位/谐波畸变率。它不适用于“测个方波频率”的课设场景但恰恰是工业现场、电力监控、电机诊断、声学传感这些对信号质量有硬性要求领域的刚需。如果你的项目涉及变频器、伺服驱动、光伏逆变器、或者任何需要区分“基波”和“干扰”的场合这个组合就是绕不开的技术栈。下面我就以STM32F407为蓝本把这条链路上每个环节的坑、参数选择的依据、实测效果全盘托出。2. 核心思路拆解为什么必须“先捕获后FFT”而不是直接FFT2.1 输入捕获不只是测周期更是系统级的“事件调度器”很多人把输入捕获理解成“测频率的快捷键”这大大低估了它的价值。在STM32F4系列中一个高级定时器如TIM1或TIM8的输入捕获通道本质是一个硬件级的异步事件检测器。它不经过CPU不消耗指令周期只要信号电平满足配置上升沿/下降沿/双边沿就能在纳秒级时间内锁存计数器当前值并置位中断标志。这个能力在实时性要求苛刻的系统中是软件轮询永远无法替代的。我做过一个对比实验用GPIO轮询方式检测一个10kHz方波的上升沿最短能稳定捕获的脉宽是8.3μs对应120kHz极限而用TIM1的CH1输入捕获同样信号下能稳定捕获到50ns宽度的窄脉冲。差距在哪轮询依赖于CPU执行一条条指令中间夹杂着中断响应、总线等待而输入捕获是定时器外设内部的纯硬件逻辑与CPU完全解耦。这意味着什么当你需要在电机启动瞬间捕捉第一个过零点来同步PWM或者在电网故障时记录暂态电压跌落的精确起始时刻输入捕获就是唯一的可靠选择。更重要的是它能帮你规避FFT计算的“盲区”。FFT要求输入数据是“等间隔采样”的而ADC的采样时钟如果和被测信号不同源就会产生频谱泄露。比如你用系统时钟分频得到的100kHz采样率去采一个50.01Hz的工频信号1024点采样窗口实际覆盖的时间是10.24ms但50.01Hz信号在这段时间内完成了512.1024个完整周期——最后那0.1024个周期被强行截断FFT结果里就会在50Hz主峰两侧出现虚假的“栅栏效应”旁瓣。而输入捕获可以作为触发源让ADC在信号的某个确定相位点比如第N个上升沿开始采样实现“同步采样”把频谱泄露控制在可接受范围内。这在电力谐波分析标准IEC 61000-4-7中是强制要求。2.2 FFT不是“算得快就行”而是“算得准才有用”提到STM32上的FFT很多人第一反应是CMSIS-DSP库里的arm_cfft_f32()函数。没错它封装了Cooley-Tukey算法调用方便。但如果你直接拿它处理原始ADC数据大概率会得到一堆“看起来很热闹实际没法用”的频谱图。问题出在三个常被忽略的环节第一数据预处理。ADC采集的原始数据是带直流偏置的比如Vref3.3V的12位ADC测0~3.3V信号理想情况下0V对应0x0003.3V对应0xFFF。但实际电路有运放失调、PCB走线耦合采集到的“静止信号”可能均值是1.68V对应0x820。这个直流分量在FFT后会变成0Hz处的一个巨大尖峰把真正关心的交流分量淹没。所以必须先做“去直流”DC Removal最简单有效的是滑动平均滤波用一个长度为N的FIFO缓存最近N个采样点实时计算均值然后从每个新点中减去该均值。N的选择有讲究——太小如N8滤不干净低频漂移太大如N1024会导致动态响应迟钝。我实测下来对于50Hz工频信号N256是个平衡点既能滤除0.2Hz以下的温漂又不会让系统对突加负载的响应延迟超过5ms。第二窗函数选择。FFT假设输入信号是周期延拓的但实际截取的是一段有限长数据。这个“突然截断”等效于乘了一个矩形窗其频域响应是一个sinc函数导致能量向邻近频率泄漏。加窗就是为了压制这个sinc旁瓣。常用窗函数里汉宁窗Hanning主瓣宽、旁瓣衰减快-31dB适合分辨频率相近的强信号布莱克曼窗Blackman旁瓣更低-58dB但主瓣更宽会降低频率分辨率。我做过一个实验用两个幅度相差20dB、频率差仅5Hz的正弦波合成信号分别用矩形窗、汉宁窗、布莱克曼窗处理。结果矩形窗下两个峰完全粘连汉宁窗能勉强分开但弱信号峰被强信号的旁瓣污染只有布莱克曼窗清晰分离出两个独立峰值。所以如果你的任务是检测微弱的故障特征频率比如滚动轴承内圈缺陷产生的1234.7Hz冲击布莱克曼窗几乎是必选项。第三幅值校准。CMSIS-DSP的FFT输出是复数数组模值代表各频率点的幅值但这模值和真实物理量如电压伏特之间没有直接换算关系。它受ADC参考电压、运放增益、窗函数能量衰减系数共同影响。比如汉宁窗的能量衰减系数是0.375意味着信号总功率被窗函数削弱了62.5%。如果不补偿FFT计算出的50Hz分量幅值会比真实值小一半还多。校准方法很简单用一个已知幅值如1Vpp、已知频率如1kHz的标准正弦信号注入系统记录FFT输出的最大模值M_cal那么后续所有测量中真实幅值 (FFT模值) × (1.0 / M_cal) × (窗函数补偿系数)。这个标定步骤我见过太多人跳过结果整个系统的谐波失真度THD计算误差高达±15%完全失去工程意义。2.3 二者协同构建一个“感知-决策-执行”的闭环把输入捕获和FFT割裂开看就像只看到发动机和变速箱却忽略了它们如何配合让汽车平稳起步。在我们的设计中它们共同构成了一个微型的“信号感知中枢”感知层输入捕获TIM2的CH2通道配置为上升沿捕获连接到信号调理电路的比较器输出。一旦检测到有效边沿立即锁存TIM2计数器值计算与上次捕获的时间差得到粗略周期T_coarse判断T_coarse是否在预设的有效范围如15ms~25ms对应40~66Hz若否置位“信号异常”标志跳过后续FFT若是则生成一个“采样使能”脉冲触发ADC1开始转换。决策层FFTADC1配置为TIM2的TRGO事件触发采样深度1024点采样率由TIM2的更新事件Update Event精确控制。当1024点DMA传输完成触发DMA中断在中断服务程序中将DMA缓冲区数据复制到FFT输入缓冲区执行去直流、加布莱克曼窗、调用arm_cfft_f32()计算各频率点模值找出最大值对应的索引index_max其物理频率 index_max × (采样率 / 1024)同时计算0Hz直流分量、50Hz基波分量、以及2~50次谐波的幅值用于计算THD。执行层应用逻辑主循环中检查FFT计算完成标志若基波频率偏离50Hz超过±0.5Hz且持续3个周期触发“频率异常告警”若5次谐波幅值超过基波的15%触发“谐波超标”告警将所有关键参数基波频率、THD、各次谐波幅值通过UART发送至上位机。这个流程里输入捕获是“哨兵”确保FFT只在信号健康时才启动避免无效计算浪费资源FFT是“分析师”提供远超周期测量的丰富信息。二者缺一不可。我曾尝试过只用FFT结果在信号短暂中断又恢复时FFT窗口内混入了中断前后的数据计算出完全错误的频谱导致系统误动作。加上输入捕获的“健康检查”后稳定性提升了一个数量级。3. 核心细节解析与实操要点从寄存器配置到代码陷阱3.1 输入捕获的魔鬼细节边沿同步、滤波与抗抖动配置STM32的输入捕获绝不是设置几个寄存器那么简单。很多初学者照着例程改了几个参数发现捕获值跳变剧烈以为是代码bug其实是没吃透硬件特性。这里我把踩过的坑和实测有效的方案全列出来。首先CCER寄存器里的CCxP和CCxNP位决定了捕获的极性。很多人只设CCxP输入捕获极性却忽略了CCxNP互补输入捕获极性。在高级定时器TIM1/TIM8中CH1和CH1N是互补通道。如果你把信号同时接到CH1和CH1N又只使能CH1的捕获那么当信号有轻微不对称时比如上升沿快、下降沿慢CH1捕获的时刻会随占空比变化而漂移。正确的做法是只接一个通道比如CH1并将CCxNP清零确保只响应单一极性。我用示波器实测过同一信号下只使能CH1捕获1000次测量的标准差是1.2个计数器tick而错误地同时使能CH1和CH1N标准差飙升到8.7个tick。其次SMCRSlave Mode Control Register里的SMS位决定了定时器的工作模式。这是极易被忽略的关键。如果你希望TIM2纯粹作为一个“自由运行的计数器”用来测量任意两个边沿之间的时间那就把SMS设为“000”即“Disable”模式。但如果你希望TIM2能被外部信号重置比如每来一个信号上升沿就让TIM2计数器归零从而直接读出周期就必须配置为“Reset Mode”SMS100。我最初没设SMS结果每次捕获到的都是从程序启动开始累积的总时间而不是相邻边沿的间隔。改完之后用逻辑分析仪抓波形捕获值与示波器光标测量值误差稳定在±2ns以内。第三输入滤波器ICF[3:0]的设置是抗干扰的核心。STM32的每个输入捕获通道都内置了一个数字滤波器可以抑制高频噪声。ICF值不是越大越好。它的原理是在检测到一个电平跳变后连续N个系统时钟周期NICF1内都检测到相同的电平才认为这是一个有效边沿。比如系统时钟为168MHzICF0xF即15那么需要16个时钟周期约95ns的稳定电平才能触发。这能完美滤除10MHz的毛刺但也会让有效信号的边沿响应延迟95ns。对于测100kHz以上高频信号这个延迟可能导致相位测量误差。我的经验是对于50Hz工频ICF0x89个周期53ns延迟足够对于10kHz信号ICF0x23个周期18ns延迟更合适而测100kHz方波时ICF必须设为0x0完全关闭滤波靠外部硬件RC滤波解决。最后也是最致命的陷阱捕获中断的“双缓冲”问题。当你使能了捕获中断CCxIE并且信号频率很高时可能出现“中断还没处理完下一个边沿又来了”的情况。此时新的捕获值会覆盖旧的CCR寄存器内容导致丢失一次捕获。解决方案是启用“捕获/比较预装载寄存器”CCxNE。在初始化时设置CCMRx寄存器的CCxS位为“01”ICx映射到TIx并设置OCxM位为“100”冻结模式虽然我们不用输出但这个位会影响预装载行为最关键的是设置CCER寄存器的CCxNE位为1。这样每次捕获的值会先存入一个影子寄存器只有在你读取CCR寄存器时才自动转移到主寄存器避免了覆盖。我在调试一个20kHz PWM信号的占空比测量时没开CCxNE丢点率高达12%开了之后10万次捕获无一丢失。3.2 ADC与DMA如何让1024点采样“零CPU干预”FFT计算需要大量数据如果靠CPU一个一个读取ADC_DR寄存器效率极低且无法保证严格的等间隔采样。必须用DMA。但DMA配置不当轻则数据错乱重则系统死机。以下是经过千次实测验证的配置要点。ADC时钟源与采样时间SMP的匹配是基础。STM32F407的ADC最大工作频率是36MHz。如果APB2时钟是84MHz那么ADC预分频器必须设为2分频ADCPRE00才能得到42MHz的ADCCLK再经内部再分频得到36MHz。采样时间SMP的选择取决于信号源的阻抗。公式是采样时间 ≥ 1.5 (R_source / R_sample) × C_sample。其中R_sample是ADC内部采样电阻典型值4.5kΩC_sample是采样电容典型值15pF。如果你的信号来自一个高阻运放R_source100kΩ那么SMP至少需要1.5 (100000/4500)×15 ≈ 335个ADCCLK周期。在12位精度下SMP位域最大值是0x0F239.5周期显然不够。这时必须外加一个RC低通滤波器比如1kΩ10nF把R_source降到1kΩ以下SMP才能设为0x0723.5周期满足高速采样需求。我第一次没加这个RC采样数据里全是高频噪声FFT结果一片雪花。DMA的Circular Mode循环模式是双缓冲的基石。很多人用Normal Mode普通模式DMA传输完1024点就停止需要手动重启。这在高频率下必然导致采样间隙。正确做法是开启Circular ModeDMA_SxCR寄存器的CIRC位。这样DMA会把内存地址指针在1024个字节的缓冲区内无限循环。关键在于你要分配两倍大小的缓冲区2048字节并用HAL库的HAL_ADC_Start_DMA()函数指定传输方向为HAL_ADC_SINGLE_BUFFER但实际使用时把缓冲区首地址传给DMA然后在DMA传输完成中断TCIF里通过检查DMA_SxNDTR寄存器的剩余数据数来判断当前正在填充的是前半区还是后半区。例如当NDTR512时说明前512点已填满后512点正在写入当NDTR0时说明整个1024点已满可以安全地将前512点拷贝出去做FFT而后512点继续接收新数据。这种“乒乓缓冲”机制实现了真正的无缝采样。触发源EXTSEL的精准选择决定同步成败。ADC的触发源必须与输入捕获的定时器严格关联。比如你用TIM2的CH2做输入捕获那么ADC的EXTSEL应该选择“TIM2 TRGO”。而TIM2的TRGO信号又由TIM2的更新事件UEV产生。所以你需要配置TIM2的ARR自动重装载值为一个固定值比如10000这样每10000个计数器tick就产生一次UEV作为ADC的采样时钟。这个ARR值结合TIM2的时钟频率就决定了最终的采样率。例如TIM2时钟为84MHzARR8399那么采样率 84000000 / 8400 10kHz。这个计算必须精确到整数否则会产生采样时钟抖动引入额外的频谱噪声。3.3 FFT计算CMSIS-DSP库的“正确打开方式”CMSIS-DSP是ST官方提供的优化库但它不是“开箱即用”的魔法盒。很多开发者抱怨“FFT结果不准”根源往往在初始化和数据准备阶段。第一步初始化FFT实例arm_cfft_instance_f32。这个结构体必须在全局作用域声明并用arm_cfft_f32_init()函数初始化。关键参数是fftLen它必须是2的幂次如1024, 2048。很多人在局部函数里声明这个实例导致栈溢出或未初始化。正确做法是// 全局定义 arm_cfft_instance_f32 S; float32_t fftInput[2048]; // 1024个复数实部虚部交替存放 float32_t fftOutput[2048]; // 在main()开头初始化 arm_cfft_f32_init(S, 1024);第二步数据格式转换。CMSIS-DSP的arm_cfft_f32()函数要求输入是“交错复数”格式[real0, imag0, real1, imag1, ..., real1023, imag1023]。而你的ADC数据是纯实数数组[sample0, sample1, ..., sample1023]。所以必须先做转换把每个sample[i]赋值给fftInput[2*i]实部fftInput[2*i1]虚部设为0。这个循环不能用memcpy必须逐点赋值因为内存布局完全不同。我曾用memcpy(fftInput, adcBuffer, 1024*4)结果FFT输出全是0查了3小时才发现是数据格式错了。第三步执行FFT与后处理。调用arm_cfft_f32(S, fftInput, 0)后结果仍在fftInput数组中原地计算。接着必须调用arm_cmplx_mag_f32(fftInput, fftOutput, 1024)来计算每个复数的模值得到fftOutput[i] sqrt(real_i² imag_i²)。这才是你真正要分析的“幅频特性”。注意fftOutput[0]是直流分量fftOutput[1]对应最低频率f_s/1024fftOutput[512]对应奈奎斯特频率f_s/2。由于实数序列FFT的共轭对称性fftOutput[513]到fftOutput[1023]是冗余的可以忽略。第四步频率索引到物理频率的换算。这个公式必须刻在脑子里f_physical index × (f_sample / N)。其中f_sample是你通过TIM2配置的实际采样率N是FFT点数1024。例如f_sample10kHz那么index10对应10 × (10000/1024) ≈ 97.66Hz。这个换算必须在代码里用浮点运算完成不能用整数除法否则会丢失精度。我见过有人写freq index * 10000 / 1024结果所有频率都向下取整50.1Hz被算成50Hz导致谐波分析完全失效。提示在调试FFT时务必先用一个已知的纯正弦波如函数发生器输出1kHz进行验证。观察fftOutput数组你应该能看到一个尖锐的峰值在index102.4附近因为10000/1024≈9.7661000/9.766≈102.4周围是接近0的噪声。如果看到多个宽峰或者峰值位置飘忽一定是前面的数据采集或预处理环节出了问题。4. 实操过程与核心环节实现从CubeMX配置到实测波形4.1 CubeMX图形化配置三步搞定硬件外设联动STM32CubeMX是现代STM32开发的起点但很多教程只教你怎么点按钮没告诉你为什么这么点。下面是我基于STM32F407ZGT6芯片从零开始配置输入捕获FFT测频的完整流程每一步都附带原理说明。第一步配置RCC与系统时钟。这是所有外设的根基。选择HSE外部晶振为8MHzPLL配置为PLLM8,PLLN336,PLLP2这样系统主频SYSCLK达到168MHz。为什么选168MHz因为这是F407的最高主频能为FFT计算提供最大算力。同时确保APB1低速总线和APB2高速总线的分频器都设为1让TIM2、ADC、DMA都运行在最高频率减少等待周期。如果APB1分频为2TIM2的时钟就只有84MHz会限制最高可测频率。第二步配置TIM2为输入捕获。在“Pinout Configuration”页找到TIM2点击进入配置界面。Clock Source:选择“Internal Clock”即TIM2自己产生计数时钟。Prescaler (PSC):设为0即不分频TIM2计数器时钟 APB1时钟 168MHz / 2 84MHz因为APB1分频为2TIM2时钟会自动倍频。Counter Period (ARR):设为0xFFFF65535这是一个足够大的值防止计数器溢出。实际测量周期时我们只关心两次捕获值的差值ARR大小不影响精度。Channel 2:设置为“Input Capture Direct Mode”这是最关键的它意味着CH2引脚的信号直接进入捕获逻辑不经过任何预分频或滤波滤波在后面单独配置。Input Capture Prescaler:设为“CK_INT”不分频确保最高时间分辨率。Input Filter:设为“15”即ICF0xF这是为了兼容各种信号后续可根据实测效果调整。GPIO Settings:确保CH2引脚PA1的GPIO模式为“Alternate Function Push-Pull”速度设为“High”。第三步配置ADC1与DMA。找到ADC1进入配置。Mode:选择“Independent mode”因为我们只用一个ADC。Resolution:“12 bits”这是F407的最高精度足够满足大多数测频需求。Data Alignment:“Right alignment”这是默认便于后续处理。Scan Conversion Mode:必须勾选因为我们通常要采样多个通道比如同时采电压和电流即使现在只用一个通道也要开启扫描模式否则DMA无法正常工作。Continuous Conversion Mode:必须勾选这是实现连续采样的前提。External Trigger Conversion:这是同步的灵魂在“External Trigger”下拉菜单中选择“TIM2 TRGO”。这意味着ADC的每一次转换都由TIM2的更新事件UEV来触发。DMA Requests:勾选“Enable DMA requests”并设置“DMA Continuous Requests”为Enabled。Channels:添加你要采样的通道如IN0对应PA0设置Sampling Time为“15 Cycles”这是针对中等阻抗信号的稳妥选择。最后生成代码。在“Project Manager”页设置Toolchain为“MDK-ARM v5”勾选“Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral”然后点击“GENERATE CODE”。生成的代码里MX_TIM2_Init()和MX_ADC1_Init()函数已经包含了所有上述配置你只需要在main.c的while(1)循环前添加启动代码HAL_TIM_IC_Start_IT(htim2, TIM_CHANNEL_2); // 启动TIM2 CH2输入捕获中断 HAL_ADC_Start_DMA(hadc1, (uint32_t*)adcBuffer, 1024, HAL_ADC_FORMAT_12_BITS, HAL_ADC_UNIT_1); // 启动ADC DMA这样硬件联动就完成了。CubeMX做的不是“自动化”而是把复杂的寄存器配置翻译成了人类可读的图形界面背后依然是严谨的硬件时序逻辑。4.2 关键代码实现从捕获中断到FFT结果解析有了CubeMX生成的基础框架下面是最核心的业务逻辑代码。我把它拆解成几个独立的函数每个函数只做一件事并附上详细注释。1. 输入捕获中断服务程序TIM2_IRQHandler// 全局变量用于存储两次捕获值 uint32_t captureValue1 0; uint32_t captureValue2 0; uint8_t captureFlag 0; // 捕获完成标志 void TIM2_IRQHandler(void) { uint32_t tmp; /* 获取并清除中断标志 */ if(__HAL_TIM_GET_FLAG(htim2, TIM_FLAG_CC2) ! RESET) { if(__HAL_TIM_GET_IT_SOURCE(htim2, TIM_IT_CC2) ! RESET) { __HAL_TIM_CLEAR_IT(htim2, TIM_IT_CC2); // 读取捕获寄存器 tmp HAL_TIM_ReadCapturedValue(htim2, TIM_CHANNEL_2); if(captureFlag 0) { captureValue1 tmp; captureFlag 1; } else { captureValue2 tmp; // 计算周期注意处理计数器溢出 if(captureValue2 captureValue1) period_us (captureValue2 - captureValue1) * 1000000ULL / 84000000ULL; else period_us ((0xFFFFFFFF - captureValue1) captureValue2 1) * 1000000ULL / 84000000ULL; // 粗略频率判断50Hz对应20000us if(period_us 15000 period_us 25000) { // 触发ADC采样通过软件触发因为硬件触发源是TIM2 TRGO已配置好 HAL_ADC_Start(hadc1); } captureFlag 0; } } } }这段代码的关键在于溢出处理。TIM2是32位计数器当captureValue2 captureValue1时说明计数器已经翻转了一圈。必须用(0xFFFFFFFF - captureValue1) captureValue2 1来计算真实差值。我第一次没处理这个测高频信号时周期值会突然跳变成几秒系统彻底紊乱。2. DMA传输完成中断DMA2_Stream0_IRQHandlerextern float32_t fftInput[2048]; extern float32_t fftOutput[2048]; extern arm_cfft_instance_f32 S; void DMA2_Stream0_IRQHandler(void) { /* 清除DMA中断标志 */ __HAL_DMA_CLEAR_FLAG(hdma_adc1, __HAL_DMA_GET_TC_FLAG_INDEX(hdma_adc1)); // 数据预处理去直流 float32_t dc_offset 0.0f; for(int i0; i1024; i) { dc_offset (float32_t)adcBuffer[i]; } dc_offset / 1024.0f; // 加布莱克曼窗并存入FFT输入缓冲区 for(int i0; i1024; i) { float32_t window 0.42f - 0.5f*cosf(2.0f*PI*i/1023.0f) 0.08f*cosf(4.0f*PI*i/1023.0f); fftInput[2*i] ((float32_t)adcBuffer[i] - dc_offset) * window; // 实部 fftInput[2*i1] 0.0f; // 虚部 } // 执行FFT arm_cfft_f32(S, fftInput, 0); arm_cmplx_mag_f32(fftInput, fftOutput, 1024); // 寻找基波频率在index 95~105范围内搜索对应46~51Hz uint16_t maxIndex 95; float32_t maxValue fftOutput[95]; for(uint16_t i96; i105; i) { if(fftOutput[i] maxValue) { maxValue fftOutput[i]; maxIndex i; } } // 计算物理频率 float32_t baseFreq (float32_t)maxIndex * 10000.0f / 1024.0f; // 采样率10kHz // 计算THD总谐波失真度 float32_t fundamental fftOutput[maxIndex]; float32_t harmonicPower 0.0f; for(uint16_t i2; i50; i) // 计算2~50次谐波 { uint16_t idx i * maxIndex; // 简化计算实际应为i * (f_base / f_res) if(idx 1024 idx 0) harmonicPower fftOutput[idx] * fftOutput[idx]; } float32_t thd sqrtf(harmonicPower) / fundamental * 100.0f; // 将结果通过UART发送 char uartBuf[100]; sprintf(uartBuf, F:%.2fHz, THD:%.2f%%\r\n, baseFreq, thd); HAL_UART_Transmit(huart2, (uint8_t*)uartBuf, strlen(uartBuf), HAL_MAX_DELAY); }这段代码展示了完整的信号处理流水线。其中布莱克曼窗的计算公式是标准的w(n) a0 - a1*cos(2πn/N)