Страница 1 из 2 12 ПоследняяПоследняя
Показано с 1 по 10 из 18

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

  1. #1
    Пользователь
    Регистрация
    15.12.2019
    Адрес
    Краснодар
    Сообщений
    10

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

    Для интеграции по RS485 счетчиков Пульсар М существует готовая библиотека для Codesys 3.5. А вот для Codesys 2.3 не нашел, ниже моя реализация. Может кому пригодится.

    По мотивам 1, 2, 3, 4 и ряда примеров с форума.

    Умеет из коробки:
    • искать адрес устройства (один раз - при старте POU по IN),
    • считывать текущие показания первого счетчика
    • считывать температуру
    • считывать напряжение встроенной батареи
    • синхронизировать время на счетчике с системным временем ПЛК (один раз - при старте POU по IN)
    • проверять формат/crc/иные ошибки фрейма данных
    • проводить опрос с заданным периодом


    Рекомендуется с синхронизацей времени по NTP.

    Пример живого POU из работающего проекта (Отладочное логгирование и FB NTP в код ниже не входят):
    PULSAR_M_FB.png

    Пример прокидывания в Modbus, обратите внимание на множители в комментариях к регистрам.
    Pulsar-Modbus.png

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

    Немного про адрес устройства
    Скрытый текст:

    В протоколе Пульсар адрес устройства задается в формате BCD. Поэтому удобно его прописывать в виде 16и-ричной переменной.

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

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


    Доступно в виде готовой библиотеки. Лицензия пусть будет LGPL3 - можно свободно использовать, изменять - но выкладывать изменения необходимо.

    LibPulsarM.lib (версия 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


    Последний раз редактировалось kirill.k2; 28.01.2025 в 02:32.

  2. #2

    По умолчанию

    Цитата Сообщение от kirill.k2 Посмотреть сообщение
    Для интеграции по RS485 счетчиков Пульсар М существует готовая библиотека для Codesys 3.5. А вот для Codesys 2.3 не нашел, ниже моя реализация. Может кому пригодится.

    По мотивам 1, 2, 3, 4 и ряда примеров с форума.

    Умеет из коробки:
    • искать адрес устройства (один раз - при старте POU по IN),
    • считывать текущие показания первого счетчика
    • считывать температуру
    • считывать напряжение встроенной батареи
    • синхронизировать время на счетчике с системным временем ПЛК (один раз - при старте POU по IN)
    • проверять формат/crc/иные ошибки фрейма данных
    • проводить опрос с заданным периодом


    Рекомендуется с синхронизацей времени по NTP.

    Живой POU:
    PULSAR_M_FB.png

    Прокидывание в Modbus
    Pulsar-Modbus.png

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

    Немного про адрес устройства
    Скрытый текст:

    В протоколе Пульсар адрес устройства задается в формате 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
    Лучше бы проект выложить.
    Последний раз редактировалось IVM; 18.01.2025 в 13:48.

  3. #3

    По умолчанию

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

    Действительно, лучше было проект выложить.
    Последний раз редактировалось kondor3000; 18.01.2025 в 14:39.

  4. #4
    Пользователь
    Регистрация
    27.11.2011
    Адрес
    Краснодар
    Сообщений
    12,280

    По умолчанию

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

  5. #5
    Пользователь
    Регистрация
    15.12.2019
    Адрес
    Краснодар
    Сообщений
    10

    По умолчанию

    Цитата Сообщение от kondor3000 Посмотреть сообщение
    Куча непоняток,
    1) откуда в программе PULSAR_M взялись входы, выходы VAR_IN_OUT ? Что такое PULSAR_LOG, LOG_BUFFER и LOG_ERROR ?
    "Туда не смотри, сюда смотри" (c) - на картинке расширенная версия, с отладкой. В теме код финальной, отладка отключена. Добавил в исходный пост, спасибо за замечание.

    Цитата Сообщение от kondor3000 Посмотреть сообщение
    Куча непоняток,
    2) откуда в ФБ NPT взялись входы In, Delay и выход Q ?
    Опять же, см.выше. Скриншот с реально рабочего проекта, для демонстрации возможностей. Конкретно то, о чем спрашиваете - это моя реализация NTP. И для работы синхронизации Пульсара она не нужна. Отвечая прямо - NTP.Delay - задержки перед повторной попыткой опроса NTP сервера, NTP.Q означает успешную синхронизацию, NTP.In - сигнал на старт. Ничего из этого не имеет прямого отношения к Пульсару.
    Последний раз редактировалось kirill.k2; 19.01.2025 в 02:44.

  6. #6
    Пользователь
    Регистрация
    15.12.2019
    Адрес
    Краснодар
    Сообщений
    10

    По умолчанию

    Цитата Сообщение от melky Посмотреть сообщение
    kirill.k2 что у вас за счетчик, на котором вы смогли синхронизировать время?
    Модель называлась как-то так (не реклама):
    Скрытый текст:

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


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

  7. #7
    Пользователь
    Регистрация
    15.12.2019
    Адрес
    Краснодар
    Сообщений
    10

    По умолчанию

    Цитата Сообщение от IVM Посмотреть сообщение
    Лучше бы проект выложить.
    Крайне неудобный формат обмена. А вот оверквотинг - точно зло, снесите, пожалуйста.

  8. #8
    Пользователь
    Регистрация
    15.12.2019
    Адрес
    Краснодар
    Сообщений
    10

    По умолчанию

    Добавил код либой. В виде либы не тестил, пока нет времени.

  9. #9
    Пользователь
    Регистрация
    27.11.2011
    Адрес
    Краснодар
    Сообщений
    12,280

    По умолчанию

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

  10. #10

    По умолчанию

    Цитата Сообщение от kirill.k2 Посмотреть сообщение
    Опять же, см.выше. Скриншот с реально рабочего проекта, для демонстрации возможностей. Конкретно то, о чем спрашиваете - это моя реализация NTP. И для работы синхронизации Пульсара она не нужна. Отвечая прямо - NTP.Delay - задержки перед повторной попыткой опроса NTP сервера, NTP.Q означает успешную синхронизацию, NTP.In - сигнал на старт. Ничего из этого не имеет прямого отношения к Пульсару.
    Переменная NPT_SINC_READY на входе UPDATE_DATE то осталась, даже если выход Q не нужен, что тогда подавать на вход UPDATE_DATE,
    просто TRUE ?
    Счётчика у меня всё равно нет, интересна просто реализация.
    А NPT синхронизация у меня не работает, думаю IP нужен другой, из серии 192.168.0.хх
    Последний раз редактировалось kondor3000; 19.01.2025 в 10:17.

Страница 1 из 2 12 ПоследняяПоследняя

Похожие темы

  1. Подключение счетчика воды "Пульсар" V46 к OwenCloud
    от piramidont в разделе Облачный сервис OwenCloud
    Ответов: 22
    Последнее сообщение: 31.08.2023, 22:49
  2. Счётчики воды "Пульсар" с RS-485
    от Vetal10 в разделе Master SCADA 4D
    Ответов: 0
    Последнее сообщение: 09.06.2022, 20:19
  3. Реализация таймера со счетчиком ST
    от Eugene69 в разделе ПЛК2хх
    Ответов: 2
    Последнее сообщение: 19.02.2022, 17:44
  4. Реализация обмена по протоколу Modbus
    от antonkh в разделе ПЛК1хх
    Ответов: 1
    Последнее сообщение: 29.04.2019, 12:41
  5. Нет обмена между СПК105 и МВ110-4ТД по RS485
    от OBc9Hka в разделе СПК1xx (архив)
    Ответов: 1
    Последнее сообщение: 27.12.2016, 00:33

Ваши права

  • Вы не можете создавать новые темы
  • Вы не можете отвечать в темах
  • Вы не можете прикреплять вложения
  • Вы не можете редактировать свои сообщения
  •