Seedbox v0.1 + firmware, generic kit/synth, ROOT/SCALE pages
Some checks failed
check / check (push) Has been cancelled

First commit of the repo. Contents beyond the v0.1 simulator and layout:

- Full Pico 2 W firmware (firmware/app, drivers, engine port, host test).
- BANK page and 404 mode A/B removed; learned pads play exactly what was learned.
- 404/MiniFreak renamed to KIT/SYNTH throughout (jacks, code, sim, docs,
  layout labels; wiring rebuilt).
- ROLL returns to page select after a re-roll; a freshly learned track turns on.
- LCD contrast 66.
- ROOT and SCALE pages: 21 scales, AUTO = the seed picks; global and sticky.

Intended music change: the scale table grew from 8 to 21 and AUTO picks from
all of them, so every seed's scale changed. Vectors regenerated with
npm run vectors, plus new key-override cases. Flash record is now version 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 20:39:36 +03:00
commit 2196633ed5
77 changed files with 52207 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
#include "encoder.h"
#include "hardware/gpio.h"
#define PIN_A 10
#define PIN_B 11
#define REST 3 /* A = B = 1: both contacts open at a detent */
#define DIR -1 /* as wired on the bench, the raw count runs down clockwise */
static volatile int32_t steps, clicks;
static volatile int8_t acc; /* edges since the last rest */
static volatile uint8_t prev;
/* index = previous AB << 2 | current AB; invalid (bounce) transitions count 0 */
static const int8_t TABLE[16] = { 0,-1, 1, 0, 1, 0, 0,-1, -1, 0, 0, 1, 0, 1,-1, 0 };
static uint8_t read_ab(void){ return (uint8_t)((gpio_get(PIN_A) << 1) | gpio_get(PIN_B)); }
static void on_edge(uint gpio, uint32_t events){
uint8_t cur;
int8_t d;
(void)events;
if(gpio != PIN_A && gpio != PIN_B) return;
cur = read_ab();
d = TABLE[(prev << 2) | cur];
steps += d; acc = (int8_t)(acc + d);
prev = cur;
if(cur == REST){
/* a full click is 4 edges; accept 2 or more in case bounce hid some, ignore wiggles of 1 */
if(acc >= 2) clicks += DIR;
else if(acc <= -2) clicks -= DIR;
acc = 0;
}
}
void encoder_init(void){
gpio_init(PIN_A); gpio_set_dir(PIN_A, GPIO_IN); gpio_pull_up(PIN_A);
gpio_init(PIN_B); gpio_set_dir(PIN_B, GPIO_IN); gpio_pull_up(PIN_B);
prev = read_ab();
gpio_set_irq_enabled_with_callback(PIN_A, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true, on_edge);
gpio_set_irq_enabled(PIN_B, GPIO_IRQ_EDGE_RISE | GPIO_IRQ_EDGE_FALL, true);
}
int32_t encoder_count(void){ return clicks; }
int32_t encoder_steps(void){ return DIR * steps; }
int encoder_ab(void){ return read_ab(); }

View File

@@ -0,0 +1,15 @@
/* Quadrature encoder on GP10 (A) / GP11 (B), pull-ups, decoded in the GPIO interrupt with a state table.
The bench encoder gives 4 edges per click and rests with both contacts open (A = B = 1). As wired, the raw
count runs down when turned clockwise, so the driver flips it: + = clockwise.
A click counts when the shaft comes back to rest, so jitter around a detent doesn't count. */
#ifndef SEEDBOX_ENCODER_H
#define SEEDBOX_ENCODER_H
#include <stdint.h>
void encoder_init(void);
int32_t encoder_count(void); /* clicks, + = clockwise */
int32_t encoder_steps(void); /* raw edges, for the bench */
int encoder_ab(void); /* current A << 1 | B */
#endif

50
firmware/drivers/inputs.c Normal file
View File

@@ -0,0 +1,50 @@
#include "inputs.h"
#include "hardware/gpio.h"
#include "pico/time.h"
static const uint8_t ROWS[3] = {2, 3, 5};
static const uint8_t COLS[4] = {6, 7, 8, 9};
static const uint8_t SWITCHES[3] = {13, 14, 12}; /* IN_SEED, IN_RUN, IN_PUSH */
#define DEBOUNCE_MS 10 /* a change must hold this long to count */
#define LOCKOUT_MS 40 /* then the input ignores further changes this long (worn switches chatter) */
/* (1,1) KICK (1,2) KICK 2 (1,3) SNARE (1,4) none; (2,1) HAT (2,2) HAT 16 (2,3) PERC (2,4) GHOST;
(3,1) BASS (3,2) MEL A (3,3) MEL B (3,4) PAD */
const int8_t INPUT_KEY_TRACK[12] = { 0, 1, 2, -1, 4, 5, 6, 3, 7, 8, 9, 10 };
static uint32_t state;
static uint8_t count[IN_COUNT], lockout[IN_COUNT];
static void input_pullup(uint pin){ gpio_init(pin); gpio_set_dir(pin, GPIO_IN); gpio_pull_up(pin); }
void inputs_init(void){
int i;
for(i = 0; i < 3; i++) input_pullup(ROWS[i]);
for(i = 0; i < 4; i++){ input_pullup(COLS[i]); gpio_put(COLS[i], 0); } /* idle: Hi-Z with pull-up (E9) */
for(i = 0; i < 3; i++) input_pullup(SWITCHES[i]);
}
static uint32_t raw_read(void){
uint32_t raw = 0;
int r, c;
for(c = 0; c < 4; c++){
gpio_set_dir(COLS[c], GPIO_OUT); /* output latch is already 0: drive low */
busy_wait_us(5); /* settle */
for(r = 0; r < 3; r++) if(!gpio_get(ROWS[r])) raw |= 1u << (r * 4 + c);
gpio_set_dir(COLS[c], GPIO_IN); /* back to Hi-Z, pull-up holds it high */
}
for(r = 0; r < 3; r++) if(!gpio_get(SWITCHES[r])) raw |= 1u << (IN_SEED + r);
return raw;
}
uint32_t inputs_scan(void){
uint32_t raw = raw_read();
int i;
for(i = 0; i < IN_COUNT; i++){
uint32_t bit = 1u << i;
if(lockout[i]){ lockout[i]--; count[i] = 0; continue; }
if((raw & bit) != (state & bit)){ if(++count[i] >= DEBOUNCE_MS){ state ^= bit; count[i] = 0; lockout[i] = LOCKOUT_MS; } }
else count[i] = 0;
}
return state;
}

17
firmware/drivers/inputs.h Normal file
View File

@@ -0,0 +1,17 @@
/* Button matrix (rows GP2 GP3 GP5, columns GP6–GP9), SEED (GP13), RUN (GP14), encoder push (GP12).
All active-low with pull-ups (RP2350 erratum E9). Call inputs_scan() about once per millisecond. */
#ifndef SEEDBOX_INPUTS_H
#define SEEDBOX_INPUTS_H
#include <stdint.h>
/* bit index: matrix key = row * 4 + col (row 0..2, col 0..3), then the switches */
enum { IN_MATRIX = 12, IN_SEED = 12, IN_RUN = 13, IN_PUSH = 14, IN_COUNT = 15 };
/* matrix key -> track index (engine order), -1 = no button. Same as PANEL in app.js. */
extern const int8_t INPUT_KEY_TRACK[12];
void inputs_init(void);
uint32_t inputs_scan(void); /* returns the debounced state, bit set = pressed / RUN on */
#endif

View File

@@ -0,0 +1,37 @@
#include "midi_uart.h"
#include "hardware/gpio.h"
#include "hardware/uart.h"
#define PIN_OUT_KIT 0 /* UART0 TX */
#define PIN_IN 1 /* UART0 RX, 6N137 output with a 1 kΩ pull-up on the board */
#define PIN_OUT_SYNTH 4 /* UART1 TX */
#define MIDI_BAUD 31250
static uart_inst_t *const PORTS[2] = { uart0, uart1 };
void midi_init(void){
uart_init(uart0, MIDI_BAUD);
uart_init(uart1, MIDI_BAUD);
uart_set_format(uart0, 8, 1, UART_PARITY_NONE);
uart_set_format(uart1, 8, 1, UART_PARITY_NONE);
gpio_set_function(PIN_OUT_KIT, GPIO_FUNC_UART);
gpio_set_function(PIN_IN, GPIO_FUNC_UART);
gpio_set_function(PIN_OUT_SYNTH, GPIO_FUNC_UART); /* GP5 (UART1 RX) stays a matrix row */
gpio_pull_up(PIN_IN);
/* each MIDI current loop draws 6–8 mA, above the 4 mA default */
gpio_set_drive_strength(PIN_OUT_KIT, GPIO_DRIVE_STRENGTH_12MA);
gpio_set_drive_strength(PIN_OUT_SYNTH, GPIO_DRIVE_STRENGTH_12MA);
}
void midi_send(int port, const uint8_t *bytes, int n){ uart_write_blocking(PORTS[port], bytes, (size_t)n); }
void midi_note_on(int port, int ch, int note, int vel){
uint8_t m[3] = { (uint8_t)(0x90 | (ch & 15)), (uint8_t)(note & 127), (uint8_t)(vel & 127) };
midi_send(port, m, 3);
}
void midi_note_off(int port, int ch, int note){
uint8_t m[3] = { (uint8_t)(0x80 | (ch & 15)), (uint8_t)(note & 127), 0 };
midi_send(port, m, 3);
}
int midi_in_read(void){ return uart_is_readable(uart0) ? uart_getc(uart0) : -1; }

View File

@@ -0,0 +1,15 @@
/* MIDI over the two UARTs: OUT KIT = UART0 TX (GP0), OUT SYNTH = UART1 TX (GP4), IN = UART0 RX (GP1). 31 250 baud, 8N1. */
#ifndef SEEDBOX_MIDI_UART_H
#define SEEDBOX_MIDI_UART_H
#include <stdint.h>
enum { MIDI_KIT = 0, MIDI_SYNTH = 1 };
void midi_init(void);
void midi_send(int port, const uint8_t *bytes, int n); /* blocking */
void midi_note_on(int port, int ch, int note, int vel); /* ch 0..15 */
void midi_note_off(int port, int ch, int note);
int midi_in_read(void); /* next byte from MIDI IN, or -1 */
#endif

View File

@@ -0,0 +1,91 @@
#include "pcd8544.h"
#include "hardware/gpio.h"
#include "hardware/spi.h"
#include "pico/time.h"
#include <string.h>
#define PIN_CE 17
#define PIN_SCK 18
#define PIN_MOSI 19
#define PIN_DC 20
#define PIN_RST 21
#define LCD_SPI spi0
#define LCD_HZ 4000000
static uint8_t fb[LCD_W * LCD_H / 8]; /* PCD8544 layout: 6 banks of 8 rows, LSB at the top */
static void write(int data, const uint8_t *b, size_t n){
gpio_put(PIN_DC, data);
gpio_put(PIN_CE, 0);
spi_write_blocking(LCD_SPI, b, n); /* returns once the bytes are shifted out */
gpio_put(PIN_CE, 1);
}
static void cmd(uint8_t c){ write(0, &c, 1); }
void lcd_set_vop(int vop){
cmd(0x21); /* extended instruction set */
cmd((uint8_t)(0x80 | (vop & 0x7F)));
cmd(0x20);
}
void lcd_init(int vop){
spi_init(LCD_SPI, LCD_HZ);
spi_set_format(LCD_SPI, 8, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST);
gpio_set_function(PIN_SCK, GPIO_FUNC_SPI);
gpio_set_function(PIN_MOSI, GPIO_FUNC_SPI);
gpio_init(PIN_CE); gpio_set_dir(PIN_CE, GPIO_OUT); gpio_put(PIN_CE, 1);
gpio_init(PIN_DC); gpio_set_dir(PIN_DC, GPIO_OUT); gpio_put(PIN_DC, 0);
gpio_init(PIN_RST); gpio_set_dir(PIN_RST, GPIO_OUT);
gpio_put(PIN_RST, 0); sleep_ms(10); gpio_put(PIN_RST, 1); sleep_ms(10);
cmd(0x21); /* extended */
cmd((uint8_t)(0x80 | (vop & 0x7F)));/* Vop */
cmd(0x04); /* temperature coefficient 0 */
cmd(0x13); /* bias 1:48 */
cmd(0x20); /* basic, horizontal addressing */
cmd(0x0C); /* normal display */
lcd_clear(); lcd_flush();
}
void lcd_clear(void){ memset(fb, 0, sizeof fb); }
void lcd_px(int x, int y, int on){
if(x < 0 || x >= LCD_W || y < 0 || y >= LCD_H) return;
if(on) fb[(y >> 3) * LCD_W + x] |= (uint8_t)(1u << (y & 7));
else fb[(y >> 3) * LCD_W + x] &= (uint8_t)~(1u << (y & 7));
}
void lcd_fill(int x, int y, int w, int h, int on){ int i, j; for(j = y; j < y + h; j++) for(i = x; i < x + w; i++) lcd_px(i, j, on); }
/* The 5 × 7 font from sim/app.js (FONT): five column bytes per glyph, bit 0 at the top. */
typedef struct { char c; uint8_t col[5]; } glyph_t;
static const glyph_t FONT[] = {
{' ',{0,0,0,0,0}},{'+',{8,8,62,8,8}},{'-',{8,8,8,8,8}},{'.',{0,96,96,0,0}},{'/',{32,16,8,4,2}},{':',{0,54,54,0,0}},
{'#',{20,127,20,127,20}},{'*',{20,8,62,8,20}},{'>',{0,65,34,20,8}},{'?',{2,1,81,9,6}},{'!',{0,0,95,0,0}},
{'0',{62,81,73,69,62}},{'1',{0,66,127,64,0}},{'2',{66,97,81,73,70}},{'3',{33,65,69,75,49}},{'4',{24,20,18,127,16}},
{'5',{39,69,69,69,57}},{'6',{60,74,73,73,48}},{'7',{1,113,9,5,3}},{'8',{54,73,73,73,54}},{'9',{6,73,73,41,30}},
{'A',{126,17,17,17,126}},{'B',{127,73,73,73,54}},{'C',{62,65,65,65,34}},{'D',{127,65,65,34,28}},{'E',{127,73,73,73,65}},
{'F',{127,9,9,9,1}},{'G',{62,65,73,73,122}},{'H',{127,8,8,8,127}},{'I',{0,65,127,65,0}},{'J',{32,64,65,63,1}},
{'K',{127,8,20,34,65}},{'L',{127,64,64,64,64}},{'M',{127,2,12,2,127}},{'N',{127,4,8,16,127}},{'O',{62,65,65,65,62}},
{'P',{127,9,9,9,6}},{'Q',{62,65,81,33,94}},{'R',{127,9,25,41,70}},{'S',{70,73,73,73,49}},{'T',{1,1,127,1,1}},
{'U',{63,64,64,64,63}},{'V',{31,32,64,32,31}},{'W',{63,64,56,64,63}},{'X',{99,20,8,20,99}},{'Y',{7,8,112,8,7}},
{'Z',{97,81,73,69,67}}
};
static const glyph_t *glyph(char c){
size_t i;
if(c >= 'a' && c <= 'z') c = (char)(c - 32);
for(i = 0; i < sizeof FONT / sizeof FONT[0]; i++) if(FONT[i].c == c) return &FONT[i];
return glyph('?');
}
void lcd_text_v(int x, int y, const char *s, int on){
for(; *s; s++, x += 6){
const glyph_t *g = glyph(*s);
int col, row;
for(col = 0; col < 5; col++) for(row = 0; row < 7; row++) if((g->col[col] >> row) & 1) lcd_px(x + col, y + row, on);
}
}
void lcd_text(int x, int y, const char *s){ lcd_text_v(x, y, s, 1); }
void lcd_box(int x, int y, int w, int h){ lcd_fill(x, y, w, 1, 1); lcd_fill(x, y + h - 1, w, 1, 1); lcd_fill(x, y, 1, h, 1); lcd_fill(x + w - 1, y, 1, h, 1); }
void lcd_flush(void){
cmd(0x80); cmd(0x40); /* X = 0, Y = 0 */
write(1, fb, sizeof fb);
}

View File

@@ -0,0 +1,19 @@
/* Nokia 5110 (PCD8544) on SPI0: SCK GP18, MOSI GP19, CE GP17, D/C GP20, RST GP21. 84 × 48, 504-byte framebuffer. */
#ifndef SEEDBOX_PCD8544_H
#define SEEDBOX_PCD8544_H
#define LCD_W 84
#define LCD_H 48
#define LCD_VOP_DEFAULT 66 /* contrast; varies between modules, so it's a setting. 66 suits the bench module. */
void lcd_init(int vop);
void lcd_set_vop(int vop); /* 0..127 */
void lcd_clear(void);
void lcd_px(int x, int y, int on);
void lcd_fill(int x, int y, int w, int h, int on);
void lcd_box(int x, int y, int w, int h); /* 1 px outline */
void lcd_text(int x, int y, const char *s); /* 5 × 7 font from app.js, 6 px per character, upper case */
void lcd_text_v(int x, int y, const char *s, int on); /* draws only the glyph pixels, in on (0 = inverse) */
void lcd_flush(void);
#endif

31
firmware/drivers/pots.c Normal file
View File

@@ -0,0 +1,31 @@
#include "pots.h"
#include "hardware/adc.h"
#define SAMPLES 16
#define HYSTERESIS 24 /* in 12-bit counts: 0.75 of a 7-bit step past the step's centre */
static int value[2], raw[2];
void pots_init(void){
adc_init();
adc_gpio_init(26); /* also turns off the digital input and pulls */
adc_gpio_init(27);
pots_update();
value[0] = raw[0] >> 5; value[1] = raw[1] >> 5;
}
void pots_update(void){
int i, k;
for(i = 0; i < 2; i++){
int sum = 0, centre, d;
adc_select_input((uint)i);
for(k = 0; k < SAMPLES; k++) sum += adc_read();
raw[i] = sum / SAMPLES;
centre = value[i] * 32 + 16;
d = raw[i] - centre;
if(d > 16 + HYSTERESIS || d < -16 - HYSTERESIS) value[i] = raw[i] >> 5;
}
}
int pot_value(int pot){ return value[pot]; }
int pot_raw(int pot){ return raw[pot]; }

12
firmware/drivers/pots.h Normal file
View File

@@ -0,0 +1,12 @@
/* ENERGY on ADC0 (GP26), DRIFT on ADC1 (GP27). Averaged, then 7-bit with hysteresis. */
#ifndef SEEDBOX_POTS_H
#define SEEDBOX_POTS_H
enum { POT_ENERGY = 0, POT_DRIFT = 1 };
void pots_init(void);
void pots_update(void); /* call every few ms */
int pot_value(int pot); /* 0..127 */
int pot_raw(int pot); /* last averaged 12-bit reading, for the bench */
#endif