# RS232 -- ISR-Driven Serial Port Library for DJGPP Interrupt-driven UART communication library supporting up to 4 simultaneous COM ports with ring buffers and hardware/software flow control. Targets 486-class DOS hardware running under DJGPP/DPMI. Ported from the DOS Serial Library 1.4 by Karl Stenerud (MIT License), stripped to DJGPP-only codepaths and restyled for the DVX project. ## Architecture The library is built around a single shared ISR (`comGeneralIsr`) that services all open COM ports. This design is necessary because COM1/COM3 typically share IRQ4 and COM2/COM4 share IRQ3 -- a single handler that polls all ports avoids the complexity of per-IRQ dispatch. ``` Application | | rs232Read() non-blocking drain from RX ring buffer | rs232Write() blocking polled write directly to UART THR | rs232WriteBuf() non-blocking write into TX ring buffer | [Ring Buffers] 2048-byte RX + TX per port, power-of-2 bitmask indexing | [ISR] comGeneralIsr -- shared handler for all open ports | [UART] 8250 / 16450 / 16550 / 16550A hardware ``` ### ISR Design The ISR follows a careful protocol to remain safe under DPMI while keeping the system responsive: 1. **Mask** all COM port IRQs on the PIC to prevent ISR re-entry 2. **STI** to allow higher-priority interrupts (timer tick, keyboard) through 3. **Loop** over all open ports, draining each UART's pending interrupt conditions (data ready, TX hold empty, modem status, line status) 4. **CLI**, send EOI to the PIC, re-enable COM IRQs, **STI** before IRET This mask-then-STI pattern is standard for slow device ISRs on PC hardware. It prevents the same IRQ from re-entering while allowing the system timer and keyboard to function during UART processing. ### Ring Buffers Both RX and TX buffers are 2048 bytes, sized as a power of 2 so that head/tail wraparound is a single AND operation (bitmask indexing) rather than an expensive modulo -- critical for ISR-speed code on a 486. The buffers use a one-slot-wasted design to distinguish full from empty: `head == tail` means empty, `(head + 1) & MASK == tail` means full. ### Flow Control Flow control operates entirely within the ISR using watermark thresholds. When the RX buffer crosses 80% full, the ISR signals the remote side to stop sending; when it drops below 20%, the ISR allows the remote to resume. This prevents buffer overflow without any application involvement. Three modes are supported: | Mode | Stop Signal | Resume Signal | |------------|-------------------|-------------------| | XON/XOFF | Send XOFF (0x13) | Send XON (0x11) | | RTS/CTS | Deassert RTS | Assert RTS | | DTR/DSR | Deassert DTR | Assert DTR | On the TX side, the ISR monitors incoming XON/XOFF bytes and the CTS/DSR modem status lines to pause and resume transmission from the TX ring buffer. ### DPMI Memory Locking The ISR code and all per-port state structures (`sComPorts` array) are locked in physical memory via `__dpmi_lock_linear_region`. This prevents page faults during interrupt handling -- a hard requirement for any ISR running under a DPMI host (DOS extender, Windows 3.x, OS/2 VDM, etc.). An IRET wrapper is allocated by DPMI to handle the real-mode to protected-mode transition on hardware interrupt entry. ## UART Type Detection `rs232GetUartType()` probes the UART hardware to identify the chip: 1. **Scratch register test** -- Writes two known values (0xAA, 0x55) to UART register 7 and reads them back. The 8250 lacks this register, so readback fails. If both values read back correctly, the UART is at least a 16450. 2. **FIFO test** -- Enables the FIFO via the FCR (FIFO Control Register), then reads bits 7:6 of the IIR (Interrupt Identification Register): - `0b11` = 16550A (working 16-byte FIFO) - `0b10` = 16550 (broken FIFO -- present in hardware but unusable) - `0b00` = 16450 (no FIFO at all) The original FCR value is restored after probing. | Constant | Value | Description | |---------------------|-------|----------------------------------------| | `RS232_UART_UNKNOWN`| 0 | Unknown or undetected | | `RS232_UART_8250` | 1 | Original IBM PC -- no FIFO, no scratch | | `RS232_UART_16450` | 2 | Scratch register present, no FIFO | | `RS232_UART_16550` | 3 | Broken FIFO (rare, unusable) | | `RS232_UART_16550A` | 4 | Working 16-byte FIFO (most common) | On 16550A UARTs, the FIFO trigger threshold is configurable via `rs232SetFifoThreshold()` with levels of 1, 4, 8, or 14 bytes. The default is 14, which minimizes interrupt overhead at high baud rates. ## IRQ Auto-Detection When `rs232Open()` is called without a prior `rs232SetIrq()` override, the library auto-detects the UART's IRQ by: 1. Saving the current PIC interrupt mask registers (IMR) 2. Enabling all IRQ lines on both PICs 3. Generating a TX Hold Empty interrupt on the UART 4. Reading the PIC's Interrupt Request Register (IRR) to see which line went high 5. Disabling the interrupt, reading IRR again to mask out persistent bits 6. Re-enabling once more to confirm the detection 7. Restoring the original PIC mask If auto-detection fails (common on virtualized hardware that does not model the IRR accurately), the library falls back to the default IRQ for the port (IRQ4 for COM1/COM3, IRQ3 for COM2/COM4). The BIOS Data Area (at segment 0x0040) is read to determine each port's I/O base address. Ports not configured in the BDA are unavailable. ## COM Port Support | Constant | Value | Default IRQ | Default Base | |--------------|-------|-------------|--------------| | `RS232_COM1` | 0 | IRQ 4 | 0x3F8 | | `RS232_COM2` | 1 | IRQ 3 | 0x2F8 | | `RS232_COM3` | 2 | IRQ 4 | 0x3E8 | | `RS232_COM4` | 3 | IRQ 3 | 0x2E8 | Base addresses are read from the BIOS Data Area at runtime. The default IRQ values are used only as a fallback when auto-detection fails. Both the base address and IRQ can be overridden before opening with `rs232SetBase()` and `rs232SetIrq()`. ## Supported Baud Rates All standard rates from 50 to 115200 bps are supported. The baud rate divisor is computed from the standard 1.8432 MHz UART crystal: | Rate | Divisor | Rate | Divisor | |--------|---------|--------|---------| | 50 | 2304 | 4800 | 24 | | 75 | 1536 | 7200 | 16 | | 110 | 1047 | 9600 | 12 | | 150 | 768 | 19200 | 6 | | 300 | 384 | 38400 | 3 | | 600 | 192 | 57600 | 2 | | 1200 | 96 | 115200 | 1 | | 1800 | 64 | | | | 2400 | 48 | | | | 3800 | 32 | | | Data bits (5-8), parity (N/O/E/M/S), and stop bits (1-2) are configured by writing the appropriate LCR (Line Control Register) bits. ## API Reference ### Error Codes | Constant | Value | Description | |-------------------------------|-------|--------------------------| | `RS232_SUCCESS` | 0 | Success | | `RS232_ERR_UNKNOWN` | -1 | Unknown error | | `RS232_ERR_NOT_OPEN` | -2 | Port not open | | `RS232_ERR_ALREADY_OPEN` | -3 | Port already open | | `RS232_ERR_NO_UART` | -4 | No UART detected at base | | `RS232_ERR_INVALID_PORT` | -5 | Bad port index | | `RS232_ERR_INVALID_BASE` | -6 | Bad I/O base address | | `RS232_ERR_INVALID_IRQ` | -7 | Bad IRQ number | | `RS232_ERR_INVALID_BPS` | -8 | Unsupported baud rate | | `RS232_ERR_INVALID_DATA` | -9 | Bad data bits (not 5-8) | | `RS232_ERR_INVALID_PARITY` | -10 | Bad parity character | | `RS232_ERR_INVALID_STOP` | -11 | Bad stop bits (not 1-2) | | `RS232_ERR_INVALID_HANDSHAKE` | -12 | Bad handshaking mode | | `RS232_ERR_INVALID_FIFO` | -13 | Bad FIFO threshold | | `RS232_ERR_NULL_PTR` | -14 | NULL pointer argument | | `RS232_ERR_IRQ_NOT_FOUND` | -15 | Could not detect IRQ | | `RS232_ERR_LOCK_MEM` | -16 | DPMI memory lock failed | ### Handshaking Modes | Constant | Value | Description | |---------------------------|-------|----------------------| | `RS232_HANDSHAKE_NONE` | 0 | No flow control | | `RS232_HANDSHAKE_XONXOFF` | 1 | Software (XON/XOFF) | | `RS232_HANDSHAKE_RTSCTS` | 2 | Hardware (RTS/CTS) | | `RS232_HANDSHAKE_DTRDSR` | 3 | Hardware (DTR/DSR) | ### Open / Close ```c int rs232Open(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake); ``` Opens a COM port. Reads the UART base address from the BIOS data area, auto-detects the IRQ, locks ISR memory via DPMI, installs the ISR, and configures the UART for the specified parameters. Returns `RS232_SUCCESS` or an error code. - `com` -- port index (`RS232_COM1` through `RS232_COM4`) - `bps` -- baud rate (50 through 115200) - `dataBits` -- 5, 6, 7, or 8 - `parity` -- `'N'` (none), `'O'` (odd), `'E'` (even), `'M'` (mark), `'S'` (space) - `stopBits` -- 1 or 2 - `handshake` -- `RS232_HANDSHAKE_*` constant ```c int rs232Close(int com); ``` Closes the port, disables UART interrupts, removes the ISR, restores the original interrupt vector, and unlocks DPMI memory (when the last port closes). ### Read / Write ```c int rs232Read(int com, char *data, int len); ``` Non-blocking read. Drains up to `len` bytes from the RX ring buffer. Returns the number of bytes actually read (0 if the buffer is empty). If flow control is active and the buffer drops below the low-water mark, the ISR will re-enable receive from the remote side. ```c int rs232Write(int com, const char *data, int len); ``` Blocking polled write. Sends `len` bytes directly to the UART THR (Transmit Holding Register), bypassing the TX ring buffer entirely. Polls LSR for THR empty before each byte. Returns `RS232_SUCCESS` or an error code. ```c int rs232WriteBuf(int com, const char *data, int len); ``` Non-blocking buffered write. Copies as many bytes as will fit into the TX ring buffer. The ISR drains the TX buffer to the UART automatically. Returns the number of bytes actually queued. If the buffer is full, some bytes may be dropped. ### Buffer Management ```c int rs232ClearRxBuffer(int com); int rs232ClearTxBuffer(int com); ``` Discard all data in the receive or transmit ring buffer by resetting head and tail pointers to zero. ### Getters ```c int rs232GetBase(int com); // UART I/O base address int32_t rs232GetBps(int com); // Current baud rate int rs232GetCts(int com); // CTS line state (0 or 1) int rs232GetData(int com); // Data bits setting int rs232GetDsr(int com); // DSR line state (0 or 1) int rs232GetDtr(int com); // DTR line state (0 or 1) int rs232GetHandshake(int com); // Handshaking mode int rs232GetIrq(int com); // IRQ number int rs232GetLsr(int com); // Line Status Register int rs232GetMcr(int com); // Modem Control Register int rs232GetMsr(int com); // Modem Status Register char rs232GetParity(int com); // Parity setting ('N','O','E','M','S') int rs232GetRts(int com); // RTS line state (0 or 1) int rs232GetRxBuffered(int com); // Bytes waiting in RX buffer int rs232GetStop(int com); // Stop bits setting int rs232GetTxBuffered(int com); // Bytes waiting in TX buffer int rs232GetUartType(int com); // UART type (RS232_UART_* constant) ``` Most getters return cached register values from the per-port state structure, avoiding unnecessary I/O port reads. `rs232GetUartType()` actively probes the hardware (see UART Type Detection above). ### Setters ```c int rs232Set(int com, int32_t bps, int dataBits, char parity, int stopBits, int handshake); ``` Reconfigure all port parameters at once. The port must already be open. ```c int rs232SetBase(int com, int base); // Override I/O base (before open) int rs232SetBps(int com, int32_t bps); // Change baud rate int rs232SetData(int com, int dataBits); // Change data bits int rs232SetDtr(int com, bool dtr); // Assert/deassert DTR int rs232SetFifoThreshold(int com, int thr); // FIFO trigger (1, 4, 8, 14) int rs232SetHandshake(int com, int handshake); // Change flow control mode int rs232SetIrq(int com, int irq); // Override IRQ (before open) int rs232SetMcr(int com, int mcr); // Write Modem Control Register int rs232SetParity(int com, char parity); // Change parity int rs232SetRts(int com, bool rts); // Assert/deassert RTS int rs232SetStop(int com, int stopBits); // Change stop bits ``` ## Usage Example ```c #include "rs232.h" int main(void) { // Open COM1 at 115200 8N1, no flow control int rc = rs232Open(RS232_COM1, 115200, 8, 'N', 1, RS232_HANDSHAKE_NONE); if (rc != RS232_SUCCESS) { return 1; } // Identify the UART chip int uartType = rs232GetUartType(RS232_COM1); // uartType == RS232_UART_16550A on most 486+ systems // Enable 16550A FIFO with trigger at 14 bytes if (uartType == RS232_UART_16550A) { rs232SetFifoThreshold(RS232_COM1, 14); } // Blocking send rs232Write(RS232_COM1, "Hello\r\n", 7); // Non-blocking receive loop char buf[128]; int n; while ((n = rs232Read(RS232_COM1, buf, sizeof(buf))) > 0) { // process buf[0..n-1] } rs232Close(RS232_COM1); return 0; } ``` ## Implementation Notes - The single shared ISR handles all four COM ports. On entry it disables UART interrupts for all open ports on the PIC, then re-enables CPU interrupts (STI) so higher-priority devices (timer, keyboard) are serviced promptly. - Ring buffers use power-of-2 sizes (2048 bytes) with bitmask indexing for zero-branch wraparound. Each port uses 4KB total (2KB RX + 2KB TX). - Flow control watermarks are at 80% (assert stop) and 20% (deassert stop) of buffer capacity. These percentages are defined as compile-time constants and apply to both RX and TX directions. - DPMI `__dpmi_lock_linear_region` is used to pin the ISR code, ring buffers, and port state in physical memory. The ISR code region is locked for 2048 bytes starting at the `comGeneralIsr` function address. - `rs232Write()` is a blocking polled write that bypasses the TX ring buffer entirely. It writes directly to the UART THR register, polling LSR for readiness between each byte. `rs232WriteBuf()` is the non-blocking alternative that queues into the TX ring buffer for ISR draining. - Per-port state is stored in a static array of `Rs232StateT` structures (`sComPorts[4]`). This array is locked in physical memory alongside the ISR code. - The BIOS Data Area (real-mode address 0040:0000) is read via DJGPP's far pointer API (`_farpeekw`) to obtain port base addresses at runtime. ## Building ``` make # builds ../lib/librs232.a make clean # removes objects and library ``` Cross-compiled with the DJGPP toolchain targeting i486+ CPUs. Compiler flags: `-O2 -Wall -Wextra -march=i486 -mtune=i586`. Objects are placed in `../obj/rs232/`, the library in `../lib/`. ## Files - `rs232.h` -- Public API header - `rs232.c` -- Complete implementation (ISR, DPMI, ring buffers, UART I/O) - `Makefile` -- DJGPP cross-compilation build rules ## Used By - `packet/` -- Packetized serial transport layer (HDLC framing, CRC, ARQ) - `seclink/` -- Secure serial link (opens and closes the COM port) - `proxy/` -- Linux serial proxy (uses a socket-based shim of this API)