// Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. // See LICENSE.md for license information. // SPDX-License-Identifier: MIT // // Serial communications add-on -- portable RS-232 byte streams. // // OPTIONAL add-on: this header is NOT pulled in by . Include it // explicitly. An app that never references jlSerial* pays nothing -- the // linker drops the module (archive granularity / IIgs dead-strip). // // One byte-stream API spans every port; the per-port HAL hides the very // different hardware: // Apple IIgs -- direct Zilog 8530 SCC (modem/printer ports) or a 6551 // Super Serial Card in a slot; its own RX interrupt fills // the ring (a card needs its interrupt switch, SW2-6, on). // DOS -- 8250/16550 UART on a COM port; interrupt-driven RX ring. // Amiga -- serial.device (async exec IO); the OS buffers RX. // Atari ST -- BIOS Aux (Modem port) via XBIOS Rsconf; TOS buffers RX. // X68000 -- SCC channel A: Human68k's RSDRV.SYS for transmit and line // setup (75-38400 bps, RTS/CTS), REQUIRED -- the open fails // without it; its own RX interrupt fills the ring (when // opened in supervisor mode, as jlInit leaves every program). // DVX -- a COM port through DVX's serial library. // Win32/16 -- a COM port through Windows' communications calls. // SDL3 (Linux), Haiku, BeOS -- a terminal device through termios // (JOEYLIB_SERIAL in the environment names the default one). // The web and the PSP have no serial port. // // Model: non-blocking + buffered. Received bytes accumulate in a background // ring (a hardware IRQ on DOS, the X68000 and the IIgs, the OS on Amiga/ST, // DVX, Windows, Linux, Haiku, and BeOS); jlSerialRead / jlSerialAvailable // never block. Call // jlSerialPoll() once per game frame. Transmit never waits long: a bounded // poll on most ports, a queue on the Amiga and Win16. // // Received bytes CAN be lost. Each port's receive hardware holds 1 to 3 bytes // (16 on a 16550), ~1-3 ms at 9600 baud, and anything that keeps interrupts off // longer drops what arrives meanwhile. JoeyLib's own work is safe with a port // open (on the IIgs it cuts its long interrupts-off stretches to ~1.7 ms while // one is), but these are not: IIgs disk access (GS/OS holds interrupts off // 65-220 ms per 3.5" floppy access, so every file call, asset load and // jlInit, which reads the audio replayer), IIgs jlAudioPlayMod (~12 ms per // KB of samples), a Super Serial Card above 2400 baud, and the program's own // interrupt masking. Disk access on the other ports is unmeasured. Schedule // them between messages, pause the peer first (jlSerialSetRts(false) or // XOFF), or use a protocol that retransmits. docs/ApiSerial.adoc, "What Can // Lose Received Bytes", has the per-port table. // // A failed jlSerialOpen is non-fatal: it returns false and every other call is // a safe no-op (Available 0, Read 0), exactly like a failed audio init. #ifndef JOEYLIB_SERIAL_H #define JOEYLIB_SERIAL_H #include "platform.h" #include "types.h" // Which physical interface to open. The IIgs is the oddball with three: // JL_SERIAL_MODEM -- IIgs modem port (SCC channel B). Elsewhere == DEFAULT. // JL_SERIAL_PRINTER -- IIgs printer port (SCC channel A). Only the IIgs has // a second built-in port; elsewhere == DEFAULT. // JL_SERIAL_SLOT -- a card/port selected by jlSerialConfigT.unit: IIgs // Super Serial Card slot (1..7); DOS or DVX COM number // (1..4); Win16 COM (1..9); Win32 COM (1..255); Amiga // serial.device unit; Linux /dev/ttyS; Haiku // and BeOS the unit'th /dev/ports entry; ST family // TOS Bconmap port (1 the Modem port; 2.. a Mega STe's, // TT's, or Falcon's others). The X68000 has one port // and opens it for every device. // JL_SERIAL_DEFAULT -- the platform's natural RS-232 port (IIgs modem, DOS, // DVX, and Windows COM1, ST Modem port, Amiga unit 0, // X68000 RS-232C, Linux /dev/ttyS0, Haiku and BeOS // the first /dev/ports entry). typedef enum { JL_SERIAL_DEFAULT = 0, JL_SERIAL_MODEM, JL_SERIAL_PRINTER, JL_SERIAL_SLOT } jlSerialDeviceE; typedef enum { JL_SERIAL_PARITY_NONE = 0, JL_SERIAL_PARITY_ODD, JL_SERIAL_PARITY_EVEN } jlSerialParityE; typedef enum { JL_SERIAL_FLOW_NONE = 0, // no handshaking JL_SERIAL_FLOW_RTSCTS, // hardware RTS/CTS JL_SERIAL_FLOW_XONXOFF // software XON/XOFF } jlSerialFlowE; // Line configuration. `unit` disambiguates JL_SERIAL_SLOT (see jlSerialDeviceE) // and is ignored for the other devices. A field left 0 takes a default (baud // 9600, 8 data bits, 1 stop bit); data bits outside 5..8, stop bits other // than 1 or 2, or a parity or flow the enums do not name fail the open. typedef struct { uint32_t baud; // 300 .. 115200 (each port runs at the nearest rate it has: docs/ApiSerial.adoc) uint8_t dataBits; // 5 .. 8 uint8_t stopBits; // 1 or 2 jlSerialParityE parity; jlSerialFlowE flow; uint8_t unit; // JL_SERIAL_SLOT: IIgs slot 1..7 / DOS, DVX COM 1..4 / Win16 COM 1..9 / Win32 COM 1..255 / Amiga unit / ST Bconmap port 1.. } jlSerialConfigT; // Open + configure the port. Only one port is open at a time; a second // jlSerialOpen without a jlSerialClose fails. Returns false (non-fatal) if the // port cannot be opened or the platform has no serial support -- every other // call then no-ops. It also fails, with jlLastError() saying why, when no UART // answers at the DOS COM port, when no Super Serial Card answers in the IIgs // slot, and when RSDRV.SYS is not loaded on the X68000. bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config); // Close the open port and release its resources. Safe to call when not open. // Bytes jlSerialWrite accepted are still sent. // A port still open when the program exits is closed then (the first // successful jlSerialOpen registers an atexit handler): where the port's RX // interrupt is taken over (DOS, X68000, IIgs), the vector points into the // program until it is closed. // // Closing does NOT hang up: DTR is left exactly as it was, so a modem call or a // BBS caller survives a program handing the line back -- a door returning to // its BBS, a terminal exiting. RTS is not part of that promise (on the Amiga, // serial.device puts back what it found at open). To hang up, drop DTR with // jlSerialSetDtr(false), wait longer than the modem's DTR-drop detection time // (Hayes S25: 50 ms by default), then close: the line stays down. void jlSerialClose(void); // Pump the RX/TX buffers. Call once per game frame. void jlSerialPoll(void); // Bytes waiting in the RX buffer, readable now without blocking. uint16_t jlSerialAvailable(void); // Copy up to `max` received bytes into `buf`; returns the count copied // (0 .. max). Never blocks. uint16_t jlSerialRead(uint8_t *buf, uint16_t max); // Send `len` bytes; returns the count accepted (may be < len if a flow- // controlled peer or a full transmit path stalls). Never waits long: a // bounded poll on most ports; the Amiga and Win16 queue instead. uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len); // Single-byte convenience. jlSerialReadByte returns -1 when none is ready. bool jlSerialWriteByte(uint8_t b); int16_t jlSerialReadByte(void); // Discard any buffered received bytes. void jlSerialFlush(void); // RAISE OR DROP DTR -- how a program hangs up. // // jlSerialOpen raises DTR and jlSerialClose leaves it as it is, so this is the one call that drops // it. To hang up a modem: jlSerialSetDtr(false), wait longer than the modem's DTR-drop detection // time (Hayes S25: 50 ms by default), then jlSerialClose -- the line stays down. `on` = true raises // it again. // // Two ports tie DTR to more than the line. An IIgs Super Serial Card's 6551 has no separate DTR // bit: dropping DTR also stops the card's receiver until DTR is raised again. On the X68000 the // line setup in the ROM and in RSDRV.SYS raises DTR whenever it runs, so the next jlSerialOpen // (or anything else that sets the line) raises it again. A closed port no-ops, so a caller can // drive it unconditionally. void jlSerialSetDtr(bool on); // ASSERT OR DROP RTS, for a caller that does its own receive-side flow control. // // JL_SERIAL_FLOW_RTSCTS asks the PORT to handshake; this is the other half - the application saying // "my buffer is nearly full, stop sending" and later "go ahead", which is what a client with a small // ring and a peer that can be told to pause actually needs. RetroNet's 6551 clients (C64, Apple II) // have done exactly this for a long time and the 16-bit tier could not, so a JoeyLib target's only // defence against an overrun was the packet layer's retransmit. // // `on` = true asserts RTS (ready to receive). A port with no controllable RTS line no-ops, and a // closed port no-ops; neither is an error, so a caller can drive it unconditionally. void jlSerialSetRts(bool on); #endif