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