irq: turn a PS/2 line over when its device waits and nothing comes

An ASUS TUF A15 booted with its internal keyboard dead and a USB one
fine. These AMD laptops' firmware describes IRQ 1's polarity wrongly --
Linux keeps a quirk list -- and an edge line programmed the wrong way
up never fires: the controller holds its byte, and only the interrupt
that never comes would read it.

irq_watch() lets a driver say how to ask its device whether something
is waiting. The poller asks every tick; 50 ms of waiting with nothing
delivered turns the line's polarity over and drains the device, which
re-arms the edge, and if it still says nothing the line is polled.
Both are announced. The keyboard and the PS/2 mouse are watched.

ISA lines now also take their mode from the MADT's override entries,
which were parsed and never read.

Tested in QEMU, which ignores polarity on edge lines, with the keyboard
line masked: the first key turns it over and typing works; masked again
after the flip, it is polled and typing still works.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-24 11:10:29 +02:00
commit 8c426b2077
4 changed files with 123 additions and 0 deletions

View file

@ -61,6 +61,12 @@ typedef void (*irq_handler_t)(void *arg);
int irq_register(int irq, irq_handler_t fn, void *arg, const char *name, int irq_register(int irq, irq_handler_t fn, void *arg, const char *name,
int level_low); int level_low);
void irq_unregister(int irq, irq_handler_t fn, void *arg); void irq_unregister(int irq, irq_handler_t fn, void *arg);
// For a device that can say it has something waiting: `waiting` reads its
// status and returns nonzero when an interrupt should have come. A line whose
// device keeps saying so while nothing arrives has its polarity turned over,
// and is polled if that does not help either. See irq_poll_thread().
void irq_watch(int irq, int (*waiting)(void));
int irq_handler_count(int irq); int irq_handler_count(int irq);
// Called from trap() with the IOAPIC pin number, not the vector. // Called from trap() with the IOAPIC pin number, not the vector.

View file

@ -807,6 +807,16 @@ consoleclose(struct inode *ip)
// The keyboard handler lives in kbd.c, but the console is what owns the // The keyboard handler lives in kbd.c, but the console is what owns the
// keyboard as an input source, so this is where the line gets claimed. // keyboard as an input source, so this is where the line gets claimed.
// A keyboard byte in the controller, which IRQ 1 should have announced. 0xFF
// is no controller at all: every status bit set, the auxiliary one included,
// so it does not count.
static int
kbd_waiting(void)
{
uchar st = inb(KBSTATP);
return st != 0xFF && (st & 0x21) == 0x01;
}
static void static void
kbd_irq(void *arg) kbd_irq(void *arg)
{ {
@ -831,4 +841,5 @@ consoleinit(void)
cons.locking = 1; cons.locking = 1;
irq_register(IRQ_KBD, kbd_irq, 0, "ps2-kbd", 0); irq_register(IRQ_KBD, kbd_irq, 0, "ps2-kbd", 0);
irq_watch(IRQ_KBD, kbd_waiting);
} }

View file

@ -7,6 +7,7 @@
#include "core/spinlock.h" #include "core/spinlock.h"
#include "core/irq.h" #include "core/irq.h"
#include "arch/x86.h" #include "arch/x86.h"
#include "dev/acpi.h"
struct irq_action { struct irq_action {
irq_handler_t fn; irq_handler_t fn;
@ -27,6 +28,10 @@ static struct {
uint64 delivered; // real interrupts, not polls uint64 delivered; // real interrupts, not polls
uint64 since; // ticks when the first handler was attached uint64 since; // ticks when the first handler was attached
uint64 unhandled; uint64 unhandled;
int (*waiting)(void); // see irq_watch()
uint64 seen; // `delivered` when the poller last looked
int stuck; // ticks the device has waited with nothing delivered
int flipped; // polarity already turned over once
} line[NIRQ]; } line[NIRQ];
static int msi_used[NIRQ]; static int msi_used[NIRQ];
@ -72,6 +77,18 @@ irq_register(int irq, irq_handler_t fn, void *arg, const char *name,
level_low &= ~IRQ_QUIET; level_low &= ~IRQ_QUIET;
if(irq < 0 || irq >= NIRQ || fn == 0) if(irq < 0 || irq >= NIRQ || fn == 0)
return -1; return -1;
// An ISA line, as the MADT describes it. The override entries were parsed
// and never read, so a line the firmware says is active low or
// level-triggered was programmed edge, active high, regardless. Only the
// mode: a line moved to another pin would need the pin numbering here to
// stop being the IRQ numbering, and no machine seen so far does that for
// anything but the timer.
if(irq < 16 && level_low == IRQ_EDGE_HIGH){
int mode;
if(acpi_isa_gsi(irq, IRQ_EDGE_HIGH, &mode) == irq)
level_low = mode;
}
if(reserved(irq)){ if(reserved(irq)){
cprintf("irq_register: %s wants irq %d, which is reserved\n", name, irq); cprintf("irq_register: %s wants irq %d, which is reserved\n", name, irq);
return -1; return -1;
@ -152,6 +169,8 @@ irq_unregister(int irq, irq_handler_t fn, void *arg)
} else { } else {
line[irq].polled = 0; line[irq].polled = 0;
line[irq].delivered = 0; line[irq].delivered = 0;
line[irq].waiting = 0;
line[irq].stuck = line[irq].flipped = 0;
} }
break; break;
} }
@ -341,6 +360,18 @@ irq_info(char *buf, int cap)
return off; return off;
} }
void
irq_watch(int irq, int (*waiting)(void))
{
if(irq < 0 || irq >= NIRQ_IOAPIC)
return;
acquire(&irqlock);
line[irq].seen = line[irq].delivered;
line[irq].stuck = 0;
line[irq].waiting = waiting;
release(&irqlock);
}
// How many handlers are attached to a line. // How many handlers are attached to a line.
// //
// Asked by kernel/dev/pciuser.c before it gives a line to a userspace driver: // Asked by kernel/dev/pciuser.c before it gives a line to a userspace driver:
@ -412,7 +443,58 @@ irq_free_msi(int irq)
// //
// The moment a real interrupt arrives the line stops being polled, unless it // The moment a real interrupt arrives the line stops being polled, unless it
// was put here for storming. // was put here for storming.
//
// An *edge* line cannot be judged by silence -- a keyboard nobody types on is
// silent too -- so a driver that can tell whether its device has something
// waiting says so with irq_watch(), and the question becomes: has it waited
// with nothing delivered? That is a line programmed the wrong way up. The
// device asserts it, the IOAPIC is looking for the opposite edge, and the
// byte sits in the controller for ever, because only the interrupt that never
// comes would read it. An ASUS TUF A15's keyboard did exactly that -- the
// internal keyboard dead, a USB one fine -- and Linux keeps a list of AMD
// laptops whose firmware describes IRQ 1 wrongly one way or the other.
//
// So the polarity is turned over and the device drained, which re-arms the
// edge; if the line still says nothing, it is polled. Either way it says so.
#define POLL_AFTER (2 * HZ) #define POLL_AFTER (2 * HZ)
#define STUCK_TICKS 5 // 50 ms waiting with nothing delivered
static void
watch_line(int irq)
{
if(line[irq].polled || !line[irq].waiting)
return;
if(line[irq].delivered != line[irq].seen || !line[irq].waiting()){
line[irq].seen = line[irq].delivered;
line[irq].stuck = 0;
return;
}
if(++line[irq].stuck < STUCK_TICKS)
return;
line[irq].stuck = 0;
if(!line[irq].flipped){
static const int other[4] = {
[IRQ_EDGE_HIGH] = IRQ_EDGE_LOW, [IRQ_EDGE_LOW] = IRQ_EDGE_HIGH,
[IRQ_LEVEL_HIGH] = IRQ_LEVEL_LOW, [IRQ_LEVEL_LOW] = IRQ_LEVEL_HIGH,
};
int was = line[irq].level_low;
line[irq].flipped = 1;
line[irq].level_low = other[was & 3];
ioapicroute(irq, 0, line[irq].level_low);
cprintf("irq %d: '%s' had data waiting and no interrupt came; the line is "
"active %s, not %s, whatever the firmware says\n", irq,
line[irq].act[0].name,
(line[irq].level_low == IRQ_EDGE_LOW ||
line[irq].level_low == IRQ_LEVEL_LOW) ? "low" : "high",
(was == IRQ_EDGE_LOW || was == IRQ_LEVEL_LOW) ? "low" : "high");
} else {
ioapicmask(irq);
line[irq].polled = 1;
cprintf("irq %d: '%s' still gets no interrupts either way up; polling "
"it\n", irq, line[irq].act[0].name);
}
}
static void static void
irq_poll_thread(void) irq_poll_thread(void)
@ -433,6 +515,20 @@ irq_poll_thread(void)
// Silent by design, and only its own handlers on it. // Silent by design, and only its own handlers on it.
if(line[irq].nquiet == line[irq].n && !line[irq].polled) if(line[irq].nquiet == line[irq].n && !line[irq].polled)
continue; continue;
int flipped = line[irq].flipped;
watch_line(irq);
if(line[irq].flipped != flipped){
// Drain what was waiting: the new polarity sees the line already
// asserted, and an edge needs it to go back first.
pushcli();
int n = line[irq].n;
for(int i = 0; i < n; i++)
line[irq].act[i].fn(line[irq].act[i].arg);
popcli();
continue;
}
int poll = line[irq].polled || int poll = line[irq].polled ||
(IRQ_IS_LEVEL(line[irq].level_low) && line[irq].delivered == 0 && (IRQ_IS_LEVEL(line[irq].level_low) && line[irq].delivered == 0 &&
now - line[irq].since > POLL_AFTER); now - line[irq].since > POLL_AFTER);

View file

@ -48,6 +48,15 @@ mouse_read(void)
return inb(KBD_S_DATA); return inb(KBD_S_DATA);
} }
// A byte from the auxiliary port, which IRQ 12 should have announced; see
// kbd_waiting() in console.c.
static int
mouse_waiting(void)
{
uchar st = inb(KBD_S_STAT);
return st != 0xFF && (st & 0x21) == 0x21;
}
static void static void
mouse_irq(void *arg) mouse_irq(void *arg)
{ {
@ -262,4 +271,5 @@ mouseinit(void)
mouse_read(); mouse_read();
irq_register(IRQ_MOUSE, mouse_irq, 0, "ps2-mouse", 0); irq_register(IRQ_MOUSE, mouse_irq, 0, "ps2-mouse", 0);
irq_watch(IRQ_MOUSE, mouse_waiting);
} }