uCOS-II在 S3C44B0x 系统上的移置

ARM 137浏览
目的在于介绍将uCOS-II源代码级移置到S3C44B0x处理器板子上的经验,希望对今后ARM的快速上手和移置系统提供一些参考。整套初始源代码来源于liming基于Atmel ARM板的ucos演示程序,Arming在此基础上做了少量的修改。 l         硬件系统的简介 S3C44B0x是基于ARM7TDMI核的处理器,没有MMU。我的板子直接使用JTAG下载和调试,可以源代码级跟踪调试。   Arm7TDMI的处理器操作状态(operation state):包括ARM态和THUMB态。ARM态可执行32位的标准ARM指令集指令;THUMB态执行THUMB扩展的16位指令集指令,并且访问的寄存器没有ARM态多。两种状态通过CPSR(当前程序状态寄存器)中的一个位设置来标记。复位后处理器处于ARM态,我们一直工作在ARM态,所以不再管THUMB,THUMB主要是可以提高所谓指令密度(不懂)。   Arm态下处理器操作模式(operation mode): · User (用户模式): The normal ARM program execution state · FIQ (快速中断模式): Designed to support a data transfer or channel process · IRQ (中断模式): Used for general-purpose interrupt handling · SCV(超级用户模式): Protected mode for the operating system,权利很大? · Abort mode (abt): Entered after a data or instruction prefetch abort,给出的地址访问失败,会进入一个这样的中断模式 · System (sys): A privileged user mode for the operating system,不太懂,类似user态。但对CPSR(当前程序状态寄存器)的访问权大些?这个状态不能通过中断进入。 · Undefined (und): Entered when an undefined instruction is executed,碰到一个硬件不认识的指令,进入这样一种中断模式   处理器的中断分成IRQ和FIQ,一个外部中断到底是FIQ还是IRQ,可以通过设置S3C44B0x的中断控制器的INTMOD端口设定。   ARM态下的寄存器: 每一列是一种操作模式下访问的寄存器,不带黑三角的所有模式共享一个物理寄存器,都能访问到相同的数据(当然也能破坏掉),带三角的每种模式都有自己的一套独用物理寄存器,在别的模式下访问不到。R15用作程序计数器PC,R14用作链接寄存器LR,这俩是硬件有特殊功能。一般地,R13用作堆栈指针(其实别的寄存器也行,只不过R13每个模式都各有一套)。   CPSR(当前程序状态寄存器)就是那个著名的程序状态寄存器。SPSR是用来保存进入当前处理器模式前的上一个模式下的CPSR的,下面的图指明了这一点。   程序被中断时所发生的事情:中断出现后CUP做一些事情,然后跳转到相应的中断处理地址上往下执行。0x00是Reset后的跳转地址;0x04是undefine中断后的跳转地址; 0x18是IRQ的…… 老模式(比如SVC)   新模式(比如IRQ)新模式下的LR(linkReg)即R14值被抛弃,装入老模式下的PC值;新模式下的SPSR值也被抛弃,装入老模式下的CPSR。新模式下的CPSR是以前的,不过相应的IRQ或FIQ被屏蔽了。然后PC装入指定地址的指令(对IRQ,就是PC装入0x0018处的指令开始往下执行)。   其中,把R15的PC值放入R14,这和连接跳转指令BL执行时的情形很相似。BL指令是调用子程序时使用的ARM指令,功能象X86系统的CALL指令。BL指令执行时,示例执行  BL    SUB1 R14中保存了调用SUB1子程序之后的那条指令地址,就是子程序的返回地址;R15即程序计数器被装入SUB1的入口地址,然后程序从这里往下执行。如果这是个底层的子程序,R14即LR的内容又没有被破坏,则返回时简单执行一条指令         MOV    PC,      LR 就可以了。如果这个子程序还要调用别的子程序,则调用时的BL指令一定会破坏LR的内容,所以如果用汇编语言写程序,LR是要自己保护的。   l         ARM的堆栈 ARM的系统用R13作堆栈指针。因为每个处理器操作模式下,都有自己的独立的R13,并且还访问不到别的模式下的R13,所以各种模式下,有自己独立的堆栈(如果用到的话)。在系统的初始化阶段,初始化程序就必须一个个进入各个模式然后给R13添一个指针值,错开这些堆栈段,免得运行起来后出现麻烦。   和堆栈操作相关的指令可以是 LDMDF         R13!,   {R0-R12,RL,PC} LDM是一次从内存依次装入多个寄存器的指令,R13!表示被装入的内存首地址在R13中,后面的“!”不要忘记,它是说每装入一个寄存器,R13的值就自动改变,指向下个该被装入的地址。LDM后的DF是说,R13改变地址的方式和降序满堆栈(堆栈从高地址向低地址生长,堆栈指针指向最后一个被压入的值的地址)相同。{ }里的是要装入的寄存器的列表。这条指令相当于X86一系列的POP指令。   STMDF         R13!,   {R0-R12,LR,PC} 这条指令正好是和上面相对的压栈指令。注意了,这不是一条实用的指令,只是为了对照上面的指令!!后面中断处理程序中会用到实际的压栈指令的。下面的图是寄存器的值在堆栈区中被储存的情况,可以看出,R15最先被压入,最后被弹出。只要在同一条指令的列表中被列出,就总是这样,这个顺序是固定的,因为STM和LDM指令用指令中的一个位的设置与否,来指示R0到R15中的某个是否需要保存或取回。这样也造成同一条指令中,某个寄存器只可能被存一次或取回一次,不能压入双份的。 低地址端  入栈后指向 R0 R1 …… R12 LR(R14) 出栈后指向PC(R15)   高地址端   这是以R13为堆栈指针的,堆栈是向地址低端生长的满堆栈。当然,别的寄存器也可以这样操作,不过用R13。   l         uCOS移置的相关问题 uCOS的设计者写的程序非常有利于移置,而且有详细的文档。我们需要做的就是从网上down一版源代码和文档比较相符的版本。   文件结构 uCOS_ii.c   (这个文件定义了一些控制宏,并且包含的所有的CPU无关代码) |---#include "os_core.c" |---#include "os_mbox.c" |---#include "os_mem.c" |---#include "os_q.c" |---#include "os_sem.c" |---#include "os_task.c" |---#include "os_time.c" |---#include "os_mutex.c" os_CPU_a.s (这个文件是移置重点,汇编代码的) os_CPU_c.c  (这个文件包含一个和CPU结构相关的任务堆栈初始化函数,以及用户可以利用的一系列钩子函数,可以处理特殊硬件扩展、MMU、调试等之用) os_CPU.h   (一个需要根据CPU的指令字长和硬件更改的头文件,我拿到的是改好的,一看就明白了)   其它的就是另一些头文件了。有关各个文件的说明可以看文档,uCOS的整体结构则可以在第三章Core中大体了解。需要仔细了解的就是和任务切换相关的几个函数、宏,以及它们之间的调用关系了。这需要查看文档的相关章节和仔细阅读一部分源代码。   和移置比较相关的几个地方大致如下: ◎任务的最初堆栈。UCOS—II的任务,在没有执行的时候就象是刚刚被中断一样,任务一经CreateTask()创建,就是这样的。堆栈则是任务上下文(contex)的一部分,CreateTask()调用OSTaskStkInit()来给任务做一个初始的任务上下文堆栈。形状如下: 低地址端      任务最初的栈指针 CPSR R0 R1 …… R12 LR(R14) PC(R15) 高地址端   每个任务都是一个函数,执行这个任务就是调用这个函数。我们设想,这个任务的函数是     void Task1(void *pdata); 执行这任务就是用BL指令来调用函数,刚执行完BL指令,就在这时,中断发生了,于是堆栈就是上面的样子。且看源代码文件os_CPU_c.c,其中的OSTaskStkInit()就改写成: OS_STK * OSTaskStkInit (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT16U opt) {     unsigned int *stk;     opt    = opt;                     /* 'opt' is not used, prevent warning     */     stk    = (unsigned int *)ptos;    /* Load stack pointer                     */       /* build a context for the new task */            //看这里呀,这就是要改的地方:     *--stk = (unsigned int) task;       /* pc */     //PC当然就是这个了,待会要从这里往下执行     *--stk = (unsigned int) task;       /* lr */ *--stk = 0;                         /* r12 */ *--stk = 0;                         /* r11 */     *--stk = 0;                         /* r10 */     *--stk = 0;                         /* r9 */     *--stk = 0;                         /* r8 */     *--stk = 0;                         /* r7 */     *--stk = 0;                         /* r6 */     *--stk = 0;                         /* r5 */     *--stk = 0;                         /* r4 */     *--stk = 0;                         /* r3 */     *--stk = 0;                         /* r2 */     *--stk = 0;                         /* r1 */ *--stk = (unsigned int) pdata;      /* r0 */     /* 注意上面一行,task1的第一个参数放这里,这是符合ARM调用规范的,就是说,规范要求汇编程序在BL指令之前,传递给函数的第一个参数要放在R0里;记住我们的堆栈是刚刚执行完BL TASKn之后的样*/     *--stk = (SVC32MODE|0x40);            /* cpsr  FIQ disable*/  //   *--stk = (SVC32MODE|0x40);          /* spsr  FIQ disable */ /* 我把上面一行去掉了,因为我的上下文中根本没有保存SPSR,这不是任务所关心的,任务运行在SVC模式,只能被中断进入其他模式,而从不用返回到别的什么模式去,因此SVC模式下的SPSR没用到。*/     return ((OS_STK *)stk); } 至此,堆栈就成了上面图中的样子。os_CPU_c.c也就OK了。   下面,应该看看Doc的Chapter 8,Porting —— porting的任务单就在这里了。前面已经说了一些东西,剩下的就是os_CPU_a.s里的了。在这里头,我们要根据硬件写4个函数:     OSStartHighRdy()     OSCtxSw()     OSIntCtxSw()     OSTickISR()   OSStartHighRdy()是系统刚刚创建完最初的若干任务后,有一个OSStart()就开始RUN了.OSStart()找到最优先的任务,最后就调用OSStartHighRdy()来启动那个任务。在OSStartHighRdy()被调用之前,优先级最高的任务的TCB(很重要的概念)的指针(同时也是刚才放好的堆栈指针)已经被OSStart()放在全局变量OSTCBHighRdy中。且看Doc,OSStartHighRdy()的任务归结如下:     进来了之后,先可以调用一个允许用户定HOOK函数;     把全局变量OSRuning设置为TRUE,标志多任务系统开始运行;     从OSHighRdy指向的TCB中拿到堆栈指针,放在R13里;     从堆栈中恢复所有其他的相关寄存器,包括CPSR,R0-R12,LR,PC。这样,任务函数就象被BL指令调用了一样,从Task的第一条指令开始执行了。源代码比较清晰这里不重复了。   除了这个最初的调度之外,系统里还会发生其他两种情况的调度。之一,一个任务调用OsxxxPend()、OSTimDelay()之类的函数,主动放弃CPU的使用权;之二,一个任务正在执行,不愿意中止却被中断了,这个中断源恰恰是OSTimTick之类,要引起系统重新调度,这时发生的是抢占式调度(preemptive scheduling)。抢占式调度是实时系统实现的法宝,也是uCOS-II的特色。   对于前者,它所调用的放弃CPU的函数最后会调用一个OS自己使用的函数OS_Sched(),这个函数找到下一个可以立即执行的最高优先级任务,把他的TCB指针(=他的堆栈指针)放在全局变量OSTCBHighRdy里,然后调用一个宏OS_TASK_SW(),这个宏实际上被定义为os_CPU_a.s中的函数OSCtxSw()。由此可以了解OSCtxSw()的任务:保存当前任务上下文,装入新任务上下文。当然还有一些其他琐碎。Doc中给出了伪代码如下: void OSCtxSw(void) {     Save processor registers;                                                   Save the current task’s stack pointer into the current task’s OS_TCB:        OSTCBCur->OSTCBStkPtr = Stack pointer;     Call user definable OSTaskSwHook();                                     OSTCBCur  = OSTCBHighRdy;                                                   OSPrioCur = OSPrioHighRdy;                                                Get the stack pointer of the task to resume:                              Stack pointer = OSTCBHighRdy->OSTCBStkPtr;     Restore all processor registers from the new task’s stack;          Execute a return from interrupt instruction; } 看得出Doc的意思是让我们用软件中断来调用OSCtxSw(),它有他的道理,我们不用软件中断,但无妨。只是在最后执行一个普通函数该执行的返回历程就可以了。源代码:         EXPORT OSCtxSw             ;这个函数别的文件要用         IMPORT  OSPrioCur           ;这是在别的文件定义的变量,当前任务优先级         IMPORT  OSPrioHighRdy       ;将要恢复执行的任务的优先级         IMPORT  OSTCBCur            ;当前任务的TCB的指针         IMPORT  OSTaskSwHook        ;调用用户定义HOOK         IMPORT  OSTCBHighRdy        ;将要恢复执行的任务的TCB指针         OSCtxSw         STMFD   sp!, {lr}           ; push pc ,因为是从OS_Sched() BL到这里的         STMFD   sp!, {r0-r12,lr}    ; push lr & register file           MRS     r4, cpsr            ; CPSR特殊,只能用MRS或MSR在寄存器间操作         STMFD   sp!, {r4}           ; push current psr           LDR     r4, =OSTCBCur       ; R4里就是OSTCBCur变量的地址了         LDR     r5, [r4]            ; R5里就是OSTCBCur —— 堆栈指针的存放地址         STR     sp, [r5]            ; store sp in preempted tasks’s TCB   OSIntCtxSw   ;这是下一个函数了,它只是这个函数的后半部,这个函数则继续往下         BL      OSTaskSwHook        ; 让用户执行一点HOOK                 LDR     r4, =OSTCBHighRdy         LDR     r4, [r4]         LDR     r5, =OSTCBCur         STR     r4, [r5]            ; OSTCBCur = OSTCBHighRdy           LDR     r6, =OSPrioHighRdy         LDRB    r6, [r6]         LDR     r5, =OSPrioCur         STRB    r6, [r5]            ; OSPrioCur = OSPrioHighRdy           LDR     sp, [r4]             LDMFD   sp!, {r4}               ; pop new task cpsr         MSR     cpsr_cxsf, r4         LDMFD   sp!, {r0-r12,lr,pc}     ; pop new task r0-r12,lr & pc 到此结束。   OSIntCtxSw()就是刚才提到的第二种情况,在中断中发生调度时用到。如果一个中断需要从中断服务程序中进行调度,uCOS-II的文档给出了这种调度的处理方法:一旦有这种中断发生,就保存正在运行任务的上下文,然后处理中断,之后调用一个操作系统提供的在ISR中使用的Mutex、MailBox之类的服务函数,在这些函数的最后都会调用OSIntExit(),在这里发生了调度,OSIntExit()找出可以运行的最高优先级任务,把他的TCB指针放在OSTCBHighRdy里,最后如果需要调度,OSIntCtxSw()被调用。这么多层的调用,在处理时一定要小心,每一个用汇编语言写成的函数都要象一个普通的函数一样,现场保存,处理,现场恢复。当然,这里保存和恢复的东西多了点,是整个的任务上下文。   关于OSIntCtxSw()就是上面那下半截,这和Doc中的伪代码有出入。这是因为:ARM硬件的中断时续并不自动压栈任何寄存器,所以免去了恢复堆栈指针的麻烦;另外,我们最好在进入ISR保存当前任务现场时一同保存好TCB中的堆栈指针,而不是在OSIntCtxSw()中保存,这样就又省了一步(实际上在ARM系统下这样做反倒容易,在OSIntCtxSw()中保存堆栈指针反倒找不着应该把指针恢复到哪里去,因为这时多了一层OSIntCtxSw()的调用,而OSIntCtxSw()是C语言写的,在OSIntCtxSw()里,我们怎么知道编译器把堆栈指针又挪了多少呢!)。下面是这个函数的伪代码和实际函数对照。 void OSIntCtxSw(void) {     Adjust the stack pointer to remove calls to:         OSIntExit(), OSIntCtxSw() and possibly the push of the processor status word;       Save the current task’s stack pointer into the current task’s OS_TCB:         OSTCBCur->OSTCBStkPtr = Stack pointer;       Call user definable OSTaskSwHook();       OSTCBCur  = OSTCBHighRdy;     OSPrioCur = OSPrioHighRdy;       Get the stack pointer of the task to resume:         Stack pointer = OSTCBHighRdy->OSTCBStkPtr;       Restore all processor registers from the new task’s stack;     Execute a return from interrupt instruction; }     os_CPU_a.s之最后一役:OSTickISR()。这是 UCOS-II 抢占式调度ISR的一个标本。当一个优先级高的任务放弃CPU使用权,示例要休眠 10 个 Tick,系统调度一个低优先级的任务执行之。OSTickISR()为休眠的任务计时,每次执行,就把休眠任务剩余的睡觉时间减去一个Tick数。如果发现一个任务睡够了,就顺便恢复它为READY态。做完该做的一切,一个对OSIntExit()的调用,使调度发生了。OSIntExit()从所有已经READY的任务中,选择一个优先级最高的,恢复现场并往下执行。所以可以相信,10个Tick之后,恢复到那个高优先级的任务现场,然后执行它。所有要做的事情看伪代码: void OSTickISR(void) {    Save processor registers;      // 注意ARM的IRQ中断发生后的PC保存,PC=LR-4,而不是前面的PC=LR 。     /* 另外,我们保存的是SVC模式下的现场,中断后处理器进入IRQ模式,访问不到        SVC模式下的R13,于是在IRQ模式下,只好先令存SPSR和LR,然后尽快退回到        SVC模式,这时的R13才是任务的堆栈指针。立即保存所有寄存器。 */     /* 记住这里我们除了压栈所有寄存器,还要保存堆栈指针到当前任务的TCB中,        因为我们在OSIntCtxSw()里没有做这个工作。OSIntCtxSw()是在        OSIntExit()中被调用的。See source code of OSIntExit(). */       Call OSIntEnter() or increment OSIntNesting;   /* 防止在嵌套的中断中发生调度 */       Call OSTimeTick();   // 完成休眠计时,任务唤醒到READY状态等工作       Call OSIntExit();    // 调度!如果发生了任务切换,下面的代码就执行不到了       /* 调度结果:不用切换任务,于是OSIntExit()退出到这里,恢复原来的现场 */     Restore processor registers;     Execute a return from interrupt instruction; } 源代码如下:     EXPORT OSTickISR     IMPORT  OSIntEnter     IMPORT  OSTimeTick     IMPORT  tick_hook       IMPORT  OSIntExit   LINK_SAVE   DCD     0 PSR_SAVE    DCD     0   OSTickISR     STMFD   sp!, {r4}                ; 这个SP是IRQ模式下的!         LDR     r4, =LINK_SAVE     STR     lr, [r4]                ; LINK_SAVE = lr_irq       MRS     lr, spsr     STR     lr, [r4, #4]            ; PSR_SAVE = spsr_irq         LDMFD   sp!, {r4}                ; 只好重新恢复R4             ORR     lr, lr, #0x80           ; Mask irq for context switching before     MSR     cpsr_cxsf, lr           ; returning back from irq mode.我们还没有 ; 保存好现场,如果打开中断,就有可能发生新的 ; 抢占式调度,于是这个现场就OVER了。现场保 ; 护和现场恢复都要一气呵成。       SUB     sp, sp, #4              ; Space for PC     STMFD   sp!, {r0-r12, lr}       LDR     r4, =LINK_SAVE     LDR     lr, [r4, #0]     SUB     lr, lr, #4              ; PC = LINK_SAVE - 4,     STR     lr, [sp, #(14*4)]       ; SAVE PC [..]the return address for pc.       LDR     r4, [r4, #4]            ; r4 = PSR_SAVE,     STMFD   sp!, {r4}                ; CPSR of the task       LDR     r4, =OSTCBCur     LDR     r4, [r4]     STR     sp, [r4]                ; OSTCBCur -> stkptr = sp  保存现场完毕       BL  OSIntEnter     BL OSTimeTick     BL  tick_hook     ; 我们在Tick_hook()里清除S3C44B0x的Tick_Int_Pend位,这个 ; 函数在main.c里,是另加的.其实用uCOS-II自己的HOOK更好     BL  OSIntExit                    ; 调度!       LDMFD   sp!, {r4}                ; pop current task cpsr     MSR     cpsr_cxsf, r4     LDMFD   sp!, {r0-r12,lr,pc}      ; pop current task r0-r12,lr & pc ;OSTickISR()完。   l         在系统里挂接IRQ的ISR 在ARM系统中,一旦IRQ中断得到响应,系统进入IRQ模式(如前所述),并跳转到内存0x0018处开始执行(0x0018在Arm中是IRQ的中断入口地址)。在我的系统中,这段代码处于板上ROM里。为了不修改ROM就可以重定位各个ISR,系统在RAM的高端开辟了一个向量表,表中的每4个字节填入一个真正的ISR入口地址。当IRQ中断响应时,ROM代码先从0x0018处再一次跳转(不跳不行啊,不然下一条指令就是另外的中断入口了),执行一段从RAM高端的向量表里查取真正IRQISR入口地址的程序,找到这个地址后,把它装入PC寄存器,于是开始了中断服务。   我们的演示程序里只有一个TickISR,是唯一的IRQISR,所以在系统初始化后,把TickISR的入口写入RAM高端的向量表里,就挂接了这个ISR。   具体的向量表定义在startup/memcfig.s里。挂接ISR的函数在src/Main.c里。   l         其他杂碎包括:在文件os_CPU.h里, OS_ENTER_CRITICAL()宏被定义为 os_CPU_a.s中的ARMDisableInt()——关闭中断; OS_EXIT_CRITICAL()宏被定义为 os_CPU_a.s中的ARMEnableInt()——打开中断;#define OS_STK_GROWTH       1       // stacks grow from high