在鸿蒙轻内核源码分析上一篇文章中,我们剖析了中断的源码,简单提到了Tick
中断。本文会继续分析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\
为例进行源码分析。
我们先看下时间管理模块的相关配置,然后再剖析如何初始化,如何启动。
时间管理模块涉及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
函数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;
}
文件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
}
LiteOS
内核时间管理常用操作时间管理提供下面几种功能,时间转换、时间统计等,这些函数定义在文件kernel\src\los_tick.c
,我们剖析下这些操作的源代码实现。
函数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;
}
函数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;
}
介绍转换函数之前,先看下一个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;
}
转换函数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;
}
函数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;
}
UINT64 LOS_TickCountGet(VOID)
函数计算自系统启动以来的Tick
中断的次数。需要注意,在关中断的情况下不进行计数,不能作为准确时间使用。每次Tick
中断发生时,在函数VOID OsTickHandler(VOID)
中会更新g_ullTickCount
数据。
LITE_OS_SEC_TEXT_MINOR UINT64 LOS_TickCountGet(VOID)
{
return g_ullTickCount;
}
UINT32 LOS_SysClockGet(VOID)
函数获取配置的系统时钟。
UINT32 LOS_SysClockGet(VOID)
{
return g_sysClock;
}
函数VOID HalGetCpuCycle(UINT32 *cntHi, UINT32 *cntLo)
定义在文件kernel\arch\arm\cortex-m7\gcc\los_timer.c
中,该函数获取系统启动以来的Cycle
数。返回结果按高、低32位的无符号数值UINT32 *cntHi, UINT32 *cntLo
分别返回。
我们看下该函数的源码。先关中断,然后⑴处获取启动启动以来的Tick
数目。⑵处通过读取当前值寄存器SysTick Current 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
到自己账户下,谢谢。
如果大家想更加深入的学习 OpenHarmony 开发的内容,不妨可以参考以下相关学习文档进行学习,助你快速提升自己:
https://qr18.cn/CgxrRy
https://qr18.cn/CgxrRy
https://qr18.cn/CgxrRy
https://qr18.cn/CgxrRy
https://qr18.cn/CgxrRy