STM32F407通用定时器PWM函数详解(二)

一、PWM

        定时器产生PWM:在计数器频率固定时,PWM 频率由自动重载寄存器(TIMx_ARR)的值决定,其占空比由捕获/比较寄存器(TIMx_CCRx)的值决定

        定时器工作在递增计数模式,纵轴是计数器的计数值 CNT,横轴表示时。当 CNT=CCRx 时,IO 输出高电平(逻辑 1);当 CNT=ARR 时,定时器溢出,CNT 的值被清零,然后继续递增,依次循环。在这个循环中,改 变 CCRx 的值,就可以改变 PWM 的占空比,改变 ARR 的值,就可以改变 PWM 的频率,这就 是 PWM 输出的原理。

        此外根据定时器工作方式还有如下的pwm方式:

         STM32F407 的定时器除了 TIM6 和 TIM7,其他的定时器都可以用来产生 PWM 输出。其 中高级定时器 TIM1 和 TIM8 可以同时产生多达 7 路的 PWM 输出。而通用定时器也能同时产 生多达 4 路的 PWM 输出。

 图片来自:(3条消息) stm32定时器概述_stm32的tim3有几个同道_haha690的博客-CSDN博客

二、寄存器

除了上一小节介绍的寄存器外,还会用到 3 个寄存器,来控制 PWM。这三个寄存器分别是:捕获/比较模式寄存器 (TIMx_CCMR1/2)、捕获/比较使能寄存器(TIMx_CCER)、捕获/比较寄存器(TIMx_CCR1~4)。

2.1捕获/比较模式寄存器 1(TIMx_CCMR1)

TIM10/TIM11/TIM13/TIM14 的捕获/比较模式寄存器,该寄存器只有 1 个:TIMx_CCMR1, TIMx_CCMR1 控制 CH1 和 CH2。TIMx_CCMR1 寄存器描述如图 所示:

 上面寄存器只有0-7位有用是因为TIM10/TIM11/TIM13/TIM14 的捕获/比较模式寄存器只有一个CH1通道,所以高位8-15没有用,看下面的寄存器:

 这个寄存器是TIM9/12的寄存器,因为他们有两个通道,所以他们的寄存器会有16位。

对于拥有4个通道的寄存器,除了有TIMx_CCMR1寄存器还有TIMx_CCMR2寄存器,ccmr1控制通道1和2,ccmr2控制3与4通道。

寄存器的有些位在不同模式下,功能不一样,我们现在只用到输出比较。关于该寄存器的详细说明,请参考《STM32F4xx 参考手册_V4(中文版).pdf》 第 476 页,16.4.7 节。比如我们要让 TIM14 的 CH1 输出 PWM 波为例进行介绍,该寄存器的模 式设置位 OC1M[2:0]就是对应着通道 1 的模式设置,此部分由 3 位组成。总共可以配置成 8 种 模式,使用 PWM 模式,所以这 3 位必须设置为 110 或者 111,分别对应 PWM 模式 1 和 PWM 模式 2。这两种 PWM 模式的区别就是输出有效电平的极性相反。

 2.2、捕获/比较使能寄存器(TIMx_CCER)

TIM10/TIM11/TIM13/TIM14 的捕获/比较使能寄存器,该寄存器控制着各个输入输出通道 的开关和极性。TIMx_CCER 寄存器描述如图所示:

TIMx_CCER 寄存器 该寄存器比较简单,要让 TIM14 的 CH1 输出 PWM 波,这里我们要使能 CC1E 位,该位 是通道 1 输入/输出使能位,要想 PWM 从 IO 口输出,这个位必须设置为 1。CC1P 位是设置通 道 1 的输出极性,默认设置 0。

2.3、捕获/比较寄存器 1(TIMx_CCR1)

捕获/比较寄存器(TIMx_CCR1),该寄存器只有 1 个,对应通道 CH1。我们使用的是通道 1,所以来看看 TIMx_CCR1 寄存器,描述如图所示:

 在输出模式下,捕获/比较寄存器影子寄存器的值与 CNT 的值比较,根据比较结果产生相 应动作,利用这点,我们通过修改这个寄存器的值,就可以控制 PWM 的占空比了。

注:对于有更多通道的定时器,比如tim9,如果要用CH1通道就配置ccr1寄存器,如果用CH2通道就配置ccr2寄存器。

三、相关函数

3.1. HAL_TIM_PWM_Init 函数

定时器的 PWM 输出模式初始化函数,其声明如下:

HAL_StatusTypeDef HAL_TIM_PWM_Init(TIM_HandleTypeDef *htim);

该函数实现的功能以及使用方法和前面配置寄存器的HAL_TIM_Base_Init基本一样,作用都是初始化定时器的 ARR 和 PSC 等参数。

但是 HAL_TIM_Base_Init 函数针对 PWM 输出定义了单独的 MSP 回调函数  HAL_TIM_PWM_MspInit,所以当我们调用 HAL_TIM_PWM_Init 进行 PWM 初始化之后,该函数内部会调用 MSP 回调函数 HAL_TIM_PWM_MspInit。

 HAL_TIM_Base_Init 初始化定时器它内部调用的回调函数是 HAL_TIM_Base_MspInit,这里大家注意区分。

3.2、HAL_TIM_PWM_ConfigChannel 函数

定时器的 PWM 通道设置初始化函数。其声明如下:

HAL_StatusTypeDef HAL_TIM_PWM_ConfigChannel(TIM_HandleTypeDef *htim, 
                                            TIM_OC_InitTypeDef *sConfig, 
                                            uint32_t Channel);

形参 1 是 TIM_HandleTypeDef 结构体类型指针变量,用于配置定时器基本参数。

形参 2 是 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 到 TIM_CHANNEL_4。比如定时器 14 只 有 2 个通道,那选择范围就只有 TIM_CHANNEL_1~2,所以要根据具体情况选择。

3.3、HAL_TIM_PWM_Start 函数

定时器的 PWM 输出启动函数,其声明如下:

HAL_StatusTypeDef HAL_TIM_PWM_Start(TIM_HandleTypeDef *htim, uint32_t Channel);

对于单独使能定时器的方法,在上一章定时器实验我们已经讲解。实际上,HAL 库也同样 提供了单独使能定时器的输出通道函数,函数为:

void TIM_CCxChannelCmd(TIM_TypeDef *TIMx, uint32_t Channel, uint32_t ChannelState);

HAL_TIM_PWM_Start 函数内部也调用了该函数。

3.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;

 注: 该函数主要配置 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);

后面的定时器初始化凡是用到内部时钟都不需要去初始化,系统默认即可。

3.5、定时器 PWM 输出模式配置步骤

1) 开启 TIMx 和通道输出的 GPIO 时钟,配置该 IO 口的复用功能输出。

首先开启 TIMx 的时钟,然后配置 GPIO 为复用功能输出。本实验我们默认用到定时器 14 通道 1,对应 IO 是 PF9,它们的时钟开启方法如下:

__HAL_RCC_TIM14_CLK_ENABLE(); /* 使能定时器 14 */
__HAL_RCC_GPIOF_CLK_ENABLE(); /* 开启 GPIOF 时钟 */

IO 口复用功能是通过函数 HAL_GPIO_Init 来配置的。

2) 初始化 TIMx,设置 TIMx 的 ARR 和 PSC 等参数

使用定时器的 PWM 输出功能时,通过 HAL_TIM_PWM_Init 函数初始化定时器参数。 注意:该函数会调用:HAL_TIM_PWM_MspInit函数。

3) 设置 TIMx_CHy 的 PWM 模式,输出比较极性,比较值等参数

HAL_TIM_PWM_ConfigChannel 函数来设置定时器为 PWM1 模式或者 PWM2 模式,根据需求设置输出比较的极性,设置比较值(控制占空比)等。

4) 使能 TIMx,使能 TIMx 的 CHy 输出

在 HAL 库中,通过调用 HAL_TIM_PWM_Start 函数来使能 TIMx 的某个通道输出 PWM。

5) 修改 TIM14_CCR1 来控制占空比

在经过以上设置之后,PWM 其实已经开始输出了,只是其占空比和频率都是固定的,而我 们通过修改比较值来控制 PWM 的输出占空比。HAL 库中提供一个修改占空比的宏定义:

__HAL_TIM_SET_COMPARE (__HANDLE__, __CHANNEL__, __COMPARE__)

__HANDLE__是 TIM_HandleTypeDef 结构体类型指针变量,

__CHANNEL__对应 PWM 的输出通道,

__COMPARE__则是要写到捕获/比较寄存器(TIMx_CCR1/2/3/4)的值。通过修改这个值就可以实现不同pwm

实际上该宏定义最终还是往对应的捕获/比较寄存器写入比较值来控制 PWM 波的占空比,如下解析: 比如我们要修改定时器 14 通道 1 的输出比较值(控制占空比),寄存器操作方法:

TIM14->CCR1 = ledrpwmval; /* ledrpwmval 是比较值,并且动态变化的,
所以我们要周期性调用这条语句,以到达及时修改 PWM 的占空比 */

__HAL_TIM_SET_COMPARE 这个宏定义函数最终也是调用这个寄存器操作的,所以说使用 HAL 库的函数其实就是间接操作寄存器的。

四、实战

复用GPIOF_PIN_9的tim14功能,通过控制占空比来控制灯的亮度。

#include "stm32f4xx.h"
#include "core_cm4.h"
#include "stm32f4xx_hal.h"
#include "stdio.h"
 #include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
 
 
TIM_HandleTypeDef g_timx_pwm_chy_handle;            /* PWM句柄 */



/* ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ LED的配置↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓*/
void led_init(void)                                         /* 对LED串口进行初始化*/
{
    GPIO_InitTypeDef gpio_init_struct;                      /* 定义结构体 */
   
    gpio_init_struct.Pin = GPIO_PIN_10;                     /* LED引脚 */
    gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP;            /* 推挽输出 */
    gpio_init_struct.Pull = GPIO_PULLUP;                    /* 上拉 */
    gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH;          /* 高速 */
 
    HAL_GPIO_Init(GPIOF, &gpio_init_struct);                /* 初始化LED引脚 */
}
 

/*   通用定时器TIM14 通道1 PWM输出 初始化函数(使用PWM模式1)
用的APB1,当PPRE1 ≥ 2分频的时候,通用定时器的时钟为APB1时钟的2倍, 而APB1为42M, 
所以定时器时钟 = 84Mhz   */

void gtim_tim14_pwm_chy_init(uint16_t psc,uint16_t arr)
{
    TIM_OC_InitTypeDef timx_oc_pwm_chy = {0};                       /* 定时器输出句柄 */
    
    g_timx_pwm_chy_handle.Instance = TIM14;                         /* 定时器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, TIM_CHANNEL_1); /* 配置TIM14通道1 */
    HAL_TIM_PWM_Start(&g_timx_pwm_chy_handle, TIM_CHANNEL_1);                           /* 开启对应PWM通道 */
}

/* ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓此函数会被HAL_TIM_PWM_Init()调用↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓*/
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM14)
    {
        GPIO_InitTypeDef gpio_init_struct;
        __HAL_RCC_TIM13_CLK_ENABLE();                                /* 使能定时器时钟 */
        __HAL_RCC_GPIOF_CLK_ENABLE();                                /* 开启CPIO时钟 */

        gpio_init_struct.Pin = GPIO_PIN_9;                             /* 通道y的CPIO口 */
        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 = GPIO_AF9_TIM14;     /* IO口REMAP设置, 是否必要查看头文件配置的说明! */
        HAL_GPIO_Init(GPIOF, &gpio_init_struct);
    }
}




int main(void)
{
    uint16_t ledrpwmval = 0;
    uint8_t dir = 1;
    
    HAL_Init();                                 /* 初始化HAL库 */
    sys_stm32_clock_init(336, 8, 2, 7);         /* 设置时钟,168Mhz */
    delay_init(168);                            /* 延时初始化 */
    usart_init(115200);                         /* 串口初始化为115200 */
    led_init();                                 /* 初始化LED */
    gtim_tim14_pwm_chy_init(84-1, 500-1);    /* 84 000 000 / 84 = 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, TIM_CHANNEL_1, ledrpwmval);
    }
}

五、特别讲解

定时器会设计到很多参数,什么预分频,计时器频率,pwm频率,占空比,有时候会很懵,所以特此写此模块对一些问题进行讲解。

5.1、定时器的句柄

定时器的参数初始化句柄用的TIM_HandleTypeDef类型,其中有个参数为TIM_TypeDef 类型的参数:

当我们使用TIMx定时器时,就会直接设定Instance=TIMx来完成初始化,TIM_TypeDef 如下:

 从定义可以看出,其实这个参数就是直接指向不同定时器的地址,在定义中可以看见定时器的各种寄存器。也就是说,其实我们可以直接对TIM_TypeDef *Instance参数进行各种参数配置,就可以对定时器完成各种初始化,但是这样会比较麻烦,所以在TIM_HandleTypeDef还有其他的参数来进行参数配置。

5.2、定时器的频率与定时

时钟频率的计算公式如下:

stm32f407的apb1为42MHZ,但是我们将APB1倍频了,所以CK_INT 为 84MHz,即上式中第一个fck_psc=84MHz。如果使用的是APB2,则APB2的频率为84MHz,后面的计算和apb1一模一样。

然后我们设定 预分频寄存器psc 也就是设定  PCS[15:0]+1  如果我们设定为8400,那么 fCK_CNT=84MHz/8400=10KHz。 这样就得到计数器的计数频率为 10KHz,即计数器 1 秒钟可以计 10000 个数。

如果我们需要 500ms 的周期,所以就让计数器计数 5000 个数就能满足要求,即需要设置自动重载寄存器arr的值 为 4999。

当定时器工作时,CNT即计数定时器就会以10KHz的频率去递增,当CNT=自动重载寄存器arr时就会发生更新事件,这时候CNT重新计时,然后以此往复。如果开启了中断,还会发生中断。

如果需要中途修改arr的值,需要利用函数

__HAL_TIM_SET_AUTORELOAD(__HANDLE__, __AUTORELOAD__)

参数1为定时器句柄,参数2为要重新设定的arr值,arr值设定,最好是在中断里面更新

#define __HAL_TIM_SET_AUTORELOAD(__HANDLE__, __AUTORELOAD__) \
  do{                                                    \
    (__HANDLE__)->Instance->ARR = (__AUTORELOAD__);  \
    (__HANDLE__)->Init.Period = (__AUTORELOAD__);    \
  } while(0)

5.3、定时器频率、PWM频率与占空比。

定时器频率上小节已经解释清楚了,这里主要解释定时器频率、PWM频率与占空比它们之间的关系。

gtim_tim14_pwm_chy_init(84-1, 500-1);

如上,在实战节章里面,我们将psc设为了84-1,所以有84 000 000 / 84 = 1 000 000 Mhz的计数频率,后面将arr设为了500-1也就是pwm频率为1 000 000 /500=2khz,也就是一秒内可以发送2k次pwm信号。

我们知道,占空比是比较计数值 CNT与CCRx 之间的值进行改变的,CCRx是通过CCRx寄存器进行改变的。

例子:我们现在是将arr设为了500-1,也就是计数器cnt每计数500次就会清零然后重新计数。我们设定ccrx的参数就能实现不同的占空比。比如我想实现0.8的占空比,那我就应该将ccrx参数设定为100,这样(500-100)/500=0.8。

问:为什么要有pwm频率,pwm频率直接用定时器频率不就行了?

答:一些元器件有规定的pwm频率,比如舵机就是要需要接受特定频率的PWM才可以控制。

在对pwm参数设定时用的句柄也是 TIM_HandleTypeDef句柄,但是pwm在参数设定的时候多了一个TIM_OC_InitTypeDef句柄,这个句柄是用来设定定时器的模式、占空比和极性的。如下图:

timx_oc_pwm_chy.Pulse = arr / 2; 就是对pwm的占空比进行设定的。但是我们在5.1讲了,在TIM_TypeDef *Instance中可以直接对定时器的寄存器进行设定:

注:这里在定义timx_oc_pwm_chy时直接是timx_oc_pwm_chy = {0}; 因为在此句柄里面还有很多其他的参数,但是我们这里只用到了三个参数,其他参数需要进行一下初始化,所以直接让其等于0。

我们设定不同占空比就是要改变CCRx的值实现的,所以我们也可以直接修改Instance->CCRx。这也就是HAL提供的,修改pwm占空比函数的原理:

#define __HAL_TIM_SET_COMPARE(__HANDLE__, __CHANNEL__, __COMPARE__) \
  (((__CHANNEL__) == TIM_CHANNEL_1) ? ((__HANDLE__)->Instance->CCR1 = (__COMPARE__)) :\
   ((__CHANNEL__) == TIM_CHANNEL_2) ? ((__HANDLE__)->Instance->CCR2 = (__COMPARE__)) :\
   ((__CHANNEL__) == TIM_CHANNEL_3) ? ((__HANDLE__)->Instance->CCR3 = (__COMPARE__)) :\
   ((__HANDLE__)->Instance->CCR4 = (__COMPARE__)))

 

我们修改ledrpwmval这个值就是在修改其占空比。

物联沃分享整理
物联沃-IOTWORD物联网 » STM32F407通用定时器PWM函数详解(二)

发表评论