文章目录
- 一、PWM 简介
- 二、硬件设计
- 三、软件设计
- 四、实验现象
- 五、STM32CubeMX 配置定时器 PWM 输出功能
上一章,我们介绍了
STM32F429
的通用定时器
TIM3
,用该定时器的中断来控制
DS1
的闪烁,这一章,我们将向大家介绍如何使用
STM32F429
的
TIM3
来产生
PWM
输出。在本章中,我们将使用
TIM3
的通道 4 来产生
PWM
来控制
DS0
的亮度。
一、PWM 简介
脉冲宽度调制(PWM
),是英文“Pulse Width Modulation”的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。简单一点,就是对脉冲宽度的控制,PWM 原理如图所示:
上图就是一个简单的 PWM
原理示意图。图中,我们假定定时器工作在向上计数 PWM
模式,且当CNT<CCRx
时,输出 0,当 CNT>=CCRx
时输出 1。那么就可以得到如上的 PWM
示意图:当 CNT
值小于 CCRx
的时候,IO
输出低电平(0),当 CNT
值大于等于 CCRx
的时候,IO
输出高电平(1),当 CNT
达到 ARR
值的时候,重新归零,然后重新向上计数,依次循环。改变 CCRx
的值,就可以改变 PWM
输出的占空比,改变 ARR
的值,就可以改变 PWM
输出的频率,这就是 PWM
输出的原理。
STM32F429
的定时器除了 TIM6
和 7
。其他的定时器都可以用来产生 PWM
输出。其中高级定时器 TIM1
和 TIM8
可以同时产生多达 7 路的 PWM
输出。而通用定时器也能同时产生多达 4 路的 PWM
输出!这里我们仅使用 TIM3
的 CH4
产生一路 PWM
输出。
要使 STM32F429
的通用定时器 TIMx
产生 PWM
输出,除了上一章介绍的寄存器外,我们还会用到 3 个寄存器来控制 PWM
的。这三个寄存器分别是:捕获/比较模式寄存器(TIMx_CCMR1/2
)、捕获/比较使能寄存器(TIMx_CCER
)、捕获/比较寄存器(TIMx_CCR1~4
)。接下来我们简单介绍一下这三个寄存器。
-
捕获/比较模式寄存器:
TIMx_CCMR1/2
该寄存器一般有 2 个:TIMx _CCMR1
和TIMx _CCMR2
。TIMx_CCMR1
控制CH1
和2
,而TIMx_CCMR2
控制CH3
和4
。以TIM3
为例,TIM3_CCMR2
寄存器各位描述如图所示:
该寄存器的有些位在不同模式下,功能不一样,所以在图中,把寄存器分了 2 层,上面一层对应输出而下面的则对应输入。关于该寄存器的详细说明,请参考《STM32F4xx中文参考手册》第 435 页。这里我们需要说明的是模式设置位OC4M
,此部分由 3 位组成。总共可以配置成 7 种模式,我们使用的是PWM
模式,所以这 3 位必须设置为110/111
。这两种PWM
模式的区别就是输出电平的极性相反。另外CC4S
用于设置通道的方向(输入/输出)默认设置为 0,就是设置通道作为输出使用。 -
捕获/比较使能寄存器:
TIMx_CCER
该寄存器控制着各个输入输出通道的开关。该寄存器的各位描述如图所示:
该寄存器比较简单,我们这里只用到了CC4E
位,该位是输入/捕获 4 输出使能位,要想PWM
从IO
口输出,这个位必须设置为 1,所以我们需要设置该位为 1。该寄存器更详细的介绍了,请参考《STM32F4xx 中文参考手册》第 436 页。 -
捕获/比较寄存器:
TIMx_CCR1~4
该寄存器总共有 4 个,对应 4 个通道CH1~4
。我们使用的是通道 4,TIM3_CCR4
寄存器的各位描述如图所示:
在输出模式下,该寄存器的值与CNT
的值比较,根据比较结果产生相应动作。利用这点,我们通过修改这个寄存器的值,就可以控制PWM
的输出脉宽了。如果是通用定时器,则配置以上三个寄存器就够了,但是如果是高级定时器,则还需要配置:刹车和死区寄存器(TIMx_BDTR
),该寄存器各位描述如图所示:
该寄存器,我们只需要关注最高位:MOE
位,要想高级定时器的PWM
正常输出,则必须设置MOE
位为 1,否则不会有输出。注意:通用定时器不需要配置这个。该寄存器更详细的介绍请参考《STM32F4xx 中文参考手册》第 386 页。
本章,我们使用的是 TIM3
的通道 4,所以我们需要修改 TIM3_CCR4
以实现脉宽控制 DS0
的亮度。
下面介绍通过 HAL
库来配置该功能的步骤,相关的函数设置在库函数文件 stm32f4xx_tim.h
和stm32f4xx_tim.c
文件中。
-
开启
TIM3
和GPIO
时钟,配置PB1
选择复用功能AF1
(TIM3
)输出
要使用TIM3
,我们必须先开启TIM3
的时钟。HAL
库使能TIM3
时钟和GPIO
时钟方法是:__HAL_RCC_TIM3_CLK_ENABLE(); //使能定时器 3 __HAL_RCC_GPIOB_CLK_ENABLE(); //开启 GPIOB 时钟
配置
PB1
为复用(AF1
)输出,才可以实现TIM3_CH4
的PWM
经过PB1
输出。接下来便是要配置PB1
复用映射为TIM3
的PWM
输出引脚。关于IO
口复用映射,在串口通信实验中有详细讲解,主要是通过函数HAL_GPIO_Init
来实现的:GPIO_InitTypeDef GPIO_Initure; GPIO_Initure.Pin=GPIO_PIN_1; //PB1 GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速 GPIO_Initure.Alternate= GPIO_AF2_TIM3; //PB1 复用为 TIM3_CH4 HAL_GPIO_Init(GPIOB,&GPIO_Initure);
在
IO
口初始化配置中,我们只需要将成员变量Mode
配置为复用推挽输出,同时成员变量Alternate
配置为GPIO_AF2_TIM3
,即可实现PB1
映射为定时器 3 通道 4 的PWM
输出引脚。
这里还需要说明一下,对于定时器通道的引脚关系,大家可以查看STM32F4
对应的数据手册,比如我们PWM
实验,我们使用的是定时器 3 的通道 4,对应的引脚PB1
可以从数据手册表中查看:
-
初始化
TIM3
,设置TIM3
的ARR
和PSC
等参数
根据上一章的讲解,初始化定时器的ARR
和PSC
等参数是通过函数HAL_TIM_Base_Init
来实现的,但是这里大家要注意,对于我们使用定时器的PWM
输出功能时,HAL
库为我们提供了一个独立的定时器初始化函数HAL_TIM_PWM_Init
,该函数声明为:HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
该函数实现的功能以及使用方法和
HAL_TIM_Base_Init
都是类似的,作用都是初始化定时器的ARR
和PSC
等参数。 为什么HAL
库要提供这个函数而不直接让我们使用HAL_TIM_Base_Init
函数呢?
这是因为HAL
库为定时器的PWM
输出定义了单独的MSP
回调函数HAL_TIM_PWM_MspInit
,也就是说,当我们调用HAL_TIM_PWM_Init
进行PWM
初始化之后,该函数内部会调用MSP
回调函数HAL_TIM_PWM_MspInit
。而当我们使用HAL_TIM_Base_Init
初始化定时器参数的时候,它内部调用的回调函数为HAL_TIM_Base_MspInit
,这里大家注意区分。
使用HAL_TIM_PWM_Init
初始化定时器时,回调函数为:HAL_TIM_PWM_MspInit
,该函数声明为:void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim);
一般情况下,上面步骤 1 的时钟使能和
IO
口初始化映射都编写在回调函数内部。 -
设置
TIM3_CH4
的PWM
模式,输出比较极性,比较值等参数
接下来,我们要设置TIM3_CH4
为PWM
模式(默认是冻结的),因为我们的DS0
是低电平亮,而我们希望当CCR4
的值小的时候,DS0
就暗,CCR4
值大的时候,DS0
就亮,所以我们要通过配置TIM3_CCMR2
的相关位来控制TIM3_CH4
的模式。
在HAL
库中,PWM
通道设置是通过函数HAL_TIM_PWM_ConfigChannel
来设置的:HAL_StatusTypeDef HAL_TIM_PWM_ConfigChannel(TIM_HandleTypeDef *htim, TIM_OC_InitTypeDef* sConfig, uint32_t Channel);
- 第一个参数
htim
是定时器初始化句柄,也就是TIM_HandleTypeDef
结构体指针类型,这和HAL_TIM_PWM_Init
函数调用时候参数保存一致即可。 - 第二个参数
sConfig
是TIM_OC_InitTypeDef
结构体指针类型,这也是该函数最重要的参数。该参数用来设置PWM
输出模式,极性,比较值等重要参数。其定义为:typedef struct { uint32_t OCMode; //PWM 模式 uint32_t Pulse; //捕获比较值 uint32_t OCPolarity; //极性 uint32_t OCNPolarity; uint32_t OCFastMode; //快速模式 uint32_t OCIdleState; uint32_t OCNIdleState; } TIM_OC_InitTypeDef;
- 成员变量
OCMode
用来设置模式,也就是我们前面讲解的7 种模式,这里我们设置为PWM
模式 1。 - 成员变量
Pulse
用来设置捕获比较值。 - 成员变量
TIM_OCPolarity
用来设置输出极性是高还是低。 - 其他的参数
TIM_OutputNState
,TIM_OCNPolarity
,TIM_OCIdleState
和TIM_OCNIdleState
是高级定时器才用到的。
- 成员变量
- 第三个参数
Channel
用来选择定时器的通道,取值范围为TIM_CHANNEL_1~TIM_CHANNEL_4
。这里我们使用的是定时器3的通道4,所以取值为TIM_CHANNEL_4
即可。
例如我们要初始化定时器 3 的通道 4 为
PWM
模式 1,输出极性为低,那么实例代码为:TIM_OC_InitTypeDef TIM3_CH4Handler; //定时器 3 通道 4 句柄 TIM3_CH4Handler.OCMode=TIM_OCMODE_PWM1; //模式选择 PWM1 TIM3_CH4Handler.Pulse=arr/2; //设置比较值,此值用来确定占空比 TIM3_CH4Handler.OCPolarity=TIM_OCPOLARITY_LOW; //输出比较极性为低 HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CH4Handler,TIM_CHANNEL_4);
- 第一个参数
-
使能
TIM3
,使能TIM3
的CH4
输出
在完成以上设置了之后,我们需要使能TIM3
并且使能TIM3_CH4
输出。在HAL
库中,函数HAL_TIM_PWM_Start
可以用来实现这两个功能,函数声明如下:HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
该函数第二个入口参数
Channel
是用来设置要使能输出的通道号。
HAL
库也同样提供了单独使能定时器的输出通道函数,函数为:void TIM_CCxChannelCmd(TIM_TypeDef* TIMx, uint32_t Channel, uint32_t ChannelState);
-
修改 TIM3_CCR4 来控制占空比
在经过以上设置之后,PWM
其实已经开始输出了,只是其占空比和频率都是固定的,而我们通过修改比较值TIM3_CCR4
则可以控制CH4
的输出占空比,继而控制DS0
的亮度。HAL
库中并没有提供独立的修改占空比函数,这里编写这样一个函数如下://设置 TIM3 通道 4 的占空比 // compare:比较值 void TIM_SetTIM3Compare4(u32 compare) { TIM3->CCR4=compare; }
实际上,因为调用函数
HAL_TIM_PWM_ConfigChanne
进行PWM
配置的时候可以设置比较值,所以我们也可以直接使用该函数来达到修改占空比的目的:void TIM_SetCompare4(TIM_TypeDef *TIMx,u32 compare) { TIM3_CH4Handler.Pulse=compare; HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CH4Handler,TIM_CHANNEL_4); }
这种方法因为要调用
HAL_TIM_PWM_ConfigChannel
函数对各种初始化参数进行重新设置,所以在使用中一定要注意,例如在实时系统中如果多个线程同时修改初始化结构体相关参数,可能导致结果混乱。
二、硬件设计
本实验用到的硬件资源有:
- 指示灯
DS0
- 定时器
TIM3
这两个我们前面都已经介绍了,因为 TIM3_CH4
可以通过 PB1
输出 PWM
,而 DS0
就是直接接在PB1
上面的,所以电路上并没有任何变化。
三、软件设计
我们直接复制“定时器中断实验”的工程模板,将复制过来的模板文件夹重新命名为“8-PWM输出实验”。修改了timer.c
和 timer.h
的内容。
打开timer.c
,代码如下:
#include "timer.h"
#include "led.h"
TIM_HandleTypeDef TIM3_Handler; //定时器3PWM句柄
TIM_OC_InitTypeDef TIM3_CH4Handler; //定时器3通道4句柄
//TIM3 PWM部分初始化
//PWM输出初始化
//arr:自动重装值
//psc:时钟预分频数
void TIM3_PWM_Init(u16 arr,u16 psc)
{
//初始化TMI3
TIM3_Handler.Instance=TIM3; //定时器3
TIM3_Handler.Init.Prescaler=psc; //定时器分频
TIM3_Handler.Init.CounterMode=TIM_COUNTERMODE_UP;//向上计数模式
TIM3_Handler.Init.Period=arr; //自动重装载值
TIM3_Handler.Init.ClockDivision=TIM_CLOCKDIVISION_DIV1;
HAL_TIM_PWM_Init(&TIM3_Handler); //初始化PWM
//设置TIM3_CH4的PWM模式,输出比较极性,比较值等参数
TIM3_CH4Handler.OCMode=TIM_OCMODE_PWM1; //模式选择PWM1
TIM3_CH4Handler.Pulse=arr/2; //设置比较值,此值用来确定占空比,默认比较值为自动重装载值的一半,即占空比为50%
TIM3_CH4Handler.OCPolarity=TIM_OCPOLARITY_LOW; //输出比较极性为低
HAL_TIM_PWM_ConfigChannel(&TIM3_Handler,&TIM3_CH4Handler,TIM_CHANNEL_4);//配置TIM3通道4
//使能 `TIM3`,使能 `TIM3` 的 `CH4` 输出
HAL_TIM_PWM_Start(&TIM3_Handler,TIM_CHANNEL_4);//开启PWM通道4
}
//定时器底层驱动,时钟使能,引脚配置
//此函数会被HAL_TIM_PWM_Init()调用
//htim:定时器句柄
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
GPIO_InitTypeDef GPIO_Initure;
__HAL_RCC_TIM3_CLK_ENABLE(); //使能定时器3
__HAL_RCC_GPIOB_CLK_ENABLE(); //开启GPIOB时钟
GPIO_Initure.Pin=GPIO_PIN_1; //PB1
GPIO_Initure.Mode=GPIO_MODE_AF_PP; //复用推挽输出
GPIO_Initure.Pull=GPIO_PULLUP; //上拉
GPIO_Initure.Speed=GPIO_SPEED_HIGH; //高速
GPIO_Initure.Alternate= GPIO_AF2_TIM3; //PB1复用为TIM3_CH4
HAL_GPIO_Init(GPIOB,&GPIO_Initure);
}
//设置TIM通道4的占空比
//compare:比较值
void TIM_SetTIM3Compare4(u32 compare)
{
TIM3->CCR4=compare;
}
其头文件timer.h
为
#ifndef __PWM_H
#define __PWM_H
#include "sys.h"
extern TIM_HandleTypeDef TIM3_Handler; //定时器3PWM句柄
extern TIM_OC_InitTypeDef TIM3_CH4Handler; //定时器3通道4句柄
void TIM3_PWM_Init(u16 arr,u16 psc);
void TIM_SetTIM3Compare4(u32 compare);
#endif
此部分代码包含三个函数,完全实现了前面的5 个配置步骤。
- 第一个函数
TIM3_PWM_Init
实现的是步骤2-4
,首先通过调用定时器HAL
库函数HAL_TIM_PWM_Init
初始化TIM3
并设置TIM3
的ARR
和PSC
等参数,其次通过调用函数HAL_TIM_PWM_ConfigChannel
设置TIM3_CH4
的PWM
模式以及比较值等参数,最后通过调用函数HAL_TIM_PWM_Start
来使能TIM3
以及使能PWM
通道TIM3_CH4
输出。 - 第二个函数
HAL_TIM_PWM_MspInit
是PWM
的MSP
初始化回调函数,该函数实现的是步骤 1,主要是使能相应时钟以及初始化定时器通道TIM3_CH4
对应的IO
口模式,同时设置复用映射关系。 - 第三个函数
TIM_SetTIM3Compare4
是用户自定义的设置比较值函数,即步骤 5。
主函数main.c
代码如下:
#include "sys.h"
#include "delay.h"
#include "usart.h"
#include "led.h"
#include "timer.h"
int main(void)
{
u8 dir=1;
u16 led0pwmval=0;
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(360,25,2,8); //设置时钟,180Mhz
delay_init(180); //初始化延时函数
uart_init(115200); //初始化USART
LED_Init(); //初始化LED
TIM3_PWM_Init(500-1,90-1); //90M/90=1M的计数频率,自动重装载为500,那么PWM频率为1M/500=2kHZ
while(1)
{
delay_ms(10);
if(dir)led0pwmval++; //dir==1 led0pwmval递增
else led0pwmval--; //dir==0 led0pwmval递减
if(led0pwmval>300)dir=0; //led0pwmval到达300后,方向为递减
if(led0pwmval==0)dir=1; //led0pwmval递减到0后,方向改为递增
TIM_SetTIM3Compare4(led0pwmval); //修改比较值,修改占空比
}
}
从死循环函数可以看出,我们控制 LED0_PWM_VAL
的值从 0
变到 300
,然后又从 300
变到 0
,如此循环,因此 DS0
的亮度也会跟着从暗变到亮,然后又从亮变到暗。至于这里的值,我们为什么取 300,是因为 PWM
的输出占空比达到这个值的时候,我们的 LED
亮度变化就不大了(虽然最大值可以设置到 499
),因此设计过大的值在这里是没必要的。
四、实验现象
使用 USB
线将开发板和电脑连接成功后(电脑能识别开发板上 CH340
串口),把编译后产生的.hex
文件烧入到芯片内。可以看到:DS0
不停的由暗变到亮,然后又从亮变到暗。
五、STM32CubeMX 配置定时器 PWM 输出功能
使用 STM32CubeMX
配置 PWM
输出的配置步骤和配置定时器中断的配置步骤非常接近,步骤如下:
在TIM3
配置项中,配置 Channel4
的值为 PWM generation CH4
,然后 Clock Source
为 Internal Clock
。操作过程如下图所示:
进入 Configuration->TIM3
配置页,在弹出的界面中点击 Parameter Settings
选项卡,Counter Settings
配置栏下面的四个选项就是用来配置定时器的预分频系数,自动装载值,计数模式以及时钟分频因子。在界面的 PWM Generation Channel4
配置栏配置 PWM
模式,比较值,极性等参数,操作方法如下图所示:
本章 PWM
输出实验,我们并没有使用到中断,所以我们不需要使能中断和配置 NVIC
。经过上面的配置就可以生成工程源码。