# Firmware ## Inhaltsverzeichnis 1. [Projektübersicht](#projektübersicht) 2. [Systemarchitektur](#systemarchitektur) 3. [Komponenten](#komponenten) 4. [Speicheranalyse](#speicheranalyse) 5. [Installation](#installation) 6. [Verwendung](#verwendung) 7. [Hauptablauf](#hauptablauf) --- ## Projektübersicht ### Features - ✅ **Dual-Channel CV-Sequencer** - 2 unabhängige Control Voltage Ausgänge - ✅ **4×3 Tastaturmatrix** - Echtzeit-Tasten-Eingabe mit Entprellung - ✅ **Recording & Playback** - Speichere Sequenzen bis 30 Sekunden - ✅ **Loop-Funktion** - Endlose Wiederholung oder Einmaledition - ✅ **Live-Modus** - Direkte Tastatur-zu-CV Ausgabe - ✅ **Multi-Key Support** - Bis zu 10 gleichzeitig aktive Tasten ### Hardware | Komponente | Modell | Funktion | |-----------|--------|---------| | Microcontroller | ESP32 | Hauptprozessor | | DAC | MCP4728 | 4-Kanal 12-Bit DAC | | I2C Bus | - | Kommunikation MCU ↔ DAC | | Tastatur | 4×3 Matrix | Benutzereingabe | | Buttons | 4× Push-Buttons | Record/Play Steuerung | --- ## Systemarchitektur ### 3-Schicht-Modell ``` ┌─────────────────────────────────────────────────────┐ │ INPUT LAYER (📥) │ ├──────────────────────┬──────────────────────────────┤ │ Keyboard Matrix │ Sequencer Buttons │ │ (4×3 Tasten) │ (Record/Play × 2) │ └──────────────┬───────┴──────────────┬───────────────┘ │ │ ┌──────────────▼─────────────────────▼───────────────┐ │ PROCESSING LAYER (⚙️) │ ├──────────────────────┬──────────────────────────────┤ │ Keyboard Klasse │ SequencerBlock (2×) │ │ (Queue-Management) │ (Recording & Playback) │ └──────────────┬───────┴──────────────┬───────────────┘ │ │ ┌──────────────▼─────────────────────▼───────────────┐ │ OUTPUT LAYER (📤) │ ├──────────────────────┬──────────────────────────────┤ │ CV Klasse (DAC) │ CV Ausgänge │ │ MCP4728 I2C │ (A=Ch1, B=Ch2) │ └──────────────┬───────┴──────────────┬───────────────┘ │ │ └──────────────┬───────┘ ▼ Externe Synthesizer / Module ``` ### Datenfluss ``` TASTATUR → KEYBOARD → SEQUENCER/LIVE → DAC → CV AUSGÄNGE ↑ BUTTONS ``` --- ## Komponenten ### 1. Keyboard Klasse **Funktion:** Verwaltet die 4×3 Matrix-Tastatur mit Entprellung ```cpp class Keyboard { public: Keyboard(uint8_t nRows, uint8_t nCols, uint8_t *pinsRow, uint8_t *pinsCol); void begin(); void update(); int getQueueLength(); Key getQueue(uint8_t index); // ... }; ``` **Merkmale:** - Debounce-Zeit: 20ms - Max. 10 gleichzeitig aktive Tasten - FIFO-Queue für Tastenreihenfolge - Rückmeldung als `Key(row, col)` Struktur ### 2. CV Klasse **Funktion:** Verwaltet DAC-Ausgänge über I2C (MCP4728) ```cpp class CV { public: CV(Adafruit_MCP4728 *dac, TwoWire *wire, uint8_t nCV, MCP4728_channel_t *cvChannelMap, uint16_t *keyToVoltage, uint8_t row, uint8_t col); bool begin(uint8_t pinSDA, uint8_t pinSCL); void setVoltage(uint8_t cvIndex, Key k); void setVoltage(uint8_t cvIndex, uint16_t mV); void clearAll(); // ... }; ``` **Merkmale:** - 2 CV-Kanäle (A, B) - I2C-Kommunikation (Pins 15, 16) - Voltage-Mapping von Tasten - Range: 0-4096mV (12-Bit) ### 3. SequencerBlock Klasse **Funktion:** Speichert und spielt Sequenzen auf 2 Kanälen ```cpp class SequencerBlock { public: SequencerBlock(uint16_t maxDurationMS, uint16_t minStepDurationMS); // Recording void startRecord(); void stopRecord(); void addStep(uint16_t voltage_ch1, uint16_t voltage_ch2); // Playback void startPlay(); void stopPlay(); void update(); // Must be called regularly void setLoop(bool loop); // Status uint8_t getStepCount(); uint16_t getCurrentVoltageCh1(); uint16_t getCurrentVoltageCh2(); // ... }; ``` **Merkmale:** - Max. 128 Steps pro Sequenz - Max. 30 Sekunden Aufnahmetime - Dual-Channel Recording - Loop-Funktion - Automatisches Time-Limit ### 4. Button-Verarbeitung **3-Mode Play-Button-System:** | Click | Aktion | Zustand | |-------|--------|---------| | 1x | Play (keine Loop) | Spielt einmalig ab | | 2x | Loop aktivieren | Endlosschleife | | 3x | Stop | Stoppt Wiedergabe | **Record-Button:** Toggle zwischen Recording starten/stoppen --- ## Speicheranalyse ```bash RAM: [= ] 6.5% (used 21176 bytes from 327680 bytes) Flash: [= ] 8.5% (used 283165 bytes from 3342336 bytes) ``` ### RAM-Verbrauch | Komponente | Größe | Menge | Gesamt | Notizen | |-----------|-------|-------|--------|---------| | SequencerBlock #1 | ~550 B | 1x | 550 B | 128 Steps × 6 Bytes + Variablen | | SequencerBlock #2 | ~550 B | 1x | 550 B | 128 Steps × 6 Bytes + Variablen | | Keyboard Objekt | ~130 B | 1x | 130 B | 8×8 Bool Arrays + Pointer | | CV Objekt | ~50 B | 1x | 50 B | DAC-Pointer + Config | | keyToVoltage Array | 24 B | 1x | 24 B | 12 Keys × uint16_t | | Button States | ~50 B | 1x | 50 B | 4 Buttons × ~12 Bytes | | Lokale Variablen | ~100 B | 1x | 100 B | Loop-Variablen | | **GESAMT (Schätzung)** | - | - | **~1.5 KB** | - | ### Flash-Verbrauch | Komponente | Größe | |-----------|-------| | Arduino/Wire Libraries | ~150 KB | | Adafruit_MCP4728 | ~20 KB | | Firmware Code | ~80 KB | | Bootloader | ~60 KB | | **GESAMT (Schätzung)** | **~310 KB** | ### Sequenzen-Speicher Detail ```cpp struct DualVoltageDurationPair { uint16_t voltage_ch1; // 2 Bytes uint16_t voltage_ch2; // 2 Bytes uint16_t duration; // 2 Bytes }; // = 6 Bytes pro Step // Berechnung: // - N_MAX_SEQUENCE_STEPS = 128 // - 128 Steps × 6 Bytes = 768 Bytes pro Sequenz // - 2 Sequenzer = 1536 Bytes (1.5 KB) ``` ### Optimierungspotential | Feature | Größenänderung | Status | |---------|-----------------|--------| | 256 Steps statt 128 | +768 B | ✅ Problemlos möglich | | 60s statt 30s Limit | 0 B | ✅ Kostenloses Upgrade | | 4 Sequenzer statt 2 | +3.3 KB | ✅ Problemlos möglich | | 8×8 Tastatur statt 4×3 | ~ +30 B | ✅ Kaum Mehraufwand | --- ## Verwendung ### Grundlegende Konfiguration `include/FIRMWARE_DEF.h`: ```cpp #define N_KEYBOARD_ROW 4 // Keyboard Reihen #define N_KEYBOARD_COL 3 // Keyboard Spalten #define N_CV_GATES 2 // CV-Ausgänge #define N_SB 2 // Sequencer // I2C Pins #define PIN_SDA 15 #define PIN_SCL 16 // Keyboard Pins (Reihen) #define PIN_K_R0 7 #define PIN_K_R1 8 #define PIN_K_R2 9 #define PIN_K_R3 10 // Keyboard Pins (Spalten) #define PIN_K_C0 1 #define PIN_K_C1 2 #define PIN_K_C2 4 ``` ### Spannung-Mapping `src/main.cpp`: ```cpp // Voltage für jede Tastaturposition (in 1/12V = 83mV Schritten = 1 Halbtonschritt) uint16_t keyToVoltage[N_KEYBOARD_ROW*N_KEYBOARD_COL] = { 1*83, 5*83, 9*83, // Row 0: C, E, G 2*83, 6*83, 10*83, // Row 1: D, F, A 3*83, 7*83, 11*83, // Row 2: E, G, B 4*83, 8*83, 12*83 // Row 3: F, A, C (Oktave) }; ``` --- ## Hauptablauf ### Main Loop Flowchart ``` START ↓ SETUP (Initialisierung) ↓ ┌─── MAIN LOOP ───────────────────────┐ │ │ ├─ Keyboard Update │ │ (Tasten auslesen) │ │ │ ├─ Button Handler │ │ (Record/Play Buttons) │ │ │ ├─ Sequencer Update │ │ (sb1 & sb2 Playback) │ │ │ ├─ Spannungen bestimmen │ │ voltage_ch1 = Queue[0] │ │ voltage_ch2 = Queue[1] │ │ │ ├─ Recording Check │ │ IF Recording: addStep() │ │ │ ├─ Output Priority │ │ IF sb1.playing() → Output SEQ1 │ │ ELSE IF sb2.playing() → Output SEQ2│ │ ELSE → Output Live │ │ │ ├─ Time-Limit Check │ │ IF Limit reached: Stop Record │ │ │ ├─ Delay 10ms │ │ │ └─────────────────────────────────────┘ ↓ [Loop zurück] ``` ### State Machine | State | Beschreibung | Aktion | |-------|-------------|--------| | **IDLE** | Leerlauf | Liest Tasten, gibt Live-Spannungen aus | | **REC** | Recording aktiv | Speichert Sequenzen, überwacht Zeit-Limits | | **PLAY** | Playback aktiv | Gibt Sequenzen aus, verwaltet Step-Übergänge | | **LOOP** | Endlosschleife | Wiederholt Sequenz nahtlos | ### Ausgabe-Prioritätssystem ``` 1. Sequencer 1 Playing? ├─ JA → Gebe SEQ1 Voltages aus (höchste Priorität) └─ NEIN ↓ 2. Sequencer 2 Playing? ├─ JA → Gebe SEQ2 Voltages aus (zweite Priorität) └─ NEIN ↓ 3. Live Input └─ Gebe Tasten-Voltages aus (Standard) ``` Dies ermöglicht nahtlose Übergänge und verhindert Konflikte. --- ## Code-Beispiele ### Beispiel 1: Live-Modus (main.cpp.1) Direkte Tastatur-zu-CV Verbindung ohne Sequencer: ```cpp void loop() { keyboard.update(); int n = keyboard.getQueueLength(); if(n > 0) { for(int i = 0; (i < N_CV_GATES) && (i < n); i++) { Key k = keyboard.getQueue(i); cv.setVoltage(i, k); // Taste direkt auf CV ausgeben } } else { cv.clearAll(); // Keine Taste → 0V } delay(50); } ``` ### Beispiel 2: Dual-Channel Sequencer (main.cpp) Vollständiger Sequencer mit 2 unabhängigen Kanälen: ```cpp void loop() { keyboard.update(); handleSequencerButtons(); sb1.update(); sb2.update(); // ... Voltage determination ... // Recording if(sb1.isRecording()) { sb1.addStep(voltage_ch1, voltage_ch2); } if(sb2.isRecording()) { sb2.addStep(voltage_ch1, voltage_ch2); } // Output mit Priorität if(sb1.isPlaying()) { cv.setVoltage(0, sb1.getCurrentVoltageCh1()); cv.setVoltage(1, sb1.getCurrentVoltageCh2()); } else if(sb2.isPlaying()) { cv.setVoltage(0, sb2.getCurrentVoltageCh1()); cv.setVoltage(1, sb2.getCurrentVoltageCh2()); } else { cv.setVoltage(0, voltage_ch1); cv.setVoltage(1, voltage_ch2); } delay(10); } ``` --- ## Dateistruktur ``` project/ ├── include/ │ ├── FIRMWARE.h # Klassen-Definitionen │ └── FIRMWARE_DEF.h # Konstanten & Pin-Definitionen │ └── src/ ├── main.cpp # Dual-Channel Sequencer Beispiel ├── main.cpp.1 # Live-Modus Beispiel ├── main.cpp.2 # Dual-Channel ohne Sequencer └── FIRMWARE.cpp # Implementierungen ``` --- ## Technische Spezifikationen ### Timings | Parameter | Wert | Funktion | |-----------|------|----------| | Keyboard Debounce | 20ms | Anti-Prellen Verzögerung | | Button Debounce | 50ms | Button Anti-Prellen | | Main Loop Delay | 10ms | Update-Rate | | Max Recording | 30s | Zeit-Limit pro Sequenz | | Min Step Duration | 50ms | Minimale Step-Länge | ### Voltage-Mapping Verwendet gleichmäßige 83mV Schritte (1/12 Oktave): ``` Key (1,0) = 1 × 83mV = 83mV (C) Key (1,1) = 5 × 83mV = 415mV (E) Key (1,2) = 9 × 83mV = 747mV (G) ... etc Key (4,2) = 12 × 83mV = 996mV (C') ``` --- **Zuletzt aktualisiert:** 2025-11-30