分类: 嵌入式
2015-07-21 22:22:12
#define OS_CRITICAL_METHOD 3
#if OS_CRITICAL_METHOD == 3
#define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();}
#define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);}
#endif
申明几个函数,这里要注意最后三个函数需要注释掉,为什么呢?
OS_CPU_SysTickHandler()定义在os_cpu_c.c中,是SysTick中断的中断处理函数,而stm32f10x_it.c,中已经有该中断函数的定义SysTick_Handler(),这里也就不需要了,是不是很奇怪官方移植版为什么会这样弄吧,后面我会解释的。
OS_CPU_SysTickInit()定义在os_cpu_c.c中,用于初始化SysTick定时器,它依赖于OS_CPU_SysTickClkFreq(),而此函数我们自己会实现,所以注释掉。
OS_CPU_SysTickClkFreq()定义在BSP.C (Micrium\Software\EvalBoards)中,而本文移植中并未用到BSP.C,后面我们会自己实现,因此可以把它注释掉。
那么中断后栈中是个什么情形呢,<<ARM Cortex-M3权威指南>>中9.1.1有介绍,xPSR,PC,LR,R12,R3-R0被自动保存到栈中的,R11-R4如果需要保存,只能手工保存。因此OSTaskStkInit()的工作就是在任务自己的栈中保存cpu的所有寄存器。这些值里R1-R12都没什么意义,这里用相应的数字代号(如R1用0x01010101)主要是方便调试。
//uc/os -ii os_cpu_c.c
OS_STK *OSTaskStkInit (void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT16U opt)
{
OS_STK *stk;
(void)opt; /* 'opt' is not used, prevent warning */
stk = ptos; /* Load stack pointer */
/* Registers stacked as if auto-saved on exception */
*(stk) = (INT32U)0x01000000L; /* xPSR */
*(--stk) = (INT32U)task; /* Entry Point */
*(--stk) = (INT32U)0xFFFFFFFEL; /* R14 (LR) (init value will cause fault if ever used) */
*(--stk) = (INT32U)0x12121212L; /* R12 */
*(--stk) = (INT32U)0x03030303L; /* R3 */
*(--stk) = (INT32U)0x02020202L; /* R2 */
*(--stk) = (INT32U)0x01010101L; /* R1 */
*(--stk) = (INT32U)p_arg; /* R0 : argument */
/* Remaining registers saved on process stack */
*(--stk) = (INT32U)0x11111111L; /* R11 */
*(--stk) = (INT32U)0x10101010L; /* R10 */
*(--stk) = (INT32U)0x09090909L; /* R9 */
*(--stk) = (INT32U)0x08080808L; /* R8 */
*(--stk) = (INT32U)0x07070707L; /* R7 */
*(--stk) = (INT32U)0x06060606L; /* R6 */
*(--stk) = (INT32U)0x05050505L; /* R5 */
*(--stk) = (INT32U)0x04040404L; /* R4 */
return (stk);
}
其他几个:
xPSR = 0x01000000L,xPSR T位(第24位)置1,否则第一次执行任务时Fault,
PC肯定得指向任务入口,
R14 = 0xFFFFFFFEL,最低4位为E,是一个非法值,主要目的是不让使用R14,即任务是不能返回的。
R0用于传递任务函数的参数,因此等于p_arg。
NVIC_INT_CTRL EQU 0xE000ED04 ;中断控制及状态寄存器ICSR的地址
NVIC_SYSPRI14 EQU 0xE000ED22 ;PendSV优先级寄存器的地址
NVIC_PENDSV_PRI EQU 0xFF ;PendSV中断的优先级为255(最低)
NVIC_PENDSVSET EQU 0x10000000 ;位28为1
//os_cpu_a.asm中
OS_CPU_SR_Save
MRS R0, PRIMASK ;读取PRIMASK到R0中 ,
;用R0来保存进入中断前系统中断屏蔽寄存器组PRIMASK的值
;如果中断里其他函数传参数,用到R0了呢?
;传参数时好像函数调用时R0-R4是自动保存的,函数返回后又会恢复
CPSID I ;PRIMASK=1,关中断(NMI和硬fault可以响应)
BX LR ;返回
OS_CPU_SR_Restore
MSR PRIMASK, R0 ;读取R0到PRIMASK中, PRIMASK是M3内核中断屏蔽寄存器组
BX LR ;返回
//Cortex-M3还在内核水平上搭载了若干特殊功能寄存器,包括
程序状态字寄存器组(PSRs)
中断屏蔽寄存器组(PRIMASK, FAULTMASK, BASEPRI)
控制寄存器(CONTROL)
////////////////////////////////////////////////////////以下为注释///////////////////////////////////////////////////////////////////////////////////////////
//os_cpu.h中
#if OS_CRITICAL_METHOD == 3
#define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();}
#define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);}
#endif
1. Cortex-M3的异常/中断屏蔽寄存器组
注:只有在特权级下,才允许访问这3个寄存器。
名 字 |
功能描述 |
PRIMASK |
只有单一比特的寄存器。置为1后,就关掉所有可屏蔽异常,只剩下NMI和硬Fault可以响应。默认值是0,表示没有关闭中断。 |
FAULTMASK |
只有单一比特的寄存器。置为1后,只有NMI可以响应。默认值为0,表示没有关异常。 |
BASEPRI |
该寄存器最多有9位(由表达优先级的位数决定)。定义了被屏蔽优先级的阈值。当它被设置为某个值后,所有优先级号大于等于此值的中断都被关。若设置成0,则不关断任何中断,0为默认值。 |
注:寄存器BASEPRI的有效位数受系统中表达优先级的位数影响,如果系统中只使用3个位来表达优先级,则BASEPRI有意义的值仅为0x00、0x20、0x40、0x60、0x80、0xA0、0xC0和0xE0
使用MRS/MSR指令访问这三个寄存器,比如:
为了快速的开关中断,CM3还专门设置了一条CPS指令,有四种用法:
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
OSStartHighRdy()由OSStart()调用,用来启动最高优先级任务,当然任务必须在OSStart()前已被创建。
OSStartHighRdy
;设置PendSV中断的优先级 #1
LDR R0, =NVIC_SYSPRI14 ;R0 = NVIC_SYSPRI14
LDR R1, =NVIC_PENDSV_PRI ;R1 = NVIC_PENDSV_PRI
STRB R1, [R0] ;*(uint8_t *)NVIC_SYSPRI14 = NVIC_PENDSV_PRI
;设置PSP为0 #2
MOVS R0, #0 ;R0 = 0
MSR PSP, R0 ;PSP = R0
;设置OSRunning为TRUE
LDR R0, =OSRunning ;R0 = OSRunning//OSRunning为全局变量标志OS running状态
MOVS R1, #1 ;R1 = 1
STRB R1, [R0] ;OSRunning = 1
;触发PendSV中断 #3
LDR R0, =NVIC_INT_CTRL ;R0 = NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET ;R1 = NVIC_PENDSVSET
STR R1, [R0] ;*(uint32_t *)NVIC_INT_CTRL = NVIC_PENDSVSET
CPSIE I ;开中断
OSStartHang ;死循环,应该不会到这里
B OSStartHang
#1.PendSV中断的优先级应该为最低优先级,原因在<<ARM Cortex-M3权威指南>>的7.6节已有说明。
#2.PSP设置为0,是告诉具体的任务切换程序(OS_CPU_PendSVHandler()),这是第一次任务切换。做过切换后PSP就不会为0了,后面会看到。
#3.往中断控制及状态寄存器ICSR(0xE000ED04)第28位写1即可产生PendSV中断。这个<<ARM Cortex-M3权威指南>>8.4.5 其它异常的配置寄存器有说明。
当一个任务放弃cpu的使用权,就会调用OS_TASK_SW()宏,而OS_TASK_SW()就是OSCtxSw()。OSCtxSw()应该做任务切换。但是在CM3中,所有任务切换都被放到PendSV的中断处理函数中去做了,因此OSCtxSw()只需简单的触发PendSV中断即可。OS_TASK_SW()是由OS_Sched()调用。
////////////////////////////////////////////////////////////////
NVIC_INT_CTRL EQU 0xE000ED04 ; 中断控制寄存器
NVIC_SYSPRI14 EQU 0xE000ED22 ; 系统优先级寄存器(优先级14).
NVIC_PENDSV_PRI EQU 0xFF ; PendSV优先级(最低).
NVIC_PENDSVSET EQU 0x10000000 ; PendSV触发值
////////////////////////////////////////////////////////////////
//os_sched()调度函数调用OSCtxSw()
OSCtxSw
;触发PendSV中断
LDR R0, =NVIC_INT_CTRL ;R0 = NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET ;R1 = NVIC_PENDSVSET
STR R1, [R0] ;*(uint32_t *)NVIC_INT_CTRL = NVIC_PENDSVSET
//运行到此触发OS_CPU_PendSVHandler?????????
BX LR ;返回
NVIC_INT_CTRL EQU 0xE000ED04 ; Interrupt control state register.
NVIC_SYSPRI14 EQU 0xE000ED22 ; System priority register (priority 14).
NVIC_PENDSV_PRI EQU 0xFF ; PendSV priority value (lowest).
NVIC_PENDSVSET EQU 0x10000000 ; Value to trigger PendSV exception.
当一个中断处理函数退出时,OSIntExit()会被调用来决定是否有优先级更高的任务需要执行。如果有OSIntExit()对调用OSIntCtxSw()做任务切换。
OSIntCtxSw
;触发PendSV中断
LDR R0, =NVIC_INT_CTRL
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
BX LR
看到这里有些同学可能奇怪怎么OSCtxSw()和OSIntCtxSw()完全一样,事实上,这两个函数的意义是不一样的,OSCtxSw()做的是任务之间的切换,如任务A因为等待某个资源或是做延时切换到任务B,而OSIntCtxSw()则是中断退出时,由中断状态切换到另一个任务。由中断切换到任务时,CPU寄存器入栈的工作已经做完了,所以无需做第二次了(参考邵老师书的3.10节)。这里只不过由于CM3的特殊机制导致了在这两个函数中只要做触发PendSV中断即可,具体切换由PendSV中断来处理
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
//os_sched() ->OSCtxSw()触发OS_CPU_PendSVHandler
OS_CPU_PendSVHandler
// ;xPSR, PC, LR, R12, R0-R3已自动保存
CPSID I ;任务切换期间需要关中断 Prevent interruption during context switch
MRS R0, PSP ;R0 = PSP PSP is process stack pointer 线程堆栈指针 从
;OS_CPU_PendSVHandler返回后,自动变为选择PSP堆栈(线程堆栈)。
;主堆栈叫MSP。
CBZ R0, OS_CPU_PendSVHandler_nosave ;如果PSP==0跳转到
;OS_CPU_PendSVHandler_nosave去执行,
;在多任务的初始化时PSP被初始化为0 Skip register save the first time
;若果PSP如果是0,标示任务没有运行过,那么不需要压栈
SUBS R0, R0, #0x20 ;R0 -= 0x20 保存R4-R11到任务堆栈共32个字节
;满递减堆栈 先让堆栈指针减到堆栈完成后的位置,
;再装值(入站)完成堆栈。
STM R0, {R4-R11} ;压栈R4-R11, 其他8个寄存器是在异常时自动压栈的
LDR R1, =OSTCBCur ;获取OSTCBCur->OSTCBStkPtr
//; d) OSTCBCur points to the OS_TCB of the task to suspend
//OSStart()里有OSTCBCur = OSTCBHighRdy;OSStartHighRdy();
LDR R1, [R1] ;R1 = *R1 (R1 = OSTCBCur)
STR R0, [R1] ;*R1 = R0 (*OSTCBCur = SP) R0 is SP of process being switched out
;将当前任务的堆栈保存到自己的任务控制块
;OSTCBCur->OSTCBStkPtr = PSP
;程序运行此位置,已经保存了当前任务的context了
; At this point, entire context of process has been saved
OS_CPU_PendSVHandler_nosave
PUSH {R14} ;保存R14,因为后面要调用函数
LDR R0, =OSTaskSwHook ;R0 = &OSTaskSwHook
BLX R0 ;调用OSTaskSwHook()
POP {R14} ;恢复R14
;OSPrioCur = OSPrioHighRdy;
LDR R0, =OSPrioCur ;R0 = &OSPrioCur
LDR R1, =OSPrioHighRdy ;R1 = &OSPrioHighRdy
LDRB R2, [R1] ;R2 = *R1 (R2 = OSPrioHighRdy)
STRB R2, [R0] ;*R0 = R2 (OSPrioCur = OSPrioHighRdy)
;OSTCBCur = OSTCBHighRdy;
LDR R0, =OSTCBCur ;R0 = &OSTCBCur
LDR R1, =OSTCBHighRdy ;R1 = &OSTCBHighRdy
LDR R2, [R1] ;R2 = *R1 (R2 = OSTCBHighRdy)
STR R2, [R0] ;*R0 = R2 (OSTCBCur = OSTCBHighRdy)
LDR R0, [R2] ;R0 = *R2 (R0 = OSTCBHighRdy), 此时R0是新任务的SP
///R0为要切换的任务堆栈地址,任务堆栈里保存了LR,本任务PC
;SP = OSTCBHighRdy->OSTCBStkPtr #3
LDM R0, {R4-R11} ;从任务堆栈SP恢复R4-R11
ADDS R0, R0, #0x20 ;R0 += 0x20 这部调整后那psp岂不是指向下图绿色位置?
//要是那样的话内核自动出栈的话是不是先出xPSR?
MSR PSP, R0 ;PSP = R0,用新任务的SP加载PSP
ORR LR, LR, #0x04 ;确保LR位2为1,返回后使用进程堆栈 #4
CPSIE I ;开中断
BX LR ;中断返回 //OS_CPU_PendSVHandler也在这里返回中断
//跳到r14(LR)链接寄存器 怎么就切换到别的TCB里去了? PUSH {R14}
//为什么要SUBS R0, R0, #0x20 ????
//STM32发生异常时不是自动会把XPSP ,pc,lr,r12,r3,r2,r1,r0保存到堆栈中,
//难道不更新PSP,要我们手动更新PSP(SUBS R0, R0, #0x20 ),
//还是什么作用?这个问题我非常纠结!
END
#1 如果PSP == 0,说明OSStartHighRdy()启动后第一次做任务切换,而任务刚创建时R4-R11已经保存在堆栈中了,所以不需要再保存一次了,OS_CPU_PendSVHandler_nosave中的nosave就是这意思。
#2 OSTCBStkPtr是任务控制块结构体的第一个变量,所以*OSTCBCur = SP(不是很科学)就是OSTCBCur->OSTCBStkPtr = SP;
#3 和#2类似。
#4 因为在中断处理函数中使用的是MSP,所以在返回任务后必须使用PSP,所以LR位2必须为1。
u C / O S 中的每一个任务都有独立的堆栈空间,并有一个称为任务控制块TCB(Task Control Block)的数据结构,其中第一个成员变量就是保存的任务堆栈指针。
ISR执行完毕并退出后,PendSV服务例程开始执行,并且在里面执行上下文切换, 当 PendSVhandler执行完毕后,回到任务 A,同时系统再次进入线程模式。
// | .... |
;// |-----------------|
;// | .... |
;// |-----------------|
;// | .... |
;// |-----------------| |---- 任务切换时PSP
;// Low Memory | .... | |
;// |-----------------| | |---------------| |----------------|
;// ^ | R4 | <----|----|--OSTCBStkPtr |<-----| (OS_TCB *) |
;// ^ |-----------------| |---------------| |----------------|
;// ^ | R5 | | | OSTCBHighRdy
;// | |-----------------| |---------------|
;// | | R6 | | |
;// | |-----------------| |---------------|
;// | | R7 | | |
;// | |-----------------| |---------------|
;// | | R8 | Task's
;// | |-----------------| OS_TCB
;// | | R9 |
;// | |-----------------|
;// | | R10 |
;// Stack |-----------------|
;// Growth | R11 |
;// = 1 |-----------------|
;// | | R0 = p_arg | <-------- 异常时的PSP (向上生长的满栈) 指向这里
;// | |-----------------|
;// | | R1 |
;// | |-----------------|
;// | | R2 |
;// | |-----------------|
;// | | R3 |
;// | |-----------------|
;// | | R12 |
;// | |-----------------|
;// | | LR |
;// | |-----------------|
;// | | PC = task |
;// | |-----------------|
;// | | xPSR |<---------------调整指针后指向这里
;// High Memory |-----------------|
;*********************************************************************************************************
; START MULTITASKING
; void OSStartHighRdy(void)
;
; Note(s) : 1) This function triggers a PendSV exception (essentially, causes a context switch) to cause
; the first task to start.
;
; 2) OSStartHighRdy() MUST:
; a) Setup PendSV exception priority to lowest;
; b) Set initial PSP to 0, to tell context switcher this is first run;
; c) Set OSRunning to TRUE;
; d) Trigger PendSV exception;
; e) Enable interrupts (tasks will run with interrupts enabled).
;*********************************************************************************************************
OSStartHighRdy
LDR R0, =NVIC_SYSPRI2 ; Set the PendSV exception priority
LDR R1, =NVIC_PENDSV_PRI
STRB R1, [R0]
MOVS R0, #0 ; Set the PSP to 0 for initial context switch call
MSR PSP, R0
LDR R0, __OS_Running ; OSRunning = TRUE
; __OS_Running DCD OSRunning
MOVS R1, #1
STRB R1, [R0]
LDR R0, =NVIC_INT_CTRL ; Trigger the PendSV exception (causes context switch)
LDR R1, =NVIC_PENDSVSET
STR R1, [R0]
CPSIE I
另一个相关的异常是PendSV,它和SVC协同使用。一方面,SVC异常是必须执行SVC指令后立即得到响应的,应用程序执行SVC时都是希望所需的请求立即得到响应是。另一方面,PendSV则不同,它可以像普通的中断一样被挂起,OS可以利用它“缓期执行”一个异常——直到其它重要的任务完成后才执行动作,挂起PendSV的方法是:手工往NVIC的PendSV挂起寄存器中写1,挂起后,如果优先级不够高,则将缓期执行。
(参见<CORTEX-M3权威指南>130页)
PendSV典型使用场合是在上下文切换时(在不同任务间切换)。例如:一个系统中有两个就绪任务,上下文切换被触发的场合可以是:
● 执行一个系统调用
● 系统滴答定时器中断(轮转调度中需要)
PendSV异常会自动延尺上下文切换的请求,直到其他的ISR都完成了处理后才放行。为了实现这个机制,需要把PendSV编程为最低优先级的异常。如果OS检测到某IRQ正在活动并且被SysTick抢占,它将挂起一个PendSV异常,以便缓期执行上下文切换。
需要修改的代码就介绍到这里,如果还有不明白之处,就再看看AN-1018.pdf,邵老师的书和<<ARM Cortex-M3权威指南>>。