4、PWM输出代码实战:HAL库PWM初始化、占空比动态调整、输出使能与关闭

理论讲完了,咱们得动真格的了。

这一章,我带你手写PWM输出的核心代码。用的是STM32的HAL库,因为现在大部分项目都在用。你想想看,搞嵌入式不会HAL库,就像开车不会挂挡,寸步难行。

我会从初始化开始,一步步带你调通。然后动态改占空比,最后控制输出的开关。嗯,这三板斧砍下来,PWM这块你就拿下了。

4.1 HAL库PWM初始化:从GPIO到定时器

初始化PWM,说白了就是告诉芯片:哪个引脚要干活,用哪个定时器,频率多快,占空比多少。

我个人习惯,先把GPIO配好,再配定时器。顺序别搞反,否则容易出一些莫名其妙的bug。

4.1.1 GPIO配置

PWM信号是从定时器的通道引脚输出的。比如TIM2的通道1,对应PA0。你得把这个脚配成复用功能。

// 使能GPIO时钟
__HAL_RCC_GPIOA_CLK_ENABLE();

GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;        // 复用推挽输出
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;      // 复用为TIM2
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

这里有个坑,我刚开始学的时候踩过。Alternate这个参数,不同芯片不一样。F1系列是GPIO_PinAFConfig,F4/H7系列是结构体里的Alternate。你查手册时一定要看仔细。

4.1.2 定时器基础配置

接下来配定时器。PWM的频率由定时器的时钟、预分频器和自动重装载值决定。

// 使能定时器时钟
__HAL_RCC_TIM2_CLK_ENABLE();

TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71;          // 预分频,72MHz / (71+1) = 1MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999;            // 自动重装载,1MHz / (999+1) = 1kHz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(&htim2);

频率怎么算?公式很简单:
PWM频率 = 定时器时钟 / (Prescaler + 1) / (Period + 1)

上面这个例子,72MHz / 72 / 1000 = 1kHz。也就是1毫秒一个周期。

4.1.3 PWM通道配置

定时器初始化完了,还得配通道。通道决定的是极性、占空比初始值。

TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;          // PWM模式1
sConfigOC.Pulse = 500;                        // 初始占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;   // 高电平有效
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);

Pulse这个值,就是比较值。占空比 = Pulse / (Period + 1)。所以500 / 1000 = 50%。

关键点:PWM模式1和模式2的区别,就是有效电平相反。模式1是计数值小于比较值时输出有效电平,模式2相反。我一般用模式1,比较直观。

4.2 占空比动态调整:让电机转起来

初始化只是开始。实际项目中,占空比是要随时变的。比如控制电机转速,你不可能让电机永远转一个速度。

动态调整占空比,HAL库给了两个函数:

函数 作用 使用场景
__HAL_TIM_SET_COMPARE 直接修改比较寄存器 快速调整,不重新配置通道
HAL_TIM_PWM_ConfigChannel 重新配置通道参数 需要改极性或模式时

我建议你用宏定义那个。速度快,而且不会触发不必要的重新初始化。

// 动态调整占空比为75%
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 750);

// 调整占空比为25%
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 250);

就这么简单。你可以在主循环里,或者中断里,随时调用这个宏。

我的经验:占空比变化不要太剧烈。比如从0%直接跳到100%,电机可能会猛冲一下。我一般做个渐变,每次加一点点,让系统平滑过渡。

4.2.1 渐变示例

// 从0%渐变到100%,每次增加1%
for(uint16_t i = 0; i <= 999; i += 10) {
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, i);
    HAL_Delay(10);  // 每10ms调整一次
}

这个例子,占空比从0慢慢升到100%,整个过程大概1秒钟。电机就会平稳加速,不会吓你一跳。

4.3 PWM输出使能与关闭:该停的时候要停

初始化完了,占空比也设好了,但PWM还没输出呢。你得手动打开。

HAL库提供了两个函数:

// 使能PWM输出
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);

// 关闭PWM输出
HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1);

使能之后,引脚上就有波形了。关闭之后,引脚会变成什么状态?

嗯,这里要注意。关闭PWM输出后,引脚默认是浮空输入状态。如果你想让引脚保持低电平,得手动拉低。

避坑指南:我曾经在一个项目中,关闭PWM后没处理引脚状态。结果电机虽然停了,但引脚上的残余电平导致驱动芯片误动作,烧了一个MOS管。从那以后,我关PWM时都会顺手把引脚拉低。

// 安全关闭PWM
HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);  // 强制拉低

4.4 完整代码框架:一整套流程

把上面这些串起来,就是一个完整的PWM控制流程。

// 1. 初始化
void PWM_Init(void) {
    // GPIO配置
    __HAL_RCC_GPIOA_CLK_ENABLE();
    GPIO_InitTypeDef GPIO_InitStruct = {0};
    GPIO_InitStruct.Pin = GPIO_PIN_0;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
    GPIO_InitStruct.Alternate = GPIO_AF1_TIM2;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    // 定时器配置
    __HAL_RCC_TIM2_CLK_ENABLE();
    htim2.Instance = TIM2;
    htim2.Init.Prescaler = 71;
    htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
    htim2.Init.Period = 999;
    htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
    htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
    HAL_TIM_PWM_Init(&htim2);

    // 通道配置
    TIM_OC_InitTypeDef sConfigOC = {0};
    sConfigOC.OCMode = TIM_OCMODE_PWM1;
    sConfigOC.Pulse = 0;          // 初始占空比0%
    sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
    sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
    HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
}

// 2. 启动输出
void PWM_Start(void) {
    HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}

// 3. 动态调整占空比
void PWM_SetDuty(uint16_t duty) {
    if(duty > 999) duty = 999;  // 限幅
    __HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, duty);
}

// 4. 停止输出
void PWM_Stop(void) {
    HAL_TIM_PWM_Stop(&htim2, TIM_CHANNEL_1);
    HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_RESET);
}

这个框架,我用了好几年。换芯片、换定时器,改改参数就能用。你直接拿去用,没问题。

4.5 本章知识体系

下面这张图,把PWM代码实战的整个流程串起来了。你对照着看,思路会更清晰。

PWM输出代码实战知识体系 PWM输出代码实战知识体系 1. HAL库PWM初始化 GPIO配置(复用推挽) 定时器配置(频率) 通道配置(占空比) 2. 占空比动态调整 宏定义:__HAL_TIM_SET_COMPARE 渐变控制(平滑过渡) 3. PWM输出使能与关闭 使能:HAL_TIM_PWM_Start 关闭:HAL_TIM_PWM_Stop + 拉低 核心流程:初始化 → 动态调整 → 使能/关闭,三步走完PWM控制

这张图把整个流程分成了三层。你从上往下看,就是代码执行的顺序。先初始化,再调整占空比,最后控制开关。每一步都有对应的HAL库函数,直接调用就行。

好了,代码实战就到这。你照着敲一遍,跑通了,PWM这块就算真正入门了。