Linux下驱动开发实例

驱动开发

驱动与硬件的分离
  1. 在传统的嵌入式系统开发中,硬件信息往往是直接硬编码在驱动代码中的。这样做的问题是,当硬件发生变化时,比如增加或更换设备,就需要修改驱动程序的代码,这会导致维护成本非常高。因此,将硬件和驱动分离的理念逐渐被广泛接受,这也是设备树的概念由来。
设备树的作用
  1. 设备树(Device Tree,简称 DT) 是一个描述硬件信息的文件,通常以文本的形式(DTS 文件)编写,最终会被编译成二进制格式(DTB 文件)。
  2. 设备树的作用是将硬件的配置细节(例如 GPIO 地址、I2C 接口、SPI 接口等)从驱动代码中独立出来,使驱动可以保持通用性而无需针对特定硬件编写。
  3. 在 Linux 启动时,内核会读取设备树的信息,并据此来配置硬件和加载合适的驱动程序。
  • 这样一来,驱动程序无需关心底层硬件的具体细节,而是通过读取设备树中的描述来适应不同的硬件配置。这大大提高了驱动程序的重用性和可维护性。
驱动、设备和总线的关系
  1. Linux 中,设备驱动模型的核心是通过抽象总线(Bus)、驱动(Driver)和设备(Device)来实现硬件和软件之间的解耦。
  2. 总线 (Bus)
  • 总线是驱动程序和硬件设备之间的桥梁。它的主要作用是将设备与驱动程序进行匹配,类似于一个中介。
  • 在 Linux 中,每种类型的硬件总线(如 PCI、I2C、SPI)都有对应的"总线驱动"来管理这些设备。这些总线驱动负责扫描总线上的设备,并通过设备树或其他方式获得设备信息。
  • 总线会根据设备的硬件信息调用驱动的 probe 函数,将设备和相应的驱动关联起来。
  1. 设备 (Device)
  • 设备指的是具体的硬件,通常通过设备树来描述硬件的具体属性(如内存地址、中断号等)。
  • 每一个设备都有与之对应的描述,可以通过设备树或者在驱动初始化过程中进行声明。
  • 当内核扫描到设备时,会通过总线找到匹配的驱动程序来加载该设备。
  1. 驱动
  • 驱动程序就是用于控制特定设备的代码,它提供了硬件的操作接口。
  • 驱动通常包括初始化、退出、设备注册和注销的逻辑代码。对于每个设备,驱动程序通过总线的 probe 函数进行绑定,以便在设备被检测到时由内核调用适当的驱动程序。
  1. Probe函数
  • probe 函数 是驱动程序与设备进行匹配的关键。内核在检测到一个设备后,会调用驱动的 probe 函数进行初始化。
设备与驱动的匹配流程
  1. 设备插入到总线时
  • 设备插入到系统中,总线会自动探测到这个设备的存在。比如,一块新的 I2C 设备接入到 I2C 总线上时,总线驱动会通过扫描或者从设备树中获得设备信息。
  • 在内核中,每个总线类型都会实现自己的探测方法,例如 PCI 总线可以通过扫描硬件接口来发现设备,而 I2C 总线则通过读取设备树来找到设备。
  1. 总线调用 probe 函数进行匹配
  • 总线会遍历所有注册在该总线上的驱动程序,寻找合适的驱动来匹配当前设备。
  • 在 Linux 驱动模型中,每个驱动程序会通过 struct device_driver 结构注册到内核中,其中包含了匹配的标识信息(例如 compatible 字段)和一个关键的函数指针------probe 函数。
  1. probe 函数的作用
  • probe 函数是驱动程序的重要组成部分,专门用于对设备进行初始化配置。当总线发现有设备与驱动程序可能匹配时,它会调用该驱动的 probe 函数。
  • 在调用 probe 之前,总线会确认当前驱动能够与设备的信息匹配,通常依据设备树中的信息或其他硬件特征(如设备 ID、类型等)。如果匹配成功,则调用 probe。
  1. 匹配成功后的处理
  • 如果 probe 函数 成功被调用,驱动程序会对该设备进行必要的初始化配置,包含以下几部分:
    • 资源分配:驱动会申请设备所需的资源,比如内存空间、I/O 端口、中断请求线等。
    • 硬件配置:对设备的硬件参数进行配置,比如设置寄存器初始值。
    • 接口注册:驱动程序还会向系统注册该设备的操作接口,这样应用程序就可以通过标准的系统调用(如 read、write 等)来与设备进行交互。
  1. 举例说明
  • 如果系统中插入了一个 USB 设备,总线会扫描这个设备,查看它的设备 ID,然后遍历所有 USB 类型的驱动,找到与该设备 ID 匹配的驱动。此时,总线会调用该驱动的 probe 函数,通过这个函数完成设备的初始化和配置,最终使设备能够正常工作。
相关使用函数
  1. platfrom_driver_register将一个平台驱动注册到内核的设备模型中。将驱动对象交给bus
  • 总线会遍历设备链表,如果发现合适的硬件devobj, 则总线会 将 devobj交给驱动进行probe
cpp 复制代码
int platform_driver_register(struct platform_driver *drv);
参数:
	struct platform_driver *drv:指向一个平台驱动结构体的指针,该结构体包含了驱动的 probe、remove 函数等属性。
返回值:
	成功返回0,失败时返回负数。
  1. platform_driver_unregister函数用于将之前注册的 platform_driver 从内核中注销。
  • 当模块被卸载时,调用 platform_driver_unregister 来解除平台驱动的注册。
cpp 复制代码
void platform_driver_unregister(struct platform_driver *drv);

参数:
	struct platform_driver *drv:指向平台驱动结构体的指针。
  1. 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; // 旧式的匹配表,用于不依赖设备树时的设备和驱动匹配
};
  1. 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
};
  1. 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 设备,释放与该设备相关的资源。
  • 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 = &reg;           // 要读取的寄存器地址
    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 协议,告诉内核模块是开放源码的
相关推荐
南猿北者3 分钟前
Docker Volume
运维·docker·容器
三菱-Liu2 小时前
三菱MR-J4-B伺服连接器和信号排列
网络·驱动开发·硬件工程·制造·mr
矛取矛求3 小时前
Linux如何更优质调节系统性能
linux
内核程序员kevin4 小时前
在Linux环境下使用Docker打包和发布.NET程序并配合MySQL部署
linux·mysql·docker·.net
kayotin4 小时前
Wordpress博客配置2024
linux·mysql·docker
Ztiddler5 小时前
【Linux Shell命令-不定期更新】
linux·运维·服务器·ssh
小小不董5 小时前
Oracle OCP认证考试考点详解082系列16
linux·运维·服务器·数据库·oracle·dba
IPdodo全球网络5 小时前
如何利用静态住宅IP优化Facebook商城的网络稳定性与运营效率
运维·服务器
a1denzzz5 小时前
Linux系统的网络设置
linux·服务器·网络
运维&陈同学6 小时前
【模块一】kubernetes容器编排进阶实战之k8s基础概念
运维·docker·云原生·容器·kubernetes·云计算