An IBM PC/XT on the ESP32: 8086 at about six times XT speed, up to 704 KB of RAM, CGA, EGA or Tandy 1000 video on a VGA monitor, PC speaker, Tandy sound and an AdLib, floppy and hard disk images on the SD card or downloaded over Wi-Fi.
ESPPC started as the PCEmulator example of FabGL by Fabrizio
Di Vittorio and has grown well beyond it with the help of Claude.
FabGL is still underneath it for VGA output, PS/2, sound and the emulated chips,
vendored in lib/FabGL with every local change listed in
lib/FabGL/VENDORED.md.
I had the board lying around and wanted to see how far it could be taken. Also, the example project this is based on had a bunch of annoying bugs, which are squashed now.
I could have written all of it myself, but PC emulation has been done to death already, there is nothing new to invent here,
and I don't think most people will mind an enhanced port done by an AI under watchful human eyes. And Claude did a very good job here. If I had done all that the old fashioned way, it would have taken me weeks of full time work. Maybe I could have gotten this to a Topbench score of 18 or 19, but sometimes good enough is good enough.
As of 30th of October i am eating my own words here, Topbench of 31 for now. And Checkit says 6.3 times as fast as an original IBM XT.
Also, this is an old MCU. The first time I saw this exact chip run emulation was on the venerable ODROID-GO, and there are other projects out there for better MCUs; heck, someone made a 386 fit into an RP2350 with PSRAM. But if you have such an old ESP32 with VGA and an SD card lying around, take this project for a spin. It is still good enough for some old games, or for writing your novel on if you want. If you can get such a board cheaply, grab it, but don't pay more than around 30€ for one, including taxes and shipping.
Pull requests are welcome of course.
- CPU: 8086, Topbench 31 to 32 with CGA, EGA or Tandy video, and about six times an IBM XT
in CheckIt (up from Topbench 11 in the original example). Checked against the SingleStepTests
8088 suite, see
tools/cputest/. - Memory: 64 to 704 KB of conventional memory. 704 KB is the classic non-IBM XT configuration: video moves up to B000 and the A000 segment becomes ordinary RAM.
- Video: per machine:
- CGA, with the Plantronics ColorPlus modes, and optionally a composite monitor that shows the NTSC artifact colours (160x200 in 16 colours for games that offer it). The 6845 is followed closely enough for the tweaked text modes (80x100, the 160x100x16 of Paku Paku and others), hardware scrolling, display pages, the border colour and 59.92 Hz retrace timing.
- EGA, driven by a real IBM EGA BIOS option ROM (
data/fs/ega.bin, seedata/README.md): the 200 and 350 line modes, split screen, the 6 bit palette. - Tandy 1000, with its 16 colour modes 8 and 9 and its three voice sound chip.
- Sound: PC speaker; the Tandy SN76496 at C0h (also as a TNDY style card on CGA or EGA); the Tandy SL/TL DAC for Sierra speech and effects; an AdLib (OPL2, DOSBox-X's DBOPL), synthesised on the second core.
- Chipset: 8259 PIC, 8253 PIT, 8237 DMA controller (with the DRAM refresh counting on channel 0), 8042 keyboard controller, an MC146818 RTC (set from NTP at power on), or instead the MM58174 clock SCO Xenix reads, or no clock at all; two 8250 serial ports, a PS/2 mouse.
- Disks: two floppies and two hard disks as image files. Geometry is guessed from the image size or given by hand. Images can be on the SD card, built into the flash file system (read only), or a URL that is downloaded to the card on first boot. DOS and games go through the BIOS; for systems with their own disk drivers (SCO Xenix 86) a machine can also have the NEC uPD765 floppy controller and the IBM XT hard disk controller, with DMA.
Developed on the Olimex ESP32-SBC-FabGL: ESP32 WROVER with PSRAM, SD card slot, VGA, PS/2 keyboard and mouse, audio out, and the U1 header for a serial port. Any ESP32 board with PSRAM, an SD card slot and FabGL's VGA wiring (such as the TTGO VGA32 v1.4) should run it, but only the Olimex board has been tested.
The board needs at least 4 MB PSRAM (the guest's RAM lives there) and 4 MB of flash.
The builds are for ESP32 revision 3 chips (ESP32-D0WD-V3, as on current WROVER-E modules),
which have the PSRAM cache bug of the earlier revisions fixed in silicon. On an older chip
(esptool.py chip_id reports revision v1.x or v2.x) that firmware stops at boot; there is a build
with the workaround for those, a little slower: the second button in the installer, or
pio run -e esp32old -t upload (sdkconfig.oldchip on top of sdkconfig.defaults).
Without building anything: open the installer in Firefox, Chrome or Edge, plug in the board and click Install. It flashes the latest release, firmware and flash file system.
To build it yourself, with PlatformIO:
pio run -t upload # the firmware (~/.platformio/penv/bin/pio if pio is not on the PATH)
pio run -t uploadfs # the flash file system: the EGA BIOS and an optional rescue disk
pio device monitor # serial console, 115200 baudThe two uploads are independent: flashing the firmware leaves the file system alone and the other
way round. data/README.md explains what goes into the file system and how much fits.
The firmware is built on plain ESP-IDF 5.2 (platform = espressif32@6.7.0,
framework = espidf), without the Arduino core: the few Arduino calls the code makes are in
src/compat.*. The IDF's settings are in sdkconfig.defaults; after changing it, delete the
generated sdkconfig.esp32dev so that it is made again. Two of them matter most:
- PSRAM is mapped by
setup()itself (esp_psram_init()), not at boot and not for the heap: the guest's RAM starts at its first byte. - No PSRAM cache workaround, which only ESP32s before revision 3 need and which slows the emulator down: the firmware requires revision 3 (see Hardware above).
The other environments in platformio.ini (psrambench, egabench, sdbench, dltest,
uarttest) are test programs for the board, not the emulator. They are still Arduino programs.
At power on the board runs a POST screen, sets the clock over Wi-Fi and shows the machine list. The first time, it asks for a Wi-Fi network and its password; both are kept in the flash.
Machine list
| Key | |
|---|---|
| Enter | boot the selected machine |
| F2 / F3 | edit it / set up a new one |
| Del | remove it from the list (the images stay on the card) |
| F4 | file browser: rename, copy, paste, new folder, delete on the SD card |
| F5 | advanced settings: time server, time zone, auto boot countdown, drive LED |
| F6 | add machines from a repository (below) |
The machine editor sets the disk images, boot drive, memory, video adapter, sound cards and serial ports, with a line of help for each field. F9 creates an empty hard disk image of a chosen size, F10 saves.
Inside a machine, Alt+PrtScr (SysReq) opens the system menu: change or eject the floppy in A: or B:, switch a CGA machine between an RGB and a composite monitor, restart the guest, restart the board, or go back to the guest.
mconfs.txt in the root of the card holds the machine list, one machine per line. The editor
writes it, but it is plain text:
desc "FreeDOS" hd0 "/SD/dos/freedos.img" boot hd0 ram 640 video ega adlib 1
desc "Rescue" fd0 "/spiffs/rescue.img" boot fd0
| Tag | |
|---|---|
desc |
the name in the machine list |
fd0 fd1 hd0 hd1 |
disk images: a path on the card, /spiffs/... for the flash, or an http(s):// URL |
chs0 chs1 |
hard disk geometry as cylinders,heads,sectors, if the guess is wrong |
boot |
fd0 (default), fd1, hd0 or hd1 |
ram |
KB of conventional memory, 64 to 704 in steps of 64, default 640 |
video |
cga (default), ega or tandy |
egarom |
another EGA BIOS than /spiffs/ega.bin |
egamem |
128 for a 128 KB EGA; the default is 256 KB, as games expect (the upper half lives in PSRAM) |
composite |
1 for a composite colour monitor on CGA |
tandysound |
1 for the SN76496 on CGA or EGA (Tandy video always has it) |
tandydac |
1 for the Tandy SL/TL DAC |
adlib |
1 for an AdLib at 388h |
hwdisk |
1 adds the floppy and XT hard disk controllers for systems with their own disk drivers |
clock |
at (default, MC146818), mm58174 (the clock SCO Xenix 86 reads) or none |
usbserial |
com1 or com2: that port goes over the USB serial |
hwserial |
com1 or com2: that port is on the U1 header (pin 5 TXD, 6 RXD, 3 GND, 3.3 V only) |
enabledebug |
1 for the serial debug console |
The comment at the top of src/mconf.h has more examples.
You can install a full OS with nothing but the board and your favourite XT compatible OS as a set of disk images.
On Linux, at least, you can mount the hard disk image to fill it with software or to make backups. Don't forget the -w option when using the GNOME Image Mounter.
A repository is a web server with a files.list of ready made machines. F6 asks for its URL,
shows what it offers, and adds the chosen machines; their images are downloaded on first boot.
http://lionscade.de/pcimages has FreeDOS and SvarDOS to start with. How to offer one is in
docs/repositories.md.
With enabledebug 1, the USB serial line becomes a debug console. Esc Esc Esc mirrors the
text screen to the terminal and types into the guest; Ctrl+C Ctrl+D freezes the guest and
opens a monitor: registers, memory dump and edit, I/O ports, breakpoints, single stepping, port
traces, screenshots, a heap map and a per opcode CPU profile. ? lists the commands, g goes
back to the guest. Without the console, the USB serial line is the guest's COM1.
tools/screenshot.pysaves the guest's screen as a PNG over the console.tools/postbox.pyandtools/postbox.commove files between the PC and the guest through a private channel on the same line, no COM port needed. See the note aboveclass Machineinsrc/machine.h.tools/cputest/runs the 8086 core on the PC against the SingleStepTests suite.tests/holds the emulator's tests: small boot floppies of their own (timer interrupts, CGA retrace timing, display pages, the CGA's picture, the keyboard, a reboot from inside an interrupt), booted headless bytests/run.shwithpctool. GitHub runs them on every push, with a check thatsrc/biosrom.hmatchestools/bios.asm.host/builds the whole emulator on a Linux PC (make -C host):host/pctoolboots a machine from disk images headless and runs a script, typing on the keyboard, dumping the text screen and memory, and writing screenshots from the emulator's own renderers. Time is virtual, so a run repeats exactly and takes a fraction of a second. The comment at the top ofhost/main.cpplists the commands.
You can also use that same line as a normal serial line. I tested it with a DOS terminal program, with my Linux box running a login console on ttyUSB0. The only drawback I found was that TERM90 doesn't like Unicode; getting that to work is beyond this document, though.
The machine's BIOS is 8086tiny's, much extended. src/biosrom.h is generated from
tools/bios.asm and checked in, so a normal build needs no assembler. After editing the
assembly:
tools/cbios.sh # needs nasm and xxdsrc/main.cpp setup, Wi-Fi and clock, the machine list, the SysReq menu
src/machine.* the PC: CPU, chipset, video, disks, timing
src/bios.* BIOS services done natively; biosrom.h is built from tools/bios.asm
src/ega.* the EGA planar memory engine
src/fdc.* hdc.* the floppy and XT hard disk controllers ("hwdisk")
src/mm58174.* the MM58174 clock ("clock mm58174")
src/sn76496.* tandydac.* adlib.* opl/ sound: Tandy chip, Tandy DAC, AdLib (DBOPL)
src/textui.* setupui.* the host's text mode screens: POST, machine editor, file browser
src/mconf.* mconfs.txt
src/repository.* repolist.* httpopen.* machine repositories and downloads
src/serialdbg.* the serial debug console and monitor
lib/FabGL/ FabGL 1.0.9, vendored and changed, see VENDORED.md
data/fs/ the flash file system: EGA BIOS, rescue disk
tools/ BIOS source, CPU tests, host side tools
host/ the emulator built on a PC, headless (pctool)
docs/ design notes and the to do list
ESPPC is GPL v3 or later, as FabGL is (see LICENSE). The AdLib emulation is DOSBox-X's DBOPL (GPL v2 or later). The BIOS
is based on 8086tiny's (MIT). data/fs/ega.bin is IBM's EGA BIOS, included as emulators such as
86Box have done for years; its copyright is IBM's.