2、开发环境搭建:交叉编译工具链配置、内核源码树准备、设备树(Device Tree)基础、目标板与主机通信(NFS/TFTP)

好,咱们正式开始动手。这一章,说白了就是搭台子。台子搭不好,后面写再多代码也是白搭。我见过太多新手,上来就急着写驱动,结果编译都过不了,最后发现是工具链没配对。嗯,咱们别走这个弯路。

2.1 交叉编译工具链配置

嵌入式开发,目标板(比如ARM、RISC-V)性能有限,跑不了编译器。所以我们在强大的PC(x86)上编译,生成目标板能运行的二进制文件。这就是“交叉编译”。

我个人习惯用 Linaro 提供的 GCC 工具链,稳定且社区支持好。当然,你也可以用芯片原厂提供的 SDK 里自带的工具链。

核心思路: 工具链的命名通常遵循 arch-vendor-os-gnu 格式。比如 arm-linux-gnueabihf-gcc,其中:

  • arm:目标架构
  • linux:目标操作系统
  • gnueabihf:使用 glibc 库,且支持硬件浮点

配置步骤其实很简单,三步走:

  1. 下载并解压:从官网或芯片厂商处获取工具链压缩包,解压到 /opt//usr/local/ 目录下。
  2. 添加环境变量:将工具链的 bin 目录加入 PATH。我习惯在 ~/.bashrc 里加一行:
export PATH=$PATH:/opt/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/bin
  1. 验证安装:执行 arm-linux-gnueabihf-gcc --version,看到版本信息就对了。

我曾经踩过的坑: 有一次我图省事,直接用了系统自带的 gcc 编译内核模块,结果 insmod 到板子上直接报错“Invalid module format”。折腾了半天,才发现是宿主机和目标板的 GCC 版本不匹配。记住:编译内核模块的工具链,必须和编译内核镜像的工具链完全一致

2.2 内核源码树准备

驱动不是凭空写的,它需要依赖内核提供的头文件和 Makefile 规则。所以,你得先准备好一份内核源码树。

这里有两种情况:

  • 情况一:芯片厂商提供了完整的内核源码包(比如从 rockchipallwinner 的 GitHub 仓库拉下来)。直接解压即可。
  • 情况二:你自己从 kernel.org 下载主线内核。然后打上芯片厂商的 BSP 补丁。

不管哪种方式,拿到源码后,第一件事是 配置并编译一次内核。为什么?因为编译过程会生成一些必要的头文件(比如 autoconf.hversion.h),这些是编译外部模块时必需的。

# 以 ARM 架构为例
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4

你想想看,这一步其实不复杂,但很多人会忽略。我建议你编译完后,把 arch/arm/boot/zImagearch/arm/boot/dts/ 下的 .dtb 文件保存好,后面烧写要用。

小技巧: 如果你只是写驱动,不需要每次都全量编译内核。用 make modules_prepare 命令可以只生成模块编译所需的依赖,能省不少时间。

2.3 设备树(Device Tree)基础

设备树,说白了就是一张“硬件配置清单”。它告诉内核:你的板子上有哪些外设、它们挂在哪条总线上、中断号是多少、寄存器地址是多少。

以前(Linux 2.6 时代),这些信息都硬编码在 arch/arm/mach-xxx 的 C 文件里。每次换一个板子,就得改代码重新编译内核。后来大家觉得太蠢了,于是引入了设备树。

一个最简单的设备树节点长这样:

uart0: serial@101f1000 {
    compatible = "arm,pl011";
    reg = <0x101f1000 0x1000>;
    interrupts = <0 12 4>;
    clock-frequency = <24000000>;
    status = "okay";
};

我来解释一下:

  • compatible:这是最关键的属性。内核通过它来匹配驱动。比如这个 "arm,pl011",内核里就会去找 of_match_table 中包含 "arm,pl011" 的驱动。
  • reg:寄存器基地址和长度。驱动通过它来 ioremap
  • interrupts:中断号、触发类型等。
  • status:设为 "okay" 表示启用,"disabled" 表示禁用。

避坑指南: 我曾经在调试一个 I2C 驱动时,死活读不到设备。查了两天,最后发现是设备树里 reg 的地址写错了,少了一个 0。嗯,设备树里的地址都是十六进制,写的时候一定要仔细核对数据手册。

编译设备树也很简单:

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- dtbs

生成的 .dtb 文件就是二进制格式的设备树,烧写到板子上,U-Boot 会把它加载到内存,然后传递给内核。

2.4 目标板与主机通信(NFS/TFTP)

驱动写好了,怎么跑到板子上验证?总不能每次都插拔 SD 卡吧?太慢了。我推荐两种方式:NFS 和 TFTP。

2.4.1 TFTP:快速传输内核和设备树

TFTP 用于在开发阶段快速把 zImage.dtb 传到板子上。主机上需要安装 TFTP 服务器:

sudo apt-get install tftpd-hpa
# 配置文件在 /etc/default/tftpd-hpa
# 默认根目录是 /srv/tftp

把编译好的 zImage.dtb 丢到 /srv/tftp/ 下。然后在 U-Boot 命令行里:

tftp 0x42000000 zImage
tftp 0x43000000 myboard.dtb
bootz 0x42000000 - 0x43000000

你看,几秒钟就启动了一个新内核。效率比烧写快多了。

2.4.2 NFS:让板子共享主机的文件系统

TFTP 只能传内核,但根文件系统怎么办?用 NFS。板子通过网络挂载主机上的一个目录作为根文件系统。这样你在主机上修改驱动模块,板子上立刻就能用。

主机上配置 NFS 导出:

sudo apt-get install nfs-kernel-server
# 编辑 /etc/exports,添加一行:
/home/user/rootfs *(rw,sync,no_root_squash,no_subtree_check)
sudo exportfs -a

板子的 U-Boot 启动参数里加上:

root=/dev/nfs nfsroot=192.168.1.100:/home/user/rootfs ip=192.168.1.101

注意: 板子和主机的 IP 必须在同一网段。我遇到过好几次,板子死活挂不上 NFS,最后发现是防火墙没关。嗯,开发阶段建议直接 sudo ufw disable 省心。

有了 NFS,驱动模块的调试流程就变成了:

  1. 在主机上交叉编译驱动模块(.ko 文件)。
  2. .ko 文件复制到 NFS 导出的根文件系统目录下。
  3. 在板子上 insmod 加载模块,测试。
  4. 如果出问题,改代码,重复步骤 1-3。

整个过程不需要重启板子,也不需要重新烧写。我个人觉得,这是嵌入式驱动开发最舒服的工作流。

我的习惯: 我会在板子的 /etc/fstab 里把主机上的代码目录也挂载进来,这样板子上可以直接 cd /mnt/host_code 查看源码,方便调试时对照。

好了,环境搭好了。下一章,咱们就开始写第一个字符设备驱动。到时候你会发现,前面这些准备工作,花得值。