1.在uC/OS-II的帮助手册内,作者特地强调绝对不能在OSInit()或者OSStart()内调用Timer初始化程序,那会破坏系统的可移植性同时带来性能上的损失。
所以,一个折中的办法就是:
在优先级最高某绦蚰诘饔?这样可以保证当OSStart()调用系统内部函数
OSStartHighRdy()开始多任务后,首先执行的就是Timer初始化程序。或者
专门开一个优先级最高的任务,只做一件事情,那就是执行 Timer初始化,
之后通过调用OSTaskSuspend()将自己挂起来,永远不再执行。不过这样会
浪费一个TCB空间。对于那些RAM吃紧的系统来说,还是不用为好。
2.(三)一些重要的uC/OS-II API介绍
任何一个操作系统都会提供大量的API供程序员使用,uC/OS-II也不例外。由于uC/OS-II面向的是嵌入式开发,并不要求大而全,所以内核提供的API也就大多和多任务息息相关。
主要的有以下几类:
1)任务类
2)消息类
3)同步类
4)时间类
5)临界区与事件类
我个人认为对于初级程序员而言,任务类和时间类是必须要首先掌握的两种类型的API。
下面我就来介绍比较重要的:
1) OSTaskCreate函数
这个函数应该至少在main函数内调用一次,在OSInit函数调用之后调用。作用就是创建一个任务。目前有四个参数,分别是任务的入口地址,任务的参数, 任务堆栈的首地址和任务的优先级。调用本函数后,系统会首先从TCB空闲列表内申请一个空的TCB指针,然后将会根据用户给出参数初始化任务堆栈,并在内部的任务就绪表内标记该任务为就绪状态。最后返回,这样一个任务就创建成功了。
2) OSTaskSuspend函数
这个函数很简单,一看名字就该明白它的作用,它可以将指定的任务挂起。如果挂起的是当前任务的话,那么还会引发系统执行任务切换先导函数OSShed来进行一次任务切换。这个函数只有一个参数,那就是指定任务的优先级。那为什么是优先级呢?事实上在系统内部,优先级除了表示一个任务执行的先后次序外,还起着分别每一个任务的作用,换句话说,优先级也就是任务的ID。所以uC/OS-II不允许出现相同优先级的任务。
3) OSTaskResume函数
这个函数和上面的函数正好相反,它用于将指定的已经挂起的函数恢复成就绪状态。如果恢复任务的优先级高于当前任务,那么还为引发一次任务切换。其参数类似 OSTaskSuspend函数,为指定任务的优先级。需要特别说明是,本函数并不要求和OSTaskSuspend函数成对使用。
4) OS_ENTER_CRITICAL宏
很多人都以为它是个函数,其实不然,仔细分析一下OS_CPU.H文件,它和下面马上要谈到的OS_EXIT_CRITICAL都是宏。他们都是涉及特定 CPU的实现。一般都被替换为一条或者几条嵌入式汇编代码。由于系统希望向上层程序员隐藏内部实现,故而一般都宣称执行此条指令后系统进入临界区。其实, 它就是关个中断而已。这样,只要任务不主动放弃CPU使用权,别的任务就没有占用CPU的机会了,相对这个任务而言,它就是独占了。所以说进入临界区了。这个宏能少用还是少用,因为它会破坏系统的一些服务,尤其是时间服务。并使系统对外界响应性能降低。
5) OS_EXIT_CRITICAL宏
这个是和上面介绍的宏配套使用另一个宏,它在系统手册里的说明是退出临区。其实它就是重新开中断。需要注意的是,它必须和上面的宏成对出现,否则会带来意想不到的后果。最坏的情况下,系统会崩溃。我们推荐程序员们尽量少使用这两个宏调用,因为他们的确会破坏系统的多任务性能。
6) OSTimeDly函数
这应该程序员们调用最多的一个函数了,这个函数完成功能很简单,就是先挂起当起当前任务,然后进行任务切换,在指定的时间到来之后,将当前任务恢复为就绪状态,但是并不一定运行,如果恢复后是优先级最高就绪任务的话,那么运行之。简单点说,就是可以任务延时一定时间后再次执行它,或者说,暂时放弃CPU的使用权。一个任务可以不显式的调用这些可以导致放弃CPU使用权的API,但那样多任务性能会大大降低,因为此时仅仅依靠时钟机制在进行任务切换。一个好的任务应该在完成一些操作主动放弃使用权,好东西要大家分享嘛!
3.我们推荐程序员们尽量少使用OS_ENTER_CRITICAL宏和 OS_EXIT_CRITICAL宏两个宏调用,因为他们的确会破坏系统的多任务性能。why??
4.在以uC/OS为操作系统的项目中,系统可能要处理各种不同的中断请求,如果某个中断处理程序需要调用uC/OS的各种Post函数向任务发出消息,那么uC/OS建议中断服务程序的写法是:
1、保存全部CPU寄存器
2、调用OSIntEnter或OSIntNesting直接加1
3、执行用户代码做中断服务
4、调用OSIntExit
5、恢复所有CPU寄存器
6、执行中断返回指令
暂且称为“标准中断”方式,这种方式实际上是将这个中断处理加入了任务调度系统,也就是说这个中断可以引起任务的切换。
如果在中断处理中没有调用各种Post函数的话,则可以用一般的、象原来没有操作系统时的写法:
1、保存中断处理程序需要用到的CPU寄存器
2、执行中断处理
3、恢复保存了的CPU寄存器
4、执行中断返回指令
暂且称为“快中断”方式,按照这种方法定义的中断永远不会引起任务切换。
在uC/OS系统中,每个任务都要定义独立的栈空间,一个栈空间的使用包括5个部分:
1、任务包括的各个函数的调用返回地址
2、任务包括的各个函数中可能在栈上分配的局部变量
3、发生了“标准中断”方式定义的中断或任务被挂起时,所要保存的任务上下 文
4、发生了“快中断”方式定义的中断时,中断处理程序所需要的栈空间
5、中断嵌套时,所要保存的中断嵌套上下文
在这些使用的部分中,1,2,3,4的内存占用量是比较容易估算的,最精确和保险的确定方法是:查看由C生成的asm文件,并计算各个函数的栈使用量。但是第5部分的栈空间使用量是随中断嵌套的深度而不断增加的,是不确定的,一般的方法只能定义一个充分大的栈空间,使之不会溢出。
为每个任务都定义一个充分大的栈空间,这在某些内存稀缺的小项目中是非常痛苦的,有时不得不增扩内存,这就会使成本增加。
我深入研究了uC/OS后,认为,可以将所有任务栈空间使用的第5部分合并,这样将会大大的降低整个系统对内存的需求。
uC/OS的任务调度是靠OS_Sched和 OSIntExit来完成的,这两个函数中都要先判断一个叫 OSIntNesting的系统变量,如果OSIntNesting不为0,则不进行任务切换。也就是说:在OSIntNesting为1(当前只有一个中断在处理中,并且没有嵌套的中断)时起,如果发生了嵌套的中断(不管嵌套的层数有深),那么在所有嵌套的中断一层一层地都返回直到 OSIntNesting再次为1时止,任务栈是不会切换的(栈指针都在一个任务的栈空间中变化)。
据此,我们可以这样改动:设置一个缓冲区OSInterruptStk,作为嵌套中断的栈空间,由所有任务共享,中断服务程序改为:
1、保存全部CPU寄存器
2、调用OSIntEnter或OSIntNesting直接加1
增加:2.1、判断OSIntNesting是否等于1,如果不是则转到3
增加:2.2、将栈指针SP保存到OSTCBCur->OSTCBStkPtr
增加:2.3、将SP指向OSInterruptStk的栈顶(注意栈增长的方向)。
3、执行用户代码做中断服务
4、调用OSIntExit
增加:4.1、判断OSIntNesting是否等于0,如果不是则转到5
增加:4.2、从OSTCBCur->OSTCBStkPtr中恢复栈指针SP
5、恢复所有CPU寄存器
6、执行中断返回指令
并且要修改OSIntCtxSw函数,原始的OSIntCtxSw函数的写法是:
1、调整栈指针来去掉在调用:OSIntExit,OSIntCtxSw过程中入栈的多余内容
2、将当前任务栈指针保存到OSTCBCur中(OSTCBCur->OSTCBStkPtr = __SP__)
3、如果需要则调用OSTaskSwHook
4、OSTCBCur = OSTCBHighRdy
5、OSPrio = OSPrioHighRdy
6、从OSTCBCur中恢复栈指针(__SP__ = OSTCBCur->OSTCBStkPtr)
7、恢复保存了的CPU寄存器
8、执行中断返回指令
新的写法只需将原写法中的1,2去掉即可,因为1,2步只是保存旧任务的栈指针,而新的写法中,这些步被移到了“中断服务程序”中的2.2。
5.注意ARM Image for uCOSII for lpc213x 模板中的TargetInit()
对于很多使用ZLG ARM Image for uCOSII for lpc213x 模板的初学者,常常会置疑使用该模板后自动生成的target.c文件,和在程序中调用的TargetInit()函数,我和 Zgpswh都是如此,这个问题当初困扰了很久:当用户程序中不调用TargetInit()时,发现内核能运行,但是等待机制失灵,调用 TargetInit(),很多硬件中断打不开,后来,在很多热心人的指点下解决了,现重新总结如下:
请仔细察看ZLG模板里的target.c文件,这里的TargetInit()如下:
void TargetInit(void)
{
OS_ENTER_CRITICAL();
srand((uint32) TargetInit);
VICInit();
Timer0Init();
OS_EXIT_CRITICAL();
}
其中的Timer0Init();用于硬件定时器0的初始化,事实上,ZLG的移植代码的μC/OS-Ⅱ的时钟节拍是通过定时器0提供的,不在主程序里调用这个函数,μC/OS-Ⅱ的时钟源就无法打开;但是,没有开启时钟源的μC/OS-Ⅱ是同样能运行的,只是内核提供的延时和等待时限机制都不起作用,系统虽能将
就运行,但因没调用TargetInit()而使内核功能不健全。
请注意,TargetInit()中的另一个函数VICInit ()是用来中断的初始化,它其中含有对UART0中断的分配,在用户程序里需要根据使用的硬件中断修改这部分代码,否则,这些硬件中断无法开启;
再者,在不调用TargetInit()的时候,硬件的中断初始化是在硬件初始化函数中完成,
这也就是Zgpswh提到的现象:不调用TargetInit()内核运行异常,调用了却开不了UART0的中断。
解决的方法如下:
这在《ARM嵌入式系统基础教程》的430页7.4.3节中论述的很清楚:
……关键在于把程序与芯片相关中断源挂接,使芯片在产生相应的中断后会调用相应的处理程序。
这需要做两方面事情:
1. 增加汇编接口的支持。……
2. 初始化向量中断控制器。……
按照一下方法完成中断源的的挂接:
1、增加汇编接口的支持。方法是修改IRQ.s文件,在末尾添加本句代码:
UART0_Handler HANDLER UART0_Exception
追加定义了通用串口0 中断句柄。
2、初始化向量中断控制器。将target.c文件中的VICInit()修改如下:
void VICInit(void)
{ extern void IRQ_Handler(void);
extern void Timer0_Handler(void);
extern void UART0_Handler(void);
VICIntEnClr = 0xffffffff;
VICDefVectAddr = (uint32)IRQ_Handler;
VICVectAddr0 = (uint32)Timer0_Handler;
VICVectCntl0 = (0x20 | 0x04);
VICIntEnable = 1 << 4;
VICVectAddr14 = (uint32)UART0_Handler;
VICVectCntl14 = (0x20 | 0x06);
VICIntEnable = 1 << 6;
}
此为初始化向量中断控制器。包括定时器0和串口0,特别要注意的是,一定不可以省略对定时器0的初始化中断里,不可以调用延时
请注意,中断里面是不支持等待机制的。请用自己编的一个软件延时,问题就可以解决了。中断源挂接正确是没问题的。
6.问
void TargetInit(void)
{
OS_ENTER_CRITICAL();
srand((uint32) TargetInit);
VICInit();
Timer0Init();
OS_EXIT_CRITICAL();
}
在此单独用srand()函数有什么作用(用了有什么好处,不用又会怎样),一般srand()用于给rand()设定种子(即srand给定rand运算式子的第一个值)。查了FAQ(P22)仅仅说明了
seed的译文。
答 2
你说的没有错,他就是用来设置随机数的种子。
每次编译一次,void TargetInit(void)函数在Ram或者Flash中的地址都不一样
(即种子也不一样)。如果你在程序中不用随机函数rand(),那么srand()
在这里对你来说是没有意义的,如果你要用rand(),那么每次编译程序后你的rand()产生的随机数就不一样。当然,你也可以自己种种子咯。
7.在ARM上移植操作系统有一点需要注意:建立任务的任务,最好不要做复杂的工作。频繁的访问其他硬件或者做时序要求比较严的工作容易造成系统死机,希望大家多多注意。我的做法是:将建立任务的任务,闲置起来,但是不能进入死循环。方法是:利用一个空邮箱,让任务无限期的等待,这样可以实现与其他任务的切换。
8.//定义与编译器无关的数据类型
typedef unsigned char BOOLEAN; //布尔变量
typedef unsigned char INT8U; // 无符号8位整型变量
typedef signed char INT8S; //有符号8位整型变量
typedef unsigned short INT16U; //无符号16位整型变量
typedef signed short INT16S; //有符号16位整型变量
typedef unsigned int INT32U; //无符号32位整型变量
typedef signed int INT32S; //有符号32位整型变量
typedef float FP32; //单精度浮点数(32位长度)
typedef double FP64; //双精度浮点数(64位长度)
typedef INT32U OS_STK; //堆栈是32位宽度
注:这里为什么用typedef,因为如果用#define,那么代码中的每一个相应的类型都会被替代,很有可能会出现问题,毕竟他只是一个替代的关系,且编译时间会增加,而用typedef则不会,它就相当于我们C++里面的引用,一样的思维,在这里面就是说多了一个名称。还有要注意的是我们怎么知道unsigned char 就是无符号8位整型变量,可以ARM公司里面下载ADS_CompilerGuide_D.PDF
文件,或者在你所装的ADS1.2目录里面有一个文件夹叫PDF,打开它就可以找到,具体页在259页。
9>OS_CPU_a.s:
;定义系统模式堆栈的大小
SVC_STACK_LEGTH EQU 32
NoInt EQU 0x80
USR32Mode EQU 0x10
SVC32Mode EQU 0x13
SYS32Mode EQU 0x1f
IRQ32Mode EQU 0x12
FIQ32Mode EQU 0x11
;T_bit用于检测进入异常前cpu是否处于THUMB状态
T_bit EQU 0x20
CODE32
AREA |subr|, CODE, READONLY
IMPORT OSTCBCur ;指向当前任务TCB的指针
IMPORT OSTCBHighRdy ;指向将要运行的任务TCB的指针
IMPORT OSPrioCur ;当前任务的优先级
IMPORT OSPrioHighRdy ;将要运行的任务的优先级
IMPORT OSTaskSwHook ;任务切换的钩子函数
IMPORT OSRunning ;uC/OS-II运行标志
IMPORT OsEnterSum ;关中断计数器(关中断信号量)
IMPORT SWI_Exception ;软中断异常处理程序
EXPORT __OSStartHighRdy
EXPORT OSIntCtxSw ;中断退出时的入口,参见startup.s中的IRQ_Handler
EXPORT SoftwareInterrupt ;软中断入口
;软件中断
//当CPU发现有软中断信号出现时,CPU的PC马上指到软中断服务地址处【见Startup.s】,然后通过跳转到下面这段软中断处理程序,进入后,首先要设置好管理模式下的堆栈,保存好用户任务的几个寄存器,其它寄存器系统会自动保存,然后取出SWI号。
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}^
StackSvc DCD (SvcStackSpace + SVC_STACK_LEGTH * 4 - 4)
OSIntCtxSw
;下面为保存任务环境
LDR R2, [SP, #20] ;获取PC//其实他就是管理模式下的LR寄存器的值,也就是用户任务的PC值,为什么是SP+20,因为该栈是满栈递减,保存了R0-R3, R12, LR,所以LR的值为[SP+#20]。
LDR R12, [SP, #16] ;获取R12
MRS R0, CPSR //用通用寄存器R0保存该管理模式下的CPSR
MSR CPSR_c, #(NoInt | SYS32Mode)//切换到系统模式
MOV R1, LR //把用户的LR保存在通用寄存器R1中
STMFD SP!, {R1-R2} ;保存LR,PC//把系统模式的LR,PC压入用户任务的栈中
STMFD SP!, {R4-R12} ;保存R4-R12//把系统模式的R4-R12压入用户栈中
MSR CPSR_c, R0 //重新回到管理模式
LDMFD SP!, {R4-R7} ;获取R0-R3//实际上把用户的R0~R3放到管理模式下的R4-R7
ADD SP, SP, #8 ;出栈R12,PC//这两个已经保存了,所以SP要往上8个字节
MSR CPSR_c, #(NoInt | SYS32Mode//系统模式
STMFD SP!, {R4-R7} ;保存R0-R3//因为管理模式的R4-R11和系统模式的R4-R11是一样的,保它保存在用户任务的栈中
//接下来的我就不分析拉,因为你如果按照我上面的分析,应该不成问题的,如果还未明白,请多看几遍,如果有问题,请留言我们一起来探讨,谢谢合作! 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]
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, #, NOINIT,ALIGN=2
SvcStackSpace SPACE SVC_STACK_LEGTH * 4 ;管理模式堆栈空间
END