joeylib2/src/atarist/serial.c
2026-09-28 21:03:46 -05:00

276 lines
11 KiB
C

// Atari ST serial HAL -- the RS-232 Modem port via TOS.
//
// The base ST's one RS-232 line is the Modem port, driven by the 68901 MFP
// USART and wrapped by TOS as BIOS logical device 1 (AUX) with a receive-
// buffered IOREC. This HAL rides that: XBIOS Rsconf sets baud/flow/format,
// Bconin/Bconout move bytes, Bconstat/Bcostat gate them. All are GEMDOS/BIOS/
// XBIOS traps that switch to supervisor internally, so -- unlike the direct
// $FFFFFAxx pokes elsewhere in the ST HAL -- serial needs no supervisor
// handling and no new headers. RX is buffered by TOS, so jlpSerialPoll is a
// no-op. (Caveat: TOS's default RS-232 IOREC is small ~256 bytes and Bconin
// blocks, so callers gate on Available; sustained high-baud input can overrun
// -- a future revision could grow the IOREC or install an MFP RX ISR.)
#include <mint/osbind.h>
#include "joey/serial.h"
#include "port.h"
// BIOS logical device 1 = RS-232 / Modem port.
#define ST_AUX_DEV 1
// 68901 MFP USART control register (UCR) bit fields, built by serialUcr:
// bit 7 clock divide (1 = /16, standard async)
// bits6-5 word length (00=8, 01=7, 10=6, 11=5) -> (8 - dataBits)
// bits4-3 stop bits (01 = 1 stop, 11 = 2 stop)
// bit 2 parity enable
// bit 1 parity even (1) / odd (0)
#define ST_UCR_CLK_DIV16 0x80
// Rsconf sentinels. -1 in any slot means "leave that field alone" (TOS writes a
// field only when the value it is handed is in range). -2 in the SPEED slot is
// the separate, documented baud inquire (mint/ostruct.h BAUD_INQUIRE): EmuTOS's
// rsconf_mfp returns iorec->baudrate and returns BEFORE touching any register,
// so it is side-effect free. Do NOT use -1 to inquire; -1 is "no change".
#define ST_RSCONF_NO_CHANGE ((int16_t)-1)
#define ST_RSCONF_INQUIRE ((int16_t)-2)
#define ST_BAUD_CODE_MAX 15
// DTR is YM2149 port A bit 4, set and cleared through XBIOS Ongibit/Offgibit. The line driver inverts
// it: the bit SET is DTR OFF, so Ongibit(ST_GI_DTR_OFF) hangs up and Offgibit(ST_GI_DTR_ON_MASK) --
// every bit but 4 kept -- raises it.
#define ST_GI_DTR_OFF 0x10
#define ST_GI_DTR_ON_MASK 0xEF
// ----- Module state -----
// What jlpSerialOpen took away, so jlpSerialClose can hand it back. Both sit at
// ST_RSCONF_NO_CHANGE when there is nothing to restore, so a close with no
// matching open -- or a second close -- lowers to an Rsconf that writes nothing.
static int16_t gSavedBaudCode = ST_RSCONF_NO_CHANGE;
static int16_t gSavedUcr = ST_RSCONF_NO_CHANGE;
// ----- Prototypes -----
static int16_t serialBaudCode(uint32_t baud);
static int32_t serialRsconf(int16_t baud, int16_t flow, int16_t ucr);
static int16_t serialUcr(const jlSerialConfigT *cfg);
// ----- Helpers -----
// Rsconf speed codes (XBIOS): 0 is fastest, 15 slowest. Unknown -> 9600.
static int16_t serialBaudCode(uint32_t baud) {
switch (baud) {
case 19200u: return 0;
case 9600u: return 1;
case 4800u: return 2;
case 3600u: return 3;
case 2400u: return 4;
case 2000u: return 5;
case 1800u: return 6;
case 1200u: return 7;
case 600u: return 8;
case 300u: return 9;
case 200u: return 10;
case 150u: return 11;
case 134u: return 12;
case 110u: return 13;
case 75u: return 14;
case 50u: return 15;
default: return 1;
}
}
// XBIOS 15 (Rsconf) done by hand, because mint's Rsconf() macro CANNOT be
// trusted with computed arguments.
//
// osbind.h's own change log admits it: the seven-word trap macro relaxed its
// operand constraints from "r" to "g" on the grounds that "these args will
// never be expressions". With "g", gcc is free to leave an argument in a stack
// temp -- and the macro then pushes seven words with movew ...,sp@-, moving sp
// out from under that temp, so the push reads the wrong offset. The disassembly
// showed exactly that: the baud code was parked at sp@(14) and pushed after sp
// had already moved. MEASURED under EmuTOS/Hatari: every requested rate (19200,
// 9600, 4800, 2400, 1200, 300) programmed 9600, and shuffling the C around only
// changed WHICH wrong code came out. The UCR happened to survive in a register.
//
// Pushing the words here, with the three variable ones bound to registers,
// removes the sp-relative temp entirely. rsr/tsr/scr are immediates: no caller
// wants them, and leaving them "no change" means there is nothing to undo.
static int32_t serialRsconf(int16_t baud, int16_t flow, int16_t ucr) {
// Bound to d0 the way osbind.h's own trap_14_* helpers bind theirs: XBIOS
// returns in d0, and an unbound output operand would leave gcc free to pick
// another register that the asm never writes (which silently returned
// garbage here until the probe caught it).
register int32_t ret __asm__("d0");
__asm__ volatile (
"movew #-1,%%sp@-\n\t" /* scr: no change */
"movew #-1,%%sp@-\n\t" /* tsr: no change */
"movew #-1,%%sp@-\n\t" /* rsr: no change */
"movew %3,%%sp@-\n\t" /* ucr */
"movew %2,%%sp@-\n\t" /* flow */
"movew %1,%%sp@-\n\t" /* baud */
"movew #15,%%sp@-\n\t" /* XBIOS 15 = Rsconf */
"trap #14\n\t"
"lea %%sp@(14),%%sp"
: "=d" (ret)
: "d" (baud), "d" (flow), "d" (ucr)
: "d1", "d2", "a0", "a1", "cc", "memory");
return ret;
}
static int16_t serialUcr(const jlSerialConfigT *cfg) {
int16_t ucr;
ucr = ST_UCR_CLK_DIV16;
ucr |= (int16_t)((8u - cfg->dataBits) << 5);
ucr |= (int16_t)((cfg->stopBits == 2u ? 3u : 1u) << 3);
if (cfg->parity != JL_SERIAL_PARITY_NONE) {
ucr |= (int16_t)(1u << 2);
if (cfg->parity == JL_SERIAL_PARITY_EVEN) {
ucr |= (int16_t)(1u << 1);
}
}
return ucr;
}
// ----- HAL entry points (alphabetical) -----
uint16_t jlpSerialAvailable(void) {
// Bconstat reports presence (-1/0), not a count: 1 == at least one byte.
return Bconstat(ST_AUX_DEV) ? 1u : 0u;
}
// The trap path hijacks no vector, but jlpSerialOpen did change the line
// settings, and those live in the MFP and TOS's iorec -- both of which outlive
// the program. So hand back the two fields TOS lets us recover: the baud code
// (from the -2 inquire) and the UCR (the high byte of what Rsconf returned when
// it installed ours). rsr/tsr/scr were never written -- open passes -1 for them
// -- so they need no undo, and scr is not in the return value anyway.
//
// Flow control is deliberately NOT restored: Rsconf has no inquire for it (TOS
// keeps it in iorec->flowctrl with no way back out), so the only options are
// leaving it or guessing, and guessing a value onto a working port is the
// mistake the X68000 _SET232C note in that port's serial.c warns about. A user
// who had hardware handshaking on keeps the app's setting until something sets
// it again; that is a documented limitation, not a silent corruption.
void jlpSerialClose(void) {
(void)serialRsconf(gSavedBaudCode, ST_RSCONF_NO_CHANGE, gSavedUcr);
gSavedBaudCode = ST_RSCONF_NO_CHANGE;
gSavedUcr = ST_RSCONF_NO_CHANGE;
}
void jlpSerialFlush(void) {
while (Bconstat(ST_AUX_DEV)) {
(void)Bconin(ST_AUX_DEV);
}
}
bool jlpSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config) {
int16_t flow;
int16_t baudCode;
int16_t ucr;
int32_t inquired;
uint32_t previous;
(void)device; // the base ST has one RS-232 line: the Modem port.
switch (config->flow) {
case JL_SERIAL_FLOW_RTSCTS: flow = 2; break;
case JL_SERIAL_FLOW_XONXOFF: flow = 1; break;
default: flow = 0; break;
}
// EVALUATE THE HELPERS INTO LOCALS FIRST -- never inline a call into the
// Rsconf() argument list. mint/osbind.h's own change log says so: the
// seven-word trap macro relaxed its operand constraints from "r" to "g"
// and notes that is "ok since these args will never be expressions", so
// gcc may materialise an argument from a stack slot AFTER the macro has
// moved sp out from under it. Passing serialBaudCode(...) inline was
// exactly that, and it lost the baud code: MEASURED under EmuTOS/Hatari,
// every requested rate -- 19200, 9600, 4800, 2400, 1200, 300 -- programmed
// code 1 (9600). serialUcr's result survived; the baud one did not.
baudCode = serialBaudCode(config->baud);
ucr = serialUcr(config);
// Raise DTR, as the open does on every port: TOS only sets it at boot, so a line an earlier
// jlSerialSetDtr(false) hung up would otherwise stay down.
Offgibit(ST_GI_DTR_ON_MASK);
// Snapshot the baud code BEFORE installing ours -- the inquire reports the
// rate currently in force. Range-gate the answer: a TOS that did not
// implement the -2 inquire would fall through to the normal path, write
// nothing (every field is out of range), and hand back the packed
// ucr/rsr/tsr instead, which is >= 0x01000000 for any non-zero UCR.
inquired = serialRsconf(ST_RSCONF_INQUIRE, ST_RSCONF_NO_CHANGE, ST_RSCONF_NO_CHANGE);
if (inquired >= 0 && inquired <= ST_BAUD_CODE_MAX) {
gSavedBaudCode = (int16_t)inquired;
}
// The return is the PREVIOUS (ucr << 24) | (rsr << 16) | (tsr << 8), so the
// same call that installs our line settings also reports the UCR we displaced.
previous = (uint32_t)serialRsconf(baudCode, flow, ucr);
gSavedUcr = (int16_t)((previous >> 24) & 0xFFu);
return true;
}
void jlpSerialPoll(void) {
// TOS buffers RX in its IOREC; nothing to pump.
}
uint16_t jlpSerialRead(uint8_t *buf, uint16_t max) {
uint16_t n;
n = 0u;
// Gate each read on Bconstat first: Bconin blocks on an empty buffer.
while (n < max && Bconstat(ST_AUX_DEV)) {
buf[n] = (uint8_t)Bconin(ST_AUX_DEV);
n++;
}
return n;
}
// DTR on demand (jlSerialSetDtr, how a program hangs up). Neither Rsconf nor anything else in EmuTOS
// touches port A bit 4 after boot (Hatari psg_write + xbios trace), and close does not either, so a
// dropped DTR stays dropped.
void jlpSerialSetDtr(bool on) {
if (on) {
Offgibit(ST_GI_DTR_ON_MASK);
} else {
Ongibit(ST_GI_DTR_OFF);
}
}
uint16_t jlpSerialWrite(const uint8_t *buf, uint16_t len) {
uint16_t n;
for (n = 0u; n < len; n++) {
uint32_t guard;
// Bounded transmitter-ready spin (Bcostat -1 = ready) so a stalled
// link (no CTS) reports a partial write instead of hanging the frame.
guard = 0u;
while (!Bcostat(ST_AUX_DEV)) {
guard++;
if (guard > 200000ul) {
return n;
}
}
Bconout(ST_AUX_DEV, (int16_t)buf[n]);
}
return n;
}