type
status
slug
date
summary
tags
category
password
icon
概念基础部分什么是嵌入式系统?嵌入式系统与通用计算机系统的差异是什么?一般RTOS内核实现了哪些基本功能?针对不同的应用,RTOS还提供了哪些可扩展功能?什么是任务?任务运行过程中可能处于哪些状态?任务和线程的区别?任务是:状态有:任务和线程区别任务调度机制任务的调度点嵌入式实时操作系统内核任务块TCB的设计为什么栈的初始化要先于TCB的初始化就绪队列的实现OS_SchedNew()任务切换中断经典问题:如果uc/OS II 正在运行,如何处理IRQ?(中断的产生及相应)2440里面中断进入的时候硬件会进行的操作如何在mini2440和ucosII环境下实现中断注册关于异常向量表和中断向量表(2440)和裸板中断对比:三种关中断(进入CRITICAL的方式)第一种方式(暴力直接,无法支持嵌套)第二种方式(手动用栈push操作保留上个cpsr状态,可支持嵌套,但是不符合函数调用标准)第三种方式(最优,没有危险的改变sp指针,利用了参数传递,以及由c编译器来管理栈的布局)时钟OSTimeDly()OSTimeTick()数据结构优化(tick部分)OS,启动硬件部分7种模式涉及的寄存器:CPSR布局装载代码boot环节问题为什么要取消remapping:对于boot部分代码的解释把取消重映射放置到这个位置是否不影响呢?GDT实模式和保护模式的切换arm2440启动流程代码补充关中断代码MMU和caches关闭Handle secondary mpcores
概念基础部分
什么是嵌入式系统?嵌入式系统与通用计算机系统的差异是什么?
嵌入式系统是一种专用的计算机系统,以应用为中心,以计算机技术为基础,软硬件可配置,对成本、功耗、可靠性、体积、功能有严格的约束
- 相对于通用计算机系统来说,嵌入式系统是为特定的应用而设计的
- 具有更高的可靠性和稳定性
- 嵌入式系统的软硬件资源有限,功耗低,集成度高
- 嵌入式系统的软件程序存储在芯片上,开发者通常无法更改
一般RTOS内核实现了哪些基本功能?针对不同的应用,RTOS还提供了哪些可扩展功能?
- 任务管理
- 中断管理
- 时钟管理
- 任务协调/调度
- 内存管理
- 可扩展功能:嵌入式网络、嵌入式文件系统、功耗管理、嵌入式数据库、流媒体支持、用户编程接口、GUI图形界面
什么是任务?任务运行过程中可能处于哪些状态?任务和线程的区别?
任务是:
任务是一个程序运行的实体、资源占用的基本单位,是相互作用的程序集合或软件实体。任务具有动态性、异步独立性、并发性
状态有:
睡眠(dormant)—>(通过taskcreate)就绪态(ready)—>(通过sched sched_new OSCtxSw)运行态(running) —>(通过OSTimeDly或者suspend) 等待态(waiting)
还有中断服务状态(ISR) 由运行态的时候遇到中断进入,退出的时候记得调用OSIntExit
任务和线程区别
在ucosII中,一个任务也称作一个线程,与线程相对的概念是进程,RTOS只实现了多线程。线程之间是共享地址的,进程之间不能相互访问对方的变量,一般通过地址保护和虚拟地址来实现。
任务调度机制
调度是RTOS的核心功能,用于确定多任务环境下任务执行的顺序和在获得CPU资源后执行时间的长度。
- RTOS为确保调度过程的正确实施,提供了基本调度机制、任务协调机制、内存管理机制、任务处理机制
任务的调度点
- 中断服务程序结束位置
- 运行任务因缺乏资源而被阻塞(如信号量机制里面,资源不够)
- 任务周期的开始或者结束时刻(OSTimeDly来做的周期)
- 高优先级任务就绪时刻(调度由systick心跳这个中断的OSIntExit来触发)
嵌入式实时操作系统内核
红色部分是重点,需要熟悉流程
任务块TCB的设计
OSTCBTbl
是给所有的TCB进行分配空间,用的栈空间,通过这种方法进行静态分配,而为了后续TCB的动态使用删除,我们有了一个OSTCBFreeList
,维护了一个TCB链表(只用了TCB里面的OSTCBNext
字段)保存空闲TCB(最开始所有的TCB都是空闲TCB),由OS_InitTCBList()
函数创建,它同时也创建了一个空链表:OSTCBList
,这个链表是存储已经分配初始化后的OS_TCB的指针(每次创建一个task,我们使用函数OS_TCBInit
,成功的话,它里面就会把从空闲链表里面取出的ptcb加入到这个存储已初始化的TCB的链表:OSTCBList
),它这个链表是双向链表(用了TCB里面的OSTCBPrev
和OSTCBNext
字段),和OSTCBFreeList不一样(只用了OSTCBNext
)。而OSTCBprioTbl
就是存放已经分配的TCB的指针的数组,它的主要作用是用于后续通过优先级得到对应的OS_TCB*
,它同时也可以像OSTCBList
一样表明它里面非0的内容的索引(对应着prio)是已经分配给具体任务的TCB的优先级,这些TCB就不会存在于空闲TCB链表里面,在函数OS_TCBInit
里面(这个函数在OSTaskCreate
中被调用,用于对任务进行初始化—设置优先级、设置堆栈(由前面的OSTaskStkInit
完成了模拟压栈并放入了任务的Entry Point)),可以看到会从OSTCBFreeList
中取出一个空闲TCB,把它放入对应的(根据优先级)OSTCBprioTbl,来完成TCB的分配。注意,在
OSTCBinit
里面链表的插入方式(插入到OSTCBList
)是前插法!流程图来自陈学长,但是有误:
最后调用完OSTCBInit后,判断有无error,有的话就OSTCBPrioTbl[prio] = 0(取消分配,但是没有error就一定是成功分配的,不是判断OSRunning)。
接着继续判断OSRunning,如果running就进行OS_Sched()进行任务调度,否则就不进行OS_Sched(),但都不会去修改OSTCBPrioTbl[prio]的值。
可以方便记忆叙述为:
进临界区→判断是否有中断嵌套→判断有没有任务占用优先级→堆栈初始化(模拟压栈,顺序是(地址由高到低):PC,LR,R12-R0,CPSR)→TCB初始化(涉及到从空闲链表取出一个TCB进行初始化:给
OSTCBStkPtr
赋值、优先级相关的赋值(OSTCBX
、OSTCBY
、OSTCBBitX
、OSTCBBitY
)、就绪状态OSTCBStat
,注册到初始化TCB链表(OSTCBList
),更新就绪队列(OSRdyTbl
)以及伴随的就绪组(OSRdyGrp
))→启动任务重调度(OS_Sched)(前提是OSRunning
并且创建TCB没有错误)为什么栈的初始化要先于TCB的初始化
因为任务的堆栈指针sp的值也会存储在TCB里面,如果TCB的初始化在堆栈前完成,由于模拟压栈的存在,在栈初始化之前存入TCB的sp的值必定不对,这样在栈初始化完成后,还需要再主动的更改TCB中保存的堆栈指针值,显然是多余的并且实现起来很麻烦。所以应该让堆栈初始化先于tcb初始化。
就绪队列的实现
就绪队列的物理对应是
OSRdyTbl
,不过多加了一个OSRdyGrp
辅助查找最高优先级的就绪TCB的优先级的过程,得到OSPrioHighRdy
后,能通过我们的OSTCBPrioTbl
这个OS_TCB *的数组(也就是OS_TCB的指针数组),根据OSPrioHighRdy
优先级号作为索引得到对应的TCB的指针OSUnMapTbl
很重要,用于找到一个8位数的第一个1(最低)的出现索引(从0开始算)看
OS_Sched
函数,在判断完是否中断嵌套以及锁嵌套后,就会通过OS_SchedNew
()来更新全局变量:OSPrioHighRdy
它是目前就绪队列里面优先级最高的任务(注意这里需要判定是否就绪队列里面优先级最高的任务就是当前的任务OSPrioCur
),如果是当前任务,就不要进行上下文切换切换任务OS_SchedNew()
而
OS_SchedNew()
的实现很简单(对于64个优先级而言),依靠OSUnMapTbl
,先是找行(y),从OSRdyGrp
(INT8U)里面找到最高优先级行索引(索引从0开始),然后从位图(OSRdyTbl[OS_LOWEST_PRIO/8 + 1]
)里面取出对应行的INT8U,然后同样利用OSUnMapTbl
找到最高优先级列索引(从0开始),最后进行拼接,行索引右移3位加上列索引就是优先级。就绪队列,你能看到,其实就是
OSRdyTbl
这个位图(一个INT8U的一维数组,而从位的角度,每个数组元素又会给它增加一个维度,从而变成一个位图),不过我们搭配了OSRdyGrp
来记录它们行(也就是每个INT8U元素是否为0),加速对就绪队列里面的最高优先级元素的查找。OSUnMapTbl[256]是一个256列的一位数组,索引为i(不超过255,也即是8位)对应的OSUnMapTbl[i]表示i的二进制表示下为1的最低位的索引(从0开始计数)。也可以说是:将0x00~0xFF中的每一个数的最低位为1的位数列出来。
任务切换
感觉可以梳理一下流程,核心是ppt这张图:
- 保护现场:保护寄存器:R0-R12 、 LR 、PC(这里主动切换的话和LR相同)还有CPSR
- 将保护好现场,压好栈的最新sp放入OSTCBCur的OSTCBstkPtr字段(也就是第一个)
- 从OSTCBHighRdy这个TCB里面取出切换任务的stack字段放入sp(也就是SP=
OSTCBHighRdy→OSTCBStkPtr
),准备恢复现场,并且设置OSPrioCur
=OSPrioHighRdy
,设置OSTCBCur
=OSTCBHighRdy
- 恢复切换任务的R0-R12、LR、PC以及CPSR,完成恢复现场,切换任务执行
上面四个步骤在
OSCtxSw
中完成,如果是
OSIntCtxSw
的话,那么第一个步骤以及第二个步骤是由统一的中断服务处理程序完成的,见下图👇:补充叙述一下这里的保存上下文的思路:
准则是:我们需要保存pc(返回任务执行的点)、LR、R12-R0、CPSR这16个寄存器到任务的栈sp里面,并且将存好的sp放入对应任务的
OSTCBStkPtr
里面,而进入中断的时候是在irq模式,任务的sp是在svc模式,这里就需要转换模式,但是一些信息已经跑到irq模式了(比如被中断的任务的pc和当时的cpsr信息都在irq模式,需要用统一的寄存器传递出来,而寄存器本身也需要保存,所以就用irq的栈来保存寄存器原来的值),所以代码里面:先将R0-R2入栈,R0保存irq下的sp指针(用于在svc模式下访问),这个时候就可以恢复irq的sp了我们只需要存入堆栈,不用让sp继续跟踪,用R0跟踪位置即可,R1用于保存LR-4对应的任务返回PC值,R2用于保存任务被中断时的cpsr(从irq模式下的spsr获得,因为中断的时候将其保存到spsr然后再修改了cpsr)
而后切换到svc模式进行入栈操作,先R1入栈保存返回的PC,然后是LR R12-R3入栈,(R0-R2的内容在irq堆栈里面),然后R0作为irq的sp索引,出栈到R3-R5(对应R0-R2),然后入栈R2-R5(R2存储的是中断前的cpsr),从而完成任务栈的保存,最后将任务sp更新入
OSTCBCur→OSTCBStkPtr
里面,完成上下文保存。所有任务的上下文在栈的布局如下:
统一的中断处理程序里面压栈后,执行第二个步骤将
OSTCBCur→OSTCBStkPtr
=sp的操作和非中断的OSCtxSw
有不同,在于这里判定了是否中断嵌套,如果中断嵌套了的话,只是仍然将被中断的上下文保存到原始的任务的堆栈里面,但是这个上下文实际上并不是任务的上下文,所以不用更新到OSTCBCur→OSTCBstkPtr
里面ps:这里可以注意一个恢复现场的细节:
LDMFD SP!,{R0}
MSR SPSR_cxsf,R0
在恢复cpsr的时候,是把内容填充如spsr_cxsf里面,由指令
LDMFD SP!,{R0-R12,LR,PC}^
这个指令包含PC并且有^,所以会恢复spsr内容进入cpsr,这样同时实现寄存器和模式的切换
我结合
OSIntExit()
来说:(ps:OSIntExit的实现代码很大部分和sched
加上sched_new
一样,原因是它的大部分功能是执行任务切换(少部分是更新OSIntNesting
),但它并没有直接调用sched,因为它需要调用OSIntCtxSw
而不是OSCtxSw
)先判定是否OS跑起来,然后修改OSIntNesting(-1)(注意enter critical需要),然后判定是否有中断嵌套或者锁嵌套,之后操作和
sched_new()
一样,查找到最高优先级的就绪任务:OSTCBHighRdy
,不过需要像sched()
一样,判定OSTCBHighRdy
是否和OSTCBCur
一样,不一样的话需要调用中断的上下文切换(OSIntCtxSw
)这就有必要开启我们下一节:中断!
中断
经典问题:如果uc/OS II 正在运行,如何处理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(保存的cpsr)入栈(svc任务栈)
- 更新中断嵌套OSIntNesting计数,然后判断是否在中断嵌套里面,如果没有嵌套则:
- 将sp保存入当前任务的TCB记录:
OSTCBCur→OSTCBStkPtr
里面
- 恢复cpsr为irq模式,也就是进入irq模式,取得INTOFFSET值来区分中断源
- 计算得出IRQ入口,保存到PC,执行ISR。
- ISR完成后,切换到svc模式,先调用OSIntExit进行可能的任务重调度以及更新OSIntNesting
- 最后是恢复现场(完成任务的恢复执行)
- 中断被定义为导致程序正常运行流程发生改变的事件
- 中断被分为硬中断、自陷、异常
- 硬中断(外部中断)是由于CPU外部原因而改变程序运行流程的过程
- 自陷(内部中断)表示通过处理器软件指令,可预期地使CPU正在执行的任务流程发生改变
- 异常:CPU自动产生的自陷,以处理特定的异常事件
2440里面中断进入的时候硬件会进行的操作
- 保存被中断程序当前的pc到LR_irq中(也就是中断返回后应该执行的下一条指令位置,所以中断里面对他-4操作再保存到任务的栈的pc位置)
- 将程序当前状态寄存器CPSR的值放入相应模式的SPSR(也即是irq的spsr)(用于中断返回时的恢复)
- 切换处理器模式为irq模式,也就是将CPSR的模式位设为相应的中断模式,并禁用相应模式的中断。如果是快中断模式,则禁用所有中断。
- 通过异常向量表找到irq应该进入的处理程序地址,放入pc中实现跳转
如何在mini2440和ucosII环境下实现中断注册
通过查找
INTOFFSET
寄存器和HANDLEINT
地址,通过计算:INTOFFSET
*4+HANDLEINT
得到对应的中断服务程序地址存放的位置INTOFFSET
是2440芯片一个用于中断管理等功能的寄存器,发生中断时,用来存放中断源分配的一个整数,这个整数唯一对应一个中断源。HandleEINT0
代表的是一个内存地址,其内容是对应中断的中断服务函数入口。见上图👆,每种中断服务函数入口的地址为32位,占据了4个字节,所以我们在计算每一种中断源对应的服务函数地址的时候会把INTOFFSET左移两位(乘以4)后再与起始地址HandleEINT0相加得到具体的中断服务函数入口地址。
关于异常向量表和中断向量表(2440)
异常向量表全部是汇编语言的跳转指令,这些指令从内存的0地址开始,连续存储在内存中。当发生对应的异常时,PC将通过硬件机制跳转到相应异常在异常向量表中的地址开始执行。因此当产生异常时,所有跳转的地址都是在CPU芯片生产时就确定且无法更改的,并且这些跳转指令都是独立的跳转指令,无法在原地实现中断服务,这也是异常向量表中的代码全是跳转指令的原因。这样,当产生异常时,通过硬件跳转到一个确定的地址,再通过跳转指令跳转到一个异常处理的代码的起始地址。
和裸板中断对比:
裸板没有操作系统,没有任务一说,栈就在统一的区域,进入中断,同样要保护现场,就把所有寄存器:R0-R12 SP(R13) LR(R14)-4(对应返回的pc) CPSR保存入栈,特别强调,这里其实是并没有保存sp的!(因为没有任务的概念,sp的区分只在于异常模式和svc模式),然后处理完中断后,会相应恢复现场,从栈里面弹出这些寄存器的值,这个期间,是支持中断嵌套的,整个过程都是在IRQ模式下进行。那没有保存sp有影响吗?没有的,因为sp的改变仅仅是用来存放了中断时的上下文,而恢复现场后,sp的值也自然的由于出栈就恢复到中断之前的值,所以不需要单独保存。
而UCOSII上,每个任务都有一个任务栈,而进入中断模式,也有irq对应的R13_irq,专门的异常处理堆栈,这个时候的现场保护,需要进入svc模式压栈(压入对应任务栈),需要保存跳入的(LR-4)(对应返回的pc,为R14_irq),R0-R12,在svc模式下的LR。这里虽然也没有保存sp在栈里面但是把它保存到了任务TCB对应的字段
OSTCBStkPtr
,这是因为需要恢复任务的时候知道任务的栈在哪里,并且还多保存了在svc里面的lr,这是不同。然后进入IRQ模式,进入对应中断服务程序,并且服务程序返回后,需要调用OSIntExit()
来出中断(涉及中断嵌套计数以及可能的任务重调度)。再返回(由以后的任务重调度返回或者没有发送任务重调度),执行恢复现场操作。中断服务程序运行在IRQ模式下,而uc/OS II 运行在SVC模式下,由于不同模式会用到不同的堆栈寄存器,因此uc/OS II任务的上下文会保存在SVC模式下的SP指针指向的堆栈中
解释返回地址的问题:
这里解释一下一个上课常问的点:为什么保存了两个LR
因为第一个LR-4是在irq模式下,是进入中断的时候,我们本来程序应该接着执行的指令地址,所以是一个PC,而第二个LR(不需要-4),是本来被中断程序的LR内容(svc模式),这个LR是程序可能是一个函数调用,是它要返回的地址,所以要保存两个。但是在
OSCtxSw
里面,我们仍然压栈了两个LR,而且值是一样的,是因为OSCtxSw
是主动切换,是任务执行的时候主动的函数调用,它就是一个函数被执行,所以LR的值(svc模式下),和任务切换回来后应该执行的pc的值是一致的,也不需要-4。但是之所以冗余的存下两个LR,(前一个代表返回地址pc,后一个代表LR值),就是为了保证任务的上下文的栈结构是一致的,这样OSCtxSw/OSIntCtxSw就是用统一的方式恢复任务上下文。三种关中断(进入CRITICAL的方式)
第一种方式(暴力直接,无法支持嵌套)
第二种方式(手动用栈push操作保留上个cpsr状态,可支持嵌套,但是不符合函数调用标准)
第三种方式(最优,没有危险的改变sp指针,利用了参数传递,以及由c编译器来管理栈的布局)
时钟
OSTimeDly()
先是我的远古记忆感受:
就是TCB里面有个delay的设置,记录了要等多少tick,tick是systick的计数,systick在我们f401里面是挂载在AHB上(84Mhz),一次systick中断就会让tick-1,当达到0时,会判定任务是否被挂起或者等待着某个事件,如果没有的话就直接恢复放到就绪队列里面就行了,调用OSTimeDly的时候就是把任务从就绪队列里面移除,设置它的
OSTCBCur→OSTCBDly
字段为传入的参数值,最后进行重调度。大概就是这么多,我们再稍微仔细看看
代码里面看起来就是先判定是否中断嵌套,是否锁嵌套,然后如果设置了有效的延迟(ticks大于0),然后就是从就绪队列里面移除任务的操作,主要是先把
OSRdyTbl
这个位图里面对应任务那个位置零,再看是否需要更新伴随的OSRdyGrp
(按照行来分组,也就是从0号任务开始,每8个任务为一组)是否需要清零表明这一组(行)的任务都不在就绪状态。最后设置任务的OSTCBDly
字段为需要等待的ticks,这个是每次在systick
中断(在stm32f401的移植里面)处理程序里面的OSTimeTick
函数里面会更新的,做完以后会主动调用重调度切换任务执行。于是乎,我们顺势讲到
OSTimeTick
:OSTimeTick()
插播一个ARM 2440的中断服务程序的不一样的操作:
它只需要清除中断相关的两个寄存器(SRCPND、INTPND)的对应位然后转入OSTimeTick即可:
对应代码我给一下:
解释一下:
- 首先得到Timer0在中断源寄存器SRCPEND中的bit位置(0x400);然后将中断源寄存器SRCPND中的值与0x400进行或操作,并将值写回,达到清中断(pending)的目的。
- 同理,将INTPND寄存器(只有一个待响应的中断处于挂起状态)的值读出再写回,清除挂起状态。最后跳转到OSTimeTick()
跳转到
OSTimeTick()
函数执行,同样的,先来个远古印象回忆:OSTimeTick
是在systick/OSTickISR里面被调用,也就是每次系统时钟中断的时候会调用这个函数进行核心的中断服务处理,它(应该是在systick
里面而不是OSTimeTick
)会记录中断次数到一个全局变量Time,并且遍历OSTcbList
(也就是使用的TCB链表),对于每个TCB去判定是否OSTimeDly
非0,非0说明被阻塞延时了,OSTimeDly
表示仍然需要等待的tick数,这个时候需要更新它(-1操作),然后进一步判断此时是否OSTimeDly
为0,为0说明等待时间到达,如果达到并且任务没有被suspend
或者没有等待某个事件,那么就把该任务放回就绪队列,最后在整个中断服务程序(systick
/OSTickISR
)返回之前,会调用UC的OSIntExit()
函数更新OSIntNesting
以及进行可能的任务重调度(OSIntCtxSw
)。OSTimeTick的核心代码:
数据结构优化(tick部分)
RTOS需要内核尽可能快地对外部事件作出响应,而实时性通常与确定性密切相关。
这里我们介绍一个差分时间等待链的方式来确定化时间。
上面的tick更新方式是一个时间等待链,需要对每个任务都遍历,更新他们剩余的等待时间,这个时间开销会随着任务的增加而增加,时间就不能是确定的。而差分时间链表的思路是:
保存第一个任务为队首,是最先会被执行的任务,然后第二个任务的delytick数是和前一个任务的时间差,也就是前一个任务结束后,第二个任务还需要等待的时间,同理对于第三个任务,是对前一个任务(第二个任务)的时间差,依次类推。。。这样的好处是,在更新任务tick时间的时候,只需要对这个差分时间链表的队列头部进行-1操作,当减到0时,就从等待链里面取出,后续节点成为新的头部结点被激活,然后继续上述操作。这样每次都不需要更新等待链中的其余结点,减少计算开销,整个时间是确定的。不过有个问题是:每次新增等待任务加入差分时间等待链链表的时候会需要同时修改它的后一个任务的delay的时间,不过这个操作同样也是固定时间的。
OS,启动
这里不能调用sched,但是和sched很像,主要区别在于这里调用的是OSStartHighRdy进行“任务切换”(切换到一个存在的最高优先级的就绪队列里面的任务),而shced里面是当需要任务切换时,调用OSCtxSw。
OSStartHighRdy不同在于,它不会保存之前任务的上下文(因为之前没有任务),其他的就是:
- 将OSTCBCur=OSTCBHighRdy
- 将OSPrioCur=OSPrioHighRdy
- 将SP=OSTCBHighRdy→OSTCBStkPtr
- 通过SP恢复任务上下文(之前每个TCB执行TCBStkInit的时候都进行了模拟压栈),恢复寄存器的值以及pc指向任务的执行程序
除了这方面外,我们移植的时候OSStartHighRdy还会设置pendsv中断优先级(因为我们的OSCtxSw用到软中断实现),还会设置MSP(用于全局的异常处理堆栈),设置OSRUNNing。
其实核心部分还是和OSCtxSw差不多的。
硬件部分
7种模式涉及的寄存器:
CPSR布局
f: flag region
s: situation region
x: extension region
c: control region
- SVC(Supervisor)模式:0b10011,十进制表示为19 16进制是:0x13
- IRQ(Interrupt)模式:0b10010,十进制表示为18 16进制是:0x12
- FIQ(Fast Interrupt)模式:0b10001,十进制表示为17 16进制是:0x11
装载代码boot环节
问题为什么要取消remapping:
地址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操作
对于boot部分代码的解释
ADR r1, ResetHandler
LDR r2, =text_start
LDR r3, =bss_start
copyloop:
LDR r0, [r1], #4
STR r0, [r2], #4
CMP r2, r3
BCC copyloop
这段代码是将flash区域的代码搬运到ram区域(ram区域对应的代码段区间是text_start到 Bss_start),因为我们的代码部分正常运行的时候应该运行在ram里面(和我们综设采取的在norflash直接运行不一样),
这里使用ADR R1,ResetHandler(相对pc寻址)是得到在运行时(此时boot部分运行的时候是在Norflash里面执行的代码)的代码地址(因为我们的目标是将存储在flash里面的代码搬运到ram里面)。
不过再补充一下后面说取消重映射的过程,这个过程在廖老师的ppt里面好像没有找到(要是你找到了也可以给我说),从他ppt的流程图:
的先后顺序,实际上,在执行这里拷贝flash部分的代码到ram(也就是流程图的Load the image into RAM)之前,我们就已经取消了重映射了。所以我认为,最开始boot还做了一个操作时将pc指针修改为实际运行的norflash地址(因为我们是norflash启动的,如果是直接烧录到ram里面,那就可以ram内启动,也就不需要搬运代码,所以你看到下面这张图左上方的红色字体代码做了一个判定,是否装载的地址(ADR R1,dummy)(因为boot是从装载处的地址执行的)就是运行时地址(LDR r4,=dummy)(因为这里是绝对地址,我们编译产生代码的地址是按照它运行的时候的地址来得出的,所以也是运行时地址)),然后取消掉重映射,然后就是执行:
这张图这部分的boot代码,进行load过程,前面注释说了,左上方红色字体是用于判定是否需要地址搬运。
最后,至于给中断向量表腾出空间这个操作,实际上不是这里做的,是我们设计内存布局的时候,我们设计了Text_start的位置以及中断向量表的位置,所以你的搬运操作只要不覆盖它就行了。
总结描述一下过程(来自彭同学,略加了一点修改):
也就是完整的过程是这样,首先会mapping,将0x40000000的地址映射到0x00000000的位置,从而使的可以从零开始执行启动代码(的一部分memory init这些,以及将pc跳转到norflash对应代码处),执行完后就取消重映射。第二步就是利用那个左上方红色代码检测是否需要搬运,最后在复制向量表之前,将原来的0x40000000位置的代码拷贝到0x06000000(也就是实现搬运到我们程序真正任务应该运行在的text段位置)
ARM架构的ADR
指令用于加载一个地址到寄存器。它是一种伪指令,实际上并不直接存在于ARM的指令集中,但在汇编阶段会被转换为一个或多个实际的ARM指令。ADR
指令的格式如下:ADR Rd, label其中,Rd
是目标寄存器,label
是一个标签,表示一个内存地址。ADR
指令的作用是计算label
标签的地址,并将这个地址加载到Rd
寄存器。计算地址的方式是基于当前指令的地址,加上或减去一个偏移量。这个偏移量是label
标签的地址与当前指令地址之间的差值。例如,如果有以下的代码:
又收集一个同学的问题:
把取消重映射放置到这个位置是否不影响呢?
之前也有想过这个,这种情况不太好说,是这样的:
你重映射的区域是
这个部分,它有可能会和上面Text_start有重叠,并不一定只有黄色INT Vector的部分,那么在你复制Norflash到RAM的位置的时候,有可能覆盖了Text_start往上的内容,那么由于存在的重映射,你就会把自己Norflash里面较高地址的内容覆盖了(如果那里也有你写的代码的话),那就出问题了。所以我们最好的位置一定是放在复制内容之前。而重映射只是为了解决最开始PC只能为0,需要初始化一些memory内容后,就可以跳转到我们的真正代码位置,取消掉重映射,然后继续执行boot(完成搬运代码等操作,最终让程序跑起来)
GDT实模式和保护模式的切换
LABEL_GDT 定义了GDT中第一个条目的描述符,该条目的Base和Limit都是0,Type是0,表示这个条目并不对应实际的段。
LABEL_DESC_CODE32定义了一个代码段的描述符,该段的Base是0,Limit是SegCode32Len-1,表示段的长度,Type的值是DA_C+DA_32表示这是个可执行的、32位的代码段。
LABEL_DESC_VIDEO定义了一个显存段的描述符,该段的Base是0xB8000,Limit是0xFFFF,表示该段的长度为64KB。Type的值DA_DRW表示该段是可读写的。这个段通常被用来在终端上显示文字和图形。
看看初始化GDT的描述符部分代码:
首先,将eax清零(通过xor eax,eax)。然后将cs寄存器的值移入ax内,因为实模式下x86需要段地址左移4位和偏移量相加,所以需要*16得到代码段基地址,然后将LABEL_SEG_CODE32的地址加上ax中的值,即可得到代码段的起始地址,将其存入eax里面。
接着就是根据描述符的结构存入我们的segbase
低2字节(一个word)ax放入LABEL_DESC_CODE32+2(字节)位置,然后右移16位(两字节),低字节存入LABEL_DESC_CODE32+4到后面的段基地址处,存入一个字节(byte),然后高字节存入段基地址2,为LABEL_DESC_CODE32+7的位置,从而完成描述符—LABEL_DESC_CODE的填写。
然后就是设置全局描述符表(GDT)
同样先清零,然后,和之前cs ax的操作一样,先将ds寄存器的值移入ax中,左移4位加上LABEL_GDT得到GDT的起始地址,然后将它存入GdtPtr字段中的2-6字节(两个字的大小,dword),接着,用lgdt指令将GdtPtr的地址作为操作数,将GDT的信息加载到GDTR寄存器中,完成GDT的初始化。
之所以要保留GdtPtr字段的低16位(2字节)是因为:
lgdt
指令需要一个 48 位的值,其中低 16 位是 GDT 的长度,高 32 位是 GDT 的基地址。例如,lgdt [GdtPtr]
会加载 GDT 指针。低16位已经通过:
GdtPtr
dw
GdtLen - 1
dd
0
的GdtLen-1赋值了长度
arm2440启动流程代码补充
关中断代码
MMU和caches关闭
先说明MCR的指令格式(MRC也一样):
MCR
指令的一般格式为:MCR p<coproc>, <op1>, <Rd>, c<CRn>, c<CRm>, <op2>
p<coproc>
是协处理器的编号,例如 p15 通常是系统控制协处理器。
<op1>
是一个可选的操作码,用于指定具体的协处理器操作。
<Rd>
是 ARM 寄存器的编号,这条指令将把这个寄存器的值写入协处理器的寄存器。
c<CRn>
和c<CRm>
是协处理器寄存器的编号。
<op2>
是另一个可选的操作码,用于指定具体的协处理器操作。
例如,
MCR p15, 0, r0, c7, c5, 0
这条指令的作用是将 ARM 寄存器 r0 的值写入系统控制协处理器 p15 的 c7 和 c5 寄存器,操作码为 0。这通常用于控制系统级的操作,例如清除缓存等。op1和op2我们一般填0
Handle secondary mpcores
MRC p15, 0, r0, c0, c0, 5
:这是一个协处理器操作,用于从协处理器寄存器读取值。这条指令读取处理器的MPIDR寄存器(多处理器仿真器ID寄存器)的值到r0寄存器。MPIDR寄存器包含了处理器的唯一标识符。
ANDS r0, r0, #0x0f
:这是一个位与操作,用于获取r0寄存器中的特定位。这条指令获取了MPIDR寄存器中的低4位,这些位表示处理器的核心编号。
BEQ clear_leds
:这是一个条件跳转指令,如果前一条指令的结果为0(也就是说,如果处理器的核心编号为0),则跳转到clear_leds
标签指定的代码位置。
BL __secondary_mpcore
:这是一个函数调用指令,调用名为__secondary_mpcore
的函数。这个函数通常用于初始化非主核心的处理器。
- 作者:liamY
- 链接:https://liamy.clovy.top/article/embedded_OS_review
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章