uCOS-ii课程资料*
重庆工学院电子信息与自动化学院 万文略
wanwenlue@cqit.edu.cn
注:
本文档仅供重庆工学院研究生学习研究操作系统时使用。
文中部分程序为周立功公司的移植代码。
文中部分程序为(美)Jean J. Labrosse所写。
由于本人水平所限,不当之处敬请指正。
请支持开源,支持正版,自觉支付版税。
操作系统分析要点
操作系统90%的代码使用C语言编写,进行移植操作系统时,处理器相关部分须汇编语言编写,所以要求使用的C编译器能够进行C汇编混合编程。 混合编成的主要问题:
1. 汇编语言存取C的全局变量
2. C程序调用汇编程序
3. 汇编调用C程序
4. C程序嵌入汇编代码
5. 处理中断
在ADS的Online Book 中的Developer Guide里详细描述了C C++和汇编混合编程的问题。
一、汇编语言存取C的全局变量
对无符号的C全局变量可以使用
LDRB/STRB 指令存取 char型变量
LDRH/STRH 指令存取short型变量
LDR/STR 指令存取int型变量.
对有符号的变量,使用对用的有符号数操作指令LDRSB 和LDRSH。 例:
Example Address of global
AREA globals,CODE,READONLY
EXPORT asmsubroutine ;用EXPORT指示符声明供外部程序使用的标号 BY wanwenlue编译器的问题
IMPORT globvar ;globvar是C程序中的无符号全局变量,使用IMPORT编译指示符声明该变量是外部变量。
asmsubroutine
LDR r1, =globvar ;从文字池中将globvar变量的地址传给R1,注意等号的使用
LDR r0, [r1] ;
ADD r0, r0, #2
STR r0, [r1]
MOV pc, lr
END
二、C 程序调用汇编。
例
Example 2 C程序调用汇编
在C程序中用extern 声明外部定义的函数
#include <stdio.h>
extern void strcopy(char *d, const char *s);//在C程序中用extern 声明外部定义的函数,这个函数在汇编程序中定义
int main()
{ const char *srcstr = "First string - source ";
char dststr[] = "Second string - destination ";
/* dststr is an array since we're going to change it */
printf("Before copying:n");
printf(" %sn %sn",srcstr,dststr);
strcopy(dststr,srcstr);
printf("After copying:n");
printf(" %sn %sn",srcstr,dststr);
return (0);
}
汇编程序
在汇编程序中用EXPORT指示符声明供外部使用的标号
Example 4-10 Assembly language string copy subroutine
AREA SCopy, CODE, READONLY
EXPORT strcopy ;在汇编程序中用EXPORT指示符声明供外部使用的标号
strcopy ; r0 points to destination string.
; r1 points to source string.
LDRB r2, [r1],#1 ; Load byte and update address.
STRB r2, [r0],#1 ; Store byte and update address.
CMP r2, #0 ; Check for zero terminator.
BNE strcopy ; Keep going if not.
MOV pc,lr ; Return.
END BY wanwenlue
注意当使用函数参数时要使用ATPCS调用规则。
三、汇编调用C程序
Example 4-11 Defining the function in C
定义C函数
int g(int a, int b, int c, int d, int e)
{
return a + b + c + d + e;
}
在汇编程序中使用IMPORT 指示符声明外部符号,实用BL指令调用C程序。 Example 4-12 Assembly language call
; int f(int i) { return g(i, 2*i, 3*i, 4*i, 5*i); }
EXPORT f
AREA f, CODE, READONLY
IMPORT g ; i is in r0
STR lr, [sp, #-4]! ; preserve lr
ADD r1, r0, r0 ; compute 2*i (2nd param)
ADD r2, r1, r0 ; compute 3*i (3rd param)
ADD r3, r1, r2 ; compute 5*i
STR r3, [sp, #-4]! ; 5th param on stack
ADD r3, r1, r1 ; compute 4*i (4th param)
BL g ; branch to C function
ADD sp, sp, #4 ; remove 5th param
LDR pc, [sp], #4 ; return
END
四、C程序嵌入汇编代码
使用__asm {
汇编指令
。。。。。。
}
在C程序中嵌入汇编,不提倡在C中嵌入汇编,C编译器的优化选项可能删除在线汇编代码。
Example 4-2 Interrupts
__inline void enable_IRQ(void)
{
int tmp;
__asm
{ BY wanwenlue
中断处理是处理器、编译器相关的。
在ARM7上移植和使用操作系统时使用最多的中断是软中断和IRQ中断。编程时的主要问题是程序接口。每个中断都由固定的入口地址,当一个特定的中断发生时,如何转到正确地中断服务程序。
ADS Online Book的Developer Guide 中详细介绍的C中的中断处理程序设计方法。
1.软中断处理
中断矢量初始化,启动代码中
Reset
LDR PC, ResetAddr
LDR PC, UndefinedAddr
LDR PC, SWI_Addr 转软中断处理
……
ResetAddr DCD ResetInit
UndefinedAddr DCD Undefined
SWI_Addr DCD SoftwareInterrupt 软中断处理程序入口
ARM7处理软中断时进入管理模式,将产生软中断时的处理器状态寄存器保存在管BY w五、处理中断 anwe MRS tmp, CPSR BIC tmp, tmp, #0x80 MSR CPSR_c, tmp } } __inline void disable_IRQ(void) { int tmp; __asm { MRS tmp, CPSR ORR tmp, tmp, #0x80 MSR CPSR_c, tmp } } int main(void) { disable_IRQ(); enable_IRQ(); } nlue
理模式的SPSR,将返回地址保存在管理模式的R14(例如LR)寄存器中。
软中断的功能号安排在软中断指令编码中。将LR-4可以读软中断指令编码 LDR r0, [lr,#-4]
对于ARM 模式,将指令编码的高8位清零得到软中断功能号
BIC r0, r0, #0xFF000000
R0 中为软中断功能号,软中断处理程序根据这个功能号跳转到相应的处理程序。 在C语言中,可以使用__SWI(功能号) 关键字声明一个不存在的函数,例如 __swi(0x03) void SoftWareInterrupt(void);
编译时,编译器在函数所在位置插入软中断汇编指令代码
SWI 0X03
这样就实现了C下的软中断处理。
2.IRQ中断处理
IRQ中断处理要麻烦些,NXP 的LPC21xx 系列处理器的外部部件使用VIC中断,共有19个外部中断源,这些中断源可以设置为IRQ或FIQ中断。这里只讨论这些外部中断源使用IRQ中断的情况。在CPU中有一个VICVectAddr寄存器,当产生外部中断时,硬件自动将中断服务程序入口地址填入VICVectAddr,VICVectAddr的定义如下代码所示:
#define VICVectAddr (*((volatile unsigned long *) 0xFFFFF030))
启动代码的初始化方式有2种,第1种是自动跳转方式。
LDR PC, [PC, #-0xff0]
要注意的是上面的指令只能安排在0x00000018处,产生外部中断时处理器自动转到这条指令处执行,执行这条指令时PC=0x00000020(由于指令流水线的作用),再减0xff0时刚好等于0xFFFFF030,也就是转到VICVectAddr中保存的地址去执行,也就转中断服务程序了。
在C语言使用IRQ中断时可以使用__irq关键字声明一个IRQ中断服务程序。 void __irq Timer0_Handler(void);
在中断初始化程序时,初始化其用的slot
VICVectAddr0 = (uint32)Timer0_Handler;
VICVectCntl0 = (0x20 | 0x04);
VICIntEnable = 1 << 4;
在使用操作系统时,需要汇编语言支持,因此周立功定义了一个中断处理宏。本文后面有详述。
还有一种方法处理IRQ中断。在启动代码中
LDR PC,IRQ_Addr
IRQ_Addr DCD IRQ_Handler
这样在发生IRQ中断时程序都转IRQ_Handler处执行,在保存断点后用BL指令转到OS_CPU_ExceptHndlr处的C代码执行。
#define VIC_IRQ (*(INT32U *)0xFFFFF030)
#define VIC_FIQ (*(INT32U *)0xFFFFF034)
typedef void (*BSP_FNCT_PTR)(void); 定义函数指针类型
void OS_CPU_ExceptHndlr (CPU_DATA except_type)
{ BY wanwenlue
指针,指向结构体的指针,指针数组,结构数组。见C程序设计有关资料。
操作系统基本概念
在实时操作系统中,任务有运行,就绪,挂起,被中断,休眠等5种状态。
运行状态是指任务获得CPU的控制权,正在运行。
就绪状态是指高优先级的任务正在运行,低优先级任务等待获得CPU控制权的状态。 挂起状态是指任务等待事件发生或等待信号,当事件发生或其他任务向挂起的任务发出信号量时,刮起状态的任务就进入就绪态。所以刮起态与就绪态有明显的区别。
被中断是一个任务正在运行时,系统产生异步中断(发生异常)使任务中止运行的状态。典型的例如时钟节拍中断。
实时系统靠时钟节拍驱动,时钟节拍得实质是系统的定时中断,根据系统的实时性要求,这个定时中断的定时时间一般在10ms~100ms左右,定时时间短则系统实时性高但系统开销大。系统可以利用定时中断检查是否有高优先级的任务就绪,以进行任务切换。也利用时钟节拍使任务延时一段时间运行。
临界区代码,指任务中的某段程序需要连续执行完,不能被其他任务打断。执行临界区的代码需要关中断。
重入,指某个函数在多个任务中都要使用,这时要避免使用全局变量,若使用全局变BY w数据结构问题 anweBSP_FNCT_PTR pfnct; pfnct是函数指针 CPU_INT32U *sp; if (except_type == OS_CPU_ARM_EXCEPT_FIQ) { pfnct = (BSP_FNCT_PTR)*VIC_FIQ; / *为指针赋值*/ while (pfnct != (BSP_FNCT_PTR)0) { /* 若不是空指针*/ (*pfnct)(); /* 执行中断处理程序 */ pfnct = (BSP_FNCT_PTR)*VIC_FIQ; /* 再读一遍,注意当中断服务程序执行完时要把*VIC_FIQ清零,否则将是死循环。 */ } else if (except_type == OS_CPU_ARM_EXCEPT_IRQ) { pfnct = (BSP_FNCT_PTR)*VIC_IRQ; /* Read the IRQ handler from the VIC. */ while (pfnct != (BSP_FNCT_PTR)0) { /* Make sure we don't have a NULL pointer.*/ (*pfnct)(); /* Execute the handler. */ pfnct = (BSP_FNCT_PTR)*VIC_IRQ; /* Read the IRQ handler from the VIC. */ } else { /* Other exception handling */ } } } CPU_DATA except_type 是R0 寄存器。 注意在使用ADS编译时选择使能ATPCS子程序调用标准。 本代码详见Jean J. Labrosse所著AN1014.PDF nlue
量,需要使用互斥信号量。
只有两种情况需要进行任务切换,一是任务主动放弃CPU ,这时进行任务切换。比如一个任务运行时需要等待一个异步事件的发生,或任务本身需要延时一段时间,这时任务要放弃CPU的控制权,以使其他任务得到运行,这属于任务级的任务切换;另一种情况是系统产生了中断使高优先级的任务就绪。这属于中断级的任务切换。任务级的任务切换切换时机是程序事先安排好的,比如在任务中安排等待事件、信号量,或延时几个时钟节拍的程序,这时正在运行的任务就被挂起,另外一个高优先级的任务得到运行。
在占先式实时操作系统中,高优先级的任务一旦就绪就立即得到运行。实现这种机制只有靠中断。中断可在任务的非临界区代码的任何一处发生。
任务切换与转子程序不同。一个程序调用另一个子程序是事先安排好的,子程序的返回也返回到调用处。但任务切换不同。任务的切换不能靠调用子程序的机制实现。因为第一何时进行任务切换不完全能事先安排好。他可能发生在程序的非临界区的任何地方。不可能事先安排好。切换到另外一个任务时,任务的入口是该任务被中断处,可能是非临界区的任何一个地点。
切换任务通常使用中断机制,硬件中断或软中断。硬件中断可在非临界区的任何地方发生。软中断是实现安排好的。通常一个任务主动放弃CPU时使用软中断切换任务。被中断时在中断服务程序实现任务切换。
操作系统采用堆栈变换实现任务的切换,每个任务有一个自己的堆栈空间。当进行任务切换时,内核要将任务断点保存到任务堆栈中,包括返回地址,所有的寄存器,关中断次数计数器。这些工作在中断服务程序内完成。然后将堆栈指针指向另一个任务的堆栈,这个堆栈的结构与被切换任务保存断点的堆栈结构相同。由于中断程序的堆栈指针被修改,所以当执行中断返回时返回到另一个任务的断点处,这样实现了任务切换。
所有任务的堆栈,堆栈指针,任务的状态等信息都要合理的保存,也要方便地被查找,以提高操作系统得运行效率。
任务控制块是一种保存任务状态的数据结构,所有任务的任务控制块在内核中以双向链表的形式存在。内核用按优先级排序的指向任务控制块的指针数组实现基于优先级的任务控制块查找。
通常操作系统管理多个任务,任务的切换基于优先级。系统运行时如何迅速地找到当前就绪的最高优先级的任务?即任务调度算法。一个好的任务调度算法可以提高系统的实时性。在uCOS-ii中使用查找表算法实现任务的查找。
就绪表及其查找算法
uCOS-ii的就绪表是一个最多由8个字节组成的数组。数组中的每一位(bit)表示一个任务的状态是就绪或非就绪。按数组的下标及一个字节的位序排列。由于数组中的每个bit表示一个任务的就绪状态,8个字节有64位,所以uCOS-ii最多可以管理64个任务,这在多数系统中已经足够了。数组下标为0的单元字节中的第0位表示优先级为0的任务的就绪状态,第1位表示优先级为1的任务的就绪状态。由于一个字节由8个二进制位组成,所以将用十进制表示的任务优先级换成8进制更容易找到任务状态位在就绪表中的位置。例如优先级为44(十进制)的任务,其八进制优先级为54O,其就绪状态在就绪表中的第5个字节的第4位。将任务优先级右移3位就可以得到该优先级在就绪表中的字节位置,将优先级与0x07相与得到位的位置。这是已知任务优先级查找就绪表的算法。这种算法用于已知任务优先级,需要修改这个任务的就绪状态时使用。
uCOS-ii中使用OSRdyTbl[8]保存就绪表,使用字节变量OSRdyGrp表示就绪组。BY wanwenlue
OSRdyGrp中的每一位表示OSRdyTbl[8]数组中的一个字节中是否有就绪任务。也就是说就绪表的OSRdyTbl[0]有任何任务就绪,就使OSRdyGrp中的第0位置1,否则清0。OSRdyTbl[1]中有任何任务就绪就使OSRdyGrp中的第1位置1,否则清0。以此类推。
uCOS-ii定义了下面的常数表方便执行就绪表的操作。
INT8U const OSMapTbl[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; 使优先级为prio的任务进入就绪态。
OSRdyGrp |=OSMapTbl[prio>>3];
OSRdyTbl[prio>>3] |=OSMapTbl[prio&0x07];
使优先级为prio的任务脱离就绪态。
If((OSRdyTbl[prio>>3]&=~OSMapTbl[prio&0x07])==0)
OSRdyGrp&=~OSMapTbl[prio>>3];
OSRdyGrp的某位置1表示一个组的状态而不是某一个任务的状态,只有对应OSRdyTbl[]中的所有位都是0的时候才将OSRdyGrp清零,所以用了if语句。
另一种查找是已知OSRdyGrp,OSRdyTbl[],需要在表中查找最高优先级的就绪任务的优先级。
uCOS-ii建立了下面的常数表,利用OSRdyGrp,OSRdyTbl[]查这个表得到已就绪任务的最高优先级。
表的建立基于八进制数的优先级,分高位和低位,用OSRdyGrp查表得到任务优先级的高位,用OSRdyTbl[]查表得到优先级的低位,再转换成16进制数。
INT8U const OSUnMapTbl[] = {
0, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
};
Y=OSUnMapTbl[OSRdyGrp]
X=OSUnMapTbl[OSRdyTbl[Y]]
Prio=Y<<3+X;
任务控制块
任务控制块是操作系统中最重要的数据结构,低版本的uCOS-ii的任务控制块较为简BY wanwenlue
单,高版本的uCOS-ii对低版本的基础上对任务控制块进行了扩展,但用途不大,对初学者的学习带来一定的困难。学习时掌握低版本的任务控制块原理就可以了。
任务控制块是一个结构体,有以下几个重要的结构成员。
typedef struct os_tcb {
OS_STK *OSTCBStkPtr; 当前任务的堆栈指针
struct os_tcb *OSTCBNext; 指向下一个任务控制块的任务控制块指针
struct os_tcb *OSTCBPrev; 指向前一个任务控制块的任务控制块指针
INT16U OSTCBDly; 任务要延时的时钟节拍数,以定时器定时时间为单位 INT8U OSTCBStat; 任务状态
INT8U OSTCBPrio; 任务优先级
INT8U OSTCBX; 八进制表示的任务优先级的低位
INT8U OSTCBY; 八进制表示的任务优先级的高位
INT8U OSTCBBitX; =OSMapTbl[OSTCBX]
INT8U OSTCBBitY; =OSMapTbl[OSTCBY]
}
任务控制块中的后4个成员使查表操作更快。
在uCOS-ii中每个任务都有一个任务控制块,任务控制块在操作系统初始化时就分配
了内存,构成了双向链表。
操作任务控制块的全局变量有
OS_EXT OS_TCB *OSTCBCur; 当前正在运行的任务控制块指针 OS_EXT OS_TCB *OSTCBFreeList; 指向空任务控制块指针,空任务控制块是没
有被任何任务使用的任务控制块。
OS_EXT OS_TCB *OSTCBHighRdy; 指向已就绪最高优先级任务控制块指针
OS_EXT OS_TCB *OSTCBList; 任务控制块双向链表的表头指针
OS_EXT OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1];指针数组,按任务的
优先级排列的任务控制块指针数组。数组的下标是优先级,数组的内容是该优先级任务的任务控制块指针。
OS_EXT OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS]; 任务控
制块数组,下标的顺序就是任务建立的顺序
内核初始化时,先建立任务控制块链表。OSTCBTbl[0]->Next= OSTCBTbl[1],OSTCBTbl[1]->Next= OSTCBTbl[2];OSTCBFreeList指向表中的第一个单元。即OSTCBFreeList=& OSTCBTbl[0]。新建任务时,设任务优先级是prio,该任务使用 *OSTCBFreeList指向的OSTCBTbl[0]空间,然后修改OSTCBFreeList 使其指向OSTCBTbl[]的下一个单元,即OSTCBTbl[1]同时让OSTCBPrioTbl[prio]= OSTCBTbl[0],以此类推。这时既建立了任务控制块双向链表,也建立了按任务优先级排列的任务控制块指针表。这样便于程序查找。
*OSTCBList这个指针的用法见void OSTimeTick (void)函数。在这里用*OSTCBList索引已建立任务的TCB,修改TCB中的延时参数OSTCBDly。这个函数在每次发生定时中断时调用,当OSTCBDly减为0时就修改任务状态为就绪。实现任务的延时等待功能。
if (OSRunning == TRUE) {
ptcb = OSTCBList; ptcb 指向双向链表的表头
while (ptcb->OSTCBPrio != OS_IDLE_PRIO) { 如果不是空闲任务则循环 OS_ENTER_CRITICAL(); 关中断
if (ptcb->OSTCBDly != 0) { 如果该任务的OSTCBDly不为0 BY wanwenlue
if (--ptcb->OSTCBDly == 0) { 减1并判断是否为0
if ((ptcb->OSTCBStat & OS_STAT_SUSPEND) == OS_STAT_RDY) {
OSRdyGrp |= ptcb->OSTCBBitY; 不是被删除的任务则使任
务进入就绪态
OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
} else {
ptcb->OSTCBDly = 1; 如果是被删除的任务则不修改任务
状态
}
}
}
ptcb = ptcb->OSTCBNext; 修改指针指向下一个任务,循环 OS_EXIT_CRITICAL();
}
在这个程序中没有使用*OSTCBPrioTbl[]指针数组来检索任务控制块,*OSTCBList是指
向已建立任务的任务控制块双向链表。通常已建立任务数小于*OSTCBPrioTbl[]指针数组指向的任务数,这样可以使查找操作更快。uCOS-ii可以动态地建立或删除任务。建立一个任务后就可以开定时中断。所以这样做的目的是牺牲存储空间,提高系统实时性。
任务堆栈:每个任务有自己的堆栈空间,在声明任务的同时就要声明任务堆栈空间,
典型地
#define TaskStkLengh 64 // 定义用户任务的堆栈长度
OS_STK TaskStk [TaskStkLengh]; // 定义用户任务0的堆栈
OS_STK Task1Stk [TaskStkLengh]; // 定义用户任务1的堆栈
void Task0(void *pdata); // Task0 用户任务0
void Task1(void *pdata); // Task1 用户任务1
ARM的堆栈是32位的字堆栈,所以任务堆栈共需要64*4个字节。
任务堆栈用来保护任务断点,当然在任务中调用子程序时也用这个空间保存断点,有
的C函数利用堆栈传递参数。到底要为任务分配多大的堆栈空间?如果分配的空间多了则浪费内存,若分配的少了则可能因为堆栈溢出导致系统崩溃。所以在分配任务堆栈空间时要对任务中可能子程序嵌套及需要传递参数的子程序所需要的空间有个大致的估计。完全分配准比较困难。
这里仅把任务切换时的堆栈结构做一个简单的介绍。
堆栈结构是处理器相关的,对ARM7 ,程序中断时需要保存所有的寄存器,从R0~R12
共13个通用寄存器,链接寄存器LR,程序计数器PC ,状态寄存器CPSR,共16个寄存器。还要记录任务的关中断次数。这时共17个字的内容需要保存。
ARM的堆栈是满递减栈。进栈时先使堆栈指针减4,然后数据进栈。顺序见堆栈初始
化函数。
OS_STK *OSTaskStkInit (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT16U opt)
{
OS_STK *stk; BY wanwenlue任务堆栈
opt = opt; /* 'opt' 没有使用。作用是避免编译器警告 */
stk = ptos; /* 获取堆栈指针 */
/* 建立任务环境,ADS1.2使用满递减堆栈 */
*stk = (OS_STK) task; /* pc */
*--stk = (OS_STK) task; /* lr */
*--stk = 0x12121212; /* r12 */
*--stk = 0x11111111; /* r11 */
*--stk = 0x10101010; /* r10 */
*--stk = 0x09090909; /* r9 */
*--stk = 0x08080808; /* r8 */
*--stk = 0x07070707; /* r7 */
*--stk = 0x06060606; /* r6 */
*--stk = 0x05050505; /* r5 */
*--stk = 0x04040404; /* r4 */
*--stk = 0x03030303; /* r3 */
*--stk = 0x02020202; /* r2 */
*--stk = 0x01010101; /* r1 */
*--stk = (unsigned int) pdata; /* r0,第一个参数使用R0传递 */ *--stk = (USER_USING_MODE|0x00); /* spsr,允许 IRQ, FIQ 中断 */
*--stk = 0; /* 关中断计数器OsEnterSum; */ return (stk);
}
堆栈初始化函数返回的堆栈指针直接写入任务控制块中的堆栈指针成员。
进行任务切换时,切换程序先将任务断点按上面的堆栈格式写入任务堆栈,然后修改堆栈指针,使之指向就绪的最高优先级任务堆栈,执行出栈指令将新任务的堆栈中的数据弹出到CPU的各对应寄存器,这时就转到新任务运行了,这部分程序是用汇编语言写的,因为要保证同一的堆栈结构。
处理器相关
ARM7TDMI-S
处理器状态:ARM状态, 32位指令状态;Thumb状态。16位指令状态。进入异常只能是ARM状态。
处理器模式:用户模式,系统模式,管理模式,快中断模式FIQ,中断模式,预取中止模式,未定义指令模式。
不同模式寄存器不同。用户模式与系统模式使用相同的寄存器组。
通过修改状态寄存器可以进行模式切换。
除用户模式外,其余模式是特权模式。
用户模式不能使用修改状态寄存器的方法进入其他特权模式。也不能开关中断。 操作系统工作在那个模式,不同的人有不同的选择,操作系统工作在系统模式。 BY wanwenlue
处理异常向量
Reset
LDR PC, ResetAddr LDR PC, UndefinedAddr LDR PC, SWI_Addr LDR PC, PrefetchAddr LDR PC, DataAbortAddr DCD 0xb9205f80
LDR PC, [PC, #-0xff0] LDR PC, FIQ_Addr
ResetAddr DCD ResetInit UndefinedAddr DCD Undefined SWI_Addr DCD SoftwareInterrupt PrefetchAddr DCD PrefetchAbort DataAbortAddr DCD DataAbort Nouse DCD 0 IRQ_Addr DCD 0
FIQ_Addr DCD FIQ_Handler 启动代码的堆栈初始化程序如下所示: InitStack
MOV R0, LR
;Build the SVC stack
;设置中断模式堆栈
MSR CPSR_c, #0xd2 LDR SP, StackIrq
;Build the FIQ stack
;设置快速中断模式堆栈
MSR CPSR_c, #0xd1 LDR SP, StackFiq
;Build the DATAABORT stack
;设置中止模式堆栈
MSR CPSR_c, #0xd7 LDR SP, StackAbt
;Build the UDF stack
;设置未定义模式堆栈
MSR CPSR_c, #0xdb LDR SP, StackUnd BY wan堆栈初始化 wenlue
;Build the SYS stack
;设置系统模式堆栈
MSR CPSR_c, #0xdf
LDR SP, =StackUsr
MOV PC, R0
StackIrq DCD IrqStackSpace + (IRQ_STACK_LEGTH - 1)* 4
StackFiq DCD FiqStackSpace + (FIQ_STACK_LEGTH - 1)* 4
StackAbt DCD AbtStackSpace + (ABT_STACK_LEGTH - 1)* 4
StackUnd DCD UndtStackSpace + (UND_STACK_LEGTH - 1)* 4
数据段定义
AREA MyStacks, DATA, NOINIT, ALIGN=2
IrqStackSpace SPACE IRQ_STACK_LEGTH * 4 ;Stack spaces for Interrupt ReQuest Mode 中断模式堆栈空间
FiqStackSpace SPACE FIQ_STACK_LEGTH * 4 ;Stack spaces for Fast Interrupt reQuest Mode 快速中断模式堆栈空间
AbtStackSpace SPACE ABT_STACK_LEGTH * 4 ;Stack spaces for Suspend Mode 中止义模式堆栈空间
UndtStackSpace SPACE UND_STACK_LEGTH * 4 ;Stack spaces for Undefined Mode 未定义模式堆栈
AREA Heap, DATA, NOINIT
bottom_of_heap SPACE 1
AREA Stacks, DATA, NOINIT
StackUsr
实际运行时
StackIrq=0x40001868
StackFiq=0x40001868
StackAbt=0x40001868
StackUnd=0x40001868
StackUsr=0x40004000
StackSvc=0x400018e8
由于除IRQ ,USER(SYS),SVC模式外其他模式系统中没有使用,共有3个堆栈空间。堆栈的地址是在分散加载文件里定义的。
分散加载
ROM_LOAD 0x0
{
ROM_EXEC 0x00000000 代码段的首地址为0
{
Startup.o (vectors, +First);最先连接启动代码的vectors段
* (+RO) 只读 BY wanwenlue
}
IRAM 0x40000000 内部RAM的首地址
{
Startup.o (MyStacks) ;启动代码的MyStacks段在这里开始,主要存放全局变量,堆空间,除用户栈外,其他模式栈也在这里分配。
* (+RW,+ZI) 读写,初始化时为0
}
HEAP +0 UNINIT
{
Startup.o (Heap) 启动代码的Heap段
}
STACKS 0x40004000 UNINIT 16K RAM的地址高端
{
Startup.o (Stacks) ;所以用户堆栈在0x40004000,满递减,设置堆栈指针指向这里,当内部RAM地址不是16K时,可修改此地址使最高地址与RAM容量相当。 }
}
注:红色部分是注释,不符合分散加载文件规范,仅供学习时参考。
当处理器执行软中断指令时,转SoftwareInterrupt处执行。处理器从系统模式切换到管理模式。当软中断发生时,系统进入管理模式,原模式下的CPSR保存在管理模式的SPSR,返回地址保存在管理模式的R14(LR)。LR地址的前一条指令是软中断指令编码。 SoftwareInterrupt
LDR SP, StackSvc ; 重新设置堆栈指针
STMFD SP!, {R0-R3, R12, LR}
MOV R1, SP ; R1指向参数存储位置
MRS R3, SPSR
TST R3, #T_bit ; 中断前是否是Thumb状态
LDRNEH R0, [LR,#-2] ; 是: 取得Thumb状态SWI号
BICNE R0, R0, #0xff00
LDREQ R0, [LR,#-4] ; 否: 取得arm状态SWI号
BICEQ R0, R0, #0xFF000000
; r0 = SWI号,R1指向参数存储位置 CMP R0, #1
LDRLO PC, =OSIntCtxSw
LDREQ PC, =__OSStartHighRdy ; SWI 0x01为第一次任务切换
BL SWI_Exception
LDMFD SP!, {R0-R3, R12, PC}^
BY wanwenlue软中断处理
StackSvc DCD (SvcStackSpace + SVC_STACK_LEGTH * 4 - 4)
IRQ异常
NXP 的LPC21XX系列ARM7 处理器的外部功能部件使用VIC 中断,当发生中断请求时处理器将中断服务程序的入口地址写入地址是0xfffff030的VICVectAddr寄存器中。使用启动代码的LDR PC, [PC, #-0xff0]指令跳转到中断服务程序。这条指令存放在0x00000018处。由于ARM有三级指令流水线,执行这条指令时PC=0x00000020(PC+8),0x00000020-0x00000ff0=0xfffff030,转到中断服务程序。
所以在使用uCOS-ii时所有外部中断应置成IRQ中断。
IRQ中断处理
为了保证在中断处理时有同一的堆栈结构,在移植代码中定义了一个宏来处理VIC中断。 CODE32
AREA IRQ,CODE,READONLY
MACRO
$IRQ_Label HANDLER $IRQ_Exception_Function
EXPORT $IRQ_Label ; 输出的标号
IMPORT $IRQ_Exception_Function ; 引用的外部标号
$IRQ_Label
SUB LR, LR, #4 ; 计算返回地址
STMFD SP!, {R0-R3, R12, LR} ; 保存任务环境,IRQ模式
MRS R3, SPSR ; 保存状态
STMFD SP, {R3, SP, LR}^ ; 保存用户状态的R3,SP,LR,注意不能回写 ; 如果回写的是用户的SP,所以后面要调整SP LDR R2, =OSIntNesting ; OSIntNesting++
LDRB R1, [R2]
ADD R1, R1, #1
STRB R1, [R2]
SUB SP, SP, #4*3
MSR CPSR_c, #(NoInt | SYS32Mode) ; 切换到系统模式
CMP R1, #1
LDREQ SP, =StackUsr
BL $IRQ_Exception_Function ; 调用c语言的中断处理程序 MSR CPSR_c, #(NoInt | SYS32Mode) ; 切换到系统模式
LDR R2, =OsEnterSum ; OsEnterSum,使OSIntExit退出时中断关闭 MOV R1, #1
STR R1, [R2]
BL OSIntExit
LDR R2, =OsEnterSum ; 因为中断服务程序要退出,所以OsEnterSum=0 MOV R1, #0
STR R1, [R2]
MSR CPSR_c, #(NoInt | IRQ32Mode) ; 切换回irq模式 BY wanwenlue
LDMFD SP, {R3, SP, LR}^ ; 恢复用户状态的R3,SP,LR,注意不能回写
; 如果回写的是用户的SP,所以后面要调整SP LDR R0, =OSTCBHighRdy
LDR R0, [R0]
LDR R1, =OSTCBCur
LDR R1, [R1]
CMP R0, R1
ADD SP, SP, #4*3 ;
MSR SPSR_cxsf, R3
LDMEQFD SP!, {R0-R3, R12, PC}^ ; 不进行任务切换
LDR PC, =OSIntCtxSw ; 进行任务切换
MEND
为什么STMFD SP, {R3, SP, LR}^ 指令不回写?
因为在指令中有堆栈指针是SP,进栈的寄存器是SP,且不是第1个要写入堆栈的寄存器。若选择回写则SP的值不可预知。见Online Book 中ARM指令参考。
本指令后加了‘^’,表示进栈的是用户模式下的寄存器。
这个程序用宏展开实现,所以不能在源代码处设置断点来调试程序,要在初始化中断矢量的IRQ跳转指令处设置断点,然后单步跟踪到程序入口。
由于uCOS-ii是可剥夺内核,所以中断服务程序有2个出口。当中断时没有发生任务切换,则返回原断点处继续执行。若进行了任务切换,则返回到新任务执行。
中断嵌套数是0时才允许中断中的任务调度。存在中断嵌套不能进行任务调度。为什么中断嵌套时不能调度,因为中断服务程序有2个出口。嵌套时的任务调度将使系统崩溃。
这个程序也许还存在问题,主要是中断服务程序的处理器模式较混乱。移植者的任务调度程序工作在管理模式,中断时处理器工作在IRQ模式。好处是没有专门为中断时任务调度专门写个程序。
在操作系统中使用了定时中断产生时钟节拍。因此使用
Timer0_Handler HANDLER Timer0_Exception
Timer0_Handler是定时中断矢量,Timer0_Exception是处理定时中断的C程序。
在Target.c中初始化定时中断矢量
void VICInit(void)
{
extern void IRQ_Handler(void);
extern void __irq Timer0_Handler(void); //注意__irq不能少!
VICIntEnClr = 0xffffffff;
VICDefVectAddr = (uint32)IRQ_Handler;
VICVectAddr0 = (uint32)Timer0_Handler; 当发生定时中断时转这里执行。
VICVectCntl0 = (0x20 | 0x04);
VICIntEnable = 1 << 4;
}
定时中断处理程序
void Timer0_Exception(void)
{ BY wanwenlue
T0IR = 0x01;
VICVectAddr = 0; //interrupt close 通知中断控制器中断结束 OSTimeTick();
}
任务切换
任务切换的汇编程序
OSIntCtxSw
;下面为保存任务环境 LDR R2, [SP, #20] ;获取PC
LDR R12, [SP, #16] ;获取R12
MRS R0, CPSR
MSR CPSR_c, #(NoInt | SYS32Mode)
MOV R1, LR
STMFD SP!, {R1-R2} ;保存LR,PC
STMFD SP!, {R4-R12} ;保存R4-R12
MSR CPSR_c, R0
LDMFD SP!, {R4-R7} ;获取R0-R3
ADD SP, SP, #8 ;出栈R12,PC MSR CPSR_c, #(NoInt | SYS32Mode)
STMFD SP!, {R4-R7} ;保存R0-R3
LDR R1, =OsEnterSum ;获取OsEnterSum
LDR R2, [R1]
STMFD SP!, {R2, R3} ;保存CPSR,OsEnterSum
;保存当前任务堆栈指针到当前任务的TCB LDR R1, =OSTCBCur
LDR R1, [R1]
STR SP, [R1]
BL OSTaskSwHook ;调用钩子函数
;OSPrioCur <= OSPrioHighRdy LDR R4, =OSPrioCur
LDR R5, =OSPrioHighRdy
LDRB R6, [R5]
STRB R6, [R4]
;OSTCBCur <= OSTCBHighRdy LDR R6, =OSTCBHighRdy
LDR R6, [R6]
LDR R4, =OSTCBCur
STR R6, [R4]
OSIntCtxSw_1
LDR R4, [R6] ;获取新任务堆栈指针
ADD SP, R4, #68 ;17寄存器CPSR,OsEnterSum,R0-R12,LR,SP LDR LR, [SP, #-8] BY wanwenlue
周立功移植的uCOS-ii的任务运行于系统模式,系统模式也是特权模式,但有与用户模式相同的寄存器组。使用软中断进行任务级的任务调度,进入软中断后系统运行在管理模式。由于ARM在不同的模式下使用不同的堆栈指针寄存器,所以移植代码较混乱。
在Jean J. Labrosse移植的uCOS-ii中,任务运行在管理模式。在IRQ中断时进入IRQ模式,若不是用FIQ,整个系统只运行于管理模式和IRQ模式。
参考文献:
1. ARM Inc.,ADS Online Book,2001
2. 周立功,ARM嵌入式系统基础教程,北京航空航天大学出版社,2004
3. 周立功,深入浅出ARM7-LPC213X/214X(上,下册),北京航空航天大学出版社,
2006
4. (美)Jean J. Labrosse著,邵贝贝译,嵌入式实时操作系统uC/OS-ii(第2版),北
京航空航天大学出版社,2003
5. 周立功,ARM微控制器基础与实战(第2版),北京航空航天大学出版社,2005
6. Jean J. Labrosse, AN1014.PDF
BY w关于操作系统的运行模式: anwe MSR CPSR_c, #(NoInt | SVC32Mode) ;进入管理模式 MOV SP, R4 ;设置堆栈指针 LDMFD SP!, {R4, R5} ;CPSR,OsEnterSum ;恢复新任务的OsEnterSum LDR R3, =OsEnterSum STR R4, [R3] MSR SPSR_cxsf, R5 ;恢复CPSR LDMFD SP!, {R0-R12, LR, PC }^ ;运行新任务 第一次启动多任务时调用下面的程序 __OSStartHighRdy MSR CPSR_c, #(NoInt | SYS32Mode) ;告诉uC/OS-II自身已经运行 LDR R4, =OSRunning MOV R5, #1 STRB R5, [R4] BL OSTaskSwHook ;调用钩子函数 LDR R6, =OSTCBHighRdy LDR R6, [R6] B OSIntCtxSw_1 定义管理模式堆栈 AREA SWIStacks, DATA, NOINIT,ALIGN=2 SvcStackSpace SPACE SVC_STACK_LEGTH * 4 ;管理模式堆栈空间 nlue