Impplemenation of EN-Pins for CV-Gates, Metronome

This commit is contained in:
2025-12-06 11:48:11 +01:00
parent 06fa584b6d
commit 7c8a90ce7d
9 changed files with 201 additions and 94 deletions

View File

@@ -4,7 +4,7 @@
@contact: etoth@tsn.at
@date: 2025-10-26
@updated: 2025-12-06
@brief: Header for FIRMWARE.cpp (FIXED)
@brief: Header for FIRMWARE.cpp (FIXED VERSION)
*/
#include <Arduino.h>
#include <Wire.h>
@@ -30,6 +30,7 @@ struct DualVoltageDurationPair
uint16_t voltage_ch1;
uint16_t voltage_ch2;
uint16_t duration;
bool active; // NEU: true wenn Step aktive Noten hat, false für Pausen
};
const Key NOT_A_KEY = {-1, -1};
@@ -117,6 +118,7 @@ class SequencerBlock
uint16_t getStepCount();
uint16_t getCurrentVoltageCh1();
uint16_t getCurrentVoltageCh2();
bool isCurrentStepActive(); // NEU: Prüft ob aktueller Step aktive Noten hat
uint16_t getTotalDuration();
private:
@@ -124,7 +126,7 @@ class SequencerBlock
* @brief Memory limiting
* @return (uint16_t) 1024
* @attention Increasing the value might lead to an overflow
* @note sizeOf(DualVoltageDurationPair) = 6 Byte ==> 6 Byte * 1024 = 6144 Byte
* @note sizeOf(DualVoltageDurationPair) = 8 Byte ==> 8 Byte * 1024 = 8192 Byte
*/
const static uint16_t _MAX_SEQUENCE_STEPS = 1024;

View File

@@ -14,35 +14,36 @@
// CONSTANTS DEFINITONS
#define N_KEYBOARD_ROW 4 // for PROD. change to 5
#define N_KEYBOARD_COL 3 // for PROD. change to 5
#define N_CV_GATES 2
#define N_SB 2
#define N_CV_GATES 2 // PROD. OK
#define N_SB 2 // PROD. OK
#define BAUDRATE 115200
#define N_MAX_SEQ_STEPS 512
// PIN DEFENTITIONS
// I2C PINS
#define PIN_SDA 15
#define PIN_SCL 16
#define PIN_SDA 15 // PROD. pin OK
#define PIN_SCL 16 // PROD. pin OK
// KEYBOARD PINS
#define PIN_K_R0 7
#define PIN_K_R1 8
#define PIN_K_R2 9
#define PIN_K_R3 10
#define PIN_K_R4 11 // DEV. not in use
#define PIN_K_C0 1
#define PIN_K_C1 2
#define PIN_K_C2 4
#define PIN_K_C3 5 // DEV. not in use
#define PIN_K_C4 6 // DEV. not in use
#define PIN_K_R0 7 // PROD. pin OK
#define PIN_K_R1 8 // PROD. pin OK
#define PIN_K_R2 9 // PROD. pin OK
#define PIN_K_R3 10 // PROD. pin OK
#define PIN_K_R4 11 // DEV. not in use - PROD. pin OK
#define PIN_K_C0 1 // PROD. pin OK
#define PIN_K_C1 2 // PROD. pin OK
#define PIN_K_C2 4 // PROD. pin OK
#define PIN_K_C3 5 // DEV. not in use - PROD. pin OK
#define PIN_K_C4 6 // DEV. not in use - PROD. pin OK
// SEQUENCER BUTTON PINS
#define PIN_SB_1_REC 42 // for PROD. change to 33 / not available on dev board
#define PIN_SB_1_PLAY 41 // for PROD. change to 34 / not available on dev board
#define PIN_SB_2_REC 40 // 35
#define PIN_SB_2_PLAY 39 // 36
#define PIN_SB_1_REC 38 // for PROD. change to 33 / not available on dev board
#define PIN_SB_1_PLAY 37 // for PROD. change to 34 / not available on dev board
#define PIN_SB_2_REC 35 // 35
#define PIN_SB_2_PLAY 36 // 36
// MISC/INFO PINS
#define PIN_ACTIVE -1 // TODO: if any key is played return HIGH
#define PIN_REC -1 // TODO: if any sb is recording return HIGH
#define PIN_BPM -1 // TODO: get bpm through potentiometer analog value
#define PIN_B_METRONOME -1 // TODO: button activates/deactivates bpm led output
#define PIN_L_METRONOME -1 // TODO: led blinks according to bpm value
#define PIN_VCO1_EN 41 // PROD. pin 37 TODO: if there is an active key mapped to CV-Gate 1 --> HIGH
#define PIN_VCO2_EN 40 // PROD. pin 38 TODO: if there is an active key mapped to CV-Gate 2 --> HIGH
#define PIN_REC 39 // PROD. pin 39 TODO: if any sb is recording LED on (active-low)
#define PIN_BPM 12 // PROD. pin 12 TODO: get bpm through potentiometer analog value -> ADC-Pin
#define PIN_B_METRONOME 13 // PROD. pin 13 TODO: button activates/deactivates bpm led output (pull-up)
#define PIN_L_METRONOME 14 // PROD. pin 14 TODO: led blinks according to bpm value (active-low)
#endif

View File

@@ -302,7 +302,7 @@ void SequencerBlock::addStep(uint16_t voltage_ch1, uint16_t voltage_ch2)
unsigned long now = millis();
// NEU: Rate-Limiting - ignoriere zu häufige Aufrufe
if((now - _lastAddStepTime) < 5) // Mindestens 5ms zwischen Updates
if((unsigned long)(now - _lastAddStepTime) < 5)
{
return;
}
@@ -333,6 +333,7 @@ void SequencerBlock::addStep(uint16_t voltage_ch1, uint16_t voltage_ch2)
_sequence[_stepCount].voltage_ch1 = voltage_ch1;
_sequence[_stepCount].voltage_ch2 = voltage_ch2;
_sequence[_stepCount].duration = 0;
_sequence[_stepCount].active = (voltage_ch1 > 0 || voltage_ch2 > 0); // NEU: Prüfe ob Note aktiv
_stepCount++;
_lastStepTime = now;
@@ -452,6 +453,7 @@ void SequencerBlock::clear()
_sequence[i].voltage_ch1 = 0;
_sequence[i].voltage_ch2 = 0;
_sequence[i].duration = 0;
_sequence[i].active = false;
}
}
@@ -506,6 +508,14 @@ uint16_t SequencerBlock::getTotalDuration()
return (total > 65535) ? 65535 : (uint16_t)total; // Clamp auf uint16
}
bool SequencerBlock::isCurrentStepActive()
{
if(!_isPlaying || _stepCount == 0) return false;
if(_currentStep >= _stepCount || _currentStep >= _MAX_SEQUENCE_STEPS) return false;
return _sequence[_currentStep].active;
}
void SequencerBlock::_finishCurrentStep()
{
if(_stepCount == 0) return;

View File

@@ -1,9 +1,7 @@
/*
* Example Code Three - Dual Channel Sequencer (FIXED)
* - Bounds Checks hinzugefügt
* - Rate-Limiting implementiert
* - Debug-Ausgaben erweitert
* - Stack Overflow verhindert
* Example Code Three - Dual Channel Sequencer (COMPLETE)
* - Alle TODOs implementiert
* - VCO Gates, Recording LED, Metronome
*/
#include "FIRMWARE_DEF.h"
#include "FIRMWARE.h"
@@ -15,16 +13,15 @@ Keyboard keyboard(N_KEYBOARD_ROW, N_KEYBOARD_COL, pins_keyboard_row, pins_keyboa
Adafruit_MCP4728 MCP4728;
MCP4728_channel_t cvMap[N_CV_GATES] = {MCP4728_CHANNEL_A, MCP4728_CHANNEL_B};
uint16_t keyToVoltage[N_KEYBOARD_ROW*N_KEYBOARD_COL] = { /* 83mV = 1/12V */
1*83, 5*83, 9*83, /* ROW 1: B D Fis */
2*83, 6*83, 10*83, /* ROW 2: H Dis G */
3*83, 7*83, 11*83, /* ROW 3: C E Gis */
4*83, 8*83, 12*83 /* ROW 4: Cis F A' */
uint16_t keyToVoltage[N_KEYBOARD_ROW*N_KEYBOARD_COL] = {
1*83, 5*83, 9*83,
2*83, 6*83, 10*83,
3*83, 7*83, 11*83,
4*83, 8*83, 12*83
};
CV cv(&MCP4728, &Wire, N_CV_GATES, cvMap, keyToVoltage, N_KEYBOARD_ROW, N_KEYBOARD_COL);
// Sequencer 30s max, 512 max Steps
SequencerBlock sb1(30000, N_MAX_SEQ_STEPS);
SequencerBlock sb2(30000, N_MAX_SEQ_STEPS);
@@ -39,14 +36,12 @@ ButtonState btn_sb1_rec;
ButtonState btn_sb1_play;
ButtonState btn_sb2_rec;
ButtonState btn_sb2_play;
ButtonState btn_metronome;
const unsigned long DEBOUNCE_DELAY = 50;
// Loop-Status für State Machine
static bool seq1_loop_active = false;
static bool seq2_loop_active = false;
// NEU: Tracking für Voltage Changes
static uint16_t last_voltage_ch1 = 0xFFFF;
static uint16_t last_voltage_ch2 = 0xFFFF;
@@ -82,6 +77,7 @@ void initButtons()
pinMode(PIN_SB_1_PLAY, INPUT_PULLUP);
pinMode(PIN_SB_2_REC, INPUT_PULLUP);
pinMode(PIN_SB_2_PLAY, INPUT_PULLUP);
pinMode(PIN_B_METRONOME, INPUT_PULLUP);
btn_sb1_rec.current = false;
btn_sb1_rec.last = false;
@@ -98,6 +94,30 @@ void initButtons()
btn_sb2_play.current = false;
btn_sb2_play.last = false;
btn_sb2_play.lastDebounceTime = 0;
btn_metronome.current = false;
btn_metronome.last = false;
btn_metronome.lastDebounceTime = 0;
}
void initOutputs()
{
// VCO Gates
pinMode(PIN_VCO1_EN, OUTPUT);
pinMode(PIN_VCO2_EN, OUTPUT);
digitalWrite(PIN_VCO1_EN, LOW);
digitalWrite(PIN_VCO2_EN, LOW);
// Recording LED (active-low)
pinMode(PIN_REC, OUTPUT);
digitalWrite(PIN_REC, HIGH); // OFF
// Metronome LED (active-low)
pinMode(PIN_L_METRONOME, OUTPUT);
digitalWrite(PIN_L_METRONOME, HIGH); // OFF
// BPM Potentiometer
pinMode(PIN_BPM, INPUT);
}
void handleSequencerButtons()
@@ -115,13 +135,13 @@ void handleSequencerButtons()
{
if(sb1.isPlaying()) sb1.stopPlay();
sb1.startRecord();
last_voltage_ch1 = 0xFFFF; // Reset voltage tracking
last_voltage_ch1 = 0xFFFF;
last_voltage_ch2 = 0xFFFF;
Serial.printf("\n\r[SEQ1] Recording started (2 channels)...");
}
}
// ===== Sequencer 1 Play Button (3 Stati: Play / Loop / Stop) =====
// ===== Sequencer 1 Play Button =====
if(readButton(PIN_SB_1_PLAY, btn_sb1_play))
{
if(!sb1.isPlaying())
@@ -160,13 +180,13 @@ void handleSequencerButtons()
{
if(sb2.isPlaying()) sb2.stopPlay();
sb2.startRecord();
last_voltage_ch1 = 0xFFFF; // Reset voltage tracking
last_voltage_ch1 = 0xFFFF;
last_voltage_ch2 = 0xFFFF;
Serial.printf("\n\r[SEQ2] Recording started (2 channels)...");
}
}
// ===== Sequencer 2 Play Button (3 Stati: Play / Loop / Stop) =====
// ===== Sequencer 2 Play Button =====
if(readButton(PIN_SB_2_PLAY, btn_sb2_play))
{
if(!sb2.isPlaying())
@@ -193,16 +213,88 @@ void handleSequencerButtons()
}
}
static bool metronome_enabled = false;
static uint16_t current_bpm = 120;
static unsigned long last_beat_time = 0;
static unsigned long last_pulse_end_time = 0;
static bool metronome_led_on = false;
void updateMetronome()
{
unsigned long now = millis();
// BPM von Potentiometer lesen (alle 100ms)
static unsigned long last_bpm_read = 0;
if((now - last_bpm_read) > 100)
{
int adc_value = analogRead(PIN_BPM);
// Map ADC (0-4095) zu BPM (40-240)
current_bpm = map(adc_value, 0, 4095, 40, 240);
last_bpm_read = now;
}
// Metronome Button (Toggle)
if(readButton(PIN_B_METRONOME, btn_metronome))
{
metronome_enabled = !metronome_enabled;
Serial.printf("\n\r[METRONOME] %s (BPM: %d)",
metronome_enabled ? "ON" : "OFF", current_bpm);
if(!metronome_enabled)
{
digitalWrite(PIN_L_METRONOME, HIGH); // Active-low: HIGH = OFF
metronome_led_on = false;
}
}
if(!metronome_enabled) return;
// Berechne Beat-Intervall in ms
unsigned long beat_interval = 60000UL / current_bpm;
// Neue Beat?
if((now - last_beat_time) >= beat_interval)
{
digitalWrite(PIN_L_METRONOME, LOW); // Active-low: LOW = ON
metronome_led_on = true;
last_beat_time = now;
last_pulse_end_time = now + 50; // 50ms Pulse
}
// Pulse beenden?
if(metronome_led_on && (now >= last_pulse_end_time))
{
digitalWrite(PIN_L_METRONOME, HIGH); // Active-low: HIGH = OFF
metronome_led_on = false;
}
}
void updateVCOGates(bool cv1_active, bool cv2_active)
{
// PIN_VCO1_EN: HIGH wenn CV1 aktiv (Key mapped to CV-Gate 1)
digitalWrite(PIN_VCO1_EN, cv1_active ? HIGH : LOW);
// PIN_VCO2_EN: HIGH wenn CV2 aktiv (Key mapped to CV-Gate 2)
digitalWrite(PIN_VCO2_EN, cv2_active ? HIGH : LOW);
}
void updateRecordingLED()
{
// PIN_REC: Active-low (LOW = LED ON)
bool any_recording = sb1.isRecording() || sb2.isRecording();
digitalWrite(PIN_REC, any_recording ? LOW : HIGH);
}
void setup()
{
Serial.begin(BAUDRATE);
delay(2000);
Serial.printf("\n\r=== FIXED VERSION v2 ===");
Serial.printf("\n\r=== COMPLETE VERSION with TODOs ===");
Serial.printf("\n\rSerial OK!");
keyboard.begin();
// Fehlerbehandlung für CV-Initialisierung
unsigned long timeout = millis() + 5000;
while(!cv.begin(PIN_SDA, PIN_SCL))
{
@@ -219,25 +311,16 @@ void setup()
Serial.printf("\n\r[OK] CV initialized");
initButtons();
initOutputs();
sb1.setLoop(false);
sb2.setLoop(false);
Serial.printf("\n\r=== Dual-Channel Sequencer System Started ===");
Serial.printf("\n\rControls:");
Serial.printf("\n\r PIN_SB_1_REC: SEQ1 Record Start/Stop (CH1+CH2)");
Serial.printf("\n\r PIN_SB_1_PLAY: SEQ1 Play Mode Toggle:");
Serial.printf("\n\r 1st click: Play once");
Serial.printf("\n\r 2nd click: Loop mode");
Serial.printf("\n\r 3rd click: Stop");
Serial.printf("\n\r PIN_SB_2_REC: SEQ2 Record Start/Stop (CH1+CH2)");
Serial.printf("\n\r PIN_SB_2_PLAY: SEQ2 Play Mode (same as SEQ1)");
Serial.printf("\n\r");
Serial.printf("\n\rFIXES:");
Serial.printf("\n\r - Bounds checks in all array accesses");
Serial.printf("\n\r - Rate limiting (5ms) for addStep()");
Serial.printf("\n\r - Only call addStep() on voltage change");
Serial.printf("\n\r - Stack overflow prevention");
Serial.printf("\n\rFeatures:");
Serial.printf("\n\r - VCO1/VCO2 Gate Outputs");
Serial.printf("\n\r - Recording LED Indicator");
Serial.printf("\n\r - BPM Metronome (40-240 BPM)");
Serial.printf("\n\r==============================================\n\r");
}
@@ -249,42 +332,44 @@ void loop()
loopCounter++;
// Debug-Ausgabe alle 5 Sekunden
if(millis() - lastDebugPrint > 5000)
{
Serial.printf("\n\r[HEARTBEAT] Loop count: %lu", loopCounter);
Serial.printf("\n\r[HEARTBEAT] Loop: %lu | BPM: %d | Metro: %s",
loopCounter, current_bpm, metronome_enabled ? "ON" : "OFF");
Serial.printf("\n\r[DEBUG] SB1: Rec=%d, Play=%d, Steps=%d",
sb1.isRecording(), sb1.isPlaying(), sb1.getStepCount());
Serial.printf("\n\r[DEBUG] SB2: Rec=%d, Play=%d, Steps=%d",
sb2.isRecording(), sb2.isPlaying(), sb2.getStepCount());
Serial.printf("\n\r[DEBUG] Free heap: %lu bytes", ESP.getFreeHeap());
lastDebugPrint = millis();
}
// ===== NON-BLOCKING TIMING SYSTEM =====
// ===== NON-BLOCKING TIMING =====
static unsigned long lastLoopTime = 0;
unsigned long now = millis();
const unsigned long LOOP_INTERVAL = 10; // 10ms
const unsigned long LOOP_INTERVAL = 10;
if((now - lastLoopTime) < LOOP_INTERVAL)
{
return; // Nicht blockierend
return;
}
lastLoopTime = now;
// ===== NORMALE UPDATE-FUNKTIONEN =====
// ===== UPDATE FUNCTIONS =====
keyboard.update();
handleSequencerButtons();
updateMetronome();
updateRecordingLED();
// Sequencer Update (für Wiedergabe)
sb1.update();
sb2.update();
int n = keyboard.getQueueLength();
// Aktuelle Spannungen für beide Kanäle ermitteln
// Aktuelle Spannungen ermitteln
uint16_t voltage_ch1 = 0;
uint16_t voltage_ch2 = 0;
bool cv1_active = false;
bool cv2_active = false;
if(n > 0)
{
@@ -292,6 +377,7 @@ void loop()
if(!isNotKey(k1))
{
voltage_ch1 = keyToVoltage[k1.row * N_KEYBOARD_COL + k1.col];
cv1_active = true;
}
}
@@ -301,51 +387,59 @@ void loop()
if(!isNotKey(k2))
{
voltage_ch2 = keyToVoltage[k2.row * N_KEYBOARD_COL + k2.col];
cv2_active = true;
}
}
// Bei Recording: Beide Kanäle aufnehmen - NUR bei Änderung!
// Recording
bool voltageChanged = (voltage_ch1 != last_voltage_ch1) || (voltage_ch2 != last_voltage_ch2);
if(sb1.isRecording())
if(sb1.isRecording() && voltageChanged)
{
if(voltageChanged)
{
sb1.addStep(voltage_ch1, voltage_ch2);
last_voltage_ch1 = voltage_ch1;
last_voltage_ch2 = voltage_ch2;
}
sb1.addStep(voltage_ch1, voltage_ch2);
last_voltage_ch1 = voltage_ch1;
last_voltage_ch2 = voltage_ch2;
}
if(sb2.isRecording())
if(sb2.isRecording() && voltageChanged)
{
if(voltageChanged)
{
sb2.addStep(voltage_ch1, voltage_ch2);
last_voltage_ch1 = voltage_ch1;
last_voltage_ch2 = voltage_ch2;
}
sb2.addStep(voltage_ch1, voltage_ch2);
last_voltage_ch1 = voltage_ch1;
last_voltage_ch2 = voltage_ch2;
}
// CV-Ausgabe: Priorität hat Sequencer-Wiedergabe
// CV-Ausgabe & VCO Gates
if(sb1.isPlaying())
{
cv.setVoltage(0, sb1.getCurrentVoltageCh1());
cv.setVoltage(1, sb1.getCurrentVoltageCh2());
uint16_t seq_v1 = sb1.getCurrentVoltageCh1();
uint16_t seq_v2 = sb1.getCurrentVoltageCh2();
cv.setVoltage(0, seq_v1);
cv.setVoltage(1, seq_v2);
// KORREKT: Nutze isCurrentStepActive() statt Spannung > 0
// Da 0V eine gültige Note sein kann!
bool gate_active = sb1.isCurrentStepActive();
updateVCOGates(gate_active, gate_active);
}
else if(sb2.isPlaying())
{
cv.setVoltage(0, sb2.getCurrentVoltageCh1());
cv.setVoltage(1, sb2.getCurrentVoltageCh2());
uint16_t seq_v1 = sb2.getCurrentVoltageCh1();
uint16_t seq_v2 = sb2.getCurrentVoltageCh2();
cv.setVoltage(0, seq_v1);
cv.setVoltage(1, seq_v2);
bool gate_active = sb2.isCurrentStepActive();
updateVCOGates(gate_active, gate_active);
}
else
{
// Live-Ausgabe wenn kein Sequencer spielt
// Live-Modus: cv1_active/cv2_active basieren auf tatsächlich gedrückten Tasten
cv.setVoltage(0, voltage_ch1);
cv.setVoltage(1, voltage_ch2);
updateVCOGates(cv1_active, cv2_active);
}
// Time-Limit Warnung
// Time-Limit Check
if(sb1.isRecording() && sb1.timeLimitReached())
{
sb1.stopRecord();