Skip to content

Embedded Linux - Software Workflow

· 55 min

特别感谢

Developing on an MCU is simple: install the vendor’s IDE, create a new project, and start programming/debugging. There might be some .c/.h files to include from a library you’d like to use, and rarely, a precompiled lib you’ll have to link against.

在 MCU 上开发很简单: 安装供应商的 IDE, 创建一个新项目, 然后开始编程/调试. 你可能需要从想使用的库中包含一些 .c/.h 文件, 少数情况下, 你还需要链接一个预编译的库.

When building embedded Linux systems, we need to start by compiling all the off-the-shelf software we plan on running — the bootloader, kernel, and userspace libraries and applications. We’ll have to write and customize shell scripts and configuration files, and we’ll also often write applications from scratch. It’s really a totally different development process, so let’s talk about some prerequisites.

在构建嵌入式 Linux 系统时, 我们需要从编译所有要运行的软件开始, 包括 bootloader (引导加载程序)、kernel (内核) 以及用户空间的库和应用程序. 我们还需要编写并定制 shell 脚本和配置文件, 并且经常需要从头编写应用程序. 这实际上是一个完全不同的开发过程, 所以让我们来谈谈一些前置知识.

If you want to build a software image for a Linux system, you’ll need a Linux system. If you’re also the person designing the hardware, this is a bit of a catch-22 since most PCB designers work in Windows. While Windows Subsystem for Linux will run all the software you need to build an image for your board, WSL currently has no ability to pass through USB devices, so you won’t be able to use hardware debuggers (or even a USB microSD card reader) from within your Linux system. And since WSL2 is Hyper-V-based, once it’s enabled, you won’t be able to launch VMware, which uses its own hypervisor ((Though a beta versions of VMWare will address this)).

如果你想为 Linux 系统构建软件镜像, 你需要一个 Linux 系统. 如果你也是设计硬件的人, 这有点像是一个死胡同, 因为大多数 PCB 设计师都在 Windows 上工作. 虽然 WSL (Windows Subsystem for Linux) 可以运行构建板级镜像所需的所有软件, 但 WSL 目前无法透传 USB 设备, 因此你无法在 Linux 系统中使用硬件调试器 (甚至 USB microSD 卡读卡器也不行). 而且由于 WSL2 基于 Hyper-V, 一旦启用, 你将无法启动 VMware ((尽管 VMware 的测试版将解决这个问题)).

Consequently, I recommend users skip over all the newfangled tech until it matures a bit more, and instead just spin up an old-school VMWare virtual machine and install Linux on it. In VMWare you can pass through your MicroSD card reader, debug probe, and even the device itself (which usually has a USB bootloader).

因此, 我建议用户在新潮的技术成熟之前不要考虑它们, 用老版的 VMWare 虚拟机, 在它上面安装 Linux. 在 VMWare 中, 你可以透传你的 MicroSD 卡读卡器、调试探头, 甚至设备本身 (通常具有 USB 引导加载程序).

Building images is a computationally heavy and highly-parallel workload, so it benefits from large, high-wattage HEDT/server-grade multicore CPUs in your computer — make sure to pass as many cores through to your VM as possible. Compiling all the software for your target will also eat through storage quickly: I would allocate an absolute minimum of 200 GB if you anticipate juggling between a few large embedded Linux projects simultaneously.

构建镜像是瓦数一项计算密集型和高度并行的任务, 因此你的计算机需要配备大功率、高的 HEDT (high end desktop)/服务器级多核 CPU——确保让你的虚拟机尽可能多地利用多核. 编译针对目标的所有软件也会迅速消耗存储空间: 如果你预计同时处理几个大型嵌入式 Linux 项目, 我建议至少分配 200 GB 空间.

While your specific project will likely call for much more software than this, these are the five components that go into every modern embedded Linux system((Yes, there are alternatives to these components, but the further you move away from the embedded Linux canon, the more you’ll find yourself on your own island, scratching your head trying to get things to work.)):

尽管你的具体项目可能需要比这更多的软件, 但这些是每个现代嵌入式 Linux 系统都包含的五个组件 (是的, 这些组件有替代品, 但当你越偏离嵌入式 Linux 的经典配置时, 就越可能发现自己孤立无援, 需要费尽心思地试图让事情正常运转):

  • cross toolchain, usually GCC + glibc, which contains your compiler, binutils, and C library. This doesn’t actually go into your embedded Linux system, but rather is used to build the other components.
  • U-boot, a bootloader that initializes your DRAM, console, and boot media, and then loads the Linux kernel into RAM and starts executing it.
  • The Linux kernel itself, which manages memory, schedules processes, and interfaces with hardware and networks.
  • Busybox, a single executable that contains core userspace components (init, sh, etc)
  • root filesystem, which contains the aforementioned userspace components, along with any loadable kernel modules you compiled, shared libraries, and configuration files.

As you’re reading through this, don’t get overwhelmed: if your hardware is reasonably close to an existing reference design or evaluation kit, someone has already gone to the trouble of creating default configurations for you for all of these components, and you can simply find and modify them. As an embedded Linux developer doing BSP work, you’ll spend way more time reading other people’s code and modifying it than you will be writing new software from scratch.

当你阅读这段文本时, 不要感到不知所措: 如果你的硬件与现有的参考设计或评估套件相当接近, 那么有人已经为你所有这些组件创建了默认配置, 你只需找到并修改它们. 作为一名嵌入式 Linux 开发人员从事 BSP 工作, 你将花费更多的时间阅读修改他人的代码, 而不是从头开始编写新软件.

Cross Toolchain#

Just like with microcontroller development, when working on embedded Linux projects, you’ll write and compile the software on your computer, then remotely test it on your target. When programming microcontrollers, you’d probably just use your vendor’s IDE, which comes with a cross toolchain — a toolchain designed to build software for one CPU architecture on a system running a different architecture. As an example, when programming an ATTiny1616, you’d use a version of GCC built to run on your x64 computer but designed to emit AVR code. With embedded Linux development, you’ll need a cross toolchain here, too (unless you’re one of the rare types coding on an ARM-based laptop or building an x64-powered embedded system).

与微控制器开发类似, 在处理嵌入式 Linux 项目时, 你将在计算机上编写和编译软件, 然后在远程的目标设备上测试它. 在编程微控制器时, 你可能只会使用供应商的 IDE, 该 IDE 附带交叉编译工具链——它是为了运行在不同架构的系统上而构建的针对单一 CPU 架构的编译工具链. 以编程 ATTiny1616 为例, 你会使用在 x64 计算机上运行但为生成 AVR 代码而设计的 GCC 版本. 在嵌入式 Linux 开发中, 你也需要这样的交叉工具链 (除非你是那些罕见的在基于 ARM 的笔记本电脑上编程或构建由 x64 驱动的嵌入式系统的人).

When configuring your toolchain, there are two lightweight C libraries to consider — musl libc and uClibc-ng — which implement a subset of features of the full glibc, while being 1/5th the size. Most software compiles fine against them, so they’re a great choice when you don’t need the full libc features. Between the two of them, uClibc is the older project that tries to act more like glibc, while musl is a fresh rewrite that offers some pretty impressive stats, but is less compatible.

当配置你的工具链时, 可以考虑这两个轻量级的 C 库——musl libc 和 uClibc-ng——它们实现了完整 glibc 功能的一个子集, 同时体积仅为 1/5. 大多数软件与它们编译良好, 因此当你不需要完整的 libc 功能时, 它们是一个很好的选择. 在这两者之间, uClibc 是一个较老的项目, 试图贴近 glibc, musl 则是一个全新的重写, 它在数据上令人印象深刻, 但兼容性较差.

U-Boot#

Unfortunately, our CPU’s boot ROM can’t directly load our kernel. Linux has to be invoked in a specific way to obtain boot arguments and a pointer to the device tree and initrd, and it also expects that main memory has already been initialized. Boot ROMs also don’t know how to initialize main memory, so we would have nowhere to store Linux. Also, boot ROMs tend to just load a few KB from flash at the most — not enough to house an entire kernel. So, we need a small program that the boot ROM can load that will initialize our main memory and then load the entire (usually-multi-megabyte) Linux kernel and then execute it.

不幸的是, 我们的 CPU 引导 ROM 无法直接加载我们的内核. Linux 必须以特定方式调用以获取引导参数、设备树和 initrd, 并且它还期望主内存已经初始化. 引导 ROM 也不知道如何初始化主内存, 所以我们无法存储 Linux. 此外, 引导 ROM 通常最多只从闪存中加载几个 KB——这不足以容纳整个内核. 因此, 我们需要一个引导 ROM 可以加载的小程序, 该程序将初始化我们的主内存, 然后加载整个 (通常是几兆字节) Linux 内核, 然后执行它.

The most popular bootloader for embedded systems, Das U-Boot, does all of that — but adds a ton of extra features. It has a fully interactive shell, scripting support, and USB/network booting.

最流行的嵌入式系统引导程序 Das U-Boot 可以完成所有这些任务, 但还添加了大量额外功能. 它具有完全交互式的 shell、脚本支持和 USB/网络引导.

If you’re using a tiny SPI flash chip for booting, you’ll probably store your kernel, device tree, and initrd / root filesystem at different offsets in raw flash — which U-Boot will gladly load into RAM and execute for you. But since it also has full filesystem support, so you could store your kernel and device tree as normal files on a partition of an SD card, eMMC device, or on a USB flash drive.

如果你使用一个小型的 SPI 闪存芯片作为引导存储介质, 你可能会将内核、设备树以及 initrd 或根文件系统存储在原始闪存的不同偏移位置——而 U-Boot 就会将它们加载到 RAM 中并执行. 由于 U-Boot 还具有完整的文件系统支持, 你也可以将内核和设备树作为普通文件存储在 SD 卡、eMMC 设备或 USB 闪存驱动器的分区中.

U-Boot has to know a lot of technical details about your system. There’s a dedicated board.c port for each supported platform that initializes clocks, DRAM, and relevant memory peripherals, along with initializing any important peripherals, like your UART console or a PMIC that might need to be configured properly before bringing the CPU up to full speed. Newer board ports often store at least some of this configuration information inside a Device Tree, which we’ll talk about later. Some of the DRAM configuration data is often autodetected, allowing you to change DRAM size and layout without altering the U-Boot port’s code for your processor ((If you have a DRAM layout on the margins of working, or you’re using a memory chip with very different timings than the one the port was built for, you may have to tune these values)). You configure what you want U-Boot to do by writing a script that tells it which device to initialize, which file/address to load into which memory address, and what boot arguments to pass along to Linux. While these can be hard-coded, you’ll often store these names and addresses as environmental variables (the boot script itself can be stored as a bootcmd environmental variable). So a large part of getting U-Boot working on a new board is working out the environment.

U-Boot 需要了解系统的大量技术细节. 每个支持的平台都会有一个专门的 board.c 文件, 用于初始化时钟、DRAM 和相关的存储外设, 同时初始化所有重要的外设, 例如 UART 控制台, CPU 提速到全速运行之前需要正确配置的 PMIC. 较新的板卡支持经常将部分配置数据存储在设备树中 (稍后会详细介绍设备树). 部分 DRAM 的配置数据通常可以自动检测, 这样可以在不修改处理器 U-Boot 端口代码的情况下更改 DRAM 的大小和布局. (如果你的 DRAM 布局处于可用的边界, 或者你使用的内存芯片的时序与端口最初支持的芯片有很大不同, 你可能需要调整这些值.) 你可以通过编写脚本来配置 U-Boot 的行为, 该脚本指定需要初始化的设备、要加载的文件/地址及其内存位置, 以及需要传递给 Linux 的启动参数. 虽然这些信息可以硬编码, 但通常会将这些名称和地址存储为环境变量 (启动脚本本身可以存储为一个 bootcmd 环境变量). 因此, 使 U-Boot 在新板卡上运行的一个重要部分就是配置其环境 (environment).

Linux Kernel#

Here’s the headline act. Once U-Boot turns over the program counter to Linux, the kernel initializes itself, loads its own set of device drivers((Linux does not call into U-Boot drivers the way that an old PC operating system like DOS makes calls into BIOS functions.)) and other kernel modules, and calls your init program.

重点来了. 一旦 U-Boot 将程序计数器 (program counter, PC) 交给 Linux, 内核就会开始初始化自身, 加载其一套设备驱动程序 ((Linux 并不会像例如 DOS老式 PC 操作系统调用 BIOS 那样调用 U-Boot 驱动程序)), 以及其他内核模块, 随后调用你的 init 程序.

To get your board working, the necessary kernel hacking will usually be limited to enabling filesystems, network features, and device drivers — but there are more advanced options to control and tune the underlying functionality of the kernel.

为了让你的板子正常工作, 所需的内核修改通常仅限于启用文件系统、网络功能和设备驱动程序——但还有更多高级选项可以用来控制和调整内核的底层功能.

Turning drivers on and off is easy, but actually configuring these drivers is where new developers get hung up. One big difference between embedded Linux and desktop Linux is that embedded Linux systems have to manually pass the hardware configuration information to Linux through a Device Tree file or platform data C code, since we don’t have EFI or ACPI or any of that desktop stuff that lets Linux auto-discover our hardware.

开启或关闭驱动很简单, 但真正让新开发者头疼的是如何配置这些驱动. 嵌入式 Linux 和桌面 Linux 的一个重要区别是, 嵌入式系统必须通过设备树文件或平台数据 C 代码手动将硬件配置信息传递给 Linux. 因为嵌入式系统没有像 EFI 或 ACPI 这样的功能来让 Linux 自动检测硬件.

We need to tell Linux the addresses and configurations for all of our CPU’s fancy on-chip peripherals, and which kernel modules to load for each of them. You may think that’s part of the Linux port for our CPU, but in Linux’s eyes, even peripherals that are literally inside our processor — like LCD controllers, SPI interfaces, or ADCs — have nothing to do with the CPU, so they’re handled totally separately as device drivers stored in separate kernel modules.

我们需要告诉 Linux 我们所有 CPU 的复杂片上外设的地址和配置, 以及为每个外设加载哪个内核模块. 你可能认为这是我们的 CPU 对 Linux 的移植部分, 但在 Linux 看来, 即使是实际上位于我们处理器内部的外设——比如 LCD 控制器、SPI 接口或 ADC——都与 CPU 无关, 因此它们需要被当作完全独立的存储在单独内核模块中的设备驱动程序.

And then there’s all the off-chip peripherals on our PCB. Sensors, displays, and basically all other non-USB devices need to be manually instantiated and configured. This is how we tell Linux that there’s an MPU6050 IMU attached to I2C0 with an address of 0x68, or an OV5640 image sensor attached to a MIPI D-PHY. Many device drivers have additional configuration information, like a prescalar factor, update rate, or interrupt pin use.

然后就轮到我们 PCB 上的所有片外外设了. 传感器、显示屏以及基本上所有非 USB 设备都需要手动实例化和配置. 这就是我们告诉 Linux 有一个 MPU6050 IMU 连接到 I2C0, 地址为 0x68, 或者一个 OV5640 图像传感器连接到 MIPI D-PHY 的方式. 许多设备驱动程序需要有额外的配置信息, 如预分频因数、更新速率或中断引脚使用.

The old way of doing this was manually adding C structs to a platform_data C file for the board, but the modern way is with a Device Tree, which is a configuration file that describes every piece of hardware on the board in a weird quasi-C/JSONish syntax. Each logical piece of hardware is represented as a node that is nested under its parent bus/device; its node is adorned with any configuration parameters needed by the driver.

旧的方法是手动将 C 结构体添加到板级的平台数据 C 代码文件中, 但现代的方法是使用设备树, 这是一个配置文件, 以奇怪的准 C/类 JSON 语法描述了板上每一块硬件. 每个逻辑硬件部件都表示为一个节点, 该节点嵌套在其父总线/设备下; 其节点上写着驱动程序所需的任何配置参数.

A DTS file is not compiled into the kernel, but rather, into a separate .dtb binary blob file that you have to deal with (save to your flash memory, configure u-boot to load, etc)((OK, I lied. You can actually append the DTB to the kernel so U-Boot doesn’t need to know about it. I see this done a lot with simple systems that boot from raw Flash devices.)). I think beginners have a reason to be frustrated at this system, since there’s basically two separate places you have to think about device drivers: Kconfig and your DTS file, and if these get out of sync, it can be frustrating to diagnose, since you won’t get a compilation error if your device tree contains nodes that there are no drivers for, or if your kernel is built with a driver that isn’t actually referenced for in the DTS file, or if you misspell a property or something (since all bindings are resolved at runtime).

DTS 文件并不会被直接编译到内核中, 而是被编译成一个独立的 .dtb 二进制文件, 你需要单独处理它 (比如存储到 Flash 内存中, 配置 U-Boot 加载它, 等等). ((好吧, 我撒谎了. 你实际上可以将 DTB 附加到内核中, 这样 U-Boot 就不需要知道它了. 这种方式在从原始 Flash 设备启动的简单系统中很常见.) 我认为初学者可能对这种系统感到沮丧, 因为关于设备驱动的配置基本上需要关注两个地方: Kconfig 和 DTS 文件. 如果这两者不同步, 诊断起来会很让人头疼, 因为即使设备树包含的节点没有对应的驱动程序, 或者内核启用了一个驱动但 DTS 文件没有引用它, 或者拼错了某个属性, 你也不会收到任何编译错误 (因为所有绑定都会在运行时解析).

BusyBox#

Once Linux has finished initializing, it runs init. This is the first userspace program invoked on start-up. Our init program will likely want to run some shell scripts, so it’d be nice to have a sh we can invoke. Those scripts might touch or echo or cat things. It looks like we’re going to need to put a lot of userspace software on our root filesystem just to get things to boot — now imagine we want to actually login (getty), list a directory (ls), configure a network (ifconfig), or edit a text file (viemacsnanovim, flamewars ensue).

一旦 Linux 完成初始化, 它将运行 init. 这是启动时调用的第一个用户空间程序, 我们的 init 程序可能想要运行一些 shell 脚本, 所以有一个 sh 可以调用会很好. 这些脚本可能 touch 或 echo 或 cat 某些内容。看起来为了启动我们可能需要在我们的根文件系统上放置大量的用户空间软件——现在想象一下我们想要登入 (getty), 列出目录 (ls), 配置网络 (ifconfig), 或编辑文本文件 (vi, emacsnanovim, 编辑器圣战来了).

Rather than compiling all of these separately, BusyBox collects small, light-weight versions of these programs (plus hundreds more) into a single source tree that we can compile and link into a single binary executable. We then create symbolic links to BusyBox named after all these separate tools, then when we call them on the command line to start up, BusyBox determines how it was invoked and runs the appropriate command. Genius!

我们并不需要单独编译这些程序, BusyBox 将这些程序 (以及数百个其他程序) 的小型轻量版本收集到一个软件源中, 我们可以将其编译并链接成一个单一的二进制可执行文件. 然后, 我们为所有这些单独的工具创建以 BusyBox 命名的符号链接, 当我们从命令行调用它们以启动时, BusyBox 确定其被如何调用并运行相应的命令. 天才!

BusyBox configuration is obvious and uses the same Kconfig-based system that Linux and U-Boot use. You simply tell it which packages (and options) you wish to build the binary image with. There’s not much else to say — though a minor “gotcha” for new users is that the lightweight versions of these tools often have fewer features and don’t always support the same syntax/arguments.

BusyBox 很容易配置, 并且使用与 Linux 和 U-Boot 相同的基于 Kconfig 的系统. 你只需告诉它你希望用哪些软件包 (和选项) 构建二进制映像就行了. 没有太多其他要说的事情——不过对于新用户来说, 一个小的 “陷阱” 是这些工具的轻量级版本通常功能较少, 并且不一定支持相同的语法/参数.

Root Filesystems#

Linux requires a root filesystem; it needs to know where the root filesystem is and what filesystem format it uses, and this parameter is part of its boot arguments.

Linux 需要根文件系统; 它需要知道根文件系统的位置以及它使用的文件系统格式, 这个参数是其引导参数的一部分.

Many simple devices don’t need to persist data across reboot cycles, so they can just copy the entire rootfs into RAM before booting (this is called initrd). But what if you want to write data back to your root filesystem? Other than MMC, all embedded flash memory is unmanaged — it is up to the host to work around bad blocks that develop over time from repeated write/erase cycles. Most normal filesystems are not optimized for this workload, so there are specialized filesystems that target flash memory; the three most popular are JFFS2, YAFFS2, and UBIFS. These filesystems have vastly different performance envelopes, but for what it’s worth, I generally see UBIFS deployed more on higher-end devices and YAFFS2 and JFFS2 deployed on smaller systems.

许多简单设备不需要在重启后保存之前的数据, 因此它们可以在启动之前将整个根文件系统复制到 RAM 中 (这被称为 initrd). 但如果你需要将数据写回到根文件系统怎么办? 除了 MMC 以外, 所有嵌入式闪存都是无管理的——主机需要自行处理由于反复写入/擦除周期而随时间产生的坏块. 大多数普通文件系统并未针对这种工作负载进行优化, 因此存在一些专门针对闪存的文件系统; 三种最流行的文件系统是 JFFS2、YAFFS2 和 UBIFS. 这些文件系统的性能差异巨大, 不过一般来说, 在高端设备上部署 UBIFS 更多, 而 YAFFS2 和 JFFS2 则更多地用于小型系统.

MMC devices have a built-in flash memory controller that abstracts away the details of the underlying flash memory and handles bad blocks for you. These managed flash devices are much simpler to use in designs since they use traditional partition tables and filesystems — they can be used just like the hard drives and SSDs in your PC.

MMC 设备内置闪存控制器, 可抽象底层闪存的细节并为你处理坏块. 这些管理型闪存设备在设计上使用起来更简单, 因为它们使用传统的分区表和文件系统——它们可以像你 PC 中的硬盘和固态硬盘一样使用.

Yocto & Buildroot#

If the preceding section made you dizzy, don’t worry: there’s really no reason to hand-configure and hand-compile all of that stuff individually. Instead, everyone uses build systems — the two big ones being Yocto and Buildroot — to automatically fetch and compile a full toolchain, U-Boot, Linux kernel, BusyBox, plus thousands of other packages you may wish, and install everything into a target filesystem ready to deploy to your hardware.

如果上一节让你感到头晕, 别担心: 实际上完全没有必要逐个手动配置和编译所有这些内容; 相反, 每个人都使用构建系统——其中两个最流行的系统是 Yocto 和 Buildroot——来自动获取和编译完整的工具链、U-Boot、Linux 内核、BusyBox, 以及你可能需要的数千个其他软件包, 并将所有内容安装到目标文件系统中, 以便部署到你的硬件上.

Even more importantly, these build systems contain default configurations for the vendor- and community-developed dev boards that we use to test out these CPUs and base our hardware from. These default configurations are a real life-saver.

更为重要的是, 这些构建系统包含了供应商和社区开发的开发板的默认配置, 而我们使用这些开发板来测试这些 CPU 并以此为基础设计硬件. 这些默认配置真的是救命稻草.

Yes, on their own, both U-Boot and Linux have defconfigs that do the heavy lifting: For example, by using a U-Boot defconfig, someone has already done the work for you in configuring U-Boot to initialize a specific boot media and boot off it (including setting up the SPL code, activating the activating the appropriate peripherals, and writing a reasonable U-Boot environment and boot script).

的确 U-Boot 和 Linux 自身都有 defconfig (默认配置文件), 可以完成大部分的工作: 例如, 使用 U-Boot 的 defconfig, 别人已经为你做了配置工作, 配置了 U-Boot 来初始化特定的启动媒介并从中启动 (包括设置 SPL 代码、激活适当的外设, 以及编写一个合理的 U-Boot 环境和启动脚本).

But the build system default configurations go a step further and integrate all these pieces together. For example, assume you want your system to boot off a MicroSD card, with U-Boot written directly at the beginning of the card, followed by a FAT32 partition containing your kernel and device tree, and an ext4 root filesystem partition. U-Boot’s defconfig will spit out the appropriate bin file to write to the SD card, and Linux’s defconfig will spit out the appropriate vmlinuz file, but it’s the build system itself that will create a MicroSD image, write U-Boot to it, create the partition scheme, format the filesystems, and copy the appropriate files to them. Out will pop an “image.sdcard” file that you can write to a MicroSD card.

但是构建系统的默认配置则可以更进一步, 将这些组件全部整合在一起. 例如, 假设你希望你的系统从 MicroSD 卡启动, U-Boot 直接写入卡的开始处, 随后是包含你的内核和设备树的 FAT32 分区, 以及 ext4 根文件系统分区. U-Boot 的 defconfig 将输出适当的 bin 文件写入 SD 卡, Linux 的 defconfig 将输出适当的 vmlinuz 文件, 但创建 MicroSD 镜像、将 U-Boot 写入其中、创建分区方案、格式化文件系统以及将适当的文件复制到其中的是构建系统本身. 将生成一个“image.sdcard”文件, 你可以将其写入 MicroSD 卡.

Almost every commercially-available dev board has at least unofficial support in either or both Buildroot or Yocto, so you can build a functioning image with usually one or two commands.

几乎每款商业可用的开发板都至少在 Buildroot 或 Yocto 中提供非官方支持, 因此你通常只需一个或两个命令即可构建一个可用的镜像.

These two build environments are absolutely, positively, diametrically opposed to each other in spirit, implementation, features, origin story, and industry support. Seriously, I have never found two software projects that do the same thing in such totally different ways. Let’s dive in.

这两个构建环境在精神、实现、功能、起源故事和行业支持方面完全、彻底地相互对立. 说实话, 我从未找到过两个以如此截然不同的方式做同样事情的软件项目. 让我们深入了解一下.

Buildroot#

Buildroot started as a bunch of Makefiles strung together to test uClibc against a pile of different commonly-used applications to help squash bugs in the library. Today, the infrastructure is the same, but it’s evolved to be the easiest way to build embedded Linux images.

Buildroot 最初是一系列 Makefile 组合在一起, 用于测试 uClibc 与众多常用应用程序, 以帮助修复库中的错误. 如今, 基础设施还是相同的, 但它已经发展成为构建嵌入式 Linux 镜像最简单的方法.

By using the same Kconfig system used in Linux, U-Boot, and BusyBox, you configure everything — the target architecture, the toolchain, Linux, U-Boot, target packages, and overall system configuration — by simply running make menuconfig. It ships with tons of canned defconfigs that let you get a working image for your dev board by loading that config and running make. For example, make raspberrypi3_defconfig && make will spit out an SD card image you can use to boot your Pi off of.

通过使用 Linux、U-Boot 和 BusyBox 中都有的 Kconfig 系统, 你可以通过简单地运行 make menuconfig 来配置一切, 包括目标架构、工具链、Linux、U-Boot、目标包以及整体系统配置. 它附带大量预定义的配置文件, 通过加载该配置并运行 make, 你可以为你的开发板获取一个可用的镜像. 例如, make raspberrypi3_defconfig && make 将生成一个 SD 卡镜像, 你可以使用它来从树莓派启动.

Buildroot can also pass you off to the respective Kconfigs for Linux, U-Boot, or BusyBox — for example, running make linux-menuconfig will invoke the Linux menuconfig editor from within the Buildroot directory. I think beginners will struggle to know what is a Buildroot option and what is a Linux kernel or U-Boot option, so be sure to check in different places.

Buildroot 还可以将你引导到相应的 Linux、U-Boot 或 BusyBox 的 Kconfigs — 例如, 运行 make linux-menuconfig 将在 Buildroot 目录内调用 Linux menuconfig 编辑器. 我认为初学者可能会难以区分 Buildroot 选项和 Linux 内核或 U-Boot 选项, 所以请务必在不同地方进行检查.

Buildroot is distributed as a single source tree, licensed as GPL v2. To properly add your own hardware, you’d add a defconfig file and board folder with the relevant bits in it (these can vary quite a bit, but often include U-Boot scripts, maybe some patches, or sometimes nothing at all). While they admit it is not strictly necessary, Buildroot’s documentation notes “the general view of the Buildroot developers is that you should release the Buildroot source code along with the source code of other packages when releasing a product that contains GPL-licensed software.” I know that many products (3D printers, smart thermostats, test equipment) use Buildroot, yet none of these are found in the officially supported configurations, so I can’t imagine people generally follow through with the above sentiment; the only defconfigs I see are for development boards.

Buildroot 以单个源代码树的形式分发, 许可协议为 GPL v2. 要正确添加自己的硬件, 你需要添加一个 defconfig 文件和包含相关标志位的板文件夹 (不同的板之间可能差异很大, 但通常包括 U-Boot 脚本、可能的一些补丁或有时什么都没有). 虽然 Buildroot 的文档说并非绝对必要, 但它也指出: “Buildroot 开发者的普遍观点是, 在发布包含 GPL 许可软件的产品时, 你应该在发布其他软件包的源代码与 Buildroot 源代码一起发布.” 我知道许多产品 (3D 打印机、智能恒温器、测试设备) 使用 Buildroot, 但官方支持的配置中都没有这些, 所以我不认为人们会普遍遵循上述观点; 我看到的仅有的 defconfig 都是针对开发板的.

And, honestly, for run-and-gun projects, you probably won’t even bother creating an official board or defconfig — you’ll just hack at the existing ones. We can do this because Buildroot is crafty in lots of good ways designed to make it easy to make stuff work. For starters, most of the relevant settings are part of the defconfig file that can easily be modified and saved — for very simple projects, you won’t have to make further modifications. Think about toggling on a device driver: in Buildroot, you can invoke Linux’s menuconfig, modify things, save that config back to disk, and update your Buildroot config file to use your local Linux config, rather the one in the source tree. Buildroot knows how to pass out-of-tree DTS files to the compiler, so you can create a fresh DTS file for your board without even having to put it in your kernel source tree or create a machine or anything. And if you do need to modify the kernel source, you can hardwire the build process to bypass the specified kernel and use an on-disk one (which is great when doing active development).

老实说, 对于一些快速开发的项目, 你可能根本不会去创建一个正式的板卡或 defconfig, 你会直接修改现有的配置. 之所以能这么做, 是因为 Buildroot 在很多方面都非常巧妙地设计了让事情变得更容易的功能. 首先, 大多数相关设置都包含在可以轻松修改并保存的 defconfig 文件中——对于非常简单的项目, 你甚至不需要进行进一步的修改. 比如说, 启用一个设备驱动程序: 在 Buildroot 中, 你可以调用 Linux 的 menuconfig, 修改配置, 保存到磁盘, 并更新你的 Buildroot 配置文件, 使用本地的 Linux 配置, 而不是源代码树中的配置. Buildroot 知道如何将树外的 DTS 文件传递给编译器, 因此你可以为你的板卡创建一个新的 DTS 文件, 而无需将其放入内核源代码树中或创建机器等. 如果确实需要修改内核源代码, 你可以自定义构建过程, 绕过指定的内核, 使用磁盘上的内核 (这在进行主动开发时非常有用).

The chink in the armor is that Buildroot is brain-dead at incremental builds. For example, if you load your defconfig, make, and then add a package, you can probably just run make again and everything will work. But if you change a package option, running make won’t automatically pick that up, and if there are other packages that need to be rebuilt as a result of that upstream dependency, Buildroot won’t rebuild those either. You can use the make [package]-rebuild target, but you have to understand the dependency graph connecting your different packages. Half the time, you’ll probably just give up and do make clean && make ((Just remember to save your Linux, U-Boot, and BusyBox configuration modifications first, since they’ll get wiped out.)) and end up rebuilding everything from scratch, which, even with the compiler cache enabled, takes forever. Honestly, Buildroot is the principal reason that I upgraded to a Threadripper 3970X during this project.

Buildroot 的一个弱点是它在增量构建方面非常差劲. 例如, 如果你加载了 defconfig, 执行 make, 然后添加一个软件包, 你可能只需再次运行 make, 一切就能正常工作. 但是, 如果你更改了一个软件包选项, 运行 make 并不会自动检测到这个更改, 并且如果有其他依赖这个更改的上游包需要重新构建, Buildroot 也不会重新构建这些包. 你可以使用 make [package]-rebuild 目标, 但你必须了解连接不同软件包的依赖图. 很多时候, 你可能会选择放弃, 直接运行 make clean && make (记得先 保存 你的 Linux、U-Boot 和 BusyBox 配置修改, 因为这些会被清空) 然后从头开始重新构建, 尽管启用了编译器缓存, 但这个过程依然需要很长时间. 老实说, Buildroot 是我在这个项目中升级到 Threadripper 3970X 的主要原因.

Yocto#

Yocto is totally the opposite. Buildroot was created as a scrappy project by the BusyBox/uClibc folks. Yocto is a giant industry-sponsored project with tons of different moving parts. You will see this build system referred to as Yocto, OpenEmbedded, and Poky, and I did some reading before publishing this article because I never really understood the relationship. I think the first is the overall head project, the second is the set of base packages, and the third is the… nope, I still don’t know. Someone complain in the comments and clarify, please.

Yocto 则是完全相反. Buildroot 是由 BusyBox/uClibc 团队创建的一个临时项目. Yocto 是一个由众多行业赞助的大项目, 包含许多不同的组成部分. 你会看到这个构建系统被称为 Yocto、OpenEmbedded 和 Poky, 我在发表这篇文章之前做了一些阅读, 因为我从未真正理解它们之间的关系. 我认为第一个是整体的主项目, 第二个是一组基础包, 第三个是……好吧, 我还是不清楚. 希望有人能在评论区中解释一下, 谢谢.

Here’s what I do know: Yocto uses a Python-based build system (BitBake) that parses “recipe” files to execute tasks. Recipes can inherit from other recipes, overriding or appending tasks, variables, etc. There’s a separate “Machine” configuration system that’s closely related. Recipes are grouped into categories and layers.

以下是我所确切知道的: Yocto 使用基于 Python 的构建系统 (BitBake), 解析 “recipe” 文件以执行任务. recipe 可以继承其他 recipe, 覆盖或追加任务、变量等. 还有一个与之紧密相关的独立 “Machine” 配置系统. recipe 被分组到类别和层中.

There are many layers in the official Yocto repos. Layers can be licensed and distributed separately, so many companies maintain their own “Yocto layers” (e.g., meta-atmel), and the big players actually maintain their own distribution that they build with Yocto. TI’s ProcessorSDK is built using their Arago Project infrastructure, which is built on top of Yocto. The same goes for ST’s OpenSTLinux Distribution. Even though Yocto distributors make heavy use of Google’s repo tool, getting a set of all the layers necessary to build an image can be tedious, and it’s not uncommon for me to run into strange bugs that occur when different vendors’ layers collide.

官方 Yocto 仓库中有很多层. 层可以被许可和单独分发, 因此许多公司维护自己的 “Yocto 层” (例如, meta-atmel), 而大公司实际上维护他们使用 Yocto 构建的自己的发行版. TI 的 ProcessorSDK 是使用他们的 Arago Project 基础设施构建的, 该基础设施建立在 Yocto 之上. ST 的 OpenSTLinux 发行版也是如此. 尽管 Yocto 发行商大量使用 Google 的 repo 工具, 但获取构建映像所需的所有层可能很繁琐. 而且我经常遇到不同供应商的层冲突时出现的奇怪错误.

While Buildroot uses Kconfig (allowing you to use menuconfig), Yocto uses config files spread out all over the place: you definitely need a text editor with a built-in file browser, and since everything is configuration-file-based, instead of a GUI like menuconfig, you’ll need to have constant documentation up on your screen to understand the parameter names and values. It’s an extremely steep learning curve.

Buildroot 使用 Kconfig (允许你使用 menuconfig), 而 Yocto 则使用散布各处的配置文件: 你绝对需要一个内置文件浏览器的文本编辑器, 由于一切都是基于配置文件, 所以你需要将常备文档保持在屏幕上, 以便理解参数名称和值. 这是一条极其陡峭的学习曲线.

However, if you just want to build an image for an existing board, things couldn’t be easier: there’s a single environmental variable, MACHINE, that you must set to match your target. Then, you BitBake the name of the image you want to build (e.g., bitbake core-image-minimal) and you’re off to the races.

然而, 如果你只想为现有板子构建一个镜像, 事情就变得很简单了: 只需设置一个环境变量 MACHINE, 使其与你的目标匹配. 然后, 你使用 BitBake 构建你想要的镜像名称 (例如, bitbake core-image-minimal), 然后就可以开始运行了.

But here’s where Yocto falls flat for me as a hardware person: it has absolutely no interest in helping you build images for the shiny new custom board you just made. It is not a tool for quickly hacking together a kernel/U-Boot/rootfs during the early stages of prototyping (say, during this entire blog project). It wasn’t designed for that, so architectural decisions they made ensure it will never be that. It’s written in a very software-engineery way that values encapsulation, abstraction, and generality above all else. It’s not hard-coded to know anything, so you have to modify tons of recipes and create clunky file overlays whenever you want to do even the simplest stuff. It doesn’t know what DTS files are, so it doesn’t have a “quick trick” to compile Linux with a custom one. Even seemingly mundane things — like using menuconfig to modify your kernel’s config file and save that back somewhere so it doesn’t get wiped out — become ridiculous tasks. Just read through Section 1 of this Yocto guide to see what it takes to accomplish the equivalent of Buildroot’s make linux-savedefconfig((Alright, to be fair: many kernel recipes are set up with a hardcoded defconfig file inside the recipe folder itself, so you can often just manually copy over that file with a generated defconfig file from your kernel build directory — but this relies on your kernel recipe being set up this way)). Instead, if I plan on having to modify kernel configurations or DTS files, I usually resort to the nuclear option: copy the entire kernel somewhere else and then set the kernel recipe’s SRC_URI to that.

但对于我这个硬件开发者来说, Yocto 让我感到很失望的是: 它完全不关心如何为你刚刚制作的全新定制板创建镜像. 它不是用于在原型设计的早期阶段快速地拼凑内核、U-Boot 或 rootfs (比如在这整个博客的项目过程中). 它从一开始就不是为此而设计的, 因此它的架构决定了它永远不会成为那样的工具. Yocto 是以非常 “软件工程化” 的方式编写的, 优先考虑封装性、抽象性和通用性. 它没办法硬编码来配置任何东西, 所以每当你想做一些最简单的事情时, 你就必须修改大量的 recipe 并创建笨重的文件覆盖. 它不知道 DTS 文件是什么, 因此没有一个 “快速技巧” 来用自定义的 DTS 编译 Linux. 甚至一些看似平常的事情——比如使用 menuconfig 修改内核的配置文件并将修改保存以防被覆盖——都变成了荒谬的任务. 只需阅读这篇 Yocto 指南的第一部分, 就能看到完成与 Buildroot 的 make linux-savedefconfig 相当的操作需要多少工作 ((好吧, 坦诚地说: 许多内核 recipe 已经在其 recipe 文件夹中设置了一个硬编码的 defconfig 文件, 因此你通常可以手动将从内核构建目录生成的 defconfig 文件复制过来——但这依赖于你的内核 recipe 是这样设置的)). 相反, 如果我打算修改内核配置或 DTS 文件, 我通常会用 “核武器” 一样的方法: 把整个内核复制到别的地方, 然后将内核 recipe 的 SRC_URI 指向它.

Yocto is a great tool to use once you have a working kernel and U-Boot, and you’re focused on sculpting the rest of your rootfs. Yocto is much smarter at incremental builds than Buildroot — if you change a package configuration and rebuild it, when you rebuild your image, Yocto will intelligently rebuild any other packages necessary. Yocto also lets you easily switch between machines, and organizes package builds into those specific to a machine (like the kernel), those specific to an architecture (like, say, Qt5), and those that are universal (like a PNG icon pack). Since it doesn’t rebuild packages unecessarily, this has the effect of letting you quickly switch between machines that share an instruction set (say ARMv7) without having to rebuild a bunch of packages.

Yocto 是一个在你拥有一个可工作的内核和 U-Boot 之后非常出色的工具, 你将专注于编写其余的根文件系统. 与 Buildroot 相比, Yocto 在增量构建方面非常智能——如果你更改了软件包配置并重新构建它, 当你重新构建镜像时, Yocto 将智能地重新构建任何必要的其他软件包. Yocto 还允许你轻松地在机器之间切换, 并将软件包构建组织成特定于机器的 (如内核)、特定于架构的 (如 Qt5) 以及通用的 (如 PNG 图标包). 它不会不必要地重新构建软件包, 这可以让你快速在有指令集 (如 ARMv7) 的机器之间切换, 而无需重新构建大量软件包.

It may not seem like a big distinction when you’re getting started, but Yocto builds a Linux distribution, while Buildroot builds a system image. Yocto knows what each software component is and how those components depend on each other. As a result, Yocto can build a package feed for your platform, allowing you to remotely install and update software on your embedded product just as you would a desktop or server Linux instance. That’s why Yocto thinks of itself not as a Linux distribution, but as a tool to build Linux distributions. Whether you use that feature or not is a complicated decision — I think most embedded Linux engineers prefer to do whole-image updates at once to ensure there’s no chance of something screwy going on. But if you’re building a huge project with a 500 MB root filesystem, pushing images like that down the tube can eat through a lot of bandwidth (and annoy customers with “Downloading….” progress bars).

这可能在刚开始时看起来并不是一个很大的区别, 但 Yocto 构建的是一个 Linux 发行版, 而 Buildroot 构建的是一个系统镜像. Yocto 知道每个软件组件是什么以及这些组件之间的依赖关系. 因此, Yocto 可以为你的平台构建一个软件包源, 使你能够像在桌面或 Linux 服务器中一样远程安装和更新软件. 这也是 Yocto 自视为一个构建 Linux 发行版的工具而不是一个 Linux 发行版的原因. 是否使用这个特性是一个复杂的决定——我认为大多数嵌入式 Linux 工程师更倾向于一次性更新整个镜像以确保没有出现什么问题. 但如果你正在构建一个庞大的项目, 拥有 500MB 的 root 文件系统, 通过网络推送这样大的镜像可能会消耗大量带宽 (并且让客户对 “正在下载……” 进度条感到不满).

When I started this project, I sort of expected to bounce between Buildroot and Yocto, but I ended up using Buildroot exclusively (even though I had much more experience with Yocto), and it was definitely the right choice. Yes, it was ridiculous: I had 10 different processors I was building images for, so I had 10 different copies of buildroot, each configured for a separate board. I bet 90% of the binary junk in these folders was identical. Yocto would have enabled me to switch between these machines quickly. In the end, though, Yocto is simply not designed to help you bring up new hardware. You can do it, but it’s much more painful.

当我开始这个项目时, 我以为我会在 Buildroot 和 Yocto 之间来回切换, 但我最终仅使用了 Buildroot (尽管我对 Yocto 更有经验), 并且这绝对是正确的选择. 是的, 这看起来很荒谬: 我为 10 个不同的处理器构建镜像, 所以我有 10 份不同的 Buildroot, 每份配置一个单独的板子. 我敢打赌, 这些文件夹中 90% 的二进制文件是完全相同的. Yocto 本可以让我快速切换这些机器, 不过说到底 Yocto 根本是为了帮助你启动新硬件而设计的. 虽然你可以做到, 但这确实要痛苦得多.