☰
STM32频率测量与相位跟踪:定时器捕获与DPLL完整实现
2026/10/21 21:30:06 网站建设 项目流程

简介:一套面向STM32开发者的频率测量与相位跟踪工程资料包,针对嵌入式实时控制与信号处理场景,利用内置ADC和定时器实现周期信号频率检测与相位差跟踪,适合入门学习者上手实践,亦可作为开发人员的参考工程。包体共316个文件,压缩后仅512KB,以C语言源码(约80个.h头文件与74个.c源文件)和Keil工程配置为主,另含hex/axf/map/o等编译链接产物、htm说明文档以及备份与批处理文件,可直接打开工程查看代码结构和编译流程。已有1391人学习下载。整套工程围绕ICAPTURE展开,覆盖定时器输入捕获配置、ADC采样配合、中断服务程序、频率公式计算、相位捕获及相位差计算等关键环节,并通过不同定时器通道或DMA方式演示多信号相位跟踪思路,便于读者根据实际需求调整参数、扩展应用。

1. 这个工程包在解决什么问题:频率测量与相位跟踪的完整闭环

拿到「基于STM32频率测量与相位跟踪.7z」这个名字,先别急着解压。它说的不是某一个函数,而是一整条信号处理链路:先测出输入信号的频率,再让系统输出一个与输入同频、相位可跟随的信号。这类需求在很多场合出现——逆变器并网前要锁市电相位、电机驱动器要跟踪编码器脉冲频率、振动台要生成与参考信号同步的激励波形。这个包的价值在于,它把「测频率」和「跟相位」两件事用一颗 STM32 的定时器资源串了起来,而不是让你在示波器和分立锁相环芯片之间来回折腾。

适合读这篇文章的人,是手里已经有 STM32 开发板、想用输入捕获做测频、又对数字锁相环 DPLL 的实现细节拿不准的从业者。新手能照着把最小工程跑起来,熟手可以重点看中间章的参数边界和最后的避坑记录。下面从定时器配置开始,一步一步拆到能直接改参数换频率范围的程度。

2. 用输入捕获在 STM32 上做频率测量:测频法与测周法的选型边界

2.1 定时器输入捕获的最小配置与测频逻辑

频率测量的本质是数脉冲。STM32 的定时器输入捕获模式,可以在信号上升沿到来时自动把计数器值锁存到捕获寄存器里,CPU 只需要在中断里读两次值,差值就是一个信号周期对应的时钟 tick 数。相比外部中断加普通计数的方式,输入捕获的硬件锁存省去了中断响应延迟带来的抖动,这是它成为首选的根本原因。

我一般把输入捕获定时器配置成 32 位自由运行模式,预分频根据被测频率范围来定。下面这段是基于 HAL 库的初始化逻辑,适用于 F1/F4 系列的大部分型号:

// 定时器输入捕获初始化:TIM2_CH1 映射到 PA0 static void TIM2_IC_Init(uint16_t psc) { GPIO_InitTypeDef gpio = {0}; __HAL_RCC_TIM2_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); gpio.Pin = GPIO_PIN_0; gpio.Mode = GPIO_MODE_AF_PP; // 复用推挽,把 PA0 交给 TIM2 gpio.Pull = GPIO_PULLDOWN; // 默认拉低,避免悬空误触发 gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOA, &gpio); htim2.Instance = TIM2; htim2.Init.Prescaler = psc; // 预分频值,决定计数时钟频率 htim2.Init.CounterMode = TIM_COUNTERMODE_UP; htim2.Init.Period = 0xFFFFFFFF; // 32 位自由运行,溢出由软件处理 htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; HAL_TIM_IC_Init(&htim2); TIM_IC_InitTypeDef ic = {0}; ic.ICPolarity = TIM_INPUTCHANNELPOLARITY_RISING; // 上升沿捕获 ic.ICSelection = TIM_ICSELECTION_DIRECTTI; // 直连通道,不复用另一路输入 ic.ICPrescaler = TIM_ICPSC_DIV1; // 不分频,每个边沿都捕获 ic.ICFilter = 0x0F; // 输入滤波,滤掉窄毛刺 HAL_TIM_IC_ConfigChannel(&htim2, &ic, TIM_CHANNEL_1); HAL_TIM_IC_Start_IT(&htim2, TIM_CHANNEL_1); }

关键是 Prescaler 和 Period 的配合。Prescaler 把 APB1 总线时钟分频成计数时钟,Period 设为 0xFFFFFFFF 是为了让定时器当作一个 32 位计数器用,这样在软件里直接用差值计算,不需要频繁处理溢出。ICFilter 取值 0x0F 表示采样频率为计数时钟分频后的较低档位,能滤掉高频毛刺,但如果被测信号本身很干净,可以设小一些以降低边沿延迟。

捕获中断里的计算是整个测频的核心部分:

// 捕获中断回调:计算信号周期 uint32_t g_last_ccr = 0; // 上一次捕获值 uint32_t g_period_ticks = 0; // 本次周期,单位:计数时钟 tick 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_ccr) { g_period_ticks = now - g_last_ccr; // 常规情况:未发生回绕 } else { // 计数器回绕:用 32 位周期减去旧值再加上新值 g_period_ticks = (0xFFFFFFFF - g_last_ccr) + now + 1; } g_last_ccr = now; // 实际频率 = 计数时钟频率 / g_period_ticks // 例如计数时钟 1MHz,g_period_ticks = 1000,则频率 = 1000Hz } }

这段代码在 32 位定时器上能直接处理计数器回绕,不需要额外的溢出计数变量。计算出周期后,频率就是计数时钟频率除以周期 tick 数,注意单位统一。如果被测信号频率很低、周期超过了 32 位计数器的最大计数值,才需要引入更新中断做溢出计数,实际工程里 1Hz 到 1MHz 范围通常用不上。

2.2 测频法与测周法的分界点:什么时候该换测量策略

上一节用的是测周法——测一个完整周期占多少 tick,再换算频率。它的特点是低频时精度高,因为周期长、计数点多;高频时精度反而下降,因为一个周期只有几个 tick,量化误差占的比例大。测频法则相反,它固定一个闸门时间,统计闸门内有多少个上升沿,高频时计数多、精度高,低频时一个闸门里只有几个脉冲,误差很大。

工程上的分界点取决于计数时钟频率。当被测频率低于计数时钟频率的约千分之一时,测周法占优;高于这个比例时,测频法更合适。举例:计数时钟 1MHz,测周法测 1kHz 信号,一个周期有 1000 个 tick,量化误差约 0.1%;测 100kHz 信号,一个周期只有 10 个 tick,量化误差高达 10%,这时就该换成测频法。

常见做法是用一个 10ms 的固定闸门做测频计数,同时保留测周通道作为低频备用。我一般会在代码里做频率区间切换:当连续三次测周结果对应的频率超过计数时钟的 1/200 时,自动切到测频模式。这个切换阈值留了 5 倍裕量,避免在分界点附近来回震荡。

提示:预分频值不是越小越好。计数时钟越高,量化误差越小,但定时器计数溢出也越快,32 位计数器在 72MHz 下约 59 秒回绕一次,如果不处理溢出,长时间测量会出错。工程上按被测频率范围选计数时钟,让信号频率落在计数时钟的 1/1000 到 1/2 之间比较合理。

3. 相位跟踪的核心:在中断里做 DPLL 数字锁相环

3.1 相位差提取的两种计算:过零时间差与本地相位差

频率测出来了,接下来要让输出信号和输入信号相位对齐。模拟锁相环用鉴相器和电荷泵,STM32 里没有这些东西,但可以用定时器的捕获值和比较值模拟同样的行为。最常见的做法是过零时间差法:输入信号的上升沿作为参考时刻,把 PWM 输出通道的相位基准也定义在这个时刻。

过零时间差法的优点是直观:输入捕获中断里记下当前时刻 t_in,输出 PWM 的比较值期望出现在 t_in 之后的一个固定延时位置,这个延时对应的 tick 数就是相位差。如果输出信号的上升沿实际出现在 t_out,那么相位误差 = (t_out - t_in - expected_delay) 的 tick 数换算成角度。它的缺点是只利用了边沿信息,受单次噪声影响大。

另一种方法是本地相位差法,也就是数字锁相环的常用做法:为系统维护一个相位累加器 phase_out,每次输入过零时,比较 phase_out 的当前值与理想值 0 的差距,这个差距就是相位误差。这种方法天然带有环路滤波,能自动抑制单次边沿抖动,是本场景下我优先推荐的方案。

3.2 DPLL 环路滤波:比例积分参数怎么设、PWM 相位怎么输出

DPLL 的中断服务函数在每次输入上升沿时执行一次,中断频率等于信号频率。在中断里做三件事:测量当前频率、计算相位误差、更新环路状态。下面的代码是一个完整的最小实现:

// 中断里执行的 DPLL 核心逻辑 float g_freq_est = 1000.0f; // 本地频率估计,初始值先给一个猜测 float g_phase_out = 0.0f; // 本地相位累加器,单位:信号周期 float g_kp = 0.3f; // 比例系数,决定相位修正速度 float g_ki = 0.05f; // 积分系数,决定频率跟踪精度 void On_Signal_ZeroCross(uint32_t period_ticks, uint32_t tim_freq) { // 1. 用最新周期更新频率估计 float f_in = (float)tim_freq / (float)period_ticks; // 2. 计算相位误差:本地相位离 0 越远,误差越大 float phase_err = g_phase_out; if (phase_err > 0.5f) phase_err -= 1.0f; // 归一化到 [-0.5, 0.5) if (phase_err < -0.5f) phase_err += 1.0f; // 3. 环路滤波:积分项修频率,比例项修相位 g_freq_est += g_ki * phase_err; g_phase_out += g_kp * phase_err + g_freq_est / f_in; // 4. 相位累加器归一化,保持在一个周期范围内 if (g_phase_out >= 1.0f) g_phase_out -= 1.0f; if (g_phase_out < 0.0f) g_phase_out += 1.0f; }

比例项 g_kp * phase_err 的作用是把现有相位误差直接掰回来,掰得快慢由 Kp 决定;积分项 g_ki * phase_err 累加到频率估计里,用于消除稳态频率误差。注意 g_phase_out 的更新里用了 g_freq_est / f_in,这是让本地相位以输入频率为基准往前走,避免相位累加器在频率偏差下快速漂移出界。

PWM 输出的相位控制,本质上就是把相位累加器的值映射到定时器的比较值。如果输出通道的周期固定为 N 个 tick,那么比较值 = (uint32_t)(g_phase_out * N)。映射在 PWM 更新中断里完成,同时把比较寄存器设置为预装载模式,确保更新瞬间不会产生毛刺。下面给出映射代码:

// PWM 输出配置:TIM3_CH1 作为相位可调的输出通道 void PWM_PhaseUpdate(TIM_HandleTypeDef *htim, float phase_out, uint32_t period_ticks) { uint32_t ccr = (uint32_t)(phase_out * (float)period_ticks); __HAL_TIM_SET_COMPARE(htim, TIM_CHANNEL_1, ccr); // 关键:CCR 是影子寄存器,在下一个更新事件才会生效 // 这样修改不会造成当前周期中途跳变 }

参数整定是 DPLL 落地时最玄学的部分。Kp 太小时相位锁定慢,Kp 太大时输出相位会围绕目标值来回过冲;Ki 太小时频率跟踪有静差,Ki 太大时整个环路会振荡。我通常先用 Kp = 0.2、Ki = 0.02 起步,观察锁定时间,再按经验值微调。这里给一个粗略的整定对照:

参数调整方向观察到的现象可能原因处理办法
Kp 过大锁定后相位在 ±5° 内快速抖动比例路径增益过高降到 0.1 量级,观察抖动是否消失
Ki 过大频率估计在真实值附近来回扫积分路径带宽过宽降到 0.01 量级,延长锁定时间换稳定
Kp/Ki 都小频率阶跃后 2 秒仍未锁定环路带宽太低按 3~5 倍步进增大 Kp,再微调 Ki
初始频率猜测偏差大第一次锁定时长异常相位误差超过 ±0.5 周期用测频结果直接覆盖 g_freq_est

需要说明的是,上面这套 DPLL 是离散事件驱动的简化版,它在输入信号连续、中断稳定触发的前提下表现良好。如果输入信号有缺脉冲或大幅抖动,环路会在一两个周期内失锁,这时需要引入更稳健的相位差提取方式,这就是下一章要展开的前端处理问题。

4. 信号整形与量化误差:影响频率测量精度的两个隐藏坑

4.1 输入信号整形:为什么直接接 GPIO 会翻车

很多初次上手的人直接把信号发生器的输出接到 STM32 引脚上,结果测出来的频率跳得像随机数。原因通常不在代码,而在信号本身。STM32 的 GPIO 输入电平标准是 TTL/CMOS,但实际信号源可能是双极性正弦波、带直流偏置的方波、或者幅度很小的感应信号。正弦波过零点附近上升速率慢,噪声会引起边沿抖动,捕获值每次差几十个 tick,测频自然不稳。

正确的做法是在进 MCU 之前加一级整形电路。常见方案是用带滞回的比较器,比如把信号先经过一个 RC 隔直滤波,再送入比较器的正输入端,负输入端接一个参考电压。滞回窗口的宽度决定了抗噪能力:窗口太窄滤不掉噪声,太宽会引入相位偏移。我一般把滞回窗口设在信号幅度的 5%~10% 之间,这是一个兼顾抗噪和相位精度的经验区间。

整形后的信号还必须注意电平匹配。比较器输出如果是开漏结构,需要接上拉电阻到 MCU 的供电电压;如果是推挽输出,直接串联一个 1kΩ 限流电阻就能进 GPIO。很多人在这里跳过限流电阻,一旦信号源输出电压异常,直接烧引脚。GPIO 的输入滤波 ICFilter 可以作为整形电路之外的第二道防线,但它只能滤极窄的毛刺,不能替代模拟整形。

4.2 量化误差与测量带宽的权衡

频率测量的精度最终受限于计数时钟的量化。测周法测一个周期,误差极限是 ±1 个 tick;测频法在闸门时间内计数,误差极限是 ±1 个脉冲。这两种误差在不同频率段的表现差异巨大,尤其是在信号频率接近计数时钟频率的百分之一到十分之一的过渡区,无论用哪种方法,相对误差都会很难看。

解决这个矛盾的一个实用思路是等精度测量。它同时测量信号脉冲数和时基时钟脉冲数,闸门的开启和关闭由信号边沿触发,因此无论信号频率高低,测量相对误差只取决于时基时钟频率和闸门时间。STM32 上用两个定时器配合可以实现:一个定时器做闸门控制,另一个做时基计数,信号边沿同时触发两个定时器的捕获。这样在 1Hz 到 1MHz 范围内,相对误差可以保持基本恒定。

另一个容易被忽略的权衡是测量带宽。中断里测频加 DPLL 运算需要时间,如果信号频率太高,中断一次都处理不完,频率测量和相位跟踪就都会失真。工程上我习惯把信号频率上限控制在中断服务函数执行周期的一半以下,也就是中断代码耗时若为 20μs,那么被测信号频率不应超过 25kHz。超出这个范围时,要么简化中断逻辑,要么改用定时器硬件自动完成部分工作,比如用 PWM 输入模式同时捕获周期和占空比,减少软件参与度。

注意:相位跟踪的带宽受制于 DPLL 的环路带宽,而环路带宽又受制于中断频率。信号频率越高,每次中断之间的间隔越短,环路能处理的相位误差信息越少,跟踪带宽反而会下降。这是数字锁相环固有的矛盾,理解它才能解释为什么频率升高后相位抖动会变大。

5. 频率测量与相位跟踪的 5 个避坑记录:现象、原因与解决

5.1 占空比不是 50% 时相位抖动 ±3°

用上升沿做捕获和相位基准,理论上没问题,但实际测方波信号时发现相位输出总有规律性抖动。排查后确认信号本身占空比是 30%,不是 50%,这意味着上升沿和下降沿之间的中点并不在预期的半周期位置,而 DPLL 只用上升沿做相位参考,把占空比不对称全部解读成了相位误差。

原因在于方波经过整形电路后的占空比被比较器的参考电平偏移改变了。解决方法是改用占空比对称的信号做参考,或者在算法里用上升沿和下降沿交替计算周期和相位,取两者的平均值作为相位基准。后者对中断开销增加不多,但能显著抑制占空比不对称带来的伪相位误差。

5.2 捕获中断优先级低于 PWM 更新中断导致输出相位跳变

工程里我把输入捕获中断优先级设得较低,结果输出 PWM 的相位每隔一段时间就跳一大格。追踪后发现是 PWM 更新中断优先级更高,它抢先执行时,输入捕获事件被延迟处理,捕获值读取时刻与实际边沿差了几个到几十个 tick。

解决方法是把输入捕获中断优先级提到 PWM 更新中断之上,因为相位测量对时间敏感度远高于 PWM 更新。但这里有个反向风险:如果捕获中断优先级太高,它可能打断系统的其他实时任务。我最终的做法是让捕获中断优先级仅低于系统节拍定时器,同时把中断服务函数压到最短——只读捕获值、置标志位,不做浮点运算,浮点计算放到主循环里执行。

5.3 高次谐波导致频率测量值周期性跳变

给一个 10kHz 方波信号测频,结果频显在 10kHz 和 30kHz 之间来回跳。方波本身包含丰富的奇次谐波,虽然基波 10kHz 最强,但谐波分量也可能触发电平比较器的翻转,造成一个周期内多次捕获。

解决这个问题的关键在整形电路的滞回窗口设置。原来滞回窗口只有 20mV,而谐波分量在过零附近的斜率叠加出了超过 30mV 的毛刺。把滞回窗口增大到 100mV 后,毛刺不再触发翻转,频率测量恢复稳定。同时在软件里把 ICFilter 调大一些,两者配合能把绝大多数谐波干扰挡在门外。

5.4 修改比较值产生输出毛刺:影子寄存器没开

DPLL 锁定后,输出 PWM 的波形上偶尔出现极窄的毛刺,用示波器单次触发才能抓到。原因是直接修改 CCR 比较值时,如果修改发生在当前计数周期中间,比较匹配可能立即触发,造成输出在非预期位置翻转。

解决方法是开启定时器通道的预装载功能,让 CCR 值只在更新事件时生效。HAL 库里需要在初始化时设置TIM_OC_PRELOAD_ENABLE,改用HAL_TIM_PWM_ConfigChannel里的 OC 结构体配置。这个坑我踩过一次之后养成了习惯:所有输出比较通道一律开预装载,宁可多等一个周期,也要保证波形干净。

5.5 DPLL 的 Ki 太大导致锁定后持续振荡

把 Ki 从 0.02 调到 0.2 想加快频率跟踪,结果锁定后输出频率在目标值附近以约 5Hz 的频率来回振荡,相位误差显示器上能看到明显的正弦波动。Ki 过大会让积分路径变成高增益振荡器,相位误差被过度放大并反馈回频率估计,形成自激。

解决方法是把 Ki 退回 0.03,同时把 Kp 略微调大以保持锁定速度。另一个有效技巧是给相位误差加一个死区,当误差小于 ±0.005 周期时不更新积分项,防止稳态时微小的量化噪声反复刺激积分路径。这个死区对最终相位精度影响很小,但能有效消除振荡。

6. 用频率阶跃测试验证跟踪带宽:一个在线调参习惯

验证一套频率测量与相位跟踪系统是否合格,最有效的测试不是看稳态精度,而是看突然改变输入频率时系统的动态响应。用信号发生器把频率从 1kHz 瞬间跳到 1.1kHz,同时用示波器观察两个信号波形重合情况。合格的标准是相位在 200ms 内重新进入 ±5° 范围,并且过程中不出现明显过冲或失锁。

我习惯把 Kp、Ki 做成两个可以在线调整的浮点变量,通过串口命令实时修改,配合这个阶跃测试来整定参数。做法是写一个简单命令解析:输入kp 0.15就更新 Kp 值,输入ki 0.02就更新 Ki 值,系统不用重新编译。然后反复做阶跃测试,记录每个参数组合下的锁定时间和过冲量,选折中值。这个方法比在代码里改一次烧一次效率高出太多,而且能让你直观感受到带宽和稳定性之间的对抗关系。

一个额外的调试技巧是把 g_freq_est 和 phase_err 通过 DAC 或 PWM 引脚输出,滤波后送到示波器,这样能直接看到锁相环的内部状态。比如 phase_err 的波形如果收敛到平稳直线,说明环路已经锁定;如果呈衰减振荡,说明 Kp/Ki 偏大;如果收敛太慢,说明带宽不足。内部信号可视化之后,调参就不再是黑匣子,而是有波形依据的工程行为。这个习惯帮我解决过很多次参数问题,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询