写点什么

踩准时钟节拍、玩转时间转换,鸿蒙轻内核时间管理有妙招

发布于: 2021 年 06 月 04 日

​​​​​​​​​​​​​​​​​​摘要: 本文带领大家一起剖析了鸿蒙轻内核的时间管理模块的源代码。时间管理模块为任务调度提供必要的时钟节拍,会向应用程序提供所有和时间有关的服务,如时间转换、统计、延迟功能。


本文分享自华为云社区《鸿蒙轻内核M核源码分析系列六 时间管理》,原文作者:zhushy 。

 

本文会继续分析 Tick 和时间相关的源码,给读者介绍鸿蒙轻内核的时间管理模块。本文中所涉及的源码,以 OpenHarmony LiteOS-M 内核为例,均可以在开源站点https://gitee.com/openharmony/kernel_liteos_m 获取。


时间管理模块以系统时钟为基础,可以分为 2 部分,一部分是 SysTick 中断,为任务调度提供必要的时钟节拍;另外一部分是,给应用程序提供所有和时间有关的服务,如时间转换、统计功能。


系统时钟是由定时器/计数器产生的输出脉冲触发中断产生的,一般定义为整数或长整数。输出脉冲的周期叫做一个“时钟滴答”,也称为时标或者 Tick。Tick 是操作系统的基本时间单位,由用户配置的每秒 Tick 数决定。如果用户配置每秒的 Tick 数目为 1000,则 1 个 Tick 等于 1ms 的时长。另外一个计时单位是 Cycle,这是系统最小的计时单位。Cycle 的时长由系统主时钟频率决定,系统主时钟频率就是每秒钟的 Cycle 数,对于 216 MHz 的 CPU,1 秒产生 216000000 个 cycles。


用户以秒、毫秒为单位计时,而操作系统以 Tick 为单位计时,当用户需要对系统进行操作时,例如任务挂起、延时等,此时可以使用时间管理模块对 Tick 和秒/毫秒进行转换。


下面,我们剖析下时间管理模块的源代码,若涉及开发板部分,以开发板工程 targets\cortex-m7_nucleo_f767zi_gcc\为例进行源码分析。

1、时间管理初始化和启动


我们先看下时间管理模块的相关配置,然后再剖析如何初始化,如何启动。

1.1 时间管理相关的配置


时间管理模块涉及 3 个配置项,系统时钟 OS_SYS_CLOCK、每秒 Tick 数目 LOSCFG_BASE_CORE_TICK_PER_SECOND 两个配置选项,还有宏 LOSCFG_BASE_CORE_TICK_HW_TIME。LOSCFG_BASE_CORE_TICK_HW_TIME 默认关闭,开启时,需要提供定制函数 VOID platform_tick_handler(VOID),在 Tick 中断处理函数中执行定制操作。这些配置项在模板开发板工程目录的文件 target_config.h 中定义,如文件 targets\cortex-m7_nucleo_f767zi_gcc\target_config.h 中定义如下:


#define OS_SYS_CLOCK                                        96000000#define LOSCFG_BASE_CORE_TICK_PER_SECOND                    (1000UL)#define LOSCFG_BASE_CORE_TICK_HW_TIME                       0
复制代码


1.2 时间管理初始化和启动


函数 INT32 main(VOID)会调用 kernel\src\los_init.c 中的函数 UINT32 LOS_Start(VOID)启动系统,该函数会调用启动调度函数 UINT32 HalStartSchedule(OS_TICK_HANDLER handler)。源码如下:


LITE_OS_SEC_TEXT_INIT UINT32 LOS_Start(VOID){    return HalStartSchedule(OsTickHandler);}
复制代码


​函数 UINT32 HalTickStart(OS_TICK_HANDLER *handler)定义在 kernel\arch\arm\cortex-m7\gcc\los_context.c,源码如下。其中函数参数为 Tick 中断处理函数 OsTickHandler(),后文会分析该 tick 中断处理函数。⑴处代码继续调用函数进一步调用函数 HalTickStart(handler)来设置 Tick 中断启动。⑵处会调用汇编函数 HalStartToRun 开始运行系统,后续任务调度系列再详细分析该汇编函数。


LITE_OS_SEC_TEXT_INIT UINT32 HalStartSchedule(OS_TICK_HANDLER handler){    UINT32 ret;⑴  ret = HalTickStart(handler);    if (ret != LOS_OK) {        return ret;    }⑵  HalStartToRun();    return LOS_OK; /* never return */}
复制代码


​函数 HalTickStart(handler)定义在文件 kernel\arch\arm\cortex-m7\gcc\los_timer.c,源码如下,我们分析下函数的代码实现。⑴处校验下时间管理模块的配置项的合法性。在开启宏 LOSCFG_USE_SYSTEM_DEFINED_INTERRUPT 时,会使用系统定义的中断。会执行⑵处的代码,调用定义在文件 kernel\arch\arm\cortex-m7\gcc\los_interrupt.c 中的函数 OsSetVector()设置中断向量,该函数在中断系列会详细分析。⑶处设置全局变量 g_sysClock 为系统时钟,g_cyclesPerTick 为每 tick 对应的 cycle 数目,g_ullTickCount 初始化为 0,表示系统 tick 中断发生次数。⑷处调用定义在 targets\cortex-m7_nucleo_f767zi_gcc\Drivers\CMSIS\Include\core_cm7.h 文件中的内联函数 uint32_t SysTick_Config(uint32_t ticks),初始化、启动系统定时器 Systick 和中断。


WEAK UINT32 HalTickStart(OS_TICK_HANDLER *handler){    UINT32 ret;
⑴ if ((OS_SYS_CLOCK == 0) || (LOSCFG_BASE_CORE_TICK_PER_SECOND == 0) || (LOSCFG_BASE_CORE_TICK_PER_SECOND > OS_SYS_CLOCK)) { return LOS_ERRNO_TICK_CFG_INVALID; }
#if (LOSCFG_USE_SYSTEM_DEFINED_INTERRUPT == 1)#if (OS_HWI_WITH_ARG == 1) OsSetVector(SysTick_IRQn, (HWI_PROC_FUNC)handler, NULL);#else⑵ OsSetVector(SysTick_IRQn, (HWI_PROC_FUNC)handler);#endif#endif
⑶ g_sysClock = OS_SYS_CLOCK; g_cyclesPerTick = OS_SYS_CLOCK / LOSCFG_BASE_CORE_TICK_PER_SECOND; g_ullTickCount = 0;
⑷ ret = SysTick_Config(g_cyclesPerTick); if (ret == 1) { return LOS_ERRNO_TICK_PER_SEC_TOO_SMALL; }
return LOS_OK;}
复制代码


1.3 Tick 中断处理函数 OsTickHandler()


文件 kernel\src\los_tick.c 定义的函数 VOID OsTickHandler(VOID),是时间管理模块中执行最频繁的函数,每当 Tick 中断发生时就会调用该函数。我们分析下该函数的源码,⑴处如果开启宏 LOSCFG_BASE_CORE_TICK_HW_TIME,会调用定制的 tick 处理函数 platform_tick_handler(),默认不开启。⑵处会更新全局变量 g_ullTickCount,⑶处如果开启宏 LOSCFG_BASE_CORE_TIMESLICE,会检查当前运行任务的时间片,在后续任务模块会详细分析下函数 OsTimesliceCheck()。⑷处会遍历任务的排序链表,检查是否有超时的任务。⑸处如果支持定时器特性,会检查定时器是否超时。


源码如下:


LITE_OS_SEC_TEXT VOID OsTickHandler(VOID){#if (LOSCFG_BASE_CORE_TICK_HW_TIME == 1)⑴  platform_tick_handler();#endif
⑵ g_ullTickCount++;
#if (LOSCFG_BASE_CORE_TIMESLICE == 1)⑶ OsTimesliceCheck();#endif
⑷ OsTaskScan(); // task timeout scan
#if (LOSCFG_BASE_CORE_SWTMR == 1)⑸ (VOID)OsSwtmrScan();#endif}
复制代码


2、LiteOS 内核时间管理常用操作


时间管理提供下面几种功能,时间转换、时间统计等,这些函数定义在文件 kernel\src\los_tick.c,我们剖析下这些操作的源代码实现。

2.1 时间转换操作

2.1.1 毫秒转换成 Tick


函数 UINT32 LOS_MS2Tick(UINT32 millisec)把输入参数毫秒数 UINT32 millisec 可以转化为 Tick 数目。代码中 OS_SYS_MS_PER_SECOND,即 1 秒等于 1000 毫秒。时间转换也比较简单,知道一秒多少 Tick,除以 OS_SYS_MS_PER_SECOND,得出 1 毫秒多少 Tick,然后乘以 millisec,计算出 Tick 数目的结果值并返回。


LITE_OS_SEC_TEXT_MINOR UINT32 LOS_MS2Tick(UINT32 millisec){    if (millisec == OS_NULL_INT) {        return OS_NULL_INT;    }
return ((UINT64)millisec * LOSCFG_BASE_CORE_TICK_PER_SECOND) / OS_SYS_MS_PER_SECOND;}
复制代码


2.1.2 Tick 转化为毫秒


函数 UINT32 LOS_Tick2MS(UINT32 tick)把输入参数 Tick 数目转换为毫秒数。时间转换也比较简单,ticks 数目除以每秒多少 Tick 数值 LOSCFG_BASE_CORE_TICK_PER_SECOND,计算出多少秒,然后转换成毫秒,计算出结果值并返回。


LITE_OS_SEC_TEXT_MINOR UINT32 LOS_Tick2MS(UINT32 ticks){    return ((UINT64)ticks * OS_SYS_MS_PER_SECOND) / LOSCFG_BASE_CORE_TICK_PER_SECOND;}
复制代码


2.1.3 Cycle 数目转化为毫秒


介绍转换函数之前,先看下一个 CpuTick 结构体,结构体比较简单,就 2 个成员,分别表示一个 UINT64 类型数据的高、低 32 位数值。


typedef struct tagCpuTick {    UINT32 cntHi; /* < 一个64位数值的高32位 */    UINT32 cntLo; /* < 一个64位数值的低32位 */} CpuTick;
复制代码


​继续看转换函数 OsCpuTick2MS(),它可以把 CpuTick 类型表示的 cycle 数目转换为对应的毫秒数,输出毫秒数据的高、低 32 位数值。看下具体的代码,⑴处校验参数是否为空指针,⑵处检查系统时钟是否配置。⑶处把 CpuTick 结构体表示的 cycle 数目转化为 UINT64 类型数据。⑷处进行数值计算,(DOUBLE)g_sysClock / OS_SYS_MS_PER_SECOND 得到每毫秒多少个 cycle 数,然后和 tmpCpuTick 做除法运算,得到 cycle 数目对应的毫秒数目。⑸处把 DOUBLE 类型转换为 UINT64 类型,然后执行⑹,分别把结果数值的高、低 64 位赋值给*msLo、*msHi。


LITE_OS_SEC_TEXT_INIT UINT32 OsCpuTick2MS(CpuTick *cpuTick, UINT32 *msHi, UINT32 *msLo){    UINT64 tmpCpuTick;    DOUBLE temp;
⑴ if ((cpuTick == NULL) || (msHi == NULL) || (msLo == NULL)) { return LOS_ERRNO_SYS_PTR_NULL; }
⑵ if (g_sysClock == 0) { return LOS_ERRNO_SYS_CLOCK_INVALID; }⑶ tmpCpuTick = ((UINT64)cpuTick->cntHi << OS_SYS_MV_32_BIT) | cpuTick->cntLo;⑷ temp = tmpCpuTick / ((DOUBLE)g_sysClock / OS_SYS_MS_PER_SECOND);
tmpCpuTick = (UINT64)temp;
*msLo = (UINT32)tmpCpuTick; *msHi = (UINT32)(tmpCpuTick >> OS_SYS_MV_32_BIT);
return LOS_OK;}
复制代码


2.1.4 Cycle 数目转化为微秒


转换函数 OsCpuTick2US(),它可以把 CpuTick 类型表示的 cycle 数目转换为对应的毫秒数,输出毫秒数据的高、低 32 位数值。该函数和 OsCpuTick2MS()类似,自行阅读即可。


LITE_OS_SEC_TEXT_INIT UINT32 OsCpuTick2US(CpuTick *cpuTick, UINT32 *usHi, UINT32 *usLo){    UINT64 tmpCpuTick;    DOUBLE temp;
if ((cpuTick == NULL) || (usHi == NULL) || (usLo == NULL)) { return LOS_ERRNO_SYS_PTR_NULL; }
if (g_sysClock == 0) { return LOS_ERRNO_SYS_CLOCK_INVALID; } tmpCpuTick = ((UINT64)cpuTick->cntHi << OS_SYS_MV_32_BIT) | cpuTick->cntLo; temp = tmpCpuTick / ((DOUBLE)g_sysClock / OS_SYS_US_PER_SECOND);
tmpCpuTick = (UINT64)temp;
*usLo = (UINT32)tmpCpuTick; *usHi = (UINT32)(tmpCpuTick >> OS_SYS_MV_32_BIT);
return LOS_OK;}
复制代码


2.2 时间统计操作

2.2.1 获取每个 Tick 等于多少 Cycle 数


函数 UINT32 LOS_CyclePerTickGet(VOID)计算 1 个 tick 等于多少 cycle。g_sysClock 系统时钟表示 1 秒多少 cycle,LOSCFG_BASE_CORE_TICK_PER_SECOND 一秒多少 tick,相除计算出 1 tick 多少 cycle 数,即 g_cyclesPerTick = g_sysClock /LOSCFG_BASE_CORE_TICK_PER_SECOND。


LITE_OS_SEC_TEXT_MINOR UINT32 LOS_CyclePerTickGet(VOID){    return g_cyclesPerTick;}
复制代码


2.2.2 获取自系统启动以来的 Tick 数


UINT64 LOS_TickCountGet(VOID)函数计算自系统启动以来的 Tick 中断的次数。需要注意,在关中断的情况下不进行计数,不能作为准确时间使用。每次 Tick 中断发生时,在函数 VOIDOsTickHandler(VOID)中会更新 g_ullTickCount 数据。


LITE_OS_SEC_TEXT_MINOR UINT64 LOS_TickCountGet(VOID){    return g_ullTickCount;}
复制代码


2.2.3 获取系统时钟


UINT32 LOS_SysClockGet(VOID)函数获取配置的系统时钟。


UINT32 LOS_SysClockGet(VOID){    return g_sysClock;}
复制代码


2.2.4 获取系统启动以来的 Cycle 数


函数 VOID HalGetCpuCycle(UINT32 *cntHi, UINT32 *cntLo)定义在文件 kernel\arch\arm\cortex-m7\gcc\los_timer.c 中,该函数获取系统启动以来的 Cycle 数。返回结果按高、低 32 位的无符号数值 UINT32 *cntHi, UINT32 *cntLo 分别返回。


我们看下该函数的源码。先关中断,然后⑴处获取启动启动以来的 Tick 数目。⑵处通过读取当前值寄存器 SysTickCurrent Value Register,获取 hwCycle。⑶处表示中断控制和状态寄存器 Interrupt Control and State Register 的第 TICK_CHECK 位为 1 时,表示挂起 systick 中断,tick 没有计数,需要加 1 校准。⑷处根据 swTick、g_cyclesPerTick 和 hwCycle 计算出自系统启动以来的 Cycle 数。⑸处获取 Cycle 数的高、低 32 位的无符号数值,然后开中断、返回。


LITE_OS_SEC_TEXT_MINOR VOID HalGetCpuCycle(UINT32 *cntHi, UINT32 *cntLo){    UINT64 swTick;    UINT64 cycle;    UINT32 hwCycle;    UINTPTR intSave;
intSave = LOS_IntLock();
⑴ swTick = g_ullTickCount;⑵ hwCycle = SysTick->VAL;
⑶ if ((SCB->ICSR & TICK_CHECK) != 0) { hwCycle = SysTick->VAL; swTick++; }
⑷ cycle = (((swTick) * g_cyclesPerTick) + (g_cyclesPerTick - hwCycle));
⑸ *cntHi = cycle >> SHIFT_32_BIT; *cntLo = cycle & CYCLE_CHECK;
LOS_IntRestore(intSave);
return;}
复制代码


小结


本文带领大家一起剖析了鸿蒙轻内核的时间管理模块的源代码。时间管理模块为任务调度提供必要的时钟节拍,会向应用程序提供所有和时间有关的服务,如时间转换、统计、延迟功能。后续也会陆续推出更多的分享文章,敬请期待,也欢迎大家分享学习、使用鸿蒙轻内核的心得,有任何问题、建议,都可以留言给我们: https://gitee.com/openharmony/kernel_liteos_m/issues 。为了更容易找到鸿蒙轻内核代码仓,建议访问 https://gitee.com/openharmony/kernel_liteos_m ,关注 Watch、点赞 Star、并 Fork 到自己账户下,谢谢。


点击关注,第一时间了解华为云新鲜技术~

发布于: 2021 年 06 月 04 日阅读数: 389
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
踩准时钟节拍、玩转时间转换,鸿蒙轻内核时间管理有妙招