···························································· 
第5 章 chapter5 
基于ARM 的嵌入式软件开发
学习嵌入式程序开发的起点是最简单的程序。一个基本的Linux应用程序可以涵
盖编程的所有基础知识,通过编写Linux应用程序,可以帮助读者快速入门程序开发。
本章主要侧重于嵌入式C语言程序设计的基础知识、设计技巧,以及C语言与汇编
混合编程的内容,旨在引导读者进入嵌入式程序开发的领域。
5.1 嵌入式C 语言程序设计基础
很多编程书籍都以输出“Hello,World!”向初学者展示如何编写程序。虽然这个程
序很简单,但它却展示了C程序的基本要素:语法格式、引用头文件、调用库函数等。本
节将展示程序的编辑、编译和执行的相关知识。
5.1.1 Hello World 
1.用VIM 编辑源代码文件hello.c 
在Linux终端中输入vimhello.c。
2.编写源代码
在屏幕的左下角会出现文件名为hello.c的标识,表示一个新建的文件,名称为hello. 
c。接下来,在键盘上输入小写字母i,屏幕的左下角将显示“插入”,表示目前进入了插入
模式,此时可以输入源代码。根据示例,输入相应的源代码。 
#include <stdio.h> 
int main(void) 
{ 
printf("Hello,World\r\n"); 
return 0; 
} 
3.保存退出
输入实例所示的源代码后,在当前状态下按Esc键,输入“:wq”,按Enter键,将保存

第◆5 章 基于ARM 的嵌入式软件开发1 61 
文件并退出VIM。
4.用GCC编译程序
编辑好源文件hello.c文件后,需要把它编译成可执行文件才可以在Linux主机上运
行。在控制终端当前目录下,输入以下命令完成编译。 
gcc hello.c -o hello 
GCC编译器会将源代码文件编译连接成Linux可以执行的二进制文件。其中,-o表
示输出的二进制文件名字为hello。
此时,可以在终端输入“./hello”命令来运行编译好的程序。 
root@altaslesson:~/c#vim hello.c 
root@altaslesson:~/c#gcc -o hello hello.c 
root@altaslesson:~/c#./hello 
Hello,Worid! 
root@altaslesson:~/c# 
5.1.2 GCC 与交叉编译器
在5.1.2节中已经使用GCC编译器编译.c文件,接下来介绍编译器的使用方法。
1.gcc命令
gcc命令的格式如下: 
gcc [选项][文件名字] 
主要选项如下。
.-c:只编译,不链接为可执行文件,编译器将输入的.c文件编译为.o的目标文件。
.-o<输出文件名>:用来指定编译结束以后的输出文件名,如果不使用这个选
项,则GCC默认编译出来的可执行文件名字为a.out。
.-g:添加调试信息,如果使用调试工具(如GDB),就必须加入此选项,此选项指示
编译的时候生成调试所需的符号信息。
.-O:对程序进行优化编译,如果使用此选项,那么整个源代码在编译、链接的时候
都会进行优化,这样产生的可执行文件的执行效率更高。
.-O2:比-O 有更大幅度的优化,生成的可执行文件的执行效率更高,但是整个编
译过程会很长。
2.编译流程
GCC编译器的工作流程一般包括预处理、汇编、编译和链接阶段。预处理阶段主要
对程序中的宏定义等相关内容进行初步处理。汇编阶段将C文件转换为汇编文件。编
译阶段将C源文件编译为以.o结尾的目标文件。生成的目标文件无法直接执行,需要进
行链接操作。如果项目中包含多个C源文件,则会生成多个目标文件,这些目标文件需

1 62 ◆嵌入式系统开发与应用
要链接在一起,组成完整的可执行文件。
在上一部分的示例程序中,只包含一个简单的文件,因此可以直接使用gcc命令生成
可执行文件。
3.交叉编译器安装
在进行ARM 裸机、Uboot移植以及Linux移植等任务时,需要在Linux环境下进行
编译操作。编译过程需要使用编译器。之前已经介绍了如何在Linux环境下进行C语
言开发,并使用了GCC编译器编译代码。然而,Linux系统自带的GCC编译器主要用于
X86架构,因此,使用该编译器生成的程序无法在ARM 架构上运行。为了在X86架构
的操作系统上进行ARM 架构目标主机的编译工作,需要借助交叉编译工具链。交叉编
译工具链包含交叉编译器GCC。在交叉编译器中,“交叉”一词表示在一个架构上编译另
一个架构的代码,实际上是将两种架构“交叉”起来。通过使用交叉编译工具链,可以在
Linux环境下为ARM 架构生成可执行程序,从而在ARM 主机上运行。交叉编译工具
链中的交叉编译器GCC就提供了这样的能力。
交叉编译器有很多种,这里以Linaro出品的交叉编译器为例进行介绍。Linaro是一
家非营利性质的开放源代码软件工程公司,开发了很多软件,最著名的就是LinaroGCC 
编译工具链(编译器),关于Linaro的详细介绍可以到Linaro官网查阅。LinaroGCC编
译器的下载地址如下:http://releases.linaro.org/components/toolchain/binaries/5.4- 
2017.05/aarch64-linux-gnu/gcc-linaro-5.4.1-2017.05-x86_64_aarch64-linux-gnu.tar.xz。
在交叉编译器下载完成后进行交叉编译工具链的安装,以下是安装步骤。
(1)登录Linux服务器。
(2)执行如下命令,切换至root用户。 
su -root 
(3)执行如下命令,创建/opt/compiler目录。 
mkdir /opt/compiler 
(4)使用文件传输工具将交叉编译工具链上传至/opt/compiler目录。
(5)进入/opt/compiler目录。 
cd /opt/compiler 
(6)执行如下命令,解压缩交叉编译工具。 
tar -xvf 交叉编译工具链-C ./ --strip-components 1 
(7)在配置文件中增加交叉编译工具链的路径。 
echo "export PATH=\$PATH:/opt/compiler/bin" >>/etc/profile 
(8)执行如下命令,使环境变量生效。 
source /etc/profile

第◆5 章 基于ARM 的嵌入式软件开发1 63 
(9)执行如下命令,查看交叉编译工具链的版本。 
aarch64-linux-gnu-gcc -v 
(10)如果显示版本信息,则表明工具链安装成功。
5.1.3 Makefile 
在前一部分中,介绍了如何在Linux环境下使用GCC编译器进行C语言编译,通过
在终端中执行gcc命令可以完成单个或少量.c文件的编译。然而,在工程规模较大的情
况下,例如存在数十、数百甚至数千个源代码文件时,在终端中逐个输入gcc命令显然是
不切实际的。
为了解决这个问题,可以编写一个描述编译源代码文件以及编译规则的文件,使用
该文件可以指定编译的源代码文件与编译方式。这样,每次需要编译整个工程时,只需
要执行该文件即可。这个文件的解决方案就是Makefile。
Makefile文件的作用是描述需要编译的文件和重新编译的条件。类似于脚本文件, 
Makefile中可以执行系统命令。使用Makefile,只需要执行make命令,整个工程就会自
动编译,大幅提高了软件开发的效率。
通过使用Makefile,程序员可以方便地管理包含大量源代码文件的工程,并定义编
译规则,使得编译过程自动化。这样,无论工程规模大小,都能更加高效地进行软件开
发。接下来以一个例子介绍make工具和Makefile语法。项目需要完成以下任务:通过
键盘输入两个整型数字,然后计算它们的和,并将结果显示在屏幕上,在这个工程中有
main.c、input.c和calcu.c三个.c文件,以及input.h、calcu.h两个头文件。其中main.c是
主体,input.c负责接收从键盘输入的数值,calcu.h进行任意两个数相加的操作,其中
main.c文件的内容如下: 
#include <stdio.h> 
#include "input.h" 
#include "calcu.h" 
int main(void) 
{ 
int a,b,num; 
input_int(&a, &b); 
num =calcu(a, b); 
printf("%d +%d =%d\r\n", a, b, num); 
} 
input.c文件的内容如下: 
#include <stdio.h> 
#include "input.h" 
void input_int(int *a, int *b) 
{ 
printf("input two num:");

1 64 ◆嵌入式系统开发与应用 
scanf("%d %d", a, b); 
printf("\r\n"); 
} 
calcu.c文件的内容如下: 
#include "calcu.h" 
int calcu(int a, int b) 
{ 
return (a +b); 
} 
input.h文件的内容如下: 
#ifndef _INPUT_H 
#define _INPUT_H 
void input_int(int *a, int *b); 
#endif 
calcu.h文件的内容如下: 
#ifndef _CALCU_H 
#define _CALCU_H 
int calcu(int a, int b); 
#endif 
以上是这个工程的所有源文件,接下来使用前面介绍的方法进行编译,在终端输入
如下命令: 
gcc main.c calcu.c input.c -o main 
上面命令的意思就是使用GCC编译器对main.c、calcu.c和input.c这三个文件进行
编译,编译生成的可执行文件叫作main。编译完成以后执行main这个程序,测试软件是
否工作正常。 
root@altaslesson:~/c# 
root@altaslesson:~/c#gcc main.c calcu.c input.c -o main 
root@altaslesson:~/c# 
root@altaslesson:~/c#./main 
input two num:5 6 
5+6=11 
root@altaslesson:~/c# 
然而,在工程规模庞大的情况下,例如当拥有数千个源文件时,若仅使用之前提到的
命令编译方式,那么一旦任何一个文件发生修改,所有文件都将被重新编译。如果工程
中拥有数万个源文件(例如Linux源码),那么每次重新编译数万个文件将耗费大量时
间。为了更高效地处理这种情况,最理想的方式是仅编译已被修改的文件,而无须重新
编译未被修改的文件。为此,需要改变编译的方法。在首次编译工程时,先编译所有源
文件。之后,在某个文件发生修改时,可以仅对这个修改的文件进行编译,而无须重新编
译其他未更改的文件。具体的命令如下:

第◆5 章 基于ARM 的嵌入式软件开发1 65 
gcc -c main.c 
gcc -c input.c 
gcc -c calcu.c 
gcc main.o input.o calcu.o -o main 
上述命令的前三行分别是将main.c、input.c和calcu.c编译成对应的.o文件,所以使
用了-c选项,只进行编译而不链接。最后一行命令是将编译出来的所有.o文件链接成可
执行文件main。假如现在修改了calcu.c这个文件,只需要将caclu.c这个文件重新编译
成.o文件,然后将所有的.o文件链接成可执行文件即可: 
gcc -c calcu.c 
gcc main.o input.o calcu.o -o main 
但是这样又会产生一个问题,如果修改了大量的文件,可能都不记得哪个文件修改
过了,然后忘记编译,为此需要这样一个工具: 
. 如果工程没有编译过,那么工程中的所有.c文件都要被编译并链接成可执行
程序; 
. 如果工程中只有个别.c文件被修改了,那么只编译这些被修改的.c文件即可; 
. 如果工程的头文件被修改了,那么需要编译所有引用这个头文件的.c文件并链接
成可执行文件。
很明显,能够完成这个功能的就是Makefile,需要在工程目录下创建名为Makefile 
的文件。
在Makefile中输入如下代码: 
main: main.o input.o calcu.o 
gcc -o main main.o input.o calcu.o 
main.o: main.c 
gcc -c main.c 
input.o: input.c 
gcc -c input.c 
calcu.o: calcu.c 
gcc -c calcu.c 
clean: 
rm *.o 
rm main 
上述代码中,所有行首需要空出来的地方一定要使用Tab键实现。
Makefile编写好后,可以使用make命令编译工程,直接在命令行中输入make即
可,make命令会在当前目录下查找是否存在Makefile这个文件,如果存在,就会按照
Makefile中定义的编译方式进行编译。
使用make命令编译完成后会在当前工程目录下生成各种.o文件和可执行文件,说
明编译成功,接下来就可以运行程序了。 
root@altaslesson:~/c#ls 
calcu.c calcu.h input.c input.h main .c Makefile 
root@altaslesson:~/c#

1 66 ◆嵌入式系统开发与应用 
root@altaslesson:~/c#make 
gcc -c main .c 
gcc-c input.c 
gcc-c calcu.c 
gcc -o main main.o input.o calcu.o 
root@altaslesson:~/c#./main 
input two num:5 6 
5+6=11 
root@altaslesson:~/c#ls 
calcu.c calcu.h calcu.o input.c input.h input.o main main.c main.o Makefile 
root@altaslesson:~/c# 
由于生成了大量.o文件,故可以执行makeclean命令将可执行程序和.o文件一起
删除。 
root@altaslesson:~/c#ls 
calcu.c calcu.h calcu.o input.c input.h input.o main main.c main.o Makefile 
root@altaslesson:~/c# 
root@altaslesson:~/c#make clean 
rm *.o 
rm main 
root@altaslesson:~/c#ls 
calcu.c calcu.h input.c input.h main.c Makefile 
root@altaslesson:~/c# 
5.1.4 CMake 
不同的IDE集成的make工具所遵循的规范和标准都不同,导致其语法、格式不同, 
也就不能很好地跨平台编译,会再次使得工作烦琐起来。
cmake为了解决这个问题而诞生了,其允许开发者指定整个工程的编译流程,然后
根据编译平台生成本地化的Makefile和工程文件,最后只需make编译即可。
简而言之,可以把cmake看成一款自动生成Makefile的工具,所以编译流程就变成
了cmake→make→用户代码→可执行文件。
1.编写CMakeLists.txt 
首先编写C语言程序,使用前面使用过的main.c函数,然后编写CMakeLists.txt文
件,并保存在与main.c源文件同一个目录下。 
#CMake 最低版本要求
cmake_minimum_required (VERSION 2.8) 
#项目信息
project (Demo) 
#指定生成目标
add_executable(Demo main.c)

第◆5 章 基于ARM 的嵌入式软件开发1 67 
CMakeLists.txt的语法比较简单,由命令、注释和空格组成,其中命令是不区分大小
写的。符号“#”后面的内容是注释。命令由命令名称、小括号和参数组成,参数之间使
用空格分隔。对于上面的CMakeLists.txt文件,依次出现了以下几个命令。
(1)cmake_minimum_required:指定运行此配置文件所需的CMake的最低版本。
(2)project:参数值是Demo,该命令表示项目的名称是Demo。
(3)add_executable:将名为main.c的源文件编译成一个名称为Demo的可执行文
件。在本例中传入了两个参数,第一个参数表示生成的可执行文件对应的文件名,第二
个参数表示对应的源文件。
2.编译项目
在当前目录执行cmake,得到Makefile后再使用make命令编译得到Demo可执行
文件。 
root@altaslesson:~/c/cmake#cmake . 
--The C compiler identification is GNU 5.4.0 
--The CXX compiler identification is GNU 5.4.0 
--Check for working C compiler: /usr/bin/cc 
--Check for working C compiler: /usr/bin/cc --works 
--Detecting C compiler ABI info 
--Detecting C compiler ABI info -done 
--Detecting C compile features 
--Detecting C compile features -done 
--Check for working CXX compiler: /usr/bin/c++ 
--Check for working CXX compiler: /usr/bin/c++--works 
--Detecting CXX compiler ABI info 
--Detecting CXX compiler ABI info -done 
--Detecting CXX compile features 
--Detecting CXX compile features -done 
--Configuring done 
--Generating done 
--Build files have been written to: /root/c/cmake 
root@altaslesson:~/c/cmake#make 
Scanning dependencies of target Demo 
[50%]Building C object CMakeFiles/Demo.dir/main.c.o 
[100%]Linking C executable Demo 
[100%]Built target Demo 
root@altaslesson:~/c/cmake#./Demo 
Hello,World 
3.运行程序
执行./Demo可执行文件即可。
4.多个源文件
若存在多个源文件,且都在同一个目录下,则可以修改CMakeLists.txt为如下:

1 68 ◆嵌入式系统开发与应用 
#查找当前目录下的所有源文件
#并将名称保存到DIR_SRCS 变量
aux_source_directory(. DIR_SRCS) 
#指定生成目标
add_executable(Demo ${DIR_SRCS}) 
5.生成并添加库文件
在平时的开发过程中,也有很多场景需要将源码编译成库文件以供使用,这个需求
也可以使用cmake做到,这就需要用到以下命令: 
add_library(libhello 静态/动态库hello.c) 
若没有设置参数,则默认生成静态库文件,可以通过增加参数的方式设置指定的库文件。 
add_library(MathFunctions SHARED hello.c) #生成动态库文件
add_library(MathFunctions STATIC hello.c) #生成静态库文件
在生成之后,用如下命令设置目标文件需要链接的库。 
target_link_libraries (Demo MathFunctions) 
6.指定头文件搜索路径 
include_directories( 
../include/ 
/usr/local/include/ 
…
)
指定编译时,头文件的路径先搜索“../include”和“/usr/local/include”,然后到系统
的默认路径搜索。
7.指定库文件搜索路径 
link_directories( 
${LIB_PATH} 
$ENV{HOME}/ascend_ddk/${ARCH}/lib/ 
${INC_PATH}/atc/lib64 
)
在编译时会先到上述目录搜索库文件,然后到系统的默认路径搜索库文件。
5.2 嵌入式C 语言程序设计技巧
本节将介绍嵌入式C语言程序设计的技巧,包括C编译器及其优化算法,以及面向
对象编程和模块化编程的思想。

第◆5 章 基于ARM 的嵌入式软件开发1 69 
5.2.1 C 编译器及其优化方法
本节将帮助读者在ARM 处理器上编写高效的C代码。本节涉及的一些技术不仅
适用于ARM 处理器,也适用于其他RISC处理器。本节首先从ARM 编译器及其优化
方法入手,讲解C编译器在优化代码时碰到的一些问题。理解这些问题将有助于编写出
在提高执行速度和减少代码尺寸方面更高效的C源代码。
本节假定读者熟悉C语言,并且有一些汇编语言编程方面的知识。
1.为编译器选择处理器结构
在编译C源文件时,必须为编译器指定正确的处理器类型,这样可以使编译的代码
最大限度地利用处理器的硬件结构,如对半字加载(halfwordload)、存储指令(store 
instructions)和指令调度(instructionscheduling)的支持。所以编译程序时,应该尽量准
确地告诉编译器该代码运行在什么类型的处理器上。有些类型的编译器不能直接支持, 
如SA-1100,这时可以使用与该类型处理器为同一指令集的基本处理器,例如对于SA-100, 
可以使用StrongARM。
指定目标处理器可能使代码与其他ARM 处理器不兼容。例如,编译时指定了
ARMv6体系结构的代码,可能不能运行在ARM920T 的处理器上(当代码中使用了
ARMv6体系结构中特有的指令)。
选择处理器类型可以使用--cpuname编译选项,该选项可以生成用于特定ARM 处
理器或体系结构的代码。
. 输入名称必须和ARM 数据表中所示严格一致,例如ARM7TDMI。该选项不接
受通配符字符。有效值是任何ARM6或更高版本的ARM 处理器。
. 选择处理器操作会选择适当的体系结构、浮点单元(FPU)以及存储结构。
. 某些--cpu选项暗含--fpu选项。
. ARM1136JF-S选项暗含--fpuvfpv2选项。隐式FPU 只覆盖命令行上出现在
--cpu选项前面的显式--fpu选项。如果没有指定--fpu 选项和--cpu 选项,则使
用--fpusoftvfp选项。
2.调试选项
如果在编译C源程序时设置了调试选项,则将很大程度地影响最终代码的大小和执
行效率。因为为了能够在调试程序时正确地显示变量或设置断点,带调试信息的代码映
像会包含很多冗余的代码和数据,所以如果想最大限度地提高程序执行效率、减少代码
尺寸,就要在编译源文件时去除编译器的调试选项。
以下选项指定调试表的生成方法。
.-g(--debug):该选项启用生成当前编译的调试表。无论是否使用-g选项,编译器
生成的代码都是相同的。唯一的区别是调试表是否存在。编译器是否对代码进
行优化是由-O 选项指定的。默认情况下,使用-g选项等价于使用-g-dwarf2 
--debug_macros。注意,编译程序时,只使用-g选项而没有使用优化选项,编译器