第三章 Linux内核驱动基础
说实话,很多刚接触AI芯片驱动开发的朋友,上来就盯着硬件手册看寄存器,结果代码写到一半发现连加载都加载不进去。嗯,这很正常。内核驱动框架这东西,说白了就是操作系统和硬件之间的「翻译官」。你想想看,没有这个翻译官,你的AI加速器再强,Linux也不认识它。
我个人习惯把驱动框架比作「入住酒店」:设备树是房型图,platform驱动是前台接待,字符设备是客房服务。今天我们就把这套流程彻底讲透。
3.1 字符设备驱动框架
字符设备,就是按字节流读写的外设。比如串口、GPIO、还有我们AI芯片的控制接口。我刚开始做驱动时,以为字符设备就是简单的open/read/write,结果踩了个大坑——忘了注册设备号。
核心要点:字符设备驱动的三要素——设备号、file_operations结构体、cdev对象。
来看一个最简的字符设备框架:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "ai_accel"
#define CLASS_NAME "ai"
static int major_num;
static struct class *ai_class = NULL;
static struct device *ai_device = NULL;
static struct cdev ai_cdev;
static int ai_open(struct inode *inodep, struct file *filep) {
pr_info("AI device opened\n");
return 0;
}
static ssize_t ai_read(struct file *filep, char __user *buffer,
size_t len, loff_t *offset) {
/* 这里实现从AI芯片读取数据 */
return 0;
}
static struct file_operations ai_fops = {
.owner = THIS_MODULE,
.open = ai_open,
.read = ai_read,
};
static int __init ai_init(void) {
dev_t dev_num;
/* 第一步:分配设备号 */
alloc_chrdev_region(&dev_num, 0, 1, DEVICE_NAME);
major_num = MAJOR(dev_num);
/* 第二步:初始化cdev并注册 */
cdev_init(&ai_cdev, &ai_fops);
cdev_add(&ai_cdev, dev_num, 1);
/* 第三步:创建设备类 */
ai_class = class_create(THIS_MODULE, CLASS_NAME);
ai_device = device_create(ai_class, NULL, dev_num, NULL, DEVICE_NAME);
pr_info("AI driver loaded, major=%d\n", major_num);
return 0;
}
static void __exit ai_exit(void) {
device_destroy(ai_class, MKDEV(major_num, 0));
class_destroy(ai_class);
cdev_del(&ai_cdev);
unregister_chrdev_region(MKDEV(major_num, 0), 1);
pr_info("AI driver unloaded\n");
}
module_init(ai_init);
module_exit(ai_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("AI Chip Driver Team");
我的经验:alloc_chrdev_region会自动分配一个空闲的设备号,比手动指定major_num要安全。我曾经在项目里硬编码了major=240,结果和系统里另一个驱动冲突,调试了一整天才发现。
3.2 platform驱动模型
为什么需要platform驱动?因为有些设备不挂在PCIe、USB这些总线上,而是直接挂在CPU的内存总线上。比如我们AI芯片的配置寄存器,就是通过内存映射访问的。
platform驱动模型的核心是「匹配」——驱动和设备通过名字或设备树节点进行匹配。我个人习惯用设备树方式,因为更灵活。
static struct platform_driver ai_platform_driver = {
.probe = ai_probe,
.remove = ai_remove,
.driver = {
.name = "ai_accel",
.of_match_table = ai_of_match,
},
};
module_platform_driver(ai_platform_driver);
probe函数是驱动的入口点,当内核发现匹配的设备时就会调用它。我在probe里通常会做三件事:
- 解析设备树获取资源(地址、中断号)
- 映射物理地址到虚拟地址
- 注册字符设备或其他子系统
注意:probe函数执行时不能睡眠太久,否则会影响系统启动速度。我见过有人直接在probe里做硬件初始化循环等待,结果内核报「scheduling while atomic」错误。
3.3 设备树解析与实战
设备树(Device Tree)是描述硬件信息的结构化数据。说白了,就是告诉内核:「我这里有个AI芯片,它的寄存器在0x40000000,中断号是42」。
来看一个典型的AI芯片设备树节点:
ai_accel: ai-accelerator@40000000 {
compatible = "vendor,ai-accel-v1";
reg = <0x40000000 0x10000>;
interrupts = <0 42 4>;
clocks = <&clkc 15>;
status = "okay";
};
在驱动中解析设备树的代码:
static int ai_probe(struct platform_device *pdev) {
struct resource *res;
void __iomem *base;
int irq_num;
/* 获取寄存器地址 */
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
if (IS_ERR(base))
return PTR_ERR(base);
/* 获取中断号 */
irq_num = platform_get_irq(pdev, 0);
if (irq_num < 0)
return irq_num;
/* 解析时钟信息 */
struct clk *clk = devm_clk_get(&pdev->dev, NULL);
if (!IS_ERR(clk))
clk_prepare_enable(clk);
dev_info(&pdev->dev, "AI accelerator at 0x%llx, irq=%d\n",
(unsigned long long)res->start, irq_num);
return 0;
}
避坑指南:我曾经在解析reg属性时用了platform_get_resource(pdev, IORESOURCE_MEM, 1),结果一直返回NULL。后来才发现设备树里只定义了一个reg区域,索引应该从0开始。嗯,这种低级错误,犯过一次就记住了。
3.4 module_init/exit机制
module_init和module_exit是驱动的入口和出口。内核通过这两个宏来管理驱动的生命周期。
加载流程是这样的:
- insmod时,内核调用module_init指定的函数
- rmmod时,内核调用module_exit指定的函数
- 如果驱动编译进内核,module_init会在系统启动时被调用
这里有个细节很多人不知道:module_init其实是一个宏,它把函数指针放到一个特定的section里。内核在启动或加载模块时,会遍历这个section来调用所有初始化函数。
/* 实际上module_init展开后类似这样 */
#define module_init(x) __initcall(x);
/* 对于编译为模块的情况 */
static int __init ai_init(void) { ... }
module_init(ai_init);
/* 对于编译进内核的情况,会变成 */
device_initcall(ai_init);
关键点:__init标记告诉内核,这个函数在初始化完成后可以释放掉。我见过有人把probe函数也标记为__init,结果第二次绑定设备时直接崩溃。__init只适用于只会执行一次的函数。
3.5 知识体系总览
为了让你更直观地理解这三者的关系,我画了一张图:
3.6 实战要点总结
| 组件 | 核心函数 | 常见错误 | 我的建议 |
|---|---|---|---|
| 字符设备 | cdev_init, cdev_add | 设备号冲突 | 用alloc_chrdev_region动态分配 |
| platform驱动 | platform_driver_register | probe不匹配 | 检查compatible字符串是否一致 |
| 设备树 | of_match_table | 解析不到资源 | 用devicetree.org的语法检查工具 |
| 模块机制 | module_init/exit | __init误用 | 只标记一次性初始化函数 |
最后提醒:调试驱动时,别忘了打开内核的dynamic debug。我习惯在probe函数里加一句dev_dbg,这样可以通过/sys/kernel/debug/dynamic_debug/control动态控制日志输出,比printk好用多了。
嗯,这一章的内容就到这里。驱动开发这东西,光看代码是不够的,一定要动手编译、加载、调试。下次遇到insmod后dmesg里啥也没有的情况,别慌——先检查module_init有没有写对。
公众号:蓝海资料掘金营,微信deep3321