目录
一、之前的学习驱动真的是驱动开发吗?
前面我们已经在Ubuntu中简单的学习了解了驱动模块开发的框架,比如module_init、module_param等。但究其本质,我们仍然是在Ubuntu中进行开发,严格意义上不能算作驱动开发,因为Ubuntu为了保护硬件完全不给你直接操作GPIO等外设的机会,只有将环境迁移到QEMU虚拟开发板中,我们才能使用原厂提供的库函数进行驱动开发。在这个过程中会遇到各种不匹配问题,我们将发现并解决。
我曾产生了一个疑问:为什么教程上直接用的具体的寄存器进行裸机开发,而我QEMU的硬件寄存器手册在哪里查询呢?经过搜寻相关资料得知,QEMU虽然仅仅模拟出了vexpress-a9这个硬件开发板,但Linux内核已经针对他的寄存器封装了一层库函数(我们在编译的时候设置了配置选项对应得上vexpress-a9)。就类似于我们学习STM32时的标准库、HAL库开发,不用太过在意寄存器的具体物理地址,一一查手册分析,而是直接调用更加标准的库函数即可。
当然,上述的描述是指该硬件开发板的寄存器操作已经被底层硬件工程师封装成库了,我们是在库的基础上进行上层业务驱动开发,就有点类似于C++后端开发;而底层硬件工程师则是写C++标准库的人。每个初学者都是从业务开始逐步往下深耕的,前期避免操作裸机寄存器能更快速入门。
二、如何查看各种外设的库函数?
在STM32中我们通常能在头文件中找到各种外设的库函数实现,在Linux内核中也不例外,他通常在路径:/home/hmy/linux-mini/linux-6.8/drivers下,图中红色框出了常见的外设目录。任意打开一个外设目录会有一堆文件,他们是Linux内核对具体外设的实现部分,我们暂时不用深究。
在编写驱动代码时,我们只需要包含内核提供的标准 API 头文件(路径在 include/linux/ 下,如 #include <linux/gpio/consumer.h>),即可调用封装好的接口,无需直接操作寄存器。

不过注意:这是我们裁剪出来的最小Linux内核源码目录,我们写代码、编译通常是在Ubuntu系统下执行的,只不过由于Ubuntu中不好查看系统源码,所以我们先在Linux-mini中简单看了看,具体的代码编写也不会放在Linux-min目录下进行,而是在Ubuntu中写代码、交叉编译后传输给QEMU中的Linux执行。
使用命令查找头文件所在位置:
bash
# 查找 consumer.h 的具体位置
find / -name "consumer.h" 2>/dev/null

三、修改编辑器配置文件
注意:这里一定要配置QEMU下Linux内核的路径,因为我们以后写的驱动代码是在这个虚拟开发板上运行的,而如果使用上面的linux-header则是用在Ubuntu中,则与开发板完全无关了。
所以要牢记:开发板上跑的是什么版本内核,就要用什么版本的头文件!
cpp
{
"configurations": [
{
"name": "ARM-Linux-Driver",
"includePath": [
"${workspaceFolder}/**",
"/home/hmy/linux-mini/linux-6.8/include",
"/home/hmy/linux-mini/linux-6.8/include/uapi",
"/home/hmy/linux-mini/linux-6.8/include/generated",
"/home/hmy/linux-mini/linux-6.8/include/generated/uapi",
"/home/hmy/linux-mini/linux-6.8/arch/arm/include",
"/home/hmy/linux-mini/linux-6.8/arch/arm/include/generated",
"/home/hmy/linux-mini/linux-6.8/arch/arm/include/uapi"
],
"defines": [
"__KERNEL__",
"MODULE",
"__LINUX_ARM_ARCH__=7",
"__ARM__"
],
"compilerPath": "/usr/bin/arm-linux-gnueabihf-gcc",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "linux-gcc-arm"
}
],
"version": 4
}
四、测试交叉编译与模块加载
源代码:
cpp
#include<linux/kernel.h>
#include<linux/init.h>
#include<linux/module.h>
static int __init hello_init(void)
{
printk("----------------------");
printk("模块加载成功\n");
printk("hello world\n");
printk("----------------------");
return 0;
}
static void __exit hello_exit(void)
{
printk("----------------------");
printk("模块卸载成功\n");
printk("ByeBye world\n");
printk("----------------------");
}
//注册
module_init(hello_init);
module_exit(hello_exit);
//开源
MODULE_LICENSE("GPL");
由于是交叉编译,需要修改编译器的选项:
bash
# 内核源码路径(你自己的路径,不用改)
KERNELDIR := /home/hmy/linux-mini/linux-6.8
# 当前目录
CURRENT_PATH := $(shell pwd)
# 【重要】这里要和你的 C 语言文件名一致!
# 你的文件叫 main.c 就写 main.o
obj-m := main.o
# 编译模块
all:
# 👇 这一行前面必须是 Tab,不能是空格
make -C $(KERNELDIR) M=$(CURRENT_PATH) modules ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf-
# 清理文件
clean:
# 👇 这一行前面也必须是 Tab
make -C $(KERNELDIR) M=$(CURRENT_PATH) clean ARCH=arm


五、利用mount命令传输驱动文件到QEMU
由于我们裁剪的最小Linux内核没有开启网卡配置选项,所以无法使用SCP命令传递文件。这个每个人的情况可能不同,最好去网上查查资料,看看有什么方法可以将Ubuntu中的文件拷贝到QEMU的Linux中。这一步也不是很困难,就好比用U盘将主机A的文件拷贝到主机B上。
bash
sudo mount /home/hmy/linux-mini/busybox-1.36.1/rootfs.ext2 /mnt
sudo cp /home/hmy/qudongkaifa/part_3/main.ko /mnt
sudo umount /mnt

细心的朋友们可以发现:sudo命令居然查无此人!这是因为我们使用的是Busybox提供的shell工具,它为了体积和效率直接没有提供用户态的权限,所有人只要一登录默认就是root权限。