当硬件变“软”时如何移植Linux
在软件开发中,可能最神秘和最负盛名的工作是将垂死的硬件注入生命——将操作系统移植到一个新的平台——这是巫师和大师的神秘领域,是新机器的灵魂软件方面。我几乎执行过所有其他的软件开发任务,并且我想要一个征服这项任务的机会。
我已经使用Linux和开源软件多年。我是一名相当有能力的软件开发人员(具有硬件经验),但在开始E12移植之前,我所做的只是调整Linux驱动程序和构建自定义配置的内核。我很幸运有一位朋友正在创建一家新公司,该公司正在开发最小的嵌入式系统之一,即Pico E12。我几乎乞求获得将Linux安装在E12上的机会。“人的目标应该超越他的能力所及,否则天堂还有什么意义?”
E12使用了Xilinx Virtex 4 FX20 FPGA(现场可编程门阵列),其中包括一个300MHz PowerPC 405处理器、128MB内存和64MB闪存ROM。我在eBay上买了一台Macintosh Lombard PowerBook笔记本电脑,作为E12的一种模拟器。它也提供了一种在没有交叉编译器的情况下为E12编写代码的方法。在等待E12进展到足以开始使用它的时候,我在网上搜寻有关Linux移植的信息,并培养了PowerPC汇编语言的能力。Linux内核编程主要使用C语言,但Linux内核的小部分——对于在新系统上安装Linux至关重要的部分——是用汇编语言编写的。我用多种汇编语言编程过——曾经用x86汇编语言编写标准C库,但PPC汇编语言是新的,花了一两天时间才学会。Linux很久以前就被移植到PowerPC,甚至不同的Xilinx FPGA。
我有一个软件书籍参考图书馆,装满了三个车库。除了少数例外,它们都积满了灰尘。我今天的主要研究工具是宽带互联网连接和搜索引擎。互联网上有大量可供Linux开发人员使用的资源。《Linux设备驱动程序指南》——设备驱动程序的Linux圣经——以及众多的邮件列表,针对Linux系统开发的各个方面。“Kernel-Newbies”是一个很好的起点(请参阅在线资源)。每个Linux子系统都有邮件列表。而且,还有几个Linux PowerPC邮件列表——一个专门针对嵌入式PowerPC Linux。这棵树的根是LKML,Linux内核邮件列表。LKML是奥林匹斯山——Linus和其他Linux神和巨人的家园。有一些网页记录了其他人将Linux移植到特定主板的经验。最后,终极参考——Linux内核源代码——可在kernel.org上获得。
最终,E12的进展足以开始工作,我通过联邦快递收到了一个。我拥有文档和规范,但真正拿到实物让我感到真实,并解答了无法从规范中读取的问题。
E12
E12是一张Compact Flash卡——与许多数码相机中的卡完全相同。它只有两个连接器:一端是CF总线连接,另一端是15针微型连接器。没有其他外部连接。E12基于FPGA。还有一些额外的组件,以及一些固定的元素,例如FPGA上的PPC405 CPU。大部分硬件是可编程的。大多数外部连接都通过FPGA。几乎所有的“硬件”在FPGA加载之前都没有形式或意义。动态更改位文件会引入一个全新的硬件设计。欢迎来到一个新时代——甚至硬件也是软件。BIT图像——本质上是FPGA的程序——由FPGA开发人员创建,编程到闪存ROM中,并在上电时自动加载到FPGA中。一旦这个BIT图像“启动”,硬件就会在FPGA中创建。现在,连接器上的引脚才有了意义。15针连接器为内部设备提供三个外部连接。它通过定制电缆支持以太网、串行和JTAG连接。CF连接器提供与主机的双向接口——在大多数情况下使用CardBus或PCMCIA适配器。任一连接器上的大多数引脚都可以是FPGA程序员选择的任何功能。现场系统可以插入CF连接器,仅仅是为了获得电力。E12用于典型嵌入式应用中的子卡,用于集群中的高性能计算机的总线板上,以及用于图像处理或代码破解等应用。它们也用于没有操作系统或极简操作系统的应用中。
Pico提供了用于托管开发的工具。标准的E12 BIT文件提供了一个带有模拟LPT3/JTAG端口的CF接口,一个称为“钥匙孔”的512字双向通信FIFO,以及主机对闪存ROM的访问。Pico还提供了主机端的Windows和Linux驱动程序,允许读取和写入闪存ROM。正常的FPGA BIT图像包含一个非常小的PPC监视器程序,它可以执行少量任务——其中大多数任务严重依赖主机的支持。其中一个功能是将两种类型的文件加载到E12中。它可以加载一个新的BIT图像,或者加载并执行二进制ELF文件——一个简单的引导加载程序。这省去了我移植引导加载程序(如U-Boot)的困难。Linux内核是E12监视器程序到目前为止加载的最复杂的ELF文件,并且加载程序需要进行一些调整。
我的首要目标是为E12编写众所周知的“Hello World”程序。我花了几天时间编写了两个不同的“Hello World”程序:一个用于钥匙孔FIFO,另一个用于Xilinx uartlite端口。
现在,我准备攻击Linux了。我决定从Linux 2.6开始。存在许多问题——充分的理由,以及赞成2.4和2.6的受尊重且相互冲突的意见。我选择使用Linux 2.6,因为我最终无论如何都必须迁移到2.6。最初,我使用PowerBook为Pico E12配置和构建我的Linux内核。这使我能够在没有交叉编译器的情况下开始。最终,我切换到在Windows上托管E12的coLinux虚拟机内部构建。大多数Pico客户都在进行Windows托管开发。关键是让一切都在该环境中工作。此外,在运行Windows的Linux虚拟机中构建PowerPC Linux内核,并将其加载到PowerPC中,意味着在我的笔记本电脑内部,Linux的数量比Windows多2比1。
我使用Xilinx ML300作为模板来创建一个新的Linux BSP(板级支持包)。我在内核源代码中grep了所有对Xilinx ML300的引用。我复制了所有与ML300相关的文件,并将它们重命名为Pico E12的新文件。E12有四个完全独特的文件
arch/ppc/platforms/4xx/pic0-e1x.c:特定于主板的设置代码。
arch/ppc/platforms/4xx/pic0-e1x.h:特定于主板的设置代码的头文件和数据结构。
arch/ppc/platforms/4xx/xparameters/xparameters_pic0-e1x.h:由创建FPGA位图像的Xilinx软件创建的一组硬件定义。
arch/ppc/configs/defconfig_pic0-e1x:E12的默认Linux配置文件。
Xilinx ML300之间存在主要相似之处,但也存在一些特定差异。E12有意实现了更少的硬件。E12的目的是提供一个非常小的基本平台,将最大比例的FPGA留给客户。最小的有用的Linux配置必须具有以太网、串行端口或钥匙孔端口。默认的E12没有中断控制器——PPC405提供了一个定时器中断,不需要PIC。E12还使用了Xilinx uartlite uart,而不是更大更常见的16550 uart。uartlite没有Linux驱动程序。其他两个ML300文件,Virtex FPGA的通用支持需要进行少量修改。
下一个主要问题是学习Linux配置系统。我没有找到太多文档。对于Linux内核编程,两个主要资源是Linux源代码本身和邮件列表。有时,系统有优秀的文档;有时,什么都没有。有时,我在网络的某个不起眼的角落找到了文档——在我自己弄清楚之后。我必须对内核构建系统有足够的了解,才能向构建系统添加新的主板、一些新的配置选项和一些新的驱动程序。
第一个要素是大多数Linux源目录中的Kconfig文件。Kconfig介于非常非常简单的脚本语言和菜单构建语言之间。Kconfig文件中的条目决定了当您执行任何Linux菜单配置构建选项(make oldconfig、make menuconfig、make xconfig)时获得的菜单结构和选项。
我必须在ppc 4xx菜单下为Pico E12创建一个新的菜单项,在drivers/serial/Kconfig文件中为uartlite和钥匙孔串行端口创建菜单项,以及为其他选项创建少量菜单项。我需要创建的Kconfig项的语法可以很容易地通过检查和少量试错来解决。我复制了类似对象的块,根据需要进行了名称更改,并且没有花费太多力气,它就工作了。在.config文件、源代码和Makefile中,Kconfig文件中定义的配置项以CONFIG_为前缀。创建Kconfig条目后,需要将条目添加到匹配的Makefile中。这主要涉及复制类似的对象并进行名称更改,除了少数非常复杂的选择之外,都非常容易。
到目前为止,我几乎没有进行实际编码。我所做的大部分工作是从新的Pico E12副本中删除ML300特定的代码。我还复制了Xilinx PIC驱动程序,并创建了一个精简的虚拟PIC驱动程序。
我现在能够为Pico E12构建Linux内核,但没有串行或以太网驱动程序。我仍然需要编写两个串行设备驱动程序:uartlite.c和keyhole.c。我特意选择使用8250驱动程序作为模板——8250及其众多的后继产品无处不在,可能比所有其他串行设备加起来还要多。我假设8250驱动程序将是迄今为止最稳定和调试良好的串行驱动程序。此外,已知许多基于8250的系统存在中断问题,因此我知道Linux 8250驱动程序必须在没有中断的情况下工作。事实证明这是一个糟糕的选择。Linux 8250驱动程序可能是迄今为止最复杂的Linux串行驱动程序。
最终,我基于m32r_sio驱动程序重塑了我的驱动程序。我对m32r_sio了解不多,但该驱动程序简洁明了,具有我需要的所有功能,没有我不需要的功能。我还必须为钥匙孔和uartlite创建一组串行端口头文件,定义uart寄存器和寄存器内的位。我还直接以8250为模型,这是一个更好的决定。我编写uart代码(包括软件uart)已经很长时间了。为钥匙孔和uartlite编写设备特定代码很简单。发送和接收字符只需要不到十几行代码。uartlite和钥匙孔与大多数Linux串行设备一样,没有调制解调器控制,并且以单一速度运行。发送字符所需的少量代码在其他地方也很有用,用于调试。钥匙孔不是真正的串行设备,但可以使其看起来像Linux的串行设备,然后在E12被托管时用作控制台。这非常重要。
将一团乱麻般的电缆连接到主机和E12上微小的外部连接器以用于以太网和uartlite串行端口,这造成了一些问题。在尝试新的内核之前,测试每个电缆连接以确保没有松动所花费的时间,比编写和测试代码的时间还要长。在我完成之前,我磨损或损坏了几个外部连接器。当使用钥匙孔时,E12和主机之间的所有连接都是内部的。将调试信息通过一个设备发送,同时使用另一个设备作为控制台也很有用。钥匙孔还有另一个极其方便的属性——我可以将16位或32位的值作为单个输出指令写入一个寄存器,并在主机端看到数据。这在调试PowerPC汇编代码时至关重要。插入代码以显示值或跟踪执行需要使用很少的指令、最小的副作用,并假设很少的东西在工作。直接将值输出到钥匙孔端口成为我闪烁连接到I/O端口的LED的等效操作。它同样简单,并且功能更强大。
在某种程度上,所有的软件开发都是在黑暗中工作,但嵌入式主板的启动尤其如此。输出就像手电筒,让你看到正在发生的事情的冰山一角。E12提供了JTAG调试的规定,可以通过模拟并行端口或15针连接器进行调试。Linux内核提供了kgdb和xmon支持。这些都假定主机端支持以及目标端工作正常的硬件和驱动程序。Linux还提供了在加载控制台驱动程序之前输出进度和调试的选项。这些主要限于8250兼容的uart。我将uartlite和钥匙孔端口添加到早期的文本调试设备中。除了说服Linux使用它之外,这主要涉及提供几行代码来输出一个字符。我拥有使用从逻辑分析仪到gdb的调试工具所需的技能。大多数时候,我发现复杂的工具提供了大量额外的信息,掩盖了问题而不是揭示问题。但是调试是一门宗教艺术,有竞争的教派,每个教派都有自己的教条。
一旦我有了uartlite和钥匙孔的工作输出例程、E12的精简版ML300代码以及为E12修改的Kconfig和Makefile,我就准备构建内核并尝试它。PowerPC的正常Linux构建过程会在arch/ppc/boot/images中以ELF格式留下一个内核映像,即zImage.elf。我将此文件从我用于构建Linux内核的PowerBook复制到E12的主机计算机上,并使用PicoUtil替换了E12闪存上的当前Linux内核映像。我使用E12的监视器执行ELF文件。Linux启动过程在不同平台和启动方法中是相似的。在我的实例中,zImage.elf文件加载到0x40000000,并从一个小的包装器开始,该包装器进行了一些早期的硬件设置、解压缩和重定位实际的Linux内核,然后跳转到早期的Linux设置代码。我将钥匙孔和uartlite的简单字符输出例程复制到文件arch/ppc/boot/simple/keyhole_tty.c和uartlite_tty.c中,这些例程在包装器执行期间提供了调试输出。
我的第一个大问题是E12的内存映射将闪存起始于物理地址0,而RAM起始于更高的物理地址。我在Linux PPC嵌入式邮件列表上收到的建议表明,如果有可能说服主板设计师更改内存映射,我真的真的不想尝试将Linux移植到RAM不在0的主板上。之前和之后都有人尝试修改Linux以支持RAM不从物理地址0开始的系统。我相信现在这个问题已经不那么重要了。不过,我还是接受了建议,经过几个小时的恳求,Pico同意重新组织内存映射到0。软硬件意味着他们能够在几个小时内为我提供一个新的位图像,其中RAM映射到0。
有一段时间,我也运行了我自己定制的监视器程序版本,传递了一个Linux期望的主板信息结构,其中包含少量关于内存大小、处理器速度和用于NIC的mac地址的信息。最终,这些修改被合并到标准的Pico监视器中。
应用于我的系统的启动过程的最佳文档在xilinx_ml300.c顶部的MontaVista注释中。这些注释没有涵盖解压缩和重定位包装器,但公开了其余的启动过程。
下一个重要问题是在arch/ppc/kernel/head_4xx.S中。在这里,Linux进行基本的MMU和异常处理设置,然后使用rfi指令从“实”模式转换为“虚拟”模式,并继续进行内核初始化。我能够执行到rfi。我能够检查所有成功执行rfi的明显条件。但是,我从未到达start_here——rfi应该继续的地方。我花了几天时间来理解Linux虚拟内存系统——大部分文档都是x86特定的。并且,我对PowerPC MMU有了更多的了解,与x86 MMU相比,它是一个相当简单的设备。它基本上是一个64条目的地址转换表。虚拟内存操作系统不可避免地会使用超过64个虚拟-物理地址映射、区域大小和权限。对MMU中不存在的虚拟地址或违反该条目设置的权限位的虚拟地址的引用会引发异常,操作系统有责任使用任何适合它的算法、方法和数据来解决它。故障处理可能需要更长的时间,因为它不是在硬件中处理的,但它更灵活、适应性更强且资源密集程度更低。在物理内存的专用区域中没有巨大的固定映射表,这在某些其他处理器上是必需的。
但是,我仍然无法弄清楚为什么rfi没有正确执行。我在MMU中添加了各种各样的额外条目,假设我实际上成功切换到虚拟模式,但无法通信,因为我的I/O端口不再可访问。我在head_4xx.S中散布了相当于“我在这里”的调试标记,并获得了我的第一个线索。我正在不断地循环遍历异常处理程序。每次我切换到虚拟模式时,我都会失去对PPC的控制,并在异常处理程序中再次以实模式重新获得控制。我有了弄清楚事情的关键线索,但我仍然感到困惑。
如果每个问题都可以分解成更小的部分,那么每个问题都可以解决。最终,我意识到有可能以更小的增量而不是像rfi那样一次性地从实模式转换为虚拟模式。我能够打开数据地址转换并将其关闭,而不会产生不良影响。我能够在MMU中为我的钥匙孔调试端口添加1-1物理到虚拟地址映射,打开它以进行一些输出并将其关闭。通过更多的努力,我能够打开指令地址转换执行代码并将其关闭。
那时,我终于意识到问题与从实模式切换到虚拟模式无关,而是rfi设置的其他东西必须启用一个否则不会发生的异常。因此,我逐位测试了MSR_KERNEL中的位——Linux使用的PPC机器状态寄存器值——直到我发现,每当我设置MSR_CE(启用机器检查异常)时,我都会失去控制。我重新定义了设置MSR_KERNEL的宏,以便它不为E12设置MSR_CE,并向Pico报告说我认为E12存在硬件问题。Pico从未找到问题,但在六个月后,Xilinx固件构建块的更新纠正了该问题。
在解决了机器检查问题后,我突然发现Linux一直启动到设置串行/控制台驱动程序。在我实际完成钥匙孔和uartlite的串行驱动程序时,我停滞了几天。Linux需要一个地方来保存根文件系统。有很多可能性。通常,嵌入式系统的规范是将嵌入式开发环境的根文件系统放在另一台机器上的NFS共享上。这需要一个工作正常的以太网驱动程序。此时,我对我的串行驱动程序没有很高的信心。此外,Pico的极简主义原则不包括将网络作为基本Linux的一部分,并且许多E12/Linux应用程序不需要它。
根文件系统可以在硬盘上(E12中没有现成的硬盘)或闪存中。E12使用非常简单的Pico文件系统,但该文件系统不适合作为根文件系统。另一种选择是将根文件系统放在RAM磁盘上。Linux提供了使用和填充RAM文件系统作为启动过程的中间步骤的能力。一个目标是将尽可能多的Linux启动代码从内核迁移到用户空间。多年来,Linux系统一直通过initrd启动,然后执行pivot_root以将根文件系统从initrd RAM磁盘切换到基于磁盘的根文件系统。使用initrd需要加载程序将压缩的Linux映像和初始RAM磁盘内容的单独映像复制到内存中,并为Linux提供指向初始RAM磁盘数据的指针。
Linux 2.6引入了一种新的变体——initramfs。initramfs和initrd之间的一个区别是,使用initramfs,初始根RAM磁盘文件系统的内容在构建期间被压缩到Linux映像中,因此只有一个文件——在我的情况下是一个ELF文件——需要加载。这意味着Pico监视器不需要更改。事实证明,这种initramfs方法非常简洁、简单且易于使用。使其工作起来很复杂且耗时,因为initramfs相当新。主要文档是LKML上的一系列帖子。要为Pico E12创建initramfs,我确定我需要在我的构建系统上创建一个目录,并用根文件系统的文件填充它。我使用menuconfig启用了initramfs选项,并告诉menuconfig在哪里找到代表我的根文件系统的目录。还有其他几种方法可以做到这一点,但这是最简单的。最初,我从我的PowerBook上的Gentoo Linux安装中解压缩了initramfs。当我错误地认为我的启动映像可能存在问题时,我最终切换到了交叉编译的BusyBox,因为二进制文件是为PowerBook而不是PPC405构建的。
在此之后,我遇到了下一个问题。Linux一直启动到执行/init,但在那里停止了。我编写了一个简单的ls版本,并将其包含在内核中,在exec'ing /init之前调用它。一切都很好。但是在exec'ing /init时,Linux变得聋哑。当马看起来像斑马时,调试可能会特别困难。我花了很多时间跟踪Linux exec过程,在许多情况下,它非常巧妙,做最少的工作,并通过页面错误加载进程。不幸的是,这使得跟踪正在发生的事情变得非常困难,并再次导致我得出(几乎)错误的结论,即我遇到了虚拟内存问题。我用PPC汇编语言编写了一个Linux版本的“Hello World”,没有外部库,并且能够将其作为/init执行。但是,我无法exec任何更复杂的东西。我最终找到并启用了系统调用跟踪,并且能够观察/init的执行过程。系统总是在虚拟内存操作过程中死机。当Linux在输出一些调试字符串的过程中变得哑口无言时,我最终遇到了失败案例——同样,总是在VM操作期间。我实际上可以通过插入额外的调试来更改故障点。我是海森堡不确定性原理的受害者——观察改变了被观察的行为。
我确信我的串行驱动程序有问题,尽管这没有道理,但是否则输出怎么会在字符串中间停止呢?所有关键线索都存在以解决这个问题,尽管其中一个线索作为机器检查问题的伪像被掩埋了。这是一个VM问题,在扭曲的意义上,它是一个串行驱动程序问题。我不会承认我花了多长时间才恍然大悟。让我们只说我重写了串行驱动程序几次,才看到虽然串行驱动程序请求并保存了内存映射硬件的虚拟地址——部分原因是使用8250串行驱动程序作为起点的错误诱导——但串行端口的虚拟地址随后被端口的物理地址覆盖了。因为在我努力调试机器检查问题的过程中,我将1-1物理-虚拟映射直接放入MMU转换后备缓冲器中,所以I/O继续工作,直到Linux VM系统覆盖了我的临时TLB条目。在认识到这一点之后,不到30分钟就完成了纠正,并且我能够将Linux启动到bash提示符。
没有什么比看到一台新机器到达shell提示符并知道我实现了它更令人兴奋的了。我已经完成了我的基本Pico E12 Linux移植。好吧,这不太正确——没有哪个移植是永远完成的。当我完成我的Pico E12移植时,我不知道有任何其他将Linux移植到Xilinx V4芯片组的移植。随后,Grant Likely为Xilinx ML403进行的Linux移植开始在Linux嵌入式PPC开发树中工作,并已被接受到发行版内核中。Pico E12与ML403不同,但它们彼此之间比我开始使用的旧ML300更相似。Grant的ML403移植反映了正在影响整个Linux PPC开发树的更改,因此我使我的Pico E12移植跟踪这些开发。
我一直依赖钥匙孔端口进行托管开发工作,因此,钥匙孔串行驱动程序逐渐变得更小,并且与Linux串行驱动程序的未来发展方向更加一致。我将不得不更新uartlite驱动程序以赶上。
我目前正在进行E12的Linux网络驱动程序的第三次迭代,而Pico正在进行底层网络硬件的第二次迭代。新的网络硬件是中断驱动的,需要PIC。
第二块Pico主板已经成熟,并且通过极小的更改,Pico E12移植已经演变为Pico E1X移植。
我正在努力使Linux内存技术设备(MTD)系统与Pico闪存一起工作。由于Pico硬件中的闪存可以由Linux和主机读取和写入,并且Pico最终计划使用应窗口化到Linux内存而不是完全映射的闪存设备大小,这使得情况变得复杂。
一旦Linux MTD工作完成,Pico希望为其简单文件系统PicoFS提供Linux(和Windows)文件系统驱动程序。
Pico正在考虑更改钥匙孔端口,以便在主机端(甚至可能在目标端),它足够且紧密地类似于8250兼容的UART,以仅使用操作系统的原生串行驱动程序。
后来,Pico为E12开发了一个名为Little Brother Board的子板,该子板允许在非托管环境中使用E12,并包括三个USB端口、一个LED和几个其他硬件组件。在一个应用中,E12/LBB组合被用作非常高性能的网络摄像头。
E12也可以作为网格托管在称为超级集群的总线板上。目前,该配置用于闪电般快速的代码破解,使用没有操作系统支持的FPGA硬件,但Linux HPC支持在愿望清单上。仅仅通过每18个月将时钟频率翻一番,就无法再实现更高的性能。集群是一个重要的替代方案;16个E12提供了巨大的马力,同时占用的空间小,功耗低。
自E12移植以来,Linux 2.6已经进行了多次迭代发布,偶尔,这些发布需要对移植进行更改。
我希望将我为Pico E1X移植和编写的驱动程序包含到Linux发行版内核中。在Linux嵌入式PPC邮件列表中,有人对此表示感兴趣。代码已移至git,以便更轻松地与新的Linux迭代合并,并生成用于提交给LKML的补丁文件。
“更大的游戏是‘弹球’。你赢了一局,你就可以再玩一局。你赢了这台机器,你就可以制造下一台。弹球才是最重要的。” ——Tracy Kidder 新机器的灵魂。我在弹球游戏中获胜了。
我在新硬件上成功启动并运行了 Linux,并且在其他硬件和其他嵌入式操作系统方面也出现了新的机会。E12 的板级启动非常困难。在 Kernel-Newbies 的某个地方,我读到给内核新手黑客的建议是先在邮件列表上潜伏几年,然后再尝试任何严肃的事情——我很庆幸我没有听从这个建议。我开始时并非完全是新手。我有很多经验,这让事情变得容易得多。这令人兴奋、充满传奇色彩且神奇。我可以称自己为 Linux 内核开发者——虽然可能不会在 Linus Torvalds、Andrew Morton 或 Alan Cox 面前大声宣扬。但是,这并不比许多其他软件任务更难——只是更有意义。
David Lynch 是一名软件顾问。编程就像艺术或音乐,他做编程是因为他热爱它。他一直在寻求新的和具有挑战性的软件项目,例如嵌入式板级启动——最好是 Linux/开源 (www.dlasys.net)。