STM32学习记录(六):定时器TIM
TIM是Timer的简写,是STM32的一种外设
定时器分为高级定时器、通用定时器、基本定时器。
通用定时器框图
这里只说明通用定时器的框图:
- 从图中可以看出使用内部时钟CK_INT作为定时器时钟输入,CK_PSC是输入预分频器PSC的时钟,CK_PSC经过PSC分频后得到定时器计数用的时钟CK_CNT;
- Auto-reload register(ARR)自动重装寄存器存放重装值,当CNT counter的值等于ARR的值,就会产生一个Update Interrupt,并将CNT counter的值重新设为0
- 图中TIMx_CH1是定时器的通道1,通道1有输入和输出模式。在输出模式下,可以选择内部时钟CK_INT作为输入时钟,也可以选择外部时钟ETR作为输入。对于STMF103C8T6芯片的PA0口默认复用功能为TIM2_ETR输入以及TIM2_CH1,因此使用TIM2_CH1作为输出时,只能选择内部时钟CK_INT作为输入。
- OC1REF是CCR1(Capture/Compare Register 1)输出的信号,经过输出控制器Output control(可以对OC1REF进行翻转),最后输出到TIMx_CH1。
内部时钟计数模式
通用定时器的计数模式分向上计数、向下计数、向上向下计数模式三种。其中向上计数模式的时序图如下图所示,CK_CNT是CK_PSC分频后的时钟,这个时序图中分频系数为1,即fCK_CNT=fCK_PSC。假设自动重装值寄存器TIMx_ARR=0x36,当计数寄存器的值达到0x36等于TIMx_ARR中的值,这时会产生一个计数溢出脉冲(Counter overflow)和更新事件脉冲(Update event),计数寄存器重新从0开始计数,同时UIF标志位为1。
要实现TIM定时1s,Counter overflow的时钟频率就应该为1Hz。Counter Overflow的频率: $$ f_{overflow}= \frac{f_{PSC}}{PSC[15:0]+1} *\frac{1}{ARR[15:0]+1} $$,$ f_{PSC}$ 的取值取决于选择内部时钟还是外部时钟,选择内部时钟作为定时器时钟源时,\(f_{PSC}=f_{CKINT}\)。令$ PSC=7200-1、ARR=10000-1$ 、$ f_{CKINT}=72MHz $,可得 $ f_{overflow}=1Hz $,即时钟周期为1s。其中PSC[15:0]是预分频寄存器中的值,ARR[15:0]是自动重装寄存器的值。将CK_PSC四分频为CK_CNT的时序图如下图所示,预分频寄存器的值写入3,也就是4分频,这也是为什么计算频率时PSC的值要+1。由于计数器寄存器(Counter register)是从0开始计数,如果要计数m个脉冲,ARR[15:0]的值应该设置为m-1。
参考手册上有关预分频器寄存器的部分也说明了计数的时钟频率:\(f_{CK\_CNT}=\frac{f_{CKPSC}}{PSC[15:0]+1}\)
内部时钟定时1秒
定时器向上计数的模式、计数时间1s,每秒钟向串口发送数据
定时器初始化
void Timer_Init()
{
/* 开启时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_InternalClockConfig(TIM2);
/**
* 时基单元初始化:
* CK_INT不分频、向上计数模式、ARR自动重载值9999、预分频系数7199、重复计数器=0(高级定时器才有)
* 每过1s产生一次更新中断
*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
/* TIM2中断源配置为更新中断 */
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
/* NVIC配置 */
NVIC_SetPriorityGrouping(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 打开定时器 */
TIM_Cmd(TIM2, ENABLE);
}
定时器中断处理函数
USART重定向有关的内容参照另一篇博客STM32学习记录(五)之串口通信
/* 定时器2中端处理函数 */
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET)
{
num++;
/* 向USART1发送num */
printf("%d", num);
TIM_ClearITPendingBit(TIM2, TIM_IT_Update); //清除TIM2更新事件的中断标志位
}
}
演示结果
定时器TIM2每秒产生一次更新中断(Update Interrupt),在中断处理函数中使用串口经过USB转串口向电脑发送数据
外部时钟计数模式
计数器时钟可以是以下的时钟源:
- 内部时钟(CK_INT)
- 外部时钟模式1:外部输入引脚 external input pin(TIx)
- 外部时钟模式2:外部触发器输入external trigger input (ETR)
- 内部触发器输入(ITRx):使用一个定时器作为令一个定时器的预分频器
外部触发器输入框图如下所示,使用ETR引脚,这也就是外部时钟模式2
参考手册上举了一个例子:计数器每2个ETR上升沿计数一次。ETR是外部触发器输入,ETRP则是经过分频后的信号,如下图ETRP是ETR二分频后的信号。ETRF是ETRP经过滤波后的信号,CK_INT作为滤波器的时钟信号,这个例子中没有滤波。Counter clock在经过2个ETR上升沿(ETRP由高变到低)后,在下一个CK_INT的上升沿时,产生一次脉冲。ETR上升沿与计数器实际时钟之间的延迟是由于ETRP信号上的再同步电路
外部时钟脉冲计数
使用外部时钟作为时钟输入,每个ETR上升沿计数一次,每记录10个ETR上升沿,产生一次更新中断(update interrupt),在更新中断中处理想要的结果。如果要每2个ETR上升沿计数一次,只需修改寄存器TIMx_SMCR的ETPS[1:0]位的值。
定时器初始化
void Timer_Init(void)
{
/* 开启时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB2Periph_GPIOA, ENABLE);
/* 配置GPIO */
GPIO_InitTypeDef GPIO_InitStructure;
/* 配置ETR对应的GPIO */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/**
* 使用外部时钟模式2:不分频、极性不翻转、触发滤波器值最大0X0F
*/
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0X0F);
/**
* 时基单元初始化:
* CK_INT不分频、向上计数模式、ARR自动重载值10、预分频系数1、重复计数器=0(高级定时器才有)
* TIM_ClockDivision对CK_INT进行分频,分频后的时钟用于ETR,TIx的数字滤波器
*
*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 10 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 1 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
/* TIM2中断配置 */
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
/* NVIC配置 */
NVIC_SetPriorityGrouping(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_Init(&NVIC_InitStructure);
/* 打开定时器 */
TIM_Cmd(TIM2, ENABLE);
}
PWM模式
TIM的Channel作为输出时,CCRx执行的操作是比较操作(与计数器的值比较)。
定时器TIM有两种PWM模式:
- PWM模式1:向上计数时,计数寄存器TIMx_CNT的当前值小于比较寄存器TIMx_CCR1的值,OCxREF输出'1';当TIMx_CNT >= TIMx_CCR1的值,OCxREF输出'0'
- PWM模式2:向上计数时,与PWM模式1正好相反
引用参考手册上的原文说明两种PWM模式:
PWM mode 1 - In upcounting, channel 1 is active as long as TIMx_CNT < TIMx_CCR1 else active (OC1REF=1). In downcounting, channel 1 is inactive (OC1REF=‘0) as long as TIMx_CNT>TIMx_CCR1 else active (OC1REF=1).
PWM mode 2 - In upcounting, channel 1 is inactive as long as TIMx_CNTTIMx_CCR1 else inactive. In downcounting, channel 1 is active as long as TIMx_CNT>TIMx_CCR1 else inactive.
向上计数的PWM模式1的时序图:
可以看到,CCRx=4时,TIMx_CNT < 捕获比较寄存器CCRx时,OCxREF输出'1';当TIMx_CNT >= 捕获比较寄存器CCRx时,OCxREF输CCRx时,OCxREF输出'0',当CCRx为其他值时,原理一样
PWM模式实现呼吸灯
使用TIM的PWM模式1实现LED呼吸灯的效果。
由上面的向上计数的PWM模式1的时序图以及脉冲宽度调制(Pulse width modulation: PWM)的原理可知,要达到呼吸灯的效果,只需改变每个周期的占空比。在TIM的PWM模式下,通过改变定时器TIM中捕获比较寄存器CCRx的值,来输出不同占空比的矩形波,每个矩形波高低电平维持时间是不一样的,以此来实现慢慢熄灭以及慢慢点亮的效果。可以看到通过修改CCR1的值,使得OC1REF的占空比不同;占空比不同,那么每次输出电平的高低的持续时间就不同。如果MCU的IO引脚输出低电平能点亮LED灯,下面的时序图表现出来的效果就是LED灯由亮--->灭。(这里输出的OC1REF没有Output control翻转,随着CCR1的值变大,低电平的持续时间变少)
定时器初始化
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;
设置ARR寄存器的值
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1;
设置PSC寄存器的值
设置TIM_Period和TIM_Prescaler是为了得到计数时钟CK_CNT,\(f_{CK\_CNT}\)=10KHz。
void Timer_Init(void)
{
/* 开启时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2 | RCC_APB2Periph_GPIOA, ENABLE);
/* 配置GPIO */
GPIO_InitTypeDef GPIO_InitStructure;
/* 配置TIM2_CH1对应的GPIO */
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
/**
* 使用内部时钟
*/
TIM_InternalClockConfig(TIM2);
/**
* 时基单元初始化:
* CK_INT不分频、向上计数模式、ARR自动重载值100、预分频系数72、重复计数器=0(高级定时器才有)
* TIM_ClockDivision对CK_INT进行分频,分频后的时钟用于ETR,TIx的数字滤波器
*
*/
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 100 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 72 - 1; //输出10000Hz的方波
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
/* 因为有些结构体成员对TIM2无效,所以要为TIM_OCInitStructure每个成员设置默认值 */
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_OCStructInit(&TIM_OCInitStructure);
/**
* 定时器设置为PWM模式1、TIM_Pulse即CCRx寄存器的值、极性不翻转
*
*/
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_Pulse = CCR1_Val;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM2, &TIM_OCInitStructure);
/* 打开定时器 */
TIM_Cmd(TIM2, ENABLE);
}
修改CCR1的值
TIM_SetCompare1函数用于修改CCR1寄存器里面的值。时序图见上。
void Timer_Init(void);
int main(void)
{
/* LED初始化,LED使用PA0 */
LED_Init();
/* 定时器初始化 */
Timer_Init();
while (1)
{
/* LED逐渐变暗 */
for(int i = 0; i < 100; i++) //i为100,因为计数周期就是100
{
TIM_SetCompare1(TIM2, i); //修改捕获/比较寄存器的值
delay_ms(10); //添加延时,能够观察到呼吸灯效果
}
/* LED逐渐变亮 */
for(int i = 99; i >= 0; i--)
{
TIM_SetCompare1(TIM2, i);
delay_ms(10);
}
}
}
演示结果
参考资料
【STM32】第16集 动画告诉你, STM32的定时器到底怎么回事
[6-4] PWM驱动LED呼吸灯&PWM驱动舵机&PWM驱动直流电机_哔哩哔哩_bilibili
《STM32F103xx固件函数库用户手册》
《STM32F10xxx Reference manual》