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.
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=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 monitorOr build for QEMU:
On Windows:
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 qemu --graphics monitorOn Linux or MacOS:
./scripts/qemu/build_qemu.shRestore 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 tools/prg32_game.py 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 tools/prg32_game.py upload build-esp32c6/pong-c.prg32 --url http://192.168.4.1For QEMU
On Windows:
build against build-qemu/PRG32.elf and stage with upload-qemu
On Linux or MacOS:
./scripts/qemu/build_qemu.sh
./scripts/qemu/qemu_inject_cartridge.sh <path_to_cartridge.prg32>
./scripts/qemu/launch_qemu.sh```
## 6. Move to Tiles and Platform Games
The platformer C example shows structs, tile flags, and camera follow:
```c
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.
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.