2、嵌入式Linux系统基础:系统架构、交叉编译、Bootloader、Kernel、Rootfs

好,咱们正式开始动手之前,得先把地基打牢。很多新手朋友一上来就急着编译内核、烧写镜像,结果遇到问题一脸懵。我当年也干过这事儿,折腾了三天发现是交叉编译工具链版本不对。所以,这一章咱们把嵌入式Linux的骨架理清楚。

2.1 嵌入式Linux系统架构:从硬件到应用的四层结构

说白了,一个能跑的嵌入式Linux系统,就四层:

  1. 硬件层:CPU、内存、Flash、网口、WiFi模组……这是物理基础。
  2. Bootloader:上电后第一个跑的程序。它负责初始化硬件,把内核从Flash搬到内存里。
  3. Kernel:操作系统的核心。管理进程、内存、驱动,提供系统调用。
  4. Rootfs:根文件系统。里面放着各种库、配置文件、应用程序。没有它,内核就是个光杆司令。

你想想看,上电那一刻,CPU先执行固化在ROM里的启动代码,然后跳转到Bootloader。Bootloader把内核加载到内存,内核再挂载Rootfs,最后启动你的应用程序。这个流程,我建议你刻在脑子里。

核心要点:这四层缺一不可。每一层都有独立的编译和配置方法,但最终要协同工作。我在项目中遇到过最头疼的问题,就是Bootloader和内核的地址配置对不上,导致系统死活起不来。

2.2 交叉编译环境搭建:为什么不能在板子上直接编译?

这个问题我经常被问到。答案很简单:性能不够。你的CPE开发板可能只有几百兆的CPU和128MB的内存,编译一个完整的内核要十几个小时。而你的PC是x86架构,性能强得多。

所以,我们在PC上安装交叉编译工具链。它运行在x86上,但生成的目标代码是ARM或MIPS架构的。嗯,这里要注意:工具链的版本必须和你的内核版本匹配。我曾经因为用了太新的GCC,编译出来的内核在旧版内核上跑出了各种诡异的内存错误。

搭建步骤其实不复杂:

# 以ARM Cortex-A7为例,下载Linaro工具链
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 -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/

# 设置环境变量
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm

个人习惯:我会把环境变量写到 ~/.bashrc 里,这样每次打开终端就不用重新设置了。另外,我建议你用一个专门的目录来存放所有工具链,方便管理。

2.3 Bootloader:系统的引路人

Bootloader是上电后第一个程序。它干三件事:

  • 初始化硬件(时钟、DDR、串口)
  • 从存储设备(NAND、eMMC、SD卡)读取内核镜像
  • 跳转到内核入口地址

在5G CPE领域,最常用的是U-Boot。它功能强大,支持网络启动、Flash操作、环境变量保存。我个人习惯把U-Boot的环境变量存在SPI Flash里,这样即使主存储坏了,还能通过网络救砖。

编译U-Boot也很直接:

make distclean
make <你的板子配置>_defconfig
make -j4

编译完成后,你会得到 u-boot.binu-boot.img。烧写到板子的启动介质上就行。

避坑指南:我曾经因为U-Boot的DTS(设备树)文件没配置对,导致网口驱动加载失败。调试了整整一天才发现是GPIO复用设置错了。所以,拿到一块新板子,第一件事就是确认U-Boot的DTS是否正确。

2.4 Kernel:操作系统的灵魂

内核是Linux的核心。它管理所有硬件资源,提供进程调度、内存管理、网络协议栈、文件系统支持。对于5G CPE来说,内核需要支持:

  • USB 3.0(连接5G模块)
  • 千兆以太网
  • WiFi(AP模式)
  • 网络协议栈(特别是IPv6和QoS)

编译内核的步骤:

make distclean
make <你的板子配置>_defconfig
make menuconfig   # 这里可以勾选你需要的驱动和功能
make -j4 zImage
make dtbs          # 编译设备树
make modules       # 编译内核模块

你想想看,内核配置有几千个选项,新手很容易迷失。我的建议是:先找一个和你板子最接近的默认配置,然后在此基础上修改。不要从头开始配,那是自找麻烦。

关键点:内核编译完成后,你会得到 zImage(内核镜像)和一堆 .dtb 文件(设备树)。设备树描述了板子的硬件信息,比如哪个GPIO控制LED、哪个I2C总线上挂了传感器。没有正确的设备树,内核就不知道该怎么和硬件打交道。

2.5 Rootfs:让系统活起来

Rootfs是根文件系统。它包含了:

  • /bin/sbin:基本命令(ls、cp、ifconfig)
  • /lib:动态链接库(libc、libm)
  • /etc:配置文件(网络、服务)
  • /usr:用户程序
  • /var:运行时数据(日志、临时文件)

构建Rootfs有三种常见方式:

方式 优点 缺点
Busybox 体积小(几MB),适合资源受限设备 功能精简,缺少高级特性
Buildroot 自动化构建,支持包管理 学习曲线稍陡
Yocto 高度可定制,企业级方案 编译时间长,配置复杂

对于5G CPE,我推荐用Buildroot。它既能保持较小的体积,又能方便地添加OpenWrt的软件包(比如防火墙、QoS、VPN)。

一个最小的Rootfs大概长这样:

/
├── bin
│   ├── busybox    # 集成了上百个命令
│   └── sh         # shell
├── dev
│   ├── console
│   └── null
├── etc
│   ├── inittab    # 初始化进程配置
│   └── fstab      # 文件系统挂载表
├── lib
│   ├── ld-linux.so.3
│   └── libc.so.6
├── proc           # 挂载点
├── sys            # 挂载点
└── usr
    └── bin

我的经验:第一次做Rootfs时,我建议你用Busybox快速搭一个能跑起来的系统。等所有功能验证通过后,再换成Buildroot或Yocto做精细化定制。这样能避免一开始就被复杂的配置搞晕。

2.6 四者如何协同工作?

最后,咱们串一下整个流程:

  1. 上电,CPU执行固化代码,初始化DDR和时钟。
  2. 跳转到U-Boot。U-Boot从SPI Flash或eMMC读取内核镜像和设备树。
  3. U-Boot把内核加载到内存,设置启动参数(比如根文件系统在哪里),然后跳转到内核入口。
  4. 内核解压自身,初始化驱动,挂载Rootfs。
  5. 内核启动第一个用户进程(通常是 /sbin/init),init根据 /etc/inittab 启动各种服务。
  6. 最终,你的5G拨号程序、Web管理界面、防火墙都跑起来了。

你看,每一步都环环相扣。任何一个环节出问题,系统都起不来。所以,我建议你从Bootloader开始,一步步验证。先确保串口能打印信息,再确认内核能启动,最后才去折腾Rootfs里的应用程序。

最后提醒:调试嵌入式系统,串口是你的眼睛。没有串口输出,你就像在黑暗中摸索。所以,第一件事就是确保串口驱动和波特率设置正确。我见过太多人卡在这一步了。