通过上一章节我们了解了定时器,接下来我们通过定时器实现精准的时间延时。
配置流程
一般使用定时器功能,都需要有以下几个步骤。
- 开启时钟(定时器时钟)
- 配置定时器参数
- 配置中断优先级
- 使能中断事件和定时器
- 编写中断服务函数
GD32VW553HQM6单片机一共有6个定时器,包括高级定时器、通用定时器和基本定时器。不同类型的定时器功能不同。这一章就以LED灯1s闪烁为实验内容进行讲解。实现定时1秒钟进入中断,只需要实现基本的定时功能,GD32的6个定时器都可以使用,这里就以基本定时器5为例进行介绍。
开启时钟
先来看一下定时器的时钟来源,在用户手册的第93页,如图所示。
从图可以看到TIMER 时钟由 AHB
时钟分频获得,它的时钟可以等于 CK_APBx
、CK_APBx
的两倍或
CK_APBx
的四倍。经过时钟配置寄存器(RCU_CFG1
)决定是APB
频率的2倍还是4倍,但这个频率不能超过AHB(max = 160MHZ)
。
这里使用TIMER5,就要先使能TIMER5的时钟,又因为TIMER5时钟来源于AHB,AHB的时钟在system_gd32vw55x.c
中定义,从图13-1-2可以看到AHB的时钟等于系统时钟,系统时钟定义如图所示。
从图上可以看到系统时钟等于__SYSTEM_CLOCK_160M_PLLDIG_HXTAL
,跳转到这个宏定义可以得知,这个宏定义的值就是(uint32_t)(160000000)
,可见系统时钟为160MHZ
。
定时器还可以通过APBx
进行分频,如图所示。
APB1
的时钟等于AHB的时钟2分频,AHB的时钟等于系统时钟SYSCLK
。回过来看APB1的时钟为160MHZ
的2分频等于80MHZ
。要设置定时器的时钟为160MHZ
,从时钟树上可以看到还需要进行最少2倍频处理,在代码里面可以配置为2倍频。
首先编写TIMER5时钟的宏定义:
#define BSP_TIMER_RCU RCU_TIMER5 // 定时器时钟
开启定时器时钟:
/* 开启时钟 */
rcu_periph_clock_enable(BSP_TIMER_RCU); // 开启定时器时钟
2
然后配置定时器时钟。在gd32vw55x_rcu.h
中寻找相关函数
void rcu_timer_clock_prescaler_config(uint32_t timer_clock_prescaler);
这个函数是配置定时器时钟。有一个参数是要配置时钟的倍频系数。可选项如图所示。
我们之前说定时器的时钟是由CK_APBx提供,定时器5的时钟是CK_APB1,其默认最大80MHz,如果要定时器配置为160MHZ,要进行2或者4倍频,顾选择RCU_TIMER_PSC_MUL2
。
/* CK_TIMERx = 2 x CK_APB1 = 2x80M = 160MHZ */
rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL2);// 配置定时器时钟
2
配置定时器参数
开启时钟之后,需要配置一些定时器的参数,比如定时时间,计数模式等。
关于基本定时器的结构框图如图所示。
宏定义TIMER5:
#define BSP_TIMER TIMER5 // 定时器
第一步先要复位定时器外设
timer_deinit(BSP_TIMER); // 复位定时器
然后在gd32vw55x_time.h
中有
void timer_init(uint32_t timer_periph, timer_parameter_struct* initpara);
这个函数是定时器初始化,并且配置参数。有两个参数,第一个参数timer_periph
就是定时器外设,第二个参数initpara
就是一个定时器参数结构体,关于结构体的定义如图所示。
要初始化定时器就要先配置这个结构体,首先定义这个结构体
timer_parameter_struct timer_initpara; // 定义定时器结构体
然后配置结构体的参数
prescaler
:这个参数是时钟的预分频值,是16位的,取值范围为1-65535。TIMER_CK经过预分频之后得到PSC_CLK。每经过一个PSC_CLK都会产生一个计数周期,prescaler参数将决定一个计数周期的时间。设预分频值为pre,则计数器时钟频率PSC_CLK=TIMER_CK / (pre + 1)
。alignedmode
:对齐模式暂没用到。counterdirection
:计数模式,基本定时器只有向上计数模式,所以配置为TIMER_COUNTER_UP
。period
:周期值,是一个16位的计数器,最大值为65535,当计数器达到设置的周期数值(自动重装载寄存器)时数值清零,配合计数器时钟频率可以计算中断时间。clockdivision
:时钟分频,在输入捕获的时候使用,定时器时钟频率与死区发生器和数字滤波器使用的采样频率之间的分频比。repetitioncounter
:重复计数器(只有高级定时器有),取值范围为0-255,配置为x,就重复x+1次才进入中断。
配置好参数之后,就可以初始化定时器了。
关于定时器参数配置和初始化定时器代码如下:
/* 配置定时器参数 */
timer_initpara.prescaler = pre -1; // 时钟预分频值 0-65535 psc_clk = CK_TIMER / pre
timer_initpara.alignedmode = TIMER_COUNTER_EDGE; // 边缘对齐
timer_initpara.counterdirection = TIMER_COUNTER_UP; // 向上计数
timer_initpara.period = per - 1; // 周期
/* 在输入捕获的时候使用 数字滤波器使用的采样频率之间的分频比例 */
timer_initpara.clockdivision = TIMER_CKDIV_DIV1; // 分频因子
/* 只有高级定时器才有 配置为x,就重复x+1次进入中断 */
timer_initpara.repetitioncounter = 0; // 重复计数器 0-255
timer_init(BSP_TIMER,&timer_initpara); // 初始化定时器
2
3
4
5
6
7
8
9
10
注意
这里有两个参数pre
和per
,这两个参数是决定定时时间的,为了使用更灵活,在调用函数的时候传递。
例如定义的函数名称为BasicTimerConfig
,函数原型:
void BasicTimerConfig(uint16_t pre,uint16_t per)
在主函数中调用,配置成1s进入一次中断:
BasicTimerConfig(16000,10000); // 定时时间 = 16000 / 160 * 10000us = 1s
/* 定时时间为:time = pre / 160 000 000 * per = 16000 / 160 000 000 * 10000 = 1s */
2
3
配置中断优先级
定时器的参数配置好之后,定时器基本就配置好了。不过我们需要在中断函数中去执行对应的功能,就要对中断进行操作。前面介绍过,如果要使用中断功能,就需要配置中断优先级。中断分组还继续沿用之前的配置。
定义TIMER5中断:
#define BSP_TIMER_IRQ TIMER5_IRQn // 定时器中断
设置中断优先级根据实际项目去配置,这里就设置为抢占优先级为2,响应优先级为2。
/* 配置中断优先级 */
nvic_irq_enable(BSP_TIMER_IRQ,2,2); // 设置中断优先级为 2,2断
2
使能中断事件和定时器
在配置好定时器中断之后还需要对中断进行使能,
void timer_interrupt_enable(uint32_t timer_periph, uint32_t interrupt);
这个函数是使能定时器中断,有两个参数,第一个参数timer_periph
就是定时器外设,第二个参数interrupt
是中断源的选择。关于第二个参数的可选选项如图所示。
从图可以看到定时器中断有好多触发方式,这里选择第一个更新中断。
/* 使能中断 */
timer_interrupt_enable(BSP_TIMER,TIMER_INT_UP); // 使能更新事件中断
2
到这里还有最后一步,使能定时器,
void timer_enable(uint32_t timer_periph)
这个函数会开启使能定时器,有一个参数就是要使能的定时器外设。
使能定时器配置如下:
/* 使能定时器 */
timer_enable(BSP_TIMER);
2
编写中断服务函数
使能中断之后,如果定时时间到,就会跳转到中断处理函数里面执行。需要编写中断处理函数。首先是中断函数名,这个是固定的,在start.s
启动文件中有定义。
宏定义为:
#define BSP_TIMER_IRQHandler TIMER5_IRQHandler // 定时器中断服务函数
在中断处理函数里需要检测中断标志位是否被置位。
FlagStatus timer_interrupt_flag_get(uint32_t timer_periph, uint32_t int_flag);
这个函数是获取中断标志位。有两个参数,第一个参数timer_periph
就是要检测的定时器外设,第二个参数int_flag
就是触发的中断源。有一个返回值FlagStatus
,返回值的状态为SET
和RESET
。需要注意的是每次中断执行完毕之后都需要清除一下中断标志位等待下一次中断发生。
中断服务函数编写代码如下:
void BSP_TIMER_IRQHandler(void)
{
//如果定时器5的更新中断被触发
if(SET == timer_interrupt_flag_get(TIMER5,TIMER_INT_FLAG_UP))
{
// 清除中断标志位
timer_interrupt_flag_clear(TIMER5,TIMER_INT_FLAG_UP);
// 执行操作
gpio_bit_toggle(GPIOC, GPIO_PIN_13);
printf("BSP_TIMER_IRQHandler\r\n");
}
}
2
3
4
5
6
7
8
9
10
11
12
到此有关定时器中断的配置就完成了。
举一反三
前面TIMER5的配置可以完成,那换成其它的定时器该怎么配置呢?
前面编写了很多的宏定义,这里换成TIMER2的定时器只需要修改宏定义为TIMER2即可。
TIMER5的宏定义如下:
/* TIMER2 */
#define BSP_TIMER_RCU RCU_TIMER2 // 定时器时钟
#define BSP_TIMER TIMER2 // 定时器
#define BSP_TIMER_IRQ TIMER2_IRQn // 定时器中断
#define BSP_TIMER_IRQHandler TIMER2_IRQHandler // 定时器中断服务函数
2
3
4
5
替换掉这个宏定义之后可以直接编译使用。
实验现象
上电后开发板持续通过串口0每隔一秒输出内容: BSP_TIMER_IRQHandler
一次,并且开发板上的LED指示灯也跟随着亮灭。
关于这一章节的代码,可以在开发板资料/03 - 软件资料/代码例程/里面的009定时器灯闪烁
。
下载中心跳转📦
资料下载中心:点击跳转🚀