定时器内部中断接线图

只需要在STM32最小系统板上插入OLED屏幕就行了,因为定时器和中断系统的电路都在STM32内部,定时中断也是为我们程序设计提供服务的,所以只需要插一个OLED显示数据就行了。
写代码
初始化定时器大体思路
写关于定时器的代码,首先就需要写定时器的初始化函数,根据定时中断的整个框架结构进行初始化。

只需要把这里的每个模块都打通,就可以让定时器工作了。
第一步。RCC开启时钟。(RCC开启时钟后,让定时器默认的时钟源是APB1,后续可以通过配置改变成不同的时钟源。)
打开时钟后,定时器的基准时钟和整个外设的工作时钟都会同时打开了。
第二步,选择时基单元的时钟源。
对于定时中断,我们就选择内部时钟源。
第三步,配置时基单元。
配置预分频器、自动重装器、计数模式等等,这些参数用一个结构体就可以配置好。
第四步,配置输出中断控制。
允许更新中断输出到NVIC。
第五步,配置NVIC。
在NVIC中打开定时器中断的通道,并分配一个优先级。
第六步,运行控制。
整个模块配置完成后,我们还需要使能一下计数器,要不然计数器是不会运行的。
当定时器使能后,计数器就会开始计数了,当计数器更新时,触发中断,最后我们再写一个定时器中断函数,这样这个中断函数每隔一段时间就能自动执行一次了。
定时器库函数
我们找一下定时器TIM的库函数,在stm32f10x_tim.h的最后可以看到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,调用一下连接就是这样的:

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捕获通道的时钟。
参数是TIMx,选择要配置的定时器。TIxExternalCLKSource选择TIx具体的某个引脚。ICPolarity和ICFilter是输入的极性和滤波器。对于外部引脚的波形,一般都会有极性选择和滤波器,这样更灵活一些。

void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity,uint16_t ExtTRGFilter);选择ETR通过外部时钟模式1输入的时钟。
第一个参数ExtTRGPrescaler是外部触发预分频器。这里可以对ETR的外部时钟再提前做一个分频。
Polarity和Filter也是一样,极性和滤波器。

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

第一个参数ExtTRGPrescaler是外部触发预分频器。这里可以对ETR的外部时钟再提前做一个分频。
Polarity和Filter也是一样,极性和滤波器。
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);
}
}