From 50af5d18aa045814c84c30bcade0d19870e29792 Mon Sep 17 00:00:00 2001 From: Sol Astrius Date: Sun, 3 May 2026 17:47:32 +0300 Subject: [PATCH 1/3] parport-pci: NetMos MCS9900 PCI parallel port with IEEE 1284 reverse Signed-off-by: Sol Astrius --- src/devices/parport-pci.c | 578 ++++++++++++++++++++++++++++++++++++++ src/devices/parport-pci.h | 63 +++++ 2 files changed, 641 insertions(+) create mode 100644 src/devices/parport-pci.c create mode 100644 src/devices/parport-pci.h diff --git a/src/devices/parport-pci.c b/src/devices/parport-pci.c new file mode 100644 index 000000000..89c146c63 --- /dev/null +++ b/src/devices/parport-pci.c @@ -0,0 +1,578 @@ +/* +parport-pci.c - PCI Parallel Port (NetMos/MosChip MCS9900) +Copyright (C) 2026 Sol Astrius + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ + +#include "parport-pci.h" +#include "compiler.h" +#include "mem_ops.h" +#include "spinlock.h" +#include "utils.h" + +PUSH_OPTIMIZATION_SIZE + +// NetMos / MosChip MCS9900 PCI parallel port (single SPP port + +// IEEE 1284 nibble-mode reverse channel). +// +// PCI identity (ASIX MCS9900 datasheet, Rev 2.00, §6.2 "PCIe +// Configuration Space", the 2S+1P parallel function): vendor 0x9710, +// device 0x9900, class 0x0701 (Communication / Parallel), subsystem +// 0xA000:0x2000. BAR 0 holds the SPP register window. We expose a single +// SPP port (prog_if 0x00); the silicon reports prog_if 0x03 (IEEE 1284 +// controller), but SPP is sufficient for a host driver to bind and use +// the nibble-mode reverse channel below. +// +// Register map (8 bytes at BAR 0), the standard PC parallel-port SPP +// layout (IEEE 1284-1994 §7.3, Compatibility Mode). Byte-addressable. +// +0x00 Data (RW) D0..D7, drives pins 2-9 of the DB-25 connector +// In nibble-reverse mode reads return last write +// (loopback latch); peripheral data flows via +// Status[3,4,5,7] per IEEE 1284 §6.3 (Nibble Mode). +// +0x01 Status (RO) bit 7 nBusy (1 = idle / data bit 3 inverted) +// bit 6 nAck (1 = idle, 0 = data acknowledged) +// bit 5 PaperOut(active high — also "PError" in 1284) +// bit 4 Select (1 = peripheral online, also xflag) +// bit 3 nError (1 = no error / end-of-data marker +// between bytes in nibble mode) +// bit 2 IRQ (set on nAck rising edge while IRQ +// enabled; read-clear) +// bits 1:0 reserved +// +0x02 Control (RW) SPP control register, in the host driver's logical +// view (the chip-to-wire inversion on bits 0/1/3 is +// invisible to software). So: +// bit 0 STROBE (1 = strobe asserted, wire low) +// bit 1 AUTOFD (1 = autofd asserted, "HostBusy" +// in IEEE 1284 negotiation) +// bit 2 INIT (1 = peripheral not in reset) +// bit 3 SELIN (1 = peripheral selected for +// compat printing; 0 during 1284 +// "active" — host deasserts to +// enter negotiation) +// bit 4 IRQ enable +// bit 5 BIDIR (data direction; 1 = input) +// bits 7:6 unused +// +0x03..0x07 EPP/ECP registers; SPP-only emulation returns 0. +// +// Forward path (PHASE_FWD_IDLE): +// Standard Centronics (IEEE 1284 §7.3). Guest writes data to +0x00, +// then writes Control with nStrobe asserted (chip bit 0 = 0) and back. +// We trigger the backend on the chip bit 0 rising edge (deassert), +// which is the wire-falling-edge that latches the byte at the +// peripheral. +// +// Reverse path (negotiate to nibble mode, then read; IEEE 1284 §7.4 +// negotiation, §6.3 nibble mode): +// The host writes the extensibility request 0x00 to Data (nibble mode, +// event 0), then drives Control through the negotiation events. We +// respond on each control transition by computing a new Status value +// and (if Control bit 4 IRQ-enable is set) firing an INTx edge on nAck +// rising. After negotiation the port is in PHASE_REV_IDLE and the host +// pulses nAutoFd to clock each nibble out via Status bits 3/4/5/7 +// (IEEE 1284 Figures 7-8). +// +// IRQ delivery uses pci_send_irq (edge), since real parport interrupts +// are edge-triggered on nAck (IEEE 1284 §7.3) and the host dismisses by +// reading Status, not via any explicit register write. + +#define PARPORT_PCI_VENDOR_ID 0x9710 // NetMos / MosChip +#define PARPORT_PCI_DEVICE_ID 0x9900 +#define PARPORT_PCI_CLASS 0x0701 // Communication / Parallel +#define PARPORT_PCI_PROG_IF 0x00 // SPP + +// Status idle byte: nBusy=1, nAck=1, PaperOut=0, Select=1, nError=1. +// Returned in PHASE_FWD_IDLE — peripheral always reports idle so lp's +// status polling never sees a busy/error condition. +#define PARPORT_STATUS_IDLE 0xD8u + +// Control reset value 0x0C is the SPP selected/idle state: bit 2 INIT=1 +// (peripheral active / not in reset), bit 3 SELIN=1 (peripheral selected +// for compat printing — wire low, active-low signal asserted). Strobe and +// AutoFd cleared (wires high, idle). +#define PARPORT_CONTROL_RESET 0x0Cu + +// Control register bits (SPP logical view). +#define CTL_STROBE (1u << 0) // 1 = strobe asserted +#define CTL_AUTOFD (1u << 1) // 1 = autofd asserted (HostBusy) +#define CTL_INIT (1u << 2) +#define CTL_SELIN (1u << 3) // 1 = compat select; 0 = 1284 active +#define CTL_IRQ_EN (1u << 4) +#define CTL_BIDIR (1u << 5) + +// Status register bits. +#define ST_ERROR (1u << 3) // nError; 1 = no error / no data +#define ST_SELECT (1u << 4) +#define ST_PAPEROUT (1u << 5) +#define ST_IRQ (1u << 2) // latched IRQ flag, read-clear +#define ST_ACK (1u << 6) // 1 = idle +#define ST_BUSY (1u << 7) // 1 = idle (active low) + +// IEEE 1284 phase. Event numbers below are IEEE 1284-1994's own +// (§7.4 negotiation, Figures 7-8; §6.3 nibble mode). +typedef enum { + PHASE_FWD_IDLE = 0, // Compat mode; Centronics writes work + PHASE_NEGOT_REPLY, // Host issued event 1; we drove event 2 reply + PHASE_REV_IDLE, // Negotiated to nibble; awaiting event 7 + PHASE_REV_LO_DAV, // First nibble on Status, nAck=0 (event 9) + PHASE_REV_LO_DONE, // First nibble done, nAck=1 (event 11) + PHASE_REV_HI_DAV, // Second nibble on Status, nAck=0 + PHASE_REV_HI_DONE, // Second nibble done, nAck=1 (transient) +} parport_phase_t; + +#define PARPORT_RING_SIZE 256u +#define PARPORT_RING_MASK (PARPORT_RING_SIZE - 1u) + +typedef struct { + pci_func_t* pci_func; + spinlock_t lock; + + uint8_t data; + uint8_t control; + + // Reverse-channel state (1284 negotiation + nibble delivery) + parport_phase_t phase; + uint8_t cur_byte; // Byte being split into nibbles + bool cur_byte_valid; + bool irq_pending; // ST_IRQ latch (cleared on Status read) + + // Input ring (host → guest). Single producer / single consumer; the + // producer is whatever thread calls parport_pci_inject_byte (see + // -parport_in path in main.c), the consumer is the MMIO write + // handler. Lock-protected — the lock is also held during status + // reads so we don't need separate atomics. + uint8_t ring[PARPORT_RING_SIZE]; + uint16_t ring_head; // producer writes here + uint16_t ring_tail; // consumer reads here + + // Backend + parport_pci_write_fn write_fn; + void* user_data; +} parport_pci_dev_t; + +static bool ring_empty(const parport_pci_dev_t* pp) +{ + return pp->ring_head == pp->ring_tail; +} + +static bool ring_full(const parport_pci_dev_t* pp) +{ + return (uint16_t)(pp->ring_head - pp->ring_tail) >= PARPORT_RING_SIZE; +} + +static bool ring_pop(parport_pci_dev_t* pp, uint8_t* out) +{ + if (ring_empty(pp)) return false; + *out = pp->ring[pp->ring_tail & PARPORT_RING_MASK]; + pp->ring_tail++; + return true; +} + +// Encode a 4-bit nibble into Status[3,4,5,7] per IEEE 1284 §6.3 nibble +// mode, where the four reverse data bits travel on the status lines: +// data bit 0 → nFault (ST_ERROR), bit 1 → Select (ST_SELECT), bit 2 → +// PError (ST_PAPEROUT), bit 3 → Busy. Busy is active-low (nBusy), so a +// set data bit 3 clears ST_BUSY. +static uint8_t encode_nibble_status_bits(uint8_t nibble) +{ + uint8_t s = 0; + if (nibble & 0x1) s |= ST_ERROR; + if (nibble & 0x2) s |= ST_SELECT; + if (nibble & 0x4) s |= ST_PAPEROUT; + if (!(nibble & 0x8)) s |= ST_BUSY; // bit 3 inverted: 1 = clear nBusy + return s; +} + +// Compute the Status register value from current device state. +// Pure function of (phase, cur_byte, ring state, irq_pending). +static uint8_t compute_status(const parport_pci_dev_t* pp) +{ + uint8_t s; + switch (pp->phase) { + case PHASE_FWD_IDLE: + // Steady-state idle. nFault=1 (no error), Select=1 (online), + // PaperOut=0 (paper present), nAck=1, nBusy=1 (idle). + s = ST_ERROR | ST_SELECT | ST_BUSY | ST_ACK; + break; + + case PHASE_NEGOT_REPLY: + // Event 2 reply. nFault=1, Select=1, PaperOut=1 (PError + // asserted), nAck=0 (asserted — peripheral is responding), + // nBusy=1. + s = ST_ERROR | ST_SELECT | ST_PAPEROUT | ST_BUSY; + break; + + case PHASE_REV_IDLE: { + // Between bytes. Set nError based on whether we have more + // data: the host checks the nFault/Error status bit at the + // start of each byte and treats set = end-of-data. So: + // ring empty AND no cached byte → ST_ERROR set (end) + // data available → ST_ERROR clear + // nAck=1 (idle), Select=1 (mode supported, xflag accept). + bool has_data = !ring_empty(pp) || pp->cur_byte_valid; + s = ST_SELECT | ST_BUSY | ST_ACK; + if (!has_data) s |= ST_ERROR; + break; + } + + case PHASE_REV_LO_DAV: + // First nibble on data lines, nAck=0 (event 9). + s = encode_nibble_status_bits(pp->cur_byte & 0xF); + break; + + case PHASE_REV_LO_DONE: + // First nibble still latched, nAck=1 (event 11). + s = encode_nibble_status_bits(pp->cur_byte & 0xF) | ST_ACK; + break; + + case PHASE_REV_HI_DAV: + s = encode_nibble_status_bits((pp->cur_byte >> 4) & 0xF); + break; + + case PHASE_REV_HI_DONE: + s = encode_nibble_status_bits((pp->cur_byte >> 4) & 0xF) | ST_ACK; + break; + + default: + s = PARPORT_STATUS_IDLE; + break; + } + + if (pp->irq_pending) s |= ST_IRQ; + return s; +} + +// Apply state-machine transitions on Control writes. Returns true if an +// IRQ should be raised after dropping the device lock (caller observes +// pp->irq_pending and pp->control to gate). Also returns the byte to +// send forward (via *fwd_byte) when the Centronics strobe trips. +static bool handle_control_transition(parport_pci_dev_t* pp, + uint8_t prev, uint8_t next, + bool* fwd_strobe, uint8_t* fwd_byte) +{ + *fwd_strobe = false; + bool ack_was_high = !!(compute_status(pp) & ST_ACK); + + // PHASE_FWD_IDLE: detect Centronics strobe (CTL_STROBE 0→1) AND + // 1284 negotiation request (event 1: AUTOFD asserts, SELIN + // deasserts — the host drives these together). + if (pp->phase == PHASE_FWD_IDLE) { + if (!(prev & CTL_STROBE) && (next & CTL_STROBE)) { + *fwd_strobe = true; + *fwd_byte = pp->data; + } + bool autofd_assert = !(prev & CTL_AUTOFD) && (next & CTL_AUTOFD); + bool selin_deassert = (prev & CTL_SELIN) && !(next & CTL_SELIN); + if (autofd_assert && selin_deassert) { + // Mode byte already in pp->data. Accept nibble (0x00); for + // anything else we stay in FWD_IDLE and the host sees no + // event-2 reply (status never matches), times out, and + // negotiation fails (not 1284 compliant). The host sends the + // extensibility request 0x00 for nibble mode (IEEE 1284 + // §7.4, event 0). + if (pp->data == 0x00) { + pp->phase = PHASE_NEGOT_REPLY; + } + } + } else { + // In any non-FWD phase, the host re-asserting SELIN (chip 0→1) + // is termination (IEEE 1284 §7.4, Figure 7; events 22-29). + // Drop back to forward idle; the host's follow-up events 24/27/29 + // just poll nAck, which our FWD_IDLE status drives high. + if (!(prev & CTL_SELIN) && (next & CTL_SELIN)) { + pp->phase = PHASE_FWD_IDLE; + pp->cur_byte_valid = false; + return false; + } + } + + // AUTOFD edges drive the phase machine in 1284 modes. "Assert" = + // chip bit 0→1 (HostBusy on wire); "release" = chip bit 1→0. + bool autofd_assert = !(prev & CTL_AUTOFD) && (next & CTL_AUTOFD); + bool autofd_release = (prev & CTL_AUTOFD) && !(next & CTL_AUTOFD); + + switch (pp->phase) { + case PHASE_NEGOT_REPLY: + // Event 4 second half: AUTOFD released. Move to REV_IDLE + // and drive nAck high (event 6). The intermediate STROBE + // pulse (events 3-4 first half) is harmless — we just don't + // act on it during negotiation. + if (autofd_release) { + pp->phase = PHASE_REV_IDLE; + } + break; + + case PHASE_REV_IDLE: + // Event 7 of first nibble: load next byte and present. + if (autofd_assert) { + if (!pp->cur_byte_valid) { + if (!ring_pop(pp, &pp->cur_byte)) { + // No data — leave phase unchanged. The host will + // see nibble bits stuck and time out at event 9. + // Normally the host checks ST_ERROR in REV_IDLE + // first and bails before reaching here. + break; + } + pp->cur_byte_valid = true; + } + pp->phase = PHASE_REV_LO_DAV; + } + break; + + case PHASE_REV_LO_DAV: + if (autofd_release) pp->phase = PHASE_REV_LO_DONE; + break; + + case PHASE_REV_LO_DONE: + if (autofd_assert) pp->phase = PHASE_REV_HI_DAV; + break; + + case PHASE_REV_HI_DAV: + if (autofd_release) { + // Byte fully transferred. Consume it and return to idle; + // ring state determines whether nFault signals more data + // or end-of-data on the next status read. + pp->cur_byte_valid = false; + pp->phase = PHASE_REV_IDLE; + } + break; + + case PHASE_REV_HI_DONE: + case PHASE_FWD_IDLE: + default: + break; + } + + // Detect nAck rising edge across the transition for IRQ delivery. + // We could just snapshot the status before/after; the cheaper test + // is "was low before, is high now" and gate on Control IRQ enable. + bool ack_now_high = !!(compute_status(pp) & ST_ACK); + if (!ack_was_high && ack_now_high && (next & CTL_IRQ_EN)) { + pp->irq_pending = true; + return true; + } + return false; +} + +// pci_dev_t doesn't expose the BAR private-data pointer back to the +// caller, so we keep a small registration map (pci_dev_t* → device +// state) and look up at inject time. The map is sized for the handful +// of parports a single machine could plausibly have. +#define PARPORT_MAX_INSTANCES 4 +static parport_pci_dev_t* g_parport_instances[PARPORT_MAX_INSTANCES]; +static pci_dev_t* g_parport_pci_dev[PARPORT_MAX_INSTANCES]; +static spinlock_t g_parport_instances_lock; + +static void parport_pci_remove(rvvm_mmio_dev_t* dev) +{ + parport_pci_dev_t* pp = dev->data; + // Drop the registration so a later inject — or a reused pci_dev_t + // pointer at the same address — can't resolve to this freed instance. + spin_lock(&g_parport_instances_lock); + for (size_t i = 0; i < PARPORT_MAX_INSTANCES; i++) { + if (g_parport_instances[i] == pp) { + g_parport_instances[i] = NULL; + g_parport_pci_dev[i] = NULL; + break; + } + } + spin_unlock(&g_parport_instances_lock); + free(pp); +} + +static rvvm_mmio_type_t parport_pci_type = { + .name = "netmos_9900_parport", + .remove = parport_pci_remove, +}; + +static bool parport_pci_mmio_read(rvvm_mmio_dev_t* dev, void* data, size_t off, uint8_t size) +{ + UNUSED(size); + parport_pci_dev_t* pp = dev->data; + spin_lock(&pp->lock); + uint8_t val = 0; + bool ok = true; + switch (off) { + case 0x00: + // Data register read. In nibble-mode reverse the actual + // peripheral byte is delivered via Status bits, not Data; + // returning the last write is the standard SPP loopback + // behavior the host expects. + val = pp->data; + break; + case 0x01: + val = compute_status(pp); + // ST_IRQ is read-clear (cleared when Status is read). + pp->irq_pending = false; + break; + case 0x02: + val = pp->control; + break; + case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: + val = 0; + break; + default: + ok = false; + break; + } + spin_unlock(&pp->lock); + if (ok) write_uint8(data, val); + return ok; +} + +static bool parport_pci_mmio_write(rvvm_mmio_dev_t* dev, void* data, size_t off, uint8_t size) +{ + UNUSED(size); + parport_pci_dev_t* pp = dev->data; + uint8_t byte = read_uint8(data); + + parport_pci_write_fn fn = NULL; + void* ud = NULL; + uint8_t out = 0; + bool fwd = false; + bool raise_irq = false; + + spin_lock(&pp->lock); + bool ok = true; + switch (off) { + case 0x00: + pp->data = byte; + break; + case 0x02: { + uint8_t prev = pp->control; + pp->control = byte; + raise_irq = handle_control_transition(pp, prev, byte, &fwd, &out); + if (fwd && pp->phase == PHASE_FWD_IDLE) { + fn = pp->write_fn; + ud = pp->user_data; + } else { + fwd = false; + } + break; + } + case 0x03: case 0x04: case 0x05: case 0x06: case 0x07: + break; + default: + ok = false; + break; + } + pci_func_t* pci_func = pp->pci_func; + spin_unlock(&pp->lock); + + if (ok && fwd && fn) { + fn(ud, out); + } + if (raise_irq && pci_func) { + pci_send_irq(pci_func, 0); + } + return ok; +} + +static void register_instance(pci_dev_t* dev, parport_pci_dev_t* pp) +{ + spin_lock(&g_parport_instances_lock); + for (size_t i = 0; i < PARPORT_MAX_INSTANCES; i++) { + if (g_parport_instances[i] == NULL) { + g_parport_instances[i] = pp; + g_parport_pci_dev[i] = dev; + break; + } + } + spin_unlock(&g_parport_instances_lock); +} + +static parport_pci_dev_t* lookup_instance(pci_dev_t* dev) +{ + parport_pci_dev_t* found = NULL; + spin_lock(&g_parport_instances_lock); + for (size_t i = 0; i < PARPORT_MAX_INSTANCES; i++) { + if (g_parport_pci_dev[i] == dev) { + found = g_parport_instances[i]; + break; + } + } + spin_unlock(&g_parport_instances_lock); + return found; +} + +PUBLIC bool parport_pci_inject_byte(pci_dev_t* dev, uint8_t byte) +{ + parport_pci_dev_t* pp = lookup_instance(dev); + if (!pp) return false; + + bool ok = false; + bool wake_irq = false; + spin_lock(&pp->lock); + if (!ring_full(pp)) { + pp->ring[pp->ring_head & PARPORT_RING_MASK] = byte; + pp->ring_head++; + ok = true; + // If the guest is already in PHASE_REV_IDLE waiting for data, + // an IRQ would be nice — but parport_wait_peripheral here is + // polling for a *Status* change (nFault going low), not an + // edge-triggered ACK event. The host re-reads Status on the + // next poll cycle and sees ST_ERROR clear, so no IRQ needed + // for correctness. We could fire one to shorten the 10ms slow + // poll latency, but only when IRQ enable is set and the host + // has a pending wait — neither of which we can detect from + // here. Skip it. + UNUSED(wake_irq); + } + spin_unlock(&pp->lock); + return ok; +} + +PUBLIC pci_dev_t* parport_pci_init(pci_bus_t* pci_bus, + parport_pci_write_fn write_fn, + void* user_data) +{ + parport_pci_dev_t* pp = safe_new_obj(parport_pci_dev_t); + pp->control = PARPORT_CONTROL_RESET; + pp->phase = PHASE_FWD_IDLE; + pp->write_fn = write_fn; + pp->user_data = user_data; + + pci_func_desc_t desc = { + .vendor_id = PARPORT_PCI_VENDOR_ID, + .device_id = PARPORT_PCI_DEVICE_ID, + .class_code = PARPORT_PCI_CLASS, + .prog_if = PARPORT_PCI_PROG_IF, + // Subsystem Vendor/Device ID the MCS9900 2S+1P parallel function + // reports (datasheet §6.2; PCI Local Bus Spec 3.0 §6.2.4). Host + // drivers that strict-match on subsystem ID need these to bind. + .subsys_vendor_id = 0xA000, + .subsys_device_id = 0x2000, + // BAR 0 is an I/O-port BAR (PCI Local Bus Spec 3.0 §6.2.5.1). The + // guest reaches it via its PCI resource + inb/outb, which on + // RISC-V routes through the PCI bridge's I/O range in the DT. + .bar_io_mask = 0x01, + .irq_pin = PCI_IRQ_PIN_INTA, + .bar[0] = { + .size = 8, + .min_op_size = 1, + .max_op_size = 4, + .read = parport_pci_mmio_read, + .write = parport_pci_mmio_write, + .data = pp, + .type = &parport_pci_type, + }, + }; + + pci_dev_t* dev = pci_attach_func(pci_bus, &desc); + if (dev) { + pp->pci_func = pci_get_device_func(dev, 0); + register_instance(dev, pp); + } + return dev; +} + +PUBLIC pci_dev_t* parport_pci_init_auto(rvvm_machine_t* machine, + parport_pci_write_fn write_fn, + void* user_data) +{ + return parport_pci_init(rvvm_get_pci_bus(machine), write_fn, user_data); +} + +POP_OPTIMIZATION_SIZE diff --git a/src/devices/parport-pci.h b/src/devices/parport-pci.h new file mode 100644 index 000000000..9cdf7a322 --- /dev/null +++ b/src/devices/parport-pci.h @@ -0,0 +1,63 @@ +/* +parport-pci.h - PCI Parallel Port (NetMos/MosChip MCS9900) +Copyright (C) 2026 Sol Astrius + +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at https://mozilla.org/MPL/2.0/. +*/ + +#ifndef PARPORT_PCI_H +#define PARPORT_PCI_H + +#include "rvvmlib.h" +#include "pci-bus.h" + +/* + * Backend callbacks for the emulated NetMos 9900 PCI parallel port. + * + * Forward path (guest → host): + * write_fn fires each time the guest pulses the Centronics strobe in + * forward (compat) mode. Called from the MMIO write path with no + * device lock held — the backend may block briefly on a host file or + * pipe write. + * + * Reverse path (host → guest): + * The host pushes bytes into the device via parport_pci_inject_byte(). + * Bytes queue in a small ring; the IEEE 1284 nibble-mode reverse + * handshake delivers them to the guest as it pulls. The guest sees + * nFault deasserted (the Error status bit set) when the ring is empty, + * which an IEEE 1284 host recognizes as "no more data". + */ +typedef void (*parport_pci_write_fn)(void *user_data, uint8_t byte); + +/* + * Attach a NetMos 9900 PCI parport to the given PCI bus. Single port, + * SPP forward + IEEE 1284 nibble-mode reverse. A host parallel-port + * driver picks it up via PCI ID match (vendor 0x9710 device 0x9900) and + * exposes the usual parport / lp / ppdev nodes (negotiating to nibble + * mode enables bidirectional reads). EPP/ECP register windows return 0. + * + * write_fn / user_data: optional. If NULL, forward bytes are silently + * dropped after the Centronics strobe. Useful for headless testing. + */ +PUBLIC pci_dev_t* parport_pci_init(pci_bus_t* pci_bus, + parport_pci_write_fn write_fn, + void* user_data); + +PUBLIC pci_dev_t* parport_pci_init_auto(rvvm_machine_t* machine, + parport_pci_write_fn write_fn, + void* user_data); + +/* + * Inject a byte from the host into the parport's reverse-channel ring. + * Returns true on success, false if the ring is full (caller should + * back off and retry). The ring holds 256 bytes; under typical 1284 + * nibble timing the guest drains roughly 50 KB/s, so a backend that + * paces its writes against this rate is fine without flow control. + * + * Safe to call from any thread; takes the device's spinlock briefly. + */ +PUBLIC bool parport_pci_inject_byte(pci_dev_t* dev, uint8_t byte); + +#endif From 64a57c959ed0ed5a879639a228cf33e69b79f5c6 Mon Sep 17 00:00:00 2001 From: Sol Astrius Date: Sun, 3 May 2026 17:47:41 +0300 Subject: [PATCH 2/3] main: wire -parport CLI flag (out=/in= backend spec) Signed-off-by: Sol Astrius --- src/main.c | 112 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/src/main.c b/src/main.c index fbdc83e09..7279ec601 100644 --- a/src/main.c +++ b/src/main.c @@ -25,8 +25,11 @@ along with this program. If not, see . #include +#include #include +#include // parport sink/source use FILE* on all targets + #include #include #include @@ -37,6 +40,7 @@ along with this program. If not, see . #include #include #include +#include #include #include #include @@ -199,6 +203,9 @@ static void rvvm_print_help(void) " -ata ... Explicitly attach storage image as ATA (IDE) device\n" " -nogui Disable display GUI\n" " -nosound Disable sound support\n" + " -parport ... Attach a parallel port (NetMos 9900): out= sinks\n" + " guest output, in= sources the reverse channel,\n" + " e.g. out=lp0.txt,in=rev.fifo; bare path = out; or null\n" " -nonet Disable networking\n" " -serial ... Add more serial ports (Via pty/pipe path), or null\n" " -dtb ... Pass custom Device Tree Blob to the machine\n" @@ -212,6 +219,58 @@ static void rvvm_print_help(void) print_stderr(help); } +// Extract a "=" field from a -parport spec ("out=a,in=b"). +// Returns a malloc'd value (caller frees), or NULL if the key is absent. +static char* parport_spec_field(const char* spec, const char* key) +{ + const char* p = rvvm_strfind(spec, key); + if (p == NULL) { + return NULL; + } + const char* val = p + rvvm_strlen(key); + const char* end = rvvm_strfind(val, ","); + size_t len = end ? (size_t)(end - val) : rvvm_strlen(val); + char* out = safe_new_arr(char, len + 1); + rvvm_strlcpy(out, val, len + 1); + return out; +} + +// Bridge for the -parport output sink: each byte the guest strobes out of +// its parallel port is appended to a host file. user_data carries the FILE* +// opened in rvvm_cli_main. Called outside the device's lock so the write +// may safely block briefly. +static void parport_main_write_fn(void* user_data, uint8_t byte) +{ + FILE* fp = (FILE*)user_data; + if (fp) fputc(byte, fp); +} + +// Reader thread for the -parport in= field: blocks reading from a host +// file or fifo and pushes each byte into the device's reverse-channel ring. +// EOF / error ends the thread, leaving any further guest reads to see +// nFault asserted (end-of-data) on Status. +typedef struct { + pci_dev_t* dev; + FILE* fp; +} parport_in_ctx_t; + +static void* parport_in_thread(void* arg) +{ + parport_in_ctx_t* ctx = arg; + uint8_t byte; + while (fread(&byte, 1, 1, ctx->fp) == 1) { + // Spin until the ring has space. Slow guest readers shouldn't + // burn the CPU, so back off when full — sched_yield is plenty + // for ringbuffer pacing. + while (!parport_pci_inject_byte(ctx->dev, byte)) { + rvvm_sched_yield(); + } + } + fclose(ctx->fp); + free(ctx); + return NULL; +} + static bool rvvm_cli_configure(rvvm_machine_t* machine, const char* bios, tap_dev_t* tap) { UNUSED(tap); @@ -374,6 +433,59 @@ static int rvvm_cli_main(int argc, char** argv) sound_hda_init_auto(machine); } + if (rvvm_has_arg("parport")) { + // -parport : attach a parallel port. spec is "null" (attach + // with no backend), a bare path (forward output sink), or a + // comma-separated field list "out=,in=" where out= + // sinks guest output and in= sources the reverse (nibble) channel. + const char* spec = rvvm_getarg("parport"); + char* out_alloc = NULL; // freed at end if allocated + char* in_alloc = NULL; + const char* out_path = NULL; + const char* in_path = NULL; + if (spec != NULL && !rvvm_strcmp(spec, "null")) { + if (rvvm_strfind(spec, "=")) { + out_alloc = parport_spec_field(spec, "out="); + in_alloc = parport_spec_field(spec, "in="); + out_path = out_alloc; + in_path = in_alloc; + } else { + out_path = spec; // bare path shorthand = output sink + } + } + + pci_dev_t* parport_dev = NULL; + if (out_path != NULL) { + FILE* fp = fopen(out_path, "wb"); + if (fp) { + setvbuf(fp, NULL, _IONBF, 0); // unbuffered — bytes appear immediately + rvvm_info("parport: output -> %s", out_path); + parport_dev = parport_pci_init_auto(machine, parport_main_write_fn, fp); + } else { + rvvm_warn("parport: failed to open %s, attaching with no backend", out_path); + } + } + if (parport_dev == NULL) { + parport_dev = parport_pci_init_auto(machine, NULL, NULL); + } + + if (parport_dev && in_path != NULL) { + FILE* in_fp = fopen(in_path, "rb"); + if (in_fp) { + rvvm_info("parport: reverse-channel input <- %s", in_path); + parport_in_ctx_t* ctx = safe_new_obj(parport_in_ctx_t); + ctx->dev = parport_dev; + ctx->fp = in_fp; + rvvm_thread_detach(rvvm_thread_create(parport_in_thread, ctx)); + } else { + rvvm_warn("parport: failed to open %s for reverse channel", in_path); + } + } + + free(out_alloc); + free(in_alloc); + } + tap_dev_t* tap = NULL; #ifdef USE_NET if (!rvvm_has_arg("nonet")) { From ea4a97d9b4e0a9e2432b15f864f3d6cfad2fcfb7 Mon Sep 17 00:00:00 2001 From: Sol Astrius Phoenix Date: Thu, 18 Jun 2026 21:24:14 +0200 Subject: [PATCH 3/3] main: create -parport sink with owner-only perms (CodeQL) Signed-off-by: Sol Astrius Phoenix --- src/main.c | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main.c b/src/main.c index 7279ec601..ef609dc8b 100644 --- a/src/main.c +++ b/src/main.c @@ -30,6 +30,11 @@ along with this program. If not, see . #include // parport sink/source use FILE* on all targets +#if !defined(HOST_TARGET_WINNT) +#include // open(), O_WRONLY/O_CREAT/O_TRUNC for the parport sink +#include // close() +#endif + #include #include #include @@ -235,6 +240,29 @@ static char* parport_spec_field(const char* spec, const char* key) return out; } +// Open the -parport output sink for writing. We keep stdio (FILE*) so the +// sink may be a pipe/fifo, but create regular files with owner-only perms +// rather than letting fopen() default to a world-writable 0666 & ~umask. +static FILE* parport_open_sink(const char* path) +{ +#if !defined(HOST_TARGET_WINNT) + // O_CREAT honours the 0600 mode only when the file is created; existing + // files and fifos keep their perms. fdopen adopts the fd on success. + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0600); + if (fd < 0) { + return NULL; + } + FILE* fp = fdopen(fd, "wb"); + if (fp == NULL) { + close(fd); + } + return fp; +#else + // Windows inherits ACLs from the parent directory; no umask-style window. + return fopen(path, "wb"); +#endif +} + // Bridge for the -parport output sink: each byte the guest strobes out of // its parallel port is appended to a host file. user_data carries the FILE* // opened in rvvm_cli_main. Called outside the device's lock so the write @@ -456,7 +484,7 @@ static int rvvm_cli_main(int argc, char** argv) pci_dev_t* parport_dev = NULL; if (out_path != NULL) { - FILE* fp = fopen(out_path, "wb"); + FILE* fp = parport_open_sink(out_path); if (fp) { setvbuf(fp, NULL, _IONBF, 0); // unbuffered — bytes appear immediately rvvm_info("parport: output -> %s", out_path);