This project is a compact, self-contained ESP32-based retro computer. It recreates a classic PC-like experience—VGA monitor, PS/2 keyboard and mouse, removable storage, and a speaker—without using an actual x86 processor.
The ESP32 runs firmware that emulates or implements the computer environment while directly generating video, processing legacy peripherals, reading storage, and producing audio.
Diagram
1. Purpose and functionality
The board combines the peripherals needed for a small retro-style computer:
VGA video output
PS/2 keyboard and mouse inputs
microSD program and data storage
Onboard audio amplifier and speaker
USB-C power
USB-to-serial programming and debugging
Boot, reset, mute, and power controls
Wi-Fi and Bluetooth through the ESP32 module
Typical firmware could implement a BASIC computer, terminal, game console, DOS-style emulator, educational computer, or network-connected retro workstation.
Despite the project name, it is not electrically an x86 PC. Any x86 compatibility would come from software emulation, which carries a substantial performance cost.
2. Core processing platform
The central component is an ESP32-WROVER-E-N16R8 module. This version provides:
Dual-core ESP32 processor
16 MB flash
8 MB PSRAM
Integrated Wi-Fi and Bluetooth
Hardware peripherals including SPI, UART, DAC, timers, and GPIO
A pre-certified RF module with an integrated antenna
The extra PSRAM is important for video frame buffers, emulation state, audio buffering, and filesystem caching. Using a module instead of a bare ESP32 simplifies RF design and manufacturing, though it costs more and occupies more board area.
The ESP32’s GPIO allocation is relatively dense:
GPIO 4, 5, 18, 19, 21, and 22 contribute to VGA color generation.
GPIO 23 and 15 generate horizontal and vertical synchronization.
GPIO 25 supplies analog audio.
GPIO 12–14 and 35 support microSD communication.
GPIO 26, 27, 32, and 33 handle PS/2 mouse and keyboard interfaces.
UART0 connects to the USB programming interface.
That achieves a broad peripheral set without an FPGA, but leaves limited GPIO capacity for expansion.
3. Power and USB implementationUSB-C input
The USB-C connector is used as a USB 2.0 device port and as the 5 V power input.
Two 5.1 kΩ CC resistors identify the board as a USB-C power sink. This is a fixed 5 V implementation; there is no USB Power Delivery controller.
The USB data lines pass through a Würth USB protection device before reaching the CH340C. This provides ESD protection for a user-accessible connector.
Power distribution
The main path is:
USB-C VBUS
Slide power switch
Switched 5 V rail
AMS1117-3.3 linear regulator
3.3 V logic rail
The 5 V rail also powers the PS/2 connectors and audio stage. Ferrite beads and local capacitors create cleaner local supplies, particularly around the audio and storage circuitry.
A power indicator LED provides immediate power-state feedback.
Power trade-off
The AMS1117 is inexpensive and simple, but inefficient:
PD=(5.0−3.3)×I
At 400 mA, it dissipates approximately 0.68 W. ESP32 radio activity, VGA generation, SD access, and peripherals can produce high and bursty current consumption, so regulator temperature and transient response are important concerns.
A modern switching regulator would reduce heat and give the design more current margin.
4. Programming and boot control
A CH340C USB-to-UART bridge connects USB D+/D− to the ESP32’s UART0.
Its modem-control outputs drive two BSS138 MOSFETs connected to ESP32 EN and GPIO0. This implements the familiar ESP32 automatic programming sequence:
EN resets the processor.
GPIO0 selects the serial bootloader when held low during reset.
DTR and RTS allow the host programming utility to perform this sequence automatically.
Manual RESET and BOOT buttons are also included. Test points expose:
3.3 V
Ground
TX
RX
EN
GPIO0
These are valuable for manufacturing test, recovery, and debugging if USB programming fails.
5. VGA video generation
The board provides a conventional 15-pin VGA connector. The ESP32 directly generates:
Red
Green
Blue
Horizontal synchronization
Vertical synchronization
Multiple GPIO outputs feed resistor networks to produce several analog levels per color channel. The resistor values include 270 Ω, 430 Ω, and 820 Ω, forming a low-cost GPIO DAC.
This is a well-established ESP32 VGA technique. It avoids a video controller or FPGA, but has several trade-offs:
Video timing consumes significant processor and DMA resources.
Available color depth is limited by the number of GPIO bits.
Output accuracy depends on resistor tolerances and the VGA load.
Higher resolutions sharply reduce CPU time available for emulation.
Simultaneous Wi-Fi activity may introduce timing pressure or visible artifacts if firmware is not carefully designed.
Practical modes are likely to favor retro resolutions and modest color depth rather than modern desktop resolutions.
6. PS/2 keyboard and mouse
Two mini-DIN connectors provide separate PS/2 keyboard and mouse ports.
PS/2 devices run from 5 V, while the ESP32 uses 3.3 V logic. Four BSS138 MOSFET stages perform bidirectional level translation for:
Keyboard data
Keyboard clock
Mouse data
Mouse clock
Pull-up resistors on both sides support the open-collector behavior of PS/2 signaling.
This is a sensible and inexpensive implementation. It also fits PS/2 particularly well because PS/2 lines are bidirectional and normally released high.
Potential challenges include:
Hot-plug transients
Cable-borne ESD
Marginal pull-up timing with long cables
Variations among modern USB-to-PS/2 adapters, many of which are passive and require a truly dual-protocol keyboard or mouse
7. microSD storage
The push-push microSD socket connects to the ESP32 using an SPI-style interface:
Clock
Command/MOSI
Data/MISO
Chip select
Card-detection circuitry
Series 0 Ω resistors provide routing flexibility and convenient tuning or isolation points. ESD diodes protect exposed SD signals. A dedicated filtered 3.3 V rail and local bypass capacitors help contain SD-card current spikes and digital noise.
SPI mode uses fewer signals and is easier to implement than full four-bit SD mode, but has lower peak throughput. It should still be sufficient for ROM images, configuration files, retro software, and moderate audio streaming.
SD access must be coordinated with VGA and audio DMA activity to prevent frame or audio underruns.
8. Audio path
ESP32 GPIO25—one of the ESP32’s DAC-capable pins—provides the audio source.
The signal passes through input conditioning to an NS4150B Class-D amplifier, followed by ferrite filtering and an onboard FS-1540 speaker. The amplifier supports a hardware mute/control input and operates from the local 5 V audio supply.
The differential Class-D output improves efficiency and produces more speaker power than direct GPIO drive.
Important implications:
Neither speaker terminal should be treated as ground.
Class-D switching currents require short output paths and careful return-current control.
Audio quality is limited by the ESP32’s internal DAC, supply noise, and firmware sample rate.
VGA, SD, radio, and audio operation can interact through power and timing.
A physical mute switch provides a simple way to silence output without depending entirely on firmware.
9. PCB and mechanical design
The PCB is approximately 72 × 100 mm and uses four copper layers. It contains:
102 physical components
61 nets
636 routed trace segments
500 conventional vias
69 smart vias
Four M2 mounting holes
All electrical connections are routed—there are currently zero airwires. Components are mostly placed on the top side, with test points on the bottom. Large connectors are distributed around the edges for enclosure access.
The project also contains three mechanical bodies:
Enclosure base
Enclosure lid
Power-indicator window
A four-layer stackup is a strong choice here because it supports cleaner ground references, improves VGA and USB return paths, reduces EMI, and makes routing this connector-heavy board practical.
The board is moderately dense by component area. Although routing is complete, routing-aware spacing is tight enough that future revisions may benefit from a slightly larger board or more deliberate use of the bottom side.
10. Key design choices and trade-offsESP32 instead of FPGA or application processor
Advantages
Low component cost
Integrated wireless connectivity
Large software ecosystem
Straightforward programming
Enough performance for retro video and lightweight emulation
Trade-offs
Video generation consumes processor resources.
Accurate x86 emulation is limited.
Memory bandwidth is shared between application, video, storage, and networking.
Peripheral timing becomes firmware-sensitive.
Resistor DAC instead of a dedicated VGA converter
Advantages
Very low cost
Minimal circuitry
Direct control from firmware
Trade-offs
Limited color depth and analog precision
Heavy GPIO usage
Resolution and timing limitations
Linear regulator instead of a buck converter
Advantages
Cheap and simple
Low component count
Low switching noise
Trade-offs
Heat generation
Lower efficiency
Reduced current margin
PS/2 instead of USB host peripherals
Advantages
Simple protocol
Low firmware overhead
No USB host stack or 5 V host-power switching
Trade-offs
Legacy peripherals are becoming less common.
Passive USB adapters are not universally compatible.
Mini-DIN connectors consume substantial board and enclosure area.
11. Performance considerations
The most important system constraint is not raw CPU frequency alone—it is contention among real-time workloads.
The ESP32 may need to perform all of these concurrently:
Generate exact VGA timing
Execute the emulated or native application
Read the keyboard and mouse
Stream or synthesize audio
Access the SD card
Handle USB serial communication
Service Wi-Fi or Bluetooth
VGA timing and audio should therefore use DMA, hardware timers, and interrupt-minimal buffering wherever possible. PSRAM helps capacity, but internal SRAM is preferable for latency-sensitive DMA buffers.
Likely optimization strategies include:
Pinning time-critical video tasks to one core
Running emulation and storage on the other core
Double-buffering audio and selected video data
Using direct-to-screen or scanline rendering instead of a full high-resolution framebuffer
Batching SD reads
Disabling or reducing radio activity during timing-critical modes
Keeping interrupt handlers short
Using fixed-point arithmetic in emulation and audio paths
12. Current design concerns
The project checks identify several items that should be reviewed before manufacturing:
Some edge-mounted connectors report placement warnings, including USB-C, VGA, and the power switch.
Keyboard, mouse, and control-switch bodies extend beyond the nominal board boundary; some of this may be intentional mechanical overhang.
A logo object overlaps the BOOT switch region.
Several deliberately unused pins are reported as floating and should be explicitly marked no-connect.
The microSD DAT1, DAT2, and separate card-detect pin are unused.
USB-C SBU pins and unused ESP32 inputs are floating, which is normally acceptable but should be documented.
The routed board contains 43 redundant trace candidates. These are not airwires, but they should be cleaned and rechecked before fabrication.
The very high via count increases routing complexity and can slightly increase fabrication risk and cost.
Connector alignment should be checked against the enclosure, particularly VGA, USB-C, PS/2, slide switch, and pushbuttons.
13. Real-world applications
This architecture is suitable for:
A standalone BASIC computer
Retro game and home-computer emulation
A serial or network terminal
Educational computer architecture demonstrations
A programmable classroom computer
A lightweight text workstation
Digital signage with VGA output
Interactive museum or arcade exhibits
A self-contained embedded diagnostics terminal
A Wi-Fi-connected retro dashboard
14. Recommended improvementsHighest priority
Replace the AMS1117 with a higher-efficiency buck regulator rated for ESP32 peak currents.
Clean redundant traces and resolve genuine placement/edge warnings.
Validate the complete board against the enclosure and connector openings.
Explicitly mark intentionally unused pins as no-connect.
Perform worst-case power and thermal testing with VGA, speaker, SD, and Wi-Fi active simultaneously.
Signal and reliability improvements
Confirm USB D+/D− routing geometry and protection-device placement.
Add or verify PS/2 connector ESD protection.
Verify the ESP32 antenna keepout includes copper, components, fasteners, and enclosure material.
Review VGA resistor values against a 75 Ω monitor termination.
Validate SD signal integrity at the maximum firmware clock.
Minimize Class-D amplifier loops and keep them away from VGA and antenna areas.
Consider a resettable fuse or current-limited load switch on USB VBUS.
Functional enhancements
Add external stereo audio or a headphone output.
Use an I²S DAC for improved sound quality.
Add USB host support for modern keyboards and mice.
Add expansion headers for GPIO, I²C, or SPI.
Provide external power input if speaker volume or peripherals exceed normal USB power.
Add an RTC and backup supply.
Add hardware battery-backed settings or nonvolatile configuration storage.
Support HDMI through an external bridge in a future high-end revision.
Use an ESP32-S3 or a small FPGA if higher-resolution video, USB host, or more deterministic timing is required.
Overall, the project is an ambitious but coherent ESP32 computer platform. Its strongest design feature is the integration of all classic-PC-facing functions onto one compact four-layer board. Its main engineering risks are power dissipation, real-time firmware contention, mechanical connector alignment, and cleanup of the remaining PCB review findings.
Every project page shows you the finished thing working. This one shows you what it cost to get there. If you're building your own version — or just wondering whether hardware is "really that hard" — this is the honest account.
The board came back from fabrication and, of course, nothing booted cleanly the first time. Here's each wall I hit, why it happened, and how I got past it. The fixes are all in the firmware in this project.
1. The reboot loop that wouldn't die
What I saw: First boot went beautifully. The emulator asked me to join WiFi, I clicked through the menu with a PS/2 mouse, typed my password, watched it download disk images to the SD card, then a progress bar filled — "Date and Time updated. Restarting…" — and the board rebooted. Good. Except it came back to the same message. And rebooted again. And again. Forever.
The hunt: The firmware syncs the clock from an NTP server on first boot, saves the time, and restarts once to fully shut WiFi down before launching the emulator. On that restart it's supposed to detect "I've already done this" and skip straight past. It never did.
The cause: The original design stored the timestamp in a special __NOINIT_ATTR region of RAM — memory that's meant to survive a soft reset. On this board, with PSRAM being initialized manually, that region was not surviving the restart. So every boot looked like the very first one: no saved time → connect WiFi → sync NTP → restart → repeat.
The fix: Stop trusting volatile RAM to remember anything across a reboot. I moved the "NTP already done" flag and the saved timestamp into NVS flash (the ESP32's Preferences store), which physically persists through any reset, crash, or power cut. Second boot now reads the flag from flash, sees the work is done, and walks right past the NTP step.
Lesson: "Survives a soft reset" and "survives my soft reset on my board" are not the same promise. When something absolutely must persist, put it in flash.
2. The crash that struck at a random moment
What I saw: With the loop broken, a new failure took its place. Partway through "Getting date-time from SNTP…" the board would panic and reboot — Guru Meditation Error: LoadProhibited. Sometimes at 16%. Sometimes 50%. Sometimes 75%. Never the same place twice.
The hunt: A crash at a random point is a different animal from a crash at a fixed point. Fixed means a bad line of code. Random means a race — two things running at once, occasionally colliding. The faulting address pointed at a null read, deep in the timing of the NTP code.
The cause: The SNTP sync was being kicked off from inside the on-screen progress dialog's task. That dialog shares the stage with FabGL's real-time display driver — the thing generating VGA every 1/60th of a second. Starting the network time stack from within that tightly-timed display task was stepping on the display controller's toes, and now and then the timing lined up just wrong and the whole thing fell over.
The fix: Pull the SNTP startup out of the dialog. Initialize the time sync first, in calm open code, and let the progress dialog do nothing but watch and report percentage. Also moved the restart call out of the dialog's callback so the ESP32 reliably tags it as a clean software reset.
Lesson: On a two-core microcontroller with a real-time job running, where you start a task matters as much as what it does. Keep heavy, blocking work away from the code that's painting the screen.
3. The SD card that worked — until it didn't
What I saw: Early on, the SD card mounted fine. Then after some refactoring it started failing at cold power-on with sdmmc_init_sd_if_cond … returned 0x108 — a timeout. The card simply wasn't answering.
The cause: Two things conspired. First, a refactor had quietly dropped the explicit SPI-bus pre-initialization the card needed before mounting. Second — the nastier one — the SD card's data line was wired to a pin that doubles as a boot strapping pin. At power-on the ESP32 reads that pin to decide flash voltage; with an SD card hanging off it, the card was pulling the line and corrupting that decision, so boot itself became unreliable with a card inserted.
The fix: Restore the SPI bus setup before the mount, and — the real solution — move the offending signal off the strapping pin in the next board revision (it now lives on a safe GPIO). The design constraint went straight into my "rules for next time" list.
Lesson: Read the strapping-pin table before routing, not after the board comes back. A pin that's free to use at runtime may not be free to use at boot.
4. Smaller skirmishes
PSRAM, handled by hand. The emulator needs PSRAM but deliberately leaves it disabled in the IDE, then brings it up at runtime — enabling it the normal way drags in a compiler workaround that slows the CPU too much for smooth emulation. Counterintuitive, but it's the right call.
The malformed comment that compiled. An old block of SD code was "commented out" with /*/ … */ — which, it turns out, opens and closes immediately, so the code inside was actually live. A reminder that the compiler reads exactly what you wrote, not what you meant.
Watchdogs underfoot. Long blocking operations during setup tripped the task watchdog until I gave the loops explicit yields. On a cooperative real-time system, you have to let the other tasks breathe.
Still open: Windows 3.0 shows no video
What I see: Windows 2.x boots and runs on the emulator without complaint. Windows 3.0, launched the same way from the same kind of disk image, gives me a black screen — no video at all. The board doesn't crash or reboot; it just goes dark and nothing appears.
Where it stands: Unsolved, and I'm documenting it honestly rather than guessing in public. I haven't yet tried launching Windows 3.0 with explicit mode switches, which is the obvious next experiment.
What I'd try next, and why: The fact that Windows 2.x is happy but 3.0 goes dark points at graphics, not at the CPU or memory. Windows 2.x is content to drive plain CGA/Hercules, exactly what this emulator provides. Windows 3.0 is more ambitious about display modes and about how it runs — so the leading suspects are:
A video mode the adapter can't produce. Windows 3.0 may be probing for or selecting a display mode beyond the emulated CGA/Hercules capability, and getting a blank signal as a result.
The run mode. Windows 3.0 can start in real mode (win /r) or standard mode; on XT-class hardware real mode is the relevant one. Forcing win /r is the first thing to test.
The installed display driver. A Windows 3.0 image configured for a CGA or Hercules driver specifically (rather than EGA/VGA) may behave very differently. Reinstalling or reconfiguring the display driver inside Windows Setup is worth a try.
If you've gotten Windows 3.0 to display on FabGL-based hardware, I'd genuinely like to hear how — drop a note on the project.
What the scars taught me
If there's a thread running through all of it, it's this: a microcontroller pretending to be a whole computer has no slack. No host OS to absorb your mistakes, no spare cores, no spare milliseconds. Memory has to persist where you promise it will, tasks have to start where they won't collide, and the hardware has to behave at the instant of power-on — not just once it's running.
Every one of these bugs was invisible in the schematic and obvious in hindsight. That's hardware. The board on the bench booting to C:\> is quietly carrying every one of these fixes.
Building your own spin on this? If you hit something new, document it and send it back — the next person's build log should be shorter than mine.