Расскажу на конкретном примере как быть, когда вам попадается устройство со своим протоколом обмена данными по 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 написаны так, чтобы можно было легко скользить по буферу и даже стоять в нём на одном месте. Для этого нужно всего лишь смотреть, сколько байтов реально удалось получить или отправить, а в следующий раз начинать на это же число дальше и просить на это же число меньше. Короче, вот:Этой строкой мы заполним буфер от начала до конца вне зависимости от того, каким числом фрагментов нам придёт ответ и сколько фрагментов окажутся пустыми. Пускай буфер у нас 10 байт, и нам идут фрагменты: 4 байта, 0 байт, 6 байт. Сначала прибавилось 4, count стало 4, в следующий скан мы даём адрес больше на 4 и просим меньше на 4, но приходит 0, который сумму не меняет (стоим на месте). И как только count становится равным ожидаемому числу байт, мы можем считать, что получили необходимый объём данных.Код:count := count + SysComRead(port, ADR(buffer) + count, SIZEOF(buffer) - 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Теперь не надо запоминать номер шагов и переключать их можно тоже по именам: step := X_SEND_QRY.Код:VAR step, _step: X_STEP; END_VAR CASE step OF X_OPEN: ... ; X_SETUP: ... ; X_MAKE_QRY: ... ; END_CASE
А в случае неоднократного таймаута вместо очередного запроса можно выставить заведомо некорректный шаг, в котором будет содержаться и последний корректный: 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, то от вас требуется только это:Ещё бывает порядок байтов, который отличается от нужного вашему процессору. Тогда используем SysMemSwap(адрес_где_перевернуть, сколько_перевернуть, по_сколько_переворачивать). Ещё есть SysMemSet — используется, как правило, для обнуления буфера, но может забить указанный участок памяти и любым другим символом кроме нуля.Код:VAR buffer: ARRAY[0..20] OF BYTE; value: REAL; END_VAR SysMemCpy(ADR(value), ADR(buffer) + 5, SIZEOF(value));
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Ещё хочется специализированный таймер, а то надоело вводить дополнительные переменные для TON. Назовём его STATE_TIMER, пускай он считает заново при любом изменении входного значения:Код: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Где у нас там был чужой код для расчёта контрольной суммы из модбаса? Причешем его чуть-чуть, потестируем, и получим функцию:Код: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