== Serial (Optional Add-On) Declared in ``. [IMPORTANT] ==== This header is *not* pulled in by ``. Include it explicitly: [source,c] ---- #include #include ---- A program that never references `jlSerial*` pays nothing -- the linker drops the module entirely. ==== One byte-stream API spans every port while the HAL hides very different hardware: the IIgs drives a Zilog 8530 SCC directly or a 6551 card in a slot; DOS uses an interrupt-driven 8250/16550 UART; the Amiga uses `serial.device`; the ST goes through the BIOS Aux channel; the X68000 transmits through IOCS and receives on its own SCC interrupt; DVX goes through DVX's serial library, which owns the COM ports for every program on the desktop (a port another program has open cannot be opened); Win32 and Win16 go through Windows' own communications calls, whose driver owns the ports the same way; SDL3 on Linux, macOS, Haiku, and BeOS open the port as a terminal device through termios. The web and the PSP have no serial port: `jlSerialOpen` fails there. The model is *non-blocking and buffered*. Received bytes accumulate in a background ring -- a hardware interrupt on DOS, the X68000, and the IIgs, the OS on Amiga, ST, DVX, Windows, Linux, Haiku, and BeOS -- and reads never block. Call `jlSerialPoll` once per frame. On the IIgs a Super Serial Card only interrupts with its interrupt switch (SW2-6) on; with it off the card still works, drained by `jlSerialPoll`, but loses what arrives while the program is busy. On the Amiga the port is `serial.device`, which AmigaOS loads from `DEVS:`: every Workbench installation has it, but Kickstart 3.1 on its own does not, so a program booted without Workbench cannot open the port. `joey-run` puts the installer's copy (component `amigaSerial`) on the boot drive; a game shipped to boot on its own must do the same. A failed `jlSerialOpen` is non-fatal: it returns `false` and every other call becomes a safe no-op, exactly like a failed audio init. === What Can Lose Received Bytes Every port receives into a small hardware buffer that an interrupt empties into the ring. A byte is lost when that buffer fills before the interrupt runs: when something keeps interrupts off, or keeps the CPU away from the handler, for longer than the buffer lasts. A byte takes about 1 ms to arrive at 9600 baud, so: [cols="3,2,2", options="header"] |=== |Port |Receive buffer |Lasts at 9600 baud |IIgs modem and printer ports (Zilog 8530) |3 bytes |~3 ms |IIgs Super Serial Card (6551) |1 byte |~1 ms |DOS, 16550A UART |16 bytes, interrupt at 14 |~2 ms from the interrupt |DOS, 8250 or 16450 UART |1 byte |~1 ms |DVX |as DOS: DVX's library drives the same UARTs |as DOS |Win32, Win16 |the UART's; Windows' driver empties it into a 4 KB queue |as DOS |Amiga (Paula) |1 byte |~1 ms |Atari ST (68901 MFP) |1 byte |~1 ms |X68000 (Zilog 8530) |3 bytes |~3 ms |=== At 19200 baud each lasts half as long, and so on. A byte that arrives with a framing, parity, or overrun error is delivered as it came on the IIgs, DOS, the X68000, Linux, Haiku, and BeOS (the port does not ask the driver to check parity); on the other ports the system's driver decides. JoeyLib's own work is safe while a port is open, with one exception on the X68000 (below). On the IIgs, where the display and sound code masks interrupts for long stretches, opening a port makes JoeyLib cut them into pieces of at most ~1.7 ms: the present and the full-surface clear, the joystick read (~2.8 ms otherwise, with the stick at full deflection), and the sound-effect calls into the NinjaTrackerPlus replayer (~2.4 ms to start an effect, and a refill interrupt that tops up every streaming effect that is due at once). A full-screen present gets about 5% slower; a joystick read costs about what it does with no port open, up to ~0.8 ms more with the stick in a corner, and can read one count different from the same stick with no port open. With no port open all of these run as before. The other ports mask interrupts only across a few register writes. These can still lose bytes, and are the program's to schedule around: * *Disk access.* On the IIgs, GS/OS holds interrupts off while it reads or writes a 3.5-inch floppy: 65 to 220 ms at a time, measured. That is every file call -- `jlFile*`, `jlSave*`, loading assets and sprite banks, and `jlInit` unless `jlConfigT.noAudio` is set, since starting audio loads the replayer from disk. The other ports' disk access goes through the operating system and has not been measured; treat it as a risk there too. * *Starting a song on the IIgs.* `jlAudioPlayMod` loads the song's instruments into the sound chip with interrupts off, roughly 12 ms per KB of samples. * *Music playing on the IIgs.* The replayer's tick runs as an interrupt handler, and has not been measured with a port open. * *A Super Serial Card above 2400 baud.* The IIgs's own interrupt handling can outlast the 6551's single byte, so the card runs loss-free at 2400 and above that relies on the protocol to recover. The built-in modem and printer ports stay loss-free at 9600 through full-screen presents, clears, joystick reads, and sound effects. * *The IIgs's built-in ports above 9600 baud.* At 19200 the present's ~1.7 ms stretches outlast the chip's 3 bytes, and a stream received through full-screen presents loses about one byte in 70. From 38400 up a steady stream keeps the 65816 in its interrupt handler until it ends, even with nothing drawn: the program gets no time until then, and the 512-byte receive buffer keeps the first 511 bytes. Those rates suit bursts that fit the buffer, a protocol that paces its blocks (XMODEM's acknowledgements do), and sending, which they do at full speed. * *Per-scanline palettes on the X68000.* The port reloads its one hardware palette as the beam scans, and a run of palette changes on consecutive lines holds serial receive off for the whole run, about 32 microseconds a line. Receive stays loss-free while such runs are under about 90 lines at 9600 baud, 40 at 19200, and 15 at 38400; palette changes three or more lines apart release it between bands. * *The program's own code*, wherever it masks interrupts. Ways to live with them: * Keep file access and `jlAudioPlayMod` out of the middle of a transfer: do them between messages, or while the other end is waiting for a reply. * Ask the other end to pause first. With hardware flow control, drop RTS with `jlSerialSetRts(false)`, allow a few character times for bytes already on the way, do the slow operation, then raise RTS again. With software flow control, send XOFF and later XON. * For data that must arrive intact, use a protocol with checksums and retransmission; XMODEM and its descendants exist for exactly this. === Flow Control `flow` in `jlSerialConfigT` picks the handshake, and every port with a serial port honors it: * `JL_SERIAL_FLOW_RTSCTS`: transmit waits while the peer holds CTS down, and the port drops RTS when its receive buffer is three quarters full, raising it again once reads have it down to a quarter. * `JL_SERIAL_FLOW_XONXOFF`: the peer's XOFF and XON (0x13 and 0x11) pause and resume transmit and never reach `jlSerialRead`, and the port sends XOFF when its receive buffer is three quarters full and XON at a quarter. The receive buffer, which also decides how long a program may go without reading when there is no flow control: [cols="3,2,2", options="header"] |=== |Port |Receive buffer |Lasts at 9600 baud |IIgs |512 bytes |~0.5 s |DOS, DVX |2 KB |~2 s |Amiga (`serial.device`) |4 KB |~4 s |Atari ST (TOS's, enlarged while the port is open) |4 KB |~4 s |X68000 |4 KB |~4 s |Win32, Win16 (Windows' driver) |4 KB |~4 s |=== Where they differ: * The IIgs modem and printer ports' connector (mini-DIN-8) has no RTS line: its one handshake output is DTR. RTS/CTS there makes transmit wait for CTS, but nothing can tell the peer to pause, so receive needs XON/XOFF or a protocol that paces itself. A Super Serial Card has RTS, and its 6551 also stops its own transmitter while RTS is down. * On the ST, the Amiga, Win32, and Win16 the operating system's driver does the handshaking, and under RTS/CTS it may change RTS after `jlSerialSetRts` did. DVX's library holds the peer off at four fifths full and lets it go at a fifth. * The X68000 drops RTS from its receive interrupt, but sends XOFF and XON from `jlSerialPoll`, `jlSerialRead`, and `jlSerialWrite`: under XON/XOFF, a program that calls none of them for longer than the last quarter of the buffer lasts (about a second at 9600 baud) can still lose bytes. === Types [source,c] ---- typedef enum { JL_SERIAL_DEFAULT = 0, // the platform's natural RS-232 port JL_SERIAL_MODEM, // IIgs modem port (SCC channel B) JL_SERIAL_PRINTER, // IIgs printer port (SCC channel A) JL_SERIAL_SLOT // card/port selected by config->unit } 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; typedef struct { uint32_t baud; // 300 .. 115200; each port runs its nearest uint8_t dataBits; // 5 .. 8 uint8_t stopBits; // 1 or 2 jlSerialParityE parity; jlSerialFlowE flow; uint8_t unit; // JL_SERIAL_SLOT only } jlSerialConfigT; ---- A field left 0 takes a default: `baud` 9600, `dataBits` 8, and `stopBits` 1. `jlSerialOpen` fails for `dataBits` outside 5 to 8, `stopBits` other than 1 or 2, or a `parity` or `flow` the enumerations do not name. `baud` is the rate asked for. Each port runs at the nearest rate it has: [cols="2,5", options="header"] |=== |Port |Rates |IIgs modem and printer ports |115200 divided by a whole number: 115200 itself straight from the ports' crystal, the rest from the SCC's baud rate generator. A rate between two of those runs at the one above it, and anything faster at 115200. |IIgs Super Serial Card |The 6551's fifteen rates, 50 to 19200; any other runs at the fastest one below it. |DOS |115200 divided by a whole number, up to 115200; a rate between two of those runs at the one above it. |DVX |DVX's library's rates, 50 to 115200; any other runs at the fastest one below it. |Amiga |The rate as asked, handed to `serial.device`. |Atari ST |The MFP's sixteen rates, 50 to 19200; any other runs at the fastest one below it. |X68000 |75 to 38400, each double the last (38400 is really 39062); any other runs at the fastest one below it. |Win32 |The rate as asked, handed to Windows' driver. |Win16 |The rate as asked up to 57600; above that, see below. |SDL3 (Linux), Haiku, BeOS |The nearest of 300, 600, 1200, 2400, 4800, 9600, 19200, 38400, 57600, and 115200, handed to the system's driver. |=== The IIgs is the machine with three device choices; elsewhere `JL_SERIAL_MODEM` and `JL_SERIAL_PRINTER` both mean the default port. `JL_SERIAL_DEFAULT` selects the IIgs modem port; COM1 on DOS, DVX, Win32, and Win16; the ST Modem port; Amiga unit 0; the X68000's RS-232C port; on Linux `/dev/ttyS0`; or on Haiku and BeOS the first entry of `/dev/ports`. On Linux, Haiku, and BeOS the `JOEYLIB_SERIAL` environment variable names another device for the default (and the modem and printer) port: a USB adapter, `JOEYLIB_SERIAL=/dev/ttyUSB0`. Haiku's and BeOS's drivers take 7 or 8 data bits only, so 5 or 6 fail the open there. On Win16 a `baud` above 57600 runs at 115200 through KPCOMM.DRV, the communications driver JoeyLib's Windows runs with in place of Windows 3.1's own (the `kpcomm` installer component, built from the mscomm project). On a Windows with its own COMM.DRV, which has no 115200, it runs at 57600. On the X68000 the serial add-on *requires* Human68k's RS-232C driver, RSDRV.SYS. Every Human68k system disk has it and the stock CONFIG.SYS loads it (`DEVICE = \SYS\RSDRV.SYS`), as do JoeyLib's own X68000 boot disks; a game shipped to boot on its own must keep it. Without it `jlSerialOpen` returns `false` and `jlLastError` says the driver is missing. With it the port runs at 75 to 38400 bps. 38400 is really 39062 bps, 1.7% fast, within the usual tolerance of a UART at the far end. `unit` disambiguates `JL_SERIAL_SLOT` and is ignored otherwise: an IIgs Super Serial Card slot 1..7, a DOS or DVX COM number 1..4, a Win16 COM number 1..9, a Win32 COM number 1..255, an Amiga `serial.device` unit, or on Linux `/dev/ttyS` and on Haiku and BeOS the unit'th entry of `/dev/ports` in name order. On the ST family it is a port in TOS's Bconmap order: 1 the Modem port, and on a Mega STe, TT, or Falcon 2 and up its other ports (a Mega STe's SCC channel B, Modem 2, is 2 and its LAN port 3; a TT adds its second MFP's port as 3 and has the LAN port at 4). The open maps TOS's AUX device to that port and the close maps it back; a plain ST has unit 1 only. The other devices open AUX wherever TOS has it, the Modem port unless the user mapped it elsewhere. The X68000 has one port, and opens it for every device, `JL_SERIAL_SLOT` included. === jlSerialOpen [source,c] ---- bool jlSerialOpen(jlSerialDeviceE device, const jlSerialConfigT *config); ---- Opens and configures the port. Only one port may be open at a time; a second open without an intervening close fails. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`device` |`jlSerialDeviceE` |Which interface. |`config` |`const jlSerialConfigT *` |Line configuration. |=== *Returns:* `true` on success. `false` if the port cannot be opened or the platform has no serial support -- and then every other call no-ops. It is also `false`, 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. === jlSerialClose [source,c] ---- void jlSerialClose(void); ---- Closes the port and releases its resources. Safe when not open. A port still open when the program exits is closed then: the first successful `jlSerialOpen` registers an `atexit` handler, because on DOS, the X68000, and the IIgs the port's receive interrupt vector points into the program until it is closed. Bytes `jlSerialWrite` accepted are still sent, so a program can write a farewell and close straight away. DOS, the ST, the X68000, Linux, Haiku, and BeOS put back the speed and framing the port had before the open (Linux, Haiku, and BeOS with HUPCL off, so the system's own last close keeps DTR up); the IIgs (whose serial chip's settings cannot be read back) and DVX leave it as JoeyLib set it. Closing does *not* hang up: `jlSerialClose` leaves DTR exactly as it is, so a modem call or a BBS caller survives the program handing the line back: a door returning to its BBS, or a terminal program exiting while the call stays up. RTS is not part of that promise; on the Amiga, `serial.device` puts back whatever RTS was before the open. Win32 cannot make the promise: the close gives the port back to Windows' driver, which has no call that keeps DTR up past it, so a Win32 program that must not hang up the line keeps the port open. Win16 keeps it through KPCOMM.DRV, the communications driver JoeyLib's Windows runs with; with Windows 3.1's own COMM.DRV it drops too. To hang up, drop DTR first: call `jlSerialSetDtr(false)`, wait longer than the modem needs to notice (a Hayes modem's S25 setting, 50 ms by default), then close. The line stays down. [NOTE] ==== Two ports cannot keep the line perfectly steady, though neither gap is long enough to hang up a modem (a Hayes modem ignores DTR drops shorter than its S25 setting, 50 ms by default): * On the Amiga, when another program had the port open first, the last close can drop DTR (`serial.device` puts back the lines as that program found them), and JoeyLib raises it again straight away: the line is off for under a millisecond. Otherwise JoeyLib raises DTR before its own open, so the close keeps it up with no gap. * On the IIgs, opening a port resets its channel, which drops DTR briefly even if it was already up: about a quarter of a millisecond on the modem and printer ports, about a millisecond on a Super Serial Card. ==== *Parameters:* none. *Returns:* nothing. === jlSerialPoll [source,c] ---- void jlSerialPoll(void); ---- Pumps the receive and transmit buffers. Call once per game frame. Where an interrupt fills the ring this does next to nothing; it is the drain for a port that cannot interrupt. *Parameters:* none. *Returns:* nothing. === jlSerialAvailable [source,c] ---- uint16_t jlSerialAvailable(void); ---- *Parameters:* none. *Returns:* the number of bytes waiting in the receive buffer, readable now without blocking. === jlSerialRead [source,c] ---- uint16_t jlSerialRead(uint8_t *buf, uint16_t max); ---- [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`buf` |`uint8_t *` |Destination buffer. |`max` |`uint16_t` |Buffer capacity. |=== *Returns:* the number of bytes copied, 0 to `max`. Never blocks. === jlSerialWrite [source,c] ---- uint16_t jlSerialWrite(const uint8_t *buf, uint16_t len); ---- Sends bytes without waiting long. Most ports feed the bytes to the line as it takes them, and give up once it has taken none for 100 ms (a peer holding transmit off, say). The Amiga queues them in two 1 KB buffers it sends from in the background, DVX in its serial library's 2 KB ring, and Win16 in Windows' driver's queue, so those never wait at all. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`buf` |`const uint8_t *` |Data to send. |`len` |`uint16_t` |Byte count. |=== *Returns:* the number of bytes accepted, which may be fewer than `len` if a flow-controlled peer or a full transmit path stalls. === jlSerialWriteByte / jlSerialReadByte [source,c] ---- bool jlSerialWriteByte(uint8_t b); int16_t jlSerialReadByte(void); ---- Single-byte convenience calls. [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`b` |`uint8_t` |Byte to send. |=== *Returns:* `jlSerialWriteByte` returns `true` if the byte was accepted. `jlSerialReadByte` returns the byte, or `-1` when none is ready. === jlSerialFlush [source,c] ---- void jlSerialFlush(void); ---- Discards any buffered received bytes. *Parameters:* none. *Returns:* nothing. === jlSerialSetDtr [source,c] ---- void jlSerialSetDtr(bool on); ---- Raises or drops DTR. `jlSerialOpen` raises DTR and `jlSerialClose` leaves it as it is, so this is how a program hangs up: [source,c] ---- jlSerialSetDtr(false); // hang up // ...wait longer than the modem's DTR-drop detection time (Hayes S25, // 50 ms by default)... jlSerialClose(); // DTR stays down ---- [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`on` |`bool` |`true` raises DTR, `false` drops it. |=== *Returns:* nothing. A closed port no-ops, so this can be driven unconditionally. Two ports tie DTR to more than the line: * On an IIgs Super Serial Card the 6551 has no DTR that is only a line: 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 another program setting the line -- raises it again. On a Mega STe's, TT's, or Falcon's ports other than the Modem port (a `JL_SERIAL_SLOT` open, or AUX mapped there) DTR stays as TOS set it: TOS has no call for those ports' lines, and JoeyLib does not reach past its driver to the SCC. Closing after a hang-up keeps DTR down, but on two ports it rises briefly on the way: on the Amiga for under a millisecond (`serial.device` puts back the lines as they were at its open) and on the X68000 for about 0.2 ms (close puts back the line settings, which raises DTR), before JoeyLib drops it again. A modem that has hung up ignores that. === jlSerialSetRts [source,c] ---- void jlSerialSetRts(bool on); ---- Asserts or drops RTS, for a caller doing 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". That is what a client with a small ring and a peer that can be told to pause actually needs. Under `JL_SERIAL_FLOW_RTSCTS` on DOS, the X68000, and a IIgs Super Serial Card, RTS is up only while both the port's handshake and this call want it; elsewhere the driver may change it again (see Flow Control). The IIgs modem and printer ports have no RTS line on their connector, so nothing reaches the peer there, and a Mega STe's, TT's, or Falcon's ports other than the Modem port keep RTS as TOS sets it (as DTR). [cols="1,1,4", options="header"] |=== |Parameter |Type |Description |`on` |`bool` |`true` asserts RTS, meaning ready to receive. |=== *Returns:* nothing. A port with no controllable RTS line no-ops, and a closed port no-ops; neither is an error, so this can be driven unconditionally.