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代码实战的整个流程串起来了。你对照着看,思路会更清晰。
这张图把整个流程分成了三层。你从上往下看,就是代码执行的顺序。先初始化,再调整占空比,最后控制开关。每一步都有对应的HAL库函数,直接调用就行。
好了,代码实战就到这。你照着敲一遍,跑通了,PWM这块就算真正入门了。