PS: 更新于2023-10-19
1. 第一个嵌入式系统
入门嵌入式系统开发,需要了解嵌入式开发和传统PC程序开发的差别和联系。
从图中可以看出,虽然嵌入式开发和PC程序开发使用的工具基本上没有相似之处,但是开发流程和编写代码的基本思想都是类似的。
在一个嵌入式系统按下电源键启动时,会发生如下的事情:
Step01:CPU会到特定的地址获取第一行指令来执行(boot),实际的细节有两种。
1. CPU重新启动后,将其PC(Program Counter)寄存器设为特定地址,只要user的程序确实存储在这个地址,就可以正确被执行。
2. CPU重新启动后,会将CPU中存储中断矢量表地址的寄存器设为某特定地址,接着引发‘RESET中断’,所以程序员只要把中断矢量表存储在这个地址,并指定‘Reset中断的处理程序’为自己写的某个函数(如:boot()),则该函数就可以在开机后被CPU执行
Step02:User程序开始运行后,会先对CPU做初始化的动作。
Step03:将程序的数据段从只读存储器(ROM或Flash)载入到RAM中。
Step04:CPU初始化完毕后,紧接着会初始化应用程序用到的硬设备。
Step05:初始化各个子系统,如嵌入式操作系统(RTOS)、动态存储器管理、图形界面系统等。
Step06:执行应用程序的主程序。
所谓的中断矢量表就是从某个地址开始,每4个Byte为一个单位(entry),每一个entry记录一个函数的地址
boot的作用
上面提到的执行主程序之前的操作,都由boot一个函数来完成,boot其实就是嵌入式系统在进入主程序运行之前,运行的函数
boot具体作如下几件事
Step01:设定某些重要的CPU寄存器,特别是堆栈指针寄存器(Stack Pointer;简称SP)与状态寄存器(Status Register)(状态寄存器:在大部分的初始化动作还未完成之前,此时,若产生中断是很麻烦的事情。所以在boot阶段最重要的事就是命令CPU禁止中断产生,直到初始化动作都做完后,才把中断功能打开。CPU的状态寄存器由很多状态标志组成,通常都会包含用来设定CPU是否接受中断的标志。)
Step02:CPU各部分功能初始化。
Step03:系统初始化。
Step04:调用应用程序的主程序。
Step05:结束(通常应用程序的主程序不会返回boot程序)
2. 实作嵌入式系统平台
如果对一个嵌入式系统进行分层,那么有如下几层:
Boot-Loader与驱动程序
OS与API
子系统与库函数
应用程序
对应图如下:
嵌入式系统平台指的就是让电子产品的‘应用程序’得以顺利开发的环境,所以系统平台不只是软件或硬件的概念而已,它是一个稳定的环境。所以嵌入式平台可以是boot-loader与驱动程序,OS与API,子系统与库函数的组合
但是上述的对应图只能描述更层级的关系,不能描述数据传输的过程,数据传输过程如下:
计算机系统的原理不外乎处理输入事件,产生结果并输出,嵌入式系统也不例外,让我们从外围设备状态发生变化开始讲起.
通常输入的来源有两个。一种是由监督程序(Monitor)
持续主动地监督某个外围设备的状态是否改变(我们称这种方法为Polling(轮询)
或Busy Waiting
);另一种则是由外围设备主动产生中断。不管是Polling或中断,基本上都是因为外围设备的状态产生变化,例如,使用者点触了触控式屏幕、或温度检测器发现异常温度等等。
当某设备状态发生变化,相应的驱动程序会被执行。如果是Polling方式,则驱动程序是由监督程序所引发;若是中断的方式,则相应的中断处理程序(ISR)会被执行。
ISR(以下就用中断方式来说明)只会马上判断硬件状态发生了什么变化,并且把新的状态往上层送(如哪个key被按下)。所谓的传送方式有很多,最简单的方式是设定某个全局变量,上层程序只要读取这个变量,就知道某个硬件事件发生了。如果硬件事件发生的很频繁,例如:网络的封包,只用一个全局变量传递信息,势必会遗失很多的硬件事件,此时就要用其它较复杂的方式,如message queue
系统层会有一个无限循环,循环内的工作就是持续检查有没有新的硬件事件到达,若有则处理之;若无则系统可暂时进入idle mode。
当硬件事件被系统处理完之后,系统会决定是否将其送给应用程序。同样的,系统程序与应用程序间有许多不同的沟通方式,例如,应用程序可以对其欲处理的硬件事件注册callback function,则当该事件发生时,系统自然会执行应用程序的事件处理函数
开发嵌入式操作系统时,最值得注意的是共享机制的设计,且调度算法应由应用程序为优先
3. 构建良好的嵌入式系统开发环境
嵌入式系统开发环境包含以下几个方面集成开发工具、 Cross compiler 、制作可执行文件的批处理文件(build.bat) 、makefile 、Link Script、 调试工具、下载工具、 其他工具(Offline Tools)、 模拟器、 版本控制工具
虽然对于绝大部分开发员工来说,没有必要熟悉开发环境的构建步骤,但是熟悉一下也没什么坏处不是嘛(
3.1集成开发工具
集成开发工具
没啥好说的,就是IDE
,只不过嵌入式服务产商中,很多都有自己的开发环境IDE,很少用到像Eclipse
这样的开源IDE
Cross compiler
指的是交叉编译工具集合,正常的编译器只能编译在本地机器上运行的程序,交叉编译器用于在本地机器上编译出在目标机器上运行的程序
3.2 批处理文件&makefile
批处理文件其实就是执行系统已有的可执行文件的程序语言.makefile和批处理文件最大的不同,就是make会比较文件之间的依存关系与日期,以决定某个文件是否为新版。
makefile其实可以编写出很恶心,让人看不懂的规则,但是其实没必要,编写makefile的重点不在于使用了多晦涩难懂的语法,而是make工具处理target间依存关系的能力,这才是用来管理软件编译、制作可执行文件的主要功能
3.3 Link Script
Link Script
是用来描述项目中的各个程序文件,分别要被定位到哪个地址的
要想了解Link Script
的作用,还要先了解一下程序文件的组成,程序文件的组成如下:
■ text段:就是所谓的程序部分,基本上,执行时期text段的内容不会改变,除非有特别的需求(例如,为了加速),否则text段可以直接在ROM里执行,无须载入到RAM。
■ Read-only-data(rodata)段:程序中定义为 ‘const’的变量,以及诸如‘Hello World’这样的字符串,都会被归类到rodata段(请见以下例子)。程序段还有载到RAM中执行的可能,但rodata段没有加速或改变内容的需求,所以应该‘待’在ROM里即可。
■ Data段:有初值的global变量都属于这个段,请见下列的程序。在连接时期,这些初值必须加入可执行文件中,在执行时期,因为这些变量的值是可以被改变的,所以它们不能被寻址在ROM的地址,即系统在开始使用这些变量之前,必须将其初值载入RAM。所以在连接时,数据的处理是比较特殊的,一方面它必须被加入可执行文件,这样才可以与可执行文件一起被烧入ROM中,但这些变量不能被寻址到ROM,它们真正被操作的地方是RAM。总体来说,必须告知linker。
data段会被加入ROM,但却要寻址到RAM的地址。
■ bss(Block Started by Symbol)段:没有初值的全局变量,请见下列程序。因为没有初值,所以无需加入程序之中,只要在连接时将其寻址到RAM即可。执行时期也没有载入的问题,但机器RESET后,不能保证RAM的内容都会是0,所以通常要由系统主动将整个bss段清为0。有些工程师在写程序时,会自己预设bss变量的初始值应该是0,在未设值的状况下就直接拿来用,所以每次开机后,该变量的初始值可能都不一样。这个问题呈现出来的状态就是某功能有时正确,有时错误,而错误的情形也都不相同,虽然这只是工程师的小小疏忽,但会让其他人觉得系统很不稳定。
总的来说,在嵌入式设备中,程序的结构分布如下图所示
图中可以引出2个概念:LMA (Load Memory Address)
:某程序区段被‘放置’的地址。其实就是一开始程序烧录在ROM上的地址
VMA (Virtual Memory Address)
:程序区段在执行时期的地址。
例如:数据会被放置在ROM,但执行时必须载入到RAM,则在ROM中的地址就称为LMA,而在RAM中的地址就是VMA
3.4 RoM Maker
在编译完成可执行文件后,还需要对其格式进行转换。
可执行文件格式很多,如ELF、COFF、HEX、S-Record及Windows用的EXE等.但是在ROM
中执行的二进制文件,内容应该就是一个个机器指令,CPU可以直接找指令出来执行
3.5 offline tools
通常我们称这种用于开发阶段必须自行开发,并且执行于PC上的工具叫做Offline-Tools。我把Offline-Tools归纳为六大类:
■ Program Generator(程序产生器) :系统配置工具(比如make config) resource manager。
■ Data Maker:我们希望系统在执行时期能够以比较有效率的方式来存取多批数据。在某些应用中,数据量可能非常大,此时就不能再使用简单的索引表格,而必须将这些数据文件整合为数据库。当然,一般嵌入式系统无法使用诸如mySQL这种数据库系统,必须根据应用的特性自行设计。以下列举一些较具代表性的DatabaseMaker应用:file system
和Database
■ Image Maker: Image Maker的主要功能就是要制作最后要烧入存储器中的image。通常这个image中不只包含程序,可能还包含了我们上述所说的产品资信、File System image、Database等。
■ 下载工具:当Image Maker产生image后,除了可以用烧录器写入存储器外,系统团队还必须提供所有工程人员另一个较方便的下载工具,通常这个工具必须具有局部下载的功能,因为有时工程师只编辑了部分程序,或只更新了文件系统里的某些文件,此时,若仍需要全部更新整个image,势必相当费时,影响开发效率。
■ 量产工具:举例来说,厂商希望每批机器出厂后都有不同的序号(可能包含厂商、日期、保密用途等信息),但这些信息在量产阶段,进行存储器前烧录才可决定,此时研发单位必须提供一个PC工具给工厂,先将工厂人员输入的出厂信息写入image的特定位置中,然后才进行存储器烧录。诸如此类的应用不少,只要研发单位把能最终用以烧录之image的格式定义清楚,就不会有问题了。
■ 模拟器: 就是在pc上模拟固件运行的程序
3.6 下载与执行
本章介绍把嵌入式程序下载到目标机器上的方法,以及如何执行。
■ 先利用ICE下载到RAM执行并测试。一个最简单的方式就是先将可执行文件寻址到RAM,并通过ICE将可执行文件下载到RAM里执行,暂时避开烧录的问题。使用这种方式首先要有足够大的RAM,可以麻烦硬件人员在开发时期先帮忙换上较大的RAM。此外,RAM比ROM与Flash的速度快得多,开发时期先在RAM上测试只是权宜之计,在正式版本推出前一定要将程序烧录到EEPROM或Flash中,并执行完整的测试。
■ 烧录器:市面上有许多专业的存储器烧录器厂商,所谓的烧录器是一个可以与PC连接的机器(基本上烧录器就是一个很典型的嵌入式系统,如图7-13与图7-14所示),使用者要先把存储器的Chip放在烧录器的socket上,然后在PC上操作烧录器厂商提供的应用程序,选择Image File并执行烧录即可。
接着的问题是,烧好的存储器芯片该如何mount到板子上?对硬件工程师而言,IC的拆焊是基本技能,可是要软件工程师拿烙铁已是勉强,还要拆拔、焊接芯片几乎是不可能的任务。而且时常拆焊对存储器和板子来说都不是好事,最好的方法就是板子上存储器芯片不要直接焊在板子上,而是通过socket与板子连接,软件工程师只要把烧录好的IC放在socket上夹好即可。
■ Update程序:假使机器可以通过某种方式与PC连接并传输数据的话(如USB、RS232或网络线等),那么我们可以写一个称为updater(系统更新程序)的程序模块,如图7-17所示。在正常状况下,系统不会执行它,只有当某个特殊事件发生时才会被执行(例如,PC端送来连接需求,或某个hot-key被按下)。当updater被调用来运行的时候,可以从PC接收新的Image File的内容,并将其写入NOR Flash中,如此一来,就完成了更新机器上程序版本的功能。
updater
其实就是在执行时期,隐藏于系统中的一个monitor,更重要的它是系统的一部分,且具有与PC程序沟通的能力,所以我认为这个monitor可以负责收集系统执行时期的状态,定时或当发生特殊事件时,将信息送到PC,例如,系统存储器使用量、存储器某个区域的内容、task的执行顺序、各个硬件中断的发生频率等。
此外,这个monitor
也能够提供API
供上层程序调用,我想到最简单的应用就是上层程序可以把debug information往PC丢,工程师可事后分析程序的执行结果与流程。总之,这个monitor就像是系统工程师派遣到系统内部的间谍,可以源源不断地将系统运作的信息往PC送,更进一步想象,PC程序可以自动分析monitor传出来的数据,整理成有用的报表,甚至主动警示系统曾经出现过异常的执行状况。所以updater是一个天然的调试工具
3.7 版本控制server
即git,svn