• Linux驱动开发——(五)内核中断


    目录

    一、内核中断简介

    1.1 中断号 

    1.2 中断API函数

    1.2.1 irq_of_parse_and_map函数

    1.2.2 gpio_to_irq函数

    1.2.3 request_irq函数

    1.2.4 free_irq函数

    1.2.5 中断处理函数

    1.2.6 中断使能与禁止函数

    二、上半部(顶半部)与下半部(底半部) 

    2.1 上半部与下半部简介

    2.2 软中断

    2.3 tasklet

    2.4 工作队列

    三、驱动代码


    一、内核中断简介

    1.1 中断号 

    每个中断都有一个中断号(中断线),通过中断号即可区分不同的中断。在 Linux内核中使用一个int变量表示中断号。

    1.2 中断API函数

    1.2.1 irq_of_parse_and_map函数

    中断信息如果写到设备树里面,则可以通过irq_of_parse_and_map函数interupts属性中提取对应的中断号

    unsigned int irq_of_parse_and_map(struct device_node *dev, int index)

    dev:设备节点。
    index:索引号interrupts属性可能包含多条中断信息,通过index指定要获取的信息。
    返回值:中断号。

    1.2.2 gpio_to_irq函数

    如果使用GPIO,则可以使用gpio_to_irq函数来获取gpio对应的中断号: 

    int gpio_to_irq(unsigned int gpio)

    gpio:要获取的GPIO编号。
    返回值:GPIO对应的中断号。

    1.2.3 request_irq函数

    在Linux内核中使用某个中断是需要申请的,request_irq函数用于申请中断

    request_irq函数可能会导致睡眠,因此不能在中断上下文或者其他禁止睡眠的代码段中使用 request_irq函数。 request_irq函数会激活(使能)中断,所以不需要我们手动去使能中断:

    int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)

    irq:要申请中断的中断号。

    handler:中断处理函数,当中断发生以后就会执行此中断处理函数。

    flags:中断标志,可以在文件include/linux/interrupt.h里面可以查看所有的中断标志,下表是常用的中断标志:

    标志描述
    IRQF_SHARED多个设备共享一个中断线,共享的所有中断都必须指定此标志。如果使用共享中断的话,request_irq函数的dev参数就是唯一区分他们的标志。
    IRQF_ONESHOT单次中断,中断执行一次就结束 。
    IRQF_TRIGGER_NONE无触发。
    IRQF_TRIGGER_RISING上升沿触发。
    IRQF_TRIGGER_FALLING下降沿触发。
    IRQF_TRIGGER_HIGH高电平触发。
    IRQF_TRIGGER_LOW低电平触发。

    name:中断名字,设置以后可以在/proc/interrupts文件中看到对应的中断名字。建议搭配gpio_request函数赋予。

    dev:如果将 flags设置为 IRQF_SHARED的话, dev用来区分不同的中断,一般情况下将dev设置为设备结构体, dev会传递给中断处理函数 irq_handler_t的第二个参数。

    返回值: 0,中断申请成功,其他负值,中断申请失败,如果返回 -EBUSY的话表示中断已经被申请了。

    1.2.4 free_irq函数

    中断使用完成以后就要通过free_irq函数释放掉相应的中断如果中断不是共享的,那么 free_irq会删除中断处理函数并且禁止中断

    void free_irq(unsigned int irq, void *dev)

    irq:要释放的中断。

    dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。

    返回值:无。

    1.2.5 中断处理函数

    函数申请中断的时候需要设置中断处理函数: 

    irqreturn_t (*irq_handler_t) (int, void *)

    第一个参数是要中断处理函数要相应的中断号;第二个参数是一个指向void的指针,也就是个通用指针,需要与request_irq函数的dev参数保持一致。用于区分共享中断的不同设备,dev也可以指向设备数据结构。中断处理函数的返回值为irqreturn_t类型,irqreturn_t类型定义如下所示:

    1. enum irqreturn {
    2. IRQ_NONE = (0 << 0),
    3. IRQ_HANDLED = (1 << 0),
    4. IRQ_WAKE_THREAD = (1 << 1),
    5. };
    6. typedef enum irqreturn irqreturn_t;

    irqreturn_t是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:

    return IRQ_RETVAL(IRQ_HANDLED)
    1.2.6 中断使能与禁止函数

    常用的中断使用和禁止函数如下所示:

    1. void enable_irq(unsigned int irq)
    2. void disable_irq(unsigned int irq)

    irq:一个中断号。

    disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数:

    void disable_irq_nosync(unsigned int irq)

    disable_irq_nosync函数调用以后立即返回,不会等待当前中断处理程序执行完毕。

    如果需要操作当前处理器的整个中断系统(全局中断),则可以使用:

    1. local_irq_enable()
    2. local_irq_disable()

    如果任务A关闭全局中断3s,在这3s内任务B打开全局中断,则系统容易崩溃。考虑到任务之间并发而竞争,此时就要用到下面两个函数:

    1. local_irq_save(flags)
    2. local_irq_restore(flags)

    这两个函数一对使用。local_irq_save函数用于禁止中断,并且将中断状态保存在flags中。
    local_irq_restore用于恢复中断,将中断到flags状态。 


    二、上半部(顶半部)与下半部(底半部) 

    2.1 上半部与下半部简介

    在使用request_irq申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。

    中断处理函数一定是越快执行完毕越好,但有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知SOC有触摸事件发生, SOC响应中断,然后通过IIC接口读取触摸坐标值并将其上报给系统。但是我们都知道IIC的速度最高也只有400Kbit/S,所以在中断中通过IIC读取数据就会浪费时间。我们可以将通过IIC读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分:

    上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。

    下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出

    哪些代码属于上半部,哪些代码属于下半部,并没有明确的规定,一切根据实际使用情况去判断。这里有一些可以借鉴的参考点:

    ①如果要处理的内容不希望被其他中断打断,那么可以放到上半部。

    ②如果要处理的任务对时间敏感,可以放到上半部。

    ③如果要处理的任务与硬件有关,可以放到上半部

    ④除了上述三点以外的其他任务,优先考虑放到下半部。

    2.2 软中断

    不推荐使用软中断!这里只是作为知识点简单介绍。

    Linux内核使用结构体softirq_action表示软中断,softirq_action结构体定义在文件include/linux/interrupt.h中:

    1. struct softirq_action
    2. {
    3. void (*action)(struct softirq_action *);
    4. };

    在kernel/softirq.c文件中一共定义了10个软中断:

    static struct softirq_action softirq_vec[NR_SOFTIRQS];

    NR_SOFTIRQS是枚举类型,定义在文件include/linux/interrupt.h中:

    1. enum {
    2. HI_SOFTIRQ=0, /* 高优先级软中断 */
    3. TIMER_SOFTIRQ, /* 定时器软中断 */
    4. NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
    5. NET_RX_SOFTIRQ, /* 网络数据接收软中断 */
    6. BLOCK_SOFTIRQ,
    7. BLOCK_IOPOLL_SOFTIRQ,
    8. TASKLET_SOFTIRQ, /* tasklet软中断 */
    9. SCHED_SOFTIRQ, /* 调度软中断 */
    10. HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
    11. RCU_SOFTIRQ, /* RCU软中断 */
    12. NR_SOFTIRQS
    13. };

    softirq_action结构体中的action成员变量是软中断的服务函数,数组softirq_vec是个全局数组,所有的CPU(对于SMP系统而言)都可以访问到。

    2.3 tasklet

    tasklet是利用软中断来实现的另外一种下半部机制,相比起软中断,更建议使用tasklet。Linux内核使用tasklet_struct结构体来表示tasklet:

    1. struct tasklet_struct
    2. {
    3. struct tasklet_struct *next; /* 下一个tasklet */
    4. unsigned long state; /* tasklet状态 */
    5. atomic_t count; /* 计数器,记录对tasklet的引用数 */
    6. void (*func)(unsigned long); /* tasklet执行的函数 */
    7. unsigned long data; /* 函数func的参数 */
    8. };

    func函数就是tasklet要执行的处理函数,用户定义函数内容,类似于中断处理函数。如果要使用 tasklet,必须先定义一个 tasklet,然后使用tasklet_init函数初始化tasklet。taskled_init函数原型如下:

    void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);
    

    t:要初始化的tasklet。

    func:tasklet的处理函数。

    data:要传递给func函数的参数。

    返回值:没有返回值。

    也可以使用宏DECLARE_TASKLET来一次性完成tasklet的定义和初始化,DECLARE_TASKLET定义在include/linux/interrupt.h文件中:

    DECLARE_TASKLET(name, func, data)

    name:要定义的tasklet名字,为tasklet_struct类型的变量。

    func:tasklet的处理函数。

    data:传递给func函数的参数。 

    在上半部(中断处理函数)中调用tasklet_schedule函数就能使tasklet在合适的时间运行:

    void tasklet_schedule(struct tasklet_struct *t)

    t:要调度的tasklet,也就是DECLARE_TASKLET宏里面的name。

    返回值:没有返回值。

    关于tasklet的参考使用示例:

    1. /* 定义taselet */
    2. struct tasklet_struct testtasklet;
    3. /* tasklet处理函数 */
    4. void testtasklet_func(unsigned long data)
    5. {
    6. /* tasklet具体处理内容 */
    7. }
    8. /* 中断处理函数 */
    9. irqreturn_t test_handler(int irq, void *dev_id)
    10. {
    11. ......
    12. /* 调度tasklet */
    13. tasklet_schedule(&testtasklet);
    14. ......
    15. }
    16. /* 驱动入口函数 */
    17. static int __init xxxx_init(void)
    18. {
    19. ......
    20. /* 初始化tasklet */
    21. tasklet_init(&testtasklet, testtasklet_func, data);
    22. /* 注册中断处理函数 */
    23. request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
    24. ......
    25. }

    2.4 工作队列

    工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度

    Linux内核使用work_struct结构体表示一个工作

    1. struct work_struct {
    2. atomic_long_t data;
    3. struct list_head entry;
    4. work_func_t func; /* 工作队列处理函数 */
    5. };

    这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示:

    1. struct workqueue_struct {
    2. struct list_head pwqs;
    3. struct list_head list;
    4. struct mutex mutex;
    5. int work_color;
    6. int flush_color;
    7. atomic_t nr_pwqs_to_flush;
    8. struct wq_flusher *first_flusher;
    9. struct list_head flusher_queue;
    10. struct list_head flusher_overflow;
    11. struct list_head maydays;
    12. struct worker *rescuer;
    13. int nr_drainers;
    14. int saved_max_active;
    15. struct workqueue_attrs *unbound_attrs;
    16. struct pool_workqueue *dfl_pwq;
    17. char name[WQ_NAME_LEN];
    18. struct rcu_head rcu;
    19. unsigned int flags ____cacheline_aligned;
    20. struct pool_workqueue __percpu *cpu_pwqs;
    21. struct pool_workqueue __rcu *numa_pwq_tbl[];
    22. };

    Linux内核使用工作者线程(worker thread)来处理工作队列中的各个工作, Linux内核使用worker结构体表示工作者线程

    1. struct worker {
    2. union {
    3. struct list_head entry;
    4. struct hlist_node hentry;
    5. };
    6. struct work_struct *current_work;
    7. work_func_t current_func;
    8. struct pool_workqueue *current_pwq;
    9. bool desc_valid;
    10. struct list_head scheduled;
    11. struct task_struct *task;
    12. struct worker_pool *pool;
    13. struct list_head node;
    14. unsigned long last_active;
    15. unsigned int flags;
    16. int id;
    17. char desc[WORKER_DESC_LEN];
    18. struct workqueue_struct *rescue_wq;
    19. };

    在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管

    先定义一个work_struct结构体变量以创建工作,然后使用INIT_WORK宏来初始化工作:

    #define INIT_WORK(_work, _func)

     _work:要初始化的工作。

    _func:工作对应的处理函数。

    也可以使用DECLARE_WORK一次性完成工作的创建和初始化:

    #define DECLARE_WORK(n, f)

    n:要定义的工作。

    f:工作对应的处理函数。

    工作的调度函数为schedule_work

    bool schedule_work(struct work_struct *work)

    work:要调度的工作。
    返回值:0,成功;其他值,失败。

    工作队列的参考使用示例:

    1. /* 定义工作(work) */
    2. struct work_struct testwork;
    3. /* work处理函数 */
    4. void testwork_func_t(struct work_struct *work);
    5. {
    6. /* work具体处理内容 */
    7. }
    8. /* 中断处理函数 */
    9. irqreturn_t test_handler(int irq, void *dev_id)
    10. {
    11. ......
    12. /* 调度work */
    13. schedule_work(&testwork);
    14. ......
    15. }
    16. /* 驱动入口函数 */
    17. static int __init xxxx_init(void)
    18. {
    19. ......
    20. /* 初始化work */
    21. INIT_WORK(&testwork, testwork_func_t);
    22. /* 注册中断处理函数 */
    23. request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
    24. ......
    25. }

    三、驱动代码

    由于此篇篇幅过长,驱动代码部分放在另一篇。 

  • 相关阅读:
    Vue中el-table条件渲染防止样式乱掉
    leetcode-判断是不是二叉搜索树-92
    Vue-06-vue-cli
    记录访问http链接,刷新页面会自动转到https问题
    EBS利用虚拟列及hint 提示优化sql案例一则
    promise使用与源码封装(二)
    windos安装Mysql8.0,及解决重新登录异常问题 ERROR 1045 (28000)
    上海华清071班
    静态和动态数码管显示
    如何设计一个项目的数据库
  • 原文地址:https://blog.csdn.net/weixin_61979510/article/details/138173315