STM32定时器—PWM 输出

Posted 韦东山

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了STM32定时器—PWM 输出相关的知识,希望对你有一定的参考价值。

26.1 关于 PWM

26.1.1 PWM 介绍

PWM(Pulse Width Modulation,脉冲宽度调制)。是一种利用微处理器的数字输出来对模拟电路进行控制的技术,广泛应用在测量、通信、功率控制等诸多领域。

举个最常见的例子,利用PWM控制显示屏亮度。屏幕背光可以看作是一个大灯,这个大灯只有亮、灭两种状态。如果把灯亮看作100%,灯灭看作0%,要实现50%的亮度,可以在某个单位时间里亮灯50%时间、灭灯50%时间,只要这个单位时间够小,由于人眼具有视觉暂留效应,就会从宏观的感觉整个灯是一直亮着,且亮度只要原来的一半。

PWM实质就是GPIO不断翻转输出高、低电平,这个效果可以写代码控制GPIO产生,但这样就会占用CPU,CPU就不方便做其它事情。此时可以利用定时器,设置好翻转时间,让其自动控制GPIO翻转,无需CPU再参与。

在一个周期内,高电平占整个信号周期的百分比,称之为占空比(Duty Cycle),如图 26.1.1 所示,占空比分别为30%、50%、70%。
在这里插入图片描述

26.1.2 STM32 的 PWM

PWM是定时器输出比较的典型应用。除STM32的基本定时器(TIM6、TIM7)外,其它定时器都支持PWM输出,每个通用定时器(TIM2、TIM3、TIM4、TIM5)可以同时产生4路PWM,每个高级定时器(TIM1、 TIM8)可以同时产生多达7路PWM。

以通用定时器为例,如上图 25.1.2 所示,每个定时器有四路输出通道:TIMx_CH1、TIMx_CH2、TIMx_CH2、TIMx_CH4,每个通道都对应一个捕获/比较寄存器:TIMx_CCR1\\TIMx_CCR2、TIMx_CCR3、 TIMx_CCR4。将计数器CNT的值,与捕获/比较寄存器相比较,由比较结果决定输出电平高低,从而实现PWM输出。

举个例子,若当前设置计数器为向上计数,定时器重载值为TIMx_ARR,通道1的捕获/比较寄存器值为TIMx_CCR1。如图 26.1.2 所示,首先定时器从0开始计数,在0t1时间段,TIMx_CNT<TIMx_CCR1,输出低电平;在t1t2时间段,TIMx_CNT>TIMx_CCR1,输出高电平;t2时,TIMx_CNT=TIMx_ARR计数器溢出,重新从0开始,如此循环。

由此可以看出,TIMx_ARR决定PWM的周期,TIMx_CCR1决定PWM的占空比,此时占空比计算公式为:
在这里插入图片描述

在这里插入图片描述
每个定时器的输出比较模式共同8种(通过配置寄存器CCMRx的位OCxM [2:0]选择),其中有两种是最常用的PWM输出模式:PWM模式1和PWM模式2。两种模式区别在于,计数器CNT与TIMx_CCRx比较的结果,输出的电平不同,如表 26.1.1 所示。
在这里插入图片描述

26.2 硬件设计

本实验通过三色LED灯的显示效果来展示PWM输出,原理图如图 26.2.1 所示,结合《数据手册.pdf》的引脚描述章节,可知:LED红色所接的PB0为TIM3的通道3;LED绿色所接的PB1为TIM3的通道4;LED蓝色所接的PB5重映射后为TIM3的通道2。

此外,当引脚为低电平时LED灯亮,因此PWM的占空比越高,LED越暗,PWM的占空比越低,LED灯越亮。
在这里插入图片描述

26.3 软件设计

26.3.1 软件设计思路

实验目的:本实验通过使用定时器的PWM输出功能,实现三色LED灯的红、绿、蓝组合,显示任意色,让读者理解PWM输出的设置方法。

  1. 初始化定时器相关参数:配置时钟、工作方式、PWM模式等;
  2. 初始化定时器涉及的硬件相关参数:初始化涉及的时钟、引脚、中断;
  3. 在定时器中断函数里,修改占空比;
  4. 在按键中断函数里,切换LED模式和修改R G B值;
  5. 主函数编写控制逻辑:实现随机模式和用户模式。随机模式即间隔1s,随机生成R G B值显示;用户模式即用户按键修改R G B值,显示自己需要的颜色。

本实验配套代码位于“5_程序源码\\18_定时器—PWM输出\\”。

26.3.2 软件设计讲解

  1. GPIO选择与接口定义
    宏定义涉及的定时器、周期、引脚等,如代码段 26.3.1 所示。

代码段 26.3.1 相关宏定义(driver_timer.h)

typedef struct
{
uint8_t rgb_red;
uint8_t rgb_green;
uint8_t rgb_blue;
}RGB;
#define TIMx TIM3
#define TIMx_IRQn TIM3_IRQn
#define TIMx_IRQHandler TIM3_IRQHandler
#define TIM_PRESCALER ((36*10)-1) // 频率 200kHz
#define TIM_PERIOD (2000-1) // 周期 2000/f_tim = 10ms
#define RGB_RED (0) // 占空比
#define RGB_GREEN (0) // 占空比
#define RGB_BLUE (0) // 占空比
#define TIM_RLED_PIN GPIO_PIN_0
#define TIM_RLED_PORT GPIOB
#define TIM_RLED_CHANNEL TIM_CHANNEL_3
#define TIM_GLED_PIN GPIO_PIN_1
#define TIM_GLED_PORT GPIOB
#define TIM_GLED_CHANNEL TIM_CHANNEL_4
#define TIM_BLED_PIN GPIO_PIN_5
#define TIM_BLED_PORT GPIOB
#define TIM_BLED_CHANNEL TIM_CHANNEL_2
#define TIM_PWM_CLK_EN() __HAL_RCC_TIM3_CLK_ENABLE()
#define TIM_PWM_GPIO_CLK_EN() __HAL_RCC_GPIOB_CLK_ENABLE()

首先初始化定时器PWM相关参数,如代码段 26.3.2 所示。
代码段 26.3.2 定时器 PWM 初始化(driver_timer.c)

/*
* 函数名:void TimerPWMInit(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:初始化定时器,输出频率 1Hz,占空比 50%的 PWMN 波
*/
void TimerPWMInit(void) {
TIM_ClockConfigTypeDef sClockSourceConfig;
TIM_OC_InitTypeDef sConfig;
// 定时器基本功能配置
hpwm.Instance = TIMx; // 指定定时器 TIM3
hpwm.Init.Prescaler = TIM_PRESCALER; // 预分频系数 PSC=360-1
hpwm.Init.CounterMode = TIM_COUNTERMODE_UP; // 向上计数
hpwm.Init.Period = TIM_PERIOD; // 自动装载器 ARR 的值,ARR=2000-1
// 72MHz 经过 360 分频后,定时器时钟为 200KHz,即计数器每间隔 5us 计数一次,从 0 计数到 ARR,经历 10ms
hpwm.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1; // 定时器时钟不从 HCLK 分频
//hpwm.Init.RepetitionCounter = 0; // 重复计数器值,仅存在于高级定时器
hpwm.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE; // 不自动重新装载
// 将 TIM3 按 PWM 模式初始化
if (HAL_TIM_PWM_Init(&hpwm) != HAL_OK)
{
Error_Handler(); }
// 定时器时钟源选择
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; // 选用内部时钟作为定时器时钟源
HAL_TIM_ConfigClockSource(&hpwm, &sClockSourceConfig);
// 配置 PWM 的输出通道参数
sConfig.OCMode = TIM_OCMODE_PWM1; // PWM 输出的两种模式:PWM1 当极性为低,CCR<CNT,输出低电平,反之高电平
sConfig.OCPolarity = TIM_OCPOLARITY_LOW; // 设置极性为低(硬件上低电平亮灯)
sConfig.OCFastMode = TIM_OCFAST_DISABLE; // 输出比较快速使能禁止(仅在 PWM1 和 PWM2 可设置)
sConfig.Pulse = RGB_RED; // 在 PWM1 模式下,通道 3(RLED)占空比
if (HAL_TIM_PWM_ConfigChannel(&hpwm, &sConfig, TIM_RLED_CHANNEL) != HAL_OK)
{
Error_Handler(); }
sConfig.Pulse = RGB_GREEN; // 在 PWM1 模式下,通道 4(GLED)占空比
if (HAL_TIM_PWM_ConfigChannel(&hpwm, &sConfig, TIM_GLED_CHANNEL) != HAL_OK)
{
Error_Handler(); }
sConfig.Pulse = RGB_BLUE; // 在 PWM1 模式下,通道 2(BLED)占空比
if (HAL_TIM_PWM_ConfigChannel(&hpwm, &sConfig, TIM_BLED_CHANNEL) != HAL_OK)
{
Error_Handler(); } }
  • 14行:选择配置哪一个定时器;
  • 15行:设置定时器时钟预分频系数PSC,这里设置为360-1,则72MHz经过360分频后,定时器时钟为200KHz,
    即定时器计数1次的时间为5us;
  • 16行:设置定时器计数方式,这里为向上计数:
  • 17行:设置自动装载器ARR的值,这里设置为2000-1,则计数器从0开始计数到2000,周期为10ms;
  • 20行:设置时钟分频,用于计数器工作时滤除高频干扰,本实验不涉及,任意即可;
  • 21行:设置重复计数器值,仅存在于高级定时器,这里使用的TIM2为通用定时器,不涉及;
  • 22行:设置是否定时器自动重新装载,本实验不需要自动装载;
  • 25~28行:将定时器按PWM功能初始化,同时该函数会调用“HAL_TIM_PWM_MspInit()”进行硬件相关初始化;
  • 31~32行:设置内部时钟作为定时器时钟源;
  • 35~36行:设置PWM的模式和极性。这里设置PWM1模式,极性为低,与LED灯低电平亮灯对应;此时,在周期和ARR确定的情况下,计数器CNT从0到CCR,输出低电平,LED灯亮,计数器CNT从CCR到ARR,输出高电平,LED灯灭。即,CCR值越小,占空比越大,灯越暗,CCR值越大,占空比越小,灯越亮,ARR值与亮度成正比;
  • 37行:输出比较快速模式,可减少输出延时,可以不使用禁止;
  • 38~54行:分别设置三色灯所对应的三个通道的占空比和前面的输出通道参数;当前占空比为0,后面代码再修改占空比值;

然后覆写定时器PWM硬件相关初始化,如代码段 26.3.3 所示。因为蓝色LED灯PB5,需要重映射才能是TIM3_CH2功能,这里需要使能重映射相关的AFIO(Alternate function I/O)时钟,然后使用HAL函数“__HAL_AFIO_REMAP_TIM3_PARTIAL()”启动TIM3重映射,同时将PB5设置为复用功能,此时PB5才能作为TIM3_CH2功能。

代码段 26.3.3 定时器 PWM 硬件相关初始化(driver_timer.c)

/*
* 函数名:void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
* 输入参数:htim-TIM 句柄
* 输出参数:无
* 返回值:无
* 函数作用:HAL_TIM_PWM_Init 回调硬件初始化
*/
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_InitStruct;
TIM_PWM_CLK_EN(); // PWM 所涉及的 TIM3 时钟使能
TIM_PWM_GPIO_CLK_EN(); // PWM 所涉及的 GPIOB 时钟使能
__HAL_RCC_AFIO_CLK_ENABLE(); // 重映射涉及时钟使能
__HAL_AFIO_REMAP_TIM3_PARTIAL(); // 启用 TIM3 重映射
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP; // 复用推挽输出
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_InitStruct.Pin = TIM_RLED_PIN;
HAL_GPIO_Init(TIM_RLED_PORT, &GPIO_InitStruct); // 初始化红灯引脚
GPIO_InitStruct.Pin = TIM_GLED_PIN;
HAL_GPIO_Init(TIM_GLED_PORT, &GPIO_InitStruct); // 初始化绿灯引脚
GPIO_InitStruct.Pin = TIM_BLED_PIN;
HAL_GPIO_Init(TIM_BLED_PORT, &GPIO_InitStruct); // 初始化蓝灯引脚
HAL_NVIC_SetPriority(TIMx_IRQn, 0, 0); // 配置定时器中断优先级
HAL_NVIC_EnableIRQ(TIMx_IRQn); // 使能 TIM3 中断
}
  1. TIM中断处理函数
    当定时器TIM3计数溢出时,会进入中断处理“TIM3_IRQHandler()”,在中断函数里,根据RGB值,设置比较寄存器CCR的值,也就实现了修改占空比,如代码段 26.3.4 所示。
    代码段 26.3.4 TIM3 中断处理函数(driver_timer.c)
/*
* 函数名:void TIM3_IRQHandler(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:TIM3 中断的中断处理函数
*/
void TIM3_IRQHandler(void) {
HAL_TIM_IRQHandler(&hpwm); }
/*
* 函数名:void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
* 输入参数:htim-TIM 句柄
* 输出参数:无
* 返回值:无
* 函数作用:TIM 中断回调周期更新函数
*/
void HAL_TIM_PWM_PulseFinishedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIMx) {
// 根据 R G B 值,修改对应通道的比较寄存器 CCR 的值(占空比)
__HAL_TIM_SET_COMPARE(&hpwm, TIM_RLED_CHANNEL, rgb.rgb_red *2000/255);
__HAL_TIM_SET_COMPARE(&hpwm, TIM_GLED_CHANNEL, rgb.rgb_green*2000/255);
__HAL_TIM_SET_COMPARE(&hpwm, TIM_BLED_CHANNEL, rgb.rgb_blue *2000/255); }
}

这里简单补充下一些色彩理论知识,常见的色彩定义标准有四种:
①HSB:基于人眼视觉的颜色模式;
②RGB:基于光色的颜色模式,是加色模式,两者组合变亮;
③CMYK:基于印刷颜料的颜色模式,是减色模式,两者组合变暗;
④Lab:基于人对颜色的感觉,与设备无关,色域宽阔;
在这里插入图片描述
LED灯属于发光色,RGB三个颜色通道的变化,以及它们相互之间的叠加来得到各式各样的颜色的,几乎包括了人类视力所能感知的所有颜色,RGB色彩定义:

  • R(Red):0~255阶,一共256阶色;
  • G(Green):0~255阶,一共256阶色;
  • B(Blue):0~255阶,一共256阶色;
    因此,这里把每个颜色的LED灯最暗用0表示,LED灯最亮用255表示,而整个计数周期为2000,因此传入的比较寄存器值为:
    在这里插入图片描述

最后使用“__HAL_TIM_SET_COMPARE()”,根据颜色色阶,设置每个通道的比较寄存器值。

  1. 按键功能
    根据需求,这里使用按键进行模式切换和RGB颜色调整。初始化按键后,在按键中断处理函数中添加每个按键的功能,如代码段 26.3.5 所示

代码段 26.3.5 按键中断处理回调函数(driver_key.c)

/*
* 函数名:void HAL_GPIO_EXTI_Callback(void)
* 输入参数:无
* 输出参数:无
* 返回值:无
* 函数作用:外部中断处理函数的回调函数,用以处理不同引脚触发的中断服务最终函数
*/
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
if(GPIO_Pin == KEY_UP_GPIO_PIN) // 按键 Up(KEY1)切换模式
{
step = !step;
rgb.rgb_red = 0; // 模式切换后,先关闭所有灯
rgb.rgb_green = 0;
rgb.rgb_blue = 0; }
if(GPIO_Pin == KEY_LEFT_GPIO_PIN) // 按键 Left(KEY3)调整红色 R {
if (rgb.rgb_red > 255)
rgb.rgb_red = 0;
rgb.rgb_red = rgb.rgb_red + 10; }
if(GPIO_Pin == KEY_DOWN_GPIO_PIN) // 按键 Down(KEY2)调整绿色 G {
if (rgb.rgb_green > 255)
rgb.rgb_green = 0;
rgb.rgb_green = rgb.rgb_green + 10; }
if(GPIO_Pin == KEY_RIGHT_GPIO_PIN) // 按键 Right(KEY4)调整蓝色 B {
if (rgb.rgb_blue > 255)
rgb.rgb_blue = 0;
rgb.rgb_blue = rgb.rgb_blue + 10; } }
  • 10~17行:当按键KEY1_UP按下后,修改模式标志位setp,同时清除RGB的初始值;
  • 19~24行:当按键KEY3_LEFT按下后,增加红色LED灯亮度(占空比);
  • 26~31行:当按键KEY2_DOWN按下后,增加绿色LED灯亮度(占空比);
  • 19~24行:当按键KEY3_RIGH按下后,增加蓝色LED灯亮度(占空比);
  1. 主函数控制逻辑
    在主函数里依次初始化时钟、串口、按键,定时器PWM等,便可以启动PWM输出了,如代码段 26.3.6所示。

代码段 26.3.6 主函数控制逻辑(main.c)

// 初始化定时器 2 输出 PWM
TimerPWMInit();
if (HAL_TIM_PWM_Start_IT(&hpwm, TIM_RLED_CHANNEL) != HAL_OK) // 启动红色 LED 所在通道定时器 PWM
{
Error_Handler(); }
if (HAL_TIM_PWM_Start_IT(&hpwm, TIM_GLED_CHANNEL) != HAL_OK) // 启动绿色 LED 所在通道定时器 PWM
{
Error_Handler(); }
if (HAL_TIM_PWM_Start_IT(&hpwm, TIM_BLED_CHANNEL) != HAL_OK) // 启动蓝色 LED 所在通道定时器 PWM
{
Error_Handler(); }
while(1) {
if(step==0) // 随机模式:三色灯随机变换颜色(默认模式) {
rgb.rgb_red = rand()%256;
rgb.rgb_green = rand()%256;
rgb.rgb_blue = rand()%256;
HAL_Delay(1000); }
else // 用户模式:用户使用按键控制 RGB 颜色
{
// 在按键中断中修改 R G B 值
// 在 TIM 中断中修改占空比
rgb.rgb_red = rgb.rgb_red%256;
rgb.rgb_green = rgb.rgb_green%256;
rgb.rgb_blue = rgb.rgb_blue%256; } }
  • 2行:初始化定时器PWM;
  • 4~7行:使用带中断的方式启动定时器PWM,传入红色LED所在通道;
  • 8~11行:使用带中断的方式启动定时器PWM,传入绿色LED所在通道;
  • 12~15行:使用带中断的方式启动定时器PWM,传入蓝色LED所在通道;
  • 17行~34行:主循环
  • 19-25行:如果KEY1_UP按下,则中断会修改step为1,进入该判断中;使用C库的“rand()”函数产生一个随机数,然后使用“%”取余,得到一个1~255之间的随机数,该随机数作为颜色色阶,定时器PWM中断产生后,便以此值作为占空比,实现LED亮度的修改;
  • 26~33行:用户模式下,无需任何操作,在按键中断中修改RGB值,在TIM中断中修改占空比,实现用户分别控制RGB亮度;

26.4 实验效果

本实验对应配套资料的“5_程序源码\\18_定时器—PWM输出\\”。打开工程后,编译,下载,串口提示如图 26.4.1 所示。
在这里插入图片描述

此时可以看到三色LED灯显示随机颜色(注意不要长时间直视三色灯,可使用薄纸巾盖住观察),按下KEY1_UP键后,三色LED不再随机变化,分别按下KEY3_LEFT、KEY2_DOWN、KEY4_RIGH可分别控制红色、绿色、蓝色的亮度。


百问网技术论坛:
http://bbs.100ask.net/

百问网嵌入式视频官网:
https://www.100ask.net/index

百问网开发板:
淘宝:https://100ask.taobao.com/
天猫:https://weidongshan.tmall.com/

技术交流群2(鸿蒙开发/Linux/嵌入式/驱动/资料下载)
QQ群:752871361

单片机-嵌入式Linux交流群:
QQ群:536785813

韦东山嵌入式培训班交流群③
QQ群:717041375

以上是关于STM32定时器—PWM 输出的主要内容,如果未能解决你的问题,请参考以下文章

STM32 PWM输出(映射)

STM32 通用定时输出PWM

STM32通用定时器PWM输出

STM32F103定时器最多可以输出多少路PWM波

STM32定时器频率是不是等于PWM输出频率?

stm32输出的pwm有啥用