| .. | ||
| Makefile | ||
| README.md | ||
| rs232.c | ||
| rs232.h | ||
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:
- Mask all COM port IRQs on the PIC to prevent ISR re-entry
- STI to allow higher-priority interrupts (timer tick, keyboard) through
- Loop over all open ports, draining each UART's pending interrupt conditions (data ready, TX hold empty, modem status, line status)
- 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:
-
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.
-
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:
- Saving the current PIC interrupt mask registers (IMR)
- Enabling all IRQ lines on both PICs
- Generating a TX Hold Empty interrupt on the UART
- Reading the PIC's Interrupt Request Register (IRR) to see which line went high
- Disabling the interrupt, reading IRR again to mask out persistent bits
- Re-enabling once more to confirm the detection
- 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_COM1throughRS232_COM4)bps-- baud rate (50 through 115200)dataBits-- 5, 6, 7, or 8parity--'N'(none),'O'(odd),'E'(even),'M'(mark),'S'(space)stopBits-- 1 or 2handshake--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_regionis 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 thecomGeneralIsrfunction 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
Rs232StateTstructures (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 headerrs232.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)