GMMOS: I2C keyboards, and every keyboard at once

The second micro driver: i2c_hid.c with everything but reading a
keyboard's input reports taken out. No ACPI -- AML cannot run on a dead
kernel -- so the live driver hands over what it found at boot
(i2c_hid_keyboards): the DesignWare controller, the address, the report
size, the AMD GPIO pin if there is one, and the parsed report descriptor.
No power commands, no reset, a read-only transfer.

usb_hid.c's report-protocol rewrite is split out as hid_boot_report(),
which does nothing but rewrite, and kernel/gmmos/hidkeys.c turns boot
reports into keys for this and the USB micro drivers to come.

GMMOS now probes every micro driver and polls all that answer.

Not run: QEMU has no DesignWare I2C, and the laptops at hand have PS/2
keyboards.

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-25 01:15:50 +02:00
commit c6d90e69fa
8 changed files with 387 additions and 17 deletions

View file

@ -116,7 +116,7 @@ KERN_ZSTD = $(addprefix $(BUILD)/zstd/, \
KERN_ZSTD += $(BUILD)/kernel/scff/scff.o
# GMMOS, what runs after a panic, and its micro drivers. See include/gmmos/gmmos.h.
KERN_GMMOS = $(addprefix $(BUILD)/kernel/gmmos/, gmmos.o ps2.o)
KERN_GMMOS = $(addprefix $(BUILD)/kernel/gmmos/, gmmos.o hidkeys.o ps2.o i2c.o)
KERN_PROC = $(addprefix $(BUILD)/kernel/proc/, \
proc.o)

View file

@ -12,6 +12,13 @@ void *hid_attach(const uchar *desc, int n, const char *what);
// One input report, report ID included when the device uses them.
void hid_attached_input(void *h, const uchar *rep, int len);
// For GMMOS, after a panic: whether a device is a keyboard, and one of its
// reports rewritten into the 8-byte boot layout (modifiers, reserved, six
// usages) with nothing else done -- no queue, no lock, no events. 0 when the
// report was not a keyboard's.
int hid_is_keyboard(void *h);
int hid_boot_report(void *h, const uchar *rep, int len, uchar *boot);
// I2C-HID: the touchpads (and some keyboards) of laptops. Started once ACPI
// and kernel threads are up; see kernel/drivers/i2c/i2c_hid.c.
void i2c_hid_start(void);

View file

@ -31,6 +31,33 @@ struct gmmos_kbd {
};
extern const struct gmmos_kbd gmmos_ps2;
extern const struct gmmos_kbd gmmos_i2c;
// A USB or I2C keyboard report in the boot layout -- modifiers, a reserved
// byte, six usages -- turned into keys. A report is a state, not an event, so
// a key is a usage that was not in the previous one. kernel/gmmos/hidkeys.c.
struct gmmos_hidkeys {
uchar prev[8];
int q[16];
int qh, qt;
};
void gmmos_hid_report(struct gmmos_hidkeys *k, const uchar *boot);
int gmmos_hid_next(struct gmmos_hidkeys *k);
// I2C keyboards, as kernel/acpi/i2c_hid.c found them at boot. GMMOS cannot run
// ACPI to find them again, so the live driver hands over what it learnt: the
// DesignWare controller, the device's address, its input report size, the
// interrupt pin when there is one, and the parsed report descriptor.
struct gmmos_i2c_kbd {
volatile uint32 *regs;
int txdepth, rxdepth;
int addr;
uint32 maxin;
volatile uint32 *pin; // AMD GPIO pin register, or 0: poll blindly
int active_low;
void *hid;
};
int i2c_hid_keyboards(struct gmmos_i2c_kbd *out, int max);
// From kernel/core/panic.c: draw the report again, and where its closing
// lines are, which GMMOS writes its status and its keys into.

View file

@ -50,6 +50,7 @@
#include <uacpi/utilities.h>
#include <uacpi/namespace.h>
#include <uacpi/resources.h>
#include "gmmos/gmmos.h"
// Each step said before it is taken, in a verbose boot: a machine that stops
// here stops with the step it was on as the last line on the screen.
@ -653,6 +654,30 @@ i2c_hid_main(void *arg)
}
}
// For GMMOS. Read without a lock: `ndev` only ever grows, and only before the
// polling loop starts, so a panic sees a list that was finished or not begun.
int
i2c_hid_keyboards(struct gmmos_i2c_kbd *out, int max)
{
int i, n = 0;
for(i = 0; i < ndev && n < max; i++){
struct i2chid *d = &devs[i];
if(!hid_is_keyboard(d->hid))
continue;
out[n].regs = d->c->regs;
out[n].txdepth = d->c->txdepth;
out[n].rxdepth = d->c->rxdepth;
out[n].addr = d->addr;
out[n].maxin = d->maxin;
out[n].pin = d->pin;
out[n].active_low = d->active_low;
out[n].hid = d->hid;
n++;
}
return n;
}
static void
i2c_hid_thread(void)
{

View file

@ -525,19 +525,19 @@ bits(const uchar *p, int len, int bit, int size)
// A report-protocol keyboard report, rewritten as a boot one: modifiers, a
// reserved byte, six usages. Six is a limit only of the rewrite; the seventh
// key held at once is dropped, which is what a boot keyboard does too.
static void
report_keyboard(struct hid *h, const uchar *rep, int len)
// Nothing else happens here, which is what lets GMMOS call it after a panic.
static int
to_boot(struct hid *h, const uchar *rep, int len, uchar *boot)
{
uchar boot[8];
int i, n = 0;
if(h->kbd_id){
if(len < 1 || rep[0] != h->kbd_id)
return;
return 0;
rep++;
len--;
}
memset(boot, 0, sizeof(boot));
memset(boot, 0, 8);
for(i = 0; i < 8; i++)
if(h->mod_bit[i] >= 0 && bits(rep, len, h->mod_bit[i], 1))
boot[0] |= 1 << i;
@ -555,9 +555,38 @@ report_keyboard(struct hid *h, const uchar *rep, int len)
if(n < 6)
boot[2 + n++] = u;
}
return 1;
}
static void
report_keyboard(struct hid *h, const uchar *rep, int len)
{
uchar boot[8];
if(to_boot(h, rep, len, boot))
keyboard_report(h, boot, sizeof(boot));
}
int
hid_is_keyboard(void *hv)
{
struct hid *h = hv;
return h && (h->rkbd || h->proto == USB_HID_PROTO_KEYBOARD);
}
int
hid_boot_report(void *hv, const uchar *rep, int len, uchar *boot)
{
struct hid *h = hv;
if(h->rkbd)
return to_boot(h, rep, len, boot);
if(h->proto != USB_HID_PROTO_KEYBOARD || len < 8)
return 0;
memmove(boot, rep, 8);
return 1;
}
static int
field(const uchar *p, int len, const struct hfield *f)
{

View file

@ -47,7 +47,30 @@ static addr_t *pcs;
static int npcs;
static int sw, sh, cw, ch;
static const struct gmmos_kbd *kbd;
// Every micro driver, in the order they are tried. All that answer are
// polled: a laptop has its own keyboard and maybe a USB one on the side.
static const struct gmmos_kbd *const drivers[] = { &gmmos_ps2, &gmmos_i2c };
static const struct gmmos_kbd *kbds[NELEM(drivers)];
static int nkbd;
static int
poll_keys(void)
{
for (int i = 0; i < nkbd; i++) {
int k = kbds[i]->poll();
if (k)
return k;
}
return 0;
}
static void
set_lights(int on)
{
for (int i = 0; i < nkbd; i++)
if (kbds[i]->leds)
kbds[i]->leds(on);
}
enum { MAIN, QRPAGES, LOG, TERM };
static int mode = MAIN;
@ -493,6 +516,7 @@ cmd_help(void)
tline("pci PCI devices");
tline("mem ADDR [LEN] bytes at a kernel address; both in hex (80, up to 400)");
tline("uptime how long it ran, and how long since");
tline("keyboards which keyboards GMMOS is reading");
tline("clear empty this screen");
tline("restart restart the machine");
tline("poweroff power it off");
@ -763,6 +787,10 @@ run(void)
else if (streq(w, "pci")) cmd_pci();
else if (streq(w, "mem")) cmd_mem(arg);
else if (streq(w, "uptime")) cmd_uptime();
else if (streq(w, "keyboards")) {
for (int i = 0; i < nkbd; i++)
tline(kbds[i]->name);
}
else if (streq(w, "clear")) tn = 0;
else if (streq(w, "restart") || streq(w, "reboot")) restart();
else if (streq(w, "poweroff")) {
@ -829,7 +857,7 @@ draw_status(void)
sb_putc(&sb, ':');
sb_putc(&sb, (char)('0' + left % 60 / 10));
sb_putc(&sb, (char)('0' + left % 10));
sb_puts(&sb, kbd ? ". Any key keeps it on." : ".");
sb_puts(&sb, nkbd ? ". Any key keeps it on." : ".");
} else {
sb_puts(&sb, "Staying on. Nothing here is lost until it restarts.");
}
@ -837,7 +865,7 @@ draw_status(void)
textn(x, y + step, sbz(&sb), (sw * 2 / 3 - x) / cw, PSOGM_DIM);
gfx_fill_rect(x, y + step * 3, sw - x, ch + 2, PSOGM_BG);
if (kbd)
if (nkbd)
textn(x, y + step * 3,
"Space log as QR L read the log T terminal R restart P power off",
(sw - x) / cw, PSOGM_FG);
@ -961,8 +989,9 @@ gmmos_run(char *r, addr_t *p, int n)
rows = (sh - my * 2) / ch;
log_snapshot();
if (gmmos_ps2.probe())
kbd = &gmmos_ps2;
for (int i = 0; i < (int)NELEM(drivers); i++)
if (drivers[i]->probe())
kbds[nkbd++] = drivers[i];
tline("GMMOS, the Guru Meditation Micro OS. The kernel is dead; this is what");
tline("runs after it. help lists the commands.");
draw_status();
@ -971,10 +1000,10 @@ gmmos_run(char *r, addr_t *p, int n)
int lights = 0, keyed = 0;
for (;;) {
int k = kbd ? kbd->poll() : 0;
int k = poll_keys();
if (k) {
if (!keyed && kbd->leds) {
kbd->leds(0); // somebody is here: the lights have done their job
if (!keyed) {
set_lights(0); // somebody is here: the lights have done their job
lights = 0;
}
keyed = 1;
@ -1001,10 +1030,10 @@ gmmos_run(char *r, addr_t *p, int n)
}
// The lights until somebody presses a key, as panic_blink did: on a
// machine whose screen is black, they are all there is to say it died.
if (!keyed && kbd && kbd->leds && t - last_blink >= 333) {
if (!keyed && t - last_blink >= 333) {
last_blink = t;
lights = !lights;
kbd->leds(lights);
set_lights(lights);
}
if (!boot_verbose && !cancelled && t - entered_ms >= OFF_MS) {
acpi_poweroff_now();

98
kernel/gmmos/hidkeys.c Normal file
View file

@ -0,0 +1,98 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// Boot-layout keyboard reports to keys, for GMMOS's USB and I2C keyboards.
//
// usb_hid.c's keyboard path with the console, /dev/input, the lock and key
// repeat taken out: what is left is edge detection and a table. US layout, as
// the live USB path is.
#include "core/types.h"
#include "core/defs.h"
#include "dev/kbd.h"
#include "gmmos/gmmos.h"
#define MOD_CTRL 0x11
#define MOD_SHIFT 0x22
static const uchar plain[0x39] = {
0, 0, 0, 0,
'a','b','c','d','e','f','g','h','i','j','k','l','m',
'n','o','p','q','r','s','t','u','v','w','x','y','z',
'1','2','3','4','5','6','7','8','9','0',
'\n', 0x1B, '\b', '\t', ' ',
'-','=','[',']','\\','#',';','\'','`',',','.','/',
};
static const uchar shifted[0x39] = {
0, 0, 0, 0,
'A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'!','@','#','$','%','^','&','*','(',')',
'\n', 0x1B, '\b', '\t', ' ',
'_','+','{','}','|','~',':','"','~','<','>','?',
};
static int
special(uchar u)
{
switch (u) {
case 0x4A: return KBD_HOME;
case 0x4B: return KBD_PGUP;
case 0x4C: return KBD_DEL;
case 0x4D: return KBD_END;
case 0x4E: return KBD_PGDN;
case 0x4F: return KBD_RT;
case 0x50: return KBD_LF;
case 0x51: return KBD_DN;
case 0x52: return KBD_UP;
case 0x56: return '-'; // keypad minus and plus: the QR size keys
case 0x57: return '+';
case 0x58: return '\n'; // keypad Enter
}
return 0;
}
static int
was_down(const uchar *rep, uchar u)
{
for (int i = 2; i < 8; i++)
if (rep[i] == u)
return 1;
return 0;
}
void
gmmos_hid_report(struct gmmos_hidkeys *k, const uchar *boot)
{
uchar mods = boot[0];
for (int i = 2; i < 8; i++) {
uchar u = boot[i];
int c;
if (u < 4 || was_down(k->prev, u))
continue;
if (u < 0x39)
c = (mods & MOD_SHIFT) ? shifted[u] : plain[u];
else
c = special(u);
if (c == 0)
continue;
if ((mods & MOD_CTRL) && c < KBD_SPECIAL) {
if (c >= 'a' && c <= 'z') c = c - 'a' + 1;
else if (c >= 'A' && c <= 'Z') c = c - 'A' + 1;
else continue;
}
if (k->qt - k->qh < (int)NELEM(k->q))
k->q[k->qt++ % NELEM(k->q)] = c;
}
memmove(k->prev, boot, 8);
}
int
gmmos_hid_next(struct gmmos_hidkeys *k)
{
if (k->qh == k->qt)
return 0;
return k->q[k->qh++ % NELEM(k->q)];
}

155
kernel/gmmos/i2c.c Normal file
View file

@ -0,0 +1,155 @@
// SPDX-License-Identifier: GPL-2.0-or-later
//
// GMMOS's I2C keyboards: kernel/acpi/i2c_hid.c with everything but reading a
// keyboard's input reports taken out.
//
// No ACPI -- the firmware's AML cannot run on a dead kernel, so the devices
// are the ones the live driver found at boot (i2c_hid_keyboards). No power
// commands, no reset, no report descriptor: the device is already on and was
// already described. No interrupt: the pin is read when the device has one,
// otherwise the input register is read every few milliseconds regardless.
// And the DesignWare transfer is read-only, since an input report is a plain
// read with no register write before it.
#include "core/types.h"
#include "core/defs.h"
#include "dev/hid.h"
#include "gmmos/gmmos.h"
#define IC_TAR 0x04
#define IC_DATA_CMD 0x10
#define IC_RAW_INTR_STAT 0x34
#define IC_CLR_INTR 0x40
#define IC_CLR_TX_ABRT 0x54
#define IC_CLR_STOP_DET 0x60
#define IC_ENABLE 0x6C
#define IC_TXFLR 0x74
#define IC_RXFLR 0x78
#define IC_ENABLE_STATUS 0x9C
#define CMD_READ (1 << 8)
#define CMD_STOP (1 << 9)
#define INTR_TX_ABRT (1 << 6)
#define INTR_STOP_DET (1 << 9)
#define AMD_PIN_STS (1u << 16)
#define NKBD 4
static struct gmmos_i2c_kbd kb[NKBD];
static int nkb, turn;
static uchar buf[4096];
static struct gmmos_hidkeys keys;
static uint32
rd(struct gmmos_i2c_kbd *d, int reg)
{
return d->regs[reg / 4];
}
static void
wr(struct gmmos_i2c_kbd *d, int reg, uint32 v)
{
d->regs[reg / 4] = v;
}
static int
enable(struct gmmos_i2c_kbd *d, int on)
{
for (int i = 0; i < 1000; i++) {
wr(d, IC_ENABLE, on);
if ((rd(d, IC_ENABLE_STATUS) & 1) == (uint32)on)
return 0;
microdelay(25);
}
return -1;
}
// n bytes from the device. Disabling the controller first also abandons
// whatever transfer the live driver had going when the kernel died.
static int
read_bytes(struct gmmos_i2c_kbd *d, uchar *r, int n)
{
int sent = 0, got = 0, spin;
if (enable(d, 0) < 0)
return -1;
wr(d, IC_TAR, d->addr);
rd(d, IC_CLR_INTR);
if (enable(d, 1) < 0)
return -1;
for (spin = 0; got < n; spin++) {
if (rd(d, IC_RAW_INTR_STAT) & INTR_TX_ABRT)
goto abort;
while (got < n && rd(d, IC_RXFLR) > 0)
r[got++] = rd(d, IC_DATA_CMD);
while (sent < n && (int)rd(d, IC_TXFLR) < d->txdepth &&
sent - got < d->rxdepth) {
wr(d, IC_DATA_CMD, CMD_READ | (sent == n - 1 ? CMD_STOP : 0));
sent++;
}
if (spin > 20000)
goto abort;
microdelay(10);
}
for (spin = 0; !(rd(d, IC_RAW_INTR_STAT) & INTR_STOP_DET); spin++) {
if (spin > 2000)
goto abort;
microdelay(10);
}
rd(d, IC_CLR_STOP_DET);
enable(d, 0);
return 0;
abort:
rd(d, IC_CLR_TX_ABRT);
rd(d, IC_CLR_INTR);
enable(d, 0);
return -1;
}
static int
probe(void)
{
nkb = i2c_hid_keyboards(kb, NKBD);
for (int i = 0; i < nkb; i++)
if (kb[i].maxin > sizeof(buf))
kb[i].maxin = sizeof(buf);
return nkb > 0;
}
static int
poll(void)
{
int k = gmmos_hid_next(&keys);
if (k)
return k;
// GMMOS polls every millisecond; a blind read is a whole report's worth of
// bus time, so a device with no pin is read every fourth time.
for (int i = 0; i < nkb; i++) {
struct gmmos_i2c_kbd *d = &kb[i];
if (d->pin) {
int high = (*d->pin & AMD_PIN_STS) != 0;
if (high == d->active_low)
continue;
} else if (turn++ % 4) {
continue;
}
if (read_bytes(d, buf, d->maxin) < 0)
continue;
int len = buf[0] | (buf[1] << 8);
uchar boot[8];
if (len > 2 && len <= (int)d->maxin &&
hid_boot_report(d->hid, buf + 2, len - 2, boot))
gmmos_hid_report(&keys, boot);
}
return gmmos_hid_next(&keys);
}
const struct gmmos_kbd gmmos_i2c = {
.name = "I2C",
.probe = probe,
.poll = poll,
};