🐱作者:一只大喵咪1201
🐱专栏:《STM32学习》
🔥格言:你只管努力,剩下的交给时间!
在上篇文章中学习了通用定时器的基本原理以后,接下来本喵就介绍一下定时器的使用。
定时器中断实验就是利用计数器CNT计数到一定时间后产生更新中断。
本喵选择TIMER3来实现定时中断,并且用LED1来体现实验效果。

查看开发板原理图,LED1和PE5口相连,且采用共阳极接法。
在这个实验中,本喵要实现每隔500ms让LED1的状态反转一次,当然不是使用延时函数实现的,而是使用定时器的更新中断实现的。
这里选择内部时钟(CK_INT)作为时钟源。
TIM3是挂载在APB1总线上的,所以它内部时钟的来源是APB1时钟。APB1时钟最大是36MHZ的,本喵使用的开发板这里的时钟就是36MHZ。

接下来就是计算内部时钟(CK_INT)了,这里有一个特殊点
除非APB1的分频系数是1,否则通用定时器的时钟等于APB1时钟的2倍。
我们这里将APB1时钟2分频,所以得到的内部时钟(CK_INT)是72MHZ的。
再就是计算计数器CNT的驱动时钟(CK_CNT)了。
这里CK_PSC的频率就是内部时钟(CK_INT)的频率,所以也是72MHZ,将CK_PSC经过PSC寄存器分频后得到的就是CK_CNT时钟频率。
它的流程如下面的框图

其中N是PSC预分频寄存器中的值。
计算具体的计时时间按照下面这个公式:
- Tout(溢出时间) = (ARR+1)*(PSC+1)/Tclk
我们想要实现500ms的定时,
- 将预分频系数PSC设置为7199
- 代入公式中就可以求出ARR中的值是4999
该寄存器是用来计数的,不需要进配置,在将定时器打开以后便自动开始计数,直到与ARR寄存器中的值相匹配产生溢出事件。
在上面我们按照定时时间已经设定了它的值,所以将7199写入即可。
通过公式计算,我们算出ARR中的值是4999,将其写入即可。

这里我们将计算模式设置为向上计算模式,也就将DIR位设为0。

同时使能CEN位,让CK_CNT时钟开始工作,驱动计数器CNT计数。

将UIE位置1,允许产生更新中断。
寄存器的配置同样是通过官方提供的标准库实现的,让本喵给大家介绍一下需要哪些库函数。
定时器初始化函数:
void TIM_TimeBaseInit(TIM_TypeDef* TIMx,TIM_TimeBaseInitTypeDef* TIM_TimeBaseInitStruct);
定时器使能函数:
void TIM_Cmd(TIM_TypeDef* TIMx, FunctionalState NewState);
定时器中断使能函数:
void TIM_ITConfig(TIM_TypeDef* TIMx, uint16_t TIM_IT, FunctionalState NewState);
标志位获取和清除
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);
使用这些库函数,按照下面的顺序就可以达到实验目的:
看本喵提供的参考代码:
main.c中的代码
#include "timer3.h"
#include "led.h"
int main()
{
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//中断优先级分组是第二组
LED_Init();//LED灯初始化,状态是灭
TIMER3_Updata_Init(4999,7199);//定时器定时500ms
while(1);//等待中断发生
}
timer3.h中的代码
#ifndef __TIMER3_H
#define __TIMER3_H
#include "sys.h"
void TIMER3_Updata_Init(u16 arr,u16 psc);
#endif
timer3.c中的代码
#include "timer3.h"
#include "led.h"
void TIMER3_Updata_Init(u16 arr,u16 psc)
{
TIM_TimeBaseInitTypeDef TIM_TimeBaseStruct;//创建TIM3结构体
NVIC_InitTypeDef NVIC_InitStruct;//创建中断管理结构体
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//使能TIM3
TIM_TimeBaseStruct.TIM_CounterMode=TIM_CounterMode_Up;//向上计数模式
TIM_TimeBaseStruct.TIM_Period=arr;//重装载寄存器中的值
TIM_TimeBaseStruct.TIM_Prescaler=psc;//预分频寄存器的值
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseStruct);//初始化
NVIC_InitStruct.NVIC_IRQChannel=TIM3_IRQn;//TIM3中断通道
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;//TIM3中断使能
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级2
NVIC_InitStruct.NVIC_IRQChannelSubPriority=2;//响应优先级2
NVIC_Init(&NVIC_InitStruct);//初始化优先级管理
TIM_ITConfig(TIM3,TIM_IT_Update,ENABLE);//使能更新中断
TIM_Cmd(TIM3,ENABLE);//使能TIM3
}
//中断服务函数
void TIM3_IRQHandler(void)
{
if(TIM_GetITStatus(TIM3,TIM_IT_Update)!=RESET)//确保是更新中断
{
LED1=!LED1;//反转LED1的状态
}
TIM_ClearITPendingBit(TIM3,TIM_IT_Update);//清除更新中断标志位
}
代码中都有相应的注释,可以借鉴。
至于LED1灯的初始化就没必要展示了。

PWM输出使用定时器的4个输出通道中的一个,通过设置CRR中的值改变PWM波的占空比来实现。通过设置ARR中的值确定PWM波的频率。
本喵使用LED0,也就是开发板上的红灯来体现实验效果。

从开发板原理图中可以看到,LED0接在PB5口上,并且是共阳极接法。

从开发手中中可以看到,TIM3的通道2正好部分重映像在了PB5,所以在使用的时候要进行一下重映像的设定。
在这个实验中,要让LED0红色灯的亮度由弱变强再变弱,如此反复。
同样还是选择内部时钟,这里的内部时钟(CK_INT)不进行分频,所以给PSC寄存器中的值就是0。所以驱动计数器CNT的时钟CK_CNT的频率是72MHZ。
时钟已经选好了,是72MHZ,这里我们设置重装载寄存器ARR的值是899,此时PWM的频率就是72000000/(899+1) = 80KHZ。

CRR的值单独修改,需要不停地变化,来改变PWM波的占空比。
这里没有中断产生,所以一定不能开启CRR预装载。
将CCMR2寄存器的OC3M按照下面的描述设置为111,设置成PWM模式2。

再将使能寄存器CCER的位9

按照

这个规则设置为0,也就是输出为高电平有效。
以上便是一些重要寄存器的设置。

这是它工作的时序图
具体寄存器的配置通过官方提供的库函数来实现。
需要用到的库函数:
设置比较值函数:
void TIM_SetCompareX(TIM_TypeDef* TIMx, uint16_t Compare2);
使能输出比较预装载
void TIM_OC2PreloadConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPreload);
输出初始化函数:
void TIM_OCxInit(TIM_TypeDef* TIMx, TIM_OCInitTypeDef* TIM_OCInitStruct);
这些是比较重要的库函数,按照下面的步骤
展示下本喵的代码:
pwm的初始化代码:
void TIMER3_PWM_Init(u16 arr,u16 psc)
{
GPIO_InitTypeDef GPIO_InitStruct;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
TIM_OCInitTypeDef TIM_OCInitStruct;
//时钟使能,3个外设
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3,ENABLE);//使能TIM3的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO|RCC_APB2Periph_GPIOB,ENABLE);//使能GPIOB和AFIO时钟
GPIO_PinRemapConfig(GPIO_PartialRemap_TIM3,ENABLE);//部分重映像使能
//复用初始化
GPIO_InitStruct.GPIO_Mode=GPIO_Mode_AF_PP;//推挽复用输出模式
GPIO_InitStruct.GPIO_Pin=GPIO_Pin_5;//引脚5
GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz;//输出速度50MHZ
GPIO_Init(GPIOB,&GPIO_InitStruct);//PB5初始化为复用推挽输出
//设置PWM的频率
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//向上计数模式
TIM_TimeBaseInitStruct.TIM_Period=arr;//重装载寄存器中的值
TIM_TimeBaseInitStruct.TIM_Prescaler=psc;//预分频系数
TIM_TimeBaseInitStruct.TIM_ClockDivision = 0; //设置时钟分割:TDTS = Tck_tim
TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStruct);//初始化PWM波的频率
//设置PWM工作模式
TIM_OCInitStruct.TIM_OCMode=TIM_OCMode_PWM2;//PWM模式2
TIM_OCInitStruct.TIM_OCPolarity=TIM_OCPolarity_High;//输出极性设置为高
TIM_OCInitStruct.TIM_OutputState=TIM_OutputState_Enable;//比较输出使能
TIM_OC2Init(TIM3,&TIM_OCInitStruct);//初始化PWM波输出模式
//CCR预装载使能
TIM_OC2PreloadConfig(TIM3,TIM_OCPreload_Enable);//使能CCR预装载
TIM_Cmd(TIM3,ENABLE);//打开定时器3
}
主程序中代码:
int main()
{
u16 pwm_val = 0;//CCR中的值
u8 flag = 1;//增减标志位
delay_init();//延时初始化
TIMER3_PWM_Init(899,0);//PWM的频率就是72000000/(899+1) = 80KHZ
while(1)
{
delay_ms(10);//延时10ms,否则看不到灯的效果
if(flag)
pwm_val++;//改变占空比
else
pwm_val--;//改变占空比
if(pwm_val > 300)
flag=0;
if(pwm_val==0)
flag = 1;
TIM_SetCompare2(TIM3,pwm_val);//将值给CCR寄存器
}
}
注释中有详细解释,可以参考。

由于是GIF,效果不是很明显,红色灯的亮度在由弱到强反复变化。
在输入捕获模式下,当检测到ICx信号上相应的边沿后,计数器的当前值被锁存到捕获/比较寄存
器(TIMx_CCRx)中,根据这个值我们可以计算出输入信号的频率。
在次实验中,本喵将WK_UP按键按下的时长通过串口发送到电脑屏幕上。

根据开发板原理图,WK_UP按键和PA0相连,另一端与高电平相连。

根据STM32芯片的引脚分布,可以看到,TIM5的CH1和PA0的复用的,所以我们这里使用TIM5的CH1通道作为输入。
同样选择内部时钟CK_INT,这里让计数器CNT按照1MHZ的频率来计数,所以预分频计数器的值是71。此时每隔1us就计数一次。
按照输入捕获的工作框图来配置相关寄存器。

配置CCMR1寄存器

这里为了防止按键发生抖动采用以CK_INT的频率采样8次来滤波,也就是将IC1F[3:0]设置为0011。
将CCEG寄存器的CC1P位

配置为0,也就是上升沿捕获。
配置CCMR1寄存器的CC1S位

配置为01,也就是映射在TI1上。
同样是CCMR1寄存器

将IC1PSC设置为00,也就是一捕获到信号的上升沿就触发一次捕获
这里本喵就不列举了,和前面的中断配置是类似的。
同样,寄存器的配置仍然是通过官方提供的库函数来实现的。
需要使用到的重要库函数:
输入捕获通道初始化函数:
void TIM_ICInit(TIM_TypeDef* TIMx, TIM_ICInitTypeDef* TIM_ICInitStruct);
通道极性设置独立函数
void TIM_OCxPolarityConfig(TIM_TypeDef* TIMx, uint16_t TIM_OCPolarity);
获取通道捕获值
uint32_t TIM_GetCapture1(TIM_TypeDef* TIMx);
这些是比较重要的函数,按照下面的流程来写具体的代码:
展示下本喵的代码:
timer5.h中的代码
void TIMER_Incapture_Init(u16 arr, u16 psc)
{
GPIO_InitTypeDef GPIO_InitStruct;
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStruct;
NVIC_InitTypeDef NVIC_InitStruct;
TIM_ICInitTypeDef TIM_ICInitStruct;
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5,ENABLE);//使能TIM5时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA,ENABLE);//使能GPIOA时钟
//PA0初始化
GPIO_InitStruct.GPIO_Pin = GPIO_Pin_0; //PA0 清除之前设置
GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IPD; //PA0 输入
GPIO_Init(GPIOA, &GPIO_InitStruct);//PA0下拉输入
GPIO_ResetBits(GPIOA,GPIO_Pin_0); //将PA0下拉到低电平
//初始化计数部分
TIM_TimeBaseInitStruct.TIM_ClockDivision=TIM_CKD_DIV1;//滤波频率和内部时钟相同
TIM_TimeBaseInitStruct.TIM_CounterMode=TIM_CounterMode_Up;//向上计数模式
TIM_TimeBaseInitStruct.TIM_Period=arr;//将重装载值设为最大
TIM_TimeBaseInitStruct.TIM_Prescaler=psc;//预分频系数
TIM_TimeBaseInit(TIM5,&TIM_TimeBaseInitStruct);//初始化计数部分
//输入捕获初始化
TIM_ICInitStruct.TIM_Channel=TIM_Channel_1;//输入通道CH1
TIM_ICInitStruct.TIM_ICFilter=0x3;//8次采样
TIM_ICInitStruct.TIM_ICPolarity=TIM_ICPolarity_Rising;//上升沿捕获
TIM_ICInitStruct.TIM_ICPrescaler=TIM_ICPSC_DIV1;//不分频
TIM_ICInitStruct.TIM_ICSelection=TIM_ICSelection_DirectTI;//映射到TI1上
TIM_ICInit(TIM5,&TIM_ICInitStruct);
//更新中断和捕获中断初始化
NVIC_InitStruct.NVIC_IRQChannel=TIM5_IRQn;//中断通道是TIM5
NVIC_InitStruct.NVIC_IRQChannelCmd=ENABLE;//使能通道
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority=2;//抢占优先级0
NVIC_InitStruct.NVIC_IRQChannelSubPriority=0;//响应优先级2
NVIC_Init(&NVIC_InitStruct);
TIM_ITConfig(TIM5,TIM_IT_Update,ENABLE);//使能更新中断
TIM_ITConfig(TIM5,TIM_IT_CC1,ENABLE);//使能通道1输入捕获中断
TIM_Cmd(TIM5,ENABLE ); //使能定时器5
}
u8 TIM5CH1_CAPTURE_STA=0;//输入捕获状态
u16 TIM5CH1_CAPTURE_VAL;//输入捕获值
//定时器5中断服务程序
void TIM5_IRQHandler(void)
{
//没有完成采集进入中断
if((TIM5CH1_CAPTURE_STA&0X80)==0)
{
//发生的是更新中断
if (TIM_GetITStatus(TIM5, TIM_IT_Update) != RESET)
{
//已经捕获到高电平,需要累计溢出
if(TIM5CH1_CAPTURE_STA&0X40)
{
//累计达到最大值,强制采集完成
if((TIM5CH1_CAPTURE_STA&0X3F)==0X3F)
{
TIM5CH1_CAPTURE_STA|=0X80;//采集完成标志
TIM5CH1_CAPTURE_VAL=0XFFFF;//强制最大值
}
//没有累计到最大
else
TIM5CH1_CAPTURE_STA++;//溢出加1
}
}
//发生捕获中断
if(TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)
{
delay_ms(20);//延时消抖
if(TIM_GetITStatus(TIM5, TIM_IT_CC1) != RESET)
{
//已经捕获到上升沿,此时捕获是下降沿
if(TIM5CH1_CAPTURE_STA&0X40)
{
TIM5CH1_CAPTURE_STA|=0X80;//捕获完成
TIM5CH1_CAPTURE_VAL=TIM_GetCapture1(TIM5);//获取当前计数值
TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Rising); //CC1P=0 设置为上升沿捕获
}
//捕获到上升沿
else
{
//开始计时,所有参数从0开始
TIM5CH1_CAPTURE_VAL=0;
TIM5CH1_CAPTURE_STA=0;
TIM5CH1_CAPTURE_STA|=0X40;//上升沿标志
TIM_SetCounter(TIM5,0);//让计数器CNT从0开始计数
TIM_OC1PolarityConfig(TIM5,TIM_ICPolarity_Falling); //CC1P=1 设置为下降沿捕获
}
}
}
}
TIM_ClearITPendingBit(TIM5, TIM_IT_CC1|TIM_IT_Update);//清除中断标志位
}
包括定时器5的初始化,以及中断服务函数。
主函数是接着PWM输出写的
extern u8 TIM5CH1_CAPTURE_STA;//输入捕获状态
extern u16 TIM5CH1_CAPTURE_VAL;//输入捕获值
int main()
{
u16 pwm_val = 0;//CCR中的值
u8 flag = 1;//增减标志位
u32 temp = 0;//计时时间
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
delay_init();//延时初始化
uart_init(115200);
TIMER3_PWM_Init(899,0);//PWM的频率就是72000000/(899+1) = 80KHZ
TIMER_Incapture_Init(0xffff,71);//计数一次用时1us,最大计数65535
while(1)
{
delay_ms(10);//延时10ms,否则看不到灯的效果
if(flag)
pwm_val++;//改变占空比
else
pwm_val--;//改变占空比
if(pwm_val > 300)
flag=0;
if(pwm_val==0)
flag = 1;
TIM_SetCompare2(TIM3,pwm_val);//将值给CCR寄存器
if((TIM5CH1_CAPTURE_STA&0x80)!=RESET)
{
temp = (TIM5CH1_CAPTURE_STA&0x3f);//统计溢出次数
temp*=65536;//计算溢出时长
temp+=TIM5CH1_CAPTURE_VAL;//加当前计时时长
printf("High Level time:%d us\r\n",temp);//打印总的高电平时间
TIM5CH1_CAPTURE_STA=0;//进行下一次计时
}
}
}
注意:

使用到这样一个标志位。
当WK_UP处于高电平且超过65536us时,次数计数器CNT就会发生溢出事件,然后计算从0开始计时,如果不将这次溢出记录下来,那么计时时长就会丢失至少65536us,所以我们用这样一个变量的低6位存放溢出的次数。
第6位是记录捕获到高电平,当高电平被捕获到以后,就开始计时。
第7位表示捕获完成,此时需要将计时时长发送出去。

在理解了通用定时器的工作原理后,它的应用还是非常容易实现的,只需要按照步骤,使用合适的库函数即可。希望对各位有所帮助。