2、驱动开发环境搭建:交叉编译工具链配置、内核源码树准备、目标板与宿主机通信设置、第一个驱动模块编译与加载
说实话,驱动开发的第一步往往最劝退人。我见过不少新手,代码写得挺溜,结果卡在环境搭建上,一卡就是好几天。今天咱们就把这层窗户纸捅破。
2.1 交叉编译工具链配置
嵌入式设备资源有限,你不可能在目标板上写代码、编译。所以得在PC上编译,然后把二进制丢到板子上跑。这就是交叉编译。
工具链的选择,说白了就是看你用啥芯片。ARM Cortex-A系列,我习惯用 arm-linux-gnueabihf- 这套。如果是aarch64,那就用 aarch64-linux-gnu-。
核心原则:工具链的gcc版本、glibc版本,必须与目标板上的内核及根文件系统匹配。否则你编译出来的模块,加载时直接报错。
安装步骤其实不复杂:
# 以Ubuntu 20.04为例,安装ARM交叉编译工具链
sudo apt-get install gcc-arm-linux-gnueabihf
# 验证安装
arm-linux-gnueabihf-gcc --version
我个人习惯把工具链路径加到 ~/.bashrc 里,省得每次都要敲全路径:
export CROSS_COMPILE=arm-linux-gnueabihf-
export ARCH=arm
嗯,这里要注意:CROSS_COMPILE 这个变量名,内核构建系统是认的。你设好了,它自动就知道用哪个编译器。
2.2 内核源码树准备
驱动模块不是凭空编译的。它需要依赖内核的头文件和编译规则。所以你得有一套完整的内核源码树。
为什么要编译内核? 你想想看,驱动模块本质上是要链接到内核里的。如果内核版本不对,或者配置选项不同,模块加载时符号对不上,直接崩。
我建议的做法是:
- 从目标板厂商获取内核源码(或者从Linux内核官网下载对应版本)
- 使用目标板相同的配置文件进行编译
- 编译完成后,记下
Module.symvers这个文件,它记录了内核导出的符号
# 假设内核源码在 ~/kernel 目录下
cd ~/kernel
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage modules -j4
小技巧:如果你只是编译模块,不需要每次都编译整个内核。只要内核源码树是干净的、配置好的,直接编译模块就行。我在项目中经常只改一个驱动文件,然后 make modules 就完事了。
2.3 目标板与宿主机通信设置
代码编好了,怎么传到板子上?常见的方式有几种:
| 方式 | 适用场景 | 速度 | 我踩过的坑 |
|---|---|---|---|
| NFS挂载 | 开发调试阶段 | 中等 | 网络不通时,板子直接挂死 |
| TFTP下载 | uboot阶段加载内核 | 快 | 文件太大容易超时 |
| 串口传输 | 应急或没有网络 | 慢 | 用 sz/rz 传大文件,等到崩溃 |
| SSH/SCP | 板子跑Linux且有网络 | 快 | 最推荐,稳定 |
我个人最常用的是 NFS + SSH 组合。NFS用来挂载根文件系统,开发时直接改PC上的文件,板子上立即可见。SSP用来传编译好的模块。
配置NFS其实就几步:
# 在宿主机上
sudo apt-get install nfs-kernel-server
sudo mkdir -p /srv/nfs/rootfs
# 编辑 /etc/exports,添加:
/srv/nfs/rootfs *(rw,sync,no_subtree_check,no_root_squash)
sudo systemctl restart nfs-server
然后在目标板的uboot参数里设置:
setenv bootargs 'console=ttyS0,115200 root=/dev/nfs nfsroot=192.168.1.100:/srv/nfs/rootfs ip=192.168.1.101'
saveenv
boot
警告:我曾经因为 no_root_squash 没加,导致板子上root用户无法写文件,排查了半天。这个选项允许板子上的root拥有和宿主机root一样的权限,开发阶段一定要加上。
2.4 第一个驱动模块编译与加载
好了,环境都齐了,咱们写个最简单的"Hello World"驱动。别小看它,这是所有驱动开发的起点。
// hello.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, Embedded GPU Driver!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, Embedded GPU Driver!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A simple Hello World module");
然后是Makefile。这个Makefile的写法有讲究,不能随便写:
# Makefile
obj-m := hello.o
KDIR := /path/to/your/kernel/tree
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
编译命令:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
编译成功后,你会看到 hello.ko 文件。把它传到板子上:
scp hello.ko root@192.168.1.101:/root/
在板子上加载:
insmod hello.ko
# 查看内核日志
dmesg | tail
# 你应该能看到 "Hello, Embedded GPU Driver!"
为什么用 insmod 而不是 modprobe? insmod 简单粗暴,直接加载指定模块。modprobe 会处理依赖关系,但需要模块在标准路径下。开发阶段,我习惯用 insmod,省事。
卸载模块:
rmmod hello.ko
dmesg | tail
# 看到 "Goodbye, Embedded GPU Driver!"
嗯,到这里,你的第一个驱动模块就跑起来了。别看它简单,背后的流程——交叉编译、内核依赖、模块加载——这些概念,是所有复杂驱动的基础。
我记得第一次调通这个流程时,盯着dmesg里那行"Hello"看了好久。那种成就感,比写一万行应用代码都强。你也会有的。