STM32的HAL库开发—TIMER(定时器) —通用定时器-PWM功能
一、通用定时器输出比较部分框图介绍
输出比较部分主框图如下:
1、 捕获/比较通道1的主电路—输出部分
图中,灰色部分为输入的,右边为输出的。
捕获/比较预装载寄存器就是主框图里边的捕获/比较1寄存器,捕获/比较1寄存器的影子寄存器就是图中的捕获比较影子寄存器,影子寄存器是没有读写权限的。
一般首先由程序员写入捕获/比较寄存器的值,也就是CCR1。然后捕获/比较寄存器的值会转移到捕获/比较影子寄存器里边。捕获/比较影子寄存器的值与计数器的值进行比较。比较的结果来到输出控制部分。
捕获/比较寄存器的值在compare_transfer为1的时候才会转移到影子寄存器里边。第一个条件就是当写入CCR1H或者CCR1L时候,writing_in_progress为1,经过一个非,就是0,再经过与门结果为0,也就是说在写入捕获/比较寄存器的时候,值不会转移到影子寄存器里边。第二个条件为CC1S[0]和CC1S[1],这两位为配置通道1是输出还是输入,如果配置成输出,那么这两位都是0,则经过或非门以后结果为1。第三个条件为设置OC1PE位,这个位是设置捕获/比较寄存器是否有缓冲功能,如果设置成0,则无缓冲功能,经过一个非门,在经过一个或门则结果为1,那么捕获/比较寄存器的值会立即转移到影子寄存器里边。反之如果设置的是1,就是有缓冲功能,不会立即转移到影子寄存器里边。当UEV为1的时候,就是发生了更新事件的时候,才能转移到影子寄存器里边。
2、输出控制部分
首先将通道配置成输出模式,也就是将TIMx_CCMR1的CC1S[1:0]位配置为00。
第二步就是设置输出比较模式,设置TIM_CCMR1寄存器的CO1M[2:0]位,对于PWM功能有两个模式,一个是PWM模式1,另一个是PWM模式2。
第三步是配置强制清零功能,这个一般不用。强制清零就是看ETR引脚电平,如果设置了OC1CE位,那个就是开启强制清零功能,一旦ETRF为高电平,则OC1REF直接输出0。
OC1REF称为输出参考信号,受ETRF强制清零控制、输出比较模式选择、还有前面的比较输出值影响。确定了OC1REF信号后。来到了极性选择器,设置TIM_CCER寄存器的CC1P位,如果设置成1,则低电平有效,因为高电平过来经过一个反相器为低电平,则输出低电平,低电平过来,经过反相器变成高电平,则输出高电平。
最后开启输出使能位,TIM_CCER寄存器的CC1E位,设置为1。
二、通用定时器输出PWM原理
计数器的值随着时间的增加而递增,当计数器的值小于捕获/比较寄存器的值的时候,IO输出值为0,当计数器的值大于捕获/比较影子寄存器值得时候,IO口输出高电平。到达ARR寄存器值得时候从0重新开始递增。
总结:ARR寄存器决定频率或者周期,CCRX捕获/比较寄存器决定PWM占空比。
三、PWM模式
PWM模式1:设置TIMX_CCMR1寄存器的OC1M[2:0]为110,在向上计数时,一旦TIMXCNT<TIMXCCR1时通道1为有效电平,否则为无效电平。OC1REF高电平有效。也就是说递增模式下,计数器的值小于捕获比较寄存器的值,OC1REF为高电平,大于捕获比较寄存器的值,OC1REF为低电平。在向下计数时,一旦TIMXCNT>TIMXCCR1时通道1为无效电平(OC1REF=0),否则为有效电平(OC1REF=1)。
注意:OC1REF是高电平有效。
总之在PWM模式1情况下,向上计数时,计数器值小于捕获比较寄存器(CCRx)时,OC1REF为高电平,大于为低电平。向下计数时,计数器值大于捕获比较寄存器(CCRx)时,OC1REF为低电平,小于为低高电平。
上面说的都是OC1REF这个信号的电平,至于IO口输出高低电平还由CCXP位决定,如果设置成0,就是直接输出OC1REF信号,设置成0,就反向输出。
四、通用定时器PWM输出实验配置步骤
1、HAL_TIM_PWM_Init()函数,配置定时器基础工作参数。这个函数与HAL_TIM_Base_init()函数功能是一样的。
2、HAL_TIM_PWM_MspInit(),配置NVIC、CLOCK、GPIO等。
3、HAL_TIM_PWM _Configchannel()函数,配置PWM模式/比较值。
4、HAL_TIM_PWM _Start()函数,使能输出并启动计数器。
5、__HAL_TIM_SET_COMPARE()宏定义,修改比较值控制占空比(可选)。
6、__HAL_TIM_ENABLE_OCXPRELOAD()宏定义,使能通道预装载(可选)。
这些函数在下面给出的程序里边都有注释 基本都使用了 ,除了最后一个没有使用,最后一个设置输出比较寄存器具有缓冲功能,在第3个函数通道配置函数里边有设置。
五、实验程序
5.1 寄存器配置PWM
使用寄存器配置程序:这个使用TIM2的通道一引脚 PA0
#include "./BSP/PWM/pwm.h"
void TIM_PWM_Init(void)
{
//这里注意 这个时钟使能必须写在操作寄存器的前面 不然向寄存器里边写值 没有反应
__HAL_RCC_TIM2_CLK_ENABLE();//开启定时器2时钟
TIM2->CR1 |= (1 << 7);//ARPE位 开启ARR缓冲
TIM2->EGR |= (1 << 0);//UG 开启更新事件
TIM2->CCMR1 |= (1 <<3);//OC1PE位 开启输出比较寄存器缓冲功能
TIM2->CCMR1 |= 0X60;//OC1M[2:0] 设置输出比较通道1模式为 PWM模式1
TIM2->CCER |= (1 << 0);//CC1E位 开启0C1信号输出对应引脚
TIM2->CCER |= (1 << 1);//设置的是输出极性为低电平有效
//TIM2->PSC = (uint16_t)((72000000 / 17000) >> 16); ;//设置定时器时钟预分频系数为7200
//TIM2->ARR = (uint16_t)(72000000/(17000*(TIM2->PSC + 1)));//设置ARR重装载值为10
//配置1KHz ARR寄存器就16位 不设置PSC结果超65535
TIM2->PSC = 9;
TIM2->ARR = 7200000/1000 - 1;
//配置2KHz
//TIM2->ARR = 72000000/2000 - 1;
TIM2->CCR1 = 0.3 * (TIM2->ARR);//设置捕获/比较寄存器值为3 也就是百分之30占空比
TIM2->CR1 |= (1 << 0);//使能计数器
__HAL_RCC_GPIOA_CLK_ENABLE();//开启GPIOA时钟
GPIOA->CRL |= 0x03;//设置MODE0[1:0]为11 输出50MHz模式
GPIOA->CRL &= ~(1<<2);//设置CNF0[1:0]为10 复用推挽输出模式
GPIOA->CRL |= (1<<3);
}
在配置寄存器过程中,发现如果定时器的时钟不开启,那么写入寄存器的值无效。这个我之前不知道。同时发现在计算PWM频率的时候,使用的是ARR + 1的值,不是ARR的值。同理计算占空比的时候,按CCR1的值比上ARR + 1。
PWM任意频率公式:
TIM2->PSC = (uint16_t)((72000000 / Freq) >> 16); ;
TIM2->ARR = (uint16_t)(72000000/(Freq*(TIM2->PSC + 1)));
TIM2->ARR = 72000000/Freq;
前两行和第三行意思一样,前面那个意思就是PSC设置成0,然后ARR的值就是把PSC为0带入公式,实测效果一样。
5.2 库函数配置PWM
pwm.h头文件程序
#ifndef __PWM_H
#define __PWM_H
#include "stm32f1xx.h"
void TIM_PWM_Init(void);
#endif
pwm.c源程序
#include "./BSP/PWM/pwm.h"
TIM_HandleTypeDef htim;
TIM_OC_InitTypeDef sConfig;
void TIM_PWM_Init(void)
{
//定时器的基本配置
htim.Instance = TIM3;
//配置ARR寄存器缓冲功能
htim.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
//配置计数器向上计数模式 通用定时器还有向下计数模式和中心对齐模式
htim.Init.CounterMode = TIM_COUNTERMODE_UP;
//设置PSC分频系数 这里分频的是系统时钟 来自APB1
htim.Init.Prescaler = 0;
//设置ARR寄存器的值
htim.Init.Period = 72000000/17000;
//定时器初始化
HAL_TIM_PWM_Init(&htim);
htim.Channel = HAL_TIM_ACTIVE_CHANNEL_1;
//设置输出比较模式
sConfig.OCMode = TIM_OCMODE_PWM1;
//设置比较值得
sConfig.Pulse = 0.3 * 72000000/17000;
//设置输出比较极性的 也就是 CCxP位
sConfig.OCPolarity = TIM_OCPOLARITY_HIGH;
HAL_TIM_PWM_ConfigChannel(&htim,&sConfig,TIM_CHANNEL_1);
//开启定时器PWM模式 主要使能计数器和CC1E位 设置CC1输出使能。
HAL_TIM_PWM_Start(&htim, TIM_CHANNEL_1);
//设置输出极性 CC1P 位 设置成1为低电平有效 0为高电平有效
TIM3->CCER |= (1 << 1);
//设置占空比 也就是设置CCR1寄存器的值
TIM3->CCR1 = 0.7 * 72000000/17000;
//__HAL_TIM_SET_COMPARE(&htim,TIM_CHANNEL_1,0.2 * 72000000/17000);
}
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE();//开启定时器3时钟
__HAL_RCC_GPIOA_CLK_ENABLE();//开启GPIOA时钟
GPIO_InitTypeDef GPIO_Init;
//由于GPIO输出需要从片上外设TIM3来 配置成复用推挽 从手册可以找到
GPIO_Init.Mode = GPIO_MODE_AF_PP;
GPIO_Init.Pin = GPIO_PIN_6;
GPIO_Init.Speed = GPIO_SPEED_FREQ_HIGH;
GPIO_Init.Pull = GPIO_PULLUP;//f1系列这个没有用
HAL_GPIO_Init(GPIOA, &GPIO_Init);
}
}
main.c主函数程序
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/PWM/pwm.h"
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
led_Init(); /* LED初始化 */
TIM_PWM_Init();
while(1)
{
LED0(1);
LED1(0);
delay_ms(500);
LED0(0);
LED1(1);
delay_ms(500);
}
}
5.3 通用定时器PWM输出实验
实验:通过定时器输出的PWM控制LED0,实现类似手机呼吸灯的效果
LED0接的是PB5,PB5正常复用功能没有对应的定时器通道,但是他的重映射连接的是定时器3的通道二,这个从数据手册可以找到。
因此在程序里需要使用AFIO配置TIM3的重映射。
pwm.h头文件程序
#ifndef __PWM_H
#define __PWM_H
#include "stm32f1xx.h"
void TIM_PWM_Init(uint16_t psc,uint16_t arr);
#endif
pwm.c
#include "./BSP/PWM/pwm.h"
TIM_HandleTypeDef htim;
void TIM_PWM_Init(uint16_t psc,uint16_t arr)
{
//定时器的基本配置
htim.Instance = TIM3;
//配置ARR寄存器缓冲功能
htim.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
//配置计数器向上计数模式 通用定时器还有向下计数模式和中心对齐模式
htim.Init.CounterMode = TIM_COUNTERMODE_UP;
//设置PSC分频系数 这里分频的是系统时钟 来自APB1
htim.Init.Prescaler = psc;
//设置ARR寄存器的值
htim.Init.Period = arr;
//定时器初始化
HAL_TIM_PWM_Init(&htim);
TIM_OC_InitTypeDef sConfig;
//设置输出比较模式
sConfig.OCMode = TIM_OCMODE_PWM1;
//设置比较值得 这个要设置ARR + 1 一半 为50%占空比
sConfig.Pulse = (arr + 1) / 2;
//设置输出比较极性的 也就是 CCxP位
sConfig.OCPolarity = TIM_OCPOLARITY_LOW;
//这个注意一定要写 不然他会改CC2S的值
sConfig.OCFastMode = TIM_OCFAST_DISABLE;
HAL_TIM_PWM_ConfigChannel(&htim,&sConfig,TIM_CHANNEL_2);
//开启定时器PWM模式 主要使能计数器和CC1E位 设置CC2输出使能。
HAL_TIM_PWM_Start(&htim, TIM_CHANNEL_2);
}
void HAL_TIM_PWM_MspInit(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM3)
{
__HAL_RCC_TIM3_CLK_ENABLE();//开启定时器3时钟
__HAL_RCC_GPIOB_CLK_ENABLE();//开启GPIOB时钟
__HAL_RCC_AFIO_CLK_ENABLE();//开启AFIO时钟
__HAL_AFIO_REMAP_TIM3_PARTIAL();//开启定时器3的部分重映射 将TIM3 通道二映射到PB5上
//AFIO->MAPR |= (1 << 11);
//AFIO->MAPR &= ~(1 < 10);
GPIO_InitTypeDef GPIO_Init;
//由于GPIO输出需要从片上外设TIM3来 配置成复用推挽 从手册可以找到
GPIO_Init.Mode = GPIO_MODE_AF_PP;
GPIO_Init.Pin = GPIO_PIN_5;
GPIO_Init.Speed = GPIO_SPEED_FREQ_LOW;
GPIO_Init.Pull = GPIO_NOPULL;//f1系列这个没有用
HAL_GPIO_Init(GPIOB, &GPIO_Init);
}
}
这里注意一定要设置OCFastMode 不然他会改CC2S的值
sConfig.OCFastMode = TIM_OCFAST_DISABLE;
补充:后面学习的时候发现,一般当定义一个句柄的时候最好初始0 不然里边的值是随机的 会影响配置
例如:TIM_IC_InitTypeDef sConfig = {0};
main.c主程序
#include "./SYSTEM/sys/sys.h"
#include "./SYSTEM/usart/usart.h"
#include "./SYSTEM/delay/delay.h"
#include "./BSP/LED/led.h"
#include "./BSP/PWM/pwm.h"
uint16_t pwm = 0;
uint8_t mode = 0; //mode 为0 比较寄存器值递增 1 比较寄存器值递减
extern TIM_HandleTypeDef htim;
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
led_Init(); /* LED初始化 */
TIM_PWM_Init(71,499);
//这个因为之前没有设置快速输出模式 影响了CC1S位 测试使用的
//写入CC2S必须停止CCER的CC2E位
// TIM3->CCER &= ~(1 << 4);
// TIM3->CCMR1 &= (~(1 << 8));
// TIM3->CCMR1 &= (~(1 << 9));
// TIM3->CCER |= (1 << 4);
while(1)
{
if(mode ==0)
{
pwm++;
}
else{
pwm--;
}
//因为满占空比就是ARR的值
if(pwm == TIM3->ARR)
{
mode = 1;
}
else if(pwm == 0)
{
mode = 0;
}
//TIM3->CCR2 = pwm;
__HAL_TIM_SET_COMPARE(&htim,TIM_CHANNEL_2,pwm);
delay_ms(10);
}
}
实现呼吸灯,在while前面那几行是因为最开始不好用,后来发现CC2S位设置的输入模式,用于测试用的。 手册里写的CCER寄存器里边的CC2E位为0,才能写入CC2S。
作者:猿~~~