Страница 1 из 7 123 ... ПоследняяПоследняя
Показано с 1 по 10 из 66

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

  1. #1
    Пользователь
    Регистрация
    13.10.2011
    Адрес
    Златоуст
    Сообщений
    1,021

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

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

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

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

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

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

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

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

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

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

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


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


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

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

    Давайте теперь распишем шагами наш автомат.
    1. Открывать порт. Если открылся, то идём дальше.
    2. Выставлять параметры порта. Если выставились, идём дальше.
    3. Сформировать запрос и идти дальше.
    4. Отправлять запрос. Когда он целиком отправлен, идти дальше.
    5. Принимать ответ. Когда он целиком принят, разобрать его и перейти к шагу 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
    Вложения Вложения
    • Тип файла: pdf modbus.pdf (346.5 Кб, Просмотров: 474)
    Последний раз редактировалось Yegor; 25.10.2015 в 17:43.

  2. #2

    По умолчанию

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

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

  3. #3
    Пользователь
    Регистрация
    13.10.2011
    Адрес
    Златоуст
    Сообщений
    1,021

    По умолчанию

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

  4. #4

    По умолчанию

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

  5. #5
    Пользователь
    Регистрация
    24.07.2012
    Адрес
    Россия
    Сообщений
    1,492

    По умолчанию

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

  6. #6
    Пользователь
    Регистрация
    30.11.2012
    Адрес
    40RUS
    Сообщений
    317

    По умолчанию

    Респект, отличное изложение. Один вопрос, Yegor, почему вы не используете префиксы в названиях переменных как то предписывает документация CDS?
    Напильник, велосипед, бубен, грабли и костыли - основные инструменты программиста.

  7. #7

    По умолчанию

    Цитата Сообщение от Yegor Посмотреть сообщение
    Я замечал, что иногда она возвращает больше, чем просили.
    Это ничего, главное чтобы SysComWrite() не вернула больше, чем просят ))

  8. #8
    Пользователь
    Регистрация
    13.10.2011
    Адрес
    Златоуст
    Сообщений
    1,021

    По умолчанию

    Один вопрос, Yegor, почему вы не используете префиксы в названиях переменных как то предписывает документация CDS?
    Рекомендует, а не предписывает (2.3). Standard.lib и другие самые часто используемые библиотеки не следуют этой рекомендации. Следовательно, венгерская нотация остается на откуп конечному программисту. Ну а раз так, то я волен её не глаголИспользовать.

  9. #9

    По умолчанию

    Цитата Сообщение от rapucha Посмотреть сообщение
    Йоу!
    Овеновский отдел тех. писателей разогнать, написание документации поручить Yegor.
    Согласен на 100%!

  10. #10
    Пользователь
    Регистрация
    10.11.2014
    Адрес
    Санкт-Петербург
    Сообщений
    1,012

    По умолчанию

    Огромное спасибо за статью.

Страница 1 из 7 123 ... ПоследняяПоследняя

Похожие темы

  1. Нестандартный протокол
    от SVVSVA в разделе ПЛК1хх
    Ответов: 2
    Последнее сообщение: 08.09.2011, 15:18
  2. нестандартный протокол
    от niklud19511 в разделе Сетевые технологии
    Ответов: 1
    Последнее сообщение: 05.05.2011, 15:02
  3. Нестандартный протокол
    от Евгений Владимирович в разделе ПЛК1хх
    Ответов: 9
    Последнее сообщение: 28.02.2010, 23:22
  4. Codesys и нестандартный протокол для ПЛК
    от Горшунов Сергей в разделе ПЛК1хх
    Ответов: 4
    Последнее сообщение: 23.10.2008, 18:19
  5. Нестандартный протокол
    от Klik в разделе ПЛК1хх
    Ответов: 36
    Последнее сообщение: 03.03.2008, 13:49

Ваши права

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