background image

Protokół Modbus/RTU 

Czyli komunikacja na łączu szeregowym RS-485 

 
Przesyłane w obydwie strony wiadomości zorganizowane są w postaci ramek o czterech ściśle określonych 
polach: 

ADDRESS  FUNCTION CODE  DATA  CHECKSUM 

 
W implementacji RTU dla zapytania mamy: 
 
• ADDRESS [1 bajt] - określa do którego urządzenia wpiętego w sieć (tu RS-485) adresowane jest 
zapytanie 
• FUNCTION CODE [1 bajt] - określa kod funkcji/zapytania 
• DATA [różnej długości] - przekazuje parametry wejściowe wywoływanej funkcji 
• CHECKSUM [2 bajty] - stanowi sumę kontrolną 
 
Dla odpowiedzi: 
 
• ADDRESS [1 bajt] - określa adres urządzenia slave odpowiadającego na zapytanie 
• FUNCTION CODE [1 bajt] - określa kod funkcji/zapytania z zapytania 
• DATA [różnej długości] - przekazuje parametry wynikowe funkcji 
• CHECKSUM [2 bajty] - stanowi sumę kontrolną 
 
Najczęściej wykorzystywane funkcje (spośród wielu dostępnych ściśle zdefiniowanych w standardzie) 
przedstawiono w poniższej tabeli: 

Function code 

Function description 

0x01 

Read Coils 

0x02 

Read Discrete Inputs 

0x03 

Read Holding Registers 

0x04 

Read Input Registers 

0x05 

Write Single Coil 

0x06 

Write Single Register 

0x08 

Loopback Diagnostic 

0x0F 

Write Multiple Coils 

0x10 

Write Multiple Registers 

      Oprócz znajomości kodów i struktury poszczególnych funkcji Modbus potrzebne są nam jeszcze adresy 
rejestrów. W przypadku modułów pomiarowych serii ADAM możemy je znaleźć w dokumentacji lub 
odczytać za pomocą ADAM.NET Utility. Dla najpopularniejszych modułów serii ADAM-4000 - czyli 
modułu wejść analogowych ADAM-4017+ i modułu we/wy cyfrowych ADAM-4055 będą to następujące 
wartości: 

ADAM-4017+ 

  

ADAM-4055 

Analog Inputs 

  

Digital Inputs 

40001 

AI-0 

  

00001 

DI-0 

40002 

AI-1 

  

00003 

DI-1 

... 

... 

... 

  

... 

... 

40008 

AI-7 

  

00008 

DI-7 

Analog Inputs Types 

  

Digital Outputs 

40201 

AI-0 typ 

R/W 

  

00017 

DO-0 

R/W 

40202 

AI-1 typ 

R/W 

  

00018 

DO-1 

R/W 

... 

... 

... 

  

... 

... 

40208 

AI-7 typ 

R/W 

  

00024 

DO-7 

R/W 

background image

Module Info 

  

Module Info 

40211 

Name-1 

  

40211 

Name-1 

40212 

Name-2 

  

40212 

Name-2 

40213  Firmware-1 

  

40213  Firmware-1 

40214  Firmware-2 

  

40214  Firmware-2 

 

 

Korzystając z tych informacji możemy już budować strukturę naszych zapytań Modbus/RTU. Na początek 
skorzystamy z funkcji 0x04 - Read input Registers. Struktura jej zapytań i stosownych odpowiedzi, 
zgodnie ze standardem Modbus wygląda następująco: 

Read input Registers - struktura zapytania 
FUNCTION CODE 

1 bajt 

0x04 

DATA 

Starting Address 

2 bajty  0x0000-0xFFFF 

Quantity of Input Registers (N)  2 bajty  0x0001-0x007D 

 

Read input Registers - struktura odpowiedzi 
FUNCTION CODE 

1 bajt 

0x04 

DATA 

Byte Count 

1 bajt 

2xN 

Input Registers 

Nx2 bajtów  - 

 
Przykładowe transakcje będą więc wyglądały następująco: 
 
• Odczyt nazwy urządzenia i wersji firmware dla modułu ADAM-4017+

01 

04 

00-D2 

00-04 

E4-30 

->   01 

04 

08 

40-17-50-00-A2-02-00-00 

C9-14 

 
• Odczyt wartości wejścia analogowego AI-0 dla modułu ADAM-4017+

01 

04 

00-00 

00-01 

31-CA 

->   01 

04 

02 

E5-F9 

33-E2 

 

 

Do ustawienia pojedynczego wyjścia cyfrowego skorzystamy z funkcji 0x05 - Write Single Coil

Write Single Coil - struktura zapytania 
FUNCTION CODE 

1 bajt 

0x05 

DATA 

Output Address 

2 bajty  0x0000-0xFFFF 

Output Value 

2 bajty  0x0000 / 0xFF00 

 

Write Single Coil - struktura odpowiedzi 
FUNCTION CODE 

1 bajt 

0x05 

DATA 

Output Address 

2 bajty  0x0000-0xFFFF 

Output Value 

2 bajty  0x0000 / 0xFF00 

 
Przykładowe transakcje - włączenie i wyłączenie wyjścia cyfrowego DO-0 w module ADAM-4055 - będą 
więc wyglądały następująco: 

02 

05 

00-10 

FF-00 

8D-CC 

->   02 

05 

00-00 

FF-00 

8C-09 

 
02 

05 

00-10 

00-00 

CC-3C 

->   02 

05 

00-00 

00-00 

CD-F9 

 

 

 

 

background image

Do ustawienia kilku wyjść cyfrowych (jedną transakcją) skorzystamy funkcji 0x0F - Write Multiple Coils

Write Multiple Coils - struktura zapytania 
FUNCTION CODE 

1 bajt 

0x0F 

DATA 

Starting Address 

2 bajty 

0x0000-0xFFFF 

Quantity of Outputs 

2 bajty 

0x0001-0x07B0 

Byte Count 

1 bajt 

Outputs Value 

N bajtów  - 

 

Write Multiple Coils - struktura odpowiedzi 
FUNCTION CODE 

1 bajt 

0x0F 

DATA 

Starting Address 

2 bajty 

0x0000-0xFFFF 

Quantity of Outputs 

2 bajty 

0x0001-0x07B0 

 
Przykładowe transakcje - włączenie i wyłączenie wszystkich wyjść cyfrowych w module ADAM-4055 - 
będą więc wyglądały następująco: 

02 

0F 

00-10 

00-08 

01 

FF 

3F-03 

->   02 

0F 

00-00 

00-08 

54-3E 

 
02 

0F 

00-10 

00-08 

01 

00 

7F-43 

->   02 

0F 

00-00 

00-08 

54-3E 

 

 

Do odczytu stanu wejść cyfrowych skorzystamy z funkcji 0x02 - Read Discrete Inputs

Read Discrete Inputs - struktura zapytania 
FUNCTION CODE 

1 bajt 

0x02 

DATA 

Starting Address 

2 bajty  0x0000-0xFFFF 

Quantity of Inputs 

2 bajty  0x0001-0x007D 

 

Read Discrete Inputs - struktura odpowiedzi 
FUNCTION CODE 

1 bajt 

0x02 

DATA 

Byte Count 

1 bajt 

Inputs Status 

N bajtów  - 

 
Przykładowa transakcja - odczyt stanu wszystkich wejść cyfrowych w module ADAM-4055 - będzie więc 
wyglądała następująco: 

02 

02 

00-00 

00-08 

79-FF 

->   02 

02 

01 

AA 

21-B3 

 

 

UWAGA: W przedtawionych wyżej przykładach, w każdej z komend (zgodnie ze standardem Modbus) występują dwa kończące bajty reprezentujące sumę 
kontrolną CRC. Do jej wygenerowania (algorytm jest bardziej złożony niż np. w przypadku protokołu ADAM-ASCII) skorzystać możemy np. z poniższej funkcji 
(Pascal/Delph): 
 
procedure GenerateModbusCRC(var modbus_command:string); 

var CRC:word; 
bajt,bit:byte; 
LSB:byte; 
begin 
  CRC:=$FFFF; 
  for bajt:=1 to length(modbus_command) do 
    begin 
    CRC:=CRC xor ord(modbus_command[bajt]); 
    for bit:=1 to 8 do 
      begin 
      LSB:=CRC and $0001; 
      if LSB=1 then CRC:=CRC-1; 
      CRC:=CRC shr 1; 
      if LSB=1 then CRC:=CRC xor $A001; 
      end; 
    end; 
  modbus_command:=modbus_command+chr(CRC and $00FF)+chr((CRC and $FF00) shr 8); 
end; 

background image

      Na podstawie przedstawionych powyżej danych i przykładów (oraz oczywiście po sięgnięciu do 
dokumentacji protokołu Modbus) możemy jak widać we własnym zakresie - od podstaw - zorganizować 
komunikację naszej aplikacji z modułami pomiarowymi ADAM - w oparciu o bezpośrednią obsługę łącza 
szeregowego. Nie jest to sprawa trywialna - trzeba pamiętać o pewnych dodatkowych, nie opisanych wyżej 
właściwościach protokołu Modbus, oraz - co bardzo ważne - odpowiednio zaimplementować obsługę 
błędów. Na szczęście nie zawsze jesteśmy do tego zmuszeni - z pomocą przychodzi nam zazwyczaj 
implementacja protokołu w systemach SCADA/HMI, plus szeroka dostępność serwerów OPC (pozycja 
taka występuje także w ofercie Advantech). Jednakże znajomość struktury tegoż protokołu będzie zapewne 
pomocna także i w takim przypadku...  

 

 

background image

Protokół Modbus/TCP 

Czyli komunikacja w sieci Ethernet 

 

      W implementacji TCP/IP zrezygnowano z pola adresowego (w odwołaniach Modbus/TCP adres 
urządzeń, do których kierowane jest zapytanie jednoznacznie określa już przy samym nawiązywaniu 
połączenia docelowy adres IP) i sumy kontrolnej. Wprowadzono natomiast rozszerzony nagłówek MBAP 
(Modbus Application Protocol Header) zawierający cztery pola: 
 
• identyfikator transakcji (2 bajty) - wykorzystywany przez urządzenie master do prawidłowego 
kojarzenia odpowiedzi uzyskiwanych na jego kolejne zapytania (wartość ta zostaje ustalona i umieszczona 
w ramce zapytania przez jednostkę master, a następnie skopiowana i umieszczona w ramce odpowiedzi 
przez jednostkę slave) 
• identyfikator protokołu (2 bajty) - pole to ma zawsze wartość 0 odpowiadającą oznaczeniu protokołu 
Modbus 
• rozmiar wiadomości (2 bajty) - liczba pozostałych bajtów wiadomości (poczynając od pola identyfikatora 
urządzenia; pole to zostało wprowadzone ze względu na możliwość dzielenia pojedynczej wiadomości na 
oddzielne pakiety TCP/IP) 
• identyfikator urządzenia (1 bajt) - odgrywający znaczenie np. w przypadku komunikacji z urządzeniami 
Modbus wyposażonymi w interfejs szeregowy za pomocą bram (Modbus Data Gateway) 
 

 

      Odnośnie wykorzystywanych funkcji i ich parametrów: obowiązują oczywiście te przedstawione 
wcześniej - jak w przypadku implementacji RTU (np. dla modułów serii ADAM-4000). Podobnie jak w 
przypadku Modbus/RTU musimy oczywiście także posiadać informacje na temat listy adresów rejestrów 
naszego urządzenia. Dla dwóch najpopularniejszych reprezentantów serii ADAM-6000 i wybranych ich 
rejestrów będą to: 

ADAM-6017 
40001-40008  AI0-AI7 Current Value 

40011-40018  AI0-AI7 Max Value 

40021-40028  AI0-AI7 Min Value 

00101-00108  AI0-AI7 Max Value Reset  R/W 
00111-00118  AI0-AI7 Min Value Reset  R/W 
00131-00138  AI0-AI7 HI Alarm Flag 

00141-00148  AI0-AI7 LO Alarm Flag 

  
ADAM-6060 
00001-00006  DI0-DI5 

00017-00022  DO0-DO5 

R/W 

40001 

CNT0 

R (32bit) 

00033 

CNT0 Start[1]/Stop[0] 

00034 

CNT0 Clear[1] 

 
 

background image

W przypadku kasety komunikacyjnej ADAM-5000/TCP adresy przypisane są natomiast do poszczególnych 
jej slotów: 
 

Slot number  Analog I/O 

Digital I/O 

40001÷40008  00001÷00016 

40009÷40016  00017÷00032 

40017÷40024  00033÷00048 

40025÷40032  00049÷00064 

40033÷40040  00065÷00080 

40041÷40048  00081÷00096 

40049÷40056  00097÷00112 

40057÷40064  00113÷00128 

 
Przykładowe transakcje Modbus/TCP będą więc wyglądały następująco: 
 
• Odczyt wartości wejścia analogowego AI-0 dla modułu ADAM-6017

45-72-00-00-00-06  01  03  00-00  00-01 

->   45-72-00-00-00-05  01  03  02  B8-FA 

 
• Odczyt stanu DI2-DI5 w module ADAM-6060

45-72-00-00-00-06  01  02  00-02  00-04 

->   45-72-00-00-00-04  01  02  01  0F 

 
• Odczyt zawartości licznika CNT0 (DI0) w module ADAM-6060

45-72-00-00-00-06  01  03  00-00  00-02 

->   45-72-00-00-00-07  01  03  04  00-19-00-00 

 
• Skasowanie zawartości licznika CNT0 (na DI0) w module ADAM-6060

45-72-00-00-00-06  01  05  00-21  FF-00 

->   45-72-00-00-00-06  01  05  00-21  FF-00 

 
• Wyłączenie/włączenie licznika CNT0 (na DI0) w module ADAM-6060

45-72-00-00-00-06  01  05  00-20  00-00 

->   45-72-00-00-00-06  01  05  00-20  00-00 

 

45-72-00-00-00-06  01  05  00-20  FF-00 

->   45-72-00-00-00-06  01  05  00-20  FF-00 

 
• Ustawienie wartości 7.000V dla wyjścia analogowego A03 w module ADAM-5024 (slot 2 kasety ADAM-
5000/TCP
): 

45-72-00-00-00-06  01  06  00-13  0B-32 

->   45-72-00-00-00-06  01  06  00-13  0B-32 

gdzie: 
 
   - adres rejestru pobrany ze schematu adresowania (40020 -> 0x0013) 
   - wartość docelowa rejestru obliczona jako 7/10*4095=2866=0x0B32 
   - wykorzystana nie opisana tutaj dotychczas funkcja 0x06 - Write Single Register

Write Single Register - struktura zapytania 
FUNCTION CODE 

1 bajt 

0x06 

DATA 

Register Address 

2 bajty  0x0000-0xFFFF 

Register Value 

2 bajty  0x0000-0xFFFF 

 

Write Single Register - struktura odpowiedzi 
FUNCTION CODE 

1 bajt 

0x06 

DATA 

Register Address 

2 bajty  0x0000-0xFFFF 

Register Value 

2 bajty  0x0000-0xFFFF 

 

 

       

background image

Na podstawie powyższych informacji możemy już ponownie próbować zaimplementować ten protokół w 
naszej aplikacji. Nasza bardzo prosta procedura ustawiająca wyjście cyfrowe DO-0 w module ADAM-
6060
 o adresie IP=192.168.2.130, korzystająca z gotowej kontrolki ClientSocket mógłaby więc wyglądać 
następująco: 

procedure DO0ON

var start,aktu:word; 
komenda:string; 
begin 
//KONFIGURACJA GNIAZDA 
Clientsocket1.Address:='192.168.2.130'; 
Clientsocket1.Port:=502; 
//NAWI

ĄZANIE POŁĄCZENIA 

Clientsocket1.Open; 
start:=gettickcount; 
repeat 
   aktu:=gettickcount; 
   Application.ProcessMessages; 
until (aktu-start>500)or(Clientsocket1.Socket.Connected=true); 
if aktu-start>500 then 
   begin 
   ShowMessage('Po

łączenie nie zostało nawiązane...'); 

   exit; 
   end; 
//KOMENDA=MBAP+ID+FUNCTIONCODE+DATA 
komenda:=chr($AA)+chr($BB)+chr($00)+chr($00)+chr($00)+chr($06)+chr($01)+chr($05)+chr($00)+chr($10)+chr($FF)+c
hr($00); 
//WYS

ŁANIE KOMENDY (ZAPYTANIA) 

Clientsocket1.Socket.SendText(komenda); 
//ZERWANIE PO

ŁĄCZENIA 

Clientsocket1.Close; 
end;