276 lines
11 KiB
C
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;
|
|
}
|