Linux下的驱动开发二

一、IO模型

  1. I/O 模型在操作系统中用于处理应用程序与设备驱动之间的数据传输。
  2. I/O 通信模型的核心是解决程序与设备之间如何高效、合理地进行数据通信。不同的模型通过阻塞、非阻塞、同步、异步的方式来控制数据流和处理 I/O 请求。

注:在驱动开发中可以定义一个全局结构体

  1. 用于全局管理驱动程序的状态和资源。
  2. 在驱动开发中,有时需要一个全局的数据结构来保存设备的状态、驱动配置、缓存、锁等信息,这样不同的驱动程序函数可以通过该结构体访问和修改共享的资源。
  3. 作用
    • 统一管理驱动状态: 全局结构体通常保存驱动程序的各种状态信息。例如,设备的注册信息、分配的内存、硬件寄存器映射等都可以保存在这个结构体中。
    • 共享资源: 驱动程序通常会有多个函数被内核调用,比如初始化函数、读写函数、中断处理函数等。使用全局结构体,可以让这些函数方便地共享和访问同样的数据或资源。
    • 简化代码结构: 将驱动程序涉及的所有全局变量封装在一个结构体中,使代码更加清晰、结构化,同时也减少了全局变量命名冲突的可能性。

一、阻塞 I/O 模型

  1. 阻塞 I/O 是最常见的 I/O 模型。当应用程序发起 I/O 请求时,如果数据没有准备好,应用程序将进入阻塞状态,等待数据准备完毕后,驱动程序会唤醒阻塞的应用程序。此时,应用程序才能继续执行。
  2. 工作流程
应用程序向驱动发送读取数据的请求。
如果驱动程序中数据尚未准备好,应用程序进入 睡眠状态,等待数据到来。
当数据准备好后,驱动程序会唤醒应用程序,程序从睡眠状态恢复,继续读取数据。
完成 I/O 操作后,应用程序继续执行其他任务。
  1. 具体步骤
    a. 定义并初始化等待队列
wait_queue_head_t wqhead;//在全局结构体中
在结构体 global_struct 中定义了 wqhead,表示等待队列头,用于管理睡眠进程的队列。

在mod_init() 初始化函数中,通过 init_waitqueue_head() 初始化等待队列头。
init_waitqueue_head(&gstruct.wqhead);

b. 进程睡眠机制

在 chdev_read 函数中,当应用程序尝试从驱动读取数据时,程序会检查数据是否可用。如果数据不可用,进程就会进入睡眠状态,挂到等待队列上。
wait_event_interruptible(pt_gstruct->wqhead, pt_gstruct->data_flag);

c. 唤醒机制

当数据到达时,会通过 chdev_write 函数进行处理
pt_gstruct->data_flag = 1;//数据写入后,data_flag 被设置为 1,表示数据已准备好。
wake_up_interruptible(&pt_gstruct->wqhead);
wake_up_interruptible() 函数用于唤醒等待队列 wqhead 上的所有进程,唤醒后,之前处于睡眠状态的进程会继续执行 chdev_read 函数中的代码,读取数据。

二、非阻塞I/O实现

  1. 非阻塞 I/O 的实现逻辑与阻塞 I/O 类似,但不进入睡眠状态。如果数据尚未准备好,非阻塞 I/O 立即返回错误 -EAGAIN,告诉应用程序稍后重试。
if (file->f_flags & O_NONBLOCK) {    // 检查文件标志是否为非阻塞模式
    if (pt_gstruct->data_flag == 0)  // 没有数据可用
        return -EAGAIN;              // 返回 EAGAIN 错误,表示无数据,稍后重试
}

三、异步通知

  1. 异步通知(Asynchronous Notification)是 Linux 内核提供的一种机制,它允许设备驱动在数据准备好或状态发生变化时,主动向应用程序发送信号(通常是 SIGIO 信号),通知应用程序及时处理。
  2. 这种机制在应用程序不需要不断轮询设备状态的情况下非常有用。
  3. 设备驱动中的结构定义
struct global_struct {
    struct class *cls;
    int major, minor;
    struct cdev cdev_obj;
    wait_queue_head_t wqhead;    // 等待队列头
    char sharebuf[128];          // 共享缓冲区
    int data_flag;               // 标记数据是否可用,0 表示无数据,1 表示有数据
    struct fasync_struct *fapp;  // 异步通知结构,用于管理异步通知
};

  1. 驱动程序的 fasync 实现
int chdev_fasync(int fd, struct file *file, int on)
{
    struct global_struct *pt_gstruct = file->private_data;

    // fasync_helper 负责将文件描述符和进程与异步通知关联或解除关联
    return fasync_helper(fd, file, on, &pt_gstruct->fapp);
}

  1. 实现数据写入时的异步通知
//当驱动程序中有新数据时,通过 kill_fasync 向注册了异步通知的进程发送信号。
ssize_t chdev_write(struct file *file, const char __user *usr, size_t sz, loff_t *loff)
{
    struct global_struct *pt_gstruct = file->private_data;
    long ret;

    // 将用户数据复制到共享缓冲区
    ret = copy_from_user(pt_gstruct->sharebuf, usr, sz);
    if (ret > 0) {
        printk("%s-%d copy_from_user err\n", __func__, __LINE__);
        return -3;
    }

    // 数据写入完成后,标记数据已经准备好
    pt_gstruct->data_flag = 1;

    // 唤醒所有等待在等待队列上的进程
    wake_up_interruptible(&pt_gstruct->wqhead);

    // 发送异步通知信号,通知应用程序有数据可读
    kill_fasync(&pt_gstruct->fapp, SIGIO, POLL_IN);

    return sz;
}

二、内核中断驱动

  1. Linux 内核中的中断处理系统使得硬件能够通知内核发生了特定事件(如数据就绪、设备完成某项任务等),并让内核采取相应的处理措施。

1. 内核中获取中断号

  1. 在 ARM 等嵌入式平台上,设备通常使用设备树(Device Tree)来描述硬件信息。驱动程序可以从设备树节点中获取与设备相关的中断号。
    • 直接写入
device_node {
    compatible = "mydevice";
    reg = <0x12340000 0x1000>;
    interrupts = <30 2>;  // 中断号 30,触发类型 2(下降沿触发)
};
    • 设备树描述了硬件信息并提供给内核使用。在编译设备树时,我们使用交叉编译工具和特定的架构。
make dtbs ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-

-  拷贝到共享目录
编译完成的 .dtb 文件需要通过某种方式传输到开发板。通常,我们会将 .dtb 文件拷贝到一个共享目录(如 NFS、TFTP 或本地磁盘),以便通过 U-Boot 加载和烧录。
cp arch/arm/boot/dts/your_device.dtb /path/to/shared/directory/

	- 通过 U-Boot 烧录设备树文件
loady 41000000
	- movi write dtb 命令将 .dtb 文件写入设备存储
	- 验证设备树文件是否生效
	- 重启linux,在 /proc/device-tree/ 目录会有你的 节点文件目录
实现代码
  • of_find_node_by_path(“/key2”); // 从设备树中查找路径为 /key2 的节点
  • irq_of_parse_and_map(of_node, 0); // 从设备节点获取中断号
  • 具体代码
// Linux 内核中断驱动程序
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/irqreturn.h>
#include <linux/of.h>
#include <linux/interrupt.h>
#include <linux/of_irq.h>

/* 定义全局结构体 */
struct global_struct {
    int irqno;  // 保存从设备树中解析出的中断号
    int xxx;    // 其他私有数据,用户可以根据需要扩展
};

/* 定义全局变量 */
struct global_struct gstruct;

/* 中断处理函数 */
irqreturn_t irq_key_handle(int irqno, void *args)
{
    struct global_struct *pt_gstruct = args;  // 从传入的参数中获取私有数据

    printk(KERN_INFO "%s - 中断发生,irqno = %d, 设备中断号 = %d\n", 
           __func__, irqno, pt_gstruct->irqno);

    // 执行中断处理逻辑,例如:读取硬件状态、清除中断标志等

    return IRQ_HANDLED;  // 返回 IRQ_HANDLED 表示中断已处理完成
}

/* 模块初始化函数 */
int mod_init(void)
{
    int ret;
    int irqno;
    struct device_node *of_node;

    printk(KERN_INFO "中断驱动模块初始化\n");

    /* 1. 通过设备树路径查找设备节点 */
    of_node = of_find_node_by_path("/key2");  // 从设备树中查找路径为 /key2 的节点
    if (!of_node) {
        printk(KERN_ERR "%s - 无法找到设备树节点 /key2\n", __func__);
        return -EINVAL;  // 返回错误码
    }

    /* 2. 从设备树节点解析中断号 */
    irqno = irq_of_parse_and_map(of_node, 0);  // 从设备节点获取中断号
    if (irqno < 0) {
        printk(KERN_ERR "%s - 无法从设备树节点中解析中断号\n", __func__);
        return -EINVAL;  // 返回错误码
    }

    printk(KERN_INFO "%s - 获取的中断号为 %d\n", __func__, irqno);

    /* 3. 保存中断号到全局结构体中 */
    gstruct.irqno = irqno;
    gstruct.xxx = 10086;  // 示例私有数据

    /* 4. 注册中断处理程序 */
    ret = request_irq(irqno, irq_key_handle, IRQF_TRIGGER_FALLING, "key2_intr", &gstruct);
    if (ret < 0) {
        printk(KERN_ERR "%s - 注册中断处理程序失败,错误码:%d\n", __func__, ret);
        return ret;  // 返回错误码
    }

    printk(KERN_INFO "中断处理程序注册成功,中断号为 %d\n", irqno);
    return 0;  // 模块初始化成功
}

/* 模块退出函数 */
void mod_exit(void)
{
    /* 释放中断 */
    free_irq(gstruct.irqno, &gstruct);  // 释放之前注册的中断
    printk(KERN_INFO "中断驱动模块卸载\n");
}

/* 声明模块的初始化和退出函数 */
module_init(mod_init);
module_exit(mod_exit);

/* 模块的许可信息 */
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Simple IRQ Device Driver using Device Tree");
MODULE_VERSION("1.0");

简明步骤
  1. 编写设备树节点:在设备树(.dts 文件)中定义设备的中断号和触发类型。
    • 在设备树(.dts 文件)中定义设备的中断号和触发类型。
key2: gpio-keys {
    compatible = "gpio-keys";
    interrupts = <30 IRQ_TYPE_EDGE_FALLING>;  // 中断号 30,下降沿触发
};

  1. 编写中断驱动程序:
  • 定义驱动的全局结构体,保存中断号和其他私有数据。
  • 实现中断处理函数。
irqreturn_t irq_key_handle(int irqno, void *args)
{
    printk(KERN_INFO "中断发生,irqno = %d\n", irqno);
    return IRQ_HANDLED;
}

  1. 从设备树获取中断号
  • 使用 of_find_node_by_path(“/key2”) 查找设备树节点。
  • 使用 irq_of_parse_and_map() 解析设备树中的中断号。
struct device_node *of_node = of_find_node_by_path("/key2");
int irqno = irq_of_parse_and_map(of_node, 0);
  1. 注册中断处理程序:使用 request_irq() 函数注册中断处理函数。
request_irq(irqno, irq_key_handle, IRQF_TRIGGER_FALLING, "key2_intr", &gstruct);

  1. 释放中断资源(在模块退出时):在 mod_exit 函数中,使用 free_irq() 释放中断资源。
free_irq(gstruct.irqno, &gstruct);

  1. 编译并加载内核模块:编译驱动模块并加载到内核中,验证中断是否正常工作。

中断下半部分

  1. 在 Linux 内核中,中断处理分为上半部分和下半部分。上半部分(Top Half)是中断处理函数(ISR,Interrupt Service Routine),它在中断发生时立即执行,尽量简短以提高系统响应速度。为了避免在中断上下文中执行复杂或耗时的操作,Linux 提供了下半部分(Bottom Half)机制,用于延迟处理不需要立即执行的任务。
  2. LINUX系统中执行单元具体分别如下:
执行单元优先级是否允许睡眠适合任务类型
进程可以睡眠耗时或不耗时任务,不紧急任务
中断下半部分耗时任务,紧急任务
中断上半部分否(禁止睡眠)短时间任务,不耗时,紧急任务

中断下半部分机制

中断下半部分机制优先级是否允许睡眠适用场景使用方式
软中断很高内核专用,处理大量事件内核开发者使用,驱动开发者不使用
Tasklet中等驱动开发中常用,处理短任务1. 定义并初始化 Tasklet 对象
2. 在中断上半部分中调度 Tasklet
工作队列处理复杂、长时间或阻塞任务1. 定义并初始化 Workqueue 对象
2. 在中断上半部分中调度 Workqueue
Tasklet的使用方法
  1. 定义tasklet_struct用于管理和定义Tasklet
struct tasklet_struct {
    void (*func)(unsigned long data);  // Tasklet 的处理函数
    unsigned long data;                // 传递给处理函数的私有数据
};
  1. 初始化Tasklet_struct
  • tasklet_init() 函数用于初始化 tasklet_struct 结构体,指定 Tasklet 的处理函数和私有数据。
void tasklet_init(struct tasklet_struct *t, 
                  void (*func)(unsigned long), unsigned long data);

  1. 调度Tasklet
  • tasklet_schedule() 用于调度 Tasklet,告诉内核这个 Tasklet 需要执行。
  • tasklet_schedule() 实际上不会立即执行 Tasklet,而是将 Tasklet 标记为可执行的。
  • 当中断处理完成后,内核会检测到 Tasklet 已经被调度,会在稍后的软中断上下文中执行 Tasklet。
void tasklet_schedule(struct tasklet_struct *t);
  1. 注意事项
  • tasklet_kill():在模块卸载时,需要调用 tasklet_kill(),它会确保 Tasklet 完成后才退出,以免在卸载模块时 Tasklet 仍在执行。
  • 软中断上下文:Tasklet 是在软中断上下文中执行的,因此不能进行睡眠操作,也不能执行阻塞的 I/O 操作。
  1. 具体代码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>

// Tasklet 处理函数
void my_tasklet_func(unsigned long data)
{
    printk(KERN_INFO "Tasklet 执行,数据: %lu\n", data);
}

// 定义 Tasklet 对象
struct tasklet_struct my_tasklet;

irqreturn_t irq_handler(int irq, void *dev_id)
{
    printk(KERN_INFO "中断发生,调度 Tasklet\n");
    // 调度 Tasklet 执行
    tasklet_schedule(&my_tasklet);
    return IRQ_HANDLED;
}

static int __init my_init(void)
{
    int irq = 19;  // 假设使用中断号 19

    // 初始化 Tasklet,传递处理函数和私有数据
    tasklet_init(&my_tasklet, my_tasklet_func, 100);

    // 注册中断处理程序
    if (request_irq(irq, irq_handler, IRQF_SHARED, "my_tasklet_device", NULL)) {
        printk(KERN_ERR "无法注册中断处理程序\n");
        return -1;
    }

    printk(KERN_INFO "模块加载成功,Tasklet 初始化完成\n");
    return 0;
}

static void __exit my_exit(void)
{
    // 杀掉 Tasklet 确保它已经完成
    tasklet_kill(&my_tasklet);

    // 释放中断
    free_irq(19, NULL);

    printk(KERN_INFO "模块卸载,Tasklet 资源已释放\n");
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("Tasklet 示例");

工作队列的使用方法
  1. 工作队列(Workqueue)是 Linux 内核中提供的一种机制,用于将任务推迟到进程上下文中执行。
  2. 与 Tasklet 和软中断不同,工作队列允许进行阻塞操作和睡眠,因此适用于处理更为复杂、耗时的任务,如文件 I/O 或长时间的计算任务。
  3. 工作队列是在内核线程中执行的,运行环境与普通内核线程一致,能够进行各种内核操作。
具体代码
  1. 定义工作队列
    • 工作队列通过 struct work_struct 定义
struct work_struct workqueue_obj;
  1. 初始化工作队列
  • 在模块初始化时,使用 INIT_WORK() 初始化工作队列对象,并关联任务函数 workqueue_handle
INIT_WORK(&gstruct.workqueue_obj, workqueue_handle);

  1. 调度工作队列
  • 在中断处理函数的下半部分 tasklet_fun 中,通过 schedule_work() 来调度工作队列
schedule_work(&gstruct.workqueue_obj);

  1. 工作队列处理函数
  • 工作队列函数 workqueue_handle 是实际执行任务的地方,它运行在内核线程上下文中,因此可以执行阻塞操作
void workqueue_handle(struct work_struct *work)
{
    struct global_struct *pt_gstruct = container_of(work, struct global_struct, workqueue_obj);

    /* 中断产生数据 */
    pt_gstruct->keycnt++;
    sprintf(pt_gstruct->sharebuf, "hello usrread, keycnt=%d", pt_gstruct->keycnt);

    /* 唤醒等待队列,通知有数据可读 */
    pt_gstruct->data_flag = 1;
    wake_up_interruptible(&pt_gstruct->wqhead);

    /* 发送信号给用户进程,通知数据到达 */
    kill_fasync(&pt_gstruct->fapp, SIGIO, POLL_IN);

    printk("%s-%d irqno=%d\n", __func__, __LINE__, pt_gstruct->irqno);
}

  1. 模块退出时的清理
  • 在模块卸载时,需要确保工作队列中的任务已完成或取消,避免在模块卸载后还有未完成的任务
cancel_work_sync(&gstruct.workqueue_obj);

  1. 代码示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>
#include <linux/gpio.h>

/* 假设 GPIO 中断号为 17 */
#define GPIO_IRQ_NUM 17

/* 全局工作队列对象 */
static struct work_struct my_workqueue;

/* 工作队列处理函数 */
void workqueue_func(struct work_struct *work)
{
    printk(KERN_INFO "工作队列任务执行:处理复杂的任务\n");

    /* 模拟耗时操作 */
    msleep(2000);  // 模拟阻塞操作

    printk(KERN_INFO "工作队列任务完成\n");
}

/* 中断处理程序(上半部分) */
irqreturn_t irq_handler(int irq, void *dev_id)
{
    printk(KERN_INFO "中断触发:调度工作队列\n");

    /* 调度工作队列 */
    schedule_work(&my_workqueue);

    return IRQ_HANDLED;
}

/* 模块初始化 */
static int __init my_module_init(void)
{
    int ret;

    /* 初始化工作队列 */
    INIT_WORK(&my_workqueue, workqueue_func);

    /* 注册中断处理程序 */
    ret = request_irq(GPIO_IRQ_NUM, irq_handler, IRQF_SHARED, "my_workqueue_device", &my_workqueue);
    if (ret) {
        printk(KERN_ERR "无法注册中断处理程序\n");
        return ret;
    }

    printk(KERN_INFO "工作队列模块已加载\n");
    return 0;
}

/* 模块卸载 */
static void __exit my_module_exit(void)
{
    /* 确保工作队列任务已完成或被取消 */
    cancel_work_sync(&my_workqueue);

    /* 释放中断 */
    free_irq(GPIO_IRQ_NUM, &my_workqueue);

    printk(KERN_INFO "工作队列模块已卸载\n");
}

/* 模块入口和出口 */
module_init(my_module_init);
module_exit(my_module_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("工作队列使用示例");

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值