Chinaunix首页 | 论坛 | 博客
  • 博客访问: 282510
  • 博文数量: 60
  • 博客积分: 2501
  • 博客等级: 少校
  • 技术积分: 774
  • 用 户 组: 普通用户
  • 注册时间: 2009-07-16 13:27
文章分类

全部博文(60)

文章存档

2011年(1)

2010年(1)

2009年(58)

我的朋友

分类: 嵌入式

2009-08-02 12:53:27

                                                                           作者:Arming

目的在于介绍将uCOSII源代码级移置到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态,所以不再管THUMBTHUMB主要是可以提高所谓指令密度(不懂)。

 

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,碰到一个硬件不认识的指令,进入这样一种中断模式

 

处理器的中断分成IRQFIQ,一个外部中断到底是FIQ还是IRQ,可以通过设置S3C44B0x的中断控制器的INTMOD端口设定。

 

ARM态下的寄存器:

每一列是一种操作模式下访问的寄存器,不带黑三角的所有模式共享一个物理寄存器,都能访问到相同的数据(当然也能破坏掉),带三角的每种模式都有自己的一套独用物理寄存器,在别的模式下访问不到。R15用作程序计数器PCR14用作链接寄存器LR,这俩是硬件有特殊功能。一般地,R13用作堆栈指针(其实别的寄存器也行,只不过R13每个模式都各有一套)。

 

CPSR(当前程序状态寄存器)就是那个著名的程序状态寄存器。SPSR是用来保存进入当前处理器模式前的上一个模式下的CPSR的,下面的图指明了这一点。

 

程序被中断时所发生的事情:中断出现后CUP做一些事情,然后跳转到相应的中断处理地址上往下执行。0x00Reset后的跳转地址;0x04undefine中断后的跳转地址;

0x18IRQ的……

老模式(比如SVC   新模式(比如IRQ

新模式下的LRlinkReg)即R14值被抛弃,装入老模式下的PC值;新模式下的SPSR值也被抛弃,装入老模式下的CPSR。新模式下的CPSR是以前的,不过相应的IRQFIQ被屏蔽了。然后PC装入指定地址的指令(对IRQ,就是PC装入0x0018处的指令开始往下执行)。

 

其中,把R15PC值放入R14,这和连接跳转指令BL执行时的情形很相似。BL指令是调用子程序时使用的ARM指令,功能象X86系统的CALL指令。BL指令执行时,例如执行

 BL      SUB1

R14中保存了调用SUB1子程序之后的那条指令地址,就是子程序的返回地址;R15即程序计数器被装入SUB1的入口地址,然后程序从这里往下执行。如果这是个底层的子程序,R14LR的内容又没有被破坏,则返回时简单执行一条指令

        MOV     PC      LR

就可以了。如果这个子程序还要调用别的子程序,则调用时的BL指令一定会破坏LR的内容,所以如果用汇编语言写程序,LR是要自己保护的。

 

l         ARM的堆栈

ARM的系统用R13作堆栈指针。因为每个处理器操作模式下,都有自己的独立的R13,并且还访问不到别的模式下的R13,所以各种模式下,有自己独立的堆栈(如果用到的话)。在系统的初始化阶段,初始化程序就必须一个个进入各个模式然后给R13添一个指针值,错开这些堆栈段,免得运行起来后出现麻烦。

 

和堆栈操作相关的指令可以是

LDMDF         R13!,   {R0R12RLPC}

LDM是一次从内存依次装入多个寄存器的指令,R13!表示被装入的内存首地址在R13中,后面的“!”不要忘记,它是说每装入一个寄存器,R13的值就自动改变,指向下个该被装入的地址。LDM后的DF是说,R13改变地址的方式和降序满堆栈(堆栈从高地址向低地址生长,堆栈指针指向最后一个被压入的值的地址)相同。{ }里的是要装入的寄存器的列表。这条指令相当于X86一系列的POP指令。

 

STMDF         R13!,   {R0R12LRPC}

这条指令正好是和上面相对的压栈指令。注意了,这不是一条实用的指令,只是为了对照上面的指令!!后面中断处理程序中会用到实际的压栈指令的。下面的图是寄存器的值在堆栈区中被储存的情况,可以看出,R15最先被压入,最后被弹出。只要在同一条指令的列表中被列出,就总是这样,这个顺序是固定的,因为STMLDM指令用指令中的一个位的设置与否,来指示R0R15中的某个是否需要保存或取回。这样也造成同一条指令中,某个寄存器只可能被存一次或取回一次,不能压入双份的。

低地址端

 


入栈后指向

R0

R1

……

R12

LRR14

出栈后指向

PCR15

 

高地址端

 

这是以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

LRR14

PCR15

高地址端

 

每个任务都是一个函数,执行这个任务就是调用这个函数。我们设想,这个任务的函数是

    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中。

且看DocOSStartHighRdy()的任务归结如下:

    进来了之后,先可以调用一个允许用户定HOOK函数;

    把全局变量OSRuning设置为TRUE,标志多任务系统开始运行;

    OSHighRdy指向的TCB中拿到堆栈指针,放在R13里;

    从堆栈中恢复所有其他的相关寄存器,包括CPSRR0R12LRPC

这样,任务函数就象被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 taskss 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; 

    // 注意ARMIRQ中断发生后的PC保存,PCLR4,而不是前面的PCLR

    /* 另外,我们保存的是SVC模式下的现场,中断后处理器进入IRQ模式,访问不到

       SVC模式下的R13,于是在IRQ模式下,只好先令存SPSRLR,然后尽快退回到

       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}                ; 这个SPIRQ模式下的!

   

    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()里清除S3C44B0xTick_Int_Pend位,这个

; 函数在main.c里,是另加的.其实用uCOSII自己的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         在系统里挂接IRQISR

ARM系统中,一旦IRQ中断得到响应,系统进入IRQ模式(如前所述),并跳转到内存0x0018处开始执行(0x0018Arm中是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 to low 

源代码文件:

文件: uCOS for S3C44B0x.rar
大小: 163KB
下载: 下载

阅读(1736) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~