Implementing IRQs on the Nitrogen93

This guide describes how to implement a GPIO interrupt driver on the Nitrogen93 platform using Ezurio's Yocto BSP. The driver uses the Linux GPIO descriptor API and is matched to a device tree node through a custom "compatible" string.

Overview

The driver configures a specific GPIO pin as an interrupt source. It requests the GPIO using the device tree, converts it into an IRQ, and registers a handler. The implementation uses devm-managed functions for automatic resource cleanup and integrates with Linux runtime power management. This approach is designed for use with Nitrogen93 SMARC hardware under Ezurio's Yocto BSP.

Device Tree Setup

Add the following node to your carrier board’s device tree overlay or directly into the DTS file used (e.g., "imx93-nitrogen-smarc.dts"):

gpioirq@0 {
    compatible = "ezurio,nitrogen93-gpioirq";
    gpio-irq-gpios = <&gpio1 3 GPIO_ACTIVE_LOW>;
};

Kernel Driver Source Code

Create a file named gpio-irq-demo.c and save it under your Yocto layer at:
meta-yourlayer/recipes-kernel/gpio-irq-demo/files/gpio-irq-demo.c
/*
 * Simple GPIO IRQ demo platform driver.
 * Requests a GPIO via the "irq-gpios" DT property, maps it to an IRQ,
 * and logs a message each time the interrupt fires.
 */

#include <linux/module.h>
#include <linux/init.h>
#include <linux/gpio/consumer.h>   /* gpiod_get/put, descriptor helpers */
#include <linux/of.h>              /* DeviceTree matching */
#include <linux/platform_device.h> /* platform_driver/probe/remove */
#include <linux/interrupt.h>       /* request_irq, irq handler types */
#include <linux/pm_runtime.h>      /* runtime PM helpers */

/* Per-device driver data */
struct gpio_irq_dev {
	struct gpio_desc *gpiod; /* GPIO descriptor obtained from DT */
	int irq;                 /* Linux IRQ number mapped from GPIO */
};

/* Top-half interrupt handler: keep it fast/minimal */
static irqreturn_t gpio_irq_handler(int irq, void *dev_id) {
	struct gpio_irq_dev *dev = dev_id;

	/* Print which GPIO fired (for demo/logging only) */
	pr_info("GPIO IRQ: triggered on GPIO %d\n", desc_to_gpio(dev->gpiod));
	return IRQ_HANDLED;
}

/* Probe is called when a matching DT node binds to this driver */
static int gpio_irq_probe(struct platform_device *pdev) {
	struct gpio_irq_dev *dev;
	int irq_flags, ret;

	/* Allocate zeroed per-device data tied to device lifecycle */
	dev = devm_kzalloc(&pdev->dev, sizeof(*dev), GFP_KERNEL);
	if (!dev)
		return -ENOMEM;

	/*
	 * Get the GPIO from DeviceTree using the "irq-gpios" property.
	 * Example DT:
	 *   irq: gpio-irq@0 {
	 *     compatible = "ezurio,nitrogen93-gpioirq";
	 *     irq-gpios = <&gpioX Y GPIO_ACTIVE_LOW>;
	 *   };
	 *
	 * In code, the consumer name "irq" maps to "<name>-gpios" => "irq-gpios".
	 */
	dev->gpiod = devm_gpiod_get(&pdev->dev, "irq", GPIOD_IN);
	if (IS_ERR(dev->gpiod))
		return dev_err_probe(&pdev->dev, PTR_ERR(dev->gpiod),
				     "Failed to get GPIO\n");

	/* Translate the GPIO to a Linux IRQ number */
	dev->irq = gpiod_to_irq(dev->gpiod);
	if (dev->irq < 0)
		return dev_err_probe(&pdev->dev, dev->irq,
				     "Failed to get IRQ\n");

	/*
	 * If DT or firmware specified an IRQ trigger, keep it; otherwise,
	 * default to falling edge for the demo.
	 */
	irq_flags = irq_get_trigger_type(dev->irq);
	if (!irq_flags)
		irq_flags = IRQF_TRIGGER_FALLING;

	/* Make dev available to remove()/PM/etc via pdev->dev.driver_data */
	platform_set_drvdata(pdev, dev);

	/*
	 * Request the IRQ with a managed (devm_) lifetime.
	 * The handler receives our 'dev' pointer as its dev_id.
	 */
	ret = devm_request_irq(&pdev->dev, dev->irq, gpio_irq_handler,
			       irq_flags, dev_name(&pdev->dev), dev);
	if (ret)
		return dev_err_probe(&pdev->dev, ret, "IRQ request failed\n");

	/* Enable runtime PM in case the platform benefits from it */
	pm_runtime_enable(&pdev->dev);

	dev_info(&pdev->dev, "GPIO IRQ driver initialized (irq=%d, flags=0x%x)\n",
		 dev->irq, irq_flags);
	return 0;
}

/* Remove is called on driver unbind; devm_ resources auto-free */
static int gpio_irq_remove(struct platform_device *pdev) {
	pm_runtime_disable(&pdev->dev);
	return 0;
}

/* Match table for of_platform binding */
static const struct of_device_id gpio_irq_of_match[] = {
	{ .compatible = "ezurio,nitrogen93-gpioirq" },
	{ /* sentinel */ }
};
MODULE_DEVICE_TABLE(of, gpio_irq_of_match);

/* Platform driver glue */
static struct platform_driver gpio_irq_driver = {
	.probe  = gpio_irq_probe,
	.remove = gpio_irq_remove,
	.driver = {
		.name = "gpio-irq-demo",
		.of_match_table = gpio_irq_of_match,
	},
};

/* Register init/exit boilerplate for a platform_driver */
module_platform_driver(gpio_irq_driver);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Ezurio");
MODULE_DESCRIPTION("GPIO IRQ driver for Nitrogen93");

Yocto Integration

Create the BitBake recipe at:
meta-yourlayer/recipes-kernel/gpio-irq-demo/gpio-irq-demo_0.1.bb
DESCRIPTION = "GPIO IRQ kernel module for Nitrogen93"
LICENSE = "GPLv2"
LIC_FILES_CHKSUM = "file://gpio-irq-demo.c;beginline=1;endline=10;md5=<FIXME>"

SRC_URI = "file://gpio-irq-demo.c"
S = "${WORKDIR}"

inherit module
KERNEL_MODULE_AUTOLOAD += "gpio-irq-demo"

To include the driver in your image, add this line to your image recipe or .bbappend file (for Ezurio use "ezurio-core-image-base"):

IMAGE_INSTALL:append = " gpio-irq-demo"

Build and Test

Rebuild and flash the image with Ezurio's image target:

bitbake gpio-irq-demo
bitbake ezurio-core-image-base

After booting, check that the driver initialized:

dmesg | grep "GPIO IRQ driver initialized"
Trigger the GPIO to verify the interrupt:
GPIO IRQ: triggered on GPIO N

If nothing is logged, confirm:

  • Device tree overlay is loaded
  • GPIO routing and electrical level are correct
  • IRQ edge polarity matches hardware
  • Module is present: lsmod | grep gpio_irq_demo