如何从 i2c io 扩展器 linux 请求 GPIO 中断(-EINVAL 失败)

How to request GPIO interrupt from i2c io expander linux (fails with -EINVAL)

我正在开发一个内核模块,需要使用 IO 扩展器 (MCP23008) 处理来自 4 个 GPIO 的中断。我正在使用内核 5.10.1-v7(buildroot 映像)开发 Raspberry Pi 3B+。当使用来自 40 针接头的“正常”GPIO 时,模块 (mymodule) 按预期工作。但是,当我尝试使用 pinctrl-mcp23s08 模块中的 GPIO 时,request_interrupt 函数失败并且 returns -22 (EINVAL).

我错过了什么?在我看来,从扩展的 GPIO 请求中断的一切都已准备就绪。其他类似的线程指向 request_interrupt 的第 5 个参数中与 NULL 一起使用的 IRQF_SHARED 标志,但我的情况并非如此。

这是相关的设备树片段:

/* for the io expander (mcp23s08) module */
&i2c1 { compatible = "brcm,bcm2835-i2c";
        pinctrl-names = "default";
        pinctrl-0 = <&i2c1_pins>;
        clock-frequency = <100000>;
        status="okay";

        mcp23008: mcp23008@20 {
                compatible = "microchip,mcp23008";
                reg=<0x20>;      /* i2c slave address */
                gpio-controller; /* mark the node as gpio-controller */
                #gpio-cells=<2>; /* pin number and flags */
                interrupt-parent = <&gpio>;
                interrupt-controller; /* mark the node as interrupt-controller */
                #interrupt-cells=<2>; /* pin number and flags */
                interrupts = <16 IRQ_TYPE_LEVEL_LOW>;
                status="okay";
        };
};

/* for mymodule */
/ {
        compatible = "raspberrypi,3-model-b-plus", "brcm,bcm2837";
        model = "Raspberry Pi 3 Model B+";

        mymodule {
                compatible = "myvendor,mymodule";
                gpios = <&mcp23008 0 GPIO_ACTIVE_HIGH>,
                        <&mcp23008 1 GPIO_ACTIVE_HIGH>,
                        <&mcp23008 2 GPIO_ACTIVE_HIGH>,
                        <&mcp23008 3 GPIO_ACTIVE_HIGH>;
                interrupt-parent = <&mcp23008>;
                interrupts = <0 IRQ_TYPE_LEVEL_HIGH>,
                             <1 IRQ_TYPE_LEVEL_HIGH>,
                             <2 IRQ_TYPE_LEVEL_HIGH>,
                             <3 IRQ_TYPE_LEVEL_HIGH>;
                status = "okay";
        };
};

我知道 mcp23s08 模块在引导时正确插入,因为我可以从 sysfs 导出 GPIO 并读取它们的值。此外,在 mymodule 的 init 函数中,gpio_request_array 函数使用 mcp23s08 模块(gpio 496 到 499)给出的 GPIO 成功。最后,/proc/interrupts 中的这一行告诉我 mcp23s08 驱动程序(在 gpio 16 上)的中断已正确分配:

$ cat /proc/interrupts
           CPU0       CPU1       CPU2       CPU3
 50:       5505          0          0          0  ARMCTRL-level   1 Edge      3f00b880.mailbox
 51:         47          0          0          0  ARMCTRL-level   2 Edge      VCHIQ doorbell
 73:          0          0          0          0  ARMCTRL-level  48 Edge      bcm2708_fb DMA
 75:      12435          0          0          0  ARMCTRL-level  50 Edge      DMA IRQ
 77:        716          0          0          0  ARMCTRL-level  52 Edge      DMA IRQ
 78:         16          0          0          0  ARMCTRL-level  53 Edge      DMA IRQ
 81:          0          0          0          0  ARMCTRL-level  56 Edge      DMA IRQ
 86:        199          0          0          0  ARMCTRL-level  61 Edge      ttyS0
 89:     887966          0          0          0  ARMCTRL-level  64 Edge      dwc_otg, dwc_otg_pcd, dwc_otg_hcd:usb1
110:          2          0          0          0  ARMCTRL-level  85 Edge      3f804000.i2c
111:          0          0          0          0  ARMCTRL-level  86 Edge      3f204000.spi
113:        119          0          0          0  ARMCTRL-level  88 Edge      mmc0
119:     214231          0          0          0  ARMCTRL-level  94 Edge      mmc1
194:          0          0          0          0  bcm2836-timer   0 Edge      arch_timer
195:      91376     256895     932964      45993  bcm2836-timer   1 Edge      arch_timer
198:          0          0          0          0  bcm2836-pmu   9 Edge      arm-pmu
199:          1          0          0          0  pinctrl-bcm2835   4 Edge      pps.-1
200:          0          0          0          0  pinctrl-bcm2835  16 Level     1-0020
205:          0          0          0          0  lan78xx-irqs  17 Edge      usb-001:004:01
FIQ:              usb_fiq
IPI0:          0          0          0          0  CPU wakeup interrupts
IPI1:          0          0          0          0  Timer broadcast interrupts
IPI2:         91         73        100         88  Rescheduling interrupts
IPI3:       4671      20748      12983      31858  Function call interrupts
IPI4:          0          0          0          0  CPU stop interrupts
IPI5:          0          0          0          0  IRQ work interrupts
IPI6:          0          0          0          0  completion interrupts
Err:          0

请求中断失败的相关初始化代码如下:

static int __init
mymodule_init(void)
{
        int i;
        int err;

        /* [...] */

        err = platform_driver_register(&gpio_driver);
        if (err) {
                printk(KERN_ERR "mymodule: failed to register platform driver\n");
                goto fail_platform_driver_register;
        }

        err = gpio_request_array(gpios, COUNT_OF(gpios));
        if (err != 0) {
                printk(KERN_ERR "mymodule: failed to request gpio array: %d\n", err);
                goto fail_gpio_request_array;
        }

        for (i = 0; i < COUNT_OF(irq_ids); ++i) {
                err = platform_get_irq(dev.platform_device, i);
                if (err > 0) {
                        irq_ids[i] = err;
                        printk(KERN_INFO "i=%d irq=%d\n", i, irq_ids[i]);
                } else {
                        printk(KERN_ERR "mymodule: gpio_to_irq failed i = %d\n", i);
                        goto fail_platform_get_irq;
                }
        }

        for (i = 0; i < COUNT_OF(irq_ids); ++i) {
                err = request_irq(
                                  irq_ids[i],
                                  (irq_handler_t) mymodule_irq_handler,
                                  IRQF_TRIGGER_HIGH,
                                  "mymodule_irq_handler",
                                  NULL
                                  );
                if (err) {
                        printk(KERN_ERR "mymodule: failed to request_irq at i=%d irq=%d: %d\n",
                               i, irq_ids[i], err);
                        goto fail_request_irq;
                }
        }

        /* success, return here */
        return 0;

        /* ommited: error handling here */
}

最后,这里是 dmesg 输出:

[   76.548689] i=0 irq=201
[   76.554473] i=1 irq=202
[   76.560189] i=2 irq=203
[   76.566033] i=3 irq=204
[   76.568525] mymodule: failed to request_irq at i=0 irq=201: -22

失败的至少一个原因是父中断控制器使用线程中断处理程序,因此子中断处理程序也需要线程化(这些将是“嵌套”线程中断处理程序)。

在“kernel/irq/manage.c”中的__setup_irq中测试了不兼容性:

    /*
     * Check whether the interrupt nests into another interrupt
     * thread.
     */
    nested = irq_settings_is_nested_thread(desc);
    if (nested) {
        if (!new->thread_fn) {
            ret = -EINVAL;
            goto out_mput;
        }
        /*
         * Replace the primary handler which was provided from
         * the driver for non nested interrupt handling by the
         * dummy function which warns when called.
         */
        new->handler = irq_nested_primary_handler;

(new->thread_fn 在请求非线程 IRQ 处理程序时是 NULL。上面的代码导致 request_irq() 的 return 值为 -EINVAL . new->handler = irq_nested_primary_handler; 部分被描述为下面 request_threaded_irq 讨论的脚注。)


可以将原始代码中的 request_irq 调用更改为以下内容以使用线程化 IRQ 处理程序:

                err = request_threaded_irq(
                                  irq_ids[i],
                                  NULL,
                                  mymodule_irq_handler,
                                  IRQF_TRIGGER_HIGH,
                                  "mymodule_irq_handler",
                                  NULL
                                  );

主要区别是“硬 IRQ”处理程序已设置为 NULL,因为它不用于纯线程 IRQ 处理程序(包括嵌套线程 IRQ 处理程序)。1 (我还从 (irq_handler_t) mymodule_irq_handler 中删除了类型转换运算符,因为如果 mymodule_irq_handler 未正确声明,那只会隐藏问题。)

作为使用 request_threaded_irq 的替代方法,可以使用 request_any_context_irq 函数,如下所示:

                err = request_any_context_irq(
                                  irq_ids[i],
                                  mymodule_irq_handler,
                                  IRQF_TRIGGER_HIGH,
                                  "mymodule_irq_handler",
                                  NULL
                                  );

这将 select 硬 IRQ 处理程序或线程 IRQ 处理程序,自动处理那些不需要硬和线程部分的 IRQ 请求。

[如果你愿意,可以在这里停止阅读。]


1 如果指定了“硬 IRQ”处理程序函数 ,则无论如何都不会调用它,因为线程中断处理程序将是“嵌套”到父线程的中断处理程序中。如上面 __setup_irq 中的代码所示,指定的“硬 IRQ”处理程序将替换为 irq_nested_primary_handler,但也不应调用该替换处理程序。它所做的只是记录一个警告:

/*
 * Primary handler for nested threaded interrupts. Should never be
 * called.
 */
static irqreturn_t irq_nested_primary_handler(int irq, void *dev_id)
{
    WARN(1, "Primary handler called for nested irq %d\n", irq);
    return IRQ_NONE;
}

在"drivers/pinctrl/pinctrl-mcp23s08.c":

mcp23s08_probe_one配置中断控制器为线程中断控制器
    if (mcp->irq && mcp->irq_controller) {
        struct gpio_irq_chip *girq = &mcp->chip.irq;

        girq->chip = &mcp->irq_chip;
        /* This will let us handle the parent IRQ in the driver */
        girq->parent_handler = NULL;
        girq->num_parents = 0;
        girq->parents = NULL;
        girq->default_type = IRQ_TYPE_NONE;
        girq->handler = handle_simple_irq;
        girq->threaded = true;
    }

girq->threaded = true; 是相关行。)

每个子 IRQ 由“drivers/gpio/gpiolib.c”中的 gpiochip_irq_map 映射并标记为使用嵌套线程处理程序:

    /* Chips that use nested thread handlers have them marked */
    if (gc->irq.threaded)
        irq_set_nested_thread(irq, 1);

父中断处理程序本身由 mcp23s08_irq_setup 在“drivers/pinctrl/pinctrl-mcp23s08.c”中设置为线程 IRQ 处理程序:

static int mcp23s08_irq_setup(struct mcp23s08 *mcp)
{
    struct gpio_chip *chip = &mcp->chip;
    int err;
    unsigned long irqflags = IRQF_ONESHOT | IRQF_SHARED;

    if (mcp->irq_active_high)
        irqflags |= IRQF_TRIGGER_HIGH;
    else
        irqflags |= IRQF_TRIGGER_LOW;

    err = devm_request_threaded_irq(chip->parent, mcp->irq, NULL,
                    mcp23s08_irq,
                    irqflags, dev_name(chip->parent), mcp);
    if (err != 0) {
        dev_err(chip->parent, "unable to request IRQ#%d: %d\n",
            mcp->irq, err);
        return err;
    }

    return 0;
}

“drivers/pinctrl/pinctrl-mcp23s08.c”中的父中断处理程序 mcp23s08_irq 调用 handle_nested_irq 来处理子中断处理程序:

            child_irq = irq_find_mapping(mcp->chip.irq.domain, i);
            handle_nested_irq(child_irq);

handle_nested_irq in "kernel/irq/chip.c" 调用子 IRQ 的子嵌套中断处理程序:

    for_each_action_of_desc(desc, action)
        action_ret |= action->thread_fn(action->irq, action->dev_id);