4. PWM初始化代码实战:时钟使能、GPIO配置、定时器初始化、PWM参数设置、启动PWM输出

好,咱们直接进入正题。上一章讲了PWM的原理,这一章咱们动手写代码。很多初学者拿到开发板,第一件事就是复制粘贴例程,结果灯不亮、电机不转,一脸懵。

我刚开始做项目时也这样,后来发现——PWM初始化是有固定套路的。你只要按顺序把五步走完,基本不会出问题。哪五步?看下面这张图就清楚了。

PWM初始化五步流程 ① 时钟使能 ② GPIO配置 ③ 定时器初始化 ④ PWM参数设置 ⑤ 启动PWM输出 💡 核心要点 顺序不能乱! 先开时钟,再配GPIO, 最后设PWM参数。

说白了,这五步就是PWM初始化的标准动作。下面我一步步拆开讲,每一步都配上代码,你跟着敲一遍就明白了。

4.1 第一步:时钟使能

为什么第一步必须是时钟使能?因为芯片里所有外设都是靠时钟驱动的。你不开时钟,寄存器写不进去,配置了也白配。

我个人习惯用STM32的HAL库做演示,因为大部分工程师都在用。其他芯片的思路完全一样,只是寄存器名字不同。

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

// 使能GPIOA的时钟(PA0通常复用为TIM2_CH1)
__HAL_RCC_GPIOA_CLK_ENABLE();
小技巧: 我习惯把时钟使能写在最前面,紧跟着就是GPIO时钟。这样后面配置GPIO时,寄存器已经能正常访问了。曾经有个同事把GPIO时钟使能写在了GPIO配置后面,结果调试了一下午——嗯,你猜怎么着?GPIO死活不工作。

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);
注意: 不同芯片的复用功能号不一样。STM32F1系列用GPIO_PinRemapConfig,F4/H7系列用Alternate参数。一定要查数据手册,别想当然。我曾经在F407上直接复制了F103的代码,结果PA0死活不出波形——查了半天才发现复用号不对。

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);
经验之谈: 我一般先确定需要的分辨率,再反推PSC和ARR。比如电机控制需要精细调节,我会把ARR设大一些(比如1000或2000),这样占空比调节更细腻。如果只是控制LED亮度,100级分辨率就足够了。

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);
重要提醒: 启动PWM后,如果修改了ARR(自动重装载值),一定要重新调用HAL_TIM_PWM_Start(),否则新ARR不会生效。我踩过这个坑——调试时改了频率,结果波形纹丝不动,折腾了半小时才发现是没重新启动。

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);
}
调试建议: 代码写完后,用示波器或逻辑分析仪看引脚波形。如果没波形,按这个顺序排查:①时钟使能了吗?②GPIO复用功能配对了没?③定时器时钟源对不对?④启动函数调了吗?我90%的调试问题都出在这四个环节。

好了,PWM初始化的五步法就这些。代码不长,但每一步都有讲究。你把这五步记牢了,换任何芯片、任何定时器,思路都一样——只是寄存器名字和库函数名不同而已。