ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

STM32G474高精度定时器配置实战:从TIM1到HRTIM的PWM控制

STM32G474高精度定时器配置实战:从TIM1到HRTIM的PWM控制 简介面向高频开关电源开发者的STM32G474高精度定时器HRTIM配置代码包解决了从DSP28335平台迁移后的定时器配置难题适用于需要对脉宽调制时序、死区时间及ADC触发时刻进行高分辨率控制的场景。压缩包体积仅8KB包含3个文件inscode源码、html说明页面及gitignore工程文件整体轻量方便直接查阅与二次移植。已有98人学习内容聚焦HRTIM初始化、中断服务、比较器与死区配置、输出波形极性控制并演示通过寄存器操作实时调整频率和占空比同时给出触发ADC的时序控制方法。代码中覆盖了定时器周期、分频、更新模式等关键参数设置并注重死区时间间隔以避免桥臂直通对理解5.44GHz倍频时钟与184皮秒分辨率下的精确时序设计很有帮助。整体结构紧凑配合HTML说明文档非常适合电力电子与开关电源方向的嵌入式工程师参考。 我刚接触 STM32G474 这款芯片时第一反应是“又一颗 M4 内核的片子能玩出什么花”。直到我把高精度定时器HRTIM和它内置的 170MHz 全速 PWM 真正跑起来才意识到这颗芯片在数字电源、电机控制这类场景里有多能打。尤其是 G474 自带的那组高精度定时器能把 PWM 边沿分辨率做到 130ps 级别这已经不只是“普通定时器”的范畴了。我花了两周时间把官方库和参考手册啃了一遍又在一个 Buck 变换器 demo 上把配置代码完整走通这篇就专门聊聊 STM32G474 高精度定时器怎么配置、在哪里容易卡住以及我实测下来的一些心得。1. 为什么数控电源和电机控制非要盯上高精度定时器很多人刚拿到 G474 时有个误区以为它和 F103、F407 一样看定时器就看“位数”和“频率”。但如果你要做的是数字电源、逆变器或者高性能 FOC 电机驱动核心指标其实是PWM 边沿分辨率。普通定时器哪怕主频跑到 170MHz一个计数时钟周期也有 5.88ns这意味着你调节占空比时每一步最小变化就是 5.88ns。对 100kHz 的 PWM 来说这个步进对应大概 0.06% 的占空比调节精度在要求严格的电源环路里完全不够用。G474 的高精度定时器能把这个步进压到 130ps 左右。它不是简单地把计数器时钟提上去而是采用了一种“粗调 细调”的组合方式粗调靠 170MHz 的计数器时钟细调靠内部的延迟锁定环DLL把每个计数时钟周期再切分成 36 到 40 个更细的时间槽。最终输出比较边沿可以在一个计数周期内的任意细槽位置触发效果就是 PWM 动作时刻的粒度从“纳秒级”变成了“皮秒级”。另外还有一个很多人没注意到的点普通定时器在调整占空比时如果时机不对会先输出一个完整周期的新占空比波形产生一个瞬时的大脉宽变化这在电源闭环里可能造成电流过冲。而高精度定时器配合影子寄存器机制可以在特定时刻原子地更新比较值甚至支持“双更新”模式让占空比变化更平滑。所以“高精度”三个字不只是分辨率高还有更新机制上的工程考量。我选择的芯片是 STM32G474RET6主频 170MHz片上带了 4 路 12bit ADC、3 个比较器、5 个运放再加多路高精度定时器非常契合数字电源场景。如果你用的是 G473 或者 G491定时器资源类似但外设数量和封装有差异选型时要注意。2. 定时器时钟树配置高精度之前先搞懂时钟源头高分辨率定时器的前提是时钟不能乱这是配置里最关键也最容易被忽视的一步。G474 的高精度定时器HRTIM1挂在 AHB 总线上但它的时钟源并不是简单的 AHB 分频而是可以直接连接到 PLL 输出或者专门的时钟域。手册里明确建议使用高精度定时器时最好把它的时钟配置为 170MHz并且要与触发 ADC、比较器、PWM 输出的相关时钟保持同源避免跨时钟域带来的相位不确定性。我的配置思路是这样的外部 25MHz 晶振HSE作为源头经过 PLL 倍频到 170MHz 系统时钟。HRTIM 的时钟在 RCC 配置里需要单独检查有些 CubeMX 版本默认不会把 HRTIM 的时钟树完全打开需要手动确认。系统时钟和 HRTIM 时钟的关系如下表时钟项配置值说明HSE25MHz外部高速晶振PLLM25 分频得到 1MHzPLLN170 倍频得到 170MHzPLLP / PLLQ / PLLR不分频PLLR 作为系统时钟SYSCLK170MHz系统主频HRTIM1 时钟170MHz与系统同频APB 定时器时钟170MHzTIM1 等高级定时器实际配置中我强烈建议用代码去检查 RCC-CFGR 和 HRTIM 相关时钟位不要只依赖 IDE 的图形界面。否则会出现一种情况系统运行正常PWM 也有输出但边沿抖动很大或者用逻辑分析仪测量的频率偏差达到几十赫兹原因就是定时器时钟被分频了。我踩过的第一个坑就在这里。当时我把 HRTIM 配置成 170MHz跑起来后用示波器量 PWM 频率发现 100kHz 变成了约 99.88kHz虽然只差了千分之一但对同步整流电源来说这个偏差会直接导致相位偏移累积。最终发现我忘了在 RCC 里把 HRTIM 的时钟源选择位改成 PLL 输出结果它默默走的是系统时钟的二分频。这个问题在 CubeMX 里可能只是在某个不显眼的选项里但丢了就是半天时间。3. TIM1 高精度 PWM 配置拆解从寄存器到完整的代码TIM1 虽然是“高级定时器”但在 G474 上它不能达到 HRTIM 那样的 130ps 分辨率。如果你的目标是“高精度”三个字最终还是要落到 HRTIM1 上。不过 MTimer 和 HRTIM 在配置思路上有很多相通的地方我先用 TIM1 的 PWM 输出代码做一个铺垫方便你理解影子寄存器、预装载、极性这些共同概念然后再上 HRTIM 的配置。3.1 使用 TIM1 输出互补 PWM 的基础配置我在 demo 项目里先用 TIM1 生成了两路互补 PWM 测试电机驱动板。TIM1 的结构是 16 位计数器支持向上、向下和中心对齐三种计数模式。互补 PWM 最关键的是死区Dead Time插入和刹车Break功能这在后续的 HRTIM 中同样重要。下面是基础代码HAL 库风格但它不是完整工程只是拆解配置逻辑void MX_TIM1_Init(void) { TIM_HandleTypeDef htim1 {0}; TIM_OC_InitTypeDef sConfigOC {0}; TIM_BreakDeadTimeConfigTypeDef sBreakDeadTimeConfig {0}; htim1.Instance TIM1; htim1.Init.Prescaler 0; // 预分频为 0直接使用定时器时钟 170MHz htim1.Init.CounterMode TIM_COUNTERMODE_CENTERALIGNED1; // 中心对齐模式 htim1.Init.Period 1699; // 计数周期 1700 个时钟PWM 频率 170M / 1700 100kHz htim1.Init.ClockDivision TIM_CLOCKDIVISION_DIV1; htim1.Init.RepetitionCounter 0; // 不触发重复中断 htim1.Init.AutoReloadPreload TIM_AUTORELOAD_PRELOAD_ENABLE; HAL_TIM_PWM_Init(htim1); }这里把 Period 设为 1699 是因为计数器从 0 计数到 1699一共 1700 个时钟周期100kHz 正好对应整数。中心对齐模式下每周期会经历向上计数和向下计数两个阶段所以实际的 PWM 频率是定时器触发频率的一半。我的项目里用了中心对齐模式看起来 Period 算出来的频率是 200kHz实际 PWM 输出是 100kHz手册里有明确说明但踩过坑之后我就一直记着这个细节。3.2 配置输出比较通道与死区TIM1 的 CH1 和 CH1N互补通道需要分别配置输出比较模式然后统一设置死区时间。以下代码设置了 PWM1 模式和 100ns 死区sConfigOC.OCMode TIM_OCMODE_PWM1; sConfigOC.Pulse 850; // 占空比 50% sConfigOC.OCPolarity TIM_OCPOLARITY_HIGH; sConfigOC.OCNPolarity TIM_OCNPOLARITY_HIGH; sConfigOC.OCFastMode TIM_OCFAST_DISABLE; sConfigOC.OCIdleState TIM_OCIDLESTATE_RESET; sConfigOC.OCNIdleState TIM_OCNIDLESTATE_RESET; HAL_TIM_PWM_ConfigChannel(htim1, sConfigOC, TIM_CHANNEL_1); sBreakDeadTimeConfig.OffStateRunMode TIM_OSSR_ENABLE; sBreakDeadTimeConfig.OffStateIDLEMode TIM_OSSI_ENABLE; sBreakDeadTimeConfig.LockLevel TIM_LOCKLEVEL_OFF; sBreakDeadTimeConfig.DeadTime 85; // 死区寄存器值注意不是纳秒直接写入 sBreakDeadTimeConfig.BreakState TIM_BREAK_ENABLE; sBreakDeadTimeConfig.BreakPolarity TIM_BREAKPOLARITY_HIGH; sBreakDeadTimeConfig.BreakFilter 0; sBreakDeadTimeConfig.AutomaticOutput TIM_AUTOMATICOUTPUT_ENABLE; HAL_TIM_DeadTimeConfig(htim1, sBreakDeadTimeConfig);死区时间计算是新手容易懵的地方。TIM1 的死区寄存器不是直接用纳秒值而是通过 DTG[7:0] 编码得到。当系统时钟 170MHz、死区寄存器值 85 时实际死区约为 85 × 1/170MHz ≈ 500ns。如果你想精确计算某个死区值需要对照手册里的死区编码表而不是简单地“填个数字”。3.3 启动 PWM 输出的正确姿势高级定时器有个坑正确配置完成后PWM 通道并不会立刻输出波形除非主动使能主输出 MOEMain Output Enable。HAL 库里面对应的是HAL_TIM_PWM_Start和HAL_TIM_PWM_Start_IT但要注意仅调用启动函数还不够还需要使能互补通道的输出HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_1); HAL_TIM_PWM_Start(htim1, TIM_CHANNEL_2); __HAL_TIM_ENABLE_OCxPRELOAD(htim1, TIM_CHANNEL_1); __HAL_TIM_MOE_ENABLE(htim1);如果你用的是不带互补输出的普通通道可能HAL_TIM_PWM_Start就够了但只要涉及互补输出就必须确保 MOE 位被置上否则输出引脚一直是高阻或无效电平。这个问题的典型现象是初始化函数全部执行完示波器测量 CH1N 引脚却没有电平变化。实际调试时我更推荐直接操作寄存器来验证 MOE 状态TIM1-BDTR | TIM_BDTR_MOE; // 使能主输出4. HRTIM 高精度输出的配置这才是 G474 真正值钱的地方HRTIM1 是 G474 上最复杂的定时器模块它内部不是单个计数器而是由一组高精度定时器单元组成包括 1 个主定时器和 6 个子定时器TIM_A 到 TIM_E。每个子定时器都能输出两路互补 PWM并且它们之间可以互相触发、同步、错相。配置难度比 TIM1 高一个量级但换来的是极大的灵活性。4.1 HRTIM 初始化流程拆解HRTIM 的初始化代码比较长我这里给出一个最小可用的配置骨架用来输出 1MHz 频率、50% 占空比的两路互补 PWM。为了让你看清关键配置点我做了简化void MX_HRTIM1_Init(void) { HRTIM_TimeBaseCfgTypeDef timeBaseCfg; HRTIM_TimerCfgTypeDef timerCfg; HRTIM_OutputCfgTypeDef outputCfg; HRTIM_CompareCfgTypeDef compareCfg; __HAL_RCC_HRTIM1_CLK_ENABLE(); /* 主定时器时基 */ timeBaseCfg.Period 170; // 170 个 170MHz 周期 1us timeBaseCfg.Repetition 0; timeBaseCfg.Prescaler HRTIM_PRESCALERRATIO_DIV1; timeBaseCfg.Mode HRTIM_MODE_CONTINUOUS; timeBaseCfg.Interrupts 0; HAL_HRTIM_TimeBaseConfig(hhrtim1, HRTIM_TIMERID_MASTER, timeBaseCfg); /* 从定时器 A 的时基使用主定时器作为时钟源 */ timerCfg.Interrupts 0; timerCfg.DACSynchro 0; timerCfg.ResetUpdate HRTIM_TIMRESETUPDATE_MASTER; timerCfg.ResetTrigger 0; timerCfg.UpdateGating 0; timerCfg.StartSync HRTIM_TIMSYNC_MASTER_START; timerCfg.UpdateTimer HRTIM_TIMUPDATE_MASTER; HAL_HRTIM_TimerConfig(hhrtim1, HRTIM_TIMERID_TIMER_A, timerCfg); /* 设置比较值 */ compareCfg.CompareValue 85; // 50% 占空比 HAL_HRTIM_CompareConfig(hhrtim1, HRTIM_TIMERID_TIMER_A, HRTIM_COMPAREUNIT_1, compareCfg); /* 输出配置 */ outputCfg.Polarity HRTIM_OUTPUTPOLARITY_HIGH; outputCfg.SetSource HRTIM_OUTPUTSET_TIMCMP1; outputCfg.ResetSource HRTIM_OUTPUTRESET_TIMPER; outputCfg.IdleState HRTIM_OUTPUTIDLESTATE_INACTIVE; outputCfg.IdleLevel 0; HAL_HRTIM_OutputConfig(hhrtim1, HRTIM_TIMERID_TIMER_A, HRTIM_OUTPUT_TIO1, outputCfg); HAL_HRTIM_OutputConfig(hhrtim1, HRTIM_TIMERID_TIMER_A, HRTIM_OUTPUT_TIO2, outputCfg); /* 使能输出 */ HAL_HRTIM_WaveformOutputStart(hhrtim1, HRTIM_OUTPUT_TIO1); HAL_HRTIM_WaveformOutputStart(hhrtim1, HRTIM_OUTPUT_TIO2); }这段代码的核心逻辑是主定时器作为全局时基周期设置为 170 个时钟周期1us。子定时器 A 的周期跟随主定时器比较值设为 85输出高电平从比较事件开始到周期结束复位。这样产生的就是 1MHz、占空比 50% 的 PWM。TIO1 和 TIO2 都配置成相同波形实际上就是两路同相位的输出如果你需要互补可以把 TIO2 的 Set 和 Reset 事件对调。4.2 HRTIM 高分辨率模式开关要让 HRTIM 真正输出高分辨率的边沿必须打开高分辨率模式。这一步骤 CubeMX 有时候会自动完成但手写代码时容易漏。关键代码如下HAL_HRTIM_HRTIMSyncConfig(hhrtim1, HRTIM_SYNC_SRC_MASTER, HRTIM_SYNC_SRC_NONE); HAL_HRTIM_HRTIMConfig(hhrtim1, HRTIM_HRTIMCFG_DLLSEL);这里HRTIM_HRTIMCFG_DLLSEL就是让内部 DLL 作为高分辨率时钟源的开关。配置完成后HRTIM 的输出比较边沿就可以在粗计数周期的内部细槽里移动。手册说分辨率可以达到 130ps实际稳定工作还会受到温度、电压和 DLL 校准的影响需要根据工况留出余量。4.3 边沿控制的实战思路绝大多数 HRTIM 项目不会只用固定占空比而是需要在运行中动态改变比较值实现输出电压调节或电机转矩控制。我之前以为直接写HAL_HRTIM_CompareConfig就行但后来发现这个操作会把比较值写入影子寄存器真正的生效时机取决于更新事件。为了确保边沿平滑且确定我配置了周期更新和重复计数并在更新中断里修改占空比void HRTIM1_IRQHandler(void) { if (__HAL_HRTIM_GET_FLAG(hhrtim1, HRTIM_FLAG_MCMP1) ! 0) { __HAL_HRTIM_CLEAR_FLAG(hhrtim1, HRTIM_FLAG_MCMP1); // 根据 ADC 采样结果更新比较值 uint32_t newCompare ComputeDutyFromADC(adcValue); HAL_HRTIM_CompareConfig(hhrtim1, HRTIM_TIMERID_TIMER_A, HRTIM_COMPAREUNIT_1, newCompare); } }注意更新只发生在特定事件点不是随便哪一刻改了马上生效。如果你需要“立即生效”的边沿调整可以手动触发更新事件但那样可能会在 PWM 周期中间产生不完整的波形电源应用里一般不推荐。5. 高精度系统里那些坑从时钟抖动到死区计算高精度定时器带来的不全是好处它对硬件设计和代码细节极其敏感。我把我实际遇到的问题和排查思路整理出来希望能帮你少走几天弯路。5.1 边沿抖动来源与排查我一开始在板子上验证 HRTIM 输出用示波器测量看到的边沿非常干净但用频谱仪看却有一些边带杂散。排查了很久才意识到问题不在定时器本身而是我给芯片供电的 LDO 高频噪声偏大。高分辨率模式下DLL 的延迟链对电源电压波动非常敏感。供电电压稍微抖一点延迟链每个级的时间常数就会跟着变最终输出边沿位置就产生了调制性抖动。这种抖动在时域上可能只有几百 ps但频谱上会表现为明显的杂散。解决方法是把定时器相关的模拟电源引脚单独滤波。STM32G474 有单独的 VDDA、VREF 引脚我用磁珠加 100nF 电容做了一个小 π 型滤波抖动杂散立竿见影地降了十几个 dB。5.2 死区时间不是简单填数字HRTIM 的死区时间配置比 TIM1 更灵活但同样有编码规则。很多人直接把 100ns 换算成时钟周期数填进去这在高频 PWM 下会出问题因为 HRTIM 还支持小数死区Fractional Deadtime也就是死区数值可以是计数时钟的分数倍。你得先确定你用的是整数死区还是小数死区模式然后按对应的格式填数。我的经验是如果死区精度要求不高比如 100ns 级别直接用整数死区模式省心。但如果你的开关频率在 1MHz 以上死区只有几十 ns这时候整数死区的一个步进 5.88ns 就可能让死区偏差达到 10% 以上就需要开小数死区功能。小数死区模式下DLL 会把一个计数周期细分死区寄存器的高字节是整数部分低字节是小数部分换算关系需要严格查手册。5.3 同步与触发时序的确定性问题在高精度场景中ADC 采样点、PWM 更新点和保护触发点之间的时序关系决定整个系统的性能。我在做电流环时最初使用 ADC 连续采样然后在中断里更新占空比。结果发现由于 ADC 采样时刻相对于 PWM 周期是漂浮的每次采样到的电流纹波位置不同换算出来的占空比就带上了周期性误差。正确的做法是让 ADC 由 HRTIM 的特定事件触发比如在主定时器周期的中心点触发采样。G474 的 HRTIM 可以直接产生触发信号给 ADC配置好后每次采样点都精确地落在 PWM 周期的同一相位上。这个“硬件触发 周期同步”是整个高精度控制闭环里非常重要的一环代码量不大但能显著提升控制质量。要在 HRTIM 里生成 ADC 触发事件我配置了主定时器的比较事件来产生触发输出HAL_HRTIM_EventTriggerConfig(hhrtim1, HRTIM_TIMERID_MASTER, triggerCfg);然后在 ADC 初始化里把触发源设置为 HRTIM1 的触发输出这样每次主定时器计数到指定值ADC 就会自动启动转换不需要 CPU 干预。5.4 调试时用逻辑分析仪还是示波器高精度的边沿验证如果只是看有没有波形普通逻辑分析仪就够。但要看 130ps 分辨率和抖动至少需要 500MHz 以上带宽的示波器而且探头一定要用差分探头接地环路也会引入噪声。有一次我用普通探头测 HRTIM 输出示波器显示的云纹图抖动明显换差分探头后云纹图变清晰不少这里很大一部分“抖动”其实是测量方式带来的。如果你暂时没有高端示波器可以退而求其次用频谱仪观察谐波分量来确定是否存在不稳定的边沿抖动这种方法在数字电源调试里很管用。6. 从 TIM1 到 HRTIM 的迁移与后续扩展如果你手里已经有一个基于 TIM1 的标准 PWM 工程不要急着推倒重来。HRTIM 和 TIM1 在很多行为上是一致的比如比较值需要影子寄存器、更新事件决定生效时机、死区有编码规则。我建议按以下顺序迁移先把 HRTIM 的时基和主/从定时器关系跑通输出固定频率固定占空比。再把互补输出和死区配置加上。然后接上 ADC 触发形成闭环。最后再做保护逻辑比如比较器触发刹车、硬件过流关断。这样每一步的风险都可控。有个很有意思的扩展HRTIM 的多个从定时器之间可以做任意相移用来实现多相交错并联电源。比如我最近在尝试用 HRTIM 的三路子定时器分别驱动三路 Buck 电感各相之间相位差 120 度这样输入电流纹波会显著减小。配置起来就是在每个从定时器的 StartSync 里选择不同的延迟事件代码层面只是多配置几个相同结构。另外G474 的 HRTIM 还能和内部的 DAC、比较器联动实现峰值电流控制而不占用 CPU。这些高级玩法需要你对 HRTIM 的每个事件触发源都很熟悉我目前也还在逐步验证中。总体上G474 的高精度定时器是一个值得花时间吃透的模块它让 170MHz 的普通 MCU 有了把它当成数字电源控制器的底气。本文还有配套的精品资源点击获取
返回列表