SchOS/kernel/drivers/usb/hid/usb_hid.c
Nexoniarz 8b7b8df74f usb-hid: bind an absolute pointer, and plumb EV_ABS through
Groundwork for usb-tablet, which is the fix for the guest cursor and the
host's never being in the same place: a PS/2 mouse reports how far it
moved, two pointers accumulating deltas independently drift apart, and no
amount of care in the compositor can make them agree. An absolute device
reports where it is, so they coincide by construction.

  - the HID driver no longer matches the boot subclass only. usb-tablet
    declares subclass 0 and was refused at the door; hid_probe decides
    what an interface actually is, and still declines what it cannot make
    sense of.
  - a non-boot HID interface is taken to be an absolute pointer and is
    not asked for boot protocol, which it does not have -- that request
    failing is what stopped it binding.
  - tablet_report decodes the fixed 6-byte layout (buttons, 16-bit X,
    16-bit Y, wheel) and scales the logical range to pixels there,
    because it is the only place that knows the range.
  - input_mouse_abs reports EV_ABS/ABS_X/ABS_Y so a reader can tell an
    absolute position from a delta, and mouse_set_abs places the pointer
    rather than nudging it.
  - Schcom treats EV_ABS as authoritative and EV_REL as a nudge.

The device binds and identifies correctly: "usb-hid: tablet (absolute) on
slot 1, endpoint 0x81 every 4 ms".

It is NOT finished, and QEMU_INPUT is empty by default because of it. No
reports have yet been observed arriving on the tablet's interrupt
endpoint, so making it the default pointer would trade a cursor that
drifts for one that does not move at all. Opt in with

  make qemu QEMU_INPUT='-device qemu-xhci,id=xhci -device usb-tablet,bus=xhci.0'

Next step is to find out why nothing arrives: whether the first transfer
is queued but never completes, whether a completion needs re-arming for
this endpoint, or whether QEMU is not routing motion to the tablet at all
(`info mice`).

bootcheck 108 [ok], schcom-test passes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 18:27:01 +02:00

359 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 "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)vga_get_width();
int h_px = (int)vga_get_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,
};