This tutorial uses the same PRG32 firmware as the assembly labs, but writes the game logic in C. It is intended for programming classes that want embedded, visual feedback without starting from hardware driver code.
- Split a program into
init,update, anddrawfunctions. - Store game state in variables, arrays, and structs.
- Read the PRG32 input bitmask.
- Draw graphics with the same API used by assembly examples.
- Package a C source file as a
.prg32cartridge. - Choose RGB565 or compact indexed sprite storage for an animation.
Open examples/games/pong/c/game.c. It exports three functions:
void pong_c_init(void);
void pong_c_update(void);
void pong_c_draw(void);The resident firmware or cartridge loader calls them in this order:
init once, then update/draw once per frame
Checkpoint: identify the variables that store paddle position, ball position, and ball velocity.
PRG32 buttons are returned as a bitmask:
uint32_t input = prg32_input_read();
if (input & PRG32_BTN_LEFT) {
paddle_x -= 3;
}
if (input & PRG32_BTN_RIGHT) {
paddle_x += 3;
}Reflection question: why does this code use & instead of ==?
The graphics API uses integer pixels and RGB565 colors:
prg32_gfx_clear(PRG32_COLOR_BLACK);
prg32_gfx_rect(paddle_x, 188, 64, 8, PRG32_COLOR_WHITE);
prg32_gfx_rect(ball_x, ball_y, 8, 8, PRG32_COLOR_YELLOW);Checkpoint: change the paddle color and ball size, then rebuild.
For a temporary lab build, add one C source to main/CMakeLists.txt:
idf_component_register(
SRCS
"main.c"
"../examples/games/pong/c/game.c"
REQUIRES prg32
INCLUDE_DIRS "."
)Then call it from main/main.c:
#include "prg32.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
void pong_c_init(void);
void pong_c_update(void);
void pong_c_draw(void);
void app_main(void) {
prg32_init();
pong_c_init();
while (1) {
pong_c_update();
pong_c_draw();
prg32_gfx_present();
vTaskDelay(pdMS_TO_TICKS(33));
}
}Build for hardware:
idf.py -B build-esp32c6 -D SDKCONFIG=build-esp32c6/sdkconfig -D SDKCONFIG_DEFAULTS=profiles/sdkconfig.defaults set-target esp32c6
idf.py -B build-esp32c6 -D SDKCONFIG=build-esp32c6/sdkconfig -D SDKCONFIG_DEFAULTS=profiles/sdkconfig.defaults build
idf.py -B build-esp32c6 -D SDKCONFIG=build-esp32c6/sdkconfig -D SDKCONFIG_DEFAULTS=profiles/sdkconfig.defaults flash monitorOr build for QEMU:
On Windows:
idf.py -B build-qemu -D SDKCONFIG=build-qemu/sdkconfig -D SDKCONFIG_DEFAULTS="profiles/sdkconfig.defaults;profiles/sdkconfig.defaults.qemu" set-target esp32c3
idf.py -B build-qemu -D SDKCONFIG=build-qemu/sdkconfig -D SDKCONFIG_DEFAULTS="profiles/sdkconfig.defaults;profiles/sdkconfig.defaults.qemu" qemu --graphics monitorOn Linux or MacOS:
python3 -m prg32 qemu buildRestore the default main/CMakeLists.txt and main/main.c after the lab.
After the resident firmware is built, package the same C source as a cartridge:
python3 -m prg32 cartridge build \
examples/games/pong/c/game.c \
--portable \
--entry-prefix pong_c \
--name pong-c \
--out build-esp32c6/pong-c.prg32Upload to the board:
python3 -m prg32 esp32c6 upload build-esp32c6/pong-c.prg32 --url http://192.168.4.1For QEMU
On Windows, build a portable cartridge with --portable and stage it with
python3 -m prg32 qemu upload.
On Linux or MacOS:
python3 -m prg32 qemu build
python3 -m prg32 qemu upload <path_to_cartridge.prg32>
python3 -m prg32 qemu runThe platformer C example shows structs, tile flags, and camera follow:
static prg32_platform_actor_t player;
prg32_platform_tile_flags(2, PRG32_TILE_FLAG_SOLID);
prg32_platform_actor_init(&player, 1, 32, 120, 8, 8);
prg32_platform_actor_step(&player, input, 2, -7, 1, 5);Use examples/games/platformer/c/game.c when the course reaches:
- structs
- arrays of tile data
- reusable helper functions
- state updated once per frame
The raycaster C example is the next step for advanced classes:
int hx = player_x + (dir_q8[angle][0] * dist) / 256;
int hy = player_y + (dir_q8[angle][1] * dist) / 256;Use examples/games/raycaster/c/game.c to discuss fixed-point arithmetic,
table-driven direction vectors, and why a small Doom-style renderer can run on
the same RISC-V runtime used for assembly exercises.
Use examples/games/wing_commander/c/game.c when the course reaches layered
rendering. It keeps the starfield and cockpit in separate playfields, then adds
enemies, laser input, score, and shield state in C.
Use examples/games/frogger/c/game.c when the course reaches multicolor sprite
assets. It draws a 24x24 RGB565 player sprite and uses simple rectangle
hitboxes for traffic collision. The matching
examples/games/frogger/graphics/game.S file shows the same
prg32_sprite_draw_24x24 call from RISC-V assembly.
For longer animations, use tools/prg32_image_convert.py --mode indexed to
generate one shared palette and prg32_indexed_sprite_t. Draw it directly with
prg32_sprite_draw_indexed, or pass the generated tagged *_SPRITE alias to
the existing prg32_sprite_anim_init workflow. Use RGB565 when unrestricted
per-pixel color is more important than asset size. The framebuffer and display
output remain RGB565 in both cases, and conversion fails rather than silently
reducing an over-limit palette.
Four-bit indexed output is the recommended starting point for ordinary game sprites because its 16-color palette is shared across animation frames and its renderer decodes two pixels per packed byte. This is a storage recommendation, not an API requirement: existing RGB565 cartridge source and portable binaries continue to use the unchanged calls and transparent-color behavior.
Break it:
- In
pong_c_update, remove the right boundary clamp. - Build and run.
- Hold RIGHT until the paddle leaves the viewport.
Fix it:
- Restore the clamp.
- Explain which condition protects the screen boundary.
- Add a second check for the ball and explain the difference.
- Use C examples for programming-language concepts.
- Use the matching assembly examples when the same behavior should be inspected at register and stack-frame level.
- Keep the runtime, board, QEMU workflow, and cartridge workflow identical across both tracks.