X68000 now handles the serial hardware directly, bypassing DOS.

This commit is contained in:
Scott Duensing 2026-08-13 12:47:11 -05:00
parent 63e76f02e6
commit e1cae64e7b
4 changed files with 230 additions and 10 deletions

1
.gitignore vendored
View file

@ -45,6 +45,7 @@ Thumbs.db
# Crap I added # Crap I added
stuff/* stuff/*
docs/* docs/*
tools/__pycache__/*
# AGI game data (Sierra/fan-made AGI games used for testing; never committed) # AGI game data (Sierra/fan-made AGI games used for testing; never committed)
examples/agi/gamedata/ examples/agi/gamedata/

View file

@ -158,6 +158,11 @@ static void installTickIsr(void);
static void uploadGfxPalette(const jlSurfaceT *src); static void uploadGfxPalette(const jlSurfaceT *src);
// serial.c: drain the tiny (~64 B) RSDRV receive ring into the port's soft ring. Called from the
// long-running paths below (present / VBL wait / input poll) so mid-frame serial bytes are never
// lost while the HAL is busy -- see the soft-ring note in serial.c.
extern void x68kSerialPump(void);
static void vdispHandler(void); static void vdispHandler(void);
static void vdispPoll(void); static void vdispPoll(void);
@ -359,6 +364,7 @@ void jlpPresent(const jlSurfaceT *src) {
uint16_t hi0, hi1, hi2, hi3; uint16_t hi0, hi1, hi2, hi3;
uint16_t lo0, lo1, lo2, lo3; uint16_t lo0, lo1, lo2, lo3;
uint8_t palBase; uint8_t palBase;
uint16_t blitRows = 0;
if (src == NULL) { if (src == NULL) {
return; return;
@ -378,10 +384,18 @@ void jlpPresent(const jlSurfaceT *src) {
if (pd == NULL) { if (pd == NULL) {
return; return;
} }
x68kSerialPump(); // the palette upload + spread-table build above are already line time
for (y = 0; y < SURFACE_HEIGHT; y++) { for (y = 0; y < SURFACE_HEIGHT; y++) {
if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) { if (!STAGE_DIRTY_ROW_TOUCHED(y) || STAGE_DIRTY_ROW_CLEAN(y)) {
continue; continue;
} }
// A blitted row costs ~0.5-0.7 ms, so pump every second one: that keeps the gap near ~1 ms,
// inside the ~2.8 ms the 64-byte RSDRV ring covers at the port's fastest rate (230400).
// Clean rows are a flag test and skip the pump, so an idle present stays trap-free.
if ((blitRows & 1u) == 0u) {
x68kSerialPump();
}
blitRows++;
// Bands are 16-bit stage words = 4 pixels = 2 bytes of a 1bpp plane. // Bands are 16-bit stage words = 4 pixels = 2 bytes of a 1bpp plane.
firstByte = (uint16_t)(gStageMinWord[y] >> 1); firstByte = (uint16_t)(gStageMinWord[y] >> 1);
rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u); rowBytes = (uint16_t)((gStageMaxWord[y] >> 1) - firstByte + 1u);
@ -502,6 +516,9 @@ void jlpInputPoll(void) {
uint8_t scan; uint8_t scan;
for (group = 0; group < X68K_KEYGROUP_COUNT; group++) { for (group = 0; group < X68K_KEYGROUP_COUNT; group++) {
// Per-group, not per-poll: the whole bitsns trap storm can pass the ~2.8 ms the 64-byte RSDRV
// ring covers at the port's fastest rate, but one group's trap never does.
x68kSerialPump();
groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF); groups[group] = (uint8_t)(_iocs_bitsns((int)group) & 0xFF);
} }
// Write both states each poll: the bitmap is live key-DOWN data, // Write both states each poll: the bitmap is live key-DOWN data,
@ -613,6 +630,11 @@ void jlpWaitVBL(void) {
guard = 0ul; guard = 0ul;
while (gFrameCount == start && guard < X68K_VBL_SPIN_LIMIT) { while (gFrameCount == start && guard < X68K_VBL_SPIN_LIMIT) {
vdispPoll(); vdispPoll();
// Every 64 spins ~= a few hundred microseconds: comfortably inside the ~2.8 ms the 64-byte
// RSDRV ring covers at 230400. Trap cost is free here -- this is a blocking wait anyway.
if ((guard & 63ul) == 0ul) {
x68kSerialPump();
}
guard++; guard++;
} }
} }

View file

@ -46,17 +46,150 @@
// write instead of hanging the frame. Same guard the ST HAL uses. // write instead of hanging the frame. Same guard the ST HAL uses.
#define X68K_TX_SPIN_LIMIT 200000ul #define X68K_TX_SPIN_LIMIT 200000ul
// ----- Soft receive ring -----------------------------------------------------
//
// RSDRV.SYS's own receive ring is tiny -- MEASURED at ~64 bytes on MAME: a live capture showed a
// 203-byte frame delivered as its first 63 bytes, a 139-byte hole, then its final byte, exactly the
// signature of a 64-entry ring filling while the client was busy and discarding until drained. Every
// other JoeyLib target rides a big interrupt-fed ring (DOS hardware IRQ ring, ST XBIOS IOREC, and the
// 8-bit clients' IRQ rings), so only this port could lose mid-frame bytes whenever the caller went
// deaf for longer than ~66 ms at 9600 baud -- and jlpPresent's full-stage blit, jlpWaitVBL's V-DISP
// spin, and jlpInputPoll's IOCS trap storm all can. The fix: drain the IOCS ring into this soft ring
// from INSIDE those long-running paths (x68kSerialPump below), so the 64-byte hardware-side window
// only ever has to cover the gap between pumps, never a whole present. No ISR is involved -- the
// pump is a plain call -- so none of the vdispst-style vector hazards apply.
//
// SIZED FOR THE FASTEST LINE, not just the 9600 default: this port inherits RSDRV.SYS's boot rate
// (the mode word is deliberately not written), and RSDRV's table reaches 230400, where the 64-byte
// IOCS ring covers only ~2.8 ms -- so every pump site keeps its interval near 1 ms (present: every
// 2 blitted rows; input poll: every bitsns group; VBL wait: every 64 spins). 4 KB here holds ~178 ms
// of full-rate 230400 traffic, longer than the worst deaf window (a >100 ms all-dirty present), so
// even an unpaced burst -- the server's flowWait timeout degrade streams a whole screen without acks
// -- fits while the loop is busy. It is plain RAM on a 4 MB machine; do not shrink it to save bytes.
#define X68K_RX_RING_CAP 4096u
// Shared with x68kSccRxIsr.s (the SCC channel A RX interrupt handler): the ISR is the producer
// (writes gX68kRxTail), the mainline is the consumer (writes gX68kRxHead). The byte is stored
// before the tail is published and the 68000 is strictly in-order, so volatile indices are the
// whole interlock. When the ISR could not be installed, x68kSerialPump below produces instead.
uint8_t gX68kRxRing[X68K_RX_RING_CAP];
volatile uint16_t gX68kRxHead = 0u; // pop side
volatile uint16_t gX68kRxTail = 0u; // push side
// ----- SCC channel A RX interrupt takeover -----------------------------------
//
// The proper fix for the tiny RSDRV ring: own the receive interrupt, the way every other JoeyLib
// target does (DOS UART IRQ ring, the 8-bit ACIA rings). serial.c reads the SCC's programmed
// vector base (RR2 on channel A returns it UNmodified) and steals the two channel A receive vectors
// -- base|0x0C "character available" and base|0x0E "special condition" (Z8530 status-affects-vector
// puts the source in bits 3-1) -- pointing them at the raw handlers in x68kSccRxIsr.s. RSDRV's own
// RX handler simply never runs again; its 64-byte ring starves and the polled pump becomes a no-op.
// If the base looks wrong (VIS off, unexpected layout) nothing is stolen and the polled pump keeps
// carrying the port exactly as before -- the fallback is automatic.
#define X68K_SCC_A_CTRL ((volatile uint8_t *)0xE98005L)
#define X68K_VECTOR(v) ((volatile uint32_t *)((uint32_t)(v) << 2))
extern void x68kSccRxIsr(void);
extern void x68kSccSpIsr(void);
static uint16_t x68kIrqMask(void);
static void x68kIrqRestore(uint16_t sr);
static void sccIsrInstall(void);
static void sccIsrUninstall(void);
static bool gSccIsrInstalled = false;
static uint8_t gSccRxVec = 0u;
static uint32_t gSccOldRx = 0ul;
static uint32_t gSccOldSp = 0ul;
static uint16_t x68kIrqMask(void) {
uint16_t sr;
__asm__ volatile("move.w %%sr,%0\n\tori.w #0x0700,%%sr" : "=d"(sr) : : "cc");
return sr;
}
static void x68kIrqRestore(uint16_t sr) {
__asm__ volatile("move.w %0,%%sr" : : "d"(sr) : "cc");
}
static void sccIsrInstall(void) {
uint16_t sr;
uint8_t base;
if (gSccIsrInstalled) {
return;
}
// RR2 read is a two-access pointer sequence on the shared SCC, so no interrupt (the channel B
// mouse) may interleave. We run supervisor (jlpInit's _dos_super), so the SR mask is legal.
sr = x68kIrqMask();
*X68K_SCC_A_CTRL = 2u;
base = *X68K_SCC_A_CTRL;
x68kIrqRestore(sr);
// With status-affects-vector the base must have bits 3-1 clear; anything else means the layout
// is not what we expect, and the polled pump stays in charge.
if (base == 0u || (base & 0x0Eu) != 0u) {
return;
}
gSccRxVec = (uint8_t)(base | 0x0Cu);
sr = x68kIrqMask();
gSccOldRx = *X68K_VECTOR(gSccRxVec);
gSccOldSp = *X68K_VECTOR(gSccRxVec + 2u);
*X68K_VECTOR(gSccRxVec) = (uint32_t)x68kSccRxIsr;
*X68K_VECTOR(gSccRxVec + 2u) = (uint32_t)x68kSccSpIsr;
x68kIrqRestore(sr);
gSccIsrInstalled = true;
}
static void sccIsrUninstall(void) {
uint16_t sr;
if (!gSccIsrInstalled) {
return;
}
sr = x68kIrqMask();
*X68K_VECTOR(gSccRxVec) = gSccOldRx;
*X68K_VECTOR(gSccRxVec + 2u) = gSccOldSp;
x68kIrqRestore(sr);
gSccIsrInstalled = false;
}
// Drain everything the IOCS ring holds into the soft ring. Called from the HAL's own long-running
// paths (present / VBL wait / input poll) and from every serial entry point. With the SCC ISR
// installed this is a cheap no-op guard (RSDRV's ring never fills); without it, it is the polled
// fallback that keeps the port working, with the caller deaf-tolerant up to the SOFT ring instead
// of RSDRV's ~64 bytes.
void x68kSerialPump(void) {
if (gSccIsrInstalled) {
return;
}
while (_iocs_isns232c() != 0) {
uint16_t next = (uint16_t)((gX68kRxTail + 1u) % X68K_RX_RING_CAP);
if (next == gX68kRxHead) {
break; // soft ring full: leave the rest in the IOCS ring rather than drop it here
}
gX68kRxRing[gX68kRxTail] = (uint8_t)(_iocs_inp232c() & 0xFF);
gX68kRxTail = next;
}
}
// ----- HAL entry points (alphabetical) ----- // ----- HAL entry points (alphabetical) -----
uint16_t jlpSerialAvailable(void) { uint16_t jlpSerialAvailable(void) {
// _iocs_isns232c reports presence, not a count: non-zero == at least one. x68kSerialPump();
return (_iocs_isns232c() != 0) ? 1u : 0u; return (gX68kRxHead != gX68kRxTail) ? 1u : 0u;
} }
void jlpSerialClose(void) { void jlpSerialClose(void) {
// IOCS path hijacks no vector -- nothing to tear down. sccIsrUninstall(); // hand channel A RX back to RSDRV before the vectors dangle
} }
@ -64,6 +197,7 @@ void jlpSerialFlush(void) {
while (_iocs_isns232c() != 0) { while (_iocs_isns232c() != 0) {
(void)_iocs_inp232c(); (void)_iocs_inp232c();
} }
gX68kRxHead = gX68kRxTail; // the ISR may be live: only the consumer side may move
} }
@ -72,26 +206,30 @@ bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
if (config == NULL) { if (config == NULL) {
return false; return false;
} }
// config is accepted but NOT applied -- see the mode-word note above. The // Line config is accepted but NOT applied -- see the mode-word note above. The
// port keeps whatever RSDRV.SYS set at boot. // port keeps whatever RSDRV.SYS set at boot. (An RTS/CTS enable via an
// _iocs_set232c(-1) read-modify-write was tried and REVERTED: the -1-as-query
// idiom is a _CRTMOD contract, undocumented for _SET232C -- XEiJ reads the
// RSDRV work area instead -- so the write could not be trusted.)
(void)config; (void)config;
sccIsrInstall(); // own the RX interrupt; falls back to the polled pump if the vectors look wrong
return true; return true;
} }
void jlpSerialPoll(void) { void jlpSerialPoll(void) {
// IOCS buffers RX itself; nothing to pump. x68kSerialPump();
} }
uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) { uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
uint16_t n; uint16_t n;
x68kSerialPump();
n = 0u; n = 0u;
// Gate each read on the status call first: _iocs_inp232c BLOCKS when the while (n < max && gX68kRxHead != gX68kRxTail) {
// buffer is empty, which would stall the frame. buf[n] = gX68kRxRing[gX68kRxHead];
while (n < max && _iocs_isns232c() != 0) { gX68kRxHead = (uint16_t)((gX68kRxHead + 1u) % X68K_RX_RING_CAP);
buf[n] = (uint8_t)(_iocs_inp232c() & 0xFF);
n++; n++;
} }
return n; return n;

59
src/x68000/x68kSccRxIsr.s Normal file
View file

@ -0,0 +1,59 @@
| Sharp X68000 RS-232C receive: own the Z8530 SCC channel A RX interrupt.
|
| RSDRV.SYS's receive ring is ~64 bytes -- far too small for a binary protocol
| whose client can be busy blitting for >100 ms -- so instead of draining it by
| polling, serial.c steals the SCC's channel A RX vectors and points them here:
| every received byte lands straight in the port's big soft ring with no RSDRV
| involvement at all. This is the same take-over-the-hardware model every other
| JoeyLib target uses (the DOS UART IRQ ring, the 8-bit ACIA rings).
|
| Entered DIRECTLY from the 68000 exception vector the SCC supplies at IACK
| (WR2 base | %110x for channel A RX -- serial.c reads RR2 and computes it), so
| these are raw interrupt handlers: every register touched is saved, and both
| return with RTE. Unlike the tick hook there is NO chaining -- the byte is
| consumed here, so running RSDRV's handler afterwards would be wrong.
|
| Ring protocol (single consumer, single producer): the mainline reads gX68kRxHead,
| this ISR writes gX68kRxTail; the byte is stored BEFORE the tail moves, and the
| 68000 is strictly in-order, so no further interlock is needed. A full ring
| drops the byte -- the frame CRC upstream turns that into a clean retransmit-less
| drop rather than corruption.
|
| GAS m68k syntax, ELF target -- symbols carry NO leading underscore.
.equ SCC_A_CTRL, 0xE98005 | channel A command/status
.equ SCC_A_DATA, 0xE98007 | channel A data (direct, no pointer)
.equ RING_MASK, 4095 | X68K_RX_RING_CAP - 1 (power of two)
.text
.even
.globl x68kSccRxIsr
.globl x68kSccSpIsr
| Channel A "receive character available".
x68kSccRxIsr:
movem.l %d0-%d2/%a0,-(%sp)
move.b SCC_A_DATA,%d0 | the received byte
move.w gX68kRxTail,%d1
move.w %d1,%d2
addq.w #1,%d2
andi.w #RING_MASK,%d2 | next tail
cmp.w gX68kRxHead,%d2
beq.s rxFull | ring full: drop, CRC upstream recovers
lea gX68kRxRing,%a0
move.b %d0,(%a0,%d1.w) | store BEFORE publishing the new tail
move.w %d2,gX68kRxTail
rxFull: move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS
movem.l (%sp)+,%d0-%d2/%a0
rte
| Channel A "special receive condition" (overrun / framing error). The byte is
| suspect, so unlatch and discard it, clear the error, and drop the IUS -- the
| frame CRC turns the loss into a clean drop.
x68kSccSpIsr:
move.l %d0,-(%sp)
move.b SCC_A_DATA,%d0 | unlatch the offending byte
move.b #0x30,SCC_A_CTRL | WR0: error reset
move.b #0x38,SCC_A_CTRL | WR0: reset highest IUS
move.l (%sp)+,%d0
rte