第一章 嵌入式Linux开发环境搭建

做嵌入式Linux开发,第一件事就是把环境搭好。这活儿看着基础,但坑不少。我记得刚入行那会儿,光装个交叉编译工具链就折腾了两天——版本不对、路径配错、库文件缺失,各种问题轮着来。后来带团队时发现,新人卡在环境搭建上的时间,往往比写代码还多。

所以这一章,咱们把交叉编译工具链、TFTP/NFS服务、开发板启动流程这三个核心点彻底讲透。你跟着走一遍,后面写驱动、移植系统时就能少踩很多坑。

1.1 交叉编译工具链安装

什么是交叉编译?说白了,就是在你的PC上编译出能在ARM开发板上运行的程序。PC是x86架构,开发板是ARM架构,指令集不同,所以需要专门的工具链。

核心概念:交叉编译工具链 = 编译器 + 链接器 + 库文件 + 头文件。常见的有arm-linux-gnueabihf-gcc、aarch64-linux-gnu-gcc等。

1.1.1 工具链的选择

我个人习惯用Linaro提供的预编译工具链,稳定且社区活跃。当然你也可以自己用crosstool-NG从头编译,但那太费时间了,除非你有特殊需求。

架构 工具链前缀 适用场景
ARM 32位 arm-linux-gnueabihf- Cortex-A7/A8/A9,带硬件浮点
ARM 64位 aarch64-linux-gnu- Cortex-A53/A72,树莓派3B+以上
ARM 软浮点 arm-linux-gnueabi- 老旧ARM9,不带VFP单元

1.1.2 安装步骤

以arm-linux-gnueabihf为例,我一般这样装:

# 下载Linaro工具链(以7.5版本为例)
wget https://releases.linaro.org/components/toolchain/binaries/7.5-2019.12/arm-linux-gnueabihf/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz

# 解压到/opt目录
sudo tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/

# 添加环境变量
echo 'export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin' >> ~/.bashrc
source ~/.bashrc

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

我曾经踩过的坑:工具链版本与内核版本不匹配。比如用gcc 9.x编译Linux 3.10内核,会出现一堆语法错误。建议工具链版本与内核大版本对应——内核4.x用gcc 7.x,内核5.x用gcc 9.x以上。

1.1.3 测试交叉编译

写个简单的hello world试试:

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

int main(void)
{
    printf("Hello, Embedded Linux!\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字样,说明编译成功了。这个文件不能在PC上直接运行,得放到开发板上跑。

1.2 TFTP/NFS服务配置

开发阶段,你不可能每次修改代码都重新烧写整个系统。TFTP和NFS就是用来解决这个问题的——TFTP传内核和设备树,NFS挂载根文件系统。

1.2.1 TFTP服务器配置

TFTP是个轻量级的文件传输协议,常用于uboot引导时加载内核镜像。我习惯用tftp-hpa这个实现:

# 安装TFTP服务器
sudo apt-get install tftp-hpa tftpd-hpa

# 创建TFTP根目录
sudo mkdir -p /tftpboot
sudo chmod 777 /tftpboot

# 修改配置文件 /etc/default/tftpd-hpa
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/tftpboot"
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="-l -c -s"

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

小技巧:把编译好的zImage和.dtb文件复制到/tftpboot目录,uboot里用tftp命令就能直接加载。我在项目中经常这样调试内核,比反复烧写快多了。

1.2.2 NFS服务器配置

NFS用于共享根文件系统。开发板通过网络挂载PC上的目录作为根目录,这样你修改PC上的文件,开发板立即可用。

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

# 创建共享目录(假设你的根文件系统在/nfs/rootfs)
sudo mkdir -p /nfs/rootfs

# 修改配置文件 /etc/exports
/nfs/rootfs *(rw,sync,no_subtree_check,no_root_squash)

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

注意:no_root_squash这个选项很重要。不加的话,开发板上的root用户会被映射成nobody,很多操作会权限不足。我曾经因为这个排查了半天,最后发现是权限问题。

1.2.3 开发板端配置

在uboot中设置启动参数,让内核从NFS挂载根文件系统:

# uboot环境变量设置示例
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.10
setenv gatewayip 192.168.1.1
setenv netmask 255.255.255.0

# 从TFTP加载内核和设备树
tftp 0x42000000 zImage
tftp 0x43000000 board.dtb

# 设置内核启动参数
setenv bootargs 'console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.10:/nfs/rootfs rw ip=192.168.1.100'

# 启动内核
bootz 0x42000000 - 0x43000000

1.3 开发板启动流程分析

理解启动流程,是系统移植的基础。你想想看,从按下电源到进入Linux shell,中间经历了多少环节?

嵌入式Linux启动流程 上电复位 BootROM SPL/MLO uboot Linux内核 根文件系统 各阶段说明 BootROM: 固化在芯片内部 SPL: 初始化DDR和时钟 uboot: 加载内核和设备树 内核: 解压并初始化硬件 根文件系统: 挂载并启动init

1.3.1 各阶段详解

  1. 上电复位:CPU从复位向量地址开始执行。ARM Cortex-A系列通常从0x00000000或0xFFFF0000开始。
  2. BootROM:芯片内部固化的代码,负责初始化基本硬件(如时钟、看门狗),然后根据启动引脚选择启动介质(SD卡、NAND Flash、eMMC等)。
  3. SPL/MLO:Secondary Program Loader,是uboot的第一阶段。它初始化DDR控制器和基本外设,然后从存储介质加载完整的uboot到DDR中。
  4. uboot:完整的引导加载程序。它提供命令行交互,加载内核镜像和设备树到内存,然后跳转到内核入口。
  5. Linux内核:解压后初始化MMU、中断控制器、定时器等核心硬件,然后挂载根文件系统。
  6. 根文件系统:内核启动init进程(通常是systemd或BusyBox init),然后启动用户空间服务。

关键点:uboot到内核的跳转,是通过一个叫"bootm"或"bootz"的命令完成的。内核启动时,uboot会传递一个参数结构体(ATAG或设备树),告诉内核内存大小、控制台参数、根文件系统位置等信息。

1.3.2 常见启动介质对比

介质 速度 可靠性 成本 典型应用
NAND Flash 慢 需坏块管理 低 低成本嵌入式设备
eMMC 快 高 中 工业控制、消费电子
SD卡 中 中 低 开发调试阶段
SPI Flash 极慢 高 低 存储uboot/SPL

我的经验:开发阶段用SD卡或TFTP/NFS启动最方便。产品阶段建议用eMMC,速度快且稳定。NAND Flash虽然便宜,但坏块管理很麻烦,我吃过不少亏。

1.4 环境验证

环境搭好了,怎么验证是否正常?我一般按这个顺序检查:

  1. 工具链:编译一个hello world,用file命令确认是ARM格式。
  2. TFTP:在开发板uboot中执行tftp 0x42000000 zImage,看能否正常下载。
  3. NFS:内核启动后,执行mount命令,确认根文件系统是NFS挂载的。
  4. 启动流程:观察串口输出,确认每个阶段都正常打印。

我曾经遇到的一个典型问题:TFTP能下载但内核启动卡住。排查后发现是设备树文件与内核版本不匹配。设备树里描述的外设地址,内核里找不到对应的驱动。所以一定要确保内核、设备树、工具链三者版本一致。

好了,环境搭建这部分就讲到这里。你把这些步骤走一遍,后面写驱动、移植系统时就能专注于代码本身,不用再为环境问题分心了。