DVX_GUI/rs232/README.md
2026-03-20 20:00:05 -05:00

16 KiB

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

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
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

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.

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.

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

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

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

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.

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

#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)