Cached at:
07/01/26, 06:03 PM
TL;DR: The author successfully built a playable Gameboy emulator on the M5 Paper S3 (ESP32 S3 + E-Ink display). By driving the e‑ink screen at high refresh rates, optimizing the emulation core, and using an innovative audio approach, classic games run smoothly on the e‑ink display.
## Hardware: M5 Paper S3 and the E‑Ink Screen
I recently got my hands on an M5 Paper S3 — an ESP32 S3 development kit that looks like a portable e‑reader. It has a 4.7‑inch 960×550 resolution e‑ink touchscreen. Its biggest difference from other small e‑ink dev kits is that the screen uses a raw row‑column driver interface (often called a parallel interface, but different from the parallel interface used for LCDs). This interface lets me bypass the usual waveform method and achieve refresh rates up to 60–70 FPS. More on that later.
A few years ago I reimplemented a Gameboy emulator on an FPGA, so I had some understanding of the Gameboy architecture. So this time I wanted to combine the two — make the first truly playable Gameboy emulator for the Paper S3.
## High Refresh Rate E‑Ink Driver
E‑ink is usually considered slow and low‑refresh. But with the right hardware you can improve it a lot. I made a dedicated video before about achieving 60 Hz and low latency on an e‑ink screen (turning it into an e‑ink monitor). The key: process pixels one by one, implement a pipeline update, and run those operations with an FPGA. Now I wanted to see if I could do the same thing on an ESP32.
Fortunately the Gameboy screen is only 160×144 pixels. I needed three pixels to represent four shades of gray. Tripling the dithered image gives me a perfect 3× pixel‑scaled and dithered image. That means I only have to handle 160×3×144 pixels at 60 Hz — far less than the 960×540 pixel target.
I store four state bits per pixel and compare them with the input buffer to decide what to send to the screen. Using LCDIF and DMA to actually output pixels, the core can compute the next batch of pixels to display. The whole process runs on the second core of the ESP32. Each iteration takes about 17 ms, giving a 60 Hz refresh rate. I also implemented double buffering so the emulator can render tear‑free images.
## Choosing an Emulator Core
Although I had implemented a Gameboy emulator on FPGA before, many people have already done emulators on microcontrollers. This time I decided to use an existing emulator instead of reinventing the wheel. I tried three cores:
- **PeanutGB**: The most popular emulator option for microcontrollers, good performance.
- **WanaCGB**: A fork of PeanutGB. I modified it to change instruction reads from 8‑bit to 16‑bit, potentially saving memory access time (mainly reducing the number of accesses to the expensive emulated memory map path).
- **Crankboy**: A fork of P Nintendo GB, also a frontend for the Playdate project. They did a lot of optimizations for portability, especially the memory map path. Performance is better, and it doesn’t need the unusual double‑read structure of WanaCGB.
I tested emulation speed in various games on the ESP32 S3 (performance measured relative to a real Gameboy, 1 = same speed). I ended up using **Crankboy** as the emulation core because it had the best performance. With dynamic frame skipping it reaches 100% speed in many games, rendering at 30–60 FPS. The actual frame rate depends on runtime conditions. With current performance, enabling Gameboy Color support is not yet practical (Gameboy Color allows the CPU to run at double speed — we’re still far from that).
## Sound Output: From PCM to Pseudo‑Polyphony
All three emulators use Mini GB APU as the sound simulation engine. The Gameboy has a four‑channel programmable sound generator: two square wave channels, one sample channel, and one noise channel, which are mixed to produce audio output. On a real Gameboy, part of the mixing happens in the analog domain; the simplest approach for emulators is to generate PCM samples at 32.768 kHz from all channels and mix them.
However the Paper S3 only has a buzzer (PWM‑driven, single tone). A known technique is to use a PWM DAC to output PCM samples: feed the DC‑biased PCM samples directly as duty cycles into the PWM module. But the PWM module on the ESP32‑S3 cannot be fed by DMA; it either pulls data or updates the duty cycle register via interrupts at the sample rate — both require CPU cycles. So I used the **PDM output feature of the I2S peripheral** (originally intended for connecting PDM‑input DACs or amplifiers). By adding an RC filter, it becomes a DAC. The first implementation worked immediately, but the volume was too low.
I tried 32 kHz PWM but it didn’t improve much, so I stuck with PDM. Still, I felt it wasn’t enough: having sound but being inaudible in many situations is not useful. To get a loud enough buzzer sound, you basically have to use a single tone — that’s what the buzzer is designed for. So why not just use a single tone? The first two channels of the Gameboy sound generator are pure square waves, the sample channel can often be approximated with a square wave (a stretch, I admit), and the noise channel can also be turned into a square wave. That gives four square wave channels.
Even though the buzzer can only produce a single tone, you can quickly switch between multiple sound channels to create a **pseudo‑polyphony** effect — a well‑known technique on platforms that have only a single buzzer (like IBM‑compatible PCs without a sound card).
The result: “surprisingly both good and bad”. On the plus side the sound is loud and clear, game music is still easily recognizable, and it’s quite interesting to listen to. On the downside, the Gameboy was never meant to sound this loud. To make sure it doesn’t steal the limited CPU resources of the emulator, I placed the entire PSG simulation on the second core. According to profiling, the audio simulation runs at most about 2 ms per frame, depending on sound complexity.
## Input and Control
Input is relatively straightforward: the M5Paper S3 has a touch screen, so it was natural to display a touch‑screen gamepad on the screen. But for hardcore players I also added **experimental Bluetooth Low Energy gamepad support**. It doesn’t fully implement the HID protocol — it just guesses the report format, so compatibility is poor. I think it’s acceptable as a demo, but there is certainly room for improvement.
## Extra Features: Quick Save
I also added **quick save and quick load** functionality, which essentially saves and loads the entire emulated Gameboy state. This allows fast checkpoint creation and return to that checkpoint. That way, after turning off the Paper S3, you can pick it up again, quick‑load, and immediately resume your game.
## Summary and Next Steps
The final result is a fairly decent and usable Gameboy emulator for the M5 Paper S3. Unfortunately the Paper S3 has been discontinued. I hope a suitable replacement will appear soon. If you want to try this firmware, you can find it on M5 Burner.
**Source:** https://www.youtube.com/watch?v=oPbOK90aJEo