驱动开发
驱动与硬件的分离
- 在传统的嵌入式系统开发中,硬件信息往往是直接硬编码在驱动代码中的。这样做的问题是,当硬件发生变化时,比如增加或更换设备,就需要修改驱动程序的代码,这会导致维护成本非常高。因此,将硬件和驱动分离的理念逐渐被广泛接受,这也是设备树的概念由来。
设备树的作用
- 设备树(Device Tree,简称 DT) 是一个描述硬件信息的文件,通常以文本的形式(DTS 文件)编写,最终会被编译成二进制格式(DTB 文件)。
- 设备树的作用是将硬件的配置细节(例如 GPIO 地址、I2C 接口、SPI 接口等)从驱动代码中独立出来,使驱动可以保持通用性而无需针对特定硬件编写。
- 在 Linux 启动时,内核会读取设备树的信息,并据此来配置硬件和加载合适的驱动程序。
- 这样一来,驱动程序无需关心底层硬件的具体细节,而是通过读取设备树中的描述来适应不同的硬件配置。这大大提高了驱动程序的重用性和可维护性。
驱动、设备和总线的关系
- Linux 中,设备驱动模型的核心是通过抽象总线(Bus)、驱动(Driver)和设备(Device)来实现硬件和软件之间的解耦。
- 总线 (Bus)
- 总线是驱动程序和硬件设备之间的桥梁。它的主要作用是将设备与驱动程序进行匹配,类似于一个中介。
- 在 Linux 中,每种类型的硬件总线(如 PCI、I2C、SPI)都有对应的“总线驱动”来管理这些设备。这些总线驱动负责扫描总线上的设备,并通过设备树或其他方式获得设备信息。
- 总线会根据设备的硬件信息调用驱动的 probe 函数,将设备和相应的驱动关联起来。
- 设备 (Device)
- 设备指的是具体的硬件,通常通过设备树来描述硬件的具体属性(如内存地址、中断号等)。
- 每一个设备都有与之对应的描述,可以通过设备树或者在驱动初始化过程中进行声明。
- 当内核扫描到设备时,会通过总线找到匹配的驱动程序来加载该设备。
- 驱动
- 驱动程序就是用于控制特定设备的代码,它提供了硬件的操作接口。
- 驱动通常包括初始化、退出、设备注册和注销的逻辑代码。对于每个设备,驱动程序通过总线的 probe 函数进行绑定,以便在设备被检测到时由内核调用适当的驱动程序。
- Probe函数
- probe 函数 是驱动程序与设备进行匹配的关键。内核在检测到一个设备后,会调用驱动的 probe 函数进行初始化。
设备与驱动的匹配流程
- 设备插入到总线时
- 设备插入到系统中,总线会自动探测到这个设备的存在。比如,一块新的 I2C 设备接入到 I2C 总线上时,总线驱动会通过扫描或者从设备树中获得设备信息。
- 在内核中,每个总线类型都会实现自己的探测方法,例如 PCI 总线可以通过扫描硬件接口来发现设备,而 I2C 总线则通过读取设备树来找到设备。
- 总线调用 probe 函数进行匹配
- 总线会遍历所有注册在该总线上的驱动程序,寻找合适的驱动来匹配当前设备。
- 在 Linux 驱动模型中,每个驱动程序会通过 struct device_driver 结构注册到内核中,其中包含了匹配的标识信息(例如 compatible 字段)和一个关键的函数指针——probe 函数。
- probe 函数的作用
- probe 函数是驱动程序的重要组成部分,专门用于对设备进行初始化配置。当总线发现有设备与驱动程序可能匹配时,它会调用该驱动的 probe 函数。
- 在调用 probe 之前,总线会确认当前驱动能够与设备的信息匹配,通常依据设备树中的信息或其他硬件特征(如设备 ID、类型等)。如果匹配成功,则调用 probe。
- 匹配成功后的处理
- 如果 probe 函数 成功被调用,驱动程序会对该设备进行必要的初始化配置,包含以下几部分:
- 资源分配:驱动会申请设备所需的资源,比如内存空间、I/O 端口、中断请求线等。
- 硬件配置:对设备的硬件参数进行配置,比如设置寄存器初始值。
- 接口注册:驱动程序还会向系统注册该设备的操作接口,这样应用程序就可以通过标准的系统调用(如 read、write 等)来与设备进行交互。
- 举例说明
- 如果系统中插入了一个 USB 设备,总线会扫描这个设备,查看它的设备 ID,然后遍历所有 USB 类型的驱动,找到与该设备 ID 匹配的驱动。此时,总线会调用该驱动的 probe 函数,通过这个函数完成设备的初始化和配置,最终使设备能够正常工作。
相关使用函数
- platfrom_driver_register将一个平台驱动注册到内核的设备模型中。将驱动对象交给bus
- 总线会遍历设备链表,如果发现合适的硬件devobj, 则总线会 将 devobj交给驱动进行probe
int platform_driver_register(struct platform_driver *drv);
参数:struct platform_driver *drv:指向一个平台驱动结构体的指针,该结构体包含了驱动的 probe、remove 函数等属性。
返回值:成功返回0,失败时返回负数。
- platform_driver_unregister函数用于将之前注册的 platform_driver 从内核中注销。
- 当模块被卸载时,调用 platform_driver_unregister 来解除平台驱动的注册。
void platform_driver_unregister(struct platform_driver *drv);参数:struct platform_driver *drv:指向平台驱动结构体的指针。
- struct platform_driver 是Linux 内核中用于表示平台驱动的结构体。专门用于与平台设备(platform_device)进行交互。
struct platform_driver {int (*probe)(struct platform_device *pdev); // 当设备和驱动匹配时调用的初始化函数int (*remove)(struct platform_device *pdev); // 当设备与驱动解绑时调用的反初始化函数void (*shutdown)(struct platform_device *pdev); // 当系统关机或重启时调用的关闭设备函数int (*suspend)(struct platform_device *pdev, pm_message_t state); // 当系统进入低功耗状态时调用的挂起函数int (*resume)(struct platform_device *pdev); // 当系统从低功耗状态恢复时调用的恢复函数struct device_driver driver; // 通用设备驱动结构体,包含驱动的基本信息,例如名字、匹配表等const struct platform_device_id *id_table; // 旧式的匹配表,用于不依赖设备树时的设备和驱动匹配
};
- struct of_device_id xof_match_table[ ] 用于描述驱动程序支持的设备树节点。
- 平台驱动结构体中的driver中的匹配表
- 这些设备树节点通常通过 compatible 属性来标识它们的类型和特性。以下是 struct of_device_id 结构体的定义示例:
struct of_device_id {char *compatible; // 用于匹配设备树节点的 compatible 字段const void *data; // 可选的附加数据,通常用于传递特定于硬件的信息
};
举例:
struct of_device_id xof_match_table[] = {{ .compatible = "platform devobj" }, // 与设备树中的 compatible 字段保持一致{ .compatible = "platform dev obj" }, // 兼容的另一种设备类型{} // 结束标记,所有字段必须为 null
};
- platdrv_probe 作用是当内核找到一个匹配的设备和驱动时,对该设备进行初始化。
- 主要功能
- 获取设备资源:读取设备的资源信息,例如内存映射地址、中断号等。
- 映射寄存器:通过 ioremap 等函数将硬件寄存器映射到内核虚拟地址空间,方便驱动程序进行操作。
- 分配资源:例如分配内存、注册中断处理函数等。
- 注册设备接口:将设备注册为字符设备或其他类型的设备,以便用户空间程序可以访问。
- 设备特性配置:根据设备树或其他信息进行设备的特性配置。
int platdrv_probe(struct platform_device *pdev);struct platform_device *pdev:这个参数是一个指向平台设备的指针,用于表示被内核找到并与该驱动匹配的设备。platform_device 结构体中包含了设备的各种信息(例如设备树节点指针 of_node,设备名等),驱动程序可以使用这些信息来完成设备的初始化。
一个简单的平台驱动
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/of.h>
#include <linux/kernel.h>#define DRIVER_NAME "simple_platform_driver"/* 匹配表 - 定义 compatible 属性以匹配设备树中的节点 */
static const struct of_device_id simple_of_match[] = {{ .compatible = "example,simple-device", }, // 与设备树中的 compatible 字段一致{ /* Sentinel (终止符) */ }
};/* probe 函数 - 当设备与驱动匹配时调用 */
static int simple_probe(struct platform_device *pdev)
{printk(KERN_INFO "Simple platform driver probed: %s\n", pdev->name);/* 在这里执行硬件初始化,例如映射寄存器或申请中断等 */return 0; // 返回 0 表示初始化成功
}/* remove 函数 - 当设备与驱动解绑时调用 */
static int simple_remove(struct platform_device *pdev)
{printk(KERN_INFO "Simple platform driver removed: %s\n", pdev->name);/* 在这里执行硬件反初始化,释放资源 */return 0;
}/* 平台驱动结构体定义 */
static struct platform_driver simple_platform_driver = {.driver = {.name = DRIVER_NAME, // 驱动的名字.of_match_table = simple_of_match, // 设备树匹配表},.probe = simple_probe, // 指定 probe 函数.remove = simple_remove, // 指定 remove 函数
};/* 模块初始化函数 */
static int __init simple_platform_driver_init(void)
{int ret;printk(KERN_INFO "Initializing simple platform driver\n");ret = platform_driver_register(&simple_platform_driver);if (ret != 0) {printk(KERN_ERR "Failed to register simple platform driver: %d\n", ret);return ret;}return 0;
}/* 模块退出函数 */
static void __exit simple_platform_driver_exit(void)
{printk(KERN_INFO "Exiting simple platform driver\n");platform_driver_unregister(&simple_platform_driver);
}module_init(simple_platform_driver_init); // 指定模块的初始化函数
module_exit(simple_platform_driver_exit); // 指定模块的退出函数MODULE_LICENSE("GPL"); // 许可证类型
MODULE_AUTHOR("Author Name");
MODULE_DESCRIPTION("A simple platform driver example");
MODULE_VERSION("1.0");
--------------设备树添加代码-----------------
simple_device: simple_device@0 {compatible = "example,simple-device";reg = <0x0 0x1000>; // 示例的寄存器信息
};
一个复杂的示例
这个驱动程序是一个用于 Linux 内核的简单平台设备驱动,目的是控制 GPIO(通用输入输出)引脚上的设备,比如一个 LED。它的功能主要包括初始化设备、配置 GPIO 引脚、中断处理,以及清理和释放资源。
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/mod_devicetable.h>
#include <linux/of.h>
#include <linux/irqreturn.h>
#include <linux/interrupt.h>
#include <asm/io.h> // ioremap - 映射寄存器地址
#include <linux/slab.h> // kmalloc - 动态内存分配
#include <linux/of_irq.h> // irq_of_parse_and_map - 解析设备树中中断
#include <linux/platform_device.h> // platform_device - 平台设备结构体相关/*
1. 定义并初始化一个驱动对象该结构体定义了驱动程序的基本属性和行为,包括它支持的设备、初始化和卸载的函数等。
*/
struct platform_driver plat_drvobj = {.driver = {.name = "plat dev obj", // 驱动的名字.of_match_table = xof_match_table, // 匹配设备树的匹配表},.probe = platdrv_probe, // 当设备匹配时调用的初始化函数.remove = platdrv_remove, // 当设备与驱动分离时调用的反初始化函数.id_table = NULL, // 旧式匹配表,这里不使用
};/*
定义设备匹配表,包含该驱动支持的设备类型。它通过设备树中的 compatible 属性进行匹配
*/
struct of_device_id xof_match_table[] = {{ .compatible = "platform devobj", }, // 与设备树中节点的 compatible 保持一致{ .compatible = "platform dev obj" }, // 兼容的另一种设备类型{} // 最后一个所有字段必须为 null,表示结束
};/*
定义一个结构体来记录设备的私有数据,每个设备实例都有自己的私有数据,用于保存特定设备的状态。
*/
struct dev_private {int reg[2]; // 寄存器地址信息void *conf, *data; // 映射后的寄存器基地址int bit; // 控制的 GPIO 位int irqno; // 中断号int keycnt; // 中断次数计数
};/*
中断处理函数,处理设备的中断请求。每当中断发生时,该函数会被调用。
*/
irqreturn_t dev_intr_handle(int irqno, void *args)
{struct dev_private *pri = args; // 获取设备的私有数据int val;// 增加按键计数pri->keycnt++;// 控制寄存器位,控制特定 GPIO 输出状态if (pri->keycnt % 2) {val = readl(pri->data);val |= 1 << pri->bit; // 设置指定位writel(val, pri->data);} else {val = readl(pri->data);val &= ~(1 << pri->bit); // 清除指定位writel(val, pri->data);}return IRQ_HANDLED; // 表示中断已被处理
}/*
probe 函数,当设备匹配成功时由内核调用,用于初始化设备
*/
int platdrv_probe(struct platform_device *pdev)
{int ret;int reg[2];void *conf, *data;int bit;int val;int irqno;struct dev_private *pri;struct device_node *of_node = pdev->dev.of_node; // 获取设备树节点指针// 为设备分配私有数据结构的内存pri = kmalloc(sizeof(*pri), GFP_KERNEL);if (!pri) {printk("%s-%d kmalloc err\n", __func__, __LINE__);return -ENOMEM; // 返回内存分配失败错误}memset(pri, 0, sizeof(*pri)); // 将分配的内存清零pdev->dev.platform_data = pri; // 将私有数据指针保存到设备的 platform_data 中// 从设备树节点中读取寄存器的地址ret = of_property_read_u32_array(of_node, "reg", reg, 2);if (ret < 0) {printk("%s-%d of_property_read_u32_array err\n", __func__, __LINE__);kfree(pri); // 释放已分配的内存return ret;}// 读取位(bit)的属性,用于 GPIO 控制ret = of_property_read_u32(of_node, "bit", &bit);if (ret < 0) {printk("%s-%d of_property_read_u32 err\n", __func__, __LINE__);kfree(pri); // 释放已分配的内存return ret;}// 将寄存器的物理地址映射到内核虚拟地址空间conf = ioremap(reg[0], 4); // 配置寄存器data = ioremap(reg[0] + 4, 4); // 数据寄存器if (!conf || !data) {printk("%s-%d ioremap err\n", __func__, __LINE__);kfree(pri); // 释放已分配的内存return -ENOMEM; // 返回映射失败错误}// 配置 GPIO 为输出模式并关闭灯(假设是控制 LED)val = readl(conf);val &= ~(0xF << 4 * bit); // 清除相应位val |= 1 << 4 * bit; // 设置为输出模式writel(val, conf);val = readl(data);val &= ~(1 << bit); // 关闭 LEDwritel(val, data);// 解析设备树中的中断号irqno = irq_of_parse_and_map(of_node, 0);if (irqno < 0) {printk("%s-%d irq_of_parse_and_map err\n", __func__, __LINE__);iounmap(conf);iounmap(data);kfree(pri); // 释放已分配的内存return irqno;}// 请求中断并绑定中断处理函数ret = request_irq(irqno, dev_intr_handle, IRQF_TRIGGER_FALLING, "devX intr", pri);if (ret < 0) {printk("%s-%d request_irq err\n", __func__, __LINE__);iounmap(conf);iounmap(data);kfree(pri); // 释放已分配的内存return ret;}// 保存设备的配置信息到私有数据结构pri->bit = bit;pri->conf = conf;pri->data = data;pri->irqno = irqno;pri->reg[0] = reg[0];pri->reg[1] = reg[1];printk("%s-%d pdev %p\n", __func__, __LINE__, pdev);return 0;
}/*
remove 函数,当设备与驱动解绑时由内核调用,用于释放资源
*/
int platdrv_remove(struct platform_device *pdev)
{int val;struct dev_private *pri = pdev->dev.platform_data; // 获取设备的私有数据// 释放中断free_irq(pri->irqno, pri);// 关闭 LED,确保设备安全状态val = readl(pri->data);val &= ~(1 << pri->bit);writel(val, pri->data);// 解除映射的寄存器iounmap(pri->conf);iounmap(pri->data);// 释放设备的私有数据结构内存kfree(pri);printk("%s-%d pdev %p\n", __func__, __LINE__, pdev);return 0;
}/*
模块初始化函数,注册平台驱动
*/
int mod_init(void)
{int ret = platform_driver_register(&plat_drvobj);if (ret < 0) {printk("%s-%d platform_driver_register \n", __func__, __LINE__);return ret;}printk("%s-%d\n", __func__, __LINE__);return 0;
}/*
模块退出函数,注销平台驱动
*/
void mod_exit(void)
{platform_driver_unregister(&plat_drvobj); // 注销平台驱动printk("%s-%d\n", __func__, __LINE__);
}module_init(mod_init); // 指定模块的初始化函数
module_exit(mod_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL"); // 指定模块遵循 GPL 许可证
- 设备树信息表
IIC驱动
-
Linux I2C 核心 API
- i2c_add_adapter(struct i2c_adapter *adapter):
- 将一个 I2C 适配器注册到 I2C 子系统,使内核能够管理它。
- i2c_add_driver(struct i2c_driver *driver):
- 将一个 I2C 驱动注册到 I2C 子系统,供内核在发现匹配的设备时调用 probe() 进行初始化。
- i2c_transfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num):
- 用于发送或接收 I2C 消息。
- msgs 包含一个或多个 I2C 读写操作,可以用于实现对设备的读写。
- i2c_new_client_device(struct i2c_adapter *adap, struct i2c_board_info *info):
- 创建一个新的 i2c_client 设备,用于表示连接到 I2C 总线的一个设备。
- i2c_unregister_device(struct i2c_client *client):
- 注销一个 I2C 设备,释放与该设备相关的资源。
- i2c_add_adapter(struct i2c_adapter *adapter):
-
MPU6050 是 InvenSense 公司生产的一款集成了三轴加速度计和三轴陀螺仪的 MEMS 传感器芯片。它是一种 6 轴运动传感器,能够同时测量设备的加速度和角速度,因此广泛应用于姿态检测、航向控制、运动追踪等领域。
-
代码实现了一个 MPU6050 I2C 设备的 Linux 驱动程序,用于与 MPU6050 加速度计和陀螺仪传感器进行通信。具体代码如下
#include <linux/kernel.h> // 内核基本函数和宏的声明,例如 printk
#include <linux/module.h> // 模块初始化、退出函数及模块信息#include <linux/mod_devicetable.h> // 设备匹配表结构体
#include <linux/i2c.h> // I2C 驱动、适配器及消息结构体
#include <linux/delay.h> // 延迟函数,如 msleep()// 定义 MPU6050 寄存器地址的宏,方便后续操作时使用寄存器名称
#define SMPLRT_DIV 0x19 // 采样率分频寄存器地址
#define CONFIG 0x1A // 配置寄存器地址
#define ACCEL_CONFIG 0x1C // 加速度计配置寄存器地址// 加速度计数据的寄存器地址
#define ACCEL_XOUT_H 0x3B
#define ACCEL_XOUT_L 0x3C
#define ACCEL_YOUT_H 0x3D
#define ACCEL_YOUT_L 0x3E
#define ACCEL_ZOUT_H 0x3F
#define ACCEL_ZOUT_L 0x40// 温度数据的寄存器地址
#define TEMP_OUT_H 0x41
#define TEMP_OUT_L 0x42#define GYRO_CONFIG 0x1B // 陀螺仪配置寄存器地址// 陀螺仪数据的寄存器地址
#define GYRO_XOUT_H 0x43
#define GYRO_XOUT_L 0x44
#define GYRO_YOUT_H 0x45
#define GYRO_YOUT_L 0x46
#define GYRO_ZOUT_H 0x47
#define GYRO_ZOUT_L 0x48#define PWR_MGMT_1 0x6B // 电源管理寄存器地址/*
如何编写一个 I2C 驱动:
1. 定义并初始化一个 I2C 驱动对象,即 struct i2c_driver 结构体。
- struct i2c_driver 表示 I2C 驱动对象,用于描述驱动的行为和支持的硬件。2. 使用 i2c_add_driver 函数将驱动注册到 I2C 子系统。
- 内核会通过总线遍历并匹配设备,匹配成功则调用 probe 函数。
*/// 设备树匹配表,列出该驱动支持的所有设备,供内核进行匹配
struct of_device_id xof_match_table[] = {{.compatible = "mpu60xx dev",}, // 设备树中 compatible 属性值匹配{} // 最后一项为空,表示结束
};// 写寄存器的函数,向设备指定寄存器写入一个值
int mpu_reg_write(struct i2c_client *i2cdev, char reg, char val)
{int ret;struct i2c_msg msg; // 定义一个 I2C 消息结构体char buf[] = {reg, val}; // 将寄存器地址和要写入的值放到缓冲区中/*i2c_transfer 函数用于传输 I2C 消息- adap: I2C 设备所属的适配器- msgs: 要传输的 I2C 消息数组- num: 需要传输的消息个数返回值:- 负值表示错误- 正值表示实际传输的消息个数*/msg.addr = i2cdev->addr; // 从设备的 I2C 地址msg.flags = 0; // flags 设为 0,表示写操作msg.buf = buf; // 要发送的数据缓冲区,包含寄存器地址和要写入的值msg.len = 2; // 要发送的数据长度:寄存器地址 + 数据 = 2ret = i2c_transfer(i2cdev->adapter, &msg, 1); // 执行 I2C 数据传输if (ret != 1) {printk("%s-%d i2c_transfer err\n", __func__, __LINE__); // 打印错误信息return -34; // 返回错误码}return 0; // 成功返回 0
}// 读寄存器的函数,从设备指定寄存器读取一个值
int mpu_reg_read(struct i2c_client *i2cdev, char reg, char *pval)
{int ret;struct i2c_msg msg[2]; // 定义一个包含两个 I2C 消息的数组// 第一个消息:发送要读取的寄存器地址msg[0].addr = i2cdev->addr; // 从设备的 I2C 地址msg[0].flags = 0; // flags 设为 0,表示写操作(发送寄存器地址)msg[0].buf = ® // 要读取的寄存器地址msg[0].len = 1; // 长度为 1,表示寄存器地址长度// 第二个消息:读取寄存器值msg[1].addr = i2cdev->addr; // 从设备的 I2C 地址msg[1].flags = 1; // flags 设为 1,表示读操作msg[1].buf = pval; // 存储读取的数据msg[1].len = 1; // 长度为 1,表示读取一个字节ret = i2c_transfer(i2cdev->adapter, msg, 2); // 执行 I2C 数据传输if (ret != 2) {printk("%s-%d i2c_transfer err\n", __func__, __LINE__); // 打印错误信息return -34; // 返回错误码}return 0; // 成功返回 0
}/*
probe 函数:
- 当设备与驱动匹配成功时由内核调用,用于对设备进行初始化。
- 通过 i2c_client 结构体可以访问 I2C 设备的各种信息,包括 I2C 地址、适配器等。
*/
int mpu_probe(struct i2c_client *i2cdev, const struct i2c_device_id *id)
{short x, y, z;char h, l;// 初始化 MPU6050 设备的各个寄存器mpu_reg_write(i2cdev, PWR_MGMT_1, 0x00); // 解除睡眠模式mpu_reg_write(i2cdev, SMPLRT_DIV, 0x07); // 设置采样率mpu_reg_write(i2cdev, CONFIG, 0x06); // 设置低通滤波器mpu_reg_write(i2cdev, GYRO_CONFIG, 0x18); // 设置陀螺仪量程mpu_reg_write(i2cdev, ACCEL_CONFIG, 0x01);// 设置加速度计量程// 进入一个无限循环读取数据(不推荐,可能导致内核模块无法正常卸载)while (1) {// 读取 X 轴加速度数据mpu_reg_read(i2cdev, ACCEL_XOUT_H, &h);mpu_reg_read(i2cdev, ACCEL_XOUT_L, &l);x = h << 8 | l;// 读取 Y 轴加速度数据mpu_reg_read(i2cdev, ACCEL_YOUT_H, &h);mpu_reg_read(i2cdev, ACCEL_YOUT_L, &l);y = h << 8 | l;// 读取 Z 轴加速度数据mpu_reg_read(i2cdev, ACCEL_ZOUT_H, &h);mpu_reg_read(i2cdev, ACCEL_ZOUT_L, &l);z = h << 8 | l;// 打印加速度数据printk("accel x=0x%x y=0x%x z=0x%x\n", x, y, z);msleep(500); // 延迟 500 毫秒}return 0; // 注意:由于有无限循环,永远不会执行到这里
}// remove 函数:当设备与驱动解绑时由内核调用,通常用于清理和释放资源
int mpu_remove(struct i2c_client *i2cdev)
{// 由于该驱动没有动态分配的资源,所以此处无需特别清理return 0;
}// 定义 I2C 驱动对象,包含驱动的基本信息和操作函数
struct i2c_driver mpu5xxx_drvobj = {.driver = {.name = "mpu6xxx drv", // 驱动的名称.of_match_table = xof_match_table, // 设备匹配表,用于匹配设备树中的设备},.probe = mpu_probe, // 设备匹配成功时调用的函数.remove = mpu_remove, // 设备解绑时调用的函数.id_table = &aaaa, // ID 表指针,不能为空,避免内核 bug
};// 模块初始化函数,注册 I2C 驱动到内核
int mod_init(void)
{int ret = i2c_add_driver(&mpu5xxx_drvobj); // 注册 I2C 驱动if (ret < 0) {printk("%s-%d i2c_add_driver err\n", __func__, __LINE__); // 打印错误信息return -34; // 返回错误码}printk("%s-%d\n", __func__, __LINE__); // 打印日志信息,表示成功加载return 0; // 成功返回 0
}// 模块退出函数,注销 I2C 驱动
void mod_exit(void)
{i2c_del_driver(&mpu5xxx_drvobj); // 从内核中删除 I2C 驱动printk("%s-%d\n", __func__, __LINE__); // 打印日志信息,表示模块已被卸载
}// 声明模块的初始化和退出函数
module_init(mod_init); // 指定模块的初始化函数
module_exit(mod_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL"); // 声明模块遵循 GPL 协议
SPI驱动
- 实现了一个 SPI 驱动的基本框架,但没有具体的实现功能。它主要演示了如何定义并初始化一个 Linux SPI 驱动对象,包含了 probe 和 remove 函数的声明以及如何与内核进行交互的基础流程。
- 具体代码
#include <linux/kernel.h> // 包含内核基本功能的声明,例如 printk() 函数
#include <linux/module.h> // 包含模块初始化、退出函数及元信息声明/*
如何实现一个 SPI 驱动
1. 定义并初始化一个 SPI 驱动对象struct spi_driver {struct device_driver driver; // SPI 驱动继承自通用设备驱动const char *name; // 驱动名字,用于区分不同的 SPI 驱动const struct of_device_id of_match_table[]; // 匹配表,记录所有该驱动支持的硬件列表// 总线会根据它来匹配设备const struct spi_device_id *id_table; // ID 表,用于与设备的硬件 ID 匹配(用于旧式非设备树匹配)int (*probe)(struct spi_device *spi); // 设备与驱动匹配成功时的回调函数,用于初始化设备int (*remove)(struct spi_device *spi); // 设备与驱动解绑时的回调函数,用于释放资源};2. 将对象交给总线管理
- 使用特定的函数,将定义的 `spi_driver` 对象注册到内核的 SPI 子系统,使内核能够管理该驱动及其对应设备。*/// 设备树匹配表,用于匹配设备树中定义的 SPI 设备
struct of_device_id xof_match_table[] = {{.compatible = "spi dev obj",}, // 匹配的设备树节点的 compatible 属性{} // 最后一项必须为空,表示匹配表的结束
};/*
probe 函数:当设备与驱动匹配成功时,由内核调用,用于对设备进行初始化
- 参数 spidev: 指向与该驱动匹配的 SPI 设备的指针
- 该函数在成功匹配到对应的设备后被内核调用,用于执行硬件初始化
*/
int spi_dev_probe(struct spi_device *spidev)
{/*spi_write_then_read 函数用于对 SPI 设备执行写-读操作- 参数 spi: 指向目标 SPI 设备的指针- 参数 txbuf: 指向要发送的数据缓冲区- 参数 n_tx: 要发送的数据长度- 参数 rxbuf: 指向用于存储接收数据的缓冲区- 参数 n_rx: 要接收的数据长度- 返回值:成功为 0,失败为负值*/int spi_write_then_read(struct spi_device *spi, const void *txbuf, unsigned n_tx, void *rxbuf, unsigned n_rx);// 在这里可以进行具体设备的初始化,例如配置 SPI 设备的寄存器// 返回 0 表示初始化成功return 0;
}/*
remove 函数:当设备与驱动解绑时由内核调用,用于释放设备占用的资源
- 参数 spidev: 指向与该驱动匹配的 SPI 设备的指针
- 该函数在设备从系统中移除或驱动卸载时被调用,通常用于释放资源
*/
int spi_dev_remove(struct spi_device *spidev)
{// 在这里进行设备的资源释放操作,例如取消注册中断、解除映射内存等// 返回 0 表示成功释放资源return 0;
}/*
定义 SPI 驱动对象 spi_drvobj
- 该对象描述了驱动的基本信息,包括名字、匹配表、`probe` 和 `remove` 函数等
- 通过该结构体,内核可以知道如何与具体的 SPI 设备进行交互
*/
struct spi_driver spi_drvobj = {.driver = {.name = "spi drvobj", // 驱动的名字,用于区分不同的 SPI 驱动.of_match_table = xof_match_table, // 匹配设备树中的 compatible 字段},.probe = spi_dev_probe, // 匹配成功时调用的初始化函数.remove = spi_dev_remove, // 设备解绑时调用的函数// .id_table 可以用于定义旧式的设备 ID 表,用于非设备树匹配设备
};/*
模块初始化函数
- 该函数在模块加载时调用,通常用于注册驱动到内核,使内核可以管理它
- 这里打印日志信息,方便确认模块加载
*/
int mod_init(void)
{printk("%s-%d\n", __func__, __LINE__); // 打印日志,显示当前执行的函数和行号// 通常我们会在这里调用 spi_register_driver(&spi_drvobj) 来注册驱动// 但此处没有调用,驱动并未被真正注册return 0; // 返回 0 表示成功加载模块
}/*
模块退出函数
- 该函数在模块卸载时调用,通常用于注销驱动,从内核中移除它
- 这里打印日志信息,方便确认模块卸载
*/
void mod_exit(void)
{printk("%s-%d\n", __func__, __LINE__); // 打印日志,显示当前执行的函数和行号// 通常我们会在这里调用 spi_unregister_driver(&spi_drvobj) 来注销驱动// 但此处没有调用,驱动未被注册也无需注销return;
}/*
宏定义:
- `module_init(mod_init)`:用于指定模块的初始化函数,在模块加载时被内核调用
- `module_exit(mod_exit)`:用于指定模块的退出函数,在模块卸载时被内核调用
- `MODULE_LICENSE("GPL")`:声明模块遵循 GPL 协议,表示该模块是开源的,并符合 GPL 许可要求
*/
module_init(mod_init); // 指定模块的初始化函数
module_exit(mod_exit); // 指定模块的退出函数
MODULE_LICENSE("GPL"); // 声明模块遵循 GPL 协议,告诉内核模块是开放源码的