Same disease as the network stack, one subsystem over. kernel/dev/vga.c
was three things wearing one name: the owner of the framebuffer, a
bitmap drawing library, and the text console. Only the first is a
driver, and because they shared a file they shared a prefix -- so
panic.c drew the crash screen with vga_fill_rect, console.c printed with
vga_putc, /dev/fb0 handed out vga_fb_phys, and every one of those files
named a display driver it had no business knowing about.
Split three ways:
- kernel/drivers/gpu/vga/vga_fb.c owns the VBE framebuffer -- the mode
the loader programmed and handed over, the aperture, and the display
class vtable. It is a driver and now lives with the driver that
finds the card on the bus.
- kernel/dev/gfx.c draws: pixels, shapes, glyphs, the cursor, scroll,
the ANSI erase operations. It names no driver. The framebuffer
arrives through gfx_attach_scanout(), so a driver publishes what it
owns and this file draws on it without knowing what it is.
- include/gfx.h is the interface, and says why it is deliberately not
the display class: that one is about frames -- acquire, present,
vblank -- and the crash screen has to work when nothing else does.
main() no longer calls a display driver. platform_register_devices()
takes the framebuffer address and brings up whatever owns it, which is
where driver names belong; kernel/drivers/registry.c is the file whose
entire job is naming every driver in the kernel.
virtio-gpu got the e1000 treatment as well: instances in an array, the
instance hung on dev->priv by an `attached` hook, and every op reading
it rather than a file-static. It refused the second adapter outright
before, which is not hypothetical -- integrated plus discrete is an
ordinary laptop. The VGA driver deliberately has no such hook, and says
so: there is one framebuffer the loader programmed however many
VGA-class controllers the bus turns up, and a second one publishing the
same scanout is the truth rather than a bug.
Private headers moved next to their drivers: include/virtio.h and
include/xhci.h each had exactly one includer. include/ is for headers
the rest of the kernel needs.
Deleted rather than moved: consoleinit() programmed the sixteen ANSI
colours into the VGA DAC and disabled text-mode blink, by writing ports
0x3C0-0x3C9 from the console. The comment three lines above the colour
table already said this does nothing in a 32bpp linear mode -- there is
no palette to index -- so it was both dead and in the wrong file.
Verified, and this is a refactor so the evidence is that nothing moved:
- bootcheck -Fb -Net: 59 [ok], no failures. That covers the console
through every command, /dev/fb0's geometry, writing pixels through
write() and mapping the framebuffer through mmap.
- make crashshot: the crash screen renders exactly as before --
shapes, glyphs, logo and QR code -- which is the whole of gfx.c
exercised at once, in a panic, with the scanout attached by a driver.
- make schcom-test: five pixel assertions from the host oracle still
pass, so the display class and the compositor above it are unmoved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
360 lines
11 KiB
C
360 lines
11 KiB
C
// USB HID, boot protocol: keyboards and mice.
|
|
//
|
|
// Boot protocol is the reason a USB keyboard works in a BIOS setup screen
|
|
// before any operating system exists. A HID device normally describes its
|
|
// reports with a report descriptor -- a small stack language that has to be
|
|
// parsed to know which bit means which key -- but a device claiming the boot
|
|
// subclass also promises to support one fixed layout on request. Eight bytes
|
|
// for a keyboard, three for a mouse, defined in the HID specification and
|
|
// identical on every device.
|
|
//
|
|
// That is worth taking. A full report-descriptor parser is a real piece of
|
|
// work and buys nothing for a keyboard; it is what a game controller or a
|
|
// touchscreen will need, and it can arrive with them.
|
|
|
|
#include "types.h"
|
|
#include "defs.h"
|
|
#include "gfx.h"
|
|
#include "param.h"
|
|
#include "spinlock.h"
|
|
#include "dma.h"
|
|
#include "usb.h"
|
|
#include "input.h"
|
|
|
|
#define NHID 4
|
|
|
|
struct hid {
|
|
int in_use;
|
|
int proto; // USB_HID_PROTO_KEYBOARD or _MOUSE
|
|
uchar prev[8]; // the previous report, for edge detection
|
|
};
|
|
|
|
static struct hid hids[NHID];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Keyboard
|
|
//
|
|
// The report is: modifiers, a reserved byte, then up to six usage IDs of keys
|
|
// currently held. It is a *state*, not an event -- a key that stays down
|
|
// appears in every report until it is released, so a character is emitted
|
|
// only for usages that were not in the previous report. Sending one per
|
|
// report instead would repeat every held key a few hundred times a second.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#define HID_MOD_CTRL 0x11 // left | right
|
|
#define HID_MOD_SHIFT 0x22
|
|
#define HID_MOD_ALT 0x44
|
|
|
|
// Usage IDs 0x00-0x38 of the Keyboard/Keypad page. 0 means "nothing to type".
|
|
static const uchar hid_unshifted[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 hid_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', ' ',
|
|
'_','+','{','}','|','~',':','"','~','<','>','?',
|
|
};
|
|
|
|
// Characters decoded from a report, waiting to be handed to the console.
|
|
//
|
|
// The console pulls input through a getc function rather than being pushed
|
|
// to (consoleintr takes one), which is exactly the shape the PS/2 driver
|
|
// needs and exactly the wrong shape here -- a USB report arrives complete,
|
|
// with up to six keys in it. So the report is decoded into this queue and
|
|
// the console then drains it through the same path as any other keyboard,
|
|
// which means the entire line discipline is reused rather than reimplemented.
|
|
#define KQSIZE 64
|
|
static struct {
|
|
uchar buf[KQSIZE];
|
|
uint r, w;
|
|
} kq;
|
|
|
|
static void
|
|
kq_put(int c)
|
|
{
|
|
if(kq.w - kq.r < KQSIZE)
|
|
kq.buf[kq.w++ % KQSIZE] = c;
|
|
}
|
|
|
|
static int
|
|
usb_kbd_getc(void)
|
|
{
|
|
if(kq.r == kq.w)
|
|
return -1;
|
|
return kq.buf[kq.r++ % KQSIZE];
|
|
}
|
|
|
|
// Was this usage in that report?
|
|
static int
|
|
was_down(const uchar *prev, uchar usage)
|
|
{
|
|
int i;
|
|
|
|
for(i = 2; i < 8; i++)
|
|
if(prev[i] == usage)
|
|
return 1;
|
|
return 0;
|
|
}
|
|
|
|
// Turn the difference between two reports into press and release events for
|
|
// /dev/input/event0.
|
|
//
|
|
// The console path below cannot do this job: it emits a character per new key
|
|
// and nothing at all for a release, because a character stream has no way to
|
|
// say "released". A program that wants to know a key is being *held* -- which
|
|
// is every game and every compositor -- needs exactly the events this makes.
|
|
static void
|
|
keyboard_events(const uchar *prev, const uchar *rep)
|
|
{
|
|
int i, bit, n = 0;
|
|
|
|
for(bit = 0; bit < 8; bit++){
|
|
int now = (rep[0] >> bit) & 1;
|
|
int then = (prev[0] >> bit) & 1;
|
|
if(now != then){
|
|
input_report(INPUT_KBD, EV_KEY, input_hid_keycode(0xE0 + bit), now);
|
|
n++;
|
|
}
|
|
}
|
|
|
|
for(i = 2; i < 8; i++){
|
|
uchar u = rep[i];
|
|
if(u && !was_down(prev, u)){
|
|
int k = input_hid_keycode(u);
|
|
if(k){ input_report(INPUT_KBD, EV_KEY, k, 1); n++; }
|
|
}
|
|
}
|
|
for(i = 2; i < 8; i++){
|
|
uchar u = prev[i];
|
|
if(u && !was_down(rep, u)){
|
|
int k = input_hid_keycode(u);
|
|
if(k){ input_report(INPUT_KBD, EV_KEY, k, 0); n++; }
|
|
}
|
|
}
|
|
|
|
if(n)
|
|
input_sync(INPUT_KBD);
|
|
}
|
|
|
|
static void
|
|
keyboard_report(struct hid *h, const uchar *rep, int len)
|
|
{
|
|
uchar mods = rep[0];
|
|
int i, c;
|
|
|
|
if(len < 8)
|
|
return;
|
|
|
|
keyboard_events(h->prev, rep);
|
|
|
|
// Grabbed: the events above still went to /dev/input/event0, but nothing
|
|
// reaches the console. Note prev[] is updated before returning, or the next
|
|
// report would look like every held key had just been pressed again.
|
|
if(input_is_grabbed(INPUT_KBD)){
|
|
memmove(h->prev, rep, 8);
|
|
return;
|
|
}
|
|
|
|
for(i = 2; i < 8; i++){
|
|
uchar usage = rep[i];
|
|
|
|
if(usage == 0 || usage >= 0x39)
|
|
continue;
|
|
if(was_down(h->prev, usage))
|
|
continue; // still held from last time, not a new press
|
|
|
|
c = (mods & HID_MOD_SHIFT) ? hid_shifted[usage] : hid_unshifted[usage];
|
|
if(c == 0)
|
|
continue;
|
|
|
|
// Ctrl folds a letter down into its control character, which is how ^C
|
|
// and ^D reach the line discipline at all.
|
|
if(mods & HID_MOD_CTRL){
|
|
if(c >= 'a' && c <= 'z')
|
|
c = c - 'a' + 1;
|
|
else if(c >= 'A' && c <= 'Z')
|
|
c = c - 'A' + 1;
|
|
else
|
|
continue;
|
|
}
|
|
kq_put(c);
|
|
}
|
|
|
|
memmove(h->prev, rep, 8);
|
|
consoleintr(usb_kbd_getc);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mouse
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// QEMU's usb-tablet, and absolute pointers generally.
|
|
//
|
|
// Report: one byte of buttons, then X and Y as 16-bit little-endian values
|
|
// over the device's logical range, then a wheel byte. The range is 0..0x7fff
|
|
// and is *not* read from the report descriptor -- parsing one is a real piece
|
|
// of work, and this decodes exactly the fixed layout the tablet declares.
|
|
// A device whose report is a different length is left alone rather than
|
|
// guessed at.
|
|
//
|
|
// Scaling happens here because this is the only place that knows the logical
|
|
// range; everything above deals in pixels.
|
|
#define TABLET_LOGICAL_MAX 0x7fff
|
|
|
|
static void
|
|
tablet_report(struct hid *h, const uchar *rep, int len)
|
|
{
|
|
(void)h;
|
|
if(len < 5)
|
|
return;
|
|
|
|
int rawx = rep[1] | (rep[2] << 8);
|
|
int rawy = rep[3] | (rep[4] << 8);
|
|
|
|
int w = (int)gfx_width();
|
|
int h_px = (int)gfx_height();
|
|
if(w <= 0) w = 800;
|
|
if(h_px <= 0) h_px = 600;
|
|
|
|
// Scale in 64-bit-free arithmetic: 0x7fff * 4096 still fits in an int.
|
|
int x = (int)(((long long)rawx * (w - 1)) / TABLET_LOGICAL_MAX);
|
|
int y = (int)(((long long)rawy * (h_px - 1)) / TABLET_LOGICAL_MAX);
|
|
|
|
mouse_set_abs(x, y, rep[0] & 0x07);
|
|
}
|
|
|
|
static void
|
|
mouse_report(struct hid *h, const uchar *rep, int len)
|
|
{
|
|
int dx, dy;
|
|
|
|
(void)h;
|
|
if(len < 3)
|
|
return;
|
|
|
|
// The deltas are signed bytes. Reading them as unsigned is the classic way
|
|
// to get a pointer that only travels right and down.
|
|
dx = (signed char)rep[1];
|
|
dy = (signed char)rep[2];
|
|
|
|
// HID's Y axis points down, and so does the screen's. The PS/2 protocol's
|
|
// points up, which is why mouseintr subtracts where this adds.
|
|
mouse_inject(dx, dy, rep[0] & 0x07);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
static void
|
|
hid_complete(struct usb_dev *dev, void *buf, int len)
|
|
{
|
|
struct hid *h = dev->drvdata;
|
|
|
|
if(h == 0)
|
|
return;
|
|
if(h->proto == USB_HID_PROTO_TABLET)
|
|
tablet_report(h, buf, len);
|
|
else if(h->proto == USB_HID_PROTO_KEYBOARD)
|
|
keyboard_report(h, buf, len);
|
|
else
|
|
mouse_report(h, buf, len);
|
|
}
|
|
|
|
static int
|
|
hid_request(struct usb_dev *dev, int request, int value)
|
|
{
|
|
struct usb_setup s;
|
|
|
|
s.bmRequestType = USB_DIR_OUT | USB_TYPE_CLASS | USB_RECIP_INTERFACE;
|
|
s.bRequest = request;
|
|
s.wValue = value;
|
|
s.wIndex = dev->iface.bInterfaceNumber;
|
|
s.wLength = 0;
|
|
return dev->ops->control(dev, &s, 0, 0);
|
|
}
|
|
|
|
static int
|
|
hid_probe(struct usb_dev *dev)
|
|
{
|
|
struct hid *h = 0;
|
|
int i, proto = dev->iface.bInterfaceProtocol;
|
|
|
|
if(!dev->has_intr_in){
|
|
cprintf("usb-hid: no interrupt IN endpoint; nothing to read\n");
|
|
return -1;
|
|
}
|
|
|
|
for(i = 0; i < NHID; i++)
|
|
if(!hids[i].in_use){ h = &hids[i]; break; }
|
|
if(h == 0)
|
|
return -1;
|
|
|
|
// A HID device that claims no boot protocol is taken to be an absolute
|
|
// pointer -- in practice a tablet, which is what a VM offers so that its
|
|
// cursor and the host's can agree. It must not be asked for boot protocol:
|
|
// it has none, the request fails, and refusing on that basis is what kept
|
|
// usb-tablet from binding at all.
|
|
if(proto != USB_HID_PROTO_KEYBOARD && proto != USB_HID_PROTO_MOUSE)
|
|
proto = USB_HID_PROTO_TABLET;
|
|
|
|
// Ask for the fixed layout explicitly. A device that supports the boot
|
|
// subclass still starts in report protocol, so without this the reports
|
|
// arrive in whatever shape its report descriptor declares -- which we have
|
|
// not read and cannot interpret.
|
|
if(proto != USB_HID_PROTO_TABLET &&
|
|
hid_request(dev, USB_HID_SET_PROTOCOL, USB_HID_PROTOCOL_BOOT) < 0){
|
|
cprintf("usb-hid: SET_PROTOCOL(boot) refused\n");
|
|
return -1;
|
|
}
|
|
|
|
// Idle rate 0 = "report only when something changes". The default is to
|
|
// resend the current state every few milliseconds forever, which for a
|
|
// keyboard means a constant stream of interrupts saying nothing.
|
|
hid_request(dev, USB_HID_SET_IDLE, 0);
|
|
|
|
if(dev->ops->open_ep(dev, &dev->intr_in) < 0)
|
|
return -1;
|
|
|
|
h->in_use = 1;
|
|
h->proto = proto;
|
|
dev->drvdata = h;
|
|
dev->complete = hid_complete;
|
|
|
|
// Arm the first transfer. Until a TRB is waiting on the endpoint the device
|
|
// has nowhere to put a report, so nothing would ever arrive.
|
|
if(dev->ops->queue_in(dev, dev->intr_in.bEndpointAddress, 0, 0) < 0)
|
|
return -1;
|
|
|
|
cprintf("usb-hid: %s on slot %d, endpoint 0x%x every %d ms\n",
|
|
proto == USB_HID_PROTO_KEYBOARD ? "keyboard" :
|
|
proto == USB_HID_PROTO_MOUSE ? "mouse" :
|
|
proto == USB_HID_PROTO_TABLET ? "tablet (absolute)" : "device",
|
|
dev->slot, dev->intr_in.bEndpointAddress, dev->intr_in.bInterval);
|
|
return 0;
|
|
}
|
|
|
|
// Any HID interface, not just the boot subclass.
|
|
//
|
|
// It was boot-only, on the reasoning that a device without boot protocol needs
|
|
// its report descriptor parsed and there would be nothing useful to do with it.
|
|
// That is right in general and wrong for the one device that matters here:
|
|
// usb-tablet declares subclass 0, so it was refused at the door -- and its
|
|
// report layout is fixed and known, so there is something useful to do with it
|
|
// after all. hid_probe decides what a given interface actually is; anything it
|
|
// cannot make sense of is still declined there.
|
|
const struct usb_driver usb_hid_driver = {
|
|
.name = "usb-hid",
|
|
.class = USB_CLASS_HID,
|
|
.subclass = 0xFF,
|
|
.protocol = 0xFF,
|
|
.devclass = DEVCLASS_INPUT,
|
|
.probe = hid_probe,
|
|
};
|