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

本文介绍了STM32单片机中定时器TIM的使用,包括基本定时器TIM6/TIM7和通用定时器TIM2/TIM3/TIM4/TIM5的特性与配置。通过实例展示了如何配置TIM6进行毫秒级延时以及通过中断控制LED灯闪烁。此外,还探讨了如何利用TIM计算按键按下次数和间隔时间,涉及中断配置、计数器和预分频器的设置。
摘要由CSDN通过智能技术生成

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

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

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

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

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 基本定时器

基本定时器结构如下

image-20210817135033618

定时器实现计数功能必须有个时钟源(否则最基础的“心跳”哪来呢),基本定时器时钟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的内容会发现有四个结构体,我们用基本定时器,只需要最基础的这一个

image-20220506171742655

  • 成员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);
  1. 复制第二个参数里面的内容,也就是TIM_IT
  2. 按下ctrl+F查询模式,点击Find what,粘贴到里面
  3. 点击Find Next

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

image-20220506173321666

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

image-20220506173820646

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

#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的外部时钟输入。(具体介绍下一章再聊)

image-20220507104844451

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

image-20220507105705649

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

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

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

image-20220507105652116

分析完毕我们得出如下配置,在原来的初始化里加入就可以了,顺便把之前和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);
	}
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

这里煤球

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值