4、HAL库定时器PWM配置:CubeMX初始化步骤、HAL_TIM_PWM_Start函数解析、通道配置

好,咱们进入实战环节。前面聊了那么多PWM的原理,说白了就是让定时器输出一串可调宽度的方波。那在STM32上具体怎么搞?我个人习惯用CubeMX先把硬件初始化搭好,再在代码里调参数。这样省事,也不容易漏配置。

这一章,我就带你走一遍完整的流程。从CubeMX的图形化配置,到生成的代码怎么理解,再到那个关键的HAL_TIM_PWM_Start函数到底干了什么。嗯,咱们一步步来。

本章核心脉络:CubeMX图形化配置 → 生成初始化代码 → 理解HAL库PWM启动函数 → 掌握多通道独立配置方法。

PWM配置知识结构图 定时器PWM配置 CubeMX图形化配置 时钟源、预分频、自动重装 HAL_TIM_PWM_Start 启动PWM信号输出 多通道独立配置 极性、占空比、互补输出 三个环节环环相扣:CubeMX生成骨架 → HAL函数驱动输出 → 通道参数决定波形 任何一个环节出错,PWM都出不来

4.1 CubeMX初始化步骤:从选定时器到生成代码

打开CubeMX,选好你的芯片型号。我习惯先配好时钟树,再管外设。因为定时器的时钟源直接决定了PWM的频率范围。

第一步:选定时器

在Pinout & Configuration界面,找到Timers分类。展开后能看到TIM1到TIMx。选哪个?

  • 高级定时器(TIM1、TIM8):带互补输出、刹车功能,适合电机控制。
  • 通用定时器(TIM2~TIM5):最常用,舵机控制绰绰有余。
  • 基本定时器(TIM6、TIM7):没有PWM输出通道,别选。

我个人做舵机控制时,首选TIM2或TIM3。为什么?因为它们的通道映射到常用的GPIO引脚,布线方便。

第二步:配置时钟源与参数

选中定时器后,在Mode栏里把Channelx选为PWM Generation CHx。然后切到Configuration → Parameter Settings,填几个关键参数:

参数名 说明 典型值(舵机50Hz)
Prescaler(预分频器) 将定时器时钟分频,降低计数频率 71(72MHz时钟→1MHz计数)
Counter Mode 计数方向,PWM一般用Up(向上计数) Up
Counter Period(自动重装值) 决定PWM周期 19999(1MHz/20000=50Hz)
Auto-reload preload 使能后,更新事件时同步加载新周期值 Enable

小技巧:计算PWM频率的公式很简单——PWM频率 = 定时器时钟 / (Prescaler+1) / (Counter Period+1)。我每次配完都会心算一遍,确认频率对不对。比如72MHz/(71+1)/(19999+1)=50Hz,正好是舵机的标准频率。

第三步:配置PWM通道参数

在Parameter Settings里往下翻,找到PWM Generation Channel x部分:

  • Mode:选PWM mode 1或PWM mode 2。mode 1是计数小于比较值时输出有效电平,mode 2相反。舵机控制用mode 1就行。
  • Pulse(比较值):这就是占空比的原始值。比如周期20000,脉宽1500对应7.5%占空比,舵机中位。
  • Output polarity:High或Low。选High,有效电平为高。

第四步:配置GPIO

CubeMX会自动把定时器通道映射到对应的GPIO引脚。你可以在Pinout视图里确认一下,确保引脚没有被其他外设占用。我遇到过好几次,引脚被SPI或I2C占了,结果PWM死活出不来。嗯,检查引脚冲突是必修课。

第五步:生成代码

点Project → Generate Code。选好IDE(MDK-ARM或STM32CubeIDE),生成。你会得到一个包含定时器初始化函数的工程。

4.2 HAL_TIM_PWM_Start函数解析:它到底干了什么?

代码生成后,你会发现main函数里并没有启动PWM。CubeMX只帮你配好了参数,但没帮你开闸放水。启动PWM,得靠这个函数:

HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

这个函数有两个参数:定时器句柄指针和通道号。它内部做了三件事:

  1. 使能定时器计数器:让定时器开始计数。没有这一步,PWM引脚就是死的。
  2. 使能通道输出:把比较器的输出连接到GPIO的复用功能。说白了,就是告诉GPIO:“别当普通IO了,让定时器接管你。”
  3. 触发更新事件:把预装载的寄存器值一次性刷到影子寄存器。这样你之前配的周期、比较值才能生效。

我曾经在项目里犯过一个低级错误:调用了HAL_TIM_PWM_Start,但忘了检查返回值。结果定时器时钟没配好,函数返回HAL_ERROR,PWM根本没启动。后来我养成了习惯,每次调用后都加一句断言:

if (HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1) != HAL_OK)
{
    Error_Handler();
}

注意:HAL_TIM_PWM_Start只启动单个通道。如果你要用多个通道,每个通道都要单独调用一次。比如控制两个舵机,就得调两次:

HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);

还有一个兄弟函数——HAL_TIM_PWM_Start_DMA。它用DMA自动更新比较值,适合需要连续改变占空比的场景,比如呼吸灯。不过咱们舵机控制用标准函数就够了。

4.3 通道配置:如何独立控制每个通道的占空比?

定时器的每个通道都有自己的比较寄存器(CCR)。改变占空比,就是改变CCR的值。HAL库提供了两个宏:

__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1500);  // 设置通道1占空比
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 2000);  // 设置通道2占空比

这两个宏直接操作CCR寄存器,实时生效。你想想看,舵机控制的核心就是不断调整这个值。比如让舵机从0°转到90°,你只需要把比较值从500逐渐增加到1500。

通道配置的几个关键点:

  • 极性设置:在CubeMX里配好,一般不动。如果发现舵机方向反了,检查极性是不是设成了Low。
  • 互补输出:高级定时器才有。电机控制里,互补输出配合死区插入,防止上下桥臂直通。
  • 空闲状态:定时器停止时,PWM引脚输出什么电平。我一般设成低电平,防止舵机误动作。

实战经验:我在做一个六轴机械臂项目时,用了TIM2的四个通道控制四个舵机。每个通道独立配置占空比,互不干扰。但要注意,所有通道共享同一个定时器周期。所以如果你需要不同的PWM频率,就得用不同的定时器。

多通道配置的典型代码结构:

// 初始化(CubeMX生成)
MX_TIM2_Init();

// 启动所有通道
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_2);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_3);
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_4);

// 主循环中动态调整占空比
while (1)
{
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 1000);  // 舵机1
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_2, 1500);  // 舵机2
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_3, 2000);  // 舵机3
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_4, 2500);  // 舵机4
    HAL_Delay(20);
}

你看,代码其实不复杂。核心就是三步:CubeMX配参数 → 启动PWM → 动态改比较值。但每一步都有坑。比如预分频器和周期算错了,PWM频率就不对;通道没启动,改比较值也没用。

调试小技巧:如果你用示波器看PWM波形,发现没有输出,先检查GPIO的复用功能配置。在CubeMX里确认引脚是不是灰色的AF模式。如果不是,手动改一下。我吃过这个亏,折腾了半小时才发现引脚模式没设对。