4、嵌入式Linux系统:交叉编译环境搭建、内核裁剪与编译、根文件系统制作、设备驱动移植
好,咱们进入第四章。这一章是实战的硬骨头,也是LTE网关方案里最核心的底层环节。说白了,你要让一个没有操作系统的裸板跑起Linux,就得过这四关:交叉编译环境、内核裁剪、根文件系统、驱动移植。
我刚开始做嵌入式那会儿,觉得交叉编译就是个“在电脑上编译,在板子上跑”的事儿。结果第一次自己搭环境,光一个工具链就折腾了两天。嗯,这里面的坑,我一个个给你说清楚。
本章核心目标:在x86主机上为ARM架构的LTE网关主控芯片(比如海思Hi3520或NXP i.MX6系列)构建完整的嵌入式Linux系统,并成功移植LTE模块驱动。
4.1 交叉编译环境搭建
为什么需要交叉编译?你想想看,你的开发电脑是x86架构,但网关板子是ARM架构。x86编译出来的二进制,ARM跑不了。所以我们需要在x86上安装一个“能生成ARM机器码的编译器”,这就是交叉编译工具链。
4.1.1 工具链的选择
我个人习惯用Linaro提供的GCC工具链,稳定且社区活跃。当然,芯片原厂也会提供自己的工具链,比如海思的HiTools。这里我推荐两种方式:
| 方式 | 工具链 | 适用场景 | 我的建议 |
|---|---|---|---|
| 官方预编译 | gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf | 快速开发,通用ARM Cortex-A系列 | 新手首选,省去编译工具链的麻烦 |
| Buildroot | 自动构建工具链+根文件系统 | 需要定制化根文件系统时 | 我项目中常用,一体化解决 |
| Yocto | 全功能嵌入式Linux发行版 | 大型项目,需要包管理 | 学习曲线陡,但功能强大 |
下载Linaro工具链后,解压到 /opt 目录,然后配置环境变量:
# 解压工具链
tar -xvf gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf.tar.xz -C /opt/
# 配置环境变量(建议写入 ~/.bashrc)
export PATH=/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin:$PATH
export ARCH=arm
export CROSS_COMPILE=arm-linux-gnueabihf-
# 验证是否安装成功
arm-linux-gnueabihf-gcc --version
小技巧:我曾经在配置环境变量时,忘记把 export 写入 ~/.bashrc,结果每次新开终端都要重新设置。建议你一次性配好,省得后面抓狂。
4.1.2 测试交叉编译
写一个简单的Hello World测试一下:
// hello.c
#include <stdio.h>
int main() {
printf("Hello from LTE Gateway!\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 字样,说明工具链工作正常。这个文件在x86电脑上是跑不了的,得放到板子上才能执行。
4.2 内核裁剪与编译
LTE网关不需要桌面系统,也不需要复杂的网络协议栈。内核裁剪的目的就是“去掉不需要的,留下刚需的”。我见过有人直接把全功能内核编译出来,结果镜像30多MB,Flash根本放不下。
4.2.1 获取内核源码
从芯片厂商获取内核源码包,或者从kernel.org下载主线内核。以海思Hi3520为例:
# 解压内核源码
tar -xvf linux-4.9.37.tar.gz
cd linux-4.9.37
# 配置内核(使用厂商提供的默认配置)
make ARCH=arm hi3520d_defconfig
# 或者手动配置
make ARCH=arm menuconfig
4.2.2 裁剪要点
进入 menuconfig 后,我一般按以下步骤裁剪:
- 关闭无用架构支持:只保留ARM架构,其他全部取消。
- 精简驱动:只保留串口、网卡、I2C、SPI、Flash控制器、USB Host等LTE模块需要的驱动。
- 关闭调试选项:
Kernel hacking里的调试选项全部关掉,能省不少空间。 - 网络协议栈:保留TCP/IP、UDP、PPP(如果LTE模块用PPP拨号),关闭IPv6(如果不需要)。
注意:我曾经在裁剪时,不小心把 EXT4 文件系统支持给关了,结果根文件系统挂载不上,板子直接kernel panic。裁剪时一定要确认你用的根文件系统类型对应的驱动是开启的。
4.2.3 编译内核
# 编译内核镜像
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4 uImage
# 编译设备树(Device Tree)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- hi3520d.dtb
# 编译内核模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules
编译完成后,在 arch/arm/boot/ 目录下会生成 uImage 和 hi3520d.dtb。这两个文件就是我们要烧录到板子上的核心文件。
4.3 根文件系统制作
内核启动后,需要挂载根文件系统。根文件系统里包含init进程、库文件、配置文件、应用程序等。说白了,它就是Linux的“家”。
4.3.1 使用BusyBox构建最小根文件系统
BusyBox是嵌入式Linux的瑞士军刀,一个二进制文件集成了上百个Linux命令。我项目中90%的场景都用它。
# 下载BusyBox
wget https://busybox.net/downloads/busybox-1.33.2.tar.bz2
tar -xvf busybox-1.33.2.tar.bz2
cd busybox-1.33.2
# 配置BusyBox
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
# 选择:Settings -> Build static binary (no shared libs)
# 静态编译,避免动态库依赖问题
# 编译并安装
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- install CONFIG_PREFIX=/home/user/rootfs
安装完成后,/home/user/rootfs 目录下会生成 bin、sbin、usr 等目录。但这还不够,还需要手动创建一些关键目录和文件:
cd /home/user/rootfs
mkdir -p dev etc lib proc sys tmp var mnt
# 创建必要的设备节点
sudo mknod dev/console c 5 1
sudo mknod dev/null c 1 3
# 创建init脚本(/etc/inittab)
cat > etc/inittab << "EOF"
::sysinit:/etc/init.d/rcS
::askfirst:-/bin/sh
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
EOF
# 创建rcS启动脚本
mkdir -p etc/init.d
cat > etc/init.d/rcS << "EOF"
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
mount -t tmpfs none /tmp
/sbin/mdev -s
EOF
chmod +x etc/init.d/rcS
4.3.2 制作文件系统镜像
根文件系统制作完成后,需要打包成镜像文件,烧录到Flash中。常用的格式有 jffs2、yaffs2、squashfs。我推荐使用 squashfs,只读压缩,节省空间且稳定。
# 安装squashfs工具
sudo apt-get install squashfs-tools
# 制作squashfs镜像
mksquashfs /home/user/rootfs rootfs.squashfs -comp xz -b 131072
避坑指南:我曾经用 jffs2 格式,结果板子掉电后文件系统损坏,导致无法启动。后来换成 squashfs + 可写分区(overlay)的方案,再也没出过问题。
4.4 设备驱动移植
LTE网关最关键的驱动就是LTE模块的驱动。以华为ME909s-821模块为例,它通过USB接口连接主控,需要移植USB串口驱动和PPP协议驱动。
4.4.1 确认内核驱动支持
在 menuconfig 中确认以下选项已开启:
Device Drivers -> USB support -> USB Serial Converter supportDevice Drivers -> USB support -> USB Serial -> USB driver for GSM and CDMA modems(选项名可能为USB_ACM或option)Device Drivers -> Network device support -> PPP (point-to-point protocol) support
4.4.2 编写驱动测试程序
驱动移植完成后,写一个简单的测试程序验证LTE模块是否正常工作:
// lte_test.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
int main() {
int fd;
char buffer[256];
// 打开LTE模块对应的串口设备
fd = open("/dev/ttyUSB0", O_RDWR | O_NOCTTY);
if (fd < 0) {
perror("Failed to open LTE module");
return -1;
}
// 发送AT指令测试
write(fd, "AT\r\n", 4);
sleep(1);
read(fd, buffer, sizeof(buffer));
printf("Response: %s\n", buffer);
// 查询信号强度
write(fd, "AT+CSQ\r\n", 8);
sleep(1);
read(fd, buffer, sizeof(buffer));
printf("Signal: %s\n", buffer);
close(fd);
return 0;
}
// 交叉编译
arm-linux-gnueabihf-gcc -o lte_test lte_test.c
4.4.3 驱动自动加载配置
为了让LTE模块在系统启动时自动识别,需要在根文件系统中添加udev规则或mdev配置:
# 在/etc/mdev.conf中添加
ttyUSB0 0:0 666
ttyUSB1 0:0 666
ttyUSB2 0:0 666
注意:不同LTE模块的USB串口数量不同。ME909s会虚拟出3个串口:ttyUSB0用于AT指令,ttyUSB1用于PPP拨号,ttyUSB2用于日志。我曾经因为只配置了ttyUSB0,结果PPP拨号一直失败,排查了半天才发现是ttyUSB1没权限。
4.5 本章知识体系
下面这张图总结了嵌入式Linux系统构建的完整流程,你可以对照着梳理自己的思路:
4.6 常见问题与避坑
最后,我把这些年遇到的典型问题列出来,你遇到了可以直接对照排查:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
编译内核时报错 arm-linux-gnueabihf-gcc: command not found |
环境变量未配置或路径错误 | 检查 ~/.bashrc 中的 PATH 设置,重新 source |
内核启动后 Kernel panic - not syncing: VFS: Unable to mount root fs |
根文件系统驱动未编译进内核 | 确认 menuconfig 中开启了对应的文件系统支持(如SquashFS) |
| LTE模块AT指令无响应 | 串口权限不足或驱动未加载 | 检查 /dev/ttyUSB* 权限,确认内核模块已加载 |
PPP拨号失败,日志显示 Timeout |
APN配置错误或SIM卡未注册网络 | 用AT指令 AT+COPS? 检查网络注册状态 |
嗯,这一章的内容就到这里。嵌入式Linux系统的构建是个熟能生巧的过程,第一次可能磕磕绊绊,但多来几次就顺手了。记住,交叉编译环境是地基,内核是骨架,根文件系统是血肉,驱动是神经——四者缺一不可。