[2023.9.9]Chapter11 Embedded Operating Systems
本章讨论了嵌入式操作系统(OS)的实现。由于嵌入式操作系统是为特定目的而设计的,所以在历史上,嵌入式操作系统通常简单、时间受限,并且在有限的内存中运行。随着嵌入式硬件的复杂性增加,这种区别随着时间的推移发生了变化。传统上只存在于台式计算机上的功能,例如虚拟内存,已经迁移到了嵌入式系统领域。
由于这是一个庞大的主题,我们将范围限制在构成嵌入式操作系统的基本组件上。我们还在第10章显示的固件示例基础上进行扩展。
本章分为两部分:第一部分介绍构成嵌入式操作系统的基本组件,并注意与 ARM 处理器相关的问题。第二部分介绍了一个名为 Simple Little Operating System (SLOS) 的示例操作系统。SLOS旨在展示基本组件的实现方式。
11.1 Fundamental Components
一个操作系统由一组常见的低级组件组成,每个组件执行指定的动作。这些组件之间的相互作用和功能确定了特定操作系统的特征。
初始化是操作系统中第一个执行的代码,涉及设置内部数据结构、全局变量和硬件。初始化在固件交出控制权后开始。对于硬件初始化,操作系统设置各种控制寄存器,初始化设备驱动程序,并且如果操作系统支持抢占式,则设置一个周期性的中断。
内存处理涉及设置系统和任务堆栈。堆栈的位置决定了可供任务或系统使用的内存量。通常在操作系统初始化期间确定系统堆栈的位置。设置任务堆栈取决于任务是静态还是动态。
静态任务在构建时定义,并包含在操作系统映像中。对于这些任务,堆栈可以在操作系统初始化期间设置。例如,SLOS 是一个基于静态任务的操作系统。
动态任务在操作系统安装和执行后加载和执行,并不属于操作系统映像的一部分。任务创建时设置堆栈(例如,Linux 中的情况)。内存处理的复杂性因操作系统而异。它取决于多个因素,如所选的 ARM 处理器核心、微控制器的功能以及最终目标硬件的物理内存布局。
例子操作系统 SLOS 在第11.2节中使用了静态内存设计。它只是在微控制器内部配置一组寄存器,并设置堆栈的位置。由于没有动态内存管理的形式,你在其中不会找到 malloc() 和 free() 的实现。这些函数通常在标准的 C 库中找到。
处理中断和异常的方法是操作系统架构设计的一部分。你需要决定如何处理各种异常:数据中止、快速中断请求、中断请求、预取中止、复位和软件中断(SWI)。
并非所有的异常都需要处理程序。例如,如果你有一个不使用 FIQ 中断的目标板,那么就不需要特定的 FIQ 处理程序。为未使用的异常提供一个无限循环作为默认处理程序总是更安全的做法。这种方法使得调试变得容易:当程序中断时,很清楚是被陷入到了特定的处理程序中。它也能保护系统免受意外的异常干扰。
像 SLOS 这样的抢占式操作系统需要一个周期性中断,通常由目标硬件上的计数/定时器设备产生。在初始化阶段,操作系统设置周期性中断的频率。通常是通过将指定的值设置到计数/定时器的内存映射寄存器中来实现。
当计数/定时器启动后,它会开始递减这个值。一旦值达到零,就会触发一个中断。该中断然后由周期性中断的适当中断服务例程(ISR)处理。ISR 首先使用新的起始值重新初始化计数/定时器,然后调用调度程序或其他专门的例程。
相比之下,非抢占式操作系统不需要周期性中断,它将使用不同的技术,例如轮询——对设备状态进行持续检查。如果设备状态发生变化,那么特定的操作可以与特定的状态变化相连接。
调度程序是一个确定下一个要执行的任务的算法。有许多可用的调度算法。其中一个最简单的是循环调度算法——按照固定的循环顺序激活任务。调度算法必须在效率、大小和复杂性之间寻找平衡。
一旦调度程序完成,新旧任务必须通过上下文切换进行交换。上下文切换将处理器中所有旧任务的寄存器保存到一个数据结构中。然后将新任务的数据加载到处理器的寄存器中。(有关此过程的更多详细信息,请参阅第11.2.6节。)
最后一个组件是设备驱动程序框架——操作系统用于在不同硬件外设之间提供一致接口的机制。该框架允许将对特定外设的新支持以标准和简单的方式整合到操作系统中。对于应用程序要访问的特定外设,必须有一个特定的设备驱动程序可用。该框架必须提供一种安全的方法来访问外设(例如,不允许多个应用程序同时访问同一个外设)。
11.2 Example: Simple Little Operating System
我们开发了一个小型操作系统,称为Simple Little Operating System (SLOS)。它展示了之前讨论的基本组件如何在一个完整的操作系统中结合在一起。我们选择了ARM7TDMI作为核心,因为它是ARM家族中最简单的核心。我们使用ARM Developers' Suite version 1.2作为开发环境,以及ARM的Evaluator-7T作为目标板。相对而言,将SLOS修改为其他开发环境下构建应该是相对容易的。SLOS使用了第10章中描述的Sandstone固件来加载和执行。
SLOS是一个抢占式操作系统。定期中断激活一个休眠任务。为了简单起见,所有的任务和设备驱动程序都是静态的,也就是说,在系统运行时它们是在构建时创建的,而不是动态创建的。SLOS还提供了一个设备驱动程序框架,详见第11.2.7节。
SLOS被设计为在没有内存管理单元或保护单元的ARM7TDMI核心上执行。假设内存映射已经由初始化代码配置好了(在本例中是Sandstone,在第10章中有介绍)。要求SRAM位于0x00000000到0x00080000之间,并且基本配置寄存器必须设置为地址0x03ff0000。
SLOS被加载到地址0x00000000,其中向量表位于该地址。这也是进入SLOS的入口点。在固件交出控制权时,重要的是ARM处理器处于SVC模式,因为SVC模式是特权模式,因此允许初始化代码通过访问cpsr来改变模式。我们利用这一点在IRQ和系统模式下设置了堆栈。
在当前配置下,SLOS包括三个任务和两个服务例程。任务1和任务2通过使用二进制信号量提供了一个互斥的示例。实现的两个服务例程是周期定时器(必需的)和一个按键中断(可选的)。任务3通过ARM Evaluator-7T的一个串口提供了一个简单的命令行界面。
SLOS中的每个任务都需要有自己的堆栈。所有的任务都在用户模式下运行,因此一个任务可以读取但不能写入cpsr。任务改变为特权模式的唯一方式是使用SWI指令调用。这是调用设备驱动程序函数的机制,因为设备驱动程序可能需要完全访问cpsr。
cpsr可以在任务中被修改,但只能通过更新条件标志位的指令来间接修改。
11.2.1 SLOS Directory Layout
SLOS可以在我们的网站上的第11章目录下找到。SLOS的目录结构类似于Sandstone固件的布局(见图10.1和11.1)。
在slos/build/src目录下有六个子目录,包含了所有操作系统的源文件。slos/build/src/core目录包含了杂项的实用文件,以及命令行解释器(CLI)源代码。
特定平台的代码存储在以该平台名称命名的目录下。例如,Evaluator-7T的代码存储在e7t目录下。
slos/build/src/e7t/devices目录包含了所有设备驱动程序文件,slos/build/src/e7t/events目录包含了处理服务、异常和中断的文件。
最后,slos/build/src/apps目录包含了特定配置的所有应用程序/任务。例如,对于Evaluator-7T实现,有三个应用程序/任务。
11.2.2 Initialization
SLOS的初始化有三个主要阶段:启动、执行进程控制块(PCB)设置代码和执行C语言初始化代码。启动代码设置FIQ寄存器以及系统、SVC和IRQ模式的堆栈。在接下来的阶段中,设置PCB,其中包含每个任务的状态,包括所有的ARM寄存器。它用于在上下文切换期间存储和恢复任务状态。设置代码将进程控制块设置为初始启动状态。最后的C语言初始化阶段调用设备驱动程序、事件处理程序和周期定时器的初始化例程。完成后,可以调用第一个任务。
控制通过复位向量传递给SLOS。向量复位是存放初始化代码起始地址的内存位置。假设固件将处理器置于SVC模式下,允许操作系统初始化代码完全访问cpsr。第一条操作系统指令将pc加载为初始化代码(coreInitialization)的开始地址。从向量表中可以看到,该指令使用相对于pc的地址的加载指令加载一个字。汇编工具已经根据pc和vectorReset地址之间的差值计算出偏移值。
AREA ENTRYSLOS,CODE,READONLY
ENTRY
LDR pc, vectorReset
LDR pc, vectorUndefined
LDR pc, vectorSWI
LDR pc, vectorPrefetchAbort
LDR pc, vectorDataAbort
LDR pc, vectorReserved
LDR pc, vectorIRQ
LDR pc, vectorFIQ
vectorReset
DCD coreInitialize
vectorUndefined DCD coreUndefinedHandler
vectorSWI
DCD coreSWIHandler
vectorPrefetchAbort DCD corePrefetchAbortHandler
vectorDataAbort DCD coreDataAbortHandler
vectorReserved DCD coreReservedHandler
vectorIRQ
DCD coreIRQHandler
vectorFIQ
DCD coreFIQHandler
作为初始化过程的一部分,我们使用了FIQ寄存器来实现低级别的调试系统,如下图所示。这些寄存器用于存储状态信息。但并不总是可以使用FIQ寄存器,因为它们可能被用于其他目的。
bringupInitFIQRegisters
MOV r2,r14
; save r14
BL switchToFIQMode ; change FIQ mode
MOV r8,#0
; r8_fiq=0
MOV r9,#0
; r9_fiq=0
MOV r10,#0
; r10_fiq=0
BL switchToSVCMode ; change SVC mode
MOV pc,r2
; return
coreInitialize
BL bringupInitFIQRegisters
下一阶段是设置SVC、IRQ和系统基本堆栈寄存器。对于SVC堆栈来说,这很简单,因为处理器已经处于SVC模式。代码如下所示:
MOV sp,#0x80000
; SVC stack
MSR cpsr_c,#NoInt|SYS32md
MOV sp,#0x40000
; user/system stack
MSR cpsr_c,#NoInt|IRQ32md
MOV sp,#0x9000
; IRQ stack
MSR cpsr_c,#NoInt|SVC32md
从代码中可以看出,一旦堆栈设置完毕,处理器将切换回SVC模式,以便继续进行剩余的初始化过程。处于特权模式下允许最后的初始化阶段通过清除I位和将处理器更改为用户模式来取消屏蔽IRQ中断。
执行启动代码的结果如下:
- 低级别调试机制已初始化。
- SVC、IRQ和系统基本堆栈已设置好。
要启动SLOS运行,必须初始化每个任务的PCB。PCB是一个保留的数据结构,保存了整个ARM寄存器集的副本(参见表11.1)。通过将相应任务的PCB数据复制到处理器寄存器中,可以激活任务。
在发生上下文切换之前,必须设置每个任务的PCB,因为切换会将PCB数据传输到r0至r15寄存器和cpsr中。如果未初始化,上下文切换将在这些寄存器中复制垃圾数据。
有四个主要部分的PCB需要初始化:程序计数器(PC)、链接寄存器(LR)、用户模式堆栈和保存的处理器状态寄存器(也就是r13、r14、r15和spsr)针对每个任务进行初始化。
; void pcbSetUp(void *entryAddr, void *PCB, UINT offset);
pcbSetUp
STR r0,[r1,#-4] ; PCB[-4]=C_TaskEntry
STR r0,[r1,#-64] ; PCB[-64]=C_TaskEntry
SUB r0,sp,r2
STR r0,[r1,#-8] ; PCB[-8]=sp-<offset>
MOV r0,#0x50
; cpsr_c
STR r0,[r1,#-68] ; PCB[-68]=iFt_User
MOV pc,lr
为了帮助说明这一点,我们提取了初始化PCB的例程。调用pcbSetUp例程来设置任务2和任务3的PCB。寄存器r0是任务的入口地址——标签entryAddr。这是任务的执行地址。寄存器r1是PCB数据结构的地址——标签pcbAddr。该地址指向一个存储任务PCB的内存块。寄存器r2是堆栈偏移量,用于在内存映射中定位堆栈。需要注意的是,任务1不需要初始化,因为它是第一个要执行的任务。
设置PCB的最后一部分是设置当前任务标识符,调度算法使用该标识符确定要执行的任务。
LDR r0,=PCB_CurrentTask
MOV r1,#0
STR r1,[r0]
LDR lr,=C_Entry
MOV pc,lr ; enter the CEntry world
在代码片段的末尾,通过将pc设置为例程的起始地址,调用了第一个C例程——C_Entry。
执行PCB设置代码的结果如下:
- 初始化所有三个任务的PCB。
- 将要执行的当前PCB设置为任务1(标识符为0)。
现在,初始化工作交给了C_Entry()例程,该例程可以在build/src/core/cinit.c文件中找到。C_Entry例程调用另一个例程cinit_init()。这个例程用于初始化设备驱动、服务和周期性中断滴答。这里展示了cinit_init()例程的内容。C代码的设计不需要初始化标准C库,因为它不调用任何标准C库函数,例如printf()、fopen()等。
void cinit_init(void)
{
eventIODeviceInit();
eventServicesInit();
eventTickInit(2);
}
函数eventIODeviceInit、eventServicesInit和eventTickInit都被调用以初始化操作系统的各个特定部分。你会注意到,eventTickInit有一个参数,值为2。这用于设置周期性滴答事件之间的毫秒数。
初始化完成后,可以启动周期性定时器,如下所示。这意味着在第一个定时器中断之前需要调用任务1。为了允许周期性事件中断处理器,必须启用IRQ并将处理器置于用户模式。完成这一步骤后,将调用任务1的入口点C_EntryTask1的地址。
int C_Entry(void)
{
cinit_init();
eventTickStart();
__asm
{
MSR cpsr_c,#0x50
}
C_EntryTask1();
return 0;
}
如果一切正常,C_Entry例程末尾的返回语句将永远不会执行。此时,所有初始化工作已经完成,操作系统完全可用。
执行所有C初始化代码的结果如下:
- 设备驱动程序被初始化。
- 服务被初始化。
- 周期性定时器滴答被初始化并启动。
- cpsr中启用了IRQ中断。
- 处理器被置于用户模式。
- 调用了任务1的入口点(即C_EntryTask1)。
11.2.3 Memory Model
我们采用了一个简单的内存模型来设计SLOS。图11.2显示了SLOS的代码部分,包括任务,位于低地址内存中,而IRQ和每个任务的堆栈位于较高地址内存中。SVC堆栈设置在内存的顶部。内存映射中的箭头表示堆栈增长的方向。
//SVC stack
/*
SVC(Supervisor Call)是一种处理器指令,用于在操作系统内核态(supervisor mode)和用户态(user mode)之间进行切换和通信。当用户程序需要执行特权指令或访问受限资源时,会触发一个SVC异常,将控制权传递给操作系统内核。
在处理SVC异常时,操作系统会使用一个称为SVC堆栈(SVC stack)的数据结构来保存相关的上下文信息。SVC堆栈通常是一个专门的内存区域,用于存储异常处理所需的寄存器值、返回地址和其他相关数据。它位于内核的地址空间中,并且在内核模式下可访问。
*/
11.2.4 Interrupts and Exceptions Handling
在这个操作系统的实现中,实际上只使用了三个异常。其他异常会通过转到特定的虚拟处理程序而被忽略,为了安全起见,这些处理程序被实现为无限循环。在完整的实现中,这些虚拟处理程序应该被替换为完整的处理程序。表11.2展示了这三个异常以及它们在操作系统中的使用方式。
11.2.4.1 Reset Exception
重置只在初始化阶段被调用一次。理论上,它可以被再次调用以重新初始化系统,例如作为响应看门狗定时器事件重置处理器。当系统长时间处于非活动状态时,看门狗定时器用于重置系统。
11.2.4.2 SWI Exception
每当应用程序调用设备驱动程序时,调用会通过SWI处理程序机制进行。SWI指令强制处理器从用户模式切换到SVC模式。
以下是核心的SWI处理程序。处理程序的第一个动作是将寄存器r0到r12保存到SVC堆栈中。
接下来的动作是计算SWI指令的地址,并将该指令加载到寄存器r10中。通过屏蔽掉最高的8位,可以获取SWI号码。然后将SVC堆栈的地址复制到寄存器r1中,并在调用SWI C处理程序时作为第二个参数使用。
接着,将spsr复制到寄存器r2,并存储在堆栈中。这仅在出现嵌套的SWI调用时才需要。然后,处理程序跳转到调用C处理程序例程的代码处。
coreSWIHandler
STMFD sp!,{r0-r12,r14} ; save context
LDR r10,[r14,#-4] ; load SWI instruction
BIC r10,r10,#0xff000000 ; mask off the MSB 8 bits
MOV r1,r13
; copy r13_svc to r1
MRS r2,spsr
; copy spsr to r2
STMFD r13!,{r2}
; save r2 onto the stack
BL swi_jumptable ; branch to the swi_jumptable
在BL指令之后的代码如下所示,将返回到调用方程序。这通过从堆栈中还原spsr并重新加载所有用户保留的寄存器,包括pc,来实现。
LDMFD r13!,{r2} ; restore the r2 (spsr)
MSR spsr_cxsf,r2 ; copy r2 back to spsr
LDMFD r13!,{r0-r12,pc}ˆ ; restore context and return
链接寄存器在BL指令中已经设置。当SWI C处理程序完成时,执行该代码。
swi_jumptable
MOV r0,r10
; move the SWI number to r0
B eventsSWIHandler ; branch to SWI handler
在图11.3中显示的C处理程序eventsSWIHandler以寄存器r0包含的SWI号码和寄存器r1指向存储在SVC堆栈上的寄存器的位置被调用。
11.2.4.3 IRQ Exception
IRQ处理程序比SWI处理程序要简单得多。它作为基本的非嵌套中断处理程序实现。处理程序首先保存上下文,然后将中断控制器状态寄存器INTPND的内容复制到寄存器r0中。然后,每个服务例程将寄存器r0与特定的中断源进行比较。如果源和中断匹配,则调用服务例程;否则,将中断视为幻影中断并忽略。
TICKINT EQU 0x400
BUTTONINT EQU 0x001
eventsIRQHandler
SUB r14, r14, #4
; r14_irq-=4
STMFD r13!, {r0-r3, r12, r14} ; save context
LDR r0,INTPND
; r0=int pending reg
LDR r0,[r0]
; r0=memory[r0]
TST r0,#TICKINT
; if tick int
BNE eventsTickVeneer
; then tick ISR
TST r0,#BUTTONINT
; if button interrupt
BNE eventsButtonVeneer ; then button ISR
LDMFD r13!, {r0-r3, r12, pc}ˆ ; return to task
对于已知的中断源,会调用中断修饰程序来处理事件。下面的代码展示了一个定时器修饰程序的示例。从示例中可以看出,修饰程序包括调用两个例程:第一个是重置定时器的eventsTickService(特定于平台的调用),第二个是调用调度器的kernelScheduler,而调度器则会进行上下文切换。
eventsTickVeneer
BL eventsTickService ; reset tick hardware
B kernelScheduler
; branch to scheduler
在IRQ堆栈上没有对寄存器r4到r12的要求,因为调度算法和上下文切换处理了所有寄存器的细节。
11.2.5 Scheduler
在SLOS中使用的低级调度器或分发器是一个简单的静态轮转算法,如下的伪代码所示。这里的“静态”表示任务仅在操作系统初始化时创建。在SLOS中,任务既不能在操作系统处于活动状态时创建,也不能被销毁。
task t=0,t’;
scheduler()
{
t’ = t + 1;
if t’ = MAX_NUMBER_OF_TASKS then
t’ = 0 // the first task.
end;
ContextSwitch(t,t’)
}
如前所述,在初始化阶段,当前活动任务t(PCB_CurrentTask)被设置为0。当周期性的时钟中断发生时,新的任务t'是通过当前任务t加1来计算得到的。如果任务编号等于任务限制(MAX_NUMBER_OF_TASKS),则任务t'将被重置为起始值0。
表11.3列出了调度器使用的标签以及它们在算法中的用法的描述。这些标签用于调度器的以下过程和代码:
1. 通过加载PCB_CurrentTask的内容获取当前任务ID。
2. 使用PCB_CurrentTask作为索引在PCB_Table中找到当前任务对应的PCB地址。
3. 使用第2步获取的地址更新PCB_PtrCurrentTask的值。
4. 使用轮转算法计算新的任务t'的ID。
5. 将新的任务t'的ID存储到PCB_CurrentTask中。
6. 通过使用更新后的PCB_CurrentTask作为索引在PCB_Table中找到下一个任务PCB的地址。
7. 将下一个任务PCB存储到PCB_PtrNextTask中。
调度下一个任务t'的代码如下:
MaxNumTasks EQU 3
FirstTask EQU 0
CurrentTask
LDR r3,=PCB_CurrentTask ; [1] r3=PCB_CurrentTask
LDR r0,[r3]
; r0= current Task ID
LDR r1,=PCB_Table ; [2] r1=PCB_Table address
LDR r1,[r1,r0,LSL#2] ; r1=mem32[r1+r0 << 2]
LDR r2,=PCB_PtrCurrentTask ; [3] r2=PCB_PtrCurrentTask
STR r1,[r2]
; mem32[r2]=r1 : task addr
; ** PCB_PtrCurrentTask - updated with the addr of the current task
; ** r2 = PCB_PtrCurrentTask address
; ** r1 = current task PCB address
; ** r0 = current task ID
NextTask
ADD r0,r0,#1
; [4] r0 = (CurrentTaskID)+1
CMP r0,#MaxNumTasks ; if r0==MaxNumTasks
MOVEQ r0,#FirstTask ; then r0 = FirstTask (0)
STR r0,[r3]
; [5] mem32[r3]=next Task ID
LDR r1,=PCB_Table ; [6] r1=PCB_Table addr
LDR r1,[r1,r0,LSL#2] ; r1=memory[r1+r0 << 2]
LDR r0,=PCB_PtrNextTask ; [7] r0=PCB_PtrNextTask
STR r1,[r0]
; memory[r0]=next task addr
执行调度器后的结果如下:
* PCB_PtrCurrentTask指向当前活动PCB的地址。
* PCB_PtrNextTask指向下一个活动PCB的地址。
* PCB_CurrentTask存储下一个任务的标识符的值。
11.2.6 Context Switch
利用调度器产生的更新信息,上下文切换将当前活动任务t与下一个任务t'进行交换。为了实现这一点,上下文切换将活动分为两个阶段,如图11.4所示。第一阶段涉及将处理器寄存器保存到由PCB_PtrCurrentTask指向的当前任务t的PCB中。第二阶段从由PCB_PtrNextTask指向的下一个任务t'的PCB中加载寄存器的数据。
//看起来每个task都有对应的PCB
现在我们将带您了解上下文切换的两个阶段的过程和代码,首先详细介绍保存当前上下文的过程,然后再加载新的上下文。
11.2.6.1 Save the Current Context
第一阶段是保存活动任务t的当前寄存器。所有任务都在用户模式下执行,因此必须保存用户模式寄存器。以下是该过程:
1. 我们必须从堆栈中恢复寄存器r0到r3和r14。这些寄存器属于当前任务。
2. 然后,使用寄存器r13指向当前任务PCB的偏移量为-60的位置。这个偏移量允许两条指令更新整个PCB。
3. 第一阶段的最后一个动作是存储所有的用户模式寄存器r0到r14。这可以通过一条指令完成。请记住,符号ˆ表示存储多个指令作用于用户模式寄存器。第二条存储指令保存spsr和返回的链接寄存器。
将寄存器保存到PCB的代码如下:
Offset15Regs EQU 15*4
handler_contextswitch
LDMFD r13!,{r0-r3,r12,r14} ; [1.1] restore registers
LDR r13,=PCB_PtrCurrentTask ; [1.2]
LDR r13,[r13]
; r13=mem32[r13]
SUB r13,r13,#Offset15Regs ; r13-=15*Reg:place r13
STMIA r13,{r0-r14}ˆ
; [1.3] save user mode registers
MRS r0, spsr
; copy spsr
STMDB r13,{r0,r14}
; save r0(spsr) & r14(lr)
保存当前上下文的结果如下:
- IRQ堆栈被重置,并保存到PCB_IRQStack中。
- 任务t的用户模式寄存器被保存到当前PCB中。
11.2.6.2 Load the Next Context
上下文切换的第二阶段涉及将任务t'的PCB转移到用户模式的保护寄存器中。完成后,程序必须将控制权交给新的任务t'。以下是该过程:
1. 加载并将寄存器r13定位在新的PCB起始处偏移量为-60的位置。
2. 首先加载寄存器spsr和链接寄存器。然后加载下一个任务的寄存器r0到r14。寄存器r14是用户模式的寄存器r14,而不是指令中的ˆ所示的IRQ寄存器r14。
3. 然后从PCB_IRQStack中恢复IRQ堆栈。
4. 通过将寄存器r14中保存的地址复制到pc,并更新cpsr,来恢复执行新的任务。
从PCB加载寄存器的代码如下:
LDR r13,=PCB_PtrNextTask ; [2.1] r13=PCB_PtrNextTask
LDR r13,[r13]
; r13=mem32[r13] : next PCB
SUB r13,r13,#Offset15Regs ; r13-=15*Registers
LDMDB r13,{r0,r14} ; [2.2] load r0 & r14
MSR spsr_cxsf, r0 ; spsr = r0
LDMIA r13,{r0-r14}ˆ ; load r0_user-r14_user
LDR r13,=PCB_IRQStack ; [2.3] r13=IRQ stack addr
LDR r13,[r13]
; r13=mem32[r13] : reset IRQ
MOVS pc,r14
; [2.4] return to next task
加载下一个上下文的结果如下:
- 上下文切换完成。
- 下一个任务的寄存器被加载到用户模式寄存器中。
- IRQ堆栈被还原为在进入IRQ处理程序之前的原始设置。
11.2.7 Device Driver Framework
设备驱动框架(DDF)使用SWI指令来实现。DDF保护操作系统免受应用程序直接访问硬件的影响,并为任务提供统一的标准接口。为了访问特定设备,任务必须首先获得一个唯一的标识号(UID)。通过调用open宏或eventsIODeviceOpen来实现这一目的。该宏会直接转换成设备驱动的SWI指令。UID用于检查是否有其他任务已经访问了相同的设备。
打开设备驱动的任务代码是:
device_treestr *host;
UID serial;
host = eventIODeviceOpen(&serial,DEVICE_SERIAL_E7T,COM1);
if (host==0)
{
/* ...error device driver not found...*/
}
switch (serial)
{
case DEVICE_IN_USE:
case DEVICE_UNKNOWN:
/* ...problem with device... */
}
该示例展示了使用设备驱动框架打开串行设备。一组宏将参数转换为寄存器r1到r3。然后,这些寄存器通过SWI机制传递给设备驱动函数。在该示例中,只有由r1指向的值,即&serial,实际上被更新。该值用于返回UID。如果返回的值为零,则表示发生了错误。
下面的代码显示了如何将宏eventIODeviceOpen转换为单个SWI指令调用:
PRE
r0 = Event_IODeviceOpen (unsigned int)
r1 = &serial (UID *u)
r2 = DEVICE_SERIAL_E7T (unsigned int major)
r3 = COM1 (unsigned int minor)
SWI 5075
POST
r1 = The data pointed to by the UID pointer is updated
SWI接口被用作在任务在非特权模式下执行时切换到特权模式的方法。这允许设备驱动程序完全访问cpsr。图11.5显示了调用设备驱动函数时实际的模式变化。从图中可以看出,设备驱动程序本身在系统模式下执行(特权模式)。
一旦执行了SWI指令,处理器就会进入SVC模式,并且IRQ中断会自动禁用。只有当处理器切换到系统模式时,中断才会重新启用。唯一的例外是在初始化阶段调用设备驱动函数时,此时中断将保持禁用状态。
11.3 Summary
在ARM处理器上执行的嵌入式操作系统的基本组件如下:
■ 初始化:设置操作系统使用的所有内部变量、数据结构和硬件设备。
■ 内存管理:组织内存以容纳内核及要执行的各种应用程序。
■ 所有中断和异常都需要一个处理程序。对于未使用的中断和异常,必须安装一个虚拟处理程序。
■ 预置定时器:对于抢占式操作系统,需要定期定时器。定时器产生一个中断,导致调度程序被调用。
■ 调度程序:是确定要执行的新任务的算法。
■ 上下文切换:保存当前任务的状态,并加载下一个任务的状态。
这些组件在名为Simple Little Operating System(SLOS)的操作系统中得到了体现:
■ 初始化:设置SLOS的所有功能,包括银行模式堆栈、每个应用程序的进程控制块(PCB)、设备驱动程序等。
■ 内存模型:SLOS内核放置在低内存中,每个应用程序都有自己的存储区域和堆栈。微控制器系统寄存器放置在ROM和SRAM之外。
■ 中断和异常:SLOS仅使用三个事件,分别是Reset、SWI和IRQ。所有其他未使用的中断和异常都有一个虚拟处理程序安装。
■ 调度程序:SLOS实现了一个简单的轮转调度程序。
■ 上下文切换:首先将当前上下文保存到一个PCB中,然后从另一个PCB中加载下一个任务的上下文。
■ 设备驱动框架:这保护操作系统免受应用程序直接访问硬件的影响。