此文章翻译自ThirtyThreeForty的Mastering Embedded Linux, Part 3: Buildroot

在本章中,我们正式开始进入实操阶段。我们将从源码编译Linux,并尝试在树莓派上启动。前面的文章中也提到了,本章将会是讨论和教程并重的一章。

本章将会介绍Buildroot,一个高度可定制的嵌入式Linux镜像构建工具。Buildroot很强大,也很容易上手使用。它提供了一系列的自动化脚本,使得镜像构建过程不再需要过多的手工参与,让你可以在编译的时候干其他的工作。

在这篇文章中,我们将:

  1. 下载Buildroot并使用6个命令启动构建
  2. 在它构建的时候,我们将讨论Buildroot做了哪些自动化工作,并且简要介绍构建的流程
  3. 最后,我们将尝试在树莓派上启动我们构建的镜像

这个过程应该是简单且有趣的1,让我们开始吧!

准备工作

在上一章的结尾,我们介绍了本章需要使用到的硬件。我们会使用树莓派作为目标硬件,FT2232作为串口转换器与树莓派通信。不过,如果你不想购买任何硬件的话,你也可以在虚拟机上尝试运行镜像。

不过,无论你使用了什么样的目标硬件,你都还需要满足以下需求:

  • 正在运行Linux的主机工作站。如果你不知道该选择什么Linux发行版,那就用Ubuntu。如果你当前没有运行Linux,那么可以安装一个虚拟机作为替代方案。这篇文章并不会告诉你如何在你的主机或虚拟机上安装Linux,所以你需要事先安装好这些东西。
  • 15GB大小的空闲磁盘空间。我们需要编译整个操作系统,虽然编译出来的产物很小,但是中间过程还是需要不少的磁盘空间。如果你正在使用虚拟机,那么要确保已经分配了足够多的磁盘空间给这台虚拟机。
  • 2~4小时的空闲时间。你需要这么多时间来阅读这篇文章,编译系统,以及测试结果。
  • 一定的Linux知识。不需要太多Linux知识,但如果能知道一些常用的Shell指令最好。

编译 Buildroot

编译 Buildroot 很简单,仅需4步:

  1. 安装主机工具
  2. 下载Buildroot
  3. 配置Buildroot
  4. 开始编译

这4步仅需6个指令就能完成,下面会详细介绍它们。

安装主机工具

编译的第一步是安装那些Buildroot没有自带的工具,包括:

  • Git:虽然Buildroot提供压缩包下载方式,但长远来说使用Git是一个更好的选择
  • 一个编译器:Buildroot需要一个初始编译器编译它自己的编译器
  • 一些杂七杂八的工具:Buildroot需要这些工具来下载和处理代码,你可以在Buildroot的文档上找到这些工具的列表。
  • screen:给FT2232用的一个串口控制台工具

可以使用下面的命令安装工具。(注意所有命令开头的~$是Shell自带的提示符,不要把这个提示符给复制进去,光复制后面的命令即可。)

~$ sudo apt install -y git build-essential wget cpio unzip rsync bc libncurses5-dev screen

这些工具为Buildroot提供了一个最小环境,Buildroot会使用这些工具构建它自己的工具并编译系统。

(上面这个命令是在Ubuntu下的命令,也是本教程中唯一一个指定发行版的命令。其他的所有命令都是和Buildroot而不是主机操作系统交互。)

下载Buildroot

Buildroot提供了 许多方式下载其源代码,但最方便的方法是使用Git。

使用Git克隆Buildroot:

~$ git clone git://git.buildroot.net/buildroot
Cloning into 'buildroot'...
remote: Enumerating objects: 356009, done.
remote: Counting objects: 100% (356009/356009), done.
remote: Compressing objects: 100% (116215/116215), done.
remote: Total 356009 (delta 248969), reused 342974 (delta 238353)
Receiving objects: 100% (356009/356009), 75.76 MiB | 1.36 MiB/s, done.
Resolving deltas: 100% (248969/248969), done.
~$ cd buildroot/

现在我们就在Buildroot目录中了。我们需要切换到一个特定的Buildroot版本来编译。在本教程中,我们将编译当前最新的2019.11.1版本。(译者:现在最新的是2020.02)

检出到2019.11.1标签:

~/buildroot$ git checkout 2019.11.1
Note: checking out '2019.11.1'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by performing another checkout.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -b with the checkout command again. Example:

  git checkout -b <new-branch-name>

HEAD is now at 57fbebac60 Update for 2019.11.1</new-branch-name>

非常简单对吧。Git在这里告诉我们当前不在一个分支上(“分离头部, detached HEAD”),我们可以忽略这个提示。

配置Buildroot

现在你需要为Buildroot选择一个适合你的硬件的配置。根据你的目标不同,输入的命令略有差异:

目标 命令
树莓派 Zero W make raspberrypi0w_defconfig
树莓派 Zero make raspberrypi0_defconfig
虚拟机 make qemu_x86_64_defconfig

make命令后面的参数是一个配置文件。你可以在configs目录下看到所有的配置文件。我们稍后将会详细解释这件事。

~/buildroot$ make raspberrypi0w_defconfig

Buildroot会打印出所有运行过的命令,所以看上去可能会有些乱。你暂时可以忽略上面所有的输出,只关注最底下的几行。出现下面这几行东西就说明你的配置已经生效了。

#
# configuration written to /home/georgev/buildroot/.config
#

编译

现在我们已经准备好编译整个系统了。Buildroot会帮我们做好剩下的工作,但编译需要一些时间。通常来说,整个编译过程需要大约2~3小时,编译时间和你的工作站运行速度以及网速有关。

准备好了吗?

让我们编译镜像吧!直接运行下面的命令即可2

~/buildroot$ make

输入这行命令后,你会看到屏幕上有一大堆文字输出,这说明Buildroot正在下载并编译系统。编译完毕后,buildroot会生成一个SD卡镜像供你烧写。

Buildroot是如何运行的

在编译的过程中,我会向你介绍Buildroot各个部分是怎么工作的。

编译流程图

这是Buildroot的编译流程图3

  1. Buildroot 编译工具链,包括了交叉编译器和用于构建系统工具(绿色部分)
  2. 各个软件的源代码(蓝色部分)从互联网上下载
  3. 使用Buildroot脚本(灰色部分)解压源代码,打上补丁,配置,编译并安装至目标输出目录,组成目标的根文件系统(root filesystem, rootfs)(紫色部分)
  4. 额外的文件,例如设备配置文件,也会复制到目标输出目录
  5. 最终,脚本将各个部分组装起来,形成根文件系统。

当然,以上过程也会有例外。某些交叉编译器是直接下载而不是从源代码编译的;有时候厂家提供给你的“板级支持包(Board Support Package, BSP)”已经为你编译好了所有的东西。流程图上的某些步骤会被省略,但大体框架还是一样的。

Buildroot目录树下面的这些目录是非常重要的,你的大多数时间都会花在和它们打交道上。

目录 用途
board/ 支持目标设备的文件和脚本
configs/ 预置的Buildroot编译配置文件,如raspberrypi0w_defconfig
package/ 软件包定义
output/host/ 在主机上运行的构建工具
output/target/ 目标输出目录,用于暂时存放目标二进制
output/images/ 最终文件系统镜像和固件镜像目录

所以,下一个问题是:make这一个神奇的命令是如何知道要构建所有东西的?要回答这个问题,首先我们需要谈论Buildroot软件包的结构。

Buildroot 软件包

Buildroot中的绝大多数东西都是软件包。在Buildroot中,编译脚本是按照“软件包”的形式分组的。

你可以在package目录下查看所有的软件包。Buildroot 2019.11提供了2289个软件包,甚至还提供了Nginx web 服务器和 Chocolate Doom 游戏引擎!你可以很容易的将这些开源的免费的软件放入你的自定义镜像中。

每个软件包都有着自己的配置选项、编译过程以及依赖项目。依赖项目规定了各个软件包的编译顺序,编译过程定义了下载和编译时用的命令,配置选项控制了软件包的编译配置。

软件包的配置选项由各个软件包的Config.in4文件定义,这是一个使用Kconfig5语言编写的配置文件。一个软件包至少包含了一个配置选项:是否编译这个软件包。如果你想要把这个软件包编译并放入镜像中,你就需要启用这个选项。当然还可以包括其他的编译控制选项,例如规定某个个额外功能是否需要编译进镜像中。禁用这些额外功能可以减少镜像的最终大小,这在存储空间有限的时候很有用。

理解了软件包的配置,也就不难理解整个构建配置了。

构建配置

将所有软件包的配置集合在一起,我们就得到了构建配置。使用预定义构建配置(defconfig)可以很方便的选择上所有相关的选项。

所以这就是make命令如何知道要构建什么的原理:所有的配置的选项都在我们一开始指定的raspberrypi0w_defconfig文件中提供了6。(GNU Make 会计算依赖树并以正确的顺序构建各个软件包)

选择一个defconfig会将里面的所有配置复制到当前配置文件中(.config文件)。menuconfig工具提供了一个友好的用户界面用于修改配置选项,你可以随意的使用这个工具修改当前配置文件。

需要注意的是.config文件是不会被Git所版本控制的。你需要使用make savedefconfig来将当前配置文件中的配置复制到一个defconfig文件中,而这个文件则会被版本控制。

启动新镜像

说了这么多,让我们来启动Linux吧。

编译过程快要结束时,你可以看到类似于这样的日志:

INFO: vfat(boot.vfat): adding file 'zImage' as 'zImage' ...
INFO: vfat(boot.vfat): cmd: MTOOLS_SKIP_CHECK=1 mcopy -bsp -i '/home/georgev/Code/buildroot-mel/output/images/boot.vfat' '/home/georgev/Code/buildroot-mel/output/images/zImage' '::' (stderr):
INFO: hdimage(sdcard.img): adding partition 'boot' (in MBR) from 'boot.vfat' ...
INFO: hdimage(sdcard.img): adding partition 'rootfs' (in MBR) from 'rootfs.ext4' ...
INFO: hdimage(sdcard.img): writing MBR

上面的日志告诉我们,为树莓派制作的SD卡镜像(sdcard.img)已经制作完毕了。这个镜像是根据位于output/target/的根文件系统构建的,你可以使用ls命令来查看结构7

~/buildroot$ ls -lh output/target/
total 64K
drwxr-xr-x 2 georgev georgev 4.0K Jan 13 22:01 bin
drwxr-xr-x 4 georgev georgev 4.0K Jan 13 20:48 dev
drwxr-xr-x 5 georgev georgev 4.0K Jan 13 22:01 etc
drwxr-xr-x 3 georgev georgev 4.0K Jan 13 22:01 lib
lrwxrwxrwx 1 georgev georgev    3 Jan 13 21:08 lib32 -> lib
lrwxrwxrwx 1 georgev georgev   11 Jan 13 21:23 linuxrc -> bin/busybox
drwxr-xr-x 2 georgev georgev 4.0K Jan 13 20:48 media
drwxr-xr-x 2 georgev georgev 4.0K Jan 13 20:48 mnt
drwxr-xr-x 2 georgev georgev 4.0K Jan 13 20:48 opt
drwxr-xr-x 2 georgev georgev 4.0K Jan 13 20:48 proc
drwxr-xr-x 2 georgev georgev 4.0K Jan 13 20:48 root
drwxr-xr-x 2 georgev georgev 4.0K Jan 13 20:48 run
drwxr-xr-x 2 georgev georgev 4.0K Jan 13 22:01 sbin
drwxr-xr-x 2 georgev georgev 4.0K Jan 13 20:48 sys
-rw-r--r-- 1 georgev georgev 1.4K Jan 13 21:08 THIS_IS_NOT_YOUR_ROOT_FILESYSTEM
drwxr-xr-x 2 georgev georgev 4.0K Jan 13 20:48 tmp
drwxr-xr-x 6 georgev georgev 4.0K Jan 13 22:01 usr
drwxr-xr-x 3 georgev georgev 4.0K Jan 13 20:48 var

我们也可以再次确认这个镜像位于output/images/目录下:

~/buildroot$ ls -lh output/images/
total 225M
-rw-r--r-- 1 georgev georgev  24K Jan 13 22:01 bcm2708-rpi-zero.dtb
-rw-r--r-- 1 georgev georgev  32M Jan 13 22:01 boot.vfat
-rw-r--r-- 1 georgev georgev 120M Jan 13 22:01 rootfs.ext2
lrwxrwxrwx 1 georgev georgev   11 Jan 13 22:01 rootfs.ext4 -> rootfs.ext2
drwxr-xr-x 3 georgev georgev 4.0K Jan 13 21:28 rpi-firmware
-rw-r--r-- 1 georgev georgev 153M Jan 13 22:01 sdcard.img
-rw-r--r-- 1 georgev georgev 4.8M Jan 13 22:01 zImage

看上去一切正常。现在让我们将这个镜像烧录到树莓派的SD卡中,并将树莓派连接到电脑上吧。

可选步骤:启动虚拟机

如果你正在使用树莓派,那么请跳过这一节。

如果你选择虚拟机作为目标,那么你可以使用这行命令8来启动虚拟机:

~/buildroot$ output/host/bin/qemu-system-x86_64 -M pc -kernel output/images/bzImage -drive file=output/images/rootfs.ext2,if=virtio,format=raw -append rootwait root=/dev/vda -net nic,model=virtio -net user

虚拟机窗口出现则说明启动成功了,接下来请跳过“启动镜像”这一节,那是为树莓派准备的。

额外说明:如何使用dmesg

dmesg是一个Linux小工具,用于显示内核日志。内核会打印出为接入设备分配的名称,所以在调试的时候是很有用的。

基础的用法很简单,只需运行:

$ dmesg -w

-w选项的意思是“watch”,即在显示已有的日志后,一直监视内核的新日志。你可以暂时忽略已有的日志,因为其过于冗长而且没有什么用。运行完这个命令后,你可以插入你的SD卡读卡器,然后你可以见到类似下面的输出:

[163513.147002] mmc0: new ultra high speed SDR50 SDHC card at address aaaa
[163513.174253] mmcblk0: mmc0:aaaa SP32G 29.7 GiB
[163513.189137]  mmcblk0: p1

在这个示例中,SD卡分配了一个名为mmcblk0设备,其含有一个简单的p1分区。所以,这个设备的完整路径是/dev/mmcblk0,分区路径是/dev/mmclbk0p1。根据读卡器类型的不同,这个设备名前面可能会有“sd”的前缀。

当你不想要继续监视日志的时候,你可以按下Ctrl+C来停止监视。

烧录SD卡

将你的SD卡使用读卡器连接到电脑,并使用dmesg获取到它的名称。使用dd命令可以将镜像复制到sd卡中。将下面命令中的/dev/mmcblkX替换为你的SD卡的真实路径。

~/buildroot$ sudo dd if=output/images/sdcard.img of=/dev/mmcblkX bs=1M status=progress

警告:

确保你的of(输出文件)是正确的。如果你错误的选择了你的工作站上的硬盘,dd命令会将你的硬盘数据完全删除。如果你不确定是否正确,请参考上一节再次确认一遍设备路径。

命令的参数意义如下:

参数 解释
if= Input File,输入文件,将从这个文件里面读取数据
of= Output File,输出文件,将数据写入到这个文件中
bs= Block Size,块大小,一次写入的数据大小
status= 显示一个进度条(有时候可能不起作用)

当这个命令运行完毕后,使用下面的命令确保内核将所有的缓存写入到SD卡中:

~/buildroot$ sync

这个命令运行完毕后,你就可以拔出你的SD卡并插入你的树莓派中了。先不着急启动你的树莓派,我们还有工作要做。(当然,启动了也不会有什么后果)

连接到树莓派的串口调试接口

我们现在需要将FT2232的UART口和树莓派的UART口连接到一起。这是树莓派的接口定义:

因为FT2232不仅是一个UART转换器,所以我们得知道其在UART模式下所用的具体IO。根据FT2232的数据手册,我们可以得到:

对于异步串口(Async Serial),ADBUS0是TXD,ADBUS1是RXD。

将树莓派的UART TXD与FT2232的UART RXD连接在一起,RXD同理。最终我们能得到类似这样的东西:

Raspberry

现在,将你的FT2232连接到你的工作站,使用dmesg找出FT2232的设备名称。名称通常可能是ttyUSB0ttyUSB1——FT2232有两个通道9

打开串口

运行 GNU Screen,指定设备为FT2232的设备,并且设置波特率,禁用流控(大多数Linux串口都使用115200的波特率并且没有硬件流控)。

~/buildroot$ sudo screen -fn /dev/ttyUSBX 115200

如果你想退出Screen,你需要先按下Ctrl+a,然后按一下\退出。

启动固件

接通树莓派的电源,此时串口控制台应该会开始打印启动信息:

[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 4.19.66 (georgev@desertvoice) (gcc version 8.3.0 (Buildroot 2019.11.1)) #1 Tue Jan 14 11:14:59 CST 2020
[    0.000000] CPU: ARMv6-compatible processor [410fb767] revision 7 (ARMv7), cr=00c5387d
[    0.000000] CPU: PIPT / VIPT nonaliasing data cache, VIPT nonaliasing instruction cache
[    0.000000] OF: fdt: Machine model: Raspberry Pi Zero Rev 1.3
[    0.000000] Memory policy: Data cache writeback
[    0.000000] cma: Reserved 8 MiB at 0x19000000
[    0.000000] random: get_random_bytes called from start_kernel+0x90/0x4a4 with crng_init=0
[    0.000000] Built 1 zonelists, mobility grouping on.  Total pages: 104545
[    0.000000] Kernel command line: coherent_pool=1M bcm2708_fb.fbwidth=720 bcm2708_fb.fbheight=480 bcm2708_fb.fbswap=1 smsc95xx.macaddr=B8:27:EB:6C:5F:E1 vc_mem.mem_base=0x1ec00000 vc_mem.mem_size=0x20000000  root=/dev/mmcblk0p2 rootwait console=tty1 console=ttyAMA0,115200
[    0.000000] Dentry cache hash table entries: 65536 (order: 6, 262144 bytes)
[    0.000000] Inode-cache hash table entries: 32768 (order: 5, 131072 bytes)
[    0.000000] Memory: 397928K/421888K available (6947K kernel code, 635K rwdata, 2080K rodata, 452K init, 796K bss, 15768K reserved, 8192K cma-reserved)

最终,它将会打印出登录提示符:

Welcome to Buildroot!
buildroot login:

你可以使用root作为用户名登录,这个账户没有密码。登录后可以尝试使用ls /usr/bin来查看可以运行什么程序。

如果你成功运行到了这步,那么恭喜你!你成功的将源代码编译成了可以启动的镜像。

总结

这是一篇关于Buildroot顶层的教程,Buildroot中还有很多东西在这篇教程中没有提到。但无论如何,这篇教程里面包含了很多信息。

在这篇教程中:

  • 你拥有了所有运行在目标设备上的代码,以及相关的编译工具链。

    此过程中的所有软件都是开源的,你使用make将他们编译出来。如果有一天某个东西损坏或你不想要了,你还可以简单的修改它们

  • 所有的工作都是自动化的。

    Buildroot使你可以在一个时间仅关注一件事情。你可以使用已有的defconfig,并在上面做自己的修改。

  • 输出根文件系统非常小。

    默认输出镜像仅有57MB,且大部分都是可以关闭的内核模块。Buildroot的“小体积镜像”的哲学使你可以在有限的空间中增加更多的功能,你甚至可以编译一个大小为4MB的镜像!

扩展阅读

以下的书籍是和本章内容密切相关的:

  • How Linux Works, 第二版10讲述了关于Linux的很多主题。其与桌面、服务器以及嵌入式Linux都有关。它涵盖了从基础的命令行到高级的X11和DBus系统的信息。如果你想知道这些东西具体是怎么工作的,这本书能够帮助到你。
  • Bootlin 的 Buildroot 培训课程,是非营利性嵌入式Linux开发组织Bootlin提供的。你可以付费参与这项课程,或者仅使用他们的教学材料。
  • Buildroot 用户手册,提供了更多如何使用Buildroot的信息。这里面详细的解释了构建系统的部分领域,以及教你如何编写一个新的软件包。当然,这份东西是使用手册而不是教程,所以看上去可能会有些冗长。建议在深入Buildroot之前详细阅读这份材料。

接下来……

下一章中,我们将会为我们刚刚编译好的系统添加一些新的功能。这会需要阅读一些文档,并用我之前演示过的menuconfig命令来自定义配置。

我们会修改配置,重新编译并测试新的修改(重新编译在Buildroot中仅需几分钟)。这就是制作固件最常用的步骤循环。

注释

  • 如果你遇到了问题:

    尝试谷歌你的错误信息,并在后面加上“Buildroot”的关键字。通常来说都会有人遇到过类似的问题,可以参考他们的解决方案来解决。嵌入式系统就是这样,充满着令人烦恼的问题。而耐心和较好的调试技能则是解决这些问题的关键。

  • 关于虚拟机的问题:

    如果你正在虚拟机中运行Buildroot,请确保你的FT2232和SD卡是直通到你的虚拟机内部的。同时,记得不要给虚拟机分配太少资源,不然编译时间会变得非常长。


    1. 有趣的定义因人而异。 ↩︎

    2. 如果你的工作站还需要运行其他东西,你可以使用 nice make命令,这样make指令就会以较低的优先级运行 ↩︎
    3. 虽然写的是Buildroot,但这一流程也适用于所有的嵌入式Linux发行版。 ↩︎
    4. 为主机编译的软件包的配置文件是Config.host.in ↩︎
    5. Kconfig原本是Linux内核开发者用于管理内核中广泛的配置项目而设计的。Buildroot使用的是类似的结构,所以也可以使用Kconfig。 ↩︎
    6. 这句话是不正确的。实际上,只有非默认的配置项目才会存储在defconfig里面。但因为默认配置已经存储在其他地方了,所以我们可以认为所有的配置都存在了defconfig内。 ↩︎
    7. Buildroot在output/target目录下添加了THIS_IS_NOT_YOUR_ROOT_FILESYSTEM文件,告诉你这并不是根文件系统。然而,这确实是根文件系统,只不过相对于真正的根文件系统还差几个步骤(如创建某些特殊文件)。 ↩︎
    8. 想知道这行奇怪的命令是怎么来的?看 boards/qemu/x86_64/readme.txt 文件就知道了。 ↩︎
    9. 如果你正在使用的是FT232(不是2232),那么这就只有一个USB通道。我们暂时不会使用第二个USB通道。 ↩︎
    10. 这个链接是一个带有推广的链接,这可以帮助我构建更好的嵌入式系统(译者:是原作者的) ↩︎

    0 条评论

    发表评论

    电子邮件地址不会被公开。 必填项已用*标注