用NFSRoot和TFTP启动你的嵌入式设备

介绍

最近在学习嵌入式 Linux 设备驱动开发。刚开始学习时,一个比较大的痛点是:总要想办法把编译好的二进制文件搬到设备上。对于内核驱动或用户态程序这类较小的文件,用 scp 之类的工具就能解决;但到了学习后期,涉及到修改内核源代码、甚至要替换 SD 卡上的内核镜像时,可用的手段就比较有限了——除非你能忍受每次改完代码都把内核重新烧写到 SD 卡或其他存储介质上。

经过一番搜索,我发现了一种很适合嵌入式开发的启动方式:用 TFTP 加载内核,用 NFSRoot 挂载根文件系统(RootFS)。这种方式可以大大减少把二进制文件拷贝到存储介质的次数。

以我家里的网络为例(配置如下图):我用笔记本进行开发,网络中还有一块嵌入式开发板和一台服务器。服务器走有线连接,网络比较稳定。借助 NFSRoot 和 TFTP,我可以把开发板的 RootFS、内核镜像和 dtb 等文件都放在服务器上。开发板上电后,会先从服务器读取内核到内存中的指定地址,再用它启动系统;内核加载过程中,又会把服务器上的 RootFS 挂载为根目录,完成启动。

这样的设置意味着:开发板启动后的“硬盘”已经不在开发板上,而是在服务器上了!如果用终端或 SSH 连接开发板并修改、保存一些文件,这些改动会直接反映到服务器上;反过来,你在服务器上对 RootFS 的改动,开发板也能实时看到。

网络配置

步骤

安装和设置 TFTP 服务器

很多嵌入式 bootloader 都支持通过 TFTP 加载内核镜像和设备树,这对系统和驱动开发人员来说非常方便:开发者在开发机上编译好内核和设备树,把产物放到 TFTP 服务器上;在设备一侧,只需重启并在 bootloader 中通过 TFTP 加载新内核,而无需拔下 SD 卡重新烧录。那么具体怎么使用 TFTP 呢?

接下来的步骤假设你使用 Linux 作为操作系统。首先安装 TFTP 服务器,Debian 系的发行版可以使用如下命令:

apt-get install tftpd-hpa
# 安装完成后启动TFTP服务器
service tftpd-hpa start

不同发行版默认的 TFTP 根目录可能不一样。要确认 TFTP 根目录,可以在 /etc/default/tftpd-hpa 文件里找到如下行:

TFTP_DIRECTORY="/srv/tftp"

在 TFTP 根目录下创建一个目录,放入为嵌入式设备编译好的内核与设备树:

/srv/tftp
        test_new_kernel/
           devicetree.dtb
           uImage

要让设备加载这个内核,我们还需要提供一个启动脚本。假设它叫 bootscript_new_kernel,同样放在 /srv/tftp 目录下:

/srv/tftp
        test_new_kernel/
           devicetree.dtb
           uImage
        bootscript_new_kernel

到这里,TFTP 就大致设置好了。接下来我们看看 bootscript_new_kernel 里到底要写什么。

bootscript_new_kernel 的内容

开发板的 bootloader(这里以最常见的 U-Boot 为例)可以通过 TFTP 把启动脚本下载到内存并执行。我们先写一个脚本源文件,记作 bootscript_new_kernel.txt:

# 服务器(TFTP/NFS)的IP地址和开发板自己的IP地址
setenv serverip 192.168.1.100
setenv ipaddr 192.168.1.150

# 通过TFTP把内核和设备树下载到内存
tftpboot ${kernel_addr_r} test_new_kernel/uImage
tftpboot ${fdt_addr_r} test_new_kernel/devicetree.dtb

# 告诉内核:根文件系统在NFS上
setenv bootargs console=ttyS0,115200 root=/dev/nfs rw nfsroot=${serverip}:/srv/nfs/rootfs,vers=3,tcp ip=dhcp

# 启动内核
bootm ${kernel_addr_r} - ${fdt_addr_r}

有几个地方需要注意:

  • kernel_addr_r 和 fdt_addr_r 是 U-Boot 预定义的内存加载地址,不同开发板的默认值不一样,可以在 U-Boot 命令行里用 printenv 确认。
  • root=/dev/nfs 并不是一个真实存在的设备节点,它只是告诉内核:根文件系统走 NFS,真正的服务器地址和路径由 nfsroot= 参数指定。
  • ip=dhcp 让内核在挂载 NFS 之前自己通过 DHCP 获取 IP。如果网络里没有 DHCP 服务器,可以改成静态形式:ip=<开发板IP>:<服务器IP>:<网关>:<子网掩码>::eth0:off。
  • 如果你的板子用的是 zImage 或 Image 而不是 uImage,把最后的 bootm 换成 bootz 或 booti 即可。

U-Boot 不能直接执行文本文件,需要用它自带的 mkimage 工具把脚本打包成镜像,输出直接放进 TFTP 根目录:

mkimage -T script -C none -n "netboot" -d bootscript_new_kernel.txt /srv/tftp/bootscript_new_kernel

之后用串口连上开发板,在 U-Boot 命令行里只需要两条命令就可以从网络启动:

tftpboot ${scriptaddr} bootscript_new_kernel
source ${scriptaddr}

如果不想每次都手动输入,可以把这两条命令存进 U-Boot 的 bootcmd 环境变量并 saveenv,这样开发板一上电就会自动从网络启动。

安装和设置 NFS 服务器

内核能通过 TFTP 加载了,但仅仅有内核,嵌入式设备还是没办法完成启动的——内核起来之后,还需要根文件系统和启动程序(init)。现在我们在服务器上把 NFS 服务搭起来:

apt-get install nfs-kernel-server

然后准备一份根文件系统。它可以直接从你现有的 SD 卡系统分区拷贝出来,也可以是 Buildroot/Yocto 编译产出的 rootfs,或者 debootstrap 生成的最小系统。假设我们把它放在 /srv/nfs/rootfs:

mkdir -p /srv/nfs/rootfs
# 例如从SD卡拷贝。注意用 -a 保留权限、属主和符号链接
cp -a /media/sdcard-rootfs/. /srv/nfs/rootfs/

接着把这个目录导出给开发板。编辑 /etc/exports,加入:

/srv/nfs/rootfs 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check)

其中 no_root_squash 很重要:开发板上的 init 和各种系统服务都是以 root 身份运行的,如果 NFS 把 root 映射成 nobody(默认行为),系统会因为权限问题起不来。当然,这个选项也意味着这个网段里的任何机器都能以 root 身份改写这份 rootfs,所以只建议在家里或实验室的内网使用。

让导出配置生效:

exportfs -ra
service nfs-kernel-server restart
# 确认导出成功
showmount -e localhost

内核需要的配置

要让内核能把 NFS 挂载为根文件系统,编译内核时下面几个选项必须直接编译进内核(=y,不能编译成模块——因为挂载根文件系统的时候,还没有地方可以加载模块):

CONFIG_NFS_FS=y
CONFIG_ROOT_NFS=y
CONFIG_IP_PNP=y
CONFIG_IP_PNP_DHCP=y

另外,你的网卡驱动同样必须是 =y。大部分开发板官方的 defconfig 已经把这些都打开了,但如果启动时卡在 “VFS: Unable to mount root fs via NFS”,可以先回来检查这几项。

把它们串起来

一切就绪后,整个启动流程是这样的:

  1. 开发板上电,U-Boot 通过 TFTP 下载并执行 bootscript_new_kernel;
  2. 脚本通过 TFTP 把 uImage 和 devicetree.dtb 下载到内存,并带着 nfsroot 参数启动内核;
  3. 内核通过 DHCP 拿到 IP,把服务器上的 /srv/nfs/rootfs 挂载为根目录,执行其中的 init,系统启动完成。

之后的开发就舒服多了:改完内核,把新的 uImage 和 dtb 拷进 /srv/tftp/test_new_kernel/,重启开发板即可;改驱动模块或应用程序,直接把编译产物拷进 /srv/nfs/rootfs 的对应目录,甚至连重启都不用。整个过程再也不需要碰 SD 卡。

总结

NFSRoot + TFTP 把「编译—拔卡—烧录—插卡—启动」的循环缩短成了「编译—拷贝—重启」,对需要频繁修改内核和驱动的嵌入式开发来说,是一次配置、长期受益的设置。希望这篇文章对你有帮助!

Leave a Reply

Your email address will not be published. Required fields are marked *