mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
Fix the blank screen, correct the multiboot note, and add Minesweeper
The blank screen was self-inflicted and the earlier explanation was wrong. The FMP3 marker at 0x100000 is an ordinary state record -- generation, image_size, image_crc32, firmware_slot/slot_inv, config_bank/bank_inv and a matching state_crc32 (0x661286F1) -- not a pending flag. What actually happened: a firmware uploaded over a state that recorded a different identity made the firmware take the restore/adopt path, which draws nothing (panel 1024/1024 bytes zero) while the serial banner printed normally. Restoring the untouched dump fixed it at once (485/1024 bytes lit) and the three apps reinstalled and were confirmed by 0x0730. Clearing the marker sectors was tried twice (a whole 8 KiB, then just the 24-byte headers) and is not a fix: it sends the firmware down MB_MARK_MISSING = fresh radio, which adopts the running firmware slowly and without drawing, and its own write-back restores the marker anyway. AGENTS.md and AGENTS.zh-CN.md now say so in place of the wrong claim. apps/minesweeper/ adds our own 9x9 minesweeper for the 4 KiB overlay: no left/right keys exist on this radio (so the cursor walks with UP/DOWN and digits pick a row then a column), 81 cells need three 9-byte bit arrays rather than a uint16_t mask (the uint16_t version compiled fine and was wrong past cell 15), mines are placed after the first reveal so it cannot lose immediately, and the source compiles clean under gcc -Wall -Wextra -Werror against upstream's real app_api.h. It has not been built for ARM or run -- no toolchain here -- and the README says so.
This commit is contained in:
1 parent
2b3222155f
commit
ff14dffb67
5 files changed
+452
-23
No files matched your search
@@ -0,0 +1,46 @@
|
||||
# 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](https://github.com/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.
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build one overlay-app blob (.app) locally, without Docker.
|
||||
#
|
||||
# Upstream runs this same file inside the uvk1-uvk5v3 image; it already prefers
|
||||
# arm-none-eabi-gcc from PATH when there is one, which is what makes a local build
|
||||
# possible. Link VMA is pinned at 0x20000280 and must match the firmware's
|
||||
# __mb_workspace_start -- the loader compares them and refuses with APP_VMA.
|
||||
set -euo pipefail
|
||||
|
||||
APP="$(basename "$PWD")"
|
||||
APP_NAME="Minesweeper" # <-- the only per-app line: the label in the 64-byte header
|
||||
APP_VER="1.0"
|
||||
APP_API_MIN=1
|
||||
APP_VMA=${APP_VMA:-0x20000280}
|
||||
OUT="${APP_NAME// /}"
|
||||
|
||||
CC=${CC:-arm-none-eabi-gcc}
|
||||
OBJCOPY=${OBJCOPY:-arm-none-eabi-objcopy}
|
||||
command -v "$CC" >/dev/null 2>&1 || {
|
||||
echo "no $CC on PATH; install the Arm GNU Toolchain (arm-none-eabi) first" >&2; exit 2; }
|
||||
|
||||
CFLAGS="-mcpu=cortex-m0plus -mthumb -Os -std=gnu11 -ffreestanding -fno-builtin -fno-common \
|
||||
-fomit-frame-pointer -ffunction-sections -fdata-sections -Wall -Wextra"
|
||||
LDFLAGS="-nostdlib -nostartfiles -T app.ld -Wl,--defsym,APP_VMA=${APP_VMA} \
|
||||
-Wl,--gc-sections -Wl,-Map=${APP}.map -Wl,--build-id=none"
|
||||
|
||||
rm -f ./*.app ./*.elf ./*.bin
|
||||
"$CC" $CFLAGS $LDFLAGS -o "${APP}.elf" "${APP}_app.c"
|
||||
"$OBJCOPY" -O binary "${APP}.elf" "${APP}.bin"
|
||||
python3 pack_app.py "${APP}.bin" "${OUT}.app" --name "$APP_NAME" --ver "$APP_VER" \
|
||||
--vma "$APP_VMA" --api-min "$APP_API_MIN"
|
||||
ls -l "${OUT}.app"
|
||||
@@ -0,0 +1,343 @@
|
||||
/* Minesweeper — overlay app for the F4HWN Labs edition.
|
||||
*
|
||||
* Written against App/apps/app_api.h. The constraints shaped it:
|
||||
* - the radio has NO left/right keys (UP, DOWN, MENU, EXIT, STAR, F, 0-9 only), so the
|
||||
* cursor walks the field with UP/DOWN and digits jump straight to a row and column
|
||||
* - 4 KiB for text+rodata+data+bss together: no lookup tables, no floats, no libc,
|
||||
* adjacency counted on the fly, one bit per cell
|
||||
* - the resident pixel helpers do not bound-check, so put() clips
|
||||
*
|
||||
* Keys: UP/DOWN move the cursor, 1-9 pick row then column (3 then 5 = row 3, col 5),
|
||||
* MENU reveal, F flag, STAR new game, EXIT quit. The first reveal is always safe:
|
||||
* the mines are placed after it, keeping the 3x3 around it clear.
|
||||
*
|
||||
* The 81 cells do not fit in any single integer type this chip shifts cheaply, so the
|
||||
* three cell sets are 9-byte bit arrays addressed by cell index (cell >> 3, cell & 7).
|
||||
* A uint16_t version of this compiled fine and would have been wrong past cell 15.
|
||||
*/
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "../app_api.h"
|
||||
|
||||
#define N 9 /* 9x9: cell pitch 6 px -> 54x54 on a 128x64 screen */
|
||||
#define PITCH 6
|
||||
#define FIELD_X 4
|
||||
#define FIELD_Y 10
|
||||
#define MINES 10
|
||||
#define CELLS (N * N)
|
||||
#define BYTES ((CELLS + 7) / 8)
|
||||
|
||||
static const app_api_t *A;
|
||||
|
||||
static uint8_t g_mine[BYTES];
|
||||
static uint8_t g_open[BYTES];
|
||||
static uint8_t g_flag[BYTES];
|
||||
static uint8_t g_cursor;
|
||||
static uint8_t g_pending; /* 0 = no digit typed, 1 = row typed */
|
||||
static uint8_t g_row_pick;
|
||||
static uint8_t g_placed;
|
||||
static uint8_t g_state; /* 0 play, 1 lost, 2 won */
|
||||
static int8_t g_mine_left;
|
||||
|
||||
static bool bit(const uint8_t *set, uint8_t cell)
|
||||
{
|
||||
return ((set[cell >> 3] >> (cell & 7)) & 1u) != 0;
|
||||
}
|
||||
|
||||
static void setbit(uint8_t *set, uint8_t cell, bool on)
|
||||
{
|
||||
uint8_t mask = (uint8_t)(1u << (cell & 7));
|
||||
if (on)
|
||||
set[cell >> 3] |= mask;
|
||||
else
|
||||
set[cell >> 3] &= (uint8_t)~mask;
|
||||
}
|
||||
|
||||
static void put(int16_t x, int16_t y, bool ink)
|
||||
{
|
||||
if (x < 0 || x >= 128 || y < 0 || y >= 64)
|
||||
return; /* the API's helpers do not clip */
|
||||
if (ink)
|
||||
A->fb[y][x >> 3] |= (uint8_t)(1u << (7u - (x & 7)));
|
||||
else
|
||||
A->fb[y][x >> 3] &= (uint8_t)~(1u << (7u - (x & 7)));
|
||||
}
|
||||
|
||||
static void invert(int16_t x, int16_t y)
|
||||
{
|
||||
if (x < 0 || x >= 128 || y < 0 || y >= 64)
|
||||
return;
|
||||
A->fb[y][x >> 3] ^= (uint8_t)(1u << (7u - (x & 7)));
|
||||
}
|
||||
|
||||
static void box(int16_t x0, int16_t y0, int16_t x1, int16_t y1, bool ink)
|
||||
{
|
||||
for (int16_t x = x0; x <= x1; x++) {
|
||||
put(x, y0, ink);
|
||||
put(x, y1, ink);
|
||||
}
|
||||
for (int16_t y = y0; y <= y1; y++) {
|
||||
put(x0, y, ink);
|
||||
put(x1, y, ink);
|
||||
}
|
||||
}
|
||||
|
||||
static int8_t neighbours(uint8_t cell)
|
||||
{
|
||||
int8_t row = (int8_t)(cell / N);
|
||||
int8_t col = (int8_t)(cell % N);
|
||||
int8_t count = 0;
|
||||
|
||||
for (int8_t dy = -1; dy <= 1; dy++) {
|
||||
for (int8_t dx = -1; dx <= 1; dx++) {
|
||||
int8_t r = (int8_t)(row + dy);
|
||||
int8_t c = (int8_t)(col + dx);
|
||||
if ((dx == 0 && dy == 0) || r < 0 || r >= N || c < 0 || c >= N)
|
||||
continue;
|
||||
if (bit(g_mine, (uint8_t)(r * N + c)))
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void place_mines(uint8_t safe)
|
||||
{
|
||||
/* No rand() in a freestanding blob, so a cheap LCG. Seeded from the first reveal and
|
||||
* a counter so two games in a row differ. */
|
||||
static uint32_t seed = 1u;
|
||||
int8_t want = MINES;
|
||||
|
||||
for (uint8_t i = 0; i < BYTES; i++)
|
||||
g_mine[i] = 0;
|
||||
|
||||
seed += (uint32_t)safe * 2654435761u + 1u;
|
||||
while (want > 0) {
|
||||
seed = seed * 1103515245u + 12345u;
|
||||
uint8_t cell = (uint8_t)((seed >> 16) % CELLS);
|
||||
int8_t row = (int8_t)(cell / N), col = (int8_t)(cell % N);
|
||||
int8_t srow = (int8_t)(safe / N), scol = (int8_t)(safe % N);
|
||||
|
||||
if (bit(g_mine, cell))
|
||||
continue;
|
||||
/* the 3x3 around the first reveal stays clear, so it cannot lose at once */
|
||||
if (row >= srow - 1 && row <= srow + 1 && col >= scol - 1 && col <= scol + 1)
|
||||
continue;
|
||||
setbit(g_mine, cell, true);
|
||||
want--;
|
||||
}
|
||||
g_placed = 1;
|
||||
}
|
||||
|
||||
static void reveal(uint8_t cell)
|
||||
{
|
||||
/* Iterative flood fill: an explicit stack of cell indices, no recursion. */
|
||||
static uint8_t stack[CELLS];
|
||||
int16_t top = 0;
|
||||
|
||||
stack[top++] = cell;
|
||||
while (top > 0) {
|
||||
uint8_t cur = stack[--top];
|
||||
|
||||
if (bit(g_open, cur))
|
||||
continue;
|
||||
setbit(g_open, cur, true);
|
||||
if (neighbours(cur) != 0)
|
||||
continue;
|
||||
|
||||
int8_t row = (int8_t)(cur / N), col = (int8_t)(cur % N);
|
||||
for (int8_t dy = -1; dy <= 1; dy++) {
|
||||
for (int8_t dx = -1; dx <= 1; dx++) {
|
||||
int8_t r = (int8_t)(row + dy), c = (int8_t)(col + dx);
|
||||
if (r < 0 || r >= N || c < 0 || c >= N)
|
||||
continue;
|
||||
uint8_t next = (uint8_t)(r * N + c);
|
||||
if (!bit(g_open, next) && !bit(g_flag, next) && top < CELLS)
|
||||
stack[top++] = next;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void new_game(void)
|
||||
{
|
||||
for (uint8_t i = 0; i < BYTES; i++) {
|
||||
g_mine[i] = 0;
|
||||
g_open[i] = 0;
|
||||
g_flag[i] = 0;
|
||||
}
|
||||
g_cursor = (uint8_t)(4 * N + 4);
|
||||
g_pending = 0;
|
||||
g_row_pick = 0;
|
||||
g_placed = 0;
|
||||
g_state = 0;
|
||||
g_mine_left = MINES;
|
||||
}
|
||||
|
||||
static void step(int8_t delta)
|
||||
{
|
||||
int8_t cell = (int8_t)((int8_t)g_cursor + delta);
|
||||
if (cell < 0)
|
||||
cell = (int8_t)(CELLS - 1);
|
||||
if (cell >= CELLS)
|
||||
cell = 0;
|
||||
g_cursor = (uint8_t)cell;
|
||||
}
|
||||
|
||||
static void two_digits(char *out, int8_t value)
|
||||
{
|
||||
if (value < 0)
|
||||
value = 0;
|
||||
if (value > 99)
|
||||
value = 99;
|
||||
out[0] = (char)('0' + (value / 10) % 10);
|
||||
out[1] = (char)('0' + value % 10);
|
||||
out[2] = 0;
|
||||
}
|
||||
|
||||
static void draw(void)
|
||||
{
|
||||
char text[3];
|
||||
|
||||
A->display_clear();
|
||||
|
||||
A->print_tiny("M", 0, 1, false, false);
|
||||
two_digits(text, g_mine_left);
|
||||
A->print_tiny(text, 8, 1, false, false);
|
||||
|
||||
if (g_state == 1)
|
||||
A->print_tiny("BOOM", 46, 1, false, false);
|
||||
else if (g_state == 2)
|
||||
A->print_tiny("CLEAR", 42, 1, false, false);
|
||||
else
|
||||
A->print_tiny("F4HWN MINES", 34, 1, false, false);
|
||||
|
||||
text[0] = (char)('A' + (g_cursor / N));
|
||||
text[1] = (char)('1' + (g_cursor % N));
|
||||
text[2] = 0;
|
||||
A->print_tiny(text, 110, 1, false, false);
|
||||
if (g_pending)
|
||||
A->print_tiny("-", 122, 1, false, false);
|
||||
|
||||
for (uint8_t cell = 0; cell < CELLS; cell++) {
|
||||
int16_t x = (int16_t)(FIELD_X + (cell % N) * PITCH);
|
||||
int16_t y = (int16_t)(FIELD_Y + (cell / N) * PITCH);
|
||||
bool opened = bit(g_open, cell);
|
||||
bool mine = bit(g_mine, cell);
|
||||
/* mines show once the game is over, whether or not they were flagged */
|
||||
bool show_mine = mine && (g_state != 0 || opened);
|
||||
|
||||
if (show_mine) {
|
||||
box(x, y, (int16_t)(x + 4), (int16_t)(y + 4), true);
|
||||
put((int16_t)(x + 2), (int16_t)(y + 2), false);
|
||||
} else if (opened) {
|
||||
int8_t n = neighbours(cell);
|
||||
if (n > 0) {
|
||||
text[0] = (char)('0' + n);
|
||||
text[1] = 0;
|
||||
A->print_tiny(text, (uint8_t)x, (uint8_t)y, false, false);
|
||||
}
|
||||
} else if (bit(g_flag, cell)) {
|
||||
box((int16_t)(x + 1), (int16_t)(y + 1), (int16_t)(x + 3), (int16_t)(y + 3), true);
|
||||
put((int16_t)(x + 2), (int16_t)(y + 4), true);
|
||||
}
|
||||
}
|
||||
|
||||
/* the cursor inverts the frame around its cell, so it shows on ink and on paper */
|
||||
int16_t cx = (int16_t)(FIELD_X + (g_cursor % N) * PITCH - 1);
|
||||
int16_t cy = (int16_t)(FIELD_Y + (g_cursor / N) * PITCH - 1);
|
||||
for (int16_t i = 0; i <= PITCH + 1; i++) {
|
||||
invert((int16_t)(cx + i), cy);
|
||||
invert((int16_t)(cx + i), (int16_t)(cy + PITCH + 1));
|
||||
invert(cx, (int16_t)(cy + i));
|
||||
invert((int16_t)(cx + PITCH + 1), (int16_t)(cy + i));
|
||||
}
|
||||
|
||||
A->blit_full();
|
||||
}
|
||||
|
||||
static void check_win(void)
|
||||
{
|
||||
int16_t closed = 0;
|
||||
|
||||
for (uint8_t cell = 0; cell < CELLS; cell++)
|
||||
if (!bit(g_open, cell) && !bit(g_mine, cell))
|
||||
closed++;
|
||||
if (closed == 0) {
|
||||
g_state = 2;
|
||||
A->play_tone(880, 120);
|
||||
A->play_tone(1320, 160);
|
||||
}
|
||||
}
|
||||
|
||||
void app_main(const app_api_t *api)
|
||||
{
|
||||
A = api;
|
||||
new_game();
|
||||
|
||||
while (true) {
|
||||
draw();
|
||||
|
||||
uint8_t key = A->get_key();
|
||||
if (key == APP_KEY_INVALID || key == APP_KEY_SAVER) {
|
||||
A->delay_ms(40);
|
||||
continue;
|
||||
}
|
||||
if (key == APP_KEY_EXIT)
|
||||
return; /* the loader restores the radio */
|
||||
if (key == APP_KEY_STAR) {
|
||||
new_game();
|
||||
continue;
|
||||
}
|
||||
if (g_state != 0) { /* after BOOM or CLEAR, MENU restarts */
|
||||
if (key == APP_KEY_MENU)
|
||||
new_game();
|
||||
continue;
|
||||
}
|
||||
if (key == APP_KEY_UP) {
|
||||
step(-1);
|
||||
continue;
|
||||
}
|
||||
if (key == APP_KEY_DOWN) {
|
||||
step(1);
|
||||
continue;
|
||||
}
|
||||
if (key >= APP_KEY_1 && key <= APP_KEY_9) {
|
||||
uint8_t digit = (uint8_t)(key - APP_KEY_0); /* 1..9 */
|
||||
if (!g_pending) {
|
||||
g_row_pick = digit; /* row first ... */
|
||||
g_pending = 1;
|
||||
} else {
|
||||
g_cursor = (uint8_t)((g_row_pick - 1) * N + (digit - 1)); /* ... then column */
|
||||
g_pending = 0;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (key == APP_KEY_F) { /* flag */
|
||||
if (!bit(g_open, g_cursor)) {
|
||||
if (bit(g_flag, g_cursor)) {
|
||||
setbit(g_flag, g_cursor, false);
|
||||
g_mine_left++;
|
||||
} else if (g_mine_left > 0) {
|
||||
setbit(g_flag, g_cursor, true);
|
||||
g_mine_left--;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (key == APP_KEY_MENU) { /* reveal */
|
||||
if (bit(g_flag, g_cursor))
|
||||
continue;
|
||||
if (!g_placed)
|
||||
place_mines(g_cursor);
|
||||
if (bit(g_mine, g_cursor)) {
|
||||
setbit(g_open, g_cursor, true);
|
||||
g_state = 1;
|
||||
A->play_tone(160, 400);
|
||||
continue;
|
||||
}
|
||||
reveal(g_cursor);
|
||||
check_win();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user