Modbus RTU – implementacja Atmega
Jakiś czas temu w pracy miałem potrzebę skomunikowania mikrokontrolera z panelem operatorskim firmy Proface. Wybór padł na protokół modbus RTU. Jako, że czytając na forum, nie znalazłem dobrze opisanej implementacji tego protokołu na Atmedze, a jest to czasami potrzebne to postanowiłem podzielić się moimi doświadczeniami.
Całość postanowiłem opisać na przykładzie, bez zbędnej teorii, wszystko można doczytać (polecam plik PDF:
http://www.modbus.org/docs/Modbus_Appli ... _V1_1b.pdf). Mój procesorek to Atmega64, natomiast panel operatorski to Proface GP4106G1D.
Na początek, zapraszam do obejrzenia załączonego filmiku, żeby było jasne o czym jest mowa
http://youtu.be/4LVJcYCdRjUPrzede wszystkim, dosyć szybko udało mi się zrozumieć o co chodzi, dzięki temu, że korzystałem z manuala od chińskiego serva, w którym zawarte są tylko konkretne informacje (fragment owego manuala, w którym opisany jest modus zamieszczam w załączniku).
Modbus opiera się na modelu master-slave. W moim przypadku Proface to master, Atmega – slave.
Na samym początku należy skonfigurować moduł UART, u mnie jest ustawiony na 8 bitów danych, bez parzystości, jeden bit stopu i prędkość 115200 bps. To samo ustawiamy w panelu, dodatkowo w masterze jest jeszcze parametr o nazwie Timeout, oznacza on czas jaki master będzie oczekiwał na odpowiedź po zadaniu pytania. U mnie jest to ustawione na 3 sekundy (tak było ustawione domyślnie). Dodatkowo jeżeli komunikacja będzie opierała się na standardzie RS-485 trzeba przewidzieć pin przełączający na nadawanie i odbiór, oczywiście będzie potrzebny też scalak MAX-485 lub inny, u mnie jest to 75176B texas inst. Oto przykładowy schemat:
Przejdźmy do samej obsługi ramki. Przykładowo na panelu wstawiamy dwie zmienne 16-bitowe, które mają być wysyłane do slavea. W tym momencie na nóżce RXD mikrokontrolera będzie dochodziła ramka danych, która w kodzie HEX wygląda tak:
01
03 02 00 00 02 C5 B3Tak powinna wyglądać odpowiedź:
01
0304 00 B1 1F 40 A3 D4Przejdźmy do opisu pierwszej ramki:
01 - adress slave
03 - kod funkcji
02 00 - adress początkowy pod który trafią dane od slavea
00 02 - ilość 16-bitowych słów, które master chce otrzymać
C5 B3 - suma kontrolna
Opis Odpowiedzi
01 - adress slave
03 - kod funkcji
04 - ilość bajtów jakie slave wysyła do mastera
00 B1 - zawartość pierwszego słowa
1F 40 - zawartość drugiego słowa
A3 D4 - suma kontrolna
Drugi bajt zawsze oznacza kod funkcji. W powyższym przykładzie jest to 0x03 Access multiple words. Pozostałe funkcję są opisane w PDF dotyczącym modbusa, do którego link podałem wcześniej. W moim kodzie obsłużyłem 3 funkcję (0x03, 0x01, 0x0F) ponieważ więcej nie miałem potrzeby.
Zabierzmy się do pisania kodu:
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Inicjalizacji UART nie będę opisywał (jest tak samo napisane jak w bluebooku inicjalizacja RS-485). Natomiast wyjaśnię obsługę otrzymywania ramki. Już nie pamiętam dlaczego ale za pomocą bufora okrężnego nie udało mi się tego obsłużyć, więc rozwiązałem to w inny sposób. Po prostu zdefiniowałem tablicę odbiorczą na rozmiar 11 bajtów ( 11 ponieważ rozmiar ramki w moim przypadku nie przekroczy 11 bajtów) i za każdym razem gdy zostaną odebrane wszystkie bajty z ramki, co jest sprawdzane w warunku if(UART_RxIndex==data_length) to zerowany jest indeks tablicy UART_RxIndeks i ustawiana flaga recieve_done, która powoduje uruchomienie fragmentu kodu gdzie jest wysyłana odpowiedź. Odpowiedź jest wykonywana w funkcji modbus();.
Zanim przejdziemy do opisu odpowiedzi na zapytanie mastera, wyjaśnię jeszcze co się dzieje w przerwaniu Compare Match, na wszelki wypadek napisałem jeszcze watchdoga. Jeśli flaga recieve_done nie jest ustawiona przez dłużej niż 250 ms to inicjalizuje parametry odbioru danych od nowa. Na filmiku jest widoczne jak przerywam komunikację poprzez odłączenie wtyczki i transmisja wstaje za każdym razem. Przy tych ustawieniach nie zdarzyło mi się aby komunikacja się zgubiła.
Teraz należało by odpowiedzieć na pytanie mastera. Wykonuję to w funkcji modbus():
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Myśle, że komentarze w kodzie wyjaśniają większość wątpliwości. Wyjaśnienia wymaga na pewno obliczanie sumy kontrolnej czyli
CRC = crc_chk(Modbus_TxBuf, data_to_send-2); //oblicz sumę kontrolną
CRC_L = CRC; //CRC Check Low (send first)
CRC_H = (CRC>>8);
Funkcja wygląda następująco:
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Funkcja ta jest gotowcem ściągniętym z manuala od serva, który jest załączony poniżej
W funkcji wysyłającej ramkę do proface też nie ma żadnych czarów:
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Jeszcze króciutko wyjaśnię co się dzieje w obsłudze poszczególnych funkcji:
0x03
W Proface mam zadeklarowane 3 zmienne 16-bitowe, więc zapytanie jest o 3 słowa, przekazuje je do bajtów, które są obszarem danych.
U mnie są to pulse – impulsy z enkodera, QB0 i QB1 są to bajty statusowe, zapalam i gaszę poszczególne bity (dzieje się to w funkcji 0x0F) i takty - to będzie coś związanego z pomiarem czasu.
0x01
Z funkcji tej aktualnie nie korzystam w moim programie, ale może ona służyć do obsługi takiej funkcji jak invert bit w panelu operatorskim, po naciśnięciu klawisza na panelu zdeklarowanego jako invert bit, proface zanim dokona operacji zmiany stanu bitu pyta się slave jaki jest aktulanie stan bitu 0 czy 1 i po otrzymaniu odpowiedzi wykonuje operację invert.
0x0F
//ustawianie bitów statusowych
if(Modbus_RxBuf[7]==1) QB0 |= (1<<Modbus_RxBuf[3]); //ustawienie bitów na bajcie QB0
if(Modbus_RxBuf[7]==0) QB0 &= ~(1<<Modbus_RxBuf[3]); //zerowanie bitów na bajcie QB0
Po zdeklarowaniu w panelu przycisku set bit, reset bit lub momentary bit, w momencie gdy bit zostanie ustawiony to na bajcie 7 pojawia się 1, jeżeli wyzerowany to 0, bajt 3 oznacza, który bit jest ustawiany, jeżeli mamy 8 przycisków to przypisujemy im numery od 0 do 7.
Mam nadzieję, że udało mi się w miarę jasno opisać obsługę modbusa. Zdaję sobie sprawę, że program jest daleki od doskonałości. Najlepiej dokonywać przełączanie na odbiór za pomocą przerwania USART_TXC_vect. Nie wykorzystałem tego, ponieważ nie chciało to dobrze działać, i z powodu braku czasu nie dopracowałem tego i jest tak jak teraz. Efekt był taki, że transmisja ruszała z kopyta, a po jakimś czasem coś się gubiło. Nie do końca też rozumiem zależności czasowym występujących w tym protokole, np. w manualu jest zdanie, że po każdej wymianie danych cisza na linii powinna trwać 10 ms. Jak sprawdzam na oscyloskopie u mnie jest to 5 ms. Jednakże u mnie to wszystko dobrze działa.
Mam niewielki staż w programowaniu uC (ledwie rok) więc proszę o wyrozumiałość. Wszystkie uwagi są dla mnie cenne, mam nadzieję, że wspólnie dopracujemy temat tak by każdy mógł bez problemu implementować ten protokół w razie potrzeby