正点原子 发表于 2022-1-22 15:22:25

《领航者ZYNQ之嵌入式Linux开发指南_V2.0》第三十三章 Linux中断

1)实验平台:正点原子领航者V2 ZYNQ开发板
2)章节摘自【正点原子】《领航者ZYNQ之嵌入式Linux开发指南_V2.0》
3)购买链接:https://detail.tmall.com/item.htm?id=609032204975
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-329957-1-1.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子FPGA技术交流QQ群:90562473







第三十三章 Linux中断实验
       不管是裸机实验还是Linux下的驱动实验,中断都是频繁使用的功能,关于ZYNQ的中断原理已经在《领航者ZYNQ嵌入式开发指南》第四章中做了详细的讲解,在裸机中使用中断我们需要做一大堆的工作,比如配置寄存器,使能IRQ等等(SDK提供的库函数中已经封装好了)。Linux内核提供了完善的中断框架,我们只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。本章我们就来学习一下如何在Linux下使用中断。

       1.1Linux中断简介
1.1.1Linux中断API函数
       先来回顾一下裸机实验里面按键中断的处理方法:
       ①、使能中断异常。
       ②、注册中断服务函数。
       ③、设置中断的触发方式。
       ④、最后使能按键中断。
       当按键中断发生以后进入到前面绑定的IRQ中断服务函数当中,然后执行相应的中断处理代码。在Linux内核中也提供了大量的中断相关的API函数,我们来看一下这些跟中断有关的API函数:
       1、中断号
       每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在Linux内核中使用一个int变量表示中断号。
       2、request_irq函数
       在Linux内核中要想使用某个中断是需要申请的,request_irq函数用于申请中断,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文件中查看所有的中断标志,这里我们介绍几个常用的中断标志,如表 33.1.1所示:

表 33.1.1 常用的中断标志
       比如开发板上的PS_KEY0使用GPIO0_IO12(也就是MIO 12),按下PS_KEY0以后为低电平,因此可以设置为下降沿触发,也就是将flags设置为IRQF_TRIGGER_FALLING。表 33.1.1中的这些标志可以通过“|”来实现多种组合。
       name:中断名字,设置以后可以在/proc/interrupts文件中看到对应的中断名字。
       dev:如果将flags设置为IRQF_SHARED的话,dev用来区分不同的中断,一般情况下将dev设置为设备结构体,dev会传递给中断处理函数irq_handler_t的第二个参数。
       返回值:0 中断申请成功,其他负值 中断申请失败,如果返回-EBUSY的话表示中断已经被申请了。
       3、free_irq函数
       使用中断的时候需要通过request_irq函数申请,使用完成以后就要通过free_irq函数释放掉相应的中断。如果中断不是共享的,那么free_irq会删除中断处理函数并且禁止中断。free_irq函数原型如下所示:
void free_irq(unsigned int irq, void *dev)       函数参数和返回值含义如下:
       irq:要释放的中断。
       dev:如果中断设置为共享(IRQF_SHARED)的话,此参数用来区分具体的中断。共享中断只有在释放最后中断处理函数的时候才会被禁止掉。
       返回值:无。
       4、中断处理函数
       使用request_irq函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:
irqreturn_t (*irq_handler_t) (int, void *)      第一个参数是要中断处理函数要相应的中断号。第二个参数是一个指向void的指针,也就是个通用指针,需要与request_irq函数的dev参数保持一致。用于区分共享中断的不同设备,dev也可以指向设备数据结构。中断处理函数的返回值为irqreturn_t类型,irqreturn_t类型定义如下所示:
示例代码 33.1.1 irqreturn_t结构体
10 enum irqreturn {
11   IRQ_NONE                        = (0 << 0),
12   IRQ_HANDLED                = (1 << 0),
13   IRQ_WAKE_THREAD      = (1 << 1),
14 };
15
16 typedef enum irqreturn irqreturn_t;       可以看出irqreturn_t是个枚举类型,一共有三种返回值。一般中断服务函数返回值使用如下形式:
return IRQ_RETVAL(IRQ_HANDLED)       5、中断使能与禁止函数
       常用的中断使用和禁止函数如下所示:
void enable_irq(unsigned int irq)
void disable_irq(unsigned int irq)       enable_irq和disable_irq用于使能和禁止指定的中断,irq就是要禁止的中断号。disable_irq函数要等到当前正在执行的中断处理函数执行完才返回,因此使用者需要保证不会产生新的中断,并且确保所有已经开始执行的中断处理程序已经全部退出。在这种情况下,可以使用另外一个中断禁止函数:
void disable_irq_nosync(unsigned int irq)       disable_irq_nosync函数调用以后立即返回,不会等待当前中断处理程序执行完毕。上面三个函数都是使能或者禁止某一个中断,有时候我们需要关闭当前处理器的整个中断系统,也就是在学习STM32的时候常说的关闭全局中断,这个时候可以使用如下两个函数:
local_irq_enable()
local_irq_disable()       local_irq_enable用于使能当前处理器中断系统,local_irq_disable用于禁止当前处理器中断系统。假如A任务调用local_irq_disable关闭全局中断10S,当关闭了2S的时候B任务开始运行,B任务也调用local_irq_disable关闭全局中断3S,3秒以后B任务调用local_irq_enable函数将全局中断打开了。此时才过去2+3=5秒的时间,然后全局中断就被打开了,此时A任务要关闭10S全局中断的愿望就破灭了,然后A任务就“生气了”,结果很严重,可能系统都要被A任务整崩溃。为了解决这个问题,B任务不能直接简单粗暴的通过local_irq_enable函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数:
local_irq_save(flags)
local_irq_restore(flags)       这两个函数是一对,local_irq_save函数用于禁止中断,并且将中断状态保存在flags中。local_irq_restore用于恢复中断,将中断到flags状态。
1.1.2上半部与下半部
       在有些资料中也将上半部和下半部称为顶半部和底半部,都是一个意思。我们在使用request_irq申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。我们都知道中断处理函数一定要快点执行完毕,越短越好,但是现实往往是残酷的,有些中断处理过程就是比较费时间,我们必须要对其进行处理,缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知SOC有触摸事件发生,SOC响应中断,然后通过IIC接口读取触摸坐标值并将其上报给系统。但是我们都知道IIC的速度最高也只有400Kbit/S,所以在中断中通过IIC读取数据就会浪费时间。我们可以将通过IIC读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分:
       上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
       下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。
       因此,Linux内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。这里有一些可以借鉴的参考点:
       ①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
       ②、如果要处理的任务对时间敏感,可以放到上半部。
       ③、如果要处理的任务与硬件有关,可以放到上半部
       ④、除了上述三点以外的其他任务,优先考虑放到下半部。
       上半部处理很简单,直接编写中断处理函数就行了,关键是下半部该怎么做呢?Linux内核提供了多种下半部机制,接下来我们来学习一下这些下半部机制。
       1、软中断
       一开始Linux内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中断和tasklet来替代“BH”机制,完全可以使用软中断和tasklet来替代BH,从2.5版本的Linux内核开始BH已经被抛弃了。Linux内核使用结构体softirq_action表示软中断,softirq_action结构体定义在文件include/linux/interrupt.h中,内容如下:
示例代码 33.1.2 softirq_action结构体
433 struct softirq_action
434 {
435   void (*action)(struct softirq_action *);
436 };       在kernel/softirq.c文件中一共定义了10个软中断,如下所示:
示例代码 33.1.3 softirq_vec数组-1
056 static struct softirq_action softirq_vec;       NR_SOFTIRQS是枚举类型,定义在文件include/linux/interrupt.h中,定义如下:
示例代码 33.1.4 softirq_vec数组-2
458 enum
459 {
460   HI_SOFTIRQ=0,
461   TIMER_SOFTIRQ,
462   NET_TX_SOFTIRQ,
463   NET_RX_SOFTIRQ,
464   BLOCK_SOFTIRQ,
465   IRQ_POLL_SOFTIRQ,
466   TASKLET_SOFTIRQ,
467   SCHED_SOFTIRQ,
468   HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
469                         numbering. Sigh! */
470   RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
471
472   NR_SOFTIRQS
473 };       可以看出,一共有10个软中断,因此NR_SOFTIRQS为10,因此数组softirq_vec有10个元素。softirq_action结构体中的action成员变量就是软中断的服务函数,数组softirq_vec是个全局数组,因此所有的CPU(对于SMP系统而言)都可以访问到,每个CPU都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个CPU所执行的软中断服务函数确是相同的,都是数组softirq_vec中定义的action函数。要使用软中断,必须先使用open_softirq函数注册对应的软中断处理函数,open_softirq函数原型如下:
void open_softirq(int nr, void (*action)(struct softirq_action *))       函数参数和返回值含义如下:
       nr:要开启的软中断,在示例代码 33.1.4中选择一个。
       action:软中断对应的处理函数。
       返回值:没有返回值。
       注册好软中断以后需要通过raise_softirq函数触发,raise_softirq函数原型如下:
void raise_softirq(unsigned int nr)       函数参数和返回值含义如下:
       nr:要触发的软中断,在示例代码 33.1.4中选择一个。
       返回值:没有返回值。
       软中断必须在编译的时候静态注册!Linux内核使用softirq_init函数初始化软中断,softirq_init函数定义在kernel/softirq.c文件里面,函数内容如下:
示例代码 33.1.5 softirq_init函数内容
634 void __init softirq_init(void)
635 {
636   int cpu;
637
638   for_each_possible_cpu(cpu) {
639         per_cpu(tasklet_vec, cpu).tail =
640             &per_cpu(tasklet_vec, cpu).head;
641         per_cpu(tasklet_hi_vec, cpu).tail =
642             &per_cpu(tasklet_hi_vec, cpu).head;
643   }
644
645   open_softirq(TASKLET_SOFTIRQ, tasklet_action);
646   open_softirq(HI_SOFTIRQ, tasklet_hi_action);
647 }       从示例代码 33.1.5可以看出,softirq_init函数默认会打开TASKLET_SOFTIRQ和HI_SOFTIRQ。
       2、tasklet
       tasklet是利用软中断来实现的另外一种下半部机制,在软中断和tasklet之间,建议大家使用tasklet。Linux内核使用结构体
示例代码 33.1.6 tasklet_struct结构体
484 struct tasklet_struct
485 {
486   struct tasklet_struct *next;                /* 下一个tasklet */
487   unsigned long state;                        /* tasklet状态 */
488   atomic_t count;                              /* 计数器,记录对tasklet的引用数 */
489   void (*func)(unsigned long);                /* tasklet执行的函数 */
490   unsigned long data;                              /* 函数func的参数 */
491 };       第489行的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在合适的时间运行,tasklet_schedule函数原型如下:
void tasklet_schedule(struct tasklet_struct *t)       函数参数和返回值含义如下:
       t:要调度的tasklet,也就是DECLARE_TASKLET宏里面的name。
       返回值:没有返回值。
       关于tasklet的参考使用示例如下所示:
示例代码 33.1.7 tasklet使用示例
/* 定义taselet */
struct tasklet_struct testtasklet;

/* tasklet处理函数 */
void testtasklet_func(unsigned long data)
{
    /* tasklet具体处理内容 */
}

/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
    ......
    /* 调度tasklet */
    tasklet_schedule(&testtasklet);
    ......
}

/* 驱动入口函数 */
static int __init xxxx_init(void)
{
    ......
    /* 初始化tasklet */
    tasklet_init(&testtasklet, testtasklet_func, data);
    /* 注册中断处理函数 */
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
    ......
}       2、工作队列
       工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果你要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或tasklet。
       Linux内核使用work_struct结构体表示一个工作,该结构体定义在内核源码目录include/linux/workqueue.h头文件中,内容如下:
示例代码 33.1.8 work_struct结构体
101struct work_struct {
102   atomic_long_t data;
103   struct list_head entry;
104   work_func_t func;                /* 工作处理函数 */
105 #ifdef CONFIG_LOCKDEP
106   struct lockdep_map lockdep_map;
107 #endif
108 };       这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示,该结构体定义在内核源码目录kernel/workqueue.c文件中,内容如下:
示例代码 33.1.9 workqueue_struct结构体
239 struct workqueue_struct {
240   struct list_head pwqs;      /* WR: all pwqs of this wq */
241   struct list_head list;                /* PR: list of all workqueues */
242
243   struct mutex mutex;                /* protects this wq */
244   int work_color;                        /* WQ: current work color */
245   int flush_color;                        /* WQ: current flush color */
246   atomic_t nr_pwqs_to_flush;                        /* flush in progress */
247   struct wq_flusher *first_flusher;                /* WQ: first flusher */
248   struct list_head flusher_queue;                /* WQ: flush waiters */
249   struct list_head flusher_overflow;                /* WQ: flush overflow list */
250
251   struct list_head maydays;                        /* MD: pwqs requesting rescue */
252   struct worker *rescuer;                              /* I: rescue worker */
253
254   int nr_drainers;                /* WQ: drain in progress */
255   int saved_max_active;      /* WQ: saved pwq max_active */
256
257   struct workqueue_attrs *unbound_attrs;                /* PW: only for unbound wqs */
258   struct pool_workqueue *dfl_pwq;                        /* PW: only for unbound wqs */
259
260 #ifdef CONFIG_SYSFS
261   struct wq_device *wq_dev;                        /* I: for sysfs interface */
262 #endif
263 #ifdef CONFIG_LOCKDEP
264   struct lockdep_map lockdep_map;
265 #endif
266   char name;                /* I: workqueue name */
267
268   /*
269      * Destruction of workqueue_struct is sched-RCU protected to allow
270      * walking the workqueues list without grabbing wq_pool_mutex.
271      * This is used to dump all workqueues from sysrq.
272      */
273   struct rcu_head rcu;
274
275   /* hot fields used during command issue, aligned to cacheline */
276   unsigned int flags ____cacheline_aligned;                /* WQ: WQ_* flags */
277   struct pool_workqueue __percpu *cpu_pwqs;      /* I: per-cpu pwqs */
278   struct pool_workqueue __rcu *numa_pwq_tbl[];      /* PWR: unbound pwqs indexed by node */
279 };       Linux内核使用工作者线程(worker thred)来处理工作队列中的各个工作,Linux内核使用worker结构体表示工作者线程,该结构体定义在内核源码目录kernel/workqueue_internal.h头文件中,worker结构体内容如下:
示例代码 33.1.10 worker结构体
239 struct workqueue_struct {
240   struct list_headpwqs;                /* WR: all pwqs of this wq */
241   struct list_headlist;                /* PR: list of all workqueues */
242
243   struct mutexmutex;                /* protects this wq */
244   int work_color;                        /* WQ: current work color */
245   int flush_color;                        /* WQ: current flush color */
246   atomic_t nr_pwqs_to_flush;      /* flush in progress */
247   struct wq_flusher*first_flusher;                /* WQ: first flusher */
248   struct list_headflusher_queue;                /* WQ: flush waiters */
249   struct list_headflusher_overflow;      /* WQ: flush overflow list */


024 struct worker {
025   /* on idle list while idle, on busy hash table while busy */
026   union {
027         struct list_headentry;                /* L: while idle */
028         struct hlist_nodehentry;      /* L: while busy */
029   };
030
031   struct work_struct*current_work;      /* L: work being processed */
032   work_func_tcurrent_func;                        /* L: current_work's fn */
033   struct pool_workqueue*current_pwq;      /* L: current_work's pwq */
034   bool desc_valid;                        /* ->desc is valid */
035   struct list_headscheduled;      /* L: scheduled works */
036
037   /* 64 bytes boundary on 64bit, 32 on 32bit */
038
039   struct task_struct*task;                /* I: worker task */
040   struct worker_pool*pool;                /* I: the associated pool */
041                                       /* L: for rescuers */
042   struct list_headnode;                        /* A: anchored at pool->workers */
043                                                /* A: runs through worker->node */
044
045   unsigned longlast_active;                /* L: last active timestamp */
046   unsigned intflags;                        /* X: flags */
047   intid;                                                /* I: worker id */
048
049   /*
050      * Opaque string set with work_set_desc().Printed out with task
051      * dump for debugging - WARN, BUG, panic or sysrq.
052      */
053   chardesc;
054
055   /* used only by rescuers to point to the target workqueue */
056   struct workqueue_struct*rescue_wq;      /* I: the workqueue to rescue */
057 };       从示例代码 33.1.10可以看出,每个worker都有一个工作队列,工作者线程处理自己工作队列中的所有工作。在实际的驱动开发中,我们只需要定义工作(work_struct)即可,关于工作队列和工作者线程我们基本不用去管。简单创建工作很简单,直接定义一个work_struct结构体变量即可,然后使用INIT_WORK宏来初始化工作,INIT_WORK宏定义如下:
#define INIT_WORK(_work, _func)       _work表示要初始化的工作,_func是工作对应的处理函数。
       也可以使用DECLARE_WORK宏一次性完成工作的创建和初始化,宏定义如下:
#define DECLARE_WORK(n, f)       n表示定义的工作(work_struct),f表示工作对应的处理函数。
       和tasklet一样,工作也是需要调度才能运行的,工作的调度函数为schedule_work,函数原型如下所示:
bool schedule_work(struct work_struct *work)       函数参数和返回值含义如下:
       work:要调度的工作。
       返回值:0 成功,其他值 失败。
       关于工作队列的参考使用示例如下所示:
示例代码 33.1.11 工作队列使用示例
/* 定义工作(work) */
structwork_struct testwork;

/* work处理函数 */
void testwork_func_t(struct work_struct *work);
{
    /* work具体处理内容 */
}

/* 中断处理函数 */
irqreturn_t test_handler(int irq, void *dev_id)
{
    ......
    /* 调度work */
    schedule_work(&testwork);
    ......
}

/* 驱动入口函数 */
static int __init xxxx_init(void)
{
    ......
    /* 初始化work */
    INIT_WORK(&testwork, testwork_func_t);
    /* 注册中断处理函数 */
    request_irq(xxx_irq, test_handler, 0, "xxx", &xxx_dev);
    ......
}1.1.3设备树中断信息节点
       如果使用设备树的话就需要在设备树中设置好中断属性信息,Linux内核通过读取设备树中的中断属性信息来配置中断。对于中断控制器而言,设备树绑定信息参考文档Documentation/devicetree/bindings/arm/gic.txt。打开arch/arm/boot/dts/zynq-7000.dtsi文件,其中的intc节点就是ZYNQ 7010/7020的中断控制器节点,节点内容如下所示:
示例代码 33.1.12 中断控制器intc节点
144 intc: interrupt-controller@f8f01000 {
145   compatible = "arm,cortex-a9-gic";
146   #interrupt-cells = <3>;
147   interrupt-controller;
148   reg = <0xF8F01000 0x1000>,
149          <0xF8F00100 0x100>;
150 };       第145行,compatible属性值为“arm,cortex-a9-gic”在Linux内核源码中搜索“arm,cortex-a9-gic”即可找到GIC中断控制器驱动文件。
       第146行,#interrupt-cells和#address-cells、#size-cells一样。表示此中断控制器下设备的cells大小,对于设备而言,会使用interrupts属性描述中断信息,#interrupt-cells描述了interrupts属性的cells大小,也就是一条信息有几个cells。每个cells都是32位整形值,对于ARM处理的GIC来说,一共有3个cells,这三个cells的含义如下:
       第一个cells:中断类型,0表示SPI中断(共享外设中断),1表示PPI中断(私有外设中断)。
       第二个cells:中断号,在《领航者ZYNQ之嵌入式开发指南》第四章中介绍过,SPI中断大约有60个,它们对应的中断号范围为32~92(61个),对于PPI中断来说中断号的范围为27~31。但是该cell描述的中断号是从0开始。
       第三个cells:标志,bit表示中断触发类型,为1的时候表示上升沿触发,为2的时候表示下降沿触发,为4的时候表示高电平触发,为8的时候表示低电平触发。bit为PPI中断的CPU掩码。
       第147行,使用interrupt-controller属性来表示当前节点是中断控制器节点。
对于gpio来说,gpio节点也可以作为中断控制器,比如zynq-7000.dtsi文件中的gpio0节点内容如下所示:
示例代码 33.1.13 gpio0设备节点
110 gpio0: gpio@e000a000 {
111   compatible = "xlnx,zynq-gpio-1.0";
112   #gpio-cells = <2>;
113   clocks = <&clkc 42>;
114   gpio-controller;
115   interrupt-controller;
116   #interrupt-cells = <2>;
117   interrupt-parent = <&intc>;
118   interrupts = <0 20 4>;
119   reg = <0xe000a000 0x1000>;
120 };       第118行,使用interrupts属性来描述gpio中断源的信息,“interrupts = <0 20 4>”中,0表示中断类型,也就是前面说到的SPI中断;20对应的就是ZYNQ 7010/7020 GPIO外设所对应的中断号;而4表示中断触发类型是高电平触发。打开ZYNQ 7010/7020的参考手册,路径为:领航者ZYNQ开发板资料盘(A盘)\8_ZYNQ&FPGA参考资料\Xilinx\User Guide\ug585-Zynq-7000-TRM.pdf,找到“Ch. 7: Interrupts”章节,找到Table 7-4,如下图所示:

图 33.1.1 中断号
       从图 33.1.1中可以看出,GPIO外设所对应的中断号为52,那么前面的20是怎么计算出来的呢?其实就是将中断号编号从0开始,将52减去SPI类型中断的起始编号32即可得出20。
       第115行,interrupt-controller属性表明了gpio0节点也是个中断控制器,用于控制ZYNQ 7010/7020的所有IO的中断。第114行,gpio-controller属性表明该节点是gpio中断控制器。
       第116行,将#interrupt-cells修改为2,所以在设备树中使用gpio中断需要提供2个参数,第一个表示GPIO编号,第二个表示该GPIO的中断触发类型。
       在设备树中如何表示一个GPIO中断呢?我们来看看下面这个示例:
示例代码 33.1.14 gpio中断使用示例
1 test-node {
2    compatible = "test-node";
3    reg = <0x1e>;
4    interrupt-parent = <&gpio0>;
5    interrupts = <12 2>;
6 };       第4行,interrupt-parent属性设置中断控制器,这里使用gpio0作为中断控制器。
       第6行,interrupts设置中断信息,12表示一个具体的GPIO引脚编号,也就是对应GPIO0_12;而2表示该GPIO的中断触发类型为下降沿触发。
       简单总结一下与中断有关的设备树属性信息:
       ①、#interrupt-cells,指定中断源的信息cells(参数)个数。
       ②、interrupt-controller,表示当前节点为中断控制器。
       ③、interrupts,指定中断号,触发方式等。
       ④、interrupt-parent,指定父中断,也就是中断控制器。
1.1.4获取中断号
      编写驱动的时候需要用到中断号,我们用到中断号,中断信息已经写到了设备树里面,因此可以通过irq_of_parse_and_map函数从interupts属性中提取到对应的设备号,函数原型如下:
unsigned int irq_of_parse_and_map(struct device_node *dev, int index)       函数参数和返回值含义如下:
       dev:设备节点。
       index:索引号,interrupts属性可能包含多条中断信息,通过index指定要获取的信息。
       返回值:中断号。
       如果使用GPIO的话,可以使用gpio_to_irq函数来获取gpio对应的中断号,函数原型如下:
int gpio_to_irq(unsigned int gpio)       函数参数和返回值含义如下:
       gpio:要获取的GPIO编号。
       返回值:GPIO对应的中断号。
       1.2硬件原理图分析
       本章实验硬件原理图参考31.2小节即可。
       1.3实验程序编写
       本实验对应的例程路径为:领航者ZYNQ开发板光盘资料(A盘)\4_SourceCode\3_Embedded_Linux\Linux驱动例程\13_irq。
       本章实验我们驱动开发板上的PS_KEY0按键,不过我们采用中断的方式,并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来。通过本章我们可以学习到Linux内核中断的使用方法,以及对Linux内核定时器的回顾。
1.3.1修改设备树文件
       本章实验使用到了按键PS_KEY0,按键PS_KEY0使用中断模式,因此需要在system-top.dts文件中的“key”节点下添加中断相关属性,添加完成以后的“key”节点内容如下所示:
示例代码 33.3.1 key节点信息
......

7 #define GPIO_ACTIVE_HIGH   0
8 #define GPIO_ACTIVE_LOW    1
9
10 /dts-v1/;
11 #include "zynq-7000.dtsi"
12 #include "pl.dtsi"
13 #include "pcw.dtsi"
14 #include <dt-bindings/interrupt-controller/irq.h>

......

51   key {
52         compatible = "alientek,key";
53         status = "okay";
54         key-gpio = <&gpio0 12 GPIO_ACTIVE_LOW>;
55
56         interrupt-parent = <&gpio0>;
57         interrupts = <12 IRQ_TYPE_EDGE_BOTH>;
58   };       第14行,使用include包含了一个头文件,在内核源码目录include/dt-bindings/interrupt-controller/irq.h。
       第56行,设置interrupt-parent属性值为“gpio0”,因为PS_KEY0所使用的GPIO为GPIO0_IO12,也就是设置PS_KEY0的GPIO中断控制器为gpio0。
       第57行,设置interrupts属性,也就是设置中断源,第一个cells的12表示GPIO0组的12号IO。IRQ_TYPE_EDGE_BOTH定义在头文件include/dt-bindings/interrupt-controller/irq.h中,定义如下:
示例代码 33.3.2 irq.h头文件内容
13 #define IRQ_TYPE_NONE                        0
14 #define IRQ_TYPE_EDGE_RISING      1
15 #define IRQ_TYPE_EDGE_FALLING      2
16 #define IRQ_TYPE_EDGE_BOTH                (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING)
17 #define IRQ_TYPE_LEVEL_HIGH                4
18 #define IRQ_TYPE_LEVEL_LOW                8       从示例代码 33.3.2中可以看出,IRQ_TYPE_EDGE_BOTH表示上升沿和下降沿同时有效,相当于PS_KEY0按下和释放都会触发中断。
       设备树编写完成以后使用下面这条命令重新编译设备树,如下所示:
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- system-top.dtb
图 33.3.1 重新编译设备树
       将编译的到的system-top.dtb文件重命名为system.dtb,用这个文件替换SD启动卡FAT分区中的dtb文件,然后重新启动开发板。
1.3.2按键中断驱动程序编写
       在drivers目录下新建名为“13_irq”的文件夹,然后在13_irq目录中新建keyirq.c源文件,在keyirq.c里面输入如下内容:
示例代码 33.3.3 keyirq.c文件代码
1 /***************************************************************
2Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3文件名    : keyirq.c
4作者      : 邓涛
5版本      : V1.0
6描述      : GPIO按键中断驱动实验
7其他      : 无
8论坛      : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
9日志      : 初版V1.0 2019/1/30 邓涛创建
10***************************************************************/
11
12 #include <linux/types.h>
13 #include <linux/kernel.h>
14 #include <linux/delay.h>
15 #include <linux/ide.h>
16 #include <linux/init.h>
17 #include <linux/module.h>
18 #include <linux/errno.h>
19 #include <linux/gpio.h>
20 #include <asm/mach/map.h>
21 #include <asm/uaccess.h>
22 #include <asm/io.h>
23 #include <linux/cdev.h>
24 #include <linux/of.h>
25 #include <linux/of_address.h>
26 #include <linux/of_gpio.h>
27 #include <linux/of_irq.h>
28 #include <linux/irq.h>
29
30 #define KEY_CNT                1                /* 设备号个数 */
31 #define KEY_NAME      "key"      /* 名字 */
32
33 /* 定义按键状态 */
34 enum key_status {
35   KEY_PRESS = 0,                // 按键按下
36   KEY_RELEASE,                // 按键松开
37   KEY_KEEP,                        // 按键状态保持
38 };
39
40 /* 按键设备结构体 */
41 struct key_dev {
42   dev_t devid;                        /* 设备号 */
43   struct cdev cdev;                /* cdev结构体 */
44   struct class *class;                /* 类 */
45   struct device *device;      /* 设备 */
46   int key_gpio;                        /* GPIO编号 */
47   int irq_num;                        /* 中断号 */
48   struct timer_list timer;      /* 定时器 */
49   spinlock_t spinlock;      /* 自旋锁 */
50 };
51
52 static struct key_dev key;                        /* 按键设备 */
53 static int status = KEY_KEEP;      /* 按键状态 */
54
55 /*
56* @description                        : 打开设备
57* @param – inode                        : 传递给驱动的inode
58* @param – filp                        : 设备文件,file结构体有个叫做private_data的成员变量
59*                                                   一般在open的时候将private_data指向设备结构体。
60* @return                              : 0 成功;其他 失败
61*/
62 static int key_open(struct inode *inode, struct file *filp)
63 {
64   return 0;
65 }
66
67 /*
68* @description                : 从设备读取数据
69* @param – filp                : 要打开的设备文件(文件描述符)
70* @param – buf                : 返回给用户空间的数据缓冲区
71* @param – cnt                : 要读取的数据长度
72* @param – offt                : 相对于文件首地址的偏移
73* @return                        : 读取的字节数,如果为负值,表示读取失败
74*/
75 static ssize_t key_read(struct file *filp, char __user *buf,
76             size_t cnt, loff_t *offt)
77 {
78   unsigned long flags;
79   int ret;
80
81   /* 自旋锁上锁 */
82   spin_lock_irqsave(&key.spinlock, flags);
83
84   /* 将按键状态信息发送给应用程序 */
85   ret = copy_to_user(buf, &status, sizeof(int));
86
87   /* 状态重置 */
88   status = KEY_KEEP;
89
90   /* 自旋锁解锁 */
91   spin_unlock_irqrestore(&key.spinlock, flags);
92
93   return ret;
94 }
95
96 /*
97* @description                : 向设备写数据
98* @param – filp                : 设备文件,表示打开的文件描述符
99* @param – buf                : 要写给设备写入的数据
100* @param – cnt                : 要写入的数据长度
101* @param – offt                : 相对于文件首地址的偏移
102* @return                        : 写入的字节数,如果为负值,表示写入失败
103*/
104 static ssize_t key_write(struct file *filp, const char __user *buf,
105             size_t cnt, loff_t *offt)
106 {
107         return 0;
108 }
109
110 /*
111* @description                : 关闭/释放设备
112* @param – filp                : 要关闭的设备文件(文件描述符)
113* @return                        : 0 成功;其他 失败
114*/
115 static int key_release(struct inode *inode, struct file *filp)
116 {
117   return 0;
118 }
119
120 static void key_timer_function(unsigned long arg)
121 {
122   static int last_val = 1;
123   unsigned long flags;
124   int current_val;
125
126   /* 自旋锁上锁 */
127   spin_lock_irqsave(&key.spinlock, flags);
128
129   /* 读取按键值并判断按键当前状态 */
130   current_val = gpio_get_value(key.key_gpio);
131   if (0 == current_val && last_val)       // 按下
132         status = KEY_PRESS;
133   else if (1 == current_val && !last_val)
134         status = KEY_RELEASE;   // 松开
135   else
136         status = KEY_KEEP;            // 状态保持
137
138   last_val = current_val;
139
140   /* 自旋锁解锁 */
141   spin_unlock_irqrestore(&key.spinlock, flags);
142 }
143
144 static irqreturn_t key_interrupt(int irq, void *dev_id)
145 {
146   /* 按键防抖处理,开启定时器延时15ms */
147   mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));
148   return IRQ_HANDLED;
149 }
150
151 static int key_parse_dt(void)
152 {
153   struct device_node *nd;
154   const char *str;
155   int ret;
156
157   /* 获取key节点 */
158   nd = of_find_node_by_path("/key");
159   if(NULL == nd) {
160         printk(KERN_ERR "key: Failed to get key node\n");
161         return -EINVAL;
162   }
163
164   /* 读取status属性 */
165   ret = of_property_read_string(nd, "status", &str);
166   if(!ret) {
167         if (strcmp(str, "okay"))
168             return -EINVAL;
169   }
170
171   /* 获取compatible属性值并进行匹配 */
172   ret = of_property_read_string(nd, "compatible", &str);
173   if(ret)
174         return ret;
175
176   if (strcmp(str, "alientek,key")) {
177         printk(KERN_ERR "key: Compatible match failed\n");
178         return -EINVAL;
179   }
180
181   /* 获取设备树中的key-gpio属性,得到按键的GPIO编号 */
182   key.key_gpio = of_get_named_gpio(nd, "key-gpio", 0);
183   if(!gpio_is_valid(key.key_gpio)) {
184         printk(KERN_ERR "key: Failed to get key-gpio\n");
185         return -EINVAL;
186   }
187
188   /* 获取GPIO对应的中断号 */
189   key.irq_num = irq_of_parse_and_map(nd, 0);
190   if (!key.irq_num)
191         return -EINVAL;
192
193   return 0;
194 }
195
196 static int key_gpio_init(void)
197 {
198   unsigned long irq_flags;
199   int ret;
200
201   /* 申请使用GPIO */
202   ret = gpio_request(key.key_gpio, "Key Gpio");
203   if (ret)
204         return ret;
205
206   /* 将GPIO设置为输入模式 */
207   gpio_direction_input(key.key_gpio);
208
209   /* 获取设备树中指定的中断触发类型 */
210   irq_flags = irq_get_trigger_type(key.irq_num);
211   if (IRQF_TRIGGER_NONE == irq_flags)
212         irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
213
214   /* 申请中断 */
215   ret = request_irq(key.irq_num, key_interrupt, irq_flags, "PS Key0 IRQ", NULL);
216   if (ret) {
217         gpio_free(key.key_gpio);
218         return ret;
219   }
220
221   return 0;
222 }
223
224 /* 设备操作函数 */
225 static struct file_operations key_fops = {
226   .owner                = THIS_MODULE,
227   .open                = key_open,
228   .read                        = key_read,
229   .write                = key_write,
230   .release                = key_release,
231 };
232
233 static int __init mykey_init(void)
234 {
235   int ret;
236
237   /* 初始化自旋锁 */
238   spin_lock_init(&key.spinlock);
239
240   /* 设备树解析 */
241   ret = key_parse_dt();
242   if (ret)
243         return ret;
244
245   /* GPIO、中断初始化 */
246   ret = key_gpio_init();
247   if (ret)
248         return ret;
249
250   /* 初始化cdev */
251   key.cdev.owner = THIS_MODULE;
252   cdev_init(&key.cdev, &key_fops);
253
254   /* 添加cdev */
255   ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME);
256   if (ret)
257         goto out1;
258
259   ret = cdev_add(&key.cdev, key.devid, KEY_CNT);
260   if (ret)
261         goto out2;
262
263   /* 创建类 */
264   key.class = class_create(THIS_MODULE, KEY_NAME);
265   if (IS_ERR(key.class)) {
266         ret = PTR_ERR(key.class);
267         goto out3;
268   }
269
270   /* 创建设备 */
271   key.device = device_create(key.class, NULL,
272               key.devid, NULL, KEY_NAME);
273   if (IS_ERR(key.device)) {
274         ret = PTR_ERR(key.device);
275         goto out4;
276   }
277
278   /* 初始化定时器 */
279   init_timer(&key.timer);
280   key.timer.function = key_timer_function;
281
282   return 0;
283
284 out4:
285   class_destroy(key.class);
286
287 out3:
288   cdev_del(&key.cdev);
289
290 out2:
291   unregister_chrdev_region(key.devid, KEY_CNT);
292
293 out1:
294   free_irq(key.irq_num, NULL);
295   gpio_free(key.key_gpio);
296
297   return ret;
298 }
299
300 static void __exit mykey_exit(void)
301 {
302   /* 删除定时器 */
303   del_timer_sync(&key.timer);
304
305   /* 注销设备 */
306   device_destroy(key.class, key.devid);
307
308   /* 注销类 */
309   class_destroy(key.class);
310
311   /* 删除cdev */
312   cdev_del(&key.cdev);
313
314   /* 注销设备号 */
315   unregister_chrdev_region(key.devid, KEY_CNT);
316
317   /* 释放中断 */
318   free_irq(key.irq_num, NULL);
319
320   /* 释放GPIO */
321   gpio_free(key.key_gpio);
322 }
323
324 /* 驱动模块入口和出口函数注册 */
325 module_init(mykey_init);
326 module_exit(mykey_exit);
327
328 MODULE_AUTHOR("DengTao <<a href="mailto:773904075@qq.com">773904075@qq.com</a>>");
329 MODULE_DESCRIPTION("Gpio Key Interrupt Driver");
330 MODULE_LICENSE("GPL");       第34~38行,定义了一个枚举类型,包含3个常量KEY_PRESS、KEY_RELEASE、KEY_KEEP,分别用来表示按键的3种不同的状态,即按键按下、按键松开以及按键状态保持。
       第41~50行,结构体key_dev为按键设备所对应的结构体,key_gpio为按键GPIO编号,irq_num为按键IO对应的中断号;除此之外,结构体当中还定义了一个定时器用于实现按键的去抖操作,还定义了一个自旋锁用于实现对关键代码的保护操作。
       第52行,定义一个按键设备key。
       第53行,定义一个int类型的静态全局变量status用来表示按键的状态。
       第75~94行,key_read函数,对应应用程序的read函数。此函数向应用程序返回按键状态信息数据;这个函数其实很简单,使用copy_to_user函数直接将statue数据发送给应用程序,status变量保存了按键当前的状态,发送完成之后再将按键状态重置即可!需要注意的是,该函数中使用了自旋锁进行保护。
       第120~142行,key_timer_function函数为定时器定时处理函数,它的参数arg在本驱动程序中我们并没有使用到;该函数中定义了一个静态局部变量last_val用来保存按键上一次读取到的值,变量current_val用来存放当前按键读取到的值;第130~136行,通过读取到的按键值以及上一次读取到的值来判断按键当前所属的状态,如果本次读取的值为0,而上一次读取的值1,则表示按键按下;如果本次读取的值为1,而上一次读取的值0,则表示按键松开;如果本次读取的值为0,而上一次读取的值也是0,则表示按键一直被按着;如果本次读取的值为1,而上一次读取的值也是1,则表示没有触碰按键。第138行,当状态判断完成之后,会将current_val的值赋值给last_val。本函数中也使用自旋锁对全局变量status进行加锁保护!
       第144~149行,key_interrupt函数是按键PS_KEY0中断处理函数,参数dev_id是一个void类型的指针,本驱动程序并没使用到这个参数;这个中断处理函数很简单直接开启定时器,延时15毫秒,用于实现按键的软件防抖。
       第151~194行,key_parse_dt函数中主要是对设备树中的属性进行了解析,获取设备树中的key节点,通过of_get_named_gpio函数得到按键的GPIO编号,通过irq_of_parse_and_map函数获取按键的中断号,irq_of_parse_and_map函数会解析key节点中的interrupt-parent和interrupts属性然后得到一个中断号,后面就可以使用这个中断号去申请以及释放中断了。
       第196~222行,key_gpio_init函数中主要对GPIO以及中断进行了相关的初始化。使用gpio_request函数申请GPIO使用权,通过gpio_direction_input将GPIO设置为输出模式;irq_get_trigger_type函数可以获取到key节点中定义的中断触发类型,最后使用request_irq申请中断,并设置key_interrupt函数作为我们的按键中断处理函数,当按键中断发生之后便会跳转到该函数执行;request_irq函数会默认使能中断,所以不需要enable_irq来使能中断,当然我们也可以在申请成功之后先使用disable_irq函数禁用中断,等所有工作完成之后再来使能中断,这样会比较安全,建议大家这样使用。
       第225~231行,按键设备的file_operations结构体。
       第233~298行,mykey_init是驱动入口函数,第238行调用spin_lock_init初始化自旋锁变量,279~280行对定时器进行初始化并将key_timer_function函数绑定为定时器定时处理函数,当定时时间到了之后便会跳转到该函数执行。
       第300~322行,mykey_exit驱动出口函数,第303行调用del_timer_sync函数删除定时器,代码中已经注释得非常详细了,这里便不再多说!
1.3.3编写测试APP
       测试APP要实现的内容很简单,通过不断的读取/dev/key设备文件来获取按键值来判断当前按键的状态,从按键驱动上传到应用程序的数据可以有3个值,分别为0、1、2;0表示按键按下时的这个状态,1表示按键松开时对应的状态,而2表示按键一直被按住或者松开;搞懂数据代表的意思之后,我们开始编写测试程序,在13_irq目录下新建名为keyApp.c的文件,然后输入如下所示内容:
示例代码 33.3.4 keyApp.c文件代码
1 /***************************************************************
2Copyright © ALIENTEK Co., Ltd. 1998-2029. All rights reserved.
3文件名            : keyApp.c
4作者                : 邓涛
5版本                : V1.0
6描述                : 按键测试应用程序
7其他                : 无
8使用方法            : ./keyApp /dev/key
9论坛                : <a href="www.openedv.com" target="_blank">www.openedv.com</a>
10日志                : 初版V1.0 2019/1/30 邓涛创建
11***************************************************************/
12
13 #include <stdio.h>
14 #include <unistd.h>
15 #include <sys/types.h>
16 #include <sys/stat.h>
17 #include <fcntl.h>
18 #include <stdlib.h>
19 #include <string.h>
20
21 /*
22* @description                : main主程序
23* @param – argc                : argv数组元素个数
24* @param – argv                : 具体参数
25* @return                        : 0 成功;其他 失败
26*/
27 int main(int argc, char *argv[])
28 {
29   int fd, ret;
30   int key_val;
31
32   /* 判断传参个数是否正确 */
33   if(2 != argc) {
34         printf("Usage:\n"
35            "\t./keyApp /dev/key\n"
36             );
37         return -1;
38   }
39
40   /* 打开设备 */
41   fd = open(argv, O_RDONLY);
42   if(0 > fd) {
43         printf("ERROR: %s file open failed!\n", argv);
44         return -1;
45   }
46
47   /* 循环读取按键数据 */
48   for ( ; ; ) {
49
50         read(fd, &key_val, sizeof(int));
51         if (0 == key_val)
52             printf("Key Press\n");
53         else if (1 == key_val)
54             printf("Key Release\n");
55   }
56
57   /* 关闭设备 */
58   close(fd);
59   return 0;
60 }
      第48~55行使用for循环不断的读取按键值,如果读取到的值是0则打印“Key Press”字符串,而过读取到的值是1则打印“Key Release”字符串。
1.4运行测试
1.4.1编译驱动程序和测试APP
1、编译驱动程序
编写Makefile文件,直接将上一章实验目录12_timer下的Makefile文件拷贝到本实验目录下,打开该Makefile文件,将obj-m变量的值改为keyirq.o,Makefile内容如下所示:
示例代码 33.4.1 Makefile文件
1 KERN_DIR := /home/zynq/linux/kernel/linux-xlnx-xilinx-v2018.3
2
3 obj-m := keyirq.o
4
5 all:
6         make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -C $(KERN_DIR) M=`pwd` modules
7
8 clean:
9         make -C $(KERN_DIR) M=`pwd` clean       第3行,设置obj-m变量的值为keyirq.o。
       修改完成之后保存退出,输入如下命令编译出驱动模块文件:
make       编译成功以后就会生成一个名为“keyirq.ko”的驱动模块文件,如下所示:

图 33.4.1 编译驱动模块
       2、编译测试APP
       输入如下命令编译测试imx6uirqApp.c这个测试程序:
arm-linux-gnueabihf-gcc keyApp.c -o keyApp       编译成功以后就会生成keyApp这个应用程序。
1.4.2运行测试
       将上一小节编译出来keyirq.ko和keyApp这两个文件拷贝到开发板根文件系统/lib/modules/4.14.0-xilinx目录中,然后重启开发板,进入到目录/lib/modules/4.14.0-xilinx中,输入如下命令加载keyirq.ko驱动模块:
depmod                              //第一次加载驱动的时候需要运行此命令
modprobe keyirq.ko      //加载驱动       驱动加载成功以后可以通过查看/proc/interrupts文件来检查一下对应的中断有没有被注册上,输入如下命令:
cat /proc/interrupts       结果如图 33.4.2所示:

图 33.4.2 查看当前系统已经使用的中断号
       图 33.4.2中红框标识的中断信息就是keyirq按键中断驱动程序中使用的中断,中断号为60,触发方式为跳边沿(Edge),中断命名为“PS Key0 IRQ”。
       接下来使用如下命令来测试按键中断工作是否正常了:
./keyApp /dev/key      按下开发板上的PS_KEY0键,终端就会打印出字符串,如图 33.4.3所示:

图 33.4.3 按键中断测试结果
       从图 33.4.3可以看出,当我们按下PS_KEY0按键时,终端会打印出“Key Press”字符串,当松开按键时,中断会打印出“Key Release”字符串;由于我们在驱动程序中加入了软件防抖处理,所以效果会比第三十一章测试时好很多。
       如果要卸载驱动的话输入如下命令即可:
rmmod keyirq.ko

页: [1]
查看完整版本: 《领航者ZYNQ之嵌入式Linux开发指南_V2.0》第三十三章 Linux中断