A Packet Driver Specification (Crynwr/FTP Software style, INT 60h) driver for the Realtek RTL8169 gigabit Ethernet chip, written for 16-bit real-mode DOS.
Tested on real hardware with mTCP (DHCP and ping working over 10/100/1000 Mbit auto-negotiated links) and EtherDFS (a raw-Ethernet-frame network redirector, no TCP/IP - large file transfers worked without issues). See "Known limitations" below before relying on this for anything serious.
- PCI bus detection via the PCI BIOS (INT 1Ah).
- Full RTL8169 initialisation: descriptor-ring based TX/RX (bus-master DMA, not the older RTL8139 ring-buffer style), interrupt handling, IRQ8-15 cascade handling.
- 10/100/1000 Mbit auto-negotiation (no forced speed - see "Design notes" for why).
- A minimal, functional subset of the Packet Driver Specification:
driver_info,access_type,release_type,send_pkt,terminate,get_address,reset_interface,get_statistics- enough for a single TCP/IP stack such as mTCP or WatTCP. - A safe, spec-compliant
-uuninstall option that unhooks the driver and frees all of its memory (including the DMA descriptor rings and packet buffers, which live outside the normal resident program image). - No
<stdio.h>/<stdlib.h>dependency, to keep the resident footprint small.
- A Realtek RTL8169 (PCI vendor:device
10ec:8169) network card. - Open Watcom v2, 16-bit
DOS target (
wcl, notwcl386). - DOS or a DOS-compatible environment (tested with MS-DOS version 7.10). See "Testing" below for why you need real hardware, not an emulator.
cd src
wmake
This produces two executables, both placed in the repository ROOT (not
src/, where only the source and intermediate .obj files live):
RTL8169P.EXE (the stripped-down "production" build - only load-time
options plus -u) and RTLDEBUG.EXE (the full-featured build, adding
-s/-d). See "Usage" below and "Design notes" in DESIGN.md for why
this split exists. See src/Makefile for the exact compiler flags
(-3 -ms -bt=dos, i.e. 386 instructions, small memory model, DOS
target). All source (.c/.h) and the Makefile live under src/;
this file, DESIGN.md and FILE_ID.DIZ stay in the repository root, so
they render normally on GitHub's repo landing page.
The build also produces RTL8169P.ZIP in the repository root (Windows
only, via PowerShell's Compress-Archive - see src/Makefile): a
ready-to-distribute archive containing RTL8169P.EXE plus this file,
DESIGN.md, FILE_ID.DIZ and LICENSE. RTLDEBUG.EXE is deliberately
left out of it - it is the developer/diagnostic build, not part of the
normal distribution.
Open the repository ROOT as the workspace folder in VS Code (so
README.md/DESIGN.md are visible alongside src/, and Claude Code
picks up .claude/CLAUDE.md automatically) - a build task is already
set up in .vscode/tasks.json: press Ctrl+Shift+B to run the
equivalent of the commands above directly, with compiler errors/
warnings parsed into the Problems panel. This requires Open Watcom v2
for Windows on PATH (wcl/wmake) - only the compiler itself, not
DOSBox-X or any DOS environment, since those tools are ordinary native
Windows executables; DOSBox-X (or real hardware) is only needed to
actually run the resulting DOS .EXE.
Load the driver with RTL8169P.EXE (smaller resident footprint) or
RTLDEBUG.EXE (same load behaviour, plus -s/-d for later use).
Either executable can then send -u/-s/-d to that running instance
regardless of which one loaded it - the packet-driver protocol calls
underneath are identical, so RTLDEBUG.EXE -s 0x60 works whether the
driver was loaded via RTL8169P.EXE or RTLDEBUG.EXE itself.
RTL8169P [-t n] [-r n] [-g us] [-m hex] <packet_int_no>
RTL8169P -u <packet_int_no>
RTLDEBUG [-t n] [-r n] [-c kb] [-g us] [-m hex] <packet_int_no>
RTLDEBUG -u <packet_int_no>
RTLDEBUG -s <packet_int_no>
RTLDEBUG -d <file> <packet_int_no>
packet_int_no- the software interrupt to use, as a hexadecimal value in the range0x60-0x80(the range reserved by the Packet Driver Specification). Required - the driver refuses to guess a default and will print usage instead.-u- uninstall the driver from the given interrupt and free its memory. Available in both executables.-t n/-r n- number of TX/RX descriptors (and matching packet buffers) to allocate, from 1 up toRTL_MAX_TX_DESC/RTL_MAX_RX_DESC(32 each by default inrtl8169p.h). Each one costsRTL_BUF_SIZE(1536) bytes of resident memory - the single biggest lever over this driver's memory footprint. Defaults (RTL_DEFAULT_TX_DESC/RTL_DEFAULT_RX_DESC, 4/8) were chosen and validated as described below - notably, TX depth matters more than it first appears to: a too-shallow TX ring does not lose data in THIS driver directly, but can exhaust mTCP's own limitedsend_pkt()retry budget, causing a real, mTCP-visible send failure (confirmed:-t 2showed real loss in mTCP's own reporting,-t 4did not); going lower on-rrisks dropped packets under real load, though a DEEPER-ris not simply safer either (see the long comment atRTL_DEFAULT_RX_DESCinrtl8169p.h) - always check with-safter a representative test before trusting a value other than the confirmed default in production use.-s- print packet/byte/error counters (get_statistics()) from a running driver instance.RTLDEBUG.EXEonly - see "Design notes" inDESIGN.mdfor why this was moved out ofRTL8169P.EXE. Works against a driver instance loaded by either executable. Useful for confirming whether packet loss under real traffic is coming from RX ring overflow - see "Known limitations" below. Works even while another application (e.g. mTCP) currently holds the driver's handle - this deliberately does not follow the spec's strict per-handle access check, since DOS is single-tasking and there is no other way to inspect counters while a transfer is stuck. See the comment above theget_statistics()case inpktdrv.cfor the reasoning.-c kb- enable a raw-frame capture ring buffer of the given size in KB (1-CAP_MAX_KB, 64 by default inrtl8169p.h; off unless given, so it costs no memory otherwise). Available in both executables (the actual recording code must stay resident either way - seertl_capture_frame()inrtl8169p.c), but only listed inRTLDEBUG.EXE's own usage text, since only that executable also has-dto read a capture back out -RTL8169P.EXEaccepts-ctoo if you type it, but offers no way to retrieve the result, so advertising it there would be pointless. Every TX and RX frame is recorded (timestamp, direction, up toCAP_SNAPLENbytes) into a fixed-size ring that silently overwrites its oldest entries once full - handy for catching what was happening right before a stall, without knowing in advance exactly when to start capturing.-d file- write a running driver's capture buffer (see-c) out as a standard.pcapfile, directly readable in Wireshark.RTLDEBUG. EXEonly. Like-s, this works even while another application still holds the driver's handle - dump it right after a transfer stalls, no need to unload or disturb anything. Timestamps use the BIOS clock tick (~55ms resolution) - coarse, but enough to see multi-second stalls; only relative timing between packets is meaningful, the absolute date/time in the resulting file is not real.-g us- delay, in microseconds, inserted between bursty, back-to- back RX frames (0-RTL_MAX_BURST_GAP_US, defaultRTL_DEFAULT_BURST_GAP_US= 0 inrtl8169p.h). Together with-mbelow, this is the confirmed fix for a data-corruption bug described inDESIGN.md. The default of 0 (this delay effectively off) relies on-m's own default (0x5151) to do the equivalent job via hardware interrupt coalescing instead - the two are a validated PAIR, not independent settings. Disabling BOTH (-g 0 -m 0) reintroduces the original bug; the driver prints a warning at load time if you do. Earlier, standalone-delay values (300us, then 100us) were also confirmed stable on their own, before-mexisted - see the long comment atRTL_DEFAULT_BURST_GAP_USinrtl8169p.hfor the full value history if you want to use-gwithout-m.-m hex- raw 16-bit value written directly to the RTL8169'sIntrMitigateregister (interrupt coalescing/moderation), e.g.-m 0x5151. DefaultRTL_DEFAULT_INTR_MITIGATE=0x5151inrtl8169p.h, paired with-gdefaulting to 0 (see above) - confirmed stable via 20+ repeated real-world transfers of a file that had reliably reproduced the original corruption, across 6 full power- cycles/reloads, with zero loss at both the driver level (-s) and mTCP's own reporting. Especially beneficial on weak retro CPUs (e.g. a 466MHz Celeron): every interrupt has a largely CPU-speed-independent fixed overhead, which weighs proportionally far more there than on modern hardware, so fewer, larger interrupts is a genuine efficiency win. Two other known values worth knowing about, decoded as TX_USECS/TX_FRAMES/RX_USECS/RX_FRAMES:0x5151(5/1/5/1, this driver's default) - originally found in the older Linuxr8169.c's RTL8168 (not RTL8169) init path.0x5100(5/1/0/0, RX coalescing disabled, TX-only) - what upstream Linux later changed0x5151to, after a report that combining RX coalescing with ASPM (PCIe active-state power management) increased packet latency. That specific concern is Linux/ASPM-driver-state specific and has not been observed to apply here - DOS has no equivalent active ASPM management - which is why this driver keeps the RX-inclusive0x5151as its default rather than following upstream to0x5100; the latter remains available to try via-mif you want to compare. See the long comment atRTL_DEFAULT_INTR_MITIGATEinrtl8169p.hfor the register's full field layout and the per-speed timing table.
Example:
RTL8169P 0x60
...
RTLDEBUG -s 0x60
...
RTL8169P -u 0x60
Running the driver with no arguments, or with an argument that is not a valid interrupt number in the required range, prints usage information and exits without touching any hardware.
No common PC emulator emulates the RTL8169. QEMU, VirtualBox, VMware
and 86Box all emulate NE2000- and/or RTL8139-family cards, but none of
them emulate the RTL8169's descriptor-ring/bus-master DMA interface. This
driver can only be tested on real hardware. A second machine on the same
network segment (running Wireshark, or just ping) is very useful for
diagnosing TX/RX issues independently of the DOS side.
For isolating whether a problem is in this driver's raw TX/RX path versus in a TCP/IP stack layered on top, testing with EtherDFS (raw Ethernet frames, no TCP/IP at all) alongside mTCP is a useful cross-check - if EtherDFS transfers large files cleanly while mTCP shows a problem, that points away from this driver's own send/receive mechanics.
- Single handle/interface only - enough for one TCP/IP stack (mTCP/WatTCP), not several packet-driver clients at once.
- Link changes are not monitored after startup - checked once, at load time.
- No jumbo frames, VLAN, or checksum offload.
- No per-chip-revision workarounds - one generic init sequence, tested against one specific card (RTL8169, not RTL8168/RTL8110).
- Gigabit throughput is architecturally limited - no interrupt coalescing; do not expect to saturate a gigabit link from DOS with this driver.
- EEPROM MAC address is not bit-banged - trusts the MAC0-5 I/O registers the card's firmware/BIOS loads on power-up.
- Resident memory size is requested as a fixed 64KB ceiling, not
computed from the actual program size - this does not mean 64KB is
actually kept resident (see
DESIGN.md).
See DESIGN.md for the full reasoning behind each of these,
and the design/debugging history behind this driver's implementation
choices.
| File | Contents |
|---|---|
src/main.c |
Command-line front end for RTL8169P.EXE (stripped down) |
src/rtldebug.c |
Command-line front end for RTLDEBUG.EXE (full-featured) |
src/pci.c/.h |
PCI BIOS (INT 1Ah) detection and configuration access |
src/rtl8169p.c/.h |
Chip register map, init, TX/RX descriptor rings, PHY/MDIO |
src/pktdrv.c/.h |
INT 60h dispatcher, hardware IRQ ISR, RX upcall to the application |
src/Makefile |
Open Watcom build file (builds ../RTL8169P.EXE, ../RTLDEBUG.EXE, ../RTL8169P.ZIP) |
RTL8169P.EXE |
Build output (see src/Makefile) |
RTLDEBUG.EXE |
Build output (see src/Makefile) |
.claude/CLAUDE.md |
Developer-facing project context, auto-loaded by Claude Code |
.vscode/ |
VS Code build task (Ctrl+Shift+B) - see "Building" above |
.gitignore |
Excludes *.obj (intermediate build files) |
DESIGN.md |
Detailed design notes and debugging/investigation history |
FILE_ID.DIZ |
Short BBS/archive-style description of the project |
LICENSE |
GNU General Public License v3.0 (full text) |
- Linux
drivers/net/ethernet/realtek/r8169_main.c- register map, init sequence, descriptor bit layout. - U-Boot
drivers/net/rtl8169.c- a simpler, bare-metal-style reference implementation. - OSDev.org RTL8169 wiki page.
- Realtek RTL8110S/RTL8169S datasheet, v1.3.
- QEMU's
hw/net/rtl8139.c(also used by 86Box's RTL8139C+ emulation) - useful for cross-checking the C+-mode descriptor bit layout, which the RTL8169 shares in spirit. - Packet Driver Specification (FTP Software / Russ Nelson / Crynwr, v1.11) - the INT 60h API this driver implements.
Copyright (C) 2026 Yoghoo
GNU General Public License v3.0 or later. See LICENSE.