概述
本章主要讲述一下定时器的实现,定时器顾名思义,就是到了指定的时间就触发中断,所以定时器本身是中断的一种应用。本节的例子中实现的每秒输出一个字符。
代码概览
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_exti.h"
#include "misc.h"
#include "stm32f10x_gpio.h"
#include "stm32f10x_rcc.h"
#include "stm32f10x_tim.h"
void tim2_init()
{
TIM_TimeBaseInitTypeDef tim2;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
tim2.TIM_Period = 1999;
tim2.TIM_Prescaler = 3499;
tim2.TIM_CounterMode = TIM_CounterMode_Up;
tim2.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &tim2);
TIM_Cmd(TIM2, ENABLE);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
}
void tim2_nvic_init(void)
{
NVIC_InitTypeDef tim2_nvic;
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
tim2_nvic.NVIC_IRQChannel = TIM2_IRQn;
tim2_nvic.NVIC_IRQChannelCmd = ENABLE;
tim2_nvic.NVIC_IRQChannelPreemptionPriority = 0;
tim2_nvic.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&tim2_nvic);
}
int count = 0;
void TIM2_IRQHandler(void)
{
if (SET == TIM_GetITStatus(TIM2, TIM_IT_Update))
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
int main(void)
{
tim2_init();
tim2_nvic_init();
while (1)
{
}
}
main函数入手
int main(void)
{
tim2_init();
tim2_nvic_init();
while (1)
{
}
}
可以看到本小节核心代码只有两行,while语句只是用来时间循环,避免程序退出。
定时器初始化
第一行代码是“tim2_init();”,来看一下它的实现:
void tim2_init()
{
TIM_TimeBaseInitTypeDef tim2;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
tim2.TIM_Period = 1999;
tim2.TIM_Prescaler = 3499;
tim2.TIM_CounterMode = TIM_CounterMode_Up;
tim2.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInit(TIM2, &tim2);
TIM_Cmd(TIM2, ENABLE);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
}
定时器类型
首先定义一个TIM_TimeBaseInitTypeDef 的结构体,用来保存配置信息; 然后是使能通过调用RCC_APB1PeriphClockCmd,传参RCC_APB1Periph_TIM2来使能APB1以及TIM2;在STM32F1系列里面(我的板子是STM32F103C8),定义了2个看门狗定时器,1个系统滴答定时器,还有8个Timer定时器;这里的TIM2就是8个定时器里面的;在8个定时器里面:
- TIM6和TIM7是基本定时器,分辨率为16位(即2^16 = 65535);只有计数/定时功能,没有外部IO功能;
- TIM2/3/4/5是通用定时器,分辨率为16位,具有计数定时,输出比较,输入捕获功能;同时有4个外部IO;代码中使能的TIM2就是属于通用寄存器;
- TIM1,TIM8是高级定时器,分辨率是16位,具有计数定时,输出比较,输入捕获,三相电机互补输出信号功能;同时有8个外部IO;
使能TIM2为什么要选择APB1总线呢?还是要回到系统架构图(3.1 System Architecture),可以看到通用定时器以及基本定时器(TIM2~TIM7)都是挂在在APB1下面,而高级定时器则是挂在APB2下面,所以使能TIM2,就要指定其所在的总线APB1:
定时器的计算
定时器的计时过程计算有点复杂,我们先来看一下他的模块图(15.2 TIMx main features):
首先是将CK_PSC值赋给prescaler(PSC),然后是将CK_CNT的值赋给CNT counter,最后由Auto-reload register(ARR)来控制Counter增加/ 下降,以及重启。这三个部分我们逐一来看; 第一部分是prescaler,中文意思是预分频器,为什么要有预分频器呢? 如果定时器采用内部时钟,一般都是很大,比如72MHz,对于Int计数上限就是65535,为什么是65535呢,因为STM32的定时器的分辨率是16位,所谓分辨率就是定时器计数可以用多少bit来表示,16位能够表示的上限,即2^16=65535,如果所以如果溢出(达到65535),只需要0.9ms。计算过程如下:
72MHz=>T = 1/72(us,微妙) <=> 1/72000(ms,毫秒);
所以65535计数所需要的时间:t = 65535 / 72000 ≈ 0.9ms; 所以这个定时器溢出时间在很多场景下是不适用的,如果我想要定时时间为1s咋个办?所以需要降频,于是需要分频,这里需要指定分频系数,来进行分频,是指达到目标量级,比如我通过72分频,将时钟频率降为1MHz,这样每个周期就是1us,于是1000次计数就可以达到1ms;10000次就可以达到1s; 可以看到,通过预分频器,可以通过调整度量时间长短,达到方便的控制定时时长,这个就是预分频器的价值。
第二部分是CNT Counter,这个就是计数器寄存器,其实最重要的还是第三部分auto-reload register(ARR);ARR作为计算代表每个周期翻转的次数,我们用T代表翻转一次所需要的时间;样溢出时间t的公式:
t = 翻转次数*每次翻转的时间;
如果ARR代表翻转的序号,从0开始,那么ARR+1代表翻转次数,f代表时钟频率,于是有:
t =(ARR+1)/ (1/f)
再考虑引入分频 倍数PSC,也是0开始;其实相当于降低频率,即f_psc = f / (PSC+1),于是有:
t = (ARR + 1) / (1 / f_psc) = (ARR+1) / (1 / f / (PSC+1));
t = (ARR + 1) * (PSC+1) / f
比如我现在想要控制时间是1us(微秒),即10^(-6),假设当前系统频率是36MHz,分频系数PSC=1,即:
10^(-6) = (ARR+1)/ (1 / 36*10^6/2) => ARR = 17
即时钟翻转18次完成一个1us计时; 我们再回到代码中:
tim2.TIM_Period = 1999;
tim2.TIM_Prescaler = 3499;
TIM_Period就是上面提到的ARR,Prescaler即分频器,分频倍数是3500;假设时钟是72MHz,通过上面的公式可以获得t约等于0.1s。
时钟分频系数
之类clockdivision是采样时钟倍频系数,如果系统时钟频率很快,但是我们可能只是需要每两个,或者每四个周期进行一次采样(这里描述的输入捕获),这里可以设置一下分频系数;详细的内容我们将会在输入捕获小节中进行讲解,这里只要知道描述的采样间隔即可,用于减少中断次数。
tim2.TIM_ClockDivision = TIM_CKD_DIV1;
定时器配置生效
TIM_TimeBaseInit函数将上面配置定时器的参数写入到相应的寄存器里面:
TIM_TimeBaseInit(TIM2, &tim2);
使能定时器计数器
TIM_Cmd这是使用定时器的计数器:
TIM_Cmd(TIM2, ENABLE);
这个函数的名称还是具有一定的迷惑性,直译是定时器使能(在STM32里面Cmd一般代表使能之意),但是,我们跟踪到TIM_Cmd的内部实现,根据注释可以看到,这个函数主要是通过配置CR1寄存器来实现使能指定定时器的计数器:
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState)
{
assert_param(IS_TIM_ALL_PERIPH(TIMx));
assert_param(IS_FUNCTIONAL_STATE(NewState));
if (NewState != DISABLE)
{
TIMx->CR1 |= TIM_CR1_CEN;
}
else
{
TIMx->CR1 &= (uint16_t)(~((uint16_t)TIM_CR1_CEN));
}
}
使能中断响应
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
这里主要是配置了定时器要响应哪种中断源,中断源也是有很多种的,这里配置的是TIM_IT_Update,下面我们看一下中断源:
"stm32f10x_tim.h"
#define TIM_IT_Update ((uint16_t)0x0001)
#define TIM_IT_CC1 ((uint16_t)0x0002)
#define TIM_IT_CC2 ((uint16_t)0x0004)
#define TIM_IT_CC3 ((uint16_t)0x0008)
#define TIM_IT_CC4 ((uint16_t)0x0010)
#define TIM_IT_COM ((uint16_t)0x0020)
#define TIM_IT_Trigger ((uint16_t)0x0040)
#define TIM_IT_Break ((uint16_t)0x0080)
下面的是TIM_ITConfig的函数注释,对于每种中断源有介绍:
"stm32f10x_tim.c"
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState)
第一种是TIM_IT_Update,也就是我们本次实验使用到的中断源,它代表响应定时器的计数器的初始化(通过软件,或者内部/ 外部触发),上溢/ 下溢,即定时器计数达到峰值之后将会触发中断;
第二种则是输出捕获(Capture)以及输入比较(Compare)触发中断你,后面的实验中将会有介绍;
第三种是触发事件,即“触发”计数后的中断响应,能够触发“计数”的场景包括计数器启动、停止、初始化或者由内部/外部触发计数; 第四种则是定时器中止事件的中断响应;
TIM2的NVIC配置
void tim2_nvic_init(void)
{
NVIC_InitTypeDef tim2_nvic;
tim2_nvic.NVIC_IRQChannel = TIM2_IRQn;
tim2_nvic.NVIC_IRQChannelCmd = ENABLE;
tim2_nvic.NVIC_IRQChannelPreemptionPriority = 0;
tim2_nvic.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&tim2_nvic);
}
TIM2的NVIC大部分都是上一个小节介绍的内容,这里就不再多做介绍。
|