第7章 嵌入式uClinux及其应用开发(1)

第7章 嵌入式uClinux及其应用开发(1)

本章从构建一个针对S3C4510B硬件平台的嵌入式uClinux操作系统和在其上进行应用程序的开发入手,逐步讲述如何在Linux环境下编写用户应用程序的方法和步骤,并为熟悉Windows操作系统的用户介绍在这种平台之上,使用何种工具编写和编译自己的应用。通过本章的学习,读者可以对嵌入式uClinux有一定的了解,并且掌握在Linux和Windows下嵌入式系统应用开发的基本方法。

 

本章主要内容有:

- 嵌入式uClinux系统概况

- 开发工具GNU的使用

- 建立uClinux开发环境

- 在uClinux下开发应用程序

7.1 嵌入式uClinux系统概况

在PC机上开发应用程序的用户都会有这样的感觉,PC机有完善的操作系统并提供应用程序接口(API),开发好的应用程序可以直接在操作系统上运行。虽然嵌入式系统的应用程序完全可以在裸板上运行,但为了使系统具有任务管理、定时器管理、存储器管理、资源管理、事件管理、系统管理、消息管理、队列管理和中断处理的能力,提供多任务处理,更好的分配系统资源的功能,用户就需要针对自己的硬件平台和实际应用选择适当的嵌入式操作系统(Embedded Operating System,以下简称EOS)。本节将结合本书所谈到的硬件平台S3C4510B,介绍一种针对不带MMU的ARM微处理器的嵌入式操作系统uClinux。

uClinux是一个完全符合GNU/GPL公约的操作系统,完全开放代码,现在由Lineo公司支持维护。uClinux的发音是“you-see-linux”,它的名字来自于希腊字母“mu”和英文大写字母“C”的结合。“mu”代表“微小”之意,字母“C”代表“控制器”,所以从字面上就可以看出它的含义,即“微控制领域中的Linux系统”。

为了降低硬件成本及运行功耗,有一类CPU在设计中取消了内存管理单元(Memory Management Unit,以下简称MMU)功能模块。最初,运行于这类没有MMU的CPU之上的都是一些很简单的单任务操作系统,或者更简单的控制程序,甚至根本就没有操作系统而直接运行应用程序。在这种情况下,系统无法运行复杂的应用程序,或者效率很低,而且,所有的应用程序需要重写,并要求程序员十分了解硬件特性。这些都阻碍了应用于这类CPU之上的嵌入式产品开发的速度。

然而,随着uClinux的诞生,这一切都改变了。

uClinux从Linux 2.0/2.4内核派生而来,沿袭了主流Linux的绝大部分特性。它是专门针对没有MMU的CPU,并且为嵌入式系统做了许多小型化的工作。适用于没有虚拟内存或内存管理单元(MMU)的处理器,例如ARM7TDMI。它通常用于具有很少内存或Flash的嵌入式系统。uClinux是为了支持没有MMU的处理器而对标准Linux作出的修正。它保留了操作系统的所有特性,为硬件平台更好的运行各种程序提供了保证。在GNU通用公共许可证(GNU GPL)的保证下,运行uClinux操作系统的用户可以使用几乎所有的Linux API函数,不会因为没有MMU而受到影响。由于uClinux在标准的Linux基础上进行了适当的裁剪和优化,形成了一个高度优化的、代码紧凑的嵌入式Linux,虽然它的体积很小,uClinux仍然保留了Linux的大多数的优点:稳定、良好的移植性、优秀的网络功能、完备的对各种文件系统的支持、以及标准丰富的API等。图7.1为uClinux的基本架构。

图7.1 uClinux的基本架构

Boot Loader:负责Linux内核的启动,它用于初始化系统资源,包括SDRAM。这部分代码用于建立Linux内核运行环境和从Flash中装载初始化ramdisk。

内核初始化:Linux内核的入口点是start_kernel()函数。它初始化内核的其他部分,包括捕获,IRQ通道,调度,设备驱动,标定延迟循环,最重要的是能够fork “init”进程,以启动整个多任务环境。

系统调用函数/捕获函数:在执行完“init”程序后,内核对程序流不再有直接的控制权,此后,它的作用仅仅是处理异步事件(例如硬件中断)和为系统调用提供进程。

设备驱动:设备驱动占据了Linux内核很大部分。同其他操作系统一样,设备驱动为它们所控制的硬件设备和操作系统提供接口。

文件系统:Linux最重要的特性之一就是对多种文件系统的支持。这种特性使得Linux很容易地同其他操作系统共存。文件系统的概念使得用户能够查看存储设备上的文件和路径而无须考虑实际物理设备的文件系统类型。Linux透明的支持许多不同的文件系统,将各种安装的文件和文件系统以一个完整的虚拟文件系统的形式呈现给用户。

下面介绍一些和uClinux相关的知识。

1、 MMU(内存管理单元) 和VM(虚拟内存)

许多嵌入式微处理器都由于没有MMU而不支持虚拟内存。没有内存管理单元所带来的好处是简化了芯片设计,降低了产品成本。由于大多数的嵌入式设备没有磁盘或者只有很有限的内存空间,所以无需复杂的内存管理机制。但是由于没有MMU的管理,操作系统对内存空间是没有保护的,所有程序访问的地址都是实际物理地址。但从嵌入式系统一般都是实现某种特定功能的角度考虑,对于内存管理的要求完全可以由程序开发人员考虑。

2、实时性的支持

uClinux本身并不支持实时性,目前存在两种不同的方案提供uClinux对实时性的支持,它们分别是RTLinux(RTL)和RTAI(Real Time Application Interface)。有了这两种方案,uClinux可以应用到对实时性要求较高的场合。

3、 平台支持

开发uClinux的工具链:

开发uClinux通常用标准的GNU工具链。经过修改的工具链支持一些高级特性,比如XIP(Execute-In-Place)技术,共享库支持等。

uClinux所适用的微控制器:

uClinux适用于摩托罗拉的ColdFire/Dragonball,ARM系列(例如Atmel, TI, Samsung等生产的芯片),Intel i960, Sparc (例如无MMU的 LEON), NEC v850,甚至是开放的可综合(到CLPD内)的CPU核,比如OPENcore。

4、与标准Linux的兼容性

uClinux除了不能实现fork()而是使用vfork()外,其余uClinux的API函数与标准Linux的完全相同。这并不是意味着uClinux不能实现多进程,实际上uClinux多进程管理是通过vfork()来实现的,或者是子进程代替父进程执行,直到子进程调用exit()函数退出,或者是子进程调用exec()函数执行一个新的进程。大多数标准的Linux应用程序在从Linux操作系统移植到uClinux系统时,几乎不用做什么大的改动,就可以完全达到对一个嵌入式应用程序的要求(例如合理的资源使用)。uClibc对libc(可用于标准Linux的函数库)做了修改为uClinux提供了更为精简的应用程序库。

5、 网络的支持

uClinux带有一个完整的TCP/IP协议,同时它还支持许多其他网络协议。uClinux 对于嵌入式系统来说是一个网络完备的操作系统。

6、应用领域

uClinux广泛应用于嵌入式系统中,例如VPN路由器/防火墙,家用操作终端,协议转换器,IP电话,工业控制器,Internet摄像机,PDA设备等。

在对uClinux有了一个初步认识之后,有必要向读者介绍在嵌入式开发中最为普遍使用的编译工具GNU GCC。


7.2 开发工具GNU的使用

GCC(gcc)的不断发展完善使许多商业编译器都相形见绌, GCC由GNU创始人Richard Stallman首创,是GNU的标志产品,由于UNIX平台的高度可移植性,GCC几乎在各种常见的UNIX平台上都有,即使是Win32/DOS也有GCC的移植。 比如说SUN的Solaris操作系统配置的编译器就是GNU的GCC。

GNU软件包括C编译器GCC,C++编译器G++,汇编器AS,链接器LD,二进制转换工具(OBJCOPY,OBJDUMP),调试工具(GDB,GDBSERVER,KGDB) 和基于不同硬件平台的开发库。在GNU GCC支持下用户可以使用流行的C/C++语言开发应用程序,满足生成高效率运行代码、易掌握的编程语言的用户需求。

这些工具都是按GPL版权声明发布,任何人可以从网上获取全部的源代码,无需使用任何费用。关于GNU和公共许可证协议的详细资料,读者可以参看GNU网站的介绍,http://www.gnu.org/home.html。

GNU开发工具都是采用命令行的方式,用户掌握起来相对比较困难,不如基于Windows系统的开发工具好用,但是GNU工具的复杂性是由于它更贴近编译器和操作系统的底层,并提供了更大的灵活性。一旦学习和掌握了相关工具后,就了解了系统设计的基础知识。

运行于Linux操作系统下的自由软件GNU gcc编译器,不仅可以编译Linux操作系统下运行的应用程序,还可以编译Linux内核本身,甚至可以作交叉编译,编译运行于其它CPU上的程序。所以,在进行嵌入式系统应用程序开发时,这些工具得到了日益广泛的应用。


7.2.1 GCC编译器

GCC是GNU组织的免费C编译器,Linux的很多发布缺省安装的就是这种。很多流行的自由软件源代码基本都能在GCC编译器下编译运行。 所以掌握GCC编译器的使用无论是对于编译系统内核还是自己的应用程序都是大有好处的。

下面通过一个具体的例子,学习如何使用GCC编译器。

在Linux操作系统中,对一个用标准C语言写的源程序进行编译,要使用GNU的gcc编译器。

例如下面一个非常简单的Hello源程序(hello.c):

/*******************************************************

* Institute of Automation, Chinese Academy of Sciences

* File Name: hello.c

* Description:introduce how to compile a source file with gcc

* Author: Xueyuan Nie

* Date:

*******************************************************/

void main()
{
printf("Hello the world/n");
}
要编译这个程序,我们只要在Linux的bash提示符下输入命令:

$ gcc -o hello hello.c

gcc 编译器就会生成一个hello的可执行文件。在hello.c的当前目录下执行./hello就可以看到程序的输出结果,在屏幕上打印出“Hello the world”的字符串来。

命令行中 gcc表示是用gcc来编译源程序;

-o outputfilename选项表示要求编译器生成文件名为outputfilename的可执行文件,如果不指定-o选项,则缺省文件名是 a.out。在这里生成指定文件名为hello的可执行文件,而hello.c是我们的源程序文件。

gcc 是一个多目标的工具。gcc最基本的用法是:

gcc [options] file... ,

其中的option是以-开始的各种选项,file是相关的文件名。在使用gcc的时候,必须要给出必要的选项和文件名。gcc的整个编译过程,实质上是分四步进行的,每一步完成一个特定的工作,这四步分别是:预处理,编译,汇编和链接。它具体完成哪一步,是由gcc后面的开关选项和文件类型决定的。

清楚的区别编译和连接是很重要的。编译器使用源文件编译产生某种形式的目标文件(object files)。在这个过程中,外部的符号引用并没有被解释或替换,然后我们使用链接器来链接这些目标文件和一些标准的头文件,最后生成一个可执行文件。 在这个过程中,一个目标文件中对别的文件中的符号的引用被解释,并报告不能被解释的引用,一般是以错误信息的形式报告出来。

gcc编译器有许多选项,但对于普通用户来说只要知道其中常用的几个就够了。在这里为读者列出几个最常用的选项:

-o选项表示要求编译器生成指定文件名的可执行文件;

-c选项表示只要求编译器进行编译,而不要进行链接,生成以源文件的文件名命名但把其后缀由.c或.cc变成.o的目标文件;

-g选项要求编译器在编译的时候提供以后对程序进行调试的信息;

-E选项表示编译器对源文件只进行预处理就停止,而不做编译,汇编和链接;

-S选项表示编译器只进行编译,而不做汇编和链接;

-O选项是编译器对程序提供的编译优化选项,在编译的时候使用该选项,可以使生成的执行文件的执行效率提高;

-Wall 选项指定产生全部的警告信息。

如果你的源代码中包含有某些函数,则在编译的时候要链接确定的库,比如代码中包含了某些数学函数,在Linux下,为了使用数学函数,必须和数学库链接,为此要加入-lm 选项。也许有读者会问,前面那个例子使用printf函数的时候为何没有链接库呢?在gcc中对于一些常用函数的实现,gcc编译器会自动去链接一些常用库,这样用户就没有必要自己去指定了。有时候在编译程序的时候还要指定库的路径,这个时候要用到编译器的-L选项指定路径。比如说我们有一个库在 /home/hoyt/mylib下,这样我们编译的时候还要加上 -L/home/hoyt/mylib。对于一些标准库来说,没有必要指出路径。只要它们在起缺省库的路径下就可以了,gcc在链接的时候会自动找到那些库的。

GNU编译器生成的目标文件缺省格式为elf(executive linked file)格式,这是Linux系统所采用的可执行链接文件的通用文件格式。elf格式由若干段(section)组成,如果没有特别指明,由标准c源代码生成的目标文件中包含以下段:.text(正文段) 包含程序的指令代码,.data(数据段)包含固定的数据,如常量,字符串等,.bss(未初始化数据段) 包含未初始化的变量和数组等。
读者若想知道更多的选项及其用法,可以查看gcc的帮助文档,那里有许多对其它选项的详细说明。

当改变了源文件hello.c后,需要重新编译它:

$gcc -c hello.c

然后重新链接生成:

$gcc –o hello.o

对于本例,因为只含有一个源文件,所以当改动了源码后,进行重新的编译链接的过程显得并不是太繁琐,但是,如果在一个工程中包含了若干的源码文件,而这些源码文件中的某个或某几个又被其他源码文件包含,那么,如果一个文件被改动,则包含它的那些源文件都要进行重新编译链接,工作量是可想而知的。 幸运的是,GNU提供了使这个步骤变得简单的工具,就是下面要介绍给大家的GNU Make 工具。


7.2.2 GNU Make

make是负责从项目的源代码中生成最终可执行文件和其他非源代码文件的工具。 make命令本身可带有四种参数:标志、宏定义、描述文件名和目标文件名。

其标准形式为:

make [flags] [macro definitions] [targets]

Unix系统下标志位flags选项及其含义为:
  -f file  指定file文件为描述文件,如果file参数为 '-' 符,那么描述文件指向标准输入。如果没有 '-f' 参数,则系统将默认当前目录下名为makefile或者名为Makefile的文件为描述文件。在Linux中, GNU make 工具在当前工作目录中按照GNUmakefile、makefile、Makefile的顺序搜索 makefile文件。
  -i   忽略命令执行返回的出错信息。
  -s   沉默模式,在执行之前不输出相应的命令行信息。
  -r   禁止使用隐含规则。

-n   非执行模式,输出所有执行命令,但并不执行。
  -t   更新目标文件。
  -q   make操作将根据目标文件是否已经更新返回"0"或非"0"的状态信息。
  -p   输出所有宏定义和目标文件描述。
  -d   Debug模式,输出有关文件和检测时间的详细信息。
  Linux下make标志位的常用选项与Unix系统中稍有不同,下面只列出了不同部分:
  -c dir   在读取 makefile 之前改变到指定的目录dir。
  -I dir   当包含其他 makefile文件时,利用该选项指定搜索目录。
  -h   help文挡,显示所有的make选项。
  -w   在处理 makefile 之前和之后,都显示工作目录。
  通过命令行参数中的target ,可指定make要编译的目标,并且允许同时定义编译多个目标,操作时按照从左向右的顺序依次编译target选项中指定的目标文件。如果命令行中没有指定目标,则系统默认target指向描述文件中第一个目标文件。
make如何实现对源代码的操作是通过一个被称之为makefile的文件来完成的,在下面的小节里,主要向读者介绍一下makefile的相关知识。


7.2.2.1 makefile基本结构

GNU Make 的主要工作是读一个文本文件 makefile。makefile 是用bash语言写的,bash语言是很像BASIC语言的一种命令解释语言。这个文件里主要描述了有关哪些目标文件是从哪些依赖文件中产生的,是用何种命令来进行这个产生过程的。有了这些信息, make 会检查磁盘的文件,如果目标文件的日期(即该文件生成或最后修改的日期)至少比它的一个依赖文件日期早的话,make 就会执行相应的命令,以更新目标文件。
makefile一般被称为“makefile”或“Makefile”。还可以在 make 的命令行中指定别的文件名。如果没有特别指定的话,make就会寻找“makefile”或“Makefile”,所以为了简单起见,建议读者使用这两名字。如果要使用其他文件作为 makefile,则可利用类似下面的 make 命令选项指定 makefile 文件:
  $ make -f makefilename
一个 makefile 主要含有一系列的规则,如下:
目标文件名 : 依赖文件名

(tab键) 命令
第一行称之为规则,第二行是执行规则的命令,必须要以tab键开始。
下面举一个简单的makefile的例子。
executable : main.o io.o
gcc main.o io.o -o executable
main.o : main.c
gcc -Wall -O -g -c main.c -o main.o
io.o : io.c
gcc -Wall -O -g -c io.c -o io.o

这是一个最简单的 makefile,make从第一条规则开始,executable是makefile最终要生成的目标文件。给出的规则说明executable依赖于两个目标文件main.o和io.o,只要executable 比它依赖的文件中的任何一个旧的话,下一行的命令就会被执行。但是,在检查文件 main.o 和 io.o 的日期之前,它会往下查找那些把 main.o 或 io.o 做为目标文件的规则。make先找到了关于 main.o 的规则,该目标文件的依赖文件是main.c。makefile后面的文件中再也找不到生成这个依赖文件的规则了。此时,make开始检查磁盘上这个依赖文件的日期,如果这个文件的日期比main.o 日期新的话,那么这个规则下面的命令 gcc -c main.c –o main.o 就会执行,以更新文件 main.o 。同样make对文件io.o 做类似的检查,它的依赖文件是io.c,对io.o的处理和main.o类似。
现在, 再回到第一个规则处,如果刚才两个规则中的任何一个被执行,最终的目标文件executable都需要重建(因为executable所依赖的其中一个 .o 文件就会比它新),因此链接命令就会被执行。
有了makefile,对任何一个源文件进行修改后,所有依赖于该文件的目标文件都会被重新编译(因为.o 文件依赖于.c 文件),进而最终可执行文件会被重新链接(因为它所依赖的.o 文件被改变了),再也不用手工去一个个修改了。


7.2.2.2 编写make

1、Makefile 宏定义

makefile 里的宏是大小写敏感的,一般都使用大写字母。它们几乎可以从任何地方被引用,可以代表很多类型,例如可以存储文件名列表,存储可执行文件名和编译器标志等。

要定义一个宏,在makefile中,任意一行的开始写下该宏名,后面跟一个等号,等号后面是要设定的这个宏的值。如果以后要引用到该宏时,使用 $ (宏名),或者是${宏名},注意宏名一定要写在圆或花括号之内。把上一小节所举的例子,用引入宏名的方法,可以写成下面的形式:

OBJS = main.o io.o
CC = gcc
CFLAGS = -Wall -O -g

executable: $(OBJS)
$(CC) $(OBJS) -o executable

main.o : main.c

$(CC) $(CFLAGS) -c main.c -o main.o

io.o : io.c

$(CC) $(CFLAGS) -c io.c -o io.o

在这个makefile中引入了三个宏定义,所以如果当这些宏中的某些值发生变化时,开发者只需在要修改的宏处,将其宏值修改为要求的值即可,makefile中用到这些宏的地方会自动变化。在make中还有一些已经定义好的内部变量,有几个较常用的变量是$@, $< ,$?,$*, $^ (注意:这些变量不需要括号括住)。

$@ 扩展为当前规则的目标文件名;

$< 扩展为当前规则依赖文件列表中的第一个依赖文件;

$? 扩展为所有的修改日期比当前规则的目标文件的创建日期更晚的依赖文件,该值只有在使用显式规则时才会被使用;

$* 扩展成当前规则中目标文件和依赖文件共享的文件名,不含扩展名;

$^ 扩展为整个依赖文件的列表(除掉了所有重复的文件名)。

利用这些变量,可以把上面的 makefile 写成:

OBJS = main.o io.o
CC = gcc
CFLAGS = -Wall -O -g

executable: $(OBJS)
$(CC) $^ -o $@

main.o : main.c

$(CC) $(CFLAGS) –c $< -o $@

io.o : io.c

$(CC) $(CFLAGS) -c $< -o $@

可以将宏变量应用到其他许多地方,尤其是当把它们和函数混合使用的时候,正确使用宏,会给开发者带来极大的便利。

2、隐含规则

请注意,在上面的例子里,几个产生 .o文件的命令都是以.c 文件作为依赖文件产生 .o 目标(obj)文件,这是一个标准的生成目标文件的步骤。如果把生成main.o和io.o的规则从makefile中删除,make 会查找它的隐含规则,然后会找到一个适当的命令去执行。实际上make已经知道该如何生成这些目标文件,它使用变量 CC 做为编译器,并且传递宏 CFLAGS 给 C 编译器(CXXFLAGS用于 C++ 编译器),CPPFLAGS(C预处理选项), TARGET_ARCH (就目前例子而言,还不用考虑这个宏),然后它加入开关选项 -c ,后面跟预定义宏 $<(第一个依赖文件名),最后是开关项 -o,后跟预定义宏$@ (目标文件名)。一个C编译的具体命令将 会是:

$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c $< -o $@

在make 工具中所包含的这些内置的或隐含的规则,定义了如何从不同的依赖文件建立特定类型的目标。Unix系统通常支持一种基于文件扩展名即文件名后缀的隐含规则。这种后缀规则定义了如何将一个具有特定文件名后缀的文件(例如.c文件),转换成为具有另一种文件名后缀的文件(例如.o文件):

系统中默认的常用文件扩展名及其含义为:

.o  目标文件
  .c  C源文件
  .f  FORTRAN源文件
  .s  汇编源文件
  .y  Yacc-C源语法
  .l  Lex源语法
而GNU make 除了支持后缀规则外还支持另一种类型的隐含规则即模式规则。这种规则更加通用,因为可以利用模式规则定义更加复杂的依赖性规则。同时可用来定义目标和依赖文件之间的关系,例如下面的模式规则定义了如何将任意一个.c 文件转换为文件名相同的.o 文件:
  %.o : %.c
   $(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<

3、伪目标

如果需要最终产生两个和更多的可执行文件,但这些文件是相互独立的,也就是说任何一个目标文件的重建,不会影响其他目标文件。此时,可以通过使用所谓的伪目标来达到这一目的。一个伪目标和一个真正的目标文件的唯一区别在于,这个目标文件本身并不存在。因此, make总是会假设它需要被生成,当make把该伪目标文件的所有依赖文件都更新后,就会执行它的规则里的命令行。

举一个简单的例子,如果makefile 开始处输入
all : executable1 executable2

这里 executable1和executable2是最终希望生成的两个可执行文件。 make 把这个 'all' 做为它的主要目标,每次执行时都会尝试把 'all' 更新。但是,由于这行规则里并没有命令来作用在一个叫 'all' 的实际文件上(事实上, all 也不会实际生成),所以这个规则并不真的改变 'all' 的状态。可既然这个文件并不存在,所以 make 会尝试更新 all 规则,因此就检查它的依赖文件 executable1, exectable2 是否需要更新,如果需要,就把它们更新,从而达到生成两个目标文件的目的。 伪目标在makefile中广泛使用。

4、函数

makefile 里的函数跟它的宏很相似,在使用的时候,用一个 $ 符号开始后跟圆括号,在圆括号内包含函数名,空格后跟一系列由逗号分隔的参数。例如,在 GNU Make 里有一个名为 'wildcard' 的函 数,它只有一个参数,功能是展开成一列所有符合由其参数描述的文件名,文件间以空格间隔。可以像下面所示使用这个命令:
SOURCES = $(wildcard *.c)
这样会产生一个所有以 '.c' 结尾的文件的列表,然后存入变量 SOURCES 里。当然你不需要一定要把结果存入一个变量。
另一个有用的函数是 patsubst (patten substitude, 匹配替换的缩写) 函数。它需要3个参数:第一个是一个需要匹配的模式,第二个表示用什么来替换它,第三个是一个需要被处理的由空格分隔的字列。例如,处理那个经过上面定义后的变量,
OBJS = $(patsubst %.c,%.o,$(SOURCES))
这个语句将处理所有在 SOURCES宏中的文件名后缀是 '.c'的文件 ,用 '.o' 把 '.c' 取代。注意这里的 % 符号是通配符,匹配一个或多个字符,它每次所匹配的字符串叫做一个‘柄’(stem) 。在第二个参数里,% 被解释成用第一参数所匹配的那个柄。

感兴趣的读者如果需要更进一步的了解,请参考GNU Make 手册。


7.2.2.3 makefile的一个具体例子

在这里给读者举一个简单的makefile的例子,通过对这个makefile的讲解,来巩固前面介绍的相关知识。

INCLUDES =-I/home/nie/mysrc/include /

-I/home/nie/mysrc/extern/include /

-I/home/nie/mysrc/src /

-I/home/nie/mysrc/libsrc /

-I. /

-I..

EXT_CC_OPTS = -DEXT_MODE

CPP_REQ_DEFINES = -DMODEL=tune1 -DRT -DNUMST=2 /

-DTID01EQ=1 -DNCSTATES=0 /

-DMT=0 -DHAVESTDIO

RTM_CC_OPTS = -DUSE_RTMODEL

CFLAGS = -O -g

CFLAGS += $(CPP_REQ_DEFINES)

CFLAGS += $(EXT_CC_OPTS)

CFLAGS +=$(RTM_CC_OPTS)

SRCS = tune1.c rt_sim.c rt_nonfinite.c grt_main.c rt_logging.c /

ext_svr.c updown.c ext_svr_transport.c ext_work.c

OBJS = $(SRCS:.c=.o)

RM = rm –f

CC = gcc

LD = gcc

all: tune1

%.o : %.c

$(CC) -c -o $@ $(CFLAGS) $(INCLUDES) $<

tune1 : $(OBJS)

$(LD) -o $@ $(OBJS) -lm

clean :

$(RM) $(OBJS)

在这个makefile中首先定义了十个宏:

' INCLUDES =-I …'(省略号代表-I后面的内容),'-I dirname' 表示将dirname所指的目录加入到程序头文件目录列表中去,是在进行预处理过程中使用的参数;

' EXT_CC_OPTS = -DEXT_MODE ' 表示在程序中定义了宏EXT_MODE,等价于在源代码写入语句' #define EXT_MODE ' ;

接下来的两个宏定义CPP_REQ_DEFINES 和RTM_CC_OPTS起到和EXT_CC_OPTS类似的作用;

'CFLAGS =-O -g '是编译器的编译选项,表示在编译的过程中对代码进行基本优化,并产生能被GNU调试器(如gdb)使用的调试信息;

'CFLAGS += ' 表示对这个宏定义在原来的基础上增加新的内容;

' SRCS = …'代表了所有要编译的源代码文件列表;

' OBJS = $(SRCS:.c=.o)'表示把宏SRC所代表的所有以.c结尾的文件名用.o结尾的文件名替换,即表示各个源文件所对应的目标文件名;

' RM = rm –f ' 表示删除命令,-f是强制删除选项,使用该符号,在对文件进行删除时,没有提示;

' CC = gcc '表示编译器是用gcc;

' LD= gcc' 表示链接命令是用gcc;

all和clean是两个伪目标,在使用make命令的时候,如果不指明目标文件名,则是以在makefile 中出现的第一个目标作为最终目标,所以如果键入命令make,则伪目标all被作为最终的目标而执行,由于这个文件并不存在,所以 make 会尝试更新 all 规则,因此就检查它的依赖文件 tune1 是否需要更新,如果需要,就把它更新,这样伪目标下面的两条规则就会被执行,从而生成可执行文件tune1。如果要执行删除命令,只需要键入命令make clean,就会把所有以.o结尾的中间文件删除。

另外,请读者注意在本makefile的例子中多次用到' /',该符号用于在makefile中,如果一条语句过长时,可以用' /'放在这条语句的右边界,通过回车换行,使下面新一行的语句成为该语句的续行。

在makefile文件中,用符号' #'作为注释行语句的开始,以增强makefile文件的可读性。

本例假设makefile文件名为makefile,当然也可按照个人的喜好取其他文件名,如果文件名不是makefile,Makefile的话,在用make命令是,请使用make –f makefilename。

到此,希望读者能够掌握make 和makefile的基本使用。


7.2.3 使用GDB调试程序

无论是多么资深的程序员在编写的程序时,都不大可能一次性就会成功,在程序运行时,会出现许许多多意想不到的错误,一味地只是查看程序用处不大,最有效的方法通过一些手段进入到程序内部进行调试。通常在调试程序的时候如果能够得到以下一些信息,对于开发者找到错误所在是很有帮助的。

1. 程序是运行到哪个语句或者表达式就发生了错误?

2. 如果错误是在执行一个函数的时候出现的,那么是程序的哪一行包含了这个函数的调用语句,在调用该函数的时候传递的实参是什么?

3. 在程序执行到某处时,所关心的某一个变量值为多少?

4.某个表达式最终运行的结果为何值?

调试器(更准确地说应该称为符号调试器)能够完成上述目标。它是一个能够运行其他程序的应用程序,它和普通意义上的程序的唯一不同之处在于,调试器能够进入到程序源码中,允许开发者进行逐行单步运行,了解程序代码执行顺序,和每条语句执行的结果,可以在程序运行的同时,查看甚至是改变任一变量值。在程序运行出错时,它为程序开发者提供程序运行时的详细细节,从而找到出错的原因。在Linux系统中,最常用到的就是GDB(GNU Degugger)。GDB是GNU自带的调试工具。


7.2.3.1 GDB常用命令

要想使用gdb,必须在对源码进行编译的时候,使用-g编译选项开关,来通知编译器,开发者希望进行程序调试。用了-g选项后,程序在编译的时候就会包含调试信息,这些调试信息存在目标文件中,它描述了每个函数或变量的数据类型以及源码行号和可执行代码地址间对应关系,gdb正是通过这些信息使源码和机器码相关联的,它实现了源码级的调试。

为了使用gdb调试,只需要在命令行中输入gdb filename(filename是用gcc编译生成的最终可执行文件名),该语句启动与调试器的文本接口。就在上一小节中所举makefile例子来说,就是键入gdb tune1,则在屏幕上会出现

[nie@uClinux mysrc]$ gdb tune1

GNU gdb Red Hat Linux 7.x (5.0rh-15) (MI_OUT)

Copyright 2001 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i386-redhat-linux"...

(gdb)

gdb虽然运行起来,但是可执行程序tune1并没有运行,此时在gdb提示符下直接键入run命令即可,如果可执行程序在运行的时候需要输入命令行参数,则在gdb提示符下可以这样键入命令:run command-line-arguments ,就如同是输入命令:tune1 command-line-arguments一样,启动了可执行程序的运行。

有时候,我们希望能够断点调试程序,让程序执行到代码某处时停止继续执行下去,此时可以使用命令break,该命令的格式为break place,这里place可以是程序代码的行号,某函数名,甚至可以是用break main ,让程序断点设置在代码一开始执行的地方,比如对于上面举的可执行文件名为tune1的例子,它调用了一个函数名为rtExtModeCheckInit的子函数,如果想让程序执行到该函数处停止,可以在gdb提示符下输入:break rtExtModeCheckInit,此时屏幕上出现下列信息:Breakpoint 1 at 0x8049a28: file grt_main.c, line 604.。当然,也可以使用行号设置中断位置,上面设置中断的语句可以等价为break 604,可以在屏幕上看到相同的效果。

当设置了断点后,程序会运行到断点处停下来,此时从屏幕上可以得到类似下面的信息:

Breakpoint 1, main (argc=4, argv=0xbffffb84) at grt_main.c:604

604 rtExtModeCheckInit();

(gdb)

当想将某个断点除去,可以在gdb提示符下输入命令:delete N,这里N表示第几个中断,第一个设置的中断序号为1,第二个设置的序号为2,依次类推。如果delete后不跟任何序号,在表示把设置的所有断点都删除。如果想查看目前设置断点的情况,可以使用命令info break,屏幕会显示出每一个设置的断点信息。

在gdb提示符下使用help命令,会给出有关gdb命令的一个简短描述和命令分类。

如果开发者想进入到程序内部进行单步调试,gdb提供两种命令供选择,step和next命令,两者的区别在于step执行每一条语句,如果遇到函数调用,会跳转到到该函数定义的开始行去执行,而next则不进入到函数内部,它把函数调用语句当作普通一条语句执行完成。continue命令是继续运行程序,直到遇到下一个断点或程序结束。

有时候使用者仅仅是在linux的bash提示符下输入命令gdb后,启动了gdb而已,此时,如果要加载可执行文件,需要在gdb提示符下键入命令:file filename(filename为可执行文件名),注意是可执行文件的名字而不是源文件名。

当在调试过程中,想查看一个变量值的时候,可以在gdb环境下输入命令:watch variablename ,这里的variablename是你想观察的变量名。

还有一个可以显示表达式值的命令print,其使用规则为print expressionname,其中expressionname为要显示的表达式名。


7.2.3.2 GDB具体调试实例

下面通过使用一个简单的程序使读者进一步熟悉用gdb的调试方法。

源程序名为example1.c,代码如下:

/*******************************************************

* Institute of Automation, Chinese Academy of Sciences

* File Name: example.c

* Description: introduce how to use gdb

* Author: Xueyuan Nie

* Date:

*******************************************************/

#include <stdio.h>

static void display(int i, int *ptr);

int main(void)

{

int x = 5;

int *xptr = &x;

printf("In main():/n");

printf(" x is %d and is stored at %p./n", x, &x);

printf(" xptr holds %p and points to %d./n", xptr, *xptr);

display(x, xptr);

return 0;

}

void display(int z, int *zptr)

{

printf("In display():/n");

printf(" z is %d and is stored at %p./n", z, &z);

printf(" zptr holds %p and points to %d./n", zptr, *zptr);

}

要使用gdb调试程序,一定要在编译程序时,使用-g编译选项,以生成参数符号表( augmented symbol table),提供调试信息。

首先使用gcc –g –o example1 example.c对源代码进行编译,这样就可以使用gdb****example1的执行细节。在bash提示符下,键入命令:gdb example1,启动了对可执行文件example1 的调试,在屏幕上会出现下面的信息:

[nie@uClinux nie]$ gdb example1

GNU gdb Red Hat Linux 7.x (5.0rh-15) (MI_OUT)

Copyright 2001 Free Software Foundation, Inc.

GDB is free software, covered by the GNU General Public License, and you are

welcome to change it and/or distribute copies of it under certain conditions.

Type "show copying" to see the conditions.

There is absolutely no warranty for GDB. Type "show warranty" for details.

This GDB was configured as "i386-redhat-linux"...

(gdb)

最后一行(gdb)就是进入到gdb调试中的提示符,此时可以在提示符下输入任何想键入的命令。

现在如果要进行断点调试的话,就需要显示一下要调试的源码,以便知道在哪个地方进行断点设置。在gdb下,Linux最常用文本编辑命令vi不能使用,可以使用list命令列出可执行文件的源代码的一部分,为了列出源代码的全部,只要多键入几次list命令即可。具体操作如下:

(gdb) list

1 #include <stdio.h>

2 static void display(int i, int *ptr);

3

4 int main(void) {

5 int x = 5;

6 int *xptr = &x;

7 printf("In main():/n");

8 printf(" x is %d and is stored at %p./n", x, &x);

9 printf(" xptr holds %p and points to %d./n", xptr, *xptr);

10 display(x, xptr);

(gdb) list

11 return 0;

12 }

13

14 void display(int z, int *zptr) {

15 printf("In display():/n");

16 printf(" z is %d and is stored at %p./n", z, &z);

17 printf(" zptr holds %p and points to %d./n", zptr, *zptr);

18 }

(gdb) list

Line number 19 out of range; example1.c has 18 lines.

(gdb)

屏幕上清楚显示出了每一个语句所在的具体行号,比如现在我们想在第五行设置断点,可以在gdb提示符下输入命令:break 5,可以看到下面的显示信息:

(gdb) break 5

Breakpoint 1 at 0x8048466: file example1.c, line 5.

断点已经设置好,现在开始让程序运行起来,键入命令run,也可以键入其缩写形式r,屏幕上出现的信息如下:

(gdb) r

Starting program: /home/nie/example1

Breakpoint 1, main () at example1.c:5

5 int x = 5;

上述信息表明,gdb已经开始执行可执行程序,目前程序运行到example1.c程序中main()函数的第五行处停止,并且显示出即将要执行的第五行语句。

现在我们进行单步调试的工作,输入命令:next,它表明单步执行程序的每一条语句,当用next命令执行到函数display处时,即当屏幕出现如下所示信息时:

(gdb) next

6 int *xptr = &x;

(gdb) next

7 printf("In main():/n");

(gdb) next

In main():

8 printf(" x is %d and is stored at %p./n", x, &x);

(gdb) next

x is 5 and is stored at 0xbffffb44.

9 printf(" xptr holds %p and points to %d./n", xptr, *xptr);

(gdb) next

xptr holds 0xbffffb44 and points to 5.

10 display(x, xptr);

为了进入到函数display内部进行调试,输入命令step,即:

(gdb) step

display (z=5, zptr=0xbffffb44) at example1.c:15

15 printf("In display():/n");

step命令使执行进入到函数内部,此时在该函数内部,可以继续使用step命令或者是next命令进行单步执行,如果不想单步执行,而是直接将程序一次执行完毕,可以输入命令continue即可。

要退出gdb,请键入命令quit,如果程序此时仍在进行,gdb会让你确认是否真的要退出,屏幕会出现类似下面的提示信息:

(gdb) quit

The program is running. Exit anyway? (y or n)

按下'y' 即退出调试程序,如果程序本身已经运行完毕,则quit命令键入后,会直接退出gdb,而不出现任何提示信息。

当然除了使用gdb进行程序调试外,如果程序比较简短,逻辑又比较简单,此时完全可以不用gdb,采用printf语句在程序当中输出中间变量的值来调试程序,也是一个不错的调试方法。

到此为止,我们已经介绍了uClinux操作系统,GNU工具的使用,有了这些预备知识后,我们将进入到本章的重点内容了。


7.3 建立uClinux开发环境

为了实现基于uClinux的应用系统的开发,建立或拥有一个完备的uClinux开发环境是十分必要的。

基于uClinux操作系统的应用开发环境一般是由目标系统硬件开发板和宿主PC机所构成。目标硬件开发板(在本书中就是基于S3C4510B的开发板)用于运行操作系统和系统应用软件,而目标板所用到的操作系统的内核编译、应用程序的开发和调试则需要通过宿主PC机来完成。双方之间一般通过串口,并口或以太网接口建立连接关系。


7.3.1 建立交叉编译器

通常的嵌入式系统的开发都是以装有Linux的PC机作为宿主机来编译内核和用户应用程序的,但是对于很多长期工作在Windows操作系统下的用户来说,突然切换到Linux环境下去开发程序会感到诸多不便,因此本书对于不同的读者提供了在宿主机装有不同操作系统时,相应的交叉编译环境建立的方法。


7.3.1.1.为安装Linux的宿主机建立交叉编译器

首先,要在宿主机上安装标准Linux操作系统,如RedHat Linux(本书使用的是Redhat 7.2),一定要确保计算机的网卡驱动、网络通讯配置正常,有关如何在PC机上安装Linux操作系统的问题,请参考有关资料和手册。

由于uClinux及它的相关开发工具集大多都是来自自由软件组织的开放源代码,所以在软件开发环境建立的时候,大多数软件都可以从网络上直接下载获得,接下来就可以建立交叉开发环境。

现在介绍一下交叉编译的概念。简单地讲,交叉编译就是在一个平台上生成可以在另一个平台上执行的代码。注意这里的平台,实际上包含两个概念:体系结构(Architecture)、操作系统(Operating System)。同一个体系结构可以运行不同的操作系统;同样,同一个操作系统也可以在不同的体系结构上运行。举例来说,我们常说的x86 Linux平台实际上是Intel x86体系结构和Linux for x86操作系统的统称;而x86 WinNT平台实际上是Intel x86体系结构和Windows NT for x86操作系统的简称。就本书所涉及到的目标硬件S3C4510B而言,之所以使用交叉编译是因为在该硬件上无法安装我们所需的编译器,只好借助于宿主机,在宿主机上对即将运行在目标机上的应用程序进行编译,生成可在目标机上运行的代码格式。

读者可以从http://mac.os.nctu.edu.tw/->download处下载工具链:arm-elf-binutils-2.11-5.i386.rpm,arm-elf-gcc-2.95.3-2.i386.rpm,genromfs-0.5.1-1.i386.rpm的文件复制到宿主机上的任一目录下。键入下面的命令来安装rpm包:

$su

# rpm –ivh *.rpm

RPM(Red Hat Package Manger)软件包管理程序,是将原本复杂的软件包安装程序,轻松利用单一操作来完成。

RPM目前支持的平台有3种类型:x86(i386),Sparc以及Alpha,可以很容易的从文件名就来判断出使用的平台。像目前下载文件比如arm-elf-binutils-2.11-5.i386.rpm,arm-elf-binutils表示文件名,2.11表示版本编号,5表示发行序号,也就是目前已经发行的次数,i386是指此软件包为适用于Intel x86的二进制(binary)程序,也就是已经编译并且可以直接安装的软件包,最后的“rpm”表示这是Red Hat的RPM程序。每一版的RPM发布后,若是发现软件有问题,都会重新进行patch和build,这样在发行序号的部分就会增加1,以表示该版本是上个版本的更新。

这里在所用的命令rpm –ivh中,-i表示Installation,就是安装指定的RPM软件包;

-h表示Hash,该参数可在安装期间出现”#”符号,来显示目前的安装过程,这个符号一直持续到安装完成后才停止;

-v表示Verbose,显示安装时候的详细信息。

至此我们把交叉编译器已经安装到了宿主机。以后我们就可以用交叉编译器arm-elf-gcc编译操作系统内核和用户应用程序了。

读者也可以从网站http://www.uclinux.org/pub/uClinux/arm-elf-tools/上下载最新的arme-elf-gcc工具,即脚本文件arm-elf-tools-20030314.sh,在宿主机上安装该工具链,在该文件所在目录下,键入:

$ su

# ls –l arm-elf-tools-20030314.sh

该命令显示文件的各种属性,如果该脚本文件属性的不是可执行的,则还需要输入命令:# chmod 755 arm-elf-tools-20030314.sh

以将其属性改为可执行属性,然后通过键入命令:

#sh ./arm-elf-tools-20030314.sh

就可以执行该文件。执行后/usr/local/bin/路径下有gcc, g++, binutils, genromfs, flthdr 和elf2flt等各种实用工具。


7.3.1.2 为安装windows的宿主机建立交叉编译器

这部分内容是专门针对那些对Linux环境和Linux中的应用程序不熟悉,宁愿用PC上基于Windows的操作系统来开发嵌入式系统的读者而写的。

1. Cygwin软件介绍

为了在Windows下开发嵌入式操作系统应用程序,可以在Windows环境下装上Cygwin软件。Cygwin是一个在Windows平台上运行的Unix模拟环境,是Cygnus Solutions公司开发的自由软件。它对于学习掌握Unix/Linux操作环境,或者进行某些特殊的开发工作,尤其是使用GNU工具集在Windows上进行嵌入式系统开发,非常有用。
Cygnus当初首先把gcc,gdb等开发工具进行了改进,使它们能够生成并解释win32的目标文件。然后,把这些工具移植到windows平台上去。一种方案是基于win32 API对这些工具的源代码进行大幅修改,这样做显然需要大量工作。因此,Cygnus采取了一种不同的方法——他们写了一个共享库(就是cygwin1.dll),把win32 API中没有的Unix风格的调用(如fork,spawn,signals,select,sockets等)封装在里面,也就是说,他们基于win32 API写了一个Unix系统库的模拟层。这样,只要把这些工具的源代码和这个共享库连接到一起,就可以使用Unix主机上的交叉编译器来生成可以在Windows平台上运行的工具集。以这些移植到Windows平台上的开发工具为基础,Cygnus又逐步把其他的工具(几乎不需要对源代码进行修改,只需要修改他们的配置脚本)软件移植到Windows上来。这样,在Windows平台上运行bash和开发工具、用户工具,感觉好像在Unix上工作。 关于Cygwin实现的更详细描述,请参考http://cygwin.com/cygwin-ug-net/cygwin-ug-net.html。

2. Cygwin软件的安装

要得到Cygwin的最新安装版本,请到Cygwin的主页http://cygwin.com/上下载最新的Cygwin,在该页面的右上角有'' Install Cygwin Now '',点击此处,就会先下载一个叫做setup.exe的GUI安装程序,用它能下载一个完整的Cygwin。图7.2所示为在点击setup.exe后出现”选择安装类型”对话框。建议读者把Cygwin整个安装包先下载到本地,再进行本地安装比较方便,即在下图先选择第二个选项,等到将Cygwin完全下载后,再选择第三个选项进行本地安装。

图7.2 选择安装类型

 

安装的时候建议最好不要安装到C:/目录下,比如安装在D:/下。

在安装的过程中,会让用户选择安装哪些包,这些包主要是确定开发环境,编译工具等,如果不能确定具体需要哪些包的话,而硬盘空间足够的情况下,就选择全部安装。在出现的对话框的''All''的右边点击''Default'',直到变成''Install'',如下图7.3所示:

图7.3 选择安装包

Cygwin的安装过程时间比较长,请读者耐心等待。当出现创建图标的画面点击“完成”按钮之后,屏幕会有几秒钟的闪动,出现类似下面的画面如图7.4所示,这是在执行Cygwin安装后的脚本配置。

 

图7.4 Cygwin安装后自动配置

自动配置结束后,出现Cygwin成功安装结束的提示框。桌面上会出现Cygwin的图标。

3. 在Cygwin下生成交叉编译器

在自己生成交叉编译器之前,首先对cygwin进行一些设置。假设Cygwin安装在d目录下,在打开Cygwin窗口之前,进入到D:/cygwin目录,在这个目录下,有一个文件名为cygwin.bat的批处理文件,编辑该文件,在第一行后加入set CYGWIN=title ntea,这是因为cygwin的启动批处理文件需要启动Unix文件系统模拟。修改完毕后,保存后退出。双击桌面上的Cygwin图标,打开后默认用户为在Windows中登录的用户名(这里所使用的操作系统是windows 2000 professsional),在如图7.5所示的界面中,在根目录(即D:/cygwin)下键入:

cd bin

mv sh.exe sh-original.exe

ln –s bash.exe sh.exe

做上述几步的原因是因为大多数linux系统将sh符号链接到bash,Cygwin上的sh.exe和bash.exe是不同的,因此必须用bash 代替sh。

从网站http://www.uclinux.org/pub/uClinux/arm-elf-tools/tools-20030314/上下载生成工具链的各种源码,根据脚本文件build-uclinux-tools.sh 建立可在windows下编译用户应用程序的交叉编译器,生成的交叉编译器最终被打包为arm-elf-tools-cygwin-yyyymmdd.tar.gz的文件,其中yyyy为生成交叉编译器的年,mm为生成交叉编译器的月份,dd 为日期。

这里,希望读者注意的是在生成交叉编译器的过程中,可能会遇到多次错误,读者应该根据给出的出错信息,进行相应文件的修改。由于习惯上的原因,linux下的压缩文件一般都是以.tar.gz或者.tgz结尾的,虽然用windows下的解压软件比如winzip或者winrar可以解压这些文件,但是推荐读者不要用这些软件在windows下解压,因为这样可能会造成某些信息的丢失。

本书生成的交叉编译器名为arm-elf-tools-cygwin-20030502.tar.gz。

图7.5 Cygwin开发环境

4. 在Cygwin环境下建立交叉编译器

在根目录下键入:

tar xvzf arm-elf-tools-cygwin-20030502.tar.gz

进行交叉编译器的解压,解压完毕后在/usr/local/bin/目录下可以看到各种GNU工具。有了交叉编译器后,熟悉Windows的读者就可以在Windows下编译在uClinux上运行的应用程序了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值