pip install ziglang cross-compiles to thumb-freestanding-eabi, which is enough to build a .app with no arm-none-eabi-gcc and no Docker. Measured refusals: --defsym, -Ttext and --section-start come back as unsupported linker args, so the VMA is resolved into a copy of app.ld; -T is forwarded (a missing script errors); --image-base is accepted but page-aligns the segments into 0x200103C8 and 0x20020C94. tools/elf2bin.py extracts allocated sections rather than program headers, because lld maps the ELF header and phdr table as a 180-byte LOAD of its own -- following the headers starts the image at 0x20000000 and APP_ERR_VMA. One division pulled in __aeabi_uidiv, which a -nostdlib blob cannot have: Minesweeper now avoids division entirely. app_main carries the .text.entry attribute upstream's apps use, so the entry is first for the loader's jump to offset 0. Minesweeper builds to 2408 bytes of code against a 4096-byte budget. Installed through the page, the firmware reads the slot header twice and then exactly code_size bytes from slot+0x1000 -- the only read of that size in the boot log -- so the blob shape, header, CRC, VMA and offset are all accepted. Whether control reaches the overlay is unproven: the 100 ms PC probe saw no overlay address, no APP ERROR screen appears, and the app does not draw. test_elf2bin pins the phantom-header-segment lesson; both AGENTS files record the rest.
Minesweeper — an overlay app for the F4HWN Labs edition
Our own app: a 9x9 minesweeper that runs on the radio inside the 4 KiB overlay that
App/apps/app_overlay.h reserves. It is written against upstream's App/apps/app_api.h
and built with upstream's app.ld — neither is vendored here (both are Apache-2.0
files from armel/uv-k1-k5v3-firmware-custom),
so drop this folder into App/apps/minesweeper/ next to them, or point -I at a copy.
Why it looks the way it does
| constraint | consequence |
|---|---|
| the radio has no left/right keys (UP, DOWN, MENU, EXIT, STAR, F, 0-9 only) | the cursor walks the field with UP/DOWN and digits jump to a row then a column: 3 5 = row 3, column 5 |
| 4 KiB for text+rodata+data+bss together | no lookup tables, no floats, no libc; adjacency is counted on the fly and each cell is one bit |
| 81 cells do not fit a 16-bit mask | three 9-byte bit arrays addressed by cell >> 3, cell & 7 — a uint16_t version compiled fine and was wrong past cell 15 |
| the resident pixel helpers do not bound-check | put()/invert() clip |
no rand() in a freestanding blob |
a small LCG; mines are placed after the first reveal, keeping the 3x3 around it clear |
Keys: UP/DOWN move, 1-9 pick row then column, MENU reveal, F flag, STAR new game,
EXIT quit. M in the corner is the remaining-mine count, A1 is the cursor.
Build
arm-none-eabi-gcc -mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding \
-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=0x20000280 \
-o minesweeper.elf minesweeper_app.c
arm-none-eabi-objcopy -O binary minesweeper.elf minesweeper.bin
pack_app.py minesweeper.bin Minesweeper.app --name Minesweeper --ver 1.0 \
--vma 0x20000280 --api-min 1 # pack_app.py lives in App/apps/
build.sh does exactly that and needs the Arm GNU Toolchain on PATH.
Then install it from the page: Overlay apps → a slot → pick Minesweeper.app →
Install → Ask the radio should answer Minesweeper. On the radio press
F then 7 and MENU to run it.
What is verified, and what is not
Verified here: the source compiles clean with gcc -Wall -Wextra -Werror against
upstream's real app_api.h (that check caught the API's actual member names —
api->fb, print_tiny(s, x, y, statusbar, fill) with five arguments — and the fact
that APP_KEY_LEFT/APP_KEY_RIGHT do not exist).
Not verified: it has never been built for ARM or run on the radio, because no
arm-none-eabi-gcc and no Docker exist on the machine it was written on. Treat the
first build and the first run as the real review.
The host harness (how far verification got)
host_test.c includes the app source and hands it a fake app_api_t, so the real state
machine runs on the PC -- every string it draws is recorded and the lit pixels are counted:
gcc -std=gnu11 -O1 -Wall -Wextra -Werror -I.. -o host_test host_test.c && ./host_test
Measured: a reveal/flag/new-game/digit/quit script returns normally, draws the title and the
mine count, and lights pixels; a script that blindly reveals 85 cells reaches a terminal state
eleven times, draws BOOM, and MENU starts a new game afterwards. The win path (CLEAR) was
not reached by blind play, so it is the one branch this harness does not exercise.
Still unverified: never built for ARM, never run on the radio -- no arm-none-eabi-gcc and no
Docker where it was written. The first build and the first run are the real review.