嵌入式操作系统
0. 嵌入式实时操作系统内核

- 红色部分是重点,要了解整个流程
1. 嵌入式操作系统概述
1.1 嵌入式操作系统概念
-
嵌入式系统是一种专用的计算机系统,以应用为中心,以计算机技术为基础,软硬件可配置,对成本、功耗、可靠性、体积、功能有严格的约束
-
嵌入式操作系统也被称为实时操作系统 (Real-Time Operating System,RTOS)
-
嵌入式操作系统与通用计算机系统的差异:
-
相对于通用计算机系统来说,嵌入式系统是为特定的应用而设计的
-
嵌入式系统具有更高的可靠性和稳定性
-
嵌入式系统的软硬件资源有限,功耗低,集成度高
-
嵌入式系统的软件程序存储在芯片上,开发者通常无法更改
-
1.2 嵌入式操作系统的特点
-
实时性
- 硬实时
- 软实时
-
可确定性
-
并发性--多任务机制
-
高可信性
-
安全性
-
可嵌入性
-
可裁剪性
-
可扩展性
1.3 嵌入式操作系统的主要功能
- 内核基本功能
- 任务管理
- 中断管理
- 时钟管理
- 任务协调/调度
- 内存管理
- 扩展功能 --用于针对各类不同的应用
- 嵌入式网络
- 嵌入式文件系统
- 功耗管理
- 嵌入式数据库
- 流媒体支持
- 用户编程接口
- 嵌入式GUI
2. 任务调度机制
-
调度 是RTOS的核心功能 ,用于确定多任务环境下 ++任务执行的顺序++ 和在获得CPU资源后 ++执行时间长度++
- RTOS提供以下机制来保证调度的正确实施
- 基本调度机制:任务创建、删除等
- 任务协调机制:任务间通信、同步等
- 内存管理机制:为任务分配内存空间
- 事务处理机制 :++事件触发机制++ --中断管理 & ++时间触发机制++--时间管理
- RTOS提供以下机制来保证调度的正确实施
-
任务调度点
-
中断服务程序结束位置
-
运行任务因缺乏资源而被阻塞(如信号量机制里面,资源不够)
-
任务周期的开始或者结束时刻(OSTimeDly来做的周期)
-
高优先级任务就绪时刻(调度由systick心跳这个中断的OSIntExit来触发)
-
2.1 任务相关基本概念
- 任务定义
- 任务是一个程序运行的实体 、资源占用的基本单位 ,也可能是系统调度的基本单元
- 任务特性
- 动态性
- 并发性
- 异步独立性:每个任务各自按相互独立、不可预知的速度运行
- 任务和线程
- ucosII中,一个任务也称作一个线程
- 线程和进程
- 进程是操作系统调度 和资源占用的基本单位
- RTOS基本没有实现多进程 ,只实现了多线程
- 主要区别
- 线程之间是共享地址的
- 进程之间不能相互访问对方的变量 ,一般通过地址保护和虚拟地址来实现
2.2 任务描述
2.2.1 OS_TCB
-
RTOS调度的基本单位是任务 ,并且通过
OS_TCB
来描述一个任务-
OS_TCB
设计
-
-
TCB没有包含任务执行代码,是因为
OSTCBStkPtr
指针指向的是一个stack(任务堆栈),这个stack保存了执行代码的地址 -
一个Task有三个存储结构
- TCB
- 执行函数代码
- Task的Stack
2.2.2 任务运行状态

-
状态定义
- 睡眠(dormant) :任务只以代码形式存在,没有分配TCB和任务堆栈
- 就绪(ready) :任务已经被创建,并被挂载到就绪队列中
- 运行(running) :任务获得CPU的执行权
- 等待(waiting) :正在运行的任务,需要等待一个事件的发生再运行,CPU使用权被剥夺
- 中断服务(iSR) :正在运行的任务一旦受到其他的干扰 就会终止运行,转而执行中断服务程序
-
状态转换
- 睡眠(dormant) --->(taskcreate)---> 就绪态(ready) --->(sched sched_new OSCtxSw)--->运行态(running) --->(OSTimeDly或者suspend) --->等待态(waiting)
- 还有中断服务状态(ISR) 由运行态的时候遇到中断进入,退出的时候记得调用OSIntExit
2.2.3 任务优先级

2.3 创建任务
- ucosII中通过
OSTaskCreate()
和OSTaskCreateExt()
两个函数来创建任务,这里主要讲解OSTaskCreate()
函数 - 任务的创建以及管理本质上就是对于TCB的创建和管理,所以我们先学习RTOS中对于TCB有关的设计
2.3.1 任务块TCB有关设计
c
OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS];
OS_EXT OS_TCB *OSTCBCur; // Pointer to currently running TCB
OS_EXT OS_TCB *OSTCBFreeList; // Pointer to list of free TCBs
OS_EXT OS_TCB *OSTCBHighRdy; // Pointer to highest priority TCB R-to-R
OS_EXT OS_TCB *OSTCBList; // Pointer to doubly linked list of TCBs
OS_EXT OS_TCB *OSTCBPrioTbl[OS_LOWEST_PRIO + 1u]; // Table of pointers to created TCBs
OS_EXT OS_TCB OSTCBTbl[OS_MAX_TASKS + OS_N_SYS_TASKS]; // Table of TCBs
-
OSTCBTbl
是给所有的TCB进行分配空间用的栈空间,通过这种方法进行静态分配- 因为我们一共就只有64个优先级,所以最多也就64个任务。但一般根据
OS_MAX_TASKS
(用户可创建任务数)和OS_N_SYS_TASKS
(系统任务数)之和来创建 - 这个变量在ucos_ii.h文件中定义
- 因为我们一共就只有64个优先级,所以最多也就64个任务。但一般根据
-
OSTCBFreeList
是一个链表头指针 ,该链表用于保存空闲的TCB-
最开始所有的TCB都是空闲TCB,所以执行
OS_Init()
时,所有的TCB都被挂载在该链表之下,具体代码如下cOSTCBFreeList = &OSTCBTbl[0];
-
单向链表,只有
OSTCBNext
-
由
OS_InitTCBList()
函数创建
-
-
OSTCBList
也是一个链表头指针 ,该链表用于保存已经分配初始化后的OS_TCB- 每次创建一个task,我们使用函数
OS_TCBInit
,成功的话,它里面就会把从空闲链表 (即OSTCBFreeList
指向的链表)里面取出的TCB加入到OSTCBList
所指链表中。 - 双向链表,有
OSTCBPrev
和OSTCBNext
(采用头插法) - 由
OS_InitTCBList()
函数创建
- 每次创建一个task,我们使用函数
-
OSTCBprioTbl
是一个指针数组 ,用于存放已经分配的TCB的指针 (即在OSTCBList
所指链表的TCB)-
它的主要作用是用于后续通过优先级得到对应的TCB的指针,具体代码如下
cOSTCBPrioTbl[prio] = ptcb;
-
在ucos_ii.h中被定义
-

2.3.2 创建任务流程
-
创建任务的流程就是
OSTaskCreate()
函数的流程,流程如下图-
流程总结
-
进临界区
-
判断是否有中断嵌套
-
判断有没有任务占用优先级
-
堆栈初始化
- 模拟压栈,顺序是(地址由高到低):PC,LR,R12-R0,CPSR)
-
TCB初始化
-
涉及到从空闲链表取出一个TCB进行初始化:
-
OSTCBStkPtr
赋值 -
优先级相关的赋值(
OSTCBX
、OSTCBY
、OSTCBBitX
、OSTCBBitY
) -
设置为就绪状态
OSTCBStat
-
注册到初始化TCB链表(
OSTCBList
) -
更新就绪队列(
OSRdyTbl
)以及伴随的就绪组(OSRdyGrp
))
-
-
-
启动任务重调度(OS_Sched)(前提是
OSRunning
并且创建TCB没有错误)
-
-
-
代码
cINT8U OSTaskCreate(void (*task)(void *p_arg), void *p_arg, OS_STK *ptos, INT8U prio) { OS_STK *psp; INT8U err; #if OS_CRITICAL_METHOD == 3u /* 为CPU状态寄存器分配空间 */ OS_CPU_SR cpu_sr = 0u; #endif #ifdef OS_SAFETY_CRITICAL_IEC61508 if (OSSafetyCriticalStartFlag == OS_TRUE) { OS_SAFETY_CRITICAL_EXCEPTION(); return (OS_ERR_ILLEGAL_CREATE_RUN_TIME); } #endif #if OS_ARG_CHK_EN > 0u if (prio > OS_LOWEST_PRIO) { /* 确定优先级分配的有效性 */ return (OS_ERR_PRIO_INVALID); } #endif OS_ENTER_CRITICAL(); if (OSIntNesting > 0u) /* 表示中断嵌套层数,为临界资源 */ { /* 确保没有在中断服务程序中创建任务 */ OS_EXIT_CRITICAL(); return (OS_ERR_TASK_CREATE_ISR); } if (OSTCBPrioTbl[prio] == (OS_TCB *)0) { /* 确保没有任务占用该优先级 */ OSTCBPrioTbl[prio] = OS_TCB_RESERVED; OS_EXIT_CRITICAL(); psp = OSTaskStkInit(task, p_arg, ptos, 0u); /* 初始化任务堆栈 */ err = OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u,"?"); if (err == OS_ERR_NONE) { OS_TRACE_TASK_CREATE(OSTCBPrioTbl[prio]); if (OSRunning == OS_TRUE) { /* 如果操作系统在运行,则从就绪队列中找到最高优先级任务执行 */ OS_Sched(); } } else { OS_TRACE_TASK_CREATE_FAILED(OSTCBPrioTbl[prio]); OS_ENTER_CRITICAL(); OSTCBPrioTbl[prio] = (OS_TCB *)0; /* 释放优先级,取消分配*/ OS_EXIT_CRITICAL(); } return (err); } OS_EXIT_CRITICAL(); return (OS_ERR_PRIO_EXIST); }
2.3.3 临界段代码保护
-
访问临界资源的代码称为临界段代码
-
临界段代码执行时,须进行关中断 和开中断操作
- ucosii中定义了两个宏 来确定关中断和开中断操作,分别为
OS_ENTER_CRITICAL()
和OS_EXIT_CRITICAL()
- 这两个操作共有三种实现方式 ,通过
OS_CRITICAL_METHOD
定义的值来选择
c#if OS_CRITICAL_METHOD == 1 #define OS_ENTER_CRITICAL() (Cli()) #define OS_EXIT_CRITICAL() (Sti()) #elseif OS_CRITICAL_METHOD == 2 #define OS_ENTER_CRITICAL() (PushAndCli()) #define OS_EXIT_CRITICAL() (Pop()) #elseif OS_CRITICAL_METHOD == 3 #define OS_ENTER_CRITICAL() (cpu_sr = OSCPUSaveSR()) #define OS_EXIT_CRITICAL() (OSCPURestoreSR(cpu_sr)) #endif
- ucosii中定义了两个宏 来确定关中断和开中断操作,分别为
-
OS_CRITICAL_METHOD == 1
- 直接通过修改CPSR对应位来暴力关中断,导致无法处理嵌套的情况
- 因为内层开中断后,外层的临界区也失去保护
- 直接通过修改CPSR对应位来暴力关中断,导致无法处理嵌套的情况
-
OS_CRITICAL_METHOD == 2
- 通过堆栈方式 来保存中断的开/关状态
- 进入临界区时,将中断状态入栈,再关中断
- 退出临界区时,将中断状态出栈,再开中断
- 可支持嵌套,但不符合函数调用标准
- 通过堆栈方式 来保存中断的开/关状态
-
OS_CRITICAL_METHOD == 3
- 通过局部变量 的方法保存/恢复程序当前状态寄存器
- 最优方法
- 没有危险的改变sp指针,利用了参数传递,以及由c编译器来管理栈的布局
2.3.4 堆栈初始化
因为堆栈初始化和硬件有关,所以以下我们主要以ARM9 S3C2440为例子进行讲解

- 其中
- R0-R7:通用寄存器
- R8-R12:影子寄存器
- R13:堆栈寄存器(SP)
- R14:连接寄存器(LR)
- R15:程序指针(PC)
- 注意 :
- 堆栈压栈顺序很重要
- R15先进后出,这样在任务调用的时候,才能正确执行
- 不初始化R13是因为SP指针一直在被使用
- 堆栈是向下生长的
- 堆栈压栈顺序很重要
2.3.5 TCB初始化
大部分内容已在2.3.1中有了讲解,这部分主要给出 OS_TCBInit()
的代码


2.3.6 任务挂载到就绪队列
本部分主要讲述如何应用优先级位图法将任务挂载到就绪队列
-
任务挂载代码,即使任务到就绪状态
cOSRdyGrp |= ptcb->OSTCBBitY; OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX;
OSRdyGrp
也称为PriorityReadyGroup
OSRdyTbl[]
也称为PriorityReadyTable[]
-
OSRdyGrp
和OSRdyTbl[]
的定义cOS_EXT INT8U OSRdyGrp; OS_EXT INT8U OSRdyTbl[OS_LOWEST_PRIO/8 + 1];
OSRdyTbl[]
的长度取决于OS_LOWEST_PRIO
-
OSRdyGrp
和OSRdyTbl[]
的作用-
任务按照优先级 分组,每8个优先级一组,一共8组
OSRdyGrp
用8位二进制数表示-
每一位对应8组中的一组
- 若某一位为1,则该位对应组的优先级 有就绪任务
- 若某一位为0,则该位对应组的优先级 无就绪任务
-
OSRdyTbl[]
为一个数组,其中每一个元素为一个8位二进制数-
每一个元素对应下标所表示的优先级组
- 若某一位为1,则该位对应的任务 就绪
- 若某一位为0,则该位对应的任务 就绪
-
-
-
-
OSRdyGrp
和OSRdyTbl[]
的使用- 当任务进入就绪状态时,该任务对应的
OSRdyGrp
和OSRdyTbl[]
的相应元素会有相应的置 1 操作
- 当任务进入就绪状态时,该任务对应的
-
OSRdyGrp
和OSRdyTbl[]
的联系- 当
OSRdyTbl[x]
中有任何位为1时,那么OSRdyGrp
对应位(对应x)为1- 如:
OSRdyTbl[5]
中有一位为1,那么OSRdyGrp
中第五位(从低到高)为1
- 如:
- 当
-
OSTCBBitY
和OSTCBBitX
-
OSTCBBitY
代表上图中的 Y 坐标;OSTCBBitX
代表上图中的 X 坐标 -
在TCB初始化中的赋值代码
cptcb->OSTCBY = (INT8U)(prio>>3); ptcb->OSTCBBitY = OSMapTbl[ptcb->OSTCBY]; ptcb->OSTCBX = (INT8U)(prio & 0x07); // 0x07=00001111 ptcb->OSTCBBitX = OSMapTbl[ptcb->OSTCBX];
-
OSMapTbl[]
:优先级映射表cINT8U const OSMapTbl[8] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};
- 就是一个mask,用于更快的把
OSRdyGrp
和OSRdyTbl[]
的第index位置1(通过|=
操作)
- 就是一个mask,用于更快的把
-
移位赋值的原因
-
-
-
以上就是优先级位图法
2.4 调度任务
- 定义 :当多个任务处于就绪队列中时,需要根据调度策略来决定任务的执行顺序
- ucosii中通过
OS_Sched()
来实现任务调度 - 调度策略:基于优先级的抢占式调度
- ucosii中通过
- 分类
- 主动调度:任务主动调用调度函数
- 被动调度 :由事件触发,例如,ticks时钟中断产生而触发任务新的周期到达,或者有高优先级任务的等待时间结束,就需要用调度函数来切换任务。
2.4.1 OS_Sched()
-
函数代码
- 执行流程:
- 判断能否调度
- 若能,找到优先级最高的任务(通过
OS_SchedNew()
) - 判断优先级最高的任务是否为当前任务
- 若不是,进行任务切换;若是,继续执行
- 执行流程:
2.4.2 OS_SchedNew()
-
OSUnMapTbl[256]
:-
优先级判定表,用于保证查找时间的确定性(从就绪队列中找到最高优先级任务所花的时间与队列长度无关)
-
OS_SchedNew()
无须从OSRdyTbl[0]
开始扫描所有位 -
OSUnMapTbl[256]
结构OSUnMapTbl[256]
罗列了++8位二进制数所能表示的所有值的个数++(256个),所以表的index为256- 其中每一个值为0~255用二进制表示,哪一位最先为1(最低位为1),就是这个表中对应index的元素的值
- 比如255是11111111,其中第0位最先为1,所以
OSUnMapTbl[255]=0
- 比如255是11111111,其中第0位最先为1,所以
-
-
OS_SchedNew()流程和代码
-
OSUnMapTbl[256]
使用- 上面这256个值就是
OSRdyGrp
和OSRdyTbl[]
的可能取值(0x00~0xFF) - 所以通过
OSRdyGrp
和OSRdyTbl[]
的值,去OSUnMapTbl[256]
查找,就可以知道首先出现1的是哪一个任务
- 上面这256个值就是
-
查找流程
- 先用
OSRdyGrp
去OSUnMapTbl[256]
查找首先出现1的那一位y - 再用
OSRdyTbl[y]
去OSUnMapTbl[256]
查找首先出现1的那一位x - 然后计算可得最高优先级的值(
OSPrioHighRdy = y<<3+x
)
- 先用
-
代码
-
2.4.3 任务切换
-
上下文切换
-
指CPU被切换到另一个任务时需要保存当前任务的运行状态 并恢复另一个任务的运行状态
- 当前任务变为就绪(或者挂起、删除)状态
- 就绪队列中优先级最高的任务变为当前任务
-
通过
OS_TASK_SW()
或者OSIntCtxSw()
切换到优先级最高的任务-
OS_TASK_SW()
被定义为**OSCtxSw()
,属于任务主动切换**c#define OS_TASK_SW() OSCtxSw()
-
OSIntCtxSw()
属于++中断导致++ 任务被动切换
-
-
上下文表示
- 任务上下文通过任务TCB表示,它包括任务状态、CPU各寄存器的值等信息
-
-
任务切换核心流程(
OSCtxSw()
函数流程)-
从Task1切换到Task2
-
保护被抢占任务的现场:
- 保护寄存器【按以下顺序】:PC、LR、R0-R12(若主动切换,PC和LR相同;被动切换则不同)还有CPSR压栈保存
-
将++保护好现场,压好栈++ 的最新sp放入*旧任务(此时的OSTCBCur)*的OSTCBstkPtr字段
- 每个任务有自己的栈顶指针,任务的栈顶指针都保存在TCB的OSTCBstkPtr字段中
-
从OSTCBHighRdy这个TCB里面取出切换任务的stack字段放入sp,准备恢复现场
SP=OSTCBHighRdy->OSTCBStkPtr
- 并且设置
OSPrioCur=OSPrioHighRdy
,OSTCBCur=OSTCBHighRdy
-
恢复切换任务的PC、LR、R0-R12还有CPSR【恢复任务时顺序是从CPSR到PC】,完成恢复现场 ,切换任务执行,此时SP指向高优先级任务的堆栈栈顶
-
-
主动切换 和被动切换导致PC、LR不同(代码中的两个LR)的原因
- 主动任务切换:
- 一个任务要主动挂起自己,这将触发
OS_Sched()
重调度,此时两个LR相同,因为ARM处理器工作状态没有发生变化
- 一个任务要主动挂起自己,这将触发
- 被动任务切换:
- 用户在中断中创建优先级更高的新任务时,第一个LR保存的是中断模式下的PC;第二个LR保存的是旧任务的LR,所以会导致两个LR(即PC和LR不同)
- 本质原因就是ARM有7种工作模式,中断发生前,处理器在系统/用户模式工作,中断发生后,处理器自动切换到外部中断模式。不同模式下的寄存器分配和使用不同,进而导致了这种现象。
- 主动任务切换:
-
若用
OSIntCtxSw()
切换,则第1,2步由统一中断服务处理程序完成- 代码复杂了,看不懂,这里也就不做过多讲解了 :(
-
2.5 习题
-
Q:为什么栈的初始化要先于TCB的初始化
-
A:
- 因为任务的堆栈指针sp的值也会存储在TCB里面
- 如果TCB的初始化在堆栈前完成,由于模拟压栈的存在,在栈初始化之前存入TCB的sp的值必定不对
- 这样在栈初始化完成后,还需要再主动的更改TCB中保存的堆栈指针值,显然是多余的并且实现起来很麻烦
- 所以应该让堆栈初始化先于TCB初始化
-
-
Q:任务就绪队列是如何实现的
- A:
- 就绪队列的物理对应是
OSRdyTbl
,不过多加了一个OSRdyGrp
辅助查找最高优先级 - 得到
OSPrioHighRdy
后,通过我们的OSTCBPrioTbl
这个指针数组 ,根据OSPrioHighRdy
优先级号作为索引得到对应的TCB的指针
- 就绪队列的物理对应是
- A:
3. 中断
3.1 中断的概念
-
中断定义:导致程序正常运行流程发生改变的事件
- 中断是一种硬件机制
-
中断分类
- 硬中断 (外部中断):由于CPU外部原因 而改变程序运行流程的过程
- 硬中断触发及响应属于异步事件
- 自陷 (内部中断):通过处理器软件指令 ,可预期地使CPU正在执行的程序流程发生变化
- 自陷是内部显式事件触发
- 异常:CPU自动产生的自陷,以处理特定的异常事件
- 硬中断 (外部中断):由于CPU外部原因 而改变程序运行流程的过程
-
中断向量表:内存中一段连续空间,该空间中按照中断向量号从小到大顺序存储对应的中断向量。
- 中断向量是中断服务程序的入口地址
-
异常向量表:全部都是汇编语言实现的跳转指令
-
中断请求处理方式
- 中断作为任务切换
- 中断作为系统调用
- 中断作为前台任务--RTOS常用
3.2 ARM中断机制
-
ARM9的 7 种异常:
- 软中断是用户模式切换到特权模式的唯一途径
- IRQ:中断异常、FIQ:快速中断异常
3.3 S3C2440裸板中断
- 实现中断须思考三个问题:
- 中断返回:中断服务程序执行完后怎么返回到被中断的程序?
- 中断注册 :如何将用户编写的中断处理函数与特定的中断源关联起来,使得当该中断触发时,CPU能够找到中断源,然后自动跳转到对应的ISR执行?
- 状态保存和现场恢复
3.3.1 中断返回
- 中断进入时,以下操作由硬件完成--裸板中断
- 保存被中断程序当前的PC 到LR_irq中
- 也就是间接保存中断返回后应该执行的下一条指令位置,所以中断服务程序里面会对LR-4再赋值给PC
- 这个就解决了上面的问题1
- 将程序当前状态寄存器CPSR的值放入相应模式的SPSR
- 即是irq的SPSR(用于中断返回时的恢复)
- 切换处理器模式为irq模式,也就是将CPSR的模式位设为相应的中断模式,并禁用相应模式的中断
- 如果是快中断模式,则禁用所有中断。
- 通过异常向量表找到irq应该进入的处理程序地址,放入PC中实现跳转
- 保存被中断程序当前的PC 到LR_irq中
3.3.2 中断注册
-
通过查找
INTOFFSET
寄存器和HANDLEINT
地址,计算:INTOFFSET
*4+HANDLEINT
得到对应的中断服务程序地址存放的位置-
也就是算出来的结果是一个地址,这个地址对应的位置存放了ISR的地址
-
INTOFFSET
是2440芯片一个用于中断管理等功能的寄存器 ,发生中断时,用来存放为中断源分配的一个整数,++这个整数唯一对应一个中断源++。(这个寄存器由芯片厂商提供) -
HandleEINT0
代表的是一个内存地址,其内容是对应中断的中断服务函数入口。(自定义中断向量表)
-
-
个人理解:
-
HandleEINT0
对应的是中断向量表的起始地址,然后通过INTOFFSET
作为偏移量(中断源编号),来访问不同中断源的中断服务程序INTOFFSET
*4是因为每一个ISR的入口地址是32位即4字节的
-
3.3.3 状态保存和现场恢复


- Q:为什么这里保存两次lr?
- 第一个LR-4是在irq模式下,是进入中断的时候,我们本来程序应该接着执行的指令地址,所以是一个PC
- 第二个LR(不需要-4),是本来被中断程序的LR内容(svc模式),这个LR是程序可能是一个函数调用,是它要返回的地址,所以要保存两个
- 但是在
OSCtxSw
里面,我们仍然压栈了两个LR,而且值是一样的,是因为OSCtxSw
是主动切换,是任务执行的时候主动的函数调用,它就是一个函数被执行,所以LR的值(svc模式下),和任务切换回来后应该执行的pc的值是一致的,也不需要-4 - 之所以冗余的存下两个LR,(前一个代表返回地址pc,后一个代表LR值),就是为了保证任务的上下文的栈结构是一致的,这样OSCtxSw/OSIntCtxSw就是用统一的方式恢复任务上下文
3.4 ucosII的中断管理机制
- ucosii的中断与裸板中断的区别
- ucosii是一个可抢占内核
- 中断处理完成后,内核调度程序
OS_Sched()
会从就绪队列中找到优先级最高的任务运行- 优先级最高的任务可能不是被中断的任务
- 因此在发生中断、进入ISR前,首先需要保存被中断任务的上下文
- uc的任务运行在svc模式下,所以被中断任务的上文需要保存在svc模式的栈中
- 完成ISR后,需要进行任务重调度以确定恢复到哪一个任务的上下文
- 中断处理完成后,内核调度程序
- ucosii允许中断嵌套
- 在ISR中需要增加中断嵌套计数器,根据计数器的值来决定是否重新调度
- ucosii是一个可抢占内核
3.4.1 中断的发生及响应
-
ARM9没有中断向量表,只有异常向量表
-
所有的中断IRQ都会进入同一个入口,所以这个入口需要完成
- 如保存上下文、增加中断嵌套计数器 等++共有操作++
- 然后通过INTOFFSET和自定义中断向量表进入ISR
- 完成ISR后,调用uc的调度函数进行重新调度
-
入口代码
- 一开始代码执行在IRQ模式下,后面转换到SVC模式。
- 一定要注意的是不同模式下对应不同的sp,也就对应着不同的堆栈
-
整个入口的流程:同时回答uc运行中,如何处理IRQ?(中断产生及响应)
-
总的流程: 保存IRQ模式下的特殊寄存器-退出中断模式-保存现场-返回中断模式中断-区分中断源-中断服务程序-退出中断模式-恢复现场
-
保存R0-R2寄存器的值压入到当前的IRQ堆栈,然后将此sp放入R0
-
irq的sp复原,将r1保存pc(irq_lr-4),r2保存中断前cpsr(irq的spsr)
-
退出中断模式,改为任务的svc模式,转到任务的堆栈
-
保存现场,pc(R1)入栈,然后是LR(R14) R12-R0
-
然后从r0指向的irq堆栈中出栈到R3-R5(对应保存的R0-R2),然后连着R2(保存的spsr)入栈(svc任务栈)
-
更新中断嵌套OSIntNesting计数,然后判断是否在中断嵌套里面,如果没有嵌套则:
- 将被中断的任务的上下文指针(sp)保存到它的TCB的OSTCBStkPtr成员中
-
恢复cpsr为irq模式,也就是进入irq模式,取得INTOFFSET值来区分中断源
-
计算得出IRQ入口,保存到PC,执行ISR。
-
ISR完成后,切换到svc模式,先调用OSIntExit进行可能的任务重调度以及更新OSIntNesting
-
最后是恢复现场(完成任务的恢复执行)
-
-
3.4.2 中断返回
-
通过
OSIntExit()
函数实现 -
判断是否有嵌套,没有嵌套就从就绪队列中找到最高优先级任务给
OSTCBHighRdy
,然后进入进入中断上下文切换- 如果最高优先级任务是当前任务(被中断任务),则不进行重调度,而是直接退出
OSIntExit()
,回到被中断任务 - 如果有比当前任务优先级更高的任务,则调用
OSIntCtxSw()
进行任务中断到任务上下文的切换,该函数不会返回,而是恢复最高优先级任务的上下文,继续运行
- 如果最高优先级任务是当前任务(被中断任务),则不进行重调度,而是直接退出
-
OSIntCtxSw()
代码
4. 时钟管理
- RTOS中的两种时钟源
- 实时时钟 (硬件时钟)
- 系统断电,也可以维持
- 定时器/计数器
- 用作系统时钟,由内核控制
- 二者的关系:
- 实时时钟是系统时钟的基准,实时内核通过读取实时时钟来初始化系统时钟
- 所以系统时钟并不是真正意义上的时钟,只有当系统运行起来以后才有效,并且由实时内核完全控制
- 实时时钟 (硬件时钟)
4.1 系统时钟详解
-
定时器一般由晶体振荡器 提供++周期信号源++ ,并通过程序对计数寄存器 进行设置,使其产生固定周期的脉冲 ,每一次脉冲的产生都将触发一个时钟中断。
-
时基(Tick) :时钟中断的频率
- tick的大小决定了整个系统的时间粒度,一般为10~100次/秒
- 上图中
- 晶体振荡器提供周期信号源,通过总线与CPU相连
- 计数寄存器(counter)的初值可以由编程设定
- 每一个晶体振荡器的输入信号都会导致counter的值增加
- 当counter溢出时,就会产生一个输出脉冲(pulse),pulse可以用来触发CPU核上的一个时钟中断
- 回答上图中的问题:
- 因为tick=32ms,所以每32ms要有一次时钟中断,即每32*10-3s,counter就得溢出
- 晶体振荡器的振荡频率是1MHz,所以每10的负六次方振荡一次,所以counter的初始值(溢出值)应为32000
- 我们可以得到公式: c o u n t e r = t i c k × f r e q u e n c y counter = tick \times frequency counter=tick×frequency
-
实时内核的时间管理以系统时钟为基础 ,通过tick处理程序来实现。产生时钟中断时,在中断服务程序中,会调用tick处理程序
4.2 时钟服务
-
OSTimeDly()
-
调用该函数后,会将正在运行的任务延迟n个ticks直到期满
- 任务从运行状态切换为等待状态
- 触发ucosii进行一次任务重调度,从而执行下一个优先级最高的就绪态任务
-
TCB中有一个OSTCBDly变量,用于记录还要延迟几个tick,每一次时钟中断都会让其减1
- 任务延迟期满或者有其他任务调用
OSTimeResume()
取消其延时,任务就会重新进入就绪状态
- 任务延迟期满或者有其他任务调用
-
OSTimeDly()
代码如下- 流程总结
- 从就绪队列里面移除任务的操作,主要是先把
OSRdyTbl
这个位图里面对应任务那个位置零 - 再看是否需要更新伴随的
OSRdyGrp
(按照行来分组,也就是从0号任务开始,每8个任务为一组)是否需要清零表明这一组(行)的任务都不在就绪状态。 - 最后设置任务的
OSTCBDly
字段为需要等待的ticks,这个是每次在时钟中断处理程序里面的**OSTimeTick
函数里面会更新的** - 做完以后会主动调用重调度切换任务执行。
- 从就绪队列里面移除任务的操作,主要是先把
- 流程总结
-
4.3 时钟中断
- arm mini2440中 0号中断对应的是时钟中断
4.3.1 OSTickISR
-
时钟中断服务程序代码如下
assemblyOSTickISR mov r5,lr mov r1,#1 mov r1,r1,lsl #10 ldr r0,=SRCPND ldr r2,[r0] orr r1,r1,r2 str r1,[r0] ldr r0,=INTPND str r1,[r0] bl OSTimeTick mov pc,r5
-
首先得到Timer0在中断源寄存器SRCPEND中的bit位置(0x400)
-
然后将中断源寄存器SRCPND中的值与0x400进行或操作,并将值写回,达到清中断(pending)的目的。
-
同理,将INTPND寄存器(只有一个待响应的中断处于挂起状态)的值读出再写回,清除挂起状态。
-
最后跳转到
OSTimeTick()
-
4.3.2 OSTimeTick()
-
ucosii中的系统时钟节拍响应函数,函数功能是处理所有任务的延时 ,并将延时结束的任务的状态转换为就绪态
- 每次系统时钟中断的时候会调用这个函数进行核心的中断服务处理,它会记录中断次数到一个全局变量Time,
- 遍历
OSTcbList
(也就是已分配使用的TCB链表),对于每个TCB去判定是否OSTimeDly
非0,非0说明被阻塞延时了,OSTimeDly
表示仍然需要等待的tick数,这个时候需要更新它(-1操作) - 然后进一步判断此时是否
OSTimeDly
为0,为0说明等待时间到达,如果达到并且任务没有被suspend
或者没有等待某个事件,那么就把该任务放回就绪队列, - 最后在整个中断服务程序(
OSTickISR
)返回之前,会调用ucos的OSIntExit()
函数更新OSIntNesting
以及进行可能的任务重调度(OSIntCtxSw
)。
-
核心代码
cptcb = OSTCBList; /* Point at first TCB in TCB list */ while (ptcb->OSTCBPrio != OS_TASK_IDLE_PRIO) { /* Go through all TCBs in TCB list */ OS_ENTER_CRITICAL(); if (ptcb->OSTCBDly != 0u) { /* No, Delayed or waiting for event with TO */ ptcb->OSTCBDly--; /* Decrement nbr of ticks to end of delay */ if (ptcb->OSTCBDly == 0u) { /* Check for timeout */ if ((ptcb->OSTCBStat & OS_STAT_PEND_ANY) != OS_STAT_RDY) { ptcb->OSTCBStat &= (INT8U) ~(INT8U)OS_STAT_PEND_ANY; /* Yes, Clear status flag */ ptcb->OSTCBStatPend = OS_STAT_PEND_TO; /* Indicate PEND timeout */ } else { ptcb->OSTCBStatPend = OS_STAT_PEND_OK; } if ((ptcb->OSTCBStat & OS_STAT_SUSPEND) == OS_STAT_RDY) { /* Is task suspended? */ OSRdyGrp |= ptcb->OSTCBBitY; /* No, Make ready */ OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX; OS_TRACE_TASK_READY(ptcb); } } } ptcb = ptcb->OSTCBNext; /* Point at next TCB in TCB list */ OS_EXIT_CRITICAL(); }
-
差分时间等待链
-
方法由来
-
RTOS需要内核尽可能快地对外部事件作出响应,而实时性通常与确定性密切相关。
-
这里我们介绍一个差分时间等待链的方式来确定化时间。
-
上面的tick更新方式是一个时间等待链,需要对每个任务都遍历,更新他们剩余的等待时间
- 这个时间开销会随着任务的增加而增加,时间就不能是确定的。
-
-
方法思路:
- 保存第一个任务为队首,是最先会被执行的任务,
- 第二个任务的delay tick数是和前一个任务的时间差,也就是前一个任务结束后,第二个任务还需要等待的时间
- 同理对于第三个任务,是对前一个任务(第二个任务)的时间差,依次类推
- 这样的好处是,在更新任务tick时间的时候,只需要对这个差分时间链表的队列头部进行-1操作,当减到0时,就从等待链里面取出,后续节点成为新的头部结点被激活,然后继续上述操作。
- 这样每次都不需要更新等待链中的其余结点,减少计算开销,整个时间是确定的。
-
方法问题:
- 每次新增等待任务加入差分时间等待链链表的时候会需要同时修改它的后一个任务的delay的时间,不过这个操作同样也是固定时间的。
-
5. 操作系统启动
c
void OSStart(void)
{
if (OSRunning == OS_FALSE)
{
OS_SchedNew(); /* Find highest priority's task priority number */
OSPrioCur = OSPrioHighRdy;
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; /* Point to highest priority task ready to run */
OSTCBCur = OSTCBHighRdy;
#if (defined(OS_TRACE_EN) && (OS_TRACE_EN > 0u))
OS_TRACE_TASK_SWITCHED_IN(OSTCBHighRdy);
#endif
OSStartHighRdy(); /* Execute target specific code to start task */
}
}
-
这里不能调用sched,但是和sched很像,主要区别在于这里调用的是OSStartHighRdy进行"任务切换"(切换到一个存在的最高优先级的就绪队列里面的任务),而shced里面是当需要任务切换时,调用OSCtxSw
-
OSStartHighRdy不同在于,它不会保存之前任务的上下文(因为之前没有任务),其他的就是:
-
将OSTCBCur=OSTCBHighRdy
-
将OSPrioCur=OSPrioHighRdy
-
将SP=OSTCBHighRdy→OSTCBStkPtr
-
通过SP恢复任务上下文(之前每个TCB执行TCBStkInit的时候都进行了模拟压栈),恢复寄存器的值以及pc指向任务的执行程序
-
- 后面的部分过一下就好了,不是重点
6. 硬件系统
-
ARM处理器的运行模式与寄存器
-
R15:
- ARM state bits [1:0]: 0,bits[31:2]: PC
- THUMB state bit [0]:0, bits[31:1]: PC
-
CPSR布局
-
SVC(Supervisor)模式:0b10011,十进制表示为19 16进制是:0x13
-
IRQ(Interrupt)模式:0b10010,十进制表示为18 16进制是:0x12
-
FIQ(Fast Interrupt)模式:0b10001,十进制表示为17 16进制是:0x11
-
-
-
大小端存储
- 大端:
- 小端:
- 大端:
-
硬件系统主要组成
-
Processor处理器
- ARM(Advanced RISC Machine)
- 32位 RISC指令集
- Boot of ARM
- Internal boot (from ROM)
- External boot (from Norflash or ......)
- ARM(Advanced RISC Machine)
-
Input输入
-
Output输出
-
Memory内存
-
Bus总线
- ARM AMBA (Advanced Microcontroller Bus Architecture) bus
- AHB (Advanced High-performance Bus)
- APB (Advanced Peripheral Bus)
- PCI bus
- CPCI
- PCIE
- ARM AMBA (Advanced Microcontroller Bus Architecture) bus
-
7. 软件系统
-
嵌入式软件系统(ESS)运行流程
-
VxWorks在x86上启动过程
-
7.1 启动boot
-
从硬件上电到Bootloader完成硬件初始化和内核加载的全过程
- Boot过程为OS Start提供硬件就绪状态(如内存、时钟、外设初始化)。
-
我们解决以下问题
-
What's the first job ?
-
Where is the entry?
-
Where is the first instruction ?
-
What are boot, loader, bootloader ?
-
-
解决问题1、2:
-
解决问题3、4
-
remap操作的解释
-
地址mapping的效果(在这里),是让一部分RAM的地址等价于对应某个flash的地址,从而使得我们最开始pc指向的0x00000000能执行到存储在flash中的boot代码,这个代码内容在这张图:
-
你可以看到norflash的地址也0x00000000,因为此时地址被remap到norflash的地方(实际地址是这张图):
-
然后我们会在boot到时候,检测到(见左上方红色代码)我们是从flash启动的,需要搬运代码段到ram区的Text_start位置,也就是copyloop。
-
如果结束这个之后,不取消remap的话,那么后续你看到copy vectors的时候使用的也是这个地址0x00000000,虽然是复制到ram的位置,但是由于remap,会把原来的boot代码覆盖掉,这个流程图里面,你看到Load the image操作在cancle remap之后,我认为是因为在代码里面左上角红色四行代码执行完成后,应该就可以cancel remapping了(这个cancel的操作代码廖勇没给出),然后再进行Load image操作
-
-
-
总结启动的过程
- 首先会mapping,将0x40000000的地址映射到0x00000000的位置,从而使的可以从零开始执行启动代码(的一部分memory init这些,以及将pc跳转到norflash对应代码处),执行完后就取消重映射。
- 第二步就是利用那个左上方红色代码检测是否需要搬运,最后在复制向量表之前,将原来的0x40000000位置的代码拷贝到0x06000000(也就是实现搬运到我们程序真正任务应该运行在的text段位置)
- 写在最后 :
- 以上笔记参考了往年学长们的笔记
- 若笔记中有错误,欢迎指正