Witam!
To mój pierwszy artykuł na portalu majsterkowo.pl. Chciałbym przedstawić zbudowany przeze mnie moduł łączący dwa światy komunikacji – sieć radiową opartą o moduły NRF24L01 oraz sieć LAN-Ethernet. Płytka ta w założeniu stanowi centralny punkt sterowania “moim inteligentnym domem”. Ma umożliwiać komunikację poprzez lokalną sieć Ethernet lub Wifi z czujnikami i układami wykonawczymi komunikującymi się tylko z wykorzystaniem połączenia radiowego 2.4GHz, wykorzystującego wspomniane powyżej moduły NRF24. Jest to pierwszy artykuł z cyklu. W następnych, będę przedstawiał moduły, które będą komunikowały się z HomeAtionMain. Zatem – do roboty.
Lista elementów:
- Atmega 328P – http://botland.com.pl/avr-tht-dip/1264-mikrokontroler-avr-atmega328p-pu-dip.html – ja używam “gołej” Atmegi, ale można też skorzystać z dowolnego Arduino – Uno lub Leonardo,
- programator USBASP – http://botland.com.pl/programatory/2014-programator-avr-zgodny-usbasp-isp-tasma-idc-rozne-kolory.html – artykuł jak wgrać bootloader Arduino do Atmegi i jak ją programować z wykorzystaniem środowiska Arduino IDE znajdziecie na forum majsterkowa – https://majsterkowo.pl/forum/programowanie-atmega8a-oraz-atmega328p-za-pomoca-arduino-t280-125.html,
- trochę drobnej elektroniki, żeby m. in. poprawnie zasilić Atmegę – rezystor 4,7K, kondensator elektrolityczny 2,2uF, 2 kondensatory ceramiczne 100nF, przycisk monostabilny tact-switch – http://botland.com.pl/tact-switch/378-tact-switch-3×6-5mm-tht-5szt.html, zielona dioda, czerwona dioda + 2 rezystory 1K,
- płytka stykowa / płytka uniwersalna + przewody,
- przełącznik DIP-switch – http://botland.com.pl/dip-switch/759-przelacznik-dip-switch-2-polowy-niebieski.html,
- moduł ENC28J60 – http://botland.com.pl/moduly-sieciowe-ethernet/1471-modul-sieciowy-ethernet-enc28j60.html,
- moduł NRF24L01+ – http://botland.com.pl/moduly-radiowe/1687-modul-radiowy-nrf24l01-24-ghz.html,
- stabilizator 3,3V – http://botland.com.pl/regulatory-napiecia/126-stabilizator-ldo-33v-lf33cv.html i dwa kondensatory – elektrolityczny 2,2uF i ceramiczny 100nF,
- złącze zasilające z portem microUSB – http://botland.com.pl/akcesoria-do-plytek-stykowych/2050-microusb-typ-b-5-pin-zlacze-do-plytki-stykowej-pololu.html,
- zasilacz 5V z wtykiem microUSB – np. taki – http://botland.com.pl/zasilacze-sieciowe-5-v/1363-zasilacz-extreme-microusb-5v-21a-raspberry-pi.html.
Budowa
Na rys.1. przedstawiam schemat elektryczny układu z mikrokontrolerem Atmega z wyprowadzeniami do podłączenia modułu Ethernet i radiowego.
Rys.1. Schemat elektryczny modułu HomeAtionMain
Numeracja pinów dla modułu NRF24:
Numeracja pinów dla modułu Ethernet:
- GND
- MOSI (SI)
- MISO (SO)
- SCK
- CS
- VCC (mój moduł jest zasilany napięciem 5V, przy 3,3V nie chciał współpracować)
Nie mając zbytniego doświadczenia w projektowaniu płytek PCB swój moduł poskładałem na płytce uniwersalnej. Żeby sobie jednak ułatwić zadanie narysowałem najpierw schemat rozmieszczenia elementów, przedstawiony na rys. 2.
Rys.2. Schemat modułu HomeAtionMain na płytce uniwersalnej
Oprogramowanie wbudowane
Na koniec zostawiłem to, co zajęło mi najwięcej czasu, czyli oprogramowanie. Czasochłonność tego elementu wynikała głównie z konieczności znalezienia bibliotek obsługujących moduły: radiowy i Ethernet, które będą chciały ze sobą współpracować. Ponieważ oba moduły komunikują się z mikrokontrolerem za pomocą interfejsu SPI nie była to sprawa trywialna. Zatem na początek lista wykorzystywanych bibliotek. Źródła bibliotek są na Githubie i chciałem serdecznie podziękować ich autorom za ten wkład w rozwój OpenSource.
- https://github.com/jcw/ethercard – biblioteka do obsługi modułu Ethernet – implementacja stosu TCP/IP z obsługą protokołu UDP i HTTP (zarówno strony klienckiej jak i serwerowej)
- https://github.com/aaronds/arduino-nrf24l01 – biblioteka obsługujące moduł NRF24, nie jest tak rozbudowana jak popularna RF24 (https://github.com/maniacbug/RF24), ale bez problemu współdziała z Ethercard
- https://github.com/qistoph/ArduinoAES256 – biblioteka do szyfrowania komunikacji przez moduł radiowy
Powyższe biblioteki można ściągnąć w postaci plików .ZIP z Githuba i zainstalować w Arduino IDE w sposób opisany tutaj. Problem może być z biblioteką arduino-nrf24l01, gdyż w pliku ZIP jest jeden folder za dużo. Tutaj – przygotowany przeze mnie ZIP do instalacji w Arduino IDE.
Kod oprogramowania jest dosyć długi. Całość możecie znaleźć na Github – https://github.com/mariusz-pazur/arduino/blob/master/HomeAtion/homeation_main_simple/homeation_main_simple.ino. Poniżej umieszczam kilka fragmentów wraz z ogólnym opisem działania.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
void setupEthernet() { digitalWrite(greenLedPin, HIGH); if (ether.begin(sizeof Ethernet::buffer, mymac, ethernetcsPin) == 0) { } else { #if STATIC ether.staticSetup(myip, gwip, dnsip, mask); digitalWrite(greenLedPin, LOW); #else if (ether.dhcpSetup()) { digitalWrite(greenLedPin, LOW); } #endif ether.udpServerListenOnPort(&udpSerialPrint, portMy); } } |
Na listingu 1 widzimy funkcję setupEthernet(). Służy ona do inicjalizacji biblioteki obsługującej moduł Ethernetowy. Rozpoczynamy od metody begin, która inicjalizuje bufor do odbierania i wysyłania danych, ustawia adres MAC modułu oraz określa pin CS (ang. chip select) dla komunikacji SPI. Następnie, zależnie od dyrektywy STATIC, następuje ustawienie statycznego adresu IP lub pobranie adresu z serwera DHCP. Na koniec uruchamiany jest nasłuch na porcie UDP (element dodatkowy w tym przykładzie – nie jest niezbędny do działania).
1 2 3 4 5 6 7 8 9 10 11 12 |
void setupRF() { Mirf.cePin = rf24cePin; Mirf.csnPin = rf24csnPin; Mirf.spi = &MirfHardwareSpi; Mirf.init(); Mirf.setRADDR(myAddress); Mirf.payload = commandAndResponseLength; Mirf.channel = 90; Mirf.configRegister( RF_SETUP, ( 1<<2 | 1<<1 | 1<<5 ) ); Mirf.config(); } |
Na listingu 2 mamy funkcję inicjalizacji modułu NRF24. Musimy podać piny wykorzystywany jako CE (ang. chip enable) i CSN (ang. chip select not), adres lokalnego modułu, wielkość wysyłanej zawartości (ang. payload) oraz kanał nadawania (musi być taki sam po stronie odbiorczej i nadawczej). Dodatkowo wykonujemy jeszcze konfigurację modułu poprzez rejestr RF_SETUP. Gdy zajrzymy do noty katalogowej układu, to możemy sprawdzić, że powyższe bity ustawiają maksymalną moc nadawania przez radio oraz prędkość nadawania na 250 kb/s.
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 |
boolean sendRF24Command(byte* commandArray, uint8_t* response) { byte encryptedCommand[commandAndResponseLength]; for (int i = 0; i< commandAndResponseLength; i++) encryptedCommand[i] = commandArray[i]; Mirf.setTADDR(remoteDevices[commandArray[0]].deviceAddress); aes256_encrypt_ecb(&ctxt, encryptedCommand); Mirf.send(encryptedCommand); while(Mirf.isSending()) { } delay(10); unsigned long started_waiting_at = millis(); while(!Mirf.dataReady()) { if ( ( millis() - started_waiting_at ) > 2000 ) { return false; } } Mirf.getData(response); aes256_decrypt_ecb(&ctxt, response); return true; } |
Na listingu 3 widzimy funkcję wysyłającą komendy i odbierającą odpowiedzi na nie, przez moduł NRF24. Dodatkowo przesyłane komendy są szyfrowane za pomocą algorytmu AES-256 (aes256_encrypt_ecb(…)). Przed wysyłką musimy ustawić adres modułu, do którego będziemy nadawać (Mirf.setTADDR(…)), a właściwa wysyłka jest realizowana za pomocą komendy Mirf.send(…). Protokół przeze mnie wymyślony do tej komunikacji zakłada, że na każdą wysłaną komendę przyjdzie odpowiedź. Oczekiwanie na odpowiedź odbywa się w pętli (Mirf.dataReady()), natomiast odczytanie danych realizujemy wywołując Mirf.getData(…). Odebrane dane są deszyfrowane (aes256_decrypt_ecb(…)).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
boolean getCommandFromQuery(char* requestLine, int requestLineLength, byte* commands) { int parameterNumber = 0; for (int i = 0; i < requestLineLength; i++) { char ch = requestLine[i]; if (ch == '=') { commands[parameterNumber] = (byte)atoi(&(requestLine[i+1])); parameterNumber++; } if (ch == '\n') break; } if (parameterNumber == 4) { return true; } else { return false; } } |
Listing 4 przedstawia funkcję wykonującą ekstrakcję parametrów z zapytania HTTP do postaci tablicy bajtów. Przyjąłem założenie, że w żądaniu HTTP przychodzą 4 parametry typu uint8_t, które są przepisywane do tablicy bajtów, a następnie przesyłane drogą radiową. Powyższa metoda wyszukuje w linijce żądania znaki ‘=’ (rozdzielające kolejne parametry w zapytaniu HTTP), a następnie za pomocą funkcji atoi(…) konwertuje je do postaci liczb jednobajtowych. Przykładowe zapytanie – http://192.168.0.5/command?id=1&type=1&cmd=0¶m=3.
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 |
void loop() { boolean hasCommandSend = false; word len = ether.packetReceive(); word pos = ether.packetLoop(len); if (pos) { delay(1); bfill = ether.tcpOffset(); char *data = (char *) Ethernet::buffer + pos; if (strncmp("GET /command", data, 12) == 0) { byte command[commandAndResponseLength]; boolean hasParameters = getCommandFromQuery(data, len, command); int numberOfRetries = 3; while(hasParameters && !hasCommandSend && numberOfRetries > 0) { numberOfRetries--; hasCommandSend = sendRF24Command(command, remoteDevices[command[0]].commandResponse); } if (hasCommandSend) { bfill.emit_p(httpOkHeaders); commandResponse(command[0], remoteDevices[command[0]].commandResponse); ether.httpServerReply(bfill.position()); } else { bfill.emit_p(httpErrorHeaders, commandErrorInfo); ether.httpServerReply(bfill.position()); } } ........ else { bfill.emit_p(http404Headers); ether.httpServerReply(bfill.position()); } } } |
Na listingu 5, w głównej pętli, program odbiera pakiety TCP i poszukuje w początkowych danych pakietu treści świadczącej o tym, że jest to żądanie HTTP – GET /command. Jeżeli takie żądanie zostanie rozpoznane, za pomocą funkcji z listingu 4 wyciągane są parametry żądania, następnie są one przesyłane drogą radiową (patrz listing 3), a odpowiedź od drugiej strony połączenia radiowego jest wstawiana w zdefiniowany w programie obiekt JSON i wysyłana jako odpowiedź na żądanie HTTP.
Poniżej zdjęcia zmontowanego i działającego modułu.
Na koniec jeszcze filmik pokazujący współpracę modułu HomeAtionMain z elementem odbierającym komendy wysyłane drogę radiową – HomeAtion-RemotePower. Ponieważ to mój pierwszy wpis na tym portalu, na pewno sporo mu brakuje do ideału. Jestem otwarty na konstruktywną krytykę i sugestie usprawniające. W następnym wpisie postaram się przedstawić jak zaprojektować i wykonać moduł odbiorczy.
Nareszcie jakiś porządny artykuł :) Dawno nie było, ode mnie leci 5. Powodzenia :)
No, nareszcie jakiś kod który nie wygląda na dzieło typowego “miszcza” Arduino.
Dzięki wielkie za dobre słowo i oceny.
Z przyjemnością czekam na obiecany dalszy ciąg serii!
Zastanawiam się dlaczego na schemacie symbol masy to +3.3V i 5V, a symbol uziemienia to masa?
Schemat był rysowany w programie Fritzing i tam są takie symbole masy i zasilania. Ostatnio się podszkoliłem trochę i nowe schematy staram się rysować w Eagle.
Widzę tu bardzo dużo włożonej pracy i bardzo starannie wykonane urządzenie, mogę zapytać, czy nie lepiej byłoby skorzystać z napisanego już oprogramowania i bibliotek MySensors. Nie neguję pisania własnego, ale tak łatwiej byłoby o integrację projektu z kilkoma dystrybucjami serwera do automatyki, a sprzętowo dokładnie to samo.
Co do pokazanego tu projektu i biblioteki do ENC24l01, czy nie zdarza się, że karta sieciowa zawiesza się niespodziewanie, z niewiadomych przyczyn i z różnymi odstępami czasu???
Pozdrawiam
Dzięki za dobre słowo. Projekt był realizowany półtora roku temu, kiedy MySensors było jeszcze w fazie beta. Napisanie własnego oprogramowania miało dla mnie wysoki walor dydaktyczny, gdyż był to mój pierwszy program na mikrokontroler. Jeśli chodzi o integrację z serwerami do automatyki to w tej chwili pracuję nad integracją z Domoticzem, więc nie widzę tutaj żadnych wad.
Jeśli zaś chodzi o wieszanie się modułu Ethernet ENC28J60 to rzeczywiście miewa on problemy. Przy kilku requestach dziennie (gdy miałem tylko moduł do uruchamiania lampek na choince) właściwie nie było problemu. Jednak gdy podłączyłem czujniki temperatury, które wysyłają dane co minutę, moduł rzeczywiście zaczął się zawieszać w sposób losowy. Zaimplementowałem jednak rozwiązanie programowe, które w pewien sposób rozwiązuje ten problem.