// 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 #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; }