[W] Własny kontroler MIDI, cz. 5 - zapis/odczyt konfiguracji
Właściwie główną część dzisiejszego wpisu miał zająć opis wykorzystania dotykowego ekranu LCD o symbolu ILI9341 - niestety, takiego opisu nie będzie, bo chwilowo straciłem zapał do tego komponentu. Najpierw okazało się, że nie doczytałem w specyfikacji, że wymaga on zasilania 3.3V, a moje Arduino Pro Micro ma tylko 5V. Dokupiłem odpowiednie konwertery (jak się wydawało), ale prócz nich powinienem też zakupić element zamieniający 5V na 3.3V (sam konwerter stanów logicznych tego nie robi). Na razie zatem dałem sobie z tym spokój, zwłaszcza że lada chwila miała pojawić się alternatywa w postaci touchpadów wymontowanych z laptopów...
Konfiguracja w pamięci
Do rozwiązania pozostał jeszcze jeden problem - czy da się w jakiś sposób przechowywać i zmieniać konfigurację Arduino? Chodzi mi o coś takiego: uruchamiam pierwszy raz mój kontroler i oczywiście wszystkie suwaki czy potencjometry są przypisane do określonych komunikatów CC. Ale chciałbym enkoderem móc zmienić takie przypisanie, tak by np. trzeci suwak nie wysyłał CC21, a CC3. Potrafię taką zmianę zrobić bez wysiłku, jednak zniknie ona wraz z wyłączeniem zasilania Arduino (np. po odpięciu go od USB). Zasadniczo znalazłem dwa w miarę proste sposoby: zapisywanie konfiguracji do pamięci EEPROM samego Arduino albo zapisywanie takiej konfiguracji na karcie pamięci.
Zbudowałem sobie "na szybkości" prosty układ - Arduino Pro Micro, enkoder, ekran LCD (1602A) oraz przełącznik hebelkowy:
Spaghetti Arduino, nowa, wspaniała potrawa!...
Całość wydaje się skomplikowana, ale chciałem prócz samego zapisu przetestować także działanie obsługi konfiguratora. Wykorzystałem sporo elementów, więc i kod programu urósł:
#include <LiquidCrystal_I2C.h> #include <EncoderButton.h> #include <MIDIUSB.h> #include <EEPROM.h> const int CONFIG_BUTTON = 9; const int S1 = 0; const int S2 = 1; const int BUTTON = 14; const int MAX_CC = 99; const int MIN_CC = 1; bool editMode = NULL; int current_value; LiquidCrystal_I2C lcd(0x27, 16, 2); EncoderButton encoder(S1, S2, BUTTON); void setup() { pinMode(CONFIG_BUTTON, INPUT_PULLUP); lcd.init(); lcd.noBacklight(); lcd.noCursor(); current_value = EEPROM.read(0); printOnLcd(current_value); encoder.setEncoderHandler(onEncoderChange); encoder.setClickHandler(onEncoderPressed); } void loop() { bool mode = digitalRead(CONFIG_BUTTON) == HIGH; if (mode != editMode) { editMode = mode; if (editMode) { lcd.backlight(); encoder.enable(true); } else { lcd.noBacklight(); encoder.enable(false); } } if (editMode) { encoder.update(); } delay(20); } void onEncoderChange(EncoderButton& eb) { int value = current_value + eb.increment(); if (value < MIN_CC) { value = MIN_CC; } else if (value > MAX_CC) { value = MAX_CC; } if (value != current_value) { current_value = value; printOnLcd(value); } } void onEncoderPressed(EncoderButton& eb) { store(current_value); lcd.clear(); lcd.setCursor(0, 0); lcd.print("Value stored..."); delay(2000); lcd.clear(); printOnLcd(current_value); } void printOnLcd(int value) { lcd.setCursor(0, 1); String str = "CC"; str += value; str += " "; lcd.print(str.c_str()); } void store(int value) { EEPROM.write(0, value); }
I tak, przełącznik hebelkowy, podłączony do pinu 9, włącza i wyłącza "tryb konfiguracyjny". Tylko w tym trybie działa enkoder (przy wyłączonym "trybie konfiguracyjnym" wyłączam ekran i deaktywuję enkoder). Z kolei enkoder pozwala ustalić numer komunikatu Continous Controller w zakresie 1-99, co ma symulować przypisywanie CC do np. określonego suwaka. Naciśnięcie enkodera powoduje wywołanie procedury store(), a ta zapisuje przekazaną jej wartość pod adresem 0.
Piny S1 i S2 enkodera podłączyłem do wejść 0 i 1 w Arduino, bo te wejścia obsługują przerwania, co oznacza, że zarzenia generują się niezależnie od głównej pętli loop(). Taką konstrukcję wspiera biblioteka EncoderButton, która umożliwia napisanie tzw. handlerów, czyli metod obsługujących zdarzenia (np. pokręcenie enkoderem czy użycie funkcji przycisku). W ten sposób główna pętla tylko odświeża stan enkodera i kontroluje "tryb konfiguracyjny", zaś obsługa enkodera odbywa się w handlerach.
Tutaj uwaga dotycząca pamięci EEPROM. Po pierwsze, w Arduino Pro Micro mamy dostępne 1024 bajty tej pamięci; w innych modelach mikrokontrolera tej pamięci może być więcej, mniej, a może też w ogóle jej nie być.
Po drugie, BARDZO WAŻNE: możemy wykonać ok. 100.000 zapisów do pamięci EEPROM. Wydaje się to dużo, ale jeśli niebacznie "zapuścimy" program, który w pętli loop() będzie miał tylko EEPROM.write(), to zużyjemy tę pamięć w bardzo, bardzo krótkim czasie (różne źródła różnie podają - od ułamka sekundy do kilku sekund, wybaczcie, że nie sprawdziłem osobiście). Zapis należy zatem robić z rozwagą i najlepiej tuż po nim dać jakąś zwłokę czasową, żeby np. móc się zorientować, gdyby coś poszło nie tak. W programie powyżej po zapisie przez dwie sekundy wyświetlany jest napis "Value stored".
Odczyt pamięci możemy za to robić do woli - warto tylko sobie zanotować, pod jakimi adresami przechowujemy co (albo stworzyć sobie strukturę do zapisywania ustawień).
Przy okazji tego prototypu wypróbowałem inną niż ostatnio bibliotekę do obsługi wyświetlacza LCD1602, czyli LiquidCrystal I2C. Ma ona tę zaletę, że umożliwia ukrycie kursora za pomocą metody noCursor(). Do "wyłączenia" ekranu używam metody noBacklight(), co jest oczywiście tylko sztuczką, bo po prostu wyłącza podświetlanie matrycy, jednak do moich celów wystarcza. Dla dociekliwych - jest jeszcze metoda noDisplay(), ale ona z kolei wyłącza wyświetlanie treści, zaś podświetlenie pozostaje bez zmian. Parę display() i noDisplay() można wykorzystać do zaprogramowania "migotania" ekranu, np. żeby zwrócić uwagę użytkownika.
Konfiguracja w pliku
Drugi sposób przechowywania danych wiąże się ze zdobyciem kontrolera kart SD - w moim przypadku kart microSD. Podłączenie do Arduino Pro Micro może na początku stanowić zagwozdkę, ale wykorzystujemy po prostu piny 16 (MOSI), 14 (MISO), 15 (SCK) oraz pin 4 dla CS:
I tym sposobem możemy z Arduino generować gigabajty danych
Żeby mieć co przechowywać, dorzuciłem przełącznik hebelkowy i to jego stan jest zapisywany do pliku.
Program technicznie nie jest niby skomplikowany i nie wymaga zewnętrznych bibliotek, a tylko standardowej biblioteki SD:
#include <SD.h> const int CS_PIN = 4; const int SWITCH_PIN = 9; const String CONFIG_FILE = "config.txt"; const int MAX_LINE_LEN = 10; File myFile; void setup() { Serial.begin(9600); pinMode(SWITCH_PIN, INPUT_PULLUP); while (!Serial) { } if (!SD.begin(CS_PIN)) { Serial.println("Nie mozna zainicjowac czytnika!"); while (1); } writeConfig(); readConfig(); } void loop() { } void writeConfig() { myFile = SD.open(CONFIG_FILE, FILE_WRITE | O_TRUNC); if (myFile) { myFile.print(SWITCH_PIN); myFile.print("="); myFile.println(digitalRead(SWITCH_PIN)); myFile.close(); Serial.println("Zapisano dane."); } else { Serial.print("Blad zapisu do pliku "); Serial.println(CONFIG_FILE); } } void readConfig() { myFile = SD.open(CONFIG_FILE); if (myFile) { String content[2][10]; int index = 0; int column = 0; while (myFile.available()) { char ch = myFile.read(); if (ch != '\n') { if (ch == '=') { column++; } else { content[column][index] += ch; } } else { index++; column = 0; } } myFile.close(); for (int i = 0; i < 10; i++) { String name = content[0][i]; if (name.length() > 0) { Serial.print(content[0][i]); Serial.print(" > "); Serial.println(content[1][i]); } } } else { Serial.print("Nie mozna odczytac pliku "); Serial.println(CONFIG_FILE); } }
Niestety, inicjowanie, zapis oraz odczyt danych wymagają już większej ilości kodu. Warto zwrócić uwagę na fakt, że w typowych przykładach zapisu pliku za pomocą Arduino, pliki na karcie SD otwiera się, korzystając z trybu FILE_WRITE. Kłopot z nim jest taki, że dane są każdorazowo DOPISYWANE do pliku. Jeśli chcemy za każdym razem tworzyć nową zawartość pliku (a przy zapisie konfiguracji właśnie z tym mamy do czynienia), należy dorzucić O_TRUNC.
O ile zapis danych nie stanowi zwykle problemu, bo wygląda zupełnie tak samo jak pisanie na konsolę, to już odczyt stanowi pewne wyzwanie - ja przyjąłem, że czytam pierwszych 10 linii, z których każda musi być zbudowana w sposób: nazwa=wartość. Początkowo myślałem o wykorzystaniu jakieś biblioteki JSON, ale chyba to jednak armata na komary - kilka wartości, jakie będę miał w moim kontrolerze, da się zapisać w taki uproszczony sposób, mogę też ostatecznie zapisać po prostu bajty z wartościami (jednak wolałbym postać tekstową, zawsze można wyjąć kartę SD z czytnika i ją wyedytować w komputerze).
Który sposób jest lepszy? Trudno powiedzieć i raczej zależy to od charakteru zapisywanych danych. Proste dane liczbowe, jak np. numery kontrolerów CC, zapisuje się w pamięci EEPROM bardzo prosto. Jeśli za to chcielibyśmy np. gromadzić jakieś dane pomiarowe z czujników, zdecydowanie lepszym wyborem będzie karta SD. Karta ma plus edycji w komputerze, co czasem może się przydać. Nie ma też w zasadzie limitu pamięci, zwłaszcza jeśli porównać pojemności kart SD z nędznym kilobajtem pamięci EEPROM.
Minusem kontrolera SD jest, rzecz jasna, konieczność pisania większej liczby kodu programu oraz wykorzystanie pinów Arduino, które być może chcielibyśmy wykorzystać do innych celów.
Koniec rozgrzewki
To chyba tyle wstępnych przygotowań - przetestowałem chyba wszystko, co chciałem, więc pora przystąpić do budowy kontrolera. No, pierwszego z kontrolerów, bo pomysłów parę się narodziło przez ten czas. Kolejna część będzie zatem zawierała opis zmagań z materią bardziej fizyczną niż kod w C++ i łączenie pinów. W momencie, gdy piszę te słowa już wiem, że kontroler zbudować się udało, ale czy spełnił wszystkie moje wymagania? Zapraszam do szóstego odcinka.
Komentarze
Prześlij komentarz