Skocz do zawartości

Pomocna odpowiedź

Napisano (edytowany)

Wstęp

Tym razem z drugiej strony medalu... nieudany projekt, a raczej projekt, który tak daje w kość, że dalsza jego budowa jest odłożona do momentu, aż będę miał lepszy nastrój. Jako, że posiadam "klawiaturę" MPK Mini IV chciałem sobie zbudować syntezator, który może odtwarzać dźwięki, by nie musieć podłączać się do Abletona.

Artykuł powstał we współpracy z firmą Botland.

(Prawie) wszystkie pliki projektu można znaleźć też na GitHubie.

IMG_1366.thumb.jpg.2c7daf36308dcefb677f298ad34959c1.jpg
Ostateczna forma urządzenia

Na początek o MIDI

MIDI to interfejs UART o dość nietypowym baud rate wynoszącym 32 500. Pierwszym napotkanym problemem było to, iż optotranzystory CNY17-3 nie mają dostatecznej szybkości i sygnał był całkowicie bezużyteczny. Jako, że większość instrumentów używa optotranzystorów 6N138 postanowiłem zajrzeć do noty i zobaczyłem, że posiadają na wyjściu układ Darlingtona. Dodając dodatkowy tranzystor na wyjściu CNY17-3 udało się uzyskać oczekiwany efekt.

Zaprojektowałem płytkę wraz ze złączem TRS 3.5mm, aczkolwiek okazało się, że posiadane przeze mnie gniazda mają błędny symbol, a co za tym idzie są praktycznie bezużyteczne do tego celu. Więcej można poczytać tutaj.

IMG_1345.thumb.jpg.39e6062ff799a7e9002ad664280f4731.jpg
Wadliwa płytka PCB

Szybko podmieniłem złącze 3.5mm na gniazdo XH2.54 i wykonałem drugą płytkę. Tym razem połączyłem ją z gniazdem TRRS 3.5mm zakupionym w Chinach, ale można też podobne dostać w kraju.

IMG_1352.thumb.jpg.a976d7e89d8656723fa01dfc5e1466ef.jpgIMG_1354.thumb.jpg.2d333cd8b63f8be5c36e0e4e546a93dd.jpg
Testy nowej płytki

Jak widać druga wersja działała "bezproblemowo".

O przewodach słów kilka

Jako, że mój AKAI MPK Mini ma wyjście DIN 5-pin, a chciałem by syntezator używał złącza 3.5mm postanowiłem najzwyczajniej zakupić przewód ze znanego portalu na A, aczkolwiek trafiłem na taki, który miał nieprawidłowy pinout (okej, o ile byłoby to MIDI B to żaden problem, ale ten miał wyprowadzone piny 1, 4 i 2 ze złącza DIN, gdy MIDI używa pinów 2, 4 i 5... No cóż... odesłałem i kupiłem elementy (wtyki DIN (5P) oraz 3.5mm TRS (3P)) by zrobić sobie go samemu (wyszło taniej niż zaufany przewód ze sklepu audio).

9838bd04d3693892d7d2844afe0e1355e9617a86.thumb.jpg.682222f3a06fd85eb279966b16a46a80.jpg
Połączenia w przewodzie

IMG_1356.thumb.jpg.932df3ee364d2a9e924f27029c47dd9d.jpg
i sam przewód...

Jako samego przewodu użyłem dwużyłowego OMY 2x0.5mm, aczkolwiek dużo lepszym wyborem byłby przewód sterowniczy 2x0.5 z osłoną podłączoną do pinu 2 (SHIELD), który w moim przypadku był pominięty (brak odpowiedniego przewodu w szufladzie).

Przy samym lutowaniu na szczęście przy lutowaniu obyło się bez problemów... za to niespodzianki czekały mnie chwilę później...

Sam syntezator...

Do budowy syntezatora użyłem Raspberry Pi Pico, wcześniej wspomianej płytki do optoizolacji MIDI, kodeka VS1053 od Adafruit oraz kilku płytek TRRS. Ustawiłem bootstrapping kodeka do trybu RT-MIDI (GP0: 0, GP1: 1) używając rezystorów 330R wlutowanych bezpośrednio w złącza płytki... Zasilanie kodeka podłączyłem do wyjścia regulatora 3.3V na Pico i okazało się, że kodek nie działa.

Kod programu dla prototypu:

/*
 * MIDI UART Pass-Through for Raspberry Pi Pico
 * 
 * UART0:  GP0 = TX, GP1 = RX @ 31250 baud (MIDI)
 * 
 * Modes:
 *   GP16 HIGH (default, pull-up): UART0 RX → UART0 TX pass-through
 *   GP16 LOW  (pin to GND):       USB Serial → UART0 TX
 */

#define USBSERIAL_BAUD  115200
#define MIDI_BAUD       31250
#define MODE_PIN        16   // GP16 — internal pull-up; LOW = USB-to-MIDI mode
#define RST_PIN         12

void setup() {
  // USB Serial (for USB-to-MIDI mode)
  Serial.begin(USBSERIAL_BAUD);

  pinMode(0, OUTPUT);
  pinMode(1, INPUT);
  

  // UART0: GP0 = TX, GP1 = RX
  // Serial1 maps to UART0 on Pico by default
  Serial1.setTX(0);
  Serial1.setRX(1);
  Serial1.begin(MIDI_BAUD);

  // Reset is needed (and has to be in high to operate)
  pinMode(RST_PIN, OUTPUT);
  digitalWrite(RST_PIN, LOW);
  delay(10);
  digitalWrite(RST_PIN, HIGH);
  delay(10);

  // Mode select pin with internal pull-up
  pinMode(MODE_PIN, INPUT_PULLUP);
}

void loop() {
  if (digitalRead(MODE_PIN) == HIGH) {
    // ── Mode A: UART RX → UART TX pass-through ──────────────────────────────
    // Forward every byte arriving on GP1 (RX) straight back out on GP0 (TX).
    // Useful for chaining MIDI devices or monitoring/relaying a MIDI stream.
    while (Serial1.available()) {
      Serial1.write(Serial1.read());
    }
  } else {
    // ── Mode B: USB Serial → UART TX ────────────────────────────────────────
    // Forward MIDI bytes arriving over USB (e.g. from a DAW or USB-MIDI
    // Forward every byte arriving on GP1 (RX) straight back out on GP0 (TX).
    // adapter driver) out to the hardware MIDI port on GP0.
    while (Serial.available()) {
      Serial1.write(Serial.read());
    }    

    while (Serial1.available()) {
      Serial1.write(Serial1.read());
    }    
  }
}

Urządzenie nie działało ani poprzez złącze Jack podłączone do UARTa, ani przez USB (oczywiście po podciągnięciu GP16 do masy)...

Problemem było to, iż nie podłączyłem pinu RST, który jest oznaczony na odwrót (w rzeczywistości jest to NRST)... Okej, poprawiłem i dodałem fragment w kodzie programu... dalej nie działa. Tym razem problemem okazało się zasilanie kodeka... Zmieniając VCC na 5V z pinu VBUS kodek zaczął działać poprawnie i grał nuty klikane na pianinie. Dźwięk był nieco cichy, ale to urok braku wzmacniacza.

Gorzej było jak chciałem przetestować inne instrumenty i okazało się, że jakimś cudem port COM przestał działać. Winowajca? Otóż kodek pobierający zasilanie z VBUS... po odłączeniu zasilania kodeka wszystko działało poprawnie, a urządzenie chciałem zasilać z USB, bo i tak ta magistrala byłaby używana do konfiguracji... W tym momencie stwierdziłem, że ten projekt zdecydowanie nie chce ze mną współpracować i trzeba go na chwilę odłożyć...

Walka z wiatrakami

Dłuższy czas szukałem przyczyny, a okazało się że znajduje się po innej stronie lutownicy... Port USB, do którego podłączyłem urządzenie nie dawał pełnego natężenia prądu. Gdy włożyłem przewód do portu, który zawsze daje minimum 0.9A wszystko nagle zaczęło działać poprawnie. Czasem te najprostsze rzeczy są najtrudniejsze.

Gdy zacząłem testować komunikację po USB ta potrafiła cały czas wysyłać wartość 0xFF, która za to powodowała problemy z odczytywaniem pakietów danych po czym urządzenie wpadało w błąd i kodek się zawieszał.

Rozwiązałem to usuwając przełącznik trybu (i tak MIDI powinno działać cały czas) oraz dodając weryfikację pakietów MIDI wysyłanych z poziomu USB. Rozwiązało to problem i urządzenie gra całkiem ładnie (pomiając, że jest ciche przez brak wzmacniacza).

/*
 * MIDI UART Pass-Through for Raspberry Pi Pico
 *
 * UART0:  GP0 = TX, GP1 = RX @ 31250 baud (MIDI)
 *
 * Both USB Serial and UART0 RX are forwarded to UART0 TX.
 * USB input is parsed and validated — System Reset (0xFF) is silently dropped.
 * UART RX is echoed back to USB Serial for monitoring.
 * VLSI chip is reset on startup via RST_PIN.
 */

#define MIDI_BAUD 31250
#define RST_PIN   12    // GP15 — adjust to your wiring

// ---------------------------------------------------------------------------
// MIDI parser — used for USB input only (UART is a trusted hardware source)
// ---------------------------------------------------------------------------

int midiMessageLength(uint8_t status) {
  if (status == 0xFF) return 1; // System Reset — handled as special case
  if (status == 0xF0) return -1; // SysEx — variable
  if (status >= 0xF0) {
    switch (status) {
      case 0xF1: return 2;  // MIDI Time Code
      case 0xF2: return 3;  // Song Position Pointer
      case 0xF3: return 2;  // Song Select
      case 0xF6: return 1;  // Tune Request
      case 0xF7: return 1;  // End of SysEx
      case 0xF8: return 1;  // Timing Clock
      case 0xFA: return 1;  // Start
      case 0xFB: return 1;  // Continue
      case 0xFC: return 1;  // Stop
      case 0xFE: return 1;  // Active Sensing
      default:   return 0;  // Unknown — discard
    }
  }

  switch (status & 0xF0) {
    case 0x80: return 3;  // Note Off
    case 0x90: return 3;  // Note On
    case 0xA0: return 3;  // Poly Aftertouch
    case 0xB0: return 3;  // Control Change
    case 0xC0: return 2;  // Program Change
    case 0xD0: return 2;  // Channel Aftertouch
    case 0xE0: return 3;  // Pitch Bend
    default:   return 0;  // Unknown — discard
  }
}

// Parser state for USB input
static uint8_t usbBuf[3];
static int     usbBufLen   = 0;
static int     usbExpected = 0;
static bool    usbInSysEx  = false;

void processUsbByte(uint8_t b) {
  // ── Real-Time messages ─────────────────────────────────────────────────────
  // Single byte, can appear anywhere. System Reset (0xFF) is dropped.
  if (b >= 0xF8) {
    if (b != 0xFF) {
      Serial1.write(b);
      Serial.write(b);    // Echo to USB
    }
    return;
  }

  // ── End of SysEx ──────────────────────────────────────────────────────────
  if (b == 0xF7) {
    if (usbInSysEx) {
      Serial1.write(b);
      Serial.write(b);
      usbInSysEx = false;
    }
    usbBufLen   = 0;
    usbExpected = 0;
    return;
  }

  // ── Start of SysEx ────────────────────────────────────────────────────────
  if (b == 0xF0) {
    usbInSysEx  = true;
    usbBufLen   = 0;
    usbExpected = 0;
    Serial1.write(b);
    Serial.write(b);
    return;
  }

  // ── SysEx data bytes ──────────────────────────────────────────────────────
  if (usbInSysEx) {
    if (b < 0x80) {
      Serial1.write(b);
      Serial.write(b);
    }
    return;
  }

  // ── Status byte ───────────────────────────────────────────────────────────
  if (b >= 0x80) {
    usbBufLen   = 0;
    usbExpected = midiMessageLength(b);
    if (usbExpected == 0) return;

    usbBuf[usbBufLen++] = b;

    if (usbExpected == 1) {
      Serial1.write(b);
      Serial.write(b);
      usbBufLen   = 0;
      usbExpected = 0;
    }
    return;
  }

  // ── Data byte ─────────────────────────────────────────────────────────────
  if (usbExpected == 0) return;   // Orphaned data — discard

  usbBuf[usbBufLen++] = b;

  if (usbBufLen >= usbExpected) {
    Serial1.write(usbBuf, usbBufLen);
    Serial.write(usbBuf, usbBufLen);   // Echo complete message to USB
    usbBufLen   = 0;
    usbExpected = 0;
  }
}

// ---------------------------------------------------------------------------

void setup() {
  Serial.begin(MIDI_BAUD);  // USB Serial

  Serial1.setTX(0);         // GP0
  Serial1.setRX(1);         // GP1
  Serial1.begin(MIDI_BAUD);

  // VLSI reset — hold LOW briefly then release HIGH to operate
  pinMode(RST_PIN, OUTPUT);
  digitalWrite(RST_PIN, LOW);
  delay(10);
  digitalWrite(RST_PIN, HIGH);
  delay(10);
}

void loop() {
  // UART RX → UART TX (trusted hardware source, pass straight through)
  // Also echo to USB so the host can monitor incoming MIDI traffic
  while (Serial1.available()) {
    uint8_t b = Serial1.read();
    Serial1.write(b);
    Serial.write(b);
  }

  // USB → UART TX (untrusted, run through MIDI parser)
  // Validated bytes are echoed back to USB inside processUsbByte()
  while (Serial.available()) {
    processUsbByte((uint8_t)Serial.read());
  }
}

Kod programu był w pełni wygenerowany przez model Claude Sonnet 4.6 Extended bazując na opisie wyprowadzeń i założeniach projektowych.

Jest to prosty skrypt kopiujący dane z wejść portów szeregowych na ich wyjścia (włącznie z kopiowaniem krzyżowym), który też uwzględnia rozmiar pakietu USB. Mogą w nim jeszcze wystąpić problemy, gdy USB zareaguje w połowie komendy otrzymanej z kontrolera MIDI, aczkolwiek jest to dość mało prawdopodobne (zadaniem USB jest głównie konfiguracja kanału/ów i instrumentu/ów).

Obudowa i ostatnie poprawki

IMG_1360.thumb.jpg.82f66ecb76a46e4c26ee5f9d7bc90c2a.jpgIMG_1362.thumb.jpg.7a74e312fb9b42bd8156bbdf5ba16741.jpg

Upchane bebechy urządzenia

Ostatnim zadaniem jakie pozostało było zaprojektowanie obudowy i wepchanie bebechów. Mocowanie chińskich wejść audio już miałem opanowane - wystarczy dać im dwa panele po bokach i jeden dokręcany śrubką z tyłu. Pod spodem umieścić podstawę, która ma wycięcie pod pinami, by wygodnie wyprowadzić przewody i gotowe. Reszta komponentów została włożona do obudowy luzem (po owinięciu taśmą izolacyjną, bo nikt nie lubi zwarć)... Czy można zaprojektować obudowę z mocowaniami? Tak. Czy warto? Raczej nie, bo to zajmuje dość sporo czasu, a może okazać się, że któryś przewód jest za krótki 😄 Czasem najprostsze rozwiązania są najlepsze (zawsze mogę potraktować elementy klejem na gorąco, by nie latały).

Pliki STL oraz plik 3MF dla Bambulab X1C są dostępne tutaj: Syntezator STL.zip

Python - programy do kontroli

controller.thumb.png.35ffdaa90ce686344cc1a492df560c99.pngfile.thumb.png.747f5ae46114d41e1bdeff799606ce41.png
Programy kontrolujące interfejs

Oprócz zwykłego działania jako pianino postanowiłem poprosić Claude o wygenerowanie programów do kontroli urządzenia jako interfejsu USB MIDI (do odtwarzania plików .mid) oraz programu konfiguracyjnego by móc łatwo zmieniać instrumenty czy poziom głośności. Poradził sobie całkiem dobrze (program do odtwarzania plików wymagał drobnej poprawki w kwestii tego, iż pliki MIDI zawierają odstęp czasowy od poprzedniego dźwięku, a nie od początku klipu). Oba programy są dostępne na GitHubie (link na początku artykułu).

Co by się jeszcze przydało?

  • Opcjonalnie można przestawić ustawienia portu USB z CDC na MIDI, by widziały go wszystkie profesjonalne programy, ale osobiście wolę CDC 🙂 
Edytowano przez H1M4W4R1

Bądź aktywny - zaloguj się lub utwórz konto!

Tylko zarejestrowani użytkownicy mogą komentować zawartość tej strony

Utwórz konto w ~20 sekund!

Zarejestruj nowe konto, to proste!

Zarejestruj się »

Zaloguj się

Posiadasz własne konto? Użyj go!

Zaloguj się »
×
×
  • Utwórz nowe...