2.13 定时器的定时中断和外部时钟

定时器内部中断接线图

只需要在STM32最小系统板上插入OLED屏幕就行了,因为定时器和中断系统的电路都在STM32内部,定时中断也是为我们程序设计提供服务的,所以只需要插一个OLED显示数据就行了。

写代码

初始化定时器大体思路

写关于定时器的代码,首先就需要写定时器的初始化函数,根据定时中断的整个框架结构进行初始化。

只需要把这里的每个模块都打通,就可以让定时器工作了。

第一步。RCC开启时钟。(RCC开启时钟后,让定时器默认的时钟源是APB1,后续可以通过配置改变成不同的时钟源。)

打开时钟后,定时器的基准时钟和整个外设的工作时钟都会同时打开了。

第二步,选择时基单元的时钟源。

对于定时中断,我们就选择内部时钟源。

第三步,配置时基单元。

配置预分频器、自动重装器、计数模式等等,这些参数用一个结构体就可以配置好。

第四步,配置输出中断控制。

允许更新中断输出到NVIC。

第五步,配置NVIC。

在NVIC中打开定时器中断的通道,并分配一个优先级。

第六步,运行控制。

整个模块配置完成后,我们还需要使能一下计数器,要不然计数器是不会运行的。

当定时器使能后,计数器就会开始计数了,当计数器更新时,触发中断,最后我们再写一个定时器中断函数,这样这个中断函数每隔一段时间就能自动执行一次了。

定时器库函数

我们找一下定时器TIM的库函数,在stm32f10x_tim.h的最后可以看到TIM的库函数数量非常多,我们先把我们需要用的挑出来讲一下,剩下的之后慢慢学。

TIM恢复默认配置

void TIM_DeInit(TIM_TypeDef* TIMx);恢复缺省配置。

在嵌入式编程中,“缺省配置”通常是指设备或外设在复位(reset)后的初始状态或默认配置。这种默认配置是由硬件设计和厂商文档规定的,通常用于确保设备在最初启动时处于已知和稳定的状态。在STM32微控制器中,TIM_DeInit函数用于将指定的定时器外设恢复到其复位后的默认状态。

时基单元初始化

void TIM_TimeBaseInit(TIM_TypeDef* TIMx, TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);时基单元初始化。

这个函数比较重要,他就是用来配置这个图里的时基单元的。这里面有两个参数,第一个是TIMx选择某个定时器,第二个是结构体,里面包含了配置时基单元的一些参数。

结构体变量恢复默认

void TIM_TimeBaseStructInit(TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);这个函数可以把结构体变量赋一个默认值。

使能计数器

void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);使能计数器。

对应在图中就是运行控制的位置,有两个参数,第一个TIMx选择定时器,第二个NewState就是使能或者失能。

使能中断输出信号

void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);用来使能中断输出信号的。

对应的就是图中的中断输出控制的位置。第一个参数TIMx选择定时器,第二个TIM_IT,选择要配置哪个中断输出。第三个Newstate使能或者失能。

这种ITConfig函数之后会经常遇到,就是使能外设的中断输出。

下面这个六个函数对应的是时基单元的时钟选择部分(可以选择RCC内部时钟、ETR外部时钟、ITRx其他定时器、TIx捕获通道):

选择内部时钟

void TIM_InternalClockConfig(TIM_TypeDef* TIMx);选择内部时钟。参数只有一个TIMx,调用一下连接就是这样的:

选择ITRx其他定时器的时钟

void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);选择ITRx其他定时器的时钟。

参数是TIMx,选择要配置的定时器。InputTriggerSource选择要接入哪个其他的定时器。

选择TIx捕获通道的时钟

void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource,uint16_t TIM_ICPolarity, uint16_t ICFilter);选择TIx捕获通道的时钟。

参数是TIMx,选择要配置的定时器。TIxExternalCLKSource选择TIx具体的某个引脚。ICPolarity和ICFilter是输入的极性和滤波器。对于外部引脚的波形,一般都会有极性选择和滤波器,这样更灵活一些。

选择ETR通过外部时钟模式1输入的时钟

void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter);选择ETR通过外部时钟模式1输入的时钟。

第一个参数ExtTRGPrescaler是外部触发预分频器。这里可以对ETR的外部时钟再提前做一个分频。

Polarity和Filter也是一样,极性和滤波器。

选择ETR通过外部时钟模式2输入的时钟

void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler,uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);选择ETR通过外部时钟模式2输入的时钟。

第一个参数ExtTRGPrescaler是外部触发预分频器。这里可以对ETR的外部时钟再提前做一个分频。

Polarity和Filter也是一样,极性和滤波器。

配置ETR引脚的预分频器等参数

void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter);

这个函数不是用来选择时钟的,就是用来单独配置ETR引脚的预分频器、极性、滤波器这些参数的。

因为在初始化结构体里有很多关键的参数,比如自动重装值和预分频值等等,这些参数可能会在初始化之后还需要更改,如果为了改某个参数还要再调用一次初始化函数,那也太麻烦了,所以这里有一些单独的函数,可以方便的更改这些关键参数。

单独写预分频值

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);用来改变计数器的计数模式,参数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);

初始化定时器

按照我们所说的步骤来初始化定时器,在这里我们初始化的定时器是TIM2也就是通用定时器。

第一步,开启时钟。

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);

第二步,选择时基单元的时钟。

TIM_InternalClockConfig(TIM2);

不过这个选择时钟的函数,我看其他很多人的代码都没有写,因为定时器上电后默认就是使用的内部时钟,如果不调用这个函数,那也是使用的内部时钟。

第三步,配置时基单元。

时基单元中有结构体类型TIM_TimeBaseInitTypeDef,我们先把这个结构体类型里面的成员一一介绍一下。

TIM_ClockDivision:指定时钟分频。这个时钟分频的参数之前讲原理的时候一直没出现过,那它是用来干啥的?我们之前说了,在定时器的外部信号引入输入引脚,一般都会有一个滤波器,比如这里:,这个滤波器可以滤掉信号的抖动干扰,那他是怎么工作的呢?其实也是非常简单的,就是在一个固定的时钟频率f下进行采样,如果连续N个采样点都为相同的电平,那就代表输入信号稳定了,就把这个采样值输出出去。如果这N个采样值不全都相同,那就说明信号有抖动,这时就保持上一次的输出,或者直接输出低电平也行,这样就能保证输出信号在一定程度上的滤波。这里的采样频率f和采样点数N都是滤波器的参数,频率越低,采样点数越多,那滤波效果就越好,不过相应的信号延迟就越大,这个是这个滤波器的工作原理。

现在关键的地方来了,这个采样频率f从哪来?那手册里写的是他可以是由内部时钟直接而来,也可以是内部时钟加一个时钟分频而来,那分频多少?就是由我们这个参数TIM_ClockDivision决定的。可见这个参数和时基单元关系并不大,在这里我们随便配一个就行了,选择一分频TIM_CKD_DIV1就行。

TIM_CounterMode:计数器模式。选择向上计数TIM_CounterMode_Up。

TIM_Period:周期,就是ARR自动重装器的值。

TIM_Prescale:就是PSC预分频器的值。

TIM_RepetitionCounter:就是重复计数器的值,是高级定时器才有的,这里不需要用,我们直接给0就好了。

这些参数就是时基单元里面每个关键寄存器的参数了,不过这里并没有CNT计数器的参数。这个如果我们之后需要的话,可以用库函数SetCounter和GetCounter操作计数器,用以读写计算器。

决定定时时间的参数就是TIM_Period和TIM_Prescale,如果我们想定一个1s的时间,就可以参考一下这个公式: (自动重装器的值+ 1)/(时钟频率/预分频器的值+1),自动重装器的值给10000-1,预分频器的值给7200-1,这样就完成了。因为预分频器和计数器都有1个数的偏差,所以这里要再减个1。在10K的频率下计10000个数,正好就是1s。

注意PSC和ARR的取值要在0~65535之间,不要超过这个范围。

	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStrcture;
	TIM_TimeBaseInitStrcture.TIM_ClockDivision=TIM_CKD_DIV1; //指定时钟分频。
	TIM_TimeBaseInitStrcture.TIM_CounterMode=TIM_CounterMode_Up;//指定计数器模式
	TIM_TimeBaseInitStrcture.TIM_Period=10000-1;//自动重装寄存器ARR的值
	TIM_TimeBaseInitStrcture.TIM_Prescaler=7200-1;//预分频器PSC的值
	TIM_TimeBaseInitStrcture.TIM_RepetitionCounter=0;//通用定时器没有重复计数器,填0
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStrcture);

第四步,配置输出中断控制。

接下来就要开始使能更新中断了,TIM_IT_Update是更新中断的意思。

TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);

这样就开启了更新中断到NVIC的通路。

第五步,配置NVIC。

	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//选择分组2
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;//NVIC通道
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;//使能该中断通道
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
	NVIC_Init(&NVIC_InitStructure);

第六步,启动定时器。

TIM_Cmd(TIM2,ENABLE);

通过这六个步骤,定时器就可以开始工作了。当产生更新时,就会触发中断,到这里,整个定时中断的初始化代码就完成了。接下来就可以写中断函数了。

中断函数

找一下启动文件startup_stm32f10x_md.s,可以找到TIM2的中断函数TIM2_IRQHandler。

void TIM2_IRQHandler(){
	if (TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
	{
		Num++;
		TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
	}
}

这样设置中断函数,表示定时器每1s触发一次中断后,中断函数使Num自增一次。我们只需要把Num表示到OLED屏上即可。

//主函数
#include "stm32f10x.h"
#include "OLED.h"
#include "timer.h"
int Num;
int main(void){
	OLED_Init();
	Timer_Init();
	OLED_ShowString(1,6,"AOOFOO");//字符串
	OLED_ShowString(2,1,"Num:");
	while(1){
	OLED_ShowNum(2,5,Num,5);
	  
	}
}

但这个程序有问题,我们可以手动复位一下我们的单片机,可以发现OLED屏上的数字是从1开始计数的,这就奇怪了,我们明明应该是从0开始计数,为什么屏幕上是从1开始的呢?

这说明中断函数在初始化后,立刻进入了一次,使得Num被自增了一次。这怎么回事呢?这个问题就出现在TimeBaseInit函数,里面有一个代码是这样写的:生成一个更新事件,来重新装载预分频器和重复计数器的值,立刻。我们知道预分频器是有个缓冲寄存器的,我们写的值只有在更新事件时,才会真正起作用,所以这里为了让值立刻起作用,就在这最后,手动生成了一个更新事件,这样预分频器的值就有效了,但同时它的副作用就是,更新事件和更新中断是同时发生的(理解:这个函数会产生一个更新中断和一个更新事件),更新中断会置更新中断标志位,当我们之后一旦初始化完了,更新中断就会立刻进入,这就是我们刚一上电,就立刻进入中断的原因。

所以解决这个问题,就要在更新中断使能前,清除一下更新中断标志位,下面是优化后的代码:

void Timer_Init(){
	//开启时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
	//选择时基单元时钟
	TIM_InternalClockConfig(TIM2);
	//配置时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStrcture;
	TIM_TimeBaseInitStrcture.TIM_ClockDivision=TIM_CKD_DIV1; //指定时钟分频。
	TIM_TimeBaseInitStrcture.TIM_CounterMode=TIM_CounterMode_Up;//指定计数器模式
	TIM_TimeBaseInitStrcture.TIM_Period=10000-1;//自动重装寄存器ARR的值
	TIM_TimeBaseInitStrcture.TIM_Prescaler=7200-1;//预分频器PSC的值
	TIM_TimeBaseInitStrcture.TIM_RepetitionCounter=0;//通用定时器没有重复计数器,填0
	
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStrcture);
	 // 清除更新中断标志位
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	
	//配置输出中断
	TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//使能更新中断。

	//配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//选择分组2
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;//NVIC通道
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;//使能该中断通道
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
	NVIC_Init(&NVIC_InitStructure);
	//启动定时器
	TIM_Cmd(TIM2,ENABLE);
	
}

定时器外部时钟接线图

通过上面我们可以知道,PA0引脚就是TIM2的ETR引脚,我们就在这个引脚输入一个外部时钟。

写代码

现在我们的基本任务还是定时中断,但是这个时钟部分,我们就不使用内部时钟了。对于模块中的对射式红外传感器,无遮挡时模块DO输出低电平,遮挡时DO输出高电平,通过使用遮光板不断地遮挡抽离实现一个外部时钟的功能。

我们设计自动重装值为9(实际是满10进行一次更新中断),预分频器值为0(实际是一分频,不对外部时钟信号进行分频,遮挡几下就有几个上升沿)。

因为对射式红外传感器使用的是GPIOA的引脚,使用前要对GPIOA的时钟进行使能,并且配置PA0的引脚。

设置好之后,在屏幕上显示计数器的值和Num的值(Num的值是更新中断的次数),即理想状态下,计数器计10次后清零,Num值加1。

遮光板没有进入之前,PA0是低电平,遮光板进入之后,PA0输出高电平,这时就会有一个上升沿。这个上升沿要怎么控制内部的电路呢?这就需要这个函数:TIM_ETRClockMode2Config();

我们上半部分我们知道时基单元时钟的选择有六个函数,这就是对应的“选择ETR通过外部时钟模式2输入的时钟”的函数,这个模式下可以让PA0的时钟作为时基单元的时钟。

TIM_ETRClockMode2Config()中有四个参数。

第一个是选择定时器,我们这里使用的是TIM2通用定时器;

第二个是用于配置定时器的外部触发预分频器(External Trigger Prescaler)。这个参数决定了外部触发信号(ETRP)的频率如何被分频。以下是具体的参数及其含义:

TIM_ExtTRGPSC_OFF: 外部触发预分频器关闭,ETRP信号直接使用,不进行分频。
TIM_ExtTRGPSC_DIV2: ETRP信号频率被2分频。
TIM_ExtTRGPSC_DIV4: ETRP信号频率被4分频。
TIM_ExtTRGPSC_DIV8: ETRP信号频率被8分频。

我们这里不对ETRP的信号进行分频,因为我们想遮挡一次,计数器的值就增加1。

第三个是参数用于配置定时器的外部触发极性(External Trigger Polarity)。这个参数决定了外部触发信号(ETRP)是对高电平或上升沿有效,还是对低电平或下降沿有效。以下是具体的参数及其含义:

TIM_ExtTRGPolarity_Inverted: 外部触发信号为低电平或下降沿时有效。
TIM_ExtTRGPolarity_NonInverted: 外部触发信号为高电平或上升沿时有效。

这个就看自己的选择来,如果你想遮挡立马就计数,就选择上升沿有效;如果你想要遮挡拿开后再计数,就选择下降沿有效。这里我们选择上升沿有效,就是遮挡后立马计数。

第四个参数是参数用于配置定时器的外部触发滤波器(External Trigger Filter)。这个参数决定了对外部触发信号进行滤波的级别,从而可以减少噪声对信号的影响。这个实际上就是之前讲的选择N的个数,N越多滤波的效果越好,我们这里不选择滤波,所以写0X00;

TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted,0x00);

至此,全部流程已然理解,写代码自然不在话下:

void Timer_Init(){
	//开启时钟
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2,ENABLE);
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);
	//配置GPIO
	GPIO_InitTypeDef GPIO_InitStrcture;
	GPIO_InitStrcture.GPIO_Mode=GPIO_Mode_IPU;
	GPIO_InitStrcture.GPIO_Pin=GPIO_Pin_0;
	GPIO_Init(GPIOA,&GPIO_InitStrcture);
	//选择时基单元时钟
	TIM_ETRClockMode2Config(TIM2, TIM_ExtTRGPSC_OFF, TIM_ExtTRGPolarity_NonInverted,0x00);
	//配置时基单元
	TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStrcture;
	TIM_TimeBaseInitStrcture.TIM_ClockDivision=TIM_CKD_DIV1; //指定时钟分频。
	TIM_TimeBaseInitStrcture.TIM_CounterMode=TIM_CounterMode_Up;//指定计数器模式
	TIM_TimeBaseInitStrcture.TIM_Period=10-1;//自动重装寄存器ARR的值
	TIM_TimeBaseInitStrcture.TIM_Prescaler=1 - 1;//预分频器PSC的值
	TIM_TimeBaseInitStrcture.TIM_RepetitionCounter=0;//通用定时器没有重复计数器,填0
	
	TIM_TimeBaseInit(TIM2,&TIM_TimeBaseInitStrcture);
	 // 清除更新中断标志位
    TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	
	//配置输出中断
	TIM_ITConfig(TIM2,TIM_IT_Update,ENABLE);//使能更新中断。

	//配置NVIC
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//选择分组2
	NVIC_InitTypeDef NVIC_InitStructure;
	NVIC_InitStructure.NVIC_IRQChannel=TIM2_IRQn;//NVIC通道
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;//使能该中断通道
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=1;
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=1;
	NVIC_Init(&NVIC_InitStructure);
	//启动定时器
	TIM_Cmd(TIM2,ENABLE);
	
}

中断函数代码:

void TIM2_IRQHandler(){
	if (TIM_GetITStatus(TIM2,TIM_IT_Update) == SET)
	{
		Num++;
		TIM_ClearITPendingBit(TIM2,TIM_IT_Update);
	}
}

逻辑函数代码:

#include "stm32f10x.h"
#include "OLED.h"
#include "timer.h"
int Num;
int main(void){
	OLED_Init();
	Timer_Init();
	OLED_ShowString(1,6,"AOOFOO");//字符串
	OLED_ShowString(2,1,"Num:");
	while(1){
	OLED_ShowNum(2,5,Num,5);
	OLED_ShowNum(3,5,TIM_GetCounter(TIM2),5);
	}
}