这两天在看UCOS-II, uCOS中断和时钟问题做个小结,以UCOS2.25自带的X86例子、《嵌入式实时操作系统uCOS-II原理及应用》为基础,相信对网友们也有帮助,理解不对的地方,请高手指正:
UCOS-II是抢占式、实时操作系统,这是核心。
1. uCOS的系统有任务级任务调度和中断级任务调度:任务级任务调度靠常常在下面的代码中自动调用,
void main()
{
OSinit();
OSTaskCreate();
OSTaskCreate();
.......
OSStart();
}
当执行OSTaskCreate时,会将任务优先级较高的队列改变为就绪状态,同时OS_CORE.C中调度器OnSched()进行任务调度(内部调用ON_TASK_SW宏,该宏为INT 0x80,进行软件中断,调用在OS_CPU_A.ASM中需要实现的OSCtxSw(OSCtxSw是宏调用通常含有软中断指令,切换是靠中断级代码完成的))。OnSched只调用优先级最高的任务,可以通过进程同步类的函数来释放CPU资源,达到任务并行进行和通信,这些函数包括对任务状态和优先级改变的函数(OSTaskSuspend, OSTaskResume, OSTaskChangePrio, OSTaskDel, OSTimeDly,ostimedlyhmsm,这几函数涉及到任务的状态迁移,见下面图), 信号量,消息队列,互斥信号量(用于防止任务优先级反转,很重要,可以另开一个话题)。
睡眠态:在ROM或RAM中,交给UCOS要调用下面两个函数之一:OSTaskCreate或者OSTaskCreateExt,调用之后告诉了UCOS任务的起始地址,优先级,要使用多少栈空间。
就绪态:建立了任务之后,就进入就绪态。如果是由任务建立的新任务且优先级更高,那么新建的任务将立即得到CPU使用权。通过OSTaskDel将一个任务返回到睡眠态。
运行态:调用OSStart可以启动多任务,该函数运行用户初始化代码中已建的优先级最高的任务。
等待态:正在运行的任务通过两个函数将自己延迟即进入等待状态。OSTimeDly或者OSTimeDlyHMSM。这两个函数将会立即执行强制任务切换,让下一个优先级最高且进入就绪态的任务运行。当延时时间到后,系统服务函数OSTimeTick将会让等待的任务进入就绪态。在运行中的任务如果调用OSFlagPend、OSSemPend、OSMutexPend、OSMboxPend或者OSQPend时时间并没有发生,那么该任务就进入等待态,此时最高优先级的就绪态任务进入运行态。当等待事件发生或者等待超时,该任务就从等待态进入就绪态。
中断服务态:正在运行的任务可以被中断。被中断了的任务进入中断服务态。响应中断时该任务被挂起,中断服务子程序可能报告多个事件发生,从而使得更高优先级的任务进入就绪态,当中断返回时会重新判断优先级,让最高优先级的任务运行,如果由更高优先级的任务那么先执行,直到被挂起的任务的优先级最高才会返回到被中断挂起的任务。
2. 下面讲重点,中断级任务调度:
当任务在运行的时候,执行完中断服务程序后,uCOS应该在任务列表中寻找已经就绪状态的任务(这些任务可能是因为调用等待资源或者调用OSTimeDly等函数进入等待状态,当资源已经具备或者OSTimeDlg时间超时),寻找最高优先级的任务执行。因此,在中断处理函数中要加入相应的代码,uCOS一般不提供中断注册的函数,中断服务程序需要实现的代码如下伪代码描述:
void isr_task()
{
保存全部CPU寄存器; (1)
调用OSIntEnter或OSIntNesting直接加1; (2)
执行用户代码做中断服务; (3)
调用OSIntExit(); (4)
}
3. 《嵌入式实时操作系统uCOS-II原理及应用》第3章中了很多这样一些(1)(2)代码,(3)中的是其他地方看到的:
(1) PC_Vectset(0x08, OSTickISR); //安装时钟服务程序, 完成各任务中执行了OSTimeDly等函数的时间到期提供处理程序
(2) PC_SetTickRate(OS_TICKS_PER_SEC); // 设置时钟频率,需要根据CPU,写汇编程序完成定时器的寄存器初始化工作
(3) PC_Vectset(0x80, OSCtxSw); // 安装软件中断时候的任务切换工作,因为OSCtxSW重要执行寄存器的操作,常常使用进入软件中断模式特权模式。
(1): 跟踪了一下PC_VectSet(0x08, OSTickISR)发现,这个函数为:
void PC_VectSet (INT8U vect, void (*isr)(void))
{
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr;
#endif
INT16U *pvect;
pvect = (INT16U *)MK_FP(0x0000, vect * 4); /* Point into IVT at desired vector location */
OS_ENTER_CRITICAL();
*pvect++ = (INT16U)FP_OFF(isr); /* Store ISR offset */
*pvect = (INT16U)FP_SEG(isr); /* Store ISR segment */
OS_EXIT_CRITICAL();
}
分析了一下,该函数只适合于80X86 CPU,因为 8 *4 = 32,正好为80X86定时器0的中断向量入口地址,和ARM不一样,Arm跳转后是执行这里的指令,而X86是执行该处32位值所指向地址的指令,因此,该函数不适合与其他CPU,如果要使用需要更改。
另外,OSTickISR在移植时,一般需要在OS_CPU_A.asm中根据CPU不同用汇编实现,完成的功能主要有:
void OSTickISR(void)
{
保存全部CPU寄存器; (1)
调用OSIntEnter或OSIntNesting直接加1; (2)
if(OSintNesting == 1)
{
OSTCBCur->OSStkPtr = SP; //在任务TCB中保存堆栈指针 (3)
}
调用OnTimeTick() ; 调用节拍处理函数该函数在OS_CORE.C中已经实现了,不需要自己实现(注意:与OS_CORE.C中同一级目录中的所有源程序都与CPU无关),节拍处理函数中自动完成了(1) OSTime ++; (2) 遍历任务控制块链表中所有控制块,把用来存放任务延时时限的OSTCBDly减1,如果OSTCBDly == 0唤醒suspend等等待线程,将其转为就绪态 (4)
调用OSIntExit(); (4)
恢复所有CPU寄存器; (5)
执行中断返回指令; (6)
}
再论,因为OS_CORE.C中OnTimeTick函数有如下一段代码:
void OSTimeTick (void)
{
#if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
OS_CPU_SR cpu_sr;
#endif
OS_TCB *ptcb;
OSTimeTickHook(); // 在OS_CPU_C.C定义,该文件不在SOURCE自带的目录下,需要自己创建钩子函数
。。。。
}
在uCOS-II \ Ix86L \ BC45 \ OS_CPU_S.C文件中定义如下,可以自己定义钩子函数
#if OS_CPU_HOOKS_EN > 0
void OSTimeTickHook (void)
{
}
#endif
(2):PC_SetTickRate(OS_TICKS_PER_SEC); 用户设置时钟频率,需要根据CPU,写汇编程序完成定时器的寄存器初始化工作。
(3): PC_Vectset(0x80, OSCtxSw); 用于任务调度切换的注册,80为X86 CPU的软件中断入口,如果是ARM,需要相应的改变,同时OSCtxSW需要在OS_CPU.A.ASM中根据处理器来操作寄存器进行实现。 OSCTxSW完成的任务有(以X86的这个例子为例,其他的类似):
<1>把当前CPU软件中断前的PC指针、PSW(程序状态字)保存到当前任务控制块的栈中,压栈操作(与<6>相对应);
<2>把当前CPU各寄存器压栈保存到当前任务控制块的栈中,压栈操作。(与<5>对应)
<3> 在任务链中取得优先级最高,且就绪的任务控制块,复制到OSTCBCur中;
<4> 把该任务优先级别赋值给OSPrioCur中;
<5> 得到OSTCBCur中栈中所保存的上一次任务切换时CPU寄存器数据,恢复到相应的寄存器中,此过程出栈数据顺序与2>中正好相反,且对应
<6>中断返回,读出OSTCBCur中栈中所保存PC指针数据和程序状态字PSW,此过程出栈数据顺序与1>中正好相反,且对应
4. 2中讲述的 在UCOS中断处理程序中,OSIntExit (OS_CORE.C中)调用的时候会触发 OSIntCtxSw()的调用,我们一般需要在OS_CPU_A.asm中实现针对具体CPU的汇编代码,完成如下中断任务级的调度,需要完成的和上面的OSCtxSw类似,不过少了(1)(2)动作,因为在中断的前部分已经执行了。
5. 要想移植一个实时操作系统,3中描述的PC_VectSet(0x08, OSTickISR)、PC_SetTickRate(OS_TICKS_PER_SEC)涉及到的时钟的几个函数需要针对CPU来具体实现,包括PC_SetTickRate中的时钟源和中断相关寄存器的设置, PC_VectSet针对不同的CPU的C语言实现,这里可以做一个通用的中断注册函数,当然写成汇编,在引导程序.s中也可以做,不过比较麻烦,另外,OSTickISR的具体实现,需要实现什么上面都有介绍。为了完成任务级的调度,还得实现ON_TASK_SW宏(默认在OS_CPU.h中定义),通常采用软件中断的方式,实现OSCtxSW在OS_CPU_A.asm的汇编代码,如果不是,改掉相应地方的代码。为了实现硬件中断级调度,在中断处理程序中所有的地方都需要按照 void isr_task()中的风格来做,同时需要加入通用的OSIntCtwSw在OS_CPU.asm中的汇编代码。
6. 因此,要想移植一个最简单的UCOS,需要在OS_CPU_A.asm中实现:
<1> OSCtxSw()
<2> OSIntCtxSw()
<3> OSTickISR() 以及针对该函数的时钟初始化
<4> OSStartHighRdy;本节没有介绍,主要在OSStart函数中被调用,应该实现如下的功能:
(1): 并关闭IRQ,FIQ中断
(2): 调用OSTaskSwHook(),这是钩子函数,一般为空,用户可以视需要自己添加内容。
(4):把OSRunning标志置1。OSRunning是UCOS系统己经启动的一个标识,它在调用UCOS中的OSStart时被置为1。
(5):SP指向最高优先级任务的堆栈。
(6):把最高优先级任务堆栈的内容保存到寄存器中
(7):开始执行最高优先级任务。
<4> 在其他地方,如OS_CPU_C.C 中,需要实现 OSTaskStkInit函数,完成任务的堆栈初始化函数;类似;OS_STK *OSTaskStkInit (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT16U opt)
{
INT16U *stk;
opt = opt; /* 'opt' is not used, prevent warning */
stk = (INT16U *)ptos; /* Load stack pointer */
*stk-- = (INT16U)FP_SEG(pdata); /* Simulate call to function with argument */
*stk-- = (INT16U)FP_OFF(pdata);
*stk-- = (INT16U)FP_SEG(task);
*stk-- = (INT16U)FP_OFF(task);
*stk-- = (INT16U)0x0202; /* SW = Interrupts enabled */
*stk-- = (INT16U)FP_SEG(task); /* Put pointer to task on top of stack */
*stk-- = (INT16U)FP_OFF(task);
*stk-- = (INT16U)0xAAAA; /* AX = 0xAAAA */
*stk-- = (INT16U)0xCCCC; /* CX = 0xCCCC */
*stk-- = (INT16U)0xDDDD; /* DX = 0xDDDD */
*stk-- = (INT16U)0xBBBB; /* BX = 0xBBBB */
*stk-- = (INT16U)0x0000; /* SP = 0x0000 */
*stk-- = (INT16U)0x1111; /* BP = 0x1111 */
*stk-- = (INT16U)0x2222; /* SI = 0x2222 */
*stk-- = (INT16U)0x3333; /* DI = 0x3333 */
*stk-- = (INT16U)0x4444; /* ES = 0x4444 */
*stk = _DS; /* DS = Current value of DS */
return ((OS_STK *)stk);
}
7.以上主要涉及到中断部分,还有一些函数需要具体实现,例如X86例子中的OS_ENTER_CRITICAL()
ON_EXIT_CRITICAL()开中断与关中断的实现