自制嵌入式实时操作系统

    返回首页    发表留言
本文作者:李德强
          第一节 任务切换
 
 

        自制操作系统需要解决的一个最核心的问题就是多任务并行。我们并不希望操作系统中只运行一个任务,而是希望操作系统中能够同时运行多个任务,并可以自由的在多个任务之间进行切换。因此我们首先需要考虑任务切换问题。为了解决这个问题,我们需要考虑一下三个内容:系统心跳、调度中断、任务切换。下面我们来逐个讲述这几个内容。

一、系统心跳

        所谓的系统心跳,本质上是一个时钟计时器,它通常被称为“时钟滴答”,英文为SystemTick。其本质上讲就是一个时钟中断,即每隔一小段时间(通常是1ms)触发一次中断,在这个中断服务程序中完成操作系统调度的所有功能。我们在Cortex-M架构的嵌入式处理器下,可以使用处理器自带的系统时钟中断来实现心跳中功能,也可以使用其定时器中断来实现心跳功能。下面分别来介绍这两个时钟中断。

1.系统时钟中断

       我们可以将系统时钟设置为每1ms触发一次中断。通常,处理器频率为72MHz/s,如果我们需要将系统时钟设置为72分频,则系统时钟将会每1us触发一次中断,但这无疑太快了。因此我们将系统时钟设置为72 * 1000分频,系统时钟将会每1ms触发一次中断。       

//初始化系统时钟
void sysclk_init()
{
	//分频
	if (SysTick_Config(72 * 1000))
	{
		while (1)
		{
		}
	}
	//关闭时钟
	SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk;

	//等待1毫秒
	for (int i = 0; i < 72 * 1000; i++)
	{
	}

	//打开时钟
	SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk;
}

//系统时钟中断,频率1000Hz
void SysTick_Handler()
{
	//系统调度功能
}

        当系统时钟中断被触发之后,处理器会自动调用并执行SystTick_Handler()函数,此函数就是其中断服务函数。也就是说,当我们配置好系统时钟中断之后,处理器会每隔1ms执行一次中断函数。这就是我们需要完成的系统心跳函数。

2.定时器中断

        通常在Cortex-M处理器内部都具有多个定时器,这些定时器同常命名为TIM1、TIM2、TIM3、TIM4……TIM7、TIM8、TIM9等。在这里我们以TIM1为例讲述如何将其作为操作系统的心跳中断时钟。

        同样的,我们出要对TIM1进行初始化,设定其分频值和周期值,然后启动TIM1:

void tim1_init()
{
	TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_TIM1, ENABLE);
	//时钟计数器周期
	TIM_TimeBaseStructure.TIM_Period = 1000 - 1;
	//设定为72分频
	TIM_TimeBaseStructure.TIM_Prescaler = 72 - 1;
	TIM_TimeBaseStructure.TIM_ClockDivision = 0;
	//向上计数模式
	TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
	TIM_TimeBaseStructure.TIM_RepetitionCounter = 0;
	TIM_TimeBaseInit(TIM1, &TIM_TimeBaseStructure);
	TIM_ClearFlag(TIM1, TIM_FLAG_Update);
	TIM_ITConfig(TIM1, TIM_IT_Update | TIM_IT_Trigger, ENABLE);

	//设定中断函数
	NVIC_InitStructure.NVIC_IRQChannel = TIM1_UP_IRQn;
	//设定主中断优先级
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
	//设定次中断优先级
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&NVIC_InitStructure);

	//使能TIM1时钟
	TIM_Cmd(TIM1, ENABLE);
}

void TIM1_UP_IRQHandler(void)
{
	//判断是否合法中断
	if (TIM_GetITStatus(TIM1, TIM_IT_Update) != RESET)
	{
		//清除终端标识,否则无法进入下一次中断
		TIM_ClearITPendingBit(TIM1, TIM_IT_Update);
		//系统调度功能
	}
}

        当TIM1时钟中断被触发之后,处理器会自动调用并执行TIM1_UP_IRQHandler()函数,此函数就是TIM1的中断服务函数。处理器会每隔1ms执行一次中断函数。这我们就完成了系统心跳函数。

 

二、调度中断

        在Cortex-M架构的处理器中通常是使用PendSV_Handler()中断来完成系统的任务调度的。PendSV_Handler的本意是可挂起异常,也就是说当这个中断函数被执行时,如果有其他中断服务被触发,PendSV_Handler将会被挂起,直到其他中断服务执行完毕之后PendSV_Handler才会继续执行。这样的设定有利于操作系统的实时性。例如当操作系统执行调度中断时,如果其他定时器中断、串行总线中断或者SPI中断等被触发,处理器会将PendSV_Handler中断服务挂起,优先执行其他中断服务,当这些中断服务执行完毕之后再恢复PendSV_Handler的执行。但是在操作系统调度的过程中,我们并不希望被其他高优先级的中断所打断,因此通常在开始调度时关闭处理器的中断功能,而在完成调度之后在开启中断功能。

        为了能够使用PendSV_Handler中断服务,我们需要对其进行配置:

@汇编代码
@PendSV中断控制器地址
.equ  NVIC_INT_CTRL,      0xE000Ed04 
@触发PendSV
.equ  NVIC_PENDSV_SET,    0x10000000 
@PendSV优先级控制地址
.equ  NVIC_SYSPRI2,       0xE000Ed22 
@PendSV设置为最低优先值255
.equ  NVIC_PENDSV_PRI,    0x000000ff

@设置中断优先级为最低
LDR     R0, =NVIC_SYSPRI2                                 
LDR     R1, =NVIC_PENDSV_PRI
STRB    R1, [R0]

@触发pendsv异常
LDR     R0, =NVIC_INT_CTRL                                  
LDR     R1, =NVIC_PENDSV_SET
STR     R1, [R0]

 

三、任务切换

        操作系统实现宏观上并行任务的效果,其本质是微观上在多个任务之间切换。因此,当操作系统由任务A切换到任务B时,需要暂停任务A的执行,并将处理器执行任务A时各个寄存器的值保存到内存的某个位置上,这个过程叫做保存现场;之后将处理器上一次执行任务B时各个寄存器的值由内存中的某个位置回复到处理的寄存器当中,这个过程叫做恢复现场。当操作系统在调度时由任务A切换到任务B时,对任务A保存现场,对任务B恢复现场,这个整体的过程称之为:上下文切换

        在每一个任务在执行之前,操作系统都需要为其分配一个专属的内存区域供其使用,这个内存区域同常被称为此任务的栈(stack)。Cortex-M有两个堆栈寄存器,主栈指针(MSP)与进程栈指针(PSP)。主程序操作系统和各个中断函数使用的是MSP指针,而各个被调度的任务(进程)使用的是PSP指针。本质上它是处理器的一个寄存器,PSP寄存器的值就是内存中任务的栈地址。

1.保存现场

        处理器会自动将任务A执行时处理器中xPSR,PC,LR,R12,R0-R3这些寄存器自动压入PSP所指向栈内存地址,而后操作系统需要将R4-R11这些寄存器压入PSP所指向的栈内存地址。如下图:

        我们需要编写操作系统保存R4-R11到任务栈的代码如下:

@通用寄存器R4-R11,一共是8个字的寄存器,占用空间是4*8=32byte
SUBS	R0, R0, #0x20
@保存寄存器R4-R11到PSP的地址
STM		R0, {R4-R11}

2.恢复现场

        保存现场结束之后,操作系统将对任务B进行恢复现场操作,实际上,恢复现场即为保存现场的逆操作,如下图:

        同样的,操作系统需要将R4-R11出栈并恢复到处理器的各个寄存器当中,之后处理器自动将xPSR、PC、LR、R12、R0-R3自动出栈并恢复到寄存器当中。代码如下:

@R0加载
LDR		R0, =Stack_B
LDR		R0, [R2]
@恢复Stack_B的R4-R11的8个寄存器	
LDM		R0, {R4-R11} 
@更新栈地址的指针到R0	
ADDS	R0, R0, #0x20

@更新R0到PSP
MSR		PSP, R0 
@栈地址为用户栈指针PSP
ORR		LR, LR, #0x04
@执行任务切换
BX LR

        需要特别注意的是,当恢复现场完成后,需要将PSP指针更新为任务B在上一次执行后的栈内存地址,并设定栈地址为用户栈指针。最后通过BX LR指令完成任务切换。

 

    返回首页    返回顶部
  看不清?点击刷新

 

  Copyright © 2015-2023 问渠网 辽ICP备15013245号