10

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

 3 years ago
source link: https://my.oschina.net/u/4526289/blog/5069427
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

踩准时钟节拍、玩转时间转换,鸿蒙轻内核时间管理有妙招 - 华为云开发者社区的个人空间 - OSCHINA - 中文开源技术交流社区

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

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

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

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

系统时钟是由定时器/计数器产生的输出脉冲触发中断产生的,一般定义为整数或长整数。输出脉冲的周期叫做一个“时钟滴答”,也称为时标或者TickTick是操作系统的基本时间单位,由用户配置的每秒Tick数决定。如果用户配置每秒的Tick数目为1000,则1个Tick等于1ms的时长。另外一个计时单位是Cycle,这是系统最小的计时单位。Cycle的时长由系统主时钟频率决定,系统主时钟频率就是每秒钟的Cycle数,对于216 MHzCPU,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_TIMELOSCFG_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等于多少cycleg_sysClock系统时钟表示1秒多少cycleLOSCFG_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中断发生时,在函数VOID OsTickHandler(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数目。⑵处通过读取当前值寄存器SysTick Current Value Register,获取hwCycle。⑶处表示中断控制和状态寄存器Interrupt Control and State Register的第TICK_CHECK位为1时,表示挂起systick中断,tick没有计数,需要加1校准。⑷处根据swTickg_cyclesPerTickhwCycle计算出自系统启动以来的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到自己账户下,谢谢。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK