3.uCOS2源码分析2-内核部分

转自 https://edu.csdn.net/lecturer/505 朱老师物联网大讲堂
3.uCOS2源码分析2-内核部分

第一部分、章节目录
4.3.1.uCOS2内核部分源码概述
4.3.2.uC-LIB部分源码分析1
4.3.3.uC-LIB部分源码分析2
4.3.4.uC-LIB部分源码分析3
4.3.5.uC-LIB部分源码分析4
4.3.6.uC-CPU部分源码分析1
4.3.7.uC-CPU部分源码分析2
4.3.8.uC-CPU部分源码分析3
4.3.9.uC-CPU部分源码分析4
4.3.10.uC-CPU部分源码分析5
4.3.11.uCOS-II部分源码分析1
4.3.12.uCOS-II部分源码分析2
4.3.13.uCOS-II部分源码分析3
4.3.14.uCOS-II部分源码分析4
4.3.15.uCOS-II部分源码分析5
4.3.16.uCOS-II部分源码分析6
4.3.17.uCOS-II部分源码分析7
4.3.18.uCOS-II部分源码分析8
4.3.19.uCOS-II部分源码分析9
4.3.20.uCOS2的就绪表设计1
4.3.21.uCOS2的就绪表设计2
4.3.22.uCOS2的任务切换详解1
4.3.23.uCOS2的任务切换详解2
4.3.24.uCOS2的任务切换详解3
4.3.25.uCOS2的任务切换详解4
4.3.26.uCOS2的任务切换详解5

第二部分、章节介绍
4.3.1.uCOS2内核部分源码概述
本节对uCOS2源码的内核部分进行概述,三个文件夹部分各自的作用,汇编和C文件的结合等进行讲述,为接着继续讲源码细节打下基础。
4.3.2.uC-LIB部分源码分析1
本节开始分析uCOS-LIB部分的源码,主要分析了lib_def.h文件中的众多宏定义。
4.3.3.uC-LIB部分源码分析2
本节接着分析uC-LIB部分的源码,主要分析了lib_mem.c中内存池有关的源码。
4.3.4.uC-LIB部分源码分析3
本节接着分析uC-LIB部分的源码,主要分析了lib_mem.c中内存池有关的源码。
4.3.5.uC-LIB部分源码分析4
本节接着分析uC-LIB部分的源码,主要分析了lib_str.c中和字符串操作有关库函数源码。
4.3.6.uC-CPU部分源码分析1
本节开始分析uC-CPU部分源码,主要是cpu_def.h等。
4.3.7.uC-CPU部分源码分析2
本节继续分析uC-CPU部分源码,主要分析了进入和退出临界区的宏的实现等源码。
4.3.8.uC-CPU部分源码分析3
本节继续分析uC-CPU部分源码,主要分析了配合临界区宏的cpu_sr变量定义的代码部分。
4.3.9.uC-CPU部分源码分析4
本节继续分析uC-CPU部分源码,主要分析了汇编实现的一些CPU操作,如关中断开中断等。
4.3.10.uC-CPU部分源码分析5
本节继续分析uC-CPU部分源码,主要分析了interrupt和exception的差异和联系等知识点。
4.3.11.uCOS-II部分源码分析1
本节开始uCOS-II部分源码的分析,详细讲了三种临界区实现方式的差异,从理论上让大家对临界区保护的实现机制有更深入理解。
4.3.12.uCOS-II部分源码分析2
本节接着讲解uCOS-II部分源码,主要讲了4种栈和ARM的满减栈,OS的上下文切换等概念。
4.3.13.uCOS-II部分源码分析3
本节接着讲解uCOS-II部分源码,主要讲解os_cpu_c.c文件中的各函数。
4.3.14.uCOS-II部分源码分析4
本节接着讲解uCOS-II部分源码,主要讲解os_task_stk_init和systick_handler等函数。
4.3.15.uCOS-II部分源码分析5
本节接着讲解uCOS-II部分源码,主要讲解systick_handler和systick_init等函数。
4.3.16.uCOS-II部分源码分析6
本节开始分析OSTimeTick函数,主要讲解了OS_TCB结构中前一部分元素。
4.3.17.uCOS-II部分源码分析7
本节接着分析OSTimeTick函数,主要讲解了OS_TCB结构中后一部分元素。
4.3.18.uCOS-II部分源码分析8
本节接着分析OSTimeTick函数,主要讲解了任务优先级相关的一些宏定义和函数实现。
4.3.19.uCOS-II部分源码分析9
本节接着分析OSTimeTick函数,主要讲解了OSTCBDly和任务延时挂起的实现机制。
4.3.20.uCOS2的就绪表设计1
本节着重分析了uCOS2的就绪表的设计和实现思路。
4.3.21.uCOS2的就绪表设计2
本节接上节讲解uCOS2的就绪表,详细分析了就绪表操作的位运算细节和相关代码的浏览理解。
4.3.22.uCOS2的任务切换详解1
本节讲解uCOS2用来实现任务切换的汇编代码,为了细致彻底的分析我们需要延续几节来完成。
4.3.23.uCOS2的任务切换详解2
本节讲解Cortex-M3权威指南中关于SVC和PendSV的部分,并向大家详细解释其中的技术细节。
4.3.24.uCOS2的任务切换详解3
本节接上节继续看PendSV部分的技术细节。
4.3.25.uCOS2的任务切换详解4
本节开始分析汇编代码,主要是OSStartHighRdy和OSCtxSw和OSIntCtxSw这几个函数。
4.3.26.uCOS2的任务切换详解5
本节主要分析了OS_CPU_PendSVHandler函数的实现,和前面讲过的PendSV部分相互印证很好理解。

第三部分、随堂记录
4.3.1.uCOS2内核部分源码概述
4.3.1.1、uC-CPU
4.3.1.2、uC-LIB
4.3.1.3、uCOS-II
4.3.1.4、注意点
(1).asm的汇编文件
(2)学习技巧:一边分析一边写注解
(3)一个有意思的疑问:从简单(边角)到难(核心)还是从难到简单

4.3.2_3.uC-LIB部分源码分析1_2
uC-LIB/lib_def.h 文件中定义了很多宏,这些宏要么是对一些常数的宏定义(譬如0 1 0xff等),要么是对一些可能用到的和环境有关的数值的定义(譬如一个月多少天,一天多少小时,一小时多少分等等)

LIB_MEM_CFG_ARG_CHK_EXT_EN 类似于这种宏在uCOS中叫做配置宏,这些宏的作用是用来配置uCOS内核的可选项目。uCOS本身设计的时候设计了很多属性,但是这些属性是可选使能或者不使能的,这样我们就可以通过一些配置宏来配置项目,在编译的时候(预处理)由编译器(预处理器)来帮我们把uCOS内核的这些特性加上或者去掉。

app/app_cfg.h文件,这个文件是设计用来在app层去配置ucos的,按照ucos的设计,我们需要在这个文件中去用宏定义来配置ucos的很多属性。

4.3.4.uC-LIB部分源码分析3
(1)abcadefahg
(2)uCOS2里自己定义并实现了一套字符串处理函数,而没有使用编译器自带的C库的字符串处理函数。
(3)MicroLIB是MDK环境中提供的一套精简版的C库。uCOS2没有使用编译环境中提供的C库中的字符串处理函数,而是自己提供了一套,这样可以让uCOS2更加独立,不那么依赖于编译环境。

u32 a = 123; // a是一个u32类型的数字,以16进制的形式存在于内存中的
我们需求:将u32类型的a转成一个字符串"123"
"123"这个字符串是一个字符串,包含4个字节,第1个字节是字符’1’,第4个字符是’\0’。
实现:根据u32类型的a,通过除法和取余两种操作来将u32类型的a分成几个数字(也就是将123分成1、2、3三个数字),然后再将1、2、3这种数字转成字符,然后拼起来成为字符串。
数字1的ASCII码是1+‘0’

4.3.5.uC-LIB部分源码分析4
汇编文件lib_mem_a.asm的解析
(1)该文件实现了一个函数mem_cpy,这个函数是用来进行内存的拷贝的。内存的拷贝是在RTOS中非常重要,内核实现也会需要用到内存拷贝,所以效率要求比较高,所以用汇编来实现。
(2)为什么uCOS2中有些东西要用汇编来实现?2个原因:一个是效率更高,另一个是不得不用汇编因为C实现不了。
(3)EXPORT是ARM汇编的一个关键字,用来从ARM汇编文件中导出一个符号,导出后这个符号就可以在其他C语言源文件或者汇编源文件中引用了,作用非常类似于C中的extern。

ATPCS

4.3.6.uC-CPU部分源码分析1
(1)临界区的概念,结合我刚才讲的,自己再去网上搜索相关的一些文章来看去理解。
(2)临界区相关的宏
CPU_CRITICAL_METHOD_NONE 没有方法解决临界区
CPU_CRITICAL_METHOD_INT_DIS_EN interrupt disable enable,中断关闭和使能来实现
CPU_CRITICAL_METHOD_STATUS_STK 中断状态保存到栈中
CPU_CRITICAL_METHOD_STATUS_LOCAL 中断状态保存在本地的局部变量

4.3.7.uC-CPU部分源码分析2
(1)CPU_CRITICAL_ENTER()宏用来进入临界区,实现其实就是函数内部关闭中断,然后备份CPSR到局部变量cpu_sr中即可。
(2)CPU_CRITICAL_EXIT()宏用来退出临界区,
(3)ARM中的总中断开关在CPSR寄存器中(bit7,记作I位),I位为0时总中断开关开,I位为1时总中断开关关闭。CPSID I指令其实就是通过将I位置1来实现中断关闭的。

4.3.8_9.uC-CPU部分源码分析3_4
(1)下面三行在宏定义配置判断成功时,其实相当于定义了一个函数内的局部变量,类型是CPU_SR,变量名是cpu_sr。定义这个局部变量的作用,是用来配合后面的2个宏。
#if (CPU_CFG_CRITICAL_METHOD == CPU_CRITICAL_METHOD_STATUS_LOCAL)
CPU_SR cpu_sr;
#endif
(2)RTOS中实现临界区常用的有三种方法,uCOS2本来是想要三种方式都支持的,所以在宏定义的时候定义了三种,但是实际上方式1和2都不靠谱,实际全是用的3,1和2名存实亡。所以CPU_CRITICAL_ENTER()和CPU_CRITICAL_EXIT()中直接使用了方式3,但是方式3的这2个宏依赖于一个名叫cpu_sr的局部变量。如果没有这个局部变量则会编译时报错,所以只要函数用到了这2个宏,就得在函数前面加上(1)中的三行。
总结:uC-CPU/ARM-Cortex-M3/cpu_c.c中主要是一些函数定义,这些函数包括位带访问区(SRAM的位带访问区和外设寄存器位带访问区)的位清零和置位函数;中断源的关闭和打开函数;中断源的各个中断的优先级的设置和获取函数。这些函数的实现很多都用到了NVIC模块的控制寄存器的位定义。

4.3.10.uC-CPU部分源码分析5
(1)interrupt中断 和 exception异常,这两个词的区别?
异常指的是CPU的非正常状态,譬如复位、指令错误、中断(IRQ、FIQ),而中断是一种特殊的异常。
中断是一种异常,而异常不一定非是中断。

4.3.11_12.uCOS-II部分源码分析1_2
(1)uCOS-II/ports/os_cpu.h中写明了三种实现临界区的方式及其特点
第一种:简单的禁止中断/打开中断指令。好处是实现简单,坏处是退出临界区后中断肯定是被打开的,不管原来进入临界区之前是关的还是开的。没有实现
第二种:在进入临界区之前会保存中断状态,然后在退出临界区时会恢复原来的状态。简单说:就是原来是开的临界区后还是开,原来是关的临界区后还是关。没有实现
第三种:在要使用临界区的函数中定义一个局部变量名叫cpu_sr,然后在进入临界区前将CPSR保存到cpu_sr中,然后在退出临界区时将cpu_sr恢复到CPU的CPSR中,以此来实现临界区中断状态保存和回复。
(2)栈有四种:满减栈、满增栈、空减栈、空增栈
满栈:栈指针SP始终指向栈中最后一个元素,当我们要再次入栈时要先移动SP再放入。出栈时直接先出栈一个元素然后SP再移动。
空栈:栈指针SP时钟指向栈中最后一个元素后面的第一个空位,入栈时直接入栈然后SP再去移动。出栈时要先移动SP再去出栈。
增栈:栈底在低内存地址处,入栈时栈指针SP向内存地址较大的方向走
减栈:栈底在高内存地址处,入栈时栈指针SP向内存地址较小的方向走
(3)(隐约记得)ARM的ATPCS中规定,ARM应该用满减栈。但是可以确定,你实际中遇到的ARM的平台都是满减栈。
(4)OS_TASK_SW,OS代表操作系统,TASK翻译为任务(uCOS中任务就相当于linux windwos中的进程、线程等概念),SW是switch,切换。OS_TASK_SW就是OS的任务切换。
(5)OSCtxSw,OS Context Switch,翻译为:OS上下文切换,或者OS任务切换。任务在切换时必须要保存和回复一些任务的上下文环境,这里的上下文环境主要指R0-R15等寄存器,任务栈(每个任务有一个独立的任务栈,栈内存供这个任务唯一的专用,栈用来维持任务自身的局部变量等)。

4.3.13.uCOS-II部分源码分析3
4.3.13.1、钩子函数(hook)
(1)在很多OS中都有,譬如windows编程中就有这个概念,不止uCOS2有。
(2)钩子函数是这样一种函数,这个函数事先并没有被实现,只有个名字(外壳,空函数名),但是被在该调用的地方调用了。我们可以通过向钩子函数内部填充函数体代码,来让整个体系在运行时执行一些我们自己写的代码。
(3)和钩子函数很像的,原理相同的还有个名字,叫回调函数(recall)。
(4)面向对象的三大特征之一:多态,和钩子函数/回调函数有一定关联。
4.3.13.2、任务栈
(1)C语言的函数运行是需要栈来支持的,因为局部变量就是栈这种数据结构来实现的。
(2)到底什么是栈?OS通过栈来向C语言提供一种内存管理方式,OS通过栈指针SP来管理栈内存的。
(3)在单片机裸机程序中,整个程序使用一个栈。
(4)OS中不一样,因为OS中有任务的概念,有不同的任务需要切换,所以不同的任务不能共用栈,每个任务都要有自己的私有栈。
(5)操作系统的任务的概念,其实就是一个独立的函数。
(6)OSTaskStkInit的作用就是对任务的栈内存进行初始化,我们每创建一个任务,都应该调用这个函数对任务的私有栈进行初始化,之后再去使用。

4.3.14_15.uCOS-II部分源码分析4_5
(1)OS_CPU_SysTickHandler是systick的中断处理程序,systick是OS的时钟节拍。systick每中断一次,OS就运行一个节拍。systcik的中断处理程序中核心是OSTimeTick和OSIntExit这两个函数,在这两个函数中封装了很多节拍处理的工作(主要是任务状态的核实,任务的切换等,譬如之前被挂起的一个更高优先级的任务突然变为就绪态,这时候我们就应该调用任务切换函数进行一个任务切换动作)
(2)OS_CPU_SysTickInit这个是systick的初始化函数,他决定了systick中断被使能从而能够产生中断,还决定了systick隔多久中断一次。通过代码分析,确定systick是1ms。

4.3.16.uCOS-II部分源码分析6
4.3.16.1、OSTimeTick
(1)uC/OS-View是uCOS2的一个可选配件(内核之外的外围功能模块),这东西现在已经废弃了,被uCProbe给替代了,这2个东西都是用来实时监测并调试uCOS内核使用的。
(2)OS_TCB,OS_TaskControlBlock,操作系统的任务管理块,其实就是一个封装好的数据结构,用来管理uCOS2的任务相关的所有数据。

4.3.17.uCOS-II部分源码分析7
(1)uCOS2中时间单位是tick的个数,也就是说在OS中记录时间的标准是发生了多少次systick。

4.3.18.uCOS-II部分源码分析8
(1)uCOS2中定义了一个全局变量OSTCBList,这个全局变量作为链表头指针,指向uCOS2维护的TCB双向链表。uCOS2中每一个任务都有一个TCB,这些TCB通过内置的2个指针(OSTCBNext和OSTCBPrev)构建了一个双向链表。这样我们整个uCOS2中所有代码任何位置都可以通过访问全局变量OSTCBList来找到所有的任务的TCB。
(2)uCOS2自己内部定义好了2个任务(及其优先级),这2个任务分别叫:状态任务、空任务。这2个任务的优先级分别是最低优先级和次低优先级。
#define OS_TASK_STAT_PRIO (OS_LOWEST_PRIO - 1)
#define OS_TASK_IDLE_PRIO (OS_LOWEST_PRIO)
这2个任务的作用:状态任务(30)的作用就是来统计OS的各种状态参数(譬如CPU使用率、内存使用率等);空任务(31)的作用就是什么都不干,空。

4.3.19.uCOS-II部分源码分析9
4.3.19.1、OSTCBDly元素的作用
(1)TCB的这个元素OSTCBDly涉及到2个功能的实现:一个是任务延时机制,另一个是等待事件超时机制。
(2)当前任务在自己当前不适合运行时(譬如可能要等待一些IO,譬如当前任务要继续执行需要某个条件但是这个条件又不具备无法继续),我们就需要调用delay函数(OSTimeDly或者OSTimeDlyHMSM)来延时相应的时间(譬如10ms,譬如2s),这个delay函数的实现就和OSTCBDly变量有关(delay函数中将要延时的时间换算成tick的数量,然后写入OSTCBDly中,并且将当前任务设置为非就绪态)。具体的实现就是在每一次systick的hanlder中在OSTimeTick函数中去将OSTCBDly减1,然后看是否等于0,如果不等于0说明delay时间还没到,如果等于0说明时间到了。时间到了就要将当前任务恢复为就绪态再调度(如果当前任务是优先级最高的就绪态任务那么就会被调度执行)。

4.3.20_21.uCOS2的就绪表设计1_2
4.3.20.1、当前面临什么问题
(1)什么是就绪表
(2)就绪表的难点和关键点
4.3.20.2、就绪表可以怎样设计
(1)最简单:一个数组元素表示一个任务状态
(2)省内存稍复杂低效率:一个bit表示一个任务状态
4.3.20.3、uCOS2的就绪表设计
(1)一个bit表示一个任务状态,构成8*8的矩阵
(2)纵向OSRdyGrp一个bit囊括一个组中8个任务
(3)横向OSRdyTbl[8]中每一个元素的8个bit分别对应组中8个任务
(4)0表示非就绪态、1表示就绪态
(5)查表时先查OSRdyGrp,再根据结果查有就绪态任务的组。实际上性能提升点在于省去了对没有就绪态的组中8个任务的遍历过程。
(6)这就是所谓算法,这就是所谓数据结构

备注:uCOS2使用的是抢占式的实时调度算法,调度原则是:在每一个systick的hanlder中,都要去判断下64个任务中所有处于就绪态的任务里,谁的优先级最高,就执行谁就行了。

自己设计的就绪表1:
u8 rdyTbl[64]; // 最简单的数组类型的就绪表,每一个元素表示一个任务的状态,0表示非就绪,1表示就绪态。因为优先级和任务是一一对应的,所以数组下标可以和优先级直接对应。
操作举例:
if (rdyTbl[15] == 1) // 判断优先级为15的任务是否处于就绪态
rdyTbl[3] = 1; // 将优先级为3的任务置于就绪态

自己设计的就绪表2:
u8 rdyTbl[8]; // 数组总共是64个位,分成8个字节。每一个位表示一个任务的状态,0表示非就绪,1表示就绪态。这样是可以省内存,但是优先级不能直接作为数组下标来使用了,所以变得复杂了。
操作举例:
if ((rdyTbl[1] & 0x80) == 0x80) // 判断优先级为15的任务是否处于就绪态
rdyTbl[0] |= (1<<3); // 将优先级为3的任务置于就绪态
rdyTbl[1] &= ~(1<<(13%8)); // 将优先级为13的任务置于非就绪态

rdyTbl[n/8] |= (1<<(n%8)); // 将优先级为n的任务置于就绪态
rdyTbl[n/8] &= ~(1<<(n%8)); // 将优先级为13的任务置于非就绪态

uCOS2的就绪表设计:
(1)rdyTbl中一个组中8个任务的就绪态全部是0时,这个组的rdyGrp对应的位才是0;只要有1个1对应的rdyGrp的位就是1.
(2)rdyGrp这个变量也是拆成8个位来使用的,这个变量的值反映了哪个组中是有就绪态任务的。假设rdyGrp的值为0x00,说明64个任务全部处于非就绪态(注意我们根本不需要去查rdyTab就知道了)。假设rdyGrp值为0x04,说明第2组(0x04是0b00000100,bit2为1,对应第2组)有就绪态的任务,其他组根本没有根本不用去查rdyTbl,只需要查第2组的rdyTbl即可。

4.3.22.uCOS2的任务切换详解1
(1)在51单片机中有一个code关键字,作用是让常量被分配在ROM上而不是在RAM上从而节省内存。
(2)在STM32上没有code关键字,那怎么实现常量被分配在ROM上而不是RAM上这个功能?用const关键字
(3)OS_SchedNew函数的作用就是通过查表和运算得到当前OSPrioHighRdy
4.3.22.2、uCOS2的任务切换
(1)所谓任务切换,就是从原来的任务中离开,转去执行新的任务。任务切换的核心是:保存上下文、恢复要去执行的任务的上下文、然后跳转到新任务中执行即可。
(2)我们并不能直接跳转到新任务执行,因此CM3引入了一个PendSV的机制来实现任务跳转。
(3)任务跳转面临的难题,以及PendSV如何解决,参考《Cortex-M3权威指南》第7章异常的最后一节,SVC和PendSV。

4.3.23_24.uCOS2的任务切换详解2_3
4.3.23.1、PendSV讲解
(1)ARM中有2个栈指针(R13),分别叫MSP(main stack pointer主栈指针)和PSP(process stack pointer进程栈指针),设计思路就是OS内核使用MSP,进程(uCOS2中叫任务)使用PSP。也就是说,OS内核和进程使用的栈不同,而且栈指针也都是独立的。这样设计的好处是:OS内核和进程运行切换时SP不用保存,安全。
(2)如何知道当前使用的是PSP还是MSP?其实在CM3设计时就考虑到了,LR寄存器的bit2指示了当前使用的是PSP还是MSP(1表示PSP,0表示MSP)。

4.3.25.uCOS2的任务切换详解4

4.3.26.uCOS2的任务切换详解5

发布了10 篇原创文章 · 获赞 14 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_40083589/article/details/90518756