joeylib2/docs/ApiSerial.adoc

536 lines
21 KiB
Text

== Serial (Optional Add-On)
Declared in `<joey/serial.h>`.
[IMPORTANT]
====
This header is *not* pulled in by `<joey/joey.h>`. Include it explicitly:
[source,c]
----
#include <joey/joey.h>
#include <joey/serial.h>
----
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,
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 honours 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<unit - 1>` 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.