Mikrokontrolery - Jak zacząć?

... czyli zbiór praktycznej wiedzy dot. mikrokontrolerów.

środa, 16 marca 2011

DIY: Kontroler MIDI z panelem dotykowym na Arduino


Autor: daniellos
Redakcja: Dondu

Zobacz inne artykułu z cyklu: Arduiono ... czyli prościej się nie da :-)

Kontrolery MIDI używane są głównie przez muzyków do przesyłania informacji pomiędzy urządzeniami (np. jaką nutę zagrać w danej chwili, jaki przycisk został naciśnięty).

Przykładem może być klawiatura sterująca, która wygląda jak keyboard, ale służy do komunikowania się z innym urządzeniem, np. komputerem, który przyjmuje sygnały MIDI i odtwarza zadane przez nią dźwięki. Zatem sama klawiatura nie wydobywa dźwięku, ale robi to za pomocą komputera.

W tym artykule chcę przedstawić kontroler MIDI domowej roboty oparty na Arduino. Moim celem było zrobienie kontrolera efektów do programu Traktor Pro (program do miksowania muzyki), ale może być dowolnie programowany i używany przez inne programy obsługujące standard MIDI. W tym przypadku urządzenie nie wysyła informacji dotyczących wciśnięcia danej nuty, ale powiadamia o zmianie stanu poszczególnych elementów na urządzeniu (CC, Control Change).





Budowa

Kontroler zawiera 7 programowalnych przycisków (B1-B7), 2 potencjometry (P1, P2) i panel dotykowy. Ósmy przycisk służy do zmiany trybu kontrolera (Shift mode on\off), więc można zaprogramować 2 razy więcej zdarzeń niż mamy dostępnych fizycznie elementów w urządzeniu. Oto rysunek poglądowy urządzenia (numery w nawiasach oznaczają piny w Arduino, do których podłączony jest dany element):


Rysunek poglądowy


Zielona dioda shift świeci się w zależności od stanu trybu shift, a dwie niebieskie diody pod panelem dotykowym służą do wizualizacji miejsca naciśnięcia panelu na osi X. Odpowiednio ściemniając się lub rozjaśniając - taki bajer :)

Jako obudowy użyłem drewnianego pudełka, które znalazłem w domu.


Pudełko i płytka uniwersalna


Pudełko z otworami

Urządzenie jest właściwie modułem podłączanym pod płytkę Arduino Uno R3, zlutowanym na płytce uniwersalnej, z gniazdami goldpin umożliwiającymi szybki montaż i demontaż Arduino.



Moduł na płytce uniwersalnej

Moduł, tył

Moduł, gniazda

Panel dotykowy posiada złącze ZIFF, a więc potrzebna była przejściówka na goldpin, której zrobienie zleciłem osobie zajmującej się takimi rzeczami, gdyż nie znalazłem sklepu, w którym mógłbym ją kupić.



Przejściówka


Wszystkie elementy umieszczone są w pudełku z wyjątkiem panelu, który postanowiłem położyć na pudełku i docisnąć odpowiednio pozaginaną blachą. Pod panelem umieściłem piankę (która z resztą była w opakowaniu z panelem). Dystansuje ona panel od powierzchni pudełka, dzięki czemu można umieścić diody pod spodem, a światło rozchodzi się dając całkiem fajny efekt.



Profil z blachy

Profil i panel


Wszystkie części


Na koniec pudełko okleiłem folią carbon (używaną do oklejania samochodów), żeby "jakoś to wyglądało" :)


Gotowy kontroler

Gotowy kontroler


USB


Wnętrze


Wnętrze

Oświetlenie panelu dotykowego




Lista użytych części

  • Arduino Uno R3
  • Panel dotykowy LCD-AG-TP-240128S
  • Przejściówka ZIFF <-> goldpin
  • 2 potencjometry 10k
  • 8 przycisków tact switch 12mm z klawiszem
  • 3 diody LED (zielona, 2 niebieskie)
  • Rezystory: 8x 10K, 3x150R
  • Gniazda i wtyczki goldpin + styki do wtyków (męskie i żeńskie)
  • 1mb taśmy 10-żyłowej do zrobienia przewodów
  • 8 pinów z listwy kołkowej goldpin
  • Trochę kabla telefonicznego do połączenia lutów na płytce uniwersalnej
  • Śruby, nakładki
  • Gumowe nóżki samoprzylepne pod obudowę (aby się nie ślizgała)
  • Drewniane pudełko
  • Folia carbon




Schemat

Na schemacie przedstawiony jest sposób podłączenia poszczególnych elementów do płytki Arduino. Proszę zwrócić uwagę, że potencjometry podłączone są do innego pinu uziemienia ze względu na zakłócenia jakie powstawały przy jednym, wspólnym pinie GND. Podłączenie przycisków na schemacie nie jest proste jak można by się spodziewać, taka kolejność była korzystniejsza ze względu na sposób lutowania połączeń na płytce uniwersalnej.


Schemat



Kod źródłowy

Przejdźmy teraz do ciekawszej kwestii jaką jest kod źródłowy. W programie korzystam z osobnego modułu TouchScreen.h oraz biblioteki MIDI.h


TouchScreen.h - obsługa panelu dotykowego

Klasa TouchScreen zawiera konstruktor, który jako parametry przyjmuje 4 wejścia analogowe, funkcję read() do odczytu współrzędnych (X,Y) oraz touched() do sprawdzenia czy panel jest w ogóle dotknięty.
Rezystancyjny panel dotykowy zbudowany jest z dwóch folii rezystancyjnych ułożonych jedna pod drugą. Na jednej folii, na przeciwległych brzegach, umieszczone są elektrody wzdłuż OX, a na drugiej wzdłuż OY.


Źródło: Texas Instruments



Odczyt współrzędnych polega na użyciu prądu stałego na jednej osi i odczyt spadku napięcia używając elektrody drugiej osi. Jako, że musimy odczytać wartości współrzędne z dwóch osi, trzeba zmieniać tryby pracy odpowiednich pinów. Przydatna jest tutaj informacja, że analogowe piny w Arduino mogą pełnić rolę pinów cyfrowych.

#include "TouchScreen.h"

TouchScreen::TouchScreen(int pinX1, int pinX2, int pinY1, int pinY2)
{
  PIN_X1 = pinX1 + OFFSET;
  PIN_X2 = pinX2 + OFFSET;
  PIN_Y1 = pinY1 + OFFSET;
  PIN_Y2 = pinY2 + OFFSET;
}

void TouchScreen::read(int *coordinates)
{
  //Konfiguracja do odczytu współrzędnej X
  pinMode(PIN_X1, INPUT);
  pinMode(PIN_X2, INPUT);
  pinMode(PIN_Y1, OUTPUT);
  digitalWrite(PIN_Y1, LOW); // Y1 jako GND
  pinMode(PIN_Y2, OUTPUT);
  digitalWrite(PIN_Y2, HIGH); //Y2 jako +5V
  delay(1);
  coordinates[0] = analogRead(PIN_X2 - OFFSET); //odczyt X

  //Konfiguracja do odczytu współrzędnej Y
  pinMode(PIN_Y1, INPUT);
  pinMode(PIN_Y2, INPUT);
  pinMode(PIN_X2, OUTPUT);
  digitalWrite(PIN_X2, LOW);
  pinMode(PIN_X1, OUTPUT);
  digitalWrite(PIN_X1, HIGH);
  delay(1);
  coordinates[1] = analogRead(PIN_Y1 - OFFSET); //odczyt Y
}

boolean TouchScreen::touched()
{
  //Konfiguracja do wykrycia dotknięcia
  pinMode(PIN_X1, INPUT);
  pinMode(PIN_Y2, INPUT);
  pinMode(PIN_X2, OUTPUT);
  digitalWrite(PIN_X2, LOW);
  pinMode(PIN_Y1, OUTPUT);
  digitalWrite(PIN_Y1, HIGH);
  
  return analogRead(PIN_X1) > 0;
}
MIDI.h

Biblioteka MIDI służy do wysyłania i odbierania komunikatów MIDI za pomocą portu szeregowego.
Dokumentacja: http://arduinomidilib.sourceforge.net/index.html


Program główny

Na początku nazywam numery pinów bardziej intuicyjnymi nazwami, definiuję zmienne globalne i deklaruję funkcje pomiarowe.

setup() - wykonywana raz przy starcie urządzenia

MIDI.begin() odpowiada za uruchomienie komunikacji MIDI na porcie szeregowym dlatego też nie możemy wtedy używać portu szeregowego do wypisywania jakiegoś tekstu w monitorze portu za pomocą np. Serial.print(). Dalej mamy ustawienie pinów do odpowiednich trybów i efekt rozjaśniania LEDów pod panelem dotykowym przy włączaniu.

Funkcja loop() - główna pętla, wykonywana cały czas
Tutaj następuje sprawdzenie każdego elementu urządzenia pod kątem zmian i ewentualne wysłanie komunikatu.

Jak wiadomo, kod w loop() to nieskończona pętla, która w czasie od naciśnięcia przycisku do jego zwolnienia może wykonać się kilka razy! Tak więc samo sprawdzenie czy dany przycisk jest naciśnięty i wysłanie komunikatu MIDI spowoduje, że zostanie on wysłany nawet kilkadziesiąt razy, a nie chcemy tego.
Można to rozwiązać dodając pustą pętlę zaraz po wysłaniu sygnału, która wykonuje się dopóki przycisk jest trzymany.

Pojawia się następujący problem: Nie możemy wtedy jednocześnie użyć innego przycisku, potencjometru ani panelu. Program nie robi nic innego jak czeka na zwolnienie przycisku.

Rozwiązanie: Tablica active[] typu boolean zawierająca stany przyciśnięcia danego elementu. Jeżeli przyciśniemy przycisk to zostanie mu przypisana wartość true, wysłany będzie komunikat MIDI i nastąpi dalsze wykonywanie programu. Przy ponownym pomiarze tego przycisku, jeśli będzie on ciągle wciśnięty to wiemy, żeby nie wysyłać ponownie komunikatu. Jeśli natomiast okaże się, że zwolniliśmy przycisk to przypiszemy mu wartość false. Dla potencjometrów zapamiętujemy wartość ostatniego pomiaru i sygnał MIDI jest wysyłany, gdy pomiar różni się od poprzedniego.

Powracając do struktury funkcji loop(), najpierw mamy czytanie współrzędnych XY jeśli panel jest dotknięty. Dalej pomiary potencjometrów, przycisków mapowalnych i trybu shift. Na koniec sprawdzane jest czy chcemy przejść do trybu mapowania.


Tryb mapowania

Zazwyczaj programy obsługujące interfejs MIDI posiadają możliwość ręcznego wprowadzenia który przycisk na urządzeniu odpowiada przyciskowi w programie lub mapowania poprzez uczenie, czyli wybieramy element w programie, a potem naciskamy przycisk na urządzeniu, który ma być przypisany do wybranego elementu.

Druga opcja jest praktycznie niemożliwa do wykonania w przypadku panelu dotykowego, ponieważ (w moim programie jak i zapewne w większości przypadków) sygnały dla współrzędnych wysyłane są bezpośrednio po sobie, cały czas do momentu zwolnienia (wtedy też wysyłany jest sygnał informujący o tym), a więc w ten sposób mamy możliwość zaprogramowania tylko zdarzenia dotknięcia/zwolnienia panelu.

Dlatego też kontroler posiada tryb mapowania, służący do wysłania pojedynczego komunikatu dla osi X lub Y. Działa to w następujący sposób: Przez 3 sekundy trzeba jednocześnie trzymać wciśnięte przyciski B1, B2, B5, B6. Kiedy diody pod panelem zaczną mrugać, wtedy naciśnięcie B3 spowoduje wysłanie sygnału dla OX, a B7 dla OY. Po naciśnięciu któregoś tych dwóch przycisków następuje wyjście z trybu mapowania.


Kod (plik główny - XYcontroller.ino):

//**********************************
// Daniel Baczyński
// Kontroler XY z panelem dotykowym
//**********************************

#include <TouchScreen.h>
#include <MIDI.h>

//potencjometry
#define pinK1 A4
#define pinK2 A5

//przyciski
#define pinB1 6
#define pinB2 5
#define pinB3 3
#define pinB4 12
#define pinB5 7
#define pinB6 4
#define pinB7 2
#define pinBShift 8

//diody LED
#define pinLEDshift 9
#define pinLEDright 11
#define pinLEDleft 10
#define LEDstandby 

//stan trybu shift
boolean shiftMode = false;
#define SHIFT_OFFSET 50
unsigned long long pressTime;

//tablica active zawiera stany przyciśnięcia dla poszczególnych przycisków (patrz funkcja measureButton) oraz panelu
//true - przyciśnięty (jeszcze nie zwolniony)
//false - zwolniony
boolean active[10] = {0,0,0,0,0,0,0,0,0,0};
#define panel 0 //indeks dla panelu dotykowego to 0
#define shiftIndex 8
#define mapIndex 9

//zmienne pomocnicze dla pomiarów potencjometrów
int valueKnob1 = -1;
int valueKnob2 = -1;

//panel dotykowy
//wartosci min i max oznaczają sprawdzone doświadczalnie pomiary przy krawędziach panelu przy użyciu palca
TouchScreen ts(3,1,0,2);
int minX=50, maxX=965;
int minY=120, maxY=880;


//deklaracje funkcji pomiarowych
void measureButton (int pin, int note, int index);
void measureKnob (int pin, int note, int &lastMeas);

void setup()
{
  MIDI.begin(1); //nadajemy na pierwszym kanale
  
  //ustawienie pinów
  pinMode(pinB1, INPUT);
  pinMode(pinB2, INPUT);
  pinMode(pinB3, INPUT);
  pinMode(pinB4, INPUT);
  pinMode(pinB5, INPUT);
  pinMode(pinB6, INPUT);
  pinMode(pinB7, INPUT);
  pinMode(pinBShift, INPUT);

  pinMode(pinLEDshift, OUTPUT);  
  pinMode(pinLEDright, OUTPUT);
  pinMode(pinLEDleft, OUTPUT);
  digitalWrite(pinLEDshift, LOW);
  
  //efekt LEDów
  for (int i=0; i<=255; i++)
  {
    analogWrite(pinLEDright, i);
    analogWrite(pinLEDleft, i);
    delay(10);
  }  
}

void loop()
{
  //panel dotykowy - obsługa
  if (ts.touched()) //jeśli jest dotknięty...
  {
    //...i nie wchodzimy w ten blok drugi raz podczas jednego dotknięcia...
    if (!active[panel])
    {
      //wyślij sygnał, że panel jest dotknięty
      if (shiftMode) MIDI.sendControlChange(0 + SHIFT_OFFSET, 127, 1);
      else MIDI.sendControlChange(0, 127, 1);
    }
    active[panel] = true;
    
    //odczyt współrzędnych
    int coords[2];
    ts.read(coords);
    coords[0] = map(coords[0], minX, maxX, 0, 127);
    coords[1] = map(coords[1], minY, maxY, 0, 127);
    
    //wyślij sygnał MIDI
    if (shiftMode)
    {
      MIDI.sendControlChange(1 + SHIFT_OFFSET, coords[0], 1); //wsp. X
      MIDI.sendControlChange(2 + SHIFT_OFFSET, coords[1], 1); //wsp. Y
    }
    else 
    {
      MIDI.sendControlChange(1, coords[0], 1);
      MIDI.sendControlChange(2, coords[1], 1);
    }
    
    //uaktualnij diody
    int coordsX = coords[0]*2;
    analogWrite(pinLEDleft, 255-coordsX);
    analogWrite(pinLEDright, coordsX);
    
    delay (5);
  }
  else
  {
    if (active[panel])
    {
      //jeśli panel zostanie zwolniony to wyślij sygnał, że panel zwolniony :)
      if (shiftMode) MIDI.sendControlChange(0 + SHIFT_OFFSET, 0, 1);
      else MIDI.sendControlChange(0, 0, 1);
      active[panel] = false;
      
      //ustaw poprawnie LEDy
      digitalWrite(pinLEDleft, HIGH);
      digitalWrite(pinLEDright, HIGH);
      
    }
  }//koniec obsługi panelu
  
  //pomiary potencjometrów  
  delay(5);
  measureKnob(pinK1, 4, valueKnob1);
  delay(5);
  measureKnob(pinK2, 5, valueKnob2);  
  
  //pomiary przycisków
  measureButton(pinB1, 11, 1);
  measureButton(pinB2, 12, 2);
  measureButton(pinB3, 13, 3);
  measureButton(pinB4, 14, 4);
  measureButton(pinB5, 15, 5);
  measureButton(pinB6, 16, 6);
  measureButton(pinB7, 17, 7);
  
  //przycisk trybu shift
  if (digitalRead(pinBShift) == HIGH)
  {
    if (!active[shiftIndex]) //jeśli pierwsze wejście podczas pomiaru...
    {
      //zmień tryb
      shiftMode = !shiftMode;
      digitalWrite(pinLEDshift, shiftMode);
        
      active[shiftIndex] = true; 
    }
  }
  else if (active[shiftIndex]) active[shiftIndex] = false;
  
  //sprawdzenie 4 przycisków - tryb mapowania
  if (digitalRead(pinB1) && digitalRead(pinB2) && digitalRead(pinB5) && digitalRead(pinB6))
  {
    if (!active[mapIndex])
    {
      //zapisujemy czas naciśnięcia
      pressTime = millis();
      active[mapIndex] = true;
    }
    else
    {
      //jeśli przyciski są trzymane 3 sekundy
      if ( (millis() - pressTime) >= 3000)
      {
        //wysyłanie sygnału MIDI dla OX lub OY
        unsigned long lightTime = millis();
        digitalWrite(pinLEDright, HIGH);
        digitalWrite(pinLEDleft, HIGH);
        
        boolean lights = true;
        boolean pushed = false;
        while (!pushed)
        {
          if (digitalRead(pinB3)) //OX
          {
            pushed = true;
            if (shiftMode) MIDI.sendControlChange(1 + SHIFT_OFFSET, 0, 1);
            else MIDI.sendControlChange(1, 0, 1);
            active[mapIndex] = false;
            while (digitalRead(pinB3)) {} // aby po wyjściu nie został od razu "naciśnięty" przycisk B3
          }
          else if (digitalRead(pinB7)) //OY
          {
            pushed = true;
            if (shiftMode) MIDI.sendControlChange(2 + SHIFT_OFFSET, 0, 1);
            else MIDI.sendControlChange(2, 0, 1);
            active[mapIndex] = false;
            while (digitalRead(pinB7)) {} // aby po wyjściu nie został od razu "naciśnięty" przycisk B7
          }
          
          //zmień stan diód
          if ( millis() - lightTime >= 200 )
          {
            lights = !lights;
            digitalWrite(pinLEDright, lights);
            digitalWrite(pinLEDleft, lights);
            lightTime = millis();
          }
        } // koniec while (!pushed)
      }
    }
  } // koniec trybu mapowania
  
} //koniec loop ------------------------------------------------------------

//funkcja do odczytu stanu potencjometrów
//pin - który pin sprawdzić
//note - jaką nutę MIDI wysłać
//lastMeas - ostatni pomiar przesyłany przez referencję, aby móc go nadpisać
void measureKnob (int pin, int note, int &lastMeas)
{
  int meas = 0;
  //odczytujemy wartość na potencjometrze jednocześnie mapując do zakresu 0-127
  //jeśli pomiar jest różny od ostatniego pomiaru (z dokładnością do 2) to powinniśmy wysłać sygnał MIDI
  
  analogRead(pin); //pomijamy pierwszy pomiar
  meas = analogRead(pin);
  
  if (abs(meas - lastMeas*8) >=2 ) 
  {
    if ( (meas = map(meas, 0, 1023, 0, 127)) != lastMeas )
    {
      lastMeas = meas;
      if (shiftMode) MIDI.sendControlChange(note + SHIFT_OFFSET, meas, 1);
      else MIDI.sendControlChange(note, meas, 1);
    }
  } 
}

//funkcja do odczytu stanu przycisku
//pin - który pin sprawdzić
//note - jaką nutę MIDI wysłać
//index - indeks przycisku w tablicy active
void measureButton (int pin, int note, int index)
{
    //jeśli przycisk jest przyciśnięty...
    if (digitalRead(pin) == HIGH) 
    {
      //..to sprawdź czy nie jest to drugi pomiar podczas jednego przyciśnięcia
      if (!active[index])
      {
        //wyślij sygnał przyciśnięta przycisku uwzględniając tryb shift
        if (shiftMode) MIDI.sendControlChange(note + SHIFT_OFFSET, 127, 1);
        else MIDI.sendControlChange(note, 127, 1);
        
        //oznaczmy przycisk jako naciśnięty, aby zapobiec ponownemu wejściu do tego bloku i wysłaniu sygnału jeszcze raz
        active[index] = true; 
      } 
    }
    else if (active[index]) active[index] = false;
}

Komunikacja z programem

Nasz program na Arduino komunikuje się z komputerem za pomocą portu szeregowego. Programy obsługujące MIDI nie odczytują danych z takiego portu, ale wybierają kontroler z listy podpiętych urządzeń MIDI.

Jednym z rozwiązań jest utworzenie wirtualnego portu MIDI i wysyłanie do niego danych z portu szeregowego. Za pomocą LoopBe utworzymy "niewidzialny przewód", którego jeden koniec podłączymy do sterowanego programu, a drugi do Hairless MIDI-Serial, który przyjmuje dane z portu szeregowego i wysyła komunikaty MIDI połączeniem utworzonym przez LoopBe.



Należy się upewnić, że stała MIDI_BAUDRATE (domyślnie 31250) w nagłówku MIDI.h jest taka sama jak ustawienie w Hairless MIDI-Serial (domyślnie 115200).



Hairless MIDI - serial


Do pobrania

Kod źródłowy z bibliotekami:
XYController.rar (kopia)

Dla zainteresowanych umieszczam także ustawienia mapowania do programu Traktor:
XYControler_mapping.tsi (kopia)


Zobacz inne artykułu z cyklu: Arduiono ... czyli prościej się nie da :-)


Oceń artykuł.
Wasze opinie są dla nas ważne, gdyż pozwalają dopracować poszczególne artykuły.
Pozdrawiamy, Autorzy
Ten artykuł oceniam na:

3 komentarze:

  1. jesteś w stanie wykonać taki dla mnie prywatnie? tylko obudowe juz zrobie sam:)

    OdpowiedzUsuń
  2. Witam

    Czy mógłbyś napisać kod dla controllera,
    1. który ma: 10 faderów i 20 przycisków (przy każdym przycisku dioda sygnalizująca jego włączenie.

    Z góry dzięki za wielką POMOC w tym artykule.

    Adam
    djstonka2@gmail.com

    OdpowiedzUsuń

Działy
Działy dodatkowe
Inne
O blogu




Dzisiaj
--> za darmo!!! <--
1. USBasp
2. microBOARD M8


Napisz artykuł
--> i wygraj nagrodę. <--


Co nowego na blogu?
Śledź naszego Facebook-a



Co nowego na blogu?
Śledź nas na Google+

/* 20140911 Wyłączona prawa kolumna */
  • 00

    dni

  • 00

    godzin

  • :
  • 00

    minut

  • :
  • 00

    sekund

Nie czekaj do ostatniego dnia!
Jakość opisu projektu także jest istotna (pkt 9.2 regulaminu).

Sponsorzy:

Zapamiętaj ten artykuł w moim prywatnym spisie treści.