PDA

Просмотр полной версии : Реализация обмена со счетчиком воды Пульсар М по RS485



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

IVM
18.01.2025, 12:48
Для интеграции по 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:
81388

Прокидывание в Modbus
81386

Используемые библиотеки: Oscat Basic, SysComLib, ComService, SysLibTime, SysLibMem.

Немного про адрес устройства

В протоколе Пульсар адрес устройства задается в формате BCD (https://ru.wikipedia.org/wiki/BCD). Поэтому удобно его прописывать в виде 16и-ричной переменной.

Например, для устройства с серийным номером 12345 адрес выглядит так: 16#00012345

Можно замкнуть выход POU найденного адреса R_ADDR на вход адреса ADDR - т.к. при первом запросе поиска устройств адрес, очевидно, не нужен.


ST POU и модули

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



Лучше бы проект выложить.

kondor3000
18.01.2025, 13:02
Куча непоняток,
1) откуда в программе PULSAR_M взялись входы, выходы VAR_IN_OUT ? Что такое PULSAR_LOG, LOG_BUFFER и LOG_ERROR ?
2) откуда в ФБ NPT взялись входы In, Delay и выход Q ?

Действительно, лучше было проект выложить.

melky
18.01.2025, 14:05
kirill.k2 что у вас за счетчик, на котором вы смогли синхронизировать время?

kirill.k2
19.01.2025, 02:27
Куча непоняток,
1) откуда в программе PULSAR_M взялись входы, выходы VAR_IN_OUT ? Что такое PULSAR_LOG, LOG_BUFFER и LOG_ERROR ?


"Туда не смотри, сюда смотри" (c) - на картинке расширенная версия, с отладкой. В теме код финальной, отладка отключена. Добавил в исходный пост, спасибо за замечание.


Куча непоняток,
2) откуда в ФБ NPT взялись входы In, Delay и выход Q ?


Опять же, см.выше. Скриншот с реально рабочего проекта, для демонстрации возможностей. Конкретно то, о чем спрашиваете - это моя реализация NTP. И для работы синхронизации Пульсара она не нужна. Отвечая прямо - NTP.Delay - задержки перед повторной попыткой опроса NTP сервера, NTP.Q означает успешную синхронизацию, NTP.In - сигнал на старт. Ничего из этого не имеет прямого отношения к Пульсару.

kirill.k2
19.01.2025, 02:38
kirill.k2 что у вас за счетчик, на котором вы смогли синхронизировать время?

Модель называлась как-то так (не реклама):


Счетчик холодной воды Ду20 Пульсар с цифровым выходом (RS485)
81407


Обычный бытовой, для холодной воды, с модулем RS485. Причем старый, "аналоговый". Программа должна и с новыми "цифровыми" работать - разве что формат данных непосредственно показаний надо проверить и, возможно, поменять с 4 на 8 байт.

kirill.k2
19.01.2025, 02:39
Лучше бы проект выложить.

Крайне неудобный формат обмена. А вот оверквотинг - точно зло, снесите, пожалуйста.

kirill.k2
19.01.2025, 03:09
Добавил код либой. В виде либы не тестил, пока нет времени.

melky
19.01.2025, 09:21
Странно, какого года выпуск? Мой требует пароля при смене даты, который каким-то образом вычисляется из серийного номера.
Давно тех поддержка отмолчалась по этому поводу.

kondor3000
19.01.2025, 09:59
Опять же, см.выше. Скриншот с реально рабочего проекта, для демонстрации возможностей. Конкретно то, о чем спрашиваете - это моя реализация NTP. И для работы синхронизации Пульсара она не нужна. Отвечая прямо - NTP.Delay - задержки перед повторной попыткой опроса NTP сервера, NTP.Q означает успешную синхронизацию, NTP.In - сигнал на старт. Ничего из этого не имеет прямого отношения к Пульсару.

Переменная NPT_SINC_READY на входе UPDATE_DATE то осталась, даже если выход Q не нужен, что тогда подавать на вход UPDATE_DATE,
просто TRUE ?
Счётчика у меня всё равно нет, интересна просто реализация.
А NPT синхронизация у меня не работает, думаю IP нужен другой, из серии 192.168.0.хх

Валенок
19.01.2025, 14:57
Одним глазком.
Pulser_stc.cmd ни разу не указатель. Как следствие за каким то каждый цикл бестолково перекладывается 250+ байт. Дальше не смотрел.

kirill.k2
19.01.2025, 19:44
Странно, какого года выпуск?

Относительно старый, покупался лет восемь назад. Пароля по-умолчанию не было.

Имхо, если не читать журнал показаний, дата на устройстве не особо важна. Де-факто у меня в проекте она используется только для контроля в HA а-ля "когда последний раз была связь с устройством", т.к. опрос производится не часто.

melky
19.01.2025, 19:49
Очень странно, у меня счётчик, где есть переменные на магнит, и ещё что-то. Которые можно сбросить только установкой часов с паролем. Который каким-то алгоритмом вычисляется из серийного номера.
Выпуск до 2016 или даже до 2015 года.

kirill.k2
19.01.2025, 19:50
Переменная NPT_SINC_READY на входе UPDATE_DATE то осталась, даже если выход Q не нужен, что тогда подавать на вход UPDATE_DATE,
просто TRUE ?


Да, верно. Если синхронизация не нужна - можно отрубать этой же переменной.



А NPT синхронизация у меня не работает, думаю IP нужен другой, из серии 192.168.0.хх

NTP, это важно ) по поводу адреса - я использую свой внутренний сервер, в той же сети что и ПЛК. однако, если на ПЛК настроена нормально маршрутизация (указан корректно гейтвей) - все работает и с внешними адресами.

kirill.k2
19.01.2025, 19:53
Pulser_stc.cmd ни разу не указатель. Как следствие за каким то каждый цикл бестолково перекладывается 250+ байт.

согласен, с обработкой буфера результата аналогично. мне так было удобнее при отладке, переделывать не стал, т.к. в проекте не риалтайм ниразу.

kirill.k2
19.01.2025, 20:05
Выпуск до 2016 или даже до 2015 года.

Сейчас не найду бумажку с паспортом, но там явно было про отсутствие пароля. А в инструкции на сайте сказано "если забыли пароль - идите с серийником в поддержку за мастер-паролем".

А вот с чем были проблемы - так это с непропаем около мк преобразования логических уровней после трансмиттера рс485. Что приводило к тому, что часть битов отъезжало и CRC пакета не сходился. Собственно, отсюда и код, запиленный на отладку в первую очередь. Неделю с осциллографом шарился, пока нашел.

kirill.k2
22.01.2025, 01:44
Обновил библиотеку до версии 0.2 - оптимизировал работу с буфером команд/результата и проверил на своем проекте.