第5章:嵌入式系统设计与开发
本章对应原书第 215~252 页。软考视角:本章通常占上午题 3~5 分,下午案例题中"开发流程""调试方法""软件移植"也常从这里出题,属于重点章节。
本章在讲什么?(先看这个)
前面几章讲的是"嵌入式系统由什么组成"(硬件、软件基础),本章回答的是"怎么把它做出来"。嵌入式开发与普通 PC 软件开发最大的不同在于:软件和硬件必须协同设计,而且目标机资源有限,所以要采用"宿主机(Host)+ 目标机(Target)"的交叉开发模式。
本章的知识框架很清晰:①嵌入式软件开发的特点和挑战(5.1);②开发环境——宿主机/目标机、编辑器/编译器/调试工具/集成开发环境(5.2);③开发流程——平台选型、软件设计、编码、下载运行(5.3);④软件移植(5.4)。其中"交叉编译""调试方法分类""设计约束""μC/OS-II 移植"都是软考爱考的点。
核心概念拆解
5.1 嵌入式软件开发概述
5.1.1 开发过程:一个嵌入式应用项目的开发是硬件设计与软件设计的综合过程,一般经历 6 步:
- 硬件的设计与实现(元器件选型、原理图编制、印制板设计、样板试制、硬件功能测试);
- 设备驱动软件的设计与实现(引导加载程序、各种设备驱动程序);
- 嵌入式操作系统的选择、移植以及 API 接口函数的设计;
- 支撑软件的设计与调试;
- 应用程序的设计与调试;
- 系统联调、样机交付。
「考点」上午题曾考过"嵌入式应用开发过程各步骤的先后顺序"。记住主线:先硬件,再驱动,再操作系统,再支撑软件,最后应用程序和联调。
5.1.2 软件开发的特点(共 5 条,是本节最核心的考点):
- 需要交叉编译工具:目标机资源有限(处理器结构简单、速度慢、内存外存小),无法在其上建立开发环境;宿主机多用 x86,而目标机是 ARM、MIPS、PowerPC 等,指令集不同,源程序必须经交叉编译才能生成目标平台上运行的二进制代码。
- 通过仿真手段进行调试:通过接口和信号线,把目标机上机器指令的执行结果和 CPU 各寄存器的值传送到集成开发平台,让开发人员观察目标机的执行状况。
- 开发板是中间目标机:应用软件先在开发板上完成所有开发任务,开发完成后才安装到目标机上运行。
- 可利用的资源有限:代码不仅要提供功能,还必须满足速度(系统期限)、内存总量、功耗等约束。
- 需要与硬件打交道:经常要对运算器、寄存器和存储器进行操作;即使有操作系统,也常允许应用程序直接访问外围寄存器。程序员既要会高级语言(C、C++、Java)和低级语言(汇编),还要懂硬件设计与除错。
5.1.3 面临的挑战(4 个方面):
- 软硬件协同设计:哪些功能用硬件实现、哪些用软件实现。硬件实现速度快,但芯片成本高、耗电量大、占空间;软件实现灵活性高(算法变了改软件即可),但要占用处理器时间和内存。例:TCP/IP 协议栈传统上用软件实现以保证灵活性,如今也出现了单芯片实现方案,可加速协议处理并集成到嵌入式硬件中。
- 嵌入式操作系统:分无操作系统(应用软件直接建立在硬件上,代码紧凑、体积小、效率高,可混合使用汇编和 C)和有操作系统(先把操作系统移植到目标处理器,在其 API 之上开发应用,开发快、代码可靠,是目前广泛采用的方式)两种情形。
- 代码优化:存储器容量和执行时间是嵌入式最主要约束,可能需要用汇编编写部分代码。
- 有限的输入/输出功能:许多嵌入式系统只有小键盘、少量 LED 或小型 LCD,有的(如过程控制系统)只用电信号输入输出,开发、测试、调试这类系统更有挑战。
5.2 嵌入式软件开发环境
5.2.1 宿主机和目标机:开发环境与目标运行环境分离。宿主机(Host)是用于开发的 PC 或工作站,运行编辑器、交叉编译器、交叉调试器、集成环境、分析工具等;目标机(Target)可以是实际运行环境或仿真系统,主要用来运行包含应用程序代码和嵌入式操作系统的可执行映像。目标机端要接收并执行宿主机发出的命令(设置断点、读写内存等),其上的"代理"负责解释执行这些命令——代理可以是软件(目标机监控器),也可以是硬件(BDM、JTAG)。
宿主机与目标机的连接分两类:
- 物理连接:串口、以太网接口、OCD(On Chip Debug,如 JTAG、BDM)三种方式,是逻辑连接的基础;
- 逻辑连接:按某种通信协议建立的通信连接。
「考点」实际开发中最常用的是以太网连接(带宽高、具网络连接优点);串口主要适用于两种情形——①应用不需要网络且代码规模受限(可裁掉操作系统的网络部分);②操作系统的网络驱动不支持内核调试时。两种方式可并存:下载映像用以太网,内核调试用串口。
5.2.2 嵌入式软件开发工具:
- 编辑器:理论上任何文本编辑器都行,常用独立编辑器有 UltraEdit(支持二进制/十六进制编辑,可直接修改 EXE/DLL)和 Source Insight(面向工程项目的源码编辑查看软件,动态保持符号信息数据库,能显示参考树、类继承图、调用树,适合大型软件)。
- 编译器:核心概念就是交叉编译器。优秀的嵌入式 C 编译器生成的代码,其长度和执行时间仅比汇编长 5%~20%——编译质量是区别嵌入式 C 编译器的重要指标。最常用的是 GNU C/C++(gcc),支持多种宿主机/目标机组合(目标机如 x86、PowerPC、MIPS、SPARC、Motorola 68K 等)。gcc 的编译过程分 4 个阶段:
- 预处理(宏定义、include 文件展开);
- 编译成汇编代码(按参数做不同程度优化);
- 汇编(汇编器生成目标代码);
- 连接(连接器把目标代码、系统目标代码和库函数连成可执行代码)。
- 调试工具(交叉调试):调试器运行在宿主机上,被调试程序运行在目标机上,两者通过串口、并口、网络、JTAG 等通信,目标机上有调试"代理"(软件或硬件)。原书介绍了 6 种调试方法,这是本章重中之重:
| 方法 | 原理 | 优缺点 |
|---|---|---|
| 直接测试法 | 固化程序到目标机运行、观察结果,不行就改代码重烧 | 无需工具但效率极低,基本无法监测程序运行,早期常用 |
| 调试监控器法 | 目标机 ROM 中固化一段监控器程序,配合宿主机调试器工作(下载、读写内存寄存器、设断点、单步) | 成本低、不需专门调试硬件,是目前使用最广泛的方式之一 |
| ROM 仿真器法 | 用硬件设备替代目标机 ROM 芯片,对目标机像 ROM、对调试器像监控器 | 不完全的调试方式,常与监控器法结合;省去开发监控器的麻烦,不占用目标机有限资源 |
| 在线仿真器法(ICE) | 用仿真器替代目标机 CPU,本身是一个有 CPU/RAM/ROM 的嵌入式系统 | 功能最强:软硬件断点、复杂断点触发、实时跟踪、非干扰查询;适合调试实时系统、驱动程序;缺点是极其昂贵(几千到几万美元) |
| 片上调试法(OCD) | CPU 芯片内置调试功能,可视为"廉价的 ICE"——价格约为 ICE 的 20%,提供约 80% 的功能 | CPU 分正常/调试两种模式;不占用通信端口资源、支持软硬件断点和时序分析;但实时性不如 ICE,不支持非干扰查询,且要求 CPU 具备 OCD 功能。常见实现:BDM、JTAG(主流)、OnCE |
| 模拟器法 | 宿主机上的纯软件,模拟目标机指令系统(指令级)或操作系统系统调用(系统调用级) | 好处是无需真实目标机即可开发、可利用宿主机资源诊断;缺点是与真实环境差别大、不能模拟所有设备、实时性差 |
「考点」ICE 的"软件断点只能到指令级别;硬件断点可由取指令、内存读写、I/O 读写、中断等多种事件触发"常出判断题。OCD 的"20% 价格、80% 功能"数字也考过。
- 软件工程工具:CVS(Concurrent Version System)是版本控制软件,只存储版本之间的区别(增量),记录每次修改的作者、时间、原因,适合分布式开发环境的版本管理;GNU make 是代码维护工具,读取 makefile 中定义的依赖关系和命令,只更新需要更新的文件,适合大中型项目的编译、连接管理,支持递归管理层次目录结构。
5.2.3 集成开发环境(IDE,Integrated Development Environment):原书举了三类例子:
- Tornado(WindRiver 公司,配套实时操作系统 VxWorks):由三部分组成——宿主机/目标机上的交叉开发工具和实用程序、目标机上的 VxWorks、连接宿主机和目标机的通信介质(以太网、串口、ICE、ROM 仿真器等)。宿主机上的工具与目标机的通信由目标服务器(Target Server)和目标代理(Target Agent)共同完成。工具包括交叉调试器 CrossWind/WDB(支持任务级和系统级调试、源码与汇编混合显示)、工程配置工具 Project(自动生成 Makefile)、诊断分析工具 WindView、集成仿真工具 VxSim、命令行执行工具 WindSh(可直接解释执行 C 语言表达式)等。
- Windows CE 开发工具:Platform Builder(创建/配置/调试操作系统内核)、eMbedded Visual C++、eMbedded Visual Basic 等。
- Linux 下的 IDE:Kdevelop、Eclipse(开放源码,通过插件可扩展到任何语言)、Anjuta。
5.3 嵌入式软件开发
5.3.1 平台选型:嵌入式系统设计分分析、设计、实现三个阶段(分析=需求阶段)。硬件平台选择处理器的考虑因素:处理性能(目标是选"能完成作业"的处理器而非最快的)、技术指标(外围设备集成度)、功耗(手持设备、PDA、手机要求高性能低功耗)、软件支持工具、是否内置调试工具。软件平台选择涉及操作系统、编程语言、集成开发环境三方面。选操作系统考虑:提供的开发工具、向硬件移植的难度、内存要求、可剪裁性和实时性能。编程语言上,汇编不可替代(直接操作硬件、效率高),一般首选汇编和 C,再考虑 C++ 或 Java。
5.3.2 软件设计:软件设计的任务包括准备工作计划、确定软件结构(任务结构、线程、公共数据结构、模块结构、内存分配等)、设计评审、维护工作计划、与硬件部门协调、工作记录存档。
软件架构(体系结构)设计原则——抽象、信息隐藏、强内聚和松耦合、关注点分离:
- 抽象:核心原则,提取主要特征和属性、忽略细节,对流程、数据、行为进行抽象;
- 信息隐藏:包括局部化设计(把信息和操作限制在一个组件内)和封装设计(外部访问形式简单统一);
- 强内聚和松耦合:内聚指组件内所有处理高度相关;耦合指组件间尽量无直接关系,便于修改和复用;
- 关注点分离:把系统中"多变的部分"(如适应性参数调整、驱动配置)设计成相对独立的组件。
架构设计用纵向分解(分层)和横向分解两种方式;一般按需求自上而下设计,也可在已有成熟"模式"时自底向上。设计方法分为三类:基于功能分解(实时结构化分析与设计;DARTS——基于并发任务结构化的设计,辅助确定并发任务并定义任务接口)、基于信息隐藏(面向对象 OO,数据与操作封装在对象中,外界只能通过消息间接访问)、基于模型驱动开发(MDD,如 IBM Rhapsody,重心从编码转移到设计)。
「考点」DARTS 的全称(Design Approach for Real-Time Systems)和"任务结构化"定位在上午题出现过。
5.3.3 特性设计技术:
- 实时性设计:分软实时(越快越好,无严格时限)和硬实时(有明确任务执行时限,不满足必须处理)。通常系统两者兼有。措施包括:①合理划分实时单元和分时单元(如信号处理系统中信号的翻译解释传递是实时单元,信息输出、故障记录可以是分时单元);②合理划分实时任务,任务分解规则——两个任务若满足以下条件之一可分开:时间(依赖的周期条件频率/时间段不同)、异步性(条件无时间关系)、优先级(需要不同优先级)、清晰性/可维护性(功能或逻辑上可分开);③程序上优化:关中断/关调度范围最小化、中断服务程序简短、循环体工作量最小化、频繁使用的变量设为寄存器变量、采用经典高效算法。
- 可扩展性设计:混合编程(汇编写硬件相关/实时要求严格的代码,高级语言写逻辑功能函数,高级语言移植性更好);引入硬件驱动层封装各类设备驱动,上层软件通过标准接口访问、与硬件隔离(CPU 片内驱动包括寄存器、时钟、中断、异常、存储管理单元;外设驱动包括串口、网口、鼠标、键盘、存储器等);模块化设计(模块分顺序模块、增量模块、并行模块;内聚性衡量模块内功能强度相关性、耦合性衡量模块间依赖相关性;数据设计要建立数据词典等)。
- 可定制性设计:包括可剪裁性(系统剪裁后通常只剩:一个引导设施、一个具备任务管理和定时功能的最基本内核、一个初始任务;耦合度越小剪裁力度越大)和可配置性(通过参数配置系统功能和规模)。
5.3.4 设计约束(软考下午题很爱出,要点多但都好理解):
- 接口设计约束:参数定义(个数、属性、单位、次序)一致;不能修改仅作输入的参数;全局变量在各引用模块中定义相同;模块间传递参数不超过 5 个,过多时用结构体传递。
- 中断设计约束:初始化阶段屏蔽无用中断并设置入口返回;开关中断范围合适;尽量避免中断嵌套;中断处理程序尽可能简短,不要在其中调用操作系统的资源申请或时间等待服务。
- 模块设计约束:除中断服务程序外采用单入口单出口结构;耦合方式按数据耦合、控制耦合、外部耦合、公共数据耦合、内容耦合的优先顺序处理(数据耦合最好);内聚按功能内聚、顺序内聚、通信内聚、时间内聚、逻辑内聚、偶然内聚的优先顺序处理(功能内聚最好);禁止使用递归设计——嵌入式任务栈小,递归易栈溢出。
- 异常设计约束:对所有可能异常进行接管,统一异常处理机制使系统转入安全状态。
- 数据安全设计约束:数据范围检查、保证精度、避免对浮点数做相等关系判断、避免浮点下溢、定期检查存储器和总线等。
- 余量设计约束:存储量、I/O 吞吐率及处理时间应留有不少于 20% 的余量(常考数字)。
- 其他约束:响应时间要求高时用抢占式调度;并发处理要用可重入函数(仅用局部变量或对全局变量互斥保护);优先使用操作系统提供的互斥机制;仅在初始化时分配内存,正常状态不释放内存(动态分配释放会产生内存碎片,导致申请不到内存、行为不确定);变量使用前应初始化(静态变量不能依赖编译器);延时避免用循环方法(编译优化可能出错),应用硬件高精度时钟。
5.3.5 编码:编码分 4 步——确定标准格式/编程规范、准备编程环境、编写代码、代码审查(先准备检查清单并设定要找到的 bug 数量)。嵌入式编码要对执行时间、存储空间、开发/维护时间三种资源进行优化。编码准则:函数短小精悍(一个函数只实现一个功能,长度不超过 100 行);封装代码;消除冗余代码;减少实时代码;遵守编码标准并借助检查工具。编程规范涉及命名规则、编码格式、注释书写三方面。性能优化上:整数运算最快,其次是有硬件支持的浮点运算,软件实现的浮点运算最慢——尽量用整数加减法,避免乘除法和无硬件支持的浮点运算。原书例子:遍历结构体数组时,用指针递增(加法)替代数组下标(每次要乘元素大小),在奔腾 4 上重复多次访问,指针方式约 1ms,下标方式约 2.13ms。
5.3.6 下载和运行:交叉编译生成的目标代码经串口或网络下载到目标机,调试通过后固化到 EEPROM、FLASH 等存储器,或 DOC/DOM 电子盘中,之后目标机脱离宿主机独立运行。常用固化手段:①用编程器把二进制映像写入存储芯片;②用 TFTP(Trivial File Transfer Protocol,简单文件传输协议)远程传送。TFTP 相比 FTP 的主要区别是没有用户权限管理(不需要认证),因此远程启动的目标板在启动完整操作系统之前就能下载启动映像;一般由 Bootloader 的 TFTP 客户端下载映像并烧写到 Flash,此后每次启动直接从 Flash 载入。
5.4 嵌入式软件移植
嵌入式软件高度依赖目标应用的软硬件环境,部分功能由汇编完成,可移植性差,但移植可缩短开发周期、降低成本。注意:可移植性和可重用性是"第二目标"——追求过高会降低实时性、增加代码量,对资源有限的嵌入式环境得不偿失。移植分三种情形:
5.4.1 无操作系统的软件移植:若软件主要是 C 语言开发,可以做层次化设计:两层结构中 I/O 模块属于设备驱动程序层(与硬件相关),向上提供与硬件无关的 API,移植时只需重写 I/O 模块、不改 API;进一步可在硬件层之上加硬件抽象层(HAL),形成三层结构,需要移植的代码进一步减少。
5.4.2 有操作系统的软件移植(整体移植):嵌入式软件体系结构分四层:设备驱动层、操作系统层、中间件层、应用软件层。整体移植到新硬件平台时,真正需要移植的是与硬件直接相关的部分:引导加载程序 BootLoader、设备驱动程序、操作系统中与处理器密切相关的代码;内核、中间件和应用软件不用修改。
「考点」BootLoader 一般分为 stage1 和 stage2:依赖 CPU 体系结构的代码(如设备初始化)放在 stage1,用汇编实现;stage2 用 C 实现。移植工作量主要在 stage1,基本要重写。
原书以 μC/OS-II 为例:其代码分三部分——与处理器无关的代码(任务管理、调度、存储管理、信号量、邮箱、消息队列等,不用改)、系统配置相关代码(OS_CFG.H、INCLUDES.H 等,用于裁剪内核)、与处理器相关的代码(OS_CPU.H、OS_CPU_A.ASM、OS_CPU_C.C 三个文件)。移植 μC/OS-II 主要修改这三个文件:
- OS_CPU.H:定义处理器栈增长方向的符号常量、关/开中断的 3 个宏定义、与编译器无关的数据类型(操作系统内部只使用这些类型);
- OS_CPU_A.ASM:用汇编编写 4 个与处理器相关的函数,包括任务切换、时钟中断服务程序等;
- OS_CPU_C.C:用 C 编写与操作系统相关的函数,一般只需改写任务堆栈初始化函数这一个。
一个移植实例可能需要编写或改写 50~300 行代码。
5.4.3 应用软件的移植(换操作系统平台):涉及编程语言和运行平台两个因素(Java 例外——既是语言又是平台)。提高可移植性的 6 条原则:
- 层次化设计和模块化设计:层次化是纵向结构,移植时只改底层不改上层;模块之间相互独立,便于裁减更新;
- 引入虚拟机层/操作系统抽象层:封装通用操作系统 API,应用只调用虚拟层 API,移植时只需重新实现虚拟层;定义虚拟层时尽量采用标准接口如 POSIX 标准;
- 尽量使用可移植的函数(标准 C 函数或自编函数);
- 用宏定义一组可移植的数据类型(如
#define INT32U unsigned int),应用内部只用这些类型; - 将不可移植部分局域化:通过宏定义和函数集中于特定文件,便于定位和修改;
- 提高代码可重用性:抽象函数使之模块化、功能专一、接口简洁。
初学者容易踩的坑 / 易错考点
- 交叉编译 vs 交叉调试:交叉编译是"在宿主机上编译出目标机的代码";交叉调试是"调试器在宿主机、被调试程序在目标机"。前者解决指令集不同的问题,后者解决目标机无法运行调试器的问题,别混为一谈。
- 调试方法容易张冠李戴:ICE 是"替代 CPU",ROM 仿真器是"替代 ROM 芯片",OCD 是"CPU 芯片自带的调试功能"。题目问"实时跟踪指令周期信息"选 ICE,问"廉价、主流、JTAG"选 OCD,问"纯软件、不需要硬件"选模拟器。
- TFTP 和 FTP 的区别:TFTP 没有(不需要)用户权限认证,这是它适合目标板远程启动的原因,不是"更安全"。
- μC/OS-II 移植改 3 个文件:OS_CPU.H、OS_CPU_A.ASM、OS_CPU_C.C;不要写成"重写整个内核"。OS_CPU_C.C 一般只改任务堆栈初始化函数。
- BootLoader 两阶段的语言:stage1 用汇编(与 CPU 体系结构相关),stage2 用 C。题目反着说就是错的。
- "禁止递归"的原因是任务栈小易溢出,而不是"效率低";"正常状态不释放内存"的原因是动态分配释放产生内存碎片,可能导致申请不到内存、软件行为不确定。
小结 & 备考提示
本章的主线是"交叉开发":因为目标机资源有限,开发环境建在宿主机上,靠交叉编译器生成目标代码,靠下载/固化手段(编程器、TFTP)部署,靠 6 种调试方法(直接测试、调试监控器、ROM 仿真器、ICE、OCD、模拟器)排错;软件设计要遵守强内聚松耦合、20% 余量等约束,移植时抓住"与硬件相关的部分"(BootLoader stage1、驱动、CPU 相关代码)即可。
必须记住的关键词:交叉编译 / 宿主机与目标机 / JTAG 与 ICE / BootLoader stage1-stage2 / μC/OS-II 移植三文件。