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..8bda437 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 1 /* 1 = light finder (issue #8, led_probe.h): one pin at a time blinks, the knob steps; no sound */ #define PA_PD (*(volatile uint32_t *)0x50014u) /* ---- Controls mapped on the real pedal (diagnostic) ---- @@ -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