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
int platform_driver_register(struct platform_driver *drv);
参数:struct platform_driver *drv:指向一个平台驱动结构体的指针,该结构体包含了驱动的 probe、remove 函数等属性。
返回值:成功返回0,失败时返回负数。
  1. platform_driver_unregister函数用于将之前注册的 platform_driver 从内核中注销。
  • 当模块被卸载时,调用 platform_driver_unregister 来解除平台驱动的注册。
void platform_driver_unregister(struct platform_driver *drv);参数:struct platform_driver *drv:指向平台驱动结构体的指针。
  1. 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; // 旧式的匹配表,用于不依赖设备树时的设备和驱动匹配
};
  1. 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
};
  1. 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 设备,释放与该设备相关的资源。
  • 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 = &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 函数的声明以及如何与内核进行交互的基础流程。
  • 具体代码
#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 协议,告诉内核模块是开放源码的

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/435496.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

机器学习周报(9.23-9.29)

文章目录 摘要Abstract1 自监督学习&#xff08;Self-Supervised Learning&#xff09;1.1 BERT1.1.1 Masking Input1.1.2 Next Sentence Prediction1.1.3 BERT的使用方式 1.2 Why does BERT work?1.3 Multi-lingual BERT 2 pytorch中tensor相关函数学习使用2.1 张量拼接与拆分…

4G模组SIM卡电路很简单,但也要注意这些坑

上次水SIM卡相关的文章&#xff0c;还是上一次&#xff1b; 上一篇文章里吹牛说&#xff0c;跟SIM卡相关的问题还有很多&#xff0c;目的是为下一篇文章埋下伏笔&#xff1b;伏笔埋是埋下了&#xff0c;但如果债老是不还&#xff0c;心里的石头就总悬着&#xff0c;搞不好老板…

MAC的几个常见的快捷方式

1.mac 查看图片好的方式 默认查看图片的方式无法直接切换上一张下一张 解决方法&#xff1a; 1.&#xff08;最好的方法&#xff09;选中图片直接按空格&#xff0c;进入快速预览图片 2.就是全部选中然后打开&#xff0c;但是说实话有点奇怪&#xff0c;而且很占内存 3.直接显示…

Linux 信号捕捉

我们知道信号的处理不是即时的&#xff0c;进程在合适的时机才会处理信号&#xff0c;而这个时机就比如从内核态返回用户态。 1. 用户态与内核态 在操作系统中&#xff0c;用户态&#xff08;User Mode&#xff09;和内核态&#xff08;Kernel Mode&#xff09;是两种不同的C…

安卓主板_MTK4G/5G音视频记录仪整机及方案定制

音视频记录仪方案&#xff0c;采用联发科MT6877平台八核2* A78 6* A55主频高达2.4GHz, 具有高能低耗特性&#xff0c;搭载Android 12.0智能操作系统&#xff0c;可选4GB32GB/6GB128GB内存&#xff0c;运行流畅。主板集成NFC、双摄像头、防抖以及多种无线数据连接&#xff0c;支…

如何在 Kubernetes 上部署和配置开源数据集成平台 Airbyte?

在 Kubernetes 上部署和配置 Airbyte 是一个复杂但非常有价值的过程&#xff0c;特别是对于需要强大数据集成和数据处理能力的企业或团队。Airbyte 是一个开源的数据集成平台&#xff0c;允许用户从各种来源提取数据并加载到目标存储中。其强大的插件系统支持多种数据源与目标&…

新能源汽车储充机器人:能源高效与智能调度

新能源汽车储充机器人&#xff1a;开启能源高效利用与智能调度的未来之门 随着全球能源危机的日益加剧和环境污染问题的不断恶化&#xff0c;新能源汽车成为了未来交通领域的重要发展方向。然而&#xff0c;新能源汽车的普及不仅需要解决电池技术的瓶颈&#xff0c;还需要构建一…

labview更换操作系统后打开原VI闪退

labview更换操作系统后打开原VI闪退 问题描述&#xff1a; Windows11由家庭版更换为专业版后&#xff0c;重新安装labview2021&#xff0c;打开原来的项目&#xff0c;项目管理器可以正常打开&#xff0c;但是打开VI却闪退&#xff0c;并报错如下 出现这种原因主要是labview在…

通信工程学习:什么是LAN局域网、MAN城域网、WAN广域网

LAN局域网、MAN城域网、WAN广域网 LAN&#xff08;Local Area Network&#xff0c;局域网&#xff09;、MAN&#xff08;Metropolitan Area Network&#xff0c;城域网&#xff09;和WAN&#xff08;Wide Area Network&#xff0c;广域网&#xff09;是计算机网络中根据覆盖范围…

vue组合式api

一、ref&#xff08;基本类型数据&#xff0c;对象类型数据&#xff09; 1.作用&#xff1a;定义响应式变量 2.语法&#xff1a;let xxx ref(初始值) 3.返回值&#xff1a;一个RefImpl的实例对象&#xff0c;简称ref对象&#xff0c;ref对象的value属性是响应式的。 4.注意…

【MySQL】视图、用户和权限管理

目录 视图创建视图数据修改影响删除视图视图优点 用户和权限管理查看当前的数据库拥有用户信息创建用户修改密码删除用户权限授权回收权限 视图 视图就是相当于创建一个表&#xff0c;将查询到的结果集给存储起来。像使用复杂的多表查询查询到的结果集就不可以对结果集操作。而…

2024/9/29周报

文章目录 摘要Abstract污水处理工艺流程整体介绍粗格栅细格栅曝气沉砂池提升泵房峰谷平策略 初沉池&#xff08;一级处理&#xff09;工作原理运行管理 氧化沟生化池&#xff08;二级处理&#xff09;二沉池工作原理运行参数 高效沉淀池功能与特点工作原理 深度处理&#xff08…

Coursera_ Algorithms I 学习笔记:Module_3_Analysis_of_Algorithm_Introduction

Coursera_ Algorithms I 学习笔记&#xff1a;Module_3_Analysis_of_Algorithm_Introduction scientific method applied to analysis of algorithms data analysis log-log plot doubling hypothesis experimental alogrithmics System independent effectsSystem dependen…

Electron 主进程与渲染进程、预加载preload.js

在 Electron 中&#xff0c;主要控制两类进程&#xff1a; 主进程 、 渲染进程 。 Electron 应⽤的结构如下图&#xff1a; 如果需要更深入的了解electron进程&#xff0c;可以访问官网 流程模型 文档。 主进程 每个 Electron 应用都有一个单一的主进程&#xff0c;作为应用…

ubuntu20.04系统下,c++图形库Matplot++配置

linux下安装c图形库Matplot&#xff0c;使得c可以可视化编程&#xff1b;安装Matplot之前&#xff0c;需要先安装一个gnuplot&#xff0c;因为Matplot是依赖于此库 gnuplot下载链接&#xff1a; http://www.gnuplot.info/ 一、gnuplot下载与安装(可以跳过&#xff0c;下面源码…

业务调度 -- OTN集群

传输网络Mesh化客观上导致了单节点的业务维度增多以及调度需求增大&#xff0c;通过组建OTN集群可实现业务跨子架调度&#xff0c;容量按需扩展&#xff0c;高效疏导网络节点流量。 什么是OTN集群 OTN集群是OTN设备的一种组合应用形式&#xff0c;多个OTN子架通过集群交叉板互…

启动hadoop集群出现there is no HDFS_NAMENODE_USER defined.Aborting operation

解决方案 在hadoop-env.sh中添加 export HDFS_DATANODE_USERroot export HDFS_NAMENODE_USERroot export HDFS_SECONDARYNAMENODE_USERroot export YARN_RESOURCEMANAGER_USERroot export YARN_NODEMANAGER_USERroot 再次运行即可。

Android 安卓内存安全漏洞数量大幅下降的原因

谷歌决定使用内存安全的编程语言 Rust 向 Android 代码库中写入新代码&#xff0c;尽管旧代码&#xff08;用 C/C 编写&#xff09;没有被重写&#xff0c;但内存安全漏洞却大幅减少。 Android 代码库中每年发现的内存安全漏洞数量&#xff08;来源&#xff1a;谷歌&#xff09…

Python办公自动化案例:实现XMind文件转换成Excel文件

案例:实现XMind文件转换成Excel文件 将XMind文件转换为Excel文件的过程可以通过几个步骤来实现,主要涉及到读取XMind文件,解析其内容,然后创建一个Excel文件并将解析的内容写入。以下是一个简化的Python脚本,展示了如何使用xmindparser库来解析XMind文件,并使用pandas库…

初识Linux · 进程终止

目录 前言&#xff1a; 进程终止在干什么 进程终止的3种情况 进程如何终止 前言&#xff1a; 由上文的地址空间的学习&#xff0c;我们已经知道了进程不是单纯的等于PCB 自己的代码和数据&#xff0c;进程实际上是等于PCB mm_struct(地址空间) 页表 自己的代码和数据。…