十六、定时器灯闪烁
1.1.配置流程
一般使用定时器功能,都需要有以下几个步骤。
- 开启时钟(定时器时钟)
- 配置定时器参数
- 配置中断优先级
- 使能中断事件和定时器
- 编写中断服务函数
GD32F450ZGT6 单片机一共有 14 个定时器,包括高级定时器、通用定时器和基本定时器。不同类型的定时器功能不同。这一章就以 LED 灯 1s 闪烁为实验内容进行讲解。实现定时 1 秒钟进入中断,只需要实现基本的定时功能,GD32 的 14 个定时器都可以使用,这里就以基本定时器 5 为例进行介绍。
1.1.1.开启时钟
先来看一下定时器的时钟来源,在数据手册的第 18 页,如图 1-1-1 所示。
从图 1-1-1 可以看到 14 个定时器的时钟来源主要分为两部分,第一部分来源于 CK_APB1,第二部分来源于 CK_APB2。然后经过时钟配置寄存器(RCU_CFG1)决定是 APB 频率的 2 倍还是 4 倍,但这个频率不能超过 AHB(max = 200MHZ)。
这里使用 TIMER5,就要先使能 TIMER5 的时钟,又因为 TIMER5 时钟来源于 CK_APB1,CK_APB1 的时钟在 system_gd32f4xx.c 中定义,如图 1-1-2 所示。从图 1-1-2 可以看到 APB1 的时钟等于 AHB 的时钟 4 分频,AHB 的时钟等于系统时钟 SYSCLK。系统时钟定义如图 1-1-3 所示。从图 1-1-3 可以看到系统时钟等于__SYSTEM_CLOCK_200M_PLL_25M_HXTAL,跳转到这个宏定义可以得知,这个宏定义的值就是(uint32_t)(200000000),可见系统时钟为 200MHZ。回过来看 CK_APB1 的时钟为 200MHZ 的 4 分频等于 50MHZ。要设置定时器的时钟为 200MHZ,从时钟树上可以看到还需要进行 4 倍频处理,在代码里面还需要配置为 4 倍频。
首先编写 TIMER5 时钟的宏定义:
#define BSP_TIMER_RCU RCU_TIMER5 // 定时器时钟
开启定时器时钟:
/* 开启时钟 */rcu_periph_clock_enable(BSP_TIMER_RCU); // 开启定时器时钟
然后配置定时器时钟 4 倍频。在 gd32f4xx_rcu.h 中寻找相关函数
void rcu_timer_clock_prescaler_config(uint32_t timer_clock_prescaler);
这个函数是配置定时器时钟。有一个参数是要配置时钟的倍频系数。可选项如图 1-1-4 所示。
定时器配置为 200MHZ,要进行 4 倍频,顾选择 RCU_TIMER_PSC_MUL4。
配置定时器时钟为 200MHZ:
/* CK_TIMERx = 4 x CK_APB1 = 4x50M = 200MHZ */
rcu_timer_clock_prescaler_config(RCU_TIMER_PSC_MUL4); // 配置定时器时钟
2
1.1.2.配置定时器参数
开启时钟之后,需要配置一些定时器的参数,比如定时时间,计数模式等。
关于基本定时器的结构框图如图 1-2-1 所示。
宏定义 TIMER5:
#define BSP_TIMER TIMER5 // 定时器
第一步先要复位定时器外设
timer_deinit(BSP_TIMER); // 复位定时器
然后在 gd32f4xx_time.h 中有
void timer_init(uint32_t timer_periph, timer_parameter_struct* initpara);
这个函数是定时器初始化,并且配置参数。有两个参数,第一个参数就是定时器外设,第二个参数就是一个定时器参数结构体,关于结构体的定义如图 1-2-2 所示。
要初始化定时器就要先配置这个结构体,首先定义这个结构体
timer_parameter_struct timer_initpara; // 定义定时器结构体
然后配置结构体的参数
- prescaler:这个参数是时钟的预分频值,是 16 位的,取值范围为 1-65535。从图 1-2-1 可以看到,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,这两个参数是决定定时时间的,为了使用更灵活,在调用函数的时候传递。
函数原型:
void basic_timer_config(uint16_t pre,uint16_t per)
在主函数中调用,配置成 1s 进入一次中断:
basic_timer_config(20000,10000); // 定时时间 = 20000 / 200 * 10000us = 1s
/* 定时时间为:time = pre / 200 000 000 * per = 20000 / 200 000 000 * 10000 = 1s */
2
3
1.1.3.配置中断优先级
定时器的参数配置好之后,定时器基本就配置好了。不过我们需要在中断函数中去执行对应的功能,就要对中断进行操作。前面介绍过,如果要使用中断功能,就需要配置中断优先级。中断分组还继续沿用之前的配置。
定义 TIMER5 中断:
#define BSP_TIMER_IRQ TIMER5_DAC_IRQn // 定时器中断
这里要特别注意一下,TIMER5 的中断和其它定时器有一点不同,TIMER5 的中断和 DAC 的中断是在一起的,具体定义在 gd32f4xx.h 文件中,感兴趣的可以去查看一下。
设置中断优先级根据实际项目去配置,这里就设置为抢占优先级为 3,响应优先级为 2。
/* 配置中断优先级 */
nvic_irq_enable(BSP_TIMER_IRQ,3,2); // 设置中断优先级为 3,2断
2
1.1.4.使能中断事件和定时器
在配置好定时器中断之后还需要对中断进行使能,
void timer_interrupt_enable(uint32_t timer_periph, uint32_t interrupt);
这个函数是使能定时器中断,有两个参数,第一个参数就是定时器外设,第二个参数是中断源的选择。关于第二个参数的可选选项如图 1-4-1 所示。
从图 1-4-1 可以看到定时器中断有好多触发方式,这里选择第一个更新中断。
/* 使能中断 */
timer_interrupt_enable(BSP_TIMER,TIMER_INT_UP); // 使能更新事件中断
2
到这里还有最后一步,使能定时器,
void timer_enable(uint32_t timer_periph);
这个函数使能定时器,有一个参数就是要使能的定时器外设,
使能定时器配置如下:
/* 使能定时器 */
timer_enable(BSP_TIMER);
2
1.1.5.编写中断服务函数
使能中断之后,如果定时时间到,就会跳转到中断处理函数里面执行。需要编写中断处理函数。首先是中断函数名,这个是固定的,在 startup_gd32f450_470.s 启动文件中有定义。
宏定义为:
#define BSP_TIMER_IRQHandler TIMER5_DAC_IRQHandler // 定时器中断服务函数;
在中断处理函数里需要检测中断标志位是否被置位。
FlagStatus timer_interrupt_flag_get(uint32_t timer_periph, uint32_t interrupt);
这个函数是获取中断标志位。有两个参数,第一个参数就是要检测的定时器外设,第二个参数就是触发的中断源。有一个返回值 FlagStatus,返回值的状态为 SET 和 RESET。需要注意的是每次中断执行完毕之后都需要清除一下中断标志位等待下一次中断发生。
中断服务函数编写代码如下:
void BSP_TIMER_IRQHandler(void){
/* 这里是定时器中断 */
if(timer_interrupt_flag_get(BSP_TIMER,TIMER_INT_FLAG_UP) == SET)
{
timer_interrupt_flag_clear(BSP_TIMER,TIMER_INT_FLAG_UP); // 清除中断标志位
/* 执行操作 */
printf("BSP_TIMER_IRQHandler\r\n");
gpio_bit_toggle(BSP_LED2_PORT,BSP_LED2_PIN); // 翻转led
}
}
2
3
4
5
6
7
8
9
10
到此有关定时器中断的配置就完成了。
1.2.举一反三
前面 TIMER5 的配置可以完成,那换成其它的定时器该怎么配置呢?
前面编写了很多的宏定义,这里换成 TIMER2 的定时器只需要修改宏定义为 TIMER2 即可。
TIMER2 的宏定义如下:
/* 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
替换掉这个宏定义之后可以直接编译使用。
1.3.实验现象
关于这一章节的代码,在资源包/04 软件资料/代码例程/里面的 008 定时器灯闪烁。
烧写我们的代码之后,每隔 1 秒钟在串口助手上打印一次 BSP_TIMER_IRQHandler,然后 LED2 会每隔 1 秒钟时间闪烁一下。