在STM32上创建一个自己的操作系统

参考文章:http://www.cnblogs.com/ansersion/p/4328800.html

二维码是我创建的QQ群,欢迎新朋友加入。

之前看了蛮多帖子,不过苦于自己对着基本上是门外汉,基本上只明白个大概,幸亏找到一个分享源码的帖子,在这上面我也实现了一个操作系统的移植;同时我也感觉到了自己学习的不足,以前总以为会用几款单片机就觉得单片机学完了,这段时间看系统的内容才发现自己原来在技术方面的欠缺这么大;


我的代码基本上和参考文章差不多,毕竟自己也不是很明白,门外汉嘛。


硬件平台:秉火开发板

软件平台:野火STM32工程模板(103系列)

系统参考来源:UCOS


我看源码的方式可能不一样,我是从ASM开始的

1.首先是:

    IMPORT     OSTCBCur
    IMPORT    OSTCBNext
    
    EXPORT    OS_ENTER_CRITICAL
    EXPORT    OS_EXIT_CRITICAL
    EXPORT    OSStart
    EXPORT    PendSV_Handler
    EXPORT    OSCtxSw
这里涉及到一个ASM的语法,就是IMPORT和EXPORT不能顶格写,我自己因为从来没接触过汇编,查了蛮久的资料才晓得的;


这有两个函数:

IMPORT ,定义表示这是一个外部变量的标号,不是在本程序定义的
EXPORT ,表示本程序里面用到的变量提供给其他模块调用的。

先说下IMPORT 的内容,汇编里面,他是从外面调用的

    IMPORT     OSTCBCur
    IMPORT    OSTCBNext
看一下我的代码里面这两个变量的来源(虽然是抄袭的别人的代码,我也不害臊的称之为我的吧)

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)

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)
这里给结构体OS_TCB传入了两个参数,OSTCBCur指向当前运行任务的指针  OSTCBNext指向下一个运行任务的指针

这个东西其实我们早有接触,不知道大家有没没有注意,我们做硬件仿真的时候,点击复位,指针的指向位置:

; Reset handler
Reset_Handler   PROC
                EXPORT  Reset_Handler             [WEAK]
                IMPORT  __main
                IMPORT  SystemInit
                LDR     R0, =SystemInit
                BLX     R0               
                LDR     R0, =__main
                BX      R0
                ENDP
我们32运行的时候首先执行了一个sysyteminit,然后再去执行我们的main函数

然后是EXPORT 内容,这是本函数自己定义的,就没什么好说的了;

    EXPORT    OS_ENTER_CRITICAL
    EXPORT    OS_EXIT_CRITICAL
    EXPORT    OSStart
    EXPORT    PendSV_Handler
    EXPORT    OSCtxSw


2.NVIC中断向量

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).

;/******************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();

这里我参考的帖子是:http://www.cnblogs.com/13chfang/p/6151565.html

这是一段从ucos截取出来的代码,这段汇编程序其实特别简单,做了以下几个事情:

1.将pendSV中断设置为最低优先级

LDR ...=:伪指令,参数、地址传递用的

STRB指令用于从源寄存器中将一个8位的字节数据传送到存储器中。该字节数据为源寄存器中的低8位。

1      LDR     R0, =NVIC_SYSPRI14                                  ; Set the PendSV exception priority
2      LDR     R1, =NVIC_PENDSV_PRI
3      STRB    R1, [R0]

2.将PSP置0

特权级下用MSP,用户级用PSP。

1     MOVS    R0, #0                                              ; Set the PSP to 0 for initial context switch call
2     MSR     PSP, R0

3.分配堆栈给MSR,这个堆栈的作用其实是在中断嵌套的时候可以将寄存器和局部变量等进行入栈。如果中断程序较大的话或者中断嵌套较多的话,建议将这个堆栈空间设置得更大一些,我们不能只是关心任务堆栈。PS.取最后一个数组元素地址的原因是因为我们CM3的堆栈方向是从高到低的。

简单普及一下:MSR的意思是move to special register from  register的缩写,可以将普通寄存器的数值保存到xpsr寄存器中。

复制代码
1 ;/*在前面的头文件里定义的,这里这是写出来容易看*/
2 ;unsigned int* OS_CPU_ExceptStackBase = &CPU_ExceptStack[1023];
3 
4 
5     LDR     R0, = OS_CPU_ExceptStackBase                          ; Initialize the MSP to the OS_CPU_ExceptStkBase
6     LDR     R1, [R0]
7     MSR     MSP, R1    
复制代码

4.触发pendSV异常,实现任务切换,顺便enable interrupts.

(查了资料CPSID/CPSIE就是开关终端的东西)

    LDR     R0, =NVIC_INT_CTRL                                  ; Trigger the PendSV exception (causes context switch)
    LDR     R1, =NVIC_PENDSVSET
    STR     R1, [R0]
    
    CPSIE   I                                                   ; Enable interrupts at processor level


根据这段内容,知道我的代码里面内容的含义:

CPSID    I                            ; OS_ENTER_CRITICAL();
关闭中断

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;
通用寄存器把参数0送到状态寄存器里面

; trigger PendSV
    LDR    R4, =NVIC_INT_CTRL              ; R4 = NVIC_INT_CTRL;
    LDR    R5, =NVIC_PENDSVSET             ; R5 = NVIC_PENDSVSET;
    STR    R5, [R4]                        ; *R4 = R5;
使能PENSV悬起寄存器

; enable interruptions
    CPSIE    I                            ; OS_EXIT_CRITICAL();
开中断


这个内容在我的代码里面只执行一次

位置是主函数:

OSStart(); // start os!
开启系统任务


3.任务切换的实现

用的是OSStart(); // start os!里面初始化的PENSV异常服务;
先来段别人的解释吧,我解释的可能比较词穷

10.SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用在上了操作系统的软件开发中。 SVC 用于产生系统函数的调用请求。例如,操作系统通常不让用户程序直接访问硬件,而是通过提供一些系统服务函数,让用户程序使用 SVC 发出对系统服务函数的呼叫请求,以这种方法调用它们来间接访问硬件。因此,当用户程序想要控制特定的硬件时,它就要产生一个SVC 异常,然后操作系统提供的 SVC 异常服务例程得到执行,它再调用相关的操作系统函数,后者完成用户程序请求的服务。    

    这种“提出要求——得到满足”的方式,很好、很强大、很方便、很灵活、很能可持续发展。首先,它使用户程序从控制硬件的繁文缛节中解脱出来,而是由 OS 负责控制具体的硬件。第二,OS 的代码可以经过充分的测试,从而能使系统更加健壮和可靠。第三,它使用户程序无需在特权级序变得与硬件无关,因此在开发应用程序时无需了解硬件的操作细节,从而简化了开发的难度和繁琐度,并且使应用程序跨硬件平台移植成为可能。开发应用程序唯一需要知道的就是操作系统提供的应用编程接口( API),并且在了解了各个请求代号和参数表后,就可以使用 SVC 来提出要求了。SVC 异常通过执行”SVC”指令来产生。该指令需要一个立即数,充当系统调用代号。 SVC 异常服务例程稍后会提取出此代号,从而获知本次调用的具体要求,再调用相应的服务函数。例如,

                                                 SVC 0x3 ; 调用 3 号系统服务
      在 SVC 服务例程执行后,上次执行的 SVC 指令地址可以根据自动入栈的返回地址计算出。找到了 SVC 指令后,就可以读取该 SVC 指令的机器码,从机器码中萃取出立即数,就获知了请求执行的功能代号。如果用户程序使用的是 PSP,服务例程还需要先执行 MRS Rn, PSP 指令来获取应用程序的堆栈指针。通过分析 LR 的值,可以获知在 SVC指令执行时,正在使用哪个堆栈。

  11.另一个相关的异常是 PendSV(可悬起的系统调用),它和 SVC 协同使用。一方面, SVC 异常是必须在执行 SVC 指令后立即得到响应的(对于 SVC 异常来说,若因优先级不比当前正处理的高,或是其它原因使之无法立即响应,将上访成硬 fault),应用程序执行 SVC 时都是希望所需的请求立即得到响应。另一方面, PendSV 则不同,它是可以像普通的中断一样被悬起的(不像SVC 那样会上访)。 OS 可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动作。悬起 PendSV 的方法是:手工往 NVIC 的 PendSV 悬起寄存器中写 1。悬起后,如果优先级不够高,则将缓期等待执行。PendSV 异常会自动延迟上下文切换的请求,直到其它的 ISR 都完成了处理后才放行。为实现这个机制,需要把 PendSV 编程为最低优先级的异常。

    

以上内容为基于CM3内核开发一个实时操作系统我们需要知道的一些关于CM3的知识,建议去看《CM3权威指南Cn


这就是前辈对这个异常服务的解释,在代码里面如果把这内容拆解:

;/******************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;

太长了,我根据注解自己尝试着修改了下

;/******************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;
    
    SUB     R0, R0, #0x20            ; R0 = R0 - 0x20;
    
    ; easy method
    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}
    ADDS    R0, R0, #0x20

    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;

这样汇编部分基本做完了,

我代码里面其他的汇编内容好像影响不是特别大,不是核心的,就不说了

4.任务创建

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;
}
借用别人的一个描述

 void vTaskCreate(TCB* tcb,void (*task)(void),unsigned int* stack)
16 {
17         unsigned int *pstrStack;
18         pstrStack = stack;
19         pstrStack = (unsigned int*)    ((unsigned int)(pstrStack)&0xfffffff8u);/* 8字节对齐 */
20         *(--pstrStack) = (unsigned int)0x01000000ul; /* XPSR*/
21         *(--pstrStack) = (unsigned int)task;       /* r15 */
22         *(--pstrStack) = (unsigned int) Task_End;       /* r14 */
23         *(--pstrStack) = (unsigned int)0x12121212ul;    /*r12*/
24         *(--pstrStack) = (unsigned int)0x03030303ul;    /*r3*/
25         *(--pstrStack) = (unsigned int)0x02020202ul;    /*r2*/
26         *(--pstrStack) = (unsigned int)0x01010101ul;    /*r1*/
27         *(--pstrStack) = (unsigned int)0x00000000ul;    /*r0*/
28     
29         *(--pstrStack) = (unsigned int)0x11111111ul;    /*r11*/
30         *(--pstrStack) = (unsigned int)0x10101010ul;    /*r10*/
31         *(--pstrStack) = (unsigned int)0x09090909ul;    /*r9*/
32         *(--pstrStack) = (unsigned int)0x08080808ul;    /*r8*/
33         *(--pstrStack) = (unsigned int)0x07070707ul;    /*r7*/
34         *(--pstrStack) = (unsigned int)0x06060606ul;    /*r6*/
35         *(--pstrStack) = (unsigned int)0x05050505ul;    /*r5*/
36         *(--pstrStack) = (unsigned int)0x04040404ul;    /*r4*/
37         
38         tcb->pstrStack = pstrStack;
39 }
复制代码

我们程序做得工作主要如下:
(1)传进了三个参数,参数1:任务TCB指针,这是一个结构体指针,此时首地址是我们存的是pstrStack;参数2是任务函数指针,也就是我们希望调用一个任务后他执行的函数; 参数3是我们分配的堆栈栈顶,可以使用动态分配或者静态分配,我们这里其实是定义了一个数组,传进来的数组的最后一个元素的地址(因为栈的生长方向是从高到低的)。

(2)定义一个变量pstrStack指针指向栈顶,接下来程序里做的事情是初始化中断返回后从栈中恢复的8个寄存器。首先初始化的是xPSP寄存器,将它的第24位置1,表示处于Thumb状态;在c语言中,我们的函数名就是函数的首地址,从这个地址开始存放着函数的指令,我们只需跳转到这个地址就可以执行函数,所以我们开始运行一个任务需要做的事情就是跳转到这个任务的函数名,所以我们接下来做的事就是让PC寄存器指向该函数的首地址;接下来我们初始化的是LR寄存器,用来保存函数的返回地址, 我们任务执行到最后会跳转到LR寄存器指向的地址,所以如果我们的任务没有写成无限循环的形式的话,最后就会跳转到LR指向的地址。为了防止因为我们忘记将任务写成无限循环而出现系统奔溃情况,我们将LR寄存器指向了一个无限循环的函数Task_End()的地址,这增加了我们代码的健壮性。在ucos中,系统在这个函数里面可以将任务删除掉。

(3)后面的寄存器我们都是简单地随便赋值,其实是为了debug可以方便点。但是其实我们还是要关注R0~R3这四个寄存器的。在ARM中(如果我没记错的话),函数传参的时候,前四个形参都是直接都过R0~R3这四个寄存器实现参数传递的,当形参个数大于4个的话,其余的入口参数则依次压入当前栈,通过栈传参。还有一个比较重要的,我们子函数通过R0寄存器将函数返回值传递给父函数。所以,我们如果要给我们的任务函数传参,我们需要把传进来的形参存放到R0~R3寄存器中。比如uCOS和freeRTOS就都用R0寄存器传参给任务函数,uCOS还通过R1存放堆栈限制增长到的内存地址。

(4)最后,我们将我们初始化好的任务堆栈地址赋值给我们任务TCB的pstrStack指针。我们只要将这个指针指向的地址赋值给我们的OSTCBHighRdyPtr就可以任务的切换了。

看一下在主函数里面对任务的创建

OSTaskCreate(Task1, (void*)0, (OS_STK*)&Task1Stk[TASK_STACK_SIZE-1]); // create task 1
    OSTaskCreate(Task2, (void*)0, (OS_STK*)&Task2Stk[TASK_STACK_SIZE-1]); // create task 2
    OSTaskCreate(Task3, (void*)0, (OS_STK*)&Task3Stk[TASK_STACK_SIZE-1]); // create task 3

主函数里面就是很普通的系统使用了,跟UCOS差不多,没什么好讲的;

本文上传代码:(可能大神们觉得这很简单吧,都不传代码,那我这种小白就乖乖的发出来了)
资源名称:我的第一个实时操作系统



猜你喜欢

转载自blog.csdn.net/jun626/article/details/54882805