定时器有很多内容,这里学习的是定时中断和时钟源选择
(一)内部中断简介
首先了解一下内部中断的流程图
通过这张图,我们可以大概了解到了如果我们要初始化内部中断要怎么做了,我们要操作的主要有这些(1)打开对应时钟,(2)选择时钟模式,(3)配置时基单元,(4)配置中断输出控制,(5)配置NVIC
定时器的操作代码都在tim.h文件中,由于代码比较多,这里挑一些会用到的新的代码列出来
void TIM_DeInit(TIM_TypeDef* TIMx);
//把定时器的设置恢复为默认值
void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//设置定时器的时基单元,操作三用来初始化时基单元里的变量
void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
//给定时器时基单元初始化结构体赋初始值,这里直接配置结构体,没有用到
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
//使能指定的时钟,等会用这个函数使能时钟2
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
//配置和使能时钟的中断,用来初始化中断输出控制,完成第四步
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
//配置内部时钟模式,在第一步选择内部时钟源时用这个函数配置时钟,完成第二步
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
//设置时钟模式为外部时钟模式1,用于第一步选择外部时钟源时配置,用于初始化第二步
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
//和之前两个函数一样,这个是选择时钟模式2,这里没有用到
void TIM_PrescalerConfig(TIM_TypeDef* TIMx, uint16_t Prescaler, uint16_t TIM_PSCReloadMode);
//这个是配置预分频的,由于在计基单元初始化的结构体中已经配置了预分频,不需要单独配置,但是在代码执行中如果想修改预分频可以用这个函数修改
void TIM_CounterModeConfig(TIM_TypeDef* TIMx, uint16_t TIM_CounterMode);
//这个函数是用来设置计时器的计数模式的,可以选择向上计数,向下计数和中央计数,计数模式在时基单元初始化的时候同样通过结构体配置了,这个是用在代码执行中单独配置的
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);
//标志位清零,用于中断中,每次进入中断标志位都要手动清零,否则会一直申请中断
了解这些函数后就可以进行初始化配置了,我们先对每一步进行初始化配置,在封装成一个整体供外部调用
(二)内部时钟模式-实现秒表
(1)初始化配置
(1)打开时钟
我们操作的通用定时器的时钟在APB1中,打开时钟要调用的是RCC_APB1PeriphClockCmd函数
void timer_clock_init()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
}
(2)选择时钟模式
我们选择的是内部时钟源,因此我们选择内部时钟模式,并且时钟选择为通用时钟TIM2
void timer_clock_channel_init()
{
TIM_InternalClockConfig(TIM2);
}
(3)时基单元配置
这里用到之前提到的TIM_TimeBaseInit函数,第一个参数是要打开的时钟,我们这里选择的是通用时钟TIM2,可以跳转定义看下初始化结构体
这里第一个参数是设置预分频的,第二个参数是设置计数方式的,第三个参数是设置自动重装值的,第四个参数是设置时钟刻度(设置采样频率用于消除干扰),最后一个是设置重复计数器值(通用定时器没有,高级定时器才有,可以忽略)
了解参数后,我们可以这样配置,首先第四个TIM_ClockDivision是设置采样频率的,有三种方式(TIM_CKD_DIV1:不分频,TIM_CKD_DIV2:二分频,TIM_CKD_DIV4:四分频),这里我们使用的是内部时钟,我们选择不分频,最后一个TIM_RepetitionCounter由于通用定时器没有这个功能,直接给0,第二个TIM_CounterMode有这几种计数模式
我们选择的是向上计数,所以参数给TIM_CounterMode_Up
最后决定计数器计数时间长短的就是预分频和自动重装值,这里我们有一个计算计时器中断的时间公式,这里我们的系统时钟频率是72MHZ,如果我们要计数器一秒产生中断我们可以让period=10000-1,让prescaler=7200-1,这样相除后T=1
这样我们就配置好了时基单元部分
void timer_time_base_init()
{
TIM_TimeBaseInitTypeDef timebase_init;
timebase_init.TIM_ClockDivision = TIM_CKD_DIV1;
timebase_init.TIM_CounterMode = TIM_CounterMode_Up;
timebase_init.TIM_Period = 10000-1; //72Mhz / (period+1)(repetion+1)
timebase_init.TIM_Prescaler = 7200-1;
timebase_init.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &timebase_init);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
}
(4)中断输出控制
这个很简单,只要调用一下中断控制函数TIM_ITConfig,配置好参数就行了
这里主要看下第二个参数,这个参数选择的是特定的中断,其中第一个TIM_IT_Update是更新中断,也就是溢出中断,后面几个是一些捕获比较中断,这里没有用到,我们选择第一个
void timer_interrupt_control_init()
{
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
}
(5)NVIC
这个就很简单了,和之前配置外部中断一样,我们配置好优先级分组和初始化结构体就可以了,这里的通道我们可以和之前一样去到stm32f10x.h上看一下
选择TIM2_IRQn就可以了
void timer_nvic_init()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef nvic_init;
nvic_init.NVIC_IRQChannel = TIM2_IRQn;
nvic_init.NVIC_IRQChannelCmd = ENABLE;
nvic_init.NVIC_IRQChannelPreemptionPriority = 1;
nvic_init.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&nvic_init);
}
(6)最后处理
最后我们还要使能一下TIM2时钟,否则时钟是不工作的
void timer_open()
{
TIM_Cmd(TIM2, ENABLE);
}
最后合成为一个外部可调用的初始化函数,就完成了timer模块的编写
#include "stm32f10x.h" // Device header
void timer_clock_init()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
}
void timer_clock_channel_init()
{
TIM_InternalClockConfig(TIM2);
}
void timer_time_base_init()
{
TIM_TimeBaseInitTypeDef timebase_init;
timebase_init.TIM_ClockDivision = TIM_CKD_DIV1;
timebase_init.TIM_CounterMode = TIM_CounterMode_Up;
timebase_init.TIM_Period = 10000-1; //72Mhz / (period+1)(repetion+1)
timebase_init.TIM_Prescaler = 7200-1;
timebase_init.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &timebase_init);
TIM_ClearFlag(TIM2, TIM_FLAG_Update); //清除上一句代码产生的中断标志位
}
void timer_interrupt_control_init()
{
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
}
void timer_nvic_init()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef nvic_init;
nvic_init.NVIC_IRQChannel = TIM2_IRQn;
nvic_init.NVIC_IRQChannelCmd = ENABLE;
nvic_init.NVIC_IRQChannelPreemptionPriority = 1;
nvic_init.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&nvic_init);
}
void timer_open()
{
TIM_Cmd(TIM2, ENABLE);
}
void timer_init()
{
timer_clock_init();
timer_clock_channel_init();
timer_time_base_init();
timer_interrupt_control_init();
timer_nvic_init();
timer_open();
}
头文件仅需要对timer_init()声明
#ifndef __TIMER_H__
#define __TIMER_H__
void timer_init(void);
#endif
(2)主函数和中断函数
打开startup_stm32f10x_md.s文件可以找到TIM2的中断函数
我们在里面例行判断一下其状态(虽然这个通道只有一个中断),然后在中断后一定要记住把中断标志位清零,再加上一些秒表的判断语句和OLED屏幕的显示语句,就可以实现在OLED上秒表计数了(当然为了简单没有开始和结束,只能按复位键重置)
#include "stm32f10x.h" // Device header
#include "timer.h"
#include "OLED.h"
unsigned char min = 0;
unsigned char sec = 0;
unsigned char hour = 0;
int main()
{
timer_init();
OLED_Init();
OLED_ShowChar(1, 3, ':');
OLED_ShowChar(1, 6, ':');
while(1)
{
OLED_ShowNum(1, 1, hour, 2);
OLED_ShowNum(1, 4, sec, 2);
OLED_ShowNum(1, 7, min, 2);
}
return 0;
}
void TIM2_IRQHandler()
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
min++;
if (min >= 60)
{
sec++;
min = 0;
if (sec >= 60)
{
hour++;
sec = 0;
if (hour >= 24)
{
hour = 0;
}
}
}
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
(三)外部时钟模式-对射式红外传感器计次
我们把红外对射传感器收到遮挡时产生的高低电平跳变作为时钟,每计数十次进入中断,值得注意的是我们的ETR是和PA0复用一个端口,因此我们要把红外对射传感器插在PA0口而不是其他口
(1)初始化
初始化和使用内部时钟模式的代码不同的地方有n点
(1)除了打开TIM2时钟外还要打开GPIOA时钟;
(2)要对PA0初始化,设置为输入模式;
(3)时钟通道选择的是外部时钟模式2,因此调用的是TIM_ETRClockMode2Config函数;
(4)把预分频设置为0,自动重装值设置为10,这样我们的红外产生十次信号就会进入中断
外部时钟配置函数可能会有一些分频(第二个:TIM_ExtTRGPrescaler)、电平翻转(第三个:TIM_ExtTRGPolarity)和滤波(最后一个:ExtTRGFilter),这里选择不分频,不电源翻转,不滤波
外部时钟滤波
这样就可以写好定时器模块了
#include "stm32f10x.h" // Device header
void external_apb_init()
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
}
void external_gpio_init()
{
GPIO_InitTypeDef gpio_initdef;
gpio_initdef.GPIO_Mode = GPIO_Mode_IPU;
gpio_initdef.GPIO_Pin = GPIO_Pin_0;
gpio_initdef.GPIO_Speed = GPIO_Speed_10MHz;
GPIO_Init(GPIOA, &gpio_initdef);
}
void external_clock_channel_init()
{
TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted, 0x0F);
}
void external_time_base_init()
{
TIM_TimeBaseInitTypeDef time_base_initdef;
time_base_initdef.TIM_ClockDivision = TIM_CKD_DIV1;
time_base_initdef.TIM_CounterMode = TIM_CounterMode_Up;
time_base_initdef.TIM_Period = 10-1;
time_base_initdef.TIM_Prescaler = 1-1;
time_base_initdef.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &time_base_initdef);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
}
void external_interrupt_init()
{
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
}
void external_nvic_init()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef nvic_initdef;
nvic_initdef.NVIC_IRQChannel = TIM2_IRQn;
nvic_initdef.NVIC_IRQChannelCmd = ENABLE;
nvic_initdef.NVIC_IRQChannelPreemptionPriority = 1;
nvic_initdef.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&nvic_initdef);
}
void external_timer_init()
{
TIM_Cmd(TIM2, ENABLE);
}
void external_clock_init()
{
external_apb_init();
external_gpio_init();
external_clock_channel_init();
external_time_base_init();
external_interrupt_init();
external_nvic_init();
external_timer_init();
}
只要把external_clock_init()放在头文件声明即可,主函数只要调用这个函数即可初始化中断
(2)主函数
主函数可以显示一下中断次数和目前的计数值
#include "stm32f10x.h" // Device header
#include "OLED.h"
#include "internal_clock_timer.h"
unsigned int count = 0;
int main()
{
OLED_Init();
external_clock_init();
while(1)
{
OLED_ShowNum(1, 1, count, 5);
OLED_ShowNum(2, 1, TIM_GetCounter(TIM2), 3);
}
return 0;
}
void TIM2_IRQHandler()
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update))
{
count++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
(四)总结
通过实现秒表和红外计次的功能,我们初步认识了内部中断,了解了内部中断的基本配置和内外时钟源的选择,内部中断远远不止这些,还要我们继续探索