RTOS系列(1):基础知识——中断嵌套
RTOS系列文章(2):PendSV功能,为什么需要PendSV
RTOS系列文章(3): 为什么将SysTick和PendSV的优先级设置为最低
RTOS系列文章(4): MDK软件仿真 + Debug-(printf)-Viewer使用方法
RTOS系列文章(5):C语言程序运行原理分析:汇编、栈、栈帧、进栈、出栈、保存现场、恢复现场、返回
RTOS系列文章(6):Cortex-M3/4之SP,MSP,PSP,Thread模式、Handler模式、内核态、用户态
RTOS系列文章(7):CM3/4之LR寄存器、EXC_RETURN深入分析
RTOS系列文章(8):深入分析中断处理过程
RTOS系列文章(9):再次分析栈帧、函数调用与中断调用的区别
在前面的系列文章中,我们详细分析了RTOS依赖的基础知识,比如函数调用、中断、MSP、PSP、中断处理过程等等,这些基础知识,对于非RTOS程序员来说,可以不关心,因为基本上是用不到的,但是想要了解RTOS运行原理的程序员来说,这些基础知识就必不可少了。本文我们实现一个最简单的RTOS,来说明RTOS的运行原理,其他像uCOS、freeRTOS等,核心原理也是一样的。
main.c
#include "led.h"
#include "delay.h"
#include "sys.h"
#include <stdio.h>
#define HW32_REG(ADDRESS) (*((volatile unsigned long *)(ADDRESS)))
#define stop_cpu __breakpoint(0)
void task0(void);
void task1(void);
void task2(void);
void task3(void);
// event to tasks
uint32_t task0_stack[64];
uint32_t task1_stack[64];
uint32_t task2_stack[64];
uint32_t task3_stack[64];
//data use by OS
uint32_t curr_task = 0; // current task
uint32_t next_task = 1; // next task
uint32_t PSP_array[4]; // process stack pointer for each task
volatile unsigned int g_systick_cnt = 0;
void SysTick_Handler(void)
{
g_systick_cnt++;
switch(curr_task){
case 0:
next_task = 1;
break;
case 1:
next_task = 2;
break;
case 2:
next_task = 3;
break;
case 3:
next_task = 0;
break;
default:
next_task = 0;
stop_cpu;
break;
}
if(curr_task != next_task){
SCB->ICSR |= SCB_ICSR_PENDSVSET_Msk;
}
return;
}
__ASM void PendSV_Handler(void)
{
// save current context
MRS R0, PSP
STMDB R0!, {R4-R11}
LDR R1, =__cpp(&curr_task)
LDR R2,[R1]
LDR R3, =__cpp(&PSP_array)
STR R0, [R3, R2, LSL #2]
// load next context
LDR R4, =__cpp(&next_task)
LDR R4,[R4] // get next task ID
STR R4,[R1] // set curr_task = next_task
LDR R0,[R3, R4, LSL #2] // load PSP value form PSP_array
LDMIA R0!, {R4-R11} //load R4-R11 from task stack
MSR PSP, R0
BX LR
ALIGN 4
}
void task0(void)
{
while(1){
if(g_systick_cnt & 0x80){
LED0 = 0;
}
}
}
void task1(void)
{
while(1){
if(g_systick_cnt & 0x100){
LED0 = 1;
}
}
}
void task2(void)
{
while(1){
if(g_systick_cnt & 0x200){
LED1 = 0;
}
}
}
void task3(void)
{
while(1){
if(g_systick_cnt & 0x400){
LED1 = 1;
}
}
}
int main(void)
{
delay_init();
LED_Init();
LED0 = 0;
LED1 = 0;
// enable double word stack
SCB->CCR |= SCB_CCR_STKALIGN_Msk;
// start the task scheduler
// crate stack frame for task0
PSP_array[0] = ((unsigned int)task0_stack) + sizeof(task0_stack) - 16 * 4;
HW32_REG((PSP_array[0] + (14 << 2))) = (unsigned long)task0;
HW32_REG((PSP_array[0] + (15 << 2))) = 0x01000000; // init the xPSR
// crate stack frame for task1
PSP_array[1] = ((unsigned int)task1_stack) + sizeof(task1_stack) - 16 * 4;
HW32_REG((PSP_array[1] + (14 << 2))) = (unsigned long)task1;
HW32_REG((PSP_array[1] + (15 << 2))) = 0x01000000; // init the xPSR
// crate stack frame for task2
PSP_array[2] = ((unsigned int)task2_stack) + sizeof(task2_stack) - 16 * 4;
HW32_REG((PSP_array[2] + (14 << 2))) = (unsigned long)task2;
HW32_REG((PSP_array[2] + (15 << 2))) = 0x01000000; // init the xPSR
// crate stack frame for task3
PSP_array[3] = ((unsigned int)task3_stack) + sizeof(task3_stack) - 16 * 4;
HW32_REG((PSP_array[3] + (14 << 2))) = (unsigned long)task3;
HW32_REG((PSP_array[3] + (15 << 2))) = 0x01000000; // init the xPSR
curr_task = 0;
__set_PSP((PSP_array[curr_task] + 16 * 4)); // set PSP to top of task0 stack
// set PendSV to lowest possible priority
NVIC_SetPriority(PendSV_IRQn, 0xFF);
__set_CONTROL(0x03); // switch to use PSP, unprivileged state
__ISB();
task0();
while(1){
stop_cpu;
}
}
delay_init()内部实现
#define OS_TICKS_PER_SEC 1000u /* Set the number of ticks in one second */
void delay_init()
{
u32 reload;
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); // set systick clock: HCLK/8
fac_us=SystemCoreClock/8000000; // 1/8
reload = SystemCoreClock/8000000;
reload *= 1000000/OS_TICKS_PER_SEC; // calucute the reload value
SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk; //
SysTick->LOAD=reload; // set reload value
SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk; // enable systick
}

- Systick Handler里只做调度计算,计算是否需要切换任务,不做具体的任务调度。
- PendSV Handler中进行任务切换,即上下文切换。
- 任务切换核心是:保存当前运行任务的现场,CPU自动保存一半,OS保存一半。然后从下一个将要运行的任务堆栈中恢复现场,OS只需要恢复一半,剩下的交给CPU。
- !!!核心点在于,任务调度涉及到2次PSP操作,开始上下文切换时,原来存储的任务psp是不能用的,需要取当前正在运行的PSP(全局寄存器) ,因为程序运行可能会动态使用堆栈空间。保存现场后,需要将最新的psp值存储到PSP_array中,便于下次调度运行这个任务时,可以恢复现场。切换到下一个任务,psp顺序是反方向,先从保存的next_task的堆栈psp开始,恢复现场,然后将恢复一半后的psp值赋值给PSP,然后退出中断,CPU根据PSP值,自动出栈,恢复另外一半值,根据PC值,切换到next_task.