写在前面:
由于时间的不足与学习的碎片化,写博客变得有些奢侈。
但是对于记录学习(忘了以后能快速复习)的渴望一天天变得强烈。
既然如此
不如以天为单位,以时间为顺序,仅仅将博客当做一个知识学习的目录,记录笔者认为最通俗、最有帮助的资料,并尽量总结几句话指明本质,以便于日后搜索起来更加容易。
标题的结构如下:“类型”:“知识点”——“简短的解释”
部分内容由于保密协议无法上传。
点击此处进入学习日记的总目录
2024.03.03
- 十、UCOSIII:常用汇编指令
- 十一、UCOSIII:OS系统初始化
- 十二、UCOSIII:启动系统
- 十三、UCOSIII:任务切换
- 1、PendSV异常
- 2、PendSV异常服务函数
十、UCOSIII:常用汇编指令
指令名称 | 作用 |
---|---|
EQU | 给数字常量取一个符号名,相当于C语言中的define |
AREA | 汇编一个新的代码段或者数据段 |
SPACE | 分配内存空间 |
PRESERVE8 | 当前文件栈需按照8字节对齐 |
EXPORT | 声明一个标号具有全局属性,可被外部的文件使用 |
DCD | 以字为单位分配内存,要求4字节对齐,并要求初始化这些内存 |
PROC | 定义子程序,与ENDP成对使用,表示子程序结束 |
WEAK | 弱定义,如果外部文件声明了一个标号,则优先使用外部文件定义的标号,如果外部文件没有定义也不出错。要注意的是:这个不是ARM的指令,是编译器的,这里放在一起只是为了方便。 |
IMPORT | 声明标号来自外部文件,跟C语言中的EXTERN关键字类似 |
B | 跳转到一个标号 |
ALIGN | 编译器对指令或者数据的存放地址进行对齐,一般需要跟一个立即数,缺省表示4字节对齐。要注意的是:这个不是ARM的指令,是编译器的,这里放在一起只是为了方便。 |
END | 到达文件的末尾,文件结束 |
IF,ELSE,ENDIF | 汇编条件分支语句,跟C语言的if else类似 |
MRS | 加载特殊功能寄存器的值到通用寄存器 |
MSR | 存储通用寄存器的值到特殊功能寄存器 |
CBZ | 比较,如果结果为0 就转移 |
CBNZ | 比较,如果结果非0 就转移 |
LDR | 从存储器中加载字到一个寄存器中 |
LDR[伪指令] | 加一个立即数或者一个地址值到一个寄存器。举例:LDR |
LDRH | 从存储器中加载半字到一个寄存器中 |
LDRB | 从存储器中加载字节到一个寄存器中 |
STR | 把一个寄存器按字存储到存储器中 |
STRH | 把一个寄存器存器的低半字存储到存储器中 |
STRB | 把一个寄存器的低字节存储到存储器中 |
LDMIA | 加载多个字,并且在加载后自增基址寄存器 |
STMIA | 存储多个字,并且在存储后自增基址寄存器 |
ORR | 按位或 |
BX | 直接跳转到由寄存器给定的地址 |
BL | 跳转到标号对应的地址,并且把跳转前的下条指令地址保存到LR |
BLX | 跳转到由寄存器REG给出的的地址,并根据REG的LSB切换处理器状态,还要把转移前的下条指令地址保存到LR。ARM(LSB=0),Thumb(LSB=1)。CM3 只在Thumb中运行,就必须保证reg 的LSB=1,否则一个fault 打过来 |
为了快速地开关中断, CM3 专门设置了一条 CPS 指令,有 4 种用法
PRIMASK和FAULTMAST是CM3里面三个中断屏蔽寄存器中的两个,还有一个是BASEPRI, 有关这三个寄存器的详细用法见表
名字 | 功能描述 |
---|---|
PRIMASK | 这是个只有单一比特的寄存器。在它被置1后,就关掉所有可屏蔽的异常,只剩下NMI和硬FAULT可以响应。它的缺省值是0,表示没有关中断。 |
FAULTMASK | 这是个只有1个位的寄存器。当它置1时,只有NMI才能响应,所有其他的异常,甚至是硬FAULT,也通通闭嘴。它的缺省值也是0,表示没有关异常。 |
BASEPRI | 这个寄存器最多有9位(由表达优先级的位数决定)。它定义了被屏蔽优先级的阈值。当它被设成 某个值后,所有优先级号大于等于此值的中断都被关(优先级号越大,优先级越低)。但若被设成0,则不关闭任何中断,0也是缺省值。 |
十一、UCOSIII:OS系统初始化
OS系统初始化一般是在硬件初始化完成之后来做的,主要做的工作就是初始化μC/OS-III中定义的全局变量。
OSInit()函数在文件 os_core.c中定义
/* RTOS初始化
** 初始化全局变量
*/
void OSInit (OS_ERR *p_err)
{
OSRunning = OS_STATE_OS_STOPPED;
OSTCBCurPtr = (OS_TCB *)0;
OSTCBHighRdyPtr = (OS_TCB *)0;
OS_RdyListInit();
*p_err = OS_ERR_NONE;
}
- OSRunning = OS_STATE_OS_STOPPED;
系统用一个全局变量OSRunning来指示系统的运行状态, 刚开始系统初始化的时候,默认为停止状态,即OS_STATE_OS_STOPPED。
- OSTCBCurPtr = (OS_TCB *)0;
全局变量OSTCBCurPtr是系统用于指向当前正在运行的任务的TCB指针,在任务切换的时候用得到。 - OSTCBHighRdyPtr = (OS_TCB *)0;
全局变量OSTCBHighRdyPtr用于指向就绪任务中优先级最高的任务的TCB,在任务切换的时候用得到。 此处暂时不支持优先级,则用于指向第一个运行的任务的TCB。 - OS_RdyListInit();
OS_RdyListInit()用于初始化全局变量OSRdyList[],即初始化就绪列表。 OS_RdyListInit()在os_core.c文件中定义 - *p_err = OS_ERR_NONE;
代码运行到这里表示没有错误,即OS_ERR_NONE。 - 全局变量OSTCBCurPtr和OSTCBHighRdyPtr均在os.h中定义
十二、UCOSIII:启动系统
任务创建好,系统初始化完毕之后,就可以开始启动系统了。系统启动函数OSStart()在os_core.c中定义
-
if ( OSRunning == OS_STATE_OS_STOPPED )
系统是第一次启动的话,if 肯定为真,则继续往下运行。 -
OSTCBHighRdyPtr = OSRdyList[0].HeadPtr;
OSTCBHIghRdyPtr 指向第一个要运行的任务的TCB。 因为暂时不支持优先级,所以系统启动时先手动指定第一个要运行的任务。 -
OSStartHighRdy();
OSStartHighRdy()用于启动任务切换,即配置PendSV的优先级为最低,然后触发PendSV异常, 在PendSV异常服务函数中进行任务切换。该函数不再返回,在文件os_cpu_a.s中定义,由汇编语言编写
-
LDR R0, = NVIC_SYSPRI14
配置PendSV的优先级为0XFF,即最低。在μC/OS-III中, 上下文切换是在PendSV异常服务程序中执行的,配置PendSV的优先级为最低,从而消灭了在中断服务程序中执行上下文切换的可能。 -
MOVS R0, #0
设置PSP的值为0,开始第一个任务切换。在任务中, 使用的栈指针都是PSP,后面如果判断出PSP为0,则表示第一次任务切换。 -
LDR R0, =NVIC_INT_CTRL
触发PendSV异常,如果中断启用且有编写PendSV异常服务函数的话, 则内核会响应PendSV异常,去执行PendSV异常服务函数。 -
CPSIE I
开中断,因为有些用户在main()函数开始会先关掉中断, 等全部初始化完成后,在启动OS的时候才开中断。
汇编常用指令作用如下
十三、UCOSIII:任务切换
1、PendSV异常
当调用OSStartHighRdy()函数,触发PendSV异常后,就需要编写PendSV异常服务函数,然后在里面进行任务切换。
PendSV异常服务函数名称必须与启动文件里面向量表中PendSV的向量名一致, 如果不一致则内核是响应不了用户编写的PendSV异常服务函数的,只响应启动文件里面默认的PendSV异常服务函数。
启动文件里面为每个异常都编写好默认的异常服务函数,函数体都是一个死循环,当你发现代码跳转到这些启动文件里面默认的异常服务函数的时候, 就要检查下异常函数名称是否写错了,没有跟向量表里面的一致。
- PendSV异常服务中主要完成两个工作,
一是保存上文,即保存当前正在运行的任务的环境参数;
二是切换下文, 即把下一个需要运行的任务的环境参数从任务栈中加载到CPU寄存器,从而实现任务的切换。
PendSV异常服务中用到了OSTCBCurPtr和OSTCBHighRdyPtr这两个全局变量,这两个全局变量在os.h中定义, 要想在汇编文件os_cpu_a.s中使用,必须将这两个全局变量导入到os_cpu_a.s中
-
IMPORT OSTCBCurPtr
IMPORT OSTCBHighRdyPtr
使用IMPORT关键字将os.h中的OSTCBCurPtr和OSTCBHighRdyPtr这两个全局变量导入到该汇编文件, 从而该汇编文件可以使用这两个变量。如果是函数也可以使用IMPORT导入的方法。 -
EXPORT OSStartHighRdy
EXPORT PendSV_Handler
使用EXPORT关键字导出该汇编文件里面的OSStartHighRdy和PendSV_Handler这两个函数, 让外部文件可见。除了使用EXPORT导出外,还要在 某个C的头文件里面声明下这两个函数(在μC/OS-III中是在os_cpu.h中声明),这样才可以在C文件里面调用这两个函数。
2、PendSV异常服务函数
;********************************************************************************************************
; PendSVHandler异常
;********************************************************************************************************
PendSV_Handler
; 任务的保存,即把CPU寄存器的值存储到任务的堆栈中
CPSID I ; 关中断,NMI和HardFault除外,防止上下文切换被中断
MRS R0, PSP ; 将psp的值加载到R0
CBZ R0, OS_CPU_PendSVHandler_nosave ; 判断R0,如果值为0则跳转到OS_CPU_PendSVHandler_nosave
; 进行第一次任务切换的时候,R0肯定为0
;-----------------------一、保存上文-----------------------------
; 任务的切换,即把下一个要运行的任务的栈内容加载到CPU寄存器中
; 在进入PendSV异常的时候,当前CPU的xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0会自动存储到当前任务堆栈,同时递减PSP的值
;--------------------------------------------------------------
STMDB R0!, {R4-R11} ; 手动存储CPU寄存器R4-R11的值到当前任务的堆栈
LDR R1, = OSTCBCurPtr ; 加载 OSTCBCurPtr 指针的地址到R1,这里LDR属于伪指令
LDR R1, [R1] ; 加载 OSTCBCurPtr 指针到R1,这里LDR属于ARM指令
STR R0, [R1] ; 存储R0的值到 OSTCBCurPtr->OSTCBStkPtr,这个时候R0存的是任务空闲栈的栈顶
;-----------------------二、切换下文-----------------------------
; 实现 OSTCBCurPtr = OSTCBHighRdyPtr
; 把下一个要运行的任务的栈内容加载到CPU寄存器中
; 任务的切换,即把下一个要运行的任务的堆栈内容加载到CPU寄存器中
;--------------------------------------------------------------
OS_CPU_PendSVHandler_nosave
; OSTCBCurPtr = OSTCBHighRdyPtr;
LDR R0, = OSTCBCurPtr ; 加载 OSTCBCurPtr 指针的地址到R0,这里LDR属于伪指令
LDR R1, = OSTCBHighRdyPtr ; 加载 OSTCBHighRdyPtr 指针的地址到R1,这里LDR属于伪指令
LDR R2, [R1] ; 加载 OSTCBHighRdyPtr 指针到R2,这里LDR属于ARM指令
STR R2, [R0] ; 存储 OSTCBHighRdyPtr 到 OSTCBCurPtr
LDR R0, [R2] ; 加载 OSTCBHighRdyPtr 到 R0
LDMIA R0!, {R4-R11} ; 加载需要手动保存的信息到CPU寄存器R4-R11
MSR PSP, R0 ; 更新PSP的值,这个时候PSP指向下一个要执行的任务的堆栈的栈底(这个栈底已经加上刚刚手动加载到CPU寄存器R4-R11的偏移)
ORR LR, LR, #0x04 ; 确保异常返回使用的堆栈指针是PSP,即LR寄存器的位2要为1
CPSIE I ; 开中断
BX LR ; 异常返回,这个时候任务堆栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0(任务的形参)
; 同时PSP的值也将更新,即指向任务堆栈的栈顶。在STM32中,堆栈是由高地址向低地址生长的。
NOP ; 为了汇编指令对齐,不然会有警告
END ; 汇编文件结束
-
CPSID I
关中断,NMI和HardFault除外,防止上下文切换被中断。 在上下文切换完毕之后,会重新开中断。 -
MRS R0, PSP
将PSP的值加载到R0寄存器。MRS是ARM 32位数据加载指令, 功能是加载特殊功能寄存器的值到通用寄存器。 -
CBZ R0, OS_CPU_PendSVHandler_nosave
判断R0,如果值为0则跳转到OS_CPU_PendSVHandler_nosave。进行第一次任务切换的时候, PSP在OSStartHighRdy初始化为0,所以这个时候R0肯定为0,则跳转到OS_CPU_PendSVHandler_nosave。 CBZ是ARM16位转移指令,用于比较,结果为0则跳转。 -
STMDB R0!, {R4-R11}
手动存储CPU寄存器R4-R11的值到当前任务的栈。
当异常发生,进入PendSV异常服务函数的时候, 当前CPU寄存器xPSR,PC(任务入口地址),R14,R12,R3,R2,R1,R0会自动存储到当前的任务栈,同时递减PSP的值, 这个时候当前任务的栈空间分布具体见 进入PendSV异常时当前任务的栈空间分布图。
-
当执行STMDB R0!, {R4-R11}代码后, 当前任务的栈空间分布具体见 当前任务执行完上文保存时的栈空间分布图
-
STR R0, [R1]
存储R0的值到OSTCBCurPtr->OSTCBStkPtr, 这个时候R0存的是任务空闲栈的栈顶。到了这里,上文的保存就总算完成。
这个时候当前任务的栈空间分布和栈指针指向具体见 当前任务执行完上文保存时的栈空间分布和StkPtr指向图
-
OS_CPU_PendSVHandler_nosave
当第一次任务切换的时候,会跳转到这里运行。当执行过一次任务切换之后, 则顺序执行到这里。这个标号以后的内容属于下文切换。 -
LDR R0, = OSTCBCurPtr
加载 OSTCBCurPtr 指针的地址到R0。在ARM汇编中,操作变量都属于间接操作, 即要先获取到这个变量的地址。这里LDR属于伪指令,不是ARM指令。举例:LDR Rd, = label,如果label是立即数, 那Rd等于立即数,如果label是一个标识符,比如指针,那存到Rd的就是label这个标识符的地址。 -
STR R2, [R0]
存储 OSTCBHighRdyPtr 到 OSTCBCurPtr, 实现下一个要运行的任务的TCB存储到OSTCBCurPtr。 -
LDR R0, [R2]
加载 OSTCBHighRdyPtr 到 R0。TCB中第一个成员是栈指针StkPtr, 所以这个时候R0等于StkPtr,后续操作任务栈都是通过操作R0来实现,不需要操作StkPtr。 -
LDMIA R0!, {R4-R11}
LDMIA中的I是increase的缩写,A是after的缩小, R0后面的感叹号“!”表示会自动调节R0里面存的指针。
将任务栈中需要手动加载的内容加载到CPU寄存器R4-R11,同时会递增R0, 让R0指向空闲栈的栈顶,栈空间的分布情况具体见 任务创建成功后栈空间的分布图。
-
当任务被创建的时候,任务的栈会被初始化, 初始化的流程是:
先让栈指针StkPtr指向栈顶, 然后从栈顶开始依次存储 异常退出时会自动加载到CPU寄存器 的值和 需要手动加载到CPU寄存器 的值, 具体代码实现见OSTaskStkInit()函数。
当把需要手动加载到CPU的栈内容加载完毕之后,栈空间的分布图和栈指针指向具体见图 手动加载栈内容到CPU寄存器后的栈空间分布图 , 注意这个时候StkPtr不变,变的是R0。
-
MSR PSP, R0
更新PSP的值,这个时候PSP与图3‑4中R0的指向一致。 -
ORR LR, LR, #0x04
设置LR寄存器的位2为1,确保异常退出时使用的栈指针是PSP。
当异常退出后,就切换到就绪任务中优先级最高的任务继续运行。 -
CPSIE I
开中断。上下文切换已经完成了四分之三,剩下的就是异常退出时自动保存的部分。 -
BX LR
异常返回,这个时候任务栈中的剩下内容将会自动加载到xPSR,PC(任务入口地址), R14,R12,R3,R2,R1,R0(任务的形参)这些寄存器。同时PSP的值也将更新,即指向任务栈的栈顶。
这样就切换到了新的任务。 这个时候栈空间的分布具体见 刚切换完成即将运行的任务的栈空间分布和栈指针指向图