17 Linux 中断-CSDN博客

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

一、Linux 中断简介

1. Linux 中断 API 函数

① 中断号

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

② request_irq 函数

  在 Linux 中想要使用某个中断是需要申请的request_irq 函数就是用来申请中断的并且 request_irq 函数会激活使能中断但 request_irq 函数会导致睡眠所以不能在中断上下文或者其他禁止睡眠的代码段中使用  request_irq 函数。

/*
 * @description : 申请内核中断并使能中断函数
 * @param - irq : 要申请中断的中断号
 * @param - handler : 中断处理函数当中断发生以后就会执行此中断处理函数
 * @param - flags : 中断标志
 * @param - name : 中断名字
 * @param - dev : 如果将 flags 设置为 IRQF_SHARED 的话 dev 用来区分不同的中断。
一般情况下将dev 设置为设备结构体dev 会传递给中断处理函数 irq_handler_t 的第二个参数
 * @return : 0 中断申请成功其他负值 中断申请失败如果返回-EBUSY 的话表示中断已经被申请了
 */
int request_irq(unsigned int irq,
                irq_handler_t handler,
                unsigned long flags,
                const char *name,
                void *dev)
中断标志描述
IRQF_SHARED多个设备共享一个中断线共享的所有中断都必须指定此标志。如果使用共享中断的话 request_irq 函数的 dev 参数就是唯一区分他们的标志。
IRQF_ONESHOT单次中断中断执行一次就结束。
IRQF_TRIGGER_NONE无触发。
IRQF_TRIGGER_RISING上升沿触发。
IRQF_TRIGGER_FALLING下降沿触发。
IRQF_TRIGGER_HIGH高电平触发。
IRQF_TRIGGER_LOW低电平触发。

③ free_irq 函数 

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

/*
 * @description : 释放中断
 * @param - irq : 要释放的中断
 * @param - dev : 如果中断设置为共享(IRQF_SHARED)的话此参数用来区分具体的中断。
                  共享中断只有在释放最后中断处理函数的时候才会被禁止掉
 * @return : 无
 */
void free_irq(unsigned int irq, void *dev);

④ 中断处理函数 

  使用 request_irq 函数申请中断的时候需要设置中断处理函数

irqreturn_t (*irq_handler_t) (int, void *);

/*
 第一个参数是要中断处理函数相应的中断号。
 第二个参数是一个指向 void 的指针也就是个通用指针需要与 request_irq 函数的 dev 参数保持一致。
 用于区分共享中断的不同设备dev 也可以指向设备数据结构。
 */

/* irqreturn_t 结构体 */
enum irqreturn 
{
    IRQ_NONE = (0 << 0),
    IRQ_HANDLED = (1 << 0),
    IRQ_WAKE_THREAD = (1 << 1),
};

typedef enum irqreturn irqreturn_t;


/* 其实一般中断服务函数返回值使用*/
return IRQ_RETVAL(IRQ_HANDLED)

⑤ 中断使能与禁止函数 

void enable_irq(unsigned int irq);        // 使能指定中断
void disable_irq(unsigned int irq);       // 禁止指定中断

// 其实他们的参数 irq 都是要使能/禁止的中断号
// disable_irq 函数有个缺点是使用者需要保证不会产生新的中断并且确保所有已经开始执行的中断处理程序已经全部退出

  但我们不能确保的情况下使用这个中断禁止函数推荐使用

// 函数调用以后立即返回不会等待当前中断处理程序执行完毕。
void disable_irq_nosync(unsigned int irq);

  当我们需要关闭当前处理器整个中断系统的时候使用以下函数

local_irq_enable();        // 使能当前处理器的中断系统
local_irq_disable();       // 禁止当前处理器的中断系统

  这里也有一个缺点是如果在中断禁止的时候使能中断这时候可能会任务崩溃所以使用推荐使用

local_irq_save(flags);        // 禁止中断并且将中断状态保存在 flags 中
local_irq_restore(flags);     // 恢复中断将中断到 flags 状态

2. 上半部与下半部 

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

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

   Linux 内核将中断分为上半部和下半部的目的就是为了实现中断处理函数的快进快出那些对时间敏感、执行速度快的操作可以放到中断处理函数中也就是上半部。剩下的所有工作都可以放到下半部去执行。

  关于哪些工作放上半部哪些放下半部可以参考

  ① 如果要处理的内容不希望被其他中断打扰放在上半部

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

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

  ④ 除了以上三点其他任务都可以放到下半部。

   上半部其实就是编写中断处理函数下半部 Linux 提供了许多机制

① 软中断

  软中断常用于处理需要及时响应的事件优先级较高的任务。它可以根据优先级和中断处理队列的情况来确定哪个软中断被优先处理。

  Linux 内核使用 softirq_action 结构体表示软中断并且在 kernel/softirq.c 文件中一共定义了 10 个软中断

static struct softirq_action softirq_vec[NR_SOFTIRQS];
// NR_SOFTIRQS 是枚举类型定义如下
enum
{
    HI_SOFTIRQ=0, /* 高优先级软中断 */
    TIMER_SOFTIRQ, /* 定时器软中断 */
    NET_TX_SOFTIRQ, /* 网络数据发送软中断 */
    NET_RX_SOFTIRQ, /* 网络数据接收软中断 */
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ, /* tasklet 软中断 */
    SCHED_SOFTIRQ, /* 调度软中断 */
    HRTIMER_SOFTIRQ, /* 高精度定时器软中断 */
    RCU_SOFTIRQ, /* RCU 软中断 */
    NR_SOFTIRQS
};

   10 个软中断所以 NR_SOFTIRQS 元素有 10 个。

  softirq_action 结构体中的 action 成员变量就是软中断的服务函数数组 softirq_vec 是个全局数组。

  如果要使用软中断必须先使用 open_softirq 函数注册对应的软中断处理函数

/*
 * @description : 注册软中断处理函数
 * @param - nr : 要开启的软中断选择 NR_SOFTIRQS 其中一个元素
 * @param - action : 软中断对应的处理函数
 * @return : 没有返回值
 */
void open_softirq(int nr, void (*action)(struct softirq_action *));

   但是软中断必须必须在编译的时候静态注册在编译时期将组件与系统进行绑定的配置方式。内核使用 softirq_init 函数进行初始化软中断

void __init softirq_init(void) 
{
    int cpu;

    for_each_possible_cpu(cpu) 
    {
        per_cpu(tasklet_vec, cpu).tail = &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail = &per_cpu(tasklet_hi_vec, cpu).head;
    }

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

   从 softirq_init函数可以看出当使用软中断的时候这个函数会自动打开

HI_SOFTIRQ, /* 高优先级软中断 */
TASKLET_SOFTIRQ, /* tasklet 软中断 */

② tasklet

  tasklet 适用于低优先级的、需要延迟处理的事件。如果事件需要尽快得到处理并具有不同的优先级那么软中断更适合如果事件可以在稍后的时间点进行处理并且没有特定的优先级要求那么 tasklet 更适合。但我们已经在下半部分了所以 tasklet 更适合使用。

  tasklet_struct 结构体如下

struct tasklet_struct
{
    struct tasklet_struct *next; /* 下一个 tasklet */
    unsigned long state; /* tasklet 状态 */
    atomic_t count; /* 计数器记录对 tasklet 的引用数 */
    void (*func)(unsigned long); /* tasklet 执行的函数 */    // 这里相当于中断处理函数
    unsigned long data; /* 函数 func 的参数 */
};

  如果要使用 tasklet必须先定义一个 tasklet_struct 变量然后使用 tasklet_init 函数对其进行初始化

/*
 * @description :tasklet初始化函数
 * @param - t : 要初始化的 tasklet
 * @param - func : tasklet 的处理函数
 * @param - data : 要传递给 func 函数的参数
 * @return : 没有返回值
 */
void tasklet_init(struct tasklet_struct *t,
                  void (*func)(unsigned long),
                  unsigned long data);

  当然也可以使用宏 DECLARE_TASKLET 一次性来完成 tasklet 的定义和初始化。

/*
 * @description : 定义和初始化tasklet
 * @param - name : 要定义的tasklet名字
 * @param - func : tasklet的处理函数
 * @param - data : 传递给 func 函数的参数
 */
DECLARE_TASKLET(name, func, data);

  除此之外在上半部分的中断处理函数需要调用 tasklet_schedule 函数这就可以让 tasklet 在合适的时间运行

// 这里的形参指针 t要调度的tasklet
void tasklet_schedule(struct tasklet_struct *t);

   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 结构体表示一个工作这些工作组成工作队列工作队列用 workqueue_struct 结构体表示有了工作队列之后Linux 内核使用 worker 结构体表示工作者线程就是管理工作队列的结构体。

  在实际的驱动开发中我们只需要定义工作work_struct即可关于工作队列和工作者线程基本不用管因为这两者都是由内核自动管理的。

  创建工作其实就直接定义一个 work_struct 结构体变量

#define INIT_WORK(_work, _func)
/*
 _work要初始化的工作
 _func工作对应的要处理函数
 */

// 也可以用使用 DECLARE_WORK 宏一次性完成工作的创建和初始化
#define DECLARE_WORK(n, f)
/*
 n要初始化的工作work_struct
 f工作对应的要处理函数
 */

   和 tasklet 一样工作也是需要调度才能工作它的调度函数为 schedule_work

/*
 * @description : 调度工作wrok_struct的函数
 * @param - work : 要调度的工作
 * @return : 0 成功其他值 失败
 */
bool schedule_work(struct work_struct *work);

  工作使用示例

/* 定义工作(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);
    ......
}

  这里的 工作 的使用流程其实跟 tasklet 一模一样。但他们区别还是蛮大的例如 工作 是在进程上下文中执行tasklet 是在软中断上下文执行等等。

3. 设备树中断信息节点

① GIC 中断控制器

  STM32MP1 有三个与中断有关的控制器 GIC、EXTI 和 NVIC 。因为 NVIC 是 Cortex-M4 内核的中断控制器暂时不考虑。

  GIC 有 4 个版本V1~V4V1 被淘汰V3 和 V4 是 64 位芯片使用这次使用的是  GIC V2。

  GIC 中断控制器是用来管理中断的优先级、中断分发、中断控制等。当 GIC 接收到外部中断信号以后就会报给 ARM 内核但是 ARM 内核只提供了四个信号给 GIC 来汇报中断情况 VFIQ、 VIRQ、 FIQ 和 IRQ他们之间的关系如下

  GIC 接受很多的外部中断然后对其进行处理最终通过4个信号报给 ARM 内核

  VFIQ虚拟快速 FIQ。

  VIRQ虚拟快速 IRQ。 

  FIQ快速中断 IRQ。 

  IRQ外部中断 IRQ。 

  虚拟 FIQ 是专门虚拟化环境设计的中断机制与传统的 FIQ 相互独立VIRQ 也是有虚拟化环境机制。

  FIQ 必须尽快处理处理结束后离开这个 FIQ。IRQ 可以被 FIQ 中断但 IRQ 不能中断 FIQ因此 FIQ 响应更快。GIC V2的逻辑图如下

  左边就是中断源中间是 GIC 控制器右边是中断控制器向处理器内核发送的中断信息。

  重点看中间部分GIC 将中断源分为三类

  ① SPI(Shared Peripheral Interrupt)共享中断所有 Core 共享的中断这个是最常见的那些外部中断都属于共享中断 。比如 GPIO 中断、串口中断等等这些中断所有的 Core 都可以处理不限定特定 Core。

  ② PPI(Private Peripheral Interrupt)私有中断GIC 是支持多核的每个核肯定有自己独有的中断。这些独有的中断肯定是要指定的核心处理因此这些中断就叫做私有中断。

  ③ SGI(Software-generated Interrupt)软件中断由软件触发引起的中断通过向寄存器 GICD_SGIR 写入数据来触发系统会使用 SGI 中断来完成多核之间的通信。 

② 中断 ID

  因为有很多中断为了区分他们必须给他们分配唯一一个 ID 号。这个 ID 号就是中断 ID。每一个 CPU 最多支持 1020 个中断 ID中断 ID 号为 ID0~ID1019。这 1020 个 ID 包含了 PPI、 SPI 和 SGI这 1020 个 ID 分配如下

  D0~ID15这 16 个 ID 分配给 SGI软件中断。

  ID16~ID31这 16 个 ID 分配给 PPI私有中断。 

  D32~ID1019这 988 个 ID 分配给 SPI共享中断。

  STM32MP157 总共分配了 265 个中断 ID加上 SGI 和 PPI就有 288 个中断ID。从 ID 32 开始的 SPI 中断

③ EXTI

  EXTI 称为 外部中断和事件控制器 EXTI 通过可配置的事件输入和直接事件输入来管理唤醒。它可以针对电源控制提供唤醒请求、针对 CPU 事件输入生成事件。 EXTI 唤醒请求可让系统从停止模式唤醒以及让 CPU 从 CSTOP 和 CSTANDBY 模式唤醒。此外 EXTI 还可以在运行模式下生成中断请求和事件请求。在实际使用中 EXTI 主要是为 STM32 的 GPIO 中断服务的。 

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

  ① 可配置事件来自能够生成脉冲的 I/O 或外设的信号这类事件具有以下特性
  – 可选择的有效触发边沿。
  – 中断挂起状态寄存器位。
  – 单独的中断和事件生成屏蔽。
  – 支持软件触发。

  ② 直接事件来自其他外设的中断和唤醒源需要在外设中清除这类事件具有以下特性
  – 固定上升沿有效触发。
  – EXTI 中无中断挂起状态寄存器位中断挂起状态由生成事件的外设提供。
  – 单独的中断和事件生成屏蔽。
  – 不支持软件触发。

  对于 GPIO 中断来说就是可配置事件EXIT 和 GIC 关系如下

  从上图中可以看出中断方式

  ① 外设直接产生中断到 GIC然后 GIC 通知 CPU 内核。

  ② GPIO 或外设产生中断到 EXTIEXTI 将信号提交给 GIC最终再将中断信号提交给 CPU。

  ③ GPIO 或外设产生中断到 EXTIEXTI 直接将中断信号提交给 CPU。

  Linux 系统会用到这三种中断方式一个外设最多可以有两种中断方式。GPIO 中断是我们最常用的。STM32 每一组 GPIO 最多 16 个 IO比如 PA0~PA15因此每组 GPIO 就有 16 个中断这 16 个 GPIO 事件输入对应 EXTI0~15其中 PA0、PB0只要是 PX0 的都是对应的是 EXTI0其实跟学习STM32裸机的时候一样

  如果要在 Linux 系统中使用中断那么就需要在设备树中设置好中断信息Linux 内核通过读取设备树中的中断属性信息来配置中断。

④ GIC 控制节点

  首先进入 /linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts 目录下打开 stm32mp151.dtsi 文件。

122     intc: interrupt-controller@a0021000 {
 123         compatible = "arm,cortex-a7-gic";    // compatible属性为"arm,cortex-a7-gic"那么内核就会去找""里的内容即可找到GIC中断驱动文件
 124         #interrupt-cells = <3>;    // #interrupt-cells 和#address-cells、 #size-cells 一样。
 125         interrupt-controller;    // 表示当前节点为中断控制器类似于gpio-controller;
 126         reg = <0xa0021000 0x1000>,
 127               <0xa0022000 0x2000>;
 128     };

/*
 详细了解 #interrupt-cells = <3>;
 表示此中断控制器下设备的 cells 大小对于设备而言会使用 interrupts 属性描述中断信息。
 #interrupt-cells 描述了 interrupts 属性的 cells 大小也就是一条信息有几个 cells。每个cells都是32位整型值。这三个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 掩码。
 */

  首先来看一下 SPI6 如何在设备树节点中描述中断信息的找到 SPI6 对应的中断号

  第一列的 Num 是 86 号但是注意这里并没有算上前面的 PPI + SGI = 32所以这里应该是 32 + 86 = 118就跟第二列的 ID 号所对应。

  打开stm32mp151.dtsi找到 SPI6 节点内容

1712         spi6: spi@5c001000 {
1713             #address-cells = <1>;
1714             #size-cells = <0>;
1715             compatible = "st,stm32h7-spi";
1716             reg = <0x5c001000 0x400>;
1717             interrupts = <GIC_SPI 86 IRQ_TYPE_LEVEL_HIGH>;    
1718             clocks = <&scmi0_clk CK_SCMI0_SPI6>;
1719             resets = <&scmi0_reset RST_SCMI0_SPI6>;
1720             dmas = <&mdma1 34 0x0 0x40008 0x0 0x0 0x0>,
1721                    <&mdma1 35 0x0 0x40002 0x0 0x0 0x0>;
1722             dma-names = "rx", "tx";
1723             power-domains = <&pd_core>;
1724             status = "disabled";

/*
 interrupts = <GIC_SPI 86 IRQ_TYPE_LEVEL_HIGH>;
 第一个表示中断类型为 GIC_SPI也就是共享中断。
 第二个表示中断号为86。
 第三个表示中断出发类型高电平触发
 */

⑤ EXTI 控制节点

  打开 stm32mp151.dtsi其中的 exti 节点就是 EXTI 中断控制器的节点

exti: interrupt-controller@5000d000 {
    compatible = "st,stm32mp1-exti", "syscon";
    interrupt-controller;    // 表示exti节点是中断控制器
    #interrupt-cells = <2>;  // 第一个cell表示中断号第二个cell表示中断标志位
    reg = <0x5000d000 0x400>;
    hwlocks = <&hsem 1 1>;    // 硬件锁指向hsem节点数字 "1 1" 是传递给硬件锁节点的参数
};

   在 GPIO 中其实也用到了 EXIT所以 GPIO 节点里面也有 EXTI 相关内容

pinctrl: pin-controller@50002000 {
1815             #address-cells = <1>;
1816             #size-cells = <1>;
1817             compatible = "st,stm32mp157-pinctrl";
1818             ranges = <0 0x50002000 0xa400>;
1819             interrupt-parent = <&exti>;    // 指定pinctrl所有子节点的中断父节点为exti这样就可以将GPIO和EXTI联系起来
1820             st,syscfg = <&exti 0x60 0xff>;
1821             hwlocks = <&hsem 0 1>;
1822             pins-are-numbered;
1823 
1824             gpioa: gpio@50002000 {
1825                 gpio-controller;
1826                 #gpio-cells = <2>;
1827                 interrupt-controller;    // 表示gpioa节点也是中断控制器
1828                 #interrupt-cells = <2>;  // 这里的第一个cell表示某个IO所处组的编号类似PA0第二个cell表示中断触发方式每个#interrupt-cells在EXTI、GPIO和GIC含义都不一样
1829                 reg = <0x0 0x400>;
1830                 clocks = <&rcc GPIOA>;
1831                 st,bank-name = "GPIOA";
                                             // 比如现在要设置PA1引脚为下降沿触发interrupts=<1 IRQ_TYPE_EDGE_FALLING>
1832                 status = "disabled";
1833             };
    ...
/* 由于GPIOA-GPIOK是连续的GPIOZ对应的寄存器地址不是连续的所以单独使用pinctrl_z来描述GPIOZ */
pinctrl_z: pin-controller-z@54004000 {
1947             #address-cells = <1>;
1948             #size-cells = <1>;
1949             compatible = "st,stm32mp157-z-pinctrl";
1950             ranges = <0 0x54004000 0x400>;
1951             pins-are-numbered;
1952             interrupt-parent = <&exti>;
1953             st,syscfg = <&exti 0x60 0xff>;
1954             hwlocks = <&hsem 0 1>;
1955 
1956             gpioz: gpio@54004000 {
1957                 gpio-controller;
1958                 #gpio-cells = <2>;
1959                 interrupt-controller;
1960                 #interrupt-cells = <2>;
1961                 reg = <0 0x400>;
1962                 clocks = <&scmi0_clk CK_SCMI0_GPIOZ>;
1963                 st,bank-name = "GPIOZ";
1964                 st,bank-ioport = <11>;
1965                 status = "disabled";
1966             };
1967         };
1968     };

  来看一个具体应用

hdmi-transmitter@39 {
    compatible = "sil,sii9022";    // sii9022是ST开发板上的HDMI芯片
    reg = <0x39>;
    iovcc-supply = <&v3v3_hdmi>;
    cvcc12-supply = <&v1v2_hdmi>;
    reset-gpios = <&gpioa 10 GPIO_ACTIVE_LOW>;    
    interrupts = <1 IRQ_TYPE_EDGE_FALLING>;    // 这个芯片是连接到PG1下降沿触发中断。
    interrupt-parent = <&gpiog>;    // 指定中断节点的父节点为 gpiog
    #sound-dai-cells = <0>;
    status = "okay";
};

// 其实在实际开发过程中只需要通过interrupts和interrupt-parent就可以指定引脚和触发方式。

   stm32mp157f-ev1-a7-examples.dts 文件再来看一个应用

 16     test_keys {
 17         compatible = "gpio-keys";
 18         #address-cells = <1>;
 19         #size-cells = <0>;
 20         autorepeat;
 21         status = "okay";
 22         /* gpio needs vdd core in retention for wakeup */
 23         power-domains = <&pd_core_ret>;
 24 
 25         button@1 {
 26             label = "PA13";
 27             linux,code = <BTN_1>;
 28             interrupts-extended = <&gpioa 13 IRQ_TYPE_EDGE_FALLING>;    // 新出现的interrupts-extended
 29             status = "okay";
 30             wakeup-source;
 31         };
 32     };

/*
 上述代码来描述一个按键此按键采用中断方式并且使用到PA13引脚。
 直接通过 interrupts-extended 一个属性描述了所有中断信息如果要用普通方式来描述的话
 interrupt-parent = <&gpioa>;
 interrupts = <13 IRQ_TYPE_EDGE_FALLING>;
 这种 interrupts-extended 更加简介。
 */

⑥ 获取中断号

  编写驱动的时候就需要中断号用到的中断号这个信息都已经写到了设备树里面。

  一个是 interrupt 属性提取对应设备号

/*
 * @description : 从interrupt属性提取到对应的设备号
 * @param - dev : 设备节点
 * @param - index : 索引号interrupts 属性可能包含多条中断信息通过 index 指定要获取的信息
 * @return : 中断号
 */
unsigned int irq_of_parse_and_map(struct device_node *dev, int index);

   一个是从 gpio 属性里提取设备号

/*
 * @description : 从 GPIO 属性提取到对应的设备号
 * @param - gpio : 要获取的GPIO编号
 * @return : GPIO 对应的中断号
 */
int gpio_to_irq(unsigned int gpio);

二、实验程序编写

  这次使用的是案件来触发中断。

  首先修改按键中的设备树打开 /linux/atk-mpl/linux/my_linux/linux-5.4.31/arch/arm/boot/dts 目录下的 stm32mp157d-atk.dts 文件修改 key 节点内容

52     key {
 53         compatible = "alientek,key";
 54         status = "okay";
 55         key-gpio = <&gpiog 3 GPIO_ACTIVE_LOW>;
 56         interrupts-extended = <&gpiog 3 IRQ_TYPE_EDGE_BOTH>;    // IRQ_TYPE_EDGE_BOTH表示上升沿和下降沿同时有效相当于按下KEY0和释放的时候同时有效
 57         // 也可以这样写
 58         // interrupt-parent = <&gpiog>;
 59         // interrupts = <3 IRQ_TYPE_EDGE_BOTH>;
 60     };

   之后编译设备树

cd
cd linux/atk-mpl/linux/my_linux/linux-5.4.31/
make dtbs

  将编译好的设备树复制

cd arch/arm/boot/dts/
sudo cp stm32mp157d-atk.dtb /home/alientek/linux/tftpboot/ -f

   在 ~/linux/atk-mpl/Drivers 目录下创建 13_irq并在里面创建 Vscode 工作区新建一个 keyirq.c 文件

#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/delay.h>
#include <linux/ide.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/errno.h>
#include <linux/gpio.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_gpio.h>
#include <linux/semaphore.h>
#include <linux/of_irq.h>
#include <linux/irq.h>
#include <asm/mach/map.h>
#include <asm/uaccess.h>
#include <asm/io.h>

#define KEY_CNT			1		/* 设备号个数 	*/
#define KEY_NAME		"key"	/* 名字 		*/

/* 定义按键状态 */
enum key_status {
    KEY_PRESS = 0,      /* 按键按下 */ 
    KEY_RELEASE,        /* 按键松开 */ 
    KEY_KEEP,           /* 按键状态保持 */ 
};

/* key设备结构体 */
struct key_dev{
	dev_t devid;				/* 设备号 	 */
	struct cdev cdev;			/* cdev 	*/
	struct class *class;		/* 类 		*/
	struct device *device;		/* 设备 	 */
	struct device_node	*nd; 	/* 设备节点 */
	int key_gpio;				/* key所使用的GPIO编号		*/
	struct timer_list timer;	/* 按键值 		*/
	int irq_num;				/* 中断号 		*/
	spinlock_t spinlock;		/* 自旋锁		*/
};

static struct key_dev key;          /* 按键设备 */
static int status = KEY_KEEP;   	/* 按键状态 */

/* 中断进入定时器定时时间是把按键抖动给延时掉 */
static irqreturn_t key_interrupt(int irq, void *dev_id)	// 中断处理函数
{
	/* 按键防抖处理开启定时器延时15ms */
	mod_timer(&key.timer, jiffies + msecs_to_jiffies(15));	// 为什么需要周期性的定时器是因为每当检测到按下一次就需要定时器延时
    return IRQ_HANDLED;		// IRQ_HANDLED是一个预定义的常量表示中断已经得到处理并且处理程序成功执行了必要的操作
}

/*
 * @description	: 初始化按键IOopen函数打开驱动的时候
 * 				  初始化按键所使用的GPIO引脚。
 * @param 		: 无
 * @return 		: 无
 */
static int key_parse_dt(void)
{
	int ret;
	const char *str;
	
	/* 设置LED所使用的GPIO */
	/* 1、获取设备节点key */
	key.nd = of_find_node_by_path("/key");
	if(key.nd == NULL) {
		printk("key node not find!\r\n");
		return -EINVAL;
	}

	/* 2.读取status属性 */
	ret = of_property_read_string(key.nd, "status", &str);
	if(ret < 0) 
	    return -EINVAL;

	if (strcmp(str, "okay"))
        return -EINVAL;
    
	/* 3、获取compatible属性值并进行匹配 */
	ret = of_property_read_string(key.nd, "compatible", &str);
	if(ret < 0) {
		printk("key: Failed to get compatible property\n");
		return -EINVAL;
	}

    if (strcmp(str, "alientek,key")) {
        printk("key: Compatible match failed\n");
        return -EINVAL;
    }

	/* 4、 获取设备树中的gpio属性得到KEY0所使用的KYE编号 */
	key.key_gpio = of_get_named_gpio(key.nd, "key-gpio", 0);
	if(key.key_gpio < 0) {
		printk("can't get key-gpio");
		return -EINVAL;
	}

    /* 5 、获取GPIO对应的中断号 */
    key.irq_num = irq_of_parse_and_map(key.nd, 0);
    if(!key.irq_num){
        return -EINVAL;
    }

	printk("key-gpio num = %d\r\n", key.key_gpio);
	return 0;
}

/* 主要进行GPIO和中断的初始化 */
static int key_gpio_init(void)
{
	int ret;
    unsigned long irq_flags;
	
	/* 使用GPIO就要申请GPIO使用权 */
	ret = gpio_request(key.key_gpio, "KEY0");
    if (ret) {
        printk(KERN_ERR "key: Failed to request key-gpio\n");
        return ret;
	}	
	
	/* 将GPIO设置为输入模式 */
    gpio_direction_input(key.key_gpio);

   /* 获取设备树中指定的中断触发类型 */
	irq_flags = irq_get_trigger_type(key.irq_num);		// 获得定义的中断触发类型
	if (IRQF_TRIGGER_NONE == irq_flags)
		irq_flags = IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING;
		
	/* 申请中断(使用中断必须申请中断) */
	ret = request_irq(key.irq_num, key_interrupt, irq_flags, "Key0_IRQ", NULL);	// request_irq会默认使能中断所以不需要enable_irq使能中断
	if (ret) {
        gpio_free(key.key_gpio);
        return ret;
    }
	// 建议申请成功后先用disbale_irq函数禁止中断等所有工作完成之后再来使能中断

	return 0;
}

/* 定时器处理函数 */
static void key_timer_function(struct timer_list *arg)
{
    static int last_val = 1;	// 保存按键上一次读取到的值
    unsigned long flags;
    int current_val;		// 存放当前按键读取的值

    /* 自旋锁上锁 */
    spin_lock_irqsave(&key.spinlock, flags);

    /* 读取按键值并判断按键当前状态 */
    current_val = gpio_get_value(key.key_gpio);
    if (0 == current_val && last_val)       /* 按下 */ 	// 读取的值为0上次的值为1则是按下
        status = KEY_PRESS;
    else if (1 == current_val && !last_val)
        status = KEY_RELEASE;  	 			/* 松开 */ 
    else
        status = KEY_KEEP;              	/* 状态保持 */ 

    last_val = current_val;

    /* 自旋锁解锁 */
    spin_unlock_irqrestore(&key.spinlock, flags);
}

/*
 * @description		: 打开设备
 * @param - inode 	: 传递给驱动的inode
 * @param - filp 	: 设备文件file结构体有个叫做private_data的成员变量
 * 					  一般在open的时候将private_data指向设备结构体。
 * @return 			: 0 成功;其他 失败
 */
static int key_open(struct inode *inode, struct file *filp)
{
	return 0;
}

/*
 * @description     : 从设备读取数据 
 * @param – filp        : 要打开的设备文件(文件描述符)
 * @param – buf     : 返回给用户空间的数据缓冲区
 * @param – cnt     : 要读取的数据长度
 * @param – offt        : 相对于文件首地址的偏移
 * @return          : 读取的字节数如果为负值表示读取失败
 */
static ssize_t key_read(struct file *filp, char __user *buf,
            size_t cnt, loff_t *offt)
{
    unsigned long flags;
    int ret;

    /* 自旋锁上锁 */
    spin_lock_irqsave(&key.spinlock, flags);

    /* 将按键状态信息发送给应用程序 */
    ret = copy_to_user(buf, &status, sizeof(int));	// 当前的status保存了按键当前的状态

    /* 状态重置 */
    status = KEY_KEEP;

    /* 自旋锁解锁 */
    spin_unlock_irqrestore(&key.spinlock, flags);

    return ret;
}

/*
 * @description		: 向设备写数据 
 * @param - filp 	: 设备文件表示打开的文件描述符
 * @param - buf 	: 要写给设备写入的数据
 * @param - cnt 	: 要写入的数据长度
 * @param - offt 	: 相对于文件首地址的偏移
 * @return 			: 写入的字节数如果为负值表示写入失败
 */
static ssize_t key_write(struct file *filp, const char __user *buf, size_t cnt, loff_t *offt)
{
	return 0;
}

/*
 * @description		: 关闭/释放设备
 * @param - filp 	: 要关闭的设备文件(文件描述符)
 * @return 			: 0 成功;其他 失败
 */
static int key_release(struct inode *inode, struct file *filp)
{
	return 0;
}

/* 设备操作函数 */
static struct file_operations key_fops = {
	.owner = THIS_MODULE,
	.open = key_open,
	.read = key_read,
	.write = key_write,
	.release = 	key_release,
};

/*
 * @description	: 驱动入口函数
 * @param 		: 无
 * @return 		: 无
 */
static int __init mykey_init(void)
{
	int ret;
	
	/* 初始化自旋锁 */
	spin_lock_init(&key.spinlock);
	
	/* 设备树解析 */
	ret = key_parse_dt();
	if(ret)
		return ret;
		
	/* GPIO 中断初始化 */
	ret = key_gpio_init();
	if(ret)
		return ret;
		
	/* 注册字符设备驱动 */
	/* 1、创建设备号 */
	ret = alloc_chrdev_region(&key.devid, 0, KEY_CNT, KEY_NAME);	/* 申请设备号 */
	if(ret < 0) {
		pr_err("%s Couldn't alloc_chrdev_region, ret=%d\r\n", KEY_NAME, ret);
		goto free_gpio;
	}
	
	/* 2、初始化cdev */
	key.cdev.owner = THIS_MODULE;
	cdev_init(&key.cdev, &key_fops);
	
	/* 3、添加一个cdev */
	ret = cdev_add(&key.cdev, key.devid, KEY_CNT);
	if(ret < 0)
		goto del_unregister;
		
	/* 4、创建类 */
	key.class = class_create(THIS_MODULE, KEY_NAME);
	if (IS_ERR(key.class)) {
		goto del_cdev;
	}

	/* 5、创建设备 */
	key.device = device_create(key.class, NULL, key.devid, NULL, KEY_NAME);
	if (IS_ERR(key.device)) {
		goto destroy_class;
	}
	
	/* 6、初始化timer设置定时器处理函数,还未设置周期所有不会激活定时器 */
	timer_setup(&key.timer, key_timer_function, 0);
	
	return 0;

destroy_class:
	class_destroy(key.class);
del_cdev:
	cdev_del(&key.cdev);
del_unregister:
	unregister_chrdev_region(key.devid, KEY_CNT);
free_gpio:
	free_irq(key.irq_num, NULL);
	gpio_free(key.key_gpio);
	return -EIO;
}

/*
 * @description	: 驱动出口函数
 * @param 		: 无
 * @return 		: 无
 */
static void __exit mykey_exit(void)
{
	/* 注销字符设备驱动 */
	cdev_del(&key.cdev);/*  删除cdev */
	unregister_chrdev_region(key.devid, KEY_CNT); /* 注销设备号 */
	del_timer_sync(&key.timer);		/* 删除timer */
	device_destroy(key.class, key.devid);/*注销设备 */
	class_destroy(key.class); 		/* 注销类 */
	free_irq(key.irq_num, NULL);	/* 释放中断 */
	gpio_free(key.key_gpio);		/* 释放IO */
}

module_init(mykey_init);
module_exit(mykey_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ALIENTEK");
MODULE_INFO(intree, "Y");

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

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

/*
 * @description		: main主程序
 * @param – argc		: argv数组元素个数
 * @param – argv		: 具体参数
 * @return			: 0 成功;其他 失败
 */
int main(int argc, char *argv[])
{
    int fd, ret;
    int key_val;

    /* 判断传参个数是否正确 */
    if(2 != argc) {
        printf("Usage:\n"
             "\t./keyApp /dev/key\n"
            );
        return -1;
    }

    /* 打开设备 */
    fd = open(argv[1], O_RDONLY);
    if(0 > fd) {
        printf("ERROR: %s file open failed!\n", argv[1]);
        return -1;
    }

    /* 循环读取按键数据 */
    while(1) {

        read(fd, &key_val, sizeof(int));
        if (0 == key_val)
            printf("Key Press\n");
        else if (1 == key_val)
            printf("Key Release\n");
    }

    /* 关闭设备 */
    close(fd);
    return 0;
}

  编写 Makefile 文件

KERNELDIR := /home/alientek/linux/atk-mpl/linux/my_linux/linux-5.4.31
CURRENT_PATH := $(shell pwd)

obj-m := keyirq.o

build: kernel_modules

kernel_modules:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) modules

clean:
	$(MAKE) -C $(KERNELDIR) M=$(CURRENT_PATH) clean

  编译 keyirq.c 和 keyirqApp.c 文件

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

  将编译好的 keyirqApp 和 keyirq.ko 文件复制

sudo cp keyirqApp keyirq.ko /home/alientek/linux/nfs/rootfs/lib/modules/5.4.31/

  开启开发板输入以下命令

cd lib/modules/modules/5.4.31/
depmod     # 第一次加载驱动需要运行此命令
modprobe keyirq.ko     # 加载驱动

  可以查看 /proc/interrputs 文件来检查对应的中断是否注册上了

cd
cat /proc/interrupts

  从上图可以看出keyirq.c 驱动文件里面的 KYE0 中断已经存在触发方式为跳边沿Edge。

  接下来测试中断

cd lib/modules/5.4.31/
./keyirqApp /dev/key

  按键值成功获取并且不会有抖动的误判发生说明消抖工作正常。

  卸载驱动

rmmod keyirq.ko

总结

  概念

  首先我们学习了 Linux 中断号并且了解了中断是如何开启的。每当使用到了中断必须去申请中断request_irq在驱动出口再释放中断free_irq如果使用了 request_irq 函数那么就不用使用使能中断 enable_irq。建议在申请成功后先用 disbale_irq 函数禁止中断等所有工作完成之后再来使能中断。

  其次学习了上半部和下半部上半部其实就是对哪些时间敏感、执行速度快的操作放在中断处理函数中也就是上半部其他的就放在下半部。下半部里面我们学习了三个东西

  ① 软中断它是处理需要及时响应的事件一般这个了解即可。

  ② tasklet是利用软中断来实现这个是适用于低优先级需要延迟的事件。这个需要掌握概念和使用方法定义->处理函数->中断处理函数里写调度->驱动入口函数里写初始化和注册中断处理函数。

  ③ 工作队列工作队列在进程上下文执行如果你的工作可以睡眠那么选择工作队列。掌握概念及使用方法使用方法和 tasklet 极为相似。

  最后学习了设备树的中断信息节点这里面有 GIC 中断控制重点了解SPI共享中断、中断ID需要查手册和 EXTI。后面又了解到了 GIC 控制节点和 EXTI 控制节点这两者 compatible 里面的元素和 #interrupt-cells 信息稍许不一样外其他类似。并且在设备树里加入中断信息的方式有两种一种是 interrupts-extended、另一种是 interrupt-parent 和 interrupts前者是后者的结合体。

  程序

  ① 在初始化阶段分开了设备树信息设置和 GPIO 初始化设置

  ② 在中断处理函数中增减定时器消除按键抖动

  ③ 定时器处理函数也就是回调函数中去判断按键的值并打印出按键的值。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: linux