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

會出現類似以下的輸出:

img

這個檔案中會顯示每個 CPU 處理中斷的次數。接下來會比較載入模組前後不同的地方。為了方便,等一下在比較的時候,會省略掉中間 CPU 的執行次數,只留下第一欄的 IRQ 編號,以及最後 4 攔的說明。

硬體配置

Raspberry Pi 的 GPIO17 透過 Logic Level Shifter 連接給 Arduino 的 A0,並且在 Logic Level Shifter 的兩端都加上適當的供電。如下圖:

img

程式: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