2.4 系统时钟(system tick)
在操作系统中,我们常常需要延时或者周期性的操作,比如任务的延时调度、周期性的触发等。基于对时钟精确的要求,每个运行的CPU平台都会提供相应的硬件定时机制。本节和下一节将主要介绍基于硬件定时机制的系统时钟和定时器(timer)原理。
2.4.1 基本概念
首先简要介绍硬件的定时机制。目前CPU提供的定时机制主要归结为以下两类(见图2-3)。
图2-3 倒计数模式和正计数模式的CPU定时机制
(1)倒计数模式:硬件定时器提供一个count寄存器,设定其初始值后,随着定时时钟的频率计数递减,递减频率即为定时器频率。当计数值为0时,定时结束,触发对应的定时处理,一般为挂载的中断处理。如果是周期模式,可以设置其每次计数为0后自动复位count到起始计数值(一般也通过寄存器设置),以此来设置触发周期。
(2)正计数模式:硬件定时器提供两个基本的寄存器——count寄存器和compare寄存器。count寄存器,随着时钟频率计数递增,递增频率即为定时器频率。当其达到compare设定的值后,即触发对应的定时处理。如果是周期模式,则需要按照时钟频率和延时周期来设置后续的compare值,即在上一次的定时处理内,设置下一次的compare寄存器。
上述两种模式具体参考所使用的CPU平台手册。
系统tick本质就是基于CPU的硬件定时机制所设置的一个基础硬件定时器。对于硬件底层而言,tick规定了定时器的循环触发周期;而对于操作系统来讲,tick提供了系统调度需要的最小基本定时单元。例如,在任务中需要进行延时操作,可以以tick为单位,每个tick占用的具体时间单元是用户可配置的。具体配置方式在下一节介绍。
不论哪种模式,都可以实现周期性的定时器功能。系统tick就是基于此定时功能,产生固定周期的一个调度定时单元。一般设定每秒100个tick,则每个tick代表10ms,系统中每延时一个tick单元,代表延时10ms。
2.4.2 内核系统时钟的基本配置
Rhino中使用tick时需要进行两项基本的配置,一项是tick周期配置,另一项是tick处理函数配置。这两项配置也是操作系统在不同系统移植中的必处理项。
对于tick周期配置,一般用k_config.h的宏RHINO_CONFIG_TICKS_PER_SECOND来配置,用户可以通过该宏来设置每秒的tick数。由前面的基础概念可知,定时器是由硬件定时器触发的,如果操作系统希望10ms能产生一次定时触发,则必须将10ms转换为定时器的cycle间隔值,并将此cycle间隔值按照实际倒计数或正计数模式来配置定时器的相关寄存器。
定时器的cycle间隔值=tick周期×硬件定时器频率
配置定时器的cycle间隔功能一般由相关CPU的sdk驱动提供,不同CPU的配置方式略有差别。
系统tick的处理函数指的是每次tick周期触发时,操作系统需要进行的处理。Rhino提供了一个统一的tick函数入口krhino_tick_proc(),一般将此函数加入tick定时器中断处理函数,该中断处理函数不同,CPU也不同,但是它们都会调用krhino_tick_proc接口,以此来达到屏蔽硬件差异的目的。tick处理的细节会在后面进一步介绍。
以本书配套的STM系列开发板为例,可以通过HAL_SYSTICK_Config接口配置定时器的触发周期:
/*Configure the Systick interrupt time*/ HAL_SYSTICK_Config(HAL_RCC_GetHCLKFreq()/RHINO_CONFIG_TICKS_PER_SECOND); 在SysTick_Handler该sdk钩子内调用操作系统的tick调度函数krhino_tick_proc: void SysTick_Handler(void) { HAL_IncTick(); krhino_intrpt_enter(); krhino_tick_proc(); krhino_intrpt_exit(); }
2.4.3 系统时钟的调度处理
系统tick的调度处理对象主要是针对任务的。任务相关的内容可以参看任务章节,此处着重说明tick处理对任务调度的影响。目前主要包括两个方面:延时任务处理和Round-Robin调度触发。
被延时调度的任务都会被放入一个延时队列g_tick_head中。任务被延时调度的原因主要有以下两种。
(1)任务主动延时:通过调用krhino_task_sleep(tick_t ticks)函数可以暂时释放当前任务的CPU资源,将当前任务临时加入g_tick_head队列,并切换到其他任务。此函数会将task延时ticks时间,当ticks时间到了之后,tick处理需要将此任务重新拉去放回ready队列。这种情况下的主要处理流程如图2-4所示。
图2-4 任务主动延时情况下处理流程
(2)等待资源暂时释放:任务需要等待共享资源如信号量、互斥锁(mutex)等,如果暂时无法获取此共享资源,则操作系统会将该任务放入该资源的阻塞队列。如果设置了等待超时时间,则该任务就被放入g_tick_head队列,如果在设置的超时时间内还未获取到共享资源,则tick处理会将该任务从g_tick_head队列中取出,并将任务状态设置为超时,超时的状态会通过接口返回给调用者。如果在超时时间内,获取到共享资源,则在获取到资源的流程里,将该任务从g_tick_head队列中直接删除,并设置回到正常状态(见图2-5)。
图2-5 等待资源暂时释放情况下处理流程
如本章前面部分所述,Round-Robin是一种基于时间片的调度,通过设置任务的最大占用时间片上限,实现同优先级的任务间CPU资源共享。而这个时间片就是以系统时钟tick的周期为单位。
任务能够基于Round-Robin调度,需要有两个前提条件:
①RR调用宏RHINO_CONFIG_SCHED_RR打开;
②任务创建时的调度策略需要设置为KSCHED_RR。
任务创建时,需要设置最大执行tick数,一旦开启了宏RHINO_CONFIG_SCHED_RR,默认设置时长为RHINO_CONFIG_TIME_SLICE_DEFAULT。
Round-Robin调度能够避免某一个任务长期一直占用CPU资源。需要注意的是,此调度策略只是将运行到上限tick数的任务放回到同优先级的任务ready队列尾部待执行,因此只能针对当前同优先级的任务进行CPU资源的切换。
其基于tick的调度流程框图如图2-6所示。
图2-6 基于tick的调度流程框图
2.4.4 tick模块使用
tick模块性质属于操作系统内部模块,用户不需要通过特定的接口去初始化、修改其配置。在芯片移植过程中,按照上述章节所介绍的,需要设置tick周期以及挂载tick的中断处理钩子函数。此外,tick提供了几个维护测试接口用来获取基本的tick信息。调用接口的具体函数格式及其简单说明如下。
函数名:sys_time_t krhino_sys_tick_get(void)
此接口返回从tick处理开始执行,到当前为止程序所运行的tick计数。
函数名:sys_time_t krhino_sys_time_get(void)
此接口返回从tick处理开始执行,到当前为止程序所运行的ms数。
函数名:tick_t krhino_ms_to_ticks(sys_time_t ms)
此接口将当前ms数转换为tick计数。
函数名:sys_time_t krhino_ticks_to_ms(tick_t ticks)
此接口将当前tick计数转换为ms值。