通用定时器 PWM 输出实验
本小节我们来学习使用通用定时器的 PWM 输出模式。
脉冲宽度调制(PWM),是英文"Pulse Width Modulation"的缩写,简称脉宽调制,是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。我们可以让定时器产生PWM,在计数器频率固定时,PWM 频率或者周期由自动重载寄存器(TIMx_ARR)的值决定,其占空比由捕获/比较寄存器(TIMx_CCRx)的值决定。PWM 产生原理示意图如下图所示:
上图中,定时器工作在递增计数模式,纵轴是计数器的计数值 CNT,横轴表示时。当CNT<CCRx 时,IO 输出低电平(逻辑 0);当 CNT>=CCRx 时,IO 输出高电平(逻辑 1);当CNT=ARR 时,定时器溢出,CNT 的值被清零,然后继续递增,依次循环。在这个循环中,改变 CCRx 的值,就可以改变 PWM的占空比,改变 ARR 的值,就可以改变 PWM 的频率,这就是 PWM 输出的原理。
定时器产生 PWM的方式有许多种,下面我们以边沿对齐模式(即递增计数模式/递减计数模式)为例,PWM 模式 1 或者 PWM 模式 2 产生 PWM 的示意图,如下图所示:
TIM2/TIM3/TIM4/TIM5 寄存器
要使 STM32F429的通用定时器 TIMx产生 PWM 输出,除了上一小节介绍的寄存器外,我们还会用到另外 3 个寄存器,来控制 PWM 输出。这三个寄存器分别是:捕获/比较模式寄存器(TIMx_CCMR1/2)、 捕 获/比 较 使 能 寄 存 器 (TIMx_CCER)、 捕 获/比较寄存器(TIMx_CCR1~4)。接下来我们简单介绍一下这三个寄存器。
捕获/比较模式寄存器 1/2(TIMx_CCMR1/2)
TIM2/TIM3/TIM4/TIM5的捕获/比较模式寄存器(TIMx_CCMR1/2),该寄存器一般有 2 个:TIMx _CCMR1 和 TIMx _CCMR2。TIMx_CCMR1 控制 CH1 和 CH2,而 TIMx_CCMR2 控制CH3 和 CH4。TIMx_CCMR2 寄存器描述如下图所示:
该寄存器的有些位在不同模式下,功能不一样,我们现在用到输出比较,先看输出比较部分,输入捕获在后面的实验再讲解。关于该寄存器的详细说明,请参考《STM32F4xx 参考手册_V4(中文版).pdf》第 432 页,15.4.7 节。比如我们要让 TIM3 的 CH4 输出 PWM 波,该寄存器的模式设置位 OC4M[2:0]就是对应着通道 4 的模式设置。总共可以配置 8 种模式,我们使用的是 PWM 模式 1 或者 PWM 模式 2,所以位 OC4M[2:0]设置为 110 或者 111。这两种 PWM模式的区别就是输出有效电平的极性相反。位 OC4PE 控制输出比较通道 4 的预装载使能,实际就是控制 CCR4 寄存器是否进行缓冲。因为 CCR4 寄存器也是有影子寄存器的,影子寄存器才是真正起作用的寄存器。CC4S[1:0]用于设置通道 4 的方向(输入/输出)默认设置为 0,就是设置通道作为输出使用。
捕获/比较使能寄存器(TIMx_ CCER)
TIM2/TIM3/TIM4/TIM5 的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道的开关和极性。TIMx_CCER 寄存器描述如图 所示:
该寄存器比较简单,要让 TIM3 的 CH4 输出 PWM 波,这里我们要使能 CC4E 位,该位是通道 4 输入/输出使能位,要想 PWM 从 IO 口输出,这个位必须设置为 1。CC4P 位是设置通道4 的输出极性,我们默认设置 0。
捕获/比较寄存器 1/2/3/4(TIMx_ CCR1/2/3/4)
捕获/比较寄存器(TIMx_ CCR1/2/3/4),该寄存器总共有 4 个,分别对应 4 个通道CH1~CH4。我们使用的是通道 4,所以来看看 TIMx_ CCR4 寄存器描述,如图所示:
在输出模式下,捕获/比较寄存器影子寄存器的值与 CNT 的值比较,根据比较结果产生相应动作,利用这点,我们通过修改这个寄存器的值,就可以控制 PWM 的占空比了。注意,对于 TIM2 和 TIM5 来说,该寄存器是 32 位有效的,对其他定时器来说,则是 16 位有效位。
硬件设计
1. 例程功能
使用 TIM3 通道 4(由 PB1 复用)输出 PWM, PB1 引脚连接了 LED0,从而实现 PWM 输出控制 LED0 亮度。
2. 硬件资源
1)LED 灯
LED0: LED0 -- PB1
2)定时器 3 输出通道 4(对应 PB1)
3. 原理图
定时器属于 STM32F429 的内部资源,只需要软件设置好即可正常工作。我们通过 LED0来间接指示定时器的 PWM 输出情况。
程序设计
定时器的 HAL 库驱动
定时器在 HAL 库中的驱动代码在前面介绍基本定时器已经介绍了部分,这里我们再介绍几个本实验用到的函数。
1. HAL_TIM_PWM_Init 函数
定时器 PWM 输出基础工作参数初始化函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);
函数描述
用于初始化定时器的基础工作参数,即初始化 TIM_HandleTypeDef 结构体成员。
函数形参:
形参 1 是 TIM_HandleTypeDef 结构体类型指针变量,基本定时器的时候已经介绍。
函数返回值: HAL_StatusTypeDef 枚举类型的值。
注意事项:
该函数实现的功能以及使用方法和 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,这里大家注意区分。
2. HAL_TIM_PWM_ConfigChannel 函数
定时器 PWM 模式通道配置函数。其声明如下:HAL_StatusTypeDef HAL_TIM_PWM_ConfigChannel(TIM_HandleTypeDef *htim,TIM_OC_InitTypeDef *sConfig, uint32_t Channel);
函数描述:
该函数用于设置定时器的 PWM 输出模式及通道等参数。
函数形参:
形参 1 是 TIM_HandleTypeDef 结构体类型指针变量,用于配置定时器基本参数。
形参 2 是 TIM_OC_InitTypeDef 结构体类型指针变量,用于配置定时器输出比较参数。
下面重点来了解一下 TIM_OC_InitTypeDef 结构体指针类型,其定义如下:
typedef struct
{
uint32_t OCMode; /* 输出比较模式选择,寄存器的时候说过了,共 8 种模式 */
uint32_t Pulse; /* 设置比较值*/
uint32_t OCPolarity; /* 设置输出比较极性 */
uint32_t OCNPolarity; /* 设置互补输出比较极性 */
uint32_t OCFastMode; /* 使能或失能输出比较快速模式 */
uint32_t OCIdleState; /* 选择空闲状态下的非工作状态(OC1 输出) */
uint32_t OCNIdleState; /* 设置空闲状态下的非工作状态(OC1N 输出) */
} TIM_OC_InitTypeDef;
我们重点关注前三个结构体成员。成员变量 OCMode 用来设置模式,这里我们设置为PWM模式 1。成员变量 Pulse 用来设置捕获比较值。成员变量 TIM_OCPolarity用来设置输出极性。其他成员 TIM_OutputNState,TIM_OCNPolarity,TIM_OCIdleState 和 TIM_OCNIdleState后面用到再介绍。
形参 3 是定时器通道,范围:TIM_CHANNEL_1~4,比如定时器 3 只有 4 个通道,那选择范围就只有 TIM_CHANNEL_1~4,所以要根据具体情况选择。
函数返回值: HAL_StatusTypeDef 枚举类型的值。
3. HAL_TIM_PWM_Start 函数
定时器 PWM 输出启动函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);
函数描述:
用于使能通道输出和启动计数器,即启动 PWM 输出。
函数形参:
形参 1 是 TIM_HandleTypeDef 结构体类型指针变量。
形参 2 是定时器通道,范围:TIM_CHANNEL_1 到 TIM_CHANNEL_4。
函数返回值: HAL_StatusTypeDef 枚举类型的值。
注意事项:
HAL 库提供了单独使能定时器输出通道的函数,其声明如下:
void TIM_CCxChannelCmd(TIM_TypeDef *TIMx, uint32_t Channel, uint32_t ChannelState); HAL_TIM_PWM_Start 函数内部也是调用了该函数。
4. HAL_TIM_ConfigClockSource 函数
配置定时器时钟源函数,其声明如下:
HAL_StatusTypeDef HAL_TIM_ConfigClockSource(TIM_HandleTypeDef *htim, TIM_ClockConfigTypeDef *sClockSourceConfig);
函数描述:
用于配置定时器时钟源。
函数形参:
形参 1 是 TIM_HandleTypeDef 结构体类型指针变量。
形参 2 是 TIM_ClockConfigTypeDef 结构体类型指针变量,用于配置定时器时钟源参数。
TIM_ClockConfigTypeDef 定义如下:
typedef struct
{
uint32_t ClockSource; /* 时钟源 */
uint32_t ClockPolarity; /* 时钟极性 */
uint32_t ClockPrescaler; /* 定时器预分频器 */
uint32_t ClockFilter; /* 时钟过滤器 */
} TIM_ClockConfigTypeDef;
函数返回值: HAL_StatusTypeDef 枚举类型的值。
注意事项:
该函数主要配置 TIMx_SMCR 寄存器。默认情况下,定时器的时钟源是内部时钟。本实验就是使用内部时钟的,所以我们不用对时钟源进行初始化,默认即可。这里只是让大家知道有这个函数可以设定时器的时钟源。比如用 HAL_TIM_ConfigClockSource 初始化选择内部时钟,
方法如下:
TIM_HandleTypeDef timx_handle; /* 定时器 x 句柄 */
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL; /* 选择内部时钟 */
HAL_TIM_ConfigClockSource(&timx_handle, &sClockSourceConfig);
后面的定时器初始化凡是用到内部时钟我们都没有去进行初始化,系统默认即可。
定时器 PWM 输出模式配置步骤
1)开启 TIMx 和相应通道输出的 GPIO 时钟,配置该 IO 口的复用功能输出
首先开启 TIMx 的时钟,然后配置 GPIO 为复用功能输出。本实验我们默认用到定时器 3通道 4,对应 IO 是 PB1,它们的时钟开启方法如下:
__HAL_RCC_TIM3_CLK_ENABLE(); /* 使能定时器 3 */
__HAL_RCC_GPIOB_CLK_ENABLE(); /* 开启 GPIOB 时钟 */
IO 口复用功能是通过函数 HAL_GPIO_Init 来配置的。
2)初始化 TIMx,设置 TIMx 的 ARR 和 PSC 等参数
使用定时器的 PWM 输出功能时,通过 HAL_TIM_PWM_Init 函数初始化定时器 ARR 和PSC 等参数。
注意:该函数会调用:HAL_TIM_PWM_MspInit 函数,我们可以通过后者存放定时器和GPIO 时钟使能、GPIO 初始化、中断使能以及优先级设置等代码。
3)设置定时器为 PWM 模式,输出比较极性,比较值等参数
在 HAL 库中,通过 HAL_TIM_PWM_ConfigChannel 函数来设置定时器为 PWM1模式或者PWM2 模式,根据需求设置输出比较的极性,设置比较值(控制占空比)等。
4)使能 TIMx,使能 TIMx 的 CHy 输出
在 HAL 库中,通过调用 HAL_TIM_PWM_Start 函数来使能 TIMx 的某个通道输出 PWM。
5)修改 TIM3_CCR4 来控制占空比
在经过以上设置之后,PWM 其实已经开始输出了,只是其占空比和频率都是固定的,而我们可以通过修改比较值来控制 PWM的输出占空比。HAL库中提供一个修改占空比的宏定义:
__HAL_TIM_SET_COMPARE (__HANDLE__, __CHANNEL__, __COMPARE__)
__HANDLE__是 TIM_HandleTypeDef 结构体类型指针变量,__CHANNEL__对应 PWM 的输出通道,__COMPARE__则是要写到捕获/比较寄存器(TIMx_ CCR1/2/3/4)的值。实际上该宏定义最终还是往对应的捕获/比较寄存器写入比较值来控制 PWM 波的占空比。如下解析:比如我们要修改定时器 3 通道 4 的输出比较值(控制占空比),寄存器操作方法:
TIM3->CCR4 = ledrpwmval; /* ledrpwmval 是比较值,并且动态变化的, 所以我们要周期性调用这条语句,已达到及时修改 PWM 的占空比 */
__HAL_TIM_SET_COMPARE这个宏定义函数最终也是调用这个寄存器操作的,所以说我们使用 HAL 库的函数其实就是间接操作寄存器的。
程序流程图
这里我们只讲解核心代码,详细的源码请大家参考本实验对应源码。通用定时器驱动源码包括两个文件:gtim.c 和 gtim.h。
首先看 gtim.h 头文件的几个宏定义
/* TIMX PWM 输出定义
* 这里输出的 PWM 控制 LED0(RED)的亮度
* 默认是针对 TIM2~TIM5
*/
#define GTIM_TIMX_PWM_CHY_GPIO_PORT GPIOB
#define GTIM_TIMX_PWM_CHY_GPIO_PIN GPIO_PIN_1
#define GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0) /* PB 口时钟使能 */
#define GTIM_TIMX_PWM_CHY_GPIO_AF GPIO_AF2_TIM3
/* 端口复用到 TIM3 */
/* TIMX REMAP 设置 */
#define GTIM_TIMX_PWM TIM3 /* TIM3 */
#define GTIM_TIMX_PWM_CHY TIM_CHANNEL_4 /* 通道 Y, 1<= Y <=4 */
#define GTIM_TIMX_PWM_CHY_CCRX TIM3->CCR4 /* 通道 Y 的输出比较寄存器 */
#define GTIM_TIMX_PWM_CHY_CLK_ENABLE()
do{ __HAL_RCC_TIM3_CLK_ENABLE(); }while(0) /* TIM3 时钟使能 */
可以把上面的宏定义分成两部分,第一部分是定时器 3输出通道 4对应的 IO口的宏定义。第二部分则是定时器 3 输出通道 4 的相应宏定义,这里的宏定义是定时器 3 通道 4 输出 PWM控制 LED0 的相关宏定义。
gtim.h 头文件就添加了这部分的程序,下面看 gtim.c 的程序,首先是通用定时器 PWM 输出初始化函数。
/**
* @brief 通用定时器 TIMX 通道 Y PWM 输出 初始化函数(使用 PWM 模式 1)
* @note
* 通用定时器的时钟来自 APB1,当 PPRE1 ≥ 2 分频的时候
* 通用定时器的时钟为 APB1 时钟的 2 倍, 而 APB1 为 45M, 所以定时器时钟 = 90Mhz
* 定时器溢出时间计算方法: Tout = ((arr + 1) * (psc + 1)) / Ft us.
* Ft = 定时器工作频率,单位:Mhz
*
* @param arr: 自动重装值
* @param psc: 预分频系数
* @retval 无
*/
void gtim_timx_pwm_chy_init(uint16_t arr, uint16_t psc)
{
TIM_OC_InitTypeDef timx_oc_pwm_chy = {0}; /* 定时器输出句柄 */
g_timx_pwm_chy_handle.Instance = GTIM_TIMX_PWM; /* 定时器 x */
g_timx_pwm_chy_handle.Init.Prescaler = psc; /* 预分频系数 */
g_timx_pwm_chy_handle.Init.CounterMode = TIM_COUNTERMODE_UP;/* 递增计数模式*/
g_timx_pwm_chy_handle.Init.Period = arr; /* 自动重装载值 */
HAL_TIM_PWM_Init(&g_timx_pwm_chy_handle); /* 初始化 PWM */
timx_oc_pwm_chy.OCMode = TIM_OCMODE_PWM1; /* 模式选择 PWM1 */
timx_oc_pwm_chy.Pulse = arr / 2; /* 设置比较值,此值用来确定占空比 */
timx_oc_pwm_chy.OCPolarity = TIM_OCPOLARITY_LOW; /* 输出比较极性为低 */
HAL_TIM_PWM_ConfigChannel(&g_timx_pwm_chy_handle, &timx_oc_pwm_chy,
GTIM_TIMX_PWM_CHY); /* 配置 TIMx 通道 y */
/* 开启对应 PWM 通道 */
HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY);
}
HAL_TIM_PWM_Init 初始化 TIM3 并设置 TIM3 的 ARR 和 PSC 等参数,其次通过调用函数 HAL_TIM_PWM_ConfigChannel 设置 TIM3_CH4 的 PWM 模式以及比较值等参数,最后通过调用函数 HAL_TIM_PWM_Start 来使能 TIM3 以及使能 PWM 通道 TIM3_CH4 输出。本实验我们使用 PWM 的 MSP 初始化回调函数 HAL_TIM_PWM_MspInit 来存放时钟、GPIO 的初始化代码,其定义如下:
/**
* @brief 定时器底层驱动,时钟使能,引脚配置
此函数会被 HAL_TIM_PWM_Init()调用
* @param htim:定时器句柄
* @retval 无
*/
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
if (htim->Instance == GTIM_TIMX_PWM)
{
GPIO_InitTypeDef gpio_init_struct;
GTIM_TIMX_PWM_CHY_GPIO_CLK_ENABLE(); /* 开启通道 y 的 GPIO 时钟 */
GTIM_TIMX_PWM_CHY_CLK_ENABLE(); /* 使能定时器时钟 */
gpio_init_struct.Pin = GTIM_TIMX_PWM_CHY_GPIO_PIN; /* 通道 y 的 GPIO 口 */
gpio_init_struct.Mode = GPIO_MODE_AF_PP; /* 复用推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
gpio_init_struct.Alternate = GTIM_TIMX_PWM_CHY_GPIO_AF;
/* IO 口 REMAP 设置, 是否必要查看头文件配置的说明! */
HAL_GPIO_Init(GTIM_TIMX_PWM_CHY_GPIO_PORT, &gpio_init_struct);
}
}
该函数首先判断定时器寄存器基地址,符合条件后,开启对应的 GPIO 时钟和定时器时钟,并且初始化 GPIO。上面是使用 HAL 库标准的做法,我们亦可把 HAL_TIM_PWM_MspInit 函数里面的代码直接放到 gtim_timx_pwm_chy_init 函数里。这样做的好处是当一个项目中用到多个定时器时,代码的移植性、可读性好,方便管理。
在 main.c 里面编写如下代码:
int main(void)
{
uint16_t ledrpwmval = 0;
uint8_t dir = 1;
HAL_Init(); /* 初始化 HAL 库 */
sys_stm32_clock_init(360, 25, 2, 8); /* 设置时钟,180Mhz */
delay_init(180); /* 延时初始化 */
usart_init(115200); /* 初始化 USART */
led_init(); /* 初始化 LED */
gtim_timx_pwm_chy_init(500 - 1, 90 - 1); /* 90 000 000 / 90 = 1 000 000
1Mhz 的计数频率,2Khz 的 PWM */
while(1)
{
delay_ms(10);
if (dir)ledrpwmval++; /* dir==1 ledrpwmval 递增 */
else ledrpwmval--; /* dir==0 ledrpwmval 递减 */
if (ledrpwmval > 300)dir = 0; /* ledrpwmval 到达 300 后,方向为递减 */
if (ledrpwmval == 0)dir = 1; /* ledrpwmval 递减到 0 后,方向改为递增 */
/* 修改比较值控制占空比 */
__HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY,
ledrpwmval);
}
}
本小节开头我们就说 PWM 频率由自动重载寄存器(TIMx_ARR)的值决定,其占空比则由捕获/比较寄存器(TIMx_CCRx)的值决定。下面结合实际看看具体怎么计算:
定时器 3 的时钟源频率为 2 倍 APB1 总线时钟频率,即频率为 90MHz,而调用gtim_timx_pwm_chy_init 初始化函数之后,就相当于写入预分频寄存器的值为 89,写入自动重载寄存器的值为 499。基本定时器讲的定时器溢出公式由公式得:
Tout= ((arr+1)*(psc+1))/Tclk= ((499+1)*(89+1))/90000000=0.0005s
再由频率是周期的倒数关系得到 PWM 的频率为 2000Hz。
占空比怎么计算的呢?结合图 20.3.1,我们分两种情况分析,输出比较极性为低和输出比较极性为高,它们的情况正好相反。因为在 main 函数中的比较值是动态变化的,不利于我们计算占空比,我们假设比较值固定为 200,在本实验中可以调用如下语句得到。
__HAL_TIM_SET_COMPARE(&g_timx_pwm_chy_handle, GTIM_TIMX_PWM_CHY, 200);
因为 LED0 是低电平有效,所以我们在 gtim_timx_pwm_chy_init 函数中设置了输出比较极性为低,那么当比较值固定为200时,占空比 = ((arr+1) -- CCR4)/ (arr+1) = (500-200)/500=60%。
其中 arr 是写入自动重载寄存器(TIMx_ARR)的值,CCR4 就是写入捕获/比较寄存器 4(TIMx_CCR4)的值。这里我们还需要提醒一下,占空比是指在一个周期内,高电平时间相对于总时间所占的比例。
另外一种情况:设置了输出比较极性为高,那么当比较值固定为 200 时,占空比 = CCR4 / (arr+1) = 200/500=40%。可以看到输出比较极性为低和输出比较极性为高的占空比正好反过来。
在这里,我们使用 DS100 示波器进行验证,效果图如下图 所示:
这里把输出比较极性低和输出比较极性高的 PWM 波形都显示出来了。本实验默认设置PWM 模式 1、输出比较极性低,当 CCR1 寄存器的值设置为 200 时,对应的 PWM 波形如上图黄色的波形图。如果把输出比较极性设置为高,对应的波形图就是绿色的波形图了。