From 7f7a3313b323783647adb652d29be96ad1b6e155 Mon Sep 17 00:00:00 2001 From: Roberto Antonacci <48053274+steponme82@users.noreply.github.com> Date: Fri, 6 Nov 2026 10:39:21 +0100 Subject: [PATCH 1/2] Light finder: a diagnostic build to map the BlackBox's button lights isod89/openpocket#8. LED_PROBE 1 (off by default) builds a firmware that plays no sound and makes one pin at a time blink at 2 Hz while its name is on the screen. The knob steps through the 50 pins, the right footswitch marks a pin, the left one redraws the screen. The screen, knob, I2S, amp enable and MASTER pins are never driven; the buttons are (on the FM-1, PH6 and PH9 are LED lines). A pin that resets the chip is skipped after the reset. USB stays up, so the web installer is the way back. tests/lpsim.c runs it against fake GPIO registers and checks that only the pin on screen ever changes, that each goes back exactly as it was, and that every line fits the screen. Added to tests/run_tests.sh. Co-Authored-By: Claude Opus 5.5 --- firmware/led_probe.h | 267 ++++++++++++++++++++++++++++++++++++++++++ firmware/openpocket.c | 13 ++ tests/lpsim.c | 267 ++++++++++++++++++++++++++++++++++++++++++ tests/run_tests.sh | 3 + 4 files changed, 550 insertions(+) create mode 100644 firmware/led_probe.h create mode 100644 tests/lpsim.c diff --git a/firmware/led_probe.h b/firmware/led_probe.h new file mode 100644 index 0000000..e028901 --- /dev/null +++ b/firmware/led_probe.h @@ -0,0 +1,267 @@ +/* SPDX-License-Identifier: GPL-3.0-only + * OpenPocket — the light finder (issue #8): which pins drive the BlackBox's button lights? + * + * Built only with LED_PROBE 1 (openpocket.c). That build plays no sound. It makes ONE pin at a time blink (an + * output, 2 Hz, so a light that is on when its pin is high and one that is on when it is low both show) while the + * pin's name is on the screen, and the knob steps through the pins. A button light that blinks in step with the + * square at the top right is on that pin, lit when the square is lit (high) or when it is dark (low). + * - The right footswitch marks the pin on screen (the list stays at the bottom, for a photo). While PA2 (the + * right footswitch itself) blinks, the left one marks instead. + * - The left footswitch redraws the screen, in case a pin upset it. + * - Never driven: PA4..PA9 (I2S MCLK, SCLK, LRCK, DAT0, DAT1 and the amp enable), PA10 (MASTER), PB1, PB5, + * PB6, PB7 and PC7 (the screen), PC0 and PC1 (the knob), port D (the flash). The buttons ARE tried: on the FM-1 + * PH6 and PH9 are LED lines, and on the Pocket X / BlackBox they read as buttons. + * - A pin that resets the chip is remembered across the reset (.noinit) and skipped from then on (the right + * footswitch on it tries it again). Switching the pedal off and on starts afresh. + * - USB stays up, as in the normal build, so the web installer takes the pedal back to a normal OpenPocket or to + * the official firmware. + * + * Included by openpocket.c (LED_PROBE 1) and by tests/lpsim.c on the PC. The including file provides: PA_IN, PC_IN, + * ENC_MASK, PA_FS_UP, PA_FS_DN, base_pa, QDEC, dbn_t / dbn_press, ms_now, fm1_wdt_feed, px_usb_service, lcd_init, + * lcd_rect, ft_box / ft_width and the fonts (ui_text.h), b_str / b_num, guard, op_diag / ST_OK, op_pwr, the UI + * colours and RED. */ +#pragma once +#include + +#ifndef LP_REG /* a GPIO port register: 0x50000 + port * 0x40 + r (bsp/chip/fm1_gpio.h) */ +#define LP_REG(port, r) (*(volatile uint32_t *)(0x50000u + (uint32_t)(port) * 0x40u + (uint32_t)(r))) +#endif +#ifndef LP_NOINIT +#define LP_NOINIT __attribute__((section(".noinit"))) +#endif +#ifndef LP_TEXT_HOOK /* tests/lpsim.c checks that every line fits */ +#define LP_TEXT_HOOK(s, f, w) ((void)0) +#endif +enum { LP_OUT = 0x00, LP_DIR = 0x08, LP_PU = 0x10, LP_PD = 0x14 }; /* DIR: 1 input, 0 output */ + +static const uint8_t LP_PORT[4] = { 0u, 1u, 2u, 7u }; /* PA, PB, PC, PH */ +static const char LP_PN[4] = { 'A', 'B', 'C', 'H' }; +static const uint32_t LP_NEVER[4] = { 0x07F0u, 0x00E2u, 0x0083u, 0x0000u }; /* PA4..10 | PB1,5,6,7 | PC0,1,7 */ +#define LP_BLINK_MS 250u /* half a period: 2 Hz */ +#define LP_MAGIC 0x4C505231u /* "LPR1" */ +static volatile struct { uint32_t magic, cur, skip[4], mark[4]; } lp_keep LP_NOINIT; /* survives a reset */ + +static struct { + uint8_t ids[64]; /* the pins tried: id = port index * 16 + bit + 1 (1..64) */ + uint32_t n, i, id; /* how many; the step on screen (0 = none); the pin driven now (0 = none) */ + uint32_t sv_out, sv_dir, sv_pu, sv_pd; /* that pin's bits as they were */ + uint32_t t_blink; + int hi, dirty; + int32_t acc; + uint8_t enc; + dbn_t fs_r, fs_l; + char note[40]; + uint16_t note_col; +} lp; + +static void lp_name(char *d, uint32_t id) /* 55 -> "PH6" */ +{ + uint32_t p = (id - 1u) >> 4, b = (id - 1u) & 15u; + int n = 0; + d[n++] = 'P'; + d[n++] = LP_PN[p]; + if (b >= 10u) { d[n++] = '1'; d[n++] = (char)('0' + b - 10u); } + else d[n++] = (char)('0' + b); + d[n] = 0; +} +static const char *lp_role(uint32_t id) /* what OpenPocket already reads there */ +{ + switch (id - 1u) { + case 0u: return "FX BUTTON"; + case 1u: return "HOME BUTTON"; + case 2u: return "RIGHT FOOTSWITCH"; + case 3u: return "LEFT FOOTSWITCH"; + case 52u: case 54u: return "KNOB PUSH"; /* PH4, PH6 (3 * 16 + bit) */ + case 55u: case 56u: return "MOD BUTTON"; /* PH7, PH8 */ + case 57u: return "AMP BUTTON"; /* PH9 */ + } + return 0; +} +static int lp_has(const volatile uint32_t *set, uint32_t id) { return (int)((set[(id - 1u) >> 4] >> ((id - 1u) & 15u)) & 1u); } +static void lp_flip(volatile uint32_t *set, uint32_t id) { set[(id - 1u) >> 4] ^= 1u << ((id - 1u) & 15u); } +static uint32_t lp_pa_driven(void) /* the PA bit being driven, if any */ +{ + return (lp.id && ((lp.id - 1u) >> 4) == 0u) ? 1u << ((lp.id - 1u) & 15u) : 0u; +} + +static void lp_take(uint32_t id) /* the pin becomes an output, low */ +{ + uint32_t P = LP_PORT[(id - 1u) >> 4], m = 1u << ((id - 1u) & 15u); + lp.sv_out = LP_REG(P, LP_OUT) & m; lp.sv_dir = LP_REG(P, LP_DIR) & m; + lp.sv_pu = LP_REG(P, LP_PU) & m; lp.sv_pd = LP_REG(P, LP_PD) & m; + lp_keep.cur = id; /* from here on, a reset names this pin */ + LP_REG(P, LP_PU) &= ~m; + LP_REG(P, LP_PD) &= ~m; + LP_REG(P, LP_OUT) &= ~m; + LP_REG(P, LP_DIR) &= ~m; +} +static void lp_level(uint32_t id, int hi) +{ + uint32_t P = LP_PORT[(id - 1u) >> 4], m = 1u << ((id - 1u) & 15u); + if (hi) LP_REG(P, LP_OUT) |= m; + else LP_REG(P, LP_OUT) &= ~m; +} +static void lp_give(uint32_t id) /* the pin back as it was */ +{ + uint32_t P = LP_PORT[(id - 1u) >> 4], m = 1u << ((id - 1u) & 15u); + if (lp.sv_dir) { /* was an input: stop driving it first */ + LP_REG(P, LP_DIR) |= m; + LP_REG(P, LP_OUT) = (LP_REG(P, LP_OUT) & ~m) | lp.sv_out; + } else { + LP_REG(P, LP_OUT) = (LP_REG(P, LP_OUT) & ~m) | lp.sv_out; + LP_REG(P, LP_DIR) &= ~m; + } + LP_REG(P, LP_PU) = (LP_REG(P, LP_PU) & ~m) | lp.sv_pu; + LP_REG(P, LP_PD) = (LP_REG(P, LP_PD) & ~m) | lp.sv_pd; + lp_keep.cur = 0; +} + +static void lp_set_note(const char *s, uint16_t col) +{ + lp.note[0] = 0; + b_str(lp.note, s); + lp.note_col = col; +} +static void lp_text(uint32_t y, const char *s, const op_font *f, uint16_t col) +{ + LP_TEXT_HOOK(s, f, 240u); + ft_box(0, y, 240, s, f, col, UI_BG, 1); +} +static void lp_dot(void) { lcd_rect(222, 10, 14, 14, (lp.id && lp.hi) ? UI_ACC : UI_PANEL); } +static void lp_draw(void) +{ + char l[48]; + uint32_t id = lp.i ? lp.ids[lp.i - 1u] : 0u, drv = lp_pa_driven(), k = 0; + int skip = id && lp_has(lp_keep.skip, id), marked = id && lp_has(lp_keep.mark, id); + const char *fs = (drv & PA_FS_UP) ? "LEFT" : "RIGHT"; + LP_TEXT_HOOK("LIGHT FINDER", &FT_MD, 200u); + ft_box(20, 6, 200, "LIGHT FINDER", &FT_MD, UI_ACC, UI_BG, 1); + lp_dot(); + lp_text(36, lp.note, &FT_SM, lp.note_col); + l[0] = 0; + if (id) lp_name(l, id); else b_str(l, "READY"); + lp_text(64, l, &FT_NM, skip ? UI_DIM : UI_TXT); + l[0] = 0; + if (!id) { b_num(l, lp.n); b_str(l, " PINS TO TRY"); } + else if (skip) b_str(l, "SKIPPED: IT RESET THE PEDAL"); + else { b_str(l, "BLINKING "); b_num(l, lp.i); b_str(l, " / "); b_num(l, lp.n); } + lp_text(104, l, &FT_SM, skip ? RED : UI_ACC); + l[0] = 0; + if (id && lp_role(id)) { b_str(l, lp_role(id)); if (!skip) b_str(l, ": DON'T PRESS IT"); } + lp_text(124, l, &FT_SM, UI_DIM); + l[0] = 0; + if (!id) b_str(l, "WATCH THE BUTTON LIGHTS"); + else if (skip) { b_str(l, fs); b_str(l, " FOOTSWITCH: TRY AGAIN"); } + else if (marked) { b_str(l, "MARKED. "); b_str(l, fs); b_str(l, ": UNMARK"); } + else { b_str(l, fs); b_str(l, " FOOTSWITCH: MARK IT"); } + lp_text(144, l, &FT_SM, marked ? UI_OK : UI_TXT); + lp_text(164, (drv & (PA_FS_UP | PA_FS_DN)) ? "" : "LEFT FOOTSWITCH: REDRAW", &FT_SM, UI_DIM); + { /* the marks, six a line */ + char m[2][40]; + m[0][0] = m[1][0] = 0; + for (uint32_t j = 1u; j <= 64u; j++) + if (lp_has(lp_keep.mark, j)) { + if (k < 12u) { + char nm[6], *t = m[k / 6u]; + lp_name(nm, j); + if (t[0]) b_str(t, " "); + b_str(t, nm); + } + k++; + } + l[0] = 0; + b_str(l, "MARKED: "); + if (k) b_num(l, k); else b_str(l, "NONE YET"); + lp_text(184, l, &FT_SM, UI_DIM); + lp_text(202, m[0], &FT_SM, UI_TXT); + lp_text(220, m[1], &FT_SM, UI_TXT); + } +} + +static void lp_select(uint32_t now) /* give the old pin back, take the new one */ +{ + uint32_t id = lp.i ? lp.ids[lp.i - 1u] : 0u; + if (lp.id) { lp_give(lp.id); lp.id = 0; } + if (id && !lp_has(lp_keep.skip, id)) { lp.id = id; lp_take(id); lp.hi = 0; lp.t_blink = now; } + lp.dirty = 1; +} + +static void lp_init(void) +{ + lp.n = 0; + for (uint32_t p = 0; p < 4u; p++) + for (uint32_t b = 0; b < 16u; b++) + if (!(LP_NEVER[p] & (1u << b))) lp.ids[lp.n++] = (uint8_t)(p * 16u + b + 1u); + lp.i = lp.id = 0; + lp.hi = 0; + lp.acc = 0; + lp.enc = (uint8_t)(PC_IN & ENC_MASK); + lp.fs_r.stab = lp.fs_r.last = lp.fs_r.cnt = 0; + lp.fs_l.stab = lp.fs_l.last = lp.fs_l.cnt = 0; + lp_set_note("TURN THE KNOB: NEXT PIN", UI_DIM); + if (lp_keep.magic != LP_MAGIC || (op_pwr.rst & 1u)) { /* first start, or switched off and on */ + lp_keep.magic = LP_MAGIC; + lp_keep.cur = 0; + for (int k = 0; k < 4; k++) { lp_keep.skip[k] = 0; lp_keep.mark[k] = 0; } + } + if (lp_keep.cur >= 1u && lp_keep.cur <= 64u) { /* the chip reset while that pin blinked */ + char nm[6]; + if (!lp_has(lp_keep.skip, lp_keep.cur)) lp_flip(lp_keep.skip, lp_keep.cur); + lp_name(nm, lp_keep.cur); + lp_set_note(nm, RED); + b_str(lp.note, " RESET THE PEDAL: SKIPPED"); + } + lp_keep.cur = 0; + guard.pending = 0; /* poked at on purpose: a reset here is */ + guard.failed = 0; /* not a failed boot (no UBOOT, no safe */ + op_diag.stage = ST_OK; /* mode screen) */ + lcd_rect(0, 0, 240, 240, UI_BG); + lp.dirty = 1; +} + +static void lp_step(uint32_t now) /* one pass of the loop */ +{ + fm1_wdt_feed(); + px_usb_service(now); /* the web installer, as in the normal build */ + { /* the knob: one detent, one step (the main loop's decoding) */ + uint8_t s = (uint8_t)(PC_IN & ENC_MASK); + if (s != lp.enc) { + int dd = 0; + lp.acc -= QDEC[(lp.enc << 2) | s]; + lp.enc = s; + if (lp.acc >= 4) { lp.acc -= 4; dd = 1; } else if (lp.acc <= -4) { lp.acc += 4; dd = -1; } + if (dd) { + uint32_t m = lp.n + 1u; + lp.i = dd > 0 ? (lp.i + 1u) % m : (lp.i + m - 1u) % m; + lp_select(now); + lp_set_note("TURN THE KNOB: NEXT PIN", UI_DIM); + } + } + } + { /* the footswitches; a pin being driven is not read */ + uint32_t drv = lp_pa_driven(), pa = (PA_IN & 0xFFFFu) ^ base_pa; + int r = !(drv & PA_FS_UP) && dbn_press(&lp.fs_r, (pa & PA_FS_UP) != 0); + int l = !(drv & PA_FS_DN) && dbn_press(&lp.fs_l, (pa & PA_FS_DN) != 0); + int mark = (drv & PA_FS_UP) ? l : r, redraw = (drv & (PA_FS_UP | PA_FS_DN)) ? 0 : l; + if (mark && lp.i) { + uint32_t id = lp.ids[lp.i - 1u]; + if (lp_has(lp_keep.skip, id)) { lp_flip(lp_keep.skip, id); lp_select(now); } /* try it again */ + else lp_flip(lp_keep.mark, id); + lp.dirty = 1; + } + if (redraw) { lcd_init(); lcd_rect(0, 0, 240, 240, UI_BG); lp.dirty = 1; } + } + if (lp.id && now - lp.t_blink >= LP_BLINK_MS) { + lp.t_blink = now; + lp.hi = !lp.hi; + lp_level(lp.id, lp.hi); + lp_dot(); + } + if (lp.dirty) { lp.dirty = 0; lp_draw(); } +} + +static void led_probe_run(void) +{ + lp_init(); + for (;;) lp_step(ms_now()); +} diff --git a/firmware/openpocket.c b/firmware/openpocket.c index 2a1956f..c3653a9 100644 --- a/firmware/openpocket.c +++ b/firmware/openpocket.c @@ -306,6 +306,7 @@ static void fm1_fault(const fm1_crash_t *c) #define OP_VERSION "v" OP_VERSION_NUM /* shown at boot: changes with every released build */ #define PROBE 0 /* 1 = GPIO diagnostic: shows the pins that change at the bottom */ #define ADC_PROBE 0 /* 1 = ADC diagnostic: shows the channels that move at the bottom (C2:512) */ +#define LED_PROBE 0 /* 1 = light finder (issue #8, led_probe.h): one pin at a time blinks, the knob steps; no sound */ #define PA_PD (*(volatile uint32_t *)0x50014u) /* ---- Controls mapped on the real pedal (diagnostic) ---- @@ -544,6 +545,12 @@ static uint32_t ed_flags(void) { return (a_bypass ? 1u : 0u) | (a_c1 ? 2u : 0u); #include "px_editor.h" /* the web editor (docs/EDITOR-PROTOCOL.md) */ #endif #endif +#if LED_PROBE +#if AUDIO_PROBE +#error "LED_PROBE needs the normal build: USB (the web installer) is the way back" +#endif +#include "led_probe.h" /* the light finder (issue #8) */ +#endif /* P3_RST_SRC as read at boot: why the chip last reset (fm1_sys.h) */ static void rst_names(char *l, uint32_t r) @@ -641,6 +648,12 @@ static void app_main(void) keys_capture_idle(); op_stage(ST_PRESET, "PRESET..."); load_preset(cur_preset); +#if LED_PROBE + px_usb_start(); /* USB-MIDI + update entry: the web installer is the way back */ + px_store_boot(); + fm1_irq_enable_all(); /* audio_start does it in the normal build; here no audio is started */ + led_probe_run(); /* never returns */ +#endif #if AUDIO_PROBE audio_probe_run(); #else diff --git a/tests/lpsim.c b/tests/lpsim.c new file mode 100644 index 0000000..67567d7 --- /dev/null +++ b/tests/lpsim.c @@ -0,0 +1,267 @@ +/* SPDX-License-Identifier: GPL-3.0-only + * OpenPocket — the light finder (firmware/led_probe.h, issue #8) on the PC, against fake GPIO registers: + * cc -O1 -Wall -Wno-unused-function -Ifirmware -o build/lpsim tests/lpsim.c && build/lpsim build/lp + * Checks, on every pass of its loop: the pins it must never touch (screen, knob, I2S, amp enable, MASTER) keep + * their registers; at most one pin differs from the state at start, the one on screen, and it blinks; every pin + * goes back exactly as it was; the footswitches mark / unmark, the right footswitch is not read while it is the + * pin being driven; a reset while a pin blinks skips that pin next time, a power-on starts afresh; the knob wraps + * both ways; USB is serviced; every line of text fits the screen. Writes PPM screenshots to OUTDIR. */ +#include +#include +#include + +enum { RED = 0xF800, GRN = 0x07E0, BLU = 0x001F, WHT = 0xFFFF, BLK = 0x0000, CYN = 0x07FF, YEL = 0xFFE0, ORG = 0xFD20 }; +enum { UI_BG = 0x0841, UI_PANEL = 0x18E3, UI_SEL = 0xFB00, UI_ACC = 0xFD20, UI_TXT = 0xEF7D, UI_DIM = 0x8410, UI_OK = 0x4E6B, UI_OFF = 0x2124 }; + +/* ---- screen: as tests/uisim.c */ +static uint16_t fb[240 * 240]; +static uint32_t wx, wy, ww, wi; +static void lcd_win(uint32_t x, uint32_t y, uint32_t w, uint32_t h) { (void)h; wx = x; wy = y; ww = w; wi = 0; } +static void lcd_px(uint16_t c) +{ + uint32_t x = wx + wi % ww, y = wy + wi / ww; + if (x < 240u && y < 240u) fb[y * 240u + x] = c; + wi++; +} +static void lcd_end(void) { } +static void lcd_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint16_t c) +{ + if (w == 0u || h == 0u || x >= 240u || y >= 240u) return; + if (x + w > 240u) w = 240u - x; + if (y + h > 240u) h = 240u - y; + for (uint32_t j = 0; j < h; j++) for (uint32_t i = 0; i < w; i++) fb[(y + j) * 240u + x + i] = c; +} +static int lcd_inits; +static void lcd_init(void) { lcd_inits++; } +static void fm1_wdt_feed(void) { } + +/* ---- the pedal around the light finder: gpio[port p (0 A, 1 B, 2 C, 7 H)][register r / 4] */ +#define LP_REG(port, r) (gpio[(port)][(r) / 4u]) +#define LP_NOINIT +static uint32_t pa_in, pc_in; /* the buttons as pressed (pa_in) and the knob */ +static uint32_t gpio[8][16]; +static uint32_t sim_pa_in(void) /* as the pad reads: an output pin reads back its own level */ +{ + uint32_t dir = gpio[0][0x08 / 4]; + return (pa_in & dir) | (gpio[0][0x00 / 4] & ~dir); +} +#define PA_IN sim_pa_in() +#define PC_IN pc_in +#define ENC_MASK 0x3u +#define PA_FS_UP (1u << 2) +#define PA_FS_DN (1u << 3) +static uint32_t base_pa; +static const int8_t QDEC[16] = {0,-1,1,0, 1,0,0,-1, -1,0,0,1, 0,1,-1,0}; +typedef struct { uint8_t stab, last, cnt; } dbn_t; +static int dbn_press(dbn_t *d, int raw) /* as openpocket.c */ +{ + if (raw == d->last) { if (d->cnt < 5u) d->cnt++; } + else { d->cnt = 0; d->last = (uint8_t)raw; } + if (d->cnt >= 3u && raw != d->stab) { d->stab = (uint8_t)raw; return raw; } + return 0; +} +static uint32_t sim_ms; +static uint32_t ms_now(void) { return sim_ms; } +static unsigned long usb_calls; +static void px_usb_service(uint32_t now) { (void)now; usb_calls++; } +static struct { uint32_t magic, pending, failed; } guard; +enum { ST_OK = 8 }; +static struct { uint32_t stage; } op_diag; +static struct { uint8_t rst; } op_pwr; +static void b_num(char *d, uint32_t v) +{ + char t[10]; int n = 0; + while (*d) d++; + do { t[n++] = (char)('0' + v % 10u); v /= 10u; } while (v && n < 9); + while (n) *d++ = t[--n]; + *d = 0; +} +static void b_str(char *d, const char *s) { while (*d) d++; while (*s) *d++ = *s++; *d = 0; } + +static int failed, clipped; +#define LP_TEXT_HOOK(s, f, w) do { \ + if (ft_width((f), (s)) > (w) || strlen(s) > 40u) { printf("text too wide: \"%s\"\n", (s)); clipped++; } \ + } while (0) +#include "ui_text.h" +#include "led_probe.h" + +/* ---- the checks */ +static const int PORTS[4] = { 0, 1, 2, 7 }; +static uint32_t snap[8][16]; +static void ok(int c, const char *what) { if (!c) { printf("FAIL: %s\n", what); failed++; } } +static void reset_gpio(void) /* a distinctive start: inputs with some pulls, some levels */ +{ + memset(gpio, 0, sizeof gpio); + for (int k = 0; k < 4; k++) { + int p = PORTS[k]; + gpio[p][LP_OUT / 4] = 0x5A5Au ^ (uint32_t)(k * 0x1111); + gpio[p][LP_DIR / 4] = 0xFFFFu & ~(0x0101u << k); /* a few outputs too */ + gpio[p][LP_PD / 4] = 0x3030u >> k; /* every pin has a pull, so a driven one always differs */ + gpio[p][LP_PU / 4] = 0xFFFFu & ~gpio[p][LP_PD / 4]; + } + gpio[1][LP_DIR / 4] &= ~0x00E2u; /* the screen pins are outputs, as lcd_init leaves them */ + memcpy(snap, gpio, sizeof snap); +} +static int diff_pins(uint32_t *only) /* how many pins differ from the start (and the last one's id) */ +{ + int n = 0; + for (int k = 0; k < 4; k++) + for (int b = 0; b < 16; b++) { + uint32_t m = 1u << b; int p = PORTS[k], d = 0; + for (int r = 0; r < 6; r++) if ((gpio[p][r] ^ snap[p][r]) & m) d = 1; + if (d) { n++; if (only) *only = (uint32_t)(k * 16 + b + 1); } + } + return n; +} +/* written out again here, not taken from led_probe.h: PA4..PA9 (I2S, amp enable), PA10 (MASTER), PB1/5/6/7 and + * PC7 (screen), PC0/1 (knob) */ +static const uint32_t FORBID[4] = { (0x7Fu << 4), (1u << 1) | (1u << 5) | (1u << 6) | (1u << 7), 3u | (1u << 7), 0u }; +static int never_touched(void) +{ + for (int k = 0; k < 4; k++) + for (int r = 0; r < 6; r++) + if ((gpio[PORTS[k]][r] ^ snap[PORTS[k]][r]) & FORBID[k]) return 0; + return 1; +} +static void run(uint32_t ms) /* the loop for ms milliseconds, checking every pass */ +{ + for (uint32_t t = 0; t < ms; t++) { + uint32_t who = 0; int n; + sim_ms++; + lp_step(ms_now()); + n = diff_pins(&who); + if (!never_touched()) { ok(0, "a pin it must never drive was changed"); return; } + if (n > 1 || (n == 1 && who != lp.id)) { ok(0, "a pin other than the one on screen changed"); return; } + } +} +static void turn(int dir) /* one detent: 0 1 3 2 0 is one step forward (the main loop's sign) */ +{ + static const uint8_t FWD[4] = { 1, 3, 2, 0 }, BACK[4] = { 2, 3, 1, 0 }; + for (int k = 0; k < 4; k++) { pc_in = (pc_in & ~3u) | (dir > 0 ? FWD[k] : BACK[k]); run(2); } +} +static void press(uint32_t fs) { pa_in ^= fs; run(20); pa_in ^= fs; run(20); } +static void go_to(uint32_t id) /* knob forward until that pin is on screen */ +{ + for (uint32_t k = 0; k <= lp.n + 1u && !(lp.i && lp.ids[lp.i - 1u] == id); k++) turn(+1); + ok(lp.i && lp.ids[lp.i - 1u] == id, "the knob reaches the pin"); +} +static uint32_t pin(int k, int b) { return (uint32_t)(k * 16 + b + 1); } +static void shot(const char *dir, const char *name) +{ + static int n; + char path[512]; + snprintf(path, sizeof path, "%s/%02d_%s.ppm", dir, n++, name); + FILE *f = fopen(path, "wb"); + if (!f) { printf("cannot write %s\n", path); failed++; return; } + fprintf(f, "P6\n240 240\n255\n"); + for (int i = 0; i < 240 * 240; i++) { + uint16_t c = fb[i]; + uint8_t rgb[3] = { (uint8_t)(((c >> 11) & 31u) * 255u / 31u), (uint8_t)(((c >> 5) & 63u) * 255u / 63u), + (uint8_t)((c & 31u) * 255u / 31u) }; + fwrite(rgb, 1, 3, f); + } + fclose(f); +} +static void boot(uint8_t rst) /* the pedal starts (registers as the hardware leaves them) */ +{ + reset_gpio(); + op_pwr.rst = rst; + guard.pending = 1; guard.failed = 1; op_diag.stage = 3; + pc_in = 0; pa_in = 0; base_pa = 0; + lp_init(); + run(5); +} + +int main(int argc, char **argv) +{ + const char *dir = argc > 1 ? argv[1] : "build/lp"; + + /* power-on, garbage in .noinit */ + lp_keep.magic = 0x12345678u; lp_keep.cur = 7; lp_keep.skip[0] = 0xFFFFu; lp_keep.mark[3] = 0xFFFFu; + boot(1u); + ok(lp.n == 50u, "50 pins to try"); + ok(lp_keep.magic == LP_MAGIC && !lp_keep.skip[0] && !lp_keep.mark[3], "a power-on starts afresh"); + ok(guard.pending == 0u && guard.failed == 0u && op_diag.stage == ST_OK, "not counted as a failed boot"); + ok(diff_pins(0) == 0, "nothing driven at start"); + ok(usb_calls > 0u, "USB serviced"); + shot(dir, "ready"); + + /* the first pin: PA0 (FX button), an output, blinking at 2 Hz, its pulls off */ + turn(+1); + ok(lp.i == 1u && lp.id == pin(0, 0), "one detent: the first pin"); + ok(!(gpio[0][LP_DIR / 4] & 1u) && !(gpio[0][LP_PU / 4] & 1u) && !(gpio[0][LP_PD / 4] & 1u), "PA0 driven, no pulls"); + { + int flips = 0; uint32_t last = gpio[0][LP_OUT / 4] & 1u; + for (int t = 0; t < 2000; t++) { run(1); if ((gpio[0][LP_OUT / 4] & 1u) != last) { flips++; last ^= 1u; } } + ok(flips >= 7 && flips <= 9, "2 Hz: 8 changes in 2 s"); + } + shot(dir, "pa0_fx_button"); + + /* every pin forward, then back to READY: all as it was */ + for (uint32_t k = 1; k < lp.n; k++) { + turn(+1); + uint32_t id = lp.ids[lp.i - 1u], who = 0; + run(300); /* past one blink */ + ok(lp.id == id && diff_pins(&who) == 1 && who == id, "each pin in turn, alone"); + } + turn(+1); + ok(lp.i == 0u && lp.id == 0u, "past the last pin: READY"); + ok(memcmp(gpio, snap, sizeof snap) == 0, "every register as at start after the whole round"); + turn(-1); + ok(lp.i == lp.n && lp.id == pin(3, 15), "back from READY: the last pin (PH15)"); + turn(+1); + ok(lp.i == 0u && memcmp(gpio, snap, sizeof snap) == 0, "and forward again: READY, all as at start"); + + /* marks: PH6 with the right footswitch; on PA2 (the right footswitch) only the left one marks */ + go_to(pin(3, 6)); + press(PA_FS_UP); + ok(lp_has(lp_keep.mark, pin(3, 6)), "right footswitch marks PH6"); + shot(dir, "ph6_marked"); + go_to(pin(3, 9)); + press(PA_FS_UP); + go_to(pin(0, 2)); + press(PA_FS_UP); + ok(!lp_has(lp_keep.mark, pin(0, 2)), "the right footswitch is not read while PA2 blinks"); + press(PA_FS_DN); + ok(lp_has(lp_keep.mark, pin(0, 2)), "the left one marks PA2"); + shot(dir, "pa2_right_footswitch"); + press(PA_FS_DN); + ok(!lp_has(lp_keep.mark, pin(0, 2)), "and unmarks it"); + { /* PA2 and PA3 blinking read back as presses: neither may mark or redraw */ + uint32_t m3 = lp_keep.mark[0]; + int i0 = lcd_inits; + go_to(pin(0, 3)); + run(2000); + go_to(pin(0, 2)); + run(2000); + ok(lp_keep.mark[0] == m3 && lcd_inits == i0, "a blinking footswitch pin is not taken for a press"); + } + go_to(pin(1, 0)); + int inits = lcd_inits; + press(PA_FS_DN); + ok(lcd_inits == inits + 1, "left footswitch redraws the screen"); + ok(!lp_has(lp_keep.mark, pin(1, 0)), "and does not mark"); + + /* a reset while PB3 blinks: PB3 skipped next boot, the marks kept; the right footswitch tries it again */ + go_to(pin(1, 3)); + ok(lp_keep.cur == pin(1, 3), "the pin being driven is remembered"); + boot(0x04u); /* watchdog reset */ + ok(lp_has(lp_keep.skip, pin(1, 3)), "after the reset PB3 is skipped"); + ok(lp_has(lp_keep.mark, pin(3, 6)) && lp_has(lp_keep.mark, pin(3, 9)), "marks kept across the reset"); + ok(lp.note_col == RED, "the reset is reported"); + shot(dir, "after_reset"); + go_to(pin(1, 3)); + run(600); + ok(lp.id == 0u && diff_pins(0) == 0, "a skipped pin is not driven"); + shot(dir, "pb3_skipped"); + press(PA_FS_UP); + ok(!lp_has(lp_keep.skip, pin(1, 3)) && lp.id == pin(1, 3), "right footswitch: try it again"); + + /* switched off and on: afresh */ + boot(1u); + ok(!lp_has(lp_keep.mark, pin(3, 6)) && !lp_has(lp_keep.skip, pin(1, 3)), "power-on clears marks and skips"); + + ok(clipped == 0, "every line fits the screen"); + printf("light finder: %s (%d failures, %lu USB passes)\n", failed ? "FAIL" : "ok", failed, usb_calls); + return failed ? 1 : 0; +} diff --git a/tests/run_tests.sh b/tests/run_tests.sh index ff82108..459a9f9 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -4,6 +4,7 @@ # - engine: every block x model x extreme knob, signal / silence / overload, with the pedal's own libm (px_math.c): # no NaN; long runs (3 s pluck + 8 s silence): nothing runs away (only delays at FDBK 100 hold on, by design) # - preset store: save / reload / power cut at every step of a save (firmware/px_store.h on a simulated flash) +# - light finder (firmware/led_probe.h): never drives the screen / knob / audio pins, one pin at a time, all restored # - packages: tools/pxpkg.py rebuilds the official PocketX_016 app area byte for byte (needs the official file) # - installer: docs/pxota.js + docs/pxpkg.js against a simulated pedal running the real update code (ota.c, ldr_core.c) # - page: docs/index.html in headless Chromium (needs Playwright: npm i playwright && npx playwright install chromium) @@ -18,6 +19,8 @@ echo "== preset store" $CC -O1 -Iengine tests/test_store.c engine/px_dsp.c firmware/px_math.c -lm -o build/test_store && build/test_store | tail -2 echo "== screen (every page fully drawn)" mkdir -p build/ui && $CC -O1 -Wall -Wno-unused-function -Iengine -Ifirmware -o build/uisim tests/uisim.c engine/px_dsp.c firmware/px_math.c -lm && build/uisim build/ui > build/ui/log.txt && if grep -q "never drawn" build/ui/log.txt; then grep "never drawn" build/ui/log.txt; echo "screen: FAIL"; else echo "screen: $(wc -l < build/ui/log.txt) pages ok (build/ui/*.ppm)"; fi +echo "== light finder (LED_PROBE build, issue #8)" +mkdir -p build/lp && $CC -O1 -Wall -Wno-unused-function -Ifirmware -o build/lpsim tests/lpsim.c && build/lpsim build/lp echo "== simulated pedal" $CC -O1 -Wall -Wno-unused-function -o build/devsim tests/devsim.c if [ -n "$1" ]; then echo "== packages"; python3 tests/test_pxpkg.py "$1"; fi From 04877b0105f7030c01203cdea17e10fcbda2417a Mon Sep 17 00:00:00 2001 From: Roberto Antonacci <48053274+steponme82@users.noreply.github.com> Date: Fri, 6 Nov 2026 10:39:28 +0100 Subject: [PATCH 2/2] TEST BUILD ONLY, do not merge: LED_PROBE 1 The light finder switched on, so CI builds the BlackBox_936.fwsc to install for isod89/openpocket#8. The real change is the commit before this one. Co-Authored-By: Claude Opus 5.5 --- firmware/openpocket.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware/openpocket.c b/firmware/openpocket.c index c3653a9..8bda437 100644 --- a/firmware/openpocket.c +++ b/firmware/openpocket.c @@ -306,7 +306,7 @@ static void fm1_fault(const fm1_crash_t *c) #define OP_VERSION "v" OP_VERSION_NUM /* shown at boot: changes with every released build */ #define PROBE 0 /* 1 = GPIO diagnostic: shows the pins that change at the bottom */ #define ADC_PROBE 0 /* 1 = ADC diagnostic: shows the channels that move at the bottom (C2:512) */ -#define LED_PROBE 0 /* 1 = light finder (issue #8, led_probe.h): one pin at a time blinks, the knob steps; no sound */ +#define LED_PROBE 1 /* 1 = light finder (issue #8, led_probe.h): one pin at a time blinks, the knob steps; no sound */ #define PA_PD (*(volatile uint32_t *)0x50014u) /* ---- Controls mapped on the real pedal (diagnostic) ----