4. PWM初始化代码实战:时钟使能、GPIO配置、定时器初始化、PWM参数设置、启动PWM输出
好,咱们直接进入正题。上一章讲了PWM的原理,这一章咱们动手写代码。很多初学者拿到开发板,第一件事就是复制粘贴例程,结果灯不亮、电机不转,一脸懵。
我刚开始做项目时也这样,后来发现——PWM初始化是有固定套路的。你只要按顺序把五步走完,基本不会出问题。哪五步?看下面这张图就清楚了。
说白了,这五步就是PWM初始化的标准动作。下面我一步步拆开讲,每一步都配上代码,你跟着敲一遍就明白了。
4.1 第一步:时钟使能
为什么第一步必须是时钟使能?因为芯片里所有外设都是靠时钟驱动的。你不开时钟,寄存器写不进去,配置了也白配。
我个人习惯用STM32的HAL库做演示,因为大部分工程师都在用。其他芯片的思路完全一样,只是寄存器名字不同。
// 使能定时器2的时钟
__HAL_RCC_TIM2_CLK_ENABLE();
// 使能GPIOA的时钟(PA0通常复用为TIM2_CH1)
__HAL_RCC_GPIOA_CLK_ENABLE();
4.2 第二步:GPIO配置
时钟开好了,接下来配GPIO。PWM输出用的是复用功能模式,不是普通的推挽输出。这个区别很重要。
你想想看,如果配成普通推挽输出,那引脚就由GPIO寄存器直接控制了,定时器根本抢不过。必须配成复用推挽输出,引脚的控制权才会交给定时器。
GPIO_InitTypeDef GPIO_InitStruct = {0};
GPIO_InitStruct.Pin = GPIO_PIN_0; // PA0
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);
4.3 第三步:定时器初始化
GPIO配好了,接下来配置定时器本身。这里要决定PWM的频率。频率由两个参数决定:预分频器(PSC)和自动重装载值(ARR)。
公式很简单:
PWM频率 = 定时器时钟 / (PSC + 1) / (ARR + 1)
举个例子。假设定时器时钟是72MHz,我想要一个1kHz的PWM,分辨率(占空比调节步数)是1000级。那怎么算?
// 目标:1kHz PWM,1000级分辨率
// 72MHz / (PSC+1) / (ARR+1) = 1000Hz
// 取 PSC = 72 - 1 = 71
// 则 ARR = 1000 - 1 = 999
// 验证:72,000,000 / 72 / 1000 = 1000Hz ✓
TIM_HandleTypeDef htim2;
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71; // PSC
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999; // ARR
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(&htim2);
4.4 第四步:PWM参数设置
定时器初始化完了,但还没完——你得告诉定时器:用哪个通道?极性是什么?初始占空比是多少?
这一步配置的是PWM模式、通道和比较值。比较值(CCR)直接决定占空比:
占空比 = CCR / (ARR + 1) × 100%
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1; // PWM模式1
sConfigOC.Pulse = 500; // CCR = 500,初始占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH; // 高电平有效
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
这里有个容易踩的坑:PWM模式1和模式2的区别。简单说:
| 模式 | 计数小于CCR时 | 计数大于CCR时 |
|---|---|---|
| PWM模式1 | 有效电平(通常是高) | 无效电平(通常是低) |
| PWM模式2 | 无效电平 | 有效电平 |
说白了,模式1是前半段高、后半段低,模式2反过来。我99%的情况都用模式1,除非有特殊需求。
4.5 第五步:启动PWM输出
前面四步都是准备工作,这一步才是真正让引脚输出波形。很多新手配完前面四步,发现引脚没波形——就是因为忘了启动。
// 启动PWM输出
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
就这一行。但要注意:启动之后,占空比是可以随时修改的。比如你想在程序运行中改变亮度或速度:
// 运行时修改占空比为75%
__HAL_TIM_SET_COMPARE(&htim2, TIM_CHANNEL_1, 750);
4.6 完整代码示例
把上面五步拼起来,就是一个完整的PWM初始化函数。我习惯把它封装好,方便以后复用。
void PWM_Init(void)
{
// 第一步:时钟使能
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_GPIOA_CLK_ENABLE();
// 第二步:GPIO配置
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);
// 第三步:定时器初始化
TIM_HandleTypeDef htim2 = {0};
htim2.Instance = TIM2;
htim2.Init.Prescaler = 71; // 72MHz / 72 = 1MHz
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999; // 1MHz / 1000 = 1kHz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
htim2.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
HAL_TIM_PWM_Init(&htim2);
// 第四步:PWM参数设置
TIM_OC_InitTypeDef sConfigOC = {0};
sConfigOC.OCMode = TIM_OCMODE_PWM1;
sConfigOC.Pulse = 500; // 初始占空比50%
sConfigOC.OCPolarity = TIM_OCPOLARITY_HIGH;
sConfigOC.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim2, &sConfigOC, TIM_CHANNEL_1);
// 第五步:启动PWM输出
HAL_TIM_PWM_Start(&htim2, TIM_CHANNEL_1);
}
好了,PWM初始化的五步法就这些。代码不长,但每一步都有讲究。你把这五步记牢了,换任何芯片、任何定时器,思路都一样——只是寄存器名字和库函数名不同而已。