3. 多核异构基础
好,咱们进入第三章。这一章我打算把Linux内核驱动框架的几个核心概念串起来讲。说实话,很多新人刚接触驱动开发时,容易被这些术语搞晕——字符设备、platform、设备树、绑定过程……感觉每个都是独立的知识点。其实它们是一套完整的体系,你搞懂了它们之间的关系,写驱动时心里就有底了。
3.1 字符设备驱动框架——最基础的驱动形态
字符设备驱动,说白了就是“按字节流读写”的设备驱动。比如串口、GPIO、LED灯,都属于这一类。我刚开始做驱动时,第一个练手的项目就是字符设备驱动——一个简单的虚拟设备,只做一件事:用户层写什么,内核层就打印什么。
字符设备驱动的核心结构,其实就三个要素:
- 设备号:主设备号 + 次设备号,内核靠它找到对应的驱动
- file_operations结构体:定义open、read、write、ioctl等操作函数
- 设备节点:用户空间看到的/dev/xxx文件
来看一个最简的字符设备驱动框架代码:
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#define DEVICE_NAME "my_char_dev"
#define CLASS_NAME "my_class"
static int major;
static struct class *my_class;
static struct cdev my_cdev;
static int my_open(struct inode *inode, struct file *file) {
pr_info("Device opened\n");
return 0;
}
static ssize_t my_read(struct file *file, char __user *buf,
size_t count, loff_t *offset) {
pr_info("Device read\n");
return 0;
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.open = my_open,
.read = my_read,
};
static int __init my_init(void) {
// 1. 分配设备号
major = register_chrdev(0, DEVICE_NAME, &fops);
if (major < 0) return major;
// 2. 创建设备类
my_class = class_create(THIS_MODULE, CLASS_NAME);
// 3. 创建设备节点
device_create(my_class, NULL, MKDEV(major, 0),
NULL, DEVICE_NAME);
pr_info("Module loaded with major %d\n", major);
return 0;
}
static void __exit my_exit(void) {
device_destroy(my_class, MKDEV(major, 0));
class_destroy(my_class);
unregister_chrdev(major, DEVICE_NAME);
pr_info("Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
这段代码虽然简单,但包含了字符设备驱动的完整生命周期:加载→注册→创建设备节点→用户空间访问→卸载。你想想看,如果只是做纯软件模拟的设备,这套框架完全够用。
3.2 platform驱动模型——让驱动和设备解耦
字符设备驱动有个问题:它把设备和驱动写死了。比如上面那个例子,设备号、设备名都是硬编码在代码里的。如果硬件变了,你得改代码重新编译。这在嵌入式多核异构场景下,简直就是灾难。
platform驱动模型就是为了解决这个问题。它把“设备”和“驱动”分开管理:
- platform_device:描述硬件资源(地址、中断、DMA通道等)
- platform_driver:描述驱动逻辑(probe、remove、suspend、resume等)
- 匹配机制:内核通过name字段或设备树,把设备和驱动绑在一起
我曾在项目中遇到过一个问题:同一个IP核,在不同芯片上有不同的基地址和中断号。如果用传统字符设备驱动,得维护N个版本的代码。改用platform模型后,只需要写一个驱动,然后为每个芯片配一个platform_device结构体就行。
platform驱动的核心结构:
static int my_probe(struct platform_device *pdev) {
struct resource *res;
void __iomem *base;
int irq;
// 获取内存资源
res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
base = devm_ioremap_resource(&pdev->dev, res);
// 获取中断号
irq = platform_get_irq(pdev, 0);
pr_info("Device probed: base=%p, irq=%d\n", base, irq);
return 0;
}
static int my_remove(struct platform_device *pdev) {
pr_info("Device removed\n");
return 0;
}
static struct platform_driver my_driver = {
.probe = my_probe,
.remove = my_remove,
.driver = {
.name = "my_platform_dev",
.of_match_table = my_of_match, // 设备树匹配表
},
};
module_platform_driver(my_driver);
3.3 设备树匹配机制——硬件描述的语言
设备树(Device Tree)是什么?说白了,它就是一份描述硬件拓扑结构的“配置文件”。以前我们用板级文件(board-xxx.c)硬编码硬件信息,每次换硬件都得改内核源码。设备树把硬件描述从内核源码中剥离出来,变成.dts文件,编译成.dtb后由bootloader传给内核。
设备树匹配机制的核心是compatible属性。驱动通过of_match_table声明自己支持哪些设备,内核根据设备树节点的compatible值进行匹配。
举个例子,设备树中的节点:
/ {
soc {
my_device: my-device@40000000 {
compatible = "vendor,my-device-v1", "vendor,my-device";
reg = <0x40000000 0x1000>;
interrupts = <0 31 4>;
clocks = <&clkc 15>;
};
};
};
对应的驱动匹配表:
static const struct of_device_id my_of_match[] = {
{ .compatible = "vendor,my-device-v1", .data = &v1_config },
{ .compatible = "vendor,my-device", .data = &v2_config },
{ /* end */ }
};
MODULE_DEVICE_TABLE(of, my_of_match);
设备树匹配的优先级是这样的:先匹配compatible列表中的第一个,如果不成功再匹配第二个,以此类推。所以你可以把最精确的版本号放在前面,通用的版本放在后面。
3.4 驱动与设备的绑定过程——从设备树到probe
好,现在我们把前面三个概念串起来。驱动和设备到底是怎么绑定的?我画了一张流程图,你看完就明白了。
这张图展示了完整的绑定流程。我解释几个关键步骤:
- 设备树解析:内核在启动时解析dtb,为每个节点创建device_node结构体
- platform_device注册:内核遍历device_node,为符合条件的节点创建platform_device
- 驱动加载:驱动模块加载时,注册platform_driver到内核
- 匹配过程:内核遍历platform_bus_type上的match函数,比较驱动的of_match_table和设备节点的compatible属性
- probe调用:匹配成功,内核调用驱动的probe函数,传入匹配到的platform_device指针
3.5 三种机制的协同关系
最后,我用一个表格总结一下字符设备驱动、platform驱动模型和设备树匹配机制的关系:
| 机制 | 核心作用 | 适用场景 | 与上下层关系 |
|---|---|---|---|
| 字符设备驱动 | 提供文件操作接口(open/read/write/ioctl) | 所有按字节流访问的设备 | 位于最上层,直接面向用户空间 |
| platform驱动模型 | 解耦设备和驱动,提供probe/remove生命周期 | 片上设备(非热插拔) | 位于中间层,管理设备和驱动的匹配 |
| 设备树匹配 | 描述硬件拓扑和资源,提供compatible匹配 | 所有基于设备树的平台 | 位于底层,提供硬件描述数据 |
在实际项目中,这三者是层层嵌套的:设备树描述硬件→platform驱动模型负责匹配→probe函数中注册字符设备。你写驱动时,通常是在probe里调用cdev_init、cdev_add来注册字符设备,同时通过device_create在/dev下创建设备节点。
嗯,这一章的内容就到这里。你理解了这三个机制的关系,后面讲具体的外设驱动(I2C、SPI、DMA等)时,你会发现它们都是在这个框架上搭建的。多核异构场景下,这套框架的优势更加明显——不同核可以共享同一份设备树,各自加载不同的驱动,互不干扰。