Do Projektu iAutomatyka dołączyli:

https://iautomatyka.pl/wp-content/uploads/2019/08/konwerter-modbus-rtu-tcp.jpg

Konwerter Modbus TCP <> RTU z Qt C++

autor: mateczek.

W poprzednim artykule o QT C++ opisałem swoją bibliotekę, którą napisałem w celu pisania małych wizualizacji w C++ Qt. Obecnie również chciałbym pozostać w tematyce C++ Qt. Tym razem inspirację znalazłem w wątku na grupie Automatyk Może Więcej. Problem dotyczył puszczenia komunikacji Modbus RTU po TCP. Takiego trybu (nie wiedzieć czemu) nie obsługiwała SCADA autora wątku.

Wypada dodać, że program testowałem tylko na symulatorach (na razie) i nie ponoszę odpowiedzialności za wszelkie szkody wynikające z jego działania :D. Z mojej strony napisanie programu miało na celu praktyczne oswojenie się z protokołem Modbus.

O czym mowa?

Program, który jest klientem Modbus TCP łączy się za pomocą TCP/IP z naszą aplikacją. Nasza aplikacja przejmuje port COM1 w komputerze PC. Drugi program pracuje jako symulator urządzenia Modbus RTU (SLAVE). Ten program z kolei przejmuje port COM40 (przelotka USB). Porty COM1 i COM40 są połączone kablem RS232 jak na rysunku.

Kod aplikacji o której mowa znajduje się na moim gitHubie, link: bramkaModbus_github. Graficzna prezentacja poniższego artykułu wygląda następująco.

Zasada działania programu

Zasadę działania programu można streścić do 7 punktów:

  1. Postawienie na komputerze lokalnym serwera Modbus TCP (adres ip = „localhost”, a port=502);
  2. Gdy serwer odbierze od SCADY ramkę Modbus TCP wówczas wyodrębnia części ramki Modbusa RTU;
  3. Policzenie sumy kontrolnej z ramki RTU CRC-16;
  4. Dodanie do ramki Modbus RTU wyznaczonej sumy kontrolnej;
  5. Posłanie do urządzenia ramki Socketem (RTU-OverTcp) lub RSem;
  6. Odebranie odpowiedzi od urządzenia i zbudowanie ramki odpowiedzi dla serwera Modbus TCP;
  7. Wysłanie odpowiedzi w standardzie Modbus TCP;


Ogarnąć TCP/IP i RS232 tym samym kodem?

W C++ mamy funkcje wirtualne, a zarówno QSerialPort  jak i QTcpSoscket dziedziczą po QIODevice. Więc z punktu widzenia programisty nie ma większego znaczenia, czy z urządzeniem skomunikujemy się po RS (Modbus-RTU) czy za pomocą Ethernetu (RTU-over-TCP).

QIODevice* urzadzenie;
if (tryb==1){
//modbus RTU
urzadzenie = new QSerialPort(this)
}else if (tryb==2){
//modbus RTU OVER TCP
urzadzenie =new QTcpSocket(this);
}

A dalej korzystamy już ze wskaźnika na „urządzenie”. Automagiczny mechanizm metod wirtualnych sam prawidłowo określi, czy wywołać metodę obiektu QSerialPort, czy obiektu QTcpSoscket. Tym oto sposobem komunikację po RS opisuje dokładnie ten sam kod co komunikację po TCP/IP.

Różnice w protokołach 

Przechwyćmy teraz ramkę żądania Protokołu Modbus TCP oraz Modbus RTU aby określić jakie są różnice w protokołach. Do nasłuchiwania posłużę się programem „Hercules SETUP utility” natomiast do nadawania ramek programem do modbusa. Poniżej przechwycona ramka Modbusa RTU:

Na kolejnym zrzucie ekranu przedstawiam przechwyconą ramkę Modbus TCP:

Ramka Modbusa TCP zawiera charakterystyczny nagłówek (kolor zielony), gdzie pierwsze dwa bajty określają numer transakcji, a ostatnie dwa to długość pola danych. Część obramowana kolorem pomarańczowym wygląda jak ramka Modbusa RTU, ale nie zawiera sumy kontrolnej. Jak już wiadomo o co chodzi z tymi protokołami, można przejść do programowania konwertera protokołów.

Objaśnienia dla kodu programu

W tej części artykułu przedstawię najważniejsze fragmenty programu

Konstruktor klasy „bramkaModbus”

bramkaModbus::bramkaModbus(QObject *parent) : QObject(parent){
QFile plik("ustawienia.conf");
if (!plik.open(QIODevice::ReadOnly | QIODevice::Text)){
QuitAPP("settings file");
return;
}
QTextStream ts(&plik);
uint16_t port;
ts>>trybTransmisji>>port;
startSerwer(port);
if(trybTransmisji==RTU_TcpIP)
setupRTU_TCP(ts);
else if(trybTransmisji==RTU_RS)
setupRTU_RS232(ts);
}

W konstruktorze otwieram plik ustawień. W zależności od trybu pracy uruchamiam funkcję ustawiającą komunikację po RS (Modbus-RTU) lub po Ethernecie (Modbus-RTU over TCP)

Funkcja „setupRTU_RS232”

W tej funkcji konfigurujemy program do komunikowania się za pomocą portu szeregowego.

void bramkaModbus::setupRTU_RS232(QTextStream &ts){
QString portname;int speed;int db;
ts>>portname>>speed>>db;                                                                           //-1-
QSerialPort *portCom=new  QSerialPort(portname,this);                        //-2-
portCom->setBaudRate(speed);                                                                      //-3-
portCom->setDataBits(static_cast<QSerialPort::DataBits>(db));
portCom->setParity(QSerialPort::NoParity);
portCom->setStopBits(QSerialPort::OneStop);
if(portCom->open(QIODevice::ReadWrite)){
remoteDevice=portCom;                                                                   //-4-!!!!
qDebug()<<"Connect to Device BY Modbus RTU ..........OK";
}else{
QuitAPP("Connect to Device BY Modbus RTU");
}
}
  1. Wczytanie ustawień portu szeregowego z pliku ustawień;
  2. Utworzenie obiektu typu „QSerialPort”  Obiekt odpowiadający za komunikację po RS232;
  3. Ustawienie parametrów dla RS232;
  4. W tej linijce ustawiamy wskaźnik na urządzenie;


Funkcja „setupRTU_TCP”

W tej funkcji konfigurujemy program do komunikowania się za pomocą „QTcpSocket”(Ethernetu).

void bramkaModbus::setupRTU_TCP(QTextStream &ts){
QString ip;uint16_t port;
ts>>ip>>port;                                                                                             //-1-
QTcpSocket*   Socket=new QTcpSocket(this);                                     //-2-
Socket->connectToHost(ip,port,QIODevice::ReadWrite);                  //-3-
if(Socket->waitForConnected()){
remoteDevice=Socket;                                                                            //-4- !!!!!
qDebug()<<"Connect to Device BY Modbus RTU_OVER TCP ................OK";
}else{
QuitAPP("Connect to Device BY Modbus RTU_OVER TCP");
}
}
  1.  Wczytanie ustawień „adres IP” oraz „port” z pliku ustawień;
  2.  Utworzenie obiektu typu „QTcpSocket”. Obiekt odpowiadający za komunikację po Ethernecie;
  3.  Połącz ze zdalnym urządzeniem (Modbus-RTU OVER TCP);
  4.  W tej linii ustawiamy wskaźnik na urządzenie;

Funkcja stawiająca serwer dla Modbus-TCP

void bramkaModbus::startSerwer(uint16_t port){
serwer=new QTcpServer(this);                                               //-1-
if (!serwer->listen(QHostAddress::AnyIPv4, port)){            //-2-
QuitAPP("Server Modbus TCP");
return;
}
qDebug()<< "Server Modbus TCP..............................OK";
connect(serwer,SIGNAL(newConnection()),this,SLOT(newConnection()));    //-3- !!!!
}
  1.  Stworzenie obiektu serwera;
  2.  Włączenie nasłuchu  – serwer gotowy na klientów;
  3.  Gdy serwer zanotuje nowego klienta wyemituje sygnał „newConnection” obsługa w funkcji o tej samej nazwie;

Slot-Funkcja „newConnection”

W tej funkcji obsłużymy nowego klienta

void bramkaModbus::newConnection(){
QTcpSocket* soc = serwer->nextPendingConnection();                                      //-1-
connect(soc,SIGNAL(readyRead()),this, SLOT(modbusQuestionIncoming()));  //-2- !!!
connect(soc,SIGNAL(disconnected()),soc,SLOT(deleteLater()));                          //-3-
}
  1. Przechwycenie namiaru na nowego klienta od serwera;
  2.  Mamy gotową ramkę „Modbusa-Tcp”. Obsługa zapytania w slocie „modbusQuestionIncoming”;
  3.  Jeśli klient się rozłączy obiekt socketa powinien zostać skasowany;

Obsługa zapytania Modbus-TCP

To jest główna funkcja odpowiadająca za odebranie zapytania i odesłanie odpowiedzi.

void bramkaModbus::modbusQuestionIncoming(){
QTcpSocket * s=qobject_cast<QTcpSocket*>(sender());
QByteArray tcpModbusQuestions = s->readAll();
QByteArray respondeFrame=tcpModbusQuestions.mid(0,5); //zapamiętanie w ramce odpowiedz numeru tranzakcji z ramki zapytania
int size=tcpModbusQuestions[5];       //wyciągnięcie bajtu odpowiadającego za rozmiar
if (tcpModbusQuestions.size()!=size+6) return;  //sprawdzenie zgodności odebranej ramki z zadeklarowanym rozmiarem
QByteArray rtuFrame=tcpModbusQuestions.mid(6); //wyodrębnienie ramki RTU z ramki TCP. Ramka RTU zaczyna się od bajtu nr 6
calcCRC16(rtuFrame);            //policzenie sumy kontrolnej ramki RTU. funkcja doda 2 Barty sumy kontrolnej do ramki
remoteDevice->write(rtuFrame);         //wysłanie ramki modubsa-RTU do urządzenia
QByteArray uframe(5+rtuFrame[5]*2,0); //delkaracja ramki (rozmiar ramki odpowiedzi)
for(int count=0;remoteDevice->bytesAvailable()<uframe.size();count++){
//czekanie aż będzie dostępna odpowiednia ilość danych
remoteDevice->waitForReadyRead(10);
if(count>10){
qDebug()<<"the device is not responding"; //timeout urządzenie nie odpowiada
return;
}
};
uframe = remoteDevice->readAll();    // odczytanie pełnej ramki odpowiedzi
if(checkCRC16(uframe)){ //sprawdzenie sumy kontrolnej odebranej ramki
respondeFrame+=uframe.size(); //przygotowanie ramki odpoeiedzi(pole rozmiar)
respondeFrame+=uframe;           //dodajemy do nagłówka modbusTCP ramkę modbusaRTU
s->write(respondeFrame);          //odesłanie do klienta.
s->waitForBytesWritten();
}
}

Tą funkcję napisałem w trybie blokującym. Dla aplikacji konsolowej jest to jak najbardziej OK. W aplikacjach graficznych można w ten sposób zamrozić GUI. Jeśli częstość zapytań była by wielka, a w funkcji obsługującej była by szeregowa, oczekująca sekwencja. Wówczas mógłby być problem z obsługą zdarzeń. Jest to poważna przesłanka do napisania programu wielowątkowego.

Obliczanie sumy kontrolnej CRC_16

Do obsługi protokołu modbus jest dostępna gotowa biblioteka „libmodbus”. Biblioteka ta jest dostępna na licencji GPL i z kodów źródłowych tej biblioteki zapożyczyłem sobie funkcje do liczenia i sprawdzania sumy kontrolnej (lekko dostosowując do swoich potrzeb).

Plik konfiguracyjny

  1. RTU over Tcp  —          1 502 192.168.1.33 502
  2. RTU_RS            —           2 502 com1 9600 8

Parametry potrzebne to: sposób działania (RS232 czy TCP/IP), port na którym ma stanąć nasz serwer, a dalej to już zależy czy z klientem się łączymy po RS? Wówczas będzie to nazwa portu, prędkość transmisji, liczba bitów danych. A jeśli łączymy się po Ethernet konieczne parametry to adres IP oraz numer portu urządzenia.

Film na YouTube prezentujący działanie programu:

https://www.youtube.com/watch?v=Bwgt2kGPyaE?

Mogę pochwalić się też, że wykonałem fizyczny konwerter z użyciem Raspberry PI. Obiecuję niedługo wrzucić film z jego działania na YouTube.

Artykuł został nagrodzony w Konkursie iAutomatyka – edycja Sierpień 2019

Nagrodę Torba sportowa 4F + zestaw gadżetów  dostarcza ambasador konkursu, firma Finder.



Utworzono: / Kategoria: , , ,

Reklama

Newsletter

Zapisz się i jako pierwszy otrzymuj nowości!



PRZECZYTAJ RÓWNIEŻ



NAJNOWSZE PUBLIKACJE OD UŻYTKOWNIKÓW I FIRM

Reklama



POLECANE FIRMY I PRODUKTY