tiny386 boots Windows 9x on an ESP32 with a 6K line CPU core
tiny 386 PC emulator; running win9x on esp32
At a glance
- What is it?
- tiny386 is an x86 PC emulator written in C99 whose real achievement is portability, since it boots Windows 9x and NT on microcontrollers such as the ESP32-S3 using a hand written i386 core of roughly 6K lines. It suits anyone interested in bare metal PC emulation or retro systems, and it is a hobby project with known gaps in debugging and privilege checks.
- Who is it for?
- Adopt tiny386 if you are interested in bare metal PC emulation, want to run a 16 or 32 bit operating system on hardware that has no business running one, or want a small readable x86 core to study. Do not adopt it for production or for general purpose virtualisation, since the core is deliberately incomplete, with debugging, hardware tasking and some permission checks missing.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 5 days ago.
- What is it written in?
- Mainly C, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The portability is the achievement, not the CPU
Tiny386 is an x86 PC emulator written in C99, and the project states plainly that the highlight is its portability, since it now boots Windows 9x and NT on an MCU such as the ESP32-S3. That framing matters, because an x86 emulator is not unusual and the interesting part here is running one on a microcontroller with a few hundred megahertz of clock and a few megabytes of RAM. The core itself is described as a built-from-scratch, simple and stupid i386 cpu emulator, kept in about 6K lines of code. The author is candid that some features are missing, for example debugging, hardware tasking and some permission checks, and that it should nevertheless run most 16 and 32 bit software. To boot a modern Linux kernel and Windows, some 486 and 586 instructions were added. The core has since grown to support i686, MMX, SSE up to SSSE3 and amd64 in 64 bit long mode only, and the documentation says that as these features are optional they may be safely disregarded if not required.
The peripherals come from TinyEMU and QEMU
The system around the CPU is what makes a PC emulator a PC, and here the approach is to port rather than reinvent. The list is the familiar set of PC era hardware: the 8259 PIC for interrupts, the 8254 PIT for timing, the 8042 keyboard controller, the CMOS real time clock, ISA VGA with Bochs VBE, an IDE disk controller, a NE2000 ISA network card, the 8257 ISA DMA controller, a PC speaker, optional Adlib OPL2 sound, and SoundBlaster 16. The FPU is a separate concern, with an optional x87 fpu emulator alongside the main core. For firmware, the BIOS and VGA BIOS come from seabios, which is a decision that saves an enormous amount of work and ties the project to that project's licence terms. There is also a path that avoids the BIOS entirely, since Tiny386 supports booting a Linux kernel directly without a traditional BIOS, an idea taken from JSLinux, and it does that with a small stub called linuxstart. The tradeoff of this porting strategy is that the emulator inherits the behaviour and the limitations of code written for other emulators, and a bug that reads as a Tiny386 bug is often a TinyEMU or QEMU bug.
The configuration file is where the system gets described
Usage starts with an ini file that describes the machine, and the shape of it is a good map of what the emulator supports:
[pc]
; set path to BIOS and VGA BIOS
bios = bios.bin
vga_bios = vgabios.bin
; set memory size and VGA memory size
mem_size = 32M
vga_mem_size = 2M
; fda/fdb for floppy disks (optional)
fda = floppy.img
; hda/hdb/hdc/hdd for hard disks (optional)
; cda/cdb/cdc/cdd for CD-ROM disks (optional)
hda = win95.img
cdb = win95_cd.iso
; "fill_cmos" fixes "MS-DOS compatibility mode" in win9x, but it breaks winNT...
fill_cmos = 1
; force 8-dot mode (640 pixel wide in text mode) if set to 1
vga_force_8dm = 0
[display]
width = 720
height = 480
[cpu]
; gen = 3/4/5/6, for 386/486/586/686
gen = 3
; fpu = 0/1, to disable/enable x87
fpu = 0Three of those keys carry real operational weight. The gen setting picks the CPU generation, and the emulator supports 3 through 6. The fpu flag toggles the x87 unit. And fill_cmos is the one to know about, because it fixes the MS-DOS compatibility mode in Windows 9x while breaking Windows NT, which is the kind of setting that tells you the emulator is emulating a specific machine rather than a specification. There is also a vga_force_8dm option that forces 8-dot mode for 640 pixel wide text, and a display section for the window size. Running the emulator is then a single command:
./tiny386 config.ini
./tiny386 -kvm config.ini # run with KVM (build with `make USE_CPUABS=y`)The KVM path is a separate build flag rather than a runtime option, which is how it should be, since the host acceleration is a compile time choice.
Building it is a single gcc command, then a make per platform
The build is designed with simplicity in mind. For a headless executable with no optional features, one command is enough:
gcc -O3 *.c -o tiny386_headless -lmEverything beyond that is a make target with a platform specific prerequisite. On Linux with rawdraw you install libslirp, libx11 and libasound2 and run make. On Linux with SDL you install libslirp and SDL1.2, or sdl12-compat, and run make USE_SDL=y. On Windows you install mingw-w64 and run make win32. For WebAssembly you install clang and run cd wasm; make. Pre-built binaries are published on the releases page. The Makefile shows how the optional pieces are wired, with feature flags at the top of the file for glibc compatibility, SDL, and the Adlib implementation, where USE_FMOPL selects between the bundled fmopl.c and the Nuked OPL3 source tree, and where the CFLAGS line adds the FPU, MMX and SSE defines. The glibc flag is worth understanding, because the comment explains that a build with recent glibc can run on an older glibc, which is marked experimental.
The ESP32 port is the reason most people arrive
The ESP32 section is where the project's uniqueness sits, and the supported hardware is listed explicitly. With ESP-IDF 5.2.x, the boards are the JC3248W535, an ESP32-S3 at 480x320, and the Elecrow CrowPanel Advance 7.0 inch HMI, an ESP32-S3 at 800x480. With ESP-IDF 6.0.x, marked experimental, the JC4880P443, an ESP32-P4 Rev1.3 at 360MHz and 800x480, is supported. A pre-built flash image is published for the JC3248W535, esp/flash_image_JC3248W535.bin, and it can be flashed directly at offset 0, with an online flasher linked for the ESP chips. Building manually means applying the IDF patches with scripts/build.sh patch_idf, running make prepare, and then using idf.py with a board define to update dependencies, build and flash. Configuration on the device lives on an SD card with a FAT or exFAT filesystem, with the ini file named tiny386.ini in the root, and the alternative is to put the BIOS, VGA BIOS, kernel and stub into flash partitions with the rest in the storage partition, described by esp/partition.csv.
Input over WiFi, and why USB and WiFi cannot coexist
Input on the microcontroller is the awkward part, and the project offers two routes with a hard constraint between them. The primary one forwards keyboard and mouse events to the board over WiFi using a tool called wifikbd, with the board listening on TCP port 9999 and the PC side running ./wifikbd esp_board_addr 9999. The second is USB HID, which is marked experimental and is enabled by adding a setting to the ini file:
[esp]
enable_usb = 1The warning that follows is the important part. Do not enable USB and WIFI at the same time on an ESP32-S3, due to insufficient memory. That is a hardware budget statement rather than a driver conflict, and it tells you how tight the memory situation is on an ESP32-S3 when a full PC emulator, a network stack and USB are all resident. On the display side, the rawdraw and SDL ports have their own key handling, where Ctrl plus right bracket grabs and ungrabs the keyboard and mouse, and Ctrl plus left bracket shows and hides an on screen display. In OSD mode the floppy and CD-ROM disks can be changed on the fly, which is a practical touch when you are emulating a machine whose whole point is running an operating system that expects a boot disk.
Known failure modes, and the licence mix
The troubleshooting section is short and every entry is a specific symptom, which makes it a good summary of where the emulation is weakest. A zero bytes of memory message during Windows 95 setup is bypassed with setup /im. A protection error during Windows 95 startup needs the patcher9x tool. An NE2000 network card that does not work usually needs the IRQ set manually to 9 or 2. And a freeze during Windows NT4, 2000 or XP startup is fixed with fill_cmos = 0 in the config file, which is the same setting that fixes Windows 9x, so the two operating systems pull in opposite directions. The licence picture is layered and worth reading carefully. The CPU emulator and the project as a whole are BSD-3-Clause. Adlib emulation is optional and requires the fmopl library, which is LGPL, and it can be excluded with make USE_FMOPL=n. SeaBIOS is distributed under the GNU LGPL-3 licence, and some parts ported from QEMU and TinyEMU are under the MIT licence. The project is not archived, the last push was on 2026-10-18, and the releases page carries a single continuous build rather than tagged versions.
Editorial conclusion
Adopt tiny386 if you are interested in bare metal PC emulation, want to run a 16 or 32 bit operating system on hardware that has no business running one, or want a small readable x86 core to study. Do not adopt it for production or for general purpose virtualisation, since the core is deliberately incomplete, with debugging, hardware tasking and some permission checks missing. Verify first that your board is one of the supported ESP32 targets with the matching ESP-IDF version, that you have the BIOS and VGA BIOS files the configuration expects, and that the software you intend to run does not need the features the emulator deliberately leaves out.
Frequently asked questions
How do I build tiny386?
For a headless executable with no optional features, run gcc -O3 *.c -o tiny386_headless -lm. For display and sound, install libslirp, libx11 and libasound2 on Linux and run make, or use make USE_SDL=y for SDL, make win32 with mingw-w64, and cd wasm; make with clang for WebAssembly.
Which ESP32 boards does tiny386 support?
With ESP-IDF 5.2.x the JC3248W535 ESP32-S3 at 480x320 and the Elecrow CrowPanel Advance 7.0 inch HMI ESP32-S3 at 800x480. With ESP-IDF 6.0.x, listed as experimental, the JC4880P443 ESP32-P4 at 360MHz and 800x480 is supported.
How do I get a Windows 95 install to boot in tiny386?
The troubleshooting notes give three fixes. Use setup /im to bypass the memory check that reports zero bytes of memory, use the patcher9x tool for a protection error during startup, and set fill_cmos = 0 in the ini file if the machine freezes during Windows NT4, 2000 or XP startup.
Can I enable USB keyboard input on the tiny386 ESP32 port?
Yes, with enable_usb = 1 in the [esp] section of the ini file, and it is marked experimental. The documentation warns not to enable USB and WIFI at the same time on an ESP32-S3, due to insufficient memory.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/hchunhui-tiny386)Was this useful?