Skip to content

Latest commit

 

History

History
621 lines (480 loc) · 27.2 KB

File metadata and controls

621 lines (480 loc) · 27.2 KB

PRG32 Framework Manual

Note

This manual is intended for users and students writing cartridges using the provided API. It is not intended for developers modifying the underlying ESP-IDF C framework.

PRG32 lets students write game logic in RISC-V assembly or C while a small framework provides hardware access.

Assembly ABI

Arguments use the standard RISC-V calling convention: a0 to a7 carry arguments and return values, ra holds the return address, and sp is kept 16-byte aligned around C calls.

Console modes

  • PRG32_MODE_UART_ONLY: serial terminal only.
  • PRG32_MODE_LCD_ONLY: text appears on the ILI9341 display.
  • PRG32_MODE_UART_LCD_MIRROR: debug text is sent both to serial and LCD.

Input

prg32_input_read() returns the PRG32 input register. The low bits are the single local joystick:

  • PRG32_BTN_LEFT
  • PRG32_BTN_RIGHT
  • PRG32_BTN_UP
  • PRG32_BTN_DOWN
  • PRG32_BTN_A
  • PRG32_BTN_B
  • PRG32_BTN_SELECT (PRG32_BTN_START is kept as an alias)

Games can use prg32_input_read_player(1) to get the same normalized low-bit mask. prg32_input_read_player(2) is kept as a source-compatible helper, but returns 0; multiplayer games should use the PRG32 multiplayer API for remote players.

QEMU and host-driven tests can inject the same bitmask through prg32_diag_set_input_state().

Menu/setup helpers call prg32_input_read_menu() to read the local joystick in the same normalized low-bit mask.

System hotkey:

  • A + B + DOWN on the local joystick: restart the ESP32-C6 firmware from anywhere in the PRG32 input path.

Random Numbers

prg32_random_number(min, max) returns an unsigned value between min and max, including both endpoints. It returns min if max <= min and supports the full 0 through UINT32_MAX range. The C and assembly demo shows how to call it and update the display on each A-button press.

Joystick Text Input

The on-screen keyboard lets games and framework setup screens collect short alphanumeric text without a USB keyboard.

Useful calls:

  • prg32_keyboard_init(keyboard, buffer, capacity)
  • prg32_keyboard_update(keyboard, input_mask)
  • prg32_keyboard_draw(keyboard, x, y)
  • prg32_text_input(buffer, capacity, title)

Controls:

  • D-pad: move around the key grid.
  • SELECT: select the highlighted on-screen key.
  • A: escape back to the previous state.
  • B: confirm, equivalent to selecting the on-screen return key.

The keyboard uses a QWERTY layout with explicit delete, shift, and return keys. shift toggles between lower-case and upper-case/symbol labels. The ascii key opens a printable ASCII page covering characters 0x20 through 0x7e. The LCD and QEMU text renderers include distinct glyphs for the full printable ASCII range; console output also treats tab and DEL/backspace as text controls.

Graphics model

The physical display is 320x240, while the normal game viewport remains 320x200. The firmware splash, setup, Wi-Fi setup, developer menu, and about screen use the full display. Game and feature-demo drawing calls use the centered 320x200 viewport so cartridges keep the same coordinate system and the retro frame. Unless a program sets a band color explicitly, the upper and lower horizontal bands are filled with the same color passed to prg32_gfx_clear.

Indexed Framebuffer

On ESP32-C6 hardware, the 320x200 game surface is a 64,000-byte, 8-bit indexed framebuffer backed by a deterministic 256-entry RGB565 palette. The ILI9341 remains in native 16-bit RGB565 mode: dirty indexed rows are expanded into the existing small RGB565 SPI strip only during presentation. Compared with the previous 128,000-byte framebuffer, this reclaims 63,488 bytes (about 62 KiB) without allocating a second full-size RGB565 surface. Existing RGB565 drawing calls remain source- and ABI-compatible through deterministic system-palette quantization; indexed-native primitives and palette cycling are available for cartridges that need exact palette control. See ILI9341 Hardware and Driver Notes.

The reference Performance Test compares matched RGB565-compatible and full indexed-native workloads. Poing is an application example that renders its procedural scene with 8-bit indices and can publish a 300-frame gameplay result through the same public performance API by pressing SELECT.

The QEMU renderer exposes the same 320x240 physical screen and centers the 320x200 PRG32 game viewport inside it. Student assembly code does not change; only the selected display backend changes.

For classroom debugging, optional helper prg32_debug_overlay_draw can print x, y, input mask, frame, and tick info on the top scanline.

Display backend selection:

  • CONFIG_PRG32_DISPLAY_ILI9341: physical ILI9341 SPI TFT, default.
  • CONFIG_PRG32_DISPLAY_QEMU_RGB: QEMU virtual RGB framebuffer.

Use the QEMU defaults file when running on a desktop:

idf.py -B build-qemu -D SDKCONFIG=build-qemu/sdkconfig -D SDKCONFIG_DEFAULTS="profiles/sdkconfig.defaults;profiles/sdkconfig.defaults.qemu" qemu --graphics monitor

When the QEMU backend is selected, main/prg32_config.h disables physical GPIO buttons and the buzzer. QEMU builds keep player 1 usable through the UART console keyboard mapper: arrows or W/A/S/D for the joystick, Enter/Space for SELECT, J/Z for A, and K/X for B.

Splash Screens

The resident firmware shows the PRG32 logo image after display initialization. It can be disabled or timed through Kconfig:

  • CONFIG_PRG32_SPLASH_ENABLED
  • CONFIG_PRG32_SPLASH_DURATION_MS
  • CONFIG_PRG32_SPLASH_SOUND_ENABLED

When splash sound is enabled, firmware plays a short welcome phrase through the I2S audio subsystem only when the configured audio pins do not conflict with the reference display/input wiring. Otherwise it uses the passive buzzer when one is configured.

Graphic games can reuse the 320x200 game splash helpers:

  • prg32_splash_show_game(title, subtitle, duration_ms, bg, fg, accent): draw a game title screen, present it, and wait.
  • prg32_splash_draw_game(title, subtitle, bg, fg, accent): draw a game title screen without delaying, useful inside a title-state loop.

Framework-owned full-screen splash helpers remain available:

  • prg32_splash_show(title, subtitle, duration_ms, bg, fg, accent): draw, present, and wait on the full 320x240 display.
  • prg32_splash_draw(title, subtitle, bg, fg, accent): draw a splash/title screen without delaying on the full 320x240 display.
  • prg32_splash_show_default(): show the firmware-style PRG32 splash.
  • prg32_gfx_set_fullscreen(enabled): switch between full-screen framework drawing and the centered game viewport.
  • prg32_gfx_set_band_color(color): set a custom color for the game viewport bands.
  • prg32_gfx_use_background_bands(): return to automatic background-colored bands.
  • prg32_gfx_lock() / prg32_gfx_unlock(): optional recursive graphics lock for advanced code that must update several draw calls atomically.
  • prg32_gfx_snapshot_row_rgb565(y, out, pixels): copy one physical 320-pixel framebuffer row as normal RGB565. The HTTP screenshot API uses this helper for both ILI9341 hardware and QEMU.

Assembly programs pass C strings in a0 and a1, duration in a2, and RGB565 colors in a3 to a5.

Status Bands

When the viewport is active, PRG32 owns the 20-pixel band above the game and the 20-pixel band below it. By default they follow the game background color. Games can opt in to status text without changing the 320x200 play area:

  • prg32_band_set_mode(PRG32_BAND_TOP, mode)
  • prg32_band_set_mode(PRG32_BAND_BOTTOM, mode)
  • prg32_band_set_text(band, text)
  • prg32_band_set_game_info(text)
  • prg32_band_log(message)
  • prg32_band_set_colors(band, fg, bg)
  • prg32_band_use_default_colors(band)

Available modes are PRG32_BAND_MODE_NONE, PRG32_BAND_MODE_FPS, PRG32_BAND_MODE_WIFI, PRG32_BAND_MODE_GAME, PRG32_BAND_MODE_DEBUG, and PRG32_BAND_MODE_CUSTOM. The setup developer menu lets a trainer choose what appears in the top and bottom bands and stores that choice in NVS.

Screenshot API

When the resident HTTP server is reachable, GET /api/screenshot.bmp streams the current full 320x240 framebuffer as a 24-bit BMP. It is intended for lab reports, debugging display output, and comparing hardware with QEMU rendering:

curl http://192.168.4.1/api/screenshot.bmp --output screenshot.bmp

The encoder holds the recursive graphics lock while it streams rows. This keeps the BMP internally consistent without allocating a complete second framebuffer in ESP32 RAM.

Performance Metrics

Optional performance metrics record update, draw, present, heap, input, FPS, and deadline information while a cartridge is running. The feature is disabled by default and controlled through Kconfig:

  • CONFIG_PRG32_METRICS_ENABLE
  • CONFIG_PRG32_METRICS_SERVER_URL
  • CONFIG_PRG32_METRICS_BOARD_ID
  • CONFIG_PRG32_METRICS_SAMPLE_PERIOD_FRAMES
  • CONFIG_PRG32_METRICS_UPLOAD_PERIOD_MS
  • CONFIG_PRG32_METRICS_QUEUE_LEN

The metrics upload queue is allocated only when a metrics run starts. Recording remains non-blocking; if the queue fills, new samples are dropped and reported with the next uploaded batch.

The public API is in prg32_metrics.h:

  • prg32_metrics_init(config)
  • prg32_metrics_start_run()
  • prg32_metrics_stop_run()
  • prg32_metrics_is_enabled()
  • prg32_metrics_record(sample)
  • prg32_metrics_run_id()

The resident firmware instruments the cartridge update/draw/present loop when metrics are enabled. prg32_metrics_record only copies into a ring buffer; HTTP upload is handled asynchronously so the measured frame code does not wait for the network. See Performance Metrics for the server, export workflow, and lab exercise.

The optional performancetest cartridge provides an unattended multi-case benchmark that retains compact per-screen/per-color-mode summaries in RAM without streaming every frame. Temporary percentile samples are released at the end of each case. The latest run is available as /api/performance.json until the next benchmark or reboot. The cartridge cases isolate clear/fill, text overlay, sprite storm, scrolling, and mixed-gameplay workloads. Every workload runs through matched RGB565 and full indexed-native primitives and indexed-color sprite probes; the final screen reports an aggregate summary and the complete result endpoint. The JSON API preserves the mode on every case summary. See the Performance Test Guide for the execution workflow, interpretation rules, and custom ABI tutorial.

screen_count remains five because it counts distinct workloads; result_count is ten because every workload produces an RGB565 result and an indexed result. Both modes ultimately present RGB565 pixels, so the comparison isolates compact-asset decoding rather than LCD wire-format bandwidth.

Cartridge runtime

The resident firmware includes a cartridge loader so games can be replaced without reflashing the whole ESP32-C6 app.

Important constants:

  • PRG32_CART_MAGIC: .prg32 package magic.
  • PRG32_CART_ABI_MAJOR / PRG32_CART_ABI_MINOR: loader ABI version.
  • PRG32_CART_META_MAGIC: optional metadata trailer magic, PRG32META.
  • PRG32_CART_META_ABI: metadata JSON ABI, prg32-metadata-1.0.
  • PRG32_CART_COLOPHON_ABI: colophon JSON ABI, prg32-colophon-1.0.
  • PRG32_CART_MAX_SIZE: maximum .prg32 package size, 64 KiB in the default physical and QEMU configurations.
  • PRG32_CART_RAM_SIZE: statically placed executable cartridge RAM window, configured by CONFIG_PRG32_CART_RAM_PROFILE. Physical ESP32-C6 and QEMU builds default to the 64 KiB extended profile. The optional PRG32_CART_RAM_LARGE_128 profile reserves 128 KiB on ESP32-C6 when built with profiles/sdkconfig.defaults.esp32c6_128k; it requires matching rebuilt cartridges and raises the stored-image limit to 128 KiB. The window remains static because legacy cartridges are linked to the exported prg32_cart_exec address; portable cartridges use the ABI table. Host tools assume the 64 KiB window unless told otherwise with --cart-ram-kib; see PRG32 Profiles.
  • PRG32_CART_SLOT_COUNT: number of persistent flash cartridge slots.

Important functions:

  • prg32_cart_load_addr(): runtime address used by the host linker.
  • prg32_cart_install(image, size, persist): validate, load, and optionally store.
  • prg32_cart_store_slot(slot, image, size): validate and store an image without running it.
  • prg32_cart_install_slot(slot, image, size, persist): install to one of cart0 through cart3.
  • prg32_cart_select_slot(slot): load a stored cartridge from one slot.
  • prg32_cart_default_slot(): read the saved default boot cartridge.
  • prg32_cart_set_default_slot(slot): save a default slot, or pass -1 to clear it.
  • prg32_cart_select_default(): load the saved default slot.
  • prg32_cart_stored_count(): count valid stored cartridges.
  • prg32_cart_get_slot_info(slot, info): inspect one persistent slot.
  • prg32_cart_call_init()
  • prg32_cart_call_update()
  • prg32_cart_call_draw()

The default app automatically calls the current cartridge every frame when one is loaded. Store-ready cartridges may include a metadata trailer after the legacy executable payload. The game colophon is shown after the cartridge is activated, before the player starts a new play. See cartridge_metadata.md, colophon_abi.md, and setup_mode_cartridge_store.md.

Wi-Fi Modes and Setup

For a full guide on connecting the board to a network, see the Network Setup and Wi-Fi Modes documentation.

PRG32 supports three Wi-Fi runtime modes (PRG32_WIFI_MODE_STA, PRG32_WIFI_MODE_AP, PRG32_WIFI_MODE_APSTA).

After the startup splash, the resident ESP32-C6 firmware enters setup mode when A and B are held during boot, whenever no stored cartridge is available, or when multiple cartridges are available but no default cartridge has been saved. If one cartridge is available, it starts automatically. If a default cartridge has been saved, that cartridge starts automatically even when multiple slots are filled.

The setup main menu contains cartridge launch, default cartridge selection, Wi-Fi setup, Cartridge Store configuration and browsing, audio setup, the developer band menu, the performance test, the about screen, and exit. The Cartridge Store integration contract adds manual/discovered store URL entry, browsing, colophon preview, and download-to-slot behavior for future firmware work. Use UP/DOWN to choose, SELECT or A to confirm, and B to cancel/back. The device smoke test is now the external DeviceDemo cartridge, which exercises display, input, audio, sprites, scrolling, playfield rendering, status bands, and small classroom sketches through the same cartridge ABI used by student games.

Normal images autoload their only stored cartridge, or the saved default when multiple cartridges are present. PRG32_BOOT_SETUP_MODE in idf.py menuconfig (PRG32 Firmware Features -> Boot Setup Mode) can force setup on every boot for custom classroom images. If PRG32_PIN_SETUP is wired, holding it low during boot also forces setup mode.

Useful calls:

  • prg32_wifi_setup_requested()
  • prg32_wifi_setup_run()
  • prg32_wifi_start_mode(config)
  • prg32_wifi_current_mode()
  • prg32_wifi_current_ip()
  • prg32_wifi_current_ssid()

Multiplayer

PRG32 multiplayer is a cartridge-level state-sharing service. A cartridge opts in by calling prg32_multiplayer_join(signature, flags) from its own code, or by being packaged with python3 -m prg32 cartridge build --multiplayer. Use short ASCII signatures such as pong-v1 or mygame:lab. Players only see peers that joined the same cartridge signature, so different games or different cartridge revisions do not share a playfield.

The ESP32-C6 transport uses Wi-Fi station mode and WebSocket over TCP:

  • ESP32-C6 has native Wi-Fi, so station mode is the right physical network transport for a classroom LAN.
  • WebSocket keeps one persistent bidirectional connection, which is lower latency and simpler than repeated HTTP polling.
  • The classroom server is Node.js with the ws package, which is small enough to run on an instructor laptop.

Useful calls:

  • prg32_multiplayer_init()
  • prg32_multiplayer_available()
  • prg32_multiplayer_join(signature, flags)
  • prg32_multiplayer_leave()
  • prg32_multiplayer_tick()
  • prg32_multiplayer_set_local_state(x, y, sprite, flags)
  • prg32_multiplayer_set_input(input)
  • prg32_multiplayer_get_peer_count()
  • prg32_multiplayer_get_peer(index, out)

Run the standalone relay server from riscv-prg32/MultiplayerServer:

git clone https://github.com/riscv-prg32/MultiplayerServer.git
cd MultiplayerServer
npm install
npm start

Configure the board-side endpoint in idf.py menuconfig (PRG32 Firmware Features -> Multiplayer Server URL) with PRG32_MULTIPLAYER_SERVER_URL. QEMU exposes the same API with an offline local stub: prg32_multiplayer_available() returns true, join succeeds for a non-empty signature, and peer snapshots are empty by default.

Tile engine

The tile engine exposes a 40x25 grid of 8x8 tiles. This matches a 320x200 retro screen exactly.

Useful calls:

  • prg32_tile_define(id, bitmap8x8, fg, bg): define a reusable tile.
  • prg32_tile_put(tx, ty, id): place a tile in the simple 40x25 tile map.
  • prg32_tile_present(): draw dirty simple-map tiles and present the frame.

The simple tile map is best for first tile exercises. Use playfields when the lesson needs scrolling, parallax, or two layers.

Scrolling and Playfields

PRG32 includes two scrollable 64x32 tile playfields. A playfield is larger than the visible 40x25 tile viewport, so it can scroll horizontally and vertically.

Useful calls:

  • prg32_playfield_clear(layer, tile_id): fill one playfield with a tile.
  • prg32_playfield_put(layer, tx, ty, id): place a tile in one playfield.
  • prg32_playfield_scroll(layer, x, y): set pixel scroll for one layer.
  • prg32_playfield_scroll_by(layer, dx, dy): move one layer by a delta.
  • prg32_playfield_camera(x, y): set a shared camera position.
  • prg32_playfield_parallax(layer, x_q8, y_q8): set camera scale per layer.
  • prg32_playfield_draw(layer, transparent_zero): draw one layer.
  • prg32_playfield_draw_dual(): draw layer 0 opaque and layer 1 transparent.

Parallax factors use Q8 fixed point:

256 = 1.0x camera speed
128 = 0.5x camera speed
 64 = 0.25x camera speed

For a parallax background, set layer 0 to a smaller factor and layer 1 to PRG32_PARALLAX_1X. The foreground layer treats tile 0 as transparent when drawn through prg32_playfield_draw_dual().

Platform Tile Engine

The platform helpers build on playfields by assigning behavior flags to tile IDs and moving an actor rectangle through the flagged world.

Tile flags:

  • PRG32_TILE_FLAG_SOLID: blocks movement from every side.
  • PRG32_TILE_FLAG_PLATFORM: one-way floor, useful for ledges.
  • PRG32_TILE_FLAG_HAZARD: marks spikes, enemies, or damage tiles.
  • PRG32_TILE_FLAG_COLLECT: marks collectible tiles.

Actor state bits:

  • PRG32_PLATFORM_ON_GROUND
  • PRG32_PLATFORM_HIT_LEFT
  • PRG32_PLATFORM_HIT_RIGHT
  • PRG32_PLATFORM_HIT_HEAD
  • PRG32_PLATFORM_HAZARD
  • PRG32_PLATFORM_COLLECT

Useful calls:

  • prg32_platform_tile_flags(tile_id, flags): define tile behavior.
  • prg32_platform_actor_init(actor, layer, x, y, w, h): create an actor.
  • prg32_platform_actor_step(actor, input, speed, jump, gravity, max_fall): apply left/right movement, jump, gravity, and tile collision.
  • prg32_platform_camera_follow(actor, deadzone_x, deadzone_y): follow an actor inside the playfield world.

The platform engine intentionally uses integer pixels and small rectangles so students can inspect every value from C or RISC-V assembly.

Assembly labs can use the PRG32_PLATFORM_ACTOR_*_OFFSET macros or treat prg32_platform_actor_t as a 24-byte record:

0:x  4:y  8:vx  12:vy  16:w  18:h  20:state  22:layer

Sprite engine

The sprite layer provides simple bitmap drawing and axis-aligned bounding-box collision detection.

Useful calls:

  • prg32_sprite_draw_8x8(x, y, bits, fg, bg): draw a monochrome sprite.
  • prg32_sprite_draw_16x16(x, y, rgb565): draw a 16x16 RGB565 sprite.
  • prg32_sprite_draw_24x24(x, y, rgb565): draw a 24x24 RGB565 sprite from 24 * 24 contiguous halfwords.
  • prg32_sprite_hitbox(...): test two axis-aligned rectangles.
  • prg32_sprite_anim_frame(now_ms, frame_count, frame_ms): compute a frame.
  • prg32_sprite_draw_frame(...): draw one frame from a sprite sheet.
  • prg32_sprite_draw_indexed(...): draw a packed 1/2/4/8-bpp palette frame.
  • prg32_sprite_draw_bitplanes(...): draw a planar 1/2/4/8-bpp palette frame.
  • prg32_gfx_pixel_indexed(...), prg32_gfx_rect_indexed(...), and prg32_gfx_clear_indexed(...): write system-palette indices directly.
  • prg32_palette_set(...) / prg32_palette_get(...): change or inspect a system-palette entry; changing it recolors existing indexed pixels.

The 16x16 and 24x24 helpers treat PRG32_COLOR_WHITE as transparent. For other sizes or another transparency key, prg32_sprite_draw_frame accepts width, height, a pointer to contiguous RGB565 frames, the frame index, and a transparent color. This keeps animated sprites usable from assembly without requiring a C object.

Compact sprites use prg32_indexed_sprite_t, which contains pointers to packed pixel data and an RGB565 palette plus width, height, frame count, bit depth, and an optional transparent palette index. They save cartridge RAM and flash for graphics and animations. On ILI9341 builds their local RGB565 palettes are mapped once per draw to the native 8-bit framebuffer; RGB565 expansion happens later in the dirty SPI strip. Existing RGB565 signatures and transparency behavior are unchanged, although non-system colors are deterministically quantized to the 6x6x6 system cube.

The asset converter emits a tagged alias for each descriptor. That alias works through the existing 16x16, 24x24, arbitrary-frame, and animation entry points; the animation initializer takes dimensions and frame count from the descriptor, and the existing animation draw call expands the selected frame. No additional framebuffer or runtime decompression buffer is allocated.

Every sprite renderer clips once, holds the graphics mutex once, and records at most one dirty rectangle. Indexed 1/2/4/8-bpp and bitplane sources decode to 8-bit destination indices on ILI9341 builds. QEMU retains its RGB565 host surface while exposing the same public palette API.

See examples/games/frogger/graphics/game.S for the assembly call sequence and examples/games/frogger/c/game.c for a fuller game that pairs the 24x24 sprite with prg32_sprite_hitbox.

Audio

PRG32 has two audio layers.

Warning

Cartridge Audio Best Practices: Please don't use the legacy buzzer functions since the physical buzzer is no longer used by default. Just use the new prg32_audio_note whenever necessary, the prg32_audio_note_on and _off if you need to leave something on, the sample functions if you actually need to play a sample, and the track functions if there is a tracker sequence.

The legacy teaching helpers still use PWM to drive a passive buzzer:

  • prg32_buzzer_tone(hz, ms, duty): PWM tone with explicit duty cycle (512 is 50%).
  • prg32_audio_note(channel, instrument, note, volume, duration_ms): play an asynchronous audio note playing via I2S on a speaker.
  • prg32_audio_notes(channel, instrument, volume, notes, count): play a blocking sequence of notes where notes is an array of prg32_midi_note_t.
  • prg32_buzzer_play_notes(notes, count): blocking sequence of notes/rests.
  • prg32_audio_note_on(channel, instrument, note, volume): start a PCM or procedural instrument note.
  • prg32_buzzer_sample_u8(samples, count, rate): play unsigned 8-bit samples via buzzer through PWM.

The I2S audio runtime lives in the prg32_audio component and targets MAX98357A DAC/amplifier boards:

  • mono mode: one MAX98357A, default, 22050 Hz, 8 voices
  • stereo mode: two MAX98357A boards, optional PRG32 Audio Plus, panned voices
  • PCM and SID-like triangle, saw, pulse, and deterministic noise voices
  • per-synth-voice ADSR and resonant low-pass filtering

Useful calls:

  • prg32_audio_init(config): start the I2S mixer runtime.
  • prg32_audio_get_mode(): return mono or stereo.
  • prg32_audio_register_sample(...): register unsigned 8-bit PCM.
  • prg32_audio_play_sample(sample_id, volume, pitch): trigger a sample.
  • prg32_audio_play_sample_pan(sample_id, volume, pitch, pan): trigger with pan.
  • prg32_audio_note_on(channel, instrument, note, volume): start a PCM or procedural instrument note.
  • prg32_audio_note_on_pan(...): start a note with a stereo pan override.
  • prg32_audio_note_off(channel): stop PCM immediately or release a synth note.
  • prg32_audio_play_track(track_id): start tracker event playback.

Pitch 1024 means natural sample speed. Volumes use 0..255. Pan uses -64..+63; mono mode accepts pan calls but outputs mono.

See docs/tools/audio.md for wiring, synth-ID encoding, ADSR/filter behavior, examples, and the cartridge AUDIO block format.

The setup audio menu auto-detects the active output path:

  • none
  • PWM buzzer
  • mono I2S
  • stereo I2S

It lets trainers set the test volume, play a short tune, and toggle the onboard RGB LED as a spectrum-style VU meter when the LED GPIO is available.

Onboard RGB LED

PRG32 exposes a small addressable RGB LED API:

  • prg32_rgb_led_init(gpio): initialize the board LED on a free GPIO.
  • prg32_rgb_led_available(): return whether the LED is ready.
  • prg32_rgb_led_set(red, green, blue): set 8-bit RGB intensity.
  • prg32_rgb_led_off(): turn the LED off.
  • prg32_rgb_led_vu(level): map a 0-255 level to a blue/green/yellow/red spectrum color.
  • prg32_audio_led_vu_enable(enabled): let the audio test and PWM helpers drive the LED as a VU meter.

The reference ILI9341 wiring uses GPIO8 for LCD D/C. Many ESP32-C6 development boards also use GPIO8 for the onboard RGB LED, so PRG32_PIN_RGB_LED defaults to -1 in idf.py menuconfig. Set it only when the LED pin is free on the chosen board wiring.

Development Guide

Important

This information is only intended for developers of the PRG32 framework.

General Framework Editing Guidelines

The public ABI is components/prg32/include/prg32.h. Keep it small, stable, and friendly to RISC-V assembly callers.

When editing framework code:

  • Keep dependencies in components/prg32/CMakeLists.txt.
  • Keep REQUIRES and PRIV_REQUIRES independent of CONFIG_* choices; ESP-IDF expands component requirements before configuration-dependent source choices.
  • Configure app-specific pins and features using idf.py menuconfig (under PRG32 framework).
  • Preserve prg32_init() as the one-call framework initializer.
  • Do not expose ESP-IDF-only types in the public ABI unless absolutely needed.
  • Return simple int status codes for APIs called from assembly.
  • Check pointer inputs in helpers that can be called from student code.
  • Keep comments short and educational where they clarify hardware or ABI behavior.