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/9] 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 21d94e2cd4c81159ac77df6e4ed9c04851419eb4 Mon Sep 17 00:00:00 2001 From: Roberto Antonacci <48053274+steponme82@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:50:13 +0100 Subject: [PATCH 2/9] LED map: drive the button lights' 74HC595 and map its bits isod89/openpocket#8. The light finder showed that no single pin lights anything: the lights sit behind one 74HC595. isod89 read its routine in BlackBox_020 (PocketX_016 has the same): DS = PC8, SHCP = PC5, STCP = PC6, OE not driven, PC7 kept high. A byte goes out MSB first, a few us per step. firmware/px_leds.h is that driver: leds_init (the three pins become outputs without pulls, SHCP 1, STCP 0, DS 0, then 0 is shifted out, as stock) and leds_write. Main loop only: every edge is a read-modify-write of PORTC OUT. LED_PROBE 1 now builds the LED map instead of the light finder (git history has the old one): no sound, the knob steps through ALL 8 BITS and BIT 0..7, which blink at 2 Hz between their byte and 0, so the light that changes is theirs whatever its polarity. Pressing the button whose light blinks writes it into a table on screen (FX, MOD, AMP, CAB, DLY, REV, HOME, PUSH, either footswitch); the same button again clears it. USB stays up. tests/lpsim.c now models the 595 from its data sheet and checks, on every pass, that only PC5/PC6/PC8 change, that each shift clock edge and the latch have a wait before them with the latch low while shifting, and that the latched byte is the one on screen (MSB first). Ten deliberate driver mistakes (LSB first, a missing wait, the latch left high, DS after the clock, a wrong pin, pulls left on, no blink...) all fail it. The normal build is unchanged: px_leds.h is only included with LED_PROBE 1. Not run on a pedal yet. Co-Authored-By: Claude Opus 5.5 --- firmware/led_probe.h | 283 +++++++++++++---------------------------- firmware/openpocket.c | 4 +- firmware/px_leds.h | 65 ++++++++++ tests/lpsim.c | 288 ++++++++++++++++++++++-------------------- tests/run_tests.sh | 4 +- 5 files changed, 304 insertions(+), 340 deletions(-) create mode 100644 firmware/px_leds.h diff --git a/firmware/led_probe.h b/firmware/led_probe.h index e028901..720b932 100644 --- a/firmware/led_probe.h +++ b/firmware/led_probe.h @@ -1,220 +1,103 @@ /* SPDX-License-Identifier: GPL-3.0-only - * OpenPocket — the light finder (issue #8): which pins drive the BlackBox's button lights? + * OpenPocket — the LED map (issue #8): which bit of the 74HC595 (px_leds.h) lights which button? * - * 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. + * Built only with LED_PROBE 1 (openpocket.c). That build plays no sound. The knob steps through ALL 8 BITS, then + * BIT 0 .. BIT 7; the bits on screen blink at 2 Hz (the byte alternates between them and 0, so the one light that + * changes is theirs, whether it is lit by a 1 or by a 0) and the square at the top right is lit while they are 1. + * - Pressing the button whose light blinks writes it into the table at the bottom (FX, MOD, AMP, CAB, DLY, REV, + * HOME, the knob's PUSH, either footswitch); the same button again clears it. A photo of the table is the map. + * - Only PC5, PC6 and PC8 (the 595's shift clock, latch and data) are driven. The first light finder (one pin at + * a time, git history) showed that no single pin lights anything: they are all behind the 595. * - 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. */ + * Included by openpocket.c (LED_PROBE 1) and by tests/lpsim.c on the PC. The including file provides: PA_IN, PH_IN, + * PC_IN, ENC_MASK, PA_FX / PA_HOME / PA_FS_UP / PA_FS_DN, PH_PUSH / PH_MOD / PH_AMP, base_pa, base_ph, keys_decode, + * adc_read2, ADC_CH_KEYS, KEY_CAB / KEY_DLY / KEY_REV, QDEC, dbn_t / dbn_press, ms_now, fm1_delay_us, fm1_wdt_feed, + * px_usb_service, lcd_rect, ft_box / ft_width and the fonts (ui_text.h), b_str / b_num, guard, op_diag / ST_OK and + * the UI colours. */ #pragma once #include +#include "px_leds.h" -#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 */ +#ifndef LP_TEXT_HOOK /* tests/lpsim.c checks that every line fits its box */ #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 */ +#define LP_BLINK_MS 250u /* half a period: 2 Hz */ +#define LP_NPOS 9u /* ALL 8 BITS, BIT 0 .. BIT 7 */ -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 */ +enum { LB_FX, LB_MOD, LB_AMP, LB_CAB, LB_DLY, LB_REV, LB_HOME, LB_PUSH, LB_FSR, LB_FSL, LB_N }; +static const char *const LP_BTN[LB_N] = { "FX", "MOD", "AMP", "CAB", "DLY", "REV", "HOME", "PUSH", "R FOOT", "L FOOT" }; 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; + uint32_t pos; /* 0 = all 8 bits, 1..8 = bit pos - 1 */ + uint32_t t_blink, t_keys; + int hi, dirty, key; int32_t acc; uint8_t enc; - dbn_t fs_r, fs_l; - char note[40]; - uint16_t note_col; + uint8_t map[8]; /* per bit: the button that lit, + 1 (0 = none yet) */ + dbn_t b[LB_N]; } 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 uint8_t lp_mask(void) { return lp.pos ? (uint8_t)(1u << (lp.pos - 1u)) : 0xFFu; } +static void lp_show(void) { leds_write(lp.hi ? lp_mask() : 0u); } -static void lp_set_note(const char *s, uint16_t col) +static void lp_hex2(char *d, uint32_t v) { - lp.note[0] = 0; - b_str(lp.note, s); - lp.note_col = col; + static const char H[] = "0123456789ABCDEF"; + while (*d) d++; + d[0] = H[(v >> 4) & 15u]; d[1] = H[v & 15u]; d[2] = 0; } -static void lp_text(uint32_t y, const char *s, const op_font *f, uint16_t col) +static void lp_text(uint32_t x, uint32_t y, uint32_t w, const char *s, const op_font *f, uint16_t col, int align) { - LP_TEXT_HOOK(s, f, 240u); - ft_box(0, y, 240, s, f, col, UI_BG, 1); + LP_TEXT_HOOK(s, f, w); + ft_box(x, y, w, s, f, col, UI_BG, align); } -static void lp_dot(void) { lcd_rect(222, 10, 14, 14, (lp.id && lp.hi) ? UI_ACC : UI_PANEL); } +static void lp_dot(void) { lcd_rect(222, 10, 14, 14, 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); + char l[44]; + uint32_t bit = lp.pos ? lp.pos - 1u : 0u; + int mapped = lp.pos && lp.map[bit]; + lp_text(20, 6, 200, "LED MAP", &FT_MD, UI_ACC, 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); + if (lp.pos) { b_str(l, "BIT "); b_num(l, bit); } else b_str(l, "ALL 8 BITS"); + lp_text(0, 36, 240, l, &FT_NM, UI_TXT, 1); 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); + b_str(l, "BYTE "); lp_hex2(l, lp_mask()); b_str(l, " / 00, TWICE A SECOND"); + lp_text(0, 74, 240, l, &FT_SM, UI_ACC, 1); 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++; - } + if (!lp.pos) b_str(l, "EVERY BUTTON LIGHT SHOULD BLINK"); + else if (mapped) { b_str(l, "BIT "); b_num(l, bit); b_str(l, " LIGHTS: "); b_str(l, LP_BTN[lp.map[bit] - 1u]); } + else b_str(l, "PRESS THE BUTTON THAT BLINKS"); + lp_text(0, 96, 240, l, &FT_SM, mapped ? UI_OK : UI_TXT, 1); + lp_text(0, 114, 240, !lp.pos ? "KNOB: ONE BIT AT A TIME" : mapped ? "SAME BUTTON AGAIN: CLEAR" : "KNOB: NEXT BIT", + &FT_SM, UI_DIM, 1); + for (uint32_t i = 0; i < 8u; i++) { /* the map: bits 0-3 left, 4-7 right */ + uint16_t col = (lp.pos && i == bit) ? UI_ACC : lp.map[i] ? UI_TXT : UI_DIM; 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); + b_str(l, "BIT "); b_num(l, i); b_str(l, ": "); b_str(l, lp.map[i] ? LP_BTN[lp.map[i] - 1u] : "--"); + lp_text(i < 4u ? 24u : 136u, 140u + (i & 3u) * 18u, 100u, l, &FT_SM, col, 0); } -} - -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; + lp_text(0, 218, 240, "SQUARE LIT = BITS AT 1", &FT_SM, UI_DIM, 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.pos = 0; + lp.hi = 1; lp.acc = 0; + lp.key = KEY_NONE; 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) */ + for (int i = 0; i < 8; i++) lp.map[i] = 0; + for (int i = 0; i < LB_N; i++) lp.b[i].stab = lp.b[i].last = lp.b[i].cnt = 0; + guard.pending = 0; /* a reset here is not a failed boot */ + guard.failed = 0; /* (no UBOOT, no safe mode screen) */ + op_diag.stage = ST_OK; + leds_init(); + lp.t_blink = lp.t_keys = ms_now(); + lp_show(); lcd_rect(0, 0, 240, 240, UI_BG); lp.dirty = 1; } @@ -231,30 +114,36 @@ static void lp_step(uint32_t now) /* one pass 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); + lp.pos = dd > 0 ? (lp.pos + 1u) % LP_NPOS : (lp.pos + LP_NPOS - 1u) % LP_NPOS; + lp.hi = 1; + lp.t_blink = now; + lp_show(); + lp.dirty = 1; } } } - { /* 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); + { /* the buttons (pressed = differs from the idle state); CAB / DLY / REV are the analog keypad, every 10 ms */ + uint32_t pa = (PA_IN & 0xFFFFu) ^ base_pa, ph = (PH_IN & 0xFFFFu) ^ base_ph; + int hit = -1, keys = now - lp.t_keys >= 10u; + int raw[LB_N] = { + (pa & PA_FX) != 0, (ph & PH_MOD) != 0, (ph & PH_AMP) != 0, 0, 0, 0, + (pa & PA_HOME) != 0, (ph & PH_PUSH) != 0, (pa & PA_FS_UP) != 0, (pa & PA_FS_DN) != 0 }; + if (keys) { lp.t_keys = now; lp.key = keys_decode(adc_read2(ADC_CH_KEYS)); } + raw[LB_CAB] = lp.key == KEY_CAB; raw[LB_DLY] = lp.key == KEY_DLY; raw[LB_REV] = lp.key == KEY_REV; + for (int i = 0; i < LB_N; i++) { + if ((i == LB_CAB || i == LB_DLY || i == LB_REV) && !keys) continue; + if (dbn_press(&lp.b[i], raw[i]) && hit < 0) hit = i; + } + if (hit >= 0 && lp.pos) { + uint8_t *m = &lp.map[lp.pos - 1u]; + *m = (*m == (uint8_t)(hit + 1)) ? 0u : (uint8_t)(hit + 1); 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) { + if (now - lp.t_blink >= LP_BLINK_MS) { lp.t_blink = now; lp.hi = !lp.hi; - lp_level(lp.id, lp.hi); + lp_show(); lp_dot(); } if (lp.dirty) { lp.dirty = 0; lp_draw(); } diff --git a/firmware/openpocket.c b/firmware/openpocket.c index c3653a9..beb7ee3 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 0 /* 1 = LED map (issue #8, led_probe.h): the knob picks a bit of the lights' 595, it blinks; no sound */ #define PA_PD (*(volatile uint32_t *)0x50014u) /* ---- Controls mapped on the real pedal (diagnostic) ---- @@ -549,7 +549,7 @@ static uint32_t ed_flags(void) { return (a_bypass ? 1u : 0u) | (a_c1 ? 2u : 0u); #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) */ +#include "led_probe.h" /* the LED map (issue #8) */ #endif /* P3_RST_SRC as read at boot: why the chip last reset (fm1_sys.h) */ diff --git a/firmware/px_leds.h b/firmware/px_leds.h new file mode 100644 index 0000000..39a1f23 --- /dev/null +++ b/firmware/px_leds.h @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: GPL-3.0-only + * OpenPocket — the button lights (issue #8): one 74HC595 shift register, 8 outputs, on port C. + * + * Pins and timing as the stock firmware drives them (BlackBox_020; PocketX_016 has the same routine), read by + * isod89 in the stock code: + * DS (data) = PC8, SHCP (shift clock) = PC5, STCP (latch) = PC6. OE is not driven by any pin. PC7 (a screen + * line) is high at the same time: OpenPocket already keeps it so, and nothing here touches it. + * Stock's init: PC5, PC6, PC8 outputs with SHCP = 1, STCP = 0, DS = 0, then one byte shifted out. + * A byte, MSB first: STCP low; for each bit SHCP low, DS = the bit, wait, SHCP high, wait; then STCP high, whose + * rising edge puts the byte on the outputs. Stock waits about 4 to 8 us at each step: LEDS_WAIT is 5 us. + * Stock's byte: bit 0 a blink signal (toggled by a timer), bits 1..6 six light states, bit 7 always 0. It rewrites + * the byte only when it changes, and writes 0 at power-off. Which bit is which light: the LED map finds out + * (led_probe.h, LED_PROBE 1). + * Not verified on hardware yet. + * + * Every edge is a read-modify-write of PORTC OUT, so these are called from the main loop only (no interrupt writes + * port C). tests/lpsim.c checks the bit order and the waits against a model of the 595. */ +#pragma once +#include + +#ifndef LEDS_PC /* PORTC register r: OUT +0x00, DIR +0x08 (1 input, 0 output), PU +0x10, PD +0x14 */ +#define LEDS_PC(r) (*(volatile uint32_t *)(0x50080u + (uint32_t)(r))) +#endif +#ifndef LEDS_WAIT +#define LEDS_WAIT() fm1_delay_us(5u) +#endif +#ifndef LEDS_EDGE /* tests/lpsim.c: the 595 model sees each edge */ +#define LEDS_EDGE() ((void)0) +#endif +#define LEDS_DS (1u << 8) /* PC8: data */ +#define LEDS_SH (1u << 5) /* PC5: shift clock, DS is taken on its rising edge */ +#define LEDS_ST (1u << 6) /* PC6: latch, the outputs change on its rising edge */ +#define LEDS_PINS (LEDS_DS | LEDS_SH | LEDS_ST) + +static uint8_t leds_byte; /* what the outputs show now */ + +static void leds_pin(uint32_t m, uint32_t hi) +{ + if (hi) LEDS_PC(0x00) |= m; + else LEDS_PC(0x00) &= ~m; + LEDS_EDGE(); +} +static void leds_write(uint8_t v) +{ + leds_pin(LEDS_ST, 0u); + for (int i = 7; i >= 0; i--) { + leds_pin(LEDS_SH, 0u); + leds_pin(LEDS_DS, ((uint32_t)v >> i) & 1u); + LEDS_WAIT(); + leds_pin(LEDS_SH, 1u); + LEDS_WAIT(); + } + leds_pin(LEDS_ST, 1u); + leds_byte = v; +} +static void leds_init(void) +{ + LEDS_PC(0x10) &= ~LEDS_PINS; /* no pull-ups (the boot leaves them on) */ + LEDS_PC(0x14) &= ~LEDS_PINS; /* no pull-downs */ + LEDS_PC(0x00) = (LEDS_PC(0x00) & ~LEDS_PINS) | LEDS_SH; /* SHCP 1, STCP 0, DS 0, as stock */ + LEDS_PC(0x08) &= ~LEDS_PINS; /* outputs */ + LEDS_EDGE(); + LEDS_WAIT(); /* SHCP high for a while, even if it was low */ + leds_write(0u); +} diff --git a/tests/lpsim.c b/tests/lpsim.c index 67567d7..0e55129 100644 --- a/tests/lpsim.c +++ b/tests/lpsim.c @@ -1,11 +1,15 @@ /* SPDX-License-Identifier: GPL-3.0-only - * OpenPocket — the light finder (firmware/led_probe.h, issue #8) on the PC, against fake GPIO registers: + * OpenPocket — the LED map (firmware/led_probe.h and the 595 driver px_leds.h, issue #8) on the PC, against fake + * GPIO registers and a model of the 74HC595: * 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. */ + * Checks: only PC5, PC6 and PC8 are ever changed (every other register bit of PA, PB, PC and PH as at start, PC7 + * included); they end as outputs without pulls; every shift clock edge and the latch's rise have a wait before them + * (the start counts as an edge, so SHCP is high for a wait even when it began low), DS is an output + * and the latch is low while shifting; the byte reaches the 595's outputs MSB first and is always the one the screen + * names, blinking at 2 Hz between the bits on screen and 0, lit right after a turn; the knob wraps both ways over the + * 9 positions; each button writes itself into the table, again clears it, another replaces it, nothing is written + * on ALL 8 BITS; the analog keys (CAB, DLY, REV) are read every 10 ms; USB is serviced; the boot is not counted as + * failed; every line of text fits its box. Writes PPM screenshots to OUTDIR. */ #include #include #include @@ -31,26 +35,35 @@ static void lcd_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint16_t c) 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 */ +/* ---- the pedal around the LED map: gpio[port p (0 A, 1 B, 2 C, 7 H)][register r / 4] */ +enum { R_OUT = 0, R_DIR = 2, R_PU = 4, R_PD = 5 }; /* register index: OUT +0, DIR +8, PU +0x10, PD +0x14 */ static uint32_t gpio[8][16]; +static uint32_t pa_in, ph_in, pc_in; /* the buttons as pressed, the knob */ 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); + uint32_t dir = gpio[0][R_DIR]; + return (pa_in & dir) | (gpio[0][R_OUT] & ~dir); } #define PA_IN sim_pa_in() +#define PH_IN ph_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; +#define PA_FX (1u << 0) /* as openpocket.c */ +#define PA_HOME (1u << 1) +#define PA_FS_UP (1u << 2) +#define PA_FS_DN (1u << 3) +#define PH_PUSH ((1u << 4) | (1u << 6)) +#define PH_MOD ((1u << 7) | (1u << 8)) +#define PH_AMP (1u << 9) +static uint32_t base_pa, base_ph; +enum { KEY_NONE = 0, KEY_CAB, KEY_DLY, KEY_REV }; +#define ADC_CH_KEYS 9u +static int sim_key; /* the analog key held down */ +static unsigned long adc_calls; +static int32_t adc_read2(uint32_t ch) { if (ch == ADC_CH_KEYS) adc_calls++; return sim_key; } +static int keys_decode(int32_t v) { return (int)v; } 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 */ @@ -67,7 +80,6 @@ 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; @@ -78,6 +90,35 @@ static void b_num(char *d, uint32_t v) } static void b_str(char *d, const char *s) { while (*d) d++; while (*s) *d++ = *s++; *d = 0; } +/* ---- the 74HC595, from its data sheet: DS is shifted in on SHCP's rising edge (the first bit in ends at Q7), the + * outputs take the shift register on STCP's rising edge. A pin's level: its OUT bit when an output, else its pull. */ +#define M_DS (1u << 8) +#define M_SH (1u << 5) +#define M_ST (1u << 6) +static uint32_t lv_prev; +static uint8_t sr, q; +static unsigned long waits, w_sh, latches; +static int e_wait, e_latch, e_dir; +static uint32_t pc_level(void) { uint32_t d = gpio[2][R_DIR]; return (gpio[2][R_OUT] & ~d) | (gpio[2][R_PU] & d); } +static void sim_wait(void) { waits++; } +static void sim_edge(void) +{ + uint32_t lv = pc_level(), rise = lv & ~lv_prev, fall = ~lv & lv_prev; + if (rise & M_SH) { + if (waits == w_sh) e_wait++; /* no wait while SHCP was low (or since the start) */ + if (lv & M_ST) e_latch++; /* shifting with the latch high: the outputs would ripple */ + if (gpio[2][R_DIR] & M_DS) e_dir++; /* DS not driven */ + sr = (uint8_t)((sr << 1) | ((lv & M_DS) ? 1u : 0u)); + w_sh = waits; + } + if (fall & M_SH) { if (waits == w_sh) e_wait++; w_sh = waits; } /* no wait while SHCP was high */ + if (rise & M_ST) { if (waits == w_sh) e_wait++; q = sr; latches++; } /* a wait after the last shift too */ + lv_prev = lv; +} +#define LEDS_PC(r) (gpio[2][(r) / 4u]) +#define LEDS_WAIT() sim_wait() +#define LEDS_EDGE() sim_edge() + 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++; } \ @@ -88,64 +129,64 @@ static int failed, clipped; /* ---- the checks */ static const int PORTS[4] = { 0, 1, 2, 7 }; static uint32_t snap[8][16]; +static int want, want2 = -1; /* the position the test expects (0 all, 1..8 bit want - 1); mid-turn either */ 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 */ +static void reset_gpio(void) /* a distinctive start; PC5, PC6, PC8 inputs with pull-ups, as booted */ { 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[p][R_OUT] = 0x5A5Au ^ (uint32_t)(k * 0x1111); + gpio[p][R_DIR] = 0xFFFFu & ~(0x0101u << k); + gpio[p][R_PD] = 0x3030u >> k; + gpio[p][R_PU] = 0xFFFFu & ~gpio[p][R_PD]; } - gpio[1][LP_DIR / 4] &= ~0x00E2u; /* the screen pins are outputs, as lcd_init leaves them */ + gpio[1][R_DIR] &= ~0x00E2u; /* the screen pins PB1/5/6/7 and PC7: outputs, PC7 high */ + gpio[2][R_DIR] &= ~(1u << 7); gpio[2][R_OUT] |= 1u << 7; + gpio[2][R_DIR] |= M_DS | M_SH | M_ST; gpio[2][R_PU] |= M_DS | M_SH | M_ST; gpio[2][R_PD] &= ~(M_DS | M_SH | M_ST); + gpio[0][R_DIR] |= 0x000Fu; /* the buttons are inputs, as input_init leaves them */ + gpio[7][R_DIR] |= PH_PUSH | PH_MOD | PH_AMP; memcpy(snap, gpio, sizeof snap); + pa_in = ph_in = pc_in = 0; sim_key = KEY_NONE; + base_pa = sim_pa_in() & 0xFFFFu; base_ph = ph_in; /* as input_baseline */ + lv_prev = pc_level(); + sr = 0x5Au; q = 0xA5u; /* a 595 holds garbage at power-on */ } -static int diff_pins(uint32_t *only) /* how many pins differ from the start (and the last one's id) */ +static int only_595_pins(void) /* every register bit as at start, but PC5/6/8's OUT, DIR, PU, PD */ { - 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); } + for (int r = 0; r < 16; r++) { + uint32_t may = (PORTS[k] == 2 && (r == R_OUT || r == R_DIR || r == R_PU || r == R_PD)) ? (M_DS | M_SH | M_ST) : 0u; + if ((gpio[PORTS[k]][r] ^ snap[PORTS[k]][r]) & ~may) return 0; } - 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 uint8_t mask_of(int pos) { return pos ? (uint8_t)(1u << (pos - 1)) : 0xFFu; } 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; } + if (!only_595_pins()) { ok(0, "a pin other than PC5 / PC6 / PC8 was changed"); return; } + if (q != 0u && q != mask_of(want) && !(want2 >= 0 && q == mask_of(want2))) { + ok(0, "the 595 shows a byte other than the bits on screen"); 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 }; + want2 = dir > 0 ? (want + 1) % 9 : (want + 8) % 9; for (int k = 0; k < 4; k++) { pc_in = (pc_in & ~3u) | (dir > 0 ? FWD[k] : BACK[k]); run(2); } + want = want2; want2 = -1; + ok(lp.pos == (uint32_t)want, "the knob moves one position per detent"); + ok(q == mask_of(want), "lit right after a turn"); } -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 go(int pos) { for (int k = 0; k < 9 && want != pos; k++) turn(+1); } +static void press_pa(uint32_t m) { pa_in ^= m; run(20); pa_in ^= m; run(20); } +static void press_ph(uint32_t m) { ph_in ^= m; run(20); ph_in ^= m; run(20); } +static void press_key(int k) { sim_key = k; run(60); sim_key = KEY_NONE; run(60); } +static int map(int bit) { return lp.map[bit] ? lp.map[bit] - 1 : -1; } static void shot(const char *dir, const char *name) { static int n; @@ -162,106 +203,75 @@ static void shot(const char *dir, const char *name) } 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"); + reset_gpio(); + guard.pending = 1; guard.failed = 1; op_diag.stage = 3; + lp_init(); 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(!(gpio[2][R_DIR] & (M_DS | M_SH | M_ST)), "PC5, PC6, PC8 are outputs"); + ok(!(gpio[2][R_PU] & (M_DS | M_SH | M_ST)) && !(gpio[2][R_PD] & (M_DS | M_SH | M_ST)), "without pulls"); + ok(latches == 2u && q == 0xFFu, "0 at start (as stock), then ALL 8 BITS lit"); + ok(only_595_pins(), "nothing else touched by the start"); + run(5); ok(usb_calls > 0u, "USB serviced"); - shot(dir, "ready"); + shot(dir, "all_bits"); - /* 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; } } + { /* 2 Hz: 8 changes in 2 s, always the bits on screen or 0 */ + int flips = 0; uint8_t last = q; + for (int t = 0; t < 2000; t++) { run(1); if (q != last) { flips++; last = q; } } 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++) { + { /* each bit alone, MSB first (an LSB-first driver would light bit 7 - n) */ + for (int b = 0; b < 8; b++) { turn(+1); run(300); } + ok(want == 8, "at BIT 7"); + turn(+1); + ok(lp.pos == 0u, "past BIT 7: ALL 8 BITS"); + turn(-1); + ok(lp.pos == 8u && q == 0x80u, "back from ALL: BIT 7"); 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"); + { /* the table: every button, again clears, another replaces, none on ALL */ + unsigned long a0 = adc_calls; uint32_t t0 = sim_ms; + press_key(KEY_DLY); + ok(memcmp(lp.map, "\0\0\0\0\0\0\0\0", 8) == 0, "nothing written on ALL 8 BITS"); + unsigned long n = adc_calls - a0, t = (sim_ms - t0) / 10u; + ok(n + 1u >= t && n <= t + 1u, "the keys every 10 ms"); + go(1); + shot(dir, "bit0"); + press_pa(PA_FX); + ok(map(0) == LB_FX, "FX on BIT 0"); + shot(dir, "bit0_fx"); + press_pa(PA_FX); + ok(map(0) < 0, "FX again clears it"); + press_pa(PA_FX); + go(2); press_pa(PA_FS_UP); + ok(map(1) == LB_FSR, "right footswitch on BIT 1"); + go(3); press_pa(PA_FS_DN); + ok(map(2) == LB_FSL, "left footswitch on BIT 2"); + go(4); press_ph(PH_AMP); + ok(map(3) == LB_AMP, "AMP on BIT 3"); + go(5); press_key(KEY_CAB); + ok(map(4) == LB_CAB, "CAB (analog) on BIT 4"); + go(6); press_ph(1u << 6); + ok(map(5) == LB_PUSH, "PUSH (PH6 alone) on BIT 5"); + go(7); press_key(KEY_REV); press_pa(PA_HOME); + ok(map(6) == LB_HOME, "HOME replaces REV on BIT 6"); + go(8); press_ph(1u << 8); + ok(map(7) == LB_MOD, "MOD (PH8 alone) on BIT 7"); + ok(map(0) == LB_FX && map(3) == LB_AMP, "the table keeps the others"); + shot(dir, "map"); + go(0); + press_pa(PA_FX); + ok(map(0) == LB_FX, "a press on ALL changes nothing"); } - 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); + ok(e_wait == 0, "a wait on each side of every shift clock edge"); + ok(e_latch == 0, "the latch is low while shifting"); + ok(e_dir == 0, "DS is driven while shifting"); + ok(clipped == 0, "every line fits its box"); + printf("LED map: %s (%d failures, %lu latches, %lu USB passes)\n", failed ? "FAIL" : "ok", failed, latches, usb_calls); return failed ? 1 : 0; } diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 459a9f9..75e5aaf 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -4,7 +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 +# - LED map (firmware/led_probe.h, px_leds.h): only the 595 pins move, MSB first with the waits, the byte on screen # - 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) @@ -19,7 +19,7 @@ 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)" +echo "== LED map and the 595 driver (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 From 713c053518d5c8eb8ee452fb15dd2dfe05842cab Mon Sep 17 00:00:00 2001 From: Roberto Antonacci <48053274+steponme82@users.noreply.github.com> Date: Fri, 6 Nov 2026 16:50:13 +0100 Subject: [PATCH 3/9] TEST BUILD ONLY, do not merge: LED_PROBE 1 (the LED map) 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 beb7ee3..a3b35b1 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 = LED map (issue #8, led_probe.h): the knob picks a bit of the lights' 595, it blinks; no sound */ +#define LED_PROBE 1 /* 1 = LED map (issue #8, led_probe.h): the knob picks a bit of the lights' 595, it blinks; no sound */ #define PA_PD (*(volatile uint32_t *)0x50014u) /* ---- Controls mapped on the real pedal (diagnostic) ---- From 2da9b0714e3cb0593bedd7244464c98f3b894248 Mon Sep 17 00:00:00 2001 From: Roberto Antonacci <48053274+steponme82@users.noreply.github.com> Date: Fri, 6 Nov 2026 18:25:56 +0100 Subject: [PATCH 4/9] LED hunt: why the 595 lit nothing (pin check, roles, LED lines) isod89/openpocket#8. The LED map drove the 74HC595 as isod89 read it in the stock firmware (data PC8, clock PC5, latch PC6) and nothing lit on a BlackBox, not even all 8 bits blinking. LED_PROBE 1 now builds the LED hunt instead (git history has the map), which tests what that reading could be missing. The knob steps through: - PIN CHECK: PC5, PC6 and PC8 driven to 1 and 0 and read back, then inputs with a pull-up and a pull-down: DRIVES OK, FREE for a pin wired only to a 595 input, STUCK / HELD for one something else holds. - ROLES 1 to 6: the six ways to give data, clock and latch to the three pins (1 is isod89's), all 8 bits blinking. - LED LINE 1 to 47: one more pin at a time blinks while the 595 blinks FF / 00 at half the rate, so all four mixes come once a second. On the FM-1 the 595 only selects a column and an LED needs its LED line driven too; a missing enable would show the same way. The pins never driven are the light finder's list (screen, knob, I2S, amp enable, MASTER, analog keys, flash) plus PC5/6/8. CAB, DLY or REV marks a test on screen. USB stays up. px_leds.h: leds_shift / leds_setup take the pins as parameters (the hunt tries other roles); leds_init / leds_write are the driver as before. tests/lpsim.c models pads with pulls and outside holds, and the 595 as isod89 read it. It checks the driver alone (each byte MSB first, no pulls left), the pin check on free and on held pins, the timing rules with isod89's roles, all four mixes on every line, that only PC5/6/8 and the line on screen ever change and are given back, and the marks. 13 of 14 deliberate mistakes fail it; the 14th (a pull-down left by the pin check) is cleared by the setup that follows. The normal build is unchanged. Not run on a pedal yet. Co-Authored-By: Claude Opus 5.5 --- firmware/led_probe.h | 338 +++++++++++++++++++++++++++++++----------- firmware/openpocket.c | 4 +- firmware/px_leds.h | 41 +++-- tests/lpsim.c | 298 +++++++++++++++++++++---------------- tests/run_tests.sh | 4 +- 5 files changed, 452 insertions(+), 233 deletions(-) diff --git a/firmware/led_probe.h b/firmware/led_probe.h index 720b932..52377df 100644 --- a/firmware/led_probe.h +++ b/firmware/led_probe.h @@ -1,105 +1,280 @@ /* SPDX-License-Identifier: GPL-3.0-only - * OpenPocket — the LED map (issue #8): which bit of the 74HC595 (px_leds.h) lights which button? + * OpenPocket — the LED hunt (issue #8): why do the BlackBox's button lights stay dark? * - * Built only with LED_PROBE 1 (openpocket.c). That build plays no sound. The knob steps through ALL 8 BITS, then - * BIT 0 .. BIT 7; the bits on screen blink at 2 Hz (the byte alternates between them and 0, so the one light that - * changes is theirs, whether it is lit by a 1 or by a 0) and the square at the top right is lit while they are 1. - * - Pressing the button whose light blinks writes it into the table at the bottom (FX, MOD, AMP, CAB, DLY, REV, - * HOME, the knob's PUSH, either footswitch); the same button again clears it. A photo of the table is the map. - * - Only PC5, PC6 and PC8 (the 595's shift clock, latch and data) are driven. The first light finder (one pin at - * a time, git history) showed that no single pin lights anything: they are all behind the 595. - * - 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. + * Built only with LED_PROBE 1 (openpocket.c). That build plays no sound. The LED map (git history) drove the + * 74HC595 as isod89 read it in the stock firmware (data PC8, clock PC5, latch PC6: px_leds.h) and nothing lit, not + * even all 8 bits blinking. This build tests the ways that reading could be incomplete. The knob steps through: + * - PIN CHECK: PC5, PC6 and PC8 driven to 1 and 0 and read back, then left as inputs with a pull-up and with a + * pull-down. A pin wired only to a 595 input reads DRIVES OK, FREE; one held by something else reads HELD. + * - ROLES 1 to 6: the six ways to give data, clock and latch to PC5, PC6 and PC8 (1 is isod89's), with all 8 + * bits blinking (FF and 00, 2 Hz). + * - LED LINE 1 to 47: one more pin at a time blinks (1 and 0, 2 Hz) while the 595 (isod89's roles) blinks FF and + * 00 at 1 Hz, so every mix of the two comes once a second. On the FM-1 the 595 only selects a column, and an LED + * lights when its LED line is driven too (bsp/boards/fm1/fm1_input.h); an enable (OE, a supply) shows the same + * way. Never driven: PA4..PA10 (I2S, amp enable, MASTER, the analog keys), PB1/5/6/7 and PC7 (the screen), + * PC0/1 (the knob), port D (the flash). The first light finder (git history) drove these same pins alone, + * without trouble. + * CAB, DLY or REV (the analog keys, never driven here) marks the test on screen, again unmarks it; the list stays at + * the bottom for a photo. USB stays up, as in the normal build, so the web installer is the way back. * - * Included by openpocket.c (LED_PROBE 1) and by tests/lpsim.c on the PC. The including file provides: PA_IN, PH_IN, - * PC_IN, ENC_MASK, PA_FX / PA_HOME / PA_FS_UP / PA_FS_DN, PH_PUSH / PH_MOD / PH_AMP, base_pa, base_ph, keys_decode, - * adc_read2, ADC_CH_KEYS, KEY_CAB / KEY_DLY / KEY_REV, QDEC, dbn_t / dbn_press, ms_now, fm1_delay_us, fm1_wdt_feed, - * px_usb_service, lcd_rect, ft_box / ft_width and the fonts (ui_text.h), b_str / b_num, guard, op_diag / ST_OK and - * the UI colours. */ + * Included by openpocket.c (LED_PROBE 1) and by tests/lpsim.c on the PC. The including file provides: PC_IN, + * ENC_MASK, keys_decode, adc_read2, ADC_CH_KEYS, KEY_NONE, QDEC, dbn_t / dbn_press, ms_now, fm1_delay_us, + * fm1_wdt_feed, px_usb_service, lcd_rect, ft_box / ft_width and the fonts (ui_text.h), b_str / b_num, guard, + * op_diag / ST_OK and the UI colours. */ #pragma once #include #include "px_leds.h" +#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_SETTLE /* before reading a pin back: an internal pull charges the pad in about a microsecond */ +#define LP_SETTLE() fm1_delay_us(50u) +#endif #ifndef LP_TEXT_HOOK /* tests/lpsim.c checks that every line fits its box */ #define LP_TEXT_HOOK(s, f, w) ((void)0) #endif -#define LP_BLINK_MS 250u /* half a period: 2 Hz */ -#define LP_NPOS 9u /* ALL 8 BITS, BIT 0 .. BIT 7 */ +enum { LP_OUT = 0x00, LP_DIR = 0x08, LP_DIE = 0x0C, LP_PU = 0x10, LP_PD = 0x14 }; /* DIR: 1 input, 0 output */ -enum { LB_FX, LB_MOD, LB_AMP, LB_CAB, LB_DLY, LB_REV, LB_HOME, LB_PUSH, LB_FSR, LB_FSL, LB_N }; -static const char *const LP_BTN[LB_N] = { "FX", "MOD", "AMP", "CAB", "DLY", "REV", "HOME", "PUSH", "R FOOT", "L FOOT" }; +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, 0x01E3u, 0x0000u }; /* PA4..10 | PB1,5,6,7 | PC0,1,5..8 */ +static const uint8_t LP_ROLE[6][3] = { /* the PC bit of data, clock, latch */ + { 8u, 5u, 6u }, { 8u, 6u, 5u }, { 5u, 8u, 6u }, { 5u, 6u, 8u }, { 6u, 5u, 8u }, { 6u, 8u, 5u } }; +static const uint8_t LP_CHK[3] = { 5u, 6u, 8u }; /* the pins the pin check reads back */ +#define LP_STEP_MS 250u +#define LP_LINES 7u /* knob: 0 pin check, 1..6 roles, 7.. lines */ +enum { CK_OK, CK_HI, CK_LO, CK_ODD }; /* a pin check result */ static struct { - uint32_t pos; /* 0 = all 8 bits, 1..8 = bit pos - 1 */ - uint32_t t_blink, t_keys; - int hi, dirty, key; + uint8_t ids[64]; /* the LED line candidates: id = port index * 16 + bit + 1 (1..64) */ + uint32_t n, pos, id; /* how many; the knob's position; the line driven now (0 = none) */ + uint32_t sv_out, sv_dir, sv_pu, sv_pd; /* that line's bits as they were */ + uint32_t t, phase, t_keys; + int role; /* the roles the 595 is driven with (LP_ROLE), -1 during the pin check */ + int dirty, key, kind; /* kind: the page on screen (0 pin check, 1 roles, 2 line; -1 none) */ int32_t acc; uint8_t enc; - uint8_t map[8]; /* per bit: the button that lit, + 1 (0 = none yet) */ - dbn_t b[LB_N]; + uint8_t chk[3][2]; /* per pin: driven 1 / 0 read back, pull-up / pull-down read back */ + uint32_t mark_role, mark[4]; + dbn_t k; } lp; -static uint8_t lp_mask(void) { return lp.pos ? (uint8_t)(1u << (lp.pos - 1u)) : 0xFFu; } -static void lp_show(void) { leds_write(lp.hi ? lp_mask() : 0u); } +static uint32_t lp_npos(void) { return LP_LINES + lp.n; } +static void lp_595(uint8_t v) +{ + const uint8_t *r = LP_ROLE[lp.role]; + leds_shift(v, 1u << r[0], 1u << r[1], 1u << r[2]); +} +static void lp_roles(int r) /* the 595 driven with roles r, 0 shifted out */ +{ + lp.role = r; + leds_setup(1u << LP_ROLE[r][0], 1u << LP_ROLE[r][1], 1u << LP_ROLE[r][2]); + lp_595(0u); +} -static void lp_hex2(char *d, uint32_t v) +static void lp_name(char *d, uint32_t id) /* 55 -> "PH6" */ { - static const char H[] = "0123456789ABCDEF"; + uint32_t p = (id - 1u) >> 4, b = (id - 1u) & 15u; while (*d) d++; - d[0] = H[(v >> 4) & 15u]; d[1] = H[v & 15u]; d[2] = 0; + *d++ = 'P'; + *d++ = LP_PN[p]; + if (b >= 10u) { *d++ = '1'; *d++ = (char)('0' + b - 10u); } + else *d++ = (char)('0' + b); + *d = 0; +} +static const char *lp_role_of(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 uint32_t *set, uint32_t id) { return (int)((set[(id - 1u) >> 4] >> ((id - 1u) & 15u)) & 1u); } + +static void lp_take(uint32_t id) /* the line 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_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_text(uint32_t x, uint32_t y, uint32_t w, const char *s, const op_font *f, uint16_t col, int align) +static void lp_level(uint32_t id, uint32_t hi) { - LP_TEXT_HOOK(s, f, w); - ft_box(x, y, w, s, f, col, UI_BG, align); + 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_dot(void) { lcd_rect(222, 10, 14, 14, lp.hi ? UI_ACC : UI_PANEL); } +static void lp_give(uint32_t id) /* the line 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; +} + +static uint32_t lp_read(uint32_t m) { LEDS_EDGE(); LP_SETTLE(); return (PC_IN & m) ? 1u : 0u; } +static uint8_t lp_class(uint32_t a, uint32_t b) { return (uint8_t)(a ? (b ? CK_HI : CK_OK) : (b ? CK_ODD : CK_LO)); } +static void lp_check(void) /* PC5, PC6, PC8: driven, then pulled */ +{ + lp.role = -1; + for (int i = 0; i < 3; i++) { + uint32_t m = 1u << LP_CHK[i], die = LP_REG(2u, LP_DIE) & m, h, l; + LP_REG(2u, LP_DIE) |= m; /* read the pad */ + LP_REG(2u, LP_PU) &= ~m; LP_REG(2u, LP_PD) &= ~m; + LP_REG(2u, LP_OUT) |= m; LP_REG(2u, LP_DIR) &= ~m; h = lp_read(m); + LP_REG(2u, LP_OUT) &= ~m; l = lp_read(m); + lp.chk[i][0] = lp_class(h, l); + LP_REG(2u, LP_DIR) |= m; LP_REG(2u, LP_PU) |= m; h = lp_read(m); + LP_REG(2u, LP_PU) &= ~m; LP_REG(2u, LP_PD) |= m; l = lp_read(m); + lp.chk[i][1] = lp_class(h, l); + LP_REG(2u, LP_PD) &= ~m; + LP_REG(2u, LP_DIE) = (LP_REG(2u, LP_DIE) & ~m) | die; + } + lp_roles(0); +} + +static void lp_text(uint32_t y, const char *s, const op_font *f, uint16_t col) +{ + LP_TEXT_HOOK(s, f, 232u); /* 4 px clear of each edge */ + ft_box(0, y, 240, s, f, col, UI_BG, 1); +} +static int lp_lit(void) /* the square: what blinks is at 1 */ +{ + if (lp.pos == 0u) return 0; + if (lp.pos < LP_LINES) return !(lp.phase & 1u); /* the 595 at FF */ + return (int)(lp.phase & 1u); /* the line at 1 */ +} +static void lp_dot(void) { lcd_rect(222, 10, 14, 14, lp_lit() ? UI_ACC : UI_PANEL); } static void lp_draw(void) { - char l[44]; - uint32_t bit = lp.pos ? lp.pos - 1u : 0u; - int mapped = lp.pos && lp.map[bit]; - lp_text(20, 6, 200, "LED MAP", &FT_MD, UI_ACC, 1); - lp_dot(); - l[0] = 0; - if (lp.pos) { b_str(l, "BIT "); b_num(l, bit); } else b_str(l, "ALL 8 BITS"); - lp_text(0, 36, 240, l, &FT_NM, UI_TXT, 1); + static const char *const CK_DRIVE[4] = { "DRIVES OK", "STUCK AT 1", "STUCK AT 0", "READS INVERTED" }; + static const char *const CK_PULL[4] = { "FREE", "HELD HIGH", "HELD LOW", "ODD" }; + static const char *const ROLE_NAME[3] = { "DATA", "CLOCK", "LATCH" }; + char l[48]; + uint32_t pos = lp.pos; + int kind = pos == 0u ? 0 : pos < LP_LINES ? 1 : 2; + if (kind != lp.kind) { lcd_rect(0, 0, 240, 240, UI_BG); lp.kind = kind; } /* no rows left from another page */ l[0] = 0; - b_str(l, "BYTE "); lp_hex2(l, lp_mask()); b_str(l, " / 00, TWICE A SECOND"); - lp_text(0, 74, 240, l, &FT_SM, UI_ACC, 1); - l[0] = 0; - if (!lp.pos) b_str(l, "EVERY BUTTON LIGHT SHOULD BLINK"); - else if (mapped) { b_str(l, "BIT "); b_num(l, bit); b_str(l, " LIGHTS: "); b_str(l, LP_BTN[lp.map[bit] - 1u]); } - else b_str(l, "PRESS THE BUTTON THAT BLINKS"); - lp_text(0, 96, 240, l, &FT_SM, mapped ? UI_OK : UI_TXT, 1); - lp_text(0, 114, 240, !lp.pos ? "KNOB: ONE BIT AT A TIME" : mapped ? "SAME BUTTON AGAIN: CLEAR" : "KNOB: NEXT BIT", - &FT_SM, UI_DIM, 1); - for (uint32_t i = 0; i < 8u; i++) { /* the map: bits 0-3 left, 4-7 right */ - uint16_t col = (lp.pos && i == bit) ? UI_ACC : lp.map[i] ? UI_TXT : UI_DIM; + if (pos == 0u) b_str(l, "PIN CHECK"); + else if (pos < LP_LINES) { b_str(l, "ROLES "); b_num(l, pos); b_str(l, " OF 6"); } + else { b_str(l, "LED LINE "); b_num(l, pos - LP_LINES + 1u); b_str(l, " OF "); b_num(l, lp.n); } + LP_TEXT_HOOK(l, &FT_MD, 200u); + ft_box(20, 6, 200, l, &FT_MD, UI_ACC, UI_BG, 1); + lp_dot(); + if (pos == 0u) { + for (int i = 0; i < 3; i++) { + int good = lp.chk[i][0] == CK_OK && lp.chk[i][1] == CK_OK; + l[0] = 0; + b_str(l, "PC"); b_num(l, LP_CHK[i]); b_str(l, ": "); + b_str(l, CK_DRIVE[lp.chk[i][0]]); b_str(l, ", "); b_str(l, CK_PULL[lp.chk[i][1]]); + lp_text(40u + (uint32_t)i * 20u, l, &FT_SM, good ? UI_OK : RED); + } + lp_text(104, "ON A 595 INPUT: DRIVES OK, FREE", &FT_SM, UI_DIM); + lp_text(124, "", &FT_SM, UI_TXT); + lp_text(144, "KNOB: NEXT TEST", &FT_SM, UI_DIM); + } else if (pos < LP_LINES) { + const uint8_t *r = LP_ROLE[pos - 1u]; + for (int i = 0; i < 3; i++) { + l[0] = 0; + b_str(l, ROLE_NAME[i]); b_str(l, " PC"); b_num(l, r[i]); + lp_text(36u + (uint32_t)i * 20u, l, &FT_SM, UI_TXT); + } + lp_text(104, pos == 1u ? "ISOD'S READING. ALL 8 BITS: FF / 00" : "SWAPPED. ALL 8 BITS: FF / 00", &FT_SM, UI_ACC); + lp_text(124, "ANY LIGHT? CAB, DLY OR REV: MARK", &FT_SM, UI_TXT); + lp_text(144, "KNOB: NEXT TEST", &FT_SM, UI_DIM); + } else { + uint32_t id = lp.ids[pos - LP_LINES]; + const char *role = lp_role_of(id); + l[0] = 0; + lp_name(l, id); + lp_text(30, l, &FT_NM, UI_TXT); + l[0] = 0; + if (role) { b_str(l, role); b_str(l, ": DON'T PRESS IT"); } + lp_text(66, l, &FT_SM, UI_DIM); + lp_text(86, "LINE 1 / 0, AND THE 595 FF / 00", &FT_SM, UI_ACC); + lp_text(104, "ALL FOUR MIXES ONCE A SECOND", &FT_SM, UI_ACC); + lp_text(124, "ANY LIGHT? CAB, DLY OR REV: MARK", &FT_SM, UI_TXT); + lp_text(144, "KNOB: NEXT PIN", &FT_SM, UI_DIM); + } + { /* the marks, six a line */ + char m[2][44]; + uint32_t k = 0; + m[0][0] = m[1][0] = 0; + for (uint32_t j = 0; j < 6u + 64u; j++) { + char nm[6]; + nm[0] = 0; + if (j < 6u) { if (!(lp.mark_role >> j & 1u)) continue; b_str(nm, "R"); b_num(nm, j + 1u); } + else { if (!lp_has(lp.mark, j - 5u)) continue; lp_name(nm, j - 5u); } + if (k < 12u) { char *t = m[k / 6u]; if (t[0]) b_str(t, " "); b_str(t, nm); } + k++; + } l[0] = 0; - b_str(l, "BIT "); b_num(l, i); b_str(l, ": "); b_str(l, lp.map[i] ? LP_BTN[lp.map[i] - 1u] : "--"); - lp_text(i < 4u ? 24u : 136u, 140u + (i & 3u) * 18u, 100u, l, &FT_SM, col, 0); + b_str(l, "MARKED: "); + if (k) b_num(l, k); else b_str(l, "NONE YET"); + lp_text(178, l, &FT_SM, UI_DIM); + lp_text(198, m[0], &FT_SM, UI_TXT); + lp_text(216, m[1], &FT_SM, UI_TXT); } - lp_text(0, 218, 240, "SQUARE LIT = BITS AT 1", &FT_SM, UI_DIM, 1); +} + +static void lp_show(void) /* the outputs for this position and phase */ +{ + if (lp.pos == 0u) return; + if (lp.pos < LP_LINES) { lp_595((lp.phase & 1u) ? 0x00u : 0xFFu); return; } + lp_level(lp.id, lp.phase & 1u); + lp_595((lp.phase & 2u) ? 0xFFu : 0x00u); +} +static void lp_enter(uint32_t now) /* leave the old test, start the new one */ +{ + if (lp.id) { lp_give(lp.id); lp.id = 0; } + lp.phase = 0; + lp.t = now; + if (lp.pos == 0u) lp_check(); + else if (lp.pos < LP_LINES) lp_roles((int)lp.pos - 1); + else { + if (lp.role != 0) lp_roles(0); + lp.id = lp.ids[lp.pos - LP_LINES]; + lp_take(lp.id); + } + lp_show(); + lp.dirty = 1; } static void lp_init(void) { - lp.pos = 0; - lp.hi = 1; + 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.pos = lp.id = 0; lp.acc = 0; lp.key = KEY_NONE; lp.enc = (uint8_t)(PC_IN & ENC_MASK); - for (int i = 0; i < 8; i++) lp.map[i] = 0; - for (int i = 0; i < LB_N; i++) lp.b[i].stab = lp.b[i].last = lp.b[i].cnt = 0; + lp.mark_role = 0; + for (int k = 0; k < 4; k++) lp.mark[k] = 0; + lp.k.stab = lp.k.last = lp.k.cnt = 0; guard.pending = 0; /* a reset here is not a failed boot */ guard.failed = 0; /* (no UBOOT, no safe mode screen) */ op_diag.stage = ST_OK; - leds_init(); - lp.t_blink = lp.t_keys = ms_now(); - lp_show(); - lcd_rect(0, 0, 240, 240, UI_BG); - lp.dirty = 1; + lp.kind = -1; + lp.t_keys = ms_now(); + lp_enter(lp.t_keys); /* the pin check */ } static void lp_step(uint32_t now) /* one pass of the loop */ @@ -114,35 +289,24 @@ static void lp_step(uint32_t now) /* one pass lp.enc = s; if (lp.acc >= 4) { lp.acc -= 4; dd = 1; } else if (lp.acc <= -4) { lp.acc += 4; dd = -1; } if (dd) { - lp.pos = dd > 0 ? (lp.pos + 1u) % LP_NPOS : (lp.pos + LP_NPOS - 1u) % LP_NPOS; - lp.hi = 1; - lp.t_blink = now; - lp_show(); - lp.dirty = 1; + uint32_t n = lp_npos(); + lp.pos = dd > 0 ? (lp.pos + 1u) % n : (lp.pos + n - 1u) % n; + lp_enter(now); } } } - { /* the buttons (pressed = differs from the idle state); CAB / DLY / REV are the analog keypad, every 10 ms */ - uint32_t pa = (PA_IN & 0xFFFFu) ^ base_pa, ph = (PH_IN & 0xFFFFu) ^ base_ph; - int hit = -1, keys = now - lp.t_keys >= 10u; - int raw[LB_N] = { - (pa & PA_FX) != 0, (ph & PH_MOD) != 0, (ph & PH_AMP) != 0, 0, 0, 0, - (pa & PA_HOME) != 0, (ph & PH_PUSH) != 0, (pa & PA_FS_UP) != 0, (pa & PA_FS_DN) != 0 }; - if (keys) { lp.t_keys = now; lp.key = keys_decode(adc_read2(ADC_CH_KEYS)); } - raw[LB_CAB] = lp.key == KEY_CAB; raw[LB_DLY] = lp.key == KEY_DLY; raw[LB_REV] = lp.key == KEY_REV; - for (int i = 0; i < LB_N; i++) { - if ((i == LB_CAB || i == LB_DLY || i == LB_REV) && !keys) continue; - if (dbn_press(&lp.b[i], raw[i]) && hit < 0) hit = i; - } - if (hit >= 0 && lp.pos) { - uint8_t *m = &lp.map[lp.pos - 1u]; - *m = (*m == (uint8_t)(hit + 1)) ? 0u : (uint8_t)(hit + 1); + if (now - lp.t_keys >= 10u) { /* the analog keys (CAB, DLY, REV), every 10 ms: any of them marks */ + lp.t_keys = now; + lp.key = keys_decode(adc_read2(ADC_CH_KEYS)); + if (dbn_press(&lp.k, lp.key != KEY_NONE) && lp.pos) { + if (lp.pos < LP_LINES) lp.mark_role ^= 1u << (lp.pos - 1u); + else lp.mark[(lp.id - 1u) >> 4] ^= 1u << ((lp.id - 1u) & 15u); lp.dirty = 1; } } - if (now - lp.t_blink >= LP_BLINK_MS) { - lp.t_blink = now; - lp.hi = !lp.hi; + if (lp.pos && now - lp.t >= LP_STEP_MS) { + lp.t = now; + lp.phase = (lp.phase + 1u) & 3u; lp_show(); lp_dot(); } diff --git a/firmware/openpocket.c b/firmware/openpocket.c index a3b35b1..c4bb061 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 1 /* 1 = LED map (issue #8, led_probe.h): the knob picks a bit of the lights' 595, it blinks; no sound */ +#define LED_PROBE 1 /* 1 = LED hunt (issue #8, led_probe.h): pin check, 595 roles, LED lines; no sound */ #define PA_PD (*(volatile uint32_t *)0x50014u) /* ---- Controls mapped on the real pedal (diagnostic) ---- @@ -549,7 +549,7 @@ static uint32_t ed_flags(void) { return (a_bypass ? 1u : 0u) | (a_c1 ? 2u : 0u); #if AUDIO_PROBE #error "LED_PROBE needs the normal build: USB (the web installer) is the way back" #endif -#include "led_probe.h" /* the LED map (issue #8) */ +#include "led_probe.h" /* the LED hunt (issue #8) */ #endif /* P3_RST_SRC as read at boot: why the chip last reset (fm1_sys.h) */ diff --git a/firmware/px_leds.h b/firmware/px_leds.h index 39a1f23..4c29dc0 100644 --- a/firmware/px_leds.h +++ b/firmware/px_leds.h @@ -9,9 +9,9 @@ * A byte, MSB first: STCP low; for each bit SHCP low, DS = the bit, wait, SHCP high, wait; then STCP high, whose * rising edge puts the byte on the outputs. Stock waits about 4 to 8 us at each step: LEDS_WAIT is 5 us. * Stock's byte: bit 0 a blink signal (toggled by a timer), bits 1..6 six light states, bit 7 always 0. It rewrites - * the byte only when it changes, and writes 0 at power-off. Which bit is which light: the LED map finds out - * (led_probe.h, LED_PROBE 1). - * Not verified on hardware yet. + * the byte only when it changes, and writes 0 at power-off. + * On a BlackBox (2026-11-06) this lit nothing, not even all 8 bits blinking: the reading misses something (the + * roles, another line, an enable). LED_PROBE 1 builds the LED hunt (led_probe.h), which looks for it. * * Every edge is a read-modify-write of PORTC OUT, so these are called from the main loop only (no interrupt writes * port C). tests/lpsim.c checks the bit order and the waits against a model of the 595. */ @@ -40,26 +40,37 @@ static void leds_pin(uint32_t m, uint32_t hi) else LEDS_PC(0x00) &= ~m; LEDS_EDGE(); } -static void leds_write(uint8_t v) +/* The pins are parameters so that a test build can try other roles (led_probe.h); the driver is leds_init / + * leds_write with the pins above. */ +static void leds_shift(uint8_t v, uint32_t ds, uint32_t sh, uint32_t st) { - leds_pin(LEDS_ST, 0u); + leds_pin(st, 0u); for (int i = 7; i >= 0; i--) { - leds_pin(LEDS_SH, 0u); - leds_pin(LEDS_DS, ((uint32_t)v >> i) & 1u); + leds_pin(sh, 0u); + leds_pin(ds, ((uint32_t)v >> i) & 1u); LEDS_WAIT(); - leds_pin(LEDS_SH, 1u); + leds_pin(sh, 1u); LEDS_WAIT(); } - leds_pin(LEDS_ST, 1u); - leds_byte = v; + leds_pin(st, 1u); } -static void leds_init(void) +static void leds_setup(uint32_t ds, uint32_t sh, uint32_t st) { - LEDS_PC(0x10) &= ~LEDS_PINS; /* no pull-ups (the boot leaves them on) */ - LEDS_PC(0x14) &= ~LEDS_PINS; /* no pull-downs */ - LEDS_PC(0x00) = (LEDS_PC(0x00) & ~LEDS_PINS) | LEDS_SH; /* SHCP 1, STCP 0, DS 0, as stock */ - LEDS_PC(0x08) &= ~LEDS_PINS; /* outputs */ + uint32_t m = ds | sh | st; + LEDS_PC(0x10) &= ~m; /* no pull-ups (the boot leaves them on) */ + LEDS_PC(0x14) &= ~m; /* no pull-downs */ + LEDS_PC(0x00) = (LEDS_PC(0x00) & ~m) | sh; /* SHCP 1, STCP 0, DS 0, as stock */ + LEDS_PC(0x08) &= ~m; /* outputs */ LEDS_EDGE(); LEDS_WAIT(); /* SHCP high for a while, even if it was low */ +} +static void leds_write(uint8_t v) +{ + leds_shift(v, LEDS_DS, LEDS_SH, LEDS_ST); + leds_byte = v; +} +static void leds_init(void) +{ + leds_setup(LEDS_DS, LEDS_SH, LEDS_ST); leds_write(0u); } diff --git a/tests/lpsim.c b/tests/lpsim.c index 0e55129..2dc982c 100644 --- a/tests/lpsim.c +++ b/tests/lpsim.c @@ -1,15 +1,17 @@ /* SPDX-License-Identifier: GPL-3.0-only - * OpenPocket — the LED map (firmware/led_probe.h and the 595 driver px_leds.h, issue #8) on the PC, against fake - * GPIO registers and a model of the 74HC595: + * OpenPocket — the LED hunt (firmware/led_probe.h and the 595 driver px_leds.h, issue #8) on the PC, against fake + * GPIO registers (pads with pulls and outside holds) and a model of the 74HC595 wired as isod89 read it: * cc -O1 -Wall -Wno-unused-function -Ifirmware -o build/lpsim tests/lpsim.c && build/lpsim build/lp - * Checks: only PC5, PC6 and PC8 are ever changed (every other register bit of PA, PB, PC and PH as at start, PC7 - * included); they end as outputs without pulls; every shift clock edge and the latch's rise have a wait before them - * (the start counts as an edge, so SHCP is high for a wait even when it began low), DS is an output - * and the latch is low while shifting; the byte reaches the 595's outputs MSB first and is always the one the screen - * names, blinking at 2 Hz between the bits on screen and 0, lit right after a turn; the knob wraps both ways over the - * 9 positions; each button writes itself into the table, again clears it, another replaces it, nothing is written - * on ALL 8 BITS; the analog keys (CAB, DLY, REV) are read every 10 ms; USB is serviced; the boot is not counted as - * failed; every line of text fits its box. Writes PPM screenshots to OUTDIR. */ + * Checks, on every pass of its loop: the pins it must never drive (screen, knob, I2S, amp enable, MASTER, analog keys) + * keep every register; besides PC5, PC6 and PC8 at most one pin differs from the start, the LED line on screen. + * The pin check tells a free pin (DRIVES OK, FREE) from one held high or low, and leaves the three pins outputs + * without pulls, their DIE as found. With isod89's roles every shift clock edge and latch has a wait before it, the + * latch is low while shifting, and the bytes arrive MSB first: ROLES 1 blinks FF / 00 at 2 Hz; on an LED line the + * line (1 / 0) and the 595 (FF / 00) go through all four mixes each second. All 47 lines in turn, each alone and + * given back exactly; the knob wraps both ways; the analog keys mark and unmark (not on the pin check); USB is + * serviced; the boot is not counted as failed; every line of text fits. The driver alone too: leds_init from pins + * with pulls leaves outputs without them and 0 shifted out, leds_write puts each byte on the outputs (MSB first). + * Writes PPM screenshots to OUTDIR. */ #include #include #include @@ -37,30 +39,23 @@ static void lcd_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint16_t c) } static void fm1_wdt_feed(void) { } -/* ---- the pedal around the LED map: gpio[port p (0 A, 1 B, 2 C, 7 H)][register r / 4] */ -enum { R_OUT = 0, R_DIR = 2, R_PU = 4, R_PD = 5 }; /* register index: OUT +0, DIR +8, PU +0x10, PD +0x14 */ +/* ---- the pedal: gpio[port p (0 A, 1 B, 2 C, 7 H)][register r / 4]; a pad is its OUT bit when an output, else its + * pull (no pull reads 0); ext_hi / ext_lo hold port C pads from outside, stronger than the pin's own driver */ +enum { R_OUT = 0, R_IN = 1, R_DIR = 2, R_DIE = 3, R_PU = 4, R_PD = 5 }; static uint32_t gpio[8][16]; -static uint32_t pa_in, ph_in, pc_in; /* the buttons as pressed, the knob */ -static uint32_t sim_pa_in(void) /* as the pad reads: an output pin reads back its own level */ +static uint32_t pc_in, ext_hi, ext_lo; +static uint32_t pc_pad(void) { - uint32_t dir = gpio[0][R_DIR]; - return (pa_in & dir) | (gpio[0][R_OUT] & ~dir); + uint32_t d = gpio[2][R_DIR]; + return (((gpio[2][R_OUT] & ~d) | (gpio[2][R_PU] & d)) | ext_hi) & ~ext_lo; } -#define PA_IN sim_pa_in() -#define PH_IN ph_in -#define PC_IN pc_in +static uint32_t sim_pc_in(void) { return (pc_in & 3u) | (pc_pad() & gpio[2][R_DIE] & ~3u); } +#define LP_REG(port, r) (gpio[(port)][(r) / 4u]) +#define PC_IN sim_pc_in() #define ENC_MASK 0x3u -#define PA_FX (1u << 0) /* as openpocket.c */ -#define PA_HOME (1u << 1) -#define PA_FS_UP (1u << 2) -#define PA_FS_DN (1u << 3) -#define PH_PUSH ((1u << 4) | (1u << 6)) -#define PH_MOD ((1u << 7) | (1u << 8)) -#define PH_AMP (1u << 9) -static uint32_t base_pa, base_ph; enum { KEY_NONE = 0, KEY_CAB, KEY_DLY, KEY_REV }; #define ADC_CH_KEYS 9u -static int sim_key; /* the analog key held down */ +static int sim_key; static unsigned long adc_calls; static int32_t adc_read2(uint32_t ch) { if (ch == ADC_CH_KEYS) adc_calls++; return sim_key; } static int keys_decode(int32_t v) { return (int)v; } @@ -90,34 +85,24 @@ static void b_num(char *d, uint32_t v) } static void b_str(char *d, const char *s) { while (*d) d++; while (*s) *d++ = *s++; *d = 0; } -/* ---- the 74HC595, from its data sheet: DS is shifted in on SHCP's rising edge (the first bit in ends at Q7), the - * outputs take the shift register on STCP's rising edge. A pin's level: its OUT bit when an output, else its pull. */ +/* ---- the 74HC595 as isod89 read the board (DS PC8, SHCP PC5, STCP PC6), from its data sheet: DS is shifted in on + * SHCP's rising edge (the first bit in ends at Q7), the outputs take the shift register on STCP's rising edge. The + * timing rules are counted only while the hunt drives it with those roles (lp.role 0). */ #define M_DS (1u << 8) #define M_SH (1u << 5) #define M_ST (1u << 6) +#define M_595 (M_DS | M_SH | M_ST) static uint32_t lv_prev; static uint8_t sr, q; -static unsigned long waits, w_sh, latches; +static unsigned long waits, w_sh, latches, settles; static int e_wait, e_latch, e_dir; -static uint32_t pc_level(void) { uint32_t d = gpio[2][R_DIR]; return (gpio[2][R_OUT] & ~d) | (gpio[2][R_PU] & d); } static void sim_wait(void) { waits++; } -static void sim_edge(void) -{ - uint32_t lv = pc_level(), rise = lv & ~lv_prev, fall = ~lv & lv_prev; - if (rise & M_SH) { - if (waits == w_sh) e_wait++; /* no wait while SHCP was low (or since the start) */ - if (lv & M_ST) e_latch++; /* shifting with the latch high: the outputs would ripple */ - if (gpio[2][R_DIR] & M_DS) e_dir++; /* DS not driven */ - sr = (uint8_t)((sr << 1) | ((lv & M_DS) ? 1u : 0u)); - w_sh = waits; - } - if (fall & M_SH) { if (waits == w_sh) e_wait++; w_sh = waits; } /* no wait while SHCP was high */ - if (rise & M_ST) { if (waits == w_sh) e_wait++; q = sr; latches++; } /* a wait after the last shift too */ - lv_prev = lv; -} +static void sim_settle(void) { settles++; waits++; } /* 50 us: time passes for the 595 too */ +static void sim_edge(void); #define LEDS_PC(r) (gpio[2][(r) / 4u]) #define LEDS_WAIT() sim_wait() #define LEDS_EDGE() sim_edge() +#define LP_SETTLE() sim_settle() static int failed, clipped; #define LP_TEXT_HOOK(s, f, w) do { \ @@ -126,10 +111,25 @@ static int failed, clipped; #include "ui_text.h" #include "led_probe.h" +static void sim_edge(void) +{ + uint32_t lv = pc_pad(), rise = lv & ~lv_prev, fall = ~lv & lv_prev; + int strict = lp.role == 0; + if (rise & M_SH) { + if (strict && waits == w_sh) e_wait++; /* no wait while SHCP was low (or since the start) */ + if (strict && (lv & M_ST)) e_latch++; /* shifting with the latch high: the outputs would ripple */ + if (strict && (gpio[2][R_DIR] & M_DS)) e_dir++; + sr = (uint8_t)((sr << 1) | ((lv & M_DS) ? 1u : 0u)); + w_sh = waits; + } + if (fall & M_SH) { if (strict && waits == w_sh) e_wait++; w_sh = waits; } + if (rise & M_ST) { if (strict && waits == w_sh) e_wait++; q = sr; latches++; } + lv_prev = lv; +} + /* ---- the checks */ static const int PORTS[4] = { 0, 1, 2, 7 }; static uint32_t snap[8][16]; -static int want, want2 = -1; /* the position the test expects (0 all, 1..8 bit want - 1); mid-turn either */ static void ok(int c, const char *what) { if (!c) { printf("FAIL: %s\n", what); failed++; } } static void reset_gpio(void) /* a distinctive start; PC5, PC6, PC8 inputs with pull-ups, as booted */ { @@ -138,55 +138,66 @@ static void reset_gpio(void) /* a distinctive start; PC5, PC6 int p = PORTS[k]; gpio[p][R_OUT] = 0x5A5Au ^ (uint32_t)(k * 0x1111); gpio[p][R_DIR] = 0xFFFFu & ~(0x0101u << k); - gpio[p][R_PD] = 0x3030u >> k; + gpio[p][R_DIE] = 0xF0F0u >> k; + gpio[p][R_PD] = 0x3030u >> k; /* every pin has a pull, so a driven one always differs */ gpio[p][R_PU] = 0xFFFFu & ~gpio[p][R_PD]; } gpio[1][R_DIR] &= ~0x00E2u; /* the screen pins PB1/5/6/7 and PC7: outputs, PC7 high */ gpio[2][R_DIR] &= ~(1u << 7); gpio[2][R_OUT] |= 1u << 7; - gpio[2][R_DIR] |= M_DS | M_SH | M_ST; gpio[2][R_PU] |= M_DS | M_SH | M_ST; gpio[2][R_PD] &= ~(M_DS | M_SH | M_ST); - gpio[0][R_DIR] |= 0x000Fu; /* the buttons are inputs, as input_init leaves them */ - gpio[7][R_DIR] |= PH_PUSH | PH_MOD | PH_AMP; + gpio[2][R_DIR] |= M_595; gpio[2][R_PU] |= M_595; gpio[2][R_PD] &= ~M_595; gpio[2][R_DIE] &= ~M_595; memcpy(snap, gpio, sizeof snap); - pa_in = ph_in = pc_in = 0; sim_key = KEY_NONE; - base_pa = sim_pa_in() & 0xFFFFu; base_ph = ph_in; /* as input_baseline */ - lv_prev = pc_level(); + pc_in = 0; sim_key = KEY_NONE; + lv_prev = pc_pad(); sr = 0x5Au; q = 0xA5u; /* a 595 holds garbage at power-on */ } -static int only_595_pins(void) /* every register bit as at start, but PC5/6/8's OUT, DIR, PU, PD */ +/* written out again here, not taken from led_probe.h: PA4..PA10, PB1/5/6/7, PC0/1/7 */ +static const uint32_t FORBID[4] = { 0x7Fu << 4, (1u << 1) | (1u << 5) | (1u << 6) | (1u << 7), 3u | (1u << 7), 0u }; +static int others_changed(uint32_t *who) /* pins other than PC5/6/8 that differ from the start */ { + int n = 0; for (int k = 0; k < 4; k++) - for (int r = 0; r < 16; r++) { - uint32_t may = (PORTS[k] == 2 && (r == R_OUT || r == R_DIR || r == R_PU || r == R_PD)) ? (M_DS | M_SH | M_ST) : 0u; - if ((gpio[PORTS[k]][r] ^ snap[PORTS[k]][r]) & ~may) return 0; + for (int b = 0; b < 16; b++) { + uint32_t m = 1u << b; int p = PORTS[k], d = 0; + if (p == 2 && (m & M_595)) continue; + for (int r = 0; r < 16; r++) if ((gpio[p][r] ^ snap[p][r]) & m) d = 1; + if (d) { n++; if (who) *who = (uint32_t)(k * 16 + b + 1); } } - return 1; + return n; } -static uint8_t mask_of(int pos) { return pos ? (uint8_t)(1u << (pos - 1)) : 0xFFu; } +static int forbidden_touched(void) +{ + for (int k = 0; k < 4; k++) + for (int r = 0; r < 16; r++) + if ((gpio[PORTS[k]][r] ^ snap[PORTS[k]][r]) & FORBID[k]) return 1; + return 0; +} +static int mix_seen[2][2]; /* on an LED line: [line level][595 at FF] */ 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()); - if (!only_595_pins()) { ok(0, "a pin other than PC5 / PC6 / PC8 was changed"); return; } - if (q != 0u && q != mask_of(want) && !(want2 >= 0 && q == mask_of(want2))) { - ok(0, "the 595 shows a byte other than the bits on screen"); return; + n = others_changed(&who); + if (forbidden_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 LED line on screen changed"); return; } + if (lp.id && (q == 0x00u || q == 0xFFu)) { + uint32_t P = LP_PORT[(lp.id - 1u) >> 4], m = 1u << ((lp.id - 1u) & 15u); + mix_seen[(gpio[P][R_OUT] & m) ? 1 : 0][q == 0xFFu]++; } } } 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 }; - want2 = dir > 0 ? (want + 1) % 9 : (want + 8) % 9; + uint32_t n = lp_npos(), want = dir > 0 ? (lp.pos + 1u) % n : (lp.pos + n - 1u) % n; for (int k = 0; k < 4; k++) { pc_in = (pc_in & ~3u) | (dir > 0 ? FWD[k] : BACK[k]); run(2); } - want = want2; want2 = -1; - ok(lp.pos == (uint32_t)want, "the knob moves one position per detent"); - ok(q == mask_of(want), "lit right after a turn"); + ok(lp.pos == want, "the knob moves one test per detent"); } -static void go(int pos) { for (int k = 0; k < 9 && want != pos; k++) turn(+1); } -static void press_pa(uint32_t m) { pa_in ^= m; run(20); pa_in ^= m; run(20); } -static void press_ph(uint32_t m) { ph_in ^= m; run(20); ph_in ^= m; run(20); } +static void go(uint32_t pos) { for (uint32_t k = 0; k <= lp_npos() && lp.pos != pos; k++) turn(+1); ok(lp.pos == pos, "the knob reaches it"); } static void press_key(int k) { sim_key = k; run(60); sim_key = KEY_NONE; run(60); } -static int map(int bit) { return lp.map[bit] ? lp.map[bit] - 1 : -1; } +static uint32_t pin(int k, int b) { return (uint32_t)(k * 16 + b + 1); } +static uint32_t pos_of(uint32_t id) { for (uint32_t i = 0; i < lp.n; i++) if (lp.ids[i] == id) return LP_LINES + i; return 0; } static void shot(const char *dir, const char *name) { static int n; @@ -203,75 +214,108 @@ static void shot(const char *dir, const char *name) } fclose(f); } +static void boot(void) +{ + reset_gpio(); + guard.pending = 1; guard.failed = 1; op_diag.stage = 3; + lp_init(); + run(5); +} +static int chk_is(int i, int drive, int pull) { return lp.chk[i][0] == drive && lp.chk[i][1] == pull; } int main(int argc, char **argv) { const char *dir = argc > 1 ? argv[1] : "build/lp"; + /* the driver alone (px_leds.h): from inputs with pulls (PC8's a pull-down), then bytes MSB first */ reset_gpio(); - guard.pending = 1; guard.failed = 1; op_diag.stage = 3; - lp_init(); + gpio[2][R_PU] &= ~M_DS; gpio[2][R_PD] |= M_DS; + memcpy(snap, gpio, sizeof snap); + lv_prev = pc_pad(); + leds_init(); + ok(q == 0u && leds_byte == 0u && latches == 1u, "leds_init: 0 shifted out"); + ok(!(gpio[2][R_DIR] & M_595) && !(gpio[2][R_PU] & M_595) && !(gpio[2][R_PD] & M_595), "leds_init: outputs, no pulls"); + { + static const uint8_t V[5] = { 0xA5u, 0x01u, 0x80u, 0x3Cu, 0x00u }; + int good = 1; + for (int i = 0; i < 5; i++) { leds_write(V[i]); if (q != V[i] || leds_byte != V[i]) good = 0; } + ok(good, "leds_write: each byte on the outputs, MSB first"); + } + ok(others_changed(0) == 0, "the driver touches only PC5, PC6, PC8"); + + /* the pin check, on a board where something holds PC8 high and PC6 low */ + ext_hi = M_DS; ext_lo = M_ST; + boot(); + ok(chk_is(0, CK_OK, CK_OK), "PC5 free: DRIVES OK, FREE"); + ok(chk_is(1, CK_LO, CK_LO), "PC6 held low: STUCK AT 0, HELD LOW"); + ok(chk_is(2, CK_HI, CK_HI), "PC8 held high: STUCK AT 1, HELD HIGH"); + ok(settles == 12u, "every read waits for the pad"); + shot(dir, "pin_check_held"); + ext_hi = ext_lo = 0; + e_wait = e_latch = e_dir = 0; /* a held latch is not the driver's fault */ + + /* as on the board isod89 read */ + boot(); ok(guard.pending == 0u && guard.failed == 0u && op_diag.stage == ST_OK, "not counted as a failed boot"); - ok(!(gpio[2][R_DIR] & (M_DS | M_SH | M_ST)), "PC5, PC6, PC8 are outputs"); - ok(!(gpio[2][R_PU] & (M_DS | M_SH | M_ST)) && !(gpio[2][R_PD] & (M_DS | M_SH | M_ST)), "without pulls"); - ok(latches == 2u && q == 0xFFu, "0 at start (as stock), then ALL 8 BITS lit"); - ok(only_595_pins(), "nothing else touched by the start"); - run(5); + ok(lp.n == 47u && lp_npos() == 54u, "47 LED lines, 54 tests"); + ok(chk_is(0, CK_OK, CK_OK) && chk_is(1, CK_OK, CK_OK) && chk_is(2, CK_OK, CK_OK), "free pins: DRIVES OK, FREE"); + ok(!(gpio[2][R_DIR] & M_595) && !(gpio[2][R_PU] & M_595) && !(gpio[2][R_PD] & M_595), "then outputs, no pulls"); + ok((gpio[2][R_DIE] & M_595) == (snap[2][R_DIE] & M_595), "their DIE as found"); + ok(q == 0u && lp.role == 0, "the 595 at 0 with isod's roles after the check"); + ok(others_changed(0) == 0, "nothing else touched"); ok(usb_calls > 0u, "USB serviced"); - shot(dir, "all_bits"); + shot(dir, "pin_check"); - { /* 2 Hz: 8 changes in 2 s, always the bits on screen or 0 */ + /* ROLES 1: all 8 bits at 2 Hz */ + turn(+1); + ok(lp.pos == 1u && lp.role == 0 && q == 0xFFu, "ROLES 1: isod's roles, lit at once"); + { int flips = 0; uint8_t last = q; - for (int t = 0; t < 2000; t++) { run(1); if (q != last) { flips++; last = q; } } - ok(flips >= 7 && flips <= 9, "2 Hz: 8 changes in 2 s"); + for (int t = 0; t < 2000; t++) { run(1); if (q != last) { flips++; last = q; } if (q && q != 0xFFu) break; } + ok(flips >= 7 && flips <= 9 && (q == 0u || q == 0xFFu), "FF / 00 at 2 Hz"); } - { /* each bit alone, MSB first (an LSB-first driver would light bit 7 - n) */ - for (int b = 0; b < 8; b++) { turn(+1); run(300); } - ok(want == 8, "at BIT 7"); - turn(+1); - ok(lp.pos == 0u, "past BIT 7: ALL 8 BITS"); - turn(-1); - ok(lp.pos == 8u && q == 0x80u, "back from ALL: BIT 7"); + shot(dir, "roles1"); + for (int r = 2; r <= 6; r++) { turn(+1); ok(lp.role == r - 1, "the next roles"); run(600); } + shot(dir, "roles6"); + + /* the LED lines: each alone, all four mixes, given back */ + for (uint32_t i = 0; i < lp.n; i++) { + uint32_t who = 0; turn(+1); + memset(mix_seen, 0, sizeof mix_seen); + run(1100); + ok(lp.role == 0 && lp.id == lp.ids[i] && others_changed(&who) == 1 && who == lp.id, "each line in turn, alone"); + ok(mix_seen[0][0] && mix_seen[0][1] && mix_seen[1][0] && mix_seen[1][1], "all four mixes within a second"); + if (lp.id == pin(3, 6)) shot(dir, "line_ph6"); } - { /* the table: every button, again clears, another replaces, none on ALL */ - unsigned long a0 = adc_calls; uint32_t t0 = sim_ms; - press_key(KEY_DLY); - ok(memcmp(lp.map, "\0\0\0\0\0\0\0\0", 8) == 0, "nothing written on ALL 8 BITS"); - unsigned long n = adc_calls - a0, t = (sim_ms - t0) / 10u; - ok(n + 1u >= t && n <= t + 1u, "the keys every 10 ms"); - go(1); - shot(dir, "bit0"); - press_pa(PA_FX); - ok(map(0) == LB_FX, "FX on BIT 0"); - shot(dir, "bit0_fx"); - press_pa(PA_FX); - ok(map(0) < 0, "FX again clears it"); - press_pa(PA_FX); - go(2); press_pa(PA_FS_UP); - ok(map(1) == LB_FSR, "right footswitch on BIT 1"); - go(3); press_pa(PA_FS_DN); - ok(map(2) == LB_FSL, "left footswitch on BIT 2"); - go(4); press_ph(PH_AMP); - ok(map(3) == LB_AMP, "AMP on BIT 3"); - go(5); press_key(KEY_CAB); - ok(map(4) == LB_CAB, "CAB (analog) on BIT 4"); - go(6); press_ph(1u << 6); - ok(map(5) == LB_PUSH, "PUSH (PH6 alone) on BIT 5"); - go(7); press_key(KEY_REV); press_pa(PA_HOME); - ok(map(6) == LB_HOME, "HOME replaces REV on BIT 6"); - go(8); press_ph(1u << 8); - ok(map(7) == LB_MOD, "MOD (PH8 alone) on BIT 7"); - ok(map(0) == LB_FX && map(3) == LB_AMP, "the table keeps the others"); - shot(dir, "map"); - go(0); - press_pa(PA_FX); - ok(map(0) == LB_FX, "a press on ALL changes nothing"); - } - ok(e_wait == 0, "a wait on each side of every shift clock edge"); - ok(e_latch == 0, "the latch is low while shifting"); - ok(e_dir == 0, "DS is driven while shifting"); + turn(+1); + ok(lp.pos == 0u && lp.id == 0u && others_changed(0) == 0, "past the last line: the pin check, all given back"); + turn(-1); + ok(lp.id == pin(3, 15), "back from the pin check: the last line (PH15)"); + turn(+1); + + /* marks: the analog keys, not on the pin check */ + press_key(KEY_REV); + ok(lp.mark_role == 0u && !lp.mark[0] && !lp.mark[1] && !lp.mark[2] && !lp.mark[3], "no mark on the pin check"); + go(3); + press_key(KEY_DLY); + ok(lp.mark_role == 4u, "DLY marks ROLES 3"); + press_key(KEY_DLY); + ok(lp.mark_role == 0u, "and unmarks it"); + press_key(KEY_CAB); + go(pos_of(pin(3, 6))); + press_key(KEY_CAB); + ok(lp_has(lp.mark, pin(3, 6)), "CAB marks PH6"); + go(pos_of(pin(0, 1))); + press_key(KEY_REV); + ok(lp_has(lp.mark, pin(0, 1)) && lp.mark_role == 4u, "REV marks PA1, the others kept"); + shot(dir, "marks"); + ok(adc_calls >= sim_ms / 10u - 2u && adc_calls <= sim_ms / 10u + 2u, "the keys every 10 ms"); + + ok(e_wait == 0, "isod's roles: a wait before every shift clock edge and latch"); + ok(e_latch == 0, "isod's roles: the latch low while shifting"); + ok(e_dir == 0, "isod's roles: DS driven while shifting"); ok(clipped == 0, "every line fits its box"); - printf("LED map: %s (%d failures, %lu latches, %lu USB passes)\n", failed ? "FAIL" : "ok", failed, latches, usb_calls); + printf("LED hunt: %s (%d failures, %lu latches, %lu USB passes)\n", failed ? "FAIL" : "ok", failed, latches, usb_calls); return failed ? 1 : 0; } diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 75e5aaf..1dbb086 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -4,7 +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) -# - LED map (firmware/led_probe.h, px_leds.h): only the 595 pins move, MSB first with the waits, the byte on screen +# - LED hunt (firmware/led_probe.h, px_leds.h): pin check, the 6 roles, 47 LED lines one at a time, the 595 timing # - 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) @@ -19,7 +19,7 @@ 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 "== LED map and the 595 driver (LED_PROBE build, issue #8)" +echo "== LED hunt and the 595 driver (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 From e68ffb0d57d14cbc25ae798fb27e868e75cf9cb7 Mon Sep 17 00:00:00 2001 From: Roberto Antonacci <48053274+steponme82@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:05:46 +0100 Subject: [PATCH 5/9] LED hunt: the stock I/O supply first (CON5 0x74, VDDIO 3.2 V) isod89/openpocket#8. The first LED hunt lit nothing on a BlackBox: no role swap, none of the 47 LED lines on ports A, B, C and H. Its pin check read PC5, PC6 and PC8 as STUCK AT 0, FREE. FREE (a pull-up reads 1, a pull-down 0) is what a 595 input looks like; STUCK AT 0 is not a finding, the AC79 SDK's gpio_read is valid only while the pin is an input. The six effect buttons light on the stock firmware, as isod89's six light bits say, so the byte layout fits and the missing piece is likely power. Since v33 OpenPocket leaves the supplies as the boot loader sets them. BlackBox_020's power init sets P3_ANA_CON5 to 0x74 (docs/AUDIO-SDK.md v30): VDDIOM 4, the I/O supply at 3.2 V (2.8 V + 0.1 V a level, AC79 SDK power_interface.h). After a power cycle it is whatever the boot loader leaves; the 595 and the lights may need the stock level. The knob now starts on POWER AS FOUND (CON5 and the voltage as found, after a power-on or kept from before a restart, all 8 bits blinking), then STOCK POWER (CON5 = 0x74, written once, kept until power-off, all 8 bits blinking), then the six roles and the 47 lines at that voltage. The pin check is gone. Any analog key reading below 900 marks, so a moved supply cannot break the marks. tests/lpsim.c fakes the always-on domain: POWER AS FOUND writes nothing, STOCK POWER writes CON5 = 0x74 once and no other power register, both blink, a restart shows the kept level. 14 of 14 deliberate mistakes fail it. The normal build is unchanged. Not run on a pedal yet. Co-Authored-By: Claude Opus 5.5 --- firmware/led_probe.h | 155 +++++++++++++++++++++--------------------- firmware/openpocket.c | 2 +- tests/lpsim.c | 135 +++++++++++++++++++++--------------- tests/run_tests.sh | 2 +- 4 files changed, 159 insertions(+), 135 deletions(-) diff --git a/firmware/led_probe.h b/firmware/led_probe.h index 52377df..d0357c0 100644 --- a/firmware/led_probe.h +++ b/firmware/led_probe.h @@ -3,24 +3,29 @@ * * Built only with LED_PROBE 1 (openpocket.c). That build plays no sound. The LED map (git history) drove the * 74HC595 as isod89 read it in the stock firmware (data PC8, clock PC5, latch PC6: px_leds.h) and nothing lit, not - * even all 8 bits blinking. This build tests the ways that reading could be incomplete. The knob steps through: - * - PIN CHECK: PC5, PC6 and PC8 driven to 1 and 0 and read back, then left as inputs with a pull-up and with a - * pull-down. A pin wired only to a 595 input reads DRIVES OK, FREE; one held by something else reads HELD. - * - ROLES 1 to 6: the six ways to give data, clock and latch to PC5, PC6 and PC8 (1 is isod89's), with all 8 - * bits blinking (FF and 00, 2 Hz). + * even all 8 bits blinking; the first LED hunt (git history) tried the six role swaps and 47 LED lines on ports A, B, + * C and H, and nothing lit either. Its pin check found the three pins free as inputs (a 595 input reads so; reading + * a pin back while it drives is not valid on this chip: the AC79 SDK's gpio_read). The six effect buttons light on + * the stock firmware, as isod89's six light bits say, so the missing piece is likely power. Since v33 OpenPocket + * leaves the supplies as the boot loader sets them; BlackBox_020's power init sets P3_ANA_CON5 to 0x74: VDDIOM 4, + * the I/O supply at 3.2 V (docs/AUDIO-SDK.md v30; the WL82 levels are 2.8 V + 0.1 V each, AC79 SDK + * power_interface.h). The 595 and the lights may need it. The knob steps through: + * - POWER AS FOUND: CON5 and the I/O voltage as the boot left them (after a power-on, or kept by the always-on + * domain from the firmware before a restart), all 8 bits blinking (FF and 00, 2 Hz, isod89's roles). + * - STOCK POWER: CON5 set to 0x74 once, as the stock firmware does (it stays until the pedal is switched off), all + * 8 bits blinking. Everything after runs at this voltage. + * - ROLES 1 to 6: the six ways to give data, clock and latch to PC5, PC6 and PC8 (1 is isod89's), all 8 bits + * blinking. * - LED LINE 1 to 47: one more pin at a time blinks (1 and 0, 2 Hz) while the 595 (isod89's roles) blinks FF and - * 00 at 1 Hz, so every mix of the two comes once a second. On the FM-1 the 595 only selects a column, and an LED - * lights when its LED line is driven too (bsp/boards/fm1/fm1_input.h); an enable (OE, a supply) shows the same - * way. Never driven: PA4..PA10 (I2S, amp enable, MASTER, the analog keys), PB1/5/6/7 and PC7 (the screen), - * PC0/1 (the knob), port D (the flash). The first light finder (git history) drove these same pins alone, - * without trouble. + * 00 at 1 Hz, so every mix of the two comes once a second. Never driven: PA4..PA10 (I2S, amp enable, MASTER, + * the analog keys), PB1/5/6/7 and PC7 (the screen), PC0/1 (the knob), PC5/6/8 (the 595), ports D and E. * CAB, DLY or REV (the analog keys, never driven here) marks the test on screen, again unmarks it; the list stays at * the bottom for a photo. USB stays up, as in the normal build, so the web installer is the way back. * * Included by openpocket.c (LED_PROBE 1) and by tests/lpsim.c on the PC. The including file provides: PC_IN, * ENC_MASK, keys_decode, adc_read2, ADC_CH_KEYS, KEY_NONE, QDEC, dbn_t / dbn_press, ms_now, fm1_delay_us, - * fm1_wdt_feed, px_usb_service, lcd_rect, ft_box / ft_width and the fonts (ui_text.h), b_str / b_num, guard, - * op_diag / ST_OK and the UI colours. */ + * fm1_wdt_feed, fm1_p33_read / fm1_p33_write, op_pwr, px_usb_service, lcd_rect, ft_box / ft_width and the fonts + * (ui_text.h), b_str / b_num, guard, op_diag / ST_OK and the UI colours. */ #pragma once #include #include "px_leds.h" @@ -28,35 +33,33 @@ #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_SETTLE /* before reading a pin back: an internal pull charges the pad in about a microsecond */ -#define LP_SETTLE() fm1_delay_us(50u) -#endif #ifndef LP_TEXT_HOOK /* tests/lpsim.c checks that every line fits its box */ #define LP_TEXT_HOOK(s, f, w) ((void)0) #endif -enum { LP_OUT = 0x00, LP_DIR = 0x08, LP_DIE = 0x0C, LP_PU = 0x10, LP_PD = 0x14 }; /* DIR: 1 input, 0 output */ +enum { LP_OUT = 0x00, LP_DIR = 0x08, LP_PU = 0x10, LP_PD = 0x14 }; /* DIR: 1 input, 0 output */ +#define LP_CON5 0x05u /* P3_ANA_CON5: [3:0] VDDIOM (2.8 V + 0.1 V a level), [5:4] VDDIOW, [7:6] VDDIO drive */ +#define LP_CON5_STOCK 0x74u /* BlackBox_020 / PocketX_016's power init: VDDIOM 4 (3.2 V), VDDIOW 3, drive 1 */ 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, 0x01E3u, 0x0000u }; /* PA4..10 | PB1,5,6,7 | PC0,1,5..8 */ static const uint8_t LP_ROLE[6][3] = { /* the PC bit of data, clock, latch */ { 8u, 5u, 6u }, { 8u, 6u, 5u }, { 5u, 8u, 6u }, { 5u, 6u, 8u }, { 6u, 5u, 8u }, { 6u, 8u, 5u } }; -static const uint8_t LP_CHK[3] = { 5u, 6u, 8u }; /* the pins the pin check reads back */ #define LP_STEP_MS 250u -#define LP_LINES 7u /* knob: 0 pin check, 1..6 roles, 7.. lines */ -enum { CK_OK, CK_HI, CK_LO, CK_ODD }; /* a pin check result */ +#define LP_ROLES 2u /* knob: 0, 1 power, 2..7 roles, 8.. lines */ +#define LP_LINES 8u +#define LP_KEY_PRESSED 900 /* an analog key reading below this is a press, even if the supply moved its level */ static struct { uint8_t ids[64]; /* the LED line candidates: id = port index * 16 + bit + 1 (1..64) */ uint32_t n, pos, id; /* how many; the knob's position; the line driven now (0 = none) */ uint32_t sv_out, sv_dir, sv_pu, sv_pd; /* that line's bits as they were */ uint32_t t, phase, t_keys; - int role; /* the roles the 595 is driven with (LP_ROLE), -1 during the pin check */ - int dirty, key, kind; /* kind: the page on screen (0 pin check, 1 roles, 2 line; -1 none) */ + int role; /* the roles the 595 is driven with (LP_ROLE) */ + int dirty, kind; /* kind: the page on screen (0 power, 1 roles, 2 line; -1 none) */ int32_t acc; - uint8_t enc; - uint8_t chk[3][2]; /* per pin: driven 1 / 0 read back, pull-up / pull-down read back */ - uint32_t mark_role, mark[4]; + uint8_t enc, con5_found, stock; + uint32_t mark_pos, mark[4]; /* marks: positions 0..7 (bit pos), LED lines (by id) */ dbn_t k; } lp; @@ -97,6 +100,21 @@ static const char *lp_role_of(uint32_t id) /* what Ope return 0; } static int lp_has(const uint32_t *set, uint32_t id) { return (int)((set[(id - 1u) >> 4] >> ((id - 1u) & 15u)) & 1u); } +static void lp_hex2(char *d, uint32_t v) +{ + static const char H[] = "0123456789ABCDEF"; + while (*d) d++; + d[0] = H[(v >> 4) & 15u]; d[1] = H[v & 15u]; d[2] = 0; +} +static void lp_volts(char *d, uint32_t con5) /* "3.2 V (CON5 74)" */ +{ + uint32_t lv = con5 & 15u; + if (lv <= 7u) { + uint32_t dv = 28u + lv; /* tenths of a volt */ + b_num(d, dv / 10u); b_str(d, "."); b_num(d, dv % 10u); b_str(d, " V"); + } else { b_str(d, "LEVEL "); b_num(d, lv); } + b_str(d, " (CON5 "); lp_hex2(d, con5); b_str(d, ")"); +} static void lp_take(uint32_t id) /* the line becomes an output, low */ { @@ -128,27 +146,6 @@ static void lp_give(uint32_t id) /* the line LP_REG(P, LP_PD) = (LP_REG(P, LP_PD) & ~m) | lp.sv_pd; } -static uint32_t lp_read(uint32_t m) { LEDS_EDGE(); LP_SETTLE(); return (PC_IN & m) ? 1u : 0u; } -static uint8_t lp_class(uint32_t a, uint32_t b) { return (uint8_t)(a ? (b ? CK_HI : CK_OK) : (b ? CK_ODD : CK_LO)); } -static void lp_check(void) /* PC5, PC6, PC8: driven, then pulled */ -{ - lp.role = -1; - for (int i = 0; i < 3; i++) { - uint32_t m = 1u << LP_CHK[i], die = LP_REG(2u, LP_DIE) & m, h, l; - LP_REG(2u, LP_DIE) |= m; /* read the pad */ - LP_REG(2u, LP_PU) &= ~m; LP_REG(2u, LP_PD) &= ~m; - LP_REG(2u, LP_OUT) |= m; LP_REG(2u, LP_DIR) &= ~m; h = lp_read(m); - LP_REG(2u, LP_OUT) &= ~m; l = lp_read(m); - lp.chk[i][0] = lp_class(h, l); - LP_REG(2u, LP_DIR) |= m; LP_REG(2u, LP_PU) |= m; h = lp_read(m); - LP_REG(2u, LP_PU) &= ~m; LP_REG(2u, LP_PD) |= m; l = lp_read(m); - lp.chk[i][1] = lp_class(h, l); - LP_REG(2u, LP_PD) &= ~m; - LP_REG(2u, LP_DIE) = (LP_REG(2u, LP_DIE) & ~m) | die; - } - lp_roles(0); -} - static void lp_text(uint32_t y, const char *s, const op_font *f, uint16_t col) { LP_TEXT_HOOK(s, f, 232u); /* 4 px clear of each edge */ @@ -156,46 +153,44 @@ static void lp_text(uint32_t y, const char *s, const op_font *f, uint16_t col) } static int lp_lit(void) /* the square: what blinks is at 1 */ { - if (lp.pos == 0u) return 0; if (lp.pos < LP_LINES) return !(lp.phase & 1u); /* the 595 at FF */ return (int)(lp.phase & 1u); /* the line at 1 */ } static void lp_dot(void) { lcd_rect(222, 10, 14, 14, lp_lit() ? UI_ACC : UI_PANEL); } static void lp_draw(void) { - static const char *const CK_DRIVE[4] = { "DRIVES OK", "STUCK AT 1", "STUCK AT 0", "READS INVERTED" }; - static const char *const CK_PULL[4] = { "FREE", "HELD HIGH", "HELD LOW", "ODD" }; static const char *const ROLE_NAME[3] = { "DATA", "CLOCK", "LATCH" }; char l[48]; uint32_t pos = lp.pos; - int kind = pos == 0u ? 0 : pos < LP_LINES ? 1 : 2; + int kind = pos < LP_ROLES ? 0 : pos < LP_LINES ? 1 : 2; if (kind != lp.kind) { lcd_rect(0, 0, 240, 240, UI_BG); lp.kind = kind; } /* no rows left from another page */ l[0] = 0; - if (pos == 0u) b_str(l, "PIN CHECK"); - else if (pos < LP_LINES) { b_str(l, "ROLES "); b_num(l, pos); b_str(l, " OF 6"); } + if (pos == 0u) b_str(l, "POWER AS FOUND"); + else if (pos == 1u) b_str(l, "STOCK POWER"); + else if (pos < LP_LINES) { b_str(l, "ROLES "); b_num(l, pos - LP_ROLES + 1u); b_str(l, " OF 6"); } else { b_str(l, "LED LINE "); b_num(l, pos - LP_LINES + 1u); b_str(l, " OF "); b_num(l, lp.n); } LP_TEXT_HOOK(l, &FT_MD, 200u); ft_box(20, 6, 200, l, &FT_MD, UI_ACC, UI_BG, 1); lp_dot(); - if (pos == 0u) { - for (int i = 0; i < 3; i++) { - int good = lp.chk[i][0] == CK_OK && lp.chk[i][1] == CK_OK; - l[0] = 0; - b_str(l, "PC"); b_num(l, LP_CHK[i]); b_str(l, ": "); - b_str(l, CK_DRIVE[lp.chk[i][0]]); b_str(l, ", "); b_str(l, CK_PULL[lp.chk[i][1]]); - lp_text(40u + (uint32_t)i * 20u, l, &FT_SM, good ? UI_OK : RED); - } - lp_text(104, "ON A 595 INPUT: DRIVES OK, FREE", &FT_SM, UI_DIM); - lp_text(124, "", &FT_SM, UI_TXT); - lp_text(144, "KNOB: NEXT TEST", &FT_SM, UI_DIM); + if (pos < LP_ROLES) { + l[0] = 0; b_str(l, "FOUND "); lp_volts(l, lp.con5_found); + lp_text(36, l, &FT_SM, lp.con5_found == LP_CON5_STOCK ? UI_OK : UI_TXT); + lp_text(56, (op_pwr.rst & 1u) ? "(AFTER A POWER-ON)" : "(KEPT FROM BEFORE A RESTART)", &FT_SM, UI_DIM); + l[0] = 0; b_str(l, "NOW "); lp_volts(l, fm1_p33_read(LP_CON5)); + lp_text(76, l, &FT_SM, lp.stock ? UI_OK : UI_TXT); + lp_text(104, pos == 0u ? "AS FOUND. ALL 8 BITS: FF / 00" : "SET AS STOCK. ALL 8 BITS: FF / 00", + &FT_SM, UI_ACC); + lp_text(124, "ANY LIGHT? CAB, DLY OR REV: MARK", &FT_SM, UI_TXT); + lp_text(144, pos == 0u ? "KNOB: SET THE STOCK POWER" : "KNOB: NEXT TEST", &FT_SM, UI_DIM); } else if (pos < LP_LINES) { - const uint8_t *r = LP_ROLE[pos - 1u]; + const uint8_t *r = LP_ROLE[pos - LP_ROLES]; for (int i = 0; i < 3; i++) { l[0] = 0; b_str(l, ROLE_NAME[i]); b_str(l, " PC"); b_num(l, r[i]); lp_text(36u + (uint32_t)i * 20u, l, &FT_SM, UI_TXT); } - lp_text(104, pos == 1u ? "ISOD'S READING. ALL 8 BITS: FF / 00" : "SWAPPED. ALL 8 BITS: FF / 00", &FT_SM, UI_ACC); + lp_text(104, pos == LP_ROLES ? "ISOD'S READING. ALL 8 BITS: FF / 00" : "SWAPPED. ALL 8 BITS: FF / 00", + &FT_SM, UI_ACC); lp_text(124, "ANY LIGHT? CAB, DLY OR REV: MARK", &FT_SM, UI_TXT); lp_text(144, "KNOB: NEXT TEST", &FT_SM, UI_DIM); } else { @@ -216,11 +211,13 @@ static void lp_draw(void) char m[2][44]; uint32_t k = 0; m[0][0] = m[1][0] = 0; - for (uint32_t j = 0; j < 6u + 64u; j++) { + for (uint32_t j = 0; j < LP_LINES + 64u; j++) { char nm[6]; nm[0] = 0; - if (j < 6u) { if (!(lp.mark_role >> j & 1u)) continue; b_str(nm, "R"); b_num(nm, j + 1u); } - else { if (!lp_has(lp.mark, j - 5u)) continue; lp_name(nm, j - 5u); } + if (j < LP_LINES) { + if (!(lp.mark_pos >> j & 1u)) continue; + if (j < LP_ROLES) { b_str(nm, "P"); b_num(nm, j + 1u); } else { b_str(nm, "R"); b_num(nm, j - LP_ROLES + 1u); } + } else { if (!lp_has(lp.mark, j - LP_LINES + 1u)) continue; lp_name(nm, j - LP_LINES + 1u); } if (k < 12u) { char *t = m[k / 6u]; if (t[0]) b_str(t, " "); b_str(t, nm); } k++; } @@ -235,7 +232,6 @@ static void lp_draw(void) static void lp_show(void) /* the outputs for this position and phase */ { - if (lp.pos == 0u) return; if (lp.pos < LP_LINES) { lp_595((lp.phase & 1u) ? 0x00u : 0xFFu); return; } lp_level(lp.id, lp.phase & 1u); lp_595((lp.phase & 2u) ? 0xFFu : 0x00u); @@ -245,8 +241,10 @@ static void lp_enter(uint32_t now) /* leave th if (lp.id) { lp_give(lp.id); lp.id = 0; } lp.phase = 0; lp.t = now; - if (lp.pos == 0u) lp_check(); - else if (lp.pos < LP_LINES) lp_roles((int)lp.pos - 1); + if (lp.pos < LP_ROLES) { + if (lp.pos == 1u && !lp.stock) { fm1_p33_write(LP_CON5, LP_CON5_STOCK); lp.stock = 1; } /* once */ + if (lp.role != 0) lp_roles(0); + } else if (lp.pos < LP_LINES) lp_roles((int)(lp.pos - LP_ROLES)); else { if (lp.role != 0) lp_roles(0); lp.id = lp.ids[lp.pos - LP_LINES]; @@ -264,17 +262,19 @@ static void lp_init(void) if (!(LP_NEVER[p] & (1u << b))) lp.ids[lp.n++] = (uint8_t)(p * 16u + b + 1u); lp.pos = lp.id = 0; lp.acc = 0; - lp.key = KEY_NONE; lp.enc = (uint8_t)(PC_IN & ENC_MASK); - lp.mark_role = 0; + lp.mark_pos = 0; for (int k = 0; k < 4; k++) lp.mark[k] = 0; lp.k.stab = lp.k.last = lp.k.cnt = 0; + lp.stock = 0; + lp.con5_found = fm1_p33_read(LP_CON5); /* before anything here changes it */ guard.pending = 0; /* a reset here is not a failed boot */ guard.failed = 0; /* (no UBOOT, no safe mode screen) */ op_diag.stage = ST_OK; lp.kind = -1; + lp_roles(0); lp.t_keys = ms_now(); - lp_enter(lp.t_keys); /* the pin check */ + lp_enter(lp.t_keys); /* power as found */ } static void lp_step(uint32_t now) /* one pass of the loop */ @@ -296,15 +296,16 @@ static void lp_step(uint32_t now) /* one pass } } if (now - lp.t_keys >= 10u) { /* the analog keys (CAB, DLY, REV), every 10 ms: any of them marks */ + int32_t v = adc_read2(ADC_CH_KEYS); + int down = keys_decode(v) != KEY_NONE || (v >= 0 && v < LP_KEY_PRESSED); lp.t_keys = now; - lp.key = keys_decode(adc_read2(ADC_CH_KEYS)); - if (dbn_press(&lp.k, lp.key != KEY_NONE) && lp.pos) { - if (lp.pos < LP_LINES) lp.mark_role ^= 1u << (lp.pos - 1u); + if (dbn_press(&lp.k, down)) { + if (lp.pos < LP_LINES) lp.mark_pos ^= 1u << lp.pos; else lp.mark[(lp.id - 1u) >> 4] ^= 1u << ((lp.id - 1u) & 15u); lp.dirty = 1; } } - if (lp.pos && now - lp.t >= LP_STEP_MS) { + if (now - lp.t >= LP_STEP_MS) { lp.t = now; lp.phase = (lp.phase + 1u) & 3u; lp_show(); diff --git a/firmware/openpocket.c b/firmware/openpocket.c index c4bb061..3994a96 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 1 /* 1 = LED hunt (issue #8, led_probe.h): pin check, 595 roles, LED lines; no sound */ +#define LED_PROBE 1 /* 1 = LED hunt (issue #8, led_probe.h): stock I/O power, 595 roles, LED lines; no sound */ #define PA_PD (*(volatile uint32_t *)0x50014u) /* ---- Controls mapped on the real pedal (diagnostic) ---- diff --git a/tests/lpsim.c b/tests/lpsim.c index 2dc982c..aa3c480 100644 --- a/tests/lpsim.c +++ b/tests/lpsim.c @@ -1,14 +1,15 @@ /* SPDX-License-Identifier: GPL-3.0-only * OpenPocket — the LED hunt (firmware/led_probe.h and the 595 driver px_leds.h, issue #8) on the PC, against fake - * GPIO registers (pads with pulls and outside holds) and a model of the 74HC595 wired as isod89 read it: + * GPIO registers (pads with pulls), a fake always-on power domain (P33) and a model of the 74HC595 wired as isod89 + * read it: * 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 drive (screen, knob, I2S, amp enable, MASTER, analog keys) * keep every register; besides PC5, PC6 and PC8 at most one pin differs from the start, the LED line on screen. - * The pin check tells a free pin (DRIVES OK, FREE) from one held high or low, and leaves the three pins outputs - * without pulls, their DIE as found. With isod89's roles every shift clock edge and latch has a wait before it, the - * latch is low while shifting, and the bytes arrive MSB first: ROLES 1 blinks FF / 00 at 2 Hz; on an LED line the - * line (1 / 0) and the 595 (FF / 00) go through all four mixes each second. All 47 lines in turn, each alone and - * given back exactly; the knob wraps both ways; the analog keys mark and unmark (not on the pin check); USB is + * POWER AS FOUND reads CON5 and writes nothing; STOCK POWER writes CON5 = 0x74, once, and no other power register; + * both blink all 8 bits. With isod89's roles every shift clock edge and latch has a wait before it, the latch is low + * while shifting, and the bytes arrive MSB first: all 8 bits blink FF / 00 at 2 Hz; on an LED line the line (1 / 0) + * and the 595 (FF / 00) go through all four mixes each second. All 47 lines in turn, each alone and given back + * exactly; the knob wraps both ways; the analog keys mark and unmark, also at a level the supply moved; USB is * serviced; the boot is not counted as failed; every line of text fits. The driver alone too: leds_init from pins * with pulls leaves outputs without them and 0 shifted out, leds_write puts each byte on the outputs (MSB first). * Writes PPM screenshots to OUTDIR. */ @@ -40,25 +41,38 @@ static void lcd_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint16_t c) static void fm1_wdt_feed(void) { } /* ---- the pedal: gpio[port p (0 A, 1 B, 2 C, 7 H)][register r / 4]; a pad is its OUT bit when an output, else its - * pull (no pull reads 0); ext_hi / ext_lo hold port C pads from outside, stronger than the pin's own driver */ + * pull (no pull reads 0) */ enum { R_OUT = 0, R_IN = 1, R_DIR = 2, R_DIE = 3, R_PU = 4, R_PD = 5 }; static uint32_t gpio[8][16]; -static uint32_t pc_in, ext_hi, ext_lo; +static uint32_t pc_in; static uint32_t pc_pad(void) { uint32_t d = gpio[2][R_DIR]; - return (((gpio[2][R_OUT] & ~d) | (gpio[2][R_PU] & d)) | ext_hi) & ~ext_lo; + return (gpio[2][R_OUT] & ~d) | (gpio[2][R_PU] & d); } -static uint32_t sim_pc_in(void) { return (pc_in & 3u) | (pc_pad() & gpio[2][R_DIE] & ~3u); } +static uint32_t sim_pc_in(void) { return pc_in & 3u; } /* the knob */ #define LP_REG(port, r) (gpio[(port)][(r) / 4u]) #define PC_IN sim_pc_in() #define ENC_MASK 0x3u enum { KEY_NONE = 0, KEY_CAB, KEY_DLY, KEY_REV }; #define ADC_CH_KEYS 9u -static int sim_key; +static int32_t sim_adc = 1023; /* the keypad's reading: 1023 idle, CAB 617, DLY 706, REV 788 */ static unsigned long adc_calls; -static int32_t adc_read2(uint32_t ch) { if (ch == ADC_CH_KEYS) adc_calls++; return sim_key; } -static int keys_decode(int32_t v) { return (int)v; } +static int32_t adc_read2(uint32_t ch) { if (ch == ADC_CH_KEYS) adc_calls++; return sim_adc; } +static int keys_decode(int32_t v) /* as openpocket.c */ +{ + static const int32_t REF[3] = { 617, 706, 788 }; + int best = -1; int32_t bd = 100000; + if (v < 0) return KEY_NONE; + for (int i = 0; i < 3; i++) { int32_t e = v - REF[i]; if (e < 0) e = -e; if (e < bd) { bd = e; best = i; } } + return (bd <= 42) ? best + 1 : KEY_NONE; +} +/* the always-on power domain: CON5 as the boot leaves it; every write logged */ +static uint8_t p33[256]; +static int p33_writes, p33_last_a, p33_last_v; +static uint8_t fm1_p33_read(uint32_t a) { return p33[a & 255u]; } +static void fm1_p33_write(uint32_t a, uint8_t v) { p33[a & 255u] = v; p33_writes++; p33_last_a = (int)a; p33_last_v = v; } +static struct { uint8_t rst; } op_pwr; 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 */ @@ -94,15 +108,13 @@ static void b_str(char *d, const char *s) { while (*d) d++; while (*s) *d++ = *s #define M_595 (M_DS | M_SH | M_ST) static uint32_t lv_prev; static uint8_t sr, q; -static unsigned long waits, w_sh, latches, settles; +static unsigned long waits, w_sh, latches; static int e_wait, e_latch, e_dir; static void sim_wait(void) { waits++; } -static void sim_settle(void) { settles++; waits++; } /* 50 us: time passes for the 595 too */ static void sim_edge(void); #define LEDS_PC(r) (gpio[2][(r) / 4u]) #define LEDS_WAIT() sim_wait() #define LEDS_EDGE() sim_edge() -#define LP_SETTLE() sim_settle() static int failed, clipped; #define LP_TEXT_HOOK(s, f, w) do { \ @@ -146,7 +158,7 @@ static void reset_gpio(void) /* a distinctive start; PC5, PC6 gpio[2][R_DIR] &= ~(1u << 7); gpio[2][R_OUT] |= 1u << 7; gpio[2][R_DIR] |= M_595; gpio[2][R_PU] |= M_595; gpio[2][R_PD] &= ~M_595; gpio[2][R_DIE] &= ~M_595; memcpy(snap, gpio, sizeof snap); - pc_in = 0; sim_key = KEY_NONE; + pc_in = 0; sim_adc = 1023; lv_prev = pc_pad(); sr = 0x5Au; q = 0xA5u; /* a 595 holds garbage at power-on */ } @@ -195,7 +207,8 @@ static void turn(int dir) /* one detent: 0 1 3 2 0 is one ok(lp.pos == want, "the knob moves one test per detent"); } static void go(uint32_t pos) { for (uint32_t k = 0; k <= lp_npos() && lp.pos != pos; k++) turn(+1); ok(lp.pos == pos, "the knob reaches it"); } -static void press_key(int k) { sim_key = k; run(60); sim_key = KEY_NONE; run(60); } +static void press_key(int32_t v) { sim_adc = v; run(60); sim_adc = 1023; run(60); } +enum { CAB = 617, DLY = 706, REV = 788 }; static uint32_t pin(int k, int b) { return (uint32_t)(k * 16 + b + 1); } static uint32_t pos_of(uint32_t id) { for (uint32_t i = 0; i < lp.n; i++) if (lp.ids[i] == id) return LP_LINES + i; return 0; } static void shot(const char *dir, const char *name) @@ -214,14 +227,15 @@ static void shot(const char *dir, const char *name) } fclose(f); } -static void boot(void) +static void boot(uint8_t con5, uint8_t rst) { reset_gpio(); + memset(p33, 0, sizeof p33); p33[LP_CON5] = con5; p33_writes = 0; + op_pwr.rst = rst; guard.pending = 1; guard.failed = 1; op_diag.stage = 3; lp_init(); run(5); } -static int chk_is(int i, int drive, int pull) { return lp.chk[i][0] == drive && lp.chk[i][1] == pull; } int main(int argc, char **argv) { @@ -243,32 +257,34 @@ int main(int argc, char **argv) } ok(others_changed(0) == 0, "the driver touches only PC5, PC6, PC8"); - /* the pin check, on a board where something holds PC8 high and PC6 low */ - ext_hi = M_DS; ext_lo = M_ST; - boot(); - ok(chk_is(0, CK_OK, CK_OK), "PC5 free: DRIVES OK, FREE"); - ok(chk_is(1, CK_LO, CK_LO), "PC6 held low: STUCK AT 0, HELD LOW"); - ok(chk_is(2, CK_HI, CK_HI), "PC8 held high: STUCK AT 1, HELD HIGH"); - ok(settles == 12u, "every read waits for the pad"); - shot(dir, "pin_check_held"); - ext_hi = ext_lo = 0; - e_wait = e_latch = e_dir = 0; /* a held latch is not the driver's fault */ - - /* as on the board isod89 read */ - boot(); + /* after a power-on, the I/O supply at 3.0 V (VDDIOM 2) */ + boot(0x62u, 0x01u); ok(guard.pending == 0u && guard.failed == 0u && op_diag.stage == ST_OK, "not counted as a failed boot"); - ok(lp.n == 47u && lp_npos() == 54u, "47 LED lines, 54 tests"); - ok(chk_is(0, CK_OK, CK_OK) && chk_is(1, CK_OK, CK_OK) && chk_is(2, CK_OK, CK_OK), "free pins: DRIVES OK, FREE"); - ok(!(gpio[2][R_DIR] & M_595) && !(gpio[2][R_PU] & M_595) && !(gpio[2][R_PD] & M_595), "then outputs, no pulls"); - ok((gpio[2][R_DIE] & M_595) == (snap[2][R_DIE] & M_595), "their DIE as found"); - ok(q == 0u && lp.role == 0, "the 595 at 0 with isod's roles after the check"); - ok(others_changed(0) == 0, "nothing else touched"); + ok(lp.n == 47u && lp_npos() == 55u, "47 LED lines, 55 tests"); + ok(lp.pos == 0u && lp.con5_found == 0x62u && p33_writes == 0, "POWER AS FOUND: CON5 read, nothing written"); + ok(!(gpio[2][R_DIR] & M_595) && !(gpio[2][R_PU] & M_595) && !(gpio[2][R_PD] & M_595), "the 595 pins outputs, no pulls"); + ok(lp.role == 0 && q == 0xFFu, "isod's roles, all 8 bits lit at once"); + { + int flips = 0; uint8_t last = q; + for (int t = 0; t < 2000; t++) { run(1); if (q != last) { flips++; last = q; } } + ok(flips >= 7 && flips <= 9 && (q == 0u || q == 0xFFu), "FF / 00 at 2 Hz"); + } ok(usb_calls > 0u, "USB serviced"); - shot(dir, "pin_check"); + shot(dir, "power_found"); + + /* STOCK POWER: CON5 0x74, once */ + turn(+1); + ok(lp.pos == 1u && p33_writes == 1 && p33_last_a == LP_CON5 && p33_last_v == 0x74 && p33[LP_CON5] == 0x74u, + "STOCK POWER: CON5 = 0x74, one write"); + ok(q == 0xFFu, "all 8 bits lit at once"); + run(1000); + shot(dir, "power_stock"); + turn(-1); turn(+1); + ok(p33_writes == 1, "set once only"); /* ROLES 1: all 8 bits at 2 Hz */ turn(+1); - ok(lp.pos == 1u && lp.role == 0 && q == 0xFFu, "ROLES 1: isod's roles, lit at once"); + ok(lp.pos == 2u && lp.role == 0 && q == 0xFFu, "ROLES 1: isod's roles, lit at once"); { int flips = 0; uint8_t last = q; for (int t = 0; t < 2000; t++) { run(1); if (q != last) { flips++; last = q; } if (q && q != 0xFFu) break; } @@ -276,7 +292,6 @@ int main(int argc, char **argv) } shot(dir, "roles1"); for (int r = 2; r <= 6; r++) { turn(+1); ok(lp.role == r - 1, "the next roles"); run(600); } - shot(dir, "roles6"); /* the LED lines: each alone, all four mixes, given back */ for (uint32_t i = 0; i < lp.n; i++) { @@ -289,29 +304,37 @@ int main(int argc, char **argv) if (lp.id == pin(3, 6)) shot(dir, "line_ph6"); } turn(+1); - ok(lp.pos == 0u && lp.id == 0u && others_changed(0) == 0, "past the last line: the pin check, all given back"); + ok(lp.pos == 0u && lp.id == 0u && others_changed(0) == 0, "past the last line: power, all given back"); turn(-1); - ok(lp.id == pin(3, 15), "back from the pin check: the last line (PH15)"); + ok(lp.id == pin(3, 15), "back from power: the last line (PH15)"); turn(+1); + ok(p33_writes == 1, "still one power write after a whole round"); - /* marks: the analog keys, not on the pin check */ - press_key(KEY_REV); - ok(lp.mark_role == 0u && !lp.mark[0] && !lp.mark[1] && !lp.mark[2] && !lp.mark[3], "no mark on the pin check"); - go(3); - press_key(KEY_DLY); - ok(lp.mark_role == 4u, "DLY marks ROLES 3"); - press_key(KEY_DLY); - ok(lp.mark_role == 0u, "and unmarks it"); - press_key(KEY_CAB); + /* marks: the analog keys, also at a level the supply moved */ + press_key(REV); + ok(lp.mark_pos == 1u, "REV marks POWER AS FOUND"); + press_key(REV); + ok(lp.mark_pos == 0u, "and unmarks it"); + turn(+1); + press_key(500); + ok(lp.mark_pos == 2u, "a key read at a moved level marks STOCK POWER"); + go(LP_ROLES + 2u); + press_key(DLY); + ok(lp.mark_pos == (2u | (1u << (LP_ROLES + 2u))), "DLY marks ROLES 3"); go(pos_of(pin(3, 6))); - press_key(KEY_CAB); + press_key(CAB); ok(lp_has(lp.mark, pin(3, 6)), "CAB marks PH6"); go(pos_of(pin(0, 1))); - press_key(KEY_REV); - ok(lp_has(lp.mark, pin(0, 1)) && lp.mark_role == 4u, "REV marks PA1, the others kept"); + press_key(REV); + ok(lp_has(lp.mark, pin(0, 1)), "REV marks PA1, the others kept"); shot(dir, "marks"); ok(adc_calls >= sim_ms / 10u - 2u && adc_calls <= sim_ms / 10u + 2u, "the keys every 10 ms"); + /* after a restart, with the stock level kept by the always-on domain */ + boot(0x74u, 0x40u); + ok(lp.con5_found == 0x74u && p33_writes == 0, "a restart: the stock level found, nothing written"); + shot(dir, "power_kept"); + ok(e_wait == 0, "isod's roles: a wait before every shift clock edge and latch"); ok(e_latch == 0, "isod's roles: the latch low while shifting"); ok(e_dir == 0, "isod's roles: DS driven while shifting"); diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 1dbb086..5709a40 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -4,7 +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) -# - LED hunt (firmware/led_probe.h, px_leds.h): pin check, the 6 roles, 47 LED lines one at a time, the 595 timing +# - LED hunt (firmware/led_probe.h, px_leds.h): stock I/O power once, the 6 roles, 47 LED lines, the 595 timing # - 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) From 9a746c6e521bcda516f0c13bf1fc0892778f4851 Mon Sep 17 00:00:00 2001 From: Roberto Antonacci <48053274+steponme82@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:15:57 +0100 Subject: [PATCH 6/9] LED_BLINK: the normal firmware with the 595 blinking (a test switch) isod89/openpocket#8. The LED hunt lit nothing with the stock I/O supply either: the pedal starts at 3.5 V (CON5 level 7), 3.2 V (stock's 0x74) changed nothing, and neither did the six roles or the 47 LED lines at it. Every probe so far ran without the normal start: no I2S (MCLK out on PA4), no amp enable (PA9, never driven by any probe), no audio interrupts. If the lights share a supply or an enable with that side, they were off in every test. LED_BLINK 1 (off by default) builds the normal firmware, sound and all, plus px_leds.h: leds_init after audio_start, then all 8 bits FF / 00 at 2 Hz from the main loop. The boot screen says LED TEST. Normal builds are unchanged (px_leds.h is not included). Co-Authored-By: Claude Opus 5.5 --- firmware/openpocket.c | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/firmware/openpocket.c b/firmware/openpocket.c index 3994a96..527e7fe 100644 --- a/firmware/openpocket.c +++ b/firmware/openpocket.c @@ -306,7 +306,9 @@ 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 1 /* 1 = LED hunt (issue #8, led_probe.h): stock I/O power, 595 roles, LED lines; no sound */ +#define LED_PROBE 0 /* 1 = LED hunt (issue #8, led_probe.h): stock I/O power, 595 roles, LED lines; no sound */ +#define LED_BLINK 0 /* 1 = test: the normal firmware with sound, plus the lights' 595 (px_leds.h) blinking + * all 8 bits at 2 Hz (issue #8: do the lights need something the normal start sets up?) */ #define PA_PD (*(volatile uint32_t *)0x50014u) /* ---- Controls mapped on the real pedal (diagnostic) ---- @@ -551,6 +553,12 @@ static uint32_t ed_flags(void) { return (a_bypass ? 1u : 0u) | (a_c1 ? 2u : 0u); #endif #include "led_probe.h" /* the LED hunt (issue #8) */ #endif +#if LED_BLINK +#if LED_PROBE || AUDIO_PROBE +#error "LED_BLINK is the normal build plus the 595 blinking" +#endif +#include "px_leds.h" /* the button lights' 74HC595 (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) @@ -593,6 +601,9 @@ static void app_main(void) b_str(l, "RST "); rst_names(l, op_pwr.rst); b_str(l, " PINR "); hex8(h, op_pwr.pinr); b_str(l, h + 6); ft_box(0, 170, 240, l, &FT_SM, UI_DIM, UI_BG, 1); } +#endif +#if LED_BLINK + ft_box(0, 196, 240, "LED TEST: ALL 8 LIGHT BITS BLINK", &FT_SM, UI_ACC, UI_BG, 1); #endif for (int i = 0; i < 6; i++) { fm1_wdt_feed(); fm1_delay_ms(100); } /* --- report of the previous boot, if it did not finish (or crashed) --- */ @@ -661,6 +672,9 @@ static void app_main(void) px_usb_start(); /* USB-MIDI device + update entry (web installer) */ px_store_boot(); /* the saved presets; start on the last one saved */ audio_start(); /* guitar -> DSP engine -> output, amp enabled */ +#if LED_BLINK + leds_init(); /* PC5 / PC6 / PC8 outputs, 0 shifted out (as stock) */ +#endif op_stage(ST_UI, "SCREEN..."); lcd_rect(0, 0, 240, 240, UI_BG); ui_invalidate(); @@ -674,6 +688,12 @@ static void app_main(void) #if !AUDIO_PROBE px_usb_service(now); /* web installer: handshake, update, UBOOT soft key */ audio_poll(now); /* the sound follows the UI (preset, settings, volume) */ +#if LED_BLINK + { /* all 8 bits FF / 00 at 2 Hz: whatever the polarity, every light on the 595 blinks */ + static uint32_t t_led; + if (now - t_led >= 250u) { t_led = now; leds_write(leds_byte ? 0x00u : 0xFFu); } + } +#endif if (ui_mode != M_TUNER) { /* DSP load at the bottom right: "C1 42%" (2nd core), "C0" or BYP */ static uint32_t t_cpu; if (now - t_cpu >= 500u) { From 4c71c852c9ca5088e36f78d85a50b717bea1085c Mon Sep 17 00:00:00 2001 From: Roberto Antonacci <48053274+steponme82@users.noreply.github.com> Date: Fri, 6 Nov 2026 19:16:14 +0100 Subject: [PATCH 7/9] TEST BUILD ONLY, do not merge: LED_BLINK 1 (normal firmware, the 595 blinking), LED_PROBE 0 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 527e7fe..ac60416 100644 --- a/firmware/openpocket.c +++ b/firmware/openpocket.c @@ -307,7 +307,7 @@ static void fm1_fault(const fm1_crash_t *c) #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 = LED hunt (issue #8, led_probe.h): stock I/O power, 595 roles, LED lines; no sound */ -#define LED_BLINK 0 /* 1 = test: the normal firmware with sound, plus the lights' 595 (px_leds.h) blinking +#define LED_BLINK 1 /* 1 = test: the normal firmware with sound, plus the lights' 595 (px_leds.h) blinking * all 8 bits at 2 Hz (issue #8: do the lights need something the normal start sets up?) */ #define PA_PD (*(volatile uint32_t *)0x50014u) From efea4f225c7211a479aae3325f34319a1f6cd938 Mon Sep 17 00:00:00 2001 From: Roberto Antonacci <48053274+steponme82@users.noreply.github.com> Date: Fri, 6 Nov 2026 20:49:55 +0100 Subject: [PATCH 8/9] Lights: PC7 is their enable (low = on); the LED map is back isod89/openpocket#8. Every probe lit nothing: the LED map, the LED hunt (role swaps, 47 extra lines, the stock I/O supply) and the normal firmware with the 595 blinking. Read with JieLi's objdump, BlackBox_016, _020 and _021 drive the lights the same way: the 595 on PC8 / PC5 / PC6 as isod89 read it, and PC7. The 595 init sets PC7 high with the three pins and loads the first byte; then PC7 goes low and stays low: in 016 / 020 as the first step of the screen init, in 021 right after that first byte (and 021 sets it high again on its way off). The stock screen runs with PC7 low, so it is not a screen line: it is the lights' enable, low = on, most likely the 595's OE. OpenPocket has always held it high, and so did every probe. px_leds.h: leds_init now loads the stock's all-off byte (FF on the wire: the stock sends its byte inverted and writes logical 0 at power-off) with PC7 high, then drives PC7 low. leds_enable(on) is the switch. LED_PROBE 1 builds the LED map again (git history), with the lights enabled: ALL 8 BITS, then BIT 0..7, each blinking, and the button table. The hunt is in git history. tests/lpsim.c models PC7 as the 595's output enable: PC7 goes low once, after the all-off byte is latched, and ends an output; the driver alone (from pins with pulls, PC7 an input), bytes MSB first, and the map's checks as before. 11 of 11 deliberate mistakes fail it (enable never set, set before the byte, inverted, PC7 left an input, wrong off byte, LSB first, a missing wait...). openpocket.c: the comments that called PC7 a screen line now say what it is. The normal build still holds it high (lights off) and is unchanged. Not run on a pedal with PC7 low yet. Co-Authored-By: Claude Opus 5.5 --- firmware/led_probe.h | 339 +++++++++++------------------------------- firmware/openpocket.c | 12 +- firmware/px_leds.h | 39 +++-- tests/lpsim.c | 334 +++++++++++++++++++---------------------- tests/run_tests.sh | 4 +- 5 files changed, 273 insertions(+), 455 deletions(-) diff --git a/firmware/led_probe.h b/firmware/led_probe.h index d0357c0..35779a5 100644 --- a/firmware/led_probe.h +++ b/firmware/led_probe.h @@ -1,280 +1,107 @@ /* SPDX-License-Identifier: GPL-3.0-only - * OpenPocket — the LED hunt (issue #8): why do the BlackBox's button lights stay dark? + * OpenPocket — the LED map (issue #8): which bit of the 74HC595 (px_leds.h) lights which button? * - * Built only with LED_PROBE 1 (openpocket.c). That build plays no sound. The LED map (git history) drove the - * 74HC595 as isod89 read it in the stock firmware (data PC8, clock PC5, latch PC6: px_leds.h) and nothing lit, not - * even all 8 bits blinking; the first LED hunt (git history) tried the six role swaps and 47 LED lines on ports A, B, - * C and H, and nothing lit either. Its pin check found the three pins free as inputs (a 595 input reads so; reading - * a pin back while it drives is not valid on this chip: the AC79 SDK's gpio_read). The six effect buttons light on - * the stock firmware, as isod89's six light bits say, so the missing piece is likely power. Since v33 OpenPocket - * leaves the supplies as the boot loader sets them; BlackBox_020's power init sets P3_ANA_CON5 to 0x74: VDDIOM 4, - * the I/O supply at 3.2 V (docs/AUDIO-SDK.md v30; the WL82 levels are 2.8 V + 0.1 V each, AC79 SDK - * power_interface.h). The 595 and the lights may need it. The knob steps through: - * - POWER AS FOUND: CON5 and the I/O voltage as the boot left them (after a power-on, or kept by the always-on - * domain from the firmware before a restart), all 8 bits blinking (FF and 00, 2 Hz, isod89's roles). - * - STOCK POWER: CON5 set to 0x74 once, as the stock firmware does (it stays until the pedal is switched off), all - * 8 bits blinking. Everything after runs at this voltage. - * - ROLES 1 to 6: the six ways to give data, clock and latch to PC5, PC6 and PC8 (1 is isod89's), all 8 bits - * blinking. - * - LED LINE 1 to 47: one more pin at a time blinks (1 and 0, 2 Hz) while the 595 (isod89's roles) blinks FF and - * 00 at 1 Hz, so every mix of the two comes once a second. Never driven: PA4..PA10 (I2S, amp enable, MASTER, - * the analog keys), PB1/5/6/7 and PC7 (the screen), PC0/1 (the knob), PC5/6/8 (the 595), ports D and E. - * CAB, DLY or REV (the analog keys, never driven here) marks the test on screen, again unmarks it; the list stays at - * the bottom for a photo. USB stays up, as in the normal build, so the web installer is the way back. + * Built only with LED_PROBE 1 (openpocket.c). That build plays no sound. The knob steps through ALL 8 BITS, then + * BIT 0 .. BIT 7; the bits on screen blink at 2 Hz (the byte alternates between them and 0, so the one light that + * changes is theirs, whether it is lit by a 1 or by a 0: the others may stay lit) and the square at the top right is + * lit while they are 1. + * - Pressing the button whose light blinks writes it into the table at the bottom (FX, MOD, AMP, CAB, DLY, REV, + * HOME, the knob's PUSH, either footswitch); the same button again clears it. A photo of the table is the map. + * - Driven: PC5, PC6 and PC8 (the 595's shift clock, latch and data) and PC7, the lights' enable, low (px_leds.h + * leds_init, as the stock firmware). The first LED map and the LED hunts (git history) lit nothing because PC7 + * stayed high: OpenPocket held it so as a screen line. + * - 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: PC_IN, - * ENC_MASK, keys_decode, adc_read2, ADC_CH_KEYS, KEY_NONE, QDEC, dbn_t / dbn_press, ms_now, fm1_delay_us, - * fm1_wdt_feed, fm1_p33_read / fm1_p33_write, op_pwr, px_usb_service, lcd_rect, ft_box / ft_width and the fonts - * (ui_text.h), b_str / b_num, guard, op_diag / ST_OK and the UI colours. */ + * Included by openpocket.c (LED_PROBE 1) and by tests/lpsim.c on the PC. The including file provides: PA_IN, PH_IN, + * PC_IN, ENC_MASK, PA_FX / PA_HOME / PA_FS_UP / PA_FS_DN, PH_PUSH / PH_MOD / PH_AMP, base_pa, base_ph, keys_decode, + * adc_read2, ADC_CH_KEYS, KEY_CAB / KEY_DLY / KEY_REV, QDEC, dbn_t / dbn_press, ms_now, fm1_delay_us, fm1_wdt_feed, + * px_usb_service, lcd_rect, ft_box / ft_width and the fonts (ui_text.h), b_str / b_num, guard, op_diag / ST_OK and + * the UI colours. */ #pragma once #include #include "px_leds.h" -#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_TEXT_HOOK /* tests/lpsim.c checks that every line fits its box */ #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 */ -#define LP_CON5 0x05u /* P3_ANA_CON5: [3:0] VDDIOM (2.8 V + 0.1 V a level), [5:4] VDDIOW, [7:6] VDDIO drive */ -#define LP_CON5_STOCK 0x74u /* BlackBox_020 / PocketX_016's power init: VDDIOM 4 (3.2 V), VDDIOW 3, drive 1 */ +#define LP_BLINK_MS 250u /* half a period: 2 Hz */ +#define LP_NPOS 9u /* ALL 8 BITS, BIT 0 .. BIT 7 */ -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, 0x01E3u, 0x0000u }; /* PA4..10 | PB1,5,6,7 | PC0,1,5..8 */ -static const uint8_t LP_ROLE[6][3] = { /* the PC bit of data, clock, latch */ - { 8u, 5u, 6u }, { 8u, 6u, 5u }, { 5u, 8u, 6u }, { 5u, 6u, 8u }, { 6u, 5u, 8u }, { 6u, 8u, 5u } }; -#define LP_STEP_MS 250u -#define LP_ROLES 2u /* knob: 0, 1 power, 2..7 roles, 8.. lines */ -#define LP_LINES 8u -#define LP_KEY_PRESSED 900 /* an analog key reading below this is a press, even if the supply moved its level */ +enum { LB_FX, LB_MOD, LB_AMP, LB_CAB, LB_DLY, LB_REV, LB_HOME, LB_PUSH, LB_FSR, LB_FSL, LB_N }; +static const char *const LP_BTN[LB_N] = { "FX", "MOD", "AMP", "CAB", "DLY", "REV", "HOME", "PUSH", "R FOOT", "L FOOT" }; static struct { - uint8_t ids[64]; /* the LED line candidates: id = port index * 16 + bit + 1 (1..64) */ - uint32_t n, pos, id; /* how many; the knob's position; the line driven now (0 = none) */ - uint32_t sv_out, sv_dir, sv_pu, sv_pd; /* that line's bits as they were */ - uint32_t t, phase, t_keys; - int role; /* the roles the 595 is driven with (LP_ROLE) */ - int dirty, kind; /* kind: the page on screen (0 power, 1 roles, 2 line; -1 none) */ + uint32_t pos; /* 0 = all 8 bits, 1..8 = bit pos - 1 */ + uint32_t t_blink, t_keys; + int hi, dirty, key; int32_t acc; - uint8_t enc, con5_found, stock; - uint32_t mark_pos, mark[4]; /* marks: positions 0..7 (bit pos), LED lines (by id) */ - dbn_t k; + uint8_t enc; + uint8_t map[8]; /* per bit: the button that lit, + 1 (0 = none yet) */ + dbn_t b[LB_N]; } lp; -static uint32_t lp_npos(void) { return LP_LINES + lp.n; } -static void lp_595(uint8_t v) -{ - const uint8_t *r = LP_ROLE[lp.role]; - leds_shift(v, 1u << r[0], 1u << r[1], 1u << r[2]); -} -static void lp_roles(int r) /* the 595 driven with roles r, 0 shifted out */ -{ - lp.role = r; - leds_setup(1u << LP_ROLE[r][0], 1u << LP_ROLE[r][1], 1u << LP_ROLE[r][2]); - lp_595(0u); -} +static uint8_t lp_mask(void) { return lp.pos ? (uint8_t)(1u << (lp.pos - 1u)) : 0xFFu; } +static void lp_show(void) { leds_write(lp.hi ? lp_mask() : 0u); } -static void lp_name(char *d, uint32_t id) /* 55 -> "PH6" */ -{ - uint32_t p = (id - 1u) >> 4, b = (id - 1u) & 15u; - while (*d) d++; - *d++ = 'P'; - *d++ = LP_PN[p]; - if (b >= 10u) { *d++ = '1'; *d++ = (char)('0' + b - 10u); } - else *d++ = (char)('0' + b); - *d = 0; -} -static const char *lp_role_of(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 uint32_t *set, uint32_t id) { return (int)((set[(id - 1u) >> 4] >> ((id - 1u) & 15u)) & 1u); } static void lp_hex2(char *d, uint32_t v) { static const char H[] = "0123456789ABCDEF"; while (*d) d++; d[0] = H[(v >> 4) & 15u]; d[1] = H[v & 15u]; d[2] = 0; } -static void lp_volts(char *d, uint32_t con5) /* "3.2 V (CON5 74)" */ -{ - uint32_t lv = con5 & 15u; - if (lv <= 7u) { - uint32_t dv = 28u + lv; /* tenths of a volt */ - b_num(d, dv / 10u); b_str(d, "."); b_num(d, dv % 10u); b_str(d, " V"); - } else { b_str(d, "LEVEL "); b_num(d, lv); } - b_str(d, " (CON5 "); lp_hex2(d, con5); b_str(d, ")"); -} - -static void lp_take(uint32_t id) /* the line 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_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, uint32_t 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 line 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; -} - -static void lp_text(uint32_t y, const char *s, const op_font *f, uint16_t col) -{ - LP_TEXT_HOOK(s, f, 232u); /* 4 px clear of each edge */ - ft_box(0, y, 240, s, f, col, UI_BG, 1); -} -static int lp_lit(void) /* the square: what blinks is at 1 */ +static void lp_text(uint32_t x, uint32_t y, uint32_t w, const char *s, const op_font *f, uint16_t col, int align) { - if (lp.pos < LP_LINES) return !(lp.phase & 1u); /* the 595 at FF */ - return (int)(lp.phase & 1u); /* the line at 1 */ + LP_TEXT_HOOK(s, f, w); + ft_box(x, y, w, s, f, col, UI_BG, align); } -static void lp_dot(void) { lcd_rect(222, 10, 14, 14, lp_lit() ? UI_ACC : UI_PANEL); } +static void lp_dot(void) { lcd_rect(222, 10, 14, 14, lp.hi ? UI_ACC : UI_PANEL); } static void lp_draw(void) { - static const char *const ROLE_NAME[3] = { "DATA", "CLOCK", "LATCH" }; - char l[48]; - uint32_t pos = lp.pos; - int kind = pos < LP_ROLES ? 0 : pos < LP_LINES ? 1 : 2; - if (kind != lp.kind) { lcd_rect(0, 0, 240, 240, UI_BG); lp.kind = kind; } /* no rows left from another page */ - l[0] = 0; - if (pos == 0u) b_str(l, "POWER AS FOUND"); - else if (pos == 1u) b_str(l, "STOCK POWER"); - else if (pos < LP_LINES) { b_str(l, "ROLES "); b_num(l, pos - LP_ROLES + 1u); b_str(l, " OF 6"); } - else { b_str(l, "LED LINE "); b_num(l, pos - LP_LINES + 1u); b_str(l, " OF "); b_num(l, lp.n); } - LP_TEXT_HOOK(l, &FT_MD, 200u); - ft_box(20, 6, 200, l, &FT_MD, UI_ACC, UI_BG, 1); + char l[44]; + uint32_t bit = lp.pos ? lp.pos - 1u : 0u; + int mapped = lp.pos && lp.map[bit]; + lp_text(20, 6, 200, "LED MAP", &FT_MD, UI_ACC, 1); lp_dot(); - if (pos < LP_ROLES) { - l[0] = 0; b_str(l, "FOUND "); lp_volts(l, lp.con5_found); - lp_text(36, l, &FT_SM, lp.con5_found == LP_CON5_STOCK ? UI_OK : UI_TXT); - lp_text(56, (op_pwr.rst & 1u) ? "(AFTER A POWER-ON)" : "(KEPT FROM BEFORE A RESTART)", &FT_SM, UI_DIM); - l[0] = 0; b_str(l, "NOW "); lp_volts(l, fm1_p33_read(LP_CON5)); - lp_text(76, l, &FT_SM, lp.stock ? UI_OK : UI_TXT); - lp_text(104, pos == 0u ? "AS FOUND. ALL 8 BITS: FF / 00" : "SET AS STOCK. ALL 8 BITS: FF / 00", - &FT_SM, UI_ACC); - lp_text(124, "ANY LIGHT? CAB, DLY OR REV: MARK", &FT_SM, UI_TXT); - lp_text(144, pos == 0u ? "KNOB: SET THE STOCK POWER" : "KNOB: NEXT TEST", &FT_SM, UI_DIM); - } else if (pos < LP_LINES) { - const uint8_t *r = LP_ROLE[pos - LP_ROLES]; - for (int i = 0; i < 3; i++) { - l[0] = 0; - b_str(l, ROLE_NAME[i]); b_str(l, " PC"); b_num(l, r[i]); - lp_text(36u + (uint32_t)i * 20u, l, &FT_SM, UI_TXT); - } - lp_text(104, pos == LP_ROLES ? "ISOD'S READING. ALL 8 BITS: FF / 00" : "SWAPPED. ALL 8 BITS: FF / 00", - &FT_SM, UI_ACC); - lp_text(124, "ANY LIGHT? CAB, DLY OR REV: MARK", &FT_SM, UI_TXT); - lp_text(144, "KNOB: NEXT TEST", &FT_SM, UI_DIM); - } else { - uint32_t id = lp.ids[pos - LP_LINES]; - const char *role = lp_role_of(id); - l[0] = 0; - lp_name(l, id); - lp_text(30, l, &FT_NM, UI_TXT); - l[0] = 0; - if (role) { b_str(l, role); b_str(l, ": DON'T PRESS IT"); } - lp_text(66, l, &FT_SM, UI_DIM); - lp_text(86, "LINE 1 / 0, AND THE 595 FF / 00", &FT_SM, UI_ACC); - lp_text(104, "ALL FOUR MIXES ONCE A SECOND", &FT_SM, UI_ACC); - lp_text(124, "ANY LIGHT? CAB, DLY OR REV: MARK", &FT_SM, UI_TXT); - lp_text(144, "KNOB: NEXT PIN", &FT_SM, UI_DIM); - } - { /* the marks, six a line */ - char m[2][44]; - uint32_t k = 0; - m[0][0] = m[1][0] = 0; - for (uint32_t j = 0; j < LP_LINES + 64u; j++) { - char nm[6]; - nm[0] = 0; - if (j < LP_LINES) { - if (!(lp.mark_pos >> j & 1u)) continue; - if (j < LP_ROLES) { b_str(nm, "P"); b_num(nm, j + 1u); } else { b_str(nm, "R"); b_num(nm, j - LP_ROLES + 1u); } - } else { if (!lp_has(lp.mark, j - LP_LINES + 1u)) continue; lp_name(nm, j - LP_LINES + 1u); } - if (k < 12u) { char *t = m[k / 6u]; if (t[0]) b_str(t, " "); b_str(t, nm); } - k++; - } + l[0] = 0; + if (lp.pos) { b_str(l, "BIT "); b_num(l, bit); } else b_str(l, "ALL 8 BITS"); + lp_text(0, 36, 240, l, &FT_NM, UI_TXT, 1); + l[0] = 0; + b_str(l, "BYTE "); lp_hex2(l, lp_mask()); b_str(l, " / 00, TWICE A SECOND"); + lp_text(0, 74, 240, l, &FT_SM, UI_ACC, 1); + l[0] = 0; + if (!lp.pos) b_str(l, "EVERY BUTTON LIGHT SHOULD BLINK"); + else if (mapped) { b_str(l, "BIT "); b_num(l, bit); b_str(l, " LIGHTS: "); b_str(l, LP_BTN[lp.map[bit] - 1u]); } + else b_str(l, "PRESS THE BUTTON THAT BLINKS"); + lp_text(0, 96, 240, l, &FT_SM, mapped ? UI_OK : UI_TXT, 1); + lp_text(0, 114, 240, !lp.pos ? "KNOB: ONE BIT AT A TIME" : mapped ? "SAME BUTTON AGAIN: CLEAR" : "KNOB: NEXT BIT", + &FT_SM, UI_DIM, 1); + for (uint32_t i = 0; i < 8u; i++) { /* the map: bits 0-3 left, 4-7 right */ + uint16_t col = (lp.pos && i == bit) ? UI_ACC : lp.map[i] ? UI_TXT : UI_DIM; l[0] = 0; - b_str(l, "MARKED: "); - if (k) b_num(l, k); else b_str(l, "NONE YET"); - lp_text(178, l, &FT_SM, UI_DIM); - lp_text(198, m[0], &FT_SM, UI_TXT); - lp_text(216, m[1], &FT_SM, UI_TXT); + b_str(l, "BIT "); b_num(l, i); b_str(l, ": "); b_str(l, lp.map[i] ? LP_BTN[lp.map[i] - 1u] : "--"); + lp_text(i < 4u ? 24u : 136u, 140u + (i & 3u) * 18u, 100u, l, &FT_SM, col, 0); } -} - -static void lp_show(void) /* the outputs for this position and phase */ -{ - if (lp.pos < LP_LINES) { lp_595((lp.phase & 1u) ? 0x00u : 0xFFu); return; } - lp_level(lp.id, lp.phase & 1u); - lp_595((lp.phase & 2u) ? 0xFFu : 0x00u); -} -static void lp_enter(uint32_t now) /* leave the old test, start the new one */ -{ - if (lp.id) { lp_give(lp.id); lp.id = 0; } - lp.phase = 0; - lp.t = now; - if (lp.pos < LP_ROLES) { - if (lp.pos == 1u && !lp.stock) { fm1_p33_write(LP_CON5, LP_CON5_STOCK); lp.stock = 1; } /* once */ - if (lp.role != 0) lp_roles(0); - } else if (lp.pos < LP_LINES) lp_roles((int)(lp.pos - LP_ROLES)); - else { - if (lp.role != 0) lp_roles(0); - lp.id = lp.ids[lp.pos - LP_LINES]; - lp_take(lp.id); - } - lp_show(); - lp.dirty = 1; + lp_text(0, 218, 240, "SQUARE LIT = BITS AT 1", &FT_SM, UI_DIM, 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.pos = lp.id = 0; + lp.pos = 0; + lp.hi = 1; lp.acc = 0; + lp.key = KEY_NONE; lp.enc = (uint8_t)(PC_IN & ENC_MASK); - lp.mark_pos = 0; - for (int k = 0; k < 4; k++) lp.mark[k] = 0; - lp.k.stab = lp.k.last = lp.k.cnt = 0; - lp.stock = 0; - lp.con5_found = fm1_p33_read(LP_CON5); /* before anything here changes it */ + for (int i = 0; i < 8; i++) lp.map[i] = 0; + for (int i = 0; i < LB_N; i++) lp.b[i].stab = lp.b[i].last = lp.b[i].cnt = 0; guard.pending = 0; /* a reset here is not a failed boot */ guard.failed = 0; /* (no UBOOT, no safe mode screen) */ op_diag.stage = ST_OK; - lp.kind = -1; - lp_roles(0); - lp.t_keys = ms_now(); - lp_enter(lp.t_keys); /* power as found */ + leds_init(); + lp.t_blink = lp.t_keys = ms_now(); + lp_show(); + lcd_rect(0, 0, 240, 240, UI_BG); + lp.dirty = 1; } static void lp_step(uint32_t now) /* one pass of the loop */ @@ -289,25 +116,35 @@ static void lp_step(uint32_t now) /* one pass 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 n = lp_npos(); - lp.pos = dd > 0 ? (lp.pos + 1u) % n : (lp.pos + n - 1u) % n; - lp_enter(now); + lp.pos = dd > 0 ? (lp.pos + 1u) % LP_NPOS : (lp.pos + LP_NPOS - 1u) % LP_NPOS; + lp.hi = 1; + lp.t_blink = now; + lp_show(); + lp.dirty = 1; } } } - if (now - lp.t_keys >= 10u) { /* the analog keys (CAB, DLY, REV), every 10 ms: any of them marks */ - int32_t v = adc_read2(ADC_CH_KEYS); - int down = keys_decode(v) != KEY_NONE || (v >= 0 && v < LP_KEY_PRESSED); - lp.t_keys = now; - if (dbn_press(&lp.k, down)) { - if (lp.pos < LP_LINES) lp.mark_pos ^= 1u << lp.pos; - else lp.mark[(lp.id - 1u) >> 4] ^= 1u << ((lp.id - 1u) & 15u); + { /* the buttons (pressed = differs from the idle state); CAB / DLY / REV are the analog keypad, every 10 ms */ + uint32_t pa = (PA_IN & 0xFFFFu) ^ base_pa, ph = (PH_IN & 0xFFFFu) ^ base_ph; + int hit = -1, keys = now - lp.t_keys >= 10u; + int raw[LB_N] = { + (pa & PA_FX) != 0, (ph & PH_MOD) != 0, (ph & PH_AMP) != 0, 0, 0, 0, + (pa & PA_HOME) != 0, (ph & PH_PUSH) != 0, (pa & PA_FS_UP) != 0, (pa & PA_FS_DN) != 0 }; + if (keys) { lp.t_keys = now; lp.key = keys_decode(adc_read2(ADC_CH_KEYS)); } + raw[LB_CAB] = lp.key == KEY_CAB; raw[LB_DLY] = lp.key == KEY_DLY; raw[LB_REV] = lp.key == KEY_REV; + for (int i = 0; i < LB_N; i++) { + if ((i == LB_CAB || i == LB_DLY || i == LB_REV) && !keys) continue; + if (dbn_press(&lp.b[i], raw[i]) && hit < 0) hit = i; + } + if (hit >= 0 && lp.pos) { + uint8_t *m = &lp.map[lp.pos - 1u]; + *m = (*m == (uint8_t)(hit + 1)) ? 0u : (uint8_t)(hit + 1); lp.dirty = 1; } } - if (now - lp.t >= LP_STEP_MS) { - lp.t = now; - lp.phase = (lp.phase + 1u) & 3u; + if (now - lp.t_blink >= LP_BLINK_MS) { + lp.t_blink = now; + lp.hi = !lp.hi; lp_show(); lp_dot(); } diff --git a/firmware/openpocket.c b/firmware/openpocket.c index ac60416..e672ef4 100644 --- a/firmware/openpocket.c +++ b/firmware/openpocket.c @@ -1,7 +1,7 @@ /* SPDX-License-Identifier: GPL-3.0-only * OpenPocket X — Milestone 1 (boot + screen) — Donner Pocket X. * SCREEN (reverse-engineered from the stock firmware): SOFTWARE SPI on PORT B - * CS = PB1, D/C = PB5, CLK = PB6, DO = PB7 (+ PC7 driven, reset/backlight?) + * CS = PB1, D/C = PB5, CLK = PB6, DO = PB7 (PC7 is not the screen's: the button lights' enable, px_leds.h) * (the FM-1 used port C: that was the cause of the black screen.) * Init sequence + 40-line offset: taken verbatim from the Pocket X firmware. * Safety net: 2 failed boots -> UBOOT (re-flashable), + backup fm1-1.bin. @@ -144,7 +144,7 @@ static void lcd_init(void) PB_DIR &= ~(CS | DC | CLK | DO); /* outputs */ PC_DIR &= ~PC7; PB_OUT |= CS; PB_OUT &= ~CLK; /* CS high, CLK low */ - PC_OUT |= PC7; /* PC7 high (reset released / backlight?) */ + PC_OUT |= PC7; /* PC7 high: the button lights off (their enable, low = on: px_leds.h) */ #if LCD_HW lspi_init(); #endif @@ -306,8 +306,8 @@ 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 = LED hunt (issue #8, led_probe.h): stock I/O power, 595 roles, LED lines; no sound */ -#define LED_BLINK 1 /* 1 = test: the normal firmware with sound, plus the lights' 595 (px_leds.h) blinking +#define LED_PROBE 1 /* 1 = LED map (issue #8, led_probe.h): the knob picks a bit of the lights' 595, it blinks; no sound */ +#define LED_BLINK 0 /* 1 = test: the normal firmware with sound, plus the lights' 595 (px_leds.h) blinking * all 8 bits at 2 Hz (issue #8: do the lights need something the normal start sets up?) */ #define PA_PD (*(volatile uint32_t *)0x50014u) @@ -338,7 +338,7 @@ static void input_init(void) PH_DIE |= 0xFFFFu; PH_PU |= 0xFFFFu; PB_DIE |= 0xFF1Du; PB_PU |= 0xFF1Du; /* except PB1/5/6/7 (screen) */ #endif - PC_DIR &= ~PC7; PC_OUT |= PC7; /* PC7 drives the screen: always an output at 1 */ + PC_DIR &= ~PC7; PC_OUT |= PC7; /* PC7, the button lights' enable: high = off until px_leds.h turns them on */ } static void input_baseline(void) { @@ -551,7 +551,7 @@ static uint32_t ed_flags(void) { return (a_bypass ? 1u : 0u) | (a_c1 ? 2u : 0u); #if AUDIO_PROBE #error "LED_PROBE needs the normal build: USB (the web installer) is the way back" #endif -#include "led_probe.h" /* the LED hunt (issue #8) */ +#include "led_probe.h" /* the LED map (issue #8) */ #endif #if LED_BLINK #if LED_PROBE || AUDIO_PROBE diff --git a/firmware/px_leds.h b/firmware/px_leds.h index 4c29dc0..e8a1b93 100644 --- a/firmware/px_leds.h +++ b/firmware/px_leds.h @@ -1,17 +1,21 @@ /* SPDX-License-Identifier: GPL-3.0-only - * OpenPocket — the button lights (issue #8): one 74HC595 shift register, 8 outputs, on port C. + * OpenPocket — the button lights (issue #8): one 74HC595 shift register, 8 outputs, on port C, and PC7, the lights' + * enable. * - * Pins and timing as the stock firmware drives them (BlackBox_020; PocketX_016 has the same routine), read by - * isod89 in the stock code: - * DS (data) = PC8, SHCP (shift clock) = PC5, STCP (latch) = PC6. OE is not driven by any pin. PC7 (a screen - * line) is high at the same time: OpenPocket already keeps it so, and nothing here touches it. - * Stock's init: PC5, PC6, PC8 outputs with SHCP = 1, STCP = 0, DS = 0, then one byte shifted out. + * Pins and timing as the stock firmware drives them (BlackBox_016, _020 and _021 have the same routine, read with + * JieLi's objdump; isod89 read it first, and finds PocketX_016's the same): + * DS (data) = PC8, SHCP (shift clock) = PC5, STCP (latch) = PC6. + * PC7 = the lights' enable, low = on (the 595's OE, by how the stock code uses it). The 595 init sets PC7 high + * with the three pins and loads the first byte; then the lights go on with PC7 low: in 016 / 020 as the first + * step of the screen init, which leaves it low, in 021 right after that first byte. 021 also sets PC7 high again + * on its way off. OpenPocket used to keep PC7 high as a screen line: it is not one (the stock screen runs with it + * low), and with it high the lights stay dark whatever the 595 holds. Every probe before this one kept it high. * A byte, MSB first: STCP low; for each bit SHCP low, DS = the bit, wait, SHCP high, wait; then STCP high, whose * rising edge puts the byte on the outputs. Stock waits about 4 to 8 us at each step: LEDS_WAIT is 5 us. - * Stock's byte: bit 0 a blink signal (toggled by a timer), bits 1..6 six light states, bit 7 always 0. It rewrites - * the byte only when it changes, and writes 0 at power-off. - * On a BlackBox (2026-11-06) this lit nothing, not even all 8 bits blinking: the reading misses something (the - * roles, another line, an enable). LED_PROBE 1 builds the LED hunt (led_probe.h), which looks for it. + * Stock sends its byte inverted (DS = 1 for a 0 bit): its logical byte has bit 0 a blink signal (a timer), bits + * 1..6 the six effect lights, bit 7 always 0; at power-off it writes logical 0, FF on the wire. leds_write here + * takes the byte as it goes on the wire. + * Not run on a pedal with PC7 low yet. * * Every edge is a read-modify-write of PORTC OUT, so these are called from the main loop only (no interrupt writes * port C). tests/lpsim.c checks the bit order and the waits against a model of the 595. */ @@ -31,6 +35,8 @@ #define LEDS_SH (1u << 5) /* PC5: shift clock, DS is taken on its rising edge */ #define LEDS_ST (1u << 6) /* PC6: latch, the outputs change on its rising edge */ #define LEDS_PINS (LEDS_DS | LEDS_SH | LEDS_ST) +#define LEDS_EN (1u << 7) /* PC7: the lights' enable, low = on */ +#define LEDS_OFF 0xFFu /* the stock's power-off byte on the wire (logical 0, inverted) */ static uint8_t leds_byte; /* what the outputs show now */ @@ -69,8 +75,17 @@ static void leds_write(uint8_t v) leds_shift(v, LEDS_DS, LEDS_SH, LEDS_ST); leds_byte = v; } -static void leds_init(void) +static void leds_enable(uint32_t on) /* PC7 an output: low = lights on */ { + if (on) LEDS_PC(0x00) &= ~LEDS_EN; + else LEDS_PC(0x00) |= LEDS_EN; + LEDS_PC(0x08) &= ~LEDS_EN; + LEDS_EDGE(); +} +static void leds_init(void) /* as stock: load with the lights off, then on */ +{ + leds_enable(0u); leds_setup(LEDS_DS, LEDS_SH, LEDS_ST); - leds_write(0u); + leds_write(LEDS_OFF); + leds_enable(1u); } diff --git a/tests/lpsim.c b/tests/lpsim.c index aa3c480..204f39c 100644 --- a/tests/lpsim.c +++ b/tests/lpsim.c @@ -1,17 +1,15 @@ /* SPDX-License-Identifier: GPL-3.0-only - * OpenPocket — the LED hunt (firmware/led_probe.h and the 595 driver px_leds.h, issue #8) on the PC, against fake - * GPIO registers (pads with pulls), a fake always-on power domain (P33) and a model of the 74HC595 wired as isod89 - * read it: + * OpenPocket — the LED map (firmware/led_probe.h) and the lights' driver (px_leds.h, issue #8) on the PC, against + * fake GPIO registers and a model of the 74HC595 with its output enable on PC7: * 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 drive (screen, knob, I2S, amp enable, MASTER, analog keys) - * keep every register; besides PC5, PC6 and PC8 at most one pin differs from the start, the LED line on screen. - * POWER AS FOUND reads CON5 and writes nothing; STOCK POWER writes CON5 = 0x74, once, and no other power register; - * both blink all 8 bits. With isod89's roles every shift clock edge and latch has a wait before it, the latch is low - * while shifting, and the bytes arrive MSB first: all 8 bits blink FF / 00 at 2 Hz; on an LED line the line (1 / 0) - * and the 595 (FF / 00) go through all four mixes each second. All 47 lines in turn, each alone and given back - * exactly; the knob wraps both ways; the analog keys mark and unmark, also at a level the supply moved; USB is - * serviced; the boot is not counted as failed; every line of text fits. The driver alone too: leds_init from pins - * with pulls leaves outputs without them and 0 shifted out, leds_write puts each byte on the outputs (MSB first). + * Checks: the driver alone (leds_init from pins with pulls, PC7 an input: PC5/6/8 outputs without pulls, the all-off byte FF + * latched while PC7 is high, then PC7 low; leds_write puts each byte on the outputs, MSB first). The LED map: only + * PC5, PC6, PC8 and PC7 are ever changed (every other register bit of PA, PB, PC and PH as at start); PC7 ends an + * output, low; every shift clock edge and the latch's rise have a wait before them, DS is an output and the latch is + * low while shifting; the byte on the outputs is always the one the screen names, blinking at 2 Hz between the bits + * on screen and 0, lit right after a turn; the knob wraps both ways over the 9 positions; each button writes itself + * into the table, again clears it, another replaces it, nothing is written on ALL 8 BITS; the analog keys (CAB, DLY, + * REV) are read every 10 ms; USB is serviced; the boot is not counted as failed; every line of text fits its box. * Writes PPM screenshots to OUTDIR. */ #include #include @@ -40,39 +38,33 @@ static void lcd_rect(uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint16_t c) } static void fm1_wdt_feed(void) { } -/* ---- the pedal: gpio[port p (0 A, 1 B, 2 C, 7 H)][register r / 4]; a pad is its OUT bit when an output, else its - * pull (no pull reads 0) */ -enum { R_OUT = 0, R_IN = 1, R_DIR = 2, R_DIE = 3, R_PU = 4, R_PD = 5 }; +/* ---- the pedal around the LED map: gpio[port p (0 A, 1 B, 2 C, 7 H)][register r / 4] */ +enum { R_OUT = 0, R_DIR = 2, R_PU = 4, R_PD = 5 }; /* register index: OUT +0, DIR +8, PU +0x10, PD +0x14 */ static uint32_t gpio[8][16]; -static uint32_t pc_in; -static uint32_t pc_pad(void) +static uint32_t pa_in, ph_in, pc_in; /* the buttons as pressed, the knob */ +static uint32_t sim_pa_in(void) /* as the pad reads: an output pin reads back its own level */ { - uint32_t d = gpio[2][R_DIR]; - return (gpio[2][R_OUT] & ~d) | (gpio[2][R_PU] & d); + uint32_t dir = gpio[0][R_DIR]; + return (pa_in & dir) | (gpio[0][R_OUT] & ~dir); } -static uint32_t sim_pc_in(void) { return pc_in & 3u; } /* the knob */ -#define LP_REG(port, r) (gpio[(port)][(r) / 4u]) -#define PC_IN sim_pc_in() +#define PA_IN sim_pa_in() +#define PH_IN ph_in +#define PC_IN pc_in #define ENC_MASK 0x3u +#define PA_FX (1u << 0) /* as openpocket.c */ +#define PA_HOME (1u << 1) +#define PA_FS_UP (1u << 2) +#define PA_FS_DN (1u << 3) +#define PH_PUSH ((1u << 4) | (1u << 6)) +#define PH_MOD ((1u << 7) | (1u << 8)) +#define PH_AMP (1u << 9) +static uint32_t base_pa, base_ph; enum { KEY_NONE = 0, KEY_CAB, KEY_DLY, KEY_REV }; #define ADC_CH_KEYS 9u -static int32_t sim_adc = 1023; /* the keypad's reading: 1023 idle, CAB 617, DLY 706, REV 788 */ +static int sim_key; /* the analog key held down */ static unsigned long adc_calls; -static int32_t adc_read2(uint32_t ch) { if (ch == ADC_CH_KEYS) adc_calls++; return sim_adc; } -static int keys_decode(int32_t v) /* as openpocket.c */ -{ - static const int32_t REF[3] = { 617, 706, 788 }; - int best = -1; int32_t bd = 100000; - if (v < 0) return KEY_NONE; - for (int i = 0; i < 3; i++) { int32_t e = v - REF[i]; if (e < 0) e = -e; if (e < bd) { bd = e; best = i; } } - return (bd <= 42) ? best + 1 : KEY_NONE; -} -/* the always-on power domain: CON5 as the boot leaves it; every write logged */ -static uint8_t p33[256]; -static int p33_writes, p33_last_a, p33_last_v; -static uint8_t fm1_p33_read(uint32_t a) { return p33[a & 255u]; } -static void fm1_p33_write(uint32_t a, uint8_t v) { p33[a & 255u] = v; p33_writes++; p33_last_a = (int)a; p33_last_v = v; } -static struct { uint8_t rst; } op_pwr; +static int32_t adc_read2(uint32_t ch) { if (ch == ADC_CH_KEYS) adc_calls++; return sim_key; } +static int keys_decode(int32_t v) { return (int)v; } 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 */ @@ -99,19 +91,37 @@ static void b_num(char *d, uint32_t v) } static void b_str(char *d, const char *s) { while (*d) d++; while (*s) *d++ = *s++; *d = 0; } -/* ---- the 74HC595 as isod89 read the board (DS PC8, SHCP PC5, STCP PC6), from its data sheet: DS is shifted in on - * SHCP's rising edge (the first bit in ends at Q7), the outputs take the shift register on STCP's rising edge. The - * timing rules are counted only while the hunt drives it with those roles (lp.role 0). */ +/* ---- the 74HC595, from its data sheet: DS is shifted in on SHCP's rising edge (the first bit in ends at Q7), the + * outputs take the shift register on STCP's rising edge. A pin's level: its OUT bit when an output, else its pull. */ #define M_DS (1u << 8) #define M_SH (1u << 5) #define M_ST (1u << 6) -#define M_595 (M_DS | M_SH | M_ST) +#define M_EN (1u << 7) static uint32_t lv_prev; static uint8_t sr, q; static unsigned long waits, w_sh, latches; static int e_wait, e_latch, e_dir; +static uint32_t pc_level(void) { uint32_t d = gpio[2][R_DIR]; return (gpio[2][R_OUT] & ~d) | (gpio[2][R_PU] & d); } static void sim_wait(void) { waits++; } -static void sim_edge(void); +static int en_falls, en_rises; /* PC7, the output enable (low = on): its edges */ +static uint8_t q_at_enable = 0x5Au; /* the outputs' byte when PC7 went low */ +static unsigned long latches_at_enable; +static void sim_edge(void) +{ + uint32_t lv = pc_level(), rise = lv & ~lv_prev, fall = ~lv & lv_prev; + if (fall & M_EN) { en_falls++; q_at_enable = q; latches_at_enable = latches; } + if (rise & M_EN) en_rises++; + if (rise & M_SH) { + if (waits == w_sh) e_wait++; /* no wait while SHCP was low (or since the start) */ + if (lv & M_ST) e_latch++; /* shifting with the latch high: the outputs would ripple */ + if (gpio[2][R_DIR] & M_DS) e_dir++; /* DS not driven */ + sr = (uint8_t)((sr << 1) | ((lv & M_DS) ? 1u : 0u)); + w_sh = waits; + } + if (fall & M_SH) { if (waits == w_sh) e_wait++; w_sh = waits; } /* no wait while SHCP was high */ + if (rise & M_ST) { if (waits == w_sh) e_wait++; q = sr; latches++; } /* a wait after the last shift too */ + lv_prev = lv; +} #define LEDS_PC(r) (gpio[2][(r) / 4u]) #define LEDS_WAIT() sim_wait() #define LEDS_EDGE() sim_edge() @@ -123,25 +133,10 @@ static int failed, clipped; #include "ui_text.h" #include "led_probe.h" -static void sim_edge(void) -{ - uint32_t lv = pc_pad(), rise = lv & ~lv_prev, fall = ~lv & lv_prev; - int strict = lp.role == 0; - if (rise & M_SH) { - if (strict && waits == w_sh) e_wait++; /* no wait while SHCP was low (or since the start) */ - if (strict && (lv & M_ST)) e_latch++; /* shifting with the latch high: the outputs would ripple */ - if (strict && (gpio[2][R_DIR] & M_DS)) e_dir++; - sr = (uint8_t)((sr << 1) | ((lv & M_DS) ? 1u : 0u)); - w_sh = waits; - } - if (fall & M_SH) { if (strict && waits == w_sh) e_wait++; w_sh = waits; } - if (rise & M_ST) { if (strict && waits == w_sh) e_wait++; q = sr; latches++; } - lv_prev = lv; -} - /* ---- the checks */ static const int PORTS[4] = { 0, 1, 2, 7 }; static uint32_t snap[8][16]; +static int want, want2 = -1; /* the position the test expects (0 all, 1..8 bit want - 1); mid-turn either */ static void ok(int c, const char *what) { if (!c) { printf("FAIL: %s\n", what); failed++; } } static void reset_gpio(void) /* a distinctive start; PC5, PC6, PC8 inputs with pull-ups, as booted */ { @@ -150,67 +145,56 @@ static void reset_gpio(void) /* a distinctive start; PC5, PC6 int p = PORTS[k]; gpio[p][R_OUT] = 0x5A5Au ^ (uint32_t)(k * 0x1111); gpio[p][R_DIR] = 0xFFFFu & ~(0x0101u << k); - gpio[p][R_DIE] = 0xF0F0u >> k; - gpio[p][R_PD] = 0x3030u >> k; /* every pin has a pull, so a driven one always differs */ + gpio[p][R_PD] = 0x3030u >> k; gpio[p][R_PU] = 0xFFFFu & ~gpio[p][R_PD]; } gpio[1][R_DIR] &= ~0x00E2u; /* the screen pins PB1/5/6/7 and PC7: outputs, PC7 high */ gpio[2][R_DIR] &= ~(1u << 7); gpio[2][R_OUT] |= 1u << 7; - gpio[2][R_DIR] |= M_595; gpio[2][R_PU] |= M_595; gpio[2][R_PD] &= ~M_595; gpio[2][R_DIE] &= ~M_595; + gpio[2][R_DIR] |= M_DS | M_SH | M_ST; gpio[2][R_PU] |= M_DS | M_SH | M_ST; gpio[2][R_PD] &= ~(M_DS | M_SH | M_ST); + gpio[0][R_DIR] |= 0x000Fu; /* the buttons are inputs, as input_init leaves them */ + gpio[7][R_DIR] |= PH_PUSH | PH_MOD | PH_AMP; memcpy(snap, gpio, sizeof snap); - pc_in = 0; sim_adc = 1023; - lv_prev = pc_pad(); + pa_in = ph_in = pc_in = 0; sim_key = KEY_NONE; + base_pa = sim_pa_in() & 0xFFFFu; base_ph = ph_in; /* as input_baseline */ + lv_prev = pc_level(); sr = 0x5Au; q = 0xA5u; /* a 595 holds garbage at power-on */ } -/* written out again here, not taken from led_probe.h: PA4..PA10, PB1/5/6/7, PC0/1/7 */ -static const uint32_t FORBID[4] = { 0x7Fu << 4, (1u << 1) | (1u << 5) | (1u << 6) | (1u << 7), 3u | (1u << 7), 0u }; -static int others_changed(uint32_t *who) /* pins other than PC5/6/8 that differ from the start */ +static int only_595_pins(void) /* every register bit as at start, but PC5/6/8's OUT, DIR, PU, PD, PC7's OUT, DIR */ { - 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; - if (p == 2 && (m & M_595)) continue; - for (int r = 0; r < 16; r++) if ((gpio[p][r] ^ snap[p][r]) & m) d = 1; - if (d) { n++; if (who) *who = (uint32_t)(k * 16 + b + 1); } + for (int r = 0; r < 16; r++) { + uint32_t may = (PORTS[k] == 2 && (r == R_OUT || r == R_DIR || r == R_PU || r == R_PD)) ? (M_DS | M_SH | M_ST) : 0u; + if (PORTS[k] == 2 && (r == R_OUT || r == R_DIR)) may |= M_EN; /* PC7: the lights' enable */ + if ((gpio[PORTS[k]][r] ^ snap[PORTS[k]][r]) & ~may) return 0; } - return n; -} -static int forbidden_touched(void) -{ - for (int k = 0; k < 4; k++) - for (int r = 0; r < 16; r++) - if ((gpio[PORTS[k]][r] ^ snap[PORTS[k]][r]) & FORBID[k]) return 1; - return 0; + return 1; } -static int mix_seen[2][2]; /* on an LED line: [line level][595 at FF] */ +static uint8_t mask_of(int pos) { return pos ? (uint8_t)(1u << (pos - 1)) : 0xFFu; } 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 = others_changed(&who); - if (forbidden_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 LED line on screen changed"); return; } - if (lp.id && (q == 0x00u || q == 0xFFu)) { - uint32_t P = LP_PORT[(lp.id - 1u) >> 4], m = 1u << ((lp.id - 1u) & 15u); - mix_seen[(gpio[P][R_OUT] & m) ? 1 : 0][q == 0xFFu]++; + if (!only_595_pins()) { ok(0, "a pin other than PC5 / PC6 / PC8 was changed"); return; } + if (q != 0u && q != mask_of(want) && !(want2 >= 0 && q == mask_of(want2))) { + ok(0, "the 595 shows a byte other than the bits on screen"); 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 }; - uint32_t n = lp_npos(), want = dir > 0 ? (lp.pos + 1u) % n : (lp.pos + n - 1u) % n; + want2 = dir > 0 ? (want + 1) % 9 : (want + 8) % 9; for (int k = 0; k < 4; k++) { pc_in = (pc_in & ~3u) | (dir > 0 ? FWD[k] : BACK[k]); run(2); } - ok(lp.pos == want, "the knob moves one test per detent"); + want = want2; want2 = -1; + ok(lp.pos == (uint32_t)want, "the knob moves one position per detent"); + ok(q == mask_of(want), "lit right after a turn"); } -static void go(uint32_t pos) { for (uint32_t k = 0; k <= lp_npos() && lp.pos != pos; k++) turn(+1); ok(lp.pos == pos, "the knob reaches it"); } -static void press_key(int32_t v) { sim_adc = v; run(60); sim_adc = 1023; run(60); } -enum { CAB = 617, DLY = 706, REV = 788 }; -static uint32_t pin(int k, int b) { return (uint32_t)(k * 16 + b + 1); } -static uint32_t pos_of(uint32_t id) { for (uint32_t i = 0; i < lp.n; i++) if (lp.ids[i] == id) return LP_LINES + i; return 0; } +static void go(int pos) { for (int k = 0; k < 9 && want != pos; k++) turn(+1); } +static void press_pa(uint32_t m) { pa_in ^= m; run(20); pa_in ^= m; run(20); } +static void press_ph(uint32_t m) { ph_in ^= m; run(20); ph_in ^= m; run(20); } +static void press_key(int k) { sim_key = k; run(60); sim_key = KEY_NONE; run(60); } +static int map(int bit) { return lp.map[bit] ? lp.map[bit] - 1 : -1; } static void shot(const char *dir, const char *name) { static int n; @@ -227,15 +211,6 @@ static void shot(const char *dir, const char *name) } fclose(f); } -static void boot(uint8_t con5, uint8_t rst) -{ - reset_gpio(); - memset(p33, 0, sizeof p33); p33[LP_CON5] = con5; p33_writes = 0; - op_pwr.rst = rst; - guard.pending = 1; guard.failed = 1; op_diag.stage = 3; - lp_init(); - run(5); -} int main(int argc, char **argv) { @@ -244,101 +219,92 @@ int main(int argc, char **argv) /* the driver alone (px_leds.h): from inputs with pulls (PC8's a pull-down), then bytes MSB first */ reset_gpio(); gpio[2][R_PU] &= ~M_DS; gpio[2][R_PD] |= M_DS; + gpio[2][R_DIR] |= M_EN; gpio[2][R_PU] |= M_EN; /* PC7 untouched here: an input with its pull-up */ memcpy(snap, gpio, sizeof snap); - lv_prev = pc_pad(); + lv_prev = pc_level(); leds_init(); - ok(q == 0u && leds_byte == 0u && latches == 1u, "leds_init: 0 shifted out"); - ok(!(gpio[2][R_DIR] & M_595) && !(gpio[2][R_PU] & M_595) && !(gpio[2][R_PD] & M_595), "leds_init: outputs, no pulls"); + ok(q == 0xFFu && leds_byte == 0xFFu && latches == 1u, "leds_init: the all-off byte FF shifted out"); + ok(!(gpio[2][R_DIR] & (M_DS | M_SH | M_ST)) && !(gpio[2][R_PU] & (M_DS | M_SH | M_ST)) && + !(gpio[2][R_PD] & (M_DS | M_SH | M_ST)), "leds_init: PC5, PC6, PC8 outputs, no pulls"); + ok(en_falls == 1 && q_at_enable == 0xFFu && latches_at_enable == 1u && !(gpio[2][R_OUT] & M_EN) && + !(gpio[2][R_DIR] & M_EN), "leds_init: PC7 an output, low, once the byte is latched"); { static const uint8_t V[5] = { 0xA5u, 0x01u, 0x80u, 0x3Cu, 0x00u }; int good = 1; for (int i = 0; i < 5; i++) { leds_write(V[i]); if (q != V[i] || leds_byte != V[i]) good = 0; } ok(good, "leds_write: each byte on the outputs, MSB first"); } - ok(others_changed(0) == 0, "the driver touches only PC5, PC6, PC8"); + ok(only_595_pins(), "the driver touches only PC5, PC6, PC8 and PC7"); + latches = 0; en_falls = en_rises = 0; latches_at_enable = 0; q_at_enable = 0x5Au; - /* after a power-on, the I/O supply at 3.0 V (VDDIOM 2) */ - boot(0x62u, 0x01u); + /* the LED map */ + reset_gpio(); + guard.pending = 1; guard.failed = 1; op_diag.stage = 3; + lp_init(); ok(guard.pending == 0u && guard.failed == 0u && op_diag.stage == ST_OK, "not counted as a failed boot"); - ok(lp.n == 47u && lp_npos() == 55u, "47 LED lines, 55 tests"); - ok(lp.pos == 0u && lp.con5_found == 0x62u && p33_writes == 0, "POWER AS FOUND: CON5 read, nothing written"); - ok(!(gpio[2][R_DIR] & M_595) && !(gpio[2][R_PU] & M_595) && !(gpio[2][R_PD] & M_595), "the 595 pins outputs, no pulls"); - ok(lp.role == 0 && q == 0xFFu, "isod's roles, all 8 bits lit at once"); - { - int flips = 0; uint8_t last = q; - for (int t = 0; t < 2000; t++) { run(1); if (q != last) { flips++; last = q; } } - ok(flips >= 7 && flips <= 9 && (q == 0u || q == 0xFFu), "FF / 00 at 2 Hz"); - } + ok(!(gpio[2][R_DIR] & (M_DS | M_SH | M_ST)), "PC5, PC6, PC8 are outputs"); + ok(!(gpio[2][R_PU] & (M_DS | M_SH | M_ST)) && !(gpio[2][R_PD] & (M_DS | M_SH | M_ST)), "without pulls"); + ok(latches == 2u && q == 0xFFu, "the all-off byte FF at start (as stock), then ALL 8 BITS lit"); + ok(en_falls == 1 && en_rises == 0 && q_at_enable == 0xFFu && latches_at_enable == 1u, + "the lights on (PC7 low) once, after the all-off byte is latched"); + ok(!(gpio[2][R_DIR] & M_EN) && !(gpio[2][R_OUT] & M_EN), "PC7 an output, low"); + ok(only_595_pins(), "nothing else touched by the start"); + run(5); ok(usb_calls > 0u, "USB serviced"); - shot(dir, "power_found"); - - /* STOCK POWER: CON5 0x74, once */ - turn(+1); - ok(lp.pos == 1u && p33_writes == 1 && p33_last_a == LP_CON5 && p33_last_v == 0x74 && p33[LP_CON5] == 0x74u, - "STOCK POWER: CON5 = 0x74, one write"); - ok(q == 0xFFu, "all 8 bits lit at once"); - run(1000); - shot(dir, "power_stock"); - turn(-1); turn(+1); - ok(p33_writes == 1, "set once only"); + shot(dir, "all_bits"); - /* ROLES 1: all 8 bits at 2 Hz */ - turn(+1); - ok(lp.pos == 2u && lp.role == 0 && q == 0xFFu, "ROLES 1: isod's roles, lit at once"); - { + { /* 2 Hz: 8 changes in 2 s, always the bits on screen or 0 */ int flips = 0; uint8_t last = q; - for (int t = 0; t < 2000; t++) { run(1); if (q != last) { flips++; last = q; } if (q && q != 0xFFu) break; } - ok(flips >= 7 && flips <= 9 && (q == 0u || q == 0xFFu), "FF / 00 at 2 Hz"); + for (int t = 0; t < 2000; t++) { run(1); if (q != last) { flips++; last = q; } } + ok(flips >= 7 && flips <= 9, "2 Hz: 8 changes in 2 s"); } - shot(dir, "roles1"); - for (int r = 2; r <= 6; r++) { turn(+1); ok(lp.role == r - 1, "the next roles"); run(600); } - - /* the LED lines: each alone, all four mixes, given back */ - for (uint32_t i = 0; i < lp.n; i++) { - uint32_t who = 0; + { /* each bit alone, MSB first (an LSB-first driver would light bit 7 - n) */ + for (int b = 0; b < 8; b++) { turn(+1); run(300); } + ok(want == 8, "at BIT 7"); + turn(+1); + ok(lp.pos == 0u, "past BIT 7: ALL 8 BITS"); + turn(-1); + ok(lp.pos == 8u && q == 0x80u, "back from ALL: BIT 7"); turn(+1); - memset(mix_seen, 0, sizeof mix_seen); - run(1100); - ok(lp.role == 0 && lp.id == lp.ids[i] && others_changed(&who) == 1 && who == lp.id, "each line in turn, alone"); - ok(mix_seen[0][0] && mix_seen[0][1] && mix_seen[1][0] && mix_seen[1][1], "all four mixes within a second"); - if (lp.id == pin(3, 6)) shot(dir, "line_ph6"); } - turn(+1); - ok(lp.pos == 0u && lp.id == 0u && others_changed(0) == 0, "past the last line: power, all given back"); - turn(-1); - ok(lp.id == pin(3, 15), "back from power: the last line (PH15)"); - turn(+1); - ok(p33_writes == 1, "still one power write after a whole round"); - - /* marks: the analog keys, also at a level the supply moved */ - press_key(REV); - ok(lp.mark_pos == 1u, "REV marks POWER AS FOUND"); - press_key(REV); - ok(lp.mark_pos == 0u, "and unmarks it"); - turn(+1); - press_key(500); - ok(lp.mark_pos == 2u, "a key read at a moved level marks STOCK POWER"); - go(LP_ROLES + 2u); - press_key(DLY); - ok(lp.mark_pos == (2u | (1u << (LP_ROLES + 2u))), "DLY marks ROLES 3"); - go(pos_of(pin(3, 6))); - press_key(CAB); - ok(lp_has(lp.mark, pin(3, 6)), "CAB marks PH6"); - go(pos_of(pin(0, 1))); - press_key(REV); - ok(lp_has(lp.mark, pin(0, 1)), "REV marks PA1, the others kept"); - shot(dir, "marks"); - ok(adc_calls >= sim_ms / 10u - 2u && adc_calls <= sim_ms / 10u + 2u, "the keys every 10 ms"); - - /* after a restart, with the stock level kept by the always-on domain */ - boot(0x74u, 0x40u); - ok(lp.con5_found == 0x74u && p33_writes == 0, "a restart: the stock level found, nothing written"); - shot(dir, "power_kept"); - - ok(e_wait == 0, "isod's roles: a wait before every shift clock edge and latch"); - ok(e_latch == 0, "isod's roles: the latch low while shifting"); - ok(e_dir == 0, "isod's roles: DS driven while shifting"); + { /* the table: every button, again clears, another replaces, none on ALL */ + unsigned long a0 = adc_calls; uint32_t t0 = sim_ms; + press_key(KEY_DLY); + ok(memcmp(lp.map, "\0\0\0\0\0\0\0\0", 8) == 0, "nothing written on ALL 8 BITS"); + unsigned long n = adc_calls - a0, t = (sim_ms - t0) / 10u; + ok(n + 1u >= t && n <= t + 1u, "the keys every 10 ms"); + go(1); + shot(dir, "bit0"); + press_pa(PA_FX); + ok(map(0) == LB_FX, "FX on BIT 0"); + shot(dir, "bit0_fx"); + press_pa(PA_FX); + ok(map(0) < 0, "FX again clears it"); + press_pa(PA_FX); + go(2); press_pa(PA_FS_UP); + ok(map(1) == LB_FSR, "right footswitch on BIT 1"); + go(3); press_pa(PA_FS_DN); + ok(map(2) == LB_FSL, "left footswitch on BIT 2"); + go(4); press_ph(PH_AMP); + ok(map(3) == LB_AMP, "AMP on BIT 3"); + go(5); press_key(KEY_CAB); + ok(map(4) == LB_CAB, "CAB (analog) on BIT 4"); + go(6); press_ph(1u << 6); + ok(map(5) == LB_PUSH, "PUSH (PH6 alone) on BIT 5"); + go(7); press_key(KEY_REV); press_pa(PA_HOME); + ok(map(6) == LB_HOME, "HOME replaces REV on BIT 6"); + go(8); press_ph(1u << 8); + ok(map(7) == LB_MOD, "MOD (PH8 alone) on BIT 7"); + ok(map(0) == LB_FX && map(3) == LB_AMP, "the table keeps the others"); + shot(dir, "map"); + go(0); + press_pa(PA_FX); + ok(map(0) == LB_FX, "a press on ALL changes nothing"); + } + ok(e_wait == 0, "a wait on each side of every shift clock edge"); + ok(e_latch == 0, "the latch is low while shifting"); + ok(e_dir == 0, "DS is driven while shifting"); ok(clipped == 0, "every line fits its box"); - printf("LED hunt: %s (%d failures, %lu latches, %lu USB passes)\n", failed ? "FAIL" : "ok", failed, latches, usb_calls); + printf("LED map: %s (%d failures, %lu latches, %lu USB passes)\n", failed ? "FAIL" : "ok", failed, latches, usb_calls); return failed ? 1 : 0; } diff --git a/tests/run_tests.sh b/tests/run_tests.sh index 5709a40..dd722a7 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -4,7 +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) -# - LED hunt (firmware/led_probe.h, px_leds.h): stock I/O power once, the 6 roles, 47 LED lines, the 595 timing +# - LED map + lights driver (led_probe.h, px_leds.h): PC7 enable after the first byte, the 595 timing, the table # - 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) @@ -19,7 +19,7 @@ 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 "== LED hunt and the 595 driver (LED_PROBE build, issue #8)" +echo "== LED map and the lights driver (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 From 2689ef9b0ec7e88ee5392549738564818c5606b7 Mon Sep 17 00:00:00 2001 From: Roberto Antonacci <48053274+steponme82@users.noreply.github.com> Date: Fri, 6 Nov 2026 21:30:26 +0100 Subject: [PATCH 9/9] Button lights: they follow the blocks (FX, AMP, MOD, DLY, REV, CAB; HOME in the tuner) isod89/openpocket#8. With PC7 low after the first byte the LED map lit every button on a BlackBox, and the map is the panel's order: bit 0 HOME, 1 FX, 2 AMP, 3 MOD, 4 DLY, 5 REV, 6 CAB, 7 none. firmware/px_lights.h decides the lights once per main-loop pass from the preset: AMP, MOD, DLY, REV and CAB lit while their block is on, FX while any block of the FX button's group is on (drive, SFX, synth, pitch, comp, gate, EQ: ui.h's UI_FXB, now shared with ui_fx_cycle), HOME blinking at 2 Hz while the tuner is open. On the wire a 0 lights, so the byte goes out inverted, and only when it changes (as the stock firmware). The normal firmware calls leds_init after audio_start (all off loaded, then PC7 low) and lights_update in the main loop; the update screen turns them off (px_usb.h PX_LIGHTS_OFF, from ota_show). Not in this change: a setting to switch the lights off. It belongs with the settings store isod89/openpocket#10 adds. LED_BLINK (the test switch) is gone; LED_PROBE 1 still builds the LED map. tests/lightsim.c: every block alone lights its own button, all on lights the six and not HOME, HOME blinks at 2 Hz in the tuner only, bit 7 never lit, nothing written when nothing changes, lights_off, only PC5..PC8 touched. Its expected bits are the LED map's, written out again; 8 of 8 deliberate mistakes fail it. In run_tests.sh the step keeps the test's exit code (a pipe into tail would hide it). Not run on a pedal yet. Co-Authored-By: Claude Opus 5.5 --- firmware/openpocket.c | 26 ++------- firmware/px_lights.h | 39 ++++++++++++++ firmware/px_usb.h | 5 ++ firmware/ui.h | 5 +- tests/lightsim.c | 120 ++++++++++++++++++++++++++++++++++++++++++ tests/run_tests.sh | 4 ++ 6 files changed, 176 insertions(+), 23 deletions(-) create mode 100644 firmware/px_lights.h create mode 100644 tests/lightsim.c diff --git a/firmware/openpocket.c b/firmware/openpocket.c index e672ef4..9b3186e 100644 --- a/firmware/openpocket.c +++ b/firmware/openpocket.c @@ -306,9 +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 1 /* 1 = LED map (issue #8, led_probe.h): the knob picks a bit of the lights' 595, it blinks; no sound */ -#define LED_BLINK 0 /* 1 = test: the normal firmware with sound, plus the lights' 595 (px_leds.h) blinking - * all 8 bits at 2 Hz (issue #8: do the lights need something the normal start sets up?) */ +#define LED_PROBE 0 /* 1 = LED map (issue #8, led_probe.h): the knob picks a bit of the lights' 595, it blinks; no sound */ #define PA_PD (*(volatile uint32_t *)0x50014u) /* ---- Controls mapped on the real pedal (diagnostic) ---- @@ -539,6 +537,8 @@ static const int8_t QDEC[16] = {0,-1,1,0, 1,0,0,-1, -1,0,0,1, 0,1,-1,0}; #if !AUDIO_PROBE #include "ui.h" +#include "px_lights.h" /* the button lights follow the blocks (issue #8; px_leds.h drives them) */ +#define PX_LIGHTS_OFF() lights_off() #include "px_usb.h" /* USB-MIDI + update entry (web installer) */ #include "px_store.h" /* the user's presets in flash */ #if !AUDIO_PROBE @@ -553,12 +553,6 @@ static uint32_t ed_flags(void) { return (a_bypass ? 1u : 0u) | (a_c1 ? 2u : 0u); #endif #include "led_probe.h" /* the LED map (issue #8) */ #endif -#if LED_BLINK -#if LED_PROBE || AUDIO_PROBE -#error "LED_BLINK is the normal build plus the 595 blinking" -#endif -#include "px_leds.h" /* the button lights' 74HC595 (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) @@ -601,9 +595,6 @@ static void app_main(void) b_str(l, "RST "); rst_names(l, op_pwr.rst); b_str(l, " PINR "); hex8(h, op_pwr.pinr); b_str(l, h + 6); ft_box(0, 170, 240, l, &FT_SM, UI_DIM, UI_BG, 1); } -#endif -#if LED_BLINK - ft_box(0, 196, 240, "LED TEST: ALL 8 LIGHT BITS BLINK", &FT_SM, UI_ACC, UI_BG, 1); #endif for (int i = 0; i < 6; i++) { fm1_wdt_feed(); fm1_delay_ms(100); } /* --- report of the previous boot, if it did not finish (or crashed) --- */ @@ -672,9 +663,7 @@ static void app_main(void) px_usb_start(); /* USB-MIDI device + update entry (web installer) */ px_store_boot(); /* the saved presets; start on the last one saved */ audio_start(); /* guitar -> DSP engine -> output, amp enabled */ -#if LED_BLINK - leds_init(); /* PC5 / PC6 / PC8 outputs, 0 shifted out (as stock) */ -#endif + leds_init(); /* the button lights: all off loaded, then on (PC7 low), as stock */ op_stage(ST_UI, "SCREEN..."); lcd_rect(0, 0, 240, 240, UI_BG); ui_invalidate(); @@ -688,12 +677,7 @@ static void app_main(void) #if !AUDIO_PROBE px_usb_service(now); /* web installer: handshake, update, UBOOT soft key */ audio_poll(now); /* the sound follows the UI (preset, settings, volume) */ -#if LED_BLINK - { /* all 8 bits FF / 00 at 2 Hz: whatever the polarity, every light on the 595 blinks */ - static uint32_t t_led; - if (now - t_led >= 250u) { t_led = now; leds_write(leds_byte ? 0x00u : 0xFFu); } - } -#endif + lights_update(&PRESET, ui_mode == M_TUNER, now); /* the button lights follow the blocks */ if (ui_mode != M_TUNER) { /* DSP load at the bottom right: "C1 42%" (2nd core), "C0" or BYP */ static uint32_t t_cpu; if (now - t_cpu >= 500u) { diff --git a/firmware/px_lights.h b/firmware/px_lights.h new file mode 100644 index 0000000..3602558 --- /dev/null +++ b/firmware/px_lights.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: GPL-3.0-only + * OpenPocket — what the button lights show (issue #8). px_leds.h drives them; this decides them, once per main-loop + * pass, from the preset being played: + * FX lit while any block of the FX button's group is on (UI_FXB: drive, SFX, synth, + * pitch, comp, gate, EQ) + * AMP, MOD, DLY, REV, CAB lit while their block is on + * HOME blinks (2 Hz) while the tuner is open, dark otherwise + * The bits are the LED map's, read on a BlackBox (2026-11-06): 0 HOME, 1 FX, 2 AMP, 3 MOD, 4 DLY, 5 REV, 6 CAB, + * 7 none: the panel's order. On the wire a 0 lights, so the byte goes out inverted (as the stock firmware sends + * it), and only when it changes. Not yet: a setting to switch the lights off (it belongs with the settings store + * isod89/openpocket#10 adds). + * Included by openpocket.c (after ui.h: UI_FXB) and by tests/lightsim.c. */ +#pragma once +#include +#include "px_leds.h" + +enum { LT_HOME, LT_FX, LT_AMP, LT_MOD, LT_DLY, LT_REV, LT_CAB }; /* the light on each bit */ +#define LT_BLINK_MS 250u /* the tuner's HOME blink: half a period */ + +static uint8_t lights_of(const px_preset_t *p, int tuner, uint32_t now) /* the lit lights, 1 = lit */ +{ + uint8_t v = 0; + for (int i = 0; i < 7; i++) + if (p->on[UI_FXB[i]]) v |= 1u << LT_FX; + if (p->on[PX_B_AMP]) v |= 1u << LT_AMP; + if (p->on[PX_B_MOD]) v |= 1u << LT_MOD; + if (p->on[PX_B_DELAY]) v |= 1u << LT_DLY; + if (p->on[PX_B_REVERB]) v |= 1u << LT_REV; + if (p->on[PX_B_CAB]) v |= 1u << LT_CAB; + if (tuner && !((now / LT_BLINK_MS) & 1u)) v |= 1u << LT_HOME; + return v; +} +static void lights_show(uint8_t lit) /* written only when the byte changes */ +{ + uint8_t wire = (uint8_t)~lit; + if (wire != leds_byte) leds_write(wire); +} +static void lights_update(const px_preset_t *p, int tuner, uint32_t now) { lights_show(lights_of(p, tuner, now)); } +static void lights_off(void) { lights_show(0u); } diff --git a/firmware/px_usb.h b/firmware/px_usb.h index 851134a..328edf4 100644 --- a/firmware/px_usb.h +++ b/firmware/px_usb.h @@ -43,6 +43,10 @@ void fm1_timer5_irq(void) /* 10 kHz; USB at 2 kHz */ } extern void isr_timer5(void); +#ifndef PX_LIGHTS_OFF /* the button lights off while an update runs (openpocket.c: px_lights.h) */ +#define PX_LIGHTS_OFF() ((void)0) +#endif + /* ---- ota.c hooks ---- */ static uint32_t ota_now_ms(void) { return ms_now(); } static void ota_idle(void) { fm1_wdt_feed(); } @@ -78,6 +82,7 @@ static int ota_fread(uint32_t off, void *p, uint32_t n) static void ota_show(uint32_t step, int32_t code) { static const char *const STEP[] = {"", "PACKAGE", "CHECK", "LOADER", "CONFIRM", "RESTART"}; + PX_LIGHTS_OFF(); lcd_rect(0, 0, 240, 240, UI_BG); ft_box(0, 92, 240, "UPDATE", &FT_MD, UI_ACC, UI_BG, 1); if (step < 9u) { diff --git a/firmware/ui.h b/firmware/ui.h index 960d2e8..b530fce 100644 --- a/firmware/ui.h +++ b/firmware/ui.h @@ -464,11 +464,12 @@ static void ui_preset_step(int dd) if (ui_mode == M_EDIT) ui_mode = M_BLOCK; if ((int)ui_item >= ui_nitems()) ui_item = 0; } +/* the FX button's blocks, in its order (the FX light follows them: px_lights.h) */ +static const uint8_t UI_FXB[7] = {PX_B_DRIVE, PX_B_SFX, PX_B_SYN, PX_B_PITCH, PX_B_COMP, PX_B_GATE, PX_B_EQ}; static void ui_fx_cycle(void) /* FX: DRV -> SFX -> SYN -> PIT -> CMP -> GT -> EQ */ { - static const uint8_t FXB[7] = {PX_B_DRIVE, PX_B_SFX, PX_B_SYN, PX_B_PITCH, PX_B_COMP, PX_B_GATE, PX_B_EQ}; uint32_t nx = PX_B_DRIVE; - for (int i = 0; i < 7; i++) if (FXB[i] == ui_blk) nx = FXB[(i + 1) % 7]; + for (int i = 0; i < 7; i++) if (UI_FXB[i] == ui_blk) nx = UI_FXB[(i + 1) % 7]; ui_blk = nx; ui_mode = M_BLOCK; ui_item = 0; } diff --git a/tests/lightsim.c b/tests/lightsim.c new file mode 100644 index 0000000..66a3ff6 --- /dev/null +++ b/tests/lightsim.c @@ -0,0 +1,120 @@ +/* SPDX-License-Identifier: GPL-3.0-only + * OpenPocket — the button lights in the normal firmware (firmware/px_lights.h over px_leds.h, issue #8) on the PC, + * against fake port C registers and a model of the 74HC595 with its output enable on PC7: + * cc -O1 -Wall -Iengine -Ifirmware -o build/lightsim tests/lightsim.c && build/lightsim + * Checks: after leds_init the lights are enabled (PC7 low) with all of them dark; every one of the twelve blocks, + * alone, lights exactly its button (the FX group's seven lights FX); all on lights the six effect buttons and not + * HOME; HOME blinks at 2 Hz in the tuner and only there; bit 7 is never lit; the byte is written only when it + * changes; lights_off darkens all; only PC5, PC6, PC7 and PC8 are ever touched. */ +#include +#include +#include +#include "px_dsp.h" + +/* the FX button's group, written out again here (ui.h's UI_FXB is the firmware's): drive, SFX, synth, pitch, comp, + * gate, EQ */ +static const uint8_t UI_FXB[7] = { PX_B_DRIVE, PX_B_SFX, PX_B_SYN, PX_B_PITCH, PX_B_COMP, PX_B_GATE, PX_B_EQ }; + +/* ---- port C and the 595: DS PC8, SHCP PC5, STCP PC6, OE PC7 (low = outputs on); a 0 on an output lights it */ +enum { R_OUT = 0, R_DIR = 2, R_PU = 4, R_PD = 5 }; +static uint32_t pc[16], pc_start[16]; +static uint32_t lv_prev; +static uint8_t sr, q; +static unsigned long latches, waits; +static uint32_t pad(void) { uint32_t d = pc[R_DIR]; return (pc[R_OUT] & ~d) | (pc[R_PU] & d); } +static void edge(void) +{ + uint32_t lv = pad(), rise = lv & ~lv_prev; + if (rise & (1u << 5)) sr = (uint8_t)((sr << 1) | ((lv >> 8) & 1u)); + if (rise & (1u << 6)) { q = sr; latches++; } + lv_prev = lv; +} +static int enabled(void) { return !(pc[R_DIR] & (1u << 7)) && !(pad() & (1u << 7)); } +static uint8_t lit(void) { return enabled() ? (uint8_t)~q : 0u; } /* what the eye sees: 1 = lit */ +#define LEDS_PC(r) (pc[(r) / 4u]) +#define LEDS_WAIT() (waits++) +#define LEDS_EDGE() edge() +#include "px_lights.h" + +static int failed; +static void ok(int c, const char *what) { printf("%-66s %s\n", what, c ? "ok" : "FAIL"); if (!c) failed++; } +/* the LED map read on a BlackBox (2026-11-06), written out again here: bit 0 HOME, 1 FX, 2 AMP, 3 MOD, 4 DLY, 5 REV, + * 6 CAB, 7 none */ +enum { W_HOME = 0, W_FX = 1, W_AMP = 2, W_MOD = 3, W_DLY = 4, W_REV = 5, W_CAB = 6 }; +static uint8_t want_of(int b) /* the light a block alone should light */ +{ + for (int i = 0; i < 7; i++) if (UI_FXB[i] == b) return 1u << W_FX; + switch (b) { + case PX_B_AMP: return 1u << W_AMP; + case PX_B_MOD: return 1u << W_MOD; + case PX_B_DELAY: return 1u << W_DLY; + case PX_B_REVERB: return 1u << W_REV; + case PX_B_CAB: return 1u << W_CAB; + } + return 0u; +} +static int only_our_pins(void) +{ + for (int r = 0; r < 16; r++) { + uint32_t may = (r == R_OUT || r == R_DIR) ? 0x1E0u : (r == R_PU || r == R_PD) ? 0x160u : 0u; + if ((pc[r] ^ pc_start[r]) & ~may) return 0; + } + return 1; +} + +int main(void) +{ + static px_preset_t p; + memset(pc, 0, sizeof pc); + pc[R_OUT] = 0x5A5Au | 0x80u; pc[R_DIR] = 0xFFFFu & ~0x80u; /* PC7 an output, high, as lcd_init left it */ + pc[R_PU] = 0xFFFFu; + memcpy(pc_start, pc, sizeof pc); + lv_prev = pad(); sr = 0x5Au; q = 0x00u; /* a 595 holds garbage at power-on */ + + leds_init(); + ok(enabled() && lit() == 0u, "after leds_init: the lights enabled, all dark"); + + memset(&p, 0, sizeof p); + lights_update(&p, 0, 0); + ok(lit() == 0u && latches == 1u, "no block on: all dark, nothing written again"); + { + int good = 1, every = 1; + for (int b = 0; b < PX_NBLK; b++) { + memset(&p, 0, sizeof p); p.on[b] = 1; + lights_update(&p, 0, 0); + if (lit() != want_of(b)) { printf(" block %d lit %02X, wanted %02X\n", b, lit(), want_of(b)); good = 0; } + if (!want_of(b)) every = 0; + } + ok(good, "each block alone lights its own button"); + ok(every, "every block has a light (the FX group's seven: FX)"); + } + memset(&p, 0, sizeof p); + for (int b = 0; b < PX_NBLK; b++) p.on[b] = 1; + lights_update(&p, 0, 0); + ok(lit() == 0x7Eu, "all on: FX AMP MOD DLY REV CAB lit, HOME dark"); + { + unsigned long l0 = latches; + for (uint32_t t = 0; t < 2000u; t++) lights_update(&p, 0, t); + ok(latches == l0, "nothing changes: nothing written"); + } + { + int flips = 0, others = 1; uint8_t last = lit(); + for (uint32_t t = 0; t < 2000u; t++) { + lights_update(&p, 1, t); + if ((lit() ^ last) & (1u << W_HOME)) flips++; + if ((lit() & 0x7Eu) != 0x7Eu) others = 0; + last = lit(); + } + ok(flips >= 7 && flips <= 9, "tuner: HOME blinks at 2 Hz"); + ok(others, "tuner: the effect lights stay as they are"); + lights_update(&p, 0, 0); + ok(!(lit() & (1u << W_HOME)), "tuner closed: HOME dark"); + } + ok(!(lit() & 0x80u) && (q & 0x80u), "bit 7 never lit"); + lights_off(); + ok(lit() == 0u && enabled(), "lights_off: all dark"); + ok(only_our_pins(), "only PC5, PC6, PC7 and PC8 touched"); + ok(waits > 0u, "the 595 timing waits are used"); + printf("\n%s\n", failed ? "lights tests FAILED" : "all lights tests passed"); + return failed ? 1 : 0; +} diff --git a/tests/run_tests.sh b/tests/run_tests.sh index dd722a7..902b256 100755 --- a/tests/run_tests.sh +++ b/tests/run_tests.sh @@ -5,6 +5,7 @@ # 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) # - LED map + lights driver (led_probe.h, px_leds.h): PC7 enable after the first byte, the 595 timing, the table +# - button lights (px_lights.h): each block lights its button, FX for its group, HOME blinks in the tuner # - 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) @@ -21,6 +22,9 @@ 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 "== LED map and the lights driver (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 "== button lights in the normal firmware (px_lights.h, issue #8)" +$CC -O1 -Wall -Wno-unused-function -Iengine -Ifirmware -o build/lightsim tests/lightsim.c +build/lightsim > build/lightsim.log || { cat build/lightsim.log; exit 1; }; tail -1 build/lightsim.log 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