[W] Własny kontroler MIDI cz. 2

W poprzedniej części opisałem tło budowania własnego kontrolera MIDI, a skoro dotarły już do mnie wszystkie części, postanowiłem wreszcie zabrać się do zabawy i zbudować jakieś działające prototypy. Do budowy będę używał płytki stykowej i przewodów połączeniowych oraz - oczywiście - klonu płytki Arduino Pro Micro i konkretnych elementów sterujących. Prócz łączenia elementów trzeba też zadbać o oprogramowanie, czyli za pomocą Arduino IDE przygotuję odpowiedni program i wgram go do układu, po czym przetestuję całość w Reaperze (ale spokojnie, gotowy kontroler będzie też testowany w docelowym środowisku, czyli Studio One).

Wszystkie opisane prototypy są osadzone w miarę możliwości na płytce stykowej, na której również osadzona jest płytka z Arduino - te dwa elementy będą stałe, więc omówię połączenie na początku, bo zawsze od takiego stanu będę zaczynał budowanie kolejnych prototypów. Przede wszystkim zasilanie odbywa się wyłącznie przez port USB-C, zatem to płytka Arduino musi za pomocą swoich styków wyprowadzić masę i napięcie +5V na płytkę stykową. Tzw. pin-out, czyli opis wyprowadzeń Arduino Pro micro, wygląda następująco:

Wyprowadzenia pinów - opracowanie własne na podstawie dokumentacji płytki

Jak widać, mamy aż trzy uziemienia (masa) i jedno wyprowadzenie napięcia +5V. Aby całość mogła działać, łączymy masę i napięcie do odpowiednich pinów płytki stykowej - zwróćmy uwagę, że płytka stykowa ma na brzegach linie niebieską (minus, czyli w naszym przypadku masa) i czerwoną (plus, czyli w naszym przypadku +5V). Po podłączeniu Arduino do płytki stykowej możemy umieszczać na niej w odpowiedni sposób elementy sterujące. Co znaczy "odpowiedni"? No cóż, to zależy od konkretnego elementu i tym się zajmę w kolejnych akapitach.

Żeby ułatwić sobie życie i zwiększyć pewność połączeń, w pierwszym kroku przylutowałem do płytki Arduino metalowe szpilki, umożliwiające łatwy montaż w płytce stykowej. Tak przygotowany układ, zamontowany i "okablowany", prezentuje się następująco:

Arduino osadzone w płytce stykowej, widoczne połączenia masy (przewód czarny) i napięcia +5V (przewód czerwony)

Procedura resetu bootloadera dla Arduino Pro Micro

Zdarzyło mi się już tak zakombinować z połączeniami podczas testów, że uszkodziłem ze dwa razy bootloader Arduino. Objawia się to tym, że płytka nie "wstaje" - mimo że po podłączeniu do portu USB Arduino IDE niby ją "widzi", to jednak nie może wybrać odpowiedniego portu i co za tym idzie, załadować nowego programu. Za pierwszym razem jest to nieco stresujące, ale na szczęście zazwyczaj odwracalne.

Metod radzenia sobie z tym problemem jest kilka, ja przetestowałem w praktyce podaną poniżej, zaczerpniętą ze strony shellhack.com. Tylko uwaga - trzeba mieć refleks i się streszczać, bo na całość próby mamy zaledwie osiem sekund. Po pierwsze, odłączamy wszystko od płytki Arduino i podłączamy dwa przewody do pinów GND i RST. Pro Micro nie ma fizycznego przycisku Reset, więc trzeba kombinować. Czyli - wracając do tematu - mamy odłączone od USB Arduino z podpiętymi przewodami do GND i RST. W Android IDE tworzymy nowy, zupełnie pusty projekt (szkic). Teraz trzeba w ciągu ośmiu sekund wykonać następujące kroki:

  • podłączamy Arduino do USB
  • szybko zwieramy przewodu masy i resetu dwukrotnie (takie "cyk-cyk")
  • w Arduino IDE na liście powinno pojawić się nasze urządzenie - szybko rozwijamy listę i klikamy na odpowiedni port
  • teraz jeszcze szybciej klikamy na Upload, żeby przesłać pusty szkic do Arduino (można użyć skrótu klawiaturowego Ctrl+U)
  • w momencie, kiedy komunikat z Compiling sketch... zmieni się na Uploading, znów dwukrotnie zwieramy masę z resetem ("cyk-cyk")
  • jeśli zobaczymy komunikat Done uploading, nasze Arduino zostało zresetowane i można bawić się dalej w pisanie programów; jeśli zaś procedura się nie powiodła, należy szukać rozwiązania "w internetach", bo jak wspomniałem, sam znalazłem kilka sposobów, ale że pierwszy zadziałał, nie sprawdzałem pozostałych

Oczywiście, sprytni użytkownicy posiadający najprostszy przycisk do Arduino mogą go wykorzystać, zamiast ręcznie zwierać przewody. Wtedy "cyk-cyk" nabiera większego sensu.

Jest jeszcze jedna, mniej stresująca metoda, ale tylko dla osób, które posiadają drugi, działający egzemplarz płytki Arduino Pro Micro - omówiono go w krótkim filmiku na kanale SavvyGaming.

Sygnalizacja

Zanim zacznę testować różne układy sterujące, dobrze jest poznać możliwość sygnalizowania użytkownikowi choćby faktu włączenia kontrolera - chciałbym w moim egzemplarzu dać możliwość włączania i wyłączania, choćby po to, żeby nie musieć się martwić, że np. przypadkowe potrącenie urządzenia wyśle jakieś dane MIDI. Najprościej sygnalizować coś podobnego za pomocą kolorowej diody. Diodę zaś można podłączyć po prostu do dowolnego pinu cyfrowego, ale uwaga! Konieczne jest użycie rezystora "przed" diodą, żeby uchronić ją przed przepaleniem i przed pobieraniem zbyt dużego prądu. Jaki to musi być rezystor? Da się to wyliczyć, jeśli znamy parametry diody:

R = (Uzasilania - Udiody) / Idiody

Jak widać, musimy znać napięcie zasilające (u nas +5V) oraz dane diody - te jednak są często trudne do zdobycia, zwłaszcza jeśli mamy diody z tak zwanego zestawu startowego. Generalnie dla projektów Arduino i tego typu "ćwiczebnych" diód powinien wystarczyć rezystor 200-1000Ω. Prąd takich "ćwiczebnych" diód to zazwyczaj 0,01-0,02A (czyli w okolicach 10-20mA), zaś napięcie diody zależy od jej koloru i typowe wartości widoczne są w tabelce:

Po zbudowaniu układ prezentuje się skromnie, ale działa:

Jeśli dioda świeci nam zbyt jasno, da się ją "przyciemnić" właśnie za pomocą doboru nieco mocniejszego rezystora. Zasadniczo jednak z diodami w projektach Arduino warto uważać i dokładnie sprawdzać, ile ich podpinamy i do czego, bo można spalić nie tylko diody, ale i mikrokontroler (jeśli wierzyć forom i komentarzom).

Pora na program do sterowania zamontowaną diodą:

int PIN = 9;

void setup() {
  Serial.begin(115200);
  pinMode(PIN, OUTPUT);
}

void loop() {
  digitalWrite(PIN, HIGH);
  delay(500);
  digitalWrite(PIN, LOW);
  delay(500);
}

Program jest bardzo prosty i w zasadzie najważniejsze jest w nim ustawienie pinu PIN, czyli pinu nr 9 na wysyłanie sygnału (linka pinMode(PIN, OUTPUT)), dzięki czemu będziemy zasilać diodę tylko wówczas, gdy pin zostanie ustawiony w stan HIGH. I dokładnie to robi główna pętla - na przemian ustawia na pinie 9 stan wysoki i niski, oddzielając je półsekundową przerwą. W efekcie dioda pięknie pulsuje.

Gramy jednym przyciskiem

Chciałem zacząć od najprostszej rzeczy, a taką wydał mi się najzwyklejszy przycisk, zresztą często dołączany w zestawach startowych Arduino. Idea była taka, że naciskam - instrument zaczyna odtwarzać nutkę. Puszczam - nutka się kończy. Chciałem użyć jednego przycisku testowo, więc nuta została odgórnie ustalona na F3 (czyli kod MIDI 65), tak samo kanał MIDI (na 0) i parametr velocity (na 120).

Połączenie okazało się banalne - wetknąłem przycisk do płytki stykowej, styk z jednej strony podłączyłem do masy, styk z przeciwległej - do cyfrowego pinu nr 9:

Na koniec pozostało napisać odpowiedni kod w Arduino IDE, korzystający z biblioteki MIDIUSB:

#include <MIDIUSB.h>

int BUTTON_PIN = 9;
int last_state = LOW;

void setup() {
  Serial.begin(115200);
  pinMode(BUTTON_PIN, INPUT_PULLUP);
}

void loop() {
  int curr_state = digitalRead(BUTTON_PIN);
  if (curr_state != last_state) {
    if (curr_state == LOW) {
      noteOn(0, 65, 120);
    } else if (curr_state == HIGH) {
      noteOff(0, 65, 0);
    }
    last_state = curr_state;
  }
  delay(5);
}

void noteOn(byte channel, byte pitch, byte velocity) {
  midiEventPacket_t noteOn = {0x09, 0x90 | channel, pitch, velocity};
  MidiUSB.sendMIDI(noteOn);
  MidiUSB.flush();
}

void noteOff(byte channel, byte pitch, byte velocity) {
  midiEventPacket_t noteOff = {0x08, 0x80 | channel, pitch, velocity};
  MidiUSB.sendMIDI(noteOff);
  MidiUSB.flush();
}

Działanie jest proste - BUTTON_PIN przechowuje numer pinu, do którego podłączyłem przycisk. Ten z kolei w metodzie setup(), uruchamianej raz przy starcie programu, jest ustawiany w tryb INPUT_PULLUP. Tryb ten stosuje się właśnie przede wszystkim przy przyciskach i sprawia on, że stan przycisku jest "podciągany" do wartości określanej jako HIGH (przycisk "wyciśnięty"). W momencie naciśnięcia przycisk zyskuje stan LOW, nie mamy więc stanów pośrednich i różnych dziwnych, zakłóconych wartości (a są, sprawdzałem).

Aby uniknąć niepotrzebnego przetwarzania, zawsze porównuję aktualnie odczytany stan przycisku ze stanem zapamiętanym poprzednio i tylko jeśli się on zmienił, dochodzi do sprawdzenia, w jakim stanie jest przycisk. Dla stanu LOW (naciśnięto przycisk) wysyłany jest komunikat NoteOn, dla stanu HIGH (przycisk zwolniony) - komunikat NoteOff.

Małe wyjaśnienie jeszcze do obsługi wysyłki - stosowane jest przede wszystkim MidiUSB.sendMIDI(), której to komendzie przekazuje się obiekt typu midiEventPacket_t. Komend MidiUSB.sendMIDI() można wysłać więcej, np. mógłbym wysłać po jednej do każdego kanału MIDI. Po zakończeniu wszystkich wysyłek dobrze jest wywołać komendę MidiUSB.flush(), która fizycznie opróżni bufor MIDI i prześle komunikaty z naszego kontrolera do komputera i programu DAW. Z tego, co wyczytałem, dobrą praktyką jest wysyłanie flush() po zakończeniu pętli głównej loop() - u mnie jest to robione w metodach noteOn() i noteOff(), bo tylko one wysyłają komunikaty MIDI.

Suwak, czyli fader

To najważniejsza rzecz, którą musiałem przetestować, bo właśnie suwaki będą bazą mojego kontrolera. Zamówiłem cztery sztuki, ale do prototypu, rzecz jasna, użyłem tylko jednego. Jako żółtodziób, kompletnie nie znający się na podzespołach, kupiłem niepotrzebne suwaki stereofoniczne, ale na szczęście takie też się nadają do moich celów, miałem tylko dylematy, co z czym połączyć.

Zasadniczo suwak to po prostu rezystor, więc wiadomo było, że z jednego końca podłączę napięcie, a z drugiego - masę. Drugi pin z jednego końca to będzie "wartość" suwaka, którą będę chciał odczytać jednym z analogowych tym razem wejść Arduino. Żeby było zabawniej, moje przypuszczenia okazały się słuszne:

Także program okazał się banalny do napisania:

#include <MIDIUSB.h>

int FADER_PIN = A0;
int last_state = 0;
byte channel = 0;
byte cc = 1;

void setup() {
  Serial.begin(115200);
}

void loop() {
  int curr_state = analogRead(FADER_PIN);
  int diff = abs(curr_state - last_state);
  if (diff > 0) {
    int value = map(curr_state, 0, 1023, 0, 127);
    controlChange(channel, cc, value);
  }
  delay(5);
}

void controlChange(byte channel, byte control, byte value) {
  midiEventPacket_t event = {0x0B, 0xB0 | channel, control, value};
  MidiUSB.sendMIDI(event);
  MidiUSB.flush();
}

Jak widać, wybrałem pin A0 do podłączenia "wartości" z suwaka. Ponownie bazuję na zapamiętanym stanie, aby móc porównać różnicę i wysyłać komunikat Continous Control tylko wtedy, gdy faktycznie użytkownik ruszy suwakiem.

Sam suwak generuje wartości 0-1023 i takie właśnie są odczytywane z pinu A0 - na potrzeby MIDI trzeba je zamienić na wartości 0-127. Nie wymaga to jakichś skomplikowanych obliczeń, ale skorzystałem z prostej funkcji map(), która przyjmuje wartość do przeliczenia oraz oba zakresy: źródłowy i docelowy, po czym zwraca już przeliczoną wartość. Wystarczy wysłać tę wartość w zdarzeniu midiEventPacket_t przez MidiUSB.sendMIDI() i już da się sterować modulacją w dowolnym instrumencie wirtualnym (modulacją, bo na stałe ustawiłem parametr cc na 1, a CC1 to właśnie modulacja).

Tak rodzi się serial

I znów wyszło dużo materiału, więc ponownie przerywam, zapowiadając część trzecią, w której przetestuję potencjometry (pokrętła), joystick (jako manipulator XY) i czujnik odległości - to jest już gotowe i przetestowane. W perspektywie za to są jeszcze wyświetlacze (w tym dotykowy jako panel XY) czy czujniki optyczne, więc naprawdę czeka mnie sporo testowania, fotografowania i pisania. Uf!

Komentarze