This guide starts from an empty working directory and finishes with a small
hello_world.prg32 cartridge that can run in QEMU, upload to a physical
ESP32-C6 PRG32 board, and be packaged for a Cartridge Store.
PRG32 has two related development loops:
- resident firmware development, where
idf.pyor PlatformIO builds the PRG32 runtime for the board or QEMU; - cartridge game development, where
tools/prg32_game.pylinks a small RISC-V assembly or C program against the portable PRG32 ABI table and produces a.prg32game package.
The resident firmware and cartridges must agree on the portable ABI major, hash, and required feature bits. Incompatible cartridges are rejected cleanly.
Use ESP-IDF when you need the complete PRG32 workflow, QEMU, firmware changes, or classroom reproducibility.
Use PlatformIO when you want a convenient VS Code workflow for the physical ESP32-C6 firmware. The checked-in PlatformIO environment targets the real ESP32-C6 board. Use ESP-IDF commands for QEMU.
Recommended combinations:
| Task | Recommended environment |
|---|---|
| Build resident firmware for ESP32-C6 | ESP-IDF or PlatformIO |
| Flash and monitor physical board | ESP-IDF or PlatformIO |
| Build QEMU firmware and run virtual screen | ESP-IDF |
Build .prg32 cartridges |
Python plus ESP-IDF RISC-V toolchain |
| Package/publish Cartridge Store bundles | Python, zip, and curl |
| Modify PRG32 framework internals | ESP-IDF |
All platforms need:
- Git;
- Python 3;
- CMake;
- Ninja;
- an ESP-IDF 5.3 or newer toolchain with
esp32c3andesp32c6installed; riscv32-esp-elf-gcc, normally supplied by ESP-IDF;- a terminal where ESP-IDF has been exported before running
idf.pyor the cartridge builder.
Useful validation commands:
git --version
python3 --version
cmake --version
ninja --version
idf.py --version
riscv32-esp-elf-gcc --version
python3 tools/prg32_game.py doctorIf idf.py or riscv32-esp-elf-gcc is missing, the usual fix is to install the
ESP-IDF tools for both targets and source the ESP-IDF export script again.
- Install Git for Windows.
- Install Visual Studio Code.
- Install the Espressif ESP-IDF extension if you want IDE integration.
- Download and run the Espressif ESP-IDF Tools Installer for Windows.
- Select ESP-IDF 5.3 or newer.
- Include both
esp32c3andesp32c6tool support. - Open the ESP-IDF PowerShell shortcut created by the installer.
Check the tools:
git --version
python --version
idf.py --version
riscv32-esp-elf-gcc --versionUse the ESP-IDF PowerShell for PRG32 commands. A normal PowerShell usually does
not have the ESP-IDF environment on PATH.
- Install Git for Windows.
- Install Visual Studio Code.
- Install the PlatformIO extension.
- Install the Microsoft C/C++ extension.
- Install the Python extension.
- Open
PRG32.code-workspace.
PlatformIO builds the physical ESP32-C6 firmware:
pio run
pio run -t upload
pio device monitor -b 115200For QEMU, use ESP-IDF PowerShell and the QEMU commands later in this guide.
Windows serial notes:
- ESP32-C6 boards usually appear as
COMx. - If flashing fails, pass the port explicitly, for example
-p COM5. - Close Arduino Serial Monitor, PlatformIO Monitor, and ESP-IDF Monitor before opening a new monitor on the same port.
These commands target Debian and Ubuntu. Install equivalent packages on Fedora, Arch, and other distributions.
sudo apt update
sudo apt install -y \
git wget flex bison gperf python3 python3-venv python3-pip \
cmake ninja-build ccache libffi-dev libssl-dev dfu-util \
libusb-1.0-0 curl zipInstall ESP-IDF:
cd "$HOME"
git clone -b v5.3 --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
./install.sh esp32c3,esp32c6
. ./export.shSerial permission:
sudo usermod -aG dialout "$USER"Log out and back in after changing groups. ESP32-C6 boards often appear as
/dev/ttyACM0; USB UART bridges often appear as /dev/ttyUSB0.
Optional PlatformIO CLI:
python3 -m venv "$HOME/.venv-platformio"
. "$HOME/.venv-platformio/bin/activate"
python3 -m pip install platformioInstall Homebrew first if it is not already available, then install host tools:
brew install git cmake ninja dfu-util ccache libusb python curl zipInstall ESP-IDF:
cd "$HOME"
git clone -b v5.3 --recursive https://github.com/espressif/esp-idf.git
cd esp-idf
./install.sh esp32c3,esp32c6
. ./export.shUseful shell alias:
alias get_idf=". $HOME/esp-idf/export.sh"Run get_idf in each new terminal before using idf.py,
riscv32-esp-elf-gcc, or tools/prg32_game.py build.
Optional PlatformIO CLI:
python3 -m venv "$HOME/.venv-platformio"
. "$HOME/.venv-platformio/bin/activate"
python3 -m pip install platformioStart from a working directory that contains no PRG32 checkout yet.
Linux/macOS:
mkdir -p "$HOME/prg32-work"
cd "$HOME/prg32-work"
git clone https://github.com/raffmont/PRG32.git
cd PRG32
. "$HOME/esp-idf/export.sh"
python3 tools/prg32_game.py doctorWindows ESP-IDF PowerShell:
mkdir $HOME\prg32-work
cd $HOME\prg32-work
git clone https://github.com/raffmont/PRG32.git
cd PRG32
python tools\prg32_game.py doctorIf the project is already cloned, start in the repository root instead.
QEMU uses the Espressif ESP32-C3 RISC-V emulator target and the PRG32 virtual RGB display backend. The physical board remains ESP32-C6.
Linux/macOS:
idf.py -B build-qemu \
-D SDKCONFIG=build-qemu/sdkconfig \
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults.qemu \
set-target esp32c3
idf.py -B build-qemu \
-D SDKCONFIG=build-qemu/sdkconfig \
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults.qemu \
buildWindows ESP-IDF PowerShell:
idf.py -B build-qemu `
-D SDKCONFIG=build-qemu\sdkconfig `
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults.qemu `
set-target esp32c3
idf.py -B build-qemu `
-D SDKCONFIG=build-qemu\sdkconfig `
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults.qemu `
buildCheckpoint:
ls build-qemu/PRG32.elfThe cartridge builder reads this ELF file to find the PRG32 runtime ABI.
Create a new local game directory outside the standard examples:
mkdir -p work/hello_worldCreate work/hello_world/hello_world.S with this source:
.option norelax
.section .text
.global hello_world_init
.global hello_world_update
.global hello_world_draw
.equ PRG32_COLOR_BLACK, 0x0000
.equ PRG32_COLOR_WHITE, 0xffff
.equ PRG32_COLOR_CYAN, 0x07ff
hello_world_init:
addi sp, sp, -16
sw ra, 12(sp)
li a0, PRG32_COLOR_BLACK
call prg32_gfx_clear
lw ra, 12(sp)
addi sp, sp, 16
ret
hello_world_update:
ret
hello_world_draw:
addi sp, sp, -16
sw ra, 12(sp)
li a0, PRG32_COLOR_BLACK
call prg32_gfx_clear
li a0, 32
li a1, 40
la a2, hello_world_title
li a3, PRG32_COLOR_CYAN
call prg32_gfx_text8
li a0, 32
li a1, 64
la a2, hello_world_line
li a3, PRG32_COLOR_WHITE
call prg32_gfx_text8
lw ra, 12(sp)
addi sp, sp, 16
ret
.section .rodata
hello_world_title:
.asciz "HELLO WORLD"
hello_world_line:
.asciz "PRG32 cartridge from scratch"The three exported symbols are the cartridge entry points:
hello_world_init;hello_world_update;hello_world_draw.
Every call into PRG32 C helpers saves and restores ra, and the stack remains
16-byte aligned around calls.
python3 tools/prg32_game.py build \
work/hello_world/hello_world.S \
--portable \
--entry-prefix hello_world \
--name hello_world \
--out build-qemu/hello_world.prg32Checkpoint:
ls -lh build-qemu/hello_world.prg32If this fails with missing tool: riscv32-esp-elf-gcc, source ESP-IDF again.
First start QEMU once so ESP-IDF creates build-qemu/qemu_flash.bin:
idf.py -B build-qemu \
-D SDKCONFIG=build-qemu/sdkconfig \
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults.qemu \
qemu --graphics monitorQuit QEMU with Ctrl+], then stage the cartridge:
python3 tools/prg32_game.py upload-qemu \
build-qemu/hello_world.prg32 \
--flash build-qemu/qemu_flash.binStart QEMU again:
idf.py -B build-qemu \
-D SDKCONFIG=build-qemu/sdkconfig \
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults.qemu \
qemu --graphics monitorThe virtual screen should show HELLO WORLD in the 320x200 game viewport. If
the setup menu appears instead, use the setup menu to run cart0, or stage the
cartridge again after QEMU creates a fresh flash image.
Use a separate build directory for the physical board:
Linux/macOS:
idf.py -B build-esp32c6 \
-D SDKCONFIG=build-esp32c6/sdkconfig \
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults \
set-target esp32c6
idf.py -B build-esp32c6 \
-D SDKCONFIG=build-esp32c6/sdkconfig \
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults \
build
idf.py -B build-esp32c6 \
-D SDKCONFIG=build-esp32c6/sdkconfig \
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults \
flash monitorWindows ESP-IDF PowerShell:
idf.py -B build-esp32c6 `
-D SDKCONFIG=build-esp32c6\sdkconfig `
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults `
set-target esp32c6
idf.py -B build-esp32c6 `
-D SDKCONFIG=build-esp32c6\sdkconfig `
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults `
build
idf.py -B build-esp32c6 `
-D SDKCONFIG=build-esp32c6\sdkconfig `
-D SDKCONFIG_DEFAULTS=sdkconfig.defaults `
flash monitorIf the serial port is not detected, add -p COM5 on Windows,
-p /dev/ttyACM0 on Linux, or -p /dev/cu.usbmodemXXXX on macOS.
PlatformIO physical build alternative:
pio run
pio run -t upload
pio device monitor -b 115200The board should show the PRG32 splash and then setup if no cartridge is stored.
Build the same source as a portable cartridge for the physical board:
python3 tools/prg32_game.py build \
work/hello_world/hello_world.S \
--portable \
--entry-prefix hello_world \
--name hello_world \
--out build-esp32c6/hello_world.prg32QEMU and hardware use the same package format and portable ABI. Keep separate outputs when the surrounding metadata or target-specific assets differ.
On the board, enter setup mode and start the PRG32 access point. The default classroom values are:
SSID: PRG32
Password: prg32game
URL: http://192.168.4.1
Connect the development computer to the PRG32 Wi-Fi network, then upload:
python3 tools/prg32_game.py upload \
build-esp32c6/hello_world.prg32 \
--url http://192.168.4.1Upload to another slot with:
python3 tools/prg32_game.py upload \
build-esp32c6/hello_world.prg32 \
--slot cart1 \
--url http://192.168.4.1The firmware stores the cartridge and can run it immediately. If multiple slots contain cartridges, use setup to run a slot or save a default cartridge.
Useful runtime checks:
python3 tools/prg32_game.py runtime --url http://192.168.4.1
curl http://192.168.4.1/api/games
curl http://192.168.4.1/api/screenshot.bmp --output hello_world.bmpThe current checked-in cartridge tool builds and uploads board/QEMU cartridges.
For store publishing, create the metadata bundle explicitly. Cartridge Store
accepts this zip at POST /api/publish/bundle; POST /api/publish is a
compatibility alias for the same zip-bundle shape. Check the store
administrator's token and editor-review policy.
Create a bundle directory:
mkdir -p build/store/hello_world
cp build-esp32c6/hello_world.prg32 \
build/store/hello_world/hello_world-esp32c6.prg32
cp build-qemu/hello_world.prg32 \
build/store/hello_world/hello_world-qemu.prg32Create build/store/hello_world/manifest.json:
{
"abi": "prg32-metadata-1.0",
"id": "org.uniparthenope.hello-world",
"title": "Hello World",
"version": "1.0.0",
"summary": "Minimal PRG32 hello world cartridge.",
"authors": [
{
"name": "Your Name",
"affiliation": "Your Course Or Lab"
}
],
"tags": ["example", "assembly", "hello-world"],
"architectures": [
{
"id": "esp32c6",
"file": "hello_world-esp32c6.prg32"
},
{
"id": "qemu",
"file": "hello_world-qemu.prg32"
}
]
}Package it:
cd build/store/hello_world
zip -r ../hello_world-1.0.0.zip manifest.json \
hello_world-esp32c6.prg32 \
hello_world-qemu.prg32
cd ../../..Checkpoint:
unzip -l build/store/hello_world-1.0.0.zipSet the store URL and, if required, the publishing token:
Linux/macOS:
export PRG32_STORE_URL=http://192.168.1.42:5080
export PRG32_STORE_TOKEN=replace-with-classroom-tokenWindows PowerShell:
$env:PRG32_STORE_URL = "http://192.168.1.42:5080"
$env:PRG32_STORE_TOKEN = "replace-with-classroom-token"Publish with curl:
curl -X POST "$PRG32_STORE_URL/api/publish/bundle" \
-H "Authorization: Bearer $PRG32_STORE_TOKEN" \
-F "bundle=@build/store/hello_world-1.0.0.zip"The compatibility alias accepts the same bundle:
curl -X POST "$PRG32_STORE_URL/api/publish" \
-H "Authorization: Bearer $PRG32_STORE_TOKEN" \
-F "bundle=@build/store/hello_world-1.0.0.zip"If the store does not require authentication, omit the Authorization header.
Verify the catalog:
curl "$PRG32_STORE_URL/api/games"
curl "$PRG32_STORE_URL/api/games/org.uniparthenope.hello-world"If the upload response says status: pending, an editor must verify the
submission before these catalog requests show the new cartridge.
Download the published physical artifact for a final smoke test:
curl "$PRG32_STORE_URL/api/games/org.uniparthenope.hello-world/download?architecture=esp32c6&version=1.0.0" \
--output build-esp32c6/hello_world_from_store.prg32
python3 tools/prg32_game.py upload \
build-esp32c6/hello_world_from_store.prg32 \
--url http://192.168.4.1| Symptom | Likely cause | Fix |
|---|---|---|
idf.py: command not found |
ESP-IDF shell not exported | Run . $HOME/esp-idf/export.sh or use ESP-IDF PowerShell |
missing tool: riscv32-esp-elf-gcc |
ESP-IDF toolchain missing or not on PATH |
Run ./install.sh esp32c3,esp32c6, then export ESP-IDF |
ninja: command not found |
Host build tool missing | Install Ninja with the platform package manager |
| QEMU build cannot find virtual RGB component | Wrong target or defaults | Use esp32c3 and sdkconfig.defaults.qemu |
| Physical display is black | QEMU build flashed to board or wrong pins | Rebuild build-esp32c6 with sdkconfig.defaults; check main/prg32_config.h |
| Upload cannot reach board | Host is not on PRG32 Wi-Fi or wrong URL | Connect to PRG32 AP and use http://192.168.4.1 |
Store publish returns 401 |
Missing or invalid token | Ask for the classroom token or omit auth only on open stores |
Store publish returns 400 |
Bad manifest or zip layout | Check manifest.json and unzip -l output |
docs/cartridges.md: deeper cartridge workflow and slot behavior.docs/qemu.md: host-specific QEMU setup and troubleshooting.docs/tutorial.md: first assembly game tutorial.docs/tutorial_graphic_game.md: graphics assembly tutorial.docs/tutorial_c_game.md: C cartridge tutorial.docs/api.md: board HTTP API and Cartridge Store API reference.docs/framework_manual.md: PRG32 runtime API and ABI details.docs/assets.md: image, sprite, tile, GIF, and audio asset conversion.