1、Linux驱动开发环境搭建:交叉编译工具链配置、内核源码树构建、NFS与TFTP调试环境搭建

说实话,很多初学者一上来就急着写代码,结果编译都过不了,或者烧进去跑不起来。我当年也踩过这个坑——花了两周调一个驱动,最后发现是工具链版本不对。所以,环境搭建这事,真不能马虎。

这一章,咱们就把开发环境从头捋一遍。说白了,就是三件事:交叉编译工具链内核源码树网络调试环境。搞定了这些,后面写驱动才能顺风顺水。

1.1 交叉编译工具链配置

为什么需要交叉编译?你想想看,你的开发机是x86架构,但目标板是ARM(或者RISC-V、MIPS)。x86上编译出来的程序,ARM跑不了。所以我们需要一套在x86上运行、但能生成ARM机器码的编译器——这就是交叉编译工具链。

1.1.1 获取工具链

我个人习惯用Linaro提供的工具链,稳定且社区支持好。以ARM Cortex-A系列为例:

# 下载ARM GCC工具链
wget https://releases.linaro.org/components/toolchain/binaries/latest-7/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/

# 添加环境变量
export PATH=$PATH:/opt/gcc-linaro-7.5.0-2019.12-x86_64_arm-linux-gnueabihf/bin
小提示:建议把环境变量写到 ~/.bashrc/etc/profile 里,避免每次开终端都要 export 一遍。

1.1.2 验证工具链

配置完以后,先别急着用。验证一下能不能正常工作:

arm-linux-gnueabihf-gcc --version
arm-linux-gnueabihf-gcc -v 2>&1 | grep Target

输出里应该能看到 Target: arm-linux-gnueabihf 字样。嗯,这里要注意:如果显示的是 x86_64 或者 i686,说明你用的还是本地编译器,路径没配对。

我曾经遇到过:有次客户反馈说编译出来的驱动在板子上段错误。查了两天,最后发现他用的是 arm-linux-gnueabi(软浮点)工具链,但内核配置了硬浮点。两种ABI不兼容,跑起来必崩。所以,工具链的ABI一定要跟内核匹配。

1.1.3 工具链的组成

一套完整的交叉工具链包含这些组件:

组件 说明 常见命令
编译器 将C/C++源码编译为汇编 arm-linux-gnueabihf-gcc
汇编器 将汇编转为机器码 arm-linux-gnueabihf-as
链接器 将目标文件链接为可执行文件 arm-linux-gnueabihf-ld
glibc/uClibc等C运行库 libc.so.6
调试器 远程调试目标板程序 arm-linux-gnueabihf-gdb

1.2 内核源码树构建

驱动开发离不开内核源码。你写的驱动模块,需要依赖内核头文件和编译框架。说白了,你得先有一套完整的内核源码树,才能编译出.ko文件。

1.2.1 获取内核源码

我建议直接从kernel.org下载主线内核,或者从芯片厂商(如NXP、TI、Rockchip)的官方仓库拉取。以Linux 5.10 LTS为例:

# 下载内核源码
wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.10.100.tar.xz

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

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

1.2.2 配置内核

内核配置有两种方式:

  • 使用厂商提供的默认配置make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- xxx_defconfig
  • 手动配置make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig

我个人习惯先用厂商的defconfig,再根据需要微调。比如:

# 以TI AM335x为例
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- omap2plus_defconfig

# 然后微调
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
核心要点:编译内核模块时,必须使用与目标板上运行的内核完全一致的源码树。版本号、配置选项、甚至编译器的版本差异,都可能导致模块加载失败。

1.2.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

# 安装模块到目标目录
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- INSTALL_MOD_PATH=/home/developer/rootfs modules_install

你想想看,如果每次改一行驱动代码都要重新编译整个内核,那效率得多低?所以,我们通常只编译内核一次,后面只编译模块。

1.3 NFS与TFTP调试环境搭建

驱动开发最痛苦的是什么?每次改完代码都要烧写、重启、插拔SD卡。太慢了。我建议用网络文件系统(NFS)和TFTP来加速调试。

1.3.1 TFTP服务器配置

TFTP用来传输内核镜像和设备树。Bootloader(如U-Boot)通过TFTP从开发机下载这些文件到板子的内存中运行。

# 安装TFTP服务器(Ubuntu/Debian)
sudo apt-get install tftpd-hpa

# 配置TFTP目录
sudo mkdir -p /srv/tftp
sudo chmod 777 /srv/tftp

# 编辑配置文件 /etc/default/tftpd-hpa
# 内容如下:
TFTP_USERNAME="tftp"
TFTP_DIRECTORY="/srv/tftp"
TFTP_ADDRESS="0.0.0.0:69"
TFTP_OPTIONS="--secure"

# 重启服务
sudo systemctl restart tftpd-hpa

然后把编译好的zImage和dtb文件拷贝到 /srv/tftp 目录下:

cp arch/arm/boot/zImage /srv/tftp/
cp arch/arm/boot/dts/am335x-boneblack.dtb /srv/tftp/

1.3.2 NFS服务器配置

NFS用来挂载根文件系统。板子启动后,通过NFS访问开发机上的rootfs,这样你修改驱动模块后,板子立刻就能用上新的.ko文件。

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

# 编辑 /etc/exports,添加共享目录
# 内容如下:
/home/developer/rootfs *(rw,sync,no_subtree_check,no_root_squash)

# 重启NFS服务
sudo systemctl restart nfs-kernel-server
注意:no_root_squash 这个选项很重要。如果不加,板子上的root用户会被映射为nobody,导致很多操作权限不足。我曾经因为这个配置,折腾了一下午才发现问题。

1.3.3 U-Boot启动参数设置

在板子的U-Boot命令行中,设置从TFTP和NFS启动:

# 设置IP地址(根据你的网络环境调整)
setenv ipaddr 192.168.1.100
setenv serverip 192.168.1.10

# 从TFTP下载内核和设备树
tftp 0x82000000 zImage
tftp 0x88000000 am335x-boneblack.dtb

# 设置内核启动参数,通过NFS挂载根文件系统
setenv bootargs console=ttyO0,115200 root=/dev/nfs nfsroot=192.168.1.10:/home/developer/rootfs rw ip=192.168.1.100:192.168.1.10:192.168.1.1:255.255.255.0::eth0:off

# 启动内核
bootz 0x82000000 - 0x88000000

嗯,这里要注意:IP地址、网关、NFS路径这些参数,一定要跟你实际网络环境一致。否则板子起不来。

1.3.4 调试流程

环境搭好以后,调试流程就变成了这样:

  1. 在开发机上修改驱动源码
  2. 交叉编译生成.ko文件
  3. 拷贝到NFS共享目录下的对应位置
  4. 在板子上用 insmodmodprobe 加载模块
  5. dmesg 查看内核日志
  6. 发现问题,回到第1步

整个过程不需要烧写、不需要重启。效率提升不是一点半点。

我的习惯:在开发机上写一个简单的脚本,一键完成编译、拷贝、通知板子重新加载模块。比如:
#!/bin/bash
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C /home/developer/kernel/linux-5.10.100 M=$(pwd) modules
cp *.ko /home/developer/rootfs/home/root/drivers/
echo "Done! Run 'modprobe -r mydriver && modprobe mydriver' on target."

小结

这一章的内容,说白了就是三件事:

  • 交叉编译工具链——让你在x86上编译出ARM能跑的程序
  • 内核源码树——驱动模块的编译依赖和框架
  • NFS+TFTP——让调试不再需要反复烧写

环境搭好了,后面写驱动才能心无旁骛。下一章,咱们开始真正接触驱动架构——字符设备驱动的基础框架。到时候我会分享一些我在实际项目中踩过的坑,保证让你少走弯路。