正点原子嵌入式linux驱动开发——Linux中断

不管是单片机裸机实验还是Linux下的驱动实验,中断都是频繁使用的功能,在裸机中使用中断需要做一大堆的工作,比如配置寄存器,使能IRQ等等。但是Linux内核提供了完善的中断框架,只需要申请中断,然后注册中断处理函数即可,使用非常方便,不需要一系列复杂的寄存器配置。

Linux中断简介

Linux中断API函数

可以先来回顾一下裸机开发里中断的处理方法:

  1. 使能中断,初始化相应的寄存器。
  2. 注册中断服务函数,也就是向irqTable数组的指定标号处写入中断服务函数。
  3. 中断发生以后进入IRQ中断服务函数,在IRQ中断服务函数在数组irqTable里面查找具体的中断处理函数,找到以后执行相应的中断处理函数。

在Linux内核中也提供了大量的中断相关的API函数,来看一下这些跟中断有关的API函数:

中断号

每个中断都有一个中断号,通过中断号即可区分不同的中断,有的资料也把中断号叫做中断线。在Linux内核中使用一个int变量表示中断号

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里面查看所有的中断标志,这里介绍几个常用的中断标志,如下图所示:
    常用中断标志
    比如STM32MP157开发板上的KEY0使用PG3,按下KEY0以后为低电平,因此可以设置为下降沿触发,也就是将flags设置为 IRQF_TRIGGER_FALLING。上图中这些标志位可以通过“|”来实现多种组合。
  • name:中断名字,设置以后可以在/proc/interrupts文件中看到对应的中断名字。
  • dev:如果将flags设置为IRQF_SHARED的话,dev用来区分不同的中断,一般情况下将dev设置为设备结构体,dev会传递给中断处理函数irq_handler_t的第二个参数。
  • 返回值:0,中断申请成功,其他负值,中断申请失败,如果返回-EBUSY的话表示中断已经被申请了。

free_irq函数

使用中断的时候需要通过request_irq函数申请,使用完成以后就要通过free_irq函数释放掉相应的中断。如果中断不是共享的,那么free_irq会删除中断处理函数并且禁止中断。free_irq函数原型如下所示:

void free_irq(unsigned int irq, void *dev) 

函数参数和返回值含义如下:

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

中断处理函数

使用request_irq函数申请中断的时候需要设置中断处理函数,中断处理函数格式如下所示:

irqreturn_t (*irq_handler_t) (int, void *) 

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

return IRQ_RETVAL(IRQ_HANDLED)

中断使能与禁止函数

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

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全局中断,产生冲突,可能导致整个系统崩溃。为了解决这个问题,B任务不能直接简单粗暴的通过local_irq_enable函数来打开全局中断,而是将中断状态恢复到以前的状态,要考虑到别的任务的感受,此时就要用到下面两个函数:

local_irq_save(flags) 
local_irq_restore(flags)

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

上半部与下半部

在有些资料中也将上半部和下半部称为顶半部和底半部,都是一个意思。在使用request_irq申请中断的时候注册的中断服务函数属于中断处理的上半部,只要中断触发,那么中断处理函数就会执行。中断处理函数一定要快点执行完毕,越短越好,但是有些中断处理过程就是比较费时间,必须要对其进行处理,缩小中断处理函数的执行时间。比如电容触摸屏通过中断通知SOC有触摸事件发生,SOC响应中断,然后通过IIC接口读取触摸坐标值并将其上报给系统。但是IIC的速度最高也只有400Kbit/S,所以在中断中通过IIC读取数据就会浪费时间。可以将通过IIC读取触摸数据的操作暂后执行,中断处理函数仅仅相应中断,然后清除中断标志位即可。这个时候中断处理过程就分为了两部分:

  • 上半部:上半部就是中断处理函数,那些处理过程比较快,不会占用很长时间的处理就可以放在上半部完成。
  • 下半部:如果中断处理过程比较耗时,那么就将这些比较耗时的代码提出来,交给下半部去执行,这样中断处理函数就会快进快出。

因此,Linux内核将中断分为上半部和下半部的主要目的就是实现中断处理函数的快进快出,那些对时间敏感、执行速度快的操作可以放到中断处理函数中,也就是上半部。剩下的所有工作都可以放到下半部去执行,比如在上半部将数据拷贝到内存中,关于数据的具体处理就可以放到下半部去执行。至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断,这个就很考验驱动编写人员的功底了。这里有一些可以借鉴的参考点:

  1. 如果要处理的内容不希望被其他中断打断,可以放到上半部。
  2. 如果要处理的任务对时间敏感,可以放到上半部。
  3. 如果要处理的任务与硬件有关,可以放到上半部。
  4. 除了上述三点以外的其他任务,优先考虑放到下半部。

上半部处理很简单,直接编写中断处理函数就行了,Linux内核提供了多种下半部机制,接下来学习一下这些下半部机制。

软中断

一开始Linux内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中断和tasklet来替代“BH”机制,完全可以使用软中断和tasklet来替代 BH,从2.5版本的 Linux内核开始BH已经被抛弃了。 Linux内核使用结构体softirq_action表示软中断,softirq_action结构体定义在文件include/linux/interrupt.h中,内容如下:

示例代码31.1.2.1 softirq_action结构体 
541 struct softirq_action 
542 { 
543     void (*action)(struct softirq_action *); 
544 };

在kernel/softirq.c文件中一共定义了10个软中断,如下所示:

示例代码31.1.2.2 softirq_vec数组 
static struct softirq_action softirq_vec[NR_SOFTIRQS];

NR_SOFTIRQS是枚举类型,定义在文件include/linux/interrupt.h中,定义如下:
softirq_vec数组
可以看出,一共有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:要开启的软中断,在示例代码31.1.2.3中选择要开启的软中断。
  • action:软中断对应的处理函数。
  • 返回值:没有返回值。

注册好软中断以后需要通过raise_softirq函数触发,raise_softirq函数原型如下:

void raise_softirq(unsigned int nr) 

函数参数和返回值含义如下:

  • nr:要触发的软中断,在示例代码31.1.2.3中选择要注册的软中断。
  • 返回值:没有返回值。

软中断必须在编译的时候静态注册!Linux内核使用softirq_init函数初始化软中断,softirq_init函数定义在kernel/softirq.c文件里面,函数内容如下:
softirq_init函数内容
从示例代码31.1.2.4可以看出,softirq_init函数默认会打开TASKLET_SOFTIRQ和HI_SOFTIRQ。

tasklet

tasklet是利用软中断来实现的另外一种下半部机制,在软中断和tasklet之间,建议使用tasklet。tasklet_struct结构体如下所示:
tasklet_struct结构体
第597行的func函数就是tasklet要执行的处理函数,用户实现具体的函数内容,相当于中断处理函数。如果要使用tasklet,必须先定义一个tasklet_struct变量,然后使用tasklet_init函
数对其进行初始化
,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的参考使用示例如下所示:

示例代码31.1.2.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); ...... 
}

工作队列

工作队列是另外一种下半部执行方式,工作队列在进程上下文执行,工作队列将要推后的工作交给一个内核线程去执行,因为工作队列工作在进程上下文,因此工作队列允许睡眠或重新调度。因此如果要推后的工作可以睡眠那么就可以选择工作队列,否则的话就只能选择软中断或tasklet

Linux内核使用work_struct结构体表示一个工作,内容如下(省略掉条件编译):
work_struct结构体
这些工作组织成工作队列,工作队列使用workqueue_struct结构体表示,内容如下(省略掉
条件编译):
workqueue_struct结构体
Linux内核使用工作者线程(worker thread)来处理工作队列中的各个工作,Linux内核使用worker结构体表示工作者线程, worker结构体内容如下:
worker结构体
从示例代码31.1.2.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一样,工作也是需要调度才能运行的,工作的调度函数为 chedule_work,函数原型如下所示:

bool schedule_work(struct work_struct *work) 

函数参数和返回值含义如下:

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

关于工作队列的参考使用示例如下所示:

示例代码31.1.2.11 工作队列使用示例 
/* 定义工作(work) */ 
struct work_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); ...... 
}

设备树中断信息节点

GIC中断控制器

STM32MP1有三个与中断有关的控制器:GIC、EXTI和NVIC,其中NVIC是Cortex-M4内核的中断控制器,正点原子的linux驱动开发教程只讲解Cortex-A7内核,因此就只剩下了GIC和EXTI。首先是GIC全称为:Generic Interrupt Controller。

GIC是ARM公司给Cortex-A/R内核提供的一个中断控制器,类似Cortex-M内核中的NVIC。目前GIC有4个版本 :V1-V4,V1是最老的版本,已经被废弃了。 V2-V4目前正在大量的使用。GIC V2是给ARMv7-A架构使用的,比如Cortex-A7、Cortex-A9、Cortex-A15等,V3和 V4是给ARMv8-A/R架构使用的,也就是64位芯片使用的。STM32MP1是Cortex-A7内核,因此主要讲解GIC V2。GIC V2最多支持8个核。ARM会根据GIC版本的不同研发
出不同的IP核,那些半导体厂商直接购买对应的IP核即可,比如ARM针对GIC V2就开发出了GIC400这个中断控制器IP核。当GIC接收到外部中断信号以后就会报给ARM内核,但是ARM内核只提供了四个信号给GIC来汇报中断情况:VFIQ、VIRQ、FIQ和IRQ,他们之间的关系如下图所示:
中断示意图
在上图中,GIC接收众多的外部中断,然后对其进行处理,最终就只通过四个信号报给ARM内核,这四个信号的含义如下:

  • VFIQ:虚拟快速FIQ。
  • VIRQ:虚拟快速IRQ。
  • FIQ:快速中断IRQ。
  • IRQ:外部中断IRQ。

VFIQ和VIRQ是针对虚拟化的,不讨论虚拟化,剩下的就是FIQ和IRQ了,正点原子这个linux驱动开发教程中只使用IRQ。所以相当于GIC最终向ARM内核就上报一个IRQ信号。GIC V2的逻辑图如下图所示:
GIC V2总体框图
上图中左侧部分就是中断源,中间部分就是GIC控制器,最右侧就是中断控制器向处理器内核发送中断信息。重点要看的肯定是中间的GIC部分,GIC将众多的中断源分为分为三类

  1. SPI(Shared Peripheral Interrupt),共享中断,顾名思义,所有Core共享的中断,这个是最常见的,那些外部中断都属于SPI中断(注意!不是SPI总线那个中断) 。比如GPIO中断、串口中断等等,这些中断所有的Core都可以处理,不限定特定Core。
  2. PPI(Private Peripheral Interrupt),私有中断,GIC是支持多核的,每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理,因此这些中断就叫做私有中断。
  3. SGI(Software-generated Interrupt),软件中断,由软件触发引起的中断,通过向寄存器GICD_SGIR写入数据来触发,系统会使用SGI中断来完成多核之间的通信。

中断ID

中断源有很多,为了区分这些不同的中断源肯定要给他们分配一个唯一ID,这些ID就是中断ID。每一个CPU最多支持1020个中断ID,中断ID号为ID0-ID1019。这1020个ID包含了PPI、SPI和SGI,这1020个ID分配如下:

  • ID0-ID15:这16个ID分配给SGI。
  • ID16-ID31:这16个ID分配给PPI。
  • ID32-ID1019:这988个ID分配给SPI,像GPIO中断、串口中断等这些外部中断 ,至于具体到某个ID对应哪个中断那就由半导体厂商根据实际情况去定义了。比如STM32MP1系列总共分配了265个中断ID(有很多并未使用,只是保留着),加上前面属于PPI和SGI的32个ID,STM32MP1的中断源共有256+32=288个,这288个中断ID对应的中断源可以在《 STM32MP157参考手册》中找到详细的解释。重点关注的是从ID32开始的SPI中断,因为这些才是STM32MP1的外设中断,其中部分如下图所示:
    STM32MP1中断源

EXTI——外部中断和事件控制器

EXTI全称是:Extended interrupt and event controller,中文一般叫做外部中断和事件控制器EXTI是ST自己设计的,用来辅助GIC管理 STM32MP1相应中断的。EXTI通过可配置的事件输入和直接事件输入来管理唤醒。它可以针对电源控制提供唤醒请求、针对CPU事件输入生成事件。EXTI唤醒请求可让系统从停止模式唤醒,以及让CPU从CSTOP和CSTANDBY模式唤醒。此外,EXTI还可以在运行模式下生成中断请求和事件请求,这个非常重要,因为在实际使用中EXTI主要是为STM32的GPIO中断服务的

EXTI主要特性如下:

  • 支持76个输入事件。
  • 两个CPU内核都支持。
  • 所有事件输入均可让CPU唤醒。

EXTI的异步输入事件可以分为2组:

  • 可配置事件(来自能够生成脉冲的I/O或外设的信号),这类事件具有以下特性:
    • 可选择的有效触发边沿。
    • 中断挂起状态寄存器位。
    • 单独的中断和事件生成屏蔽。
    • 支持软件触发。
  • 直接事件(来自其他外设的中断和唤醒源,需要在外设中清除),这类事件具有以下特性:
    • 固定上升沿有效触发。
    • EXTI中无中断挂起状态寄存器位(中断挂起状态由生成事件的外设提供)。
    • 单独的中断和事件生成屏蔽。
    • 不支持软件触发。

对于GPIO中断来说,就是可配置事件,EXTI和GIC的关系如下图所示:
EXTI框架
从上图可以看出STM32MP1的中断处理方式有5种:

  1. 外设直接产生中断到GIC,然后由GIC通知CPU内核。
  2. GPIO或外设产生中断到EXTI,EXTI将信号提交给GIC,最终再将中断信号提交给CPU。
  3. GPIO或外设产生中断到EXTI,EXTI直接将中断信号提交给CPU。

Linux系统会用到这三种中断方式,一个外设最多可以有两种中断方式

STM32MP1的所有GPIO都有中断功能,而GPIO中断是最常用的功能。STM32每一组GPIO最多有16个IO,比如PA0-PA15,因此每组GPIO就有16个中断,这16个GPIO事件输入对应EXTI0-15,其中PA0、PB0等都对应EXTI0,如下图所示:
EXTI0-15对应的GPIO
如果要在Linux系统中使用中断,那么就需要在设备树中设置好中断属性信息,Linux内核通过读取设备树中的中断属性信息来配置中断,GIC控制器的设备树绑定信息参考文档Documentation/devicetree/bindings/interrupt-controller/arm,gic.yaml,EXTI控制器的设备树绑定
信息参考文档Documentation/devicetree/bindings/interrupt-controller/st,stm32-exti.txt。

GIC控制器节点

打开stm32mp151.dtsi文件,其中的intc节点就是GIC的中断控制器节点,节点内容如下所示:

示例代码31.1.3.1 中断控制器intc节点 
1 intc: interrupt-controller@a0021000 { 
2     compatible = "arm,cortex-a7-gic"; 
3     #interrupt-cells = <3>; 
4     interrupt-controller; 
5     reg = <0xa0021000 0x1000>, 
6           <0xa0022000 0x2000>; 
7 };

第2行,compatible属性值为“arm,cortex-a7-gic”,在Linux内核源码中搜索“arm,cortex-a7-gic”即可找到GIC中断控制器驱动文件。

第3行,#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:中断号,对于SPI中断来说中断号的范围为32-287(256个),对于PPI中断来说中断号的范围为16-31,但是该cell描述的中断号是从0开始。
  • 第三个cells:标志bit[3:0]表示中断触发类型,为1的时候表示上升沿触发,为2的时候表示下降沿触发,为4的时候表示高电平触发,为8的时候表示低电平触发。bit[15:8]为PPI中断的CPU掩码。

第4行, interrupt-controller节点为空,表示当前节点是中断控制器。

来看一下STM32MP1的SPI6是如何在设备树节点中描述中断信息的,首先是查阅《 STM32MP157参考手册》第“21.2 GIC Interrupts”小节中的表117。找到SPI6对应的中断号,如下图所示:
SPI6中断
从上图可以看出,第一列的“Num”就是SPI6的中断号:86,注意这里并没有算前面32个中断号,如果加上前面32个中断号的话就是第二列“ID”,为86+32=118。

打开stm32mp151.dtsi,找到SPI6节点内容,如下所示:
SPI6节点
第6行,interrupts描述中断中断源的信息,第一个表示中断类型,为GIC_SPI,也就是共享中断。第二个表示中断号为86,来源就是上图。第三个表示中断触发类型是高电平触发。

EXTI控制器节点

对于GPIO中断而言,要用到EXTI,所以接下来看一下EXTI设备节点。打开stm32mp151.dtsi文件,其中的exti节点就是EXTI的中断控制器节点,节点内容如下所示:
中断控制器exti节点
第3行,表明exti节点是个中断控制器。

第4行,interrupt-cells=2,表明exti的子节点里面第一个cell表示为中断号,也可以叫EXTI事件编号,第二个cell表示中断标志位。其它的设备树属性和GIC控制器是一样的。

GPIO用到了EXTI,因此GPIO节点信息里面的EXTI相关内容,在stm32mp151.dtsi文件中找到如下所示内容:
GPIO中断控制器节点
第1-131行,pinctrl节点,此节点有11个子节点,gpioa-gpiok,分别对应GPIOA-GPIOK这11组 IO。

第8行,通过interrupt-parent属性指定pinctrl所有子节点的中断父节点为exti,这样GPIO的中断就和EXTI联系起来了。

第11-20行为gpioa节点,第14行表明gpioa节点也是个中断控制器,第15行设置interrupt-cells为2,那么对于具体的GPIO而言,interrupts属性第一个cell为某个IO在所处组的编号,第二个cell表示中断触发方式

第133-154行,pinctrl_z节点,由于GPIOZ这一组对应的寄存器地址和GPIOA~GPIOK不是连续的,所以单独使用pinctrl_z来描述GPIOZ,含义和pinctrl一样

可以找一个具体的应用,打开stm32mp15xx-dkx.dtsi文件,找到如下所示内容:
hdmi节点信息
sii9022是ST官方在开发板上的一个HDMI芯片,上述代码就是sii9022的节点信息,sii9022a芯片有一个中断,此引脚链接到了STM32MP1的PG1上,此中断是下降沿触发。

第7行,interrupts设置中断信息,1表示本组内第一个IO,在这里就是PG1。IRQ_TYPE_EDGE_FALLING表示下降沿触发 。

第8行,interrupt-parent属性设置中断控制器,这里是有gpiog作为中断控制器。结合上面的interrupts属性,这两行的目的就是设置PG1为下降沿触发。

可以看出使用起来是非常的简单,在实际编写代码的时候,只需要通过interrupt-parent和interrupts这两个属性即可设置某个GPIO的中断功能

简单总结一下与中断有关的设备树属性信息:

  1. #interrupt-cells,指定中断源的信息cells个数。
  2. interrupt-controller,表示当前节点为中断控制器。
  3. interrupts,指定中断号,触发方式等。
  4. interrupt-parent,指定父中断,也就是中断控制器。
  5. interrupts-extended,指定中断控制器、中断号、中断类型和触发方式,这个属性比较特殊,是新加入的。前面说了,要通过interrupts和interrupt-parent一起设置某个IO的中断方式。这里也可以只使用interrupts-extended属性一次性指定中断父节点,IO编号,中断方式等

打开stm32mp157f-ev1-a7-examples.dts文件,里面有如下所示代码:
test_keys节点
很明显,上述代码用于描述一个按键,此按键采用中断方式,这个按键使用到了PA13这个引脚。第13行直接通过interrupts-extended属性描述了所有的中断信息,第一个参数为gpioa,第二个参数为13,第三个参数表示下降沿触发。如果使用interrupts和interrupt-parent来描述的话就是:

interrupt-parent = <&gpioa>; 
interrupts = <13 IRQ_TYPE_EDGE_FALLING>;

获取中断号

编写驱动的时候需要用到中断号,用到的中断号,中断信息已经写到了设备树里面,可以通过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对应的中断号。

硬件原理图分析

就是按键的原理图,之前的笔记中已经有过分析了,这里不再赘述。

实验程序编写

本章实验驱动正点原子的STM32MP157开发板上的KEY0按键,不过采用中断的方式,并且采用定时器来实现按键消抖,应用程序读取按键值并且通过终端打印出来

修改设备树文件

本章实验使用到了按键KEY0,按键KEY0使用中断模式,因此需要在“key”节点下添加中断相关属性,添加完成以后的“key”节点内容如下所示:

示例代码31.3.1.1 key节点信息 
1 key { 
2     compatible = "alientek,key"; 
3     status = "okay"; 
4     key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>; 
5     interrupt-parent = <&gpiog>; 
6     interrupts = <3 IRQ_TYPE_EDGE_BOTH>; 
7 };

第5行,设置interrupt-parent属性值为“gpiog”,因为KEY0所使用的GPIO为PG3,所以要设置KEY0的中断控制器为gpiog。

第6行,设置interrupts属性,也就是设置中断源,第一个cells的3表示GPIOG组的3号IO。IRQ_TYPE_EDGE_BOTH定义在文件include/linux/irq.h中,定义如下:

示例代码31.3.1.2 中断状态 
75 enum { 
76     IRQ_TYPE_NONE = 0x00000000, 
77     IRQ_TYPE_EDGE_RISING = 0x00000001, 
78     IRQ_TYPE_EDGE_FALLING = 0x00000002, 
79     IRQ_TYPE_EDGE_BOTH = (IRQ_TYPE_EDGE_FALLING | IRQ_TYPE_EDGE_RISING), 
80     IRQ_TYPE_LEVEL_HIGH = 0x00000004, 
81     IRQ_TYPE_LEVEL_LOW = 0x00000008, 
82     IRQ_TYPE_LEVEL_MASK = (IRQ_TYPE_LEVEL_LOW |IRQ_TYPE_LEVEL_HIGH), 
...... 
100 };

从示例代码31.3.1.2中可以看出,IRQ_TYPE_EDGE_BOTH表示上升沿和下降沿同时有效,相当于KEY0按下和释放都会触发中断。

设备树编写完成以后使用“make dtbs”命令重新编译设备树,然后使用新编译出来的stm32mp157d-atk.dtb文件启动Linux系统。

按键中断驱动程序编写

首先要定义一个枚举类型,来通过3个常量KEY_PRESS、KEY_RELEASE、KEY_KEEP来表征3种按键状态。

然后定义一个key_dev来完成设备结构体的编写,这里主要最后添加一个struct timer_list timer进行按键值的定时判断;int irq_num来表示中断号;最后加一个自旋锁spinlock_t spinlock。

之后具象化一个key表示按键设备,同时初始化一个static int status来表示按键状态,初始化为KEY_KEEP。

编写一个中断处理函数irqreturn_t key_interrupt,通过15ms的定时器延时来做按键防抖,直接mod_timer激活定时器即可,然后return一个IRQ_HANDLED。

之后编写key_parse_dt函数,该函数主要是对设备树中的属性进行解析,整个的初始化与之前的都差不多,显示走流程获取节点,读取status和compatible属性,然后获取设备树的gpio属性;这之后就有区别了,通过irq_of_parse_and_map来获取中断号并存入key.irq_num。

之后写一个key_gpio_init,里面通过gpio_request申请GPIO口,设置GPIO为输入模式,这些跟之前一样;之后,需要通过irq_get_trigger_type获取中断触发类型,之后通过request_irq申请中断,并设置key_interrupt为中断处理函数,且request_irq默认会使能中断(更安全一点,可以申请成功后先disable_irq,然后所有工作完成后再使能中断)。

之后编写key_timer_function,来判断按键状态,需要先上锁然后通过static一个last_val和current_val,gpio_get_value后比较来完成消抖和状态判断。

之后写key_read对应应用程序的read函数,这个就比较简单,直接copy_to_user把state发送给应用程序,发完重置按键状态即可。

最后是mykey_init来完成驱动入口函数,先spin_lock_init初始化自旋锁,然后通过key_parse_dt解析设备树,key_gpio_init初始化IO口,然后走流程注册字符设备驱动,最后加一下timer_setup设置定时器处理函数。

编写测试APP

测试APP要实现的内容很简单,通过不断的读取/dev/key设备文件来获取按键值来判断当前按键的状态,从按键驱动上传到应用程序的数据可以有3个值,分别为0、1、2。0表示按键按下时的这个状态,1表示按键松开时对应的状态,而2表示按键一直被按住或者松开。

这里就是open了之后,进入死循环来读取设备,直接read出来,然后判断读取的值打印就可以了。

运行测试

编译驱动程序和测试APP

编译驱动程序

这里同样的,直接修改一下Makefile的obj-m,改为keyirq.o,然后“make”以下就可以了。

编译测试APP

输入如下命令即可:

arm-none-linux-gnueabihf-gcc keyirqApp.c -o keyirqApp

运行测试

将上一小节编译出来keyirq.ko和keyirqApp这两个文件拷贝到rootfs/lib/modules/5.4.31目录中,重启开发板,进入到目录lib/modules/5.4.31中,输入如下命令加载keyirq.ko驱动模块:

depmod //第一次加载驱动的时候需要运行此命令
modprobe keyirq.ko //加载驱动

驱动加载完成后可以通过查看/proc/interrupts文件夹检查时候有对应中断被注册:

cat /proc/interrupts

结果如下图所示:
proc/interrupts文件内容
从上图可以看出keyirq.c驱动文件里面的KEY0中断已经存在了,触发方式为跳边沿(Edge)。

接下来使用如下命令来测试中断:

./keyirqApp /dev/key

按下开发板的KEY0按键,会在终端输出按键值,如下图所示:
读取到的按键值
从上图可以看出,按键值获取成功,并且不会有按键抖动导致的误判发生,说明按键消抖工作正常。如果要卸载驱动的话输入如下命令即可:

rmmod keyirq.ko

总结

使用中断的时候,可以直接在对应的GPIO节点上面添加interrupt-parent设定开启中断的GPIO,然后通过interrupts设置具体的引脚号以及触发沿。

然后需要编写中断处理函数,然后需要写一个针对设备树进行解析的函数,通过of_get_named_gpio获取GPIO编号,之后通过irq_of_parse_and_map获取中断号,通过这个中断号申请和释放中断。

GPIO的初始化函数中,要添加中断的初始化,需要irq_get_trigger_type来获取中断触发类型,最后通过request_irq申请中断,其中传入之前写好的中断处理函数。

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

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

相关文章

跟我一起写个虚拟机 .Net 7(四)- LC_3 解析实例

没想到这篇文章持续了这么久&#xff0c;越学越深&#xff0c;愣是又买了一本书《计算机系统概论》&#xff0c;当然&#xff0c;也看完了&#xff0c;受益匪浅。 系统化的学习才是正确的学习方式&#xff0c;我大学就没看到过这本书&#xff0c;如果早点看到&#xff0c;可能…

STM32F4之系统滴答定时器

一、系统滴答定时器概述 传统定时器&#xff1a;如手机闹钟&#xff0c;闹钟等就是一个简单地计数器。 定时器概念&#xff1a;由时钟源计数器计数值组成的计数单元。 系统嘀嗒定时器首先是存在于内核里&#xff0c;系统嘀嗒时钟假如用的是同一个内核那么里面相关的配置&…

Android问题笔记 - NoSuchmethodException: could not find Fragment constructor

点击跳转>Unity3D特效百例点击跳转>案例项目实战源码点击跳转>游戏脚本-辅助自动化点击跳转>Android控件全解手册点击跳转>Scratch编程案例点击跳转>软考全系列 &#x1f449;关于作者 专注于Android/Unity和各种游戏开发技巧&#xff0c;以及各种资源分享&…

守护进程深度分析

思考 代码中创建的会话&#xff0c;如何关联控制终端&#xff1f; 新会话关联控制终端的方法 会话首进程成功打开终端设备 (设备打开前处于空闲状态) 1、关闭标准输入输出和标准错误输出2、将 stdin 关联到终端设备&#xff1a;STDIN_FILENO > 03、将 stdout 关联到终端设…

每日刷题|贪心算法初识

食用指南&#xff1a;本文为作者刷题中认为有必要记录的题目 推荐专栏&#xff1a;每日刷题 ♈️今日夜电波&#xff1a;悬溺—葛东琪 0:34 ━━━━━━️&#x1f49f;──────── 3:17 &#x1f…

idea中还原dont ask again

背景 在使用idea打开另外一个项目的时候&#xff0c;一不小心勾选为当前项目而且是不在下次询问&#xff0c;导致后面每次打开新的项目都会把当前项目关闭&#xff0c;如下图所示 下面我们就一起看一下如何把这个询问按钮还原回来 preferences/settings->Appearance&…

展馆导览系统之AR互动式导航与展品语音讲解应用

一、项目背景 随着科技的进步和人们对于文化、艺术、历史等方面需求的提升&#xff0c;展馆在人们的生活中扮演着越来越重要的角色。然而&#xff0c;传统的展馆导览方式&#xff0c;如纸质导览、人工讲解等&#xff0c;已无法满足参观者的多元化需求。为了提升参观者的体验&a…

vulnhub靶机Funbox11

下载地址&#xff1a;Funbox: Scriptkiddie ~ VulnHub 主机发现 arp-scan -l 目标192.168.21.164 端口扫描 nmap --min-rate 1000 -p- 192.168.21.164 端口好多处理一下吧 nmap --min-rate 1000 -p- 192.168.21.164 |grep open |awk -F / {print $1} |tr \n , 端口服务版本…

【ARM裸机】ARM入门

1.ARM成长史 2.ARM的商业模式和生态系统 ARM只设计CPU&#xff0c;但是不生产CPU 3.为什么使用三星&#xff1a;S5PV210 4.各种版本号 0. ARM和Cortex Cortex就是ARM公司一个系列处理器的名称。比如英特尔旗下处理器有酷睿&#xff0c;奔腾&#xff0c;赛扬。ARM在最初的处理器…

P1950 长方形

题目&#xff1a; P1950 长方形 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 算法&#xff1a; dp动态规划 代码&#xff1a; #include<iostream> #include<string> typedef unsigned long long ull; const int N 1010; using namespace std;int r, c, i, …

打印新闻标题,使用封装get、set方法,打印前15个字符串

package day21; import java.util.ArrayList; import java.util.Collections;/*** author monian* Wo yi wu ta,wei shou shu er!*/ public class Homework01 {SuppressWarnings({"all"})public static void main(String[] args) {News news1 new News("新冠确…

6.5 Elasticsearch(五)Spring Data Elasticsearch - 增删改查API

文章目录 1.Spring Data Elasticsearch2.案例准备2.1 在 Elasticsearch 中创建 students 索引2.2 案例测试说明 3.创建项目3.1 新建工程3.2 新建 springboot module&#xff0c;添加 spring data elasticsearch 依赖3.3 pom.xml 文件3.4 application.yml 配置 4.Student 实体类…

在 Python 中使用 Pillow 进行图像处理【2/4】

第二部分 一、说明 该文是《在 Python 中使用 Pillow 进行图像处理》的第二部分&#xff0c;主要介绍pil库进行一般性处理&#xff1a;如&#xff1a;图像卷积、钝化、锐化、阈值分割。 二、在 Python 中使用 Pillow 进行图像处理 您已经学习了如何裁剪和旋转图像、调整图像大…

机器学习终极指南:统计和统计建模03/3 — 第 -3 部分

系列上文&#xff1a;机器学习终极指南&#xff1a;特征工程&#xff08;02/2&#xff09; — 第 -2 部分 一、说明 在终极机器学习指南的第三部分中&#xff0c;我们将了解统计建模的基础知识以及如何在 Python 中实现它们&#xff0c;Python 是一种广泛用于数据分析和科学计…

Spring Security认证架构介绍

在之前的Spring Security&#xff1a;总体架构中&#xff0c;我们讲到Spring Security整个架构是通过Bean容器和Servlet容器对过滤器的支持来实现的。我们将从过滤器出发介绍Spring Security的Servlet类型的认证架构。 1.AbstractAuthenticationProcessingFilter AbstractAut…

Git的介绍和命令汇总

目录 一、git介绍 1、git的工作区域 2、git中文件的四种状态 二、常用命令 1、基础命令 2、提交类命令 3、删除类命令 4、分支类相关命令 5、 查看类相关命令 6、撤销类命令 一、git介绍 1、git的工作区域 在Git中&#xff0c;有四个工作区域&#xff1a;工作区域&am…

CorelDRAW Graphics Suite2024完整版最新功能介绍

CorelDRAW平面设计软件通常也被叫做CDR&#xff0c;CDR广泛应用于排版印刷、矢量图形编辑及网页设计等领域。通过CorelDRAW体验极具个性的自由创作&#xff0c;大胆展现真我&#xff0c;交付出众的创意作品。CorelDRAW拥有矢量插图、页面布局、图片编辑和设计工具&#xff0c;无…

迅为RK3568开发板RTMP推流之视频监控

1 搭建 RTMP 媒流体服务器 nginx-rtmp 是一个基于 nginx 的 RTMP 服务模块&#xff0c;是一个功能强大的流媒体服务器模块&#xff0c; 它提供了丰富的功能和灵活的配置选项&#xff0c;适用于构建各种规模的流媒体平台和应用。无论是搭建实时视频直播平台、点播系统或多屏互…

leetcode 114. 二叉树展开为链表

2023.10.22 本题第一反应就是将 原二叉树的节点值 用先序遍历的方式保存到一个集合数组中。然后再重新构造出新的二叉树。 java代码如下&#xff1a; /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode rig…

我国跨境电商行业研究报告(2022)

我国跨境电商行业研究报告 我国跨境电商规模突飞猛进&#xff0c;2022年进出口规模超2万亿元&#xff0c;2023年上半年跨境电商出口8210亿元&#xff0c;增长19.9%。全国跨境电商主体已超10万家&#xff0c;近年来涌现出一批上市公司&#xff0c;以及广州希音等全球独角兽企业。…