This commit is contained in:
Sergey Yarkov 2025-05-23 16:06:48 +03:00
commit da1a2a5897
4 changed files with 412 additions and 0 deletions

BIN
Im151_mo.zip Normal file

Binary file not shown.

10
README.md Normal file
View File

@ -0,0 +1,10 @@
# IM151-ModbusTCP-Multigraph
Пример чтения данных по протоколу Modbus для прибора Мультиграф Сталь без использования библиотеки Modbus.
Опрос данных осуществлятся за счет следующих функций: `TCON`, `TRCV`, `TSEND` и `TDISCONN`.
[Ссылка на прибор.](https://tpchel.ru/Multigraf_stal)
[Карта Modbus регистров](./modbus-register-map.pdf)
В репозитории находится проект Step7 v5.6 для `IM151-8F PN/DP CPU`.

BIN
modbus-register-map.pdf Normal file

Binary file not shown.

402
read.SCL Normal file
View File

@ -0,0 +1,402 @@
(* Текущий отправляемый запрос на получение значения по адресу Modbus. *)
TYPE UDT_MbCurrReqData
STRUCT
FnCode : BYTE := B#16#0;
StartAddr : WORD := W#16#0;
QtyRegs : WORD := W#16#0;
END_STRUCT
END_TYPE
(* Структура Modbus TCP пакета. *)
TYPE UDT_MbTCPAduReq
STRUCT
TransactionId : WORD := W#16#1;
ProtocolType : WORD := W#16#0;
DataLength : WORD := W#16#6;
UnitId : BYTE := B#16#0;
FnCode : BYTE := B#16#4;
StartAddr : WORD := W#16#0;
QtyRegs : WORD := W#16#0;
END_STRUCT
END_TYPE
(* Параметры TCP соединения. *)
TYPE TCON_PAR
STRUCT
(* Размер типа `TCON_PAR`. Всегда 64 байт*)
block_length : WORD := W#16#40;
(* Идентификатор соединения. От W#16#1 до W#16#0FFF *)
id : WORD := W#16#1;
(*
* Тип соединения.
* W#16#11 — TCP/IP native
* W#16#12 — ISO on TCP
* W#16#13 — UDP
* W#16#01 — TCP/IP native (режим совместимости)
*
* Примечание:
* TCP (compatibility mode): B#16#01 for CPU 315 or 317 <= FW V2.3
* TCP: B#16#11 for CPU 315 or 317 >= FW V2.4,
* IM 151-8 PN/DP CPU, CPU314C, CPU319, CPU412(H), CPU414(H), CPU416(H), CPU417(H)
* These specifications data may vary depending on the firmware used.
* For additional information, refer to:
* http://support.automation.siemens.com/WW/view/en/24294554
*
*)
connection_type : BYTE := B#16#11;
(*
* Идентификатор типа установления соединения.
* Для TCP и ISO on TCP:
* TRUE: Активная установка соединения (клиент).
* FALSE: Пассивная установка соединения (сервер).
*
* Для UDP — всегда FALSE.
*)
active_est : BOOL := FALSE;
(*
* Идентификатор PN/IE интерфейса. Находится в конфигурации HW как номер слота:
* IM 151-8 PN/DP CPU — B#16#1
* CPU 314C, 315 или 317 — B#16#2
* CPU 319 — B#16#3
* CPU 412(H), 414(H), 416(H) and 417(H) — B#16#5
* CPU в стойке 1 станции H — B#16#15
*
* Примечание:
* In H stations: The S7400 PN CPU in rack 0 communicates via local_device_id = 5 and the
* CPU in rack 1 communicates via local_device_id = 15Hex.
*)
local_device_id : BYTE := B#16#5;
(*
* Размер параметра `local_tsap_id` в байтах:
* Для TCP — 0 или 2 (для активного соединения допустимо только 0).
* Для ISO on TCP — 2 или 16.
* Для UDP — 2.
*)
local_tsap_id_len : BYTE := B#16#2;
(* Параметр не используется и должно присвоено значение 0. *)
rem_subnet_id_len : BYTE := B#16#0;
(* Размер параметра `rem_staddr`:
* Неустановленное соединение — B#16#0
* Установленное — B#16#4
*)
rem_staddr_len : BYTE := B#16#0;
(* Размер параметра `rem_tsap_id`. Активное соединения — 2, пассивное — 0. *)
rem_tsap_id_len : BYTE := B#16#0;
(* Размер параметра `next_staddr`. Для PN интерфейса — B#16#0. *)
next_staddr_len : BYTE := B#16#0;
(* Параметр используется для установки локального порта. В режиме сервера (пассивное соединение).
*
* Для `connection_type` W#16#01:
* local_tsap_id[1] младший байт порта в hex
* local_tsap_id[2] старший байт порта в hex
* local_tsap_id[3-16] B#16#00
*
* Для `connection_type` W#16#11:
* local_tsap_id[1] старший байт порта в hex
* local_tsap_id[2] младший байт порта в hex
* local_tsap_id[3-16] B#16#00
*)
local_tsap_id : ARRAY[1..16] OF BYTE;
(* Параметр не используется и должно присвоено значение 0. *)
rem_subnet_id : ARRAY[1..6] OF BYTE;
(* Массив байтов IP адреса удаленного партнера. IP адрес не назначается для неустановленного соединения.
* Порядок байтов зависит от `connection_type`.
* Пример для IP адреса 192.168.0.1:
* Для `connection_type` — B#16#01
* rem_staddr[1] = B#16#01 (1),
* rem_staddr[2] = B#16#00 (0),
* rem_staddr[3] = B#16#A8 (168),
* rem_staddr[4] = B#16#C0 (192),
* rem_staddr[5-6]= B#16#00 (reserved)
* Для `connection_type` — B#16#11
* rem_staddr[1] = B#16#C0 (192),
* rem_staddr[2] = B#16#A8 (168),
* rem_staddr[3] = B#16#00 (0),
* rem_staddr[4] = B#16#01 (1),
* rem_staddr[5-6]= B#16#00 (reserved)
*)
rem_staddr : ARRAY[1..6] OF BYTE;
(*
* Параметр используется для установки порта партнера. Порядок байтов зависит от `connection_type`. Диапозон зависит от CPU.
*
* Для `connection_type` — B#16#01:
* local_tsap_id[1] младший байт порта в hex
* local_tsap_id[2] старший байт порта в hex
* local_tsap_id[3-16] B#16#00
*
* Для `connection_type` — B#16#11
* local_tsap_id[1] старший байт порта в hex
* local_tsap_id[2] младший байт порта в hex
* local_tsap_id[3-16] B#16#00
*)
rem_tsap_id : ARRAY[1..16] OF BYTE;
(* Этот параметр определяет номер стойки и слота коммуникационного процессора который будет использоваться.
* Если CPU использует встроенный PN интерфейс, значение параметра должно быть 0.
*)
next_staddr : ARRAY[1..6] OF BYTE;
(* Параметр не используется и должно присвоено значение 0. *)
spare : WORD := W#16#0;
END_STRUCT
END_TYPE
(* Блок DB хранения данных о приборе. *)
DATA_BLOCK "multigraphSteelDataDB"
STRUCT
IP : ARRAY[1..6] OF BYTE := B#16#C0, B#16#A8, B#16#0B, B#16#02; // IP адрес: 192.168.11.2
SerialNumber : DWORD; // Серийный номер прибора
DeviceType : WORD; // Тип прибора
SteelTempSquare : REAL; // Измеренная площадка по температуре в град.
CurrDate : DWORD; // Текущая дата прибора
CurrTime : DWORD; // Текущее время прибора
END_STRUCT
BEGIN
END_DATA_BLOCK
FUNCTION_BLOCK "multigraphSteelReadFB"
TITLE = 'multigraphSteelRead'
VERSION: '0.1'
VAR_INPUT
netId : WORD := W#16#1; // Идентификатор сетевого подключения
ip : ARRAY[1..6] OF BYTE;
unitId : BYTE; // Идентификатор подчиненного Modbus устройства, всегда `1`
intervalBit : BOOL; // Чтение данных по запросу
connect : BOOL; // Запрос за подключение к прибору
END_VAR
VAR_OUTPUT
netError : BOOL; // Ошибка соединения
netStatus : WORD; // Статус ошибки подключения
isConnected : BOOL; // Статус подключения
isBusy : BOOL;
rdError : BYTE; // Код ошибки Modbus
END_VAR
CONST
mbRdInputRegsFn := 16#04; // Код функции Modbus на чтение входных регистров
mbHRRegsFn := 16#03; // Код функции Modbus на чтение и запись регистров хранения
mbADULength := 12; // Кол-во байт при отправке TCP пакета
mbMaxResLength := 260; // Кол-во байт в ответе от сервера
mbMaxReqRegsLength := 5; // Максимальное кол-во опрашиваемых адресов
(*
* Modbus адреса прибора.
* Карта регистров: https://tpchel.ru/upload/news/2024/2.556.124%20%D0%9F%D1%80%D0%BE%D1%82%D0%BE%D0%BA%D0%BE%D0%BB%20Modbus_%D0%9C-%D0%A1%D0%A2%D0%90%D0%9B%D0%AC_05.02.2024.pdf
*)
mbDeviceTypeAddr := W#16#0006; // Тип прибора / Формат: U16
mbSerialNumberAddr := W#16#0047; // Серийный номер прибора / Формат: U32
mbSteelTempSquareAddr := W#16#1388; // Измеренная площадка по температуре в град. / Формат: F32
mbCurrDateAddr := W#16#0069; // Текущая дата прибора / Формат: U32 / (1-2й байты - год(старший), 3й байт - месяц, 4й байт - день)
mbCurrTimeAddr := W#16#006B; // Текущее время прибора / Формат: U32 / (1й байты - час (старший), 2й байт - минута, 3й байт - секунда, 4й - не исп.)
END_CONST
VAR
netConnect : TCON;
netDisconn : TDISCON;
netSend : TSEND;
netReceive : TRCV;
netConfig : "TCON_PAR";
onRisingEdgeBit : BOOL;
intervalBitLastScan : BOOL;
isReqJobDone : BOOL;
isReqSent : BOOL := FALSE;
mbReqADU : "UDT_MbTCPAduReq";
mbRawResADU : ARRAY[1..mbMaxResLength] OF BYTE;
mbReqRegisters : ARRAY[1..mbMaxReqRegsLength] OF "UDT_MbCurrReqData"; // Опрашиваемые адреса
mbReqIdx : INT := 1;
END_VAR
VAR_TEMP
i : INT;
END_VAR
BEGIN
isReqJobDone := netConnect.DONE;
isBusy := netConnect.BUSY;
netError := netConnect.ERROR;
netStatus := netConnect.STATUS;
IF NOT isConnected THEN
IF NOT netConnect.BUSY THEN
netDisconn(ID := netId, REQ := intervalBit);
isReqSent := FALSE;
END_IF;
(* ** Конфигурирование параметров TCP *************************************************** *)
netConfig.block_length := W#16#40;
netConfig.connection_type := B#16#11; // TCP
netConfig.id := netId;
netConfig.rem_subnet_id_len := 0;
netConfig.local_tsap_id_len := 0;
netConfig.local_device_id := B#16#01; // Slot ID (X1) / IM 151-8 PN/DP CPU
(* IP адрес прибора *)
netConfig.rem_staddr := ip;
netConfig.rem_staddr_len := B#16#4;
(* Порт прибора *)
netConfig.rem_tsap_id[1] := B#16#01;
netConfig.rem_tsap_id[2] := B#16#F6;
netConfig.rem_tsap_id_len := B#16#02;
(* Клиент *)
netConfig.active_est := TRUE;
(* ** Конфигурирование адресов Modbus прибора ******************************************* *)
mbReqRegisters[1].FnCode := mbRdInputRegsFn;
mbReqRegisters[1].StartAddr := mbDeviceTypeAddr;
mbReqRegisters[1].QtyRegs := W#16#0001;
mbReqRegisters[2].FnCode := mbRdInputRegsFn;
mbReqRegisters[2].StartAddr := mbSerialNumberAddr;
mbReqRegisters[2].QtyRegs := W#16#0002;
mbReqRegisters[3].FnCode := mbRdInputRegsFn;
mbReqRegisters[3].StartAddr := mbSteelTempSquareAddr;
mbReqRegisters[3].QtyRegs := W#16#0002;
mbReqRegisters[4].FnCode := mbHRRegsFn;
mbReqRegisters[4].StartAddr := mbCurrDateAddr;
mbReqRegisters[4].QtyRegs := W#16#0002;
mbReqRegisters[5].FnCode := mbHRRegsFn;
mbReqRegisters[5].StartAddr := mbCurrTimeAddr;
mbReqRegisters[5].QtyRegs := W#16#0002;
netConnect(REQ := connect, ID := netId, CONNECT := netConfig);
(* Инициализация на первый запрос *)
mbReqADU.UnitId := unitId;
mbReqADU.TransactionId := 1;
mbReqIdx := 1;
mbReqADU.FnCode := mbReqRegisters[mbReqIdx].FnCode;
mbReqADU.StartAddr := mbReqRegisters[mbReqIdx].StartAddr;
mbReqADU.QtyRegs := mbReqRegisters[mbReqIdx].QtyRegs;
END_IF;
isConnected := netConnect.STATUS = 0 AND netConnect.ERROR = 0;
onRisingEdgeBit := intervalBit AND NOT intervalBitLastScan;
IF isConnected THEN
(* ** Прием данных *********************************************************************************************************************************************************** *)
(* 1. При получении данных идет проверка на ошибку Modbus и случае ошибки от сервера Modbus `rdError` будет его кодом (https://www.simplymodbus.ca/exceptions.htm).
* 2. Если ошибка отсутствует, то происходит проверка запрошенного адреса с адресом в массиве `mbReqRegisters`, и происходит чтение данных.
* 3. После успешного чтения происходит запрос другого Modbus адреса.
*)
netReceive(ID := netId, EN_R := TRUE, DATA := mbRawResADU, LEN := 0); // данные приходят асинхронно.
IF netReceive.NDR THEN
IF BYTE_TO_BOOL(SHR(IN := mbRawResADU[8], N := 7)) AND 1 THEN
rdError := mbRawResADU[9];
ELSE
rdError := 0;
IF mbReqADU.StartAddr = mbSerialNumberAddr THEN
"multigraphSteelDataDB".SerialNumber := READ_U32_BE(BYTE1 := mbRawResADU[10], BYTE2 := mbRawResADU[11], BYTE3 := mbRawResADU[12], BYTE4 := mbRawResADU[13]);
ELSIF mbReqADU.StartAddr = mbDeviceTypeAddr THEN
"multigraphSteelDataDB".DeviceType := READ_U16_BE(BYTE1 := mbRawResADU[10], BYTE2 := mbRawResADU[11]);
ELSIF mbReqADU.StartAddr = mbSteelTempSquareAddr THEN
"multigraphSteelDataDB".SteelTempSquare := READ_F32(BYTE1 := mbRawResADU[10], BYTE2 := mbRawResADU[11], BYTE3 := mbRawResADU[12], BYTE4 := mbRawResADU[13]);
ELSIF mbReqADU.StartAddr = mbCurrDateAddr THEN
"multigraphSteelDataDB".CurrDate := READ_U32_BE(BYTE1 := mbRawResADU[10], BYTE2 := mbRawResADU[11], BYTE3 := mbRawResADU[12], BYTE4 := mbRawResADU[13]);
ELSIF mbReqADU.StartAddr = mbCurrTimeAddr THEN
"multigraphSteelDataDB".CurrTime := READ_U32_BE(BYTE1 := mbRawResADU[10], BYTE2 := mbRawResADU[11], BYTE3 := mbRawResADU[12], BYTE4 := mbRawResADU[13]);
END_IF;
FOR i := 1 TO mbMaxResLength DO mbRawResADU[i] := 0; END_FOR;
mbReqIdx := mbReqIdx + 1;
IF mbReqIdx > mbMaxReqRegsLength THEN
mbReqIdx := 1;
END_IF;
(* Следующий запрос *)
mbReqADU.FnCode := mbReqRegisters[mbReqIdx].FnCode;
mbReqADU.StartAddr := mbReqRegisters[mbReqIdx].StartAddr;
mbReqADU.QtyRegs := mbReqRegisters[mbReqIdx].QtyRegs;
END_IF;
END_IF;
(* *************************************************************************************************************************************************************************** *)
(* ** Отправка на запрос адреса Modbus *************************************************************************************************************************************** *)
IF netSend.ERROR THEN
isConnected := FALSE;
END_IF;
(*IF onRisingEdgeBit THEN
IF netSend.BUSY THEN
RETURN; // Не отсылать новый запрос если предыдущий еще не отправился
ELSE
IF isReqSent THEN
RETURN; // Не отсылать новый запрос запрос если придыдущий успешно отправлен но данные еще не пришли.
END_IF;
isReqSent := TRUE;
END_IF;
END_IF;*)
netSend(ID := netId, LEN := mbADULength, REQ := onRisingEdgeBit, DATA := mbReqADU);
IF netSend.DONE THEN
isReqSent := FALSE;
mbReqADU.TransactionId := INT_TO_WORD(WORD_TO_INT(mbReqADU.TransactionId) + 1);
END_IF;
intervalBitLastScan := intervalBit;
END_IF;
(* *************************************************************************************************************************************************************************** *)
END_FUNCTION_BLOCK
FUNCTION "READ_U16_BE" : WORD
VAR_INPUT
BYTE1 : BYTE;
BYTE2 : BYTE;
END_VAR
BEGIN
READ_U16_BE := SHL(IN := BYTE_TO_WORD(BYTE1), N := 8) OR BYTE_TO_WORD(BYTE2);
END_FUNCTION
FUNCTION "READ_U32_BE" : DWORD
VAR_INPUT
BYTE1 : BYTE;
BYTE2 : BYTE;
BYTE3 : BYTE;
BYTE4 : BYTE;
END_VAR
BEGIN
READ_U32_BE := SHL(IN := BYTE_TO_DWORD(BYTE1), N := 24) OR SHL(IN := BYTE_TO_DWORD(BYTE2), N := 16) OR SHL(IN := BYTE_TO_DWORD(BYTE3), N := 8) OR BYTE_TO_DWORD(BYTE4);
END_FUNCTION
FUNCTION "READ_F32" : REAL
VAR_INPUT
BYTE1 : BYTE;
BYTE2 : BYTE;
BYTE3 : BYTE;
BYTE4 : BYTE;
END_VAR
BEGIN
READ_F32 := DWORD_TO_REAL(IN := SHL(IN := BYTE_TO_DWORD(BYTE1), N := 24) OR SHL(IN := BYTE_TO_DWORD(BYTE2), N := 16) OR SHL(IN := BYTE_TO_DWORD(BYTE3), N := 8) OR BYTE_TO_DWORD(BYTE4));
END_FUNCTION