驱动开发
驱动与硬件的分离
- 在传统的嵌入式系统开发中,硬件信息往往是直接硬编码在驱动代码中的。这样做的问题是,当硬件发生变化时,比如增加或更换设备,就需要修改驱动程序的代码,这会导致维护成本非常高。因此,将硬件和驱动分离的理念逐渐被广泛接受,这也是设备树的概念由来。
设备树的作用
- 设备树(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
cpp
int platform_driver_register(struct platform_driver *drv);
参数:
struct platform_driver *drv:指向一个平台驱动结构体的指针,该结构体包含了驱动的 probe、remove 函数等属性。
返回值:
成功返回0,失败时返回负数。
- platform_driver_unregister函数用于将之前注册的 platform_driver 从内核中注销。
- 当模块被卸载时,调用 platform_driver_unregister 来解除平台驱动的注册。
cpp
void platform_driver_unregister(struct platform_driver *drv);
参数:
struct platform_driver *drv:指向平台驱动结构体的指针。
- struct platform_driver 是Linux 内核中用于表示平台驱动的结构体。专门用于与平台设备(platform_device)进行交互。
cpp
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 结构体的定义示例:
cpp
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 等函数将硬件寄存器映射到内核虚拟地址空间,方便驱动程序进行操作。
- 分配资源:例如分配内存、注册中断处理函数等。
- 注册设备接口:将设备注册为字符设备或其他类型的设备,以便用户空间程序可以访问。
- 设备特性配置:根据设备树或其他信息进行设备的特性配置。
cpp
int platdrv_probe(struct platform_device *pdev);
struct platform_device *pdev:这个参数是一个指向平台设备的指针,用于表示被内核找到并与该驱动匹配的设备。platform_device 结构体中包含了设备的各种信息(例如设备树节点指针 of_node,设备名等),驱动程序可以使用这些信息来完成设备的初始化。
一个简单的平台驱动
c
#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 引脚、中断处理,以及清理和释放资源。
cpp
#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); // 关闭 LED
writel(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 加速度计和陀螺仪传感器进行通信。具体代码如下
c
#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; // 要发送的数据长度:寄存器地址 + 数据 = 2
ret = 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 函数的声明以及如何与内核进行交互的基础流程。
- 具体代码
cpp
#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 协议,告诉内核模块是开放源码的