一步步学习操作系统(1)——参照ucos,在STM32上实现一个简单的多任务(“啰里啰嗦版”)
该篇为“啰里啰嗦版”,另有相应的“精简版”供参考
“不到长城非好汉;不做OS,枉为程序员”
OS之于程序员,如同梵蒂冈之于天主教徒,那永远都是块神圣的领土。若今生不能亲历之,实乃憾事!
但是,圣域不是想进就能进的呀……
OS融合了大量的计算机的基础知识,各个知识领域之间交织紧密,初来乍到者一不小心就会绕出个死结。
我的方法是:死结就死结,不管三七二十一,直接剪断,先走下去再说,回头我们再把这个剪断的死结再接上。
我们知道,“多任务”一般在介绍OS的书籍中,是属于中间或者靠后的部分,又或者分散在各个章节中。而我决定上手就说它。
一、整体纵览:
1、硬件:
STM32F103RC
2、IDE:
MDK5
3、文件架构:
(1)标准文件:
startup_stm32f10x_hd.s:STM32官方启动文件(注意:这是针对stm32硬件配置的文件,型号要是不同,你的可能和我不一样哦)
(2)自编文件——这才是我们的重头戏哦(共5个文件:1x".asm"+2x".c"+2x".h"):
main.c:主函数和任务定义;
os_cpu_a.asm:中断与任务切换;
myos.c:硬件初始化与任务切换时的堆栈保存;
include.h和myos.h:两个头文件。
二、逐文解析:
1、main.c
顺着main函数这条主线,我们看到最终的OS其实就是执行了7行代码,共5个函数:
1、OSInit();
2、OSTaskCreate(Task1, (void*)0, (OS_STK*)&Task1Stk[TASK_STACK_SIZE-1]);
3、OSTaskCreate(Task2, (void*)0, (OS_STK*)&Task2Stk[TASK_STACK_SIZE-1]);
4、OSTaskCreate(Task3, (void*)0, (OS_STK*)&Task3Stk[TASK_STACK_SIZE-1]);
5、SysTickInit(5);
6、LedInit();
7、OSStart(); 简单说说main函数功能:
当OSStart()执行之后,Task1、Task2、Task3轮流执行,即Task1()、Task2()、Task3()三个函数轮流执行:
Task1()->Task2()->Task3()->Task1()->Task2()->Task3()->Task1()->......
每个任务(或函数)执行相等的时间片,并有SysTick中断来触发PendSV中断,从而实现任务切换。
OSStart()执行之后,永不返回。
各个函数基本做了些什么,代码后面都附加了注解。
其中需要注意的地方是:OSStart()。
这个函数一旦执行了就不会返还,有点像死循环(但不是死循环哦,后来会明白的)。
仔细想想后,确实也应当如此,如果main函数return掉了的话,程序也就结束啦!
“main函数结束”就意味着CPU现在只会喝喝茶、看看报了,什么抢劫、着火它都装作没看见。 main函数就这么短,那么上面七个函数的实现在哪里呢?
这时你肯定想到"#include"了吧!
“太好了!#include只有一行!”
那我们去include.h那里看看吧。
#include "include.h"
extern OS_TCB OSTCBTbl[OS_MAX_TASKS]; // (OS Task Control Block Table)
extern OS_STK TASK_IDLE_STK[TASK_STACK_SIZE]; //("TaskIdle" Stack)
extern OS_TCB *OSTCBCur; // Pointer to the current running task(OS Task Control Block Current)
extern OS_TCB *OSTCBNext; // Pointer to the next running task(OS Task Control Block Next)
extern INT8U OSTaskNext; // Index of the next task
extern INT32U TaskTickLeft; // Refer to the time ticks left for the current task
extern INT32U TimeMS; // For system time record
extern INT32U TaskTimeSlice; // For system time record OS_STK Task1Stk[TASK_STACK_SIZE]; // initialize stack for task1
OS_STK Task2Stk[TASK_STACK_SIZE]; // initialize stack for task2
OS_STK Task3Stk[TASK_STACK_SIZE]; // initialize stack for task3 void Task1(void *p_arg); // flip the led1 every 0.5s
void Task2(void *p_arg); // flip the led2 every 1.0s
void Task3(void *p_arg); // do nothing int main(void)
{ OSInit(); // OS initialization
OSTaskCreate(Task1, (void*), (OS_STK*)&Task1Stk[TASK_STACK_SIZE-]); // create task 1
OSTaskCreate(Task2, (void*), (OS_STK*)&Task2Stk[TASK_STACK_SIZE-]); // create task 2
OSTaskCreate(Task3, (void*), (OS_STK*)&Task3Stk[TASK_STACK_SIZE-]); // create task 3
SysTickInit(); // configure the SysTick as 5ms
LedInit(); // leds initialization
OSStart(); // start os! return ; // never come here
} void Task1(void *p_arg)
{
while() {
delayMs(); // delay 100 * 5ms = 0.5s
LED1TURN(); // flip the switch of led1
}
}
void Task2(void *p_arg)
{
while() {
delayMs(); // delay 200 * 5ms = 1.0s
LED2TURN(); // flip the switch of led2
}
} void Task3(void *p_arg)
{
while() {
}
}
小白兔笔记:
(1)“啥是extern变量啊?”
"快去复习复习c语言教程吧。"
(2)"OS_TCB、OS_MAX_TASKS什么的都是些啥?“
"myos.h"里都有它们的定义。
(3)”'OSTCBCur'都是些啥怪名字?"
“针对词义复杂的变量,注意看定义那行注释,后面的括号会有对变量的简短说明,如:
extern OS_TCB *OSTCBCur; // Pointer to the current running task(OS Task Control Block Current)
2、include.h
“我去!这不是欺骗我感情吗?main函数倒是1个#include,怎么到了这里却又来了三个!”
等等!先别灰心嘛,容我慢慢道来。
我们知道,main里的
#include "include.h"
其实就等于:
#include <stdlib.h>
#include "myos.h"
#include "stm32f10x.h"
但是我们为什么要那么费劲再弄一个"inlcude.h"文件呢?
假设我们想给这个OS再加个内存管理的功能,于是要添加几个".c"文件,而这些文件也要包含上面那几个"#include",那么我们不是要再把那几个“#include”都写一遍吗?
不过,有了这个include.h之后,这些".c"文件只要
#include "include.h"就可以了。
多加了1个"include.h",但却清爽了n个”xxxxx.c"文件。
当然,这只是好处之一,其他的就不多说了,毕竟我们的主题是OS嘛。
#ifndef INCLUDE_H
#define INCLUDE_H #include <stdlib.h>
#include "myos.h"
#include "stm32f10x.h" //stm32官方头文件 #endif
小白兔笔记:
(1)“#ifndef INCLUDE_H之类的东西是什么意思?”
这是为了防止多重包含头文件。
既然你问了这个问题,估计你也听不懂啥是”多重包含头文件“。
这是和编译器有关的约定,简单点说,不这么写,编译器”可能“——我说”可能“——找你茬。
(2)”stm32f10x.h“是官方根据stm32的各种硬件配置编写的头文件,可要找准着你的你自己的硬件配置使用哦!
3、myos.h
#ifndef MYOS_H
#define MYOS_H
#include "stm32f10x.h" /**********CPU DEPENDENT************/
#define TASK_TIME_SLICE 5 // 5ms for every task to run every time typedef unsigned char INT8U; // Unsigned 8 bit quantity
typedef unsigned short INT16U; // Unsigned 16 bit quantity
typedef unsigned int INT32U; // Unsigned 32 bit quantity typedef unsigned int OS_STK; // Each stack entry is 32-bit wide(OS Stack) // assembling functions
void OS_ENTER_CRITICAL(void); // Enter Critical area, that is to disable interruptions
void OS_EXIT_CRITICAL(void); // Exit Critical area, that is to enable interruptions
void OSCtxSw(void); // Task Switching Function(OS Context Switch)
void OSStart(void); OS_STK* OSTaskStkInit(void (*task)(void *p_arg), // task function
void *p_arg, // (pointer of arguments)
OS_STK *p_tos); // (pointer to the top of stack)
/**********CPU INDEPENDENT************/ #define OS_MAX_TASKS 16 #define TASK_STATE_CREATING 0
#define TASK_STATE_RUNNING 1
#define TASK_STATE_PAUSING 2 #define TASK_STACK_SIZE 64 #define LED1TURN() (GPIOA->ODR ^= 1<<8) // reverse the voltage of LED1 !!!HARDWARE RELATED
#define LED2TURN() (GPIOD->ODR ^= 1<<2) // reverse the voltage of LED2 !!!HARDWARE RELATED typedef struct os_tcb {
OS_STK *OSTCBStkPtr; // (OS Task Control Block Stack Pointer)
INT8U OSTCBStat; // (OS Task Control Block Status)
} OS_TCB; // (OS Task Control Block) void OSInit(void); // (OS Initialization)
void LedInit(void);
void OS_TaskIdle(void *p_arg);
void OSInitTaskIdle(void); // (OS Initialization of "TaskIdle")
void OSTaskCreate(void (*task)(void *p_arg), // task function
void *p_arg, // (pointer of arguments)
OS_STK *p_tos); // (pointer to the top of stack)
void OSTCBSet(OS_TCB *p_tcb, OS_STK *p_tos, INT8U task_state); void SysTickInit(INT8U Nms); // (System Tick Initialization)
void SysTick_Handler(void); // The interrupt function INT32U GetTime(void);
void delayMs(volatile INT32U ms); // The argument can't be too large #endif
这是最后一个头文件了,也是一个简单易懂的文件。同时也是最后的平原,过了这个平原,我们可就要翻雪山啦!
(1)简单说说2个函数:
void OS_ENTER_CRITICAL(void);
void OS_EXIT_CRITICAL(void);
有一种东西叫“临界区”(CRITICAL),这些所谓“临界区”指的是一些变量所在的内存,可以直接理解成“就是些特殊变量”。
要访问这些变量必须得关掉“中断”,访问结束后再开启“中断”,开关“中断”就是这两个函数的任务了。
猜猜看哪个是开“中断”,哪个是关“中断”呢?
(2)系统时钟中断void SysTick_Handler(void)的由来:
来自官方启动文件startup_stm32f10x_hd.s。
小白兔笔记:
小白兔表示“感觉不会再爱了……”
4、os_cpu_a.asm和myos.c:
上文说过,这两个文件关系暧昧,扯开来只讲其中一个很没味道,这里先把它们都贴出来:
os_cpu_a.asm(“;”后的注释是对应"C"语言的解释):
IMPORT OSTCBCur
IMPORT OSTCBNext EXPORT OS_ENTER_CRITICAL
EXPORT OS_EXIT_CRITICAL
EXPORT OSStart
EXPORT PendSV_Handler
EXPORT OSCtxSw NVIC_INT_CTRL EQU 0xE000ED04 ; Address of NVIC Interruptions Control Register
NVIC_PENDSVSET EQU 0x10000000 ; Enable PendSV
NVIC_SYSPRI14 EQU 0xE000ED22 ; System priority register (priority 14).
NVIC_PENDSV_PRI EQU 0xFF ; PendSV priority value (lowest). PRESERVE8 ; align 8 AREA |.text|, CODE, READONLY
THUMB ;/******************OS_ENTER_CRITICAL************/
OS_ENTER_CRITICAL
CPSID I ; Enable interruptions(Change Processor States: Interrupts Disable)
BX LR ; Return ;/******************OS_EXIT_CRITICAL************/
OS_EXIT_CRITICAL
CPSIE I ; Disable interruptions
BX LR ; Return ;/******************OSStart************/
OSStart
; disable interruptions
CPSID I ; OS_ENTER_CRITICAL();
; initialize PendSV
; Set the PendSV exception priority
LDR R0, =NVIC_SYSPRI14 ; R0 = NVIC_SYSPRI14;
LDR R1, =NVIC_PENDSV_PRI ; R1 = NVIC_PENDSV_PRI;
STRB R1, [R0] ; *R0 = R1; ; initialize PSP as 0
; MOV R4, #0
LDR R4, =0x0 ; R4 = 0;
MSR PSP, R4 ; PSP = R4; ; trigger PendSV
LDR R4, =NVIC_INT_CTRL ; R4 = NVIC_INT_CTRL;
LDR R5, =NVIC_PENDSVSET ; R5 = NVIC_PENDSVSET;
STR R5, [R4] ; *R4 = R5; ; enable interruptions
CPSIE I ; OS_EXIT_CRITICAL(); ; should never get here
; a endless loop
OSStartHang
B OSStartHang ;/******************PendSV_Handler************/
PendSV_Handler
CPSID I ; OS_ENTER_CRITICAL();
; judge if PSP is 0 which means the task is first invoked
MRS R0, PSP ; R0 = PSP;
CBZ R0, PendSV_Handler_NoSave ; if(R0 == 0) goto PendSV_Handler_NoSave; ; R12, R3, R2, R1
SUB R0, R0, #0x20 ; R0 = R0 - 0x20; ; store R4
STR R4 , [R0] ; *R0 = R4;
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;
; store R5
STR R5 , [R0] ; *R0 = R5;
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;
; store R6
STR R6 , [R0] ; *R0 = R6;
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;
; store R7
STR R7 , [R0] ; *R0 = R7;
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;
; store R8
STR R8 , [R0] ; *R0 = R8;
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;
; store R9
STR R9, [R0] ; *R0 = R4;
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;
; store R10
STR R10, [R0] ; *R0 = R10;
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;
; store R11
STR R11, [R0] ; *R0 = R11;
ADD R0, R0, #0x4 ; R0 = R0 + 0x4; SUB R0, R0, #0x20 ; R0 = R0 - 0x20; ; easy method
;SUB R0, R0, #0x20
;STM R0, {R4-R11} LDR R1, =OSTCBCur ; R1 = OSTCBCur;
LDR R1, [R1] ; R1 = *R1;(R1 = OSTCBCur->OSTCBStkPtr)
STR R0, [R1] ; *R1 = R0;(*(OSTCBCur->OSTCBStkPrt) = R0) PendSV_Handler_NoSave
LDR R0, =OSTCBCur ; R0 = OSTCBCur;
LDR R1, =OSTCBNext ; R1 = OSTCBNext;
LDR R2, [R1] ; R2 = OSTCBNext->OSTCBStkPtr;
STR R2, [R0] ; *R0 = R2;(OSTCBCur->OSTCBStkPtr = OSTCBNext->OSTCBStkPtr) LDR R0, [R2] ; R0 = *R2;(R0 = OSTCBNext->OSTCBStkPtr)
; LDM R0, {R4-R11}
; load R4
LDR R4, [R0] ; R4 = *R0;(R4 = *(OSTCBNext->OSTCBStkPtr))
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;(OSTCBNext->OSTCBStkPtr++)
; load R5
LDR R5, [R0] ; R5 = *R0;(R5 = *(OSTCBNext->OSTCBStkPtr))
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;(OSTCBNext->OSTCBStkPtr++)
; load R6
LDR R6, [R0] ; R6 = *R0;(R6 = *(OSTCBNext->OSTCBStkPtr))
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;(OSTCBNext->OSTCBStkPtr++)
; load R7
LDR R7 , [R0] ; R7 = *R0;(R7 = *(OSTCBNext->OSTCBStkPtr))
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;(OSTCBNext->OSTCBStkPtr++)
; load R8
LDR R8 , [R0] ; R8 = *R0;(R8 = *(OSTCBNext->OSTCBStkPtr))
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;(OSTCBNext->OSTCBStkPtr++)
; load R9
LDR R9 , [R0] ; R9 = *R0;(R9 = *(OSTCBNext->OSTCBStkPtr))
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;(OSTCBNext->OSTCBStkPtr++)
; load R10
LDR R10 , [R0] ; R10 = *R0;(R10 = *(OSTCBNext->OSTCBStkPtr))
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;(OSTCBNext->OSTCBStkPtr++)
; load R11
LDR R11 , [R0] ; R11 = *R0;(R11 = *(OSTCBNext->OSTCBStkPtr))
ADD R0, R0, #0x4 ; R0 = R0 + 0x4;(OSTCBNext->OSTCBStkPtr++) MSR PSP, R0 ; PSP = R0;(PSP = OSTCBNext->OSTCBStkPtr)
; P42
; P139 (key word: EXC_RETURN)
; use PSP
ORR LR, LR, #0x04 ; LR = LR | 0x04;
CPSIE I ; OS_EXIT_CRITICAL();
BX LR ; return; OSCtxSw ;OS context switch
PUSH {R4, R5}
LDR R4, =NVIC_INT_CTRL ; R4 = NVIC_INT_CTRL
LDR R5, =NVIC_PENDSVSET ; R5 = NVIC_PENDSVSET
STR R5, [R4] ; *R4 = R5
POP {R4, R5}
BX LR ; return; align
end
myos.c:
#include "myos.h"
#include "stm32f10x.h" OS_TCB OSTCBTbl[OS_MAX_TASKS]; // (OS Task Control Block Table)
OS_STK TASK_IDLE_STK[TASK_STACK_SIZE]; //("TaskIdle" Stack)
OS_TCB *OSTCBCur; // Pointer to the current running task(OS Task Control Block Current)
OS_TCB *OSTCBNext; // Pointer to the next running task(OS Task Control Block Next)
INT8U OSTaskNext; // Index of the next task
INT32U TaskTickLeft; // Refer to the time ticks left for the current task
INT32U TimeMS;
INT32U TaskTimeSlice;
char * Systick_priority = (char *)0xe000ed23;
// Initialize the stack of a task, it is of much relationship with the specific CPU
OS_STK* OSTaskStkInit(void (*task)(void *p_arg),
void *p_arg,
OS_STK *p_tos)
{
OS_STK *stk;
stk = p_tos; *(stk) = (INT32U)0x01000000L; // xPSR
*(--stk) = (INT32U)task; // Entry Point // Don't be serious with the value below. They are of random
*(--stk) = (INT32U)0xFFFFFFFEL; // R14 (LR)
*(--stk) = (INT32U)0x12121212L; // R12
*(--stk) = (INT32U)0x03030303L; // R3
*(--stk) = (INT32U)0x02020202L; // R2
*(--stk) = (INT32U)0x01010101L; // R1 // pointer of the argument
*(--stk) = (INT32U)p_arg; // R0 // Don't be serious with the value below. They are of random
*(--stk) = (INT32U)0x11111111L; // R11
*(--stk) = (INT32U)0x10101010L; // R10
*(--stk) = (INT32U)0x09090909L; // R9
*(--stk) = (INT32U)0x08080808L; // R8
*(--stk) = (INT32U)0x07070707L; // R7
*(--stk) = (INT32U)0x06060606L; // R6
*(--stk) = (INT32U)0x05050505L; // R5
*(--stk) = (INT32U)0x04040404L; // R4
return stk;
} // Only to initialize the Task Control Block Table
void OSInit(void)
{
INT8U i;
OS_ENTER_CRITICAL();
for(i = ; i < OS_MAX_TASKS; i++) {
OSTCBTbl[i].OSTCBStkPtr = (OS_STK*);
OSTCBTbl[i].OSTCBStat = TASK_STATE_CREATING;
}
OSInitTaskIdle();
OSTCBCur = &OSTCBTbl[];
OSTCBNext = &OSTCBTbl[];
OS_EXIT_CRITICAL();
} void OSInitTaskIdle(void)
{
OS_ENTER_CRITICAL();
OSTCBTbl[].OSTCBStkPtr = OSTaskStkInit(OS_TaskIdle, (void *), (OS_STK*)&TASK_IDLE_STK[TASK_STACK_SIZE - ]);
OSTCBTbl[].OSTCBStat = TASK_STATE_RUNNING;
OS_EXIT_CRITICAL();
} void OSTaskCreate(void (*task)(void *p_arg),
void *p_arg,
OS_STK *p_tos)
{
OS_STK * tmp;
INT8U i = ;
OS_ENTER_CRITICAL();
while(OSTCBTbl[i].OSTCBStkPtr != (OS_STK*)) {
i++;
}
tmp = OSTaskStkInit(task, p_arg, p_tos);
OSTCBSet(&OSTCBTbl[i], tmp, TASK_STATE_CREATING);
OS_EXIT_CRITICAL();
} void OSTCBSet(OS_TCB *p_tcb, OS_STK *p_tos, INT8U task_state)
{
p_tcb->OSTCBStkPtr = p_tos;
p_tcb->OSTCBStat = task_state;
} void OS_TaskIdle(void *p_arg)
{
p_arg = p_arg; // No use of p_arg, only for avoiding "warning" here.
for(;;) {
// OS_ENTER_CRITICAL();
// Nothing to do
// OS_EXIT_CRITICAL();
}
} // void SysTick_Handler(void)
// {
// // OS_ENTER_CRITICAL();
// // OS_EXIT_CRITICAL();
// }
void SysTick_Handler(void)
{
OS_ENTER_CRITICAL();
if((--TaskTimeSlice) == ){
TaskTimeSlice = TASK_TIME_SLICE;
OSTCBCur = OSTCBNext;
OSCtxSw();
OSTaskNext++;
while(OSTCBTbl[OSTaskNext].OSTCBStkPtr == (OS_STK*)) {
OSTaskNext++;
if(OSTaskNext >= OS_MAX_TASKS) {
OSTaskNext = ;
}
}
OSTCBNext = &OSTCBTbl[OSTaskNext];
TaskTimeSlice = TASK_TIME_SLICE;
}
TimeMS++;
OS_EXIT_CRITICAL();
} void SysTickInit(INT8U Nms)
{ OS_ENTER_CRITICAL(); TimeMS = ;
TaskTimeSlice = TASK_TIME_SLICE; SysTick->LOAD = * Nms - ;
*Systick_priority = 0x00;
SysTick->VAL = ;
SysTick->CTRL = 0x3;
OS_EXIT_CRITICAL();
} INT32U GetTime(void)
{
return TimeMS;
} void delayMs(volatile INT32U ms)
{
INT32U tmp;
tmp = GetTime() + ms;
while(){
if(tmp < GetTime()) break;
}
} void LedInit(void)
{
RCC->APB2ENR |= <<;
RCC->APB2ENR |= <<;
//GPIOE->CRH&=0X0000FFFF;
//GPIOE->CRH|=0X33330000; GPIOA->CRH &= 0xfffffff0;
GPIOA->CRH |= 0x00000003;
//GPIOA->ODR &= 0xfffffeff;
GPIOA->ODR |= <<; GPIOD->CRL &= 0xfffff0ff;
GPIOD->CRL |= 0x00000300;
//GPIOD->ODR &= 0xfffffffd;
GPIOD->ODR |= <<; //LED1TURN();
LED2TURN(); }
现在我们将按照下列过程展开叙述:
初始化OS--》创建任务--》初始化OS时间单位--》初始化LED灯--》启动OS;
“咦?怎么感觉这些步骤似曾相识呢?”
当然“相识”啦,这个就是main函数的那几行代码的意义啊!快看快看,第一行是OSInit(),这个函数到底做了些什么呢?
(1)初始化OS:
OSInit将完成以下工作:
I. 初始化全局变量OSTCBTbl结构体数组;
II. 创建一个“Idle task”;
III.初始化OSTCBCur和OSTCBNext。
void OSInit(void)
{
INT8U i;
OS_ENTER_CRITICAL();
for(i = ; i < OS_MAX_TASKS; i++) {
OSTCBTbl[i].OSTCBStkPtr = (OS_STK*);
OSTCBTbl[i].OSTCBStat = TASK_STATE_CREATING;
}
OSInitTaskIdle();
OSTCBCur = &OSTCBTbl[];
OSTCBNext = &OSTCBTbl[];
OS_EXIT_CRITICAL();
}
首先是第4行,就是进入“临界区”啦,也就是关中断。
接着是第5~8行的循环,其实就是初始化全局变量OSTCBTbl这个结构体数组,关键是这个结构体的指针OSTCBStkPtr,它之后会指向每个任务对应的堆栈,
此处全部初始化为“0”。当任务被创建时,它就会指向实际的内存地址,作为任务的堆栈:
第9行,创建一个“Idle Task”。此处不继续深挖它,我们后面会深讲创建一个任务的具体过程,之后就自然能看懂该函数的具体内容了。
此处要有一个概念,也就是我们在这里已经创建了一个”Task“了,即便我们后来一个“task”也不创建,CPU也会执行“Idle task”的。
然后是第10~11行,使OSTCBCur和OSTCBNext都指向“Idle task”,OSTCBCur指“OS Task Control Block Current”,
OSTCBNext当然是指“OS Task Control Block Next”咯,这两个指针是后来用于进行“任务切换”的,不知你可否体会呢^_^?
最后是第12行,离开临界区,也就是开中断。
(2)创建任务:
OSTaskCreate会完成以下工作:
I. 找到一个“空闲的”OSTCBTbl;
II. 初始化参数“p_tos”所指向的内存,并将其作为任务堆栈,最重要的是,使堆栈记录参数task所指向的函数的入口地址;
III. 设置新的OSTCBTbl的状态为TASK_STATE_CREATING
(这个状态变量在本OS中算是个bug,但好在没有用到这个变量,所以就暂且没管,所以你也可以暂且不管)
void OSTaskCreate(void (*task)(void *p_arg),
void *p_arg,
OS_STK *p_tos)
{
OS_STK * tmp;
INT8U i = ;
OS_ENTER_CRITICAL();
while(OSTCBTbl[i].OSTCBStkPtr != (OS_STK*)) {
i++;
}
tmp = OSTaskStkInit(task, p_arg, p_tos);
OSTCBSet(&OSTCBTbl[i], tmp, TASK_STATE_CREATING);
OS_EXIT_CRITICAL();
}
不得不提醒“小白兔们”,现在我们已经来到了这座OS之山最险峻的地方了!!!
如果实在坚持不下去了,就先休息休息。有时候,就差那么点“心领神会”,施主若是与OS有缘,那么缘分总会来的。
首先讲第8~9行的循环是什么意思:
在“OS初始化”中,我们把OSTCBTbl这个结构体数组的OSTCBStkPtr指针都初始化成了“0”,当然这些被初始化的“OSTCBTbl”都是“空闲的”,也就是没有被分配给具体任务,所以它指向堆栈地址的指针肯定是“0”,如果不是空闲的,它就应当指向具体的堆栈地址。此处循环的跳出条件就是找到某个OSTCBTbl的堆栈指针为“0”,也就是找到空闲的OSTCBTbl,此时的“i”为其偏移量。记住,“我们要创建一个新task,所以我们就需要一个空闲的OSTCBTbl来记录这个task的堆栈信息。”这样就能明白这个循环的目的了。
接着是第11行,也就是最难的地方了。
“OSTaskStkInit”,就是这个函数,它会完成以下工作:
I. 因为参数p_tos指向堆栈栈顶(Pointer Top Of Stack),所以我们要将其依次递减,并初始化其下的一段内存中的内容。
OSTaskCreate(Task1, (void*)0, (OS_STK*)&Task1Stk[TASK_STACK_SIZE-1]);
这是main函数调用的部分,这个参数指向的正是栈顶!
OS_STK* OSTaskStkInit(void (*task)(void *p_arg),
void *p_arg,
OS_STK *p_tos)
{
OS_STK *stk;
stk = p_tos; *(stk) = (INT32U)0x01000000L; // xPSR
*(--stk) = (INT32U)task; // Entry Point // Don't be serious with the value below. They are of random
*(--stk) = (INT32U)0xFFFFFFFEL; // R14 (LR)
*(--stk) = (INT32U)0x12121212L; // R12
*(--stk) = (INT32U)0x03030303L; // R3
*(--stk) = (INT32U)0x02020202L; // R2
*(--stk) = (INT32U)0x01010101L; // R1 // pointer of the argument
*(--stk) = (INT32U)p_arg; // R0 // Don't be serious with the value below. They are of random
*(--stk) = (INT32U)0x11111111L; // R11
*(--stk) = (INT32U)0x10101010L; // R10
*(--stk) = (INT32U)0x09090909L; // R9
*(--stk) = (INT32U)0x08080808L; // R8
*(--stk) = (INT32U)0x07070707L; // R7
*(--stk) = (INT32U)0x06060606L; // R6
*(--stk) = (INT32U)0x05050505L; // R5
*(--stk) = (INT32U)0x04040404L; // R4
return stk;
}
关键是为何要这样初始化呢?第一次看的话,先从下文找点感觉,看完全部后,还需回来体味体味。
首先,参照《Cortex-M3权威指南(中文版)》P135,表9.1
中断发生时,CPU会将以上寄存器按上述顺序压入PSP中,当我们只有一个main任务需要执行时,PSP就足够帮我们保留现场的了,以至于在中断返回时,从PSP去取出先前的main任务的寄存器内容就可以了(你是不是能看出上表与我们的函数内容的对应关系呢?)。
但是“多任务”当然就不止一个main任务了,假设我们有3个任务,task1,task2,task3:
task1--》task2;PSP会保留task1的寄存器信息;
task2--》task3;PSP会再保留task2的寄存器信息,但是task1的寄存器信息就被覆盖掉了!
task3-----????-----task1
那么接下来就悲剧了。
当然,这个函数只是先给以后会用到的堆栈初始化,至于具体值并不重要,除了第8、9、19行:
第8行指定的是一个程序正常运行时,状态寄存器PSR该有的值;
第9行指定的是任务的入口地址。这个当然重要啦!因为我们的任务就是这个地址所指向的函数。
第19行指定的是任务函数参数的所在地址,因为我们一直赋值为“0”,所以暂且没有太多意义。
至于其他初始化的值,比如
(INT32U)0x08080808L;
这都无关紧要,随你怎么设置都行。
最后第30行,函数返回堆栈指针,该指针现在指向地址的内容为0x04040404L,依照注释,就是以后存储R4寄存器内容的地址。
回到 OSTaskCreate函数第12行,它使得先前找到的OSTCBTbl不再“空闲了”。
(3)初始化SysTick中断和LED:
在main函数中
SysTickInit(5);
LedInit();
都是与硬件相关的,唯一需要说明的是“SysTickInit(5)”当中的“5”没有太多含义,只是越大,中断发生的时间间隔就越大,具体依照硬件时钟而定。
(4)main函数最后一行:
OSStart()
该函数一旦执行,将永不返回。注意,接下来我们要和汇编交手了,从汇编部分(os_cpu_a.asm)第31行开始。
这里要请大家注意,每行汇编语言后都有对应的C语言注释,对照着看更容易理解。
第32行:
CPSID I
参照《Cortex-M3权威指南(中文版)》P42,该命令即为关中断,相当于给PRIMASK写“1”。
(注:CPSID指的是属于CPS(Control Processor State)指令的Interrupt Disable指令)
第36~38行:
LDR R0, =NVIC_SYSPRI14
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0]
设置PendSV中断优先级。
第42~43行:
LDR R4, =0x0
MSR PSP, R4
初始PSP寄存器,使之为0。这个“0”表示的是我们的OS才启动,还没有任务被运行,后面很快就有说明。
第46~48行:
LDR R4, =NVIC_INT_CTRL
LDR R5, =NVIC_PENDSVSET
STR R5, [R4]
触发PendSV中断。
“什么!触发中断了!哎呀哎呀,怎么办?中断函数在哪儿?”
小白兔请先别着急,由于先前我们已经关掉中断了,所以程序还会继续往下执行,直到我们再开启中断。
由于我们真正的目的就是要开启PendSV中断,所以下一步我们要开中断啦!
第51行:
CPSIE I
开中断。由于PendSV被触发了,所以接下来CPU跳到PendSV的中断处理函数处执行。也就是os_cpu_a.asm的第59行。
第60行是关中断。
第62~63行:
MRS R0, PSP
CBZ R0, PendSV_Handler_NoSave
比较PSP是否为0,若是0就跳转到PendSV_Handler_NoSave处执行,由于OSStart先前将PSP初始化为0,所以就直接调到PendSV_Handler_NoSave处执行咯。
第104~107行
LDR R0, =OSTCBCur
LDR R1, =OSTCBNext
LDR R2, [R1]
STR R2, [R0]
将OSTCBNext所指向地址的前4个字节,赋给OSTCBCur所指向地址的前4个字节,而这4个字节正是两个指针所指向结构体的OSTCBStkPtr变量!
这段代码做的就是将下个任务的堆栈指针(OSTCBNext)赋给当前任务的堆栈指针(OSTCBCur),因为PendSV中断所做的事情就是进行任务切换。
我们知道,一开始OSTCBCur和OSTCBNext一开始都是指向“Idle Task”的,所以下一个要运行的任务就是“Idle Task”。
第109行:
LDR R0, [R2]
将所要切换的堆栈指针地址赋给R0。
第112~134行:
LDR R4, [R0]
ADD R0, R0, #0x4
LDR R5, [R0]
ADD R0, R0, #0x4
……
LDR R11 , [R0]
ADD R0, R0, #0x4
将堆栈指针R0所指向的堆栈内容赋给R4~R11。问个问题,“这些值都是多少你知道吗?”
“好了,这时候你是不是想问个问题,R0~R3怎么不给它们也赋值呢?”
回到先前那个要大家需要体味的地方
参照《Cortex-M3权威MSR PSP, R0指南(中文版)》P135,表9.1
还有接下来第136行:
MSR PSP, R0
我只说一句:CPU会自动从PSP保存和加载8个量至R0~R3,R12,LR,程序入口(这个是理解的关键)和xPSR,我们无需动手。
第140行:
ORR LR, LR, #0x04
参照《Cortex-M3权威MSR PSP, R0指南(中文版)》P139,这行是为了在回到任务后,确保继续使用PSP堆栈。
第141行开中断,此时一般还不会有中断介入,但我们要记住,现在我们还处在PendSV中断中,第142从PendSV中断返回,返回后CPU则去新的任务处执行了。
让我们再回到OS_cpu_a.asm的第62~63行,当PSP不为零,也就是已经有任务运行了,那么我们就需要先保存这个将被切换出去的任务的寄存器信息。
现在PSP指向的堆栈地址如图所示:
完成第62~66行命令之后,我们得到:
该图中有两个”R0",并不是很得体,我还是解释一下,左边的R0指的是PendSV中断下正在使用的R0寄存器,右边的R0指的是被中断的任务的R0寄存机所存储的值。
保护现场,保护现场啦!将R4~R11全部存储起来。
“为什么R0~R3不用保护呢?”
因为……你看啊,其实CPU自己已经在PendSV中断发生时,“擅自”把它们存储了,就在上图啊!
完成第69~91行命令之后,再将R0减去0x20,堆栈就变成这样啦:
然后是第99~101行:
把R0的值赋给OSTCBCur的OSTCBStkPtr指针,这下我们就放心了,所有有关任务的信息都被存放在了相应的OSTCBTbl中了。
接下来就是该切换进入新任务了。
(5)谁来触发PendSV,实现切换任务
如果到此为止,那么CPU只会一直没完没了的执行第一个任务:Idle Task。OSStart确实完成了一次任务切换,但也仅仅就“一次”。
main函数已经没得指望了,它早就撒手不管了,那么谁来再次触发PendSV,从而执行Task1、Task2呢?
别忘了,我们还有一个关键角色没有登场呢:SysTick中断。
让我们回到myos.c的106行的SysTick的中断服务函数:SysTick_Handler。
这个函数逻辑很简单:每发生一次SysTick中断,就将TimeTaskSlice递减,当TimeTaskSlice为0时,就进行切换任务的工作,并将TimeTaskSlice的值还原。
每一次中断发生,TimeMS都会加1,从而记录整个系统的时间。
第111行:
OSTCBCur = OSTCBNext;
将当前任务指针指向下一个任务。
第112行:
OSCtxSw();
这是个汇编实现的函数,在os_cpu_a.asm的第144~150行。
非常简单,它就是在触发PendSV中断!!!
当然,这个触发不会立即发生,因为现在还处于关中断状态。
第113行:
OSTaskNext++;
之前忘记介绍了,这是个全局整型变量,用来记录下一个任务的偏移量的。由于我们的任务没有优先级,只是轮换执行,所以将OSTaskNext向后偏移一个就行了。
但是,如果我们偏移到了最后一个任务怎么办呢?我们得从第一个任务重新开始才是,所以就有了第144~149的循环部分。
第120行:
OSTCBNext = &OSTCBTbl[OSTaskNext];
设置新的下一个要运行的任务的任务指针。
第121行有点多余,不要也行。
第124行开中断,这行命令执行完之后,PendSV就会被触发,接着就是去执行新的任务咯。
至此,关于本OS的关键代码部分就解析完毕了。
真心希望大家多提意见,鄙人将感激涕零!
ansersion@sina.com
一步步学习操作系统(1)——参照ucos,在STM32上实现一个简单的多任务(“啰里啰嗦版”)的更多相关文章
- 一步步学习操作系统(2)——在STM32上实现一个可动态加载kernel的"my-boot"
如果要做嵌入式Linux,我们首先要在板子上烧写的往往不是kernel,而是u-boot,这时需要烧写工具帮忙.当u-boot烧写成功后,我们就可以用u-boot附带的网络功能来烧写kernel了.每 ...
- 区块链开发学习第三章:私有链上部署helloBlockchain简单合约
前面讲了部署私有链以及新增账户,现在进行到了部署合约了,此操作真是踩了无数无数无数的坑,到写文章为止确实是已经部署好了,但是还有些坑是还没有解决的! 一.Solidity编译器 开始的时候用的http ...
- 学习vi和vim编辑(3):一个简单的文本编辑器(2)
然后文章,继续评论vi编辑简单的文本编辑命令. 本文主要是删除的文字.复制,运动命令. 删除文本: 正如上一篇文章中讲过的,对于删除命令("d")也具有"(command ...
- [shiro学习笔记]第二节 shiro与web融合实现一个简单的授权认证
本文地址:http://blog.csdn.net/sushengmiyan/article/details/39933993 shiro官网:http://shiro.apache.org/ shi ...
- opengl学习笔记(五):组合变换,绘制一个简单的太阳系
创建太阳系模型 描述的程序绘制一个简单的太阳系,其中有一颗行星和一颗太阳,用同一个函数绘制.需要使用glRotate*()函数让这颗行星绕太阳旋转,并且绕自身的轴旋转.还需要使用glTranslate ...
- pipelinewise 学习二 创建一个简单的pipeline
pipelinewise 提供了方便的创建简单pipeline的命令,可以简化pipeline 的创建,同时也可以帮我们学习 生成demo pipeline pipelinewise init --n ...
- 009-2010网络最热的 嵌入式学习|ARM|Linux|wince|ucos|经典资料与实例分析
前段时间做了一个关于ARM9 2440资料的汇总帖,很高兴看到21ic和CSDN等论坛朋友们的支持和鼓励.当年学单片机的时候datasheet和学习资料基本都是在论坛上找到的,也遇到很多好心的高手朋友 ...
- 一步步学习javascript基础篇(0):开篇索引
索引: 一步步学习javascript基础篇(1):基本概念 一步步学习javascript基础篇(2):作用域和作用域链 一步步学习javascript基础篇(3):Object.Function等 ...
- 一步步学习javascript基础篇(8):细说事件
终于学到事件了,不知道为何听到“事件”就有一种莫名的兴奋.可能是之前的那些知识点过于枯燥无味吧,说起事件感觉顿时高大上了.今天我们就来好好分析下这个高大上的东西. 可以说,如果没有事件我们的页面就只能 ...
随机推荐
- canvas一周一练 -- canvas基础学习
从上个星期开始,耳朵就一直在生病,里面长了个疙瘩,肿的一碰就疼,不能吃饭不能嗨 (┳_┳)……在此提醒各位小伙伴,最近天气炎热,一定要注意防暑上火,病来如山倒呀~ 接下来我正在喝着5块一颗的药学习ca ...
- hashcode-equals方法
package com.charles.collection; import java.util.HashSet; import java.util.Set; public class Point { ...
- ASP.NET MVC Bundles 用法和说明(打包javascript和css)
本文主要介绍了ASP.NET MVC中的新功能Bundles,利用Bundles可以将javascript和css文件打包压缩,并且可以区分调试和非调试,在调试时不进行压缩,以原始方式显示出来,以方便 ...
- 设计模式--工厂方法模式(Factory method pattern)及应用
面向对象的好处: 通过封装,继承,多态把程序的耦合度降低. 用设计模式可以使程序更加灵活,容易修改,且易于复用. 1. 工厂方法模式 Define an interface for creating ...
- app耗电优化之四 使用AlarmManager对任务进行合理安排
AlarmManager 是用来设定定时任务.即用来设定那个任务在什么时候开始执行.为什么和省电有关系?这个需要和AlarmManager的使用先说起.AlarmManager 实际上只起到一个定时发 ...
- Maven常用的几个核心概念
在使用Maven的过程中,经常会遇到几个核心的概念,准确的理解这些概念将会有莫大的帮助. 1. POM(Project Object Model)项目对象模型 POM 与 Java 代码实现了解耦,当 ...
- centos7 Mysql备份还原并下载到windos
数据库备份 1.show databases; #查看一下数据库 2.对应数据库做备份开始: mysqldump -u root -p 需要备份的数据库 > db.sql #把它备份到 ...
- OpenLayers3--ol3--新特性
OL3: A high-performance, feature-packed library for all your mapping needs < 一个可以满足各种地图应用的高性能的.功能 ...
- Web项目、Http协议简介
Web 静态web项目 静态web项目就是一个文件夹.静态Web项目 就是文件夹中都是静态资源. 如何将web项目部署到tomcat? 将web项目的文件夹复制到webapps目录下.比如把test文 ...
- [bzoj1592] Making the Grade
[bzoj1592] Making the Grade 题目 FJ打算好好修一下农场中某条凹凸不平的土路.按奶牛们的要求,修好后的路面高度应当单调上升或单调下降,也就是说,高度上升与高度下降的路段不能 ...