分类: 嵌入式
2009-08-02 12:53:27
作者:Arming
目的在于介绍将uCOS-II源代码级移置到S
l 硬件系统的简介
S
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,可以通过设置S
ARM态下的寄存器:
每一列是一种操作模式下访问的寄存器,不带黑三角的所有模式共享一个物理寄存器,都能访问到相同的数据(当然也能破坏掉),带三角的每种模式都有自己的一套独用物理寄存器,在别的模式下访问不到。R15用作程序计数器PC,R14用作链接寄存器LR,这俩是硬件有特殊功能。一般地,R13用作堆栈指针(其实别的寄存器也行,只不过R13每个模式都各有一套)。
CPSR(当前程序状态寄存器)就是那个著名的程序状态寄存器。SPSR是用来保存进入当前处理器模式前的上一个模式下的CPSR的,下面的图指明了这一点。
程序被中断时所发生的事情:中断出现后CUP做一些事情,然后跳转到相应的中断处理地址上往下执行。0x00是Reset后的跳转地址;0x04是undefine中断后的跳转地址;
0x18是IRQ的……
老模式(比如SVC) 新模式(比如IRQ)
其中,把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中的某个是否需要保存或取回。这样也造成同一条指令中,某个寄存器只可能被存一次或取回一次,不能压入双份的。
低地址端
入栈后指向 | |
| |
R1 | |
…… | |
R12 | |
LR(R14) | |
出栈后指向 | |
| |
| |
高地址端 |
这是以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()来给任务做一个初始的任务上下文堆栈。形状如下:
低地址端
| |
| |
任务最初的栈指针 | |
| |
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()里清除S
; 函数在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 to low
源代码文件:
文件:
uCOS for S3C44B0x.rar
大小:
163KB
下载:
下载