ucOS-II Version : V2.86 CPU STM32F103VET6
移植步骤:
usos官网下载demo工程
解压后如下: 以上这些都是下载下来的官方资源。有没有发现,uC/OS 的代码文件都被 分开放到不同的文件夹里了?呵呵,这个是官方移植好到 STM32 的 uC/OS 系 统,他已经帮我们对 uC/OS 的文件进行分类存放。
需要移植的文件
模块之间的关系:
需要移植的文件:
os_cpu.h
os_cpu_c.c
os_cpu_a.asm
1. os_cpu.h [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2T0dCMLw-1663256089304)(C:\Users\wangfeitao\AppData\Roaming\Typora\typora-user-images\image-20220913213355435.png)]
事实上,有 3 种开关中断的方法,根据不同的处理器选用不同的方法。大 部分情况下,选用第 3 种方法。具体定义宏 OS_ENTER_CRITICAL()和 OS_EXIT_CRITICAL(),其中 OS_CPU_SR_Save()和 OS_CPU_SR_Restore()是用汇编代码写的,代码在 os_cpu_a.asm 中。 中断调用中断函数SysTick_Handler()来实现任务的调度
void SysTick_Handler ( void )
{
OS_CPU_SR cpu_sr;
OS_ENTER_CRITICAL ( ) ;
OSIntNesting++ ;
OS_EXIT_CRITICAL ( ) ;
OSTimeTick ( ) ;
OSIntExit ( ) ;
}
这里首先定义了一个OS_CPU_SR变量,用于接受PRIMASK中断屏蔽寄存器的值。然后调用OS_ENTER_CRITICAL()进入临界段,所谓临界段就是系统不希望被其他中断打扰(NMI和硬fault除外,具体功能大家查一下手册),然后将OSIntNesting(表示中断嵌套的层数)加1,执行完之后便退出临界段,临界段所处的时间越短越好,太长时间将会影响到其他中断的响应,对实时性不利,同样其他不希望被中断打扰一切操作都可以用这两个函数来进行控制。接着便调用OSTimeTick()和OSIntExit()完成这次任务调度。
先看一下OS_ENTER_CRITICAL(),代码如下: #define OS_ENTER_CRITICAL() {cpu_sr = OS_CPU_SR_Save();}
这里将PRIMASK存入R0,然后关闭总中断,跳回原来函数,这样就进入了临界段,不会有其他中断打扰。PRIMASK在手册中解释为这个寄存器只有一个位,置1后,将关闭所有可屏蔽中断的异常,只剩NMI和硬fault,默认值为0。 R0是什么,就是调用函数传进来的第一个参数,也就是cpu_sr。在汇编中R0~R3会依次接受传进来的不多于4个参数,再多的话建议采用指针或者堆栈。 OS_EXIT_CRITICAL(),代码如下: #define OS_EXIT_CRITICAL() {OS_CPU_SR_Restore(cpu_sr);}
也是一个宏定义,和上述一样,OS_CPU_SR_Restore(cpu_sr)是一个汇编程序:将cpu_sr中的值恢复,然后返回。
下面前4个实现在 os_cpu_a.asm中,后三个用来配置systick产生os心跳,可以根据项目配置。
void OSCtxSw ( void ) ;
void OSIntCtxSw ( void ) ;
void OSStartHighRdy ( void ) ;
void OS_CPU_PendSVHandler ( void ) ;
2. os_cpu_c.c
ARM Cortex-M3端口的每个任务的堆栈帧
OS_STK * OSTaskStkInit ( void ( * task) ( void * p_arg) , void * p_arg, OS_STK * ptos, INT16U opt)
{
OS_STK * stk;
( void ) opt;
stk = ptos;
* ( stk) = ( INT32U) 0x01000000L ;
* ( -- stk) = ( INT32U) task;
* ( -- stk) = ( INT32U) 0xFFFFFFFEL ;
* ( -- stk) = ( INT32U) 0x12121212L ;
* ( -- stk) = ( INT32U) 0x03030303L ;
* ( -- stk) = ( INT32U) 0x02020202L ;
* ( -- stk) = ( INT32U) 0x01010101L ;
* ( -- stk) = ( INT32U) p_arg;
* ( -- stk) = ( INT32U) 0x11111111L ;
* ( -- stk) = ( INT32U) 0x10101010L ;
* ( -- stk) = ( INT32U) 0x09090909L ;
* ( -- stk) = ( INT32U) 0x08080808L ;
* ( -- stk) = ( INT32U) 0x07070707L ;
* ( -- stk) = ( INT32U) 0x06060606L ;
* ( -- stk) = ( INT32U) 0x05050505L ;
* ( -- stk) = ( INT32U) 0x04040404L ;
return ( stk) ;
}
在 OS_CPU.H 中定义任务栈压栈方向是递减的: #define OS_STK_GROWTH 1 /* Stack grows from HIGH to LOW memory on ARM */
OSTaskStkInit()说之前还是得先说一下任务切换,因为初始化任务堆栈,是为任务切换服务的。代码在正常运行时,一行一行往下执行,怎么才能跑到另一个任务(即函数)执行呢?首先大家可以回想一下中断过程,当中断发 生时,原来函数执行的地方(程序计数器 PC、处理器状态寄存器及所有通用寄存 器,即当前代码的现场)被保存到栈里面去了,然后开始取中断向量,跑到中断函 数里面执行。执行完了呢,想回到原来函数执行的地方,该怎么办呢,只要把栈中保存的原来函数执行的信息恢复即可(把栈中保存的代码现场重新赋给 cpu 的各个寄存器),一切就都回去了,好像什么事都没发生一样。这个过程大家应该都比较 熟悉,任务切换和这有什么关系,试想一下,如果有 3 个函数 foo1(), foo2(), foo3() 像是刚被中断,现场保存到栈里面去了,而中断返回时做点手脚(调度程序的作用), 想回哪个回哪个,是不是就做了函数(任务)切换了。看到这里应该有点明白 OSTaskStkInit()的作用了吧,它被任务创建函数调用,所以要在开始时,在栈中作出该任务好像刚被中断一样的假象。(关于任务切换的原理邵老师书中的 3.06 节有 介绍)。
那么中断后栈中是个什么情形呢,《ARM Cortex-M3 权威指南>》中 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。
注释下面SysTick 的中断服务函数和初始化函数实现,可以根据项目配置;os_cpu_c.c中的其它函数都是些 HOOK。
/* void OS_CPU_SysTickHandler (void) */
/* void OS_CPU_SysTickInit (void) */
3. os_cpu_a.asm EXTERN申明这些变量是在其他文件定义的,类似于C语言中的extern关键字。 关于 EXPORT 的用法和意义,可以参考 RealView 编译工具 4.0 版《汇编器指 南》第 7.8.7 小节 EXPORT 或 GLOBAL: EXPORT 指令声明一个符号,链接器可以使用该符号解析不同对象和库文件中的符号引用。 GLOBAL 是 EXPORT 的同义词。 使用 EXPORT 可使其他文件中的代码能够访问当前文件中的符号,即表示这些函数位于该文件内,供其他文件调用。 EQU和C语言中的define关键字一样,用于宏定义,定义了一些寄存器的地址。
AREA |.text| 表示:选择段 |.text|;CODE 表示代码段,READONLY 表示只读(缺省);ALIGN=2 表示 4 字节对齐。若 ALIGN=n,这 2^n 对齐。
Thumb 代码
指定当前文件要求堆栈八字节对齐
令指定当前文件保持堆栈八字节对齐
中断控制及状态寄存器 ICSR 的地址: 为系统设备而设的“可悬挂请求”系统异常优先级寄存器 PRI_14: NVIC_PENDSV_PRI EQU 0xFF ;定义 PendSV 的可编程优先级为 255,即最低
NVIC_PENDSVSET EQU 0x10000000 ;中断控制及状态寄存器 ICSR 的位 28
OSStartHighRdy()由OSStart()调用,用来启动最高优先级任务,当然任务必须在OSStart()前已被创建。 当一个任务放弃cpu的使用权,就会调用OS_TASK_SW()宏,而OS_TASK_SW()就是 OSCtxSw()。OSCtxSw()应该做任务切换。但是在CM3中,所有任务切换都被放到PendSV的中断处理函数中去做了,因此OSCtxSw()只需简单的触发PendSV中断即可。OS_TASK_SW()是由OS_Sched()调用。
为什么在PendSV的中断去进行任务切换?
SVC(系统服务调用,亦简称系统调用)和 PendSV(可悬起系统调用),它们多用在上了操作 系统的软件开发中。SVC 用于产生系统函数的调用请求。 当一个中断处理函数退出时,OSIntExit()会被调用来决定是否有优先级更高的任务需要执行。如果有OSIntExit()对调用OSIntCtxSw()做任务切换。 这个OSCtxSw()怎么和OSIntCtxSw()完全一样,事实上,这两个函数的意义是不一样的。
OSCtxSw()做的是任务之间的切换。例如任务因为等待某个资源或做延时,就会调用这个函数来进行任务调度,有任务调度进行任务切换。
OSIntCtxSw()则是中断退出时,如果最高优先级就绪任务并不是被中断的任务就会被调用,由中断状态切换到最高优先级就绪任务中,所以OSIntCtxSw()又称中断级的中断任务。
由于调用OSIntCtxSw()之前肯定发生了中断,所以无需保存CPU寄存器的值了。这里只不过由于CM3的特殊机制导致了在这两个函数中只要做触发PendSV中断即可,具体切换由PendSV中断服务来处理。 前面已经说过真正的任务切换是在PendSV中断处理函数里做的,由于CM3在中断时会有一半的寄存器自动保存到任务堆栈里,所以在PendSV中断处理函数中只需保存R4-R11并调节堆栈指针即可。 CPSID I
关中断 MRS R0, PSP
R0=PSP,PSP就是栈指针。 CBZ R0, OSPendSV_nosave
CBZ , 比较R0存器,为零则跳转到label。
SUBS R0, R0, #0x20
减法指令 SUBS Rd, Rn, Operand2,Rd = Rn - Operand2,为下一步手动压栈做准备,装载 r4-11 到栈 ,共 8 个寄存器,32 位,4 个字节;即 8*4=32=0x20。 STM R0, {R4-R11}
多数据存储指令 STM{addr_mode}{cond} Rn{!}, reglist{^},R0为基址寄存器,装有传送数据的起始地址,将列表的寄存器值存到地址上,addr_mode省略表示每次传送后地址加4,其中寄存器从左到右执行。
LDR R1, =OSTCBCur
LDR伪指令的语法形式,将指定标号的值赋给R1,这里取得的是标号OSTCBCur
的绝对地址,这个绝对地址(链接地址)是在链接的时候确定的,加载OSTCBCur指针的地址到R1,即:R1=&OSTCBCur。 LDR R1, [R1]
将R1地址中的值赋给R1,加载OSTCBCurPtr指针到R1,即:R1=*R1 (R1=OSTCBCur) 。 STR R0, [R1]
将R0寄存器取32位字数据到R1地址中,将R0(栈顶数据的地址)赋值给OSTCBCur->SP,则OSTCBCur->SP与R0指向的位置相同,这段程序的OSTCBCur指的都是old_task的指针,此时保存上文完成,即:R1 = R0 (*OSTCBCur = SP)。
OSTCBCur
是一个指向正在运行任务控制块的指针。R0是正在切换的进程的SP。将指向正在运行任务控制块的指针指向要切换的任务块就完成了任务切换。
任务切换就是中止正在运行的当前任务,转而去运行另外一个任务的操作。当然这个任务应该就是就绪任务中优先级别最高的哪个任务。那么被中止运行(可能因为中断或者调用)的任务将来又是怎么“无缝”地恢复到运行态呢? 要实现 这种“无缝”的接续运行,则必须在任务被中止时就把该任务的断点数据保存到堆栈中;而在被重新运行时,则要把堆栈中的这些断点数据再恢复到CPU的各寄存器中,只有这样才能使被中止运行的任务在恢复运行时可以实现“无缝”的接续运行。 一个任务在被中止运行时保护断点数据的动作如下图所示。 由上可知,一个被中止的任务能否正确 地在断点处恢复运行,其关键在于是否能正确地在CPU各寄存器中恢复断点数据;而能够正确恢复断点数据的关键是CPU的堆栈指针SP是否有正确的指向。因此也可知,在系统中存在多个任务时,如果在恢复断点数据时用另一个任务的任务堆栈指针 (存放在控制块成员OSTCBStkPtr中)来改变CPU的堆栈指针SP,那么CPU运行的就不是刚才被中止运行的任务,而是另一个任务了,也就是实现任务切换了。当然,为防止被中止任务堆栈指针的丢失,被中止任务在保存断点时,要把当时CPU的SP的值保存到该任务控制块的成员OSTCBStkPtr中。
如果 PSP == 0,才会跳转到OS_CPU_PendSVHandler_nosave
,说明 OSStartHighRdy()启动后第一次做任务切换,而任务刚创建时 R4-R11 已经保存在堆栈中了,所以不需要再保存一次了。
PUSH {R14}
保存 R14。 LDR R0, =OSTaskSwHook
将指定标号的值OSTaskSwHook地址赋给R0,即:R0 = &OSTaskSwHook 。 BLX R0
调用钩子函数 OSTaskSwHook()。 POP {R14}
恢复 R14。
LDR R0, =OSPrioCur
将当前正在运行的任务的优先级OSPrioCur ,一个无符号8位的变量地址赋给R0,即:R0 = &OSPrioCur。 LDR R1, =OSPrioHighRdy
将具有最高优先级别的就绪任务的优先级OSPrioHighRdy,一个无符号8位的变量地址赋给R1,即:R1 = &OSPrioHighRdy。 LDRB R2, [R1]
将R1地址中的值赋给R2,即:R2 = *R1 (R2 = OSPrioHighRdy)。 STRB R2, [R0]
将R2寄存器取8位字节数据到R0地址中,即:*R0 = R2 (OSPrioCur = OSPrioHighRdy)。至此,设置当前优先级为最高优先级就绪任务的优先级。
LDR R0, =OSTCBCur
取指向正在运行任务控制块的指针的绝对地址到R0。 LDR R1, =OSTCBHighRdy
取指向最高级优先级就绪任务控制块的指针的绝对地址到R1。 LDR R2, [R1]
将指向最高级优先级就绪任务控制块的指针赋给R2。 STR R2, [R0]
将最高级优先级就绪任务控制块赋给指向正在运行任务控制块的指针,即设置当前任务控制块指针为最高优先级的任务。
LDR R0, [R2]
R0 是新的 SP。 LDM R0, {R4-R11}
从新的栈恢复 R4-R11。 ADDS R0, R0, #0x20
MSR PSP, R0
PSP=R0,用新的栈 SP 加载 PSP。 ORR LR, LR, #0x04
确保 LR 位 2 为 1,返回后使用进程堆栈。因为在中断处理函数中使用的是 MSP,所以在返回任务后必须使用 PSP。 CPSIE I
开中断。 BX LR
返回。