3. 嵌入式Linux开发环境搭建:交叉编译工具链、TFTP/NFS服务配置、Bootloader烧录、内核与根文件系统下载

说实话,很多刚入行的朋友觉得嵌入式Linux开发最难的是写驱动。但我个人经验是,环境搭不好,后面全是坑。我见过太多人花两周调一个驱动,最后发现是交叉编译链版本不对——这种冤枉路,咱们不走。

这一章,咱们把开发环境彻底捋一遍。从工具链到网络服务,从烧录到下载,每一步我都会把实际项目中踩过的坑指给你看。

本章核心目标:让PC能编译ARM代码,让板子能从网络启动,让Bootloader、内核、文件系统都能顺利跑起来。

3.1 交叉编译工具链:为什么你的PC不能直接编译?

你的电脑是x86架构,ARM板子是ARM架构。x86编译出来的二进制,ARM CPU不认识。所以我们需要交叉编译工具链——在x86上生成ARM指令集的机器码。

说白了,就是一套能在PC上运行、但生成ARM代码的gcc工具集。

3.1.1 工具链的命名规则

我刚开始学的时候,看到一堆名字就头大:arm-linux-gnueabihf-gcc、aarch64-linux-gnu-gcc……其实拆开看就清楚了:

组成部分 含义 举例
架构 目标CPU架构 arm, aarch64, mips
操作系统 目标系统类型 linux, none(bare-metal)
库类型 C库实现 gnu(glibc), uclibc, musl
ABI/浮点 应用二进制接口 eabi, gnueabihf(硬浮点)

arm-linux-gnueabihf-gcc 就是:目标ARM架构、Linux系统、glibc库、硬浮点ABI的交叉编译器。

我的建议:新手直接用Linaro提供的预编译工具链,稳定省事。别自己从源码编译,那是个大坑。我当年为了省几兆空间自己编gcc,折腾了三天——最后发现预编译版更好用。

3.1.2 安装与验证

以ARM Cortex-A7为例,我常用的是 gcc-arm-linux-gnueabihf:

# 下载并解压
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
tar -xf gcc-linaro-*.tar.xz -C /opt/

# 添加环境变量
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH

# 验证
arm-linux-gnueabihf-gcc --version

看到版本信息就对了。然后写个hello world试试:

# hello.c
#include <stdio.h>
int main() {
    printf("Hello, Embedded Linux!\n");
    return 0;
}

# 编译
arm-linux-gnueabihf-gcc -o hello hello.c

# 查看文件信息
file hello
# 输出: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)

看到 ARM 字样,说明编译成功了。这个文件在PC上跑不了,得放到板子上才能执行。

注意:工具链版本必须与内核版本匹配。比如Linux 4.x内核,建议用gcc 7.x或8.x。版本差太多,编译时会报一些莫名其妙的错误。我曾经用gcc 4.6编5.10内核,链接阶段直接崩了——查了两天才发现是工具链太老。

3.2 TFTP/NFS服务配置:让板子从网络启动

开发阶段,频繁插拔SD卡烧写太慢了。我习惯用网络启动:TFTP传内核和设备树,NFS挂载根文件系统。改完代码,重新编译,重启板子就能看到效果——这才是开发该有的节奏。

3.2.1 TFTP服务器:传内核和dtb

TFTP(Trivial File Transfer Protocol)很简单,就是用来从PC往板子上传文件的。配置起来也快:

# Ubuntu上安装
sudo apt-get install tftpd-hpa

# 配置文件 /etc/default/tftpd-hpa
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"        # 文件存放目录
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="--secure --create"

# 创建目录并设置权限
sudo mkdir -p /srv/tftp
sudo chmod 777 /srv/tftp

# 重启服务
sudo systemctl restart tftpd-hpa
sudo systemctl enable tftpd-hpa

把编译好的 zImage 和 *.dtb 文件丢到 /srv/tftp 里。板子上的U-Boot就能通过 tftp 命令拉过去:

# U-Boot命令行
setenv serverip 192.168.1.100    # PC的IP
setenv ipaddr 192.168.1.10       # 板子的IP
tftp 0x82000000 zImage
tftp 0x83000000 imx6ull.dtb

避坑:防火墙!防火墙!防火墙!我调试时经常遇到 TFTP timeout,十有八九是防火墙拦了UDP 69端口。直接 sudo ufw disable 或者放行端口:sudo ufw allow 69/udp。

3.2.2 NFS服务器:挂载根文件系统

NFS(Network File System)让板子把PC上的一个目录当成自己的根文件系统。这样你改PC上的文件,板子立刻就能用。

# 安装NFS服务器
sudo apt-get install nfs-kernel-server

# 配置文件 /etc/exports
# 格式:共享目录 客户端(选项)
/home/user/rootfs *(rw,sync,no_subtree_check,no_root_squash)

# 创建并导出
sudo mkdir -p /home/user/rootfs
# 把制作好的根文件系统解压到这里
sudo tar -xf rootfs.tar -C /home/user/rootfs

# 重启服务
sudo systemctl restart nfs-kernel-server
sudo exportfs -a

板子上U-Boot启动时,传给内核的参数要加上NFS挂载信息:

# U-Boot中设置bootargs
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.100:/home/user/rootfs,proto=tcp rw ip=192.168.1.10:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off'

这一长串参数,我拆开解释一下:

参数 含义
root=/dev/nfs 告诉内核根文件系统在NFS上
nfsroot=server_ip:path NFS服务器IP和共享路径
ip=板子IP:服务器IP:网关:掩码 网络配置,顺序不能乱

注意:no_root_squash 这个选项很重要。不加的话,板子上的root用户会被映射成nobody,很多操作会权限不足。我刚开始就漏了这个,结果板子上连 mkdir 都报Permission denied——折腾了半小时才发现是exports写错了。

3.3 Bootloader烧录:让板子先跑起来

Bootloader是板子上电后第一个运行的程序。嵌入式Linux最常用的是U-Boot。它负责初始化硬件、加载内核、传递启动参数。

3.3.1 编译U-Boot

以i.MX6ULL为例:

# 下载U-Boot源码
git clone https://github.com/u-boot/u-boot.git
cd u-boot
git checkout v2023.04

# 配置(不同板子配置不同)
make mx6ull_14x14_evk_defconfig

# 编译
make CROSS_COMPILE=arm-linux-gnueabihf- -j4

编译完成后,会生成 u-boot.imx 或 u-boot.bin。这个文件就是要烧录到板子上的。

3.3.2 烧录方式

不同板子烧录方式不一样。我列几种常见的:

方式 适用场景 工具
SD卡烧录 最常用,适合开发板 dd命令、Win32DiskImager
USB下载 空片烧录,适合量产 mfgtools(官方工具)
JTAG/SWD 调试阶段,适合裸机 OpenOCD、JLink
网络烧录 已有U-Boot时更新 tftp + U-Boot命令

SD卡烧录最直接:

# 查看SD卡设备(通常是 /dev/sdb 或 /dev/mmcblk0)
sudo fdisk -l

# 烧录U-Boot到SD卡(注意:不是分区,是整卡偏移)
sudo dd if=u-boot.imx of=/dev/sdb bs=512 seek=2

# 同步并弹出
sync
sudo eject /dev/sdb

关键点:seek=2 表示跳过前2个扇区(1KB),因为i.MX6ULL的启动ROM会从第1KB处读取U-Boot。不同芯片的偏移量不同,一定要看芯片手册。我见过有人把U-Boot烧到偏移0处,结果板子死活起不来——查了三天手册才发现是偏移搞错了。

3.4 内核与根文件系统下载:让系统完整跑起来

Bootloader起来了,接下来就是加载内核和根文件系统。我习惯用网络下载,开发效率最高。

3.4.1 编译内核

# 下载内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.100.tar.xz
tar -xf linux-5.10.100.tar.xz
cd linux-5.10.100

# 配置(使用默认配置或板级配置)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig

# 编译内核和设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j4
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

# 编译模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules -j4
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_install INSTALL_MOD_PATH=/home/user/rootfs

编译完成后,arch/arm/boot/zImage 就是内核镜像,arch/arm/boot/dts/ 下是设备树文件。

3.4.2 制作根文件系统

根文件系统最简单的做法是用BusyBox:

# 下载BusyBox
wget https://busybox.net/downloads/busybox-1.36.0.tar.bz2
tar -xf busybox-1.36.0.tar.bz2
cd busybox-1.36.0

# 配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 确保选中 "Build static binary (no shared libs)"

# 编译安装
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install CONFIG_PREFIX=/home/user/rootfs

然后手动创建必要的目录和设备节点:

cd /home/user/rootfs
mkdir -p proc sys dev etc/init.d

# 创建init脚本
cat > etc/init.d/rcS << 'EOF'
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
echo "Welcome to Embedded Linux!"
EOF
chmod +x etc/init.d/rcS

# 创建设备节点
sudo mknod dev/console c 5 1
sudo mknod dev/null c 1 3

3.4.3 网络下载启动

所有文件准备好后,在U-Boot中执行:

# 设置网络
setenv serverip 192.168.1.100
setenv ipaddr 192.168.1.10

# 下载内核
tftp 0x82000000 zImage

# 下载设备树
tftp 0x83000000 imx6ull.dtb

# 设置启动参数(NFS根文件系统)
setenv bootargs console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.100:/home/user/rootfs,proto=tcp rw ip=192.168.1.10:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off

# 启动
bootz 0x82000000 - 0x83000000

看到内核启动日志,最后出现 Welcome to Embedded Linux!,就说明整个环境搭建成功了。

我的习惯:把常用的U-Boot命令写成脚本,存到板子的EEPROM或SD卡里。每次重启自动执行,省得一遍遍敲。比如 setenv bootcmd 'tftp 0x82000000 zImage; tftp 0x83000000 imx6ull.dtb; bootz 0x82000000 - 0x83000000',然后 saveenv。下次上电自动网络启动,爽得很。

3.5 本章知识体系

下面这张图把整个开发环境搭建的流程串起来了。从PC端到板子端,每个环节的产物和流向都标清楚了:

嵌入式Linux开发环境搭建流程 嵌入式Linux开发环境搭建流程 PC端(x86) 板子端(ARM) 交叉编译工具链 arm-linux-gnueabihf-gcc U-Boot源码 编译 → u-boot.imx Linux内核源码 编译 → zImage + dtb 根文件系统(BusyBox + 库) 制作 → rootfs目录 TFTP服务器 传 zImage + dtb NFS服务器 挂载 rootfs U-Boot(Bootloader) 初始化硬件 → 加载内核 → 传递参数 Linux内核 解压 → 挂载根文件系统 → 启动init 根文件系统(/) /bin /sbin /etc /dev /proc /sys 用户应用程序 驱动测试 → 业务逻辑 → 产品功能 编译产物 烧录(SD/USB/JTAG) TFTP下载 NFS挂载 开发阶段推荐流程 ① 编译U-Boot并烧录一次 ② 每次修改代码 → 编译 → TFTP/NFS启动 量产阶段推荐流程 ① 所有文件固化到Flash ② 从Flash启动,无需网络依赖

这张图把整个流程分成了PC端和板子端。左边是开发环境,右边是目标硬件。箭头标明了数据流向——从源码编译到网络传输,再到板子上运行。开发阶段用虚线路径(TFTP/NFS),量产阶段用实线路径(Flash固化)。

总结一下:环境搭建的核心就三件事——交叉编译(让PC生成ARM代码)、网络服务(TFTP传内核、NFS挂载文件系统)、启动流程(Bootloader→内核→根文件系统)。这三件事理顺了,后面的驱动开发就顺风顺水。