☰
STM32H7 HRTIM复杂PWM波形生成:从死区到多相交错的完整实战
2026/10/20 15:24:29 网站建设 项目流程

从 STM32F103 的通用定时器切到 STM32H7 的 HRTIM 那阵子,我原以为这不过是一个高级 PWM 外设,多几路输出而已。直到为一块 100kHz 三相逆变器板卡生成驱动波形时,六路互补、死区 200ns、相位一致、运行时可调占空比这些需求叠在一起,我才明白 HRTIM 的复杂程度——它不是用来替代普通定时器的,而是为了解决普通定时器根本做不到的波形场景。这篇文章把我在 STM32H7 上用 HRTIM 生成复杂 PWM 波形的完整思路整理出来,从硬件结构、寄存器操作到典型波形设计和调试验证,适合已经会用基础定时器、正准备做高频电源或电机驱动项目的开发者参考。

1. 为什么普通定时器生成不了这种波形

1.1 一个 100kHz 逆变器的真实需求拆解

当时手头的板卡需求并不复杂:三对桥臂,载波频率 100kHz,上下管互补输出,死区 200ns,每周期占空比可变化。听上去是三相全桥最基本的要求,但在实现层面,任何一路输出在高电平和低电平之间切换时,如果上下管同时导通哪怕几十纳秒,桥臂直通电流就足以让驱动芯片和功率管一起报废。所以死区必须由硬件在输出级强制加入,不能依赖软件跑到中断里做延时。

另外,三相逆变器要求三对 PWM 的载波相位基准必须一致。若用三路独立的定时器各管一相,那么启动时刻、计数周期、时基漂移都会成为相位不一致的来源。更麻烦的是,占空比需要持续更新,如果更新瞬间没有在统一的计数周期边界对齐,某一路先变了、另外两路还没变,电机的电流波形就会先出现一个错误的交叉状态。这也是我后来把方案从“三个定时器加 GPIO 扩展”改成 HRTIM 的直接原因。

1.2 普通定时器在分辨率、同步、输出结构上的三处硬伤

第一处硬伤是分辨率。以常见的 168MHz 定时器时钟计算,在 100kHz 载波下,占空比调节的步进是 1/1680,约 0.06%,看起来够用。可如果把开关频率提到 500kHz,步进就只剩不到 0.01%,在闭环控制里已经谈不上精细了。死区分辨率同样受限于一个计数时钟,想插 20ns 左右的死区,要么凑不出来,要么只能四舍五入。

第二处硬伤是同步。普通高级定时器虽然有主从同步机制,但多个定时器之间的相位对齐主要靠触发事件和外部信号,精度受引脚传播和中断响应影响。到了多相电源这种需要精确移相 60 度、90 度的场景,定时器之间的时基漂移会成为电流纹波异常的直接来源。

第三处硬伤是输出结构。要用普通定时器产生互补带死区波形,通常得借助主输出和互补输出通道;要在一个周期内产生多次电平翻转,比如斩波或高频脉冲群,就得反复进中断改比较寄存器。中断繁忙之外,更致命的是写入时机不确定,稍一偏差,PWM 波形里就会出现一个宽度异常的毛刺。

1.3 HRTIM 的核心解法:不是“分辨率更高”,而是“波形引擎”

HRTIM 真正改变问题的不是简单把时钟频率提高到 480MHz,而是把死区插入、互补输出、周期同步、故障封锁、斩波调制这些功能全部做成硬件模块,放进定时器内部。你需要做的只是把目标和约束告诉寄存器,它会在每一个计数周期自动完成边沿生成,不依赖任何 CPU 中断。

举个直观例子:死区 200ns,在 480MHz 计数时钟下对应 96 个计数周期,写成寄存器就是 96,硬件会在上下管边沿之间精确插入这段延时。即使后面把占空比从 10% 改到 90%,死区依然是硬件级固定值,不会因为中断负载波动而变化。我不太关心单周期内“翻转了几次”,而是关心这套硬件链路能不能覆盖我所有边沿组合——HRTIM 在这一点上给了相当充足的操作空间。

2. HRTIM 的硬件结构:主从定时器与比较事件

2.1 整体结构先看懂

HRTIM 由一个主定时器和五个从定时器(A 到 E)组成。以一条流水线做类比,主定时器是整条产线的节拍器,从定时器才是每台工位上的加工机床。每个从定时器都有独立的周期寄存器、比较寄存器、输出通道和死区模块,五个通道相互独立,也能共享主定时器的全局事件。

这个结构带来的第一个实际好处是:五路 PWM 可以采用完全相同的周期和计数模式,却带上不同的相位偏移。你可以把主定时器的比较事件作为公共基准,某一个从定时器的启动或复位绑定到这个基准上,其他从定时器再各自配置不同的偏移值,从而实现硬件级同步。这个“五个动作一致、但节奏微调”的机制,就是多相交错电源常用构架的硬件基础。

2.2 比较事件是波形生成的最小单元

从定时器内部的核心逻辑,是“计数器一路向上数,到了某个比较值就翻转一次输出”。别小看这个机制,HRTIM 的每个从定时器不是只有一对比较寄存器,而是有多组比较事件,配合周期寄存器和计数方向,可以在一个周期内生成多次翻转。向上计数是一种波形,向下计数又是另一种,波形极性完全由比较事件定义。

说到这里必须提影子寄存器。HRTIM 的比较寄存器支持预装载,也就是说,你写入的新值不会立刻生效,而要等到下一个更新事件才整体替换。这很像工厂里的排班表:白天贴出来的明天安排,真正执行是明天的开场。没有这个机制,运行中改占空比就可能在任意计数位置产生边沿,造成局部脉冲异常。

2.3 输出路径里的死区、极性和斩波

每个从定时器的两路输出,在送出引脚之前,会经过输出管理模块。死区插入器负责让两条互补通道之间插入指定的安全间隔;极性控制可以把输出整体反相;斩波模块则会把连续脉冲切成一串高频小脉冲。让我最意外的是,这些模块并不需要 CPU 参与,一旦配置完成就会在每个计数周期自动工作,占空比更新也只是修改比较值,不会打乱输出路径上的时序。

3. 三种典型复杂 PWM 波形的设计方法

3.1 全桥互补输出:死区要怎么算

先算一组参数的例子:HRTIM 时钟 480MHz,目标载波 100kHz,死区 100ns。周期寄存器 PER 取 4800-1,比较寄存器 CMP 决定占空比。死区寄存器 DTR 的值不是微秒,而是“计数周期个数”,计算方法是死区时间乘以时钟频率,即 100ns 乘以 480MHz 等于 48。

这个计算看起来简单,实际我吃过亏。死区插入的位置是在输出路径上,如果前面输出极性配置有误,死区可能被加到错误的一条通道上,导致上下管直通时间比预期更多。我的习惯是最终用示波器测量两路输出之间的实际间隔,跟寄存器设定值对比,不要把计算结果当成最终结果。

3.2 多相交错:利用主定时器做相位基准

多相交错电源的典型需求是:多路 PWM 频率相同,相位均匀错开。以五相为例,每相相位差是 360/5 度,折算到 100kHz 载波周期(10us),就是 2us 一个相位。实现思路是把主定时器的比较事件作为同步源,让五个从定时器的计数起点都跟随这个基准,同时每个从定时器内部设置不同的启动或复位偏移。配置方法主要是设置各从定时器的复位源来自主定时器事件,再通过主定时器的不同比较点产生相位差。

这里有个关键细节:相位偏移不是“等到某一时刻才启动”,而是每个周期都对齐到同一个基准事件上。只有这样,后续的占空比更新才能保证五相之间的相位关系长期稳定,不会跑偏。

3.3 斩波输出:给红外载波和变压器驱动用

斩波模式适合那种“脉冲本身很宽、但负载需要载波能量”的场合。比如红外发射器的载波编码,或者隔离变压器驱动时用交变脉冲避免磁饱和。HRTIM 的斩波模块可以把输出的每个导通时间切成一串频率固定的脉冲串,斩波频率和占空比都可以独立配置。

以 1MHz 斩波为例,斩波周期是 480 个计数周期,配置到斩波频率寄存器后,硬件会自动在输出高电平时间段内产生连续的高频脉冲。这种方式放到普通定时器上,几乎只能靠中断抖动去凑,而 HRTIM 是在硬件比较链路里完成的,脉冲位置和宽度都有确定性。这一点对红外测距这种时序敏感的传感器来说非常关键。

4. 寄存器与 LL 库配置实操

4.1 时钟与引脚复用

配置 HRTIM 第一步不是看 HRTIM 手册,而是先确认内核时钟和 PLL。HRTIM 的计数时钟一般来自独立 PLL,例如由 PLL2P 输出 480MHz。很多人习惯直接用系统时钟分频,结果发现 HRTIM 频率只有几十 MHz,死区精度也跟着下降。以下是我的初始化顺序:

__HAL_RCC_HRTIM1_CLK_ENABLE(); __HAL_RCC_GPIOB_CLK_ENABLE(); __HAL_RCC_GPIOE_CLK_ENABLE(); // 根据实际引脚选择

每次初始化 GPIO 我都要核对一遍复用功能号,HRTIM 的引脚复用在不同封装、不同引脚上可能不同,千万别照抄别人工程的 AF 号。顺手把 GPIO 速度设为 VERVY_HIGH,PWM 开关频率高时,普通 GPIO 速度档位会在上升沿表现不佳。

4.2 定时器初始化:周期、比较寄存器与预装载

以单从定时器 A 输出 100kHz、50% 占空比为例,LL 库写法如下:

// 主定时器:全局基准 LL_HRTIM_Master_SetCounterMode(HRTIM, LL_HRTIM_COUNTERMODE_UP); LL_HRTIM_Master_SetPeriod(HRTIM, 4799); // 从定时器 A:PWM 通道 LL_HRTIM_TIMERA_SetCounterMode(HRTIM, LL_HRTIM_COUNTERMODE_UP); LL_HRTIM_TIMERA_SetPeriod(HRTIM, 4799); LL_HRTIM_TIMERA_SetCompare1(HRTIM, 2400); // 需要预装载时,打开对应位的影子寄存器功能 HRTIM->sTimerxRegs.TIMx_CR |= HRTIM_TIMCR_PRE_EN; // 示意,具体位名以参考手册为准

这里我专门把预装载问题单独拿出来说。LL 库里有的版本封装了使能接口,有的版本需要直接操作寄存器,所以不要死记函数名。重点是想清楚一个问题:你到底希望比较值在什么时候生效。如果你需要动态调占空比,建议开启影子寄存器;如果只是固定输出,直接写也没毛病,但项目里几乎不可能不做动态调节。

4.3 死区配置与输出使能

死区寄存器在从定时器自己的寄存器组里,例如:

HRTIM->sTimerxRegs.TIMx_DTR = HRTIM_TIMDTR_DTR(48) | HRTIM_TIMDTR_DTF(48);

这里 DTR 是上升沿死区,DTF 是下降沿死区,48 对应 100ns 在 480MHz 时钟下的计数个数。两路互补通道通常在使能输出后自动产生平移效果。我一般习惯把两路都填成同样值,再用示波器验证,毕竟不同晶振和分频会带来细微偏差。

输出使能要在所有配置完成后最后做。有一个很容易漏的步骤:即使 GPIO 复用功能配置正确,如果从定时器的输出开关没打开,引脚上依然毛都没有。使能输出可以参考寄存器位TIMx_OUT.OUTEN,同时确认没有故障输入把输出锁死。

4.4 运行中动态调占空比的更新策略

实际控制程序中,占空比不会是写死的。我的做法是把 CMP1 的预装载打开,然后在周期更新事件中写入新值。这样,所有通道的边沿变化都在同一个周期边界生效,不会出现一路先变、另一路后变的中间状态。更新事件可以用软件触发,也可以用主定时器的比较事件同步多个从定时器,后者更适合多相交错电源。

如果配置了多个从定时器,最好用统一更新源。否则每个从定时器各自更新,波形会有一段“手足无措”的过渡周期,电流波形出现异常尖刺。我会把更新源配置到主定时器事件上,所有 CMP 一起更新,实际测试中占空比变化相当干净。

5. 调试实录:波形出不来时的完整排查链路

5.1 输出一直是低电平:先查使能链

这是我踩得最多的坑。HRTIM 功能正常但 GPIO 输出低电平,原因通常不在 HRTIM 本身,而在“时钟到引脚复用到输出开关再到故障锁闭”这条链路上。我现在的排查顺序如下:

  1. 确认 HRTIM 时钟源使能并且频率正确;
  2. 用示波器测 GPIO 是否有微弱电平变化,区分“定时器没跑”和“输出被锁”;
  3. 确认 GPIO 复用功能号,直接对照 datasheet 的 AF 表;
  4. 查看输出使能寄存器是否置位;
  5. 最后查故障输入状态,故障激活时默认会关断输出。

一次调试中,我把故障输入引脚悬空了,结果第一次上电输出正常,第二次就不见了。原因是有个干扰脉冲触发了故障检测,输出被硬件锁住。对这种看似“偶发却不恢复”的现象,优先查故障状态寄存器,而不是反复重启。这个坑折腾了我一个下午,排查价值非常高。

5.2 相位偏移不对:事件路径才是关键

多相交错时相位不对,多半不是 CMP 值算错,而是同步事件路径没选对。HRTIM 里“从定时器复位”和“从定时器启动”是两种不同的事件行为,前者让计数器回归零并从头走,后者只是给出一个启动信号。相位对齐必须依赖周期性的同步源,而不是启动瞬间。

调试这一步时,我通常临时在中断里置一个 GPIO 翻转,每次同步事件来一次就翻转一次,再用逻辑分析仪看相位差。如果中断里看到的事件频率和标称一致,但波形相位仍不对,基本就是从定时器与主定时器的绑定关系配置错了,需要检查复位和比较事件源。

这里有个小技巧:多相同时开启时,不要用软件逐路启动,而是配置所有从定时器绑定同一个主定时器触发,让它们在硬件的同一拍里开始计数。软件启动的延迟虽然只有几微秒,但在驱动场景里会被放大成明显的电流偏移。

5.3 动态更新期间的毛刺:影子寄存器与更新源

运行中直接改 CMP1 后,波形看起来偶尔会出现一次窄脉冲,很多人会怀疑是中断竞争。实际上原因是比较寄存器没有走预装载路径,新值在任意计数位置生效,边沿变化落入当前周期中部,形成异常宽度。

解决方法是把 CMP 和 PER 寄存器全部配置成影子模式,统一由更新事件生效。若还想进一步降低抖动,可以把更新事件连接到主定时器的周期上,让所有从定时器同时刷新。经过这个改动之后,我用示波器连续观测几万周期,没有再发现毛刺。

5.4 故障保护行为别当成无从查起的 Bug

HRTIM 的故障保护既强大也会有误导性。它有多个故障输入源,可以通过硬件比较器、外部引脚或软件触发。在调试早期,我建议把所有未使用的故障输入先禁用,等 PWM 基本稳定后再逐步打开,否则波形“间歇性消失”会掩盖所有其他操作失误。

一旦打开了故障保护,要留意锁存和自动恢复两种模式的区别。锁存模式需要软件清除故障标志才恢复输出;自动恢复模式则会在故障消失后重新开工。对电机驱动来说,我用锁存模式更安全,但在用红外或电源测试时,锁存模式反而容易让系统卡死,所以两种模式都要会用。

6. 最后留在实验室里的两个细节

第一个细节是死区余量。我实际测过很多次,HRTIM 的死区计算按“计数周期乘数值”来算没错,但输出级还有延时路径、PCB 走线也有寄生延时,实测死区总会比寄存器设定值多一两纳秒甚至十几纳秒。我的习惯是在 DTR 值里预留 10 到 20ns 的余量,宁可比理论值小一点,也不要造成直通。小功率实验无所谓,大功率阶段这个余量会救下你的驱动板。

第二个细节是中断标志的利用。很多人调 HRTIM 只看示波器,却忽视了内部中断标志。示波器看“相位漂移”很慢,但看中断状态位却很快:哪个定时器的更新事件没到,哪个比较事件没触发,一目了然。我一般会在关键事件上开中断并采样标志位,确认硬件链路全部按预期工作后,再把中断关掉恢复纯硬件运行。

如果你刚接触 HRTIM,我的建议是从一个最简单的来自定时器 PWM 做起,先把输出、死区、更新、故障这几个模块逐个确认,再组合成复杂波形。HRTIM 的学习曲线比较陡,但每确认一层,后面排错的速度就会快很多。

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

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

立即咨询