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 内核源码树准备

驱动模块不是凭空编译的。它需要依赖内核的头文件和编译规则。所以你得有一套完整的内核源码树。

为什么要编译内核? 你想想看,驱动模块本质上是要链接到内核里的。如果内核版本不对,或者配置选项不同,模块加载时符号对不上,直接崩。

我建议的做法是:

  1. 从目标板厂商获取内核源码(或者从Linux内核官网下载对应版本)
  2. 使用目标板相同的配置文件进行编译
  3. 编译完成后,记下 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"看了好久。那种成就感,比写一万行应用代码都强。你也会有的。