joeylib2/include/joey/serial.h
2026-10-19 15:46:47 -05:00

177 lines
9.1 KiB
C

// 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 <joey/joey.h>. 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<unit-1>; 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
// defense 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