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

定时触发的上下文切换

上一篇文章中,我们为每个任务都定义了一个超级循环, 并且由任务自身触发上下文切换。这样做的一个问题就是,我们在设计任务时必须考虑上下文切换的问题,如果一个任务始终不触发切换,将导致其余任务无法被执行。

为了解决上述的问题,在操作系统中常见的做法是,为每一个任务或者进程分配一个时间片,要求每个进程运行一段时间之后,必须切换到其它进程。 如果切换的频率足够快,人们会产生一种计算机在同时执行多个任务的错觉,这也是为什么我们使用计算机时,能够同时运行好多个软件的基本原理。

为了实现这种时间片划分的机制,一般都会用到CPU上的计时器资源,定时产生一个中断,触发判定当前进程的时间片是否用完,如果用完则进行任务调度。 在Cortex-M3/M4系列的MCU中,都有一个SysTick的系统计时器,可以用来进行任务管理和触发上下文切换。本文将详细介绍SysTick的结构和用法, 并在此基础上实现一个定时触发的上下文切换机制。

1. SysTick的结构

SysTick是Cortex-M4内核中设计的一个24位向下计数的计时器,一般会被操作系统拿来用作系统的计时器。因为它是在处理器内核中实现的, 只要是Cortex-M3/M4架构的处理器都会有这个计时器的,系统的可移植性好。图1是SysTick的结构简图,它一共就只有四个寄存器。

图1 SysTick结构简图

其中SysTick->VAL中记录了当前的计数值,在每个系统时钟或者参考时钟的上升沿到来之际,该寄存器中的值就向下减一。 当减到0时,SysTick-VAL将从SysTick->LOAD寄存器中装载新的初始值,重新计数。同时如果SysTick->CTRL.TICKINT位置1了,将触发SysTick中断。

SysTick->CTRL是其控制寄存器,其中Enable位控制开启计数,TICKINT位控制是否产生中断,CLKSOURCE位指定了SysTick的时钟源,CONTFLAG则标记了计时器是否计数到0过。 在STM32中,CLKSOURCE置位标识使用的是CPU时钟,若为0则由RCC通过AHB总线时钟8分频后作为参考时钟驱动SysTick,相当于CPU时钟的1/8。

SysTick->CALIB是其校准数值寄存器。它是一个只读寄存器,其中NOREF标记了MCU实现是否提供了参考时钟,STM32中该位应为0。 SKEW标识着校准值是否为准确的10ms,TENMS则是校准值它是10ms内VAL寄存器应有的倒计数值,如果该位读0则不能确定校准值是否准确。 在我看来这个校准值寄存器可以用来参考实现一个较为精确的计时功能。

这样看来,SysTick的配置过程很简单。如下例程中,我们只需根据切换频率提供一个重装载数据,指定时钟源,开启计时中断就完成了配置。 其中第5行是配置优先级的,这里暂时设定为最高的优先级。需要说明的是SysTick是Cortex-M4内核提供的计数器,它的中断优先级是由系统控制模块SCB来管理的,而其他STM32提供的外设的中断是由中断控制器NVIC来管理的。

        void systick_init(uint32 ticks) {
            SYSTICK->LOAD.bits.cnt = ticks;         // 重装载数据
            SYSTICK->VAL.bits.cnt = 0;              // 清除当前计数

            SCB->SHP[SysTicks_Irq_n - 4] = 0x00;    // 暂时赋予最高优先级

            SYSTICK->CTRL.bits.clksource = 1;       // 使用CPU时钟168MHz
            SYSTICK->CTRL.bits.tickint = 1;         // 开启计数溢出中断
            SYSTICK->CTRL.bits.en = 0;              // 暂不开启计时器
        }

2. 使用SysTick定时触发上下文切换

首先我们对具有上下文切换的任务框架中定义的两个任务做些简单的修改。 我们去掉了延时函数,也不在超级循环中主动请求进程切换了。我们将要在SysTick的中断函数中进行切换。

        void taska() {
            while (1) {
                LED_R = ON;
                LED_G = OFF;
                LED_B = OFF;
            }
        }
        void taskb() {
            while (1) {
                LED_R = OFF;
                LED_G = ON;
                LED_B = OFF;
            }
        }

在main函数中,我们先对三色灯和SysTick进行初始化。因为SysTick使用的是CPU的168MHz的时钟,systick_init的参数168000将使得系统每一个毫秒产生一次中断。 这里存在着一个矛盾,系统在各个进程之间切换的越频繁,用户就越容易被欺骗,认为系统是在同时执行很多个任务的,但频繁的切换进程,大部分时间都拿来做上下文切换了, 会带来额外的开销使得处理器的利用率降低。这是一种浪费的行为,所以我们需要根据实际情况合理的安排进程切换的频率。

接着创建了两个任务taskA和taskB,并使得gp_xtos_next_task指向taskA以保证xtos开启后第一个执行的任务就是taskA。最后我们打开SysTick计时器, 并调用xtos_start开启操作系统。

        int main(void) {
            led_init();
            systick_init(168000);

            xtos_create_task(&taskA, taska, &taskA_Stk[TASKA_STK_SIZE - 1]);
            xtos_create_task(&taskB, taskb, &taskB_Stk[TASKB_STK_SIZE - 1]);

            gp_xtos_next_task = &taskA;

            SYSTICK->CTRL.bits.en = 1;
            xtos_start();

            while(1) {}
        }

taska和taskb都不会主动切换进程,我们要在systick的中断服务函数中触发这一操作。为了使我们能够通过肉眼看到三色灯的变化, 这里在systick的服务函数中添加了一个静态变量,计时一秒后触发切换。

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

            if ((counter++ % 1000) == 0) {
                task_switch();
            }
        }

这里打包了本文示例中所用的代码和工程文件, 不出意外的话,编译下载到芯片上之后就可以看到三色灯以一秒的频率在红灯和绿灯之间变换。

3. 总结

在Cortex-M3/M4架构的处理器内核中就有一个24位的计时器SysTick。一般操作系统都会用它来计时触发上下文切换,这样做系统具有比较高的可移植性,我们也采用相同的方式。

针对操作系统进程切换,我们需要合理的设计切换频率,以获得较高的资源利用率,同时满足各个进程的使用需求。 在本文的例程中,我们实现了一个两个进程以一秒的频率相互切换的程序。




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