Przedmiotem artykułu jest kontroler serw sterowany przez interfejs DMX512. Sterownik oparty jest na platformie Arduino, konkretnie Arduino Nano. Jak to często bywa, część niezbędnego kodu została już przez kogoś stworzona, mianowicie dostępnych jest w sieci kilka bibliotek, które realizują protokół DMX512 (o większości z nich wspomniano na stronie Arduino), ja wybrałem bibliotekę DMXSerial.
W sieci jest sporo materiałów na temat samego protokołu DMX512, dlatego przedstawię tylko krótki jego opis, a dla chcących bardziej zagłębić się w temat podaję pod artykułem kilka linków.
DMX512 – co to takiego?
Interfejs DMX512 miał swoje początki w połowie lat 80. ubiegłego wieku, przez ten czas przeszedł szereg zmian. Stosowany jest do sterowania oświetleniem, generatorami dymu i wieloma innymi urządzeniami scenicznymi. Do przesyłu informacji używany jest interfejs RS-485, który składa się z dwóch linii przesyłających sygnał symetrycznie, co pozwala na przesyłanie sygnału na duże odległości w obecności dużych zakłóceń (w praktyce zasięg nawet do 1 km). Każda ramka protokołu DMX512 przesyła maksymalnie 512 8-bitowych wartości (stąd „512” w nazwie), każda z tych wartości nazywana jest kanałem, kanały numerowane są od 1 do 512. Ramki wysyłane są przez sterownik systemu, odbiorniki wyposażone są w gniazdo wejściowe i gniazdo wyjściowe, wyjście sterownika dołączane jest do wejścia pierwszego odbiornika, wyjście pierwszego odbiornika dołączane jest do wejścia kolejnego odbiornika itd. Każda ramka przetwarzana jest przez wszystkie odbiorniki, od konfiguracji odbiornika zależy które kanały odczyta i wykona związane z przesyłaną wartością operacje (możliwe jest używanie tego samego kanału dla kilku urządzeń).
Jak połączyć Arduino z DMX512
Mikrokontrolery zastosowane w Arduino nie są wyposażone w interfejs RS-485, natomiast można użyć dodatkowego modułu, który przekonwertuje interfejs RS-485 na UART, ja zastosowałem moduł firmy Waveshare z układem MAX485.
Jak widać moduł ma 5-pinowe złącze. Ponieważ dane będą tylko odbierane, wystarczy dołączyć do Arduino dwie linie:
- RO – linia odbiorcza UART (wyjście danych UART z RS-485), jest dołączona do linii RX0 Arduino
- RSE – ta linia odpowiada za przełączanie trybu pracy układu RS-485, stan niski uruchamia tryb odbioru danych, stan wysoki – tryb nadawania, linia dołączona jest do linii D7 Arduino
Linię RSE można tak naprawdę dołączyć do masy zasilania (o ile mamy pewność, że tryb nadawania nie będzie nigdy używany).
Do interfejsu DMX512 moduł można dołączyć na trzy sposoby:
- za pomocą 2-pinowego złącza goldpin
- za pomocą złącza śrubowego
- za pomocą złącza RJ-11
Wszystkie te złącza zawierają linie A i B, linia A zawiera sygnał D+, linia B – sygnał D-. Sposób połączenia Arduino Nano, modułu z RS-485, złącza DMX512 oraz złącz serw widać na schemacie:
Na schemacie widoczne jest złącze dla sześciu serw, wszystkie piny w rzędach GND i + są ze sobą zwarte, natomiast linie PWM dołączone są do linii D3, D5, D6, D9, D10 i D11, na których Arduino może generować sygnały PWM. Dodatkowo na schemacie widać zworkę na linii łączącej wyjście RO modułu z wejściem RX0 Arduino, zworka jest niezbędna, ponieważ linia RX0 jest używana przez Arduino do komunikacji z wbudowanym układem FTDI232, który umożliwia programowanie Arduino, dodatkowo umożliwia konfigurację sterownika przez terminal. Przy normalnej pracy sterownika zworka musi być założona, podczas programowania i konfiguracji sterownika zworka musi być zdjęta.
Do prawidłowego działania programu wymagana jest instalacja biblioteki DMXSerial, instalacja jak zwykle w Arduino polega na rozpakowaniu archiwum do katalogu libraries.
Sposób działania sterownika
Podczas wykonywania programu kluczowa jest struktura, która zawiera konfigurację serwa:
1 2 3 4 5 6 7 8 9 |
typedef struct ServoControl { Servo servo; byte mode; // tryb pracy serwa: 0 - tryb ciągły, 1 - tryb przerywany, PWM // wysyłany jest po zmianie wartości kanału przez liczbę milisekund // określoną przez stałą SERVO_DELAY unsigned int pin; // numer pinu Arduino dołączonego do serwa unsigned int DMXChannel; // numer kanału interfejsu DMX512 unsigned long lastChangeTime; // czas ostatniej zmiany wartości kanału DMX (używany dla mode=1 } |
Program używa tablicy takich struktur:
1 |
ServoControl servos[SERVOS_CNT]; |
Po dołączeniu zasilania sterownik wywołuje funkcję setup:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
void setup() { unsigned long t; char confCmd[5]; int confCmdIndex = 0; pinMode(7, OUTPUT); // konfiguracja linii dołączonej do linii RSE modułu z MAX485 digitalWrite(7, LOW); // przełączenie MAX485 w tryb odbioru pinMode(13, OUTPUT); // konfiguracja linii diody LED for (int i = 0; i < SERVOS_CNT; i++) // odczyt konfiguracji serw z pamięci EEPROM readConfig(i); confSerial.begin(9600); // konfiguracja programowego UART dla trybu konfiguracji confSerial.println("Aby wejsc w tryb konfiguracji nalezy wyslac polecenie KONF"); t = millis(); while(t + 5000 > millis()) { // oczekiwanie na 5 znaków przez 5 sekund if ((millis() / 100) % 2) // miganie diodą oznacza oczekiwanie na wysłanie polecenia KONF digitalWrite(13, HIGH); else digitalWrite(13, LOW); if (confSerial.available()) { confCmd[confCmdIndex] = confSerial.read(); confSerial.print(confCmd[confCmdIndex++]); if (confCmdIndex == 5) { // jeśli otrzymano 5 znaków i są to znaki KONF<LF> to uruchom tryb konfiguracji, jeśli znaki są inne to idziemy dalej if ((confCmd[0] == 'K') && (confCmd[1] == 'O') && (confCmd[2] == 'N') && (confCmd[3] == 'F') && (confCmd[4] == 10)) { digitalWrite(13, HIGH); confMode(); } else break; } } delay(10); } confSerial.end(); // wyłącz programowy UART digitalWrite(13, LOW); // wyłącz diodę LED servos[0].pin = 3; // ustawienie linii Arduino dla linii PWM serw servos[1].pin = 5; servos[2].pin = 6; servos[3].pin = 9; servos[4].pin = 10; servos[5].pin = 11; for (int i = 0; i < SERVOS_CNT; i++) { // konfiguracja serw DMXSerial.write(servos[i].DMXChannel, 0); servos[i].servo.attach(servos[i].pin); servos[i].servo.write(0); if (servos[i].mode == 1) { servos[i].servo.detach(); servos[i].lastChangeTime = millis(); } } DMXSerial.init(DMXReceiver); // uruchomienie odbioru sygnału DMX512 } |
Funkcja odczytuje zapisaną w pamięci EEPROM konfigurację, pamięć EEPROM zawiera po kolei trójki bajtów dla każdego serwa, trójki zawierają:
- młodszy bajt numeru kanału DMX,
- starszy bajt kanału DMX,
- tryb pracy kanału – dla wartości 0 serwo otrzymuje sygnał PWM przez cały czas, dla wartości 1 serwo otrzymuje sygnał PWM przez czas określony w stałej SERVO_DELAY (w milisekundach) od ostatniej zmiany wartości kanału DMX512
Potem przez UART wysyłany jest komunikat „Aby wejsc w tryb konfiguracji nalezy wyslac polecenie KONF” i przez 5 sekund program oczekuje na ciąg „KONF” potwierdzony znakiem nowej linii (10). W praktyce jeśli zworka jest założona i dołączony nadajnik DMX coś wysyła, to program natychmiast przechodzi dalej, ponieważ odbiera „śmieci” (DMX pracuje z prędkością 250000 b/s, a UART – 9600 b/s), jeśli zworka jest zdjęta i wyślemy przez terminal polecenie KONF, to sterownik przechodzi w tryb konfiguracji, o którym dalej.
Następnie funkcja setup dokonuje konfiguracji odpowiednich zmiennych, inicjalizuje bibliotekę DMXSerial i program przechodzi do cyklicznego wywoływania funkcji loop:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void loop() { for (int i = 0; i < SERVOS_CNT; i++) { if (servos[i].mode == 0) // jeśli serwo pracuje w trybie ciągłym to przelicz wartość kanału DMX na stopnie i ustaw serwo servos[i].servo.write(DMXSerial.read(servos[i].DMXChannel + 1)*180.0/255.0); else { // jeśli serwo pracuje w trybie przerywanym, to sprawdzamy, czy wartość kanału DMX się zmieniła if (servos[i].servo.read() != (int)(DMXSerial.read(servos[i].DMXChannel + 1)*180.0/255.0)) { if (!servos[i].servo.attached())// jeśli serwo nie otrzymuje PWM, to dołącz serwo servos[i].servo.attach(servos[i].pin); // ustaw kąt serwa servos[i].servo.write(DMXSerial.read(servos[i].DMXChannel + 1)*180.0/255.0); // zapisz czas zmiany wartości kanału DMX512 servos[i].lastChangeTime = millis(); } if ((servos[i].servo.attached()) && (millis() - servos[i].lastChangeTime >= SERVO_DELAY)) servos[i].servo.detach(); // jeśli serwo jest otrzymuje PWM i czas SERVO_DELAY już minął to odłącz serwo } } } |
Myślę, że komentarz zawarty w kodzie wystarczy do zrozumienia sposobu jego działania.
Tryb konfiguracji
Konfiguracja sterownika możliwa jest po usunięciu zworki przedstawionej na schemacie. Należy dołączyć Arduino do komputera za pomocą kabla USB i uruchomić program terminala (np. ten, który uruchamia się po naciśnięciu Ctrl+Shift+M w IDE Arduino) pamiętając oczywiście o wyborze odpowiedniego portu COM. Po wysłaniu polecenia „KONF” instrukcja konfiguracji wyświetli się w programie terminala. Polecenie konfiguracyjne ma następującą postać:
<numer_serwa>,<numer_kanału_DMX>,<tryb_pracy>
Parametry określają kolejno:
- Numer serwa, które chcemy konfigurować (pierwsze serwo ma numer 1), drugi parametr
- Numer kanału DMX dla serwa
- Tryb pracy serwa (0 – PWM wysyłany przez caly czas, 1 – PWM wysyłany przez SERVO_DELAY milisekund od ostatniej zmiany)
Dodatkowo po wysłaniu znaku 'P’ można wydrukować konfigurację serw. Po skonfigurowaniu sterownika należy do odłączyć od zasilania i dołączyć ponownie oraz założyć zworkę.
Pełny kod programu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
#include <Servo.h> #include <EEPROM.h> #include <DMXSerial.h> #include <SoftwareSerial.h> #define SERVOS_CNT 6 // liczba kanałów PWM dla serw #define SERVO_DELAY 5000 // czas wysyłania sygnału PWM dla mode=1 typedef struct ServoControl { Servo servo; byte mode; // tryb pracy serwa: 0 - tryb ciągły, 1 - tryb przerywany, PWM // wysyłany jest po zmianie wartości kanału przez liczbę milisekund // określoną przez stałą SERVO_DELAY unsigned int pin; // numer pinu Arduino dołączonego do serwa unsigned int DMXChannel; // numer kanału interfejsu DMX512 unsigned long lastChangeTime; // czas ostatniej zmiany wartości kanału DMX (używany dla mode=1 }; ServoControl servos[SERVOS_CNT]; SoftwareSerial confSerial(0, 1); int getNumber(char * s, int len) { int mult = 1, result = 0; for (int i = len - 1; i>=0; i--) { result += mult * (s[i] - '0'); mult *= 10; } return result; } void printConf(unsigned int servoNo, unsigned int DMXChannel, byte mode) { confSerial.print("Konfiguracja serwa "); confSerial.print(servoNo); confSerial.print(": kanal "); confSerial.print(DMXChannel); confSerial.print(", tryb pracy"); if (mode == 0) confSerial.println(" ciagly"); else confSerial.println(" przerywany"); } void processCmd(char * cmd) { unsigned int servoNo, DMXChannel; byte mode; int commas[2], commaIndex = 0, i = 0; if ((cmd[0] == 'P') && (cmd[1] == 10)) { confSerial.println(""); confSerial.println("Konfiguracja serw:"); for (int j = 0; j < SERVOS_CNT; j++) { readConfig(j); printConf(j + 1, servos[j].DMXChannel, servos[j].mode); } return; } while(cmd[i] != 10) { if (((cmd[i] >= '0') && (cmd[i] <= '9')) || (cmd[i]==',')) { if (cmd[i] == ',') { if (commaIndex == 2) { confSerial.println("Bledna składnia!"); return; } commas[commaIndex++] = i; } } else { confSerial.println("Bledna składnia!"); return; } i++; } if (commaIndex != 2) { confSerial.println("Bledna składnia!"); return; } servoNo = getNumber(cmd, commas[0]); DMXChannel = getNumber(&(cmd[commas[0] + 1]), commas[1] - commas[0] - 1); mode = getNumber(&(cmd[commas[1] + 1]), 1); if (servoNo > SERVOS_CNT) { confSerial.println("Numer serwa poza zakresem!"); return; } if (DMXChannel > 512) { confSerial.println("Numer kanału poza zakresem!"); confSerial.println(DMXChannel); return; } if (mode > 1) { confSerial.println("Tryb pracy serwa poza zakresem!"); return; } writeConfig(servoNo - 1, DMXChannel, mode); printConf(servoNo, DMXChannel, mode); } void confMode() { char cmd[20]; int cmdIndex; confSerial.println("Tryb konfiguracji"); confSerial.print("Liczba kanalow PWM: "); confSerial.println(SERVOS_CNT); confSerial.println("Konfiguracja kanalu: <NUMER_SERWA>,<NUMER_KANALU_DMX>,<TRYB_STEROWANIA>"); confSerial.print(" - <NUMER_SERWA> - liczba z zakresu 1.."); confSerial.println(SERVOS_CNT); confSerial.println(" - <NUMER_KANALU_DMX> - liczba z zakresu 1..512"); confSerial.println(" - <TRYB_STEROWANIA> - 0 lub 1:"); confSerial.println(" 0 - serwo otrzymuje syngal PWM bez przerwy"); confSerial.println(" 1 - serwo otrzymuje syngal PWM przez 5 sekund od zmiany wartosci kanalu"); confSerial.println(" Przyklad: 1,10,0"); confSerial.println("Aby odczytac konfiguracje nalezy wyslac polecenie P"); while (confSerial.available()) confSerial.read(); while(1) { cmdIndex = 0; cmd[0] = 0; while(1) { if (confSerial.available()) cmd[cmdIndex++] = confSerial.read(); if (cmdIndex == 20) { confSerial.println("Polecenie zbyt długie"); break; } if (cmd[cmdIndex - 1] == 10) { processCmd(cmd); break; } } } } int readConfig(unsigned int servoNo) { servos[servoNo].DMXChannel = EEPROM.read(3 * servoNo) | (EEPROM.read(3 * servoNo + 1) << 8); servos[servoNo].mode = EEPROM.read(3 * servoNo + 2); } void writeConfig(unsigned int servoNo, unsigned int DMXChannel, byte mode) { EEPROM.write(3 * servoNo, DMXChannel & 0xFF); EEPROM.write(3 * servoNo + 1, (DMXChannel >> 8) & 0xFF); EEPROM.write(3 * servoNo + 2, mode); } void setup() { unsigned long t; char confCmd[5]; int confCmdIndex = 0; pinMode(7, OUTPUT); // konfiguracja linii dołączonej do linii RSE modułu z MAX485 digitalWrite(7, LOW); // przełączenie MAX485 w tryb odbioru pinMode(13, OUTPUT); // konfiguracja linii diody LED for (int i = 0; i < SERVOS_CNT; i++) // odczyt konfiguracji serw z pamięci EEPROM readConfig(i); confSerial.begin(9600); // konfiguracja programowego UART dla trybu konfiguracji confSerial.println("Aby wejsc w tryb konfiguracji nalezy wyslac polecenie KONF"); t = millis(); while(t + 5000 > millis()) { // oczekiwanie na 5 znaków przez 5 sekund if ((millis() / 100) % 2) // miganie diodą oznacza oczekiwanie na wysłanie polecenia KONF digitalWrite(13, HIGH); else digitalWrite(13, LOW); if (confSerial.available()) { confCmd[confCmdIndex] = confSerial.read(); confSerial.print(confCmd[confCmdIndex++]); if (confCmdIndex == 5) { // jeśli otrzymano 5 znaków i są to znaki KONF<LF> to uruchom tryb konfiguracji, jeśli znaki są inne to idziemy dalej if ((confCmd[0] == 'K') && (confCmd[1] == 'O') && (confCmd[2] == 'N') && (confCmd[3] == 'F') && (confCmd[4] == 10)) { digitalWrite(13, HIGH); confMode(); } else break; } } delay(10); } confSerial.end(); // wyłącz programowy UART digitalWrite(13, LOW); // wyłącz diodę LED servos[0].pin = 3; // ustawienie linii Arduino dla linii PWM serw servos[1].pin = 5; servos[2].pin = 6; servos[3].pin = 9; servos[4].pin = 10; servos[5].pin = 11; for (int i = 0; i < SERVOS_CNT; i++) { // konfiguracja serw DMXSerial.write(servos[i].DMXChannel, 0); servos[i].servo.attach(servos[i].pin); servos[i].servo.write(0); if (servos[i].mode == 1) { servos[i].servo.detach(); servos[i].lastChangeTime = millis(); } } DMXSerial.init(DMXReceiver); // uruchomienie odbioru sygnału DMX512 } void loop() { for (int i = 0; i < SERVOS_CNT; i++) { if (servos[i].mode == 0) // jeśli serwo pracuje w trybie ciągłym to przelicz wartość kanału DMX na stopnie i ustaw serwo servos[i].servo.write(DMXSerial.read(servos[i].DMXChannel + 1)*180.0/255.0); else { // jeśli serwo pracuje w trybie przerywanym, to sprawdzamy, czy wartość kanału DMX się zmieniła if (servos[i].servo.read() != (int)(DMXSerial.read(servos[i].DMXChannel + 1)*180.0/255.0)) { if (!servos[i].servo.attached())// jeśli serwo nie otrzymuje PWM, to dołącz serwo servos[i].servo.attach(servos[i].pin); // ustaw kąt serwa servos[i].servo.write(DMXSerial.read(servos[i].DMXChannel + 1)*180.0/255.0); // zapisz czas zmiany wartości kanału DMX512 servos[i].lastChangeTime = millis(); } if ((servos[i].servo.attached()) && (millis() - servos[i].lastChangeTime >= SERVO_DELAY)) servos[i].servo.detach(); // jeśli serwo jest otrzymuje PWM i czas SERVO_DELAY już minął to odłącz serwo } } } |
Linki
- Opis protokołu DMX512 w Wikipedii
- Inny opis protokołu DMX512 w języku polskim
- Opis interfejsu DMX512 z wieloma dodatkowymi praktycznymi wskazówkami (strona producenta m.in. sterowników DMX512 z interfejsem USB)
Płytkę Arduino Nano oraz moduł firmy Waveshare z układem MAX485 dostarczył sklep internetowy dla elektroników Kamami.pl.