Для интеграции по 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Модуль mNextStepКод:(* Настройка порта 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();
Модуль mNextStepDelayКод:(* Формируем ID запроса инкрементом *) wReqId := wReqId + 1; IF wReqId = 16#FFFF THEN wReqId := 1; END_IF STC(IN:=FALSE); STATE := STATE + 1;
Код:(* Задержка между запросами *) 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