PWM 从入门到入土
XiaoMa 博士生

引言

在嵌入式开发中,除了对电路进行简单的数字量控制(如【打开/关闭】,也就是“状态”控制),还会进行一些模拟电路控制(也就是“程度”控制)。举个栗子:比如现在控制一盏灯,简单的开关灯,就是对数字量(0、1)的控制;而控制灯的亮度,如把灯调得暗一些,就是对模拟量的控制。PWM 就是用来做“程度”控制的一种技术。

概念

PWM(Pulse Width Modulation,脉宽调制)是一种调制技术,用于控制模拟信号的幅度、频率和相位等特性。PWM 技术通过改变信号的脉冲宽度,来控制信号的平均电压值和电平持续时间,从而实现对模拟信号的控制。

在 PWM 技术中,一个周期的时间被分为若干个等分的时间片,每个时间片内的电平状态由信号的脉冲宽度决定,通常用占空比(Duty Cycle)表示,即高电平时间与一个周期的比值。例如,如果一个周期的时间为 1 秒,高电平时间为 0.5 秒,则占空比为 50%。

image-20230407175807992
图 1.1 PWM 输出原理图

图注:定时器工作模式为向上计数,当 CNT<CCRx 时,输出低电平,当 CNT>CCRx 时,输出高电平。当 CNT=ARR 时,重新归零,然后重新向上计数,依次循环。改变 CCRx 的值,就可以改变 PWM 的占空比。改变 ARR 的值,就可以改变 PWM 的输出频率。

举栗:定时器 TIM2 的 APB1 桥频率为 84MHz,PSC = 84 -1,经过预分频器,频率变成了 1MHz(=1/106=0.001ms),ARR 设置为 1000-1,则相当于 1KHz(= 1ms)重载一次,也就意味着 1ms 产生一次中断。①如果想要 0.2s 产生一次中断,则将 ARR 的值设为 200x103-1,如果想要 0.5s 产生一次中断,则将 ARR 的值设为 500x103-1。②如果想要占空比为 50%,则将 CCRx 的值设置为 ARR 的一半。

实操

例程 1:PWM 呼吸灯

步骤
  1. 打开 STM32CubeMX 软件,按照 STM32CubeMX 通用配置[1]配置完成后。将 PB0、PB1 引脚分别设置为 TIM3_CH3TIM3_CH4。接着配置 TIM3 的 CH3、CH4,如图所示:
image-20230407230043429
图 3.1 LED 灯引脚定时器功能配置
  1. 配置完成后生成代码。打开 Keil5 MDK 工程文件,按照 Keil5 MDK 通用配置[1]配置完成后,开始编写代码。

    打开 main.c 文件。在【main】函数中开启 PWM。

    1
    2
    3
    4
    /* USER CODE BEGIN 2 */
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_3);
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_4);
    /* USER CODE END 2 */

    在【main】函数中的【while】循环中写 PWM 控制呼吸灯功能。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    /* Infinite loop */
    /* USER CODE BEGIN WHILE */
    while (1)
    {
    /** LED0 配置 **/
    /* LED0 逐渐熄灭 */
    for(uint16_t pwmVal=0; pwmVal<1000; pwmVal++)
    {
    __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_4, pwmVal);
    HAL_Delay(1);
    }
    /* LED0 逐渐点亮 */
    for(uint16_t pwmVal=1000; pwmVal>0; pwmVal--)
    {
    __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_4, pwmVal);
    HAL_Delay(1);
    }

    /** LED1 配置 **/
    /* LED1 逐渐熄灭 */
    for(uint16_t pwmVal=0; pwmVal<1000; pwmVal++)
    {
    __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_3, pwmVal);
    HAL_Delay(1);
    }
    /* LED1 逐渐点亮 */
    for(uint16_t pwmVal=1000; pwmVal>0; pwmVal--)
    {
    __HAL_TIM_SetCompare(&htim3, TIM_CHANNEL_3, pwmVal);
    HAL_Delay(1);
    }
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
  2. 编译代码,下载到开发板。例程成功运行。

总结

Q1:pwmVal 变量的值与 LED 灯亮度的关系?

A1:当 pwmVal 的值从 0 到 1000 变化时,PWM 波形的占空比会从 0% 逐渐变化到 100%。具体来说,当 pwmVal 为 0 时,PWM 波形的占空比为 0%,即输出为低电平;当 pwmVal 为 1000 时,PWM 波形的占空比为 100%,即输出为高电平。正点原子阿波罗这款开发板的 LED 等是低电平亮、高电平灭,因此,当 pwmVal 的值增加时,占空比增加,高电平增加,亮度变暗。当 pwmVal 的值减小时,占空比减小,低电平增加,亮度变亮。pwmVal 的值与 LED 灯亮度呈反比关系。

pwmVal 占空比 高电平 亮度

Q2:【HAL_Delay】函数的作用?

A2:【HAL_Delay】函数的作用是产生延时,使程序在逐渐增加或降低 LED 亮度的过程中,能够有足够的时间让人眼观察到亮度变化。如果没有 Delay 函数,LED 的亮度会在很短的时间内逐渐变亮或变暗,人眼很难察觉到亮度变化。延时时间越长,越能充分观察到 LED 亮度的变化。但是如果延时时间过长,则会影响程序的响应速度。另外,建议使用【HAL_Delay】作为延时函数而不是另外写一个,因为这个更准确,可以产生 1ms 延时。如下面这个函数,通过循环计数的方式实现延时,即在一个循环中执行一定的操作,直到计数器减为 0,从而产生一定的延时,但是其单位是 CPU 时钟周期或者是毫秒,就不够准确了,所以还是不要用了。

1
2
3
4
void Delay(unsigned int t)
{
while(t--);
}

例程 2:PWM 舵机

功能实现:①使用 TIM2 中断控制 LED0 的闪烁,用于检测显示程序正在实时运行。②使用 TIM3_CH1 控制 PA6 引脚输出 PWM,控制舵机(SG90 180°)。③使用按键 KEY0 控制 PWM 输出。④使用按键 KEY1 控制 LED1 的亮灭,用于检测显示程序正在实时运行。

步骤
  1. 打开 STM32CubeMX 软件,按照 STM32CubeMX 通用配置[1]配置完成后。将 LED 灯引脚 PB0、PB1 引脚设置为 GPIO_Output,将按键引脚 PH2、PH3 引脚设置为 GPIO_Input,将 PA6 引脚设置为 TIM3_CH1
image-20230408210843835
图 3.2 LED 灯、按键、PWM 引脚设置
  1. 将按键引脚 PH2、PH3 改为上拉(Pull-up)输入。
image-20230408213106845
图 3.3 按键引脚配置
  1. 定时器 TIM3 配置。

    180° 舵机周期为 20ms,则 ARR = 20*103-1。

image-20230408213427558
图 3.4 定时器 TIM3_CH1 配置
  1. 定时器 TIM2 配置。

    • 每 200ms 翻转一次 LED0 电平,ARR = 200*103-1。

    • 使能中断。

image-20230408214340107
图 3.5 定时器 TIM2 ARR 配置
image-20230408220018741
图 3.6 定时器 TIM2 中断使能
  1. 配置完成后生成代码。打开 Keil5 MDK 工程文件,按照 Keil5 MDK 通用配置[1]配置完成后,开始编写代码。

    分步代码

    ① 开启定时器中断和 PWM:打开 main.c 文件。

    1
    2
    3
    4
    5
    6
    7
    -----------------------
    ↓↓↓注:main() 函数内↓↓↓
    -----------------------
    /* USER CODE BEGIN 2 */
    HAL_TIM_Base_Start_IT(&htim2); //开启定时器中断
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); //开启 PWM
    /* USER CODE END 2 */

    ② 使用定时器 TIM2 中断控制 LED0 的闪烁:打开 main.c 文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    -----------------------
    ↓↓↓注:main() 函数外↓↓↓
    -----------------------
    /* USER CODE BEGIN 0 */
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
    /* TIM2--LED0 每 20ms 翻转一次电平 */
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1);
    }
    /* USER CODE END 0 */

    ③ KEY1 控制 LED1 亮灭:打开main.c 文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    -----------------------
    ↓↓↓注:main() 函数外↓↓↓
    -----------------------
    /* USER CODE BEGIN 0 */
    /* 宏定义 KEY0、KEY1 */
    # define KEY0 HAL_GPIO_ReadPin(GPIOH,GPIO_PIN_3)
    # define KEY1 HAL_GPIO_ReadPin(GPIOH,GPIO_PIN_2)

    void Scan_Keys()
    {
    /* KEY1--LED1 按下控制亮灭 */
    if(KEY1 == 0)
    {
    HAL_Delay(200); //延时函数去抖动
    if(KEY1 == 0)
    {
    HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);
    while(KEY1 == 0);
    }
    }
    }
    /* USER CODE END 0 */
    -----------------------
    ↓↓↓注:main() 函数内↓↓↓
    -----------------------
    /* USER CODE BEGIN WHILE */
    while (1)
    {
    Scan_Keys();
    /* USER CODE END WHILE */
    /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */

    ④ KEY0 控制 PWM 输出:打开 main.c 文件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    -----------------------
    ↓↓↓注:main() 函数外↓↓↓
    -----------------------
    /* USER CODE BEGIN 0 */
    /* 宏定义 KEY0、KEY1 */
    # define KEY0 HAL_GPIO_ReadPin(GPIOH,GPIO_PIN_3)
    # define KEY1 HAL_GPIO_ReadPin(GPIOH,GPIO_PIN_2)

    void Scan_Keys()
    {
    /* KEY0--PWM 按下控制舵机旋转 90° */
    if(KEY0 == 0)
    {
    HAL_Delay(200); //延时函数去抖动
    if(KEY0 == 0)
    {
    /* 运行逻辑:开始舵机在 0° 位置,1500 表示转到 90°,500 表示转到 0°,此处代码的意思是按下 KEY0,舵机由初始位置 0° 旋转到 90°,然后再回到初始位置 0° */
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 1500);
    HAL_Delay(1000); // 延时 1s,为了让舵机旋转到 90° 时停一下再旋转
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);
    while(KEY0 == 0);
    }
    }
    }
    /* USER CODE END 0 */
    -----------------------

    完整代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    -----------------------
    ↓↓↓注:main() 函数外↓↓↓
    -----------------------
    /* USER CODE BEGIN 0 */
    # define KEY0 HAL_GPIO_ReadPin(GPIOH,GPIO_PIN_3)
    # define KEY1 HAL_GPIO_ReadPin(GPIOH,GPIO_PIN_2)

    /* TIM2 中断回调函数:控制 LED0 闪烁 */
    void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
    {
    /* TIM2--LED0 每 20ms 翻转一次电平 */
    HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_1);
    }

    /* 功能函数:扫描按键 */
    void Scan_Keys()
    {
    /* KEY0--PWM 按下控制舵机旋转 90° */
    if(KEY0 == 0)
    {
    HAL_Delay(100); //延时函数去抖动
    if(KEY0 == 0)
    {
    /* 运行逻辑:开始舵机在 0° 位置,1500 表示转到 90°,500 表示转到 0°,此处代码的意思是按下 KEY0,舵机由初始位置 0° 旋转到 90°,然后再回到初始位置 0° */
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 1500);
    HAL_Delay(1000); // 延时 1s,为了让舵机旋转到 90° 时停一下再旋转
    __HAL_TIM_SET_COMPARE(&htim3, TIM_CHANNEL_1, 500);
    while(KEY0 == 0);
    }
    }

    /* KEY1--LED1 按下控制亮灭 */
    if(KEY1 == 0)
    {
    HAL_Delay(200); //延时函数去抖动
    if(KEY1 == 0)
    {
    HAL_GPIO_TogglePin(GPIOB,GPIO_PIN_0);
    while(KEY1 == 0);
    }
    }
    }
    /* USER CODE END 0 */

    -----------------------
    ↓↓↓注:main() 函数内↓↓↓
    -----------------------
    /* USER CODE BEGIN 2 */
    HAL_TIM_Base_Start_IT(&htim2); //开启定时器中断
    HAL_TIM_PWM_Start(&htim3, TIM_CHANNEL_1); //开启 PWM
    /* USER CODE END 2 */

    /* USER CODE BEGIN WHILE */
    while (1)
    {
    Scan_Keys();
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    }
    /* USER CODE END 3 */
  2. 编译代码,下载到开发板。例程成功运行,各功能均已实现。

总结

参考

[1] 成为点灯大师 LED Master | Story Begins……

[2] STM32 HAL 库实现舵机旋转 | Story Begins……