Reorganize source tree: kernel/, v3/, v4/ split and board infrastructure

Source tree reorganization:
- Move StarForth v3 engine to v3/ (src/, include/, Makefile)
- Move kernel to kernel/ (src/, include/, linker/, Makefile)
- Create v4/ skeleton for F18-ISA golden model (DECOMPOSITION.md, JUSTIFICATION.md)
- Move FABRIC-0..4.md to docs/fabric/
- Move ONTOLOGY.md and ROADMAP.md to docs/

Board infrastructure:
- Add boards/ser5/, boards/raspi/, boards/milkv/, boards/zynq7020/
- Each board has board.mk (ISA, CPU flags, boot recipe) and README.md
- Root Makefile becomes thin dispatcher: boot_image, all, clean, docs take TARGET
- make boot_image TARGET=SER5|RASPI|MILKV builds one GPT/MBR image per board
- ZYNQ7020 target exists but stops with clear error (ARMv7 port not built yet)
- scripts/mkdiskimage.sh builds disk images for all boards

Docs pipeline:
- docs/book/ with LaTeX master (main.tex) and Makefile
- pandoc converts Markdown to LaTeX at build time
- Two Lua filters: table-widths.lua (wide tables wrap), code-breaks.lua (inline code breaks)
- make docs builds single PDF (754 pages, 0 missing characters)
- make docs TARGET=<board> adds board appendix
- build/docs/<book|board>/meta.tex stamps git commit into PDF

Bug fixes:
- 42 include paths that only worked by accident now use correct relative paths
- clang-18 hardcode replaced with configurable CC variable (fixed aarch64 build)
- Pi 5: kernel_2712.img linked at 0x80000, .bss zeroed, memory reserved
- Doxyfile, .clang-tidy, README.md, Kconfig paths updated

Verified:
- Hosted v3 build passes 1012 tests, 0 failures
- SER5 image boots in QEMU (OVMF), POST passes, K exact (65536 = Q48_ONE)
- Milk-V image boots in QEMU (OpenSBI + U-Boot + bootefi), POST passes
- make clean TARGET=<board> removes only that board and its ISA objects
- make all builds all boards, hosted v3, and docs in one run

Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
rajames
2026-10-01 15:40:09 -04:00
co-authored by Junie
parent 3c709c115b
commit a8b70e88d3
630 changed files with 2646 additions and 2036 deletions
+553
View File
@@ -0,0 +1,553 @@
/*
* virtio_input.c — Virtio 1.0 input device driver for StarKernel
* (riscv64: item 4.3.5c; aarch64: item 4.3.5e)
*
* 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
* (riscv64) or GIC (aarch64) -- see virtio_input_isr(), called from
* arch/riscv64/interrupts.c or arch/aarch64/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 item 4.3.5c rather than refactoring the working, tested
* virtio_blk.c, carried forward here.
*
* Interrupt routing (source computation/enable) is arch-guarded below,
* riscv64 (PLIC) and aarch64 (GIC) each with their own slot/pin-to-source
* formula (FABRIC-0.md §27.5.1 aarch64, §27.5.2 riscv64, both derived live
* from this host's own QEMU DTB, not assumed identical to each other). 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 <stddef.h>
#include <stdint.h>
#include <string.h>
#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
#if defined(ARCH_AARCH64)
/* Defined in arch/aarch64/apic.c (item 4.3.5d). Extern-in-place, same
* convention that file's own cross-file declarations already use. */
extern void apic_spi_enable(uint32_t intid);
#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;
/* GIC INTID (aarch64) this device's INTx routes to, computed at init. 0 is
* not a valid SPI INTID (SPIs start at 32), so it is likewise a safe
* "invalid/not found" sentinel. Checked by arch/aarch64/interrupts.c's
* dispatch. */
uint32_t g_virtio_input_gic_intid = 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-0.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, item 4.3.5c) and aarch64 (GIC, item
* 4.3.5e). Both compute their target source from the same PCI slot/pin
* swizzle shape (§27.5.1 aarch64, §27.5.2 riscv64) but with different base
* offsets and controller calls -- derived independently from each board's
* own DTB, not assumed identical. The #else path (amd64, which never
* attaches this device) 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;
}
#elif defined(ARCH_AARCH64)
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 spi = 3u + ((slot + (uint32_t)pin - 1u) % 4u);
uint32_t intid = 32u + spi;
apic_spi_enable(intid);
g_virtio_input_gic_intid = intid;
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-0.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, &notify_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) or arch/aarch64/interrupts.c on
* a matching GIC IRQ (g_virtio_input_gic_intid).
* ------------------------------------------------------------------------- */
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-0.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();
}