PDA

Просмотр полной версии : Читаем нестандартный протокол (SysLibCom, SysLibMem)



Yegor
23.10.2015, 13:15
Расскажу на конкретном примере как быть, когда вам попадается устройство со своим протоколом обмена данными по RS-232/485, и его надо подключить к ПЛК.

0. Как вообще работают с портом

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

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

В итоге для постоянного чтения какого-то параметра вы должны делать примерно так: открыть порт с определёнными параметрами (скорость, чётность и т.д.), отправить запрос, принять ответ, отправить запрос, принять ответ и т.д. пока контроллер не выключат. Конечно, ответ надо ждать. И никто не запрещает ждать его бесконечно, но разумнее спустя какое-то время (таймаут) после предыдущего запроса отправить запрос повторно или ещё как-то обработать ситуацию.

1. Как работают с портом на ПЛК

На ПЛК порт зачастую используется всё время работы, и поэтому его можно вообще не закрывать.

Специфика отправки и получения внешних данных на ПЛК в том, что вам нельзя тормозить саму программу для ожидания, ведь она — не забываем — должна пересчитываться много-много раз в секунду как можно быстрее. Вдобавок не предусматривается способа как-то подписаться на получение данных, чтобы по мере поступления ответа что-то извне вызывало ваши функции обработки или иным образом сообщало о поступлении данных, как это бывает возможно при программировании под современный персональный компьютер. Поэтому на ПЛК после отправки запроса мы сами должны постоянно проверять наличие ответа.

2. Чем работают с портом на ПЛК

Для работы с последовательным портом используется библиотека SysLibCom. В ней нас интересуют 4 функции:



SysComOpen(номер_порта) — открыть порт, то есть выразить системе своё намерение его единолично использовать. По документации эта функция должна возвращать дескриптор порта (билет на применение порта в других функциях). Однако в случае с ПЛК110/160 она возвращает некую ерунду, по которой можно определить, открылся ли порт, а в других функциях вместо дескриптора используется номер порта.
SysComSetSettings(параметры) — устанавливает параметры порта (скорость, чётность и т.д.), по коду возврата можно определить, получилось ли задать параметры;
SysComWrite(дескриптор/номер_порта, указатель_на_буфер, размер_буфера) — эта функция пишет данные из указанного вами буфера в порт и возвращает число, указывающее, сколько байт ей удалось записать в этот раз;
SysComRead(дескриптор/номер_порта, указатель_на_буфер, сколько_прочитать) — эта функция читает данные из порта в указанный вами буфер и возвращает число, указывающее, сколько байт ей удалось прочитать в этот раз.


3. Как организовать последовательный опрос устройства

Чтобы не запутаться, на каком шаге вы остановились, и чтобы вообще «шагать», я рекомендую просто запоминать номер шага одной переменной и менять его по мере прохождения. Можно, конечно, использовать и большее число переменных-признаков, чтобы запомнить различные события вроде «запрос отправлен» и «ответ прочитан», как это делается в других примерах, но я нахожу такой способ сложным в изолировании шагов друг от друга и в добавлении/удалении шагов.

Давайте теперь распишем шагами наш автомат.

Открывать порт. Если открылся, то идём дальше.
Выставлять параметры порта. Если выставились, идём дальше.
Сформировать запрос и идти дальше.
Отправлять запрос. Когда он целиком отправлен, идти дальше.
Принимать ответ. Когда он целиком принят, разобрать его и перейти к шагу 3.


Плюс общее правило: если мы слишком долго стоим на одном шаге, то считать это таймаутом. Прошу заметить, что я использовал несовершенные формы глаголов — открывать, выставлять, отправлять, принимать. Так как мы не тормозим программу, то все эти действия могут выполняться не сразу, особенно приём данных — нет гарантии, что ответ придёт одним куском. Поэтому мы не можем просто сделать, мы будем делать пока не сделаем. Только формирование запроса, как правило, удаётся сделать в один приём.

Такие простые автоматы удобнее всего реализуются конструкцией CASE языка ST. С человеческого переводится почти дословно:
CASE step OF
0:
port := SysComOpen(0);
IF port = удалось_открыть THEN
step := step + 1;
END_IF
1:
IF SysComSetSettings(настройки) = получилось THEN
step := step + 1;
END_IF
2:
buffer[0] := 1; (* адрес устройста *)
buffer[1] := 48; (* код запроса *)
buffer[2] := 9F; (* контрольная сумма *)
step := step + 1;
3:
...
END_IFНу а общее правило о том, чтобы слишком долго не стоять на одном месте, мы можем сделать где-то за пределами этой конструкции.

4. Что за буферы, как отправлять их содержимое и как принимать в них данные

Буфер это временное хранилище данных. Байтовый массив, как правило; буквально ARRAY OF BYTE. Вы в своём буфере будете подготавливать запрос и собирать по кусочкам ответ. Контроллер в своём буфере тоже будет собирать ваши данные для отправки и получения. Так что не ждите, что ваш запрос уйдёт прямо моментально. Да и для различных частей контроллера ваша программа тоже не моментальна, оттого данные и приходится буферизовать, чтобы более равномерно их обрабатывать.

В предыдущем пункте было упомянуто, что данные могут не уйти и не прийти одним куском. А если их много, то почти наверняка получится несколько фрагментов. Поэтому функции SysComWrite и SysComRead написаны так, чтобы можно было легко скользить по буферу и даже стоять в нём на одном месте. Для этого нужно всего лишь смотреть, сколько байтов реально удалось получить или отправить, а в следующий раз начинать на это же число дальше и просить на это же число меньше. Короче, вот:
count := count + SysComRead(port, ADR(buffer) + count, SIZEOF(buffer) - count);Этой строкой мы заполним буфер от начала до конца вне зависимости от того, каким числом фрагментов нам придёт ответ и сколько фрагментов окажутся пустыми. Пускай буфер у нас 10 байт, и нам идут фрагменты: 4 байта, 0 байт, 6 байт. Сначала прибавилось 4, count стало 4, в следующий скан мы даём адрес больше на 4 и просим меньше на 4, но приходит 0, который сумму не меняет (стоим на месте). И как только count становится равным ожидаемому числу байт, мы можем считать, что получили необходимый объём данных.

5. Тривиальный протокол

Допустим, мы опрашиваем светофор. К нему прилагается спецификация протокола:

В запросе нулевой байт это адрес светофора, первый байт — код функции, второй байт — параметр функции. Есть два кода: 54 — узнать текущий сигнал, 68 — сменить сигнал. Сигналы обозначаются так: 1 — красный, 2 — жёлтый, 4 — зелёный. Ответ светофора на обе функции: свой адрес, код сигнала, контрольная сумма. Контрольная сумма это адрес+код_сигнала. Порт светофора: 19200/8/N/1. Ждать ответа дольше 300 мс не имеет смысла.

Погнали:
PROGRAM PLC_PRG
VAR
step, _step, count: DINT; (* номер шага, номер шага на предыдущем цикле ПЛК, счётчик данных *)
rx_buffer, tx_buffer: ARRAY [0..5] OF BYTE; (* буферы на отправку и на приём, можно обойтись одним *)
timeout: TON := (PT := T#300ms); (* этим таймером будем делать таймауты *)
END_VAR
VAR CONSTANT
port: COMSETTINGS := (Port := COM1, dwBaudRate := 19200);
traffic_light_address: BYTE := 1; (* Адрес светофорного блока *)
END_VAR

CASE step OF

0:
(* Сравнение с нулём вместо захвата
дескриптора потому что Овен *)
IF SysComOpen(port.Port) = 0 THEN
step := step + 1;
END_IF
1:
IF SysComSetSettings(port.Port, ADR(port)) = 0 THEN
step := step + 1;
END_IF
2:
tx_buffer[0] := traffic_light_address; (* Адрес светофора *)
tx_buffer[1] := 54; (* Узнать текущий сигнал *)
tx_buffer[2] := 0; (* Чтение сигнала не требует параметров *)
count := 0;
3:
(* Как правило, короткие запросы уходят одним куском, но мы перестрахуемся *)
count := count + SysComWrite(port.Port, ADR(tx_buffer) + count, 3 - count, 0);
IF count >= 3 THEN
count := 0; (* Обнуляем счётчик для следующего шага *)
step := step + 1;
END_IF
4:
count := count + SysComRead(port.Port, ADR(rx_buffer) + count, 3 - count, 0);
IF count >= 3 THEN
IF rx_buffer[0] = traffic_light_address AND rx_buffer[2] = rx_buffer[1] + rx_buffer[0] THEN
;(* Верный ответ. В rx_buffer[1] лежит код текущего сигнала светофора *)
ELSE
;(* Нам ответил не тот светофор или данные повреждены, хоть их и пришло достаточно *)
END_IF
step := 2; (* Можно и 3, т.к. запрос не меняется *)
END_IF
END_CASE


timeout(IN := step = _step);
IF timeout.Q THEN
(* Мы дольше 300 мс на одном шаге.
Можно попробовать отправить запрос повторно *)
step := 3;
END_IF
_step := step;В принципе, этот код должен быть рабочим. Только проверить его не на чем.

6. Читаемость и отлаживаемость

В нагрузку к тому, что состояние автомата выражается всего одной переменной, которую всегда можно поменять одним движением, получаем ещё возможность проименовать шаги вместо их нумерации. Достаточно ввести тип-перечисление:
TYPE X_STEP : (X_OPEN, X_SETUP, X_MAKE_QRY, X_SEND_QRY, X_RECEIVE_RESPONSE); END_TYPEСоответственно можем написать
VAR
step, _step: X_STEP;
END_VAR

CASE step OF X_OPEN: ... ;
X_SETUP: ... ;
X_MAKE_QRY: ... ;
END_CASEТеперь не надо запоминать номер шагов и переключать их можно тоже по именам: step := X_SEND_QRY.

А в случае неоднократного таймаута вместо очередного запроса можно выставить заведомо некорректный шаг, в котором будет содержаться и последний корректный: step := step + 100. И теперь коды со 100 до 104 будут означать завершение опроса на определённом шаге из-за таймаута. Вдобавок кодесис подсветит такие значения красным, т.к. они не будут входить в диапазон значений типа X_STEP.

7. Оформление в виде функционального блока

У функционального блока не должно быть зависимостей от конкретных данных в проекте, поэтому открывание порта выносится «за скобки». Кроме того, функциональные блоки используются через выходные и выходные переменные. Входными можно назначить такие вещи, как адрес, код функции, код нужного значения и даже активацию блока (вход Enable как в modbus.lib от Овена). На выход можно пустить признак успешности обмена, полученное значение, код ошибки и т.д.

8. Жонглируем байтами

Разумеется, почти любой нетривиальный протокол имеет поля, которые занимают больше одного байта и из буфера их простым "a := b[5]" не вытащить. На помощь приходят два средства: указатели и SysLibMem. Первое я не буду рассматривать из-за ограничений по кратности, которые доставляют много хлопот в данном случае. А вот во втором есть простая и полезная функция SysMemCpy (memory copy). Указываете ей куда, откуда и сколько байт скопировать, и получаете, например, REAL, который лежал в буфере с N по N+3 байты. То есть если спецификация протокола говорит, что в ответе с 5 по 8 байты лежит текущее значение в формате IEEE754, то от вас требуется только это:
VAR
buffer: ARRAY[0..20] OF BYTE;
value: REAL;
END_VAR


SysMemCpy(ADR(value), ADR(buffer) + 5, SIZEOF(value));Ещё бывает порядок байтов, который отличается от нужного вашему процессору. Тогда используем SysMemSwap(адрес_где_перевернуть, сколько_перевернуть, по_сколько_переворачивать). Ещё есть SysMemSet — используется, как правило, для обнуления буфера, но может забить указанный участок памяти и любым другим символом кроме нуля.

9. Реальный протокол

Как-то я столкнулся с тензоусилителем-преобразователем одной белорусской фирмы. По их заявлениям устройство поддерживало протокол modbus. Меня ещё перед заказом смутило то, что я не смог нигде выяснить раскладку modbus-регистров преобразователя. Когда устройство наконец пришло, то из документов я узнал, что из общего с модбасом там только метод вычисления контрольной суммы. Вот ссылка на спецификацию (продублирую вложением): http://tilkom.com/download_files/download/modbus.pdf. Обратите внимание на имя файла и на заголовок в документе. Знающие люди не дадут соврать — в действительности модбасом там не пахнет. С другой стороны, протокол несложный, так что смело расчехляем кодесис. Дальше мои рассуждения во время ознакомления со спецификацией и боевой код из проекта.

Нам требуется только текущее значение усилия, и значит, нас интересуют лишь некоторые функции. После беглого чтения выясняется, что устройству сначала надо подать запрос на начало измерений (код 101), а потом уже запрашивать текущие показания (код 104). Заканчивать измерения (102) и закрывать порт смысла нет, т.к. мы просто читаем текущее показания пока не снимут питание, а дальше хоть трава не расти — и устройство, и порт будут в исходном состоянии при включении.

Запросы и ответы небольшие. Возьмём буфер в 17 байт на отправку запроса и в 32 байта на приём ответа (можно посчитать максимальную длину запроса и ответа и принять их в качестве размеров).

Контрольная сумма вычисляется как в модбасе — слямзим из какого-нибудь примера или переведём с другого языка.

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

Читать также требуется максимально быстро, так что не будем делать планирование по таймеру. Да и если бы не требовалось, всё равно проще запрашивать сразу после получения ответа, чем выжидать интервал.

Для достижения максимальной частоты опроса попробуем избавиться от холостого цикла, который возникает при использовании CASE-автомата, т.е. когда ответ уже принят, но запрос будет отправлен только на следующем цикле ПЛК. Можно обернуть CASE-блок в цикл REPEAT, чтобы получить возможность прогнать CASE повторно выставлением признака для UNTIL. Чтобы не начудить и не скатиться в бесконечный повтор, сделаем признак по умолчанию запрещающим повтор. В итоге этот подход даст прирост со 180 до 220 чтений в секунду.

Начинаем (ш)кодить. Перепишем коды запросов в перечислимый тип:
TYPE TLK_CMD : (
READ_VERSION := 66, GET_CURRENT_TIME, SET_CURRENT_TIME, GET_MESSAGE,


START_MEASURING := 101, STOP_MEASURING, GET_ID, READ_BASE,
READ_SPEED, READ_TEMP, READ_COMPLEX, STREAM_BASE, SET_PARAM
);
END_TYPEЗачем переписал все, если нужны только два — не знаю. Не важно, не мешают. Пропишем шаги для автомата (на самом деле постепенно добавлялись):
TYPE TLK_XSTATE : (
TLKX_IDLE, (* бездействие *)
TLKX_START_QRY, (* запрос на начало измерений *)
TLKX_START_ACK, (* приём ответа на запрос начала измерений *)
TLKX_VALUE_QRY, (* запрос на текущее показание *)
TLKX_VALUE_RSP, (* приём текущего показания *)
TLKX_SUCCESS, (* здесь побудем для индикации успешного обмена, пускай все знают *)
TLKX_STOP_QRY,
TLKX_STOP_ACK,
TLKX_ERROR
);
END_TYPEЕщё хочется специализированный таймер, а то надоело вводить дополнительные переменные для TON. Назовём его STATE_TIMER, пускай он считает заново при любом изменении входного значения:
FUNCTION_BLOCK STATE_TIMER
VAR_INPUT
STATE: DWORD;
END_VAR
VAR_OUTPUT
ET: TIME; (* ET grows until STATE is changed, which causes it to reset to T#0s *)
END_VAR
VAR
timer: TON;
_STATE: DWORD;
END_VAR

timer(IN := STATE = _STATE, ET => ET, PT := DWORD_TO_TIME(NOT 0));
_STATE := STATE;Где у нас там был чужой код для расчёта контрольной суммы из модбаса? Причешем его чуть-чуть, потестируем, и получим функцию:
FUNCTION CRC16 : WORD
VAR_INPUT
data_ptr: POINTER TO BYTE;
length: UDINT;
END_VAR
VAR
iter: UDINT;
table_idx: BYTE;
END_VAR
VAR CONSTANT
C_LOOKUP: ARRAY [0..255] OF WORD :=
16#0000, 16#C0C1, 16#C181, 16#0140, 16#C301, 16#03C0, 16#0280, 16#C241,
16#C601, 16#06C0, 16#0780, 16#C741, 16#0500, 16#C5C1, 16#C481, 16#0440,
16#CC01, 16#0CC0, 16#0D80, 16#CD41, 16#0F00, 16#CFC1, 16#CE81, 16#0E40,
16#0A00, 16#CAC1, 16#CB81, 16#0B40, 16#C901, 16#09C0, 16#0880, 16#C841,
16#D801, 16#18C0, 16#1980, 16#D941, 16#1B00, 16#DBC1, 16#DA81, 16#1A40,
16#1E00, 16#DEC1, 16#DF81, 16#1F40, 16#DD01, 16#1DC0, 16#1C80, 16#DC41,
16#1400, 16#D4C1, 16#D581, 16#1540, 16#D701, 16#17C0, 16#1680, 16#D641,
16#D201, 16#12C0, 16#1380, 16#D341, 16#1100, 16#D1C1, 16#D081, 16#1040,
16#F001, 16#30C0, 16#3180, 16#F141, 16#3300, 16#F3C1, 16#F281, 16#3240,
16#3600, 16#F6C1, 16#F781, 16#3740, 16#F501, 16#35C0, 16#3480, 16#F441,
16#3C00, 16#FCC1, 16#FD81, 16#3D40, 16#FF01, 16#3FC0, 16#3E80, 16#FE41,
16#FA01, 16#3AC0, 16#3B80, 16#FB41, 16#3900, 16#F9C1, 16#F881, 16#3840,
16#2800, 16#E8C1, 16#E981, 16#2940, 16#EB01, 16#2BC0, 16#2A80, 16#EA41,
16#EE01, 16#2EC0, 16#2F80, 16#EF41, 16#2D00, 16#EDC1, 16#EC81, 16#2C40,
16#E401, 16#24C0, 16#2580, 16#E541, 16#2700, 16#E7C1, 16#E681, 16#2640,
16#2200, 16#E2C1, 16#E381, 16#2340, 16#E101, 16#21C0, 16#2080, 16#E041,
16#A001, 16#60C0, 16#6180, 16#A141, 16#6300, 16#A3C1, 16#A281, 16#6240,
16#6600, 16#A6C1, 16#A781, 16#6740, 16#A501, 16#65C0, 16#6480, 16#A441,
16#6C00, 16#ACC1, 16#AD81, 16#6D40, 16#AF01, 16#6FC0, 16#6E80, 16#AE41,
16#AA01, 16#6AC0, 16#6B80, 16#AB41, 16#6900, 16#A9C1, 16#A881, 16#6840,
16#7800, 16#B8C1, 16#B981, 16#7940, 16#BB01, 16#7BC0, 16#7A80, 16#BA41,
16#BE01, 16#7EC0, 16#7F80, 16#BF41, 16#7D00, 16#BDC1, 16#BC81, 16#7C40,
16#B401, 16#74C0, 16#7580, 16#B541, 16#7700, 16#B7C1, 16#B681, 16#7640,
16#7200, 16#B2C1, 16#B381, 16#7340, 16#B101, 16#71C0, 16#7080, 16#B041,
16#5000, 16#90C1, 16#9181, 16#5140, 16#9301, 16#53C0, 16#5280, 16#9241,
16#9601, 16#56C0, 16#5780, 16#9741, 16#5500, 16#95C1, 16#9481, 16#5440,
16#9C01, 16#5CC0, 16#5D80, 16#9D41, 16#5F00, 16#9FC1, 16#9E81, 16#5E40,
16#5A00, 16#9AC1, 16#9B81, 16#5B40, 16#9901, 16#59C0, 16#5880, 16#9841,
16#8801, 16#48C0, 16#4980, 16#8941, 16#4B00, 16#8BC1, 16#8A81, 16#4A40,
16#4E00, 16#8EC1, 16#8F81, 16#4F40, 16#8D01, 16#4DC0, 16#4C80, 16#8C41,
16#4400, 16#84C1, 16#8581, 16#4540, 16#8701, 16#47C0, 16#4680, 16#8641,
16#8201, 16#42C0, 16#4380, 16#8341, 16#4100, 16#81C1, 16#8081, 16#4040;
END_VAR

CRC16 := NOT 0;


FOR iter := 0 TO length - 1 DO
table_idx := WORD_TO_BYTE(CRC16 XOR data_ptr^);
CRC16 := SHR(CRC16, 8);
CRC16 := CRC16 XOR C_LOOKUP[table_idx];
data_ptr := data_ptr + 1;
END_FORТут важен подход — сам я это с нуля не писал и никому не советую. Учимся гуглить и подсматривать.

Наконец есть всё, чтобы реализовать опрос. Шутка. На самом деле опрос этого конкретного устройства сначала выглядел как опрос светофора в п. 5, а уже потом код оброс плюшками. Барабанная дробь...
FUNCTION_BLOCK TLK_READER (* Считывает показания тензопреобразователей ООО Тилком (Белоруссия) *)VAR_INPUT
enable: BOOL; (* Пуск/остановка передатчика *)
proceed: BOOL; (* Разрешение на отправку очередного запроса *)
port_handle: DWORD; (* Дескриптор открытого и настроенного порта *)
address: BYTE; (* Адрес устройства *)
timeout: TIME := T#300ms; (* Таймаут ожидания ответа *)
END_VAR
VAR_OUTPUT
value: REAL; (* Измеренное значение в заданных единицах *)
done: BOOL; (* Признак завершения обмена *)
success: BOOL; (* Признак успешного обмена *)
END_VAR
VAR
poller_state: TLK_XSTATE; (* Состояние опрашивающего автомата *)
pstate_timer: STATE_TIMER; (* Секундомер состояний автомата *)
starter: R_TRIG; (* Пускатель автомата *)
stopper: F_TRIG; (* Стоп автомата *)
cmd_buf: ARRAY [0..16] OF BYTE; (* Буфер формирования команд к отправке *)
rcv_buf: ARRAY[0..31] OF BYTE; (* Буфер получения данных *)
i, valid_frame_count, timeout_count: DINT;
crc: WORD;
rpt: BOOL;
END_VAR

starter(CLK := enable);
stopper(CLK := enable);


IF starter.Q THEN (* 'enable' rising edge *)
COM_DISCARD(port_handle);
poller_state := TLKX_START_QRY;
END_IF


IF stopper.Q THEN (* 'enable' falling edge *)
COM_DISCARD(port_handle);
poller_state := TLKX_STOP_QRY;
END_IF


REPEAT
rpt := FALSE;
CASE poller_state OF


TLKX_START_QRY:
(* Заполнение командного пакета StartMeasuring *)
SysMemSet(ADR(cmd_buf), 0, SIZEOF(cmd_buf));
cmd_buf[0] := address;
cmd_buf[1] := INT_TO_BYTE(START_MEASURING);
cmd_buf[2] := 16#C;
cmd_buf[4] := 16#1;
cmd_buf[10] := 16#E8;
cmd_buf[11] := 16#03;
crc := CRC16(ADR(cmd_buf), 15);
cmd_buf[15] := WORD_TO_BYTE(crc);
cmd_buf[16] := WORD_TO_BYTE(SHR(crc, 8));
(* ----->8----- *)
SysComWrite(port_handle, ADR(cmd_buf), 17, 0);
poller_state := TLKX_START_ACK;
i := 0;


TLKX_START_ACK: (* Receive StartMeasuring ACK *)
IF i < 3 THEN
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, 3 - i, 0);
ELSE
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, rcv_buf[2] - i + 5, 0);
IF i >= rcv_buf[2] + 5 AND pstate_timer.ET > T#200ms THEN
poller_state := TLKX_VALUE_QRY;
END_IF
END_IF
IF pstate_timer.ET > timeout THEN
poller_state := TLKX_START_QRY;
timeout_count := timeout_count + 1;
END_IF

TLKX_VALUE_QRY:
(* Заполнение командного пакета ReadBase *)
SysMemSet(ADR(cmd_buf), 0, SIZEOF(cmd_buf));
cmd_buf[0] := address;
cmd_buf[1] := INT_TO_BYTE(READ_BASE);
crc := CRC16(ADR(cmd_buf), 3);
cmd_buf[3] := WORD_TO_BYTE(crc);
cmd_buf[4] := WORD_TO_BYTE(SHR(crc, 8));
(* ----->8----- *)
IF proceed THEN
SysComWrite(port_handle, ADR(cmd_buf), 5, 0);
poller_state := TLKX_VALUE_RSP;
i := 0;
END_IF


TLKX_VALUE_RSP: (* Receive ReadBase response *)
IF i < 3 THEN
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, 3 - i, 0);
ELSE
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, rcv_buf[2] - i + 5, 0);
IF i >= rcv_buf[2] + 5 AND i >= 17 THEN
poller_state := TLKX_SUCCESS;
valid_frame_count := valid_frame_count + 1;
SysMemCpy(ADR(value), ADR(rcv_buf) + 11, SIZEOF(value));
END_IF
END_IF
IF pstate_timer.ET > timeout THEN
poller_state := TLKX_START_QRY;
timeout_count := timeout_count + 1;
END_IF

TLKX_SUCCESS:
IF proceed THEN
poller_state := TLKX_VALUE_QRY;
rpt := TRUE;
END_IF

TLKX_STOP_QRY:
(* Заполнение командного пакета *)
SysMemSet(ADR(cmd_buf), 0, SIZEOF(cmd_buf));
cmd_buf[0] := address;
cmd_buf[1] := INT_TO_BYTE(STOP_MEASURING);
crc := CRC16(ADR(cmd_buf), 3);
cmd_buf[3] := WORD_TO_BYTE(crc);
cmd_buf[4] := WORD_TO_BYTE(SHR(crc, 8));
(* ----->8----- *)
SysComWrite(port_handle, ADR(cmd_buf), 5, 0);
poller_state := TLKX_STOP_ACK;
i := 0;


TLKX_STOP_ACK:
IF i < 3 THEN
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, 3 - i, 0);
ELSE
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, rcv_buf[2] - i + 5, 0);
IF i >= rcv_buf[2] + 5 AND pstate_timer.ET > T#200ms THEN
poller_state := TLKX_IDLE;
END_IF
END_IF


TLKX_ERROR:
;

END_CASE
UNTIL NOT rpt END_REPEAT


pstate_timer(STATE := poller_state);
Из неописанного ранее здесь только чистка порта и двухэтапное чтение ответа. Первое, может, и не сильно надо, а вот второе давайте обсудим:
IF i < 3 THEN
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, 3 - i, 0);
ELSE
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, rcv_buf[2] - i + 5, 0);
IF i >= rcv_buf[2] + 5 AND i >= 17 THEN
poller_state := TLKX_SUCCESS;
valid_frame_count := valid_frame_count + 1;
SysMemCpy(ADR(value), ADR(rcv_buf) + 11, SIZEOF(value));
END_IF
END_IFВ тривиальном примере мы заранее знали, сколько байт в ответе. В этом реальном протоколе длина ответа заранее неизвестна, но указывается неподалёку от начала ответа — в третьем байте. Поэтому сначала мы читаем до третьего байта, а после него уже становится ясно, сколько нужно прочитать ещё. Можно сделать иначе — сколько получится, а потом уже разбираться, но это мне кажется неряшливым.

Можно видеть, как переменная rpt делается TRUE, чтобы из состояния TLKX_SUCCESS тут же перейти в состояние TLKX_VALUE_QRY. Именно это позволило разогнаться со 180 до 220 чтений в секунду. Есть потенциал для дальнейшего разгона, но по-видимому, тензопреобразователь уже начинает запинаться. Для наблюдения введены переменные для подсчета успешных чтений и неуспешных.

Вызывается этот ФБ следующим образом:
VAR
tenso_port_opener: COM_OPENER := (
PORT := C_TENSO_COM_PORT,
CFG := (dwBaudRate := C_TENSO_COM_BDRATE));

tenso_reader: TLK_READER := (
address := C_TENSO_ADDR,
enable := TRUE, proceed := TRUE);
END_VAR
VAR CONSTANT
(* Параметры порта тензометра *)
C_TENSO_COM_PORT: DWORD := 0; (* RS-485 *)
C_TENSO_COM_BDRATE: DWORD := 115200;
C_TENSO_ADDR: BYTE := 1;
END_VAR

tenso_port_opener();
IF tenso_port_opener.READY THEN
tenso_reader(port_handle := tenso_port_opener.HANDLE, value => tension);
END_IF

rapucha
20.12.2015, 01:55
Йоу!
Овеновский отдел тех. писателей разогнать, написание документации поручить Yegor.

Я кстати до твоего текста никак не вдуплял, что ж такое возвращает SysComRead(). Спасибо.

Yegor
20.12.2015, 08:12
Я замечал, что иногда она возвращает больше, чем просили. Не страшно, но надо иметь в виду при получении и разборе ответа.

rapucha
20.12.2015, 13:40
Я замечал, что иногда она возвращает больше, чем просили..

Нда. Овен-то ладно, но КодеСись могли бы пару лишних строк черкнуть в своей доке.

Scream
20.12.2015, 18:17
Респект за статью. Часто приходится работать с нестандартными протоколами.
Былоб не плохо нечто такого о сокетах, так сказать итог последних лет 7 о том как все таки работают сокеты у овена, они бы сами почитали.

_Pavel_
21.12.2015, 16:37
Респект, отличное изложение. Один вопрос, Yegor, почему вы не используете префиксы в названиях переменных как то предписывает документация CDS?

rapucha
21.12.2015, 16:56
Я замечал, что иногда она возвращает больше, чем просили.
Это ничего, главное чтобы SysComWrite() не вернула больше, чем просят ))

Yegor
21.12.2015, 18:44
Один вопрос, Yegor, почему вы не используете префиксы в названиях переменных как то предписывает документация CDS?Рекомендует, а не предписывает (2.3). Standard.lib и другие самые часто используемые библиотеки не следуют этой рекомендации. Следовательно, венгерская нотация остается на откуп конечному программисту. Ну а раз так, то я волен её не глаголИспользовать (http://stackoverflow.com/questions/111933/why-shouldnt-i-use-hungarian-notation).

kolyan
22.12.2015, 13:47
Йоу!
Овеновский отдел тех. писателей разогнать, написание документации поручить Yegor.



Согласен на 100%!

Спорягин Кирилл
15.01.2016, 16:45
Огромное спасибо за статью.

Kostennikov
22.01.2016, 15:00
Yegor, а вы капитально разбираетесь в контроллерах). Сейчас понимаю что мне много еще что познать надо... А может вы мне подскажете по библиотеке NetVar для кдс 2 если вы в ней копались основательно.

madiggo
23.06.2016, 13:09
Тоже столкнулся с продукцией данной компании, только с датчиком момента и скорости. Естественно, тоже уперся в то, что они называют Модбасом. И что самое удивительное - это действительно Модбас. В протоколе действительно зарезервированы номера функций 101-110 и 66-69 (могу ошибаться в диапазоне) для расширений производителя. А раз так, то нас может выручить блок MB_UNI_IO из Modbus.lib. Но он не выручает, потому как при попытке заменить им блоки MB_RD_* (MB_WR_*) компилятор ругается на конфликт типов данных в ComHandle.
Выяснилось, что за каким-то лядом все входные переменные в блоке были сделаны VAR_IN_OUT, что и приводило к ошибкам компиляции. Пришлось вскрывать Modbus.lib и править объявление на такое:

FUNCTION_BLOCK MB_UNI_IO
VAR_INPUT
Mode: MB_MODE; (* Serial Transmission Mode of MODBUS networks *)
ComHandle:DWORD; (* &#228;&#229;&#241;&#234;&#240;&#232;&#239;&#242;&#238;&#240; &#239;&#238;&#241;&#235;&#229;&#228;&#238;&#226;&#224;&#242;&#229;&#235;&#252;&#237;&#238;&#227;&#238; &#239;&#238;&#240;&#242;&#224; &#225;&#232;&#225;&#235;&#232;&#238;&#242;&#229;&#234;&#232; SysLibCom *)
TimeOut: TIME; (* &#226;&#240;&#229;&#236;&#255; &#242;&#224;&#233;&#236;-&#224;&#243;&#242;&#224; [&#236;&#241;] - &#236;&#224;&#234;&#241;. &#231;&#224;&#228;&#229;&#240;&#230;&#234;&#224; &#237;&#224; &#238;&#225;&#240;&#224;&#225;&#238;&#242;&#234;&#243; &#231;&#224;&#239;&#240;&#238;&#241;&#224; *)
END_VAR
VAR_IN_OUT
Complete: BOOL; (* &#229;&#241;&#235;&#232; = TRUE, &#242;&#238; &#238;&#225;&#236;&#229;&#237; &#231;&#224;&#226;&#229;&#240;&#248;&#229;&#237; *)
Exception: BYTE; (* &#232;&#241;&#234;&#235;&#254;&#247;&#229;&#237;&#232;&#255; &#239;&#240;&#238;&#242;&#238;&#234;&#238;&#235;&#224; MODBUS *)
DataBuf: ARRAY[0..255] OF BYTE; (* &#225;&#243;&#244;&#229;&#240; &#228;&#224;&#237;&#237;&#251;&#245; &#234;&#224;&#228;&#240;&#224; *)
DataSize: BYTE; (* &#240;&#224;&#231;&#236;&#229;&#240; &#234;&#224;&#228;&#240;&#224; &#225;&#229;&#231; &#234;&#238;&#237;&#242;&#240;&#238;&#235;&#252;&#237;&#238;&#233; &#241;&#243;&#236;&#236;&#251; *)
END_VAR
После этого пример из библиотеки заработал, как это ни странно, с минимальными переделками:

PROGRAM PLC_PRG
VAR
(*&#207;&#240;&#232;&#236;&#229;&#240; &#240;&#224;&#225;&#238;&#242;&#251; &#241; &#247;&#224;&#241;&#242;&#238;&#242;&#237;&#251;&#236; &#239;&#240;&#232;&#226;&#238;&#228;&#238;&#236; &#206;&#194;&#197;&#205; &#207;&#215;&#194; &#239;&#238; RS-485*)
com_num1: PORTS; (*&#205;&#238;&#236;&#229;&#240; &#239;&#238;&#240;&#242;&#224; &#237;&#224; &#234;&#238;&#242;&#238;&#240;&#251;&#233; &#239;&#238;&#228;&#234;&#235;&#254;&#247;&#229;&#237;&#238; &#238;&#225;&#238;&#240;&#243;&#228;&#238;&#226;&#224;&#237;&#232;&#229;*)
com_hndl: DWORD;
Settings1: COMSETTINGS; (*&#207;&#224;&#240;&#224;&#236;&#229;&#242;&#240;&#251; &#239;&#238;&#240;&#242;&#224; *)
COM_SERVICE1: COM_SERVICE; (*&#193;&#235;&#238;&#234; &#238;&#242;&#234;&#240;&#251;&#242;&#232;&#255; &#232; &#237;&#224;&#241;&#242;&#240;&#238;&#233;&#234;&#232; &#239;&#238;&#240;&#242;&#224; *)
com_ready1: BOOL; (*&#209;&#242;&#224;&#242;&#243;&#241; &#239;&#238;&#240;&#242;&#224; - &#238;&#242;&#234;&#240;&#251;&#242;/&#231;&#224;&#234;&#240;&#251;&#242;*)
UNI:MB_UNI_IO;
i: BYTE;
command_word:WORD:=16#847C;
RT: R_TRIG;
t_zad:TIME:=T#20ms;
buffer: ARRAY[0..255] OF BYTE;
result: ARRAY[0..255] OF BYTE;
id: ARRAY[0..255] OF BYTE;
mode: MB_MODE := MB_RTU;
data_size: BYTE := 17;
compl: BOOL;
except: BYTE;
port_opened: BOOL;
mtype: INT;
crc: WORD;
crc1: WORD;
END_VAR



(************************************************* ****
&#207;&#240;&#232;&#236;&#229;&#240; &#240;&#224;&#225;&#238;&#242;&#251; &#241; &#225;&#232;&#225;&#235;&#232;&#238;&#242;&#229;&#234;&#238;&#233; SysLibCom

&#244;&#238;&#240;&#236;&#232;&#240;&#238;&#226;&#224;&#237;&#232;&#229; &#234;&#238;&#236;&#224;&#237;&#228;&#251; &#226; &#228;&#226;&#238;&#232;&#247;&#237;&#238;&#236; &#226;&#232;&#228;&#229;
************************************************** ***)


(************************************************* ******
&#206;&#242;&#234;&#240;&#251;&#226;&#224;&#229;&#236; &#239;&#238;&#240;&#242;
************************************************** *****)

(*&#206;&#242;&#234;&#240;&#251;&#226;&#224;&#229;&#236; &#239;&#229;&#240;&#226;&#251;&#233; &#239;&#238;&#240;&#242;*)
IF NOT com_ready1 THEN
(*
&#205;&#238;&#236;&#229;&#240; &#239;&#238;&#240;&#242;&#224; (com_number):
0 - RS-485
1 - RS-232
4 - RS-232 DEBUG
*)
com_num1 := 0;
(*
&#205;&#224;&#241;&#242;&#240;&#238;&#233;&#234;&#232; &#239;&#238;&#240;&#242;&#224; (com_settings):
byParity - &#208;&#229;&#230;&#232;&#236; &#239;&#240;&#238;&#226;&#229;&#240;&#234;&#232; &#247;&#229;&#242;&#237;&#238;&#241;&#242;&#232; 0 = &#237;&#229;&#242;, 1 = &#237;&#229;&#247;&#229;&#242;, 2 = &#247;&#229;&#242;
byStopBits - &#202;&#238;&#235;-&#226;&#238; &#241;&#242;&#238;&#239;&#238;&#226;&#251;&#245; &#225;&#232;&#242; 0 =&#238;&#228;&#232;&#237;, 1=&#239;&#238;&#235;&#242;&#238;&#240;&#224; , 2=&#228;&#226;&#224;
dwBaudRate - &#209;&#234;&#238;&#240;&#238;&#241;&#242;&#252; &#238;&#225;&#236;&#229;&#237;&#224; 4800, 9600, 19200, 38400, 57600, 115200 &#225;&#232;&#242;/&#241;.
dwBufferSize - &#205;&#229; &#232;&#241;&#239;&#238;&#235;&#252;&#231;&#243;&#229;&#242;&#241;&#255; &#228;&#238;&#235;&#230;&#237;&#238; &#225;&#251;&#242;&#252; =0
dwScan - &#205;&#229; &#232;&#241;&#239;&#238;&#235;&#252;&#231;&#243;&#229;&#242;&#241;&#255; &#228;&#238;&#235;&#230;&#237;&#238; &#225;&#251;&#242;&#252; =0
dwTimeout - &#205;&#229; &#232;&#241;&#239;&#238;&#235;&#252;&#231;&#243;&#229;&#242;&#241;&#255; &#228;&#238;&#235;&#230;&#237;&#238; &#225;&#251;&#242;&#252; =0
Port - &#205;&#238;&#236;&#229;&#240; &#239;&#238;&#240;&#242;&#224; &#226; &#244;&#238;&#240;&#236;&#224;&#242;&#229; &#239;&#229;&#240;&#229;&#247;&#232;&#241;&#235;&#229;&#237;&#232;&#255; PORTS (com_number)
*)
Settings1.Port:=com_num1;
Settings1.dwBaudRate:=115200;
Settings1.byParity:=0;
Settings1.dwTimeout:=0;
Settings1.byStopBits:=0;
Settings1.dwBufferSize:=0;
Settings1.dwScan:=0;
(*
&#205;&#224;&#241;&#242;&#240;&#238;&#233;&#234;&#224; &#232; &#238;&#242;&#234;&#240;&#251;&#242;&#232;&#229; &#239;&#238;&#240;&#242;&#224; (COM_SERVICE)
&#207;&#238;&#234;&#224; &#237;&#224; &#226;&#251;&#245;&#238;&#228;&#229; Ready &#237;&#229; &#225;&#243;&#228;&#229;&#242; &#241;&#232;&#227;&#237;&#224;&#235; TRUE, &#241;&#242;&#243;&#247;&#232;&#236;&#241;&#255; &#226; &#239;&#238;&#240;&#242;
&#207;&#238;&#240;&#242; &#238;&#242;&#234;&#240;&#251;&#226;&#224;&#229;&#242;&#241;&#255; &#241; &#237;&#224;&#241;&#242;&#240;&#238;&#233;&#234;&#224;&#236;&#232;, &#243;&#234;&#224;&#231;&#224;&#237;&#237;&#251;&#236;&#232; &#226; com_settings,
&#224; &#237;&#224; &#226;&#245;&#238;&#228; Task &#239;&#238;&#228;&#224;&#254;&#242;&#241;&#255; &#241;&#235;&#229;&#228;&#243;&#254;&#249;&#232;&#229; &#231;&#237;&#224;&#247;&#229;&#237;&#232;&#255;:

0 - &#206;&#242;&#234;&#240;&#251;&#242;&#232;&#229; &#232; &#237;&#224;&#241;&#242;&#240;&#238;&#233;&#234;&#224; &#239;&#238;&#240;&#242;&#224; &#226; &#241;&#238;&#238;&#242;&#226;&#229;&#242;&#241;&#242;&#226;&#232;&#232; &#241; &#237;&#224;&#241;&#242;&#240;&#238;&#233;&#234;&#224;&#236;&#232; &#226; &#241;&#242;&#240;&#243;&#234;&#242;&#243;&#240;&#229; Settings
1 - &#200;&#231;&#236;&#229;&#237;&#229;&#237;&#232;&#229; &#237;&#224;&#241;&#242;&#240;&#238;&#229;&#234; &#243;&#230;&#229; &#238;&#242;&#234;&#240;&#251;&#242;&#238;&#227;&#238; &#239;&#238;&#240;&#242;&#224; &#237;&#224; &#237;&#224;&#241;&#242;&#240;&#238;&#233;&#234;&#232; &#243;&#234;&#224;&#231;&#224;&#237;&#237;&#251;&#229; &#226; Settings
2 - &#199;&#224;&#234;&#240;&#251;&#242;&#232;&#229; &#239;&#238;&#240;&#242;&#224;, &#237;&#238;&#236;&#229;&#240; &#239;&#238;&#240;&#242;&#224; &#225;&#229;&#240;&#229;&#242;&#241;&#255; &#232;&#231; Settings.Port
*)
COM_SERVICE1(Enable:=TRUE , Settings:=Settings1 , Task:=OPEN_TSK );
IF COM_SERVICE1.Ready THEN
COM_SERVICE1.Enable := FALSE;
com_ready1:=TRUE;
END_IF
END_IF


(************************************************* ******
&#208;&#224;&#225;&#238;&#242;&#224; &#241; &#239;&#238;&#240;&#242;&#238;&#236;
************************************************** *****)

IF (com_ready1=TRUE )THEN
CASE mtype OF
0:
IF NOT UNI.Active THEN
buffer[0] := 1;
buffer[1] := 101;
buffer[2] := 12;
buffer[3] := 0;
buffer[4] := 1;
buffer[5] := 0;
buffer[6] := 0;
buffer[7] := 0;
buffer[8] := 0;
buffer[9] := 0;
buffer[10] := 16#F4;
buffer[11] := 16#1;
buffer[12] := 0;
buffer[13] := 0;
buffer[14] := 0;
crc := MB_CRC(ADR(buffer[0]), 15);
SysMemCpy(ADR(buffer[15]), ADR(crc), 2);
(* buffer[15] := 145;
buffer[16] := 185; *)
data_size := 17;
END_IF
UNI(
Mode:=mode ,
ComHandle:=com_num1,
TimeOut:=t_zad,
DataBuf := buffer,
DataSize := data_size,
Complete := compl,
Exception := except
);

RT(CLK:=compl);

IF rt.Q THEN
MB_MOVED(ADR(result[0]), ADR(buffer[0]), data_size+2);
IF result[3] = 0 THEN
mtype :=1;
END_IF
END_IF

1:
IF NOT UNI.Active THEN
buffer[0] := 1;
buffer[1] := 16#67;
buffer[2] := 0;
crc1 := MB_CRC(ADR(buffer[0]), 3);
SysMemCpy(ADR(buffer[3]), ADR(crc1), 2);
(* buffer[15] := 145;
buffer[16] := 185; *)
data_size := 5;
END_IF
UNI(
Mode:=mode ,
ComHandle:=com_num1,
TimeOut:=t_zad,
DataBuf := buffer,
DataSize := data_size,
Complete := compl,
Exception := except
);

RT(CLK:=compl);

IF rt.Q THEN
MB_MOVED(ADR(id[0]), ADR(buffer[0]), data_size+2);
mtype :=2;
END_IF

2:
IF NOT UNI.Active THEN
buffer[0] := 1;
buffer[1] := 16#6B;
buffer[2] := 0;
crc := MB_CRC(ADR(buffer[0]), 3);
SysMemCpy(ADR(buffer[3]), ADR(crc), 2);
(* buffer[3] := 16#0A;
buffer[4] := 16#30;*)
data_size := 5;
END_IF
UNI(
Mode:=mode ,
ComHandle:=com_num1,
TimeOut:=t_zad,
DataBuf := buffer,
DataSize := data_size,
Complete := compl,
Exception := except
);

RT(CLK:=compl);

IF rt.Q THEN
MB_MOVED(ADR(result[0]), ADR(buffer[0]), data_size+2);
SysMemCpy(ADR(Torque), ADR(result[11]), 4);
SysMemCpy(ADR(Speed), ADR(result[19]), 4);
SysMemCpy(ADR(Temper), ADR(result[15]), 4);
ELSE
IF except <> 0 THEN
Torque := 0;
Speed := 0;
Temper := 0;
END_IF
END_IF
END_CASE
END_IF

В принципе, в Тилкомовской документации приводятся примеры посылаемых команд даже с контрольной суммой, но я посчитал, что будет правильнее считать её. В коде специально оставил закомментированной контрольную сумму из документации.

Yegor
23.06.2016, 20:14
В протоколе действительно зарезервированы номера функций 101-110 и 66-69 (могу ошибаться в диапазоне) для расширений производителя.Когда все стандартные вещи реализуются через расширения это уже сложно назвать модбасом. Как бы ни было, отличная находка, спасибо.

melky
24.06.2016, 12:07
madiggo а вы не могли бы код выложить, чтобы коменты на русском были ? :)

Владимир Ситников
24.06.2016, 12:14
madiggo а вы не могли бы код выложить, чтобы коменты на русском были ? :)

На всякий случай, тут расшифровщик: https://www.artlebedev.ru/tools/decoder/


FUNCTION_BLOCK MB_UNI_IO
VAR_INPUT
Mode: MB_MODE; (* Serial Transmission Mode of MODBUS networks *)
ComHandle:DWORD; (* дескриптор последовательного порта библиотеки SysLibCom *)
TimeOut: TIME; (* время тайм-аута [мс] - макс. задержка на обработку запроса *)
END_VAR
VAR_IN_OUT
Complete: BOOL; (* если = TRUE, то обмен завершен *)
Exception: BYTE; (* исключения протокола MODBUS *)
DataBuf: ARRAY[0..255] OF BYTE; (* буфер данных кадра *)
DataSize: BYTE; (* размер кадра без контрольной суммы *)
END_VAR


PROGRAM PLC_PRG
VAR
(*Пример работы с частотным приводом ОВЕН ПЧВ по RS-485*)
com_num1: PORTS; (*Номер порта на который подключено оборудование*)
com_hndl: DWORD;
Settings1: COMSETTINGS; (*Параметры порта *)
COM_SERVICE1: COM_SERVICE; (*Блок открытия и настройки порта *)
com_ready1: BOOL; (*Статус порта - открыт/закрыт*)
UNI:MB_UNI_IO;
i: BYTE;
command_word:WORD:=16#847C;
RT: R_TRIG;
t_zad:TIME:=T#20ms;
buffer: ARRAY[0..255] OF BYTE;
result: ARRAY[0..255] OF BYTE;
id: ARRAY[0..255] OF BYTE;
mode: MB_MODE := MB_RTU;
data_size: BYTE := 17;
compl: BOOL;
except: BYTE;
port_opened: BOOL;
mtype: INT;
crc: WORD;
crc1: WORD;
END_VAR


(************************************************* ****
Пример работы с библиотекой SysLibCom

формирование команды в двоичном виде
************************************************** ***)


(************************************************* ******
Открываем порт
************************************************** *****)

(*Открываем первый порт*)
IF NOT com_ready1 THEN
(*
Номер порта (com_number):
0 - RS-485
1 - RS-232
4 - RS-232 DEBUG
*)
com_num1 := 0;
(*
Настройки порта (com_settings):
byParity - Режим проверки четности 0 = нет, 1 = нечет, 2 = чет
byStopBits - Кол-во стоповых бит 0 =один, 1=полтора , 2=два
dwBaudRate - Скорость обмена 4800, 9600, 19200, 38400, 57600, 115200 бит/с.
dwBufferSize - Не используется должно быть =0
dwScan - Не используется должно быть =0
dwTimeout - Не используется должно быть =0
Port - Номер порта в формате перечисления PORTS (com_number)
*)
Settings1.Port:=com_num1;
Settings1.dwBaudRate:=115200;
Settings1.byParity:=0;
Settings1.dwTimeout:=0;
Settings1.byStopBits:=0;
Settings1.dwBufferSize:=0;
Settings1.dwScan:=0;
(*
Настройка и открытие порта (COM_SERVICE)
Пока на выходе Ready не будет сигнал TRUE, стучимся в порт
Порт открывается с настройками, указанными в com_settings,
а на вход Task подаются следующие значения:

0 - Открытие и настройка порта в соответствии с настройками в структуре Settings
1 - Изменение настроек уже открытого порта на настройки указанные в Settings
2 - Закрытие порта, номер порта берется из Settings.Port
*)
COM_SERVICE1(Enable:=TRUE , Settings:=Settings1 , Task:=OPEN_TSK );
IF COM_SERVICE1.Ready THEN
COM_SERVICE1.Enable := FALSE;
com_ready1:=TRUE;
END_IF
END_IF


(************************************************* ******
Работа с портом
************************************************** *****)

IF (com_ready1=TRUE )THEN
CASE mtype OF
0:
IF NOT UNI.Active THEN
buffer[0] := 1;
buffer[1] := 101;
buffer[2] := 12;
buffer[3] := 0;
buffer[4] := 1;
buffer[5] := 0;
buffer[6] := 0;
buffer[7] := 0;
buffer[8] := 0;
buffer[9] := 0;
buffer[10] := 16#F4;
buffer[11] := 16#1;
buffer[12] := 0;
buffer[13] := 0;
buffer[14] := 0;
crc := MB_CRC(ADR(buffer[0]), 15);
SysMemCpy(ADR(buffer[15]), ADR(crc), 2);
(* buffer[15] := 145;
buffer[16] := 185; *)
data_size := 17;
END_IF
UNI(
Mode:=mode ,
ComHandle:=com_num1,
TimeOut:=t_zad,
DataBuf := buffer,
DataSize := data_size,
Complete := compl,
Exception := except
);

RT(CLK:=compl);

IF rt.Q THEN
MB_MOVED(ADR(result[0]), ADR(buffer[0]), data_size+2);
IF result[3] = 0 THEN
mtype :=1;
END_IF
END_IF

1:
IF NOT UNI.Active THEN
buffer[0] := 1;
buffer[1] := 16#67;
buffer[2] := 0;
crc1 := MB_CRC(ADR(buffer[0]), 3);
SysMemCpy(ADR(buffer[3]), ADR(crc1), 2);
(* buffer[15] := 145;
buffer[16] := 185; *)
data_size := 5;
END_IF
UNI(
Mode:=mode ,
ComHandle:=com_num1,
TimeOut:=t_zad,
DataBuf := buffer,
DataSize := data_size,
Complete := compl,
Exception := except
);

RT(CLK:=compl);

IF rt.Q THEN
MB_MOVED(ADR(id[0]), ADR(buffer[0]), data_size+2);
mtype :=2;
END_IF

2:
IF NOT UNI.Active THEN
buffer[0] := 1;
buffer[1] := 16#6B;
buffer[2] := 0;
crc := MB_CRC(ADR(buffer[0]), 3);
SysMemCpy(ADR(buffer[3]), ADR(crc), 2);
(* buffer[3] := 16#0A;
buffer[4] := 16#30;*)
data_size := 5;
END_IF
UNI(
Mode:=mode ,
ComHandle:=com_num1,
TimeOut:=t_zad,
DataBuf := buffer,
DataSize := data_size,
Complete := compl,
Exception := except
);

RT(CLK:=compl);

IF rt.Q THEN
MB_MOVED(ADR(result[0]), ADR(buffer[0]), data_size+2);
SysMemCpy(ADR(Torque), ADR(result[11]), 4);
SysMemCpy(ADR(Speed), ADR(result[19]), 4);
SysMemCpy(ADR(Temper), ADR(result[15]), 4);
ELSE
IF except <> 0 THEN
Torque := 0;
Speed := 0;
Temper := 0;
END_IF
END_IF
END_CASE
END_IF

Вольд
24.06.2016, 13:23
Отличная ссылка, спасибо.

voale
03.05.2017, 09:49
Отличная статья, спасибо Yegor

Svorog
08.05.2017, 10:10
Всем доброго дня и с наступающим праздником! спасибо автору действительно отличная статья, но к сожалению в моем случае немного не подходит, не сталкивался ли кто с аналогичной проблемой при использовании СПК? есть ли кого примеры использования библиотеки CAA SerialCom?
У меня стоит задача подключение стороннего цифрового датчика с протоколом обмена UAI к СПК107, датчик кстати отечественный, однако заводом предусмотрено подключение только к ПК с помощью специального преобразователя, как поделилась тех поддержка производителя обмен происходит по RS232 и в качестве примера выслал несколько команд, меня собственно тоже интересует открытие порта в момент включения и съем показаний пока не снимется питание.

Командами 37 — чтение и 38 — запись
пример чтения параметров
запрос «:01;37;00001E;41405»
ответ «!1;01;44032»

Буду очень признателен за любую информацию по теме.

Евгений Кислов
08.05.2017, 10:21
Всем доброго дня и с наступающим праздником! спасибо автору действительно отличная статья, но к сожалению в моем случае немного не подходит, не сталкивался ли кто с аналогичной проблемой при использовании СПК? есть ли кого примеры использования библиотеки CAA SerialCom?
У меня стоит задача подключение стороннего цифрового датчика с протоколом обмена UAI к СПК107, датчик кстати отечественный, однако заводом предусмотрено подключение только к ПК с помощью специального преобразователя, как поделилась тех поддержка производителя обмен происходит по RS232 и в качестве примера выслал несколько команд, меня собственно тоже интересует открытие порта в момент включения и съем показаний пока не снимется питание.

Командами 37 — чтение и 38 — запись
пример чтения параметров
запрос «:01;37;00001E;41405»
ответ «!1;01;44032»

Буду очень признателен за любую информацию по теме.

Добрый день. См. документацию:
http://ftp.owen.ru/index.html/CoDeSys3/11_Documentation/01_SPK/SPK_NonstandardProtocols_v.1.0.pdf

Svorog
08.05.2017, 10:35
Спасибо, этот документ читал, но к сожалению, ввиду малого опыта, многое моменты остались непонятны. Может есть живой пример на CFC?

Евгений Кислов
08.05.2017, 10:47
Спасибо, этот документ читал, но к сожалению, ввиду малого опыта, многое моменты остались непонятны. Может есть живой пример на CFC?

Графические языки практически не используются для реализации обмена - поскольку в них сложно реализовывать ветвление, циклы и т.д.

Safron
02.09.2017, 20:20
Всем доброй ночи! Разбираюсь как работать с библиотекой Syslibcom...Возникли вопросы некоторые скорее касательно настройки портов. Задача состоит в том , чтобы считать через один интерфейс RS232 ПЛК 100 с одного дивайса данные (весовой терминал со своим протоколом) и оправить через другой RS232 на ПК.
1) Непонятно как при этом настраивать параметры портов в ПЛК Конфигурации???...и нужно вообще их там указывать???? потому как уже на программном уровне задаются параметры скорости и .д. ???
2) Какой из двух интерфейсов RS232 есть первый порт, а который второй?
Спасибо!!

Евгений Кислов
02.09.2017, 20:28
1) При работе с портом через библиотеку - в конфигурации его добавлять не нужно.
2) RS-232 - COM1, RS-232 Debug - COM4

Safron
03.09.2017, 14:53
Спасибо.
У меня ещё вопрос. В программах полезности, пример работы с COMSERICE есть такой фрагмент..

(*Вызываем таймер ожидания ответа*)
T1();
(*В переменную byte_read мы получаем число принятых байт при чтении порта*)
byte_read:=SysComRead(port_number, ADR(buf_otvet), 8, 0);
(*Если мы получили ответ от устройства, то принятую информацию собираем в более большой буфер ответа
то есть собираем ответ в удобный для обработки формат*)
IF byte_read>0 THEN
FOR i:=0 TO byte_read-1 DO
otvet[l+i]:=buf_otvet[i];
END_FOR
l:=l+byte_read;

(*В данном месте программы необходимо реализовать обработку принятой из порта информации*)
(*Дабы сильно не перегружать пример информации ниже используется подобие заглушки*)
(*Известно что длина ответа на используемые в программе запросы сторого 8 байт*)
(*Поэтому ниже производится анализ размера принятого ответа*)
(*Как только приходит за отведенный интервал времени нужное количество байт, начинаем обработку принятой команды*)
IF ( l=7) THEN
(*Получили нужное количество байт*)
(*Производим распаковку ответа и записываем его на выход блока*)
p1:=ADR(otvet[4]);
p:=ADR(rez);
p^:=p1^;
p1:=ADR(otvet[3]);
p:=p+1;
p^:=p1^;
(*Останавливаем таймер ожидания ответа*)
T1(IN:=FALSE, PT:=T#0s );
(*Ошибок нет*)
errors:=0;
(*Значиния на выходе блока сформированы, их можно использовать в программе*)
ready:=TRUE;
(*Переводим блок в режим отправки запроса*)
status:=1;
END_IF

Вопрос по таймеру Т1: Почему вызов таймера написан просто Т1(); без указания времени и переменной на входе...например Т1(in:=TRUE, PT:=#1s);
Но при этом подразумевается, что этот таймер был включен (шёл отсчёт времени), до тех пор пока не поступила команда
(*Останавливаем таймер ожидания ответа*)
T1(IN:=FALSE, PT:=T#0s );
Мне как то странно ..Неужто, если я просто вызову Т1(); то он у меня сразу запустится без подачи команды на вход???????

Евгений Кислов
03.09.2017, 15:50
Вероятно, аргументы таймера были заданы где-то выше приведенного кода.

Safron
04.09.2017, 07:06
Нет. Не заданы..в том то и дело..

capzap
04.09.2017, 07:39
Нет. Не заданы..в том то и дело..

только что посмотрел проект, Вы буквально поняли где смотреть, а таймер задается ниже :)
В начали ищите, где разрешение запустить блок, статус принимает значение 1, в кейсе под номером один идет как раз установка параметров для таймера

Safron
04.09.2017, 10:12
Блин.точно. Спасибо.

tomas111
16.02.2018, 16:04
Хорошая статья! Но стал вопрос можно ли просто читать? Точнее как это сделать?
Подключить следующей цепочкой ПК ---rs-232--- ПЛК110 (через программу COM Port Toolkit) отправить с ПК данные и увидеть их на ПЛК110 в режиме реально времени? Или нужно знать конкретный пакет (длина, скорость и т.д.)?

SA104
26.04.2018, 12:47
Доброго времени.
Объясните, уважаемые знатоки обмена по Modbus,
Почему вот здесь на 32 стр. https://cloud.mail.ru/public/JKkZ/bqLHJWaHT авторами протокол называется Modbus RTU, а меня не покидает ощущение,
что это нечто подобное. Иными словами - возможно ли например при помощи СПК107 по Modbus RTU прочитать данные с этого прибора.
К RTU привязался потому, что на порту есть еще другие устройства.

Евгений Кислов
26.04.2018, 12:56
Почему вот здесь на 32 стр. https://cloud.mail.ru/public/JKkZ/bqLHJWaHT авторами протокол называется Modbus RTU

Потому что это протокол Modbus RTU.


возможно ли например при помощи СПК107 по Modbus RTU прочитать данные с этого прибора.

Возможно.

capzap
26.04.2018, 12:57
В приложении 3 расписан modbusASCII, в четвертом RTU и для него всего одна команда запроса, если СПК поддерживает передачу 24 регистров то почему нет

SA104
26.04.2018, 14:12
В приложении 3 расписан modbusASCII, в четвертом RTU и для него всего одна команда запроса, если СПК поддерживает передачу 24 регистров то почему нет

В СПК команду на групповое чтение нашел 0х23 , а в протоколе к тому контроллеру 0х3 указана для группы. И данные не однотипные по этому запросу идут, а вперемешку INT c Float. Если намекнете, как по RTU запросу этот винегрет расхлебать, при условии , что на линии еще другие "нормальные" модули имеются - буду признателен.

capzap
26.04.2018, 14:17
однозначно надо пользоваться третьей функцией, а не 35, именно 0х3 входит в стандартные функции которые есть на любом мастере. Принимайте просто в массив, а потом его через указатель в структуру с соответствующими типами данных или в ручную, если возникнут проблемы с переворачиванием слов/байт

SA104
26.04.2018, 14:34
Просто в массив принимать была мысль, смутило что данные разнотипные в пакете.
Пока под рукой данного девайса нет. Появится - буду пробовать.
Спасибо.

SA104
26.04.2018, 14:35
Потому что это протокол Modbus RTU.



Возможно.

Евгений, не интригуйте.
Намекните, какой Вы способ предлагаете?

Евгений Кислов
26.04.2018, 14:42
Стандартный способ, про который уже написали в #34.

SA104
26.04.2018, 16:36
Всем Спасибо за толчок в нужном направлении.
С виртуальным слейвом сымитировал данный протокол.
Осталось на реальном железе проверить.

SA104
27.04.2018, 05:48
Принимайте просто в массив, а потом его через указатель в структуру с соответствующими типами данных или в ручную, если возникнут проблемы с переворачиванием слов/байт
Не подскажете, где про структуры в Codesys3 можно почитать или ссылка может есть на пример?
В Google искал, на форуме тоже, только по второму кодесису кое-что попадается.

capzap
27.04.2018, 07:26
у меня перед глазами нет третьего КДС, но как обычно в дереве ПКМ -> добавить элемент и выбрать UDT, заполнять как во втором

SA104
27.04.2018, 09:06
Спасибо, попробую разобраться.

capzap
27.04.2018, 09:21
Спасибо, попробую разобраться.

перепутал с семеном, DUT https://help.codesys.com/api-content/2/codesys/3.5.12.0/en/_cds_obj_dut/

SA104
02.05.2018, 07:40
Доброго времени.
Очередной вопрос любителям ребусов.

Рвется отправка строки, если встречается NULL символ - 0х00
Пример взят отсюда http://www.owen.ru/uploads/txtlic.php?url=http://ftp.owen.ru/index.html/CoDeSys3/20_Educational_materials/NonstandardProtocols/Example_MV110_8A_DCON.zip

Все, что изменил - добавил свой массив байт для отправки.
Если в массиве нет 0х00, все ОК отправляется.
Если есть то в процессе формирования буфера отправки сам 0х00 и байты за ним не передаются.
Почему так происходит в принципе понимаю.
Как обойти эту засаду в рамках данного примера?

Евгений Кислов
02.05.2018, 09:39
Вы же собственно сами все написали. DCON - строковый протокол, адрес там тоже в виде ASCII-кодов задается.
Причем ASCII-кодов HEX-символов, т.е. для адреса 255 надо добавить в запрос символы FF - поэтому в исходном примере используется функция BYTE_TO_STRH.
NULL там не может потребоваться в принципе (т.к. число 0 передается в виде ASCII-кода, который естественно отличен от 0x00).

Кроме того, мне не доводилось видеть, чтобы в DCON использовались адреса более 255 (а вы в личку писали - у вас адрес 400).

Поэтому непонятно, что у вас за протокол?
Какое устройство вы пытаетесь опросить?

SA104
02.05.2018, 17:36
Устройство - инвертор. Описание протокола во вложении.
Передачу в принципе поборол. не так красиво, как в примере, но работает.
Теперь основная задача -- как эхо по каждому принятому байту сделать.

deniska13
17.07.2018, 11:34
Расскажу на конкретном примере как быть, когда вам попадается устройство со своим протоколом обмена данными по RS-232/485, и его надо подключить к ПЛК.

0. Как вообще работают с портом

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

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

В итоге для постоянного чтения какого-то параметра вы должны делать примерно так: открыть порт с определёнными параметрами (скорость, чётность и т.д.), отправить запрос, принять ответ, отправить запрос, принять ответ и т.д. пока контроллер не выключат. Конечно, ответ надо ждать. И никто не запрещает ждать его бесконечно, но разумнее спустя какое-то время (таймаут) после предыдущего запроса отправить запрос повторно или ещё как-то обработать ситуацию.

1. Как работают с портом на ПЛК

На ПЛК порт зачастую используется всё время работы, и поэтому его можно вообще не закрывать.

Специфика отправки и получения внешних данных на ПЛК в том, что вам нельзя тормозить саму программу для ожидания, ведь она — не забываем — должна пересчитываться много-много раз в секунду как можно быстрее. Вдобавок не предусматривается способа как-то подписаться на получение данных, чтобы по мере поступления ответа что-то извне вызывало ваши функции обработки или иным образом сообщало о поступлении данных, как это бывает возможно при программировании под современный персональный компьютер. Поэтому на ПЛК после отправки запроса мы сами должны постоянно проверять наличие ответа.

2. Чем работают с портом на ПЛК

Для работы с последовательным портом используется библиотека SysLibCom. В ней нас интересуют 4 функции:



SysComOpen(номер_порта) — открыть порт, то есть выразить системе своё намерение его единолично использовать. По документации эта функция должна возвращать дескриптор порта (билет на применение порта в других функциях). Однако в случае с ПЛК110/160 она возвращает некую ерунду, по которой можно определить, открылся ли порт, а в других функциях вместо дескриптора используется номер порта.
SysComSetSettings(параметры) — устанавливает параметры порта (скорость, чётность и т.д.), по коду возврата можно определить, получилось ли задать параметры;
SysComWrite(дескриптор/номер_порта, указатель_на_буфер, размер_буфера) — эта функция пишет данные из указанного вами буфера в порт и возвращает число, указывающее, сколько байт ей удалось записать в этот раз;
SysComRead(дескриптор/номер_порта, указатель_на_буфер, сколько_прочитать) — эта функция читает данные из порта в указанный вами буфер и возвращает число, указывающее, сколько байт ей удалось прочитать в этот раз.


3. Как организовать последовательный опрос устройства

Чтобы не запутаться, на каком шаге вы остановились, и чтобы вообще «шагать», я рекомендую просто запоминать номер шага одной переменной и менять его по мере прохождения. Можно, конечно, использовать и большее число переменных-признаков, чтобы запомнить различные события вроде «запрос отправлен» и «ответ прочитан», как это делается в других примерах, но я нахожу такой способ сложным в изолировании шагов друг от друга и в добавлении/удалении шагов.

Давайте теперь распишем шагами наш автомат.

Открывать порт. Если открылся, то идём дальше.
Выставлять параметры порта. Если выставились, идём дальше.
Сформировать запрос и идти дальше.
Отправлять запрос. Когда он целиком отправлен, идти дальше.
Принимать ответ. Когда он целиком принят, разобрать его и перейти к шагу 3.


Плюс общее правило: если мы слишком долго стоим на одном шаге, то считать это таймаутом. Прошу заметить, что я использовал несовершенные формы глаголов — открывать, выставлять, отправлять, принимать. Так как мы не тормозим программу, то все эти действия могут выполняться не сразу, особенно приём данных — нет гарантии, что ответ придёт одним куском. Поэтому мы не можем просто сделать, мы будем делать пока не сделаем. Только формирование запроса, как правило, удаётся сделать в один приём.

Такие простые автоматы удобнее всего реализуются конструкцией CASE языка ST. С человеческого переводится почти дословно:
CASE step OF
0:
port := SysComOpen(0);
IF port = удалось_открыть THEN
step := step + 1;
END_IF
1:
IF SysComSetSettings(настройки) = получилось THEN
step := step + 1;
END_IF
2:
buffer[0] := 1; (* адрес устройста *)
buffer[1] := 48; (* код запроса *)
buffer[2] := 9F; (* контрольная сумма *)
step := step + 1;
3:
...
END_IFНу а общее правило о том, чтобы слишком долго не стоять на одном месте, мы можем сделать где-то за пределами этой конструкции.

4. Что за буферы, как отправлять их содержимое и как принимать в них данные

Буфер это временное хранилище данных. Байтовый массив, как правило; буквально ARRAY OF BYTE. Вы в своём буфере будете подготавливать запрос и собирать по кусочкам ответ. Контроллер в своём буфере тоже будет собирать ваши данные для отправки и получения. Так что не ждите, что ваш запрос уйдёт прямо моментально. Да и для различных частей контроллера ваша программа тоже не моментальна, оттого данные и приходится буферизовать, чтобы более равномерно их обрабатывать.

В предыдущем пункте было упомянуто, что данные могут не уйти и не прийти одним куском. А если их много, то почти наверняка получится несколько фрагментов. Поэтому функции SysComWrite и SysComRead написаны так, чтобы можно было легко скользить по буферу и даже стоять в нём на одном месте. Для этого нужно всего лишь смотреть, сколько байтов реально удалось получить или отправить, а в следующий раз начинать на это же число дальше и просить на это же число меньше. Короче, вот:
count := count + SysComRead(port, ADR(buffer) + count, SIZEOF(buffer) - count);Этой строкой мы заполним буфер от начала до конца вне зависимости от того, каким числом фрагментов нам придёт ответ и сколько фрагментов окажутся пустыми. Пускай буфер у нас 10 байт, и нам идут фрагменты: 4 байта, 0 байт, 6 байт. Сначала прибавилось 4, count стало 4, в следующий скан мы даём адрес больше на 4 и просим меньше на 4, но приходит 0, который сумму не меняет (стоим на месте). И как только count становится равным ожидаемому числу байт, мы можем считать, что получили необходимый объём данных.

5. Тривиальный протокол

Допустим, мы опрашиваем светофор. К нему прилагается спецификация протокола:

В запросе нулевой байт это адрес светофора, первый байт — код функции, второй байт — параметр функции. Есть два кода: 54 — узнать текущий сигнал, 68 — сменить сигнал. Сигналы обозначаются так: 1 — красный, 2 — жёлтый, 4 — зелёный. Ответ светофора на обе функции: свой адрес, код сигнала, контрольная сумма. Контрольная сумма это адрес+код_сигнала. Порт светофора: 19200/8/N/1. Ждать ответа дольше 300 мс не имеет смысла.

Погнали:
PROGRAM PLC_PRG
VAR
step, _step, count: DINT; (* номер шага, номер шага на предыдущем цикле ПЛК, счётчик данных *)
rx_buffer, tx_buffer: ARRAY [0..5] OF BYTE; (* буферы на отправку и на приём, можно обойтись одним *)
timeout: TON := (PT := T#300ms); (* этим таймером будем делать таймауты *)
END_VAR
VAR CONSTANT
port: COMSETTINGS := (Port := COM1, dwBaudRate := 19200);
traffic_light_address: BYTE := 1; (* Адрес светофорного блока *)
END_VAR

CASE step OF

0:
(* Сравнение с нулём вместо захвата
дескриптора потому что Овен *)
IF SysComOpen(port.Port) = 0 THEN
step := step + 1;
END_IF
1:
IF SysComSetSettings(port.Port, ADR(port)) = 0 THEN
step := step + 1;
END_IF
2:
tx_buffer[0] := traffic_light_address; (* Адрес светофора *)
tx_buffer[1] := 54; (* Узнать текущий сигнал *)
tx_buffer[2] := 0; (* Чтение сигнала не требует параметров *)
count := 0;
3:
(* Как правило, короткие запросы уходят одним куском, но мы перестрахуемся *)
count := count + SysComWrite(port.Port, ADR(tx_buffer) + count, 3 - count, 0);
IF count >= 3 THEN
count := 0; (* Обнуляем счётчик для следующего шага *)
step := step + 1;
END_IF
4:
count := count + SysComRead(port.Port, ADR(rx_buffer) + count, 3 - count, 0);
IF count >= 3 THEN
IF rx_buffer[0] = traffic_light_address AND rx_buffer[2] = rx_buffer[1] + rx_buffer[0] THEN
;(* Верный ответ. В rx_buffer[1] лежит код текущего сигнала светофора *)
ELSE
;(* Нам ответил не тот светофор или данные повреждены, хоть их и пришло достаточно *)
END_IF
step := 2; (* Можно и 3, т.к. запрос не меняется *)
END_IF
END_CASE


timeout(IN := step = _step);
IF timeout.Q THEN
(* Мы дольше 300 мс на одном шаге.
Можно попробовать отправить запрос повторно *)
step := 3;
END_IF
_step := step;В принципе, этот код должен быть рабочим. Только проверить его не на чем.

6. Читаемость и отлаживаемость

В нагрузку к тому, что состояние автомата выражается всего одной переменной, которую всегда можно поменять одним движением, получаем ещё возможность проименовать шаги вместо их нумерации. Достаточно ввести тип-перечисление:
TYPE X_STEP : (X_OPEN, X_SETUP, X_MAKE_QRY, X_SEND_QRY, X_RECEIVE_RESPONSE); END_TYPEСоответственно можем написать
VAR
step, _step: X_STEP;
END_VAR

CASE step OF X_OPEN: ... ;
X_SETUP: ... ;
X_MAKE_QRY: ... ;
END_CASEТеперь не надо запоминать номер шагов и переключать их можно тоже по именам: step := X_SEND_QRY.

А в случае неоднократного таймаута вместо очередного запроса можно выставить заведомо некорректный шаг, в котором будет содержаться и последний корректный: step := step + 100. И теперь коды со 100 до 104 будут означать завершение опроса на определённом шаге из-за таймаута. Вдобавок кодесис подсветит такие значения красным, т.к. они не будут входить в диапазон значений типа X_STEP.

7. Оформление в виде функционального блока

У функционального блока не должно быть зависимостей от конкретных данных в проекте, поэтому открывание порта выносится «за скобки». Кроме того, функциональные блоки используются через выходные и выходные переменные. Входными можно назначить такие вещи, как адрес, код функции, код нужного значения и даже активацию блока (вход Enable как в modbus.lib от Овена). На выход можно пустить признак успешности обмена, полученное значение, код ошибки и т.д.

8. Жонглируем байтами

Разумеется, почти любой нетривиальный протокол имеет поля, которые занимают больше одного байта и из буфера их простым "a := b[5]" не вытащить. На помощь приходят два средства: указатели и SysLibMem. Первое я не буду рассматривать из-за ограничений по кратности, которые доставляют много хлопот в данном случае. А вот во втором есть простая и полезная функция SysMemCpy (memory copy). Указываете ей куда, откуда и сколько байт скопировать, и получаете, например, REAL, который лежал в буфере с N по N+3 байты. То есть если спецификация протокола говорит, что в ответе с 5 по 8 байты лежит текущее значение в формате IEEE754, то от вас требуется только это:
VAR
buffer: ARRAY[0..20] OF BYTE;
value: REAL;
END_VAR


SysMemCpy(ADR(value), ADR(buffer) + 5, SIZEOF(value));Ещё бывает порядок байтов, который отличается от нужного вашему процессору. Тогда используем SysMemSwap(адрес_где_перевернуть, сколько_перевернуть, по_сколько_переворачивать). Ещё есть SysMemSet — используется, как правило, для обнуления буфера, но может забить указанный участок памяти и любым другим символом кроме нуля.

9. Реальный протокол

Как-то я столкнулся с тензоусилителем-преобразователем одной белорусской фирмы. По их заявлениям устройство поддерживало протокол modbus. Меня ещё перед заказом смутило то, что я не смог нигде выяснить раскладку modbus-регистров преобразователя. Когда устройство наконец пришло, то из документов я узнал, что из общего с модбасом там только метод вычисления контрольной суммы. Вот ссылка на спецификацию (продублирую вложением): http://tilkom.com/download_files/download/modbus.pdf. Обратите внимание на имя файла и на заголовок в документе. Знающие люди не дадут соврать — в действительности модбасом там не пахнет. С другой стороны, протокол несложный, так что смело расчехляем кодесис. Дальше мои рассуждения во время ознакомления со спецификацией и боевой код из проекта.

Нам требуется только текущее значение усилия, и значит, нас интересуют лишь некоторые функции. После беглого чтения выясняется, что устройству сначала надо подать запрос на начало измерений (код 101), а потом уже запрашивать текущие показания (код 104). Заканчивать измерения (102) и закрывать порт смысла нет, т.к. мы просто читаем текущее показания пока не снимут питание, а дальше хоть трава не расти — и устройство, и порт будут в исходном состоянии при включении.

Запросы и ответы небольшие. Возьмём буфер в 17 байт на отправку запроса и в 32 байта на приём ответа (можно посчитать максимальную длину запроса и ответа и принять их в качестве размеров).

Контрольная сумма вычисляется как в модбасе — слямзим из какого-нибудь примера или переведём с другого языка.

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

Читать также требуется максимально быстро, так что не будем делать планирование по таймеру. Да и если бы не требовалось, всё равно проще запрашивать сразу после получения ответа, чем выжидать интервал.

Для достижения максимальной частоты опроса попробуем избавиться от холостого цикла, который возникает при использовании CASE-автомата, т.е. когда ответ уже принят, но запрос будет отправлен только на следующем цикле ПЛК. Можно обернуть CASE-блок в цикл REPEAT, чтобы получить возможность прогнать CASE повторно выставлением признака для UNTIL. Чтобы не начудить и не скатиться в бесконечный повтор, сделаем признак по умолчанию запрещающим повтор. В итоге этот подход даст прирост со 180 до 220 чтений в секунду.

Начинаем (ш)кодить. Перепишем коды запросов в перечислимый тип:
TYPE TLK_CMD : (
READ_VERSION := 66, GET_CURRENT_TIME, SET_CURRENT_TIME, GET_MESSAGE,


START_MEASURING := 101, STOP_MEASURING, GET_ID, READ_BASE,
READ_SPEED, READ_TEMP, READ_COMPLEX, STREAM_BASE, SET_PARAM
);
END_TYPEЗачем переписал все, если нужны только два — не знаю. Не важно, не мешают. Пропишем шаги для автомата (на самом деле постепенно добавлялись):
TYPE TLK_XSTATE : (
TLKX_IDLE, (* бездействие *)
TLKX_START_QRY, (* запрос на начало измерений *)
TLKX_START_ACK, (* приём ответа на запрос начала измерений *)
TLKX_VALUE_QRY, (* запрос на текущее показание *)
TLKX_VALUE_RSP, (* приём текущего показания *)
TLKX_SUCCESS, (* здесь побудем для индикации успешного обмена, пускай все знают *)
TLKX_STOP_QRY,
TLKX_STOP_ACK,
TLKX_ERROR
);
END_TYPEЕщё хочется специализированный таймер, а то надоело вводить дополнительные переменные для TON. Назовём его STATE_TIMER, пускай он считает заново при любом изменении входного значения:
FUNCTION_BLOCK STATE_TIMER
VAR_INPUT
STATE: DWORD;
END_VAR
VAR_OUTPUT
ET: TIME; (* ET grows until STATE is changed, which causes it to reset to T#0s *)
END_VAR
VAR
timer: TON;
_STATE: DWORD;
END_VAR

timer(IN := STATE = _STATE, ET => ET, PT := DWORD_TO_TIME(NOT 0));
_STATE := STATE;Где у нас там был чужой код для расчёта контрольной суммы из модбаса? Причешем его чуть-чуть, потестируем, и получим функцию:
FUNCTION CRC16 : WORD
VAR_INPUT
data_ptr: POINTER TO BYTE;
length: UDINT;
END_VAR
VAR
iter: UDINT;
table_idx: BYTE;
END_VAR
VAR CONSTANT
C_LOOKUP: ARRAY [0..255] OF WORD :=
16#0000, 16#C0C1, 16#C181, 16#0140, 16#C301, 16#03C0, 16#0280, 16#C241,
16#C601, 16#06C0, 16#0780, 16#C741, 16#0500, 16#C5C1, 16#C481, 16#0440,
16#CC01, 16#0CC0, 16#0D80, 16#CD41, 16#0F00, 16#CFC1, 16#CE81, 16#0E40,
16#0A00, 16#CAC1, 16#CB81, 16#0B40, 16#C901, 16#09C0, 16#0880, 16#C841,
16#D801, 16#18C0, 16#1980, 16#D941, 16#1B00, 16#DBC1, 16#DA81, 16#1A40,
16#1E00, 16#DEC1, 16#DF81, 16#1F40, 16#DD01, 16#1DC0, 16#1C80, 16#DC41,
16#1400, 16#D4C1, 16#D581, 16#1540, 16#D701, 16#17C0, 16#1680, 16#D641,
16#D201, 16#12C0, 16#1380, 16#D341, 16#1100, 16#D1C1, 16#D081, 16#1040,
16#F001, 16#30C0, 16#3180, 16#F141, 16#3300, 16#F3C1, 16#F281, 16#3240,
16#3600, 16#F6C1, 16#F781, 16#3740, 16#F501, 16#35C0, 16#3480, 16#F441,
16#3C00, 16#FCC1, 16#FD81, 16#3D40, 16#FF01, 16#3FC0, 16#3E80, 16#FE41,
16#FA01, 16#3AC0, 16#3B80, 16#FB41, 16#3900, 16#F9C1, 16#F881, 16#3840,
16#2800, 16#E8C1, 16#E981, 16#2940, 16#EB01, 16#2BC0, 16#2A80, 16#EA41,
16#EE01, 16#2EC0, 16#2F80, 16#EF41, 16#2D00, 16#EDC1, 16#EC81, 16#2C40,
16#E401, 16#24C0, 16#2580, 16#E541, 16#2700, 16#E7C1, 16#E681, 16#2640,
16#2200, 16#E2C1, 16#E381, 16#2340, 16#E101, 16#21C0, 16#2080, 16#E041,
16#A001, 16#60C0, 16#6180, 16#A141, 16#6300, 16#A3C1, 16#A281, 16#6240,
16#6600, 16#A6C1, 16#A781, 16#6740, 16#A501, 16#65C0, 16#6480, 16#A441,
16#6C00, 16#ACC1, 16#AD81, 16#6D40, 16#AF01, 16#6FC0, 16#6E80, 16#AE41,
16#AA01, 16#6AC0, 16#6B80, 16#AB41, 16#6900, 16#A9C1, 16#A881, 16#6840,
16#7800, 16#B8C1, 16#B981, 16#7940, 16#BB01, 16#7BC0, 16#7A80, 16#BA41,
16#BE01, 16#7EC0, 16#7F80, 16#BF41, 16#7D00, 16#BDC1, 16#BC81, 16#7C40,
16#B401, 16#74C0, 16#7580, 16#B541, 16#7700, 16#B7C1, 16#B681, 16#7640,
16#7200, 16#B2C1, 16#B381, 16#7340, 16#B101, 16#71C0, 16#7080, 16#B041,
16#5000, 16#90C1, 16#9181, 16#5140, 16#9301, 16#53C0, 16#5280, 16#9241,
16#9601, 16#56C0, 16#5780, 16#9741, 16#5500, 16#95C1, 16#9481, 16#5440,
16#9C01, 16#5CC0, 16#5D80, 16#9D41, 16#5F00, 16#9FC1, 16#9E81, 16#5E40,
16#5A00, 16#9AC1, 16#9B81, 16#5B40, 16#9901, 16#59C0, 16#5880, 16#9841,
16#8801, 16#48C0, 16#4980, 16#8941, 16#4B00, 16#8BC1, 16#8A81, 16#4A40,
16#4E00, 16#8EC1, 16#8F81, 16#4F40, 16#8D01, 16#4DC0, 16#4C80, 16#8C41,
16#4400, 16#84C1, 16#8581, 16#4540, 16#8701, 16#47C0, 16#4680, 16#8641,
16#8201, 16#42C0, 16#4380, 16#8341, 16#4100, 16#81C1, 16#8081, 16#4040;
END_VAR

CRC16 := NOT 0;


FOR iter := 0 TO length - 1 DO
table_idx := WORD_TO_BYTE(CRC16 XOR data_ptr^);
CRC16 := SHR(CRC16, 8);
CRC16 := CRC16 XOR C_LOOKUP[table_idx];
data_ptr := data_ptr + 1;
END_FORТут важен подход — сам я это с нуля не писал и никому не советую. Учимся гуглить и подсматривать.

Наконец есть всё, чтобы реализовать опрос. Шутка. На самом деле опрос этого конкретного устройства сначала выглядел как опрос светофора в п. 5, а уже потом код оброс плюшками. Барабанная дробь...
FUNCTION_BLOCK TLK_READER (* Считывает показания тензопреобразователей ООО Тилком (Белоруссия) *)VAR_INPUT
enable: BOOL; (* Пуск/остановка передатчика *)
proceed: BOOL; (* Разрешение на отправку очередного запроса *)
port_handle: DWORD; (* Дескриптор открытого и настроенного порта *)
address: BYTE; (* Адрес устройства *)
timeout: TIME := T#300ms; (* Таймаут ожидания ответа *)
END_VAR
VAR_OUTPUT
value: REAL; (* Измеренное значение в заданных единицах *)
done: BOOL; (* Признак завершения обмена *)
success: BOOL; (* Признак успешного обмена *)
END_VAR
VAR
poller_state: TLK_XSTATE; (* Состояние опрашивающего автомата *)
pstate_timer: STATE_TIMER; (* Секундомер состояний автомата *)
starter: R_TRIG; (* Пускатель автомата *)
stopper: F_TRIG; (* Стоп автомата *)
cmd_buf: ARRAY [0..16] OF BYTE; (* Буфер формирования команд к отправке *)
rcv_buf: ARRAY[0..31] OF BYTE; (* Буфер получения данных *)
i, valid_frame_count, timeout_count: DINT;
crc: WORD;
rpt: BOOL;
END_VAR

starter(CLK := enable);
stopper(CLK := enable);


IF starter.Q THEN (* 'enable' rising edge *)
COM_DISCARD(port_handle);
poller_state := TLKX_START_QRY;
END_IF


IF stopper.Q THEN (* 'enable' falling edge *)
COM_DISCARD(port_handle);
poller_state := TLKX_STOP_QRY;
END_IF


REPEAT
rpt := FALSE;
CASE poller_state OF


TLKX_START_QRY:
(* Заполнение командного пакета StartMeasuring *)
SysMemSet(ADR(cmd_buf), 0, SIZEOF(cmd_buf));
cmd_buf[0] := address;
cmd_buf[1] := INT_TO_BYTE(START_MEASURING);
cmd_buf[2] := 16#C;
cmd_buf[4] := 16#1;
cmd_buf[10] := 16#E8;
cmd_buf[11] := 16#03;
crc := CRC16(ADR(cmd_buf), 15);
cmd_buf[15] := WORD_TO_BYTE(crc);
cmd_buf[16] := WORD_TO_BYTE(SHR(crc, 8));
(* ----->8----- *)
SysComWrite(port_handle, ADR(cmd_buf), 17, 0);
poller_state := TLKX_START_ACK;
i := 0;


TLKX_START_ACK: (* Receive StartMeasuring ACK *)
IF i < 3 THEN
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, 3 - i, 0);
ELSE
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, rcv_buf[2] - i + 5, 0);
IF i >= rcv_buf[2] + 5 AND pstate_timer.ET > T#200ms THEN
poller_state := TLKX_VALUE_QRY;
END_IF
END_IF
IF pstate_timer.ET > timeout THEN
poller_state := TLKX_START_QRY;
timeout_count := timeout_count + 1;
END_IF

TLKX_VALUE_QRY:
(* Заполнение командного пакета ReadBase *)
SysMemSet(ADR(cmd_buf), 0, SIZEOF(cmd_buf));
cmd_buf[0] := address;
cmd_buf[1] := INT_TO_BYTE(READ_BASE);
crc := CRC16(ADR(cmd_buf), 3);
cmd_buf[3] := WORD_TO_BYTE(crc);
cmd_buf[4] := WORD_TO_BYTE(SHR(crc, 8));
(* ----->8----- *)
IF proceed THEN
SysComWrite(port_handle, ADR(cmd_buf), 5, 0);
poller_state := TLKX_VALUE_RSP;
i := 0;
END_IF


TLKX_VALUE_RSP: (* Receive ReadBase response *)
IF i < 3 THEN
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, 3 - i, 0);
ELSE
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, rcv_buf[2] - i + 5, 0);
IF i >= rcv_buf[2] + 5 AND i >= 17 THEN
poller_state := TLKX_SUCCESS;
valid_frame_count := valid_frame_count + 1;
SysMemCpy(ADR(value), ADR(rcv_buf) + 11, SIZEOF(value));
END_IF
END_IF
IF pstate_timer.ET > timeout THEN
poller_state := TLKX_START_QRY;
timeout_count := timeout_count + 1;
END_IF

TLKX_SUCCESS:
IF proceed THEN
poller_state := TLKX_VALUE_QRY;
rpt := TRUE;
END_IF

TLKX_STOP_QRY:
(* Заполнение командного пакета *)
SysMemSet(ADR(cmd_buf), 0, SIZEOF(cmd_buf));
cmd_buf[0] := address;
cmd_buf[1] := INT_TO_BYTE(STOP_MEASURING);
crc := CRC16(ADR(cmd_buf), 3);
cmd_buf[3] := WORD_TO_BYTE(crc);
cmd_buf[4] := WORD_TO_BYTE(SHR(crc, 8));
(* ----->8----- *)
SysComWrite(port_handle, ADR(cmd_buf), 5, 0);
poller_state := TLKX_STOP_ACK;
i := 0;


TLKX_STOP_ACK:
IF i < 3 THEN
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, 3 - i, 0);
ELSE
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, rcv_buf[2] - i + 5, 0);
IF i >= rcv_buf[2] + 5 AND pstate_timer.ET > T#200ms THEN
poller_state := TLKX_IDLE;
END_IF
END_IF


TLKX_ERROR:
;

END_CASE
UNTIL NOT rpt END_REPEAT


pstate_timer(STATE := poller_state);
Из неописанного ранее здесь только чистка порта и двухэтапное чтение ответа. Первое, может, и не сильно надо, а вот второе давайте обсудим:
IF i < 3 THEN
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, 3 - i, 0);
ELSE
i := i + SysComRead(port_handle, ADR(rcv_buf) + i, rcv_buf[2] - i + 5, 0);
IF i >= rcv_buf[2] + 5 AND i >= 17 THEN
poller_state := TLKX_SUCCESS;
valid_frame_count := valid_frame_count + 1;
SysMemCpy(ADR(value), ADR(rcv_buf) + 11, SIZEOF(value));
END_IF
END_IFВ тривиальном примере мы заранее знали, сколько байт в ответе. В этом реальном протоколе длина ответа заранее неизвестна, но указывается неподалёку от начала ответа — в третьем байте. Поэтому сначала мы читаем до третьего байта, а после него уже становится ясно, сколько нужно прочитать ещё. Можно сделать иначе — сколько получится, а потом уже разбираться, но это мне кажется неряшливым.

Можно видеть, как переменная rpt делается TRUE, чтобы из состояния TLKX_SUCCESS тут же перейти в состояние TLKX_VALUE_QRY. Именно это позволило разогнаться со 180 до 220 чтений в секунду. Есть потенциал для дальнейшего разгона, но по-видимому, тензопреобразователь уже начинает запинаться. Для наблюдения введены переменные для подсчета успешных чтений и неуспешных.

Вызывается этот ФБ следующим образом:
VAR
tenso_port_opener: COM_OPENER := (
PORT := C_TENSO_COM_PORT,
CFG := (dwBaudRate := C_TENSO_COM_BDRATE));

tenso_reader: TLK_READER := (
address := C_TENSO_ADDR,
enable := TRUE, proceed := TRUE);
END_VAR
VAR CONSTANT
(* Параметры порта тензометра *)
C_TENSO_COM_PORT: DWORD := 0; (* RS-485 *)
C_TENSO_COM_BDRATE: DWORD := 115200;
C_TENSO_ADDR: BYTE := 1;
END_VAR

tenso_port_opener();
IF tenso_port_opener.READY THEN
tenso_reader(port_handle := tenso_port_opener.HANDLE, value => tension);
END_IF

Добрый день!
Может Вы мне сможете подсказать, как решить мою задачу. Есть ПЛК находится в одной локальной сети в ПК к которому подключен преобразователь Ethernet-COM (MOXA) - 4 СОМ порта в Ethernet. К двум портам этого MOXA подключены датчики, которые циклично, без остановки, шлют в порт (у каждого датчика свой порт) данные в виде FF FF 75 2A 4B 9C (3 слова), где FF FF - это начало получаемых данных, 75 2A - собственно само показание датчика и 4B 9C - текущее напряжение питания. На этом ПК установлена программка для эмуляции двух СОМ портов и самописное приложение, которое читает данные и выводит графики и таблицы. Задача следующая: как можно читать эти данные при помощи ПЛК?

ВалераМ
07.04.2020, 06:13
Всем доброго времени суток! Может всё таки найдется человечек, который эти библиотеки использует в проектах на CFC? Понимаю что имел ввиду Евгений Кислов, о том что в графических языках сложно реализовывать ветвления, циклы и т.д. Но как же быть если ST прям очень сложно дается и даже вовсе не дается:(. А на CFC проекты любой сложности и объема даются легко, но скорость через конфигуратор, не дает еще больше расширять проекты. Закиньте добрые люди проектик с использованием библиотек на CFC наверняка хоть у кого то есть.

metrolog_77
01.05.2020, 12:08
Здравствуйте! Есть весовой терминал ХК3118-Т1 фирмы Keli. Нужно считать данные, нашел описание обмена.Знающие люди дайте направление как это лучше сделать.
Спасибо.

melky
01.05.2020, 12:17
metrolog_77 - это не описание протокола, а описание работы с платной библиотекой работы с весовым терминалом.

metrolog_77
01.05.2020, 13:36
понятно, попробую производителю написать ,насчет протокола

Филоненко Владислав
01.05.2020, 17:08
Всем доброго времени суток! Может всё таки найдется человечек, который эти библиотеки использует в проектах на CFC? Понимаю что имел ввиду Евгений Кислов, о том что в графических языках сложно реализовывать ветвления, циклы и т.д. Но как же быть если ST прям очень сложно дается и даже вовсе не дается:(. А на CFC проекты любой сложности и объема даются легко, но скорость через конфигуратор, не дает еще больше расширять проекты. Закиньте добрые люди проектик с использованием библиотек на CFC наверняка хоть у кого то есть.

Вы думаете на CFC оно летать будет? CFC не для низкоуровневых операций и весьма медленный язык.

zelenbii
04.04.2021, 12:27
Здравствуйте, есть несколько вопросов в применении syslibcom для плк110-60м м2. прошивка таргет- последние на момент поста.
1. Более менее стабильной работы удалось добиться при следующем алгоритме работы - "открыл порт"->"загрузил конфигурацию"->"отправил запрос"->"получил ответ"->"закрыл порт". и так для каждого запроса. В требуемой реализации не требуется 24/7 трындеть с портом, но что то мне подсказывает что так работать не совсем верно.в вариациях без закрытия порта порт просто переставал откликаться в какие то моменты(может и я кривой а может и софт, помимо адресатов на порт заодно цеплялся терминалом чтобы понимать кто у меня там шалить начинает).
2. Что происходит с аппаратным буфером после закрытия порта? надо ли его вычитывать после открытия порта?
3. Бибилиотека comservice я так понимаю работает через syslibcom и какого-то особенного профита в ее использовании нет.
4. вопрос немного не по теме, но все же.- какого черта плк решил ни с чего взять и поменять себе айпишник на езернет порту? с 10.0.0.6 на 10.2.11.119, думал плк совсем ласты склеил но дебаг-232 из комплекта помог понять что произошло.

Филоненко Владислав
05.04.2021, 08:56
2. Очищается
4. Файл настроек IP был удалён (разными методами) и ПЛК перешёл в аварийный заводской режим с резервным IP

GDFR
29.03.2022, 21:37
Добрый день! Прошу помощи, есть расходомер СНТ с нестандартным протоколом, может кто-нибудь может дать какие-либо пояснения по поводу работы с ним. Описание прилагаю.

ВладОвен
11.08.2022, 12:39
Привет.
В проекте 2 устройства, которые подключаются к ПЛК.
Первое устройство подключается по ModBus. В проекте настраивается порт и CodeSys автоматически, циклически вычитывает оперативные параметры.
Второе устройство имеет нестандартный протокол.
Смогут ли эти устройства работать на одном порту?
Смогут ли как-то делить шину RS-485 самостоятельно?

imaex
14.08.2022, 07:20
Привет.
Второе устройство имеет нестандартный протокол.
Смогут ли эти устройства работать на одном порту?
Смогут ли как-то делить шину RS-485 самостоятельно?

Если у 2-го устройства на физическом уровне стандартный 485-ый, то смогут, скорее всего. Во всяком случае приборы "Логики" на общей шине с прочими устройствами нормально сосуществуют.

Правда, пассажа про "один порт" я не понял.

melky
14.08.2022, 15:58
ВладОвен если Modbus через конфигурацию, то не смогут. Если через библиотеки, то вполне смогут, так как опрос будет последовательный.

kondor3000
17.08.2022, 10:39
Привет.
В проекте 2 устройства, которые подключаются к ПЛК.
Первое устройство подключается по ModBus. В проекте настраивается порт и CodeSys автоматически, циклически вычитывает оперативные параметры.
Второе устройство имеет нестандартный протокол.
Смогут ли эти устройства работать на одном порту?
Смогут ли как-то делить шину RS-485 самостоятельно?

Провёл эксперимент, даже два.
Опрос через биб-ки, таргет можно заменить на любой другой. Пробовал делать опрос и через ФБ и через две программы.
1) Подключил по одному порту 485, приборы по протоколу Модбас RTU ( 12 канальный Термодат, адрес 32) и протоколу Овен (МВ110-8А, адрес 16), параметры 115200, 8N1 у обоих приборов. По отдельности опрос работает, вместе нет. Протокол Модбас затыкает Овен.
Работает Термодат, опрос МВ110-8а отключён 62155 Работает опрос МВ110-8а 62156 Вместе не работают 62157
Файл проверки прилагаю

2)Второй, один прибор по Модбас, второй через нестандартный протокол Тензо-М. По отдельности работают, месте нет. Скины делать не стал.

Николай Суриков
17.08.2022, 11:06
Конечно не будут, вы же паралельно опрашиваете оба прибора сразу.
Открыли порт - опросили МВ110 - закрыли порт - открыли порт (даже с другой скоростью) - опросили Термодат - закрыли порт - goto шаг 1

melky
17.08.2022, 11:45
kondor3000 совершенно верно вам заметили, обязательно делать последовательный опрос, иначе если у вас параллельно работают бибки, то это монописуально, что Modbus на конфигурации а еще библиотекой опрашиваете другое.

kondor3000
17.08.2022, 13:27
Так а смысл то какой? Если читать не постоянно, а например по 5 секунд по очереди, пока читаешь один, на другом уже авария может быть.
Кроме того если постоянно открывать, закрывать порт, на сколько времени хватит?

Николай Суриков
17.08.2022, 14:36
Читайте раз в 0,1 секунду если нужна скорость. Если у вас микросекундные процессы, то точно не стоит выбирать МВ110 (время опроса одного входа с термосопротивлением 0,9 секунды, 8 входов измеряются за 7 секунд) читать чаще нет смысла.
Порт не сокеты. Да, вы неправильно понимаете.
Можно не переоткрывать порт перед чтением следующего прибора если они работают на одинаковых настройках скорости, четности и т.д.

melky
17.08.2022, 19:58
kondor3000 У вас опросы должны быть последовательными, Modbus опросил, передал другому, другой опросил, вернул опять на опрос Modbus и так по кругу.
Не пытайтесь одновременно опрашивать, будет мусор или для одного или для другого...

Alex_A
26.07.2023, 14:34
В попытке подружить ПЛК110-60 М02 и прибор с нестандартным протоколом по 485 порту наткнулся на эту тему

В тестовый проект перенес наработки ув. автора, но при компиляции ругается на несуществующий тип COM_OPENER , я так понимаю чего то не хватает , еще какая то библиотека была ?
кто нибудь может пособобить где мне её найти ?

kondor3000
26.07.2023, 16:16
В попытке подружить ПЛК110-60 М02 и прибор с нестандартным протоколом по 485 порту наткнулся на эту тему

В тестовый проект перенес наработки ув. автора, но при компиляции ругается на несуществующий тип COM_OPENER , я так понимаю чего то не хватает , еще какая то библиотека была ?
кто нибудь может пособобить где мне её найти ?

Проект выложите.

Alex_A
27.07.2023, 16:27
Вроде получилось законектиться,переписал часть под SYSLIBCOM, но спасибо.

Чувствую что вопросов будет много, впервые к SYSLIBCOM притрагиваюсь