资讯动态

STM32频率测量与相位跟踪实战:从捕获到跟踪的完整工程

发布时间:2026/10/26 11:18:50 来源:尧图企业网站定制
简介这份资源面向嵌入式开发学习者与信号处理方向的工程师聚焦如何借助STM32内置ADC与定时器完成周期性信号的频率检测与相位跟踪适合作为实时控制与信号采集项目的实践参考。压缩包共316个文件以80个h头文件、74个c源文件为核心代码辅以42个o目标文件、42个d依赖文件及40个crf等编译中间产物另有hex、map、axf等烧录与调试输出整体约512KB工程结构完整可直接在Keil环境下打开分析。已有1392人学习下载说明其在同类项目中具备一定参考价值。资源内含定时器捕获配置、ADC采样与中断处理、频率与相位差计算等实现思路读者可据此理解预分频与自动重装载参数的设定方法并借鉴多通道捕获或DMA连续读取来跟踪相位变化进而掌握从采样到结果输出的完整链路为后续优化滤波与系统参数调整打下基础。1. 频率测量与相位跟踪一个能直接跑在 STM32 上的完整工程去年帮一个做电机控制的朋友看板子他卡在一个很具体的问题上编码器反馈的方波频率忽高忽低相位还老是漂PID 调了半天以为是参数问题最后发现是测频环节本身就不稳。这类场景其实很常见——你要测的不是一个静态频率而是一个持续变化、还带噪声的信号光靠输入捕获读一个周期值根本不够用。这份基于 STM32 的频率测量与相位跟踪工程解决的正是这件事从信号进 MCU 开始到频率值稳定输出、相位差持续跟踪整条链路都给了可运行的实现。适合正在做电机反馈、电源同步、锁相环预研或者单纯想把 STM32 定时器捕获玩明白的开发者。下面按「这工程怎么组织 → 怎么跑起来 → 核心代码怎么读 → 怎么改成自己的」拆开讲。2. 工程结构与技术选型定时器捕获加软件跟踪的双层设计2.1 为什么不是单纯读捕获值先把这个工程的设计思路说清楚不然后面看代码会懵。STM32 测频最直接的做法是开一个定时器做输入捕获测两个上升沿之间的计数值换算成频率。这做法在信号干净、频率稳定时没问题但一旦信号带抖动单次测量值就会跳你拿这个值去做控制等于把噪声直接喂进环路。这个工程的做法是两层底层用定时器捕获拿到原始的时间戳序列上层用软件做频率估计和相位跟踪把原始测量里的毛刺滤掉同时输出一个连续的相位量。常见做法是底层捕获 上层滑动平均或一阶滤波这个工程在此基础上加了相位跟踪所以能同时给出频率和相位两个量。技术栈上就是标准的 STM32 裸机工程CMSIS 启动文件、标准外设库或 HAL 库、定时器输入捕获、串口输出调试。没有 RTOS没有复杂中间件好处是时序可控、移植简单坏处是所有调度得自己管。对于测频这种对时序敏感的任务裸机反而是更稳的选择中断里该多快就多快不用担心任务切换带来的抖动。2.2 目录结构怎么读拿到压缩包解压后目录大致是这样组织的不同工程命名略有差异按功能归类看即可FrequencyPhaseTrack/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── tim_capture.h // 定时器捕获接口 │ │ ├── freq_estimate.h // 频率估计 │ │ └── phase_track.h // 相位跟踪 │ └── Src/ │ ├── main.c │ ├── tim_capture.c │ ├── freq_estimate.c │ └── phase_track.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ // 或标准库看具体版本 ├── MDK-ARM/ // Keil 工程文件 └── README.md读这种工程有个习惯先看main.c里的初始化顺序再看中断服务函数最后看算法模块。初始化顺序决定了外设之间的依赖关系中断服务函数决定了数据从哪来、到哪去算法模块才是真正要改的地方。别一上来就扎进算法文件容易看不懂数据是怎么流进来的。2.3 从零跑起来的步骤工程一般带 Keil 或 IAR 的工程文件直接打开编译下载即可。如果工程文件缺失或者你想换到 CubeIDE按下面几步重建# 1. 确认芯片型号工程里通常是 STM32F103 系列 # 在 main.h 或工程配置里能看到具体型号宏定义 # 2. 确认时钟配置测频精度直接依赖定时器时钟 # 常见是 72MHz 系统时钟定时器挂在 APB1/APB2 上 # 3. 确认捕获引脚通常是 TIMx_CH1对应某个 GPIO # 看 tim_capture.c 里的 GPIO 初始化就能找到关键参数有三个定时器预分频PSC、自动重装值ARR、捕获边沿极性。PSC 决定了计数分辨率ARR 决定了最大可测周期。举个例子72MHz 定时器、PSC 设为 71计数频率就是 1MHz一个计数代表 1 微秒测 1kHz 信号时周期计数值是 1000分辨率足够但如果测 100kHz 信号周期只有 10 个计数量化误差就很大了。所以 PSC 要根据你的目标频率范围来定这是第一个要改的地方。提示先把串口调通让工程能往电脑打印原始捕获值和计算出的频率后面所有调试都靠这个口子。没有输出测频出问题就是黑匣子。3. 核心模块拆解捕获、估计、跟踪三段链路3.1 输入捕获拿到原始时间戳底层捕获的逻辑不复杂但细节决定成败。核心是在捕获中断里记录当前计数器的值并处理计数器溢出。下面这段是典型的捕获中断处理骨架// 捕获中断服务函数简化示意 volatile uint32_t capture_buf[2]; // 存相邻两次捕获的计数值 volatile uint8_t capture_idx 0; volatile uint32_t overflow_cnt 0; // 计数器溢出次数 void TIMx_IRQHandler(void) { if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_CC1)) { __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_CC1); // 先读溢出标志再读捕获值顺序不能反 if (__HAL_TIM_GET_FLAG(htim, TIM_FLAG_UPDATE)) { __HAL_TIM_CLEAR_FLAG(htim, TIM_FLAG_UPDATE); overflow_cnt; } uint32_t now __HAL_TIM_GET_CAPTURE(htim, TIM_CHANNEL_1); capture_buf[capture_idx 0x01] now; capture_idx; } }这段代码有两个容易翻车的点。第一溢出标志和捕获值的读取顺序。如果先读捕获值再读溢出标志可能在两者之间刚好发生溢出导致时间戳算错一个整周期。正确做法是先处理溢出再取捕获值或者用「捕获值 溢出次数 × ARR」拼成完整时间戳。第二capture_idx用位与取模而不是取余是为了在中断里省几个周期测频中断里每一微秒都值得抠。参数说明capture_buf只存最近两次值因为频率计算只需要相邻两个边沿的时间差overflow_cnt是 32 位配合 ARR 能覆盖很长的周期测低频信号时不会因为计数器回绕而算错。如果你的信号频率很低比如几赫兹ARR 要设大一些或者靠溢出计数来扩展量程。3.2 频率估计把抖动压下去拿到相邻两次捕获的时间差 Δt 后频率就是 1/Δt换算好时钟单位。但直接用这个值会跳所以工程里做了估计。常见做法是滑动窗口平均窗口长度取 4 到 8 个周期既能压噪声又不至于引入太大滞后#define FREQ_WIN 8 static uint32_t period_win[FREQ_WIN]; static uint8_t win_pos 0; static uint8_t win_cnt 0; float freq_estimate(uint32_t period_ticks, float tick_hz) { period_win[win_pos] period_ticks; win_pos (win_pos 1) % FREQ_WIN; if (win_cnt FREQ_WIN) win_cnt; uint32_t sum 0; for (uint8_t i 0; i win_cnt; i) { sum period_win[i]; } float avg_period (float)sum / win_cnt; return tick_hz / avg_period; // tick_hz 是计数频率如 1e6 }逻辑说明period_ticks是相邻边沿的计数值差tick_hz是定时器计数频率PSC 决定。窗口没填满时用实际个数求平均避免开机阶段出现异常值。窗口长度是个权衡窗口越长频率越稳但响应越慢做电机控制时窗口太长会导致环路滞后我一般从 4 开始试看波形再调。这里有个血泪经验如果信号占空比不是 50%测上升沿到上升沿的周期没问题但如果你测的是高电平宽度算出来的就不是频率。捕获边沿极性要和你的信号匹配别想当然。3.3 相位跟踪让相位量连续输出相位跟踪是这个工程比普通测频多出来的一层。思路是维护一个内部相位累加器每个捕获周期根据实测周期和估计频率的偏差去修正累加步长输出一个 0 到 2π 连续变化的相位量。简化实现如下static float phase 0.0f; float phase_track_update(float freq_est, float dt_seconds) { // 标称角频率 float omega 2.0f * 3.1415926f * freq_est; // 相位按角频率推进 phase omega * dt_seconds; // 回绕到 [0, 2π) while (phase 2.0f * 3.1415926f) phase - 2.0f * 3.1415926f; while (phase 0.0f) phase 2.0f * 3.1415926f; return phase; }参数说明freq_est是上一步估计出的频率dt_seconds是本次更新距上次的时间间隔秒。这个相位量可以直接拿去和参考信号做差得到相位误差喂给后续的锁相或同步逻辑。注意dt_seconds的精度直接影响相位推进的准确性如果更新周期不稳定相位会累积误差所以这个函数最好放在固定周期的定时中断里调用而不是在主循环里随手调。注意相位跟踪的初始相位需要和实际信号对齐否则开机瞬间相位误差很大。工程里一般在上电后等第一个完整周期再启动跟踪别一上电就累加。4. 避坑与排查测频工程最容易栽的五个地方4.1 频率读数跳变严重现象串口打印的频率值上下跳几十赫兹信号源明明是稳定的。原因多半是捕获中断里读了捕获值但没处理溢出或者 PSC 设得太小导致量化误差大。解决先确认溢出计数逻辑正确再把 PSC 调大让计数频率降下来提高单周期计数值。如果信号本身有噪声在捕获引脚前加个简单的 RC 低通硬件滤波比软件滤波省事。4.2 低频信号测不出来现象输入几赫兹的信号频率输出是 0 或者乱值。原因定时器 ARR 太小一个信号周期内计数器回绕多次溢出计数没跟上时间戳拼错。解决把 ARR 设到最大比如 0xFFFFPSC 适当调大让一个信号周期内的计数值落在合理范围同时确认溢出中断使能了别只开捕获中断。4.3 相位输出不连续、有跳变现象相位量在 2π 附近突然跳到 0 之外或者偶尔倒退。原因相位回绕逻辑写成了单次 if 而不是 while一次更新跨过多个 2π 时没处理干净或者dt_seconds用了不稳定的时间源。解决回绕用 while 循环更新周期用定时器固定中断驱动别用主循环里的软件计时。4.4 换芯片后编译过但测频不准现象代码从 F103 挪到 F4 上能跑但频率值差一截。原因不同系列定时器时钟源和分频链路不一样PSC 的基准变了tick_hz没跟着改。解决把计数频率做成宏或者运行时计算从系统时钟和 APB 分频推出来别写死。这是移植时最常见的翻车点。4.5 中断里耗时太长导致丢捕获现象高频信号下偶尔丢一个边沿频率算出来是正常值的一半。原因捕获中断里做了浮点运算或者串口打印中断执行时间超过了信号周期。解决中断里只做最轻量的记录把计算挪到主循环或者低优先级任务里用标志位传递。测频工程的中断服务函数应该短到能用汇编数周期。5. 二次开发把测频链路改成你自己的闭环5.1 改造切入点这个工程的价值不在于它现在测什么而在于它给了一条「捕获 → 估计 → 跟踪」的完整数据链。你要做二次开发先想清楚自己缺哪一段。如果只是要一个稳定的频率值改freq_estimate.c里的窗口长度和滤波方式就够了如果要做相位同步phase_track.c里的相位推进逻辑就是你要接参考信号的地方如果要做闭环控制把相位误差取出来接到你的 PID 输入上输出接 PWM 占空比一个简单的锁相环就成型了。改造时有个原则别动捕获层的时序逻辑那部分是最容易引入 bug 的地方。要改就改算法层捕获层只负责把干净的时间戳交出来。我见过有人为了「优化」把捕获和计算揉在一个中断里结果高频下直接丢边沿查了两天才发现是中断超时。5.2 验证方法改完之后怎么确认没改坏三个层次的验证验证层次方法判断标准捕获层信号源输入已知频率方波打印原始周期计数值计数值与理论值误差在量化误差内估计层对比原始频率和估计频率估计值波动明显小于原始值且无系统性偏差跟踪层观察相位输出随时间的变化相位连续、无跳变频率变化时相位平滑跟随信号源可以用函数发生器没有的话用另一块 MCU 的 PWM 输出做参考也行关键是频率已知且稳定。验证时先把窗口长度设小看原始数据的抖动程度再逐步加大窗口观察平滑效果和响应滞后的平衡点。5.3 一个具体技巧如果你要把这套东西用到电机控制里相位跟踪的输出最好和 PWM 载波同步采样而不是异步更新。做法是在 PWM 周期中断里调用phase_track_updatedt_seconds直接用 PWM 周期这样相位推进和功率级开关严格对齐环路里不会引入采样抖动。这个细节在数据手册里不会写但实际调试时能省掉很多「明明参数对了但就是不稳」的玄学问题。从那以后我每次碰测频类工程都强制先把串口原始数据打出来看一遍确认捕获层干净了再往上叠算法不然调上层就是给自己找后悔药。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑