Skip to main content

TECH VEDA

Embedded Linux on Edge-AI 23rd Sept 2026 enrollingLinux kernel & Device drivers starts on 24th Oct 2026 enrollingCorporate on-site training - Submit proposal Pick your modulesSharpen your kernel skills: deep dives, drivers, Yocto, CVEs, careers — updated daily. Read the blog →Embedded Linux fast track starts 23rd sept 2026 enrollingEmbedded Linux Mastery track starts 23rd sept 2026 enrollingLinux systems engineering starts 23rd sept 2026 enrolling
Debug Stories

Debug Story: A Spurious Interrupt Storm That Disabled a Shared IRQ

A shared, level-triggered interrupt line was disabled after a "nobody cared" spurious interrupt storm. Here is how the kernel detects it and how to fix the handler.

Debug Story: A Spurious Interrupt Storm That Disabled a Shared IRQ

A touch controller stopped responding after a few minutes of use. The kernel had printed “irq 61: nobody cared” and then “Disabling IRQ #61”. The root cause was a second driver on the same shared, level-triggered line: its interrupt handler checked only one status bit, so when a different event fired it returned IRQ_NONE without clearing the hardware. The line stayed asserted, the kernel counted the interrupt as unhandled 99,900 times in a row, and it disabled the whole line, taking the innocent touch controller down with it. The fix was to make the handler acknowledge every enabled interrupt source.

This is a debug story about a spurious interrupt storm on an ARM64 embedded board. The symptom looked like a touchscreen driver bug, but the real fault was in a different driver that shared the same interrupt line. It is a good example of why a shared interrupt handler must acknowledge its own device correctly, and what the kernel does when one does not.

The symptom: a touch controller that stopped working

On a custom i.MX8-class board, the resistive touch controller worked at boot and then stopped generating events after two to five minutes. No panic, no reboot, just silence from the input device. Restarting the userspace stack did not help. The only reliable recovery was a full reboot.

The first useful signal was in the kernel log.

First look: dmesg and the “nobody cared” splat

The kernel ring buffer held a clear message:

raghu@techveda.org:~$ dmesg | tail -n 25
[  742.118332] irq 61: nobody cared (try booting with the "irqpoll" option)
[  742.118350] CPU: 1 PID: 0 Comm: swapper/1 Not tainted 6.6.30 #1
[  742.118360] Hardware name: ACME i.MX8 board (DT)
[  742.118366] Call trace:
[  742.118370]  dump_backtrace+0x9c/0x100
[  742.118382]  show_stack+0x20/0x38
[  742.118390]  dump_stack_lvl+0x60/0x80
[  742.118400]  dump_stack+0x18/0x28
[  742.118408]  __report_bad_irq+0x54/0x120
[  742.118418]  note_interrupt+0x2b0/0x2f8
[  742.118427]  handle_irq_event+0xf4/0x118
[  742.118436]  handle_fasteoi_irq+0xac/0x230
[  742.118446]  generic_handle_domain_irq+0x34/0x50
[  742.118456]  gic_handle_irq+0x4c/0x110
[  742.118520] handlers:
[  742.118530] [<0000000012345678>] acme_fpga_isr [acme_fpga]
[  742.118544] [<00000000abcdef01>] acme_tsc_isr [acme_tsc]
[  742.118558] Disabling IRQ #61

Three facts stand out. First, the message is about Linux interrupt number 61, not about the touch driver by name. Second, two handlers are registered on that number: acme_fpga_isr and acme_tsc_isr. The line is shared. Third, the kernel disabled the interrupt. Once an interrupt is disabled, every device on that line goes quiet, which explained why the touch controller stopped even though its own code was correct.

How the kernel detects a spurious interrupt

The message comes from the kernel’s spurious interrupt detector in kernel/irq/spurious.c. After each hardware interrupt, the core calls note_interrupt() with the combined return value of all handlers on the line. When no handler claims the interrupt, every handler returns IRQ_NONE, and the core treats the interrupt as unhandled.

The detector keeps two counters per interrupt in struct irq_desc: irq_count and irqs_unhandled. The relevant logic is:

desc->irq_count++;
if (likely(desc->irq_count < 100000))
        return;

desc->irq_count = 0;
if (unlikely(desc->irqs_unhandled > 99900)) {
        /* The interrupt is stuck */
        __report_bad_irq(desc, action_ret);
        pr_emerg("Disabling IRQ #%d\n", irq);
        desc->istate |= IRQS_SPURIOUS_DISABLED;
        desc->depth++;
        irq_disable(desc);
        ...
}

In plain terms: if 99,900 of the previous 100,000 interrupts on a line were not handled by anyone, the kernel assumes the line is stuck. It prints the “nobody cared” report, lists the registered handlers, prints “Disabling IRQ #N”, and disables the line. This protects the system from spending all of its time in a hardirq that never ends. The report function __report_bad_irq() is exactly where the “nobody cared” and “handlers:” lines are printed.

A line only reaches this state if something keeps asserting the interrupt while no handler acknowledges it. On a level-triggered line, that condition repeats immediately: as long as the hardware holds the line active, the interrupt fires again the moment it is re-enabled.

Reading /proc/interrupts: the shared line

Before the line was disabled, /proc/interrupts showed the count climbing very fast. After the disable, it froze:

raghu@techveda.org:~$ cat /proc/interrupts
           CPU0       CPU1
 61:     104213     998727     GICv3  87 Level     acme-fpga, acme-tsc
...

This confirmed the picture. Linux interrupt 61 maps to GIC hardware interrupt 87, which is shared platform interrupt (SPI) 55 in the device tree, because SPI numbering starts at 32. The trigger type is Level. Two devices are listed on the line: the board’s FPGA event block and the touch controller. Their interrupt outputs are wired together to one SoC input, a common cost-saving choice on custom boards.

The device tree reflected the shared wiring:

&i2c1 {
        touch: touchscreen@48 {
                compatible = "acme,acme-tsc";
                reg = <0x48>;
                interrupt-parent = <&gic>;
                interrupts = <GIC_SPI 55 IRQ_TYPE_LEVEL_HIGH>;
        };
};

fpga: fpga@30000000 {
        compatible = "acme,acme-fpga";
        reg = <0x30000000 0x1000>;
        interrupt-parent = <&gic>;
        interrupts = <GIC_SPI 55 IRQ_TYPE_LEVEL_HIGH>;
};

Root cause: a handler that never acknowledged its device

Because two handlers shared the line, the next step was to find which device was asserting the interrupt without being acknowledged. The touch handler was correct. The FPGA handler was not. Its interrupt code looked like this:

#define ACME_INT_STATUS  0x10  /* write-1-to-clear */
#define ACME_INT_ENABLE  0x14
#define ACME_INT_DONE    BIT(0)
#define ACME_INT_ERROR   BIT(1)

static irqreturn_t acme_fpga_isr(int irq, void *dev_id)
{
        struct acme_fpga *fpga = dev_id;
        u32 status;

        status = readl(fpga->base + ACME_INT_STATUS);

        /* Only the DONE event is checked here */
        if (!(status & ACME_INT_DONE))
                return IRQ_NONE;

        acme_fpga_complete(fpga);
        writel(ACME_INT_DONE, fpga->base + ACME_INT_STATUS);
        return IRQ_HANDLED;
}

The driver’s setup code enabled two interrupt sources in the FPGA:

writel(ACME_INT_DONE | ACME_INT_ERROR, fpga->base + ACME_INT_ENABLE);

The handler only checked and cleared ACME_INT_DONE. When the FPGA raised an ACME_INT_ERROR event, which happened a few minutes into a run under a specific data pattern, the status register had ACME_INT_ERROR set but not ACME_INT_DONE. The handler read the status, saw that ACME_INT_DONE was clear, and returned IRQ_NONE without clearing anything. The FPGA kept the level line asserted because its ACME_INT_ERROR bit was still set. The interrupt fired again immediately, the touch handler correctly returned IRQ_NONE for an event that was not its own, and the cycle repeated at hardware speed until the kernel’s counter crossed the threshold and disabled interrupt 61.

The fix

The handler must acknowledge every source it enabled. The corrected version reads both the status and the enable mask, handles whatever is pending, and clears all pending bits with a single write-1-to-clear:

static irqreturn_t acme_fpga_isr(int irq, void *dev_id)
{
        struct acme_fpga *fpga = dev_id;
        u32 status, enabled, pending;

        status  = readl(fpga->base + ACME_INT_STATUS);
        enabled = readl(fpga->base + ACME_INT_ENABLE);
        pending = status & enabled;

        if (!pending)
                return IRQ_NONE;

        if (pending & ACME_INT_DONE)
                acme_fpga_complete(fpga);
        if (pending & ACME_INT_ERROR)
                acme_fpga_handle_error(fpga);

        /* Clear every source we serviced */
        writel(pending, fpga->base + ACME_INT_STATUS);
        return IRQ_HANDLED;
}

After this change, an ACME_INT_ERROR event is serviced and cleared, the level line drops, and the interrupt count in /proc/interrupts increments at a normal rate. The touch controller continued to work because the shared line was never disabled again.

Masking the interrupt with the correct set of bits still matters: a handler that returns IRQ_NONE is only correct when the device genuinely did not raise the interrupt. Returning IRQ_NONE while your own device is still asserting the line is what produces the storm.

How to avoid this

  • In a shared handler, check every source you enabled, not just the one you care about most.
  • Always clear the hardware interrupt on a level-triggered line before returning IRQ_HANDLED. If the source is not cleared, the line stays asserted and the interrupt repeats.
  • Only return IRQ_NONE when your device truly did not raise the interrupt. Compare the status register against the enable mask to decide.
  • Watch /proc/interrupts during testing. A count that climbs far faster than the expected event rate is an early sign of an interrupt that is not being acknowledged.
  • Treat “nobody cared” and “Disabling IRQ #N” as an instruction to inspect every handler on that line, including drivers that seem unrelated to the visible symptom.

If you want structured practice with interrupt handling, shared lines, and level versus edge triggering, our Linux Device Drivers course covers this material with hands-on labs on real boards.

Key takeaways

  • “irq N: nobody cared” means the kernel saw about 99,900 unhandled interrupts out of 100,000 on a line and disabled it.
  • The report lists every handler on the line, which is the fastest way to find a shared interrupt.
  • The fault is usually a handler that returns IRQ_NONE without clearing a source it enabled, so a level-triggered line stays asserted.
  • When a shared line is disabled, every device on it stops, so the visible symptom can be in a different driver from the one at fault.
Was this worth your time?

Frequently asked questions

What does “irq N: nobody cared” mean?
It means the kernel’s spurious interrupt detector saw more than 99,900 of the previous 100,000 interrupts on that line go unhandled, so it assumed the line was stuck and disabled it. Every registered handler returned IRQ_NONE for those interrupts.

Why did disabling one interrupt break a device that was working correctly?
The interrupt line was shared by two devices. When the kernel disabled the line because of one misbehaving driver, both devices lost their interrupt, so the correctly written driver also stopped receiving events.

Why does a level-triggered line cause a storm but an edge-triggered one might not?
A level-triggered interrupt stays asserted as long as the hardware holds the line active. If the handler never clears the source, the interrupt fires again immediately after it is re-enabled, which repeats until the kernel intervenes.

When is it correct for a handler to return IRQ_NONE?
Only when the handler’s own device did not raise the interrupt. The safe test is to compare the device’s interrupt status register against its enable mask and return IRQ_NONE only if nothing the driver enabled is pending.

Further reading

RB
Raghu Bharadwaj

Founder, TECH VEDA — 20+ years teaching the Linux kernel, device drivers and embedded systems.

Follow on LinkedIn

Get new posts by email

Kernel, embedded Linux and AI-era engineering — a few sharp reads a month. No spam.

We email occasionally and never share your address.