No. 69 / project of 147 on the ladder

Framebuffer revival — pixels from the chip to a host window

introduces — chip-renders-pixels / host-displays plumbing on the modern core (Verilator harness `+fb_dump`, RGB565 viewer)

harden statelast run2026-05-04
signoff
  • DRCNOT RUN
  • LVSNOT RUN
  • antennaNOT RUN

P44 had the chip write pixels into RAM and a host viewer read them out — but P44’s RTL was P09-vintage (RV32I, no Sv32, no FreeRTOS modernisation). P69 brings that same pattern forward to the P66/P68 chip with zero new RTL: the framebuffer is just RAM the chip writes to via direct address.

Headline: 24 animated frames at 128×96 RGB565, rendered by a bare-metal RV32IMAC program in ~25M cycles, dumped per-frame by the Verilator harness on a MMIO_FRAME_READY strobe, stitched into an animated GIF for the page.

P69 chip-rendered animation — scrolling sine wave on a hue-rotating gradient

The animation above is real chip output. A bare-metal program running on the P68-vintage core (RV32IMAC, no FP, no OS) draws each of those 24 frames into RAM at 0x00800000, pulses MMIO_FRAME_READY (0x10001ffc) with the frame number, and the Verilator harness writes frame_NNNN.bin for each pulse. The viewer turns them into a PIL-encoded GIF.

The single-frame test pattern from earlier still works too:

P69 test pattern — color bars, gradient, sine waveform

The image above came out of the chip. Six color bars across the top third (red/orange/yellow/green/blue/magenta), a smooth red×green gradient on a half-blue background through the middle, and an absolute-sine waveform with a soft tail at the bottom. The chip-side code that drew it lives at projects/69_framebuffer_revival/app/main.c and is about 60 lines including the test pattern.

How the plumbing works

Two paths — single-frame on halt, and per-frame on a strobe:

  1. The chip’s bare-metal program writes RGB565 halfwords to volatile uint16_t * const FB = (uint16_t *)0x00800000u; — plain stores into upper RAM.
  2. Per-frame: after each frame, the program writes the frame number to MMIO_FRAME_READY (0x10001ffc). The chip’s MMIO module pulses a frame_strobe output and latches the value as frame_seq. The Verilator harness watches the strobe; on each pulse it dumps the FB region to <dir>/frame_<seq>.bin.
  3. Single-frame: when the program is done it writes MMIO_HALT = 1. The chip halts. The harness, given +fb_dump=path, writes the FB region one last time to that path.
  4. viewer/viewer.py reads frames and either pops up a pygame window (single or movie), saves a PNG, or stitches an animated GIF.
nix develop                            # at the repo root
cd projects/69_framebuffer_revival/test
make verilator                         # build the chip simulator
make run                               # build app + run; writes captured/fb.bin
make view                              # opens captured/fb.bin in a pygame window

Why this matters

This is the scaffolding for everything graphical that comes next:

  • A raylib build — cross-compile raylib for rv32imac with a software-rendered backend that writes to 0x00800000 directly. Any raylib program — cubes, sprites, text, basic 3D — renders on the chip and pops up on the host.
  • AtomVM graphics — once the soft-float toolchain wall on P68 is resolved, an Erlang process can draw into the framebuffer. Message-passing concurrency driving pixel output is the demo.
  • Animated frames — add an MMIO frame-strobe register (0x10001ffc, the same convention P44 used) and have the harness dump per-frame into frame_NNNN.bin. Viewer plays the sequence as a movie. Plasma demos, particle systems, scrollers — all unlocked by that one register and a sequence of dumps.

What’s not here

  • No animation. The chip writes one frame and halts.
  • No double-buffering. Tearing wouldn’t matter at single-frame.
  • No font rendering. The waveform and color bars are mathy patterns, not text.
  • No iverilog flow yet. The harness changes are Verilator-only; iverilog dumping is a follow-up if we want it.

These are all natural next steps. The scaffolding is the rung — this page exists so you can look at the PNG and know the chip put each pixel there.

Build & run cheatsheet

# build the renderer ELF + boot blob
make all

# build the Verilator binary against the chip RTL
make verilator

# run the chip in Verilator and dump the FB
make run

# open captured/fb.bin in a pygame window (q to quit)
make view

# or render a PNG instead
uv run --with pillow ../viewer/viewer.py captured/fb.bin 128 96 \
    --scale 4 --png captured/fb.png

Pinned chip RTL: copy of P68’s, which adds RV32C (compressed-instruction support) on top of the P66 optimization arc. P69 itself introduces no chip-side changes.