Day 28:IRQ (Part 2) - 中断突进!简单的 IRQ 程序

接下来的实验中,会写一个把 GPIO 当作是中断的来源的程序。这个 GPIO 由 Arduino 发出,每当边缘上升时,忌讳触发一次 IRQ。

这个应用比如说 DHT11 在核心的驱动程序,就是用这种机制来实作从 DHT11 收到的讯号:每次发生 edge-triggered 时,都把资料纪录推进 buffer 最後方,最後再解析整个 buffer 的内容,去判断读取的数值是多少。

/proc/interupts: 目前的 IRQ

在这之前,可能会想先知道一下关於目前的 IRQ 相关的资讯与统计资料。这可以透过察看 /proc/interrupts 这个内容得知:

$ cat /proc/interrupts

会出现类似以下的输出:

这个档案中会显示每个 CPU 处理中断的次数。接下来会比较载入模组前後不同的地方。为了方便,等一下在比较的时候,会省略掉中间 CPU 的执行次数,只留下第一栏的 IRQ 编号,以及最後 4 拦的说明。

硬体配置

Raspberry Pi 的 GPIO17 透过 Logic Level Shifter 连接给 Arduino 的 A0,并且在 Logic Level Shifter 的两端都加上适当的供电。如下图:

程序:Raspberry Pi

在程序能执行之前,当然少不了装置树的准备。不过这个跟前面大同小异,所以就把内容放在附录。

Step 1:资料结构

这边资料结构的设计,就是把 irq 编号跟对应的 gpio descriptor 形成一个结构体:

struct minirq_dev {
    struct gpio_desc *gpiod;
    struct work_struct work;
    int irq;
};

Step 2:资源配置

就是在 probe 当中,把对应的资源,比如说记忆体空间与 GPIO 等等进行初始化:

static int minirq_probe(struct platform_device *pdev)
{
    struct device *dev = &(pdev-> dev);
    struct minirq_dev *minirq;
    int ret = 0;
    minirq = devm_kzalloc(dev, sizeof(struct minirq_dev), GFP_KERNEL);
    minirq->gpiod = devm_gpiod_get_index(dev, "minirq", 0, GPIOD_IN);
    ...
}

为了清楚,错误处理的程序没有列出来。详细的程序会於最後面附上。

Step 3:找到 GPIO 对应的 IRQ

参考 GPIO 文件的 GPIOs mapped to IRQs

static int minirq_probe(struct platform_device *pdev)
{
    ...
    minirq->irq = gpiod_to_irq(minirq->gpiod);
    ...
}

这边有个前提是:GPIO 的 controller 要可以作为中断的来源,才可以这样做。关於这点可以去看装置树:

$ dtc -I fs /proc/device-tree | less

就会在 GPIO 的部分,发现 interrupts 相关的属性:

gpio@7e200000 {
        compatible = "brcm,bcm2835-gpio";
        gpio-controller;
        #interrupt-cells = < 0x02 >;
        interrupts = < 0x02 0x11 0x02 0x12 >;
        phandle = < 0x10 >;
        reg = < 0x7e200000 0xb4 >;
        #gpio-cells = < 0x02 >;
        pinctrl-names = "default";
        interrupt-controller;
    ...
};

Step 4:实作上半部

上半部要实作一个 prototype 为:

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

的函数。其中,第一个参数是刚刚得到的 irq 编号,而第二个参数是一个「能用以辨认装置的唯一结构」。不过通常都是把类似 struct platform_device,或是 struct device 这类的资料结构传进去:

static irqreturn_t irq_top_half(int irq, void *p)
{
    struct platform_device *pdev = p;
    struct minirq_dev *minirq;

    minirq = platform_get_drvdata(pdev);
    schedule_work(&(minirq -> work));
    return IRQ_HANDLED;
}

在这个 top-half 中,做的事情就是把一个工作用 schedule_work 推给一个核心全域的 Workqueue 做,然後就结束。而这个回传的值必须是个 irqreturn_t,相关的定义可以在这里找到:

enum irqreturn {
	IRQ_NONE		= (0 << 0),
	IRQ_HANDLED		= (1 << 0),
	IRQ_WAKE_THREAD		= (1 << 1),
};
typedef enum irqreturn irqreturn_t;

其中,如果这个 IRQ 是现在这个执行单元需要负责处理的,那麽处理完之後就回传 IRQ_HANDLED。而那个 IRQ_NONE 会出现的原因是:不同的执行单元有可能会同时帮一个 IRQ 注册各自的 IRQ handler。这时如果有 IRQ,那麽这所有的 handler 就会同时被触发。

如果这种共享的状况可能发生,那麽在 handler 里面就要判断被触发时,是不是当下的执行单元需要去理会的?如果发现不是,就什麽都不做,直接回传 IRQ_NONE 就好; 反之,如果是的话,就去把它处理掉,最後再回传 IRQ_HANDLED

而如同昨天描述的,上半部是 interrupt context,所以不能在里面休眠。而且处理要快,所以这边就把工作交给 Workqueue 去处理,然後就速速离开。这个下半部也可以比如说是 taskletkthread,或是整个用 threaded IRQ 来处理。但总之这边使用 Workqueue

Step 5:实作下半部

至於要把什麽样的工作推给 Workqueue 呢?这边的 struct work_struct 是嵌入在刚刚的 minirq 中的那个成员。我们就把他在 probe 里面时初始化成下面这个东西:

static int minirq_probe(struct platform_device *pdev)
{
    ...
    INIT_WORK(&minirq->work, irq_bottom_half);
    ...
}

其中,irq_bottom_half 是一个下面这样的函数:

void irq_bottom_half(struct work_struct *work)
{
    pr_info("Rising edge detected!\n");
    return;
}

没错,他就是简单印出一个资料,让我们知道 IRQ 被触发了:

Step 6:注册 IRQ

上半部跟下半部都处理好之後,接着就在 probe 当中帮这个 IRQ 「注册」这个 IRQ handler

static int minirq_probe(struct platform_device *pdev)
{
    ...
    ret = devm_request_irq(dev, minirq->irq, irq_top_half, 
            IRQF_TRIGGER_RISING, "minirq", pdev);
    ...
}

首先,这个函式有几种变形。最一开始的是 request_irq()

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

其中,irq 是 IRQ 编号; handler 就是前面所说的,上半部的函式:

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

flag 则是这个 IRQ 的细部调整,比如说是上升触发还是下降触发?有没有跟其他装置共享?是不是 timer 触发的?等等。详细的叙述可以在include/linux/interrupt.h 中找到。最後,那个 dev 会在参数中的 handler 被呼叫时,作为他的二个参数传给 handler

而如果使用 request_irq,那麽事後就要有对应的 free_irq。而类似地,虽然文件中没有写到,但这个函数有 devm_* 版本的函式,会自动跟装置有关的资源 (也就是这里使用的),因此就不用担心清理的问题。

最後一个版本是 threaded IRQ,虽然说用法很类似 (事实上是更方便),但本质上跟现在这个 IRQ 不同。这个之後会另外介绍。

Step 7:其他工作

大致上就是装置树的配置、提供 of_device_id、模组的初始化、提供 platform_driver 的资料结构等等。为版面简洁,这边就不多赘述。完整程序码附於後方。

程序:Arduino

每 0.5 秒改变一次电位高低:

void setup() {
    pinMode(A0, OUTPUT);
    Serial.begin(9600);
}

void loop() {
    digitalWrite(A0, HIGH);
    delay(500);
    digitalWrite(A0, LOW);
    delay(500);
}

换句话说,每 1 秒会有一个上升的边缘。

结果

载入模组之後,再去用 /proc/interrupts 观察,既可以发现 pinctrl-brcm2835 後面出现了一个 minirq 的装置:

 161:  bcm2836-timer    0 Edge     arch_timer
 162:  bcm2836-timer    1 Edge     arch_timer
 165:  bcm2836-pmu      9 Edge     arm-pmu
-167:  pinctrl-bcm2835 17 Edge   
+167:  pinctrl-bcm2835 17 Edge     minirq
 FIQ:  usb_fiq
 IPI0: CPU wakeup interrupts
 IPI1: Timer broadcast interrupts

除此之外,用 dmesg 也可以看见每秒一次印出的讯息:

[ 4222.224497] Rising edge detected!
[ 4223.225439] Rising edge detected!
[ 4224.226387] Rising edge detected!
[ 4225.227340] Rising edge detected!
[ 4226.228285] Rising edge detected!
[ 4227.229231] Rising edge detected!
[ 4228.230173] Rising edge detected!
[ 4229.231120] Rising edge detected!
[ 4230.232070] Rising edge detected!
[ 4231.233014] Rising edge detected!
[ 4232.233958] Rising edge detected!

附录:完整程序

装置树:minirq.dts

装置树的部分跟 IIO 时类似,只是名称有所不同:

/dts-v1/;
/plugin/;
/ {
    compatible="brcm,brcm2835";
    fragment@0 {
        target = <&gpio>;
        __overlay__ {
            minirq: minirq_gpio_pins {
                brcm,pins = <0x11>;
                brcm,function = <0x0>;
                brcm,pull = <0x1>;
            };
        };
    };
    fragment@1 {
        target-path = "/";
        __overlay__ {
            minirq {
                minirq-gpios = <&gpio 0x11 0x0>;
                compatible = "minirq";
                status = "ok";
                pinctrl-0 = <&minirq>;
                pinctrl-names = "default";
            };
        };
    };
};

Raspberry Pi:minirq.c

#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/platform_device.h>
#include <linux/gpio/consumer.h>
#include <linux/of.h>
#include <linux/gpio/consumer.h>
#include <linux/interrupt.h>
#include <linux/workqueue.h>

struct minirq_dev {
    struct gpio_desc *gpiod;
    struct work_struct work;
    int irq;
};

void irq_bottom_half(struct work_struct *work)
{
    pr_info("Rising edge detected!\n");
    return;
}

static irqreturn_t irq_top_half(int irq, void *p)
{
    struct platform_device *pdev = p;
    struct minirq_dev *minirq;

    minirq = platform_get_drvdata(pdev);
    schedule_work(&(minirq -> work));
    return IRQ_HANDLED;
}

static int minirq_probe(struct platform_device *pdev)
{
    struct device *dev = &(pdev-> dev);
    struct minirq_dev *minirq;
    int ret = 0;

    minirq = devm_kzalloc(dev, sizeof(struct minirq_dev), GFP_KERNEL);
    if (!minirq) {
        dev_err(dev, "Failed to allocate memory.\n");
	    return -ENOMEM;
    }

    minirq->gpiod = devm_gpiod_get_index(dev, "minirq", 0, GPIOD_IN);
    if (IS_ERR(minirq->gpiod)) {
        dev_err(dev, "Failed to get gpio descriptor.\n");
        return PTR_ERR(minirq -> gpiod);
    }
    
    ret = gpiod_to_irq(minirq->gpiod);
    if (ret < 0) {
        dev_err(dev, "Failed to get irq from gpiod.\n");
        return ret;
    }
    minirq->irq = ret;

    INIT_WORK(&minirq->work, irq_bottom_half);

    ret = devm_request_irq(dev, minirq->irq, irq_top_half, 
            IRQF_TRIGGER_RISING, "minirq", pdev);

    if (ret < 0) {
        dev_err(dev, "Failed to request IRQ.\n");
        return ret;
    }

    platform_set_drvdata(pdev, minirq);
    return 0;
}

static const struct of_device_id minirq_ids[] = {
    {.compatible = "minirq",},
    {}
};

static struct platform_driver minirq_driver = {
    .driver = {
        .name = "minirq",
	    .of_match_table = minirq_ids,
    },
    .probe = minirq_probe
};
MODULE_LICENSE("GPL");
module_platform_driver(minirq_driver);

Makefile

PWD := $(shell pwd)
KVERSION := $(shell uname -r)
KERNEL_DIR := /lib/modules/$(shell uname -r)/build

MODULE_NAME = minirq
obj-m := $(MODULE_NAME).o

all:
	make -C $(KERNEL_DIR) M=$(PWD) modules
clean:
	make -C $(KERNEL_DIR) M=$(PWD) clean
	rm -f $(MODULE_NAME).dtbo
dts:
	dtc -@ -I dts -O dtb -o	$(MODULE_NAME).dtbo $(MODULE_NAME).dts

顺带一提,可以直接:

$ make dts

来编译 .dtbo


<<:  [Day 28] - Gatsby feat. EC ( 下 )

>>:  这些日子我学到的JavaScript:Day25- to-do list 练习

Flask 防止 injection

在写好flask 服务之後,可能会将服务给弱点分析软件进行扫描, 之後会显示出一些高风险的漏洞, 而...

Decorator 装饰器模式

今天要介绍的装饰器模式,跟之前提到过的转接器模式有点类似(但其实结果完全不一样)。 转接器模式的功能...

Day 03 - 关於 const, static, extern 的三两事

#前言 由於前一篇使用了const与extern,但对这两者还不太了解,於是又去看了其他人的文章,试...

[Day07] 什麽是广度优先搜寻法

#993 - Cousins in Binary Tree 连结: https://leetcod...

[前端暴龙机,Vue2.x 进化 Vue3 ] Day7. Vue资料的使用方式

Vue资料的使用方式 在前一篇中,我们已经会一些用来表示内容的方式了,但仅仅只是呈现~ 所以今天就会...