本篇内容比较简单,但却很繁琐,篇幅也很长,毕竟是囊括了整个操作系统的生命周期。这篇文章的目的是作为后续设计多任务开发的铺垫,后续会单独再抽出一篇分析任务的相关知识。另外本篇文章以单核MCU为背景,并且以最新的3.1.xLTS版本源码进行分析。主要内容目录如下:
-
基于bsp/stm32/stm32f103-mini-system为背景
-
Cortex-M3的堆栈基础概念
-
C语言main函数和rt-thread的main
-
rt-thread操作系统的传统初始化与自动初始化组件
-
任务是怎样运行起来的
-
Idle任务与新的构想
基于bsp/stm32/stm32f103-mini-system的开机介绍
关于体系结构的知识这里不做过多的介绍,因为这些知识要讲清楚的话足以写出一本大部头的书出来。不过会简单介绍一些必要的东西。
Stm32f103单片机是cortex-m3内核,在cortex-m3内核中使用双堆栈psp和msp,模式分为线程模式和handler模式,权限级别分为非特权级别和特权级别(现在只需要知道这么多就行了),handler模式就是当处理发生中断的时候自动进入的模式,其handler模式永远为特权级。
上电开机最开始运行的是MCU内部的ROM部分,这部分代码我们通常看不到,其通常是对芯片进行必要的初始化,比如FLASH和RAM的时钟初始化等,然后跳转到用户flash区域运行用户代码。在STM32中用户flash地址从0x08000000开始。我们写的代码都是从这里开始运行的。其次由于cortexM规定其用户FLASH区域的最前面必须是一张中断向量表。所以也就是说STM32的0x08000000开始是一张中断向量表,这是必须的也是默认的,当然在之后还可以重映射其它地方的向量表。这张向量表中的第一项是一个栈地址,第二项复位向量地址。下面贴一段向量表部分代码(摘录自startup_stm32f103xb.s):
__Vectors DCD __initial_sp ; Topof Stack DCD Reset_Handler ; Reset Handler DCD NMI_Handler ; NMIHandler DCD HardFault_Handler ; Hard Fault Handler DCD MemManage_Handler ; MPU Fault Handler DCD BusFault_Handler ; Bus Fault Handler DCD UsageFault_Handler ; Usage Fault Handler DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD 0 ; Reserved DCD SVC_Handler ; SVCall Handler DCD DebugMon_Handler ; Debug Monitor Handler DCD 0 ; Reserved DCD PendSV_Handler ; PendSV Handler DCD SysTick_Handler ; SysTick Handler
另外需要注意的是开机后会自动进入复位异常,通常我们叫上电复位过程,不过意外的是上电复位处理的模式是特权级线程模式。在特权模式下堆栈指针将使用MSP,非特权模式下可以被切换到PSP。RT-Thread操作系统就是这么做的。所以回过头来看,中断向量表第一项指定了MSP的栈起始地址,并被自动加载到MSP,第二项指定了复位向量地址,也被自动加载到PC并运行。这样一来开机后我们能通过debug看到PC指针最先指向复位向量的第一条指令上。我们看一下stm32f103在armcc编译器上的复位向量代码:
; Reset handler Reset_Handler PROC EXPORT Reset_Handler [WEAK] IMPORT __main IMPORT SystemInit LDR R0, =SystemInit BLX R0 LDR R0, =__main BX R0 ENDP
Cortex-M3的堆栈基础概念
在Cortex-M3的处理器内核上堆栈指针分为PSP和MSP。handler模式下总是使用MSP,线程模式可以通过CONTROL寄存器来配置(修改的时候必须处于特权模式才可以)。
之所以需要这样设计就是为了将普通软件和系统软件通过权限隔离开,避免普通用户权限操作系统关键资源带来安全风险。当我们使用带有操作系统的环境进行开发时,操作系统就会将关键操作例如任务切换、中断处理等在特权模式操作。而其它的操作都会运行在非特权模式下完成。
操作系统一般都会将必要的操作封装出API接口,以提供给普通软件调用。而这背后的设计思想就是通过触发异常,然后进入特权模式运行异常向量处理程序。而这段异常处理程序早就让操作系统实现了,进而这部分特权操作是操作系统接管处理的。这也就避免用户普通软件去进行不必要的特权操作。例如用户任务想主动放弃CPU从而调用yield,yield将进行任务切换,其中过程大概是“选出另一个任务”->”触发SVC或者Pendsv异常”->进入SVC/Pendsv的handler异常处理程序,此时是特权模式,完成操作后返回到新任务运行。在RT-Thread中进入任务切换是通过触发Pendsv异常。
C语言main函数和RT-Thread的main
前面提到过开机启动最后进入复位向量处运行,最终调用__main就跑到我们外面写的C语言的main函数了。但这并非这么简单,在从__main到我们的main中间还有一系列操作比如初始化堆栈、初始化全局变量区域、初始化C运行时库等,然后再在最后调用用户的main函数。
不过在不同的编译器上这个__main并非是固定的,这里也就armcc是如此,如果是GCC和IAR的话其就不太一样,不过不影响我们分析核心主题。这里仅以借用armcc为例来分析主题中心思想。另外在说明RT-Thread中开启RT_USING_USER_MAIN的时候在ARMCC编译器上还有一个支持挂钩的操作,这种操作一般见于补丁修复的时候。其实现方式是在原有函数的名字前加上$Sub$$
前缀就可以将原有函数劫持下来,并通过加上$Super$$
前缀再调用原始函数。具体如下:The followingexample shows how to use $Super$$and $Sub$$ to insert a callto the function ExtraFunc() before the call to the legacy functionfoo().
ex