32单片机基础教程:定时器定时中断详解
接线图如图所示
如何初始化定时器呢?下图是定时器框架的所有结构,我们只需要把这里面的每个模块都打通,就可以让定时器工作了,
第一步:RCC开启时钟,这个基本上没=每个代码都是第一步,在这里打开时钟后,定时器的基准时钟和整个外设的工作时钟就会同时打开了。
第二步:选择时基单元的时钟源,对于定时中断,我们就选择内部时钟源。
第三步:配置时基单元,包括预分频器,自动重装器,计数模式等,这些参数用结构体配置
第四步:配置输出中断控制,允许更新中断输出到NVIC.
第五步:配置NVIC,在NVIC中打开定时器中断的通道,并分配一个优先级
第六步:运行控制
整个模块配置完成后,我们还需要使能一下计数器。要不然计数器是不会运行的。
当计数器使能后,计数器开始计数了,当计数器更新时,触发中断。
最后写一个定时器中断函数
这样这个中断函数每隔一段时间就能自动执行一次了。
打开相对应的库函数,可以看到这个库函数非常多,我们把本节要用的函数讲一下
1.void TIM_DeInit(TIM_TypeDef* TIMx); (不重要)
恢复缺省配置,
2.void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);(重要)
时基单元初始化,它就是用来配置这个图里这里的时基单元的。
两个参数,第一个TIMx,选择某个定时器,第二个是结构体,里面包含了配置时基单元的一些参数
第一个按钮,添加书签,第二个,跳到上一个书签,第三个,跳到下一个书签,第四个,删除所以书签。
3.void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
这个函数可以把一个结构体变量赋给一个默认值
4.void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
这个是用来使能计数器的,对应下图所示,两个参数,TIMx选择定时器。 NewState,新的状态,也就是使能还是失能。使能,计数器可以运行,失能,计数器就不运行
5.void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
这个是用来使能中断输出信号的,对应下图位置,看一下参数,TIMx选择哪一个定时器,TIM_IT,选择要配置哪一个中断输出。 NewState新的状态。这个ITConfig函数以后还会经常遇到
下面这六个函数对应时基单元时钟选择部分,对应下图这一块,可以选择RCC内部时钟,ETR外部时钟等
1.void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
选择内部时钟,调用一下,就可以这样连接
2.void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
选择ITRx其他定时器的时钟,参数是TIMx,选择要配置的定时器和 InputTriggerSource选择要接入哪个其他定时器。调用一下,连接如下图
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,
uint16_t TIM_ICPolarity, uint16_t ICFilter);
选择TIx捕获通道时钟,TIM_TIxExternalCLKSource,选择TIx具体的某一个引脚。TIM_ICPolarity,ICFilter输入的极性和滤波器。对于外部引脚的波形,一般都会有极性选择和滤波器,这样更加灵活一些,调用一下这个函数,连接如图
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
选择ETR通过外部时钟模式1输入时钟,ExtTRGPrescaler外部触发预分频器,这里可以对ETR的外部时钟再提前做一个分频,
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,
uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
选择ETR通过外部时钟模式2输入的时钟,
对于ETR输入的时钟而言,这两个函数是等效的。如果不需要触发输入功能,那这两个函数可以互换。
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
这个不是用来选择时钟的,就是单独用来配置ETR引脚的预分频器,极性,滤波器这些参数的。
以上就是对最上面那个图的具体配置了,这样初始化基本就OK了。
下面我们再来看几个比较关键的函数,因为在初始化结构体里有很多关键的参数,比如自动重装值和预分频等等,这些参数可能在初始化之后还需要更改,如果为了改某个参数还要调用一次初始化函数,太麻烦了,所以这里有一些单独的函数,可以方便的更改这些关键参数
如:
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
就是用来单独写预分频值的,看一下参数,Prescaler就是要写入的预分频值,PSCReloadMode写入的模式,预分频有一个缓冲器,写入的值是在更新事件发生后才有效的,所以这里有个写入的模式,可以选择时听从安排,在更新事件生效。或者是,在写入后,手动产生一个更新事件,让这个值立刻生效。不过这些都是细节问题,影响不大,你就知道这个是写预分频值的函数就行了。
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
用来改变计数器的计数模式,参数TIM_CounterMode,选择新的计数器模式
void TIM_ARRPreloadConfig(TIM_TypeDef* TIMx, FunctionalState NewState);
自动重装器预装功能配置。
void TIM_SetCounter(TIM_TypeDef* TIMx, uint16_t Counter);
给计数器写一个值,如果你想手动给计数值,就可以用这个函数
void TIM_SetAutoreload(TIM_TypeDef* TIMx, uint16_t Autoreload);
给自动重装器写一个值,如果你想手动给一个自动重装值,就可以用这个函数。
uint16_t TIM_GetCounter(TIM_TypeDef* TIMx);
获取当前计数器的值,如果你想看当前计数器计到哪里了,就可以调用这个函数,返回值就是当前的计数器的值,
uint16_t TIM_GetPrescaler(TIM_TypeDef* TIMx);
获取当前的预分频器的值,如果想看预分频器的值,就调用这个函数
FlagStatus TIM_GetFlagStatus(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);
ITStatus TIM_GetITStatus(TIM_TypeDef* TIMx, uint16_t TIM_IT);
void TIM_ClearITPendingBit(TIM_TypeDef* TIMx, uint16_t TIM_IT);
这四个函数就是用来获取标志位和清除标志位的,
OK,我们来初始化定时器,首先,我们用的是TIM2,也就是通用定时器
首先开启时钟,要使用APB1的开启时钟函数,因为TIM2是APB1总线的外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE); //开启时钟
我选择的是内部时钟作为时基单元的时钟,但是也可以不写这个函数,因为定时器上电后默认就是使用内部时钟,
TIM_InternalClockConfig(TIM2); //选择时基单元的时钟
配置时基单元
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;//定义数组
TIM_TimeBaseInitStructure.TIM_ClockDivision= TIM_CKD_DIV1 ;//使用1分频
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_Period:周期,就是ARR自动重装器的值
TIM_Prescaler:就是PSC预分频的值
TIM_RepetitionCounter:是重复计数器的值,是高级定时器才有的,这里不需要用,直接给0就OK。
这些参数就是时基单元里每一个关键寄存器的参数了。不过这里没有CNT计数器的参数,这个如果我们之后需要的话,可以使用之前说的SetCounter 和GetCounter来操作计数器。决定定时时间的参数,就是TIM_Period,TIM_Prescale。如果我们想定一个1S的时间,参考下图那个公式。
定时频率=72M/(PSC+1)/(ARR+1),定时1s,也就是定时频率为1hz,那我们就可以给PSC给一个7200,ARR给一个10000.然后两个参数都减一,因为预分频器和计数器都有一个数的偏差,所以这里要再减一个1.然后注意PSC和ARR的取值都要在0~65535之间,不要超过范围,而且取值唯一。在这里我们预分频是对72M进行7200分频,得到的是10K的计数频率,在10K的频率下,计10000个数,那就是1s的时间。
下面使能更新中断了,
TIM_ITConfig(TIM2,TIM_EventSource_Update,ENABLE);
NVIC,上一篇博文讲过,自己取回顾
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel= TIM2_IRQn ;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
启动定时器:
TIM_Cmd(TIM2,ENABLE);
这样定时器就可以开始工作了,
当产生跟新时,就可以触发中断,到这里,整个定时器初始化的代码就完成了
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_InternalClockConfig(TIM2);
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);
TIM_ITConfig(TIM2,TIM_EventSource_Update,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel= TIM2_IRQn ;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2,ENABLE);
}
接下来写中断函数,打开启动文件
如图所示,这个就是定时器2的中断函数了,
老规矩,判断一下标志位,判断更新标志的标志位,
if(TIM_GetITStatus(TIM2, TIM_IT_Update)==SET)
别忘了最后清除标志位,最后的代码为
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update)==SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
使用跨文件的变量,用extern声明即可。
调用如下函数时,会出现一个奇怪的现象,按复位键,Num为1,这说明中断函数在初始化之后就立刻进入了一次
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main()
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Num:");
while(1)
{
OLED_ShowNum(1,5,Num,5);
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update)==SET)
{
Num++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
打开这个TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure); 函数,看到有以下注释,生成一个更新事件,来重新装在预分频和重复计数器的值,;立刻,为啥要加这一句呢,我们知道这个预分频是有一个缓存计寄存器的,我们写的值只有在更新事件时,才会真正起到作用。所以为了让值立起作用,手动生成了一个更新事件,这样,预分频器的值就有效了但同时,他的副作用是更新事件和更新中断是同时发生的,更新中断会置更新中断标志位,当我们一旦初始化完了更新中断就会立刻进入。这就是我们刚一上电,就立刻进中断的原因。
TIM_ClearFlag(TIM2,TIM_IT_Update);加入这个函数,就能避免刚进入初始化就进中断的问题了
第一个代码如下:
Timer.c
#include "stm32f10x.h" // Device header
//extern uint16_t Num;
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision= TIM_CKD_DIV1 ;
TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up ;
TIM_TimeBaseInitStructure.TIM_Period=1000-1;
TIM_TimeBaseInitStructure.TIM_Prescaler=7200-1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter=0;
TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2,TIM_IT_Update);
TIM_ITConfig(TIM2,TIM_EventSource_Update,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel= TIM2_IRQn ;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2,ENABLE);
}
// void TIM2_IRQHandler(void)
// {
// if(TIM_GetITStatus(TIM2, TIM_IT_Update)==SET)
// {
// Num++;
// TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
// }
// }
Timer.h
#ifndef __OLED_H
#define __OLED_H
void Timer_Init(void);
#endif
main.c
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main()
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Num:");
while(1)
{
OLED_ShowNum(1,5,Num,3);
OLED_ShowNum(2,5,TIM_GetCounter(TIM2),5);
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update)==SET)
{
Num++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
我们再写一个类型的项目:定时器外部时钟
接线图如下:加入一个对射式红外传感器
在上面的那个程序稍微改一下,现在我们的任务仍然是定时中断,时钟部分,不再使用内部时钟
用TIM_ETRClockMode2Config这个函数,通过ETR引脚的外部时钟模式2配置。跳转到函数找到要的参数。
第四个参数就是以一个采样频率f采用N个点,如果N个点都一样,才会有效输出,那这个参数就是来决定f和N的,具体怎样的对应关系,手册有介绍。
我们暂时不用滤波,就写0x00就行了。
引脚要用到GPIO,所以在这之前还是要先配置GPIO。
不过我没有选择浮空输入,而是上拉输入
代码如下:
main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main()
{
OLED_Init();
Timer_Init();
OLED_ShowString(1,1,"Num:");
OLED_ShowString(2,1,"CNT:");
while(1)
{
OLED_ShowNum(1,5,Num,3);
OLED_ShowNum(2,5,Timer_GetCounter(),5);
}
}
void TIM2_IRQHandler(void)
{
if(TIM_GetITStatus(TIM2, TIM_IT_Update)==SET)
{
Num++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
Timer.h
#ifndef __OLED_H
#define __OLED_Hvoid Timer_Init(void);
uint16_t Timer_GetCounter(void);#endif
Timer.c
#include "stm32f10x.h" // Device header
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IPU;//上拉输入
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_0;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_Init(GPIOA,&GPIO_InitStructure);
TIM_ETRClockMode2Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_NonInverted,0x00);
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);
TIM_ClearFlag(TIM2,TIM_IT_Update);
TIM_ITConfig(TIM2,TIM_EventSource_Update,ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel= TIM2_IRQn ;
NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2,ENABLE);
}
//实时看一下CN计数器的值
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2);
}
作者:爱学C语音的猫