宋宝华谈 ARM 的嵌入式 Linux 移植体验之三:操作系统

时间:2021-06-23 16:34:14


        在笔者撰写的《C 语言嵌入式系统编程修炼之道》一文中,主要陈诉的软件架构是单任务无操作系统平台的,而本文的侧重点则在于讲述操作系统嵌入的软件架构,二者的区别如下图

宋宝华谈 ARM 的嵌入式 Linux 移植体验之三:操作系统
        嵌入式操作系统并不总是必须的,因为程序完全可以在裸板上运行。尽管如此,但对于复杂的系统,为使其具有任务管理、定时器管理、存储器管理、资源管理、事件管理、系统管理、消息管理、队列管理和中断处理的能力,提供多任务处理,更好的分配系统资源的功能,很有必要针对特定的硬件平台和实际应用移植操作系统。鉴于 Linux 的源代码开放性,它成为嵌入式操作系统领域的很好选择。国内外许多知名大学、公司、研究机构都加入了嵌入式Linux的研究行列,推出了一些著名的版本:
        ·RT-Linux 提供了一个精巧的实时内核,把标准的 Linux 核心作为实时核心的一个进程同用户的实时进程一起调度。RT-Linux 已成功地应用于航天飞机的空间数据采集、科学仪器测控和电影特技图像处理等广泛的应用领域。如 NASA(美国国家宇航局)将装有 RT-Linux 的设备放在飞机上,以测量 Georage 咫风的风速;
        ·uCLinux(Micro-Control-Linux,u 表示 Micro,C 表示 Control)去掉了MMU(内存管理)功能,应用于没有虚拟内存管理的微处理器/微控制器,它已经被成功地移植到了很多平台上。
        本章涉及的 mizi-linux 由韩国 mizi 公司根据 Linux 2.4 内核移植而来,支持 S3C2410A 处理器。
        1.Linux 内核要点
        和其他操作系统一样,Linux 包含进程调度与进程间通信(IPC)、内存管理(MMU)、虚拟文件系统(VFS)、网络接口等,下图给出了 Linux 的组成及其关系:

宋宝华谈 ARM 的嵌入式 Linux 移植体验之三:操作系统
        Linux 内核源代码包括多个目录:
        (1)arch:包括硬件特定的内核代码,如 arm、mips、i386 等;
        (2)drivers:包含硬件驱动代码,如 char、cdrom、scsi、mtd 等;
        (3)include:通用头文件及针对不同平台特定的头文件,如 asm-i386、asm-arm 等;
        (4)init:内核初始化代码;
        (5)ipc:进程间通信代码;
        (6)kernel:内核核心代码;
        (7)mm:内存管理代码;
        (8)net:与网络协议栈相关的代码,如 ipv4、ipv6、ethernet 等;
        (9)fs:文件系统相关代码,如 nfs、vfat 等;
        (10)lib:库文件,与平台无关的 strlen、strcpy 等,如在 string.c 中包含:

  1. char * strcpy(char * dest,const char *src)  
  2. {  
  3. char *tmp = dest;   
  4. while ((*dest++ = *src++) != '/0')  
  5. /* nothing */;  
  6. return tmp;  
  7. }  
 
        (11)Documentation:文档
        在 Linux 内核的实现中,有一些数据结构使用非常频繁,对研读内核的人来说至为关键,它们是: 
         1.task_struct
        Linux 内核利用 task_struct 数据结构代表一个进程,用 task_struct 指针形成一个 task 数组。当建立新进程的时候,Linux 为新的进程分配一个 task_struct 结构,然后将指针保存在 task 数组中。调度程序维护 current 指针,它指向当前正在运行的进程。
         2.mm_struct
        每个进程的虚拟内存由 mm_struct 结构代表。该结构中包含了一组指向 vm-area_struct 结构的指针,vm-area_struct 结构描述了虚拟内存的一个区域。
         3.inode
        Linux 虚拟文件系统中的文件、目录等均由对应的索引节点(inode)代表。
         2.Linux 移植项目
        mizi-linux 已经根据 Linux 2.4 内核针对 S3C2410A 这一芯片进行了有针对性的移植工作,包括:
        (1)修改根目录下的 Makefile 文件
        a.指定目标平台为 ARM:

  1. #ARCH := $(shell uname -m | sed -e s/i.86/i386/ -e s/sun4u/sparc64/ -e s/arm.*/arm/ -e s/sa110/arm/)  
  2. ARCH := arm  
 
        b.指定交叉编译器:

  1. CROSS_COMPILE = arm-linux-  
 
        (2)修改 arch 目录中的文件
        根据本章第一节可知,Linux 的 arch 目录存放硬件相关的内核代码,因此,在 Linux 内核中增加对 S3C2410 的支持,最主要就是要修改 arch 目录中的文件。
        a.在 arch/arm/Makefile 文件中加入:

  1. ifeq ($(CONFIG_ARCH_S3C2410),y)  
  2. TEXTADDR = 0xC0008000  
  3. MACHINE = s3c2410  
  4. Endif  
 
        b.在 arch/arm/config.in 文件中加入:

  1. if [ "$CONFIG_ARCH_S3C2410" = "y" ]; then  
  2. comment 'S3C2410 Implementation'  
  3. dep_bool ' SMDK (MERI TECH BOARD)' CONFIG_S3C2410_SMDK $CONFIG_ARCH_S3C2410  
  4. dep_bool ' change AIJI' CONFIG_SMDK_AIJI  
  5. dep_tristate 'S3C2410 USB function support' CONFIG_S3C2410_USB $CONFIG_ARCH_S3C2100  
  6. dep_tristate ' Support for S3C2410 USB character device emulation' CONFIG_S3C2410_USB_CHAR $CONFIG_S3C2410_USB  
  7. fi # /* CONFIG_ARCH_S3C2410 */  
 
        arch/arm/config.in 文件还有几处针对 S3C2410 的修改。
        c.在 arch/arm/boot/Makefile 文件中加入:

  1. ifeq ($(CONFIG_ARCH_S3C2410),y)  
  2. ZTEXTADDR = 0x30008000  
  3. ZRELADDR = 0x30008000  
  4. endif  
 
        d.在 linux/arch/arm/boot/compressed/Makefile 文件中加入:

  1. ifeq ($(CONFIG_ARCH_S3C2410),y)  
  2. OBJS += head-s3c2410.o  
  3. endif  
 
        加入的结果是 head-s3c2410.S 文件被编译为 head-s3c2410.o。
        e.加入 arch/arm/boot/compressed/ head-s3c2410.S 文件

  1. #include <linux/config.h>  
  2. #include <linux/linkage.h>  
  3. #include <asm/mach-types.h>  
  4.   
  5. .section ".start", #alloc, #execinstr  
  6.   
  7. __S3C2410_start:  
  8.   
  9. @ Preserve r8/r7 i.e. kernel entry values  
  10. @ What is it?  
  11. @ Nandy  
  12.   
  13. @ Data cache, Intstruction cache, MMU might be active.  
  14. @ Be sure to flush kernel binary out of the cache,  
  15. @ whatever state it is, before it is turned off.  
  16. @ This is done by fetching through currently executed  
  17. @ memory to be sure we hit the same cache  
  18.   
  19. bic r2, pc, #0x1f  
  20. add r3, r2, #0x4000 @ 16 kb is quite enough...  
  21. 1: ldr r0, [r2], #32  
  22. teq r2, r3  
  23. bne 1b  
  24. mcr p15, 0, r0, c7, c10, 4 @ drain WB  
  25. mcr p15, 0, r0, c7, c7, 0 @ flush I & D caches  
  26.   
  27. #if 0  
  28. @ disabling MMU and caches  
  29. mrc p15, 0, r0, c1, c0, 0 @ read control register  
  30. bic r0, r0, #0x05 @ disable D cache and MMU  
  31. bic r0, r0, #1000 @ disable I cache  
  32. mcr p15, 0, r0, c1, c0, 0  
  33. #endif  
  34.   
  35. /* 
  36. * Pause for a short time so that we give enough time 
  37. * for the host to start a terminal up. 
  38. */  
  39. mov r0, #0x00200000  
  40. 1: subs r0, r0, #1  
  41. bne 1b  
 
        该文件中的汇编代码完成 S3C2410 特定硬件相关的初始化。
        f.在 arch/arm/def-configs 目录中增加配置文件
        g.在 arch/arm/kernel/Makefile 中增加对 S3C2410 的支持

  1. no-irq-arch := $(CONFIG_ARCH_INTEGRATOR) $(CONFIG_ARCH_CLPS711X) /  
  2. $(CONFIG_FOOTBRIDGE) $(CONFIG_ARCH_EBSA110) /  
  3. $(CONFIG_ARCH_SA1100) $(CONFIG_ARCH_CAMELOT) /  
  4. $(CONFIG_ARCH_S3C2400) $(CONFIG_ARCH_S3C2410) /  
  5. $(CONFIG_ARCH_MX1ADS) $(CONFIG_ARCH_PXA)  
  6. obj-$(CONFIG_MIZI) += event.o  
  7. obj-$(CONFIG_APM) += apm2.o  
 
        h.修改 arch/arm/kernel/debug-armv.S 文件,在适当的位置增加如下关于 S3C2410 的代码:

  1. #elif defined(CONFIG_ARCH_S3C2410)  
  2.   
  3. .macro addruart,rx  
  4. mrc p15, 0, /rx, c1, c0  
  5. tst /rx, #1 @ MMU enabled ?  
  6. moveq /rx, #0x50000000 @ physical base address  
  7. movne /rx, #0xf0000000 @ virtual address  
  8. .endm  
  9.   
  10. .macro senduart,rd,rx  
  11. str /rd, [/rx, #0x20] @ UTXH  
  12. .endm  
  13.   
  14. .macro waituart,rd,rx  
  15. .endm  
  16.   
  17. .macro busyuart,rd,rx  
  18. 1001: ldr /rd, [/rx, #0x10] @ read UTRSTAT  
  19. tst /rd, #1 << 2 @ TX_EMPTY ?  
  20. beq 1001b  
  21. .endm  
 
        i.修改 arch/arm/kernel/setup.c文件
        此文件中的 setup_arch 非常关键,用来完成与体系结构相关的初始化:

  1. void __init setup_arch(char **cmdline_p)  
  2. {  
  3. struct tag *tags = NULL;  
  4. struct machine_desc *mdesc;  
  5. char *from = default_command_line;  
  6.   
  7. ROOT_DEV = MKDEV(0, 255);  
  8.   
  9. setup_processor();  
  10. mdesc = setup_machine(machine_arch_type);  
  11. machine_name = mdesc->name;  
  12.   
  13. if (mdesc->soft_reboot)  
  14. reboot_setup("s");  
  15.   
  16. if (mdesc->param_offset)  
  17. tags = phys_to_virt(mdesc->param_offset);  
  18.   
  19. /* 
  20. * Do the machine-specific fixups before we parse the 
  21. * parameters or tags. 
  22. */  
  23. if (mdesc->fixup)  
  24. mdesc->fixup(mdesc, (struct param_struct *)tags,  
  25. &from, &meminfo);  
  26.   
  27. /* 
  28. * If we have the old style parameters, convert them to 
  29. * a tag list before. 
  30. */  
  31. if (tags && tags->hdr.tag != ATAG_CORE)  
  32. convert_to_tag_list((struct param_struct *)tags,  
  33. meminfo.nr_banks == 0);  
  34.   
  35. if (tags && tags->hdr.tag == ATAG_CORE)  
  36. parse_tags(tags);  
  37.   
  38. if (meminfo.nr_banks == 0) {  
  39. meminfo.nr_banks = 1;  
  40. meminfo.bank[0].start = PHYS_OFFSET;  
  41. meminfo.bank[0].size = MEM_SIZE;  
  42. }  
  43.   
  44. init_mm.start_code = (unsigned long) &_text;  
  45. init_mm.end_code = (unsigned long) &_etext;  
  46. init_mm.end_data = (unsigned long) &_edata;  
  47. init_mm.brk = (unsigned long) &_end;  
  48.   
  49. memcpy(saved_command_line, from, COMMAND_LINE_SIZE);  
  50. saved_command_line[COMMAND_LINE_SIZE-1] = '/0';  
  51. parse_cmdline(&meminfo, cmdline_p, from);  
  52. bootmem_init(&meminfo);  
  53. paging_init(&meminfo, mdesc);  
  54. request_standard_resources(&meminfo, mdesc);  
  55.   
  56. /* 
  57. * Set up various architecture-specific pointers 
  58. */  
  59. init_arch_irq = mdesc->init_irq;  
  60.   
  61. #ifdef CONFIG_VT  
  62. #if defined(CONFIG_VGA_CONSOLE)  
  63. conswitchp = &vga_con;  
  64. #elif defined(CONFIG_DUMMY_CONSOLE)  
  65. conswitchp = &dummy_con;  
  66. #endif  
  67. #endif  
  68. }  
 
        j.修改 arch/arm/mm/mm-armv.c 文件(arch/arm/mm/ 目录中的文件完成与 ARM 相关的 MMU 处理)
        修改

  1. init_maps->bufferable = 0;  
 
        为

  1. init_maps->bufferable = 1;  
 
        要轻而易举地进行上述马拉松式的内核移植工作并非一件轻松的事情,需要对 Linux 内核有很好的掌握,同时掌握硬件特定的知识和相关的汇编。幸而 mizi 公司的开发者们已经合力为我们完成了上述工作,这使得小弟们在将 mizi-linux 移植到自身开发的电路板的过程中只需要关心如下几点:
        (1)内核初始化:Linux 内核的入口点是 start_kernel() 函数。它初始化内核的其他部分,包括捕获,IRQ 通道,调度,设备驱动,标定延迟循环,最重要的是能够 fork"init" 进程,以启动整个多任务环境。
        我们可以在 init 中加上一些特定的内容。
        (2)设备驱动:设备驱动占据了 Linux 内核很大部分。同其他操作系统一样,设备驱动为它们所控制的硬件设备和操作系统提供接口。
        本文第四章将单独讲解驱动程序的编写方法。
        (3)文件系统:Linux 最重要的特性之一就是对多种文件系统的支持。这种特性使得 Linux 很容易地同其他操作系统共存。文件系统的概念使得用户能够查看存储设备上的文件和路径而无须考虑实际物理设备的文件系统类型。 Linux 透明的支持许多不同的文件系统,将各种安装的文件和文件系统以一个完整的虚拟文件系统的形式呈现给用户。 
        我们可以在 K9S1208 NAND FLASH 上移植 cramfs、jfss2、yaffs 等 FLASH 文件系统。
         3. init 进程
        在 init 函数中"加料",可以使得 Linux 启动的时候做点什么,例如广州友善之臂公司的 demo 板在其中加入了公司信息:

  1. static int init(void * unused)  
  2. {  
  3. lock_kernel();  
  4. do_basic_setup();  
  5.   
  6. prepare_namespace();  
  7.   
  8. /* 
  9. * Ok, we have completed the initial bootup, and 
  10. * we're essentially up and running. Get rid of the 
  11. * initmem segments and start the user-mode stuff.. 
  12. */  
  13. free_initmem();  
  14. unlock_kernel();  
  15.   
  16. if (open("/dev/console", O_RDWR, 0) < 0)  
  17. printk("Warning: unable to open an initial console./n");  
  18.   
  19. (void) dup(0);  
  20. (void) dup(0);  
  21.   
  22. /* 
  23. * We try each of these until one succeeds. 
  24. * 
  25. * The Bourne shell can be used instead of init if we are  
  26. * trying to recover a really broken machine. 
  27. */  
  28.   
  29. printk("========================================/n");  
  30. printk("= Friendly-ARM Tech. Ltd. =/n");  
  31. printk("= http://www.arm9.net =/n");   
  32. printk("= http://www.arm9.com.cn =/n");  
  33. printk("========================================/n");  
  34.   
  35. if (execute_command)  
  36. execve(execute_command,argv_init,envp_init);  
  37. execve("/sbin/init",argv_init,envp_init);  
  38. execve("/etc/init",argv_init,envp_init);  
  39. execve("/bin/init",argv_init,envp_init);  
  40. execve("/bin/sh",argv_init,envp_init);  
  41. panic("No init found. Try passing init= option to kernel.");  
  42. }  
 
        这样在 Linux 的启动过程中,会额外地输出:

  1. ========================================  
  2. = Friendly-ARM Tech. Ltd. =  
  3. = http://www.arm9.net =  
  4. = http://www.arm9.com.cn =  
  5. ========================================   
 
         4.文件系统移植
        文件系统是基于被划分的存储设备上的逻辑上单位上的一种定义文件的命名、存储、组织及取出的方法。如果一个 Linux 没有根文件系统,它是不能被正确的启动的。因此,我们需要为 Linux 创建根文件系统,我们将其创建在 K9S1208 NAND FLASH 上。
        Linux 的根文件系统可能包括如下目录(或更多的目录):
        (1)/bin (binary):包含着所有的标准命令和应用程序; 
        (2)/dev (device):包含外设的文件接口,在 Linux 下,文件和设备采用同种地方法访问的,系统上的每个设备都在 /dev 里有一个对应的设备文件;
        (3)/etc (etcetera):这个目录包含着系统设置文件和其他的系统文件,例如 /etc/fstab(file system table)记录了启动时要 mount 的 filesystem;
        (4)/home:存放用户主目录;
        (5)/lib(library):存放系统最基本的库文件;
        (6)/mnt:用户临时挂载文件系统的地方;
        (7)/proc:linux 提供的一个虚拟系统,系统启动时在内存中产生,用户可以直接通过访问这些文件来获得系统信息; 
        (8)/root:超级用户主目录;
        (9)/sbin:这个目录存放着系统管理程序,如 fsck、mount 等;
        (10)/tmp(temporary):存放不同的程序执行时产生的临时文件;
        (11)/usr(user):存放用户应用程序和文件。
        采用 BusyBox 是缩小根文件系统的好办法,因为其中提供了系统的许多基本指令但是其体积很小。众所周知,瑞士军刀以其小巧轻便、功能众多而闻名世界,成为各国军人的必备工具,并广泛应用于民间,而 BusyBox 也被称为嵌入式 Linux 领域的"瑞士军刀"。
        此地址可以下载 BusyBox: http://www.busybox.net,当前最新版本为 1.1.3。编译好 busybox 后,将其放入 /bin 目录,若要使用其中的命令,只需要建立 link,如:

  1. ln -s ./busybox ls  
  2. ln -s ./busybox mkdir  
 
         4.1 cramfs
        在根文件系统中,为保护系统的基本设置不被更改,可以采用 cramfs 格式,它是一种只读的闪存文件系统。制作 cramfs 文件系统的方法为:建立一个目录,将需要放到文件系统的文件 copy 到这个目录,运行"mkcramfs 目录名 image 名"就可以生成一个 cramfs 文件系统的 image 文件。例如如果目录名为 rootfs,则正确的命令为:

  1. mkcramfs rootfs rootfs.ramfs  
 
        我们使用下面的命令可以 mount 生成的 rootfs.ramfs 文件,并查看其中的内容:

  1. mount -o loop -t cramfs rootfs.ramfs /mount/point  
 
        此地址可以下载 mkcramfs 工具: http://sourceforge.net/projects/cramfs/
         4.2 jfss2
        对于 cramfs 闪存文件系统,如果没有 ramfs 的支持则只能读,而采用jfss2(The Journalling Flash File System version 2)文件系统则可以直接在闪存中读、写数据。jfss2 是一个日志结构(log-structured)的文件系统,包含数据和原数据(meta-data)的节点在闪存上顺序地存储。jfss2 记录了每个擦写块的擦写次数,当闪存上各个擦写块的擦写次数的差距超过某个预定的阀值,开始进行磨损平衡的调整。调整的策略是,在垃圾回收时将擦写次数小的擦写块上的数据迁移到擦写次数大的擦写块上以达到磨损平衡的目的。 
        与 mkcramfs 类似,同样有一个 mkfs.jffs2 工具可以将一个目录制作为 jffs2 文件系统。假设把 /bin 目录制作为 jffs2 文件系统,需要运行的命令为:

  1. mkfs.jffs2 -d /bin -o jffs2.img  
 
         4.3 yaffs
        yaffs 是一种专门为嵌入式系统中常用的闪存设备设计的一种可读写的文件系统,它比 jffs2 文件系统具有更快的启动速度,对闪存使用寿命有更好的保护机制。为使 Linux 支持 yaffs 文件系统,我们需要将其对应的驱动加入到内核中 fs/yaffs/,并修改内核配置文件。使用我们使用 mkyaffs 工具可以将 NAND FLASH 中的分区格式化为 yaffs 格式(如 /bin/mkyaffs /dev/mtdblock/0 命令可以将第 1 个 MTD 块设备分区格式化为 yaffs),而使用 mkyaffsimage(类似于 mkcramfs、mkfs.jffs2)则可以将某目录生成为 yaffs 文件系统镜像。
        嵌入式 Linux 还可以使用 NFS(网络文件系统)通过以太网挂接根文件系统,这是一种经常用来作为调试使用的文件系统启动方式。通过网络挂接的根文件系统,可以在主机上生成 ARM 交叉编译版本的目标文件或二进制可执行文件,然后就可以直接装载或执行它,而不用频繁地写入 flash。
        采用不同的文件系统启动时,要注意通过第二章介绍的 BootLoader 修改启动参数,如广州友善之臂的 demo 提供如下三种启动方式: 
        (1)从 cramfs 挂接根文件系统:root=/dev/bon/2(); 
        (2)从移植的 yaffs 挂接根文件系统:root=/dev/mtdblock/0;
        (3)从以太网挂接根文件系统:root=/dev/nfs。
         5.小结
        本章介绍了嵌入式 Linux 的背景、移植项目、init 进程修改和文件系统移植,通过这些步骤,我们可以在嵌入式系统上启动一个基本的 Linux。