任务切换是通过产生一个PendSV中断,在中断服务程序中实现的。进入中断时保存了近一半的寄存器的值。我的理解是应该把这些值保存到任务堆栈中,但下面的代码并没有体现出。我很困惑,这些寄存器的值究竟保存在什么地方,与任务的堆栈的是否有关系?困扰许久,跪求高人指点 OS_CPU_PendSVHandler CPSID I ; Prevent interruption during context switch MRS R0, PSP ; PSP is process stack pointer CBZ R0, OS_CPU_PendSVHandler_nosave ; Skip register save the first time SUBS R0, R0, #0x20 ; Save remaining regs r4-11 on process stack STM R0, {R4-R11} LDR R1, =OSTCBCur ; OSTCBCur->OSTCBStkPtr = SP; LDR R1, [R1] STR R0, [R1] ; R0 is SP of process being switched out ; At this point, entire context of process has been saved OS_CPU_PendSVHandler_nosave PUSH {R14} ; Save LR exc_return value LDR R0, =OSTaskSwHook ; OSTaskSwHook(); BLX R0 POP {R14} LDR R0, =OSPrioCur ; OSPrioCur = OSPrioHighRdy; LDR R1, =OSPrioHighRdy LDRB R2, [R1] STRB R2, [R0] LDR R0, =OSTCBCur ; OSTCBCur = OSTCBHighRdy; LDR R1, =OSTCBHighRdy LDR R2, [R1] STR R2, [R0] LDR R0, [R2] ; R0 is new process SP; SP = OSTCBHighRdy->OSTCBStkPtr; LDM R0, {R4-R11} ; Restore r4-11 from new process stack ADDS R0, R0, #0x20 MSR PSP, R0 ; Load PSP with new process SP ORR LR, LR, #0x04 ; Ensure exception return uses process stack CPSIE I BX LR ; Exception return will restore remaining context END |
RE:ucosII在stm32的疑问,困扰许久【悬赏问答】
移植详解1和2中主要讲了移植需要用到的基础知识,本文则对具体的移植过程进行介绍。
首先从micrium网站上下载官方移植版本(编译器使用ARM/Keil的,V2.86版本,V2.85有问题)。
下载地址:http://micrium.com/page/downloads/ports/st/stm32
解压缩后得到如下文件夹和文件:
Micrium\
AppNotes
Licensing
Software
ReadMe.pdf
AppNotes包含ucosii移植说明文件。这两个文件中我们仅需关心Micrium\AppNotes\AN1xxx-RTOS\AN1018-uCOS-II-Cortex-M3\AN-1018.pdf。因为这个文件对ucosii在CM3内核移植过程中需要修改的代码进行了说明。
Licensing包含ucosii使用许可证。
Software下有好几个文件夹,在本文的移植中仅需关心uCOS-II即可。
CPU: stm32标准外设库
EvalBoards: micrium官方评估板相关代码
uc-CPU: 基于micrium官方评估板的ucosii移植代码
uC-LCD:micrium官方评估板LCD驱动代码
uc-LIB: micrium官方的一个库代码
uCOS-II: ucosii源代码
uC-Probe: 和uC-Probe相关代码
ReadMe.pdf就不说了。
好了,官方的东西介绍完了,该我们自己建立工程着手移植了。关于建立工程,并使用stm32标准外设库在我之前的文章《stm32标准外设库使用详解》已有介绍,这里请大家下载其中模板代码(http://download.csdn.net/source/3448543),本文的移植是基于这个工程的。
建立文件夹template\src\ucosii, template\src\ucosii\src, template\src\ucosii\port;
把Micrium\Software\uCOS-II\Source下的文件拷贝至template\src\ucosii\src;
把Micrium\Software\uCOS-II\Ports\ARM-Cortex-M3\Generic\RealView下的文件拷贝至
template\src\ucosii\port;
ucosii\src下的代码是ucosii中无需修改部分,ucosii\port下的代码是移植时需要修改的。为防止对源码的误改动造成移植失败,可以把ucosii\src下的代码文件设为只读。
这里根据AN-1018.pdf和移植详解1、2中介绍的移植基础知识,对ucosii\port下的代码解释一下。
os_cpu.h
#ifdef OS_CPU_GLOBALS
#define OS_CPU_EXT
#else
#define OS_CPU_EXT extern
#endif
typedef unsigned char BOOLEAN;
typedef unsigned char INT8U;
typedef signed char INT8S;
typedef unsigned short INT16U;
typedef signed short INT16S;
typedef unsigned int INT32U;
typedef signed int INT32S;
typedef float FP32;
typedef double FP64;
就不解释了。
typedef unsigned int OS_STK;
typedef unsigned int OS_CPU_SR;
因为CM3是32位宽的,所以OS_STK(堆栈的数据类型)被类型重定义为unsigned int。
因为CM3的状态寄存器(xPSR)是32位宽的,因此OS_CPU_SR被类型重定义为unsigned int。OS_CPU_SR是在OS_CRITICAL_METHOD方法3中保存cpu状态寄存器用的。在CM3中,移植OS_ENTER_CRITICAL(),OS_EXIT_CRITICAL()选方法3是最合适的。
#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_ENTER_CRITICAL()和OS_EXIT_CRITICAL(),其中OS_CPU_SR_Save()和OS_CPU_SR_Restore()是用汇编代码写的,代码在os_cpu_a.asm中,到时再解释。
#define OS_STK_GROWTH 1
CM3中,栈是由高地址向低地址增长的,因此OS_STK_GROWTH定义为1。
#define OS_TASK_SW() OSCtxSw()
定义任务切换宏,OSCtxSw()是用汇编代码写的,代码在os_cpu_a.asm中,到时再解释。
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR OS_CPU_SR_Save(void);
void OS_CPU_SR_Restore(OS_CPU_SR cpu_sr);
#endif
void OSCtxSw(void);
void OSIntCtxSw(void);
void OSStartHighRdy(void);
void OS_CPU_PendSVHandler(void);
void OS_CPU_SysTickHandler(void);
void OS_CPU_SysTickInit(void);
INT32U OS_CPU_SysTickClkFreq(void);
申明几个函数,这里要注意最后三个函数需要注释掉,为什么呢?
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,后面我们会自己实现,因此可以把它注释掉。
os_cpu_c.c
ucosii移植时需要我们写10个相当简单的C函数。
OSInitHookBegin()
OSInitHookEnd()
OSTaskCreateHook()
OSTaskDelHook()
OSTaskIdleHook()
OSTaskStatHook()
OSTaskStkInit()
OSTaskSwHook()
OSTCBInitHook()
OSTimeTickHook()
这些函数除了OSTaskStkInit(),都是一些hook函数。这些hook函数如果不使能的话,都不会用上,也都比较简单,看看就应该明白了,所以就不介绍。
下面就说一说OSTaskStkInit()。说之前还是得先说一下任务切换,因为初始化任务堆栈,是为任务切换服务的。代码在正常运行时,一行一行往下执行,怎么才能跑到另一个任务(即函数)执行呢?首先大家可以回想一下中断过程,当中断发生时,原来函数执行的地方(程序计数器PC、处理器状态寄存器及所有通用寄存器,即当前代码的现场)被保存到栈里面去了,然后开始取中断向量,跑到中断函数里面执行。执行完了呢,想回到原来函数执行的地方,该怎么办呢,只要把栈中保存的原来函数执行的信息恢复即可(把栈中保存的代码现场重新赋给cpu的各个寄存器),一切就都回去了,好像什么事都没发生一样。这个过程大家应该都比较熟悉,任务切换和这有什么关系,试想一下,如果有3个函数foo1(), foo2(), foo3()像是刚被中断,现场保存到栈里面去了,而中断返回时做点手脚(调度程序的作用),想回哪个回哪个,是不是就做了函数(任务)切换了。看到这里应该有点明白OSTaskStkInit()的作用了吧,它被任务创建函数调用,所以要在开始时,在栈中作出该任务好像刚被中断一样的假象。(关于任务切换的原理邵老师书中的3.06节有介绍)。
那么中断后栈中是个什么情形呢,中9.1.1有介绍,xPSR,PC,LR,R12,R3-R0被自动保存到栈中的,R11-R4如果需要保存,只能手工保存。因此OSTaskStkInit()的工作就是在任务自己的栈中保存cpu的所有寄存器。这些值里R1-R12都没什么意义,这里用相应的数字代号(如R1用0x01010101)主要是方便调试。
其他几个:
xPSR = 0x01000000L,xPSR T位(第24位)置1,否则第一次执行任务时Fault,
PC肯定得指向任务入口,
R14 = 0xFFFFFFFEL,最低4位为E,是一个非法值,主要目的是不让使用R14,即任务是不能返回的。
R0用于传递任务函数的参数,因此等于p_arg。
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 */
/* R14 (LR) (init value will cause fault if ever used)*/
*(--stk) = (INT32U)0xFFFFFFFEL;
*(--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);
}
把OS_CPU_SysTickHandler(), OS_CPU_SysTickInit()注释掉。
#define OS_CPU_CM3_NVIC_ST_CTRL (*((volatile INT32U *)0xE000E010))
#define OS_CPU_CM3_NVIC_ST_RELOAD (*((volatile INT32U *)0xE000E014))
#define OS_CPU_CM3_NVIC_ST_CURRENT (*((volatile INT32U *)0xE000E018))
#define OS_CPU_CM3_NVIC_ST_CAL (*((volatile INT32U *)0xE000E01C))
#define OS_CPU_CM3_NVIC_ST_CTRL_COUNT 0x00010000
#define OS_CPU_CM3_NVIC_ST_CTRL_CLK_SRC 0x00000004
#define OS_CPU_CM3_NVIC_ST_CTRL_INTEN 0x00000002
#define OS_CPU_CM3_NVIC_ST_CTRL_ENABLE 0x00000001
把上面这些宏定义也注释掉,因为它们都用于OS_CPU_SysTickHandler(), OS_CPU_SysTickInit()。
os_cpu_a.asm
这个文件包含着必须用汇编写的代码。
EXTERN OSRunning ; External references
EXTERN OSPrioCur
EXTERN OSPrioHighRdy
EXTERN OSTCBCur
EXTERN OSTCBHighRdy
EXTERN OSIntNesting
EXTERN OSIntExit
EXTERN OSTaskSwHook
申明这些变量是在其他文件定义的,本文件只做引用(有几个好像并未引用,不过没有关系)。
EXPORT OS_CPU_SR_Save ; Functions declared in this file
EXPORT OS_CPU_SR_Restore
EXPORT OSStartHighRdy
EXPORT OSCtxSw
EXPORT OSIntCtxSw
EXPORT OS_CPU_PendSVHandler
申明这些函数是在本文件中定义的。
NVIC_INT_CTRL EQU 0xE000ED04 ;中断控制及状态寄存器ICSR的地址
NVIC_SYSPRI14 EQU 0xE000ED22 endSV优先级寄存器的地址
NVIC_PENDSV_PRI EQU 0xFF endSV中断的优先级为255(最低)
NVIC_PENDSVSET EQU 0x10000000 ;位28为1
定义几个常量,类似C语言中的#define预处理指令。
OS_CPU_SR_Save
MRS R0, PRIMASK ;读取PRIMASK到R0中,R0为返回值
CPSID I RIMASK=1,关中断(NMI和硬fault可以响应)
BX LR ;返回
OS_CPU_SR_Restore
MSR PRIMASK, R0 ;读取R0到PRIMASK中,R0为参数
BX LR ;返回
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 SP = R0
;设置OSRunning为TRUE
LDR R0, =OSRunning ;R0 = OSRunning
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中断的优先级应该为最低优先级,原因在的7.6节已有说明。
#2.PSP设置为0,是告诉具体的任务切换程序(OS_CPU_PendSVHandler()),这是第一次任务切换。做过切换后PSP就不会为0了,后面会看到。
#3.往中断控制及状态寄存器ICSR(0xE000ED04)第28位写1即可产生PendSV中断。这个8.4.5 其它异常的配置寄存器有说明。
当一个任务放弃cpu的使用权,就会调用OS_TASK_SW()宏,而OS_TASK_SW()就是OSCtxSw()。OSCtxSw()应该做任务切换。但是在CM3中,所有任务切换都被放到PendSV的中断处理函数中去做了,因此OSCtxSw()只需简单的触发PendSV中断即可。OS_TASK_SW()是由OS_Sched()调用。
void OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr = 0;
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting == 0) {
if (OSLockNesting == 0) {
OS_SchedNew();
if (OSPrioHighRdy != OSPrioCur) {
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
#if OS_TASK_PROFILE_EN > 0
OSTCBHighRdy->OSTCBCtxSwCtr++;
#endif
OSCtxSwCtr++;
OS_TASK_SW(); /* 触发PendSV中断 */
}
}
}
/* 一旦开中断,PendSV中断函数会执行(当然要等更高优先级中断处理完) */
OS_EXIT_CRITICAL();
}
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
BX LR ;返回
当一个中断处理函数退出时,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中断来处理。
前面已经说过真正的任务切换是在PendSV中断处理函数里做的,由于CM3在中断时会有一半的寄存器自动保存到任务堆栈里,所以在PendSV中断处理函数中只需保存R4-R11并调节堆栈指针即可。
PendSV中断处理函数伪代码如下:
OS_CPU_PendSVHandler()
{
if (PSP != NULL) {
Save R4-R11 onto task stack;
OSTCBCur->OSTCBStkPtr = SP;
}
OSTaskSwHook();
OSPrioCur = OSPrioHighRdy;
OSTCBCur = OSTCBHighRdy;
PSP = OSTCBHighRdy->OSTCBStkPtr;
Restore R4-R11 from new task stack;
Return from exception;
}
OS_CPU_PendSVHandler ;xPSR, PC, LR, R12, R0-R3已自动保存
CPSID I ;任务切换期间需要关中断
MRS R0, PSP ;R0 = PSP
;如果PSP == 0,跳到OS_CPU_PendSVHandler_nosave执行 #1
CBZ R0, OS_CPU_PendSVHandler_nosave
;保存R4-R11到任务堆栈
SUBS R0, R0, #0x20 ;R0 -= 0x20
STM R0, {R4-R11} ;保存R4-R11到任务堆栈
;OSTCBCur->OSTCBStkPtr = SP;
LDR R1, =OSTCBCur ;R1 = &OSTCBCur
LDR R1, [R1] ;R1 = *R1 (R1 = OSTCBCur)
STR R0, [R1] ;*R1 = R0 (*OSTCBCur = SP) #2
OS_CPU_PendSVHandler_nosave
;调用OSTaskSwHook()
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
;SP = OSTCBHighRdy->OSTCBStkPtr #3
LDM R0, {R4-R11} ;从任务堆栈SP恢复R4-R11
ADDS R0, R0, #0x20 ;R0 += 0x20
MSR PSP, R0 SP = R0,用新任务的SP加载PSP
ORR LR, LR, #0x04 ;确保LR位2为1,返回后使用进程堆栈 #4
CPSIE I ;开中断
BX LR ;中断返回
END
#1 如果PSP == 0,说明OSStartHighRdy()启动后第一次做任务切换,而任务刚创建时R4-R11已经保存在堆栈中了,所以不需要再保存一次了。
#2 OSTCBStkPtr是任务控制块结构体的第一个变量,所以*OSTCBCur = SP(不是很科学)就是OSTCBCur->OSTCBStkPtr = SP;
#3 和#2类似。
#4 因为在中断处理函数中使用的是MSP,所以在返回任务后必须使用PSP,所以LR位2必须为1。
os_dbg.c
用于系统调试,可以不管。
需要修改的代码就介绍到这里,如果还有不明白之处,就再看看AN-1018.pdf,邵老师的书和。
RE:ucosII在stm32的疑问,困扰许久【悬赏问答】
RE:ucosII在stm32的疑问,困扰许久【悬赏问答】
是的,应该都是保存在堆栈中的。
RE:ucosII在stm32的疑问,困扰许久【悬赏问答】
RE:ucosII在stm32的疑问,困扰许久【悬赏问答】
RE:ucosII在stm32的疑问,困扰许久【悬赏问答】
任务切换是在PendSV中断处理函数里做的,由于CM3在中断时会有一半的寄存器自动保存到任务堆栈里,所以在PendSV中断处理函数中只需保存R4-R11并调节堆栈指针即可,参考下这个例程的
当一个任务放弃cpu的使用权,就会调用OS_TASK_SW()宏,而OS_TASK_SW()就是OSCtxSw()。OSCtxSw()应该做任务切换。但是在CM3中,所有任务切换都被放到PendSV的中断处理函数中去做了,因此OSCtxSw()只需简单的触发PendSV中断即可。OS_TASK_SW()是由OS_Sched()调用。
void OS_Sched (void)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cpu_sr = 0;
#endif
OS_ENTER_CRITICAL();
if (OSIntNesting == 0) {
if (OSLockNesting == 0) {
OS_SchedNew();
if (OSPrioHighRdy != OSPrioCur) {
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
#if OS_TASK_PROFILE_EN > 0
OSTCBHighRdy->OSTCBCtxSwCtr++;
#endif
OSCtxSwCtr++;
OS_TASK_SW(); /* 触发PendSV中断 */
}
}
}
/* 一旦开中断,PendSV中断函数会执行(当然要等更高优先级中断处理完) */
OS_EXIT_CRITICAL();
}
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
BX LR ;返回
当一个中断处理函数退出时,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中断来处理。
前面已经说过真正的任务切换是在PendSV中断处理函数里做的,由于CM3在中断时会有一半的寄存器自动保存到任务堆栈里,所以在PendSV中断处理函数中只需保存R4-R11并调节堆栈指针即可。
PendSV中断处理函数伪代码如下:
OS_CPU_PendSVHandler()
{
if (PSP != NULL) {
Save R4-R11 onto task stack;
OSTCBCur->OSTCBStkPtr = SP;
}
OSTaskSwHook();
OSPrioCur = OSPrioHighRdy;
OSTCBCur = OSTCBHighRdy;
PSP = OSTCBHighRdy->OSTCBStkPtr;
Restore R4-R11 from new task stack;
Return from exception;
}
OS_CPU_PendSVHandler ;xPSR, PC, LR, R12, R0-R3已自动保存
CPSID I ;任务切换期间需要关中断
MRS R0, PSP ;R0 = PSP
;如果PSP == 0,跳到OS_CPU_PendSVHandler_nosave执行 #1
CBZ R0, OS_CPU_PendSVHandler_nosave
;保存R4-R11到任务堆栈
SUBS R0, R0, #0x20 ;R0 -= 0x20
STM R0, {R4-R11} ;保存R4-R11到任务堆栈
;OSTCBCur->OSTCBStkPtr = SP;
LDR R1, =OSTCBCur ;R1 = &OSTCBCur
LDR R1, [R1] ;R1 = *R1 (R1 = OSTCBCur)
STR R0, [R1] ;*R1 = R0 (*OSTCBCur = SP) #2
OS_CPU_PendSVHandler_nosave
;调用OSTaskSwHook()
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
;SP = OSTCBHighRdy->OSTCBStkPtr #3
LDM R0, {R4-R11} ;从任务堆栈SP恢复R4-R11
ADDS R0, R0, #0x20 ;R0 += 0x20
MSR PSP, R0 SP = R0,用新任务的SP加载PSP
ORR LR, LR, #0x04 ;确保LR位2为1,返回后使用进程堆栈 #4
CPSIE I ;开中断
BX LR ;中断返回
END
#1 如果PSP == 0,说明OSStartHighRdy()启动后第一次做任务切换,而任务刚创建时R4-R11已经保存在堆栈中了,所以不需要再保存一次了。
#2 OSTCBStkPtr是任务控制块结构体的第一个变量,所以*OSTCBCur = SP(不是很科学)就是OSTCBCur->OSTCBStkPtr = SP;
#3 和#2类似。
#4 因为在中断处理函数中使用的是MSP,所以在返回任务后必须使用PSP,所以LR位2必须为1。
os_dbg.c
用于系统调试,可以不管。
需要修改的代码就介绍到这里,如果还有不明白之处,就再看看AN-1018.pdf,邵老师的书和。
详解3中有一个问题还没解释,就是stm32f10x_it.c中已经有SysTick中断函数的定义SysTick_Handler(),为什么官方版非要弄个OS_CPU_SysTickHandler()。答案就在启动文件上,一般我们自己开发基于stm32芯片的软件,都会使用标准外设库CMSIS中提供的启动文件,而官方移植的启动文件却是自己写的,在两个文件init.s,vectors.s中(Micrium\Software\EvalBoards\ST\STM3210B-EVAL\RVMDK)。init.s负责进入main(),vectors.s设置中断向量。OS_CPU_SysTickHandler和OS_CPU_PendSVHandler就是在vectors.s中被设置的。
我的移植是使用标准外设库CMSIS中startup_stm32f10x_hd.s作为启动文件的,那该怎么在这个文件中设置OS_CPU_SysTickHandler呢,事实上在startup_stm32f10x_hd.s文件中,PendSV中断向量名为PendSV_Handler,所以只需用OS_CPU_PendSVHandler把所有出现PendSV_Handler的地方替换掉就可以了。
那么为什么OS_CPU_SysTickHandler不用这种方式处理呢,这样也就不用注释os_cpu.c中的OS_CPU_SysTickHandler(),这主要是基于两个原因:
1. startup_stm32f10x_hd.s尽量少该,能不改就不改。
2. 如果保留OS_CPU_SysTickHandler(),在以后开发过程中,改动OS_CPU_SysTickHandler()中的内容可能性是非常大的,如果一不小把该文件其他部分改了造成了问题,这个bug就非常难查了,所以我一般移植好后就把ucosii的这些文件设置为只读。
对于上面的原因1,一开始移植时,我曾做过在PendSV_Handler()中调用OS_CPU_PendSVHandler(),后来发现这样不行,这是为什么呢?问题出在LR寄存器上。
PendSV_Handler()
{
OS_CPU_PendSVHandler();
}
汇编出来的代码会是这样:
PendSV_Handler PROC
PUSH {r4,lr}
BL OS_CPU_PendSVHandler
POP {r4,pc}
ENDP
这样在进入OS_CPU_PendSVHandler之后,LR寄存器中存放的是指令POP {r4,pc}的地址+1。在OS_CPU_PendSVHandler中的ORR LR, LR, #0x04就不会起作用,也就无法使用PSP,移植因此失败。其实在AN-1018.pdf的3.04.06中也有强调OS_CPU_PendSVHandler必须被放置在中断向量表中。一开始我也没注意。
到这里移植的大部分工作都做完了,下面剩下的就是把工程配置好,SysTick中断处理好。
在工程中建立ucosii组,把ucosii下的文件都加进该组。这里别忘了把os_cpu_a.asm加入。
在工程的Options中,c/c++选项卡的Include Paths中添加.\src\ucosii\src;.\src\ucosii\port。
编译工程,会发现缺少app_cfg.h和os_cfg.h文件,app_cfg.h是用来配置应用软件的,主要是任务的优先级和堆栈大小,中断优先级等信息。目前还没有基于ucosii开发应用软件,所以只需在include文件夹中创建一个空的app_cfg.h文件即可。os_cfg.h是用来配置ucosii系统的。拷贝Micrium\Software\EvalBoards\ST\STM3210B-EVAL\RVMDK\OS-Probe\os_cfg.h到template\include,对其做如下修改:
#define OS_APP_HOOKS_EN 0
#define OS_DEBUG_EN 0
#define OS_EVENT_MULTI_EN 0
#define OS_SCHED_LOCK_EN 0
#define OS_TICK_STEP_EN 0
#define OS_TASK_CHANGE_PRIO_EN 0
#define OS_TASK_QUERY_EN 0
#define OS_TASK_STAT_EN 0
#define OS_TASK_STAT_STK_CHK_EN 0
#define OS_TASK_SUSPEND_EN 0
#define OS_FLAG_EN 0
#define OS_MBOX_EN 0
#define OS_TIME_DLY_HMSM_EN 0
#define OS_TIME_DLY_RESUME_EN 0
#define OS_TIME_GET_SET_EN 0
#define OS_TIME_TICK_HOOK_EN 0
所做的修改主要是把一些功能给去掉,减少内核大小,也利于调试。等移植完成后,如果需要该功能,再做开启。
接下来就剩下处理好SysTick中断和启动任务了。SysTick是系统的“心跳”,本质上来说就是一个定时器。先把原来main.c中的内容删除,添加如下代码:
#include "ucos_ii.h"
#include "stm32f10x.h"
static OS_STK startup_task_stk[STARTUP_TASK_STK_SIZE];
static void systick_init(void)
{
RCC_ClocksTypeDef rcc_clocks;
RCC_GetClocksFreq(&rcc_clocks);
SysTick_Config(rcc_clocks.HCLK_Frequency / OS_TICKS_PER_SEC);
}
static void startup_task(void *p_arg)
{
systick_init(); /* Initialize the SysTick. */
#if (OS_TASK_STAT_EN > 0)
OSStatInit(); /* Determine CPU capacity. */
#endif
/* TODO: create application tasks here */
OSTaskDel(OS_PRIO_SELF);
}
int main(void)
{
OSInit();
OSTaskCreate(startup_task, (void *)0,
&startup_task_stk[STARTUP_TASK_STK_SIZE - 1],
STARTUP_TASK_PRIO);
OSStart();
return 0;
}
systick_init()用来初始化并启动SysTick定时器。
RCC_GetClocksFreq()用来获取系统时钟。
SysTick_Config()初始化并使能SysTick定时器。
这里要注意的是OS_TICKS_PER_SEC,它是每秒钟的ticks数,如果为1000,就是1s中1000个ticks,也就是说1ms就会产生一个SysTick中断。系统的时间片为1ms。
在邵老师的书中3.11节已有明确说明,必须在调用OSStart()之后,才能开启时钟节拍器(SysTick)。一般会把它放在第一个任务(启动任务)中。
startup_task()用来创建其他应用任务,创建完其他任务后,就会自己删除自己。
文件中的STARTUP_TASK_STK_SIZE,STARTUP_TASK_PRIO需要在app_cfg.h中定义。代码如下:
/* task priority */
#define STARTUP_TASK_PRIO 4
/* task stack size */
#define STARTUP_TASK_STK_SIZE 80
在stm32f10x_it.c中,还需要添加SysTick中断的处理代码:
void SysTick_Handler(void)
{
OSIntEnter();
OSTimeTick();
OSIntExit();
}
RE:ucosII在stm32的疑问,困扰许久【悬赏问答】
回复:ucosII在stm32的疑问,困扰许久【悬赏问答】
RE:ucosII在stm32的疑问,困扰许久【悬赏问答】