Implementiere USB-MIDI-Controller für Arduino Pro Micro mit Hardware- und Software-Architektur, einschließlich Button- und LED-Steuerung, Multiplexer-Integration und MIDI-Kommunikation.

This commit is contained in:
2025-07-19 15:46:14 +02:00
parent f6cbffe217
commit e64048e99b
13 changed files with 820 additions and 11 deletions

74
src/button_handler.cpp Normal file
View File

@@ -0,0 +1,74 @@
#include "button_handler.h"
#include "config.h"
ButtonHandler::ButtonHandler(Adafruit_MCP23X17* mcp1, Adafruit_MCP23X17* mcp2) {
this->mcp1 = mcp1;
this->mcp2 = mcp2;
// Initialisiere alle Button-Zustände
for(int i = 0; i < NUM_BUTTONS; i++) {
buttonStates[i] = false;
lastButtonStates[i] = false;
lastDebounceTime[i] = 0;
}
}
void ButtonHandler::init() {
// Konfiguriere alle Button-Pins als Eingänge mit Pull-Up
// MCP1: Buttons 0-15 (Port A und B)
for(int i = 0; i < 16; i++) {
mcp1->pinMode(i, INPUT_PULLUP);
}
// MCP2: Buttons 16-19 (Port A, Pins 0-3)
for(int i = 0; i < 4; i++) {
mcp2->pinMode(i, INPUT_PULLUP);
}
Serial.println("Button Handler initialisiert - 20 Buttons konfiguriert");
}
void ButtonHandler::update() {
unsigned long currentTime = millis();
// Lese alle Button-Zustände
for(int i = 0; i < NUM_BUTTONS; i++) {
bool currentState;
// Bestimme welcher MCP und welcher Pin
if(i < 16) {
// Buttons 0-15 auf MCP1
currentState = !mcp1->digitalRead(i); // Invertiert wegen Pull-Up
} else {
// Buttons 16-19 auf MCP2
currentState = !mcp2->digitalRead(i - 16); // Invertiert wegen Pull-Up
}
// Debouncing: Prüfe ob genug Zeit vergangen ist
if(currentTime - lastDebounceTime[i] > DEBOUNCE_TIME) {
if(currentState != lastButtonStates[i]) {
// Zustand hat sich geändert - speichere neuen Zustand
lastButtonStates[i] = buttonStates[i];
buttonStates[i] = currentState;
lastDebounceTime[i] = currentTime;
}
}
}
}
bool ButtonHandler::isPressed(uint8_t buttonIndex) {
if(buttonIndex >= NUM_BUTTONS) return false;
return buttonStates[buttonIndex];
}
bool ButtonHandler::wasPressed(uint8_t buttonIndex) {
if(buttonIndex >= NUM_BUTTONS) return false;
// Button wurde gedrückt wenn er jetzt gedrückt ist, aber vorher nicht
return buttonStates[buttonIndex] && !lastButtonStates[buttonIndex];
}
bool ButtonHandler::wasReleased(uint8_t buttonIndex) {
if(buttonIndex >= NUM_BUTTONS) return false;
// Button wurde losgelassen wenn er jetzt nicht gedrückt ist, aber vorher schon
return !buttonStates[buttonIndex] && lastButtonStates[buttonIndex];
}

72
src/led_controller.cpp Normal file
View File

@@ -0,0 +1,72 @@
#include "led_controller.h"
#include "config.h"
LEDController::LEDController(Adafruit_MCP23X17* mcp1, Adafruit_MCP23X17* mcp2) {
this->mcp1 = mcp1;
this->mcp2 = mcp2;
// Initialisiere alle LED-Zustände als AUS
for(int i = 0; i < NUM_LEDS; i++) {
ledStates[i] = false;
}
}
void LEDController::init() {
// Konfiguriere alle LED-Pins als Ausgänge
// MCP1: LEDs 0-15 (Port A und B)
// Port A = Pins 0-7, Port B = Pins 8-15
for(int i = 0; i < 16; i++) {
mcp1->pinMode(i, OUTPUT);
mcp1->digitalWrite(i, LOW); // LEDs initial ausschalten
}
// MCP2: LEDs 16-19 (Port A, Pins 4-7)
// Pins 0-3 sind für Buttons reserviert, LEDs nutzen Pins 4-7
for(int i = 4; i < 8; i++) {
mcp2->pinMode(i, OUTPUT);
mcp2->digitalWrite(i, LOW); // LEDs initial ausschalten
}
Serial.println("LED Controller initialisiert - 20 LEDs konfiguriert");
}
void LEDController::setLED(uint8_t ledIndex, bool state) {
if(ledIndex >= NUM_LEDS) return;
ledStates[ledIndex] = state;
// Aktualisiere Hardware sofort
if(ledIndex < 16) {
// LEDs 0-15 auf MCP1
mcp1->digitalWrite(ledIndex, state ? HIGH : LOW);
} else {
// LEDs 16-19 auf MCP2 (Pins 4-7)
mcp2->digitalWrite((ledIndex - 16) + 4, state ? HIGH : LOW);
}
}
void LEDController::toggleLED(uint8_t ledIndex) {
if(ledIndex >= NUM_LEDS) return;
setLED(ledIndex, !ledStates[ledIndex]);
}
bool LEDController::getLEDState(uint8_t ledIndex) {
if(ledIndex >= NUM_LEDS) return false;
return ledStates[ledIndex];
}
void LEDController::updateHardware() {
// Aktualisiere alle LED-Zustände in der Hardware
// Diese Funktion kann aufgerufen werden, um sicherzustellen,
// dass alle LEDs den gewünschten Zustand haben
for(int i = 0; i < NUM_LEDS; i++) {
if(i < 16) {
// LEDs 0-15 auf MCP1
mcp1->digitalWrite(i, ledStates[i] ? HIGH : LOW);
} else {
// LEDs 16-19 auf MCP2 (Pins 4-7)
mcp2->digitalWrite((i - 16) + 4, ledStates[i] ? HIGH : LOW);
}
}
}

View File

@@ -1,18 +1,171 @@
#include <Arduino.h>
/*
* USB-MIDI-Controller für Arduino Pro Micro (ATmega32u4)
*
* Hardware:
* - 2x MCP23017 I/O Expander (I2C)
* - 1x CD74HC4067 Multiplexer (16-Kanal, analog)
* - 8x Drehpotis + 4x Schieberegler (12 analoge Regler total)
* - 20x Buttons (Tact Switches)
* - 20x LEDs (optisches Feedback)
*
* Funktionen:
* - Analoge Regler senden MIDI-CC (CC1-CC12)
* - Buttons senden MIDI Note-On/Off
* - LEDs werden via eingehende MIDI-Nachrichten angesteuert
* - USB-MIDI Kommunikation
*/
// put function declarations here:
int myFunction(int, int);
#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_MCP23X17.h>
// Eigene Module
#include "config.h"
#include "multiplexer.h"
#include "button_handler.h"
#include "led_controller.h"
#include "midi_handler.h"
// Hardware-Objekte
Adafruit_MCP23X17 mcp1; // Erste MCP23017 (Adresse 0x20)
Adafruit_MCP23X17 mcp2; // Zweite MCP23017 (Adresse 0x21)
// Modul-Objekte
Multiplexer mux(MUX_S0, MUX_S1, MUX_S2, MUX_S3, MUX_ANALOG_PIN);
ButtonHandler buttons(&mcp1, &mcp2);
LEDController leds(&mcp1, &mcp2);
// Variablen für Timing
unsigned long lastAnalogRead = 0;
unsigned long lastButtonRead = 0;
unsigned long lastMIDIRead = 0;
// Timing-Intervalle (in Milliseconds)
const unsigned long ANALOG_READ_INTERVAL = 10; // 100Hz für analoge Regler
const unsigned long BUTTON_READ_INTERVAL = 5; // 200Hz für Buttons (responsive)
const unsigned long MIDI_READ_INTERVAL = 1; // 1000Hz für MIDI (sehr responsive)
// Callback-Funktionen für eingehende MIDI-Nachrichten
void onMIDINoteOn(uint8_t note, uint8_t velocity) {
// Suche die entsprechende LED für diese Note
for(int i = 0; i < NUM_BUTTONS; i++) {
if(BUTTON_NOTE_MAP[i] == note) {
leds.setLED(i, true);
Serial.print("LED ");
Serial.print(i);
Serial.print(" eingeschaltet für Note ");
Serial.println(note);
break;
}
}
}
void onMIDINoteOff(uint8_t note, uint8_t velocity) {
// Suche die entsprechende LED für diese Note
for(int i = 0; i < NUM_BUTTONS; i++) {
if(BUTTON_NOTE_MAP[i] == note) {
leds.setLED(i, false);
Serial.print("LED ");
Serial.print(i);
Serial.print(" ausgeschaltet für Note ");
Serial.println(note);
break;
}
}
}
void setup() {
// put your setup code here, to run once:
int result = myFunction(2, 3);
// Serielle Kommunikation für Debugging
Serial.begin(115200);
while(!Serial && millis() < 3000); // Warte max. 3 Sekunden auf Serial
Serial.println("=== USB-MIDI-Controller Start ===");
Serial.println("Initialisiere Hardware...");
// I2C initialisieren
Wire.begin();
Serial.println("I2C Bus initialisiert");
// MCP23017 Chips initialisieren
if (!mcp1.begin_I2C(MCP1_ADDRESS)) {
Serial.println("FEHLER: MCP23017 #1 nicht gefunden!");
while(1); // Stoppe hier bei Fehler
}
Serial.println("MCP23017 #1 (0x20) initialisiert");
if (!mcp2.begin_I2C(MCP2_ADDRESS)) {
Serial.println("FEHLER: MCP23017 #2 nicht gefunden!");
while(1); // Stoppe hier bei Fehler
}
Serial.println("MCP23017 #2 (0x21) initialisiert");
// Module initialisieren
mux.init();
buttons.init();
leds.init();
MIDIHandler::init();
// MIDI-Callbacks registrieren
MIDIHandler::onNoteOnCallback = onMIDINoteOn;
MIDIHandler::onNoteOffCallback = onMIDINoteOff;
// LED-Test: Alle LEDs kurz blinken lassen
Serial.println("LED-Test...");
for(int i = 0; i < NUM_LEDS; i++) {
leds.setLED(i, true);
delay(50);
leds.setLED(i, false);
}
Serial.println("=== Initialisierung abgeschlossen ===");
Serial.println("MIDI-Controller bereit!");
Serial.println("");
}
void loop() {
// put your main code here, to run repeatedly:
}
// put function definitions here:
int myFunction(int x, int y) {
return x + y;
unsigned long currentTime = millis();
// 1. Analoge Regler verarbeiten (Potis & Schieberegler)
if(currentTime - lastAnalogRead >= ANALOG_READ_INTERVAL) {
lastAnalogRead = currentTime;
for(int i = 0; i < NUM_ANALOG_CONTROLS; i++) {
if(mux.hasChanged(i, ANALOG_THRESHOLD)) {
// Lese den aktuellen Wert
int rawValue = mux.readChannel(i);
// Konvertiere zu MIDI-Wert (0-127)
uint8_t midiValue = map(rawValue, 0, 1023, 0, 127);
// Sende MIDI-CC
MIDIHandler::sendControlChange(ANALOG_CC_MAP[i], midiValue);
}
}
}
// 2. Buttons verarbeiten
if(currentTime - lastButtonRead >= BUTTON_READ_INTERVAL) {
lastButtonRead = currentTime;
// Button-Zustände aktualisieren
buttons.update();
// Prüfe auf Button-Events
for(int i = 0; i < NUM_BUTTONS; i++) {
if(buttons.wasPressed(i)) {
// Button wurde gedrückt - sende Note-On
MIDIHandler::sendNoteOn(BUTTON_NOTE_MAP[i], 127);
}
else if(buttons.wasReleased(i)) {
// Button wurde losgelassen - sende Note-Off
MIDIHandler::sendNoteOff(BUTTON_NOTE_MAP[i], 0);
}
}
}
// 3. Eingehende MIDI-Nachrichten verarbeiten
if(currentTime - lastMIDIRead >= MIDI_READ_INTERVAL) {
lastMIDIRead = currentTime;
MIDIHandler::processIncomingMIDI();
}
}

106
src/midi_handler.cpp Normal file
View File

@@ -0,0 +1,106 @@
#include "midi_handler.h"
#include "config.h"
// Statische Callback-Pointer initialisieren
void (*MIDIHandler::onNoteOnCallback)(uint8_t note, uint8_t velocity) = nullptr;
void (*MIDIHandler::onNoteOffCallback)(uint8_t note, uint8_t velocity) = nullptr;
void MIDIHandler::init() {
// MIDI USB wird automatisch initialisiert
Serial.println("MIDI Handler initialisiert - USB MIDI bereit");
}
void MIDIHandler::noteOn(byte channel, byte pitch, byte velocity) {
midiEventPacket_t noteOn = {0x09, 0x90 | channel, pitch, velocity};
MidiUSB.sendMIDI(noteOn);
}
void MIDIHandler::noteOff(byte channel, byte pitch, byte velocity) {
midiEventPacket_t noteOff = {0x08, 0x80 | channel, pitch, velocity};
MidiUSB.sendMIDI(noteOff);
}
void MIDIHandler::controlChange(byte channel, byte control, byte value) {
midiEventPacket_t event = {0x0B, 0xB0 | channel, control, value};
MidiUSB.sendMIDI(event);
}
void MIDIHandler::sendNoteOn(uint8_t note, uint8_t velocity) {
noteOn(MIDI_CHANNEL - 1, note, velocity); // MIDI-Kanäle sind 0-basiert
MidiUSB.flush();
Serial.print("MIDI Note-On gesendet: Note ");
Serial.print(note);
Serial.print(", Velocity ");
Serial.println(velocity);
}
void MIDIHandler::sendNoteOff(uint8_t note, uint8_t velocity) {
noteOff(MIDI_CHANNEL - 1, note, velocity); // MIDI-Kanäle sind 0-basiert
MidiUSB.flush();
Serial.print("MIDI Note-Off gesendet: Note ");
Serial.print(note);
Serial.print(", Velocity ");
Serial.println(velocity);
}
void MIDIHandler::sendControlChange(uint8_t controller, uint8_t value) {
controlChange(MIDI_CHANNEL - 1, controller, value); // MIDI-Kanäle sind 0-basiert
MidiUSB.flush();
Serial.print("MIDI CC gesendet: Controller ");
Serial.print(controller);
Serial.print(", Wert ");
Serial.println(value);
}
void MIDIHandler::processIncomingMIDI() {
midiEventPacket_t rx;
do {
rx = MidiUSB.read();
if (rx.header != 0) {
// Extrahiere MIDI-Daten
uint8_t messageType = rx.byte1 & 0xF0;
uint8_t channel = rx.byte1 & 0x0F;
uint8_t data1 = rx.byte2;
uint8_t data2 = rx.byte3;
// Prüfe auf den konfigurierten MIDI-Kanal
if(channel == (MIDI_CHANNEL - 1)) {
switch(messageType) {
case 0x90: // Note-On
if(data2 > 0) { // Velocity > 0 = echtes Note-On
Serial.print("MIDI Note-On empfangen: Note ");
Serial.print(data1);
Serial.print(", Velocity ");
Serial.println(data2);
if(onNoteOnCallback != nullptr) {
onNoteOnCallback(data1, data2);
}
} else { // Velocity = 0 = Note-Off
Serial.print("MIDI Note-Off empfangen (via Note-On): Note ");
Serial.println(data1);
if(onNoteOffCallback != nullptr) {
onNoteOffCallback(data1, 0);
}
}
break;
case 0x80: // Note-Off
Serial.print("MIDI Note-Off empfangen: Note ");
Serial.println(data1);
if(onNoteOffCallback != nullptr) {
onNoteOffCallback(data1, data2);
}
break;
}
}
}
} while (rx.header != 0);
}

73
src/multiplexer.cpp Normal file
View File

@@ -0,0 +1,73 @@
#include "multiplexer.h"
Multiplexer::Multiplexer(uint8_t s0Pin, uint8_t s1Pin, uint8_t s2Pin, uint8_t s3Pin, uint8_t analogPin) {
this->s0 = s0Pin;
this->s1 = s1Pin;
this->s2 = s2Pin;
this->s3 = s3Pin;
this->analogPin = analogPin;
// Initialisiere alle Werte mit -1 um erste Änderung zu erkennen
for(int i = 0; i < 16; i++) {
lastValues[i] = -1;
lastReadTime[i] = 0;
}
}
void Multiplexer::init() {
// Konfiguriere Multiplexer-Steuerpins als Ausgänge
pinMode(s0, OUTPUT);
pinMode(s1, OUTPUT);
pinMode(s2, OUTPUT);
pinMode(s3, OUTPUT);
// Konfiguriere analogen Eingang
pinMode(analogPin, INPUT);
Serial.println("CD74HC4067 Multiplexer initialisiert");
}
void Multiplexer::selectChannel(uint8_t channel) {
// Stelle sicher dass der Kanal im gültigen Bereich ist
if(channel > 15) return;
// Setze die Multiplexer-Steuerleitungen entsprechend dem Kanal
digitalWrite(s0, (channel & 0x01) ? HIGH : LOW);
digitalWrite(s1, (channel & 0x02) ? HIGH : LOW);
digitalWrite(s2, (channel & 0x04) ? HIGH : LOW);
digitalWrite(s3, (channel & 0x08) ? HIGH : LOW);
// Kurze Verzögerung für Multiplexer-Umschaltung
delayMicroseconds(50);
}
int Multiplexer::readChannel(uint8_t channel) {
if(channel > 15) return -1;
// Wähle den Kanal aus
selectChannel(channel);
// Lese den analogen Wert
int value = analogRead(analogPin);
// Speichere den Wert und die Zeit
lastValues[channel] = value;
lastReadTime[channel] = millis();
return value;
}
bool Multiplexer::hasChanged(uint8_t channel, int threshold) {
if(channel > 15) return false;
// Lese aktuellen Wert
int currentValue = readChannel(channel);
// Prüfe ob sich der Wert signifikant geändert hat
if(lastValues[channel] == -1) {
// Erster Aufruf - als Änderung werten
return true;
}
return abs(currentValue - lastValues[channel]) >= threshold;
}