/* * virtio_input.c — Virtio 1.0 input device driver for StarKernel (item 4.3.5c) * * Modern virtio 1.0 interface only (device ID 0x1052). No legacy fallback * exists for virtio-input (unlike virtio-blk's 0x1001) -- input postdates * the legacy 0.9.5 spec. * * Split virtqueue (eventq, index 0) with pre-posted device-writable * buffers: unlike virtio_blk.c's synchronous request/response pattern, the * driver posts empty VirtioInputEvent buffers up front and the device * fills+posts them to the used ring asynchronously, signalled by the PLIC * (see virtio_input_isr(), called from arch/riscv64/interrupts.c on a * matching claim) -- no polling anywhere in this path. * * The PCI capability walker below is a deliberate duplicate of * virtio_blk.c's static walk_virtio_caps(), not a shared/promoted version * -- decided at this item rather than refactoring the working, tested * virtio_blk.c (FABRIC.md item 4.3.5c note, carries forward to 4.3.5e). * * Interrupt routing (PLIC source computation/enable) is riscv64-specific * and arch-guarded below; the capability walk, feature negotiation, and * queue/event handling are arch-agnostic and compiled for all three * targets (same idiom as pci.c itself: config-space access already * differs per arch internally, this file adds one more arch-guarded * piece on top rather than a second copy of the whole file). */ #ifndef __STARKERNEL__ #error "virtio_input.c is kernel-only" #endif #include #include #include #include "starkernel/pci.h" #include "starkernel/virtio_input.h" #include "starkernel/kmalloc.h" #include "console.h" #if defined(ARCH_RISCV64) #include "starkernel/plic.h" #endif /* ------------------------------------------------------------------------- * Virtio 1.0 PCI capability structures -- duplicated from virtio_blk.c * (see file header for why), plus VIRTIO_PCI_CAP_ISR_CFG actually used * here (virtio_blk.c defines the constant but never reads it -- blk polls * and never needed to; this is the first interrupt-driven virtio-pci * device in this tree). * ------------------------------------------------------------------------- */ #define VIRTIO_PCI_CAP_VENDOR_ID 0x09u #define VIRTIO_PCI_CAP_COMMON_CFG 1u #define VIRTIO_PCI_CAP_NOTIFY_CFG 2u #define VIRTIO_PCI_CAP_ISR_CFG 3u #define VCAP_OFF_CAP_VNDR 0u #define VCAP_OFF_CAP_NEXT 1u #define VCAP_OFF_CFG_TYPE 3u #define VCAP_OFF_BAR 4u #define VCAP_OFF_OFFSET 8u #define VCAP_OFF_LENGTH 12u #define VCAP_OFF_NOTIFY_MULT 16u /* virtio common config (MMIO layout, virtio 1.0 §4.1.4.3) -- not packed, * same rationale as virtio_blk.c's VirtioCommonCfg (riscv64 strict * alignment; QEMU's common-cfg MMIO handler mishandles byte reads of the * 16-bit fields otherwise). */ typedef struct { volatile uint32_t device_feature_select; volatile uint32_t device_feature; volatile uint32_t driver_feature_select; volatile uint32_t driver_feature; volatile uint16_t config_msix_vector; volatile uint16_t num_queues; volatile uint8_t device_status; volatile uint8_t config_generation; volatile uint16_t queue_select; volatile uint16_t queue_size; volatile uint16_t queue_msix_vector; volatile uint16_t queue_enable; volatile uint16_t queue_notify_off; volatile uint64_t queue_desc; volatile uint64_t queue_driver; volatile uint64_t queue_device; volatile uint16_t queue_notify_data; volatile uint16_t queue_reset; } VirtioCommonCfg; #define VIRTIO_STATUS_ACKNOWLEDGE 0x01u #define VIRTIO_STATUS_DRIVER 0x02u #define VIRTIO_STATUS_DRIVER_OK 0x04u #define VIRTIO_STATUS_FEATURES_OK 0x08u #define VIRTIO_STATUS_FAILED 0x80u #define VIRTIO_F_VERSION_1 (1ULL << 32) /* ------------------------------------------------------------------------- * Split virtqueue structures (virtio 1.0 §2.6) -- duplicated shape from * virtio_blk.c, sized for the event queue instead. * ------------------------------------------------------------------------- */ #define EVENTQ_SIZE 8u /* power of 2; headroom for several queued events */ #define VRING_DESC_F_WRITE 2u typedef struct { uint64_t addr; uint32_t len; uint16_t flags; uint16_t next; } VirtqDesc; typedef struct { uint16_t flags; uint16_t idx; uint16_t ring[EVENTQ_SIZE]; uint16_t used_event; } VirtqAvail; typedef struct { uint32_t id; uint32_t len; } VirtqUsedElem; typedef struct { uint16_t flags; uint16_t idx; VirtqUsedElem ring[EVENTQ_SIZE]; uint16_t avail_event; } VirtqUsed; /* ------------------------------------------------------------------------- * Diagnostic ring -- consumed by keyboard_words.c's VKBD-EVENT, same * relationship KBD-SCAN has to i8042's ring (raw hardware-boundary peek, * no scancode-to-ASCII layout, no REPL wiring -- that's 4.3.5f/M8). * ------------------------------------------------------------------------- */ #define DIAG_RING_SIZE 16u typedef struct { uint16_t code; uint32_t value; } DiagEvent; static DiagEvent g_diag_ring[DIAG_RING_SIZE]; static volatile uint32_t g_diag_head = 0; static volatile uint32_t g_diag_tail = 0; static void diag_push(uint16_t code, uint32_t value) { uint32_t next = (g_diag_head + 1u) % DIAG_RING_SIZE; if (next == g_diag_tail) return; /* full: drop, same as i8042's bounded ring */ g_diag_ring[g_diag_head].code = code; g_diag_ring[g_diag_head].value = value; g_diag_head = next; } int virtio_input_pop_event(uint16_t *code, uint32_t *value) { if (g_diag_tail == g_diag_head) return 0; if (code) *code = g_diag_ring[g_diag_tail].code; if (value) *value = g_diag_ring[g_diag_tail].value; g_diag_tail = (g_diag_tail + 1u) % DIAG_RING_SIZE; return 1; } /* Standing diagnostic, same shape as g_plic_claim_count/g_i8042_isr_count. */ volatile uint32_t g_virtio_input_isr_count = 0; /* ------------------------------------------------------------------------- * Driver state * ------------------------------------------------------------------------- */ typedef struct { VirtioCommonCfg *common; volatile uint16_t *notify; uint32_t notify_off_mult; uint16_t queue_notify_off; volatile uint8_t *isr_status; /* VIRTIO_PCI_CAP_ISR_CFG -- read to * deassert the (level-triggered) line */ VirtqDesc *desc; VirtqAvail *avail; VirtqUsed *used; VirtioInputEvent *bufs; /* EVENTQ_SIZE event buffers, device-writable */ uint16_t avail_idx; uint16_t last_used_idx; } VirtInputState; static VirtInputState g_vinput; static int g_vinput_ready = 0; /* PLIC source (riscv64) this device's INTx routes to, computed at init. * 0 = invalid/not found -- matches plic_claim()'s own "0 = nothing * pending" sentinel, so an unfound device can never accidentally match a * real claim. Checked by arch/riscv64/interrupts.c's dispatch. */ uint32_t g_virtio_input_plic_source = 0; /* ------------------------------------------------------------------------- * Capability walker -- duplicated from virtio_blk.c, see file header. * ------------------------------------------------------------------------- */ static void *walk_virtio_caps(const PciDevice *d, uint8_t cap_type, uint32_t *extra_out) { uint8_t cap_ptr = pci_read8(d, (uint16_t)PCI_CFG_CAP_PTR) & 0xFCu; if (!cap_ptr) return NULL; int limit = 48; while (cap_ptr && limit--) { uint8_t vndr = pci_read8(d, cap_ptr + (uint16_t)VCAP_OFF_CAP_VNDR); uint8_t next = pci_read8(d, cap_ptr + (uint16_t)VCAP_OFF_CAP_NEXT); uint8_t ctype = pci_read8(d, cap_ptr + (uint16_t)VCAP_OFF_CFG_TYPE); if (vndr == (uint8_t)VIRTIO_PCI_CAP_VENDOR_ID && ctype == cap_type) { uint8_t bar = pci_read8 (d, cap_ptr + (uint16_t)VCAP_OFF_BAR); uint32_t offset = pci_read32(d, cap_ptr + (uint16_t)VCAP_OFF_OFFSET); uint32_t length = pci_read32(d, cap_ptr + (uint16_t)VCAP_OFF_LENGTH); if (bar > 5u) { cap_ptr = next & 0xFCu; continue; } uint64_t bar_base = pci_bar(d, (int)bar); if (!bar_base) { cap_ptr = next & 0xFCu; continue; } if (pci_map_bar(bar_base, (uint64_t)length + offset) != 0) { cap_ptr = next & 0xFCu; continue; } if (extra_out && cap_type == VIRTIO_PCI_CAP_NOTIFY_CFG) { *extra_out = pci_read32(d, cap_ptr + (uint16_t)VCAP_OFF_NOTIFY_MULT); } return (void *)(uintptr_t)(bar_base + offset); } cap_ptr = next & 0xFCu; } return NULL; } static inline void wmb(void) { /* Compiler barrier only, same as virtio_blk.c -- reported not fixed * there (FABRIC.md item 4.3.5e note); this item is the first place an * interrupt-driven used-ring (device writes concurrently with the * driver's re-post loop) leans on the ordering harder than blk's * synchronous polled loop ever did. */ __asm__ volatile("" : : : "memory"); } /* ------------------------------------------------------------------------- * Interrupt routing -- riscv64 (PLIC). aarch64 (GIC, item 4.3.5e) is not * yet implemented; the #else path reports rather than silently no-ops, so * a future arch attempting to use this file without adding its own * routing fails loudly instead of "working" with a permanently-pending, * never-enabled source. * ------------------------------------------------------------------------- */ #if defined(ARCH_RISCV64) static int enable_interrupt_route(const PciDevice *pci) { uint8_t pin = pci_read8(pci, (uint16_t)PCI_CFG_INT_PIN); if (pin < 1u || pin > 4u) { console_println("virtio-input: bad/absent INT_PIN"); return -1; } uint32_t slot = pci->device; uint32_t source = 32u + ((slot + (uint32_t)pin - 1u) % 4u); plic_set_priority(source, 1u); plic_enable(source); g_virtio_input_plic_source = source; return 0; } #else static int enable_interrupt_route(const PciDevice *pci) { (void)pci; console_println("virtio-input: interrupt routing not implemented on this arch"); return -1; } #endif /* ------------------------------------------------------------------------- * Device initialisation -- status/feature negotiation mirrors * vblk_init_device() (virtio_blk.c:361-499); differences noted inline. * ------------------------------------------------------------------------- */ static int vinput_init_device(const PciDevice *pci) { VirtInputState *s = &g_vinput; pci_enable(pci); /* pci_enable() only sets IO/MEM/BUS_MASTER -- it never clears PCI * COMMAND bit 10 (Interrupt Disable). If firmware left it set, INTx * never asserts and every step below "succeeds" while producing zero * real interrupts (FABRIC.md item 4.3.5c/4.3.5e note). Check and clear * explicitly. */ { uint16_t cmd = pci_read16(pci, (uint16_t)PCI_CFG_COMMAND); if (cmd & (uint16_t)PCI_CMD_INTX_DISABLE) { pci_write16(pci, (uint16_t)PCI_CFG_COMMAND, cmd & (uint16_t)~PCI_CMD_INTX_DISABLE); } } uint32_t notify_mult = 0; VirtioCommonCfg *common = (VirtioCommonCfg *) walk_virtio_caps(pci, VIRTIO_PCI_CAP_COMMON_CFG, NULL); volatile uint16_t *notify = (volatile uint16_t *) walk_virtio_caps(pci, VIRTIO_PCI_CAP_NOTIFY_CFG, ¬ify_mult); volatile uint8_t *isr_status = (volatile uint8_t *) walk_virtio_caps(pci, VIRTIO_PCI_CAP_ISR_CFG, NULL); if (!common || !notify || !isr_status) { console_println("virtio-input: cap walk failed"); return -2; } s->common = common; s->notify = notify; s->notify_off_mult = notify_mult; s->isr_status = isr_status; common->device_status = 0; wmb(); common->device_status = (uint8_t)VIRTIO_STATUS_ACKNOWLEDGE; wmb(); common->device_status = (uint8_t)(VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER); wmb(); common->driver_feature_select = 1; wmb(); common->driver_feature = (uint32_t)(VIRTIO_F_VERSION_1 >> 32); common->driver_feature_select = 0; wmb(); common->driver_feature = 0; wmb(); common->device_status = (uint8_t)(VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER | VIRTIO_STATUS_FEATURES_OK); wmb(); if (!(common->device_status & (uint8_t)VIRTIO_STATUS_FEATURES_OK)) { console_println("virtio-input: FEATURES_OK rejected"); common->device_status = (uint8_t)VIRTIO_STATUS_FAILED; return -2; } /* Queue 0 = eventq (device-to-driver key events). Queue 1 = statusq * (driver-to-device, e.g. LED state) -- not needed to read keypresses; * left unconfigured, consistent with this item's scope (input, not * output reports). Read num_queues back rather than assume 2. */ if (common->num_queues < 1u) { console_println("virtio-input: no queues reported"); return -2; } common->queue_select = 0; wmb(); uint16_t max_size = common->queue_size; if (max_size == 0u) { console_println("virtio-input: bad eventq size"); return -2; } uint16_t qsize = (max_size < (uint16_t)EVENTQ_SIZE) ? max_size : (uint16_t)EVENTQ_SIZE; common->queue_size = qsize; s->queue_notify_off = common->queue_notify_off; wmb(); common->config_msix_vector = 0xFFFFu; common->queue_msix_vector = 0xFFFFu; wmb(); size_t desc_bytes = (size_t)qsize * sizeof(VirtqDesc); size_t avail_bytes = sizeof(uint16_t) * 2u + (size_t)qsize * sizeof(uint16_t) + sizeof(uint16_t); size_t used_bytes = sizeof(uint16_t) * 2u + (size_t)qsize * sizeof(VirtqUsedElem) + sizeof(uint16_t); s->desc = (VirtqDesc *) kmalloc_aligned(desc_bytes, 64); s->avail = (VirtqAvail *)kmalloc_aligned(avail_bytes, 2); s->used = (VirtqUsed *) kmalloc_aligned(used_bytes, 4); s->bufs = (VirtioInputEvent *) kmalloc_aligned((size_t)qsize * sizeof(VirtioInputEvent), 8); if (!s->desc || !s->avail || !s->used || !s->bufs) { console_println("virtio-input: alloc failed"); return -2; } memset(s->desc, 0, desc_bytes); memset(s->avail, 0, avail_bytes); memset(s->used, 0, used_bytes); memset(s->bufs, 0, (size_t)qsize * sizeof(VirtioInputEvent)); s->avail_idx = 0; s->last_used_idx = 0; common->queue_desc = (uint64_t)(uintptr_t)s->desc; common->queue_driver = (uint64_t)(uintptr_t)s->avail; common->queue_device = (uint64_t)(uintptr_t)s->used; wmb(); common->queue_enable = 1; wmb(); /* Pre-post every buffer, device-writable, single descriptor each * (events are fixed 8-byte structs, no chaining needed) -- unlike * vblk_io()'s per-request chain-of-three, this posts the whole queue * up front and never waits synchronously; the device fills them * asynchronously as real input events arrive. */ uint16_t i; for (i = 0; i < qsize; i++) { s->desc[i].addr = (uint64_t)(uintptr_t)&s->bufs[i]; s->desc[i].len = (uint32_t)sizeof(VirtioInputEvent); s->desc[i].flags = (uint16_t)VRING_DESC_F_WRITE; s->desc[i].next = 0; s->avail->ring[i] = i; } wmb(); s->avail->idx = qsize; s->avail_idx = qsize; wmb(); uint16_t notify_idx = (uint16_t)(s->queue_notify_off * (s->notify_off_mult & 0xFFFFu)); volatile uint16_t *doorbell = s->notify + notify_idx; *doorbell = 0; wmb(); if (enable_interrupt_route(pci) != 0) { console_println("virtio-input: interrupt route unavailable, device idle"); return -2; } common->device_status = (uint8_t)(VIRTIO_STATUS_ACKNOWLEDGE | VIRTIO_STATUS_DRIVER | VIRTIO_STATUS_FEATURES_OK | VIRTIO_STATUS_DRIVER_OK); wmb(); return 0; } int virtio_input_find_keyboard(void) { PciDevice pci; int found = pci_find_first(VIRTIO_INPUT_PCI_VENDOR_ID, VIRTIO_INPUT_PCI_DEVICE_ID, &pci); if (found != 0) { console_println("virtio-input: no device on PCI bus 0"); return -1; } console_println("virtio-input: found device"); int rc = vinput_init_device(&pci); if (rc != 0) return rc; g_vinput_ready = 1; console_println("virtio-input: driver ready"); return 0; } /* ------------------------------------------------------------------------- * Interrupt path -- called from arch/riscv64/interrupts.c on a matching * PLIC claim (g_virtio_input_plic_source). * ------------------------------------------------------------------------- */ void virtio_input_isr(void) { VirtInputState *s = &g_vinput; if (!g_vinput_ready) return; g_virtio_input_isr_count++; /* Mandatory: reading ISR status is what deasserts the (level-triggered, * per FABRIC.md §27.5.2's decoded interrupt-map flags) line. Skipping * this leaves the PLIC source's condition latched -- storm or hang, * not a subtle bug (same finding 4.3.5e's plan flags). */ (void)*s->isr_status; while (s->used->idx != s->last_used_idx) { uint16_t used_slot = s->last_used_idx % EVENTQ_SIZE; uint32_t desc_id = s->used->ring[used_slot].id; VirtioInputEvent *ev = &s->bufs[desc_id]; if (ev->type == (uint16_t)VIRTIO_INPUT_EV_KEY) { diag_push(ev->code, ev->value); } /* EV_SYN and other event types are received but not surfaced to * the diagnostic ring -- this item decodes EV_KEY specifically, * per its own scope; a full input-event model is later/REPL work. */ /* Re-post this buffer immediately -- same descriptor, still * device-writable, not re-initialised (already zeroed content * doesn't matter, the device overwrites it in full each time). */ uint16_t avail_slot = s->avail_idx % EVENTQ_SIZE; s->avail->ring[avail_slot] = (uint16_t)desc_id; wmb(); s->avail_idx++; s->avail->idx = s->avail_idx; wmb(); s->last_used_idx++; } uint16_t notify_idx = (uint16_t)(s->queue_notify_off * (s->notify_off_mult & 0xFFFFu)); volatile uint16_t *doorbell = s->notify + notify_idx; *doorbell = 0; wmb(); }