Linux驱动设备模型
Linux字符设备就是一种设备(device),在Linux中作为文件保存,并且其中的各种操作方法集合还可以自定义
除了字符设备之外还有块设备和网络设备
现在的设备中大部分都很复杂,不只是写一下简单的打开、关闭、读写操作就可以的
所以引入了->驱动设备模型<-
设备驱动模型分为三个部分:设备(device)、驱动(driver)和链接两者之间的总线(bus)
将设备、驱动程序和总线(如PCI、USB)抽象为统一的对象,并建立了设备和驱动程序之间的绑定机制。
以下是三个主要的数据结构:
struct device:表示一个设备对象
struct device_driver:表示一个驱动程序对象
struct bus_type:表示一种总线类型(如PCI、USB)
其中,总线是Linux内核负责(实例为platform_bus_type),我们只需要关心设备和驱动
默认称设备为platform_device
驱动为Platform_driver
他们的结构体是:
#include <linux/module.h>
#include <linux/platform_device.h>//用到这两个头文件
/* 驱动程序结构体 */
struct platform_driver {
int (*probe)(struct platform_device *pdev);
/* probe函数指针,当设备与驱动匹配时调用,用于初始化设备 */
int (*remove)(struct platform_device *pdev);
/* remove函数指针,当设备从系统中移除时调用,用于释放资源 */
void (*shutdown)(struct platform_device *pdev);
/* shutdown函数指针,在系统关机或重启时调用,通常用于执行设备的安全关闭操作 */
int (*suspend)(struct platform_device *pdev, pm_message_t state);
/* suspend函数指针,在设备进入低功耗状态时调用 */
int (*resume)(struct platform_device *pdev);
/* resume函数指针,在设备从低功耗状态恢复时调用 */
struct device_driver driver;
/* device_driver结构体,包含了驱动程序的基本信息和操作 */
};
Platform Device 结构体
platform_device结构体用于描述一个平台设备。它的定义通常在内核头文件linux/platform_device.h中。
#include <linux/module.h>
#include <linux/platform_device.h>//用到这两个头文件
/* 设备结构体 */
struct platform_device {
const char *name;
/* 设备名称,用于标识设备 */
int id;
/* 设备ID,用于区分同类型的多个设备 */
struct device dev;
/* device结构体,包含设备的基本信息和操作 */
u32 num_resources;
/* 设备资源的数量 */
struct resource *resource;
/* 设备资源数组,描述设备使用的IO端口、内存和中断等资源 */
const struct platform_device_id *id_entry;
/* 设备ID表,用于驱动程序匹配设备 */
char *driver_override;
/* 可选的驱动程序覆盖名称,用于指定设备使用特定的驱动程序 */
};
现在要写驱动,就要把驱动和设备分开写。
bus来匹配二者的时候,有三种匹配方式,1.名称 2.id 3.设备树(dts)
设备驱动和设备一样,在使用之前要向内核注册,代码如下:
static int __init my_driver_init(void) {
return platform_driver_register(&my_platform_driver);
}
static void __exit my_driver_exit(void) {
platform_driver_unregister(&my_platform_driver);
}
module_init(my_driver_init);
module_exit(my_driver_exit);
驱动和设备匹配的时候要执行probe函数,配套地,要写一个remove函数来释放资源
probe函数的主要作用是当设备被检测到时(通常在设备被插入或系统启动时),
内核调用该函数来初始化设备并将其绑定到驱动程序上
最后Probe函数再调用驱动自己的Probe函数,完成驱动的初始化
Probe函数的实现过程:
static int my_device_probe(struct platform_device *pdev)
{
// 1. 检查设备和驱动程序是否匹配
// 2. 分配资源
// 3. 初始化设备
// 4. 注册设备
// 5. 创建设备文件
return 0; // 返回0表示成功,负值表示失败
}
直接实例化一个驱动:
static struct platform_driver my_platform_driver = {
.probe = my_device_probe,
.remove = my_device_remove,
.driver = {
.name = "my_device",
.owner = THIS_MODULE,
.of_match_table = my_device_of_match,
},
};
module_platform_driver(my_platform_driver);
通过驱动设备模型来写device的时候直接写在设备树上,名称、id、资源等,用到设备树是为了有更好的兼容性
以下是一个简单的设备树:(假如这个设备叫xiaolaoshu,id为666666)
/dts-v1/;
/* 设备树版本声明,必须有 */
#include <dt-bindings/input/input.h>
/* 包含了一些设备树绑定的宏定义,这里是为了展示,可以根据需要添加或省略 */
{
/* 定义根节点 */
compatible = "my,device-tree";
/* 兼容性字符串,用于描述这个设备树的类型,驱动程序可以通过它来匹配设备树 */
aliases {
/* 定义设备别名,方便引用设备 */
mouse = &xiaolaoshu;
/* 将别名“mouse”指向设备节点“xiaolaoshu” */
};
xiaolaoshu: mouse@666666 {
/* 定义设备节点,名字为“xiaolaoshu”,地址为“666666” */
compatible = "my,xiaolaoshu";
/* 设备的兼容性字符串,驱动程序可以通过它来匹配设备 */
reg = <0x666666>;
/* 设备的寄存器地址,假设为“666666” */
interrupt-parent = <&intc>;
/* 指定中断控制器,引用下文定义的“intc”节点 */
interrupts = <1 2>;
/* 中断配置,假设中断号为1,类型为2(具体含义根据硬件手册确定) */
gpio = <&gpio1 10 0>;
/* GPIO配置,引用下文定义的“gpio1”控制器,使用其第10号引脚 */
status = "okay";
/* 设备状态,表示设备可用 */
/* 定义虚构的资源配置 */
resource1 {
resource-name = "power-gpio";
/* 资源名称为“power-gpio” */
resource-gpio = <&gpio1 10 0>;
/* 资源使用的GPIO配置,引用“gpio1”控制器的第10号引脚 */
};
resource2 {
resource-name = "data-gpio";
/* 资源名称为“data-gpio” */
resource-gpio = <&gpio1 11 0>;
/* 资源使用的GPIO配置,引用“gpio1”控制器的第11号引脚 */
};
};
intc: interrupt-controller {
/* 定义中断控制器节点,名字为“intc” */
compatible = "my,intc";
/* 中断控制器的兼容性字符串 */
#interrupt-cells = <2>;
/* 定义中断单元数,通常是2,表示中断号和中断类型
address-cell和size-cell是为了规定设备节点怎么去写地址,值1代表占32位,值2代表64位*/
interrupt-controller;
/* 声明这是一个中断控制器 */
};
gpio1: gpio@1 {
/* 定义GPIO控制器节点,名字为“gpio1”,地址为1 */
compatible = "my,gpio";
/* GPIO控制器的兼容性字符串 */
#gpio-cells = <2>;
/* 定义GPIO单元数,通常是2,表示GPIO号和标志位 */
gpio-controller;
/* 声明这是一个GPIO控制器 */
};
};
关于设备树:一般是由硬件厂商写好的,我们只需要看懂它,添加或修改一部分即可