本文翻译自 ThirtyThreeForty 的 Mastering Embedded Linux, Part 4: Adding Features

这是我们精通嵌入式 Linux 文章系列的第四章,本系列文章旨在帮助你成为一名低成本嵌入式 Linux 系统的开发大师。在本章中,我会接着之前的自定义固件的话题,谈谈如何为你的自定义固件添加一些高级功能。我们会选择需要的开源软件,配置并编译这个软件,最后将其集成到你的系统中。

我们会在上一章制作的树莓派 Zero-W 的固件基础上,为其添加一个 Wi-Fi 接入点功能。要跟着我们一起做的话,你会需要用到第二章中所提到的硬件。

当然,本章所使用的方法可以应用到任何功能的添加工作中。首先,我们选择需要的软件,并找出其工作的原理;然后,我们实际上手将其添加到我们的固件中。

新功能的工作流

完整的工作流会像下面的任务单一样,你会发现这个任务单和传统的软件工程开发并没有什么不同。

  1. 计划你的功能对用户是如何展现的,以及在系统方面是如何实现的。问问自己是否需要新的硬件来支持这项功能?这项新功能/子系统是如何处理错误的?
  2. 选择实现这项功能所需的软件。你可能需要启用新的内核驱动、新的守护进程,甚至要自己写一些脚本让这些功能共同工作。
  3. 配置你选择的软件,使其更适合于你的需求。好的软件通常都是设计为通用可配置的,你需要根据你的需求来设置这些软件。
  4. 测试你的修改。尝试启动你的新固件,看看新功能是否工作?如果不工作,那就找找原因并重新尝试。如果你处于一个专业的工程团队中,你可能还需要写一些自动测试;但如果只是跑来玩玩,那其实可以不用写自动测试1
  5. 最后提交你的更改

如何实现一个Wi-Fi接入点

我们的目标是将树莓派的功能配置为类似 Wi-Fi 路由器:在启动后,树莓派会自动创建一个 Wi-Fi 接入点,并且能够使用 DHCP 自动地2为每个接入的客户端分配一个IP地址。

为了实现以上功能,我们的“固件功能”实际上是由两个守护进程和额外的 Wi-Fi 芯片驱动所构成的3。我们会使用 hostapd 和 dnsmasq 共同完成用户空间的功能,并直接安装预编译好的驱动。

hostapd 用于创建一个可以被其他用户接入的 Wi-Fi 接入点。它会使用内核提供的 Wi-Fi API 将硬件设置为接入点,然后选择 Wi-Fi 的信道和加密方式。之后,它会管理所有与这个接入点相关的事件和网络流量,例如连接/断开事件等。为了演示方便,我们只创建一个简单的 2.4GHZ 开放的接入点。

dnsmasq 会通过 DHCP 协议为各个客户端分配一个 IP 地址。客户端成功连接后,它会尝试使用 DHCP 协议从路由器上获取一个 IP 地址。为了演示方便,我们会使用 IPv4 地址(10.33.40.0/24网段)

树莓派自己也需要一个静态的IP地址,以便客户端能够将网络流量流向它。我们会为wlan0接口使用10.33.40.1这样一个IP地址。当前的 Buildroot 配置项目使用的是 /etc/network/interfaces 文件(而不是 Systemd的 networkd),所以你需要在这个文件中添加几行配置。

这个配置会被固化在固件中,所以你可以直接烧录这个固件并使用,而不用进行后续的手工调整。

配置软件

让我们开始写配置文件吧!我就直接无耻的从帮助文档中复制了这些配置——毕竟这不是一篇论文4

# dnsmasq.conf
interface=wlan0

# 为客户端分配 10.33.40.{10-200} 地址, 子网 /24, 24小时的租期
dhcp-range=10.33.40.10,10.33.40.200,255.255.255.0,24h
# hostapd.conf
interface=wlan0

# 内核驱动,大部分现代驱动使用 nl80211
driver=nl80211

# 接入点名称,可以改为任意你喜欢的名称
ssid=MasteringEmbeddedLinux

# 使用 802.11g, 而不是缓慢的 b
hw_mode=g

# 随意从 1, 6, 11 中选择一个 Wi-Fi 信道,这三个信道是 2.4Ghz 最有用的信道
channel=6

Buildroot 会自动创建一个 ifupdown 配置,并指示 eth0 端口使用 DHCP 获取 IP 地址。但是我们的树莓派 Zero-W 没有以太网口,我们就需要使用我们自己的配置覆盖它了。

# /etc/network/interfaces
auto wlan0
iface wlan0 inet static
    address 10.33.40.1
    netmask 255.255.255.0

这些配置文件都是与 Buildroot 无关的,所以我们可以提前将其写完。

提示

现在,你可以尝试在你的电脑上安装上述程序并使用上面的配置来测试是否正确了。这通常是可行的,因为你的工作站的运行速度要比嵌入式设备要快得多。(同样的,可以考虑将编写和打包嵌入式 Linux 软件两项工作分开;开发者通常能在自己搭工作站上获得更高的运行速度)

启动脚本

如果你尝试查看我们在上一章使用的镜像的操作日志,你会发现ip link命令并没有显示wlan0设备。这是因为我们的 Buildroot 固件并没有启动 Wi-Fi 功能。在启动的时候,我们需要载入 Wi-Fi 芯片的驱动(内核模块)5

(当然,由于一些未知的原因,Buildroot 的 hostapd 软件包并没有提供启动脚本,所以我们需要手工添加一个。)当前 Buildroot 配置是使用init scripts,我会在下一章中详细说明这个脚本。现在,你只要知道它会按字母顺序执行/etc/init.d/目录下的可执行文件。它被限制为仅执行以S+数字开头的文件,即将字母顺序转换为数字顺序。我们想要最先加载驱动并最后启动 hostapd,所以我们将它们命名为S02modulesS90hostapd

这就是模块加载器,一行很简单的代码:

#!/bin/sh

/sbin/modprobe brcmfmac

这个是 hostapd 的脚本。(大部分从已经存在的脚本中复制,所以它们的大部分都是相同的)

#!/bin/sh

case "$1" in
        start)
                printf "Starting hostapd: "
                start-stop-daemon -S -x /usr/sbin/hostapd -- -B /etc/hostapd.conf
                [ $? = 0 ] && echo "OK" || echo "FAIL"
                ;;
        stop)
                printf "Stopping hostapd: "
                start-stop-daemon -K -q -x /usr/sbin/hostapd
                [ $? = 0 ] && echo "OK" || echo "FAIL"
                ;;
        restart|reload)
                $0 stop
                $0 start
                ;;
        *)
                echo "Usage: $0 {start|stop|restart}"
                exit 1
esac

exit 0

为 Buildroot 移植配置

配置文件是与特定的硬件关联的,它们不属于软件包,因为软件包是硬件无关的。

在 Buildroot 中,硬件关联的配置需要放置在 overlay 文件夹下。这个文件夹下的文件会放入并覆盖镜像中对应的文件。我们只要简单的将这些配置文件放入 overlay 文件夹,它们就会神奇的出现在最终镜像中的对应位置。

实现它!

让我们开始吧!如果你想要查看最终的 Buildroot 库,请访问 Github 上的项目页面并切换到 part-4 分支。

旁注:如何使用 menuconfig

menuconfig 是一个终端界面,用于修改当前的配置。你可以简单的运行make menuconfig启动这个配置工具。使用键盘上下方向键可以上下移动,使用左右方向键可以移动底部的按钮。

各个菜单拥有一些标记:

  • --->:一个子菜单。使用底部的<select>进入这个菜单,并使用<exit> 按钮退出到上一层菜单。
  • [ ]:一个复选框。可以使用空格键启用或禁用。部分菜单会同时有复选框和子菜单
  • ***:注释

按下/键(Vim 里面的搜索键)可以搜索。你可以搜索程序的名称(例如dnsmasq)或者是配置变量的名称(如BR2_PACKAGE_DNSMASQ)。搜索结果中会显示配置项目的位置——在这个示例中,你可以看到“dnsmasq”位于“Target packages”的“Networking applications”下。

它也显示了当前软件包是否启用([=n]),并列出了它的依赖。如果你无法在菜单中看到这个选项,说明它的部分依赖没有启用。

软件的依赖有时候会很恼人,你可能会需要一路向上查找依赖并将其启用。为了缓解这种问题,这些依赖一般来说都是重量级的项目,如C++支持等。其他较小的依赖(如库)会被软件包“选择(selected)”,即它们会自动被启用。

启用 CCache

在 menuconfig 中,你需要做的第一件事应该就是启用 ccache。ccache 是一个编译缓存工具,它会缓存输入的命令和输出的编译结果。ccache 的行为非常保守,如果编译器、输入文件、编译命令行被修改了,它就会将其视为一次新的编译并且不命中缓存。但 Buildroot 在每次重新编译的时候会运行同样的命令,所以它很适合用在这里。

CCache 的选项在 Menuconfig 的“Build Options” – “Enable compiler cache” 下,使用空格将其启用,并保存退出即可。

现在,编译器的编译结果会被 ccache 缓存。在有缓存的情况下,在我的笔记本电脑上重新编译一次的时间大约是 20 分钟。如果你需要更新你的缓存,你可以在睡觉前使用make clean && make指令重新编译。

安装守护进程和固件

现在,我们可以启用我们需要的软件包了。dnsmasq 和 hostapd 都位于“Target packages” 的 “Networking applications” 下,不过如果你不清楚的话,你还可以使用搜索功能搜索。

Enabling the dnsmasq package in menuconfig

额外的,Wi-Fi 芯片的固件也需要安装。这个固件是以二进制文件形式存在的(存放于/lib/firmware),并且会在驱动加载时写入到芯片中。树莓派的 Wi-Fi 固件在 Target packages – Hardware handling – Firmware 选项下(BR2_PACKAGE_RPI_WIFI_FIRMWARE)

提示

有些人不喜欢使用 vi 编辑器。你可以在 “Target packages” – ”Text editors and viewers“ 下安装其他编辑器。

创建 Overlay 文件夹

现在我们需要为我们的配置文件创建 Overlay 文件夹了。我们首先创建一个目录board/raspberrypi/rootfs_overlay/,然后创建一些子目录:

$ mkdir board/raspberrypi/rootfs_overlay/
$ mkdir board/raspberrypi/rootfs_overlay/etc/
$ mkdir board/raspberrypi/rootfs_overlay/etc/init.d/
$ mkdir board/raspberrypi/rootfs_overlay/etc/network/

然后将前面提到的5个配置和启动项文件放入文件夹中

文件 rootfs_overlay/ 中的位置
dnsmasq 配置 etc/dnsmasq.conf
hostapd 配置 etc/hostapd.conf
ifupdown 配置 etc/network/interfaces
modprobe 启动脚本 etc/init.d/S02modprobe
hostapd 启动脚本 etc/init.d/S90hostapd

确保所有的启动脚本都是可执行的。如果一个启动脚本不可执行,那么启动过程中就会报一个“permission denied”错误。可以使用下面的命令批量为启动脚本添加可执行权限:

chmod +x board/raspberrypi/rootfs_overlay/etc/init.d/*

现在,我们需要在 Buildroot 配置中指定这个 Overlay 文件夹。对应的配置项目是BR2_ROOTFS_OVERLAY。当前它可能是空的。这是一个以空格分割的路径列表,路径是相对于 Buildroot 根目录的路径。(相信聪明的你一定能找到这个配置项目在哪配置)

重新编译镜像并烧录

现在,使用make指令直接重新编译。这个指令会编译你选择的软件包并生成一个新的镜像。根据下面我们讨论的重新编译逻辑,这一行指令只需几分钟。编译完成后,检查一下output/target/etc目录,你的新配置文件应该会出现在这里。

旁注:Buildroot 的重新编译逻辑

一旦 Buildroot 完成了某个子软件包的编译(如dnsmasq-install-target),你再次使用make命令重新编译的时候,这个子软件包将不会重新编译。如果你修改了这个软件包,例如添加了某些补丁或修改了配置文件,你就需要告诉 Buildroot 重新执行这个子软件包的编译过程,因为它是不会自动触发重新编译的。

当然,你也可以直接运行make clean。这个命令是一项“核武器”,在你修改了某些配置,影响; 很多软件包的时候是很有用的。例如,当你修改了处理器架构、编译器版本或 C 库的时候,你就需要这个命令了。在发布固件前执行这个命令并重新编译也是很有用的,这可以防止你做了某些手动修改,导致下次构建和本次构建行为不一致。在使用了 CCache 的情况下,定期运行这个命令确实是一个不错的选项。

然后,在大部分时候,你只需要重新编译单独一个软件包。如果将make clean比作一个核武器命令,那么软件包的<package>-dirclean命令就是一个战术核武器。这个命令会删除output/build/</package><package> 目录,强制 Buildroot 在下次运行 make 的时候重新编译这个软件包。如果你修改了一个软件包的编译选项,那么使用这个命令可以保证所有的文件都使用同样的选项编译。然而,这个命令的局限在于它只删除了上面那个命令,而不会删除output/target目录的对应的文件。

还有一个比较重要的区别。make 的各项子命令,不会在每次构建时重新运行;但是编译后脚本,会在每次make后运行。这些脚本执行着类似装配的工作,会生成最终的镜像。所以如果你修改了output/target目录里面的文件,运行make命令后,这些修改会被打包到新的镜像中。

测试

使用和上一章一样的烧录命令:

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

你会发现,串口控制台在启动后会出现一些新的信息:

Starting dnsmasq: OK
Starting hostapd: OK

如果一切正常的话,你会在你的手机或电脑的网络列表中找到一个无线接入点6

The new Wi-Fi AP displayed on my workstation's chooser

提交所有变更

工作正常?那真是,太棒了!接下来我们就要把这些工作保存到 Git 中。

在上一章中,我讨论了工作的配置和 defconfig 的区别。当前我们所做的所有变更都是在工作配置上做的,而这个配置并不会被 Git 所跟踪。我们需要使用下面的指令更新我们用的 defconfig

$ make savedefconfig

现在你会发现原有的 defconfig 被修改了,并且我们的 overlay 目录并没有被跟踪:

$ git status
HEAD detached at 2019.11.1
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
    modified:   configs/raspberrypi0w_defconfig

Untracked files:
  (use "git add <file>..." to include in what will be committed)
    board/raspberrypi/rootfs_overlay/

no changes added to commit (use "git add" and/or "git commit -a")

跟踪这些文件,并提交:

$ git add board/raspberrypi/rootfs_overlay/
$ git add configs/
$ git commit -m "rpi: implement simple wireless access point"

如何直接测试

在这篇文章中,所有的软件、配置文件和操作步骤都已经准备好了,并且能够正常的运行,不需要进入“修改-编译-测试”的循环。

我可以向你保证,这种第一次尝试就成功的事情,一般一年最多只会发生一两次。大部分情况下,在工作正常之前,你都会尝试多次。最快的调试方法还是直接在目标板上运行未完成的镜像。你可以使用串口控制台直接修改配置文件,并手工做各种测试。

当你把这些配置文件打磨完毕后,你可以把这些文件复制回 Buildroot 的 overlay 文件夹中。如果你需要在测试中使用什么新的软件,你就得重新构建镜像,然后烧写镜像。如果你不把这些文件复制出来,那么你之前做的调试工作就都白费了。记住,Buildroot 的优点在于镜像文件的可重复性。如果你在构建后还手动修改镜像的话,那这一项优点就白费了。

要点

  • 计划、选择、配置、测试、完成这一循环是嵌入式系统定制的核心。如果你需要在目标板上测试,那也是可以的——只要记得把做过的修改复制回 Buildroot 配置中就行。
  • 尽管这是一项工程,但在本章中并没有写过任何代码7。大部分的嵌入式 Linux 工作就是将已经完成的软件包放入一个已经构架好的 系统中。
  • ccache可以显著的降低重新编译的时间,这对于频繁重新构建的项目十分有用。
  • 根文件系统 Overlay 是一个硬件相关的自定义目录。最好将那些硬件相关、与软件包无关的脚本放入这里。
  • 我们所做的所有改动都会被 Buildroot 自动的记录,Git 则会帮我们管理好这些记录的历史版本。在此基础上,你可以很容易重现另一份配置文件的结果,仅需执行make指令即可。

推荐阅读

下一章……

在下一章中,我们会谈论平台守护进程。

我们会运用本章所用的一些知识,并扩展一些功能:

  • 编写一个简单的嵌入式 Web 服务器,用于控制 GPIO 接口
  • 为 Buildroot 制作一个新的自定义软件包,并增加自动启动脚本
  • 添加到我们的镜像中,就像本章所做的那样

学习如何将你的软件移植到嵌入式 Linux 可以让你完全掌控你的 Linux 固件,让它做任何你想做的事情。

我想在此老生常谈的说,你拥有系统的所有源代码。如果什么东西不工作了,或者你想要什么新功能,你不必等待发行版发布新的版本,因为你就是发行者,你可以手工实现它们。


  1. 在硬件上做单元测试是一件非常困难的事情。如果你已经熟悉了软件单元测试,那么想像一下每一个测试执行文件、数据等都是一个实际存在的硬件,并且他们的大小至少有一个桌游盒子那么大。你无法像做软件测试那样使用一些奇技淫巧,你必须设置一个物理的测试框架,这个测试框架能够完整的控制嵌入式系统。 ↩︎
  2. “无需额外设置”的哲学使这篇教程与其他的“为你的树莓派添加功能X”教程完全不一样。在这篇文章中的所有功能都是可以通过源代码和配置完美重现的,而不是随机选择一个已经设置好相关功能的 SD 卡来实现。 ↩︎
  3. 这个描述是非常简易的描述。有许多优秀的教程详细的解释了 Wi-Fi、hostapd 和 DHCP,但本文章的重点在于为你的嵌入式系统添加功能,而不是着重于 Wi-Fi ↩︎
  4. 感谢 这篇树莓派的文章 提供了配置脚本 ↩︎
  5. 如果你启用了很多依赖,你可以使用 eudev(在 System Configuration 菜单下)自动加载正确的内核模块。在这里我选择直接加载模块,毕竟这也是个介绍启动脚本的好机会。 ↩︎
  6. 使用安卓手机连接的时候,可能会遇到一些问题。安卓手机会检测此热点是否有互联网访问(iOS手机应该也会,但我没有试过所以不知道)。使用你的电脑应该不会出现这些问题。 ↩︎
  7. 我们实际上是写了一些配置文件,但那不算代码。你可以发现你写的配置越多,你改代码的机率也就越少。
     ↩︎

0 条评论

发表回复

Avatar placeholder

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