The shared board runtime for emulators on the Makerfabs MaTouch ESP32-S3 7" and the M5Stack Tab5 (ESP32-P4).
Used by esp32ste (Atari STE), esp32mac (Macintosh Plus, SE, Classic), esp32cpc (Amstrad CPC), esp32zx (ZX Spectrum 128K, +2, +3), esp32cbm (Commodore 64, 128), esp32xl (Atari 800XL, 130XE), esp32msx (MSX1, MSX2), esp32gs (Apple IIGS), esp32coco (Tandy Color Computer 2, 3), esp32mac2 (Macintosh LC and II, Tab5 only), esp32mac3 (Macintosh LC II and LC III, Tab5 only), esp32arc (Acorn Archimedes, Tab5 only), esp32pc (IBM PC/XT and PC/AT) and esp32ami (Amiga 500, 2000 and 600, Tab5 only).
esp32emu is everything an emulator on these boards needs apart from the
machine itself: the display and touch panel, the SD card, WiFi with the clock
from the internet, Bluetooth keyboards and mice (the touch screen as a
touchpad too), a setup screen, a web page for
disk images, a serial console for testing, and the CPU cores and chips the
machines share: a 68000 (or 68020), a Z80, a 6502, a 65C816, a 6809, an ARM2, an x86 (8088, 8086, 80286) and a uPD765 floppy controller. A machine
describes itself in one struct, and emu_run() does the rest, on either board:
the build target picks it (esp32s3 the MaTouch, esp32p4 the Tab5).
| Component | What |
|---|---|
components/musashi |
The 68000 core: Musashi, evansm7's embedded fork, with static opcode tables (see its README); a 68020 with CONFIG_M68K_68020, and its FPU as a 68881; the type chosen at run time with CONFIG_M68K_RUNTIME |
components/z80 |
The Z80 core (passes zexall), timed in T-states, optionally with the ZX Spectrum's memory contention, or in Amstrad CPC microseconds, optionally reading unmapped banks through the machine, or with the MSX's memory (eight 8 KB banks and the subslot register at $FFFF) (menuconfig Z80 core); test/ runs zexdoc/zexall on a desktop |
components/m6502 |
The 6502 core (6510, 8502, disk drives, the Atari's 6502C): every bus cycle as on the chip, all 256 opcodes including the undocumented ones, several CPUs at once, cycle stealing (the VIC-II's, or the Atari's ANTIC, whose DMA cycles on a line it skips from a table by itself); test/ runs Klaus Dormann's functional test on a desktop |
components/w65816 |
The 65C816 core (the Apple IIGS): emulation and native mode, all 256 opcodes, decimal mode in both widths, memory through tables of 256-byte pages with the machine's callbacks for the rest, vectors from a bank of the machine's choosing; test/ runs the SingleStepTests 65816 suite (5 million tests, bus cycles included) on a desktop |
components/m6809 |
The 6809 and 6309 core (the CoCo): each instruction's documented cycles, all of the 6309 (its registers, native mode, the new instructions, its traps), memory through tables of 256-byte pages with the machine's callbacks for the rest, HALT for disk controllers; test/ checks table-driven cases and every opcode's cycles and length against the data sheet on a desktop |
components/emu8950 |
Mitsutaka Okazaki's emu8950 (MIT): the YM3812 (OPL2) for the IBM PC's AdLib and Sound Blaster, its tables and state in PSRAM |
components/x86 |
The x86 core (the IBM PC): the 8088, 8086 and 80286 as models of one core, with their differences (undefined opcodes, shift counts, PUSH SP, the flags, the divide error, the 286's instructions and faults, and its protected mode: descriptor tables, privilege levels, gates, task switches, LOADALL), the 8087/287 coprocessor, the undefined flags as the chips leave them, timed in CPU clocks with a model of the 8088's prefetch queue, memory through a table of 4 KB pages with the machine's callbacks for the rest and direct windows per segment; one run loop per model with the instruction set inlined (a 12 MHz 286 in real time on the Tab5); test/ runs the SingleStepTests 8088, 8086 and 286 real mode suites (5.1 million hardware-recorded tests) on a desktop; the debugger's x86 adapter (x86_dbg.c) |
components/arm2 |
The ARM2 core (the Acorn Archimedes): the 26-bit ARM with its modes, banked registers, exceptions and aborts, timed in the machine's memory cycles, memory through a table of 4 KB pages with the machine's callbacks for the rest; a threaded interpreter keeping its state in registers; test/ checks hand-written cases and random instructions against Unicorn on a desktop |
components/upd765 |
The uPD765A floppy controller as the CPC and the Spectrum +3 wire it up, with DSK and extended DSK images, and as the PC's (DMA, its interrupt, four units, raw images) |
components/emudbg |
The debugger for the programs in the machines: breakpoints, steps, registers, memory, disassembly and the screen of every emulated CPU over TCP, for the VS Code extension (protocol: docs/debugger.md); also in the machines' desktop builds |
components/emu |
The runtime: tasks and pacing, LCD presenter, touch, SD card, WiFi + NTP, BLE HID host, LVGL setup screen, web server, disk image manager, serial console; on the Tab5 also the ESP32-C6 radio, USB keyboards and mice, the Tab5 Keyboard, battery charging and the real-time clock |
components/fatfs |
ESP-IDF 5.5.4's FAT file system with exFAT turned on (from esp32_basic), so SD cards formatted exFAT (64 GB and up) work too |
components/esp_lcd |
ESP-IDF 5.5.4's LCD drivers with one fix in the RGB panel's (the MaTouch): after refill interrupts were held off (a flash write, a busy core), the picture could stay shifted by 10 lines with artefacts until a restart; now it's back in step at the next frame |
boards/m5stack_tab5/sdkconfig.defaults |
The Tab5's settings, loaded after a project's own when it builds for esp32p4 |
tools/emu_console.py |
Console client: log, screenshots of the machine or the whole LCD, commands, a profile of the emulation (hottest functions) |
In the project's top-level CMakeLists.txt:
set(EXTRA_COMPONENT_DIRS "${CMAKE_CURRENT_LIST_DIR}/../esp32emu/components")
# The Tab5: the board's settings after this project's own
if(IDF_TARGET STREQUAL "esp32p4" OR "$ENV{IDF_TARGET}" STREQUAL "esp32p4")
set(SDKCONFIG_DEFAULTS "${CMAKE_CURRENT_LIST_DIR}/sdkconfig.defaults"
"${CMAKE_CURRENT_LIST_DIR}/../esp32emu/boards/m5stack_tab5/sdkconfig.defaults")
endif()
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
if(IDF_TARGET STREQUAL "esp32p4")
# its own components (the BSP, esp_hosted, ...): its own lock file
idf_build_set_property(DEPENDENCIES_LOCK "${CMAKE_CURRENT_LIST_DIR}/dependencies.lock.esp32p4")
endif()
project(...)The MaTouch build is the default (idf.py build, its sdkconfig and
build/); the Tab5's lives beside it:
idf.py -B build-tab5 -DIDF_TARGET=esp32p4 -DSDKCONFIG=build-tab5/sdkconfig build
idf.py -B build-tab5 -DIDF_TARGET=esp32p4 -DSDKCONFIG=build-tab5/sdkconfig -p PORT flashThen main requires emu, and the machine's core requires musashi,
z80, m6502, w65816, m6809, arm2 or x86 (and upd765 for DSK drives). The machine fills in an
emu_machine_t (components/emu/include/emu/emu.h):
static const emu_machine_t machine = {
.name = "Macintosh", .short_name = "Mac",
.view_width = 898, .scale = EMU_SCALE_SMOOTH, /* picture width at 600 lines high */
.reserve_memory = reserve_memory, /* big blocks first */
.start = start, .reset = mac_reset, .run_frame = mac_run_frame,
.screen_mode = screen_mode, .screen_signature = mac_screen_signature,
.render_line = mac_render_line_rgb565,
.hid_key = hid_key, .hid_mouse = hid_mouse, /* Bluetooth */
.touchpad = hid_mouse, /* optional: touch as a touchpad */
.choices = choices, .choice_count = 2, /* setup screen rows: label, names, get, set, live */
.disks = &disks, /* drives and image folder */
.status = status, /* web page rows and drives */
.web_script = macfiles_js_start, /* optional: converters for the page */
.web_script_end = macfiles_js_end,
...
};
void app_main(void)
{
emu_run(&machine);
}emu_run() brings up the board, shows the setup screen, starts the machine,
and runs it on core 1, one frame at a time, paced to its refresh rate (or to
the speaker). Everything else runs on core 0. The picture is scaled to the
panel's height (600 lines on the MaTouch, 720 on the Tab5), as wide as
view_width says for 600 lines. A machine's screen_mode() can give a
frame its own size on the panel and the columns of it shown (view_w,
view_h, src_x, src_w in emu_screen_t): the Amiga shows its
super-hires frames one to one that way.
| Setup screen | WiFi networks and password, Bluetooth pairing, the machine's choices (e.g. system and memory), its drives. Before the first start (Start), and any time by holding the top-right corner of the screen for 2 s (Back returns, Reset resets the machine); changing a choice there restarts the machine (Restart), unless the machine applies it at once (e.g. the ST's CPU speed on the Tab5). With the speaker on, a Sound row for every machine: a mute button and a volume slider, applied at once and kept for every emulator on the board |
| Disks | One folder of images on the SD card for the machine's drives; inserted disks are remembered in NVS. Blank disks of the sizes the machine offers |
| Web page | http://<hostname>.local: machine, drives, board and Bluetooth status live; upload (drag and drop, with a progress bar: how far, how fast, how long still; ZIP files are unpacked in the browser), insert, eject, download and delete images, make blank disks (a machine can add a script that turns other files into images in the browser, e.g. the Mac's StuffIt archives, or packs dropped files and folders onto one, the PC's FAT images); a machine can take the disk folder's subfolders as drives too (the PC's read-only hard disks) |
| Console | Native USB port (MaTouch: the UART with the speaker on): screenshots, simulated touches, disk changes, reset, a sampling profiler, the speaker's volume, plus the machine's own commands |
| Bluetooth | BLE keyboard and mouse (NimBLE central), paired on the setup screen, reconnecting by themselves; keys arrive as HID usages. On the Tab5 the radio is its ESP32-C6, through esp_hosted |
| USB keyboard and mouse | Tab5: on its USB-A port (boot protocol), plugged in at any time; keys and motion arrive like a Bluetooth keyboard's and mouse's |
| Tab5 Keyboard | M5Stack's keyboard for the Tab5 (Ext.Port1), found at start-up; its keys arrive like a Bluetooth keyboard's |
| Touchpad | On both boards, for when there's no mouse at hand (a Bluetooth or USB mouse is the better way): the touch screen drives the machine's mouse. A finger moving anywhere on it moves the pointer as far as it goes on the picture, a tap clicks the left button, a tap and touching again drags, holding still for half a second holds the right one (menus). The machine turns the moves (in its screen's pixels) into its mouse's counts |
| Debugger | TCP port 6502, announced over mDNS: debug the program the machine runs from VS Code (esp32emu-vscode), with source lines, breakpoints, steps, registers, memory and the screen. Every CPU of the machine stops between two instructions; without a client the cores test one pointer per instruction |
The setup screen, here esp32xl's, on the MaTouch (1024×600) and the Tab5 (1280×720):
one layout, scaled to the panel; the machine adds its choice rows (the WiFi and clock
lines above them are cut out here)
| Request | |
|---|---|
GET /api/info |
Machine name, drives, image kinds, blank disk sizes |
GET /machine.js |
The machine's script for the page (web_script), if it has one |
GET /api/status |
Machine (state, speed, the machine's rows, the clock and where it came from), drives, board (its name, WiFi, memory, load, SD card), Bluetooth |
GET /api/disks |
Images in the folder: name, size, drive (and the subfolders, "folder": true, for a machine that makes drives of them) |
PUT /disks/<name> |
Upload (body = the image); also POST |
GET /disks/<name> / DELETE /disks/<name> |
Download / delete (not while in a drive) |
PUT /api/drive/<n> |
Insert the image named in the body into drive n (1, 2, ...) |
DELETE /api/drive/<n> |
Eject drive n |
POST /api/drive/<n>/offer |
Take what the machine offers for drive n's disk (the status's offers: e.g. an Amiga game's machine), which changes its settings and restarts it |
POST /api/new?size=<bytes> |
Make a blank disk image |
POST /api/reset |
Cold-reset the machine (same as the console's r) |
curl -T game.st http://atari-ste.local/disks/ uploads from a script.
/floppies/ works as /disks/ for older scripts. Two transfers can run at
once. On the MaTouch an upload runs at about 550 KB/s and a download at
about 310 KB/s; the Tab5 receives a whole file into PSRAM before writing it
to the SD card, and reads one before sending it (its SD card and WiFi
share a controller), with larger TCP windows: 1.3 to 2 MB/s up, 0.75 to
1.6 MB/s down.
idf.py menuconfig → Emulator board (each project sets its own defaults,
such as the network name):
| Option | Default | |
|---|---|---|
| WiFi | on | Stays connected while the machine runs; the clock comes from the internet (on the Tab5 from its own clock at once, then from the internet) |
| Network name | esp32emu |
http://<name>.local |
| Time zone | Central Europe | POSIX TZ string |
| Web page | on | |
| Setup screen | on | Optionally starting the machine by itself after a countdown |
| BLE keyboard and mouse | on | Mouse speed divider |
| Sound on the speaker | MaTouch off, Tab5 on | Volume until one is set on the setup screen. MaTouch: moves the console to the UART port |
| Debugger (menu Debugger) | on | TCP port (6502) |
| Makerfabs MaTouch ESP32-S3 7" | M5Stack Tab5 | |
|---|---|---|
| Chip | ESP32-S3 (Xtensa, 2 × 240 MHz) | ESP32-P4 (RISC-V, 2 × 360 MHz) |
| Memory | 16 MB flash, 8 MB PSRAM | 16 MB flash, 32 MB PSRAM |
| Screen | 7" 1024×600 RGB panel, GT911 touch | 5" 1280×720 MIPI-DSI panel (ILI9881C, ST7123 or ST7121), GT911 or ST7123 touch, held landscape |
| Radio | built in | an ESP32-C6, over SDIO (esp_hosted) |
| Sound | MAX98357A amplifier | ES8388 codec and amplifier |
| Storage | microSD (1-bit) | microSD (4-bit) |
| Keyboard | Bluetooth | Bluetooth, USB, or the Tab5 Keyboard |
| Pins | components/emu/include/emu/board_matouch7.h |
Espressif's m5stack_tab5 BSP, board_tab5.h |
On the Tab5 the runtime draws a 1280×720 landscape picture and the P4's PPA
turns the changed rows onto the portrait panel. The display, touch, SD card
and codec come through Espressif's m5stack_tab5 BSP; the radio, keyboard
and charging code follow M5Stack Tab5 projects that run on the hardware
(esp32_basic, wmbus_hub). The Tab5's IP2326 charger is switched on at start:
it charges the battery only when told to.
MIT. Musashi is MIT licensed too. components/fatfs is ESP-IDF's
(Apache 2.0) with ChaN's FatFs (BSD-style licence), exFAT turned on, and
components/esp_lcd ESP-IDF's (Apache 2.0) with the RGB fix. LVGL
(MIT), NimBLE, ESP-IDF, the Tab5 BSP and esp_hosted (Apache 2.0) are fetched
at build time and keep their own licences.