前导:本文的目的与,意在于面向应用的学习单片机,故不会涉及太多的原理知识,例如寄存器之类的。
主要目的在于面向应用的学习单片机,学会单片机的基础用法,开发板采取野火的指南者f103。
作者大二小白,写的不好的地方轻点喷,欢迎评论区交流
全部工程代码开源在Gitee仓库
文章目录
1 前言
上一章讲了SysTick系统定时器实现延时等的,这次我们来讲讲定时器TIM
区别 | SysTick | TIM |
---|---|---|
位置 | 属于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);
}
}