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 库,且支持硬件浮点
配置步骤其实很简单,三步走:
- 下载并解压:从官网或芯片厂商处获取工具链压缩包,解压到
/opt/或/usr/local/目录下。 - 添加环境变量:将工具链的
bin目录加入PATH。我习惯在~/.bashrc里加一行:
export PATH=$PATH:/opt/gcc-arm-9.2-2019.12-x86_64-arm-none-linux-gnueabihf/bin
- 验证安装:执行
arm-linux-gnueabihf-gcc --version,看到版本信息就对了。
我曾经踩过的坑: 有一次我图省事,直接用了系统自带的 gcc 编译内核模块,结果 insmod 到板子上直接报错“Invalid module format”。折腾了半天,才发现是宿主机和目标板的 GCC 版本不匹配。记住:编译内核模块的工具链,必须和编译内核镜像的工具链完全一致。
2.2 内核源码树准备
驱动不是凭空写的,它需要依赖内核提供的头文件和 Makefile 规则。所以,你得先准备好一份内核源码树。
这里有两种情况:
- 情况一:芯片厂商提供了完整的内核源码包(比如从
rockchip、allwinner的 GitHub 仓库拉下来)。直接解压即可。 - 情况二:你自己从 kernel.org 下载主线内核。然后打上芯片厂商的 BSP 补丁。
不管哪种方式,拿到源码后,第一件事是 配置并编译一次内核。为什么?因为编译过程会生成一些必要的头文件(比如 autoconf.h、version.h),这些是编译外部模块时必需的。
# 以 ARM 架构为例
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j4
你想想看,这一步其实不复杂,但很多人会忽略。我建议你编译完后,把 arch/arm/boot/zImage 和 arch/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,驱动模块的调试流程就变成了:
- 在主机上交叉编译驱动模块(
.ko文件)。 - 把
.ko文件复制到 NFS 导出的根文件系统目录下。 - 在板子上
insmod加载模块,测试。 - 如果出问题,改代码,重复步骤 1-3。
整个过程不需要重启板子,也不需要重新烧写。我个人觉得,这是嵌入式驱动开发最舒服的工作流。
我的习惯: 我会在板子的 /etc/fstab 里把主机上的代码目录也挂载进来,这样板子上可以直接 cd /mnt/host_code 查看源码,方便调试时对照。
好了,环境搭好了。下一章,咱们就开始写第一个字符设备驱动。到时候你会发现,前面这些准备工作,花得值。