diff --git a/Makefile b/Makefile index 0ba6fde..8ddba03 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/include/dev/hid.h b/include/dev/hid.h index a390ddb..e6c21f4 100644 --- a/include/dev/hid.h +++ b/include/dev/hid.h @@ -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); diff --git a/include/gmmos/gmmos.h b/include/gmmos/gmmos.h index 0e8b31d..ccd119c 100644 --- a/include/gmmos/gmmos.h +++ b/include/gmmos/gmmos.h @@ -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. diff --git a/kernel/acpi/i2c_hid.c b/kernel/acpi/i2c_hid.c index b4309a8..e9a6c88 100644 --- a/kernel/acpi/i2c_hid.c +++ b/kernel/acpi/i2c_hid.c @@ -50,6 +50,7 @@ #include #include #include +#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) { diff --git a/kernel/drivers/usb/hid/usb_hid.c b/kernel/drivers/usb/hid/usb_hid.c index 94cea98..af185bb 100644 --- a/kernel/drivers/usb/hid/usb_hid.c +++ b/kernel/drivers/usb/hid/usb_hid.c @@ -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,7 +555,36 @@ report_keyboard(struct hid *h, const uchar *rep, int len) if(n < 6) boot[2 + n++] = u; } - keyboard_report(h, boot, sizeof(boot)); + 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 diff --git a/kernel/gmmos/gmmos.c b/kernel/gmmos/gmmos.c index 5bad2ca..be7b9a8 100644 --- a/kernel/gmmos/gmmos.c +++ b/kernel/gmmos/gmmos.c @@ -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(); diff --git a/kernel/gmmos/hidkeys.c b/kernel/gmmos/hidkeys.c new file mode 100644 index 0000000..e924f9e --- /dev/null +++ b/kernel/gmmos/hidkeys.c @@ -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)]; +} diff --git a/kernel/gmmos/i2c.c b/kernel/gmmos/i2c.c new file mode 100644 index 0000000..82f11e4 --- /dev/null +++ b/kernel/gmmos/i2c.c @@ -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, +};