综合资讯 技术文章 原文阅读 在线商城 下载专区 DATASHEET 技术论坛 商务频道

电子技术 | 技术资料 | 嵌入式系统 | 单片机专题 | DSP专题
EDA/PLD专题 | 电源技术专题 | 电子制作专题 | 其他综合 | 芯片选型

所在的位置:首页在线阅读嵌入式系统ARM应用系统开发详解正文
 
第7章 嵌入式uClinux及其应用开发(1)

 

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

本章主要内容有:

嵌入式uClinux系统概况

开发工具GNU的使用

建立uClinux开发环境

uClinux下开发应用程序

7.1  嵌入式uClinux系统概况

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

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

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

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

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

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/DragonballARM系列(例如Atmel, TI, Samsung等生产的芯片)Intel i960, Sparc (例如无MMU LEON), NEC v850,甚至是开放的可综合(CLPD)CPU核,比如OPENcore

4、与标准Linux的兼容性

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

5、  网络的支持

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

6、应用领域

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

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

7.2  开发工具GNU的使用

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

GNU软件包括C编译器GCCC++编译器G++,汇编器AS链接器LD,二进制转换工具(OBJCOPYOBJDUMP),调试工具(GDBGDBSERVERKGDB) 和基于不同硬件平台的开发库。在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编译器

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

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

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

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

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

 * Institute of Automation, Chinese Academy of Sciences

 * File Name    hello.c

 * Descriptionintroduce how to compile a source file with gcc  

 * Author       Xueyuan Nie

 * Date    

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

 

void main()
    {
       printf("Hello the world\n")

     }
   要编译这个程序,我们只要在Linuxbash提示符下输入命令:

$ 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 工具在当前工作目录中按照GNUmakefilemakefileMakefile的顺序搜索 makefile文件。
  -i   忽略命令执行返回的出错信息。
  -s   沉默模式,在执行之前不输出相应的命令行信息。
  -r   禁止使用隐含规则。

-n   非执行模式,输出所有执行命令,但并不执行。
  -t   更新目标文件。
  -q   make操作将根据目标文件是否已经更新返回"0"或非"0"的状态信息。
  -p   输出所有宏定义和目标文件描述。
  -d   Debug模式,输出有关文件和检测时间的详细信息。
  Linuxmake标志位的常用选项与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 的主要工作是读一个文本文件 makefilemakefile 是用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 

    这是一个最简单的 makefilemake从第一条规则开始,executablemakefile最终要生成的目标文件。给出的规则说明executable依赖于两个目标文件main.oio.o,只要executable 比它依赖的文件中的任何一个旧的话,下一行的命令就会被执行。但是,在检查文件 main.o io.o 的日期之前,它会往下查找那些把 main.o io.o 做为目标文件的规则。make先找到了关于 main.o 的规则,该目标文件的依赖文件是main.cmakefile后面的文件中再也找不到生成这个依赖文件的规则了。此时,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

1Makefile 宏定义

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.oio.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