MCU Systick深度解析:实现简单延时与系统运行时间精准获取!
GD MCU 使用Systick实现板级us ms延时函数及获取系统运行时间
Systick是什么
Systick 是一种简单的定时器,属于Cortex M 核中的NVIC 控制器的一部分。
主要用来周期性的计数和产生中断。
可以用来给实时操作系统使用,也可以用做其他的时间上的计数。
MCU中Systick关键参数
核心: 计数宽度为24bit 的向下递减的自动重装载计数器,计数器每计数一次的时间是1/ClkSource,ClkSource 一般是基于系统时钟的分频,大部分情况下是1分频,即和系统时钟保持一致。
所以在实际配置或者操作Systick的过程中,我们核心关注计数器的周期、当前计数值,就可以将计数转换成我们需要的时间。
周期和具体的计数值在我们的寄存器中的值为如下图:
对应代码中的变量为:
typedef struct
{
__IO uint32_t CTRL; /*!< Offset: 0x000 (R/W) SysTick Control and Status Register */
__IO uint32_t LOAD; /*!< Offset: 0x004 (R/W) SysTick Reload Value Register */
__IO uint32_t VAL; /*!< Offset: 0x008 (R/W) SysTick Current Value Register */
__I uint32_t CALIB; /*!< Offset: 0x00C (R/ ) SysTick Calibration Register */
} SysTick_Type;
工程中使用:
SysTick->VAL : 当前计数值
SysTick->LOAD:装载值(计数总数)
SysTick->CTRL:控制及状态寄存器
SysTick->CALIB:校准寄存器(不常用)
在配置时,我们会操作LOAD、VAL、CTRL,
使用计数实现其他功能时,则更多的使用VAL 和LOAD进行转换
Systick配置
常见的Systick配置如下,核心实现时uint32_t SysTick_Config(uint32_t ticks)和配置中断优先级,具体如下:
/**
* System Clock Configuration
*/
void SystemClock_Config(void)
{
//SystemCoreClock 是系统主时钟,RT_TICK_PER_SECOND 是分频系数,一般是1KHz
SysTick_Config(SystemCoreClock / RT_TICK_PER_SECOND);
NVIC_SetPriority(SysTick_IRQn, 0);
}
__STATIC_INLINE uint32_t SysTick_Config(uint32_t ticks)
{
if ((ticks - 1) > SysTick_LOAD_RELOAD_Msk) return (1); /* Reload value impossible */
SysTick->LOAD = ticks - 1; /* set reload register */
NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1); /* set Priority for Systick Interrupt */
SysTick->VAL = 0; /* Load the SysTick Counter Value */
SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk |
SysTick_CTRL_TICKINT_Msk |
SysTick_CTRL_ENABLE_Msk; /* Enable SysTick IRQ and SysTick Timer */
return (0); /* Function successful */
}
上述配置中,
以主频72M为例,按照1Khz的分频,实际配置的计数值LOAD为 72000 000 / 1000 – 1 = 71999;,实际工程中的配置也是如此(如下图):
也就是说,每次计数减到0,发生Systick中断,这个中断的频率为72M/(71999 + 1) = 1Khz,**即每1/1Khz = 0.001s(1ms)发生一次中断**
1ms一次的Systick 的中断关系,是我们实现延时或者获取系统的运行时间的核心。
使用Systick实现延时原理
还是以72M主频+71999 的装载值为例
1、实现us延迟
Systick 每1ms触发一次中断,重新装载VAL再进行倒数计数,也就是,有如下对应关系:
1ms = (load + 1) 个计数
1us = (load + 1) / 1000 个计数
n us = n* (load + 1) / 1000个计数
那实现延迟的原理不就是:在Systick 未满足n us个计数时,系统就循环死等,直到tick计数大于等于n us个计数,此时才认为延迟时间到了,跳出死等的循环。
实现思路:
- 延迟函数入参为us;
- 进入延迟函数获取当前的滴答数记录为t_old;
- 获取系统的load值(为接下来将us转换为tick做准备);
- 将us转换为需要等待的tick数:t_us_ticks = us * (t_load + 1) / 1000;
- 进入死循环等待tick到
这里要注意:
判断tick计数其实就是计算t_old 和实时的t_now = SysTick->VAL; 的差值,也即经过的tick数,这里要考虑到t_now 和 t_old的大小,因为t_now减到0后,它会重新装载大数,继续减,所以主要有如下两种情况:
情况1:
t_old >= t_now,经过的tick cnt = t_old - tnow;
情况2:
val tick 重新装载了,此时cnt tick = told(上一次的tick已经跑过了,额外又经过了load+1 - t_now个tick)
t_old < t_now,cnt_tick = told + ((load+1) - t_now);
- 判断经过的cnt_tick 和n us的tick大小,超过n us的tick大小,则退出死循环,至此延时结束。
代码实现如下:
/*
使用systick 自减的到0,会触发重新装载的逻辑来实现延时的逻辑
系统中的load 值71999,减到0的频率是72000000/(71999 + 1) = 1Khz,
也就是每1/1Khz = 1ms触发一次重新装载的中断,所以时间关系如下:
1ms = (load + 1) 个计数
1us = (load + 1) / 1000 个计数
n us = n* (load + 1) / 1000个计数
原理:
将us转换为计数的tick值,死循环,然后判断实时的tick有无达到us的计数值,达到后退出死循环,时间目的达到
1进入该函数获取获取当前的tick值t_old,循环中获取实时的tick值t_now
通过不断比较us转换后的tick和经过的tick数
当经过的tick数大于us的tick后,即可退出
对于经过的tick数来说,有如下两种情况:
情况1:
t_old >= t_now,经过的tick cnt = t_old - tnow;
情况2:val tick 重新装载了,此时cnt tick = told(上一次的tick已经跑过了,额外又经过了load+1 - t_now个tick)
t_old < t_now,cnt_tick = told + ((load+1) - t_now);
*/
void delay_us(uint64_t us)
{
uint64_t t_old = SysTick->VAL;
uint64_t t_load = SysTick->LOAD;
uint64_t t_now = 0, cnt_tick = 0;
uint64_t t_us_ticks = us * (t_load + 1) / 1000;
do
{
t_now = SysTick->VAL;
cnt_tick += t_old >= t_now ? (t_old - t_now) : (t_old + ((t_load + 1) - t_now));//记得需要考虑cnt_tick溢出的场景
t_old = t_now;
}while(cnt_tick < t_us_ticks);
}
2、实现ms延时
基于实现的us延时,循环1000次即可
void delay_ms(uint64_t ms)
{
for(int i = 0; i < ms; i++)
{
delay_us(1000);
}
}
使用Systick 实现获取系统运行时间原理
还是以72M主频+71999 的装载值为例:
Systick 每1ms触发一次中断,重新装载VAL再进行倒数计数,也就是,有如下对应关系:
1ms = (load + 1) 个计数
1us = (load + 1) / 1000 个计数
n us = n* (load + 1) / 1000个计数
那么每个计数所耗费的时间就是1/(load + 1) ms,也即1* 1000000 / (load + 1) ns。
cnt个计数,对应的时间为n * 1000000 / (load + 1) ns。
所以实现的原理是:
- 每发生一次Systick中断,记录一次ms的计数(过去经过了多少时间),在Systick中断处理函数中增加ms自增计数
- 获取当前的计数值,统计未满LOAD计数对应的时间
实现代码如下:
// 1、Sytick 中断处理函数增加ms自增函数
/**
* This is the timer interrupt service routine.
*
*/
void SysTick_Handler(void)
{
hw_inc_tick();
}
//2、自增计数变量定义
volatile static uint32_t hw_sys_tick_ms = 0; //ms 自增计数变量
void hw_inc_tick(void)
{
++hw_sys_tick_ms;
}
//3、 获取自增ms计数
uint32_t hw_return_tick_ms(void)
{
return hw_sys_tick_ms;
}
//4、实现获取ns 系统函数
/*
获取系统从启动后到现在过了多少ns
原理:使用systick 自减的到0,会触发重新装载的逻辑来实现延时的逻辑
系统中的load 值71999,减到0的频率是72000000/(71999 + 1) = 1Khz,
也就是每1/1Khz = 1ms触发一次重新装载的中断,所以时间关系如下:
1ms = (load + 1) 个计数
1us = (load + 1) / 1000 个计数
1ns = (load + 1) / 1000000个计数
即:1个计数 = 1000000 / (load + 1) ns
n us = n* (load + 1) / 1000个计数
*/
uint64_t hw_system_get_ns(void)
{
uint64_t t_ns = hw_return_tick_ms() * 1000000;
uint64_t t_now = SysTick->VAL;
uint64_t t_load = SysTick->LOAD;
uint64_t t_cnt = t_load + 1 - t_now; //暂未特殊考虑t_now 等于0的情况
t_ns += t_cnt * (1000000 / (t_load + 1));
return t_ns;
}
至此,如何利用Systick 进行时间延时、系统运行时间获取均已结束。
当然,上述的代码中,并未考虑到变量溢出、或者计数为0的场景,更严谨的情况下,应该考虑这些场景。
本文暂不赘述,只做梳理!
作者:Blue不够,得是金牌Blue才行!