首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

系统时钟

具有上下文切换的任务框架一文中, 我们通过用户进程主动请求切换进程来控制三色灯在红灯和蓝灯之间闪烁。为了能够用肉眼观测到LED的闪烁, 我们在任务中加了一段用于延时的代码,如下所示。在这段代码中,有两个for循环,但却没有做任何事情,白白浪费了宝贵的CPU计算资源。 既然我们现在已经有了上下文切换的机制,完全可以在等待的时候切换到其它进程,延时结束之后再切换回来,这样用户程序既有了延时的效果, 又提高了CPU的利用率。

        for (int i = 0; i < 1000; i++)
            delay(10000);
        
        void delay(int c) {
            for (int i = 0; i < c; i++);
        }

在进程a延时的时候,如果切换到其它进程,那么系统该如何知道进程a延迟的时间到了呢?在原先的延时方案中, 我们可以根据for循环空转的次数和CPU运行频率粗略估计出延迟的时间。但切换到其它进程时,执行了什么语句占用了多少个时钟周期都是不可能估计的。 为了得到一个相对精准的延时功能,就需要有一个系统时钟。

系统时钟存在的意义不仅仅是要提供一个延时功能。它应当能够记录系统的运行时间,甚至是每个进程的运行时间,为我们通过CPU占用率进行优化提供了可能。 在我们要实现的时间片机制中,系统时钟的计时功能是必不可少的一个组件。所以系统时钟对于操作系统而言是一个重要的组件。

在本文中,我们对定时触发的上下文切换一文中计时功能做一些修改, 得到一个系统时钟的雏形,以后会根据系统功能的完善,不断改进。

1. 系统时钟的实现

系统时钟虽然存在的意义重大,但它的根本功能需求很简单,就是提供一个计时器,使得我们能够知道系统运行了多长时间。 我们通过查询和保存系统当前的运行时间就可以扩展出延时、计算CPU占用率等功能来。

说到计时器,很自然就会联想到STM32上的片上计时器, 以及Cortex-M4内核计时器SysTick。这些计时器能够根据配置和驱动时钟, 以一个固定的频率产生中断,而且精度也很高。假设计时器每1ms产生一次中断,我们在中断服务函数里,对一个全局的整型变量加1, 那么这个整型变量就记录了计时器的中断次数,相应的我们就可以计算出系统的运行时间。

定时触发的上下文切换一文中,我们就已经实现了这一计时功能。 在这篇文章中,我们利用systick产生一个1kHz的中断,并通过计数使得每1s触发一次进程调度,从而使得三色灯以1Hz的频率在红灯和绿灯之间闪烁。 其中断服务函数如下:

        void SysTick_Handler(void) {
            static int counter = 0;

            if ((counter++ % 1000) == 0) {
                task_switch();
            }
        }
在SysTick_Handler中定义了一个static的局部变量counter并赋予初始值0,这个定义效果与在函数外定义一个全局变量差不多, 唯一的区别就是counter只能在SysTick_Handler中访问。对于静态变量,其内存空间和初始值在编译的时候就已经确定了, 所以每次进入函数SysTick_Handler时都不会先重新为counter开辟内存并赋初值。以至于,进入函数时counter的值就是上次退出时的值。 然后在一个if语句中通过counter++进行自增的操作。同时对counter自增之前的值与1000取模,也就是除以1000求余数。 根据C语言的规则counter++会先把counter的值返回参与取模运算,然后自加1。如果余数为0,说明距离上次进程调度已经产生了1000次计时中断, 也就是过了1s,应该再次触发进程调度了。

那么把这个counter变量和中断服务函数用作系统时钟,存在两方面的问题:

  1. counter虽然能够计时,但无法被其它函数访问。这就导致,我们无法在中断服务函数之外获知系统的运行时间。
  2. counter作为一个整型变量,在系统长时间运行后,有可能溢出。
解决第一个方面的问题很简单,只需要将counter改为一个全局的变量就好了。但因为计时器的总要性,为了防止程序任意修改counter的值, 我们应该提供一个函数获取系统时间,counter仍然不应该暴露给用户程序。为了解决第二个方面的问题, 我们就需要在counter溢出之前,为其设置一个新值。因此我们还需要提供一个函数用于设置系统时钟。

此外,我们稍微考虑一下系统的可移植性。对于Cortex系列的处理器,是有一个叫做systick的计时器,但换了一个系列或者厂家甚至架构的处理器, 就不会有这个systick了。好在系统计时的需求是很普遍的,一般都会至少有一个硬件的计时器存在。我们应当将系统时钟与计时器尽可能的解耦, 所以专门定义了一个计时函数xtos_tick(),在计时器的中断服务函数中调用。这样仅仅是调用一个计时函数, 开发者就能够很方便的根据资源和需求,选择一个计时器用于驱动系统时钟了。我们对SysTick_Handler()进行如下的修改,并定义函数xtos_tick():

        void SysTick_Handler(void) {
            xtos_tick();
        }
        void xtos_tick(void) {      
            gXtosTicks++;
        }
在SysTick_Handler()中我们只是调用了xtos_tick()。在xtos_tick()也仅仅是对一个全局变量gXtosTicks进行了自加运算, gXtosTicks就是我们前面讨论的需要一个全局的变量供其它函数访问,以获取系统的运行时间。下面给出了获取和设置系统时间的函数,目前很简单不再细讲。 针对xtos_tick(),以后将逐渐扩展出更为复杂的功能。
        uint32 xtos_get_ticks(void) {
            return gXtosTicks;
        }
        void xtos_set_ticks(uint32 ticks) {
            gXtosTicks = ticks;
        }

2. 延时函数

现在我们在系统时钟的基础上实现一个较为精准的延时函数xtos_delay_ticks()如下:

        void xtos_delay_ticks(uint32 ticks) {
            uint32 origin = gXtosTicks;
            uint32 delta = gXtosTicks - origin;
        
            while (delta < ticks) {
                xtos_schedule();
                delta = gXtosTicks - origin;
            }
        }
这个函数需要输入一个参数ticks,表示期望延迟至少ticks个计时中断。在刚进入函数时,我们用一个局部变量origin保存了当前的系统时间,作为初始时间。 用变量delta计算当前系统时间与初始时间的差值,作为已经延迟的计时中断数。然后在一个while循环中,判定是否已经满足了期望的延迟时间, 如果不满足,需要继续延迟,这里通过调用xtos_schedule()触发进程调度,切换到其它进程中。当从xtos_schedule()返回时,说明再次调度到本进程了, 此时,我们重新计算已经经过的系统时间,如果仍然不满足期望的延迟时间,则继续切换到其它进程中,直到满足为止。

这个延时函数就能够保证在一个进程等待的时候,系统去执行其它进程中的任务。之所以说它是较为精准的延时函数, 是因为我们能保证至少延迟了ticks-1个计时中断。我们不能保证延迟时间的上限,因为我们不能估计切换到本进程的时间上限, 这与进程数和其它进程的任务内容相关。如果我们使用了定时触发的上下文切换的机制,那么时间上限,就只与进程数有关系了。 延迟时间的下限之所以是ticks-1是因为,系统计时是由计时器中断触发的,用户程序在调用延时函数时一定在两次中断事件之间。 最坏的情况就是,进程刚一调用延时函数,origin保存了当前中断计数后,就产生了第二次中断事件,这样我们的延时计数就会少1。

下面我们对一直以来的两个示例进程做如下的修改,使其一个控制LED闪烁,一个控制串口不断发送字符'A'和'B'。为了得到肉眼可见的现象, 我们在任务函数中加入了延时函数。

        void taska() {
            while (1) {
                led_set_color(100, 0, 5);
                xtos_delay_ticks(1000);
                led_set_color(5, 0, 100);
                xtos_delay_ticks(1000);
            }
        }
        void taskb() {
            while (1) {
                uart4_send_byte('A');
                xtos_delay_ticks(1000);
                uart4_send_byte('B');
                xtos_delay_ticks(1000);
            }
        }
如果读者下载了我们的例程, 就可以看到LED不断地以1Hz的频率在红色和蓝色之间闪烁,同时通过串口每过1ms接收到一个'A','B'交替出现的字符。

3. 总结

在本文中,我们从延迟函数的角度出发,为系统提供了一个计时功能。实际上系统时钟,不仅仅用在延时函数上, 操作系统中任何与时间相关的功能,都与系统时钟有着密切的关系。

虽然,我目前并没有打算在其它处理器上移植XiaoTuOS,但还是稍微考虑了一点移植性的问题。把系统时钟进行了简单的封装, 降低了其与硬件相关的计时器之间的耦合度。

最后,我们在这个系统时钟的雏形上,实现了一个延迟函数。当某一个进程需要等待一段时间时,不再令处理器空转,而是切换到其它进程中执行任务。 这样可以提高系统的效率。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1