type
status
date
slug
summary
tags
category
icon
password

Introduction

嵌入式系统简介

嵌入式系统概念

任何含有一个计算机的设备都能被称为嵌入式系统,如消费电子产品、信号处理器等

嵌入式系统使用场景

  • 嵌入式计算机
  • 无线传感器网络
  • 航空电子设备
      notion image
      这个设备是有一个仲裁电路来决定到底是主系统起作用还是备用系统起作用(当主系统瘫痪的时候,备用系统将获取主系统的所有信息并继续工作)
  • 火星探测器
  • 普适计算:普适计算是指环境中越来越普遍的、连接的计算设备的趋势,这是由先进的电子(特别是无线)技术和Internet的融合带来的趋势(这是什么鸡毛)
  • 跨学科的

实时系统简介

实时系统的概念

任何一个 计算机对外部事件的及时响应至关重要 的系统都被称为实时系统

实时系统的使用场景

  • 航空电子设备
  • 火星探测器
  • 制造业
  • 核反应堆控制
可以发现嵌入式系统和实时系统的使用场景是重叠的,在重叠部分,就是嵌入式实时系统。即:
notion image

嵌入式实时系统简介

嵌入式实时系统特点

  • 应用特定化
    • 将特定应用的设计定制化。所以常用的嵌入式处理器的种类非常多,都是为了实现定制化设计的:
      • notion image
    • 优化定制化的设计。如针对信号处理的系统需要使用DSP等:
      • notion image
        上图也是,由于系统的任务是计算密集型(如音频视频处理啥的)的,所以在内部增加了加法器,为了支持一些更复杂的计算,如下图:
        notion image
  • 需要考虑硬件和软件
  • 需要考虑非功能性约束(或者说是非功能性需求),如:
    • Real-time(实时性。不同的应用场景对实时性要求不同)
    • Memory(内存使用效率)
    • Power(能源使用效率)
    • Cost(成本)
    • Reliability (Security, Safety……)(安全性)

嵌入式实时系统的软件架构

  • Polling loop(轮询系统):
      notion image
  • Foreground/Background(Interruption)(前后台系统,这里的前台应该才是中断,后台是轮询系统)
      notion image
      也正如我前面理解的,所有的中断都算是前台,然后后台是自己的主函数轮询
  • Multi-task(多任务系统)
      notion image
      需要注意的是,多任务系统需要嵌入式实时操作系统的支持,才能实现任务的调度与并发。上图中的意思应该是应用程序发出一个请求需要使用网络,然后这个时候内核就向网卡模块发起请求,等待网卡模块返回之后再将结果返回给应用程序任务

常见的嵌入式实时操作系统

  • VxWorks ,INTEGRITY(MULTI) ,TinyOS, Nuclear, Windows CE (mobile), uC/OS, DeltaOS, pSOS+, VRTX, QNX, RTEMS, Cisco-IOS, NOKIA/ERISON-EPOC, Sybian, Android,IOS ……(还有FreeRTOS
  • Linux(嵌入式Linux)
    • RTLinux
    • RTAI
    • Linux-SRT
    • Embdix
    • ETLinux
    • uCLinux
    • uLinux

Kernel of ERTOS

  • 一个策略:调度策略
  • 四个机制:任务协同机制、内存管理机制、事件触发机制(中断)、时间触发机制(时钟)

任务管理

主要涉及到调度策略,以及任务的各种操作,如任务创建等

内核的主要功能

  • 内核的主要功能:调度

内核调度的对象

内核调度的对象

内核调度的对象是任务

什么是任务

  • 任务是一个可执行的软件实体,他负责执行一些特定的操作。他包括一个指令序列、相关的数据以及计算机资源
  • 任务是一个无限循环的函数。他就像一个普通函数一样需要接收参数,但是他永远不会返回。任务的返回值类型永远是void

任务的特点

  • Concurrence (Simultaneousness)(并发与并行)
  • Independence & Asynchrony (dependence & Synchrony)(独立异步与不独立同步)
  • Dynamic(动态。就跟操作系统课上讲的是一样的,就是进程是一个动态实体,而程序是一个静态实体)
任务与函数的区别是啥来着?疑问

任务的描述

任务的概念

在内核调度的对象中已经描述了,稍微记一下就好了

任务的执行体

相当于是任务的三要素中的指令序列了。并且在这里其实也涉及到了数据,因为程序和数据一般是相互依存的
一个简单任务的执行体如下:
notion image
需要注意的是,任务是一个无限循环,并且返回值永远为void

任务与进程的区别

只需要把任务当成一个线程就好了。而线程是没有内存隔离的,而进程是有内存隔离的
notion image
上图中创建了一个进程,那么这个时候就会存在内存隔离,那么在进程test中的变量i就是主进程中i的一份拷贝,所以在主进程中执行i++并不会影响进程test中的i,所以在进程i中数据i的值应该为1
notion image
上图中创建了一个任务,而任务相当于是一个线程,他的地址空间是与其他在同一进程中的线程共享的,所以在主线程中执行i++将影响test线程地址空间中的i变量,所以test任务将输出2

TCB

这里主要了解的是任务TCB结构体中一些重要的成员,并没有涉及到创建任务啥的。
  • OSTCBStkPtr:任务栈指针指向任务栈,是TCB的第一个成员,存放在地址最低处
  • OSTCBNext与OSTCBPrev:用于存已经初始化的TCB队列(即OSTCBList)中的下一个和前一个TCB(已初始化队列是一个双向链表,是为了便于进行快速的链表操作。这样只要找到一个TCB就能马上获取他的前后TCB并进行操作)
  • 这里在额外说一下这些各种奇怪的组织TCB的结构。
    • OSTCBTbl:用于存放所有的TCB块,是一个数组,为的是空间的确定性(初始化的数量是根据用户的需求确定的,也就是用户需要的最低优先级)
    • OSTCBFreeList:用于记录空闲的TCB(初始化的时候执指向的是OSTCBTbl的头部)
    • OSTCBList:用于记录已经使用的TCB(初始化的时候为空)
    • OSTCBPrioTbl:用于存放已经初始化了的TCB的指针,便于根据优先级快速查询到对应任务的TCB
  • OSTCBDly:代表任务的延时Tick数
  • OSTCBStat:代表任务的状态
  • 任务的状态转换图如下(这个可跟操作系统课上讲的进程五态图不一样):
    • notion image
      在这个图中需要记一些会引起状态转换的地典型事件,也就是上图中标红的部分(还需要额外记一个任务的删除。并且注意任务删除之后任务只是进入冬眠状态
  • OSTCBPrio:代表任务的优先级(在uC中任务优先级唯一,就相当于是任务的ID号了)
  • OSTCBX、OSTCBY、OSTCBBitX、OSTCBBitY:用于优先级位图法,牺牲空间赢得时间,以实现时间的确定性(关于优先级位图后面会详细介绍。只需要知道这这里带Bit的都是掩码就好了)

优先级位图法

因为后面都与优先级位图法有关,所以这里需要介绍一下优先级位图法
  • 优先级位图法使用的数据结构
  • 这里分为全局变量和TCB成员变量两部分进行介绍
    • 全局变量
      • OSMapTbl数组:掩码映射表,将一个0-7取值的数映射为对应的掩码(如传入1,则返回00000010)
      • OSUnMapTbl数组:掩码取消映射表,获取一个8位数据中1所在的最低位(如传入10100100,则返回2)
      • 上面这两个数组更像是一个定死的工具
      • OSRdyGrp变量:记录优先级组的情况,即记录优先级位图中的某一行是否有任务在就绪队列中,如果有任务,则对应位置1
      • OSRdyTbl数组:优先级位图的本体,在64优先级的情况下被初始化为8个8位数据组成的数组。若某个优先级当前有任务在就绪队列中,则该位为1
    • TCB成员变量
      • OSTCBX:当前任务优先级在优先级位图中的列号
      • OSTCBY:当前任务优先级在优先级位图中的行号
      • OSTCBBitX:OSTCBX对应的掩码
      • OSTCBBitY:OSTCBY对应的掩码
      下面结合一个实际例子来说明上面介绍的变量
      notion image
      如上图,这个时候创建了一个优先级为35的任务(或者说是TCB)。首先先将优先级转换为二进制数:0010 0011(由于只有64个优先级,所以这里最高两位恒为0)
      由二进制数实际上就能确定优先级在位图中的行号和列号了:二进制数右移3位得到的一个3位数据实际上就是行号,而二进制数本身的第三位就是列号(因为行号*8+列号=优先级,也可以通过rCore多级页表的思想来看。rCore的三级页表是将一个27位虚拟页号拆分成3个9位数据,而每个9位数据就是在该级页表中的偏移量。而这里就相当于是一个二级页表,前三位索引行,后三位索引列)
      所以有赋值语句:ptcb->OSTCBY = (INT8U)(prio >> 3)与ptcb->OSTCBX = (INT8U)(prio & 0x07)
      而OSTCBBitY与OSTCBBitX就是OSTCBY与OSTCBX的掩码,可以直接通过掩码表得到,所以有赋值语句:
      ptcb->OSTCBBitY = OSMapTbl[ptcb->OSTCBY]与ptcb->OSTCBBitX = OSMapTbl[ptcb->OSTCBX]
      至此TCB的成员变量已经初始化完成。
      但是在TCB初始化的过程中还需要对OSRdyGrp变量以及OSRdyTbl数组进行修改。
      对于OSRdyGrp变量,由于需要置入就绪队列的任务优先级为35,位于优先级分组4中,所以需要将OSRdyGrp的第四位置1。这个时候OSTCBBitY与OSTCBBitX 的作用就展现出来了,因为OSTCBBitY就是00010000,所以有赋值语句:OSRdyGrp|=ptcb->OSTCBBitY
      对于OSRdyTbl变量,由于需要置入就绪队列的任务优先级为35,应该位于优先级分组4中的第3个,所以需要将该为置1。首先需要先取出改组的优先级情况,即为OSRdyTbl[ptcb->OSTCBY](OSTCBY就是行号),然后需要将第三位置1,同样的OSTCBBitX的值就是00001000,所以有语句OSRdyTbl[ptcb->OSTCBY] |= ptcb->OSTCBBitX
      至此所有有关就绪队列优先级位图的变量都介绍完毕了
接下来介绍三个优先级位图法使用的地方
  • 任务进入就绪队列
  • 实际上就是上面举的例子,这里不赘述了
  • 任务退出就绪队列
  • 退出就绪队列同样需要提供任务的优先级。实际上就只需要修改OSRdyTbl数组与OSRdyGrp变量即可。唯一需要注意的点是,如果OSRdyTbl中该行(该行指的是需要被移出就绪队列的任务所在的行)中对应当前任务的位被置0后该行没有任务了,这个时候才需要将OSRdyGrp的对应位置0。其余的操作无非就是取反然后按位与,这里也不再赘述了
  • 找到最高优先级任务
  • 这里需要详细介绍一下。就以64位位图为例
    • 需要知道的是,寻找最高优先级的任务是与特定的TCB无关的,所以并不会涉及到TCB成员变量的操作
      首先需要寻找最高优先级任务所在的优先级组,这个时候就需要使用OSUnMapTbl数组了,他返回的是传入数字中的1所在的位置,故有语句:y= OSUnMapTbl[OSRdyGrp];,此时y就是最高优先级任务的行号了(就相当于是TCB的成员变量OSTCBY)
      获取到行号之后就需要查找最高优先级任务在当前分组中的列号了,同样需要使用OSUnMapTbl,但是此时传入的参数就不再是OSRdyGrp,而应该是优先级位图中该优先级分组的情况了,所以最高优先级任务的列号应该为:OSUnMapTbl[OSRdyTbl[y]]);
      知道行号和列号之后就可以计算出最高优先级:
      OSPrioHighRdy= (INT8U)((y << 3) +OSUnMapTbl[OSRdyTbl[y]]);
      实际上就是行号*8+列号。
      而如果查找到了最高优先级,就能通过OSTCBPrioTbl数组找到最高优先级任务的TCB

任务创建

详细介绍任务创建函数。这个需要熟悉创建的流程
OSTaskCreate具体的步骤如下:
  • 判断中断嵌套层数。在创建任务时是不允许中断嵌套的
  • 根据OSTCBPrioTbl判断当前优先级是否已经被某个任务使用了。如果没有被使用的话就要占位
  • 任务栈初始化:也就是进行模拟压栈(需要按照ARM要求的寄存器顺序压栈,编号高的寄存器需要压在高地址处)
  • 下面直接给出模拟压栈的代码:
    • 需要注意的是传入的栈底单元是可以使用的,所以需要先向*(stk)写值将栈变成一个满递减栈,然后就只需要将寄存器按顺序压入即可(PC寄存器的位置压入任务执行体指针,SP寄存器不压入,R0压入任务参数,最下面一个单元压入任务状态)(另外需要有一个C语言的小知识,对指针做减法减去的是这个类型的大小,而不是1)
      最后压栈结果如下图:
      notion image
      这个TaskStartAStk应该就是为任务开出来的栈空间,由于之前这个任务从来没有使用过栈,所以栈顶指针应该是&TaskStartAStk[255](也就是栈空间中最后一个单元的地址)
  • 任务TCB初始化:初始化当前任务的TCB。实际上就是把上面介绍的TCB成员都初始化一下(如:栈指针,Dly,状态,优先级,优先级位图变量)。另外再修改一些全局变量。需要修改的全局变量如下:
    • OSTCBFreeList指针:因为TCB块需要从OSTCBFreeList中取出
    • OSTCBPrioTbl数组:因为需要支持根据优先级快速查找任务
    • OSTCBList指针:因为TCB初始化之后就应该从OSTCBFreeList链表中取出并放在OSTCBList链表中
    • 变量OSRdyGrp:为了支持优先级位图
    • OSRdyTbl数组:为了支持优先级位图
  • TCB初始化如果没有问题的话,就进行任务调度
这里总结一下任务创建的全过程(因为上面写的有点多)
  • 判断中断嵌套层数
  • 判断当前优先级是否被某个任务占用
  • 初始化任务栈(模拟压栈)
  • 初始化任务TCB
  • 没有问题就开始调度任务

任务调度

任务调度OS_Sched的主要步骤如下(要求中断嵌套层数为0)
  • 判断中断嵌套层数以及锁
  • 找到最高优先级
  • 这个是通过函数OS_SchedNew函数根据优先级位图法对全局变量OSPrioHighRdy赋值实现的。
    • 这里就稍微介绍一下优先级位图法是如何找到最高优先级的。首先根据OSRdyGrp找到最高优先级所在的组(并赋值给y),然后根据OSRdyTbl[y]找到最高优先级的任务。
  • 判断最高优先级任务是否是当前任务,如果不是当前任务的话就进行任务的上下文切换

任务上下文切换

实际上就是将原任务的CPU现场保护在原任务的栈中,然后将新任务的CPU执行现场从新任务的栈中恢复出来(这里就可以讨论一下创建任务时进行的模拟压栈的作用了,实际上就是为了能够在任务上下文切换的时候进行统一处理)
上下文切换的概念:当内核决定要执行一个不同的任务时,操作系统就会把当前CPU的执行执行现场保存在当前任务的栈上,然后新任务的上下文将从新任务的栈中恢复然后继续执行新任务,这个过程被称为上下文切换
  • 上下文切换的主要步骤
    • 保存当前任务的执行现场(需要按照模拟压栈的顺序保存,也就是先保存PC,然后保存剩余的除了SP寄存器的通用,然后保存状态寄存器)
    • 需要注意的是在上下文切换函数中LR寄存器中存储的应该是PC的值,所以首先应该将LR寄存器压栈,然后再压入剩余的寄存器(这里还要再压入一次LR寄存器,是为了占位。这个时候压栈的时候PC与LR寄存器的值就是相同的了,但是作用是不一样的)
      • 但是实际上就算在中断的情况下LR寄存器中的值大部分时候也是没有意义的,因为LR寄存器会在一个函数进入的时候被保存到栈空间中,所以在中断上下文切换的时候虽然LR和PC的值不同,但是LR寄存器的值还是没啥用
    • 保存当前任务的栈指针到该任务的TCB中(也就是存储在TCB的首个字单元中)
    • 需要注意的点是使用LDR+等号伪指令的时候取出的是变量的地址
    • 将OSTCBCur切换为高优先级任务的TCB指针
    • 需要注意的点同样是:LDR+等号伪指令的时候取出的是变量的地址
    • 将OSPrioCur切换为高优先级任务的优先级
    • 需要注意的点同样是:LDR+等号伪指令的时候取出的是变量的地址
    • 获取新任务的栈指针
    • 也就是从高优先级任务的TCB中取出第一个字单元作为栈指针
    • 恢复新任务的执行现场
    • 只需要注意这里需要先将状态寄存器恢复到备份寄存器中(因为状态寄存器在保存现场时是最后一个入栈的),并且在恢复其他的寄存器时需要在LDMFD指令后面加上^以便在现场恢复的时候顺便将SPSR寄存器中的值拷贝到CPSR中。也就是下面的代码:

    调度点

    这里就直接把调度点都列举出来(实际上就是操作系统课上讲的调度点,对于这样可抢占的操作系统,就是当就绪队列发生变化或者当前任务不再占据CPU的时候就需要进行调度)
    • 操作系统启动
    • 中断退出可能改变了就绪队列。这里的中断尤其是时钟中断)
    • 新任务创建(改变了就绪队列)
    • 当前任务进入等待状态(当前任务不再占据CPU)
    • 当前任务进入冬眠状态(当前任务不再占据CPU)
    • 当前任务执行结束(当前任务不再占据CPU)

    临界区

    这里其实有三种方式能够实现临界区:
    下面注意介绍三个方式
    • 方法一:直接关闭中断,然后打开中断——但是这样没办法应对中断的嵌套
    • 方法二:将CPSR寄存器直接压栈,然后退出时出栈——可能会出现问题(手动压栈时会出现问题,导致栈没有办法还原)
    • 方法三:使用变量保存CPSR寄存器(这样与第二种方式几乎没有区别,只不过是将CPSR寄存器的值存放在函数的局部变量区域)
    疑问这个的细节会不会考?比如说第二种方式什么时候会出问题?(好像之前已经讨论过了,这个是跟编译器有关系的,并不会出现找不到函数参数的情况,因为经过反汇编之后会出现函数的栈帧指针,这样就能找到函数的参数了)。第三种方式应该是不会出现问题的,因为不会在同一个函数中出现临界区嵌套的情况

    同步、通信与互斥

    主要涉及到任务之间的协同机制。在课程中没有涉及

    内存管理

    主要涉及到内存管理机制,在课程中没有涉及

    中断与时间管理

    主要涉及到事件触发的机制以及时间触发的机制

    中断

    中断的分类

    • Interruption
      • Outside interruption(外部中断)
      • Hard interruption(硬件中断,或者说是异常)
    • Trap
      • Inside interruption(软中断陷入)
      • Soft interruption(这也是软件中断)
    • Exception(这个不就是内部中断吗。。。)

    2440裸板中断

    首先需要先明确一下这里将的是裸机情况下的中断(因为这里退出中断的时候并没有出现重调度
    notion image
    这里其实已经很明白了,其实就是跟ARM课上讲的是一样的,就是总共就只有七个中断(包括异常),但是这里快速中断只能有一个,而一般中断就是一组中断了。在一级中断向量表中就是存放一般中断的公共入口,而在公共入口之中将中断进行分发(这里前面的几条mov指令好像没啥意义?)
    另外需要注意的是,这里中断公共入口的最后一行是:sub pc,lr,#4,这里需要减4是因为这里是流水线的问题。
    因为当进行中断跳转的时候PC指向的是跳转指令的下两条指令(所以跳转的时候拷贝的LR也是下两条指令的地址),所以返回的时候需要将LR-4
    • 2440+uC一般中断的公共入口
    • 关于中断分发的代码如下:
      • 这里的INTOFFSET就是当前相应的一般中断在二级中断向量表中的位置(位置从零开始),而这个INTOFFSET相当于是一个内存中的变量。
        HandleEINT0是中断向量表中的第一个中断向量。而每一个中断向量都是一个32位地址(四个字节),所以通过左移两位来实现中断向量表的查找(也就是左移两位,乘四)
        二级中断向量表的定义如下:
        notion image
        所以这里能看见其实中断向量表就是一堆标号(或者说变量),而变量的值是实际的ISR的入口(从代码上看应该就是一个地址,但是在ARM课上讲的是这里存放的可能是一条跳转指令,包括在后面的ppt中也介绍了是一条长跳转指令。疑问这里到底是什么?一级表是一个长跳转,二级表是地址)。
        接下来能看看完整的中断服务程序公共入口(这个是裸机情况下的中断服务程序公共入口):
        需要注意的是这里并不是在末尾来根据流水线调整返回地址的,而是在公共入口的开始处先将LR寄存器减四了。
        从这里可以看见裸板中断的主要流程就是:
      • 硬件跳转至公共入口
      • 保存中断现场(是保存在中断栈中的,保存的内容跟任务切换时保存的内容差不多,但是并没有完整保存所有,因为这里的LR是不需要保存的)
      • 获取中断向量并跳转
      • 返回公共入口并恢复现场
      • 中断返回
      • 而对于一个具体的中断,并不一定所有的代码都直接在中断服务程序中体现了,有额外的函数跳转
        这里以OSTickISR中断为例,介绍一下在进入真正的中断处理事务之前还需要处理哪些东西
        (这里不知道开头的R5,LR有什么作用)这里可以发现实际上就是去操作了源挂起寄存器以及目的挂起寄存器,将一些中断的标志位清除了。这里的代码好像有点问题,就是他清除了所有的中断请求,并且这里MOV R1, R1, LSL #10的操作也是有点意义不明,因为根据ARM课上提供的手册,第10位是0号外部中断,没看太明白。
        notion image
        但是总而言之,只需要知道在真正中断程序的入口处也需要清除一下源挂起和目的挂起寄存器即可。
    至此裸板中断的全过程应该都梳理完了。关于裸板中断的整体流程,这里再复述一遍(顺便回忆一下)
    • 硬件将PC寄存器保存在LR寄存器中,并且跳转至一级中断服务程序处(对于除一般中断以外的中断,就是ISR;对一般中断而言,就是一个公共入口,下面就以一般中断为例)
    • 在中断服务程序入口中进行中断现场保存(不保存LR)
    • 获取中断服务程序的入口地址,然后跳转至中断服务程序
    • 在中断服务程序中需要清除中断标志位(源挂起、目的挂起)
    • 返回公共入口程序中恢复现场
    • 中断返回
    到这里其实只回答了廖老ppt上的四个问题:
    • P1: Where to return ?——返回PC-4的位置,这是因为流水线的存在
    • P2: How to distinguish different interrupts?——根据INTOFFSET寄存器进行区分
    • P3: How to save the information of the interrupted program?——通过二级中断向量表来存放不同中断服务程序的信息
    • P6: Is it an address in each vector?——不一定,按照代码来看是一个地址,但是按照廖老的意思应该是这里存放的是一条长跳转指令。包括在ARM课上讲的也是,这里存放的是一条长跳转指令。到底是啥?
        notion image
        认为这里存放的就是一条跳转指令吧,书应该写错了

    2440+uC中断

    其实有操作系统的中断与裸板中断两个最大的区别就是有操作系统的中断的CPU执行现场不能随意保存。一定要保存在当前任务的栈上,而不能像裸板中断一样将CPU执行现场保存在中断栈上。(当然如果能实现在退出中断之后程序还能执行一次调度的话,那么就根本不需要在中断中进行任务的调度了。但是可惜的是中断何时来根本不知道,就更不知道什么时候结束了,所以没有办法确定中断结束的位置并在该处进行一次任务的调度,所以就只能在中断中进行任务的调度了。需要注意的是,这里说的中断中其实是指在中断公共入口的末尾)
    但实际上也完全可以在中断公共入口现场恢复之后,还未返回之前进行一次相对特殊的任务调度(这是因为在中断的情况下PC和LR的值是不同的,所以主要不同的地方在上下文切换的汇编代码中),这样也完全能实现中断结束后调度任务,并且现场也是保存在任务的栈上的(也就是实现效果与PPT上的实现效果是完全一致的,性能应该是略胜于书上的)(这种情况就是将所有的参数先保存在中断的栈上,然后切换回SVC模式的时候只需要保存中断栈的栈指针信息,就可以直接在SVC模式下去操作中断栈,将栈中的成员弹出以便进行上下文切换。但是需要注意的是在进行当前任务的现场恢复的时候并不能一下子将所有的CPU现场都恢复了,至少中断栈中的LR寄存器不能恢复,因为如果恢复的话,那么跳转到上下文切换函数中的时候还是会被覆盖,所以只能在上下文切换函数中将LR寄存器弹出,然后执行之前的上下文切换。所以总的来说,中断的上下文切换和非中断的上下文切换还是有一点点区别的)
    关于有操作系统的中断服务程序公共入口的代码如下:
    这里主要对比裸板中断的流程来看。这里再复习一下裸板中断公共入口的流程:
    • 保存中断现场
    • 查询入口
    • 保存断点并跳转
    • 返回并恢复现场
    • 中断返回
    而结合上面的代码,由于ARM在不同模式下sp、lr指针是不同的,所以这里多了很多东西(因为不能简单将现场保存在中断栈上)。
    • 保存任务的PC指针以及状态寄存器(这同样是借用中断栈来保存的。需要保存这两个寄存器是因为这两个寄存器在不同状态下都是)
    • 切换到SVC模式保存除sp指针以外的现场(需要注意的是手动切换模式的话只会改变使用的sp寄存器以及lr寄存器,并不会改变其他寄存器的值。也就是不会执行SPSR自动恢复什么的)(关于SPSR自动恢复可以使用上面的那个带^的指令)
    • 中断嵌套层数+1(这个是为了操作系统维护才增加的)
    • 若中断嵌套层数为1,则需要保存当前任务栈指针(因为只有最外层的中断需要保存当前任务的现场)
    • 至此对应裸板中断的第一步——保存中断现场(只不过这个时候是将现场保存在了当前的任务栈上)
    • 切换回中断模式并计算中断向量地址
    • 对应裸板中断的第二步——查询入口
    • 保存断点并跳转
    • 对应第三步——保存断点并跳转
    • 返回公共入口
    • 切换为SVC模式并执行OSIntExit(在OSIntExit中首先会将嵌套层数-1,然后如果嵌套层数为0的话才开始调度。如果需要调度的话,该函数会找到最高优先级的任务,如果不是当前任务的话就会恢复最高优先级任务的现场)
    • 在这个函数中调用了OSIntCtxSw,这个函数相较于OSCtxSw只是没有保存现场的部分了,因为保存现场已经在中断程序公共入口中实现了
    • 如果当前任务仍为最高优先级任务,则恢复现场
    • 对应第四步——返回并恢复现场
    • 中断返回
    • 对应第五步——中断返回

    时间

    系统时钟的大小

    关于系统Tick的计算没什么好说的,就正常的分频、计数就好了

    时钟管理

    • 任务延时
    • 主要介绍函数OSTimeDly。同样的,介绍一下这个函数的主要步骤
      • 首先先将当前任务移出就绪队列(如果延时数大于0的话)
      • 然后将当前任务的Dly设置为需要的参数
      • 调度新任务(这个就相当于是当前任务放弃CPU的情况)
    • 不同的时间线
      • 绝对时间
      • 差分时间。记录与上一个任务相差的时间(也就是需要比上一个任务多延时几个tick)
    • 关于绝对时间和差分时间,如下图:
      • notion image
        绝对时间为上面的链表,差分时间为下面的链表(其实这个已经涉及到调度策略了,因为完全可以将所有的需要延时的任务都排序组织起来,然后使用上面的两种链表来存储)
        如果使用绝对时间的话,执行Tick的时候就需要遍历链表,但是链表不需要排序
        如果使用差分时间的话,执行Tick的时候就只需要操作第一个TCB的Dly。但是链表需要排序。并且插入的时候需要修改后面一个节点的Dly值
    • 看门狗时钟
    • 系统时钟中断
    • 这里主要介绍的就是OSTimeTick函数(Tick本质上也是一个中断,而OSTimeTick函数就是一个中断服务程序)。同样的,介绍一下这个函数的主要流程
      • 这个函数主要任务就是遍历所有的已有任务并修改Dly的值
      • 将符合唤醒条件的任务加入就绪队列(涉及到就绪队列优先级位图相关的操作),所以这个时候也会进行任务调度(需要注意的是这里并不是在Tick函数中进行调度的,还是在中断公共入口中进行调度的)

    Hardware System

    这一章里面最重要的应该就是后面的代码拷贝部分
    这里就稍微扫一遍截几张我觉得比较重要的图:
    notion image
    上图是关于各个中断的返回时的指令(涉及流水线)
    notion image
    这个是有讲过的,关于循环展开啥的,就能使流水线更流畅
    notion image
    这个是协处理器指令
    看到53页,这里比较重要,是有关一些拷贝代码的部分
    notion image
    上图是什么意思?好像是中断向量表有两张的意思。解决确实有两张中断向量表,一级中断向量表中存放的是跳转指令,而二级中断向量表中存放的是ISR的地址。这段代码应该在启动文件中,并且上面的这两张表合起来才能被称为中断向量表(跳转表+注册表)
    另外,这里的LDR PC,地址实际上就是从该地址处取出值存放在PC中(这里的LDR指令就不是伪指令了),而VECTORTABLE中存放的就是ISR的地址。当一个中断或者异常发生了,这里就会跳转到Hal那个表中对应的中断向量(所以这个HALVECTR_START才是真正的中断向量表,下面那张表就只是在注册中断,或者说像是一个文字缓冲池,如果没有注册表的话,标号跳转又需要使用ADR指令,而ADR指令又不能将地址读取到PC寄存器中)
    这里使用LDR指令+注册表的形式就相当于是手动实现了LDR伪指令的PC长跳转功能
    • 代码拷贝
    • 代码通常存放在flash中以便长久保存,但是他又不适合执行代码(指nandflash),所以这里需要将代码拷贝到RAM中执行(下面给出的例子是norflash代码拷贝。廖老上课说的是如果拷贝了的话,norflash就能用来做更重要的事情)
      • notion image
        这里的拷贝循环其实没有什么需要注意的,只需要注意这里获取ResetHandler的地址是通过ADR指令获取的
        这是因为:
        总的来说,ADR 和 LDR 伪指令的主要区别在于它们加载到寄存器中的值:ADR 加载的是一个相对地址,而 LDR 加载的是一个立即数或者一个绝对地址。
      • ADR r1, ResetHandler:这条指令的目的是获取 ResetHandler 的地址。由于 ResetHandler 是一个标签,它的地址是相对于当前指令的。因此,使用 ADR 伪指令可以直接计算出这个相对地址。
      • LDR r2, =text_startLDR r3, =bss_start:这两条指令的目的是获取 text_startbss_start 的地址。这两个地址通常是在链接器脚本中定义的,它们表示代码段和数据段的开始位置。由于这些地址是绝对的,而不是相对于当前指令的,因此需要使用 LDR 伪指令来加载这些地址。
      • 因为在拷贝代码之前ResetHandler是存放在flash中的,而此时PC在flash中取址,所以此时所有的在flash中的标号都应该使用相对寻址获取(所有的标号函数在编译的时候都是一个相对地址,而在经过连接之后就会变成一个绝对地址。但是就算是绝对地址也还是使用ADR指令来获取,因为如果使用LDR指令的话就要在缓冲池中存储这个标号的地址了)
        而text与bss都是在连接脚本中指定的绝对地址,所以这里需要使用LDR指令获取
        说白了就是,ADR指令用于获取相对地址(如函数名,标号啥的);而LDR指令用于获取立即数或者绝对地址(如链接脚本中指定的标号)
        或者换一句话说,LDR是完全根据编译后生成的绝对地址确定的标号地址(就是完全相对于链接脚本中指定的运行域来确定的),而ADR指令是要区分当前代码的位置(就是需要关心当前代码是在运行域还是加载域的,在这两个域上使用ADR指令的结果是不一样的。所以如果运行域与加载域完全相同,那么就会出现LDR指令与ADR指令计算地址相同的情况)
    • 中断向量表拷贝
    • 首先需要知道的是,中断向量表的拷贝是在代码拷贝之后的(从连接脚本中看到的,32的拷贝是先拷贝了数据段,然后再将中断向量表放在最低地址处)
      • 经过代码拷贝之后,加载域中的代码就被拷贝到运行域了,这个时候LDR与ADR指令获取的地址就是完全相同的了(此时PC应该也跳转到了运行域中),这个时候就可以直接使用LDR指令了:
        notion image
        需要注意的是这里还多了一步判断,就是判断中断向量表是不是已经被放在0地址处了,如果是的话就没有必要再次进行拷贝了,就可以直接执行real_code了。至于拷贝的代码,就没什么好说的了

    Software System

    boot与loader

    boot

    boot主要执行的任务就是进行初始化,如初始化RAM之类的硬件

    loader

    loader主要执行的任务就是将代码从加载域拷贝到运行域

    ARM的启动过程

    notion image
    我认为的过程是,在初始化了内存之后应该建立映射,也就是将Flash的地址映射到0x00000000,然后在代码拷贝结束的时候再取消映射(所以我感觉上面的load和前面的去取消映射的顺序反了)
    从上面的顺序中也能看出来,拷贝代码是在拷贝中断向量表之前执行的
    疑问
    这个要不然直接背下来吧。。。
    下面按照这个步骤来将ARM的启动流程

    关闭中断

    notion image
    就是向CPSR寄存器中写值,没什么好说的

    关闭内存管理单元以及缓存

    这里需要用到协处理器p15
    notion image
    协处理器指令,p15 协处理器是用于管理内存的,所以关闭cache等操作需要使用p15协处理器
    可以总结出:协处理器p15中的c7寄存器是用于存储缓存相关信息的;c1寄存器是用于使能内存相关设备的;c5指令码表示的是关闭指令缓存;c6指令码表示的是关闭数据缓存

    处理协处理器

    相当于是协处理器的初始化
    notion image
    就只有这一张图片,只需要知道如果这个代码是在非主核上面跑的话,就会跳转到sec子函数中执行。
    疑问这段代码算是哪个阶段?

    内存初始化以及取消重映射

    内存初始化是硬件和boot完成的。RAM存储器需要在上电时设置刷新频率等,所以需要先初始化RAM。
    取消重映射我认为是在加载镜像之后,所以这里我也不介绍了

    加载镜像

    我这里就认为这个加载镜像的意思就是代码拷贝。这个是比较重要的部分,涉及到ADR指令以及LDR指令
    没有取消映射的时候会向两个地方都写入值吗?疑问我想的是只会在一个地方写值
    关于加载镜像,其实只要啃下来下面这张图就好了:
    notion image
    首先需要先理解一下这个映射,按照ARM课上的说法,实际上就相当于是将boot RAM搬到了0x00的位置(所以就是只映射前4k),并不是将所有的内存地址都进行了映射,而是只映射了boot RAM的4k部分,所以其他的地址就都认为是恒等映射(如这里的text等)
    下面介绍一下加载镜像的主要步骤(这里省去了初始化flash的部分。这部分代码应该也是在boot ram中实现的):
    • 首先根据dummy来判断当前的代码是否是加载域与运行域分开的(通过ADR与LDR指令实现)
    • 上图中其实就是运行域与加载域分开,因为ADR指令取出的地址是0x4,而LDR指令取出的加载域地址是0x0,所以这里就需要进行镜像拷贝
    • 如果当前代码并不处在加载域中,那么就需要将代码从加载域(ADR Reset)拷贝到运行域(text_st)
    • 这些实际上就已经不是虚拟地址了,而是物理地址,所以可以直接使用
    • 拷贝结束之后就可以取消地址映射然后跳转到RAM中执行了
    当然,上面考虑的是boot ram的4k够用的情况,如果不够用的话还需要跳转到内存继续执行启动(但是这个部分在廖老的ppt上好像没有体现)
    notion image
    这页ppt是不是错了。。。应该是40变成06,而不是40变成00

    拷贝中断向量表

    代码拷贝结束之后代码就完全位于运行域中了,所以这个时候就可以直接使用LDR指令获取地址,不需要使用ADR指令获取地址了
    notion image
    所以这里获取标号就可以直接使用LDR指令了
    同样需要注意的是这里需要判断一下中断向量表是不是已经被存放在0x0的位置了(就相当与是前面dummy的作用)
    接下来就是使用后变址去拷贝中断向量表(后变址的指令格式稍微记一下,这里是没有!号的,并且中括号没有包含立即数)

    初始化栈空间

    notion image
    这里实际上就是通过切换模式将当前的sp指针切换为对应模式的sp寄存器,然后再向sp寄存器中写值(预先分配的栈地址)即可

    清零bss段

    清零bss段主要是因为语言要求所有未初始化的静态变量以及全局变量都应该为0,所以这里需要将bss段清零。
    notion image
    上图是主要的清零代码,实际上跟拷贝代码的操作很像,只不过这里改成向特定地址写0了

    x86相关

    全局描述符

    Descriptor是一个x86全局宏定义,接下来第一个字段表示的是这个内存段的基地址,第二个字段表示的是这个内存段的大小,第三个字段表示的是这个内存段的权限
    可以发现这里待填的只有内存段的基地址了
    需要注意的是,这里的标号LABEL_GDT只是为了标志全局描述符表的开始
    这里就是在计算全局描述符表的相关信息(这段代码紧跟上面的代码)
    这里的$表示的是当前地址,LABEL_GDT记录的是全局描述符表的开始地址,这样就能计算出长度了。
    而计算长度是为了得到GdtPtr。GdtPtr是一个48位的指针,前十六位存储的是长度信息,后面32位存储的是GdtPtr的线性基地址(这里就先不用管什么是线性基地址了,就当成物理地址就好了)
    notion image
    这段代码是在计算各内存段在全局描述符表中的偏移量(这段代码是紧跟上面的代码的)
    下面还有一段汇编代码:
    这里要将代码段寄存器左移4位是因为cs寄存器中存储的是代码段基址/16的结果。(所有的段的基地址都是16字节对齐的)
    ax是eax寄存器的低16位
    选择子虽然是一个相对于gdt的偏移量,但是这里跳转到选择子的过程中处理器做了额外的工作,他会查找对应的gdt表项,然后开始执行该全局描述符所代表的段
    软件工程与实践前端学习
    Loading...
    Noah
    Noah
    永远年轻,永远热泪盈眶
    公告
    ❗❗复习笔记问题❗❗
    由于兼容性问题
    导入md文件可能导致了一些格式错误
    🌹如发现格式错误,请联系我~🌹
    🌹如博客内容有误也欢迎指出~🌹