简介:这份资源面向使用STM32进行电源控制、电机驱动等中高频控制开发的嵌入式工程师与学习者,聚焦STM32F334高分辨率定时器(HRTIM)的PWM波形输出实例。HRTIM是ST针对电源控制方向设计的创新外设,最大计数频率可达4.608GHz,时间控制精度高达217ps,性能领先,适合对时序要求严苛的应用场景。压缩包内共2个文件,包含1个c源文件与1个h头文件,分别承载HRTIM的PWM配置实现与相关宏定义、接口声明,整体约2KB,体量轻巧,便于直接移植到工程中参考。目前已有4438人学习下载,说明该实例在同类资料中具备一定认可度。读者可从中获取HRTIM产生PWM的完整配置思路,理解高分辨率定时器在频率、占空比与精度控制上的实现方式,并以此为模板快速搭建自己的电源控制或高频PWM输出实验,减少从手册到代码的摸索成本。
1. 从一次“PWM 抖动到怀疑人生”说起:HRTIM 到底解决什么问题
如果你用 STM32 的普通定时器(TIM1/TIM8 之类)做过高频 PWM,大概率遇到过这种场景:主频 170MHz,想输出 1MHz 以上、占空比还要能精细调节的 PWM,结果发现 ARR 只剩几十个计数,占空比一档跳好几个百分点,死区时间也粗糙得没法看。更难受的是,几路 PWM 之间要求相位严格对齐、还要在运行中动态改频率,普通定时器改 ARR 会引入毛刺,波形直接翻车。
HRTIM(High-Resolution Timer,高分辨率定时器)就是为这类场景准备的。它最早出现在 STM32F334 这类面向数字电源、照明、电机驱动的型号上,后来在 G4、H7 等系列也有覆盖。核心卖点有两个:一是高分辨率,靠内部延迟锁相环把等效计数时钟推到几 GHz 量级,官方口径常见是 184ps 左右一个步进;二是多定时器单元协同,一个 HRTIM 外设里通常有 Master 加 5 个 Timer 单元,每个单元能独立输出两路,天然适合做移相全桥、LLC、交错 PFC 这类拓扑。
这篇笔记围绕“HRTIM 输出 PWM”这个最小可用目标展开:先讲清它和普通 TIM 的本质差别,再落到 CubeMX 配置、寄存器级参数计算、代码实现,最后把我在调试中踩过的坑一条条列出来。适合已经会点 STM32 基础、但第一次碰 HRTIM 的工程师,也适合想从普通 TIM 迁移到 HRTIM 的老手对照参数边界。
2. HRTIM 的架构与选型:为什么不是“又一个定时器”
2.1 高分辨率是怎么来的:DLL 校准与 184ps 步进
普通定时器的分辨率就是 1/计数时钟频率。170MHz 下,一个计数周期约 5.88ns,你想把占空比调到 0.1% 精度,在高频 PWM 下根本不够用。HRTIM 的做法是在计数时钟之外,再挂一条高分辨率延迟链,由 DLL(Delay Locked Loop)把延迟单元校准到固定步长。
以常见配置为例,HRTIM 的等效高分辨率时钟可以到 4.608GHz 左右,对应约 217ps;不同系列、不同校准结果会有差异,官方数据手册里常引用 184ps 这个典型值。关键点在于:这个精度不是靠提高总线时钟硬堆出来的,而是靠延迟链插值。所以你在算周期和占空比时,不能只按 fHRCK 去算,还要理解 HRTIM 的“高分辨率部分”和“粗计数部分”是怎么拼起来的。
一个周期值通常由两部分构成:粗计数(基于 fHRCK 的整数部分)加上高分辨率余数(基于延迟链的小数部分)。寄存器里对应的是 PER 和 CMP 这类字段,写入时要把目标时间换算成“粗计数 + 高分辨率步数”。这也是为什么很多人第一次配 HRTIM,波形频率总差那么一点点——只算了粗计数,没补高分辨率余数。
2.2 单元结构:Master 与 Timer A~E 的分工
HRTIM 不是一个定时器,而是一组。典型结构是 1 个 Master Timer 加 5 个 Timer 单元(Timer A 到 Timer E)。Master 不直接输出引脚,它的作用是给各个 Timer 提供同步基准、周期基准和事件触发。每个 Timer 单元有两路输出(比如 TA1、TA2),各自可以独立设置置位/复位事件。
这种结构带来的好处是:你可以让 Timer A 做主桥臂,Timer B 做滞后桥臂,两者共享 Master 的周期,但相位可以错开,天然实现移相。每个 Timer 的比较寄存器可以分别控制两路输出的翻转点,死区也能在单元内部插入,不用外部逻辑。
选型上要注意:不是所有 STM32 都有 HRTIM。F334、G474、H742 等型号有,F103、F407 这些经典型号没有。如果你手上是 F103 想复现本文,得先确认芯片是否带 HRTIM,否则代码编不过。CubeMX 里如果 Pinout 视图找不到 HRTIM1,那就是不支持。
2.3 和普通 TIM 做 PWM 的对比:什么时候该换
| 维度 | 普通 TIM | HRTIM |
|---|---|---|
| 分辨率 | 约 5.88ns(170MHz) | 约 184~217ps |
| 高频 PWM 占空比精度 | 差,ARR 小则档位粗 | 好,高分辨率插值 |
| 多路相位控制 | 靠主从触发,较绕 | Master+多 Timer 原生支持 |
| 死区插入 | 部分型号有 | 每单元独立,精度高 |
| 动态改频 | 改 ARR 易出毛刺 | 支持预装载与更新事件 |
| 适用场景 | 普通电机、LED、舵机 | 数字电源、LLC、移相全桥、交错 PFC |
结论很直接:低频、精度要求不高的 PWM,普通 TIM 足够,别为了用 HRTIM 而用。一旦涉及高频、高精度占空比、多路相位协同,HRTIM 的优势才体现出来。我一般判断标准是:PWM 频率超过 200kHz 且占空比需要 0.5% 以下精度,或者需要两路以上严格移相,就上 HRTIM。
3. CubeMX 配置与参数计算:把 184ps 落到寄存器
3.1 时钟树与 HRTIM 时钟源确认
第一步永远是确认 HRTIM 的时钟来源。HRTIM 的粗计数时钟 fHRCK 通常来自 APB2 或专用时钟路径,经过倍频后进入 HRTIM。CubeMX 的 Clock Configuration 里能看到 HRTIM1 的时钟输入,常见做法是让它跑到 144MHz 或 170MHz 量级,具体取决于芯片和 PLL 配置。
这里有个容易忽略的点:HRTIM 的高分辨率精度依赖 DLL 校准,而 DLL 需要一个参考时钟。参考时钟频率不对,校准就失败,高分辨率功能直接失效,波形会退化成普通精度。所以配置完时钟后,一定要在代码里检查 DLL 校准状态位,别默认它一定成功。
/* 检查 HRTIM DLL 校准是否就绪,常见做法是上电后轮询 */ if ((HRTIM1->sCommonRegs.ISR & HRTIM_ISR_DLLRDY) == 0U) { /* 校准未完成,高分辨率不可用,需要检查参考时钟配置 */ Error_Handler(); }这段代码的逻辑是:HRTIM 公共寄存器里的 ISR 有一个 DLLRDY 标志,只有它为 1 才说明延迟链校准完成。参数上不需要你手动填,但前提是时钟树配对了。如果这里卡住,先回去查 HRTIM 参考时钟是不是在允许范围内,而不是急着改 PWM 参数。
3.2 周期与占空比的换算:粗计数加高分辨率余数
假设目标 PWM 频率 1MHz,fHRCK 为 144MHz,高分辨率等效步进按 184ps 算。周期 1us 对应粗计数 144 个,但 144 是整数,实际周期会有误差。HRTIM 允许你在周期寄存器里补高分辨率余数,把误差压下去。
换算思路是:先把目标周期时间除以粗计数时钟周期,取整数部分写进 PER 的粗计数字段;余数部分再除以高分辨率步进,得到高分辨率步数,写进对应的高分辨率字段。占空比同理,CMP 寄存器也是粗计数加高分辨率两部分。
/* 以 1MHz PWM、fHRCK=144MHz、高分辨率步进 184ps 为例做换算 */ #define FHRCK_HZ 144000000UL #define HR_STEP_PS 184UL #define TARGET_FREQ_HZ 1000000UL /* 目标周期,单位 ps */ uint32_t period_ps = 1000000000000UL / TARGET_FREQ_HZ; /* 1us = 1000000ps */ /* 粗计数部分 */ uint32_t period_coarse = period_ps / (1000000000000UL / FHRCK_HZ); /* 余数转高分辨率步数 */ uint32_t period_remain_ps = period_ps % (1000000000000UL / FHRCK_HZ); uint32_t period_hr = period_remain_ps / HR_STEP_PS; /* 占空比 50% 同理计算 CMP 值 */ uint32_t cmp_ps = period_ps / 2U; uint32_t cmp_coarse = cmp_ps / (1000000000000UL / FHRCK_HZ); uint32_t cmp_remain_ps = cmp_ps % (1000000000000UL / FHRCK_HZ); uint32_t cmp_hr = cmp_remain_ps / HR_STEP_PS;逻辑说明:先统一到 ps 单位避免浮点误差,再分别算粗计数和余数。参数上,FHRCK_HZ 和 HR_STEP_PS 必须按你实际芯片的时钟和校准结果改,不能照抄。很多人波形频率差一点,就是这里余数没补或者步进值用错。注意 period_coarse 和 period_hr 最终要按寄存器位宽截断,超出范围要报错而不是硬写。
3.3 输出配置与死区插入
每个 Timer 单元的两路输出可以配置成互补或独立。做半桥时,TA1 和 TA2 互补,死区在单元内部插入。CubeMX 里可以设死区上升沿和下降沿的粗计数与高分辨率值,思路和周期换算一样。
配置顺序建议是:先设周期,再设比较值,再设死区,最后使能输出。顺序反了可能出现瞬间的异常脉冲。输出极性也要确认,有些驱动芯片是低有效,配反了直接炸管,这是血泪经验。
/* 配置 Timer A 周期与比较值,使能 TA1/TA2 互补输出 */ HRTIM1->sTimerxRegs[HRTIM_TIMERINDEX_TIMER_A].PERxR = period_coarse; HRTIM1->sTimerxRegs[HRTIM_TIMERINDEX_TIMER_A].CMP1xR = cmp_coarse; /* 死区配置,具体字段按参考手册填写 */ HRTIM1->sTimerxRegs[HRTIM_TIMERINDEX_TIMER_A].DTxR = deadtime_config; /* 使能输出 */ HRTIM1->sCommonRegs.OENR |= HRTIM_OENR_TA1OEN | HRTIM_OENR_TA2OEN;参数说明:PERxR 和 CMP1xR 是粗计数部分,高分辨率部分在另外的寄存器里补。DTxR 的死区字段要按参考手册的位定义填,不同系列位域可能不同。OENR 是输出使能寄存器,TA1OEN、TA2OEN 分别对应两路。写寄存器前建议先关输出,配完再开,避免中间态。
4. 代码实现与调试验证:从寄存器到示波器
4.1 初始化流程与 HAL 库的取舍
ST 提供了 HAL 库的 HRTIM 驱动,但实际项目里很多人直接用寄存器或 LL 库,原因是 HAL 的 HRTIM 封装在某些系列上不够全,高分辨率字段的写入也不够直观。我一般做法是:时钟和 GPIO 用 CubeMX 生成,HRTIM 核心寄存器自己写,这样参数可控。
初始化顺序:使能 HRTIM 时钟,配置 GPIO 复用,等待 DLL 校准,配置 Master 周期,配置各 Timer 单元,使能输出,最后启动计数。每一步之间留一点延时或状态检查,尤其是 DLL 校准,别跳过。
/* 简化的 HRTIM 初始化骨架 */ __HAL_RCC_HRTIM1_CLK_ENABLE(); /* GPIO 复用配置略,按 CubeMX 生成即可 */ while ((HRTIM1->sCommonRegs.ISR & HRTIM_ISR_DLLRDY) == 0U) { } /* 配置 Master 周期 */ HRTIM1->sMasterRegs.MPER = period_coarse; /* 配置 Timer A */ HRTIM1->sTimerxRegs[HRTIM_TIMERINDEX_TIMER_A].PERxR = period_coarse; HRTIM1->sTimerxRegs[HRTIM_TIMERINDEX_TIMER_A].CMP1xR = cmp_coarse; /* 使能并启动 */ HRTIM1->sMasterRegs.MCR |= HRTIM_MCR_SYNC_IN_DIS | HRTIM_MCR_CONT; HRTIM1->sCommonRegs.OENR |= HRTIM_OENR_TA1OEN | HRTIM_OENR_TA2OEN;逻辑说明:先开时钟再等校准,顺序不能反。MPER 是 Master 周期,Timer 单元的 PERxR 可以跟它一致也可以独立。MCR 里的 CONT 位决定连续计数模式。这段只是骨架,实际还要配输出极性、死区、预装载等。
4.2 用示波器验证:看什么、怎么判断对错
配完不是结束,必须上示波器。先看频率对不对,再看占空比,再看死区,最后看多路相位。频率偏差超过预期,先查高分辨率余数有没有补;占空比跳变,查 CMP 寄存器是不是被预装载影响;死区不对,查 DTxR 位域和极性。
如果波形有窄毛刺,常见原因是输出使能顺序不对,或者预装载没配好导致更新瞬间用了旧值。我一般会在启动前先把所有参数写全,再一次性使能,避免边配边输出。
4.3 动态调频与占空比的正确姿势
运行中改频率或占空比,不能直接写 PER 和 CMP,否则可能出毛刺。正确做法是利用预装载寄存器和更新事件,让新值在周期边界生效。HRTIM 支持这种机制,但要把预装载使能位配对。
/* 动态改占空比,走预装载,周期边界生效 */ HRTIM1->sTimerxRegs[HRTIM_TIMERINDEX_TIMER_A].CMP1xR = new_cmp_coarse; /* 高分辨率部分同步更新 */ /* 触发更新事件,具体位按参考手册 */ HRTIM1->sCommonRegs.CR2 |= HRTIM_CR2_TA1SET; /* 示例,实际按手册 */参数说明:new_cmp_coarse 是新比较值,写入后不会立即生效,要等更新事件。不同系列触发更新的寄存器位不同,务必查参考手册。这一步做错,轻则波形抖动,重则功率管直通。
5. 避坑与常见问题排查:那些让我返工的细节
5.1 DLL 没校准就配 PWM,波形精度直接退化
现象:PWM 频率和占空比怎么调都对不上,精度像普通定时器。原因:DLL 未校准完成就写了高分辨率相关寄存器,高分辨率功能没生效。解决:上电后先轮询 DLLRDY,确认校准完成再配置;同时检查 HRTIM 参考时钟是否在允许范围,参考时钟不对校准永远不成功。
5.2 只算粗计数,忘了高分辨率余数
现象:目标 1MHz,实测 0.98MHz 或 1.02MHz,占空比也有固定偏差。原因:周期和比较值只写了粗计数部分,余数没补。解决:按第 3.2 节的换算,把余数转成高分辨率步数写进对应字段;同时确认 HR_STEP_PS 用的是实际校准值,不是照抄的典型值。
5.3 输出使能顺序不对,上电瞬间出窄脉冲
现象:每次上电,输出会先冒一个窄脉冲再进入正常 PWM。原因:先使能了输出,再配参数,中间态被输出到引脚。解决:先把周期、比较值、死区、极性全部配好,最后再使能输出;必要时先让 GPIO 处于安全电平。
5.4 死区极性配反,半桥直通
现象:半桥上下管同时导通,电流飙升。原因:死区极性或输出极性配反,互补关系变成同相。解决:对照驱动芯片手册确认高低有效,死区上升沿和下降沿分别对应哪一路;上电先用低压、限流电源验证,别直接上母线电压。
5.5 动态改参数没走预装载,波形出毛刺
现象:运行中改占空比,示波器上看到周期边界处有毛刺或丢脉冲。原因:直接写 CMP 寄存器,新值在周期中间生效。解决:使能预装载,通过更新事件让新值在周期边界生效;改频率同理,别直接写 PER。
6. 进阶技巧:用 Master 同步多单元做移相,以及我的验证习惯
单路 PWM 跑通后,HRTIM 真正的价值在多单元协同。以移相全桥为例,Timer A 和 Timer B 分别驱动两个桥臂,两者周期相同,但相位差可调。做法是让 Master 提供周期基准,Timer B 的复位事件由 Master 的某个比较事件触发,或者用 Timer A 的周期事件去同步 Timer B,相位差就体现在触发点上。
/* 用 Master 比较事件触发 Timer B 复位,实现移相 */ HRTIM1->sMasterRegs.MCMP1R = phase_shift_coarse; /* 移相量 */ HRTIM1->sTimerxRegs[HRTIM_TIMERINDEX_TIMER_B].TIMxCR |= HRTIM_TIMxCR_SYNC; /* 同步使能,按手册 */ /* Timer B 的复位源选择 Master 比较1,具体位域查参考手册 */参数说明:phase_shift_coarse 是移相量对应的粗计数,高分辨率部分同样要补。SYNC 位和复位源选择位在不同系列里名字可能不同,务必查对应参考手册。移相量改的时候也要走预装载,否则相位会跳。
验证移相时,我习惯用示波器的双通道看两路驱动,先确认频率一致,再量相位差,最后看死区是否仍然正确。多单元场景下,死区、极性、相位三者要一起查,单独看某一项容易漏。
还有一个实用技巧:把关键参数做成宏或结构体,换芯片或换频率时只改一处。HRTIM 的寄存器位域在不同系列间有差异,把差异集中到一层封装里,移植时省很多事。
从那以后我每次配 HRTIM,都强制先跑一遍 DLL 校准检查、再核对高分辨率余数、最后上电前用限流电源验证死区,这三步少一步都不敢直接上母线。希望帮到你。
本文还有配套的精品资源,点击获取