HOME> 伊涅斯塔世界杯> 面向应用学习stm32(6)-TIM基本定时器-计数计时

面向应用学习stm32(6)-TIM基本定时器-计数计时

2025-10-23 15:56:18

前导:本文的目的与,意在于面向应用的学习单片机,故不会涉及太多的原理知识,例如寄存器之类的。

主要目的在于面向应用的学习单片机,学会单片机的基础用法,开发板采取野火的指南者f103。

作者大二小白,写的不好的地方轻点喷,欢迎评论区交流

全部工程代码开源在Gitee仓库

文章目录

1 前言2 定时器分类3 基本定时器4 代码配置4.1 思路分析4.2 代码

5 小实验5.1 定时器控制灯泡亮灭5.1.1思路分析5.1.2 代码

5.2 计算两个时间段内,按键次数5.2.1 思路分析

5.3 计算两次按键按下之间的间隔5.3.1思路分析5.3.2代码分析

1 前言

上一章讲了SysTick系统定时器实现延时等的,这次我们来讲讲定时器TIM

区别SysTickTIM位置属于ARM内核的外设属于芯片上的外设计数模式只能向下计数可以向上也可以向下(除了TIM6/TIM7)是否能捕获外界输入否能(除TIM6/7)是否有IO接口无TIM6/TIM7无,TIM2/3/4/5有四个IO,TIM1/8有八个IO是否能产生PWM波否能(除了TIM6/7)

看完这个表格,先不说看不看得懂,光从功能上,很明显TIM比SysTick多了很多,性能也会强大很多。尤其是有个别定时器具有非常多的功能。

为什么需要定时器,并且需要这么多定时器呢?

这是因为STM32的处理器是单核处理模式,也就是单线程处理,这这时如果没有一个专门的外设,那么在软件定时期间,程序就会阻塞住,直到定时结束,这段时间就无法处理其他的工作。

这里的解决思路其实就是之前的中断的概念,所以我们需要定时器,到一定的时间,产生中断通知主程序。从而完成最基本的定时工作。

这里有个问题是,SysTick不行吗?

答案是 SysTick的功能和性能不够强大,就像前面的对比图一样,还是内核级别的,滥用会产生非常多的负面影响。

2 定时器分类

定时器在STM32中分为三类

基本定时器:TIM6/7通用定时器:TIM2/3/4/5高级定时器:TIM1/8

3 基本定时器

基本定时器结构如下

定时器实现计数功能必须有个时钟源(否则最基础的“心跳”哪来呢),基本定时器时钟TIMxCLK只能来自内部时钟CK_INT该时钟经过APB1分频器分频提供,库函数中默认会把该时钟配置为72M。

该时钟经过 PSC 预分频器之后,即 CK_CNT,用来驱动计数器计数。(粉色线)

PSC 是一个16 位的预分频器

可以对定时器时钟进行 1~65536 之间的任何一个数进行分频。计算方法为: CK_CNT = TIMxCLK / (PSC+1)默认情况下,我们的TIMxCLK = 72M, 所以 CK_CNT = 72M/(PSC+1); 计数器 CNT 是一个 16 位的计数器

只能往上计数,最大计数值为 65535。当计数达到自动重装载计数器的时候产生一个更新中断事件,然后清零开始重新计数(类似SysTick) 自动重装载寄存器(Auto-reload Register)(ARR)是一个16位的寄存器

这里面装着计数器能计数的最大数值。当计数到这个值的时候,如果使能了中断的话,定时器就产生溢出中断。

由上述几点,我们可以总结出定时时间的计算:

​ TimeOut = ((PSC+ 1) * (ARR+ 1) ) / TIMxCLK 单位秒 ;(TIMxCLK 我们的板子默认72M)

4 代码配置

在这里我们进行定时器最基本的配置

4.1 思路分析

就像前面原理描述的一样

配置中断

开启定时器时钟

设置预分频系数

设置自动重装载计数值

初始化等的,最后使能。

首先我们去查看stm32f10x_tim.h的内容会发现有四个结构体,我们用基本定时器,只需要最基础的这一个

成员1:PSC预分频器成员2:计数模式(TIM6/7只有向上计数,这里不管)成员3:自动重装载计数值成员4:时钟分频因子(TIM6/7没有,不用管)成员5:重置计数器的值(TIM6/7没有,不用管)

所以其实我们结构体需要配置的只有两项,PSC和Period(也就是ARR)

基于前面的基础,这里我们直接开始分文件编程,省的每次主函数里写了一堆在开始分文件,太麻烦了。新建tim.c文件和tim.h文件并添加到User里的流程这里就不再说了,第一篇文章讲了。

4.2 代码

这里我采取传入psr和arr的形式,较为方便。

void TIM6_Init(int psc,int arr)

{

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

//这个之前说过,去rcc.h里找

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);

//只需要这两行,就完成配置,准备初始化

TIM_TimeBaseStructure.TIM_Prescaler = psc

TIM_TimeBaseStructure.TIM_Period = arr;

//写入初始化

TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);

}

基本的配置到这里就结束了。可是因为我们的TIM要写成中断触发的形式,也就是时间到的时候产生中断。

那自然就要配置中断优先组这部分之前也聊过了,所以直接copy函数,然后把中断源改成TIM6_IRQn(在stm32f10x.h)里面找

void TIM6_NVIC_Config(void)

{

NVIC_InitTypeDef NVIC_InitStructure;

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

NVIC_InitStructure.NVIC_IRQChannel = TIM6_IRQn ;

NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;

NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;

NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

NVIC_Init(&NVIC_InitStructure);

}

然后需要在定时器中,开启计数器的更新中断配置,顺便先清一波标志位,防止初始化产生影响。

头文件中有这两行函数。第一个函数的第二个参数,以及第二个函数的第二个参数

我们看起来有点懵,这里告诉大家一个快速的查询方法

void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);

void TIM_ClearFlag(TIM_TypeDef* TIMx, uint16_t TIM_FLAG);

复制第二个参数里面的内容,也就是TIM_IT按下ctrl+F查询模式,点击Find what,粘贴到里面点击Find Next

就跳转到了这里,告诉了我们都有哪些可用参数,前面我们提到,计数器达到的时候,产生的是更新中断事件,所以我们选择第一个参数 TIM_IT_Update

TIM_Flag也同理会找到这个东西,我们选择更新中断标志位

最终我们的配置代码改造如下

#include "tim.h"

void TIM6_Init(int psr,int arr)

{

//声明结构体

TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;

//配置中断组

TIM6_NVIC_Config();

//打开时钟

RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM6,ENABLE);

//设置psr和arr

TIM_TimeBaseStructure.TIM_Prescaler = psr;

TIM_TimeBaseStructure.TIM_Period = arr;

//使能TIM6的更新中断

TIM_ITConfig(TIM6, TIM_IT_Update, ENABLE);

//清除标志位

TIM_ClearFlag(TIM6, TIM_FLAG_Update);

//初始化TIM6

TIM_TimeBaseInit(TIM6, &TIM_TimeBaseStructure);

//使能TIM6

TIM_Cmd(TIM6,ENABLE);

}

void TIM6_NVIC_Config()

{

NVIC_InitTypeDef NVIC_InitStructure;

// 设置中断组为0

NVIC_PriorityGroupConfig(NVIC_PriorityGroup_0);

NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn ;

NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;

NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;

NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;

NVIC_Init(&NVIC_InitStructure);

}

5 小实验

5.1 定时器控制灯泡亮灭

5.1.1思路分析

利用定时器的定时机制,每当产生中断的时间抵达某个时刻例如1s切换颜色的话,那假设1ms进一次中断,那么当中断进入了1000次时,标志位取反,点亮不同颜色。

5.1.2 代码

上部分里,我们已经完成了定时器的初始化配置,现在我们准备开始写定时器控制LED

首先我们的主函数里写入一行这个

TIM6_Init(72-1,1000-1);//72 * 1000 / 72 000 000 = 0.001s = 1ms

代表配置1ms中断一次。

然后我们就可以去写中断服务函数了,函数名还是从startup.s里面复制,复制TIM6_IRQHandler

我的习惯是写在tim.c里,但是写在it.c里也可以

这里和之前的EXTI中断思路其实很像

int time = 0;

extern int flag;

void TIM6_IRQHandler()

{

//如果产生了中断

//这里有个问题就是:为什么进到中断了,还需要判断中断是否发生。

//答案是 TIM的中断和EXTI不一样,TIM的中断有很多类,而每一类产生时都会进入到TIMx_IRQHandler

//所以我们要判断具体产生中断的是哪个事件。

if(TIM_GetITStatus(TIM6,TIM_IT_Update) != RESET)

{

if(++time==1000)

{

flag = !flag;

time = 0;

}

//清除标志位

TIM_ClearITPendingBit(TIM6,TIM_FLAG_Update);

}

}

int flag = 0;

int main (void)

{

KEY_Init();//LED初始化

LED_Init();//按键初始化

TIM6_Init(72-1,1000-1); //72 * 1000 / 72 000 000 = 0.001s = 1ms

SysTick_Config(SystemCoreClock / 1000);

ILI9341_Init();//液晶初始化

LCD_SetColors(BLACK,WHITE);//设置白底黑字

LCD_SetFont(&Font8x16);//设置字体大小

ILI9341_Clear(0,0,LCD_X_LENGTH,LCD_Y_LENGTH);//清屏

ILI9341_GramScan ( 6 );//设置显示模式

LED_Color(LED_OFF);//关灯

while(1)

{

LED_Color(flag);

}

}

5.2 计算两个时间段内,按键次数

5.2.1 思路分析

这里的话,有计时的解法可以解决,但是那样就和之前的定时没什么区别了。

所以我决定提前说一下另外一种解法,外部时钟。这个是基本定时器没有的,我们需要使用通用定时器,并且要指定端口,由于我们的按键绑定的是PA0,所以查阅数据手册得到如下结果。

PA0接了TIM2_CH1_ETR的外部时钟输入。(具体介绍下一章再聊)

那么我们配置TIM2的外部时钟,在头文件发现有这个函数 翻译为 TIM外部时钟模式配置

看起来有点懵,那我们只好右键跳到它的c文件里,查看具体介绍

可以看到四个@param,就是四个参数具体的介绍了

第一个参数没什么好说的,选择TIM2第二个参数是PSC分频,我们可以不要,选第一个取值,不开启分频。第三个参数是触发极性,由于我们是按键,所以两个参数都可以选。第四个参数是滤波器,我们也不要,那就选0x00

分析完毕我们得出如下配置,在原来的初始化里加入就可以了,顺便把之前和TIM6相关的全部改成TIM2

TIM_ETRClockMode1Config(TIM2,TIM_ExtTRGPSC_OFF,TIM_ExtTRGPolarity_Inverted,0x00);

到这里,PA0的外部时钟触发配置就完成了。每次我们按键按下松手,都会相当于触发一次下降沿,每次下降沿将会被计数器记录下来。获取计数器的值采用 TIM_GetCounter(TIM2);

然后获取完后记得要清零计数值,否则无法做到每次都是新记录的计数值TIM_SetCounter(TIM2,0);

然后我们回到主函数,添加这三个函数

void Before_Get_Count()

{

TIM_Cmd(TIM2,ENABLE);

LED_Color(LED_RED);

Delay_ms(3000);

}

void Get_Count()

{

count = TIM_GetCounter(TIM2);

TIM_SetCounter(TIM2,0);

}

void After_Get_Count()

{

sprintf(disp,"Count:%2d",count);

ILI9341_DispStringLine_EN(LINE(1),disp);

LED_Color(LED_OFF);

TIM_Cmd(TIM2,DISABLE);

Delay_ms(1000);

}

Before_Get_Count 红灯亮起,计数器使能,就可以开始计数。

Get_Count 延时3s后,记录这段时间里产生的下降沿个数,也就是按键次数,然后清零计数值

After_Get_Count 显示出按键次数,关灯,延时1s后重新开始记录。

while(1)

{

Before_Get_Count();

Get_Count();

After_Get_Count();

}

5.3 计算两次按键按下之间的间隔

5.3.1思路分析

当按键按下的时候,使能定时器,当按键再次按下的时候,失能定时器。

利用一个变量记录下,定时器使能时进入中断的时间就好了。

5.3.2代码分析

初始化的时候先不使能定时器

void TIM6_Init(int psr,int arr)

{

//...省略...

TIM_Cmd(TIM6,DISABLE);

}

按键按下,使能定时器,取反,再次按下的时候就被失能了。

while (1)

{

if(KEY_Scan()=='1')

{

TIM_Cmd(TIM6,flag);

flag = !flag;

}

sprintf(disp,"Count:%d",time);

ILI9341_DispStringLine_EN(LINE(1),disp);

}

extern int time;

void TIM6_IRQHandler()

{

//如果产生了中断

if(TIM_GetITStatus(TIM6,TIM_IT_Update) != RESET)

{

time++;

//清除标志位

TIM_ClearITPendingBit(TIM6,TIM_FLAG_Update);

}

}

2025高收益网上理财产品对比:五大热门选择助你稳健增值

微众银行怎么样?

最新发表 newmodule
友情链接 newmodule