freeRTOS小结——嵌入式系统的背景知识
freeRTOS的移植和维护,了解嵌入式系统的一些基本原理和相关背景是十分必要的。本人在最初从事RTOS的移植和维护时,遇到的最大的困难就是不了解这些基本原理和相关背景,跳了无数的坑,想想都是泪。
以下都是自己原创,源于在RTOS移植和维护中各种跳坑经验,欢迎各位同学指出其中错误反馈给我,也欢迎分享转载,但请注明出处(整理成文不容易)。
好了,废话不多说,进入正题。
PS: 20180610,更正部分错误,添加一些新内容
嵌入式系统硬件架构
如下图所示,嵌入式系统的硬件由核(core)、总线(bus)和各类外设(peripheral)组成。其中core是最重要的部分,当前core大多使用哈佛架构(HarvardArchitecture),包括如下部分
· CPU用于执行指令,
· IRAM用于存放CPU运行指令集合,也被记为ICache或是其他名称
DRAM用于存放指令相关数据,也被记为DCache或是其他名称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。
当然,还可以在这个基础上进行扩展,即系统包含多个sector,不同sector之间在也设置专门的内部存储器(即L3 Cache)进行通信,如此可获取更加强大的处理性能。
CPU运行机制
CPU正常运行过程
CPU的基本运行过程如下图所示,CPU总是从IRAM中取出指令,然后按照指令进行计算或读写数据的操作,
其中,
· 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位置,如下图所示,
由此可见,栈的大小确定了函数调用的最大深度。
压栈过程中,CPU处理流程如下图所示,
出栈过程中,CPU处理流程如下图所示,
CPU上电初始化过程
初始上电时,CPU总是从固定的默认位置去提取指令,该默认位置存放的指令集合记为Boot Instruction, 其主要作用是让CPU将存放在外部存储器(External Memory)中的指令集合搬移到IRAM中,并让CPU执行从外部搬移而来的指令集合。
一般而言,外部搬移的指令集合入口函数名都记为main,这也是C/C++编程语言中,主入口函数总是写为main函数的原因。
Boot Instruction也被记为booter,多存放在仅要求可读的Flash存储器中,其在断电后仍然能够保存数据。
存放指令的外部存储器也要求断电后仍然能够保存数据,但与保存booter的存储器不同,它要求存储器可读可写。
IRAM在初始上电过程中必须为可写,在正常运行时必须为可读。
CPU处理中断过程
除压栈时需要保存全部寄存器信息外,CPU处理中断的过程与CPU调用函数的过程完全一致。
压栈和出栈之间的处理过程,CPU会根据中断ID去中断处理表中读取指令执行。
一般而言,中断处理表中的指令往往都是调用用户设置的某个函数(本文记为ISR,interrupt service routine),因此压栈时需要保存全部寄存器信息——调用函数由用户设置,其使用寄存器情况是不定的。
压栈过程中,CPU处理流程如下图所示,
出栈过程中,CPU处理流程如下图所示,
大部分的 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;
}