freeRTOS小结——嵌入式系统的背景知识

      freeRTOS的移植和维护,了解嵌入式系统的一些基本原理和相关背景是十分必要的。本人在最初从事RTOS的移植和维护时,遇到的最大的困难就是不了解这些基本原理和相关背景,跳了无数的坑,想想都是泪。

      以下都是自己原创,源于在RTOS移植和维护中各种跳坑经验,欢迎各位同学指出其中错误反馈给我,也欢迎分享转载,但请注明出处(整理成文不容易)。

      好了,废话不多说,进入正题。

      PS: 20180610,更正部分错误,添加一些新内容

嵌入式系统硬件架构

如下图所示,嵌入式系统的硬件由核(core)、总线(bus)和各类外设(peripheral)组成。其中core是最重要的部分,当前core大多使用哈佛架构(HarvardArchitecture),包括如下部分

·        CPU用于执行指令,

·        IRAM用于存放CPU运行指令集合,也被记为ICache或是其他名称

DRAM用于存放指令相关数据,也被记为DCache或是其他名称

freeRTOS小结——嵌入式系统的背景知识


peripheral通过bus与core进行数据交互,常见的外设包括

·        通用异步收发传输器,即UART,多用于与PC的数据交互。

·        数据搬移加速器,即DMA,用于搬移数据。

·        定时器,即Timer。

·        存储器,即Memory,主要是核外的存储器(IRAM和DRAM也是memory的一类),当下常用的主要分为断电后数据丢失和不丢失两类,如DDR和flash。

peripheral之间可以通过总线进行数据交互,也可通过其内部专用数据通道进行数据交互。

 

对处理能力要求高的嵌入式系统还往往使用多核系统(multi-core system),如下图所示,其中core与core之间通过内部bus(InternalBus)进行通信,内部存储器(Internal Memory)用于支持core与core之间的高速数据交互,也被记为二级缓存(L2 Cache)。相应地,一级缓存即为IRAM和DRAM。其中,多个core,InternalMemory和Internal Bus组成的整体本文记为sector。

freeRTOS小结——嵌入式系统的背景知识

        当然,还可以在这个基础上进行扩展,即系统包含多个sector,不同sector之间在也设置专门的内部存储器(即L3 Cache)进行通信,如此可获取更加强大的处理性能。


CPU运行机制

 

CPU正常运行过程

CPU的基本运行过程如下图所示,CPU总是从IRAM中取出指令,然后按照指令进行计算或读写数据的操作,

freeRTOS小结——嵌入式系统的背景知识

其中,

·        IRAM分为以下两部分,

·       中断处理表(Interrupt Handler Table),存放各个中断对应的处理函数,将在后边提到。

·       用户指令域(User Instructions Domain),存放用户定义的指令结合。

·        DRAM分为以下三部分,

·       用户数据域(User Data Domain),存放用户定义的数据(全局变量、静态变量等)。

·       堆(Heap),用于动态存储分配的存储空间,后边将会提到。

·       栈(stack),用于支持函数调用时保存寄存器信息,后边将会提到。

·        指令提取模块(Instruction Fetcher),用于从IRAM中提取指令。

·        计算模块(ALU&MAC),用于进行加法(ALU)和乘法(MAC),计算的输入数据和输出数据可以来源于寄存器或DRAM

·        数据读写模块(Data Reader/Writer),用于读取或写入数据,其可以从DRAM中读取数据写入到DRAM或寄存器中,也可以从寄存器中读取数据写入寄存器或DRAM中

·        寄存器(Register),用于暂存CPU执行指令时保存的中间数据和CPU控制与状态信息,主要分为三类

·       数据寄存器(Data Register),主要用于存放计算数据信息,大多记为r0,r1,…,为防止计算溢出,部分Data Register还附带有保护寄存器(Guard Register),用于存放溢出的数据位

·       地址寄存器(Address Register),主要用于存放地址信息,大多记为a0,a1,…,也可用于计算数据信息

·       控制寄存器(Control Register),用于存放CPU控制与状态信息,如中断控制、循环控制、指令计数器等。

·        中断检测模块(Interrupt Detector),用于检测中断。

·        启动器(booter)和外部存储器(External Memory)用于CPU上电初始化过程,将在后边提到

CPU总是按照自己的频率处理指令,这个频率即为CPU的主频,CPU处理一次指令的时间(本文记为cycle)。显然,主频越高,CPU处理指令的速率越高,性能也越强,但相应的功耗也越大。早期的CPU的主频总是固定的,因此CPU处理一次指令的时间总是固定的。后来处于降低功耗的考虑,一些CPU将主频做成了可动态配置的,让用户根据自己需要在运行过程中去动态调整,以平衡功耗和性能。

需要注意,以上所说的CPU处理一次指令,并非是指CPU执行一条指令——早期的CPU确实如此,当前的大部分CPU都支持在一个cycle内处理多条指令。这主要依赖于以下几点事实,

·        读写数据的指令和运算的指令涉及的寄存器和存储位置互不相关,可允许它们同时执行。

·        某些CPU可能配置有多个ALU和MAC模块,并并行进行多次加法和乘法运行。

因此当前很多CPU都会提前从IRAM中取出多条指令,进行分析,预测它们是否能够同时执行。与CPU配套的编译器也会在生成指令时将可以并行执行的指令排列在一起。

 

CPU调用函数过程

CPU调用函数时,需要将其可能使用的全部寄存器全部保存到stack中,即“压栈”(push stack);在调用完成后基于stack恢复这些寄存器的数据,即“出栈”(pop stack)。如此,调用函数完成后,CPU还能恢复到调用函数之前的状态。

实际上,当使用高级语言进行编程时,对于编程者而言,压栈和出栈的操作是不可感知的——它们是通过编译器自动生成的。使用汇编语言进行编程时,压栈和出栈则需要编程者手动编辑。大多数情况下,函数的调用都是不可避免的,因此压栈和出栈几乎是必须的操作。

为支持完成调用函数后,能够找到stack恢复寄存器数据,往往会设置一个特殊的寄存器,保存当前stack的位置,本文将其记为Stack Point Register。Stack Point Register只可用于压栈和除栈处理。压栈和出栈时,Stack Point Register会更新到新的stack位置,如下图所示,

freeRTOS小结——嵌入式系统的背景知识

由此可见,栈的大小确定了函数调用的最大深度。

 

压栈过程中,CPU处理流程如下图所示,


freeRTOS小结——嵌入式系统的背景知识

出栈过程中,CPU处理流程如下图所示,

freeRTOS小结——嵌入式系统的背景知识

CPU上电初始化过程

初始上电时,CPU总是从固定的默认位置去提取指令,该默认位置存放的指令集合记为Boot Instruction, 其主要作用是让CPU将存放在外部存储器(External Memory)中的指令集合搬移到IRAM中,并让CPU执行从外部搬移而来的指令集合。

freeRTOS小结——嵌入式系统的背景知识

一般而言,外部搬移的指令集合入口函数名都记为main,这也是C/C++编程语言中,主入口函数总是写为main函数的原因。

Boot Instruction也被记为booter,多存放在仅要求可读的Flash存储器中,其在断电后仍然能够保存数据。

存放指令的外部存储器也要求断电后仍然能够保存数据,但与保存booter的存储器不同,它要求存储器可读可写。

IRAM在初始上电过程中必须为可写,在正常运行时必须为可读。

CPU处理中断过程

除压栈时需要保存全部寄存器信息外,CPU处理中断的过程与CPU调用函数的过程完全一致。

压栈和出栈之间的处理过程,CPU会根据中断ID去中断处理表中读取指令执行。

一般而言,中断处理表中的指令往往都是调用用户设置的某个函数(本文记为ISR,interrupt service routine),因此压栈时需要保存全部寄存器信息——调用函数由用户设置,其使用寄存器情况是不定的。

压栈过程中,CPU处理流程如下图所示,

freeRTOS小结——嵌入式系统的背景知识

出栈过程中,CPU处理流程如下图所示,

freeRTOS小结——嵌入式系统的背景知识

大部分的 CPU还允许中断嵌套(InterruptNest),即CPU在执行中断A的ISR的过程中收到优先级更高的中断B,转而暂停执行中断A的ISR,优先执行中断B的ISR,与函数调用的场景类似。

编译和连接

编译是将代码转化为CPU能识别的指令集合的过程。

链接是将编译得到的指令集合,以及这些指令使用到的存储空间映射到IRAM和DRAM中的过程。

以下边给出的hello world程序为例,该程序编译得到的指令集合,通过链接过程映射到IRAM中特定位置存放,其定义的全局变量printStr[]则会映射到DRAM中特定位置(用户数据域),指令集合对于printStr[]的操作则是直接对DRAM中映射存储位置的数据进行操作。

 

const char printStr[]= “Hello world!\n”

void main()

{

  print(printStr);

  return;

}