开发环境:
IDE:MKD 5.30
开发板:RA-Eco-RA4M2
MCU:R7FA4M2AD3CFP
Cortex-M的内核中包含Systick定时器了,只要是Cortex-M系列的MCU就会有Systick,因此这是通用的,下面详细分析。
1 Systick工作原理分析
SysTick 定时器被捆绑在 NVIC 中,用于产生 SysTick 异常。在以前,操作系统和所有使用了时基的系统都必须有一个硬件定时器来产生需要的“滴答”中断,作为整个系统的时基。滴答中断对操作系统尤其重要。例如,操作系统可以为多个任务分配不同数目的时间片,确保没有一个任务能霸占系统 ;或者将每个定时器周期的某个时间范围赐予特定的任务等,操作系统提供的各种定时功能都与这个滴答定时器有关。因此,需要一个定时器来产生周期性的中断,而且最好还让用户程序不能随意访问它的寄存器,以维持操作系统“心跳”的节律。
R7FA4M2AD3CFP 内核是Cortex-M33,且带有Security Extension,就会有两个24位SysTick定时器,一个非安全SysTick定时器和安全SysTick定时器。在没有安全扩展的实现中,只有一个使用24位SysTick定时器。所有的 CM33 芯片至少都带有一个SysTick定时器,软件在不同芯片生产厂商的 CM33 器件间的移植工作就得以简化。该定时器的时钟源可以是内部时钟,或者是外部时钟。不过,外部时钟的具体来源则由芯片设计者决定,因此不同产品之间的时钟频率可能大不相同。因此,需要阅读芯片的使用手册来确定选择什么作为时钟源。在R7FA4M2中SysTick 以 SYSTICCLK或ICLK作为运行时钟。
SysTick 定时器能产生中断,CM33为它专门开出一个异常类型,并且在向量表中有它的一席之地。它使操作系统和其他系统软件在 CM33器件间的移植变得简单多了,因为在所有 CM33产品间,SysTick 的处理方式都是相同的。SysTick 定时器除了能服务于操作系统之外,还能用于其他目的,如作为一个闹铃、用于测量时间等。Systick 定时器属于Cortex-M内核部件。
2 Systick寄存器分析
在传统的嵌入式系统软件按中通常实现 Delay(N) 函数的方法为:
for(i = 0; i <= x; i ++);
x --- ;
对于R7FA4M2系列微处理器来说,执行一条指令只有几十个 ns,进行 for 循环时,要实现 N 毫秒的 x 值非常大,而且由于系统频率的宽广,很难计算出延时 N 毫秒的精确值。针对R7FA4M2微处理器,需要重新设计一个新的方法去实现该功能,以实现在程序中使用 Delay(N)。
SysTick 为一个 24 位递减计数器,SysTick 设定初值并使能后,每经过 1 个系统时钟周期,计数值就减 1。计数到 0 时,SysTick 计数器自动重装初值并继续计数,同时内部的 COUNTFLAG 标志会置位,触发中断 (如果中断使能情况下)。
在 R7FA4M2的应用中,使用 Cortex-M33内核的 SysTick 作为定时时钟,设定每一毫秒产生一次中断,在中断处理函数里对 N 减一,在Delay(N) 函数中循环检测 N 是否为 0,不为 0 则进行循环等待;若为 0 则关闭 SysTick 时钟,退出函数。
注:全局变量 TimingDelay , 必须定义为volatile 类型, 延迟时间将不随系统时钟频率改变。
Cortex-M33中的Systick部分内容属于NVIC控制部分,一共有4个寄存器。
Ø SYST_CSR,0xE000E010-- 控制寄存器
位段 |
名称 |
类型 |
复位值 |
描述 |
16 |
COUNTFLAG |
R |
0 |
如果在上次读取本寄存器后,SysTick已经数到了0,则该位为1。如果读取该位,该位将自动清零 |
2 |
CLKSOURCE |
R/W |
0 |
0=外部时钟源 1=内核时钟 |
1 |
TICKINT |
R/W |
0 |
1=SysTick倒数到0时产生SysTick异常请求 0=数到0时无动作 |
0 |
ENABLE |
R/W |
0 |
SysTick定时器的使能位 |
Ø SYST_RVR,0xE000E014-- 重载寄存器
位段 |
名称 |
类型 |
复位值 |
描述 |
23:0 |
RELOAD |
R/W |
0 |
当倒数至零时,将被重装载的值 |
Systick是一个递减的定时器,当定时器递减至0时,重载寄存器中的值就会被重装载,继续开始递减。SYST_RVR重载寄存器是个24位的寄存器最大计数0xFFFFFF。
Ø SYST_CVR,0xE000E018-- 当前值寄存器
位段 |
名称 |
类型 |
复位值 |
描述 |
23:0 |
CURRENT |
R/W |
0 |
读取时返回当前倒计数的值,写它则使之清零,同时还会清除在SysTick控制及状态寄存器中的COUNTFLAG标志 |
也是个24位的寄存器,读取时返回当前倒计数的值,写它则使之清零,同时还会清除在SysTick 控制及状态寄存器中的COUNTFLAG标志。
Ø SYST_CALIB,0xE000E01C-- 校准值寄存器
位段 |
名称 |
类型 |
复位值 |
描述 |
31 |
NOREF |
R |
- |
1=没有外部参考时钟 0=外部参考时钟可用 |
30 |
SKEW |
R |
- |
1=校准值不是准确的10ms 0=校准值是准确的10ms |
23:0 |
TENMS |
R/W |
0 |
10ms的时间内倒计数的格数。芯片设计者应该通过Cortex-M33的输入信号提供该数值。若该值读回零,则表示无法使用校准功能 |
校准值寄存器提供了这样一个解决方案:它使系统即使在不同的CM33产品上运行,也能产生恒定的SysTick中断频率。最简单的作法就是:直接把TENMS的值写入重装载寄存器,这样一来,只要没突破系统极限,就能做到每10ms来一次 SysTick异常。如果需要其它的SysTick异常周期,则可以根据TENMS的值加以比例计算。只不过,在少数情况下,CM33芯片可能无法准确地提供TENMS的值(如,CM33的校准输入信号被拉低),所以为保险起见,最好在使用TENMS前检查器件的参考手册。
SysTick定时器除了能服务于操作系统之外,还能用于其它目的:如作为一个闹铃,用于测量时间等。要注意的是,当处理器在调试期间被喊停(halt)时,则SysTick定时器亦将暂停运作。
3 Systick定时器实现
SysTick属于Cortex-M内核的部分,因此其相关的定义在core_cm33.h文件中。
3.1 hal_entry文件分析
hal_entry()函数如下:
void hal_entry(void)
{
SysTick_Init();
while(1)
{
Delay_ms(1000);
R_IOPORT_PinWrite(&g_ioport_ctrl, GPIO_LED3, BSP_IO_LEVEL_LOW);
R_IOPORT_PinWrite(&g_ioport_ctrl, GPIO_LED1, BSP_IO_LEVEL_HIGH);
Delay_ms(1000);
R_IOPORT_PinWrite(&g_ioport_ctrl, GPIO_LED1, BSP_IO_LEVEL_LOW);
R_IOPORT_PinWrite(&g_ioport_ctrl, GPIO_LED2, BSP_IO_LEVEL_HIGH);
Delay_ms(1000);
R_IOPORT_PinWrite(&g_ioport_ctrl, GPIO_LED2, BSP_IO_LEVEL_LOW);
R_IOPORT_PinWrite(&g_ioport_ctrl, GPIO_LED3, BSP_IO_LEVEL_HIGH);
}
#if BSP_TZ_SECURE_BUILD
R_BSP_NonSecureEnter();
#endif
}
在hal_entry()函数中,SysTick_Init和SysTick_ms () 这两个函数比较陌生,它们的功能分别是配置好 SysTick 定时器和进行精确延时。整个hal_entry()函数的流程就是初始化 LED 及SysTick 定时器之后,就进入死循环,点亮LED的时间为精确的1s。
3.2 ra4m2_bsp_systick.c文件分析
配置并启动 SysTick
我们看一下systick_init()这个函数,其功能是启动系统滴答定时器 SysTick。
void SysTick_Init(void)
{
if (SysTick_Config(SystemCoreClock / 100000))
{
while (1);
}
SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;
NVIC_SetPriority(SysTick_IRQn, 0x00U);
}
本函数实际上只是调用了 SysTick_Config() 函数,它是属于内核层的 Cortex-M3 通用函数,位于 core_cm33.h 文件中。若调用 SysTick_Config() 配置 SysTick 不成功,则进入死循环,初始化 SysTick 成功后,先关闭定时器,在需要的时候再开启。SysTick_Config() 函数无法在R7FA4M2AD3CFP外设固件库文件中找到其使用方法。所以我们在 Keil 环境下直接跟踪这个函数到 core_cm33.h 文件,查看函数的定义。
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if ((ticks - 1UL) > SysTick_LOAD_RELOAD_Msk)
{
return (1UL);
}
SysTick->LOAD = (uint32_t)(ticks - 1UL);
NVIC_SetPriority (SysTick_IRQn, (1UL << __NVIC_PRIO_BITS) - 1UL);
SysTick->VAL = 0UL;
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk;
return (0UL);
}
在这个函数定义的前面有关于它的注释,如果我们不想去研究它的具体实现,可以根据这段注释了解函数的功能 :这个函数启动了 SysTick ;并把它配置为计数至 0 时引起中断 ;输入的参数 ticks 为两个中断之间的脉冲数,即相隔 ticks 个时钟周期会引起一次中断 ;配置 SysTick 成功时返回 0,出错时返回 1。但是,这段注释并没有告诉我们它把 SysTick 的时钟设置为 SYSTICCLK时钟还是ICLK,这是一个十分关键的问题,于是,我们将对这个函数的具体实现进行分析,与大家再分享一下如何分析底层库函数。分析底层库函数,要有 SysTick 定时器工作分析的知识准备。
检查输入参数
SysTick_Config() 第 3 行代码是检查输入参数 ticks,因为 ticks 是脉冲计数值,要被保存到重载寄存器 SYST_RVR寄存器中,再由硬件把SYST_RVR值加载到当前计数值寄存器SYST_CVR中使用,SYST_RVR 和SYST_CVR都是 24 位的,所以当输入参数 ticks 大于其可存储的最大值时,将由这行代码检查出错误并返回。
位指示宏及位屏蔽宏
检查 ticks 参数没有错误后,就稍稍处理一下把 ticks-1 赋值给SYST_RVR寄存器,要注意的是减 1,若 SYST_CVR从 ticks−1 向下计数至 0,实际上就经过了 ticks 个脉冲。这句赋值代码使用了宏 SysTick_LOAD_RELOAD_Msk,与其他库函数类似,这个宏是用来指示寄存器的特定位置或进行位屏蔽的。
#define SysTick_CTRL_COUNTFLAG_Pos 16U
#define SysTick_CTRL_COUNTFLAG_Msk (1UL << SysTick_CTRL_COUNTFLAG_Pos)
#define SysTick_CTRL_CLKSOURCE_Pos 2U
#define SysTick_CTRL_CLKSOURCE_Msk (1UL << SysTick_CTRL_CLKSOURCE_Pos)
#define SysTick_CTRL_TICKINT_Pos 1U
#define SysTick_CTRL_TICKINT_Msk (1UL << SysTick_CTRL_TICKINT_Pos)
#define SysTick_CTRL_ENABLE_Pos 0U
#define SysTick_CTRL_ENABLE_Msk (1UL )
#define SysTick_LOAD_RELOAD_Pos 0U
#define SysTick_LOAD_RELOAD_Msk (0xFFFFFFUL )
#define SysTick_VAL_CURRENT_Pos 0U
#define SysTick_VAL_CURRENT_Msk (0xFFFFFFUL )
#define SysTick_CALIB_NOREF_Pos 31U
#define SysTick_CALIB_NOREF_Msk (1UL << SysTick_CALIB_NOREF_Pos)
#define SysTick_CALIB_SKEW_Pos 30U
#define SysTick_CALIB_SKEW_Msk (1UL << SysTick_CALIB_SKEW_Pos)
#define SysTick_CALIB_TENMS_Pos 0U
#define SysTick_CALIB_TENMS_Msk (0xFFFFFFUL )
其中寄存器位指示宏 :SysTick_xxx_Pos ,宏展开后即为 xxx 在相应寄存器中的位置,如控制 SysTick 时钟源的 SysTick_CTRL_CLKSOURCE_Pos ,宏展开为 2,这个寄存器位正是寄存器 STK_CTRL 中的 Bit2。
而寄存器位屏蔽宏 :SysTick_xxx_Msk,宏展开是 xxx 的位全部置 1 后,左移SysTick_xxx_Pos 位。如控制 SysTick 时钟源的 SysTick_CTRL_CLKSOURCE_Msk,宏展开为“1ul << SysTick_CTRL_CLKSOURCE_Pos”, 把无符号长整型数值(ul) 1 左移 2 位, 得 到 了 一 个 只 有 Bit2 :CLKSOURCE 位被置 1,其他位为 0 的数值,这样的数值配合位操作 &(按位与)、| (按位或)可以很方便地修改寄存器的某些位。假如控制 CLKSOURCE 需 要 4 个寄存器位,这个宏就应该被改为( 0xf ul <<SysTick_CTRL_CLKSOURCE_Pos),这样就会得到一个关于 CLKSOU RCE 的 4位被置 1 的值,这些宏的参数就是这样被确定的。寄存器位指示宏和位屏蔽宏在操作寄存器的代码(大部分库函数)中用得十分广泛。
配置中断向量及重置SYST_CVR寄存器
回到 SysTick_Config() 函数,接下来调用了 NVIC_SetPriority () 函数并配置了 SysTick中断,如果想修改SysTick的优先级,也可以在外部使用 NVIC 配置 SysTick 中断。配置好SysTick 中断后把 STK_VAL 寄存器重新赋值为 0(在使能 SysTick 时,硬件会把存储在SYST_RVR寄存器 中的 ticks 值加载给它)。
配置 SysTick 时钟
在这段代码最后,向SYST_CSR寄存器写入了 SysTick 的控制参数,配置为使用内核时钟,使能计数至 0 时引起中断,使能 SysTick。执行了这行代码,SysTick 就开始运行并进行脉冲计数了。
若想要使用外部时钟作为时钟,可以直接在SysTick_Config()函数中对SysTick->CTRL进行修改,当然最好自定义SysTick_Init()函数中修改。
使能、关闭定时器
由于调用 SysTick_Config() 函数之后,SysTick 定时器就被开启了,但我们在初始化的时候并不希望这样,而是根据需要再开启。所以在SysTick_Init() 函数中,调用完SysTick_Config() 并配置好后,应先把定时器关闭了。SysTick 的开启和关闭由寄存器SYST_CSR的 Bit0 :ENABLE 位来控制,使用位屏蔽宏以操作寄存器的方式实现。
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
SysTick->CTRL &= ~ SysTick_CTRL_ENABLE_Msk;
定时时间的计算
在调用SysTick_Config()函数时,向它输入的参数为SystemCoreClock / 100000,SystemCoreClock为定义了系统时钟频率的宏,即等于 ICLK的时钟频率。ICLK被配置为 100MHz 的,也就是这个 SystemCoreClock 宏展开为数值 10000 0000。
根据前面对 SysTick_Config() 函数的介绍,它的输入参数为 SysTick 将要计时的脉冲数,经过 ticks 个脉冲(经过 ticks 个时钟周期)后将触发中断,触发中断后又重新开始计数。由此我们可以算出定时的时间,下面为计算公式 :
T=ticks×(1/f)
其中,T 为要定时的总时间 ;ticks 为 SysTick_Config() 的输入参数 ;1/ f 即为SysTick 使用的时钟源的时钟周期,f 为该时钟源的时钟频率,当时钟源确定后为常数。
本例中使用时钟源为ICLK时钟,其频率被配置为 100 MHz。调用函数时,把 ticks 赋值为 ticks=SystemFrequency / 100000 =1000,表示 1000 个时钟周期中断一次 ;1/f 是时钟周期的时间,此时1/f =1/100 us,所以最终定时总时间 T=1000x(1/100),为1000 个时钟周期,正好是 10us。
SysTick 定时器的定时时间(配置为触发中断,即为中断周期)由 ticks 参数决定,最大定时周期不能超过 224个。
编写中断服务函数
一旦我们调用了Delay_us() 函数,SysTick 定时器就被开启,按照设定好的定时周期递减计数,当 SysTick 的计数寄存器的值减为 0 时,就进入中断函数,当中断函数执行完毕之后重新计时,如此循环,除非它被关闭。
void Delay_us(__IO uint32_t nTime)
{
TimingDelay = nTime;
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
while(TimingDelay != 0);
}
使能了 SysTick 之后,就使用while(TimingDelay != 0)语句等待TimingDelay 变量变为 0,这个变量是在中断服务函数中被修改的。因此,我们需要编写相应的中断服务程序,在本实验室中我们配置为 10us 中断一次,每次中断把 TimingDelay减 1。
void SysTick_Handler(void)
{
TimingDelay_Decrement();
}
SysTick中断属于系统异常向量,在gd32f10x_it.c文件中已经默认有了它的中断服务函数SysTick_Handler(),但内容为空。我们找到这个函数,其调用了用户函数delay_decrement()。后者是由用户编写的一个应用程序。
void TimingDelay_Decrement(void)
{
if (TimingDelay != 0x00)
{
TimingDelay--;
}
}
每次进入 SysTick 中断就调用一次 TimingDelay_Decrement ()函数,使全局变量TimingDelay自减一次。用户函数 Delay_us ()在TimingDelay被减至0时,才退出延时循环,即我们对 delay 赋的值为要中断的次数。所以总的延时时间 :
T 延时 = T 中断周期 * TimingDelay
至此,SysTick 的精确延时功能讲解完毕。
3.3 实验现象
将编译好的程序下载到板子中,可以看到LED灯不同地闪烁。