文章目录
前言
提示:本文主要用作在学习江科大自化协STM32入门教程后做的归纳总结笔记,旨在学习记录,如有侵权请联系作者
本文主要探讨基于TIM定时中断实现定时器定时中断和定时器外部时钟的功能。
一、定时器定时中断应用案例
1.1 应用案例简介
本案例实现了一个定时器定时中断的功能。定时器使用内部时钟定了一个1秒的时间,每隔1秒申请一次中断,然后在中断函数里执行Num++,最后在OLED上显示Num。
1.2 电路接线图
1.3 应用案例代码
定时器头文件Timer.h:
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
#endif
定时器实现文件Timer.c:
#include "stm32f10x.h" // Device header
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_ClearFlag(TIM2, TIM_FLAG_Update);
TIM_ITConfig(TIM2, TIM_IT_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)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
*/
主程序文件main.c:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main(void)
{
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);
}
}
1.4 应用案例分析
首先,初始化定时器。那怎么初始化定时器呢?我们可以看一下下面这个定时中断的整体框架图,我们只需要把这里的每个模块都打通就可以让定时器工作。
大体上的步骤如下:
第一步,RCC开启时钟。打开时钟后,定时器的基准时钟和整个外设的工作时钟就都会同时打开。
第二步,选择时基单元的时钟源。对于定时中断,我们就选择内部时钟源。
第三步,配置时基单元。包括这里的预分频器、自动重装器以及计数模式等等。
第四步,配置输出中断控制。允许更新中断输出到NVIC。
第五步,配置NVIC。在NVIC中打开定时器中断的通道,并分配一个优先级。
第六步,运行控制。整个模块配置完成后,我们还需要使能一下计数器,要不然计数器是不会运行的。当定时器使能后,计数器就会开始计数了,当计数器更新时,触发中断。
最后我们再写一个定时器的中断函数,这样,这个中断函数每隔一段时间就能自动执行一次了。
以上这些就是初始化定时器的大体思路了。
1.4.1 初始化定时器
先看一下定时器的库函数有哪些,找到stm32f10x_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);
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
void TIM_InternalClockConfig(TIM_TypeDef* TIMx);
void TIM_ITRxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_InputTriggerSource);
void TIM_TIxExternalClockConfig(TIM_TypeDef* TIMx, uint16_t TIM_TIxExternalCLKSource, uint16_t TIM_ICPolarity, uint16_t ICFilter);
void TIM_ETRClockMode1Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
void TIM_ETRClockMode2Config(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
void TIM_ETRConfig(TIM_TypeDef* TIMx, uint16_t TIM_ExtTRGPrescaler, uint16_t TIM_ExtTRGPolarity, uint16_t ExtTRGFilter);
首先是第一个,TIM_DeInit,恢复缺省配置。
第二个,TIM_TimeBaseInit,时基单元初始化。这个函数比较重要,它就是用来配置时基单元的。第一个参数TIMx,选择某个定时器。第二个是结构体TIM_TimeBaseInitTypeDef,里面包含了配置时基单元的一些参数。
接着看第三个函数TIM_TimeBaseStructInit,这个函数可以把结构体变量赋一个默认值。
然后是第四个函数TIM_Cmd,这个是用来使能计数器的。对应的就是定时中断的整体框架图中的运行控制。第一个参数TIMx,选择定时器。第二个NewState,新的状态,也就是选择使能还是失能。使能,计数器就可以运行,失能,计数器就不运行。
然后第五个是函数TIM_ITConfig,这个是用来使能中断输出信号的。对应的就是定时中断的整体框架图中的中断输出控制。第一个参数TIMx,选择定时器。第二个参数TIM_IT,选择要配置哪个中断输出。第三个参数NewState,新的状态,使能还是失能。
接下来看一下TIM_InternalClockConfig、TIM_ITRxExternalClockConfig、TIM_TIxExternalClockConfig、TIM_ETRClockMode1Config、TIM_ETRClockMode2Config以及TIM_ETRConfig这六个函数,这六个函数对应的就是定时中断的整体框架图中的时基单元的时钟选择部分,可以选择RCC内部时钟、ETR外部时钟、ITRx其他定时器、TIx捕获通道这些。
- TIM_InternalClockConfig函数,选择内部时钟。
- TIM_ITRxExternalClockConfig函数,选择ITRx其他定时器的时钟。第一个参数TIMx,选择要配置的定时器。第二个参数TIM_InputTriggerSource,选择要接入哪个的其他定时器。
- TIM_TIxExternalClockConfig函数,选择TIx捕获通道的时钟。第一个参数TIMx,选择要配置的定时器。第二个参数TIM_TIxExternalCLKSource,选择TIx具体的某个引脚。接下来还有TIM_ICPolarity和ICFilter,输入的极性和滤波器。对于外部引脚的波形,一般都会有极性选择和滤波器,这样更灵活一些。
- TIM_ETRClockMode1Config函数,选择ETR通过外部时钟模式1输入的时钟。参数TIM_ExtTRGPrescaler,外部触发预分频器,这里可以对ETR的外部时钟再提前做一个分频。接下来参数TIM_ExtTRGPolarity和ExtTRGFilter,也是一样,极性和滤波器。
- TIM_ETRClockMode2Config函数,选择ETR通过外部时钟模式2输入的时钟。它的参数和TIM_ETRClockMode1Config函数一摸一样,这里就不再介绍。对于ETR输入的外部时钟而言,这两个函数是等效的,它们的参数也是一样的,如果不需要触发输入的功能,那两个函数可以互换。
- TIM_ETRConfig函数,这个不是用来选择时钟的,是用来单独配置ETR引脚的预分频器、极性、滤波器这些参数的。
到这里,关键部分的函数就讲完了。时钟源选择用TIM_InternalClockConfig、TIM_ITRxExternalClockConfig、TIM_TIxExternalClockConfig、TIM_ETRClockMode1Config、TIM_ETRClockMode2Config以及TIM_ETRConfig这六个函数。时基单元,用TIM_TimeBaseInit函数。中断输出控制,用TIM_ITConfig函数。运行控制,用TIM_Cmd函数。这样初始化就基本ok了。
接下来,我们再看几个函数。因为在TIM_TimeBaseInit函数里在初始化结构体里有很多关键的参数,比如自动重装值和预分频值等等,这些参数可能会在初始化之后还需要更改,如果为了改某个参数还要再调一次初始化函数,这样做太麻烦了。所以这里有一些单独的函数可以方便地更改这些关键参数。
比如,TIM_PrescalerConfig函数,单独写预分频值。参数Prescaler就是要写入的预分频值。参数TIM_PSCReloadMode,写入的模式。预分频器有个缓冲器,写入的值是在更新事件发生后才有效的。所以这里有个写入的模式,可以选择是听从安排,再更新事件生效,或者是在写入后,手动产生一个更新事件,让这个值立即生效。不过这些都是细节问题,影响不大,你只需要知道这个是写预分频值的函数就行了。
然后下一个,TIM_CounterModeConfig函数,用来改变计数器的计数模式。参数TIM_CounterMode,选择新的计数器模式。
然后再往下,TIM_ARRPreloadConfig函数,自动重装器预装功能配置。
然后再往下,TIM_SetCounter函数,给计数器写入一个值。如果你想要手动给一个计数值,就可以用这个函数。
然后再往下,TIM_SetAutoreload函数,给自动重装器写一个值。
再往下,TIM_GetCounter函数,获取当前计数器的值。
再往下,TIM_GetPrescaler函数,获取当前预分频器的值。
最后,TIM_GetFlagStatus、TIM_ClearFlag、TIM_GetITStatus以及TIM_ClearITPendingBit函数,这些函数就是用来获取标志位和清除标志位的,
好,现在开始写代码,在这里我准备初始化的是TIM2,也就是通用定时器。
1. RCC开启时钟
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
这里注意,要使用APB1的开启时钟函数,因为TIM2是APB1总线的外设。
2. 选择时基单元时钟
TIM_InternalClockConfig(TIM2);
在这里我们选择内部时钟源,这样TIM2的时基单元就由内部时钟来驱动了。
3. 配置时基单元
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_ClockDivision :指定时钟分频。在这里选择TIM_CKD_DIV1,1分频。
- TIM_CounterMode :计数模式。在这里选择向上计数模式TIM_CounterMode_Up。
- TIM_Period :ARR自动重装器的值。
- TIM_Prescaler :PSC预分频器的值。
- TIM_RepetitionCounter :重复计数器的值。这个是高级定时器才有的,我们不需要用,直接给0即可。
在这里定时时间为1s,计数器溢出频率:CK_CNT_OV = CK_CNT / (ARR + 1) = CK_PSC / (PSC + 1) / (ARR + 1)
4. 配置输出中断控制
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
注意:在是能TIM2之前手动清除一下更新中断标志位,这样可以避免刚初始化完就进入中断的问题。
5. 配置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);
6. 运行控制(使能计时器)
TIM_Cmd(TIM2, ENABLE);
1.4.2 编写定时器中断函数
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
Num ++;
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
定时中断完整工程:TIM定时中断之定时器定时中断应用案例
二、定时器外部时钟应用案例
2.1 应用案例简介
本案例实现了一个定时器外部时钟的功能。该程序使用了外部时钟来驱动定时器,我们可以在定时器指定的外部引脚上输入一个方波信号来提供定时器计数的时钟,在这里使用的是对射式红外传感器来手动模拟一个外部时钟。当我们用挡光片依次遮挡、移开、遮挡、移开这样的操作来提供一个方波信号时,可以看到OLED上这个CNT就是定时器中计数器的值。每遮挡移开一次,计数器加1,然后计数器记到9后自动清零,同时申请中断,执行Num++。(使用定时器的外部时钟,可以提供一个更加精准的时钟来计时,或者也可以把外部时钟当作一个计数器用来统计引脚上电平翻转的次数,毕竟定时器本质上就是一个计数器对吧)
2.2 电路接线图
2.3 应用案例代码
定时器头文件Timer.h:
#ifndef __TIMER_H
#define __TIMER_H
void 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, 0x0F);
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_FLAG_Update);
TIM_ITConfig(TIM2, TIM_IT_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);
}
uint16_t Timer_GetCounter(void)
{
return TIM_GetCounter(TIM2);
}
/*
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
*/
主程序文件main.c:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "Timer.h"
uint16_t Num;
int main(void)
{
OLED_Init();
Timer_Init();
OLED_ShowString(1, 1, "Num:");
OLED_ShowString(2, 1, "CNT:");
while (1)
{
OLED_ShowNum(1, 5, Num, 5);
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);
}
}
2.4 应用案例分析
整体思路基本跟定时中断的案例一致,区别只是时钟源的选择以及多了一个GPIO的初始化,这里就不再累述了。
定时器外部时钟完整工程:TIM定时中断之定时器外部时钟应用案例