417 lines
16 KiB
Markdown
417 lines
16 KiB
Markdown
# 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)
|