将 Linux 移植到 PowerPC 开发板
我们相信 Linux 将在嵌入式应用中发挥重要作用。它符合 POSIX 1003.1 标准并支持 POSIX 软实时扩展。理论上,它能够支持广泛的仅需要软实时性能的嵌入式应用,例如互联网路由。易于定制使其更具吸引力。为了研究使用 Linux 作为嵌入式系统平台的可能性,我们进行了一项实验,将内核移植到基于 PowerPC 的开发板上。移植工作花费了几个星期。我们的成果是一个 Linux 移植版本,在此称为 elinux,它基于 Linux 内核 2.1.132,这是我们开始实验时的最新版本。elinux 可以通过串行端口在控制台上运行数十个命令和程序,包括 bash 和 vi。移植 Linux 实际上是一次非常愉快的经历。此外,所有工作完全基于开源软件。
Linux 是可移植的。我们已经看到许多 Linux 移植版本在各种处理器上运行。然而,很少有文档描述如何移植 Linux。相关信息分散在各种文档和源代码中。对于任何操作系统来说,移植都不是一件容易的工作。即使对内核代码进行微小的更改以适应特定的硬件,也需要付出相当大的努力。幸运的是,对于 Linux 而言,内核的所有主要组件都已设计为与体系结构无关。这使得这项工作相对容易得多。
将 Linux 移植到使用新处理器的开发板比移植到 Linux 已经支持的处理器上更难。在后一种情况下,我们可以重用与开发板无关的代码;例如,内存管理的代码。只有相对较小一部分内核代码是与开发板相关的。当我们考虑实现 elinux 时,我们试图避免重复造轮子。我们将大多数必要的更改限制在与开发板相关的部分。我们的实验是在基于 PowerPC 的开发板上完成的。Linux 已经有针对基于 PowerPC 的机器的移植版本,例如 Power Macintosh 和一些基于 PowerPC 的嵌入式开发板。但是,由于开发板架构、配置和启动方法的多样性,当我们考虑新的开发板时,需要进行修改。
在我们的案例中,对内核进行了一些更改,并创建了一些新的小程序来支持 elinux。在下面的文章中,我们重点强调我们在工作中最为关注的问题的经验,而不是实现细节。这些包括建立交叉开发平台、设计启动顺序、修改内核、创建可执行镜像和根文件系统镜像以及调试。
我们的目标不是将 Linux 移植到特定类型的硬件。相反,我们对将 Linux 内核移植到潜在的嵌入式系统的方法感兴趣。因此,我们选择哪种开发板并不重要,只要它呈现一种典型的情况即可。我们在实验中实际使用的开发板是基于 PCI 总线构建的 PowerPC 开发板。它具有 PowerPC 603e 处理器、MPC 106 作为内存控制器和 PCI 桥接器、32MB DRAM 内存、用于两个串行端口的 PC16552 DUART 芯片、非易失性存储器中的内存映射实时时钟以及一个简单但定制化的中断控制器。它还具有两个闪存插槽和一个 Intel 82558 LAN 控制器,以提供三个 LAN 端口。
该开发板在 ROM 中有自己的引导代码。此代码执行硬件初始化,并提供简单的本地文件系统和 TFTP 支持。尽管我们使用了这两项服务来启动 elinux,但我们的方法可以支持完全从 ROM 启动。
仅仅从任何 Linux 发行版安装二进制文件并不能保证一个可用的交叉开发平台。有些人遇到了设置完整交叉开发环境的困难。我们的经验表明,这不仅是可能的,而且还为我们带来了相当大的便利,因为我们可以使用一些最流行的软件包。我们的经验教训是适当的发行版、适当的配置和重新编译。
以下是在 Pentium 机器上为 PowerPC 设置交叉开发平台的实际步骤
在配备 256MB RAM 和 8GB SCSI 硬盘的 Dell OptiPlex Pentium II 400 MHz PC 上安装 Red Hat 5.2。
获取最新的稳定 Linux 内核 2.0.36 的源代码,这是我们开始工作时的版本。
从内核源代码重新编译内核,以确保包含对环回设备、RAM 磁盘和其他必要项的支持。
使用新重新编译的镜像来启动开发平台。
在基本开发系统准备就绪后,我们按如下方式安装交叉开发支持
首先,安装二进制实用程序 binutils-2.9.1.0.15 的源代码,其中包括交叉汇编器、交叉加载器和其他交叉实用程序。
为 PowerPC with Linux 重新编译并安装交叉实用程序。
安装编译器 gcc 2.8.1 的源代码。
为交叉编译器重新编译并安装。
启动操作系统似乎很容易。您只需打开系统电源,过一会儿,您就会在控制台上看到一个提示符,表明系统正在运行。如果我们深入研究启动内部机制,我们会得到一个更复杂的视图。启动包括硬件初始化和软件启动,这些操作因开发板而异。在 Linux 体系结构相关的源代码目录 linux/arch/ 中,每种类型的开发板都存在不同的启动代码。对于新的开发板,我们通常必须添加新的启动顺序。
典型的嵌入式开发板没有软盘和硬盘。代码和数据最初放在 ROM 中,或者可以通过网络连接下载。我们设计了一种从 ROM 启动此类系统的通用方法。
我们的方法是将启动分为两个阶段,由两个单独的加载器支持。一个称为镜像加载器 iloader,另一个称为 Linux 内核加载器 kloader。iloader 可以存储在 ROM 中。也就是说,系统上电后,它启动,执行必要的硬件初始化,然后将 Linux 内核加载器从 ROM 移动到 RAM 中的正确位置。kloader 在 iloader 完成后开始运行。首先,它执行更多的硬件初始化。然后,它通过解压缩 Linux 内核镜像来设置启动 Linux 内核的环境。最后,它跳转到内核代码以开始主要的 Linux 启动序列。
为了更清楚地说明问题,请考虑 elinux。最终的 elinux 镜像,我们称之为 elinux ball,被打包成一个包含三个项目的单个文件
静态链接的 iloader 可执行二进制文件
我们的 zImage,由未压缩的内核镜像 vmlinux.bin.gz 加上 kloader 组成
压缩的根文件系统镜像 ramdisk.gz
elinux ball 的大小取决于包含的服务和程序的数量。在我们的实验中,它被限制为 2MB,这对于大多数情况来说已经足够大了。如果需要更大的程序,可以在系统启动后下载。最好保持 ball 小巧。打包工作是通过一个名为 packbd (打包二进制文件和数据镜像) 的工具简单完成的。elinux ball 是使用以下命令获得的
packbd iloader kloader vmlinux.bin.gz ramdisk.gz\ elinuxiloader 是启动 elinux ball 的入口点。因为它可以在 ROM 中存储,所以整个 elinux ball 也可以在 ROM 中存储。但是,我们不必将其放入 ROM 即可启动系统。实际上,在我们的开发中,我们使用本地网络服务 TFTP 将 elinux ball 下载到 RAM 区域并开始执行。
除了通常需要更多精力的硬件初始化之外,iloader、kloader 和 packbd 的实现对于任何系统来说都很简单。
内核修改是移植中最困难的部分。幸运的是,Linux 被设计为可移植的,其源代码组织良好,呈树状结构。一旦您对内核源代码进行了相当多的研究,需要做的事情就会变得清晰起来。正如我们之前提到的,当将 Linux 移植到新的开发板时,需要进行更改甚至编写新的代码。基本上,即使我们使用 Linux 支持的处理器,也必须修改或调整所有与开发板相关的代码。当有新的需求或任何错误修复时,也需要进行更改。大多数更改都集中在少数文件中,因此希望这可以帮助我们方便地跟上新的版本发布。
我们为 elinux 使用了实验性内核版本 2.1.132 PPC 移植。几乎所有更改都限制在与开发板相关的部分,即我们 PowerPC 开发板的子目录 linux/arch/ppc 中。已经进行了数十项更改;许多是为了适应新硬件,其他是为了错误修复和新需求,例如新的内存映射。
elinux 的主要更改包括硬件初始化、PCI 总线初始化、内存管理、定时器处理和中断处理。
硬件初始化是 elinux 中最繁琐的部分。加载器 iloader 应该完成工作中最重要的部分,例如内存控制器和 PCI 控制器的初始化。然而,我们 iloader 的实现只是忽略了这部分,因为我们发现,在开发板的 ROM 代码完成初始化之后,再次执行初始化是不必要的。当然,如果 iloader 是第一个从 ROM 运行的代码,则 iloader 必须执行初始化。内核初始化执行其他操作,例如内存保护和总线设备初始化。
原始的 PPC 移植版本通过与 BIOS 的接口与 PCI 设备通信。对于我们的开发板,我们假设没有类似的东西。我们只是在开始时将接口留空。每当我们添加新的 PCI 设备时,我们都会直接编写代码来设置相关的基地址、IRQs 和访问方法。
内存管理需要考虑一些事项。尽管我们不需要对内存管理的主要部分(例如虚拟内存管理和分页)进行任何更改,但我们确实有修改请求。它们主要用于设置特定的内存大小和范围、在内核启动期间重新排列内存、使用 PowerPC BAT(块地址转换)寄存器对以及物理地址和虚拟地址之间的内存映射。
对于定时器处理,修改了两件事。一是调整参数以设置 PowerPC 的递减器,使其适应开发板的总线速率。此递减器用于每 jiffy 时间(10 毫秒)生成一个定时器中断。另一个更改是提供与板载实时时钟 (RTC) 直接访问的接口。
另一个主要更改是针对中断控制器。此控制器很简单,仅通过状态寄存器、掩码寄存器和锁存寄存器控制 16 个 IRQ。所有寄存器均为 16 位。每个位对应一个 IRQ。添加了新的简单代码来处理它。
我们通过将符号 KERNELBASE 重新定义为 Makefile 宏,成功地在虚拟空间中重定位了内核。这涉及到内核初始化代码中的一些更改。由于我们能够重定位内核,我们可以为某些特殊目的保留空间。例如,要将内核加载到地址 0xa0000000 而不是默认地址 0xc0000000,我们只需在顶层 Makefile 中这样定义 KERNELBASE
KERNELBASE = 0xa0000000
在各种 Makefile 中进行了细微的更改。这些更改是必要的,因为我们需要新规则来创建 elinux ball,我们有一些新文件需要编译和链接,并且在进行交叉编译时,我们在 Makefile 中发现了错误。
至于设备驱动程序,我们只关心控制台端口的串行驱动程序。如果需要,以后可能会添加其他驱动程序,例如 LAN 驱动程序和控制定制设备的驱动程序。在我们的实验中,我们使用 minicom 通过串行端口与 elinux 通信。我们使用来自 Linux 代码 drivers/char/serial.c 的串行驱动程序。对调整波特率进行了细微的更改,并且由于串行端口具有不同的 IRQ 编号,因此在其头文件中也进行了另一项更改。
在一切都正确完成后,我们通过控制台看到 elinux 启动并愉快地运行。
ELINUX VERSION 0.001 March 1999 Start booting Linux on Experiment Board ... ... (omitted long booting messages) # (we start ash after the kernel is up)
如前所述,elinux ball 需要一个静态链接的内核镜像和一个根文件镜像。它们的准备需要一些技巧。
Linux 源代码提供了为不同平台创建各种内核镜像的规则。对于我们的系统,我们需要一个压缩的二进制内核镜像,vmlinux.bin.gz。创建它的步骤依次是配置、编译和链接、转换为二进制格式以及压缩。在配置中,确保选择 RAM 磁盘支持和初始根文件系统支持,并禁用所有不必要的选项。编译和链接内核,生成名为 vmlinux 的 ELF 可执行文件。然后通过如下命令转换为二进制格式并压缩
(CROSS_COMPILE)objcopy -S -O binary vmlinux\ vmlinux.bin gzip -vf9 vmlinux.bin
因为我们选择在内核启动后在 RAM 中挂载初始根文件系统,所以我们必须准备一个名为 ramdisk.gz 的根镜像,并将其放入 elinux ball 中。我们通过在交叉开发平台上 4MB 的 RAM 磁盘中创建 EXT2 文件系统来做到这一点。接下来,创建子目录,例如 /etc、/dev、/bin 和 /lib。然后,将脚本、二进制文件、设备节点等复制到 RAM 磁盘上。最后,压缩 RAM 磁盘镜像,得到 ramdisk.gz。例如,要在 /dev/ram1 中创建 RAM 磁盘,请输入
rdev -r /dev/ram1 4096通过输入以下命令创建文件系统并将其挂载到 /tmp
mke2fs -vm0 /dev/ram1 4096 mount /dev/ram1 tmp要创建设备,请使用 cp -d 或 mknod,如下所示
mknod ttyS0 c 4 64这将为 elinux 上的串行控制台端口创建一个设备节点,主设备号为 4,次设备号为 64。
在 /tmp 中的所有内容准备就绪后,通过输入以下命令压缩它
dd if=/dev/ram1 bs=1k count=4096 | gzip -v9 > ramdisk.gz
初始根 RAM 磁盘中应包含的内容将取决于我们的需求。我们复制最少数量的共享库,以及一些程序,如 bash 和 vi 用于测试。
Bug 是不可避免的。“只要有足够的眼睛,所有的 bug 都是肤浅的” 这句话只对了一部分。大多数时候,我们必须在没有外部帮助的情况下进行调试。调试可能很痛苦,尤其是对于系统启动而言。有一种由特殊硬件支持的启动调试工具,但我们不想依赖它。其他调试机制,如 printk 和 gdb 在许多情况下有所帮助,但它们需要太多的系统服务。例如,Linux 内核调试支持 printk 仅在系统准备好写入控制台或文件系统后才工作。如果系统在那之前崩溃,即使 printk 使用内存缓冲区来存储过程早期阶段的信息,我们也无法从 printk 获得任何信息。
在这种情况下,简单意味着效率。为了帮助解决这个问题,我们添加了 rprintf。它是一个使用原始输出的简单打印函数,它将字符直接写入控制台 I/O 端口,而无需任何缓冲和任何其他支持。rprintf 类似于 printf,但仅基于这种原始输出。它在 iloader 运行后很快就可以工作,因此它也可以用于调试 kloader 和 Linux 内核。rprintf 帮助我们解决了启动早期阶段的大多数问题。在 rprintf 初始化之前,我们确实遇到了一些问题,但我们并非无助。我们的建议是插入一个操作以强制系统重启;这样,您可以很快找到问题所在。我们假设您知道开发板何时开始重启。我们提供了一个名为 rreboot 的函数,通过简单地跳转到 ROM 中的系统重启入口点,为我们的开发板完成这项工作。
与其他项目不同,这项工作严重依赖互联网。我们学到了很多关于 Linux 的知识,并从 Web 上获得了诸如内核代码和 Linux 相关文档之类的资源。要在 Linux 上做一些有意义的事情,请尽可能利用开源代码,遵循经过验证的模式。重用可以运行的代码片段,即使需要进行更改。一开始不要太雄心勃勃。在进入下一阶段之前,花大量时间进行调查。此外,花时间在开始时进行良好的设计并选择良好的调试支持。每当您需要帮助时,请始终首先参考 Linux 书籍和网站(请参阅“资源”)。读者可以轻松地在 Web 上发现大量相关信息。
同一件事可以用几种方法来完成。我们的实验远非全面,但我们对 Linux 在某些嵌入式系统中的潜力充满信心。我们希望我们的经验能够帮助其他想要将 Linux 内核移植到其嵌入式系统的人。还有许多相关问题需要我们研究——前方有很多乐趣。

