kirill.k2
18.01.2025, 00:27
Для интеграции по RS485 счетчиков Пульсар М существует готовая библиотека для Codesys 3.5. А вот для Codesys 2.3 не нашел, ниже моя реализация. Может кому пригодится.
По мотивам 1 (https://ftp.owen.ru/CoDeSys3/04_Library/05_3.5.11.5/02_Libraries/02_vendor_protocols/protokol_pulsar_m.pdf), 2 (https://ftp.owen.ru/CoDeSys3/04_Library/05_3.5.11.5/02_Libraries/02_vendor_protocols/pulsar_m.pdf), 3 (https://owen.ru/forum/showthread.php?t=34449), 4 (https://owen.ru/uploads/373/syslibcom_ru.pdf) и ряда примеров с форума.
Умеет из коробки:
искать адрес устройства (один раз - при старте POU по IN),
считывать текущие показания первого счетчика
считывать температуру
считывать напряжение встроенной батареи
синхронизировать время на счетчике с системным временем ПЛК (один раз - при старте POU по IN)
проверять формат/crc/иные ошибки фрейма данных
проводить опрос с заданным периодом
Рекомендуется с синхронизацей времени по NTP (https://owen.ru/forum/showthread.php?t=29703&page=5&p=292282&viewfull=1#post292282).
Пример живого POU из работающего проекта (Отладочное логгирование и FB NTP в код ниже не входят):
81388
Пример прокидывания в Modbus, обратите внимание на множители в комментариях к регистрам.
81386
Используемые библиотеки: Oscat Basic, SysComLib, ComService, SysLibTime, SysLibMem.
Немного про адрес устройства
В протоколе Пульсар адрес устройства задается в формате BCD (https://ru.wikipedia.org/wiki/BCD). Поэтому удобно его прописывать в виде 16и-ричной переменной.
Например, для устройства с серийным номером 12345 адрес выглядит так: 16#00012345
Можно замкнуть выход POU найденного адреса R_ADDR на вход адреса ADDR - т.к. при первом запросе поиска устройств адрес, очевидно, не нужен.
Доступно в виде готовой библиотеки. Лицензия пусть будет LGPL3 - можно свободно использовать, изменять - но выкладывать изменения необходимо.
81482 (версия 0.2)
Ниже - ST POU и модули версии 0.1
PULSAR_M
(*
Периодический запрос показателей, параметров, запрос/установка даты счетчика Пульсар М по RS485
Описание протокола https://ftp.owen.ru/CoDeSys3/04_Library/05_3.5.11.5/02_Libraries/02_vendor_protocols/protokol_pulsar_m.pdf
*)
PROGRAM PULSAR_M
VAR_INPUT
IN: BOOL;
(* Адрес устройства *)
ADDR: DWORD := 16#00000000;
(* Для ПЛК110/160: COM0 – RS485-1; COM1 – RS232; COM2 – RS485-2; COM4 – RS232-Debug; *)
ComPort: PORTS := COM2;
(* Задержка между запросами *)
DELAY: TIME :=T#30s;
(* Обновлять время счетчика *)
UPDATE_DATE: BOOL := FALSE;
END_VAR
VAR_OUTPUT
R_ADDR: DWORD;
R_VALUE: DWORD;
R_VOLTAGE: REAL;
R_TEMP: REAL;
R_DT: DT;
ERROR: PULSAR_ERRORS;
END_VAR
VAR CONSTANT
(* Таймаут ожидания запросов *)
READ_TIMEOUT: TIME :=T#3000ms;
(* Задержка между повторными запросами *)
REPEAT_DELAY: TIME :=T#1000ms;
(* Задержка между запросами в одногм цикле *)
STEP_DELAY: TIME :=T#500ms;
(* Поиск адреса устройства. Аргумент - 1 байт нулей *)
FUN_BROADCAST: BYTE := 16#00;
(* Широковещательный адрес устройства *)
BROADCAST_ADDR: DWORD := 16#F00F0FF0;
(* Получить время *)
FUN_TIME: BYTE := 16#04;
(* Получить показания устройства. Аргумент - 4 байта маски канала *)
FUN_VALUE: BYTE := 16#01;
(* Маска первого канала *)
VALUE_MASK: DWORD := 16#01;
(* Получить параметры прибора. Аргумент - 2 байта код параметра *)
FUN_PARAM: BYTE := 16#0A;
(* Получить напряжение батареи *)
VALUE_VOLTAGE: DWORD := 16#0A;
(* Получить температуру *)
VALUE_TEMP: DWORD := 16#0B;
(* Нулевой параметр *)
VALUE_ZERO: DWORD := 16#00;
(* Установить время. Аргумент - 6 байт времени *)
FUN_SET_TIME: BYTE := 16#05;
(* Записать показания устройства. Аргументы - 4 байта маски канала, 4/8 байт значения *)
FUN_SET_VALUE: BYTE := 16#03;
(* Позиция данных в массиве буфера *)
DATA_OFFSET: BYTE := 6;
(* Позиции адреса при broadcast-запросе *)
BC_ADDR_OFFSET: BYTE := 4;
END_VAR
VAR
STC: PULSAR_STC;
ComService : COM_SERVICE;
Settings : COMSETTINGS;
CTX: CurTimeEx;
TimeAndDate: SystemTimeDate;
Sys_Time: SysTime64;
cmd: BUFER;
res: BUFER;
(* Для конвертации температуры *)
prTmp: POINTER TO REAL;
dwTmp: DWORD;
(*Таймер периодичности опроса*)
tonDelay: TON;
(*Таймер задержки между шагами*)
tonStep: TON;
(* ID запроса - начальное значение должно быть отлично от нуля*)
wReqId: WORD := 1;
(* Дата для установки *)
abCurrentDate: ARRAY [0..5] OF BYTE;
i: WORD;
STATE : BYTE := 0;
bTimeIsSet: BOOL := FALSE;
END_VAR
(* Настройка порта 9600, 8бит, нет четности, один стоп бит *)
IF STATE = 0 AND IN THEN
Settings.Port := ComPort;
Settings.dwBaudRate := 9600;
Settings.byParity := 0;
Settings.dwTimeout := 0;
Settings.byStopBits := 0;
Settings.dwBufferSize := 0;
Settings.dwScan := 0;
ComService(ENABLE := TRUE, SETTINGS := Settings, TASK := OPEN_TSK);
tonStep(IN:=TRUE, PT:=STEP_DELAY);
bTimeIsSet := FALSE;
END_IF
IF NOT IN THEN
ComService(ENABLE := FALSE, SETTINGS := Settings, TASK := CLOSE_TSK);
STATE := 0;
tonStep(IN:=FALSE, PT:=T#0s);
tonDelay(IN:=FALSE, PT:=T#0s);
END_IF
IF NOT ComService.Ready THEN
RETURN;
END_IF
CASE STATE OF
0:
IF ComService.Ready THEN
STATE := 1;
END_IF
(* Поиск устройств - чтение адреса *)
1:
cmd := PULSAR_CMD(ADDR := BROADCAST_ADDR, FUN := FUN_BROADCAST, ID:= 0, DATA := VALUE_ZERO, DATA_LEN := 1);
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=10);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
R_ADDR := SHL(STC.OUT.BUF[BC_ADDR_OFFSET + 0], 24) OR SHL(STC.OUT.BUF[BC_ADDR_OFFSET + 1], 16) OR SHL(STC.OUT.BUF[BC_ADDR_OFFSET + 2], 8) + SHL(STC.OUT.BUF[BC_ADDR_OFFSET + 3], 0);
mNextStep;
END_IF
END_IF
2:
mNextStepDelay;
(* Чтение показателей *)
3:
cmd := PULSAR_CMD(ADDR := ADDR, FUN := FUN_VALUE, ID:= wReqId, DATA := VALUE_MASK, DATA_LEN := 4);
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=0);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
R_VALUE := SHL(STC.OUT.BUF[DATA_OFFSET + 0], 0) OR SHL(STC.OUT.BUF[DATA_OFFSET + 1], 8) OR SHL(STC.OUT.BUF[DATA_OFFSET + 2], 16) + SHL(STC.OUT.BUF[DATA_OFFSET + 3], 24);
mNextStep;
END_IF
END_IF
4:
mNextStepDelay;
(* Чтение напряжения батареи *)
5:
cmd := PULSAR_CMD(ADDR := ADDR, FUN := FUN_PARAM, ID:= wReqId, DATA := VALUE_VOLTAGE, DATA_LEN := 2);
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=0);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
dwTmp := SHL(STC.OUT.BUF[DATA_OFFSET + 0], 0) OR SHL(STC.OUT.BUF[DATA_OFFSET + 1], 8) OR SHL(STC.OUT.BUF[DATA_OFFSET + 2], 16) OR SHL(STC.OUT.BUF[DATA_OFFSET + 3], 24);
prTmp := ADR(dwTmp);
R_VOLTAGE := prTmp^;
mNextStep;
END_IF
END_IF
6:
mNextStepDelay;
(* Чтение температуры *)
7:
cmd := PULSAR_CMD(ADDR := ADDR, FUN := FUN_PARAM, ID:= wReqId, DATA := VALUE_TEMP, DATA_LEN := 2);
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=0);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
dwTmp := SHL(STC.OUT.BUF[DATA_OFFSET + 0], 0) OR SHL(STC.OUT.BUF[DATA_OFFSET + 1], 8) OR SHL(STC.OUT.BUF[DATA_OFFSET + 2], 16) OR SHL(STC.OUT.BUF[DATA_OFFSET + 3], 24);
prTmp := ADR(dwTmp);
R_TEMP := prTmp^;
mNextStep;
END_IF
END_IF
8:
mNextStepDelay;
(* Обновление даты счетчика *)
9:
(* Пропускаем, если уже обновили ИЛИ нет признака обновления даты*)
IF bTimeIsSet OR NOT UPDATE_DATE THEN
STATE := STATE + 2;
ELSE
(* Чтение системного времени через юзабилищный SysLibTime *)
TimeAndDate.Day :=0; TimeAndDate.DayOfWeek :=0; TimeAndDate.dwHighMsec :=0;
TimeAndDate.dwLowMSecs :=0; TimeAndDate.Milliseconds :=0; TimeAndDate.MINUTE :=0;
TimeAndDate.SECOND :=0; TimeAndDate.HOUR :=0; TimeAndDate.Year :=0; TimeAndDate.Month :=0;
Sys_time.ulHigh :=0; Sys_time.ulLow :=0;
CTX (SystemTime:=Sys_Time , TimeDate:= TimeAndDate);
abCurrentDate[0] := UINT_TO_BYTE(TimeAndDate.YEAR - 2000);
abCurrentDate[1] := UINT_TO_BYTE(TimeAndDate.Month);
abCurrentDate[2] := UINT_TO_BYTE(TimeAndDate.Day);
abCurrentDate[3] := UINT_TO_BYTE(TimeAndDate.HOUR);
abCurrentDate[4] := UINT_TO_BYTE(TimeAndDate.MINUTE);
abCurrentDate[5] := UINT_TO_BYTE(TimeAndDate.SECOND);
cmd := PULSAR_CMD_PTR(ADDR := ADDR, FUN := FUN_SET_TIME, ID:= wReqId, DATA_PTR := ADR(abCurrentDate), DATA_LEN := SIZEOF(abCurrentDate));
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=0);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
IF STC.OUT.BUF[DATA_OFFSET] = 16#01 THEN
bTimeIsSet := TRUE;
END_IF;
mNextStep;
END_IF
END_IF
END_IF
10:
mNextStepDelay;
(* Чтение даты *)
11:
cmd := PULSAR_CMD(ADDR := ADDR, FUN := FUN_TIME, ID:= wReqId, DATA := 0, DATA_LEN := 0);
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=0);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
R_DT := SET_DT(
year := 2000 + STC.OUT.BUF[DATA_OFFSET + 0],
month := STC.OUT.BUF[DATA_OFFSET + 1],
day := STC.OUT.BUF[DATA_OFFSET + 2],
HOUR := STC.OUT.BUF[DATA_OFFSET + 3],
MINUTE := STC.OUT.BUF[DATA_OFFSET + 4],
SECOND := STC.OUT.BUF[DATA_OFFSET + 5]
);
mNextStep;
END_IF
END_IF
12:
mNextStepDelay;
(* Пауза между пакетами *)
13:
(* Задержка между запросами *)
IF NOT tonDelay.IN THEN
tonDelay(IN:=TRUE, PT:=DELAY);
ELSIF tonDelay.Q THEN
tonDelay(IN:=FALSE, PT:=T#0s);
STATE := STATE +1;
END_IF
(* Возврат в начало цикла *)
ELSE
STATE := 3;
END_CASE
tonDelay();
tonStep();
Модуль mNextStep
(* Формируем ID запроса инкрементом *)
wReqId := wReqId + 1;
IF wReqId = 16#FFFF THEN
wReqId := 1;
END_IF
STC(IN:=FALSE);
STATE := STATE + 1;
Модуль mNextStepDelay
(* Задержка между запросами *)
IF NOT tonStep.IN THEN
(* Таймер задержки и следующий шаг *)
tonStep(IN:=TRUE, PT:=STEP_DELAY);
ELSIF tonStep.Q THEN
tonStep(IN:=FALSE, PT:=T#0s);
STATE:=STATE+1;
END_IF
PULSAR_STC
(* Реализация взаимодействия по COM порту со спецификой Пульсар М *)
FUNCTION_BLOCK PULSAR_STC
(* Входы функционального блока *)
VAR_INPUT
(* Вход разрешающий работу блока *)
IN: BOOL:=FALSE;
(* Указатель на буфер с массивом байт в котором содержится пересылаемая команда *)
CMD: BUFER;
(* Количество байт в ответе - ноль для вычисления из пакета сообщения Пульсар *)
ANSWER_LEN: WORD := 0;
(* Задержка повторного запроса *)
DELAY: TIME := T#500ms;
(* Номер порта в который отсылается команда и из которого принимаются данные *)
PORT: PORTS := 2;
(* Таймаут ответа *)
TIMEOUT: TIME := T#3000ms;
END_VAR
(* Выходы функционального блока *)
VAR_OUTPUT
(* Признак окончания чтения *)
Q: BOOL := FALSE;
(* Результат принятый от устройства *)
OUT: BUFER;
ERROR: PULSAR_ERRORS := NO_ERROR;
END_VAR
VAR
(*Таймер ожидания ответа*)
tonReply: TON;
(*Таймер периодичности опроса*)
tonDelay: TON;
(*Тригер позволяющий определить фронт сигнала IN и выполнить инициализацию блока*)
rtStart: R_TRIG;
rfEnd: F_TRIG;
(* Буфер чтения и указатель на текущую позицию в буфере *)
bufRead: BUFER;
ptrBufRead: POINTER TO BYTE;
(*Число принятых байт*)
dwBytesReceived: DWORD;
(*Число отправленных байт*)
dwBytesSend: DWORD;
(* Для расчета CRC, ID *)
wInCrc: WORD;
wCrc: WORD;
(*Идентификатор запроса для сверки*)
wID: WORD;
wCmdID: WORD;
bFun: BYTE;
bCmdFun: BYTE;
(* Ожидаемый размер ответа *)
wAnswerLen : WORD;
(* Статус работы блока -
1: задержка повтора,
2: очистка буфера,
3: отправка данных,
4: прием данных
*)
STATE: BYTE:=2;
END_VAR
VAR CONSTANT
(* Сколько читаем байт за цикл. Рекомендуется как минимум захватывать байт с размером *)
READ_ONCE: UINT := 10;
(* Смещение функции в буфере *)
FUN_OFFSET: BYTE := 4;
(* Смещение адреса в буфере *)
LEN_OFFSET: UINT := 5;
(* Смещение данных в буфере *)
DATA_OFFSET: UINT := 6;
(* Таймаут ввода\вывода *)
IO_TIMEOUT: DWORD := 500;
(* Смещение CRC c конца буфера *)
CRC_OFFSET: INT := -2;
(* Смещение ID c конца буфера *)
ID_OFFSET: INT := -4;
(* Конечное состояние *)
END_STATE: BYTE := 5;
END_VAR
(* Анализируем запуск функционального блока *)
rtStart(CLK:=IN);
rfEnd(CLK:=IN);
(* Если пришел разрешающий сигнал запуска блока, то необходимо выполнить его инициализацию *)
IF rtStart.Q THEN
(* Выходы блока не сформированы *)
Q := FALSE;
ERROR := 0;
(* Переводим блок в режим отправки команды *)
STATE := 2;
END_IF
IF rfEnd.Q THEN
(* Выходы блока не сформированы *)
Q := FALSE;
ERROR := NO_ERROR;
(* Сборос таймера повторного запроса *)
tonDelay(IN:=FALSE, PT:=T#0s);
tonReply(IN:=FALSE, PT:=T#0s);
ANSWER_LEN := 0;
END_IF
IF NOT IN THEN
RETURN;
END_IF
(* Если работа блока разрешена, то выполняем чтение и запись в порт с заданными параметрами *)
CASE STATE OF
(* Задержка перед повторным запросом *)
1:
IF tonDelay.IN=FALSE THEN
(* Сборос таймера повторного запроса *)
tonDelay( IN:=TRUE, PT:=DELAY );
ELSIF tonDelay.Q=TRUE THEN
(* Останавливаем таймер задержки *)
tonDelay(IN:=FALSE, PT:=T#0s);
(* Переводим блок в следующий статус*)
STATE := STATE + 1;
END_IF
(* Перед отправкой команды необходимо очистить буфер от предыдущего ответа *)
2:
(* сбросить указатель буфера *)
ptrBufRead := ADR(OUT.BUF);
(* очистить буфер *)
_BUFFER_CLEAR(ptrBufRead, SIZEOF(OUT.BUF));
(* и обнулить размер принятого ответа *)
OUT.LEN := 0;
dwBytesSend := 0;
(* для возможности получения сообщений произвольного размера *)
wAnswerLen := ANSWER_LEN;
(* Запускаем таймер ожидания ответа *)
tonReply(IN:=TRUE, PT:=TIMEOUT );
(* Переводим блок в следующий статус *)
STATE := STATE + 1;
(* Отправка команды *)
3:
(* получаем число отправленных байт *)
dwBytesSend := dwBytesSend + SysComWrite(PORT, ADR(CMD.BUF[dwBytesSend]), CMD.LEN - dwBytesSend, IO_TIMEOUT);
(* Если запись в порт совершена - размер команды совпадает с размером отправленных блоком отправки, то *)
IF dwBytesSend >= CMD.LEN THEN
(* Переводим блок в следующий статус *)
STATE := STATE + 1;
(* Если таймер запущен и закончил работать, а команда не отправлена, то формируем ошибку *)
ELSIF (tonReply.Q=TRUE AND tonReply.IN=TRUE) THEN
ERROR := WRITE_INCOMPLETE;
(* Выходы блока сформированы *)
Q := TRUE;
(* Переводим блок в следующий статус *)
STATE := STATE + 1;
END_IF
(* Чтение буфера приема *)
4:
(* В переменную dwBytesReceived получаем число принятых байт при чтении порта *)
dwBytesReceived := SysComRead(PORT, ptrBufRead, READ_ONCE, IO_TIMEOUT);
(* Если получили ответ от устройства, то принятую информацию собираем в буфер ответа *)
IF dwBytesReceived>0 THEN
(* Проверяем выход за пределы буфера *)
IF (OUT.LEN + DWORD_TO_UINT(dwBytesReceived) + READ_ONCE > BUFER_MAX_SIZE) THEN
ERROR := INVALID_RESPONSE_LEN;
Q := TRUE;
(* Переводим блок в следующий статус *)
STATE := END_STATE;
ELSE
ptrBufRead := ptrBufRead + dwBytesReceived;
OUT.LEN := OUT.LEN + DWORD_TO_UINT(dwBytesReceived);
END_IF
(* Если размер ответа не задан при вызове - берем размер из соответствующего байта фрейма *)
IF wAnswerLen = 0 THEN
IF (OUT.LEN > LEN_OFFSET) THEN
wAnswerLen := OUT.BUF[LEN_OFFSET];
END_IF;
ELSE
(* Получили нужное количество байт *)
IF OUT.LEN = wAnswerLen THEN
(* Вычисляем CRC *)
wInCrc := OUT.BUF[OUT.LEN+CRC_OFFSET+0] OR SHL(OUT.BUF[OUT.LEN+CRC_OFFSET+1], 8);
wCrc := MB_CRC_16(ADR(OUT.BUF), OUT.LEN - 2);
(* Вычисляем ID *)
wCmdID := CMD.BUF[CMD.LEN+ID_OFFSET+0] OR SHL(CMD.BUF[CMD.LEN+ID_OFFSET+1], 8);
wID := OUT.BUF[OUT.LEN+ID_OFFSET+0] OR SHL(OUT.BUF[OUT.LEN+ID_OFFSET+1], 8);
(* Вычисляем Функцию *)
bCmdFun := CMD.BUF[FUN_OFFSET];
bFun := OUT.BUF[FUN_OFFSET];
(* Проверяем CRC *)
IF NOT wCrc = wInCrc THEN
(* Если ответ пришел, но неправильный CRC *)
ERROR := INVALID_CRC;
(* Проверяем ID *)
ELSIF wCmdID <> 0 AND wID <> wCmdID THEN
(* Если ответ пришел, но ID отличается *)
ERROR := INVALID_TRANSACTION_ID ;
ELSIF bCmdFun <> 0 AND bFun = 0 THEN
(* Если ответ пришел, c ошибкой *)
ERROR := OUT.BUF[DATA_OFFSET] + 100;
IF ERROR > 108 THEN
ERROR := ERROR_UNKNOWN;
END_IF
ELSIF bCmdFun <> 0 AND bFun <> bCmdFun THEN
(* Если ответ пришел, но функция отличается *)
ERROR := INVALID_FUNCTION;
ELSE
(* Ошибок нет *)
ERROR := NO_ERROR;
END_IF
Q := TRUE;
(* Переводим блок в следующий статус *)
STATE := END_STATE;
ELSIF OUT.LEN > wAnswerLen THEN
(* Получено слишком много байт *)
ERROR := INVALID_RESPONSE_LEN;
Q := TRUE;
(* Переводим блок в следующий статус *)
STATE := END_STATE;
END_IF
END_IF
END_IF
(* Если таймер запущен и закончил работать, а нужного ответа нет, то формируем ошибки *)
IF (tonReply.Q=TRUE AND tonReply.IN=TRUE) THEN
(* Если ответ не пришел вообще, значит устройство не отвечает на запрос *)
IF OUT.LEN=0 THEN
(* Формируем ошибку нет связи *)
ERROR := TIME_OUT;
ELSE
(* Если ответ пришел, но не полный, формируем ошибку Timeout *)
ERROR := INVALID_RESPONSE_LEN;
END_IF
(* Выходы блока сформированы *)
Q := TRUE;
(* Переводим блок в следующий статус *)
STATE := STATE +1;
END_IF
(* Переводим блок в следующий статус *)
ELSE
(* Останавливаем работу таймера ожидания ответа *)
tonReply(IN:=FALSE, PT:=T#0s );
(* Выходы блока не сформированы *)
Q := FALSE;
STATE := 1;
END_CASE;
(* Вызываем таймер ожидания ответа и задержки повтора*)
tonReply();
tonDelay();
PULSAR_CMD
(* Создание запроса в формате фрейма Пульсар М*)
FUNCTION PULSAR_CMD: BUFER
VAR_INPUT
ADDR: DWORD := 16#F00F0FF0;
FUN: BYTE := 0;
ID: WORD := 16#0000;
DATA: DWORD := 16#00000000;
DATA_LEN: BYTE := 0;
END_VAR
VAR CONSTANT
FUN_OFFSET: BYTE := 4;
LEN_OFFSET: BYTE := 5;
DATA_OFFSET: BYTE := 6;
ID_OFFSET: BYTE := 6;
CRC_OFFSET: BYTE := 8;
END_VAR
VAR
wCrc: WORD;
bOffset: BYTE;
dwShift: DWORD;
END_VAR
(*Адрес устройства *)
PULSAR_CMD.BUF[0] := DWORD_TO_BYTE(SHR(ADDR, 24 ) AND 16#FF);
PULSAR_CMD.BUF[1] := DWORD_TO_BYTE(SHR(ADDR, 16 ) AND 16#FF);
PULSAR_CMD.BUF[2] := DWORD_TO_BYTE(SHR(ADDR, 8 ) AND 16#FF);
PULSAR_CMD.BUF[3] := DWORD_TO_BYTE(SHR(ADDR, 0 ) AND 16#FF);
(* Функция *)
PULSAR_CMD.BUF[FUN_OFFSET] := FUN;
(* Блок данных если есть *)
IF DATA_LEN > 0 THEN
dwShift := 0;
FOR bOffset := 0 TO DATA_LEN - 1 DO
PULSAR_CMD.BUF[DATA_OFFSET + bOffset] := DWORD_TO_BYTE(SHR(DATA, dwShift) AND 16#FF);
dwShift := dwShift + 8;
END_FOR
END_IF;
(* Размер: адрес + функция + длина + данные + ID + CRC *)
PULSAR_CMD.LEN := 4 + 1 + 1 + DATA_LEN + 2 + 2;
(* Исключение для нулевой функции *)
IF FUN = 0 THEN
PULSAR_CMD.BUF[LEN_OFFSET] := 0;
ELSE
PULSAR_CMD.BUF[LEN_OFFSET] := UINT_TO_BYTE(PULSAR_CMD.LEN);
END_IF
(* ID запроса *)
PULSAR_CMD.BUF[ID_OFFSET + DATA_LEN + 1] := DWORD_TO_BYTE(SHR(ID, 8) AND 16#FF);
PULSAR_CMD.BUF[ID_OFFSET + DATA_LEN + 0] := DWORD_TO_BYTE(SHR(ID, 0) AND 16#FF);
(* CRC *)
wCrc := MB_CRC_16(ADR(PULSAR_CMD.BUF), PULSAR_CMD.LEN-2);
PULSAR_CMD.BUF[CRC_OFFSET + DATA_LEN + 1] := DWORD_TO_BYTE(SHR(wCrc, 8) AND 16#FF);
PULSAR_CMD.BUF[CRC_OFFSET + DATA_LEN + 0] := DWORD_TO_BYTE(SHR(wCrc, 0) AND 16#FF);
PULSAR_CMD_PTR
(* Создание запроса в формате фрейма Пульсар М из массива*)
FUNCTION PULSAR_CMD_PTR: BUFER
VAR_INPUT
ADDR: DWORD := 16#F00F0FF0;
FUN: BYTE := 16#00;
ID: WORD := 16#0000;
DATA_PTR: POINTER TO BYTE;
DATA_LEN: DWORD := 0;
END_VAR
VAR CONSTANT
FUN_OFFSET: BYTE := 4;
LEN_OFFSET: BYTE := 5;
DATA_OFFSET: BYTE := 6;
ID_OFFSET: BYTE := 6;
CRC_OFFSET: BYTE := 8;
END_VAR
VAR
wCrc: WORD;
dwOffset: DWORD;
END_VAR
PULSAR_CMD_PTR.BUF[0] := DWORD_TO_BYTE(SHR(ADDR, 24 ) AND 16#FF);
PULSAR_CMD_PTR.BUF[1] := DWORD_TO_BYTE(SHR(ADDR, 16 ) AND 16#FF);
PULSAR_CMD_PTR.BUF[2] := DWORD_TO_BYTE(SHR(ADDR, 8 ) AND 16#FF);
PULSAR_CMD_PTR.BUF[3] := DWORD_TO_BYTE(SHR(ADDR, 0 ) AND 16#FF);
PULSAR_CMD_PTR.BUF[FUN_OFFSET] := FUN;
IF DATA_LEN > 0 THEN
FOR dwOffset := 0 TO DATA_LEN - 1 DO
PULSAR_CMD_PTR.BUF[DATA_OFFSET + dwOffset] := DATA_PTR^;
DATA_PTR := DATA_PTR + 1;
END_FOR
END_IF;
(* Размер: адрес + функция + длина + данные + ID + CRC *)
PULSAR_CMD_PTR.LEN := 4 + 1 + 1 + DWORD_TO_UINT(DATA_LEN) + 2 + 2;
PULSAR_CMD_PTR.BUF[LEN_OFFSET] := UINT_TO_BYTE(PULSAR_CMD_PTR.LEN);
PULSAR_CMD_PTR.BUF[ID_OFFSET + DATA_LEN + 1] := DWORD_TO_BYTE(SHR(ID, 8) AND 16#FF);
PULSAR_CMD_PTR.BUF[ID_OFFSET + DATA_LEN + 0] := DWORD_TO_BYTE(SHR(ID, 0) AND 16#FF);
wCrc := MB_CRC_16(ADR(PULSAR_CMD_PTR.BUF), PULSAR_CMD_PTR.LEN-2);
PULSAR_CMD_PTR.BUF[CRC_OFFSET + DATA_LEN + 1] := DWORD_TO_BYTE(SHR(wCrc, 8) AND 16#FF);
PULSAR_CMD_PTR.BUF[CRC_OFFSET + DATA_LEN + 0] := DWORD_TO_BYTE(SHR(wCrc, 0) AND 16#FF);
MB_CRC_16
(* Вычисление контрольной суммы кадра MODBUS RTU CRC *)
FUNCTION MB_CRC_16 : WORD
VAR_INPUT
DATA: POINTER TO BYTE; (* указатель на блок данных *)
SIZE: WORD; (* размер блока данных *)
END_VAR
VAR
bCnt: BYTE; (* счетчик битов *)
END_VAR
MB_CRC_16 := 16#FFFF;
WHILE SIZE > 0 DO
MB_CRC_16 := MB_CRC_16 XOR DATA^;
FOR bCnt := 0 TO 7 DO
IF MB_CRC_16.0 = 0 THEN
MB_CRC_16 := SHR(MB_CRC_16, 1);
ELSE
MB_CRC_16 := SHR(MB_CRC_16, 1) XOR 16#A001;
END_IF
END_FOR;
DATA := DATA + 1;
SIZE := SIZE - 1;
END_WHILE
Типы данных
BUFER
TYPE BUFER :
STRUCT
LEN: UINT := 0;
BUF: ARRAY [0..BUFER_MAX_SIZE] OF BYTE;
END_STRUCT
END_TYPE
PULSAR_ERRORS
(* Ошибки протокола Пульсар (при реализации с помощью Pulsar_STC) *)
TYPE PULSAR_ERRORS :
(
(* Общие коды ошибок *)
NO_ERROR := 0,
TIME_OUT := 5001,
HANDLE_INVALID := 5003,
ERROR_UNKNOWN := 5004,
WRONG_PARAMETER := 5005,
WRITE_INCOMPLETE := 5006,
(* Специфические ошибки протокола Пульсар-М *)
(* Отсутствует запрашиваемый код функции *)
INVALID_FUNCTION := 101,
(* Ошибка в битовой маске запроса *)
INVALID_MASK := 102,
(* Ошибочная длина запроса *)
INVALID_REQUEST_LEN := 103,
(* Отсутствует параметр *)
INVALID_PARAM := 104,
(* Запись заблокирована, требуется авторизация *)
AUTHORIZATION_REQUIRED := 105,
(* Записываемое значение (параметр) находится вне заданного диапазона *)
PARAM_OUT_OF_RANGE := 106,
(* Отсутствует запрашиваемый тип архива *)
NO_ARCHIVE_TYPE := 107,
(* Превышение максимального количества считываемых архивных значений за один пакет *)
TOO_MUCH_ARCHIVE_DATA := 108,
(* Некорректная длина ответа *)
(* (<10 байт или значение поля LEN не соответствует фактической длине ответа) *)
INVALID_RESPONSE_LEN := 120,
(* Некорректный адрес устройства в ответе *)
INVALID_ADDR := 121,
(* Некорректный идентификатор пакета в ответе *)
INVALID_TRANSACTION_ID := 122,
(* Некорректная CRC в ответе *)
INVALID_CRC := 123
) := NO_ERROR;
END_TYPE
Глобальные переменные
VAR_GLOBAL CONSTANT
BUFER_MAX_SIZE: BYTE := 255;
END_VAR
По мотивам 1 (https://ftp.owen.ru/CoDeSys3/04_Library/05_3.5.11.5/02_Libraries/02_vendor_protocols/protokol_pulsar_m.pdf), 2 (https://ftp.owen.ru/CoDeSys3/04_Library/05_3.5.11.5/02_Libraries/02_vendor_protocols/pulsar_m.pdf), 3 (https://owen.ru/forum/showthread.php?t=34449), 4 (https://owen.ru/uploads/373/syslibcom_ru.pdf) и ряда примеров с форума.
Умеет из коробки:
искать адрес устройства (один раз - при старте POU по IN),
считывать текущие показания первого счетчика
считывать температуру
считывать напряжение встроенной батареи
синхронизировать время на счетчике с системным временем ПЛК (один раз - при старте POU по IN)
проверять формат/crc/иные ошибки фрейма данных
проводить опрос с заданным периодом
Рекомендуется с синхронизацей времени по NTP (https://owen.ru/forum/showthread.php?t=29703&page=5&p=292282&viewfull=1#post292282).
Пример живого POU из работающего проекта (Отладочное логгирование и FB NTP в код ниже не входят):
81388
Пример прокидывания в Modbus, обратите внимание на множители в комментариях к регистрам.
81386
Используемые библиотеки: Oscat Basic, SysComLib, ComService, SysLibTime, SysLibMem.
Немного про адрес устройства
В протоколе Пульсар адрес устройства задается в формате BCD (https://ru.wikipedia.org/wiki/BCD). Поэтому удобно его прописывать в виде 16и-ричной переменной.
Например, для устройства с серийным номером 12345 адрес выглядит так: 16#00012345
Можно замкнуть выход POU найденного адреса R_ADDR на вход адреса ADDR - т.к. при первом запросе поиска устройств адрес, очевидно, не нужен.
Доступно в виде готовой библиотеки. Лицензия пусть будет LGPL3 - можно свободно использовать, изменять - но выкладывать изменения необходимо.
81482 (версия 0.2)
Ниже - ST POU и модули версии 0.1
PULSAR_M
(*
Периодический запрос показателей, параметров, запрос/установка даты счетчика Пульсар М по RS485
Описание протокола https://ftp.owen.ru/CoDeSys3/04_Library/05_3.5.11.5/02_Libraries/02_vendor_protocols/protokol_pulsar_m.pdf
*)
PROGRAM PULSAR_M
VAR_INPUT
IN: BOOL;
(* Адрес устройства *)
ADDR: DWORD := 16#00000000;
(* Для ПЛК110/160: COM0 – RS485-1; COM1 – RS232; COM2 – RS485-2; COM4 – RS232-Debug; *)
ComPort: PORTS := COM2;
(* Задержка между запросами *)
DELAY: TIME :=T#30s;
(* Обновлять время счетчика *)
UPDATE_DATE: BOOL := FALSE;
END_VAR
VAR_OUTPUT
R_ADDR: DWORD;
R_VALUE: DWORD;
R_VOLTAGE: REAL;
R_TEMP: REAL;
R_DT: DT;
ERROR: PULSAR_ERRORS;
END_VAR
VAR CONSTANT
(* Таймаут ожидания запросов *)
READ_TIMEOUT: TIME :=T#3000ms;
(* Задержка между повторными запросами *)
REPEAT_DELAY: TIME :=T#1000ms;
(* Задержка между запросами в одногм цикле *)
STEP_DELAY: TIME :=T#500ms;
(* Поиск адреса устройства. Аргумент - 1 байт нулей *)
FUN_BROADCAST: BYTE := 16#00;
(* Широковещательный адрес устройства *)
BROADCAST_ADDR: DWORD := 16#F00F0FF0;
(* Получить время *)
FUN_TIME: BYTE := 16#04;
(* Получить показания устройства. Аргумент - 4 байта маски канала *)
FUN_VALUE: BYTE := 16#01;
(* Маска первого канала *)
VALUE_MASK: DWORD := 16#01;
(* Получить параметры прибора. Аргумент - 2 байта код параметра *)
FUN_PARAM: BYTE := 16#0A;
(* Получить напряжение батареи *)
VALUE_VOLTAGE: DWORD := 16#0A;
(* Получить температуру *)
VALUE_TEMP: DWORD := 16#0B;
(* Нулевой параметр *)
VALUE_ZERO: DWORD := 16#00;
(* Установить время. Аргумент - 6 байт времени *)
FUN_SET_TIME: BYTE := 16#05;
(* Записать показания устройства. Аргументы - 4 байта маски канала, 4/8 байт значения *)
FUN_SET_VALUE: BYTE := 16#03;
(* Позиция данных в массиве буфера *)
DATA_OFFSET: BYTE := 6;
(* Позиции адреса при broadcast-запросе *)
BC_ADDR_OFFSET: BYTE := 4;
END_VAR
VAR
STC: PULSAR_STC;
ComService : COM_SERVICE;
Settings : COMSETTINGS;
CTX: CurTimeEx;
TimeAndDate: SystemTimeDate;
Sys_Time: SysTime64;
cmd: BUFER;
res: BUFER;
(* Для конвертации температуры *)
prTmp: POINTER TO REAL;
dwTmp: DWORD;
(*Таймер периодичности опроса*)
tonDelay: TON;
(*Таймер задержки между шагами*)
tonStep: TON;
(* ID запроса - начальное значение должно быть отлично от нуля*)
wReqId: WORD := 1;
(* Дата для установки *)
abCurrentDate: ARRAY [0..5] OF BYTE;
i: WORD;
STATE : BYTE := 0;
bTimeIsSet: BOOL := FALSE;
END_VAR
(* Настройка порта 9600, 8бит, нет четности, один стоп бит *)
IF STATE = 0 AND IN THEN
Settings.Port := ComPort;
Settings.dwBaudRate := 9600;
Settings.byParity := 0;
Settings.dwTimeout := 0;
Settings.byStopBits := 0;
Settings.dwBufferSize := 0;
Settings.dwScan := 0;
ComService(ENABLE := TRUE, SETTINGS := Settings, TASK := OPEN_TSK);
tonStep(IN:=TRUE, PT:=STEP_DELAY);
bTimeIsSet := FALSE;
END_IF
IF NOT IN THEN
ComService(ENABLE := FALSE, SETTINGS := Settings, TASK := CLOSE_TSK);
STATE := 0;
tonStep(IN:=FALSE, PT:=T#0s);
tonDelay(IN:=FALSE, PT:=T#0s);
END_IF
IF NOT ComService.Ready THEN
RETURN;
END_IF
CASE STATE OF
0:
IF ComService.Ready THEN
STATE := 1;
END_IF
(* Поиск устройств - чтение адреса *)
1:
cmd := PULSAR_CMD(ADDR := BROADCAST_ADDR, FUN := FUN_BROADCAST, ID:= 0, DATA := VALUE_ZERO, DATA_LEN := 1);
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=10);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
R_ADDR := SHL(STC.OUT.BUF[BC_ADDR_OFFSET + 0], 24) OR SHL(STC.OUT.BUF[BC_ADDR_OFFSET + 1], 16) OR SHL(STC.OUT.BUF[BC_ADDR_OFFSET + 2], 8) + SHL(STC.OUT.BUF[BC_ADDR_OFFSET + 3], 0);
mNextStep;
END_IF
END_IF
2:
mNextStepDelay;
(* Чтение показателей *)
3:
cmd := PULSAR_CMD(ADDR := ADDR, FUN := FUN_VALUE, ID:= wReqId, DATA := VALUE_MASK, DATA_LEN := 4);
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=0);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
R_VALUE := SHL(STC.OUT.BUF[DATA_OFFSET + 0], 0) OR SHL(STC.OUT.BUF[DATA_OFFSET + 1], 8) OR SHL(STC.OUT.BUF[DATA_OFFSET + 2], 16) + SHL(STC.OUT.BUF[DATA_OFFSET + 3], 24);
mNextStep;
END_IF
END_IF
4:
mNextStepDelay;
(* Чтение напряжения батареи *)
5:
cmd := PULSAR_CMD(ADDR := ADDR, FUN := FUN_PARAM, ID:= wReqId, DATA := VALUE_VOLTAGE, DATA_LEN := 2);
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=0);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
dwTmp := SHL(STC.OUT.BUF[DATA_OFFSET + 0], 0) OR SHL(STC.OUT.BUF[DATA_OFFSET + 1], 8) OR SHL(STC.OUT.BUF[DATA_OFFSET + 2], 16) OR SHL(STC.OUT.BUF[DATA_OFFSET + 3], 24);
prTmp := ADR(dwTmp);
R_VOLTAGE := prTmp^;
mNextStep;
END_IF
END_IF
6:
mNextStepDelay;
(* Чтение температуры *)
7:
cmd := PULSAR_CMD(ADDR := ADDR, FUN := FUN_PARAM, ID:= wReqId, DATA := VALUE_TEMP, DATA_LEN := 2);
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=0);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
dwTmp := SHL(STC.OUT.BUF[DATA_OFFSET + 0], 0) OR SHL(STC.OUT.BUF[DATA_OFFSET + 1], 8) OR SHL(STC.OUT.BUF[DATA_OFFSET + 2], 16) OR SHL(STC.OUT.BUF[DATA_OFFSET + 3], 24);
prTmp := ADR(dwTmp);
R_TEMP := prTmp^;
mNextStep;
END_IF
END_IF
8:
mNextStepDelay;
(* Обновление даты счетчика *)
9:
(* Пропускаем, если уже обновили ИЛИ нет признака обновления даты*)
IF bTimeIsSet OR NOT UPDATE_DATE THEN
STATE := STATE + 2;
ELSE
(* Чтение системного времени через юзабилищный SysLibTime *)
TimeAndDate.Day :=0; TimeAndDate.DayOfWeek :=0; TimeAndDate.dwHighMsec :=0;
TimeAndDate.dwLowMSecs :=0; TimeAndDate.Milliseconds :=0; TimeAndDate.MINUTE :=0;
TimeAndDate.SECOND :=0; TimeAndDate.HOUR :=0; TimeAndDate.Year :=0; TimeAndDate.Month :=0;
Sys_time.ulHigh :=0; Sys_time.ulLow :=0;
CTX (SystemTime:=Sys_Time , TimeDate:= TimeAndDate);
abCurrentDate[0] := UINT_TO_BYTE(TimeAndDate.YEAR - 2000);
abCurrentDate[1] := UINT_TO_BYTE(TimeAndDate.Month);
abCurrentDate[2] := UINT_TO_BYTE(TimeAndDate.Day);
abCurrentDate[3] := UINT_TO_BYTE(TimeAndDate.HOUR);
abCurrentDate[4] := UINT_TO_BYTE(TimeAndDate.MINUTE);
abCurrentDate[5] := UINT_TO_BYTE(TimeAndDate.SECOND);
cmd := PULSAR_CMD_PTR(ADDR := ADDR, FUN := FUN_SET_TIME, ID:= wReqId, DATA_PTR := ADR(abCurrentDate), DATA_LEN := SIZEOF(abCurrentDate));
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=0);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
IF STC.OUT.BUF[DATA_OFFSET] = 16#01 THEN
bTimeIsSet := TRUE;
END_IF;
mNextStep;
END_IF
END_IF
END_IF
10:
mNextStepDelay;
(* Чтение даты *)
11:
cmd := PULSAR_CMD(ADDR := ADDR, FUN := FUN_TIME, ID:= wReqId, DATA := 0, DATA_LEN := 0);
STC(in:=IN, DELAY:=REPEAT_DELAY, TIMEOUT:=READ_TIMEOUT, PORT:=ComPort, CMD:=cmd, ANSWER_LEN:=0);
IF STC.Q THEN
ERROR := STC.ERROR;
IF ERROR = 0 THEN
R_DT := SET_DT(
year := 2000 + STC.OUT.BUF[DATA_OFFSET + 0],
month := STC.OUT.BUF[DATA_OFFSET + 1],
day := STC.OUT.BUF[DATA_OFFSET + 2],
HOUR := STC.OUT.BUF[DATA_OFFSET + 3],
MINUTE := STC.OUT.BUF[DATA_OFFSET + 4],
SECOND := STC.OUT.BUF[DATA_OFFSET + 5]
);
mNextStep;
END_IF
END_IF
12:
mNextStepDelay;
(* Пауза между пакетами *)
13:
(* Задержка между запросами *)
IF NOT tonDelay.IN THEN
tonDelay(IN:=TRUE, PT:=DELAY);
ELSIF tonDelay.Q THEN
tonDelay(IN:=FALSE, PT:=T#0s);
STATE := STATE +1;
END_IF
(* Возврат в начало цикла *)
ELSE
STATE := 3;
END_CASE
tonDelay();
tonStep();
Модуль mNextStep
(* Формируем ID запроса инкрементом *)
wReqId := wReqId + 1;
IF wReqId = 16#FFFF THEN
wReqId := 1;
END_IF
STC(IN:=FALSE);
STATE := STATE + 1;
Модуль mNextStepDelay
(* Задержка между запросами *)
IF NOT tonStep.IN THEN
(* Таймер задержки и следующий шаг *)
tonStep(IN:=TRUE, PT:=STEP_DELAY);
ELSIF tonStep.Q THEN
tonStep(IN:=FALSE, PT:=T#0s);
STATE:=STATE+1;
END_IF
PULSAR_STC
(* Реализация взаимодействия по COM порту со спецификой Пульсар М *)
FUNCTION_BLOCK PULSAR_STC
(* Входы функционального блока *)
VAR_INPUT
(* Вход разрешающий работу блока *)
IN: BOOL:=FALSE;
(* Указатель на буфер с массивом байт в котором содержится пересылаемая команда *)
CMD: BUFER;
(* Количество байт в ответе - ноль для вычисления из пакета сообщения Пульсар *)
ANSWER_LEN: WORD := 0;
(* Задержка повторного запроса *)
DELAY: TIME := T#500ms;
(* Номер порта в который отсылается команда и из которого принимаются данные *)
PORT: PORTS := 2;
(* Таймаут ответа *)
TIMEOUT: TIME := T#3000ms;
END_VAR
(* Выходы функционального блока *)
VAR_OUTPUT
(* Признак окончания чтения *)
Q: BOOL := FALSE;
(* Результат принятый от устройства *)
OUT: BUFER;
ERROR: PULSAR_ERRORS := NO_ERROR;
END_VAR
VAR
(*Таймер ожидания ответа*)
tonReply: TON;
(*Таймер периодичности опроса*)
tonDelay: TON;
(*Тригер позволяющий определить фронт сигнала IN и выполнить инициализацию блока*)
rtStart: R_TRIG;
rfEnd: F_TRIG;
(* Буфер чтения и указатель на текущую позицию в буфере *)
bufRead: BUFER;
ptrBufRead: POINTER TO BYTE;
(*Число принятых байт*)
dwBytesReceived: DWORD;
(*Число отправленных байт*)
dwBytesSend: DWORD;
(* Для расчета CRC, ID *)
wInCrc: WORD;
wCrc: WORD;
(*Идентификатор запроса для сверки*)
wID: WORD;
wCmdID: WORD;
bFun: BYTE;
bCmdFun: BYTE;
(* Ожидаемый размер ответа *)
wAnswerLen : WORD;
(* Статус работы блока -
1: задержка повтора,
2: очистка буфера,
3: отправка данных,
4: прием данных
*)
STATE: BYTE:=2;
END_VAR
VAR CONSTANT
(* Сколько читаем байт за цикл. Рекомендуется как минимум захватывать байт с размером *)
READ_ONCE: UINT := 10;
(* Смещение функции в буфере *)
FUN_OFFSET: BYTE := 4;
(* Смещение адреса в буфере *)
LEN_OFFSET: UINT := 5;
(* Смещение данных в буфере *)
DATA_OFFSET: UINT := 6;
(* Таймаут ввода\вывода *)
IO_TIMEOUT: DWORD := 500;
(* Смещение CRC c конца буфера *)
CRC_OFFSET: INT := -2;
(* Смещение ID c конца буфера *)
ID_OFFSET: INT := -4;
(* Конечное состояние *)
END_STATE: BYTE := 5;
END_VAR
(* Анализируем запуск функционального блока *)
rtStart(CLK:=IN);
rfEnd(CLK:=IN);
(* Если пришел разрешающий сигнал запуска блока, то необходимо выполнить его инициализацию *)
IF rtStart.Q THEN
(* Выходы блока не сформированы *)
Q := FALSE;
ERROR := 0;
(* Переводим блок в режим отправки команды *)
STATE := 2;
END_IF
IF rfEnd.Q THEN
(* Выходы блока не сформированы *)
Q := FALSE;
ERROR := NO_ERROR;
(* Сборос таймера повторного запроса *)
tonDelay(IN:=FALSE, PT:=T#0s);
tonReply(IN:=FALSE, PT:=T#0s);
ANSWER_LEN := 0;
END_IF
IF NOT IN THEN
RETURN;
END_IF
(* Если работа блока разрешена, то выполняем чтение и запись в порт с заданными параметрами *)
CASE STATE OF
(* Задержка перед повторным запросом *)
1:
IF tonDelay.IN=FALSE THEN
(* Сборос таймера повторного запроса *)
tonDelay( IN:=TRUE, PT:=DELAY );
ELSIF tonDelay.Q=TRUE THEN
(* Останавливаем таймер задержки *)
tonDelay(IN:=FALSE, PT:=T#0s);
(* Переводим блок в следующий статус*)
STATE := STATE + 1;
END_IF
(* Перед отправкой команды необходимо очистить буфер от предыдущего ответа *)
2:
(* сбросить указатель буфера *)
ptrBufRead := ADR(OUT.BUF);
(* очистить буфер *)
_BUFFER_CLEAR(ptrBufRead, SIZEOF(OUT.BUF));
(* и обнулить размер принятого ответа *)
OUT.LEN := 0;
dwBytesSend := 0;
(* для возможности получения сообщений произвольного размера *)
wAnswerLen := ANSWER_LEN;
(* Запускаем таймер ожидания ответа *)
tonReply(IN:=TRUE, PT:=TIMEOUT );
(* Переводим блок в следующий статус *)
STATE := STATE + 1;
(* Отправка команды *)
3:
(* получаем число отправленных байт *)
dwBytesSend := dwBytesSend + SysComWrite(PORT, ADR(CMD.BUF[dwBytesSend]), CMD.LEN - dwBytesSend, IO_TIMEOUT);
(* Если запись в порт совершена - размер команды совпадает с размером отправленных блоком отправки, то *)
IF dwBytesSend >= CMD.LEN THEN
(* Переводим блок в следующий статус *)
STATE := STATE + 1;
(* Если таймер запущен и закончил работать, а команда не отправлена, то формируем ошибку *)
ELSIF (tonReply.Q=TRUE AND tonReply.IN=TRUE) THEN
ERROR := WRITE_INCOMPLETE;
(* Выходы блока сформированы *)
Q := TRUE;
(* Переводим блок в следующий статус *)
STATE := STATE + 1;
END_IF
(* Чтение буфера приема *)
4:
(* В переменную dwBytesReceived получаем число принятых байт при чтении порта *)
dwBytesReceived := SysComRead(PORT, ptrBufRead, READ_ONCE, IO_TIMEOUT);
(* Если получили ответ от устройства, то принятую информацию собираем в буфер ответа *)
IF dwBytesReceived>0 THEN
(* Проверяем выход за пределы буфера *)
IF (OUT.LEN + DWORD_TO_UINT(dwBytesReceived) + READ_ONCE > BUFER_MAX_SIZE) THEN
ERROR := INVALID_RESPONSE_LEN;
Q := TRUE;
(* Переводим блок в следующий статус *)
STATE := END_STATE;
ELSE
ptrBufRead := ptrBufRead + dwBytesReceived;
OUT.LEN := OUT.LEN + DWORD_TO_UINT(dwBytesReceived);
END_IF
(* Если размер ответа не задан при вызове - берем размер из соответствующего байта фрейма *)
IF wAnswerLen = 0 THEN
IF (OUT.LEN > LEN_OFFSET) THEN
wAnswerLen := OUT.BUF[LEN_OFFSET];
END_IF;
ELSE
(* Получили нужное количество байт *)
IF OUT.LEN = wAnswerLen THEN
(* Вычисляем CRC *)
wInCrc := OUT.BUF[OUT.LEN+CRC_OFFSET+0] OR SHL(OUT.BUF[OUT.LEN+CRC_OFFSET+1], 8);
wCrc := MB_CRC_16(ADR(OUT.BUF), OUT.LEN - 2);
(* Вычисляем ID *)
wCmdID := CMD.BUF[CMD.LEN+ID_OFFSET+0] OR SHL(CMD.BUF[CMD.LEN+ID_OFFSET+1], 8);
wID := OUT.BUF[OUT.LEN+ID_OFFSET+0] OR SHL(OUT.BUF[OUT.LEN+ID_OFFSET+1], 8);
(* Вычисляем Функцию *)
bCmdFun := CMD.BUF[FUN_OFFSET];
bFun := OUT.BUF[FUN_OFFSET];
(* Проверяем CRC *)
IF NOT wCrc = wInCrc THEN
(* Если ответ пришел, но неправильный CRC *)
ERROR := INVALID_CRC;
(* Проверяем ID *)
ELSIF wCmdID <> 0 AND wID <> wCmdID THEN
(* Если ответ пришел, но ID отличается *)
ERROR := INVALID_TRANSACTION_ID ;
ELSIF bCmdFun <> 0 AND bFun = 0 THEN
(* Если ответ пришел, c ошибкой *)
ERROR := OUT.BUF[DATA_OFFSET] + 100;
IF ERROR > 108 THEN
ERROR := ERROR_UNKNOWN;
END_IF
ELSIF bCmdFun <> 0 AND bFun <> bCmdFun THEN
(* Если ответ пришел, но функция отличается *)
ERROR := INVALID_FUNCTION;
ELSE
(* Ошибок нет *)
ERROR := NO_ERROR;
END_IF
Q := TRUE;
(* Переводим блок в следующий статус *)
STATE := END_STATE;
ELSIF OUT.LEN > wAnswerLen THEN
(* Получено слишком много байт *)
ERROR := INVALID_RESPONSE_LEN;
Q := TRUE;
(* Переводим блок в следующий статус *)
STATE := END_STATE;
END_IF
END_IF
END_IF
(* Если таймер запущен и закончил работать, а нужного ответа нет, то формируем ошибки *)
IF (tonReply.Q=TRUE AND tonReply.IN=TRUE) THEN
(* Если ответ не пришел вообще, значит устройство не отвечает на запрос *)
IF OUT.LEN=0 THEN
(* Формируем ошибку нет связи *)
ERROR := TIME_OUT;
ELSE
(* Если ответ пришел, но не полный, формируем ошибку Timeout *)
ERROR := INVALID_RESPONSE_LEN;
END_IF
(* Выходы блока сформированы *)
Q := TRUE;
(* Переводим блок в следующий статус *)
STATE := STATE +1;
END_IF
(* Переводим блок в следующий статус *)
ELSE
(* Останавливаем работу таймера ожидания ответа *)
tonReply(IN:=FALSE, PT:=T#0s );
(* Выходы блока не сформированы *)
Q := FALSE;
STATE := 1;
END_CASE;
(* Вызываем таймер ожидания ответа и задержки повтора*)
tonReply();
tonDelay();
PULSAR_CMD
(* Создание запроса в формате фрейма Пульсар М*)
FUNCTION PULSAR_CMD: BUFER
VAR_INPUT
ADDR: DWORD := 16#F00F0FF0;
FUN: BYTE := 0;
ID: WORD := 16#0000;
DATA: DWORD := 16#00000000;
DATA_LEN: BYTE := 0;
END_VAR
VAR CONSTANT
FUN_OFFSET: BYTE := 4;
LEN_OFFSET: BYTE := 5;
DATA_OFFSET: BYTE := 6;
ID_OFFSET: BYTE := 6;
CRC_OFFSET: BYTE := 8;
END_VAR
VAR
wCrc: WORD;
bOffset: BYTE;
dwShift: DWORD;
END_VAR
(*Адрес устройства *)
PULSAR_CMD.BUF[0] := DWORD_TO_BYTE(SHR(ADDR, 24 ) AND 16#FF);
PULSAR_CMD.BUF[1] := DWORD_TO_BYTE(SHR(ADDR, 16 ) AND 16#FF);
PULSAR_CMD.BUF[2] := DWORD_TO_BYTE(SHR(ADDR, 8 ) AND 16#FF);
PULSAR_CMD.BUF[3] := DWORD_TO_BYTE(SHR(ADDR, 0 ) AND 16#FF);
(* Функция *)
PULSAR_CMD.BUF[FUN_OFFSET] := FUN;
(* Блок данных если есть *)
IF DATA_LEN > 0 THEN
dwShift := 0;
FOR bOffset := 0 TO DATA_LEN - 1 DO
PULSAR_CMD.BUF[DATA_OFFSET + bOffset] := DWORD_TO_BYTE(SHR(DATA, dwShift) AND 16#FF);
dwShift := dwShift + 8;
END_FOR
END_IF;
(* Размер: адрес + функция + длина + данные + ID + CRC *)
PULSAR_CMD.LEN := 4 + 1 + 1 + DATA_LEN + 2 + 2;
(* Исключение для нулевой функции *)
IF FUN = 0 THEN
PULSAR_CMD.BUF[LEN_OFFSET] := 0;
ELSE
PULSAR_CMD.BUF[LEN_OFFSET] := UINT_TO_BYTE(PULSAR_CMD.LEN);
END_IF
(* ID запроса *)
PULSAR_CMD.BUF[ID_OFFSET + DATA_LEN + 1] := DWORD_TO_BYTE(SHR(ID, 8) AND 16#FF);
PULSAR_CMD.BUF[ID_OFFSET + DATA_LEN + 0] := DWORD_TO_BYTE(SHR(ID, 0) AND 16#FF);
(* CRC *)
wCrc := MB_CRC_16(ADR(PULSAR_CMD.BUF), PULSAR_CMD.LEN-2);
PULSAR_CMD.BUF[CRC_OFFSET + DATA_LEN + 1] := DWORD_TO_BYTE(SHR(wCrc, 8) AND 16#FF);
PULSAR_CMD.BUF[CRC_OFFSET + DATA_LEN + 0] := DWORD_TO_BYTE(SHR(wCrc, 0) AND 16#FF);
PULSAR_CMD_PTR
(* Создание запроса в формате фрейма Пульсар М из массива*)
FUNCTION PULSAR_CMD_PTR: BUFER
VAR_INPUT
ADDR: DWORD := 16#F00F0FF0;
FUN: BYTE := 16#00;
ID: WORD := 16#0000;
DATA_PTR: POINTER TO BYTE;
DATA_LEN: DWORD := 0;
END_VAR
VAR CONSTANT
FUN_OFFSET: BYTE := 4;
LEN_OFFSET: BYTE := 5;
DATA_OFFSET: BYTE := 6;
ID_OFFSET: BYTE := 6;
CRC_OFFSET: BYTE := 8;
END_VAR
VAR
wCrc: WORD;
dwOffset: DWORD;
END_VAR
PULSAR_CMD_PTR.BUF[0] := DWORD_TO_BYTE(SHR(ADDR, 24 ) AND 16#FF);
PULSAR_CMD_PTR.BUF[1] := DWORD_TO_BYTE(SHR(ADDR, 16 ) AND 16#FF);
PULSAR_CMD_PTR.BUF[2] := DWORD_TO_BYTE(SHR(ADDR, 8 ) AND 16#FF);
PULSAR_CMD_PTR.BUF[3] := DWORD_TO_BYTE(SHR(ADDR, 0 ) AND 16#FF);
PULSAR_CMD_PTR.BUF[FUN_OFFSET] := FUN;
IF DATA_LEN > 0 THEN
FOR dwOffset := 0 TO DATA_LEN - 1 DO
PULSAR_CMD_PTR.BUF[DATA_OFFSET + dwOffset] := DATA_PTR^;
DATA_PTR := DATA_PTR + 1;
END_FOR
END_IF;
(* Размер: адрес + функция + длина + данные + ID + CRC *)
PULSAR_CMD_PTR.LEN := 4 + 1 + 1 + DWORD_TO_UINT(DATA_LEN) + 2 + 2;
PULSAR_CMD_PTR.BUF[LEN_OFFSET] := UINT_TO_BYTE(PULSAR_CMD_PTR.LEN);
PULSAR_CMD_PTR.BUF[ID_OFFSET + DATA_LEN + 1] := DWORD_TO_BYTE(SHR(ID, 8) AND 16#FF);
PULSAR_CMD_PTR.BUF[ID_OFFSET + DATA_LEN + 0] := DWORD_TO_BYTE(SHR(ID, 0) AND 16#FF);
wCrc := MB_CRC_16(ADR(PULSAR_CMD_PTR.BUF), PULSAR_CMD_PTR.LEN-2);
PULSAR_CMD_PTR.BUF[CRC_OFFSET + DATA_LEN + 1] := DWORD_TO_BYTE(SHR(wCrc, 8) AND 16#FF);
PULSAR_CMD_PTR.BUF[CRC_OFFSET + DATA_LEN + 0] := DWORD_TO_BYTE(SHR(wCrc, 0) AND 16#FF);
MB_CRC_16
(* Вычисление контрольной суммы кадра MODBUS RTU CRC *)
FUNCTION MB_CRC_16 : WORD
VAR_INPUT
DATA: POINTER TO BYTE; (* указатель на блок данных *)
SIZE: WORD; (* размер блока данных *)
END_VAR
VAR
bCnt: BYTE; (* счетчик битов *)
END_VAR
MB_CRC_16 := 16#FFFF;
WHILE SIZE > 0 DO
MB_CRC_16 := MB_CRC_16 XOR DATA^;
FOR bCnt := 0 TO 7 DO
IF MB_CRC_16.0 = 0 THEN
MB_CRC_16 := SHR(MB_CRC_16, 1);
ELSE
MB_CRC_16 := SHR(MB_CRC_16, 1) XOR 16#A001;
END_IF
END_FOR;
DATA := DATA + 1;
SIZE := SIZE - 1;
END_WHILE
Типы данных
BUFER
TYPE BUFER :
STRUCT
LEN: UINT := 0;
BUF: ARRAY [0..BUFER_MAX_SIZE] OF BYTE;
END_STRUCT
END_TYPE
PULSAR_ERRORS
(* Ошибки протокола Пульсар (при реализации с помощью Pulsar_STC) *)
TYPE PULSAR_ERRORS :
(
(* Общие коды ошибок *)
NO_ERROR := 0,
TIME_OUT := 5001,
HANDLE_INVALID := 5003,
ERROR_UNKNOWN := 5004,
WRONG_PARAMETER := 5005,
WRITE_INCOMPLETE := 5006,
(* Специфические ошибки протокола Пульсар-М *)
(* Отсутствует запрашиваемый код функции *)
INVALID_FUNCTION := 101,
(* Ошибка в битовой маске запроса *)
INVALID_MASK := 102,
(* Ошибочная длина запроса *)
INVALID_REQUEST_LEN := 103,
(* Отсутствует параметр *)
INVALID_PARAM := 104,
(* Запись заблокирована, требуется авторизация *)
AUTHORIZATION_REQUIRED := 105,
(* Записываемое значение (параметр) находится вне заданного диапазона *)
PARAM_OUT_OF_RANGE := 106,
(* Отсутствует запрашиваемый тип архива *)
NO_ARCHIVE_TYPE := 107,
(* Превышение максимального количества считываемых архивных значений за один пакет *)
TOO_MUCH_ARCHIVE_DATA := 108,
(* Некорректная длина ответа *)
(* (<10 байт или значение поля LEN не соответствует фактической длине ответа) *)
INVALID_RESPONSE_LEN := 120,
(* Некорректный адрес устройства в ответе *)
INVALID_ADDR := 121,
(* Некорректный идентификатор пакета в ответе *)
INVALID_TRANSACTION_ID := 122,
(* Некорректная CRC в ответе *)
INVALID_CRC := 123
) := NO_ERROR;
END_TYPE
Глобальные переменные
VAR_GLOBAL CONSTANT
BUFER_MAX_SIZE: BYTE := 255;
END_VAR