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端到板子端,每个环节的产物和流向都标清楚了:
这张图把整个流程分成了PC端和板子端。左边是开发环境,右边是目标硬件。箭头标明了数据流向——从源码编译到网络传输,再到板子上运行。开发阶段用虚线路径(TFTP/NFS),量产阶段用实线路径(Flash固化)。
总结一下:环境搭建的核心就三件事——交叉编译(让PC生成ARM代码)、网络服务(TFTP传内核、NFS挂载文件系统)、启动流程(Bootloader→内核→根文件系统)。这三件事理顺了,后面的驱动开发就顺风顺水。