1. 嵌入式Linux开发环境搭建
做嵌入式Linux开发,第一件事就是把环境搭好。这活儿看着琐碎,但要是哪个环节没弄对,后面调试起来能把你折腾到怀疑人生。我这些年带过不少新人,十有八九的问题都出在环境上——不是工具链版本不对,就是网络服务没配好。
说白了,这一章就是帮你把地基打牢。咱们一步步来。
1.1 交叉编译工具链安装
先聊聊交叉编译。你想想看,你的开发机是x86架构,但目标板是ARM(或者RISC-V、MIPS),直接在PC上编译出来的程序,ARM芯片跑不了。所以我们需要一套能在x86上运行、但生成ARM机器码的编译器——这就是交叉编译工具链。
核心概念:交叉编译工具链 = 编译器(gcc) + 链接器(ld) + 库文件(glibc/uclibc) + 调试器(gdb) + 二进制工具(objdump, readelf等)
我个人习惯用Linaro提供的预编译工具链,省时省力。当然你也可以自己用crosstool-NG从头构建,但那属于进阶玩法,新手没必要折腾。
安装步骤(以ARM Cortex-A7为例)
- 下载工具链:从Linaro官网下载gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz
- 解压到指定目录:
tar -xvf gcc-linaro-*.tar.xz -C /opt/ - 添加环境变量:编辑
~/.bashrc,加入以下内容
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
- 验证安装:
arm-linux-gnueabihf-gcc --version
小技巧:我建议把工具链路径写在/etc/profile.d/下,这样所有用户都能用。曾经有个项目,同事把环境变量配在自己目录下,结果自动化构建脚本跑不起来,查了半天才发现是环境变量没生效。
工具链命名规则
| 字段 | 含义 | 示例 |
|---|---|---|
| arch | 目标架构 | arm, aarch64, riscv64 |
| vendor | 供应商/工具链构建者 | linaro, buildroot, none |
| os | 目标操作系统 | linux, none(bare-metal) |
| abi | 应用二进制接口 | gnueabi, gnueabihf |
这里有个坑——gnueabi和gnueabihf的区别。前者用软浮点,后者用硬浮点。如果你的芯片带硬件FPU(比如Cortex-A7),一定要选hf版本,性能差距能到30%以上。我刚开始做BSP移植时没注意这个,结果浮点运算慢得离谱,查了三天才发现是工具链选错了。
1.2 TFTP/NFS服务配置
开发阶段,你不可能每次修改都重新烧写整个镜像。太慢了。所以我们需要网络引导——TFTP用来传输内核和设备树,NFS用来挂载根文件系统。
说白了,就是开发板从网络启动,代码放在服务器上,改完直接重启就能看到效果。
TFTP服务器配置
TFTP是个极简的文件传输协议,没有认证,没有加密,但胜在轻量。U-Boot默认就支持它。
# 安装TFTP服务器(Ubuntu/Debian)
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 systemctl restart tftpd-hpa
注意:防火墙要放行UDP 69端口。我曾经在一个客户现场折腾了一下午,TFTP就是连不上,最后发现是机房防火墙策略把UDP包全丢了。嗯,从那以后我配完服务第一件事就是telnet localhost 69测连通性。
NFS服务器配置
NFS用来共享根文件系统。开发板上电后,内核启动,然后通过NFS挂载服务器上的目录作为/。
# 安装NFS服务器
sudo apt-get install nfs-kernel-server
# 编辑 /etc/exports,添加共享目录
/srv/nfs/rootfs 192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
# 重启服务
sudo systemctl restart nfs-kernel-server
这里no_root_squash很关键。不加这个选项,开发板上的root用户会被映射成nobody,写文件时会报权限错误。我见过有人因为这个折腾了一整天,最后发现是exports写错了。
开发板端U-Boot设置
在U-Boot命令行中,设置以下环境变量:
setenv serverip 192.168.1.100 # 服务器IP
setenv ipaddr 192.168.1.10 # 开发板IP
setenv netmask 255.255.255.0
setenv gatewayip 192.168.1.1
# 从TFTP加载内核和设备树
setenv bootcmd 'tftp 0x82000000 zImage; tftp 0x83000000 imx6ull.dtb; bootz 0x82000000 - 0x83000000'
# 通过NFS挂载根文件系统
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.1.100:/srv/nfs/rootfs,proto=tcp rw ip=192.168.1.10'
saveenv
boot
避坑指南:U-Boot的bootz命令只支持zImage格式。如果你用Image格式,要用booti。另外,有些SoC的DRAM起始地址不是0x82000000,比如全志系列常用0x42000000。查清楚你的芯片手册再填地址。
1.3 开发板启动流程分析
理解启动流程,是BSP移植的基础。你想想看,如果不知道芯片上电后先执行哪段代码,出了问题你连从哪儿查起都不知道。
嵌入式Linux的启动,大致分四个阶段:
- ROM Bootloader:芯片内部固件,不可修改
- SPL/MLO:二级引导程序,初始化DDR
- U-Boot:主引导程序,加载内核
- Linux Kernel:操作系统内核
下面这张图把整个流程串起来了:
各阶段详解
1. ROM Bootloader
芯片上电后,CPU从固定地址(通常是0x00000000或0xFFFF0000)开始执行。这段代码固化在芯片内部ROM里,你改不了。它做的事情很简单:初始化最基本的硬件(比如内部SRAM、看门狗),然后根据启动引脚的电平状态,决定从哪个介质加载下一级代码。
2. SPL/MLO
SPL(Secondary Program Loader)是U-Boot的精简版。为什么要有它?因为DDR还没初始化,芯片内部SRAM通常只有几十KB到几百KB,装不下完整的U-Boot。SPL的任务就是初始化DDR和时钟,然后把U-Boot从存储介质拷贝到DDR中。
关键点:SPL的大小受限于内部SRAM。比如i.MX6ULL的内部SRAM只有128KB,SPL必须小于这个尺寸。如果SPL编译出来超了,你得裁剪功能——去掉不必要的驱动、用更小的压缩算法。
3. U-Boot
U-Boot是真正的引导程序。它提供命令行交互,你可以手动加载内核、设置启动参数。U-Boot做的事情包括:
- 初始化更多外设(网卡、USB、显示等)
- 读取环境变量(从哪里加载内核、用什么参数)
- 从TFTP/NFS/SD卡/eMMC加载内核和设备树
- 跳转到内核入口点
4. Linux Kernel
内核启动后,会解压自身、初始化各个子系统(内存管理、进程调度、设备驱动等),最后挂载根文件系统,启动init进程。到这一步,系统就活起来了。
常见问题:内核启动到一半卡住,最常见的原因是设备树(DTB)和内核版本不匹配。我遇到过最离谱的一次,是设备树里GPIO的bank编号写错了,导致串口驱动初始化失败,控制台没有任何输出。嗯,从那以后我每次改设备树都会用dtc -I dtb -O dts反编译检查一遍。
1.4 本章小结
这一章我们干了三件事:装了交叉编译工具链、配了TFTP和NFS网络服务、理清了开发板的启动流程。这些都是嵌入式Linux开发的"基本功",后面每一章都会用到。