【ucos】ucos移植到s3c2440

目录

一、前言

二、ucos移植前的准备

三、任务栈的概念

四、ucos的任务切换实现原理分析(os_cpu_a.s)

五、ucos临界区实现

六、ucos3.06移植

七、工程编译时遇到的问题

八、总结

九、参考资料


一、前言

ucos是micrium开发的一款专门针对MCU的嵌入式操作系统,具有优秀的性能。之前上大学的时候学过一门嵌入式系统相关的课程,书本上也是用ucos这款操作系统作为范例来进行讲解学习的,可见这个操作系统是得到业界的广泛认可的。我最早接触到嵌入式操作系统也是这个。如今网上可以查到非常多的ucos移植资料,包括将它移植到各种各样的平台上。如果不喜欢拿网上别人改过的代码的话,我们可以到官网去下载源码(https://www.micrium.com/downloadcenter/),官网提供不同版本的ucos源代码和许多已经在各种平台上移植好了的demo工程,可以根据需求下载。目前ucos主要分为ucos2和ucos3两个版本,ucos3在ucos2的基础上做了很大改进,我把ucos2.91和ucos3.06都移植到了s3c2440上,感受下两个版本的差异,对移植的部分来说没什么差异,也就是ucos3比ucos2多一些文件。本文使用的开发板是韦东山的JZ2440开发板,三星的S3C2440 ARM9芯片,本文只以ucos2.91移植为例子,ucos3.06是一样的,两个移植好的工程都会在附件资料中给出。

 

二、ucos移植前的准备

其实网上也找到一些ucos2移植到S3C2440平台的例子,我下载下来看了下不想捡现成的直接拿来用,还是要锻炼下自己,所以打算从官网下载demo进行修改,直接从官网代码修改到适合自己开发板的代码。怎么找适合自己的demo呢?和uboot等移植一样,我们不一定可以直接找到对得上自己CPU型号的demo,那么我们就找个架构相似的CPU修改一下。在micrium官网看了下,没有S3C2440这款芯片的demo工程,那么就选个arm9架构的其他芯片demo,最相近的应该是Samsung S3C2410这个芯片的demo。

Samsun S3C2410这个demo工程(Michael_Anburaj_S3C2410_arm_ucos_110.zip文后资料里附上)里面没有ucos的源代码,只有移植时需要用到的一些文件,包括os_cpu_a.s,os_cpu_c.c,os_cpu.h,这个三个文件,其中os_cpu_a.s是核心的任务切换相关的汇编代码,我们基本不需要再做修改可以直接拿来用了,又下载了个ucos2.9.1的源码(Micrium-uCOS-II-V290.zip文后资料里附上)。最后工程的目录结构如下(用的VSCode编辑器)。

cpu文件夹放跟具体的处理器移植相关的代码,Source放ucos2的源码,外面放开发板的一些启动文件和外设初始化文件,这样工程就搭好了,下面可以做移植相关的工作了。

 

三、任务栈的概念

嵌入式操作系统有一个非常重要的概念,就是“任务栈”。对于s3c2440来说,代码的执行需要依赖r0-r12、lr、sp、pc、cpsr,spsr这些寄存器,代码执行到不同的地方,这些寄存器会有不同的值来表示当前的工作状态(工作环境),要实现任务间的中断及切换就是要能够保存当前任务的工作环境,然后切换到下一个任务的工作环境,下个任务执行完后又去恢复这个任务的环境继续执行之后的代码。所以任务栈就是用来保存每个任务的工作寄存器的,当然任务函数里面申请的局部变量也是保存在任务栈里的。在ucos里面每个任务栈都需要我们定义一个全局数组来表示,然后会传到OSTaskStkInit里面进行初始化,如下:

OS_STK *OSTaskStkInit (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT16U opt)
{
    OS_STK *stk;


    opt    = opt;                           /* 'opt' is not used, prevent warning                      */
    stk    = ptos;                          /* Load stack pointer                                      */
    *(stk) = (OS_STK)task;                  /* Entry Point                                             */
    *(--stk) = (INT32U)0;                   /* lr                                                      */
    *(--stk) = (INT32U)0;                   /* r12                                                     */
    *(--stk) = (INT32U)0;                   /* r11                                                     */
    *(--stk) = (INT32U)0;                   /* r10                                                     */
    *(--stk) = (INT32U)0;                   /* r9                                                      */
    *(--stk) = (INT32U)0;                   /* r8                                                      */
    *(--stk) = (INT32U)0;                   /* r7                                                      */
    *(--stk) = (INT32U)0;                   /* r6                                                      */
    *(--stk) = (INT32U)0;                   /* r5                                                      */
    *(--stk) = (INT32U)0;                   /* r4                                                      */
    *(--stk) = (INT32U)0;                   /* r3                                                      */
    *(--stk) = (INT32U)0;                   /* r2                                                      */
    *(--stk) = (INT32U)0;                   /* r1                                                      */
    *(--stk) = (INT32U)pdata;               /* r0 : argument                                           */
    *(--stk) = (INT32U)(SVCMODE|0x0);       /* PSR                                                     */
    *(--stk) = (INT32U)(SVCMODE|0x0);       /* SPSR                                                    */

        return (stk);
}

栈里面默认依次压入task(任务的函数地址),lr,r12-r0,cpsr,spsr,除了sp外所有寄存器都统统保存了,返回的stk就是sp寄存器应该赋的值,在函数外面赋值给了任务控制块tcb,任务控制块tcb会记录这个任务目前栈所处的位置,当切换进这个任务时需要用到它。

 

四、ucos的任务切换实现原理分析(os_cpu_a.s)

我认为嵌入式RTOS的代码量还是比较少的,当然由于大多数时候网上和官网都能搜到CPU相同或极其相似的demo可以供我们直接复制,随便拖一拖文件就好了不需要理解这些实现原理,但是万一我们需要移植到一个很陌生找不到demo的平台或者是应用中出了一些问题的时候,我们就会很被动,不如刚开始学的时候就弄懂这些,毕竟写这些RTOS的都是大佬,从中能学到的东西还是很多的。所以移植的话最好能把所有的代码都分析一便,ucos中跟处理器相关的代码无非就是任务切换的汇编代码和中断相关的代码了,实际上移植到不同的处理器就是去修改好这两部分的代码,让它们能够适应这个平台完成对应的功能。

一、ucos2实现任务间的切换有以下三种途径,

(1)调用OSStartHighRdy函数。

(2)调用OS_TASK_SW函数。

(3)系统定时器中断内切换任务(_IntCtxSw函数)。

下面详细介绍每种途径,把这三种途径分析完后,实际上就把os_cpu_a.s这个文件全部分析完了:

1、调用OSStartHighRdy函数:

这个函数只会调用一次,在调用OSInit和OSTaskCreate初始化和创建完用户任务之后,需要调用OSStart来启动ucos系统,

void  OSStart (void)
{
    if (OSRunning == OS_FALSE) {
        OS_SchedNew();                               /* Find highest priority's task priority number   */
        OSPrioCur     = OSPrioHighRdy;
        OSTCBHighRdy  = OSTCBPrioTbl[OSPrioHighRdy]; /* Point to highest priority task ready to run    */
        OSTCBCur      = OSTCBHighRdy;
        OSStartHighRdy();                            /* Execute target specific code to start task     */
    }
}

由于系统还没启动OSRunning = OS_FALSE,所以会调用OS_SchedNew寻找当前优先级最高的任务,之后调用OSStartHighRdy函数,这个函数在os_cpu_a.s中用汇编实现如下,功能是恢复最高优先级任务的现场

@*********************************************************************************************************
@                                          START MULTITASKING
@                                       void OSStartHighRdy(void)
@
@ Note : OSStartHighRdy() MUST:
@           a) Call OSTaskSwHook() then,
@           b) Set OSRunning to TRUE,
@           c) Switch to the highest priority task.
@*********************************************************************************************************

        .extern  OSTaskSwHook
        .extern  OSRunning
        .extern  OSTCBHighRdy

        .global  OSStartHighRdy

OSStartHighRdy:

        bl OSTaskSwHook             @ Call user defined task switch hook

        ldr r4,=OSRunning           @ Indicate that multitasking has started
        mov r5,#1
        strb r5,[r4]

        ldr r4,=OSTCBHighRdy        @ Get highest priority task TCB address

        ldr r4,[r4]                 @ get stack pointer
        ldr sp,[r4]                 @ switch to the new stack

        ldmfd sp!,{r4}              @ pop new task's spsr
        msr SPSR_cxsf,r4
        ldmfd sp!,{r4}              @ pop new task's psr
        msr CPSR_cxsf,r4
        ldmfd sp!,{r0-r12,lr,pc}    @ pop new task's r0-r12,lr & pc

OSTaskSwHook是个钩子函数,每当要发生任务切换时去调用一下,源码里是个空函数,想做什么事情可以自己加。随后OSRunning置1表示操作系统跑起来了,然后从OSTCBHighRdy变量取得当前最高优先级的任务控制块(就是任务堆栈底部位置),然后把寄存器组全部恢复出来完成现场的恢复,之后跳到对应的任务去执行。

2、调用OS_TASK_SW函数:

在调用延时等函数时,实际上会调用到OS_Sched函数,这个函数里面又去调用OS_TASK_SW,这个函数被宏定义为调用OSCtxSw了,这个实现是在os_cpu_a.s里面的,功能是保存当前任务现场,恢复下一个任务的现场

@*********************************************************************************************************
@                                PERFORM A CONTEXT SWITCH (From task level)
@                                           void OSCtxSw(void)
@
@ Note(s):    Upon entry, 
@             OSTCBCur     points to the OS_TCB of the task to suspend
@             OSTCBHighRdy points to the OS_TCB of the task to resume
@
@*********************************************************************************************************

        .extern  OSTCBCur
        .extern  OSTaskSwHook
        .extern  OSTCBHighRdy
        .extern  OSPrioCur
        .extern  OSPrioHighRdy

        .global  OSCtxSw

OSCtxSw:
@ Special optimised code below:
        stmfd sp!,{lr}              @ push pc (lr should be pushed in place of PC)
        stmfd sp!,{r0-r12,lr}       @ push lr & register file
        mrs r4,cpsr
        stmfd sp!,{r4}              @ push current psr
        mrs r4,spsr
        stmfd sp!,{r4}              @ push current spsr

        @ OSPrioCur = OSPrioHighRdy
        ldr r4,=OSPrioCur
        ldr r5,=OSPrioHighRdy
        ldrb r6,[r5]
        strb r6,[r4]
        
        @ Get current task TCB address
        ldr r4,=OSTCBCur
        ldr r5,[r4]
        str sp,[r5]                 @ store sp in preempted tasks's TCB

        bl OSTaskSwHook             @ call Task Switch Hook

        @ Get highest priority task TCB address
        ldr r6,=OSTCBHighRdy
        ldr r6,[r6]
        ldr sp,[r6]                 @ get new task's stack pointer

        @ OSTCBCur = OSTCBHighRdy
        str r6,[r4]                 @ set new current task TCB address

        ldmfd sp!,{r4}              @ pop new task's spsr
        msr SPSR_cxsf,r4
        ldmfd sp!,{r4}              @ pop new task's psr
        msr CPSR_cxsf,r4
        ldmfd sp!,{r0-r12,lr,pc}    @ pop new task's r0-r12,lr & pc

第一部分:把当前任务的工作组寄存器全部保存进任务栈,

第二部分:令OSPrioCur = OSPrioHighRdy将最任务的最高优先级号赋值给当前任务的优先级,任务切换了,优先级当然也变了。

第三部分:把sp赋值给当前任务的TCB,因为第一步已经把除sp外的所有寄存器保存了起来,sp也要记录下来以便下次知道要从任务栈的哪个地方去拿数据恢复现场。

第四部分:调用OSTaskSwHook钩子函数。

第五部分:从OSTCBHighRdy变量里获取最高优先级任务的TCB,然后赋值给sp。

第六部分:从新的sp里面恢复出所有工作寄存器。注意cpsr和spsr是不能直接进/出栈的,要用寄存器中转一下。

3、系统定时器中断内切换任务(_IntCtxSw函数):

我定义了一个周期性中断的定时器,以200Hz的频率进行中断作为操作系统的心跳。每次中断进入UCOS_IRQHandler这个函数,这个函数也是在os_cpu_a.s里面的,功能是计时和保存当前任务现场,恢复下一个任务的现场

@*********************************************************************************************************
@                                PERFORM A CONTEXT SWITCH (From an ISR)
@                                        void OSIntCtxSw(void)
@
@ Note(s): This function only flags a context switch to the ISR Handler
@
@*********************************************************************************************************

        .extern  OSIntCtxSwFlag

        .global  OSIntCtxSw

OSIntCtxSw:

        @OSIntCtxSwFlag = True
        ldr r0,=OSIntCtxSwFlag
        mov r1,#1
        str r1,[r0]
        mov pc,lr
        

@*********************************************************************************************************
@                                            IRQ HANDLER
@
@        This handles all the IRQs
@        Note: FIQ Handler should be written similar to this
@
@*********************************************************************************************************

        .extern  C_IRQHandler
        .extern  OSIntEnter
        .extern  OSIntExit

        .extern  OSIntCtxSwFlag
        .extern  OSTCBCur
        .extern  OSTaskSwHook
        .extern  OSTCBHighRdy
        .extern  OSPrioCur
        .extern  OSPrioHighRdy

@ NOINT   EQU 0xc0
.equ	NOINT, 0xc0

        .global  UCOS_IRQHandler
UCOS_IRQHandler:

        stmfd sp!,{r0-r3,r12,lr}

        bl OSIntEnter
        bl C_IRQHandler
        bl OSIntExit

        ldr r0,=OSIntCtxSwFlag
        ldr r1,[r0]
        cmp r1,#1
        beq _IntCtxSw

        ldmfd sp!,{r0-r3,r12,lr}
        subs pc,lr,#4


_IntCtxSw:
        mov r1,#0
        str r1,[r0]

        ldmfd sp!,{r0-r3,r12,lr}
        stmfd sp!,{r0-r3}
        mov r1,sp
        add sp,sp,#16
        sub r2,lr,#4

        mrs r3,spsr
        orr r0,r3,#NOINT
        msr spsr_c,r0
		
        ldr r0,=.+8
        movs pc,r0

        stmfd sp!,{r2}              @ push old task's pc
        stmfd sp!,{r4-r12,lr}       @ push old task's lr,r12-r4
        mov r4,r1                   @ Special optimised code below
        mov r5,r3
        ldmfd r4!,{r0-r3}
        stmfd sp!,{r0-r3}           @ push old task's r3-r0
        stmfd sp!,{r5}              @ push old task's psr
        mrs r4,spsr
        stmfd sp!,{r4}              @ push old task's spsr
        
        @ OSPrioCur = OSPrioHighRdy
        ldr r4,=OSPrioCur
        ldr r5,=OSPrioHighRdy
        ldrb r5,[r5]
        strb r5,[r4]
        
        @ Get current task TCB address
        ldr r4,=OSTCBCur
        ldr r5,[r4]
        str sp,[r5]                 @ store sp in preempted tasks's TCB

        bl OSTaskSwHook             @ call Task Switch Hook

        @ Get highest priority task TCB address
        ldr r6,=OSTCBHighRdy
        ldr r6,[r6]
        ldr sp,[r6]                 @ get new task's stack pointer

        @ OSTCBCur = OSTCBHighRdy
        str r6,[r4]                 @ set new current task TCB address

        ldmfd sp!,{r4}              @ pop new task's spsr
        msr SPSR_cxsf,r4
        ldmfd sp!,{r4}              @ pop new task's psr
        msr CPSR_cxsf,r4

        ldmfd sp!,{r0-r12,lr,pc}    @ pop new task's r0-r12,lr & pc

进入UCOS_IRQHandler之后,立刻将r0-r3,r12,lr保存进栈,防止下面函数破坏,为什么不把所有寄存器全进栈而只保存这几个呢?难道后面调用OSIntEnter、C_IRQHandler和OSIntExit这些函数的时候不会破坏r4-r11、r14(sp)这些寄存器吗?答案是肯定的,C语言函数有个ATPCS规则要遵循,网上可以搜到很多相关资料,C函数里用到r4-r11寄存器的话在进入函数前要把用到的寄存器先进栈保存出函数再出栈恢复,所以不担心调用C函数会破坏r4-r11,r14是栈指针C函数一进一出栈指针是不会变的。所以demo这里只保存了r0-r3,r12,lr,接下来调用OSIntEnter函数:

void  OSIntEnter (void)
{
    if (OSRunning == OS_TRUE) {
        if (OSIntNesting < 255u) {
            OSIntNesting++;                      /* Increment ISR nesting level                        */
        }
    }
}

这个函数令OSIntNesting加1,记录中断发生的次数。

void C_IRQHandler(void)
{	
	BYTE bIntOffset = REG_INTOFFSET;

	//清中断挂起位
	REG_SRCPND = (DWORD)1 << bIntOffset; 
	REG_INTPND = REG_INTPND; 
	
	OSTimeTick();
}

C_IRQHandler是个钩子函数,用来清中断挂起状态和调用OSTimeTick来使系统计时增加。

void  OSIntExit (void)
{
#if OS_CRITICAL_METHOD == 3u                               /* Allocate storage for CPU status register */
    OS_CPU_SR  cpu_sr = 0u;
#endif



    if (OSRunning == OS_TRUE) {
        OS_ENTER_CRITICAL();
        if (OSIntNesting > 0u) {                           /* Prevent OSIntNesting from wrapping       */
            OSIntNesting--;
        }
        if (OSIntNesting == 0u) {                          /* Reschedule only if all ISRs complete ... */
            if (OSLockNesting == 0u) {                     /* ... and not locked.                      */
                OS_SchedNew();
                OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
                if (OSPrioHighRdy != OSPrioCur) {          /* No Ctx Sw if current task is highest rdy */
#if OS_TASK_PROFILE_EN > 0u
                    OSTCBHighRdy->OSTCBCtxSwCtr++;         /* Inc. # of context switches to this task  */
#endif
                    OSCtxSwCtr++;                          /* Keep track of the number of ctx switches */
                    OSIntCtxSw();                          /* Perform interrupt level ctx switch       */
                }
            }
        }
        OS_EXIT_CRITICAL();
    }
}

OSIntExit函数每进一次都会令OSIntNesting减1,如果OSIntNesting为0且当前任务不是最高优先级任务时会调用OSIntCtxSw函数,这个函数无非就是让OSIntCtxSwFlag赋值为1,

        ldr r0,=OSIntCtxSwFlag
        ldr r1,[r0]
        cmp r1,#1
        beq _IntCtxSw

如果OSIntCtxSwFlag为1,那么调用_IntCtxSw进行任务切换。所以一句话来说就是当前任务不是可切换的最高优先级任务时,切换到最高优先级的任务,否则不切换也就是纯粹计时用。

小插曲:看这段汇编的时候有个小插曲,纠结了好久,就是在_IntCtxSw中有几条语句:

        msr spsr_c,r0
		
        ldr r0,=.+8
        movs pc,r0

刚开始的时候总感觉"msr spsr_c,r0"这条应该写成"msr cpsr_c,r0",因为我误认为"movs pc,r0"这条语句只是将r0的值赋值给pc而已,如果是这样的话,_IntCtxSw这段代码因为没切换回SVC模式会导致寄存器恢复错误,但是烧写程序跑起来正常。改成我那句后也是正常,纠结了好久,后来查了下资料,原来movs除了r0赋值给pc外,还会把spsr的内容赋值给cpsr使得IRQ模式切换回SVC模式。这才豁然开朗,还是汇编指令理解不过关,其实这三条汇编语句和"msr cpsr_c,r0"是等价的。

 

五、ucos临界区实现

有时候有些代码是不希望被中断打断的,它们需要一口气整段代码执行完不被外界打扰,所以需要使用OS_ENTER_CRITICAL()来进入临界区,这条语句相当于关闭mcu的中断,这样中断无法切入,其他任务也就无法切换了,在这样的状态下不应做耗时过长的操作,否则影响系统的实时性能,当我们的代码执行完后,再使用OS_EXIT_CRITICAL()退出临界区打开中断。

#if      OS_CRITICAL_METHOD == 3
#define  OS_ENTER_CRITICAL()  (cpu_sr = OSCPUSaveSR())    /* Disable interrupts                        */
#define  OS_EXIT_CRITICAL()   (OSCPURestoreSR(cpu_sr))    /* Restore  interrupts                       */
#endif

OS_ENTER_CRITICAL()和OS_EXIT_CRITICAL()分别映射到下面的汇编代码:

        .global  OSCPUSaveSR
OSCPUSaveSR:

        mrs r0,CPSR
        orr r1,r0,#NOINT
        msr CPSR_c,r1
        mov pc,lr


        .global  OSCPURestoreSR
OSCPURestoreSR:

        msr CPSR_c,r0
        mov pc,lr

进入临界区时会禁止中断,然后将原来的cpsr返回给cpu_sr保存,出临界区时会通过cpu_sr里面保存的值将之前的cpsr恢复。

至此,移植需要掌握的东西就描述完了,上面只讲了跟ucos移植密切相关的移植过程,至于定时器、中断初始化、启动文件,makefile等等都是基本操作就没必要写了。

 

六、ucos3.06移植

ucos2.91移植完之后,又趁热打铁移植了下ucos3,在官网上选了一下,发现没有同时满足s3c2440相近的cpu且版本是ucos3之后的demo可以参考,然后下了个IMX6的demo来移植(Micrium_MCIMX6SLEVK_OS3.zip),把我用不着的一些文件都给删了,把ucos2.91移植的os_cpu_a.s文件复制了过去,ucos3.06这里需要改一下,spsr不需要进任务栈保存,因为用不上(其实ucos2.91也用不上),后来编译工程又出了幺蛾子,发现os.h缺了一些变量没定义,一看发现os.h是V3.03的不是V3.06的,真纳闷官网给的demo怎么感觉有点随便,一些文件是V3.06的一些是V3.03组合在一起编译少东西,然后又在官网上下了个V3.06的其他demo,逐个文件看了下都是V3.06的这才编译通过。

 

七、工程编译时遇到的问题

工程编译时,因为OS_TaskStat这个统计任务的函数里面有条语句“OSCPUUsage   = (INT8U)(100uL - OSIdleCtrRun / OSIdleCtrMax);”,用了32位除32位的除法,我用的是arm-none-linux-gnueabi 4.3.2的编译器,编译报__aeabi_udiv的错误,后来指定了编译选项-lgcc和库目录-L/opt/usr/local/arm/4.3.2/lib/gcc/arm-none-linux-gnueabi/4.3.2,编译通过,bin烧进开发板运行后在这条除法语句这里进了异常中断,后来发现是libgcc.a这个库文件目录给错了,虽然之前指定的这个目录也有个libgcc.a,但是这个在这不能用,这级目录下还有个armv4t目录,要指定-L/opt/usr/local/arm/4.3.2/lib/gcc/arm-none-linux-gnueabi/4.3.2/armv4t/下的libgcc.a才行。

 

八、总结

通过本次ucos2移植到s3c2440实验,加深了自己对于ARM汇编的理解和掌握,了解了C函数的ATPCS规则,对嵌入式RTOS的任务切换原理和实现有了深刻的理解,后面又看了看FreeRTOS、rt-Thread这些嵌入式RTOS,其实他们的任务切换原理都是一样的,掌握了ucos的移植,通篇看了下ucos各个模块的实现原理,真的学到了很多东西,收获良多,在后面学习其他RTOS的时候会有触类旁通的效果。

 

九、参考资料

Michael_Anburaj_S3C2410_arm_ucos_110.zip

Micrium-uCOS-II-V290.zip

Micrium_MCIMX6SLEVK_OS3.zip

ucos2.91_s3c2440.rar

ucos3.06_s3c2440.rar

资料链接:链接:https://pan.baidu.com/s/1FppAwyxSBHhomMdH1znpGg  提取码:ti0z 
 

  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值