860 likes | 1.15k Views
第 6 章 嵌入式 Linux 开发. 主讲 : 李岩. 现在 Linux 广泛用于各类计算应用,不仅包括 IBM 的微型 Linux 腕表、手持设备( PDA 和蜂窝电话)、因特网装置、瘦客户机、防火墙、工业机器人和电话基础设施设备,甚至还包括了基于集群的超级计算机。. 本章主要介绍了: 怎样应用 Linux 来进行嵌入式系统的开发。 选用 Linux 操作系统的优势。 Linux 的简化版本 uClinux 相关知识。如内存管理、进程管理、启动过程等。 开发工具 Hittool 的具体应用。. 6.1 嵌入式 Linux 概况.
E N D
第6章 嵌入式Linux开发 主讲:李岩
现在 Linux广泛用于各类计算应用,不仅包括IBM的微型 Linux腕表、手持设备(PDA和蜂窝电话)、因特网装置、瘦客户机、防火墙、工业机器人和电话基础设施设备,甚至还包括了基于集群的超级计算机。
本章主要介绍了: • 怎样应用Linux来进行嵌入式系统的开发。 • 选用Linux操作系统的优势。 • Linux的简化版本uClinux相关知识。如内存管理、进程管理、启动过程等。 • 开发工具Hittool的具体应用。
6.1 嵌入式Linux概况 • Linux操作系统开放源代码,可以裁剪内核,并已在x86、Alpha、Sparc、MIPS、PPC、Motorola、NEC和ARM等硬件平台上稳定、高效地运行。 • Linux操作系统作为一种多任务、稳定可靠、内核可裁剪的系统,是开发嵌入式软硬件产品的优秀软件平台。 • 嵌入式Linux是一种开放源码、软实时、多任务的嵌入式操作系统。
6.1.1 嵌入式Linux • 在嵌入式开发中,随着微处理器的产生,价格低廉、结构小巧的CPU和外设接口提供了稳定可靠的硬件架构。 • 高端嵌入式系统要求许多高级的功能,如图形用户界面和网络支持。许多高端RTOS供应商已经提供了这些功能,但其价格也很高,一般人难以接受。 • Linux为嵌入式操作系统提供了一个极有吸引力的选择,它是一个类UNIX的、以内核为基础的、有完备内存保护机制的、多任务多进程的操作系统。 • 由于Linux具有开放性,所以许多人认为Linux非常适合多数Intemet设备。他们认为Linux可以支持不同的设备,支持不同的配置。
6.1.1 嵌入式Linux 1. 可运行于多种硬件平台 • Linux符合IEEE POSIX.1标准,使应用程序具有较好的可移植性。 • Linux内核已经被移植到多种硬件平台上,这对受开销、时间限制的研究与开发项目是很有吸引力的。 2. 可裁剪,性能优异,应用软件丰富 • Linux的动态模块加载使Linux的裁剪极为方便,高度模块化的部件使添加非常容易。 • Linux是一个全面的多任务和真正的32位操作系统。系统运行稳定,功能强大,支持多种硬件平台,应用工具多。 3. 使用成本低 Linux是免费软件,只要遵守GPL(GNU General Public License)的规定,就可以免费获得拷贝,并进行开发和商业发行。
6.1.1 嵌入式Linux 4. 强大的网络功能 • Linux操作系统最突出的是网络部分,基本上所有的网络协议和网络接口都可以在Linux上找到。 • Linux内核对网络协议栈的设计是从简捷实用的角度出发的,它有一整套的网络协议模块。 • Linux的网络功能十分强大,更重要的是, Linux的网络功能和协议是以内核可选的模块方式提供的,它允许用户自由地裁剪和优化。 5. GUI开发支持 • Linux本身有性能优秀的X Window系统,在X Window系统的支持下,能方便地进行图形用户界面的开发。
6.1.1 嵌入式Linux • X Window是一个在大多数UNIX工作站上使用的图形用户界面。 • X Window系统应用于嵌入式系统时,要考虑嵌入式系统的特殊条件。 6. 丰富的开发技术资源 • Linux在这几年中不断成熟,越来越多的人加入了Linux的行列。 • 这意味着对新硬件的Linux驱动程序甚至比用于其他UNIX系统(如Solaris的驱动程序)还来得及时。 • Linux庞大的志愿者网络在生产“补丁”程序方面反应很快。
6.1.2 嵌入式Linux的组成 最基本的嵌入式Linux系统需要3个基本元素: • 系统引导程序,用于完成机器加电后的系统定位引 • Linux系统内核,为嵌入式应用提供一个软件环境,为应用程序完成基本的底层的资源管理工作; • 初始化过程,完成基本的初始化。 • 为使这个最小嵌入式系统具有一定的实用性,还需加上硬件的驱动程序及—个或几个应用进程以提供必要的应用功能支持。 • 如果应用比较复杂,可能还需要添加一个可以在ROM或RAM中使用的文件系统、TCP/IP网络协议栈等。 • 在PDA领域,还需要加上—个GUl支持。
6.1.3 嵌入式Linux的版本 • 将Linux移植以满足实时要求的实时操作系统,应用于一些关键的控制场合,如Fsmlabs公司的RT Linux,Monta Vista的Hard Hat Linux。 • 尽可能保留Linux的强大功能,尽可能地减少其体积,以满足许多嵌入式系统对体积的要求,如uClinux。 • 针对特定嵌入式领域采用整合方案,如Lineo,TimeSys,合肥华恒等。接下来,简要介绍常用的两个嵌入式Linux操作系统。
6.1.3 嵌入式Linux的版本 • 实时Linux(RT Linux) • RT Linux是美国新墨西哥州大学计算机系研制开发的。 • RT Linux实现的内核位于通常的Linux内核之下,这个内核是一个实时内核,它只需要完成底层的任务创建、中断服务程序,并为底层任务、ISR和Linux进程之间进行通信排队。 • RT Linux的调度方法和用户实时任务的工作是通过Linux的可导入模块的方式进行的。 2. uClinux • uClinux是一个GNU的项目,代码完全开放。 • uClinux的英文解释是Micro Control Linux,可理解为“微控制领域中的Linux系统”。 • 它专门应用于没有MMU的CPU,并专为嵌入式系统做了许多小型化的工作,已支持的微处理器包括Motorola MC68000,MCF5206和MCF5207ColdFire等。
6.2 uCLinux开发介绍 • 目前,uClinux往往基于两个Linux内核版本,2.0.38 是一个比较成熟的版本,2.4.x是最新的版本 • Hitool 套件同时提供了对他们的支持。 • 一般uClinux的内核大小在500k左右,如果加上一些基本的应用,也就在900k左右.非常适合于嵌入式系统。 • uClinux架构如图6-1所示(下页),一些重要的模块在下面描述。
6.2.1 启动过程(Bootstrap) • 启动模块Bootstrap负责用来起动Linux内核,包括Chip Selector初始化,系统堆栈的初始化,把压缩的Linux映像从Flash中解压到RAM中,并把控制权交给内核的初始化例程。 • 这部分工作是与你的硬件高度相关的,所以这部分的代码要尽量精简。
6.2.2 内核初始化(Kernel Initialization) • 内核初始化的入口地址是: start_kernel(在init/main.c中)。 • 它初始化内核的其它部分,包括异常(trap)、中断(IRQ)、内存页(Page)、调度(Scheduling)、驱动程序等等。并启动“init”进程进入多任务环境。
6.2.3 系统调用处理/异常处理 • 当“init”程序运行后,内核对整个系统的运行不再进行直接控制,而是通过系统调用给应用程序提供服务和响应外部及内部的异步事件,例如:程序错误,硬件中断等。 • 在ARM中,系统调用采用swi指令所产生的软件异常来实现, 如例所示:swi 0x900004(其中0x900004表示这个系统调用为sys_write)。 00002550 <write>: 2550: ef900004 swi 0x00900004 2554: e3700a01 cmn r0, #4096 ; 0x1000 2558: 2afff6b0 bcs 20 <\<>__syscall_error<\>> 255c: e1a0f00e mov pc, lr
6.2.4 驱动程序(Device Driver) • 驱动程序是整个Linux内核的主要组成部分,它们控制着操作系统和外部设备的交互。 • Linux的驱动程序是可选的,但是典型的系统应该包括一个控制台(Console),一个通用串口驱动程序,一个块设备 (用来存放根文件系统) 驱动程序。 • 当Linux内核起动的时候,需要一个输出调试信息的设备。这个设备往往通过串口来实现。 • 这个调试终端可以通过register-console这个函数来创建。而所有的调试信息都通过Printk例程通过这个调试终端来输出。
6.2.5 文件系统(File System) • 支持多种文件系统是Linux一个重要的特性,uClinux同样把这一特性带进了嵌入式系统中,并针对嵌入式系统作了一些取舍。 • 在Hitool for uClinux包括了romfs, Ext2 FS, RAM FS, NFS 。 • 其中,romfs 是最简单的只读文件系统,所占用的空间最少,我们用它来做根文件系统(root file system)。 • 根文件系统里存放Linux启动时要用到的设备文件,配置文件和应用程序。 • 例如:/dev/tty0, /etc/rc , /bin/init ,/bin/sh,等等。
6.2.6 内存管理 • uClinux不能使用处理器的虚拟内存管理技术但仍然采用存储器的分页管理,系统在启动时把实际存储器进行分页。在加载应用程序时程序分页加载。但是由于没有MMU管理,所以实际上uClinux采用实存储器管理策略(物理内存)。 • uClinux系统对于内存的访问是直接的,(它对地址的访问不需要经过MMU,而是直接送到地址线上输出),所有程序中访问的地址都是实际的物理地址。 • 由于应用程序加载时必须分配连续的地址空间,所以开发人员在开发应用程序时必须考虑内存的分配情况并关注应用程序需要运行空间的大小。 • 从内存的访问角度来看,开发人员的权利增大了(开发人员在编程时可以访问任意的地址空间),但与此同时系统的安全性也大为下降。
6.2.7 进程管理 • uClinux和Linux之间最大的区别在于平面存储器模型。没有虚拟存储器可以提供非常有效的fork系统调用。 • 由于在使用fork时,内核会将父进程拷贝一份给子进程,但是这样的做法相当浪费时间,因为大多数的情形都是程序在调用fork后就立即调用exec,这样刚拷贝来的进程区域又立即被新的数据覆盖掉。 • 因此Linux系统提供一个系统调用vfork,vfork假定系统在调用完成vfork后会马上执行exec,因此vfork不拷贝父进程的页面,只是初始化私有的数据结构与准备足够的分页表。 • 这样实际在vfork调用完成后父子进程事实上共享同一块存储器(在子进程调用exec或是exit之前),因此子进程可以更改父进程的数据及堆栈信息, • 因此vfork系统调用完成后,父进程进入睡眠,直到子进程执行exec。 并且在这段时间内,子进程不能更改数据段和堆栈的内容。
6.2.7 进程管理 • 当子进程执行exec时,由于exec要使用被执行程序的数据,代码覆盖子进程的存储区域, • 这样将产生写保护错误(do_wp_page)(这个时候子进程写的实际上是父进程的存储区域)。这个错误导致内核为子进程重新分配存储空间。 • 当子进程正确开始执行后,将唤醒父进程,使得父进程继续往后执行。 • 所以,在uClinux中,想让父子进程同时运行同一个程序是不可以的。
6.2.8 运行时间库及应用程序 运行时间库提供了用户程序和内核程序的接口。 1. 程序的入口代码 • 用户自己写程序一般从main开始,其实程序在运行时真正的进入点是C运行库的一部分。这部分代码执行初始静态数据,例如错误号(errno)以及环境指示符(environ),并且将参数argc 和argv传输到main。 • 参数和环境一般根据宏程序start_thread(定义在include/proc/processer.h) 的调用约定,放在寄存器或者堆栈上。execve 系统调用利用start_thread建立一个虚拟环境,程序的入口代码因此必须实现把start_thread所建立的环境恢复出来,并调用__uClibc_main。在__uClibc_main中再调用用户程序的main。
.text .global _start .global __uClibc_main .type _start,%function .type __uClibc_main,%function .text _start: /* 清帧缓冲区指针 */ mov fp, #0 /* argc, argv 和 envp 出栈 */ ldr r0,[sp, #0] ldr r1,[sp, #4] ldr r2,[sp, #8] /* 运行 uClibc的 main() 函数—不应返回 */ bl__uClibc_main
6.2.8 运行时间库及应用程序 2. 堆(heap)分配 • 用户程序通过调用malloc从堆中分配内存。在有虚拟存储器的系统中,堆可以使用sbrk调用来扩大。 • 因为虚拟存储器支持分段存储,所以可以在堆的最高端到堆栈的最底部之间留有很大的虚拟空间来做扩展之用。 • 但在平面存储器系统中,没有这个空间。所以,堆空间改用mmap调用来分配。在uClibc中,内存分配的算法非常简单直接,存储器的管理完全依靠内核的页机制来完成。见图6-2。
6.2.8 运行时间库及应用程序 • 从图6-2中,可以看出另一项区别,因为物理上堆栈与静态数据相连,所以,堆栈必须有足够的空间,以防止覆盖静态数据和代码。 • 在标准Linux程序中的虚拟内存允许堆和堆栈之间留有足够大的空间来让他们扩展。 系统可以监控页的使用情况,防止堆栈溢出。 • 但在uClinux中要预留这样大的物理空间是不可能的。 • 另外,因为在uClinux中没有内存保护机制来防止堆栈溢出,在程序链接时要特别小心设定堆栈的大小。
6.2.8 运行时间库及应用程序 4. 执行文件格式(Flat binary format) • uClinux系统使用flat可执行文件格式,可以使用elf2flt工具把ELF格式的可执行文件转化为flat文件。 • 当用户执行一个应用时,内核的执行文件加载器(fs/binfmt_flat.c)将对flat文件进行进一步处理,主要是对代码段和数据段进行修正。 • 为什么要修正呢?因为应用程序在连接时并不知道它要在系统的那一段地址上面运行,这样就要求程序在任何地址上都要能运行,也就是说与地址无关(PIC–position independent code)。 • 这个特性由针对uClinux的编译器和连接器来提供。在ARM中有一条相对跳转指令: ebxxxxxx bl xxxxx; 可以用来实现地址无关的跳转。
6.3 uCLinux启动过程 uClinux系统的启动可以分为两个步骤: 1. 运行bootloader初始化程序 • SRAM 、SDRAM等存储设备属于挥发性的存储器,掉电以后其中的内容就会全部丢失,所以必须把操作系统的内核镜像存放在Flash等不挥发性存储介质上。 • 从本质上来讲,bootloader不属于操作系统内核。它采用汇编语言编写,因此针对不同的CPU体系结构,这一部分代码不具有可移植性。 • 在移植操作系统时,这部分代码必须加以改写。
6.3 uCLinux启动过程 具体来讲,bootloader在系统启动时主要完 成以下 几项工作: • 将操作系统内核从Flash拷贝到SDRAM中,如果是压缩格式的内核,还要将之解压缩。 • 改写系统的memory map,原先flash起始地址映射为0地址,这时需要将RAM的起始地址映射为0。 • 设置堆栈指针并将bss段清零。将来执行C语言程序和调用子函数时要用到。 • 改变pc值,使得CPU开始执行真正的操作系统内核。
6.3 uCLinux启动过程 2. 运行操作系统内核 • bootloader程序执行完上述的各项工作后,通过一条跳转指令,转而执行init目录下C语言源文件main.c中的函数start_kernel( )。 • 因为在此之前bootloader已经创建好一个初始化环境,C函数可以开始执行了。 • 整个操作系统内核的初始化工作从这里才算是真正开始。这个函数的长度比较短,代码如下:
void __init start_kernel(void) { char * command_line; unsigned long mempages; extern char saved_command_line[]; /* * Interrupts are still disabled. Do necessary setups, then * enable them */ lock_kernel(); printk(linux_banner); setup_arch(&command_line); printk("Kernel command line: %s\n", saved_command_line); parse_options(command_line); trap_init(); init_IRQ(); sched_init(); softirq_init(); time_init();
/* * HACK ALERT! This is early. We're enabling the console before * we've done PCI setups etc, and console_init() must be aware of * this. But we do want output early, in case something goes wrong. */ console_init(); #ifdef CONFIG_MODULES init_modules(); #endif if (prof_shift) { unsigned int size; /* only text is profiled */ prof_len = (unsigned long) &_etext - (unsigned long) &_stext; prof_len >>= prof_shift; size = prof_len * sizeof(unsigned int) + PAGE_SIZE-1; prof_buffer = (unsigned int *) alloc_bootmem(size); }
kmem_cache_init(); sti(); calibrate_delay(); #ifdef CONFIG_BLK_DEV_INITRD if (initrd_start && !initrd_below_start_ok && initrd_start < min_low_pfn << PAGE_SHIFT) { printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - " "disabling it.\n",initrd_start,min_low_pfn << PAGE_SHIFT); initrd_start = 0; } #endif mem_init(); kmem_cache_sizes_init(); pgtable_cache_init(); mempages = num_physpages; fork_init(mempages); proc_caches_init();
vfs_caches_init(mempages); buffer_init(mempages); page_cache_init(mempages); #if defined(CONFIG_ARCH_S390) ccwcache_init(); #endif signals_init(); #ifdef CONFIG_PROC_FS proc_root_init(); #endif #if defined(CONFIG_SYSVIPC) ipc_init(); #endif check_bugs(); printk("POSIX conformance testing by UNIFIX\n");
/* * We count on the initial thread going ok Like idlers init is an unlocked kernel thread,which will * make syscalls (and thus be locked). */ smp_init(); rest_init(); }
内核启动之后需要执行的第一个函start_kernel()(在linux/init/main.c文件中)。start_kernel()内核启动之后需要执行的第一个函start_kernel()(在linux/init/main.c文件中)。start_kernel() 完成下面一系列初始化的工作。 • printk(1inux_banner),显示Linux内核的版本信息。 • setup_arch(&command_line),做与体系结构相关的初始化工作。 • parse_options(command_line),解释系统参数。 • trap_init(),设置系统异常的入口点。 • init_IRQ(),初始化系统中断服务。 • sched_init(),系统调度器的初始化。 • time_init(),时钟、定时器初始化。 • softirq_init(),系统软中断的初始化。 • console_init(),控制台初始化。 • kmem_cache_init(),内核cache的初始化。
calibrate_delay(),校准时钟。 • mem_init(),内存初始化。 • kmem_cache_sizes_init(),创建及设置通用cache。 • fork_init(mempages),建立uidcache,并且根据系统内存大小来确定最大进程数目。 • buffer_init(mempages),块设备缓冲区的初始化。初始化一系列的cache。 • check_bugs(),检查体系结构漏洞。 • kernel_thread(init NULL,CLONE_FS | CLONE_FILES | CLONE_SIGNAL),创建第一个核心进程,启动init进程。 • cpu_idle(),运行idle进程。
接下去做的工作由init()函数来完成。 init()首先要锁定内核,然后调用do_basic_setup( )来完成外部设备以及驱动程序的初始化。外设的初始化要根据内核的配置来决定,一般需要做下面的初始化工作: ◆PCI总线初始化。 ◆网络初始化。 ◆一系列其他设备的初始化。 ◆start_context_thread()创建事件管理核心进程keventd。 ◆通过do_initcalls()函数来启动任何使用__initcall标识的函数。 ◆文件系统初始化。 ◆加载文件系统。
6.4 开发工具介绍 • 针对没有MMU的CPU架构,uClinux采用了一种平板式(Flat)的内存模型来去除对MMU的依赖, 并且改变了用户程序的加载方式,开发了运用于uClinux的C函数库--uCLibc. • 由于这些变化,一般的Linux开发工具(例如GDB)在开发uClinux时会碰到一些困难,包括内核的移植,驱动程序及应用程序的调试。 • 针对这样状况,Hitool System公司开发了Hitool foruClinux开发套件,来帮助用户开发基于uClinux的系统。
6.4 开发工具介绍 Hitool for uClinux与其它的Linux开发工具相比,有几个优点: • 整个开发过程只在Windows环境下完 成,包括内核的配置、编译,应用程 序的编译,文件系统的生成,内核的 调试,用户程序的调试。 • 可以采用多种调试方式,既可以采用JTAG方式来调试,也可通过网口用Hitool自己的监控程序(MDB)来调试。 • 通过JTAG方式调试,可以调试内核和驱动程序, 也同时可以调试应用程序。 一般的开发工具做不到这一点。 • 对于那些只要调试应用程序的客户, 就可以采用MDB的方式。采用这种方式,你不但不需要仿真器,而且在调试一个应用时,不会影响别的应用的运行。 • 提供了一个内核的追踪工具(Trace)来帮助用户分析整个系统。
6.4.1 安装 安装 Hitool for ARM supporting uClinux的过程主要有三个步骤 : 1. 安装 Cygwin: 你需要安装的cygwin套件如下 : BASE : 全部 Devel : 全部 Interpreters: expect, gawk, m4, perl Libs: tcltk Shells: bash Text: less Utils: bzip2
6.4.1 安装 2. 安装 Hitool Debugger : (1)Hitool安装时会检查Cygwin是否己安装, 若不使用 Win32 环境 Build linux image则可略过 Cygwin安装。 (2)因为 Hitool可支持众多 Target , 因此建议 在安装时选择不同的 uclinux安装目录, 例如将 Atmel Target开发环境安装于 \uclinux.atmel 而把 Samsung Target的uclinux kernel安装于 \uclinux.Samsung。 (3) Hitool会安装于指定安装目录之下的Hitool目录。(如 \uclinux.Samsung\hitool)
6.4.1 安装 3. 安装 armtools 及 uClinux 套件 : Hitool 安装完成后,会将 uClinux套件复制到 uclinux安装目录下。 (1)执行Armtools解压缩与安装 : $ sh unpack_armtools.sh (2)执行uClinux套件解压缩与安装 : $ sh unpack_uClinux.sh
6.4.2 开发环境的建立 • 建立uClinux的内核开发环境,你需要一个JTAG仿真器(PowerProbe或JEDI),与开发板和宿主机相连。 • 再使宿主机的串口与开发板的串口相连,并在宿主机上启动HyperTerm程序,设置波特率为9600。 如图6-3所示:
6.4.2 开发环境的建立 • 其中HyperTerm用来作为uClinux的标准输入输出。网口可以作为MDB的调试界面,也可以通过它来使用网络文件系统(NFS)。 • 当所有连接都正确后,启动Configuration of Hitool(只有在第一次使用时才需要)。 • 根据JTAG仿真器类型,选择正确的JTAG协议。具体配置可参照Hitool for ARM用户手册或者在线帮助。
6.4.3 调试方法 调试主要有三种方法:JTAG调试方法;MDB调试方法;FTP调试方法 1 JTAG调试方法 • 首先在uClinux的控制台(PC上的HyperTerm)上执行这个程序。 • 当这个用户程序被内核加载时,会有一个对话框跳出问你是否想要调试此程序。选择No按钮表示继续此程序,不进行调试。 • 如果你不希望下次运行此程序时对话框再次跳出,只需选择“Don’t show this message again when this program launches”选项。 • 如果点击Yes按钮, 一个‘Add Symbol Table’对话框将跳出,请你选择下载合适的符号文件(*.elf)。
6.4.3 调试方法 在对话框最下面的三个编辑框显示的是这个程序被加载后在RAM中各个段的位置,请不要改动它们,如图6-4所示。 图6-4 uClinux应用程序调试对话框
6.4.3 调试方法 • 在符号文件下载以后,这个程序将被执行到main函数后停止。 • 这是你可以像普通嵌入式程序一样调试它。 • 而且你仍然可以在内核上设置断点,当程序用到系统调用或设备驱动程序时,你可以设断点直接执行到断点。
6.4.3 调试方法 2. FTP调试方法 关于JFFS/JFFS2文件系统的建立,请读者参考相关技术资料。在此仅描述如何将用户程序通过局域网,从FTP服务器(运行Linux宿主机)上,传输到运行uClinux的目标系统(FTP客户机)并执行的过程: (1)将目标系统与Linux宿主机连接在同一网段中,在宿主机的任意目录下编写应用程序,并用交叉编译工具生成flat格式的文件。 (2)启动目标系统的uClinux,通过超级终端,输入下面的命令: ifconfig eth0 192.168.100.50 ifconfig命令用于配置目标系统的IP地址其中,eth0代表目标系统的网络设备,IP地址192.168.100.50为目标系统的IP地址,此时宿主机的IP地址为:192.1681.100.21。 执行命令: ifconfig –all 可以看到目标系统的IP地址已被正确配置