1. 嵌入式Linux驱动开发环境搭建

做驱动开发,第一件事不是写代码,而是把环境搭好。我见过太多新手上来就撸代码,结果编译报错半天找不到原因——最后发现是交叉编译链版本不对。说白了,环境就是地基,地基不稳,楼盖得再漂亮也得塌。

这一章我们就把地基打牢。我会带你一步步搞定三件事:交叉编译链安装、NFS/TFTP服务器配置、内核源码树构建。这三板斧抡好了,后面写驱动、调bug才能顺手。

1.1 交叉编译链安装

先说说什么是交叉编译。你在PC上写代码,PC的CPU是x86架构,但ARM开发板是ARM架构。你不能直接在PC上编译出ARM能跑的程序吗?不能。因为编译器生成的是x86指令。所以我们需要一个特殊的编译器——它跑在x86上,但生成ARM指令。这就是交叉编译链。

核心概念:交叉编译链 = 运行在宿主机(x86)上、生成目标机(ARM)可执行代码的工具集。

1.1.1 获取交叉编译链

常用的ARM交叉编译链有几种:

工具链名称 适用场景 获取方式
arm-linux-gnueabihf- ARM Cortex-A系列,硬件浮点 Linaro官网 / 芯片厂商SDK
arm-linux-gnueabi- ARM Cortex-A系列,软件浮点 Linaro官网 / 芯片厂商SDK
aarch64-linux-gnu- ARM64位(Cortex-A53等) Linaro官网 / 芯片厂商SDK
arm-none-eabi- 裸机开发(无操作系统) ARM官网 / GCC ARM Embedded

我个人习惯用Linaro的版本,更新及时,社区活跃。以arm-linux-gnueabihf为例,安装步骤很简单:

# 下载工具链(以gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf为例)
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

# 解压到指定目录
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

小技巧:解压路径我习惯放 /opt/ 下,方便多版本管理。别放 /usr/local/,容易和系统自带的工具链冲突。

1.1.2 验证工具链

装完别急着高兴,先写个hello world验证一下:

# 编写测试文件
echo '#include <stdio.h>
int main() {
    printf("Hello ARM!\n");
    return 0;
}' > test.c

# 交叉编译
arm-linux-gnueabihf-gcc test.c -o test_arm

# 查看生成的文件类型
file test_arm
# 输出应为:test_arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV)

看到"ARM"字样就对了。我曾经遇到一个坑——装完工具链后file命令显示的是x86格式,折腾半天发现是环境变量没生效,source了一下就好了。嗯,这种低级错误谁都会犯,别慌。

1.2 NFS/TFTP服务器配置

开发板和PC之间怎么传文件?两种方式最常用:NFS和TFTP。简单说,NFS适合挂载根文件系统,TFTP适合快速下载内核或dtb文件。

1.2.1 NFS服务器配置

NFS(Network File System)让开发板能直接访问PC上的目录,就像访问本地磁盘一样。调试驱动时,我把编译好的模块放到NFS共享目录,开发板直接insmod,省去反复烧写的麻烦。

# 安装NFS服务器(Ubuntu/Debian)
sudo apt-get install nfs-kernel-server

# 创建共享目录
sudo mkdir -p /srv/nfs/rootfs

# 配置导出文件
sudo vim /etc/exports
# 添加一行:
/srv/nfs/rootfs *(rw,sync,no_subtree_check,no_root_squash)

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

# 验证挂载
showmount -e localhost

注意:no_root_squash 这个选项很关键。不加的话,开发板上root用户访问共享目录会被映射成nobody,权限问题会让你怀疑人生。我刚开始学的时候就被这个坑过,搞了一下午才发现是权限问题。

开发板端挂载命令:

# 在开发板上执行
mount -t nfs -o nolock 192.168.1.100:/srv/nfs/rootfs /mnt

1.2.2 TFTP服务器配置

TFTP(Trivial File Transfer Protocol)比NFS更轻量,适合传输单个文件。U-Boot引导时经常用它下载内核镜像。

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

# 配置TFTP目录
sudo vim /etc/default/tftpd-hpa
# 修改为:
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="--secure"

# 创建目录并设置权限
sudo mkdir -p /srv/tftp
sudo chmod 777 /srv/tftp

# 重启服务
sudo systemctl restart tftpd-hpa

U-Boot中下载内核的典型命令:

# 在U-Boot命令行
tftp 0x80800000 zImage
tftp 0x83000000 imx6ull.dtb
bootz 0x80800000 - 0x83000000

经验之谈:TFTP的根目录权限要放开,不然传文件时报"Permission denied"很烦。另外,防火墙记得关或者开放69端口——我有一回调了一晚上,最后发现是ufw在作怪。

1.3 内核源码树构建

写驱动离不开内核源码树。说白了,内核源码树就是Linux内核的完整代码,驱动编译时需要它提供的头文件和Makefile规则。

1.3.1 获取内核源码

两种方式:从kernel.org下载主线内核,或者从芯片厂商获取定制内核。我建议初学者先用主线内核,干净、文档全。

# 下载内核源码(以4.19.123为例)
wget https://cdn.kernel.org/pub/linux/kernel/v4.x/linux-4.19.123.tar.xz

# 解压
tar -xvf linux-4.19.123.tar.xz -C /home/developer/kernel/

# 进入源码目录
cd /home/developer/kernel/linux-4.19.123

1.3.2 配置内核

内核配置是门学问。你可以从零开始配,也可以基于厂商的defconfig修改。

# 清理之前的配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- distclean

# 使用默认配置(以imx_v7_defconfig为例)
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_v7_defconfig

# 或者手动配置
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

menuconfig界面里,你可以按需开关驱动模块。我一般先把所有驱动都编成模块(M),调试时再决定哪些编进内核(Y)。

1.3.3 编译内核

# 编译内核镜像
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j4

# 编译设备树
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

# 编译内核模块
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules -j4

# 安装内核模块到NFS共享目录
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- modules_install INSTALL_MOD_PATH=/srv/nfs/rootfs

关键点:每次编译驱动前,确保内核源码树已经编译过一次。因为驱动编译依赖内核生成的头文件(如autoconf.h、version.h等)。我第一次写驱动时没编译内核就直接make,报了一堆"undefined reference",后来才发现要先编译内核。

1.3.4 构建驱动编译环境

驱动模块的Makefile怎么写?看这个模板:

# Makefile for external kernel module
obj-m := my_driver.o
my_driver-objs := main.o ioctl.o

KERNEL_DIR := /home/developer/kernel/linux-4.19.123
CROSS_COMPILE := arm-linux-gnueabihf-

all:
	make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) modules

clean:
	make -C $(KERNEL_DIR) M=$(PWD) ARCH=arm CROSS_COMPILE=$(CROSS_COMPILE) clean

编译驱动:

make
# 生成 my_driver.ko

把.ko文件拷到NFS共享目录,开发板上insmod就能加载了。

1.4 环境搭建知识体系

下面这张图把整个环境搭建的脉络理清楚了:

嵌入式Linux驱动开发环境搭建知识体系 交叉编译链 arm-linux-gnueabihf-gcc 环境变量配置 验证:file命令检查 NFS/TFTP服务器 NFS TFTP 挂载根文件系统 下载内核/dtb no_root_squash权限 内核源码树 获取源码(kernel.org) menuconfig配置 编译:zImage + dtbs 驱动开发环境就绪 编译驱动模块 调试与测试 故障定位 三个模块缺一不可:交叉编译链负责编译,NFS/TFTP负责传输,内核源码树提供编译依赖

1.5 常见问题与避坑

环境搭建过程中,有几个坑我几乎每次培训都会遇到:

  • 工具链版本不匹配:内核版本和工具链版本要对应。比如Linux 4.19用gcc 7.x没问题,但用gcc 9.x可能报错。我建议先查内核文档里的"Compiler versions"章节。
  • NFS挂载卡死:检查防火墙和网络。开发板和PC要在同一网段,IP不能冲突。我曾经把PC的IP设成192.168.1.100,开发板设成192.168.1.100——嗯,你猜怎么着?两台设备都ping不通。
  • TFTP传输超时:文件太大或网络不稳定。U-Boot的tftp命令默认超时时间短,可以加参数延长:tftp 0x80800000 zImage 10000000(最后一个参数是文件大小,单位字节)。
  • 内核编译报错"missing sys/...":没装必要的依赖包。Ubuntu上执行:sudo apt-get install build-essential libncurses-dev bison flex libssl-dev

重要提醒:每次修改内核配置后,一定要重新编译内核和模块,并且把新模块安装到NFS共享目录。否则开发板加载的还是旧模块,驱动行为可能和预期不一致。我吃过这个亏——改了个驱动参数,insmod后没效果,查了半天发现是模块没更新。

好了,环境搭建就到这里。这三样东西配好了,后面写驱动、调bug才能顺风顺水。记住:交叉编译链是武器,NFS/TFTP是运输线,内核源码树是兵工厂。三者缺一不可。