第三章:嵌入式Linux系统构建

嵌入式Linux系统构建,说白了就是给硬件“装系统”。但跟你在PC上装个Ubuntu不一样,这里每一步都得自己动手。我刚开始做嵌入式时,觉得这活儿挺枯燥的,后来才发现——系统构建的扎实程度,直接决定了后面应用开发的幸福感。

这一章,我们聚焦三个核心环节:交叉编译环境搭建、U-Boot移植、内核配置与编译。嗯,咱们一个一个来。

3.1 交叉编译环境搭建

为什么需要交叉编译?你想想看,你的开发电脑是x86架构,但目标板可能是ARM、RISC-V或者MIPS。在x86上编译出能在ARM上跑的程序,这就是“交叉编译”的本质。

我个人习惯用crosstool-NG来构建工具链,当然你也可以直接用Linaro或ARM官方提供的预编译工具链。但如果你想深入理解工具链的构成,我建议自己动手构建一次。

3.1.1 工具链的组成

一个完整的交叉编译工具链包括:

  • binutils:汇编器、链接器、目标文件工具
  • gcc:C/C++编译器
  • glibc(或uClibc、musl):C标准库
  • linux kernel headers:内核头文件,用于系统调用接口

我在项目中遇到过一个问题:用错了内核头文件版本,结果编译出的程序在目标板上跑起来就段错误。排查了两天才发现是头文件与内核版本不匹配。所以,头文件版本一定要跟目标内核版本一致

3.1.2 使用crosstool-NG构建

crosstool-NG是目前最主流的工具链构建工具。它的配置方式跟内核的menuconfig很像,上手很快。

# 下载crosstool-NG
git clone https://github.com/crosstool-ng/crosstool-ng
cd crosstool-ng
./bootstrap
./configure --enable-local
make
make install

# 配置工具链
./ct-ng menuconfig

配置时,你需要指定:

  • 目标架构(Target Architecture):比如armv7-a、aarch64
  • 字节序(Endianness):小端还是大端
  • C库类型:glibc适合功能丰富的场景,musl适合对体积敏感的场景
  • 线程模型:NPTL(Native POSIX Thread Library)是主流选择
我的建议:第一次构建时,尽量保持默认配置,只修改架构和C库类型。等熟悉了流程,再尝试定制化。

3.1.3 验证工具链

工具链构建完成后,验证一下是否可用:

# 查看编译器版本
arm-linux-gnueabihf-gcc --version

# 编译一个简单的测试程序
echo 'int main(){ return 0; }' > test.c
arm-linux-gnueabihf-gcc -o test test.c

# 查看生成的文件类型
file test
# 输出应该类似:ELF 32-bit LSB executable, ARM, version 1 (SYSV)

如果file命令显示的是ARM架构的ELF文件,恭喜你,工具链搭建成功了。

3.2 Bootloader(U-Boot)移植

U-Boot是嵌入式Linux系统启动的第一段软件。它的任务很简单:初始化硬件、加载内核到内存、跳转到内核入口。但做起来,细节多得很。

我记得第一次移植U-Boot时,板子死活起不来,串口没有任何输出。后来发现是时钟配置错了,PLL锁相环根本没锁定。嗯,这种问题最折磨人。

3.2.1 U-Boot源码结构

U-Boot的源码组织得很清晰:

  • board/:板级支持包,每个板子一个目录
  • arch/:架构相关代码,比如arch/arm、arch/riscv
  • drivers/:驱动代码,串口、网卡、存储等
  • include/configs/:板级配置文件,以头文件形式存在

3.2.2 移植步骤

移植U-Boot,说白了就是三步:

  1. 选择参考板:找一个跟你目标板硬件最接近的板子作为起点
  2. 修改板级配置:调整内存大小、时钟频率、外设地址等
  3. 添加设备树:描述硬件资源,U-Boot和内核共用

以i.MX6ULL为例,移植过程大致如下:

# 1. 选择参考板
make mx6ull_14x14_evk_defconfig

# 2. 修改配置
make menuconfig
# 在Device Tree Control中,指定自己的设备树文件

# 3. 编译
make CROSS_COMPILE=arm-linux-gnueabihf-

# 4. 烧录到SD卡
sudo dd if=u-boot.imx of=/dev/sdb bs=1k seek=1
注意:U-Boot的烧录位置因芯片而异。i.MX系列烧录到偏移1KB处,而Allwinner系列烧录到偏移8KB处。烧错位置,板子就成砖了。我曾经就干过这种事,还好有JTAG救回来。

3.2.3 调试U-Boot

U-Boot启动时,串口输出是最重要的调试手段。如果串口没输出,检查以下几点:

  • 串口引脚配置是否正确(GPIO复用)
  • 波特率是否匹配(通常是115200)
  • 时钟源是否使能(UART模块时钟)

我曾经遇到一个坑:板子的串口芯片是MAX3232,但原理图上标的是SP3232,电平标准不一样。折腾了半天才发现是硬件问题。所以,拿到新板子,先拿示波器看串口波形,这是最直接的方法。

3.3 内核配置与编译

内核配置,说白了就是决定“内核里要装哪些功能”。功能越多,内核越大,启动越慢。嵌入式系统讲究“够用就好”,所以裁剪是必修课。

3.3.1 获取内核源码

我建议从kernel.org下载长期支持版(LTS),比如5.10.x或6.1.x。这些版本维护周期长,社区支持好。

# 下载内核
wget https://cdn.kernel.org/pub/linux/kernel/v6.x/linux-6.1.38.tar.xz
tar -xf linux-6.1.38.tar.xz
cd linux-6.1.38

3.3.2 配置内核

内核配置有三种方式:

配置方式 命令 适用场景
默认配置 make defconfig 快速开始,但功能冗余
基于现有配置 make oldconfig 升级内核版本时使用
图形化配置 make menuconfig 最常用,交互式裁剪

我个人习惯先用defconfig生成一个基础配置,再用menuconfig进行裁剪。这样效率最高。

3.3.3 关键配置项

嵌入式系统需要特别关注的配置项:

  • Processor type and features:选择正确的CPU型号,开启SMP(多核)支持
  • General setup:控制组(Cgroups)、命名空间(Namespace)——容器化必备
  • Device Drivers:只保留板子上实际用到的外设驱动,比如网卡、I2C、SPI、GPIO
  • File systems:根据存储介质选择,NAND Flash用UBIFS,eMMC用ext4
  • Networking support:如果做网关,TCP/IP协议栈、Netfilter、Bridge、VLAN都要开启
避坑指南:我曾经在裁剪内核时,把“RAM block device support”给去掉了,结果initramfs无法加载,系统启动到一半就卡住了。所以,不确定的功能不要乱删,先保留,等系统跑起来了再逐步裁剪。

3.3.4 编译内核

配置完成后,开始编译:

# 设置交叉编译工具链
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-

# 编译内核镜像
make zImage -j4

# 编译设备树
make dtbs

# 编译内核模块
make modules

# 安装内核模块到目标目录
make modules_install INSTALL_MOD_PATH=/path/to/rootfs

编译完成后,你会得到两个关键文件:

  • arch/arm/boot/zImage:压缩后的内核镜像
  • arch/arm/boot/dts/你的板子.dtb:设备树二进制文件

3.3.5 启动测试

把内核和设备树放到U-Boot能访问的地方(比如SD卡或TFTP服务器),然后通过U-Boot启动:

# U-Boot命令行
setenv bootargs 'console=ttymxc0,115200 root=/dev/mmcblk0p2 rw'
fatload mmc 0:1 0x80800000 zImage
fatload mmc 0:1 0x83000000 imx6ull-myboard.dtb
bootz 0x80800000 - 0x83000000

如果串口输出了内核启动信息,最后出现了“Welcome to Buildroot”或者你定制的文件系统提示符,那就说明系统构建成功了。

3.4 本章小结

嵌入式Linux系统构建,说白了就是三件事:

  • 工具链:让x86电脑能编译出ARM程序
  • U-Boot:让板子能启动并加载内核
  • 内核:裁剪出最适合你硬件的操作系统核心

每一步都有坑,但踩过一次就记住了。我刚开始做的时候,光工具链就折腾了一周。现在回头看,那些坑恰恰是最宝贵的经验。嗯,下一章我们聊聊根文件系统的构建,那是系统启动的最后一块拼图。