wtorek, 23 października 2018

[29] STM32F7 - Modbus RTU RS485 Slave - Przykładowa implementacja

W tym poście chciałem przedstawić podstawową implementację protokołu Modbus RTU.

[Źródło: http://www.st.com/en/evaluation-tools/32f746gdiscovery.html]

Modbus:


Jest to protokół, który określa zasadę wymiany informacji między jednym bądź wieloma urządzeniami. Jedno urządzenie jest nadrzędne tzw Master. Pozostałe pełnią rolę podrzędną czyli slave. Opisywany protokół występuje w dwóch odmianach ASCII oraz późniejszej implementacji czyli RTU.

Ramka danych protokołu MODBUS RTU wygląda następująco:
  • 1 bajt - adres urządzenia - adres urządzenia slave czyli od 1 do 247 bądź 0 jako adres rozgłoszeniowy.
  • 1 bajt - kod funkcji
  • 0 - 252 bajtów - ilość rejestrów do odczytania oraz dane odczytane lub do zapisania.
  • 2 bajty - suma kontrolna CRC16

Podstawowe funkcje:

  • Read Coils - Odczyt wyjść binarnych - 0x01
  • Read Discrete Inputs - Odczyt wejść binarnych - 0x02
  • Read Holding Register - Odczyt rejestrów pamiętających - 0x03
  • Read Input Register - Odczyt rejestrów wejściowych - 0x04
  • Write Single Coils - Zapis pojedynczego wyjścia binarnego - 0x05
  • Write Single Register - Zapis do jednego rejestru typu holding - 0x06
  • Write Multiple Coils - Zapis wielu wyjść binarnych - 0x0F
  • Write Multiple Registers - Zapis wielu rejestrów - 0x10
  • Error - Diagnostyka błędów - 0x08

Program:


W programie zaimplementowałem wykonywanie rozkazów na przerwaniach od UARTa, zarówno wysyłanie jak i odebranie danych.

Funkcja obsługi błędów:

  1. static uint8_t TransmitError(ModbusDataStructure *ModbusStrPtr, ModbusErrorTypeDef_Enum error_type, uint8_t *brodcastMsgPtr)
  2. {
  3.     errorMsgCounter++;
  4.     if(*brodcastMsgPtr == 1)
  5.     {
  6.         return 1;
  7.     }
  8.     ModbusStrPtr->sendRecBuffer[0] = SLAVE_ID;
  9.     ModbusStrPtr->sendRecBuffer[1] = (ModbusStrPtr->sendRecBuffer[1] | 0x80);
  10.     ModbusStrPtr->sendRecBuffer[2] = error_type;
  11.     ModbusStrPtr->txlen=5;
  12.     return 0;
  13. }

Do funkcji podawana jest struktura z danymi dla protokołu, kod błędu oraz znacznik czy odebrano dane z adresu rozgłoszeniowego. Jeśli przesłano ramkę z takim adresem to nie przesyłamy kodu błędu.

Funkcja odpowiedzialna za obsługę zapisu ramki danych:

  1. static uint8_t writeSingleRegister(ModbusDataStructure *ModbusStructPtr)
  2. {
  3.     uint16_t tmpData = 0;
  4.  
  5.     tmpData=((ModbusStructPtr->sendRecBuffer[2]<<8) + ModbusStructPtr->sendRecBuffer[3]); //Starting address
  6.  
  7.     if(tmpData < HOLDING_REGISTER_SIZE) /* Check exception Illegal Data Address */
  8.     {
  9.         ModbusStructPtr->transmitDataLength = ModbusStructPtr->recDataCount; //responce length
  10.         res_table[tmpData]=(ModbusStructPtr->sendRecBuffer[4]<<8) + ModbusStructPtr->sendRecBuffer[5]; //Register status
  11.  
  12.         return 0;
  13.     }
  14.     else /* Illegal data adrress */
  15.     {
  16.        TransmitError(ModbusStructPtr, Error_IllegalDataAddress, &brodcastMsgRec);
  17.        return 0;
  18.     }
  19. }

W celu zapisania pojedynczego rejestru przesyłany jest adres układu slave po nim następuje adres rejestru do zapisania. Dalej wpisywany jest adres rejestru do zapisu po nim następuje wartość jaka będzie wpisana pod podany adres. Na końcu następuje kod CRC.

Teraz przejdę przez komendą zapisu wielu danych do rejestrów:

  1. static uint8_t writeMultipleRegisters(ModbusDataStructure *ModbusStructPtr)
  2. {
  3.     if (brodcastMsgRec == 1)
  4.     {
  5.         return 1;
  6.     }
  7.  
  8.     // check if the recieved number of bytes matches the calculated bytes minus the request bytes
  9.     // id + function + (2 * address bytes) + (2 * no of register bytes) + byte count + (2 * CRC bytes) = 9 bytes
  10.     if ((ModbusStructPtr->sendRecBuffer[6]) == (ModbusStructPtr->recDataCount - 9))
  11.     {
  12.         uint16_t tmpData_1 = 0;
  13.         tmpData_1=((ModbusStructPtr->sendRecBuffer[2]<<8) + ModbusStructPtr->sendRecBuffer[3]); //Starting address
  14.  
  15.         if(tmpData_1 < HOLDING_REGISTER_SIZE) // check exception 2 ILLEGAL DATA ADDRESS
  16.         {
  17.             uint16_t tmpData_2 = 0;
  18.             tmpData_2=((ModbusStructPtr->sendRecBuffer[4]<<8) + ModbusStructPtr->sendRecBuffer[5]); //Number of register bytes
  19.  
  20.             if((((tmpData_1+tmpData_2)<HOLDING_REGISTER_SIZE) && (tmpData_2<MODBUS_WORD_SIZE+1)))// check exception 3 ILLEGAL DATA VALUE
  21.             {
  22.                 uint16_t maxDataValue = tmpData_1 + tmpData_2;
  23.                 uint8_t positionInBuffer=0;
  24.                 positionInBuffer = 7; // start at the 8th byte in the frame
  25.  
  26.                 for (uint8_t index = tmpData_1; index < maxDataValue; index++)
  27.                 {
  28.                     res_table[index]=(ModbusStructPtr->sendRecBuffer[positionInBuffer]<<8) + ModbusStructPtr->sendRecBuffer[positionInBuffer]; //Register status
  29.                     positionInBuffer+=2;
  30.                 }
  31.  
  32.                 ModbusStructPtr->transmitDataLength = 8; //responce length
  33.  
  34.                 return 0;
  35.              }
  36.              else
  37.              {
  38.                TransmitError(ModbusStructPtr, Error_IllegalDataValue, &brodcastMsgRec);
  39.                return 0;
  40.              }
  41.         }
  42.         else
  43.         {
  44.             TransmitError(ModbusStructPtr, Error_IllegalDataAddress, &brodcastMsgRec);
  45.             return 0;
  46.         }
  47.     }
  48.     return 1;
  49. }

Tutaj po adresie oraz kodzie funkcji przesyłany jest adres startowy, zapisany na dwóch bajtach. Następnie pojawia się ilość rejestrów do wpisania, Po nich następuje ilość danych jakie mają zostać wpisane. W odpowiedzi należy przesłać adres, kod funkcji, rejestr startowy, ilość rejestrów zapisanych oraz kod CRC.

Kolejnym elementem jest komenda pozwalająca na zapis jednego wyjścia binarnego:

  1. static uint8_t writeSingleCoil(ModbusDataStructure *ModbusStructPtr)
  2. {
  3.     uint16_t tmpData = 0;
  4.     tmpData=((ModbusStructPtr->sendRecBuffer[2]<<8) + ModbusStructPtr->sendRecBuffer[3]);
  5.  
  6.     if(tmpData < COILS_DATA_REG_SIZE)
  7.     {
  8.         if(ModbusStructPtr->sendRecBuffer[4] == 0xFF && ModbusStructPtr->sendRecBuffer[5] == 0x00)
  9.         {
  10.             coils[tmpData] = 1;
  11.         }
  12.         else
  13.         {
  14.             coils[tmpData] = 0;
  15.         }
  16.         ModbusStructPtr->transmitDataLength = ModbusStructPtr->recDataCount; //responce length
  17.         return 0;
  18.     }
  19.     else
  20.     {
  21.         TransmitError(ModbusStructPtr,Error_IllegalDataAddress, &brodcastMsgRec);
  22.         return 0;
  23.     }
  24.     return 1;
  25. }

Wpisanie pojedynczego wyjścia polega na podaniu adresu, kodu informacji po nich następują dwa bajty danych z adresem tych informacji, po tym następuje informacja o stanie czyli czy wejście uruchomione czy wyłączone. Na samym końcu podawane jest kod CRC.

Poniżej przedstawię funkcje odczytujące status przekaźników:

  1. static uint8_t readCoil(ModbusDataStructure *ModbusStructPtr)
  2. {
  3.     uint16_t tempData_1 = 0;
  4.     uint16_t tempData_2 = 0;
  5.     uint16_t positionInBuffer=0;
  6.     uint16_t packageLength = 3;
  7.     uint8_t temporaryData = 0;
  8.  
  9.     //Start address
  10.     tempData_1=((ModbusStructPtr->sendRecBuffer[2]<<8) + ModbusStructPtr->sendRecBuffer[3]) + 1;
  11.  
  12.     //Number of coils to read
  13.     tempData_2=((ModbusStructPtr->sendRecBuffer[4]<<8) + ModbusStructPtr->sendRecBuffer[5]);
  14.  
  15.     if((((tempData_1+tempData_2)<HOLDING_REGISTER_SIZE)&(tempData_2<MODBUS_WRD_SZ+1)))
  16.     {
  17.         for(positionInBuffer=0;positionInBuffer<tempData_2;positionInBuffer++)
  18.         {
  19.             for(uint8_t i = positionInBuffer; i<8; i++)
  20.             {
  21.                 if(coils[i+tempData_1] == 1)
  22.                 {
  23.                     temporaryData = temporaryData<<1;
  24.                     temporaryData |= 1;
  25.                 }
  26.                 else
  27.                 {
  28.                     temporaryData = temporaryData<<1;
  29.                     temporaryData |= 0;
  30.                 }
  31.                 positionInBuffer++;
  32.             }
  33.  
  34.             temporaryData = reverseByte(temporaryData);
  35.  
  36.             ModbusStructPtr->sendRecBuffer[packageLength]=temporaryData;
  37.             packageLength+=1;
  38.         }
  39.         ModbusStructPtr->sendRecBuffer[2] = packageLength - 3;
  40.         ModbusStructPtr->transmitDataLength = ModbusStructPtr->sendRecBuffer[2] + 5;
  41.  
  42.         return 0;
  43.     }
  44.     else
  45.     {
  46.         TransmitError(ModbusStructPtr,Error_IllegalDataAddress, &brodcastMsgRec);
  47.         return 0;
  48.     }
  49.     return 1;
  50. }

Odczyt rejestrów wejściowych:

  1. static uint8_t readInputRegister(ModbusDataStructure *ModbusStructPtr)
  2. {
  3.     uint16_t tempData_1 = 0;
  4.     uint16_t tempData_2 = 0;
  5.     uint16_t positionInBuffer=0;
  6.     uint16_t packageLength = 3;
  7.     int16_t temporaryData = 0;
  8.     int16_t temporaryPosition = 0;
  9.  
  10.     tempData_1=((ModbusStructPtr->sendRecBuffer[2]<<8) + ModbusStructPtr->sendRecBuffer[3]);
  11.     tempData_2=((ModbusStructPtr->sendRecBuffer[4]<<8) + ModbusStructPtr->sendRecBuffer[5]);
  12.  
  13.     if((((tempData_1+tempData_2)<HOLDING_REGISTER_SIZE) && (tempData_2<MODBUS_WRD_SZ+1)))
  14.     {
  15.         for(positionInBuffer=0;positionInBuffer<tempData_2;positionInBuffer++)
  16.         {
  17.             temporaryData=res_table[positionInBuffer+tempData_1];
  18.  
  19.             if(temporaryData<0)
  20.             {
  21.                 temporaryPosition=temporaryData;
  22.                 ModbusStructPtr->sendRecBuffer[packageLength]=(temporaryPosition>>8)|0b10000000;
  23.                 ModbusStructPtr->sendRecBuffer[packageLength+1]=temporaryPosition;
  24.             }
  25.             else
  26.             {
  27.                 ModbusStructPtr->sendRecBuffer[packageLength]=temporaryData>>8;
  28.                 ModbusStructPtr->sendRecBuffer[packageLength+1]=temporaryData;
  29.             }
  30.             packageLength+=2;
  31.         }
  32.         ModbusStructPtr->sendRecBuffer[2] = positionInBuffer*2;
  33.         ModbusStructPtr->transmitDataLength = positionInBuffer*2+5;       
  34.  
  35.         return 0;
  36.     }
  37.     else
  38.     {
  39.         TransmitError(ModbusStructPtr,Error_IllegalDataAddress, &brodcastMsgRec);
  40.         return 0;
  41.     }
  42.     return 1;
  43. }

Odczyt rejestrów pamiętających:

  1. static uint8_t readHoldingRegister(ModbusDataStructure *ModbusStructPtr)
  2. {
  3.     uint16_t tempData_1 = 0;
  4.     uint16_t tempData_2 = 0;
  5.     uint16_t positionInBuffer=0;
  6.     uint16_t packageLength = 3;
  7.     int16_t temporaryData = 0;
  8.     int16_t temporaryPosition = 0;
  9.  
  10.     tempData_1=((ModbusStructPtr->sendRecBuffer[2]<<8) + ModbusStructPtr->sendRecBuffer[3]);     //Starting adress bytes
  11.     tempData_2=((ModbusStructPtr->sendRecBuffer[4]<<8) + ModbusStructPtr->sendRecBuffer[5]);    //Number of register bytes
  12.  
  13.     if((((tempData_1+tempData_2)<HOLDING_REGISTER_SIZE)&(tempData_2<MODBUS_WORD_SIZE+1)))
  14.     {
  15.         for(positionInBuffer=0;positionInBuffer<tempData_2;positionInBuffer++)
  16.         {
  17.             temporaryData = res_table[positionInBuffer+tempData_1];
  18.  
  19.             if(temporaryData < 0)
  20.             {
  21.                 temporaryPosition=temporaryData;
  22.                 ModbusStructPtr->sendRecBuffer[packageLength]=(temporaryPosition>>8)|0b10000000;
  23.                 ModbusStructPtr->sendRecBuffer[packageLength+1]=temporaryPosition;
  24.             }
  25.             else
  26.             {
  27.                 ModbusStructPtr->sendRecBuffer[packageLength]=temporaryData>>8;
  28.                 ModbusStructPtr->sendRecBuffer[packageLength+1]=temporaryData;
  29.             }
  30.             packageLength+=2;
  31.         }
  32.  
  33.         ModbusStructPtr->sendRecBuffer[2] = positionInBuffer*2;     //byte count
  34.         ModbusStructPtr->transmitDataLength = positionInBuffer*2+5;                 //responce length
  35.  
  36.         return 0;
  37.     }
  38.     else
  39.     {
  40.         TransmitError(ModbusStructPtr,Error_IllegalDataAddress, &brodcastMsgRec);
  41.         return 0;
  42.     }
  43.  
  44.     return 1;
  45. }

Funkcje odczytu przesyłają ramkę, która wygląda następująco: najpierw adres, następnie ilość odczytanych bajtów, po nim następują dane przesyłane od najniższego adresu. Na końcu wprowadza się kod CRC16.

Zdekodowanie ramki i przesłanie odpowiedzi odbywa się w jednej głównej funkcji:

  1. void modbusDecodeMsg(ModbusDataStructure *ModbusStructPtr)
  2. {
  3.     uint16_t operationStatus = 0;
  4.     checkIfBroadcastMsgRecSetFlag(ModbusStructPtr, &brodcastMsgRec);
  5.  
  6.     if((ModbusStructPtr->sendRecBuffer[0] == SLAVE_ID) && (ModbusStructPtr->recDataCount>5) &&
  7.             ((ModbusStructPtr->sendRecBuffer[0]==SET_PAR[0]) || (ModbusStructPtr->sendRecBuffer[0]==255)))
  8.     {
  9.         operationStatus=calculateCrc16(ModbusStructPtr->sendRecBuffer,ModbusStructPtr->recDataCount-2);
  10.  
  11.       if((ModbusStructPtr->sendRecBuffer[ModbusStructPtr->recDataCount-2]==(operationStatus&0x00FF)) &&
  12.               (ModbusStructPtr->sendRecBuffer[ModbusStructPtr->recDataCount-1]==(operationStatus>>8)))
  13.       {
  14.          switch(ModbusStructPtr->sendRecBuffer[1])
  15.          {
  16.              case READ_COILS:
  17.                  operationStatus = readCoil(ModbusStructPtr);
  18.                  break;
  19.              case READ_HOLDING_REGISTER:
  20.                 operationStatus = readHoldingRegister(ModbusStructPtr);
  21.                 break;
  22.              case READ_INPUT_REGISTER:
  23.                  operationStatus = readInputRegister(ModbusStructPtr);
  24.                  break;
  25.              case WRITE_SINGLE_COILS:
  26.                  operationStatus = writeSingleCoil(ModbusStructPtr);
  27.                  break;
  28.              case WRITE_SINGLE_REGISTER:
  29.                  operationStatus = writeSingleRegister(ModbusStructPtr);
  30.                  break;
  31.              case WRITE_MULTIPLE_REGISTERS:
  32.                 operationStatus = writeMultipleRegisters(ModbusStructPtr);
  33.                 break;
  34.              default:
  35.                 operationStatus = TransmitError(ModbusStructPtr, Error_IllegalFunction, &brodcastMsgRec);
  36.                 break;
  37.          }
  38.  
  39.          if(brodcastMsgRec == 1)
  40.          {
  41.              brodcastMsgRec = 0;
  42.          }
  43.  
  44.        if(operationStatus == 0)
  45.        {
  46.            operationStatus=calculateCrc16(ModbusStructPtr->sendRecBuffer,ModbusStructPtr->transmitDataLength-2);
  47.            ModbusStructPtr->sendRecBuffer[ModbusStructPtr->transmitDataLength-2]=operationStatus;
  48.            ModbusStructPtr->sendRecBuffer[ModbusStructPtr->transmitDataLength-1]=operationStatus>>8;
  49.            ModbusStructPtr->transmitDataCount=0;
  50.        }
  51.     }
  52.   }
  53.     ModbusStructPtr->recDataGap=0;
  54.     ModbusStructPtr->recDataCount=0;
  55.     ModbusStructPtr->rxTimeoutTimer=0xFFFF;
  56. }


Pozostałe funkcje pomocnicze zostały opisane bezpośrednio w bibliotece.

Bibliotekę do postu można pobrać z dysku Google pod tym linkiem.