2、嵌入式Linux基础:嵌入式Linux系统架构、交叉编译环境搭建、Bootloader与内核启动流程

好,咱们正式开始聊嵌入式Linux在小基站里的那些事。这一章,我打算把最基础的东西先捋清楚。你想想看,小基站说白了就是一台专用的嵌入式计算机,跑着Linux系统。那这个系统到底长什么样?我们怎么给它编译程序?它上电后又是怎么活过来的?这三个问题,就是本章要啃的硬骨头。

2.1 嵌入式Linux系统架构:分层拆解

嵌入式Linux系统,说白了就是四层结构。我习惯从下往上讲,这样符合硬件上电的逻辑。

  • 硬件层:CPU、内存、Flash、网口、基带芯片。小基站里通常用ARM Cortex-A系列或者MIPS架构的SoC。
  • Bootloader层:负责初始化硬件,加载内核。小基站里最常见的是U-Boot。
  • 内核层:Linux内核,提供进程调度、内存管理、网络协议栈。小基站对实时性有要求,所以内核通常会打上RT-Preempt补丁。
  • 用户空间层:文件系统、应用程序、库文件。小基站的核心应用——L2/L3协议栈、PHY控制程序,都跑在这一层。

重要: 这四层不是孤立的。硬件给Bootloader提供寄存器地址,Bootloader给内核传递设备树,内核给用户空间提供系统调用。每一层都依赖下一层的服务。

我在项目中遇到过一个问题:某款小基站启动到一半就卡死了。查了两天,最后发现是内核和设备树不匹配。设备树里描述的内存大小,和硬件实际焊接的内存颗粒不一致。嗯,这种低级错误,一旦出现就很要命。

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

你可能会问:为什么不在小基站板子上直接编译代码?原因很简单——板子资源太有限了。小基站的CPU主频可能只有1GHz,内存256MB,Flash 128MB。你想想看,一个完整的Linux内核编译,需要几GB的临时空间,耗时几十分钟。这谁受得了?

所以,我们采用交叉编译。在性能强劲的PC(x86架构)上,编译出目标板(ARM架构)能运行的二进制文件。

搭建交叉编译环境,我建议按以下步骤来:

  1. 选择工具链:ARM官方提供gcc-arm-none-eabi(裸机)和gcc-arm-linux-gnueabihf(带Linux系统)。小基站一般用后者。
  2. 安装工具链:下载预编译的二进制包,解压到/opt目录。我个人习惯用Linaro提供的版本,稳定且更新及时。
  3. 设置环境变量:把工具链的bin目录加入PATH。比如:export PATH=$PATH:/opt/gcc-arm-linux-gnueabihf/bin
  4. 验证安装:运行arm-linux-gnueabihf-gcc --version,看到版本信息就对了。

提示: 我曾经踩过一个坑——工具链版本太新,导致编译出的内核在旧版U-Boot上无法启动。后来我学乖了,工具链版本尽量和芯片厂商的SDK保持一致。

这里给一个简单的交叉编译示例,编译一个hello world程序:

// hello.c
#include <stdio.h>

int main() {
    printf("Hello, Small Cell!\n");
    return 0;
}

编译命令:

arm-linux-gnueabihf-gcc -o hello hello.c
file hello
# 输出:hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)

看到ARM字样,说明编译成功了。把这个hello文件拷贝到小基站的根文件系统里,就能直接运行。

2.3 Bootloader与内核启动流程:从按下电源到Shell出现

小基站按下电源键后,到底发生了什么?我把它拆成几个阶段来讲。

2.3.1 Bootloader的第一阶段(SPL)

芯片上电后,CPU从固定的地址(通常是ROM里的BootROM)开始执行。BootROM会初始化最基本的硬件——时钟、DDR内存控制器,然后从Flash里加载SPL(Secondary Program Loader)。

SPL的代码非常精简,主要做两件事:

  • 初始化DDR内存
  • 从Flash加载完整的U-Boot到DDR中

注意: SPL的大小通常被限制在几十KB以内。所以它不能做复杂的事情,比如文件系统解析。它只能从固定的Flash偏移地址读取数据。

2.3.2 Bootloader的第二阶段(U-Boot)

U-Boot被加载到DDR后,开始执行。它会做更复杂的初始化:

  • 初始化串口(方便我们看打印信息)
  • 初始化网卡、Flash、SD卡等外设
  • 解析设备树(DTB)
  • 加载内核镜像(zImage或Image)到内存

U-Boot启动时,你会看到类似这样的打印信息:

U-Boot 2020.01 (Jan 15 2024 - 10:30:00 +0800)

CPU:   ARM Cortex-A53 @ 1.2GHz
DRAM:  256 MiB
NAND:  128 MiB
MMC:   mmc@ff0f0000: 0
Net:   eth0: ethernet@ff0e0000

Hit any key to stop autoboot:  0

看到"Hit any key to stop autoboot"时,你有3秒时间按下任意键,进入U-Boot命令行。否则,U-Boot会自动执行bootcmd命令,启动内核。

我记得有一次,小基站死活起不来,串口打印到U-Boot就停了。后来发现是Flash里存储的内核镜像被意外擦除了。解决办法很简单——进入U-Boot命令行,用tftp从网络下载内核镜像,再写到Flash里。

2.3.3 内核启动

U-Boot把内核镜像加载到内存后,通过bootzbootm命令跳转到内核入口。内核开始执行后,会经历以下阶段:

  1. 解压内核:如果内核是压缩过的(zImage),先自解压。
  2. 架构初始化:设置页表、初始化中断控制器、初始化定时器。
  3. 设备树解析:内核读取U-Boot传递的设备树,了解硬件布局。
  4. 驱动初始化:按设备树描述,逐个初始化外设驱动。
  5. 挂载根文件系统:从Flash、SD卡或NFS挂载根文件系统。
  6. 启动init进程:执行/sbin/init,启动用户空间的第一个进程。

内核启动过程中,你会看到类似这样的打印:

[    0.000000] Booting Linux on physical CPU 0x0
[    0.000000] Linux version 5.10.120-rt60 (builder@buildserver) 
[    0.000000] CPU: ARMv8 Processor [410fd034] revision 4
[    0.000000] Memory: 256MB 128MB available
[    1.234567] mmc0: new high speed SD card at address 1234
[    1.345678] VFS: Mounted root (ext4 filesystem) on device 179:2.
[    1.456789] Freeing unused kernel memory: 1024K
[    1.567890] Run /sbin/init as init process

看到"Run /sbin/init as init process",说明内核已经成功启动,把控制权交给了用户空间。

2.3.4 用户空间初始化

init进程启动后,会读取配置文件(/etc/inittab或使用systemd),启动各种服务。小基站里,通常会启动以下关键服务:

  • 网络服务:配置IP地址,启动DHCP客户端或静态IP。
  • SSH服务:方便远程登录调试。
  • 小基站核心应用:L2/L3协议栈、OAM(操作维护管理)进程。

关键点: 小基站对启动时间有严格要求。3GPP标准要求基站从上电到提供服务,不能超过几分钟。所以,我们通常会优化启动流程——去掉不必要的内核模块、使用轻量级文件系统(如BusyBox)、并行启动服务。

我曾经优化过一款小基站的启动时间。从原来的45秒,压缩到18秒。怎么做的?说白了就是三招:内核裁剪(去掉不用的驱动)、使用initramfs(减少磁盘I/O)、服务并行启动。嗯,效果立竿见影。

2.4 本章小结

这一章,我们聊了嵌入式Linux的架构分层、交叉编译环境的搭建方法,以及从Bootloader到内核再到用户空间的完整启动流程。这些知识,是后面所有章节的基础。你想想看,如果连系统怎么启动的都不清楚,出了问题怎么排查?

下一章,我们会深入小基站的核心——LTE/5G协议栈在Linux上的实现。到时候,我会结合一个实际的开源项目,带你手把手跑通一个简化版的小基站协议栈。

个人建议: 学完这一章,最好找一块ARM开发板(比如树莓派或BeagleBone),亲手搭一遍交叉编译环境,编译一个内核,再写一个简单的U-Boot脚本。纸上得来终觉浅,绝知此事要躬行。