Эти массивы структур, как правило, обрабатывались в функциях. В принципе, ничто не мешало делать обработку и в функциональных блоках, но всегда вставал важный вопрос — как работать с таймерами в этих случаях? Стандартные таймеры предполагают либо номер (S5), либо экземпляр функционального блока (IEC). Речь, напоминаю, идет об обработке массивов структур для классических ПЛК Simatic, и «вкрячить» в эти структуры еще и номера таймеров, а тем более — экземпляры — либо сложно, либо просто невозможно.
По этой причине и создавался собственный функционал таймера в виде функции. В принципе, для работы любого таймера необходимо знать всего несколько вещей — состояние входа, уставку времени и сколько времени уже прошло с момента активации.
Для 300 и 400 серии определить это время можно было двумя способами. Первый — смотреть время выполнения главного OB1 (есть соответствующая переменная в самом OB1) или циклических OB и увеличивать внутренний аккумулятор времени при каждом вызове таймера при условии подачи «истины» на вход. Не очень хороший вариант, поскольку это время отличается для OB1 и циклических OB. Второй способ — системная функция TIME_TCK, которая при каждом вызове возвращала одно-единственное значение — внутренний счетчик миллисекунд центрального процессора.
Таким образом, для таймера типа TON (задержка включения) алгоритм работы был таков:
- по переднему фронту запроса срабатывания сбрасываем выход и запоминаем текущее значение системного таймера TIME_TCK
- если на вход запроса продолжает поступать «истина» определяем текущее значение системного таймера и вычитаем из него значение, которые мы запомнили на этапе запуска таймера (не забываем при это, что TIME_TCK возвращает значение от 0 до (2 ^ 31 — 1), а при превышении верхнего порогового значения начинает отсчет с нуля). В результате получили, сколько миллисекунд прошло с момента активации отсчета времени. Если прошло меньше заданного, подаем на выход «ложь», в противном случае — «истину»
- если на вход запроса приходит «ложь», обнуляем выход таймера
С появлением «тысячной» серии ситуация немного изменилась. Дело в том, что линейка S7-1500 унаследовала поддержку системного вызова TIME_TCK, и любители подхода «стоя и в гамаке» (а как еще можно назвать программу, которая только и делает, что обрабатывает массивы структур, оперируя при этом жуткими индексами?) могли спокойно продолжать использовать свои наработки.
Линейка базовых контроллеров S7-1200 основана на другой архитектуре, и в ней есть ряд отличий от S7-1500. В том числе — отсутствие системного вызова TIME_TCK. В рядах разработчиков, не обладающих достаточной гибкостью мышления, пошло недовольство — невозможно выполнить копи/паст старых программ. Тем не менее, поставленную задачу определения, сколько времени прошло с момента предыдущего вызова, можно выполнить, используя функцию runtime.
Данная функция возвращает время, прошедшее с ее предыдущего вызова, в секундах в виде вещественного числа двойной точности LREAL. Подробности описаны в справке. Для внутренних целей необходимо дополнительная переменная MEM (тоже типа LREAL).
Приведу исходники первого приближения функции, и дам некоторые примечания.
Объявление функции:
FUNCTION "PerversionTON" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
IN : Bool; // Вход таймера
PT : Real; // Уставка времени в секундах
END_VAR
VAR_OUTPUT
Q : Bool; // Выход таймера
END_VAR
VAR_IN_OUT
INPrv : Bool;
MEM : LReal;
TimeACC : UDInt;
END_VAR
VAR_TEMP
udiCycle : UDInt;
udiPT : UDInt;
END_VAR
Со входами/выходами все ясно: IN, Q и PT. Уставку времени завел в виде вещественного, это секунды. Просто так захотелось (а зря, но об этом ниже). Далее о переменных области InOut. Поскольку у нас именно функция, то у нас нет области STAT, нет переменных, которые сохраняют свое значение при последующем вызове функции, а такие переменные необходимы:
INPrv — для определения положительного фронта запроса
MEM — вспомогательная переменная для работы системного вызова runtime
TimeACC — аккумулятор времени, который будет хранить количество микросекунд идущей в настоящий момент задержки срабатывания.
Переменные TimeACC, udiCycle и udiPT заданы в формате UDINT, беззнаковое целое, 4 байта. Несмотря на то, что и задание времени я указал в виде вещественного, и функция runtime возвращает вещественное аж двойной точности, я предпочту выполнять простые операции суммирования и сравнения целочисленными операндами для экономии процессорного времени. Время в моем случае учитывается с точностью до микросекунды. Причина проста — если огрублять время до миллисекунды, то при почти пустом OB1 (например, если во всей программе контроллера вызывается только один таймер и более ничего) возможны «пропуски» циклов, программа иногда выполняется и за 250 мкс. Но в этом случае максимально допустимое значение аккумулятора времени составит 4 294 секунды, почти 4 295 (2 ^ 32 — 1 = ?4 294 967 295). Ничего не поделать, такая «оптимизация» требует жертв.
Текст функции.
#udiCycle := LREAL_TO_UDINT(RUNTIME(#MEM) * 1000000); //прошло микросекунд со времени предыдущего вызова
#udiPT := REAL_TO_UDINT(#PT * 1000000); //уставка таймера в микросекундах
IF (#IN AND (NOT #INPrv)) THEN //по переднему фронту входа обнулить аккумулятор времени и сбросить выход
#TimeACC := 0;
#Q := FALSE;
ELSIF (#IN AND #INPrv) THEN //если на вход продолжает поступать "истина"
#TimeACC += #udiCycle; //увеличть аккумулятор времени на величину "прошло времени с прошлого вызова"
IF #TimeACC >= #udiPT THEN //если накопленное время достигло или превысило уставку
#Q := TRUE; //дать выход "истина"
#TimeACC := #udiPT; //зафиксировать аккумулятор временеи
ELSE //если накопленное время не достигло уставки времени
#Q := FALSE; //сбросить выход
END_IF;
ELSE //во всех остальных случаях - сбросить выход и обнулить аккумулятор времени
#Q := FALSE;
#TimeACC := 0;
END_IF;
#INPrv := #IN; //предыдущее значение запроса
ENO := #Q; //выход ENO для красоты при использовании этой функции в языках LAD и FBD
Первые две строчки — пересчет уставки таймера из количестве секунд, задаваемого в формате REAL, в количество микросекунд. Так же определяется время в микросекундах, прошедшее с предыдущего вызова программного блока.
Далее алгоритм следующий, и я его уже приводил:
- по переднему фронту входа IN обнуляем выход Q и сбрасываем аккумулятор времени
- если на вход продолжает поступать «истина», увеличиваем аккумулятор времени на известную уже величину udiCycle и сравниваем его с уставкой времени. При превышении уставки времени таймер отработал, дать на выход «истину», в противном случае — дать на выход «ложь»
- в случаях подачи на вход IN значения «ложь» обнулить выход Q и сбросить аккумулятор времени.
В конце функции для возможности определения фронта входа IN запомнить его предыдущее значение. Так же дать на выход ENO (при использовании функции в графических языках, типа LAD) значение выхода Q.
Убеждаемся в работоспособности функции, после чего становится интересным оценить ее быстродействие и при необходимости улучшить (уже на первый взгляд становится ясно, что ряд вычислений идет в холостую и зря отнимает процессорное время). Для оценки быстродействия объявляю массив из 1000 структур данных таймера.
Объявление структуры. Её поля дублируют входные и выходные переменные функции таймера.
TYPE "typePervTONdata"
VERSION : 0.1
STRUCT
IN : Bool; // Вход таймера
PT : Real; // Задание времени таймера
Q : Bool; // Выход таймера
INPrv : Bool; // Для определения фронта входа
MEM : LReal; // Для вычисления прошедшего времени
TimeACC : UDInt; // Аккумулятор времени
END_STRUCT;
END_TYPE
В глобальном блоке данных «TortureTON» объявляется массив структур:
TONs : Array[0..999] of "typePervTONdata";
В организационном блоке OB1 выполняется следующий код:
FOR #i := 0 TO 999 DO
"TortureTON".TONs[#i].IN := "startton";
"PerversionTON"(IN := "TortureTON".TONs[#i].IN,
PT := "TortureTON".TONs[#i].PT,
Q := "TortureTON".TONs[#i].Q,
INPrv := "TortureTON".TONs[#i].INPrv,
MEM := "TortureTON".TONs[#i].MEM,
TimeACC := "TortureTON".TONs[#i].TimeACC);
END_FOR;
Объявлено 1000 «экземпляров» таймеров, у каждого задано время в 10 секунд. Вся 1000 таймеров начинает отсчет времени по значению маркерной переменной startton.
Запускаю диагностические функции контроллера (S7-1214C DC/DC/DC, версия FW 4.4, версия Step7 — V16) и смотрю время цикла сканирования контроллера. На «холостом ходе» (когда на вход таймеров поступает «ложь») вся тысяча обрабатывается в среднем за 36-42 миллисекунды. Во время отсчета десяти секунд это показание вырастает примерно на 6-8 миллисекунд и временами зашкаливает за 50 мс.
Смотрим, что можно улучшить в коде функции. Во-первых, строки в самом начале программного блока:
#udiCycle := LREAL_TO_UDINT(RUNTIME(#MEM) * 1000000); //прошло микросекунд со времени предыдущего вызова
#udiPT := REAL_TO_UDINT(#PT * 1000000); //уставка таймера в микросекундах
Они вызываются всегда, вне зависимости от того, считает ли время таймер, не считает или уже посчитал. Большое расточительство — нагружать не особо мощный CPU серии 1200 расчетами, связанными с вещественными двойной точности. Разумно перенести обе строчки в часть кода, обрабатывающая отсчет времени (если на вход продолжает поступать «истина»). Так же необходимо продублировать вычисление udiCycle в код, обрабатывающий положительный фронт на входе таймера. Это должно разгрузить «холостую работу» таймера, когда на вход поступает значение «ложь». На практике таймеры в программируемых логических контроллерах чаще всего работают «на холостую». Например, время фильтрации дребезга контактов — это десятки миллисекунд. Управляющий импульс дискретного выхода — несколько сотен миллисекунд, обычно от 0.5 до 1.0 секунды. Время контроля выполнения команды агрегата (например, время полного открытия задвижки) — от десятков секунд до нескольких минут. ПЛК на производстве же работает 24 часа в сутки и 365 (а иногда и больше!) дней в году. То есть, чаще всего на входе таймера находится либо «ноль», и таймер ничего не считает, либо длительное время поступает «единица», и таймер уже все посчитал. Для разгрузки CPU холостого хода второго вида (таймер уже посчитал) необходимо на этапе «на вход продолжает поступать истина» проверять — а посчитал ли уже таймер все время и выставил ли выход в истину. В этом случае никаких вычислений выполнять не следует.
Для внесения этих изменений необходимо выход Q таймера перенести из области OUTPUT в область IN_OUT, и значение выхода будет сохраняться во внешних переменных (в данном примере — в массиве структур). После доработки весь код функции, включая объявление, выглядит следующим образом:
FUNCTION "PerversionTON" : Void
{ S7_Optimized_Access := 'TRUE' }
VERSION : 0.1
VAR_INPUT
IN : Bool; // Вход таймера
PT : Real; // Уставка времени в секундах
END_VAR
VAR_IN_OUT
Q : Bool; // Выход таймера
INPrv : Bool;
MEM : LReal;
TimeACC : UDInt;
END_VAR
VAR_TEMP
udiCycle : UDInt;
udiPT : UDInt;
END_VAR
BEGIN
IF (#IN AND (NOT #INPrv)) THEN //по переднему фронту входа обнулить аккумулятор времени и сбросить выход
#TimeACC := 0;
#Q := FALSE;
#udiCycle := LREAL_TO_UDINT(RUNTIME(#MEM) * 1000000); //фиксируем "базу времени" по фронту
ELSIF (#IN AND #INPrv) THEN //если на вход продолжает поступать "истина"
IF (NOT #Q) THEN
#udiCycle := LREAL_TO_UDINT(RUNTIME(#MEM) * 1000000); //прошло микросекунд со времени предыдущего вызова
#udiPT := REAL_TO_UDINT(#PT * 1000000); //уставка таймера в микросекундах
#TimeACC += #udiCycle; //увеличить аккумулятор времени на величину "прошло времени с прошлого вызова"
IF #TimeACC >= #udiPT THEN //если накопленное время достигло или превысило уставку
#Q := TRUE; //дать выход "истина"
#TimeACC := #udiPT; //зафиксировать аккумулятор времени
END_IF;
END_IF;
ELSE //во всех остальных случаях - сбросить выход и обнулить аккумулятор времени
#Q := FALSE;
#TimeACC := 0;
END_IF;
#INPrv := #IN; //предыдущее значение запроса
ENO := #Q; //выход ENO для красоты при использовании этой функции в языках LAD и FBD
END_FUNCTION
После этого время выполнения улучшается: холостой ход обработки таймеров составляет 23 мс, при работающей фильтрации времени 37-40 мс.
В этом коде функции отсутствует проверка на недопустимое значение уставки таймера — отрицательную величину (при переводе вещественного в беззнаковое целое произойдет искажение уставки) или величину больше 4294.9 секунд (произойдет переполнение и искажение уставки времени). Необходимо либо контролировать значение величины PT в коде, либо доверить задачу проверки диапазона временной уставки (от 0 до 4294.9 секунд) операторской системе верхнего уровня. Проверка диапазона средствами программы PLC увеличивает время обработки примерно до 45-46 мс (а, вообще, самый правильный способ — это задавать время таймера не в формате REAL, а в формате UDINT в миллисекундах и заниматься ерундой).
Проект прикладной программы с таймером для среды TIA Portal Step 7 версии 16 доступен по ссылке.
AlexanderLifanov
Саша, а ты не пробовал сравнивать вышеописанную конструкцию с банальным инкрементом в циклическом ОВ? Да, вызовов больше, но сама вызываемая процедура проще.
akcount Автор
В циклическом OB (OB35, например) ты просто будешь знать время приращения аккумулятора времени, как константу. Но в целом задача сводится к предыдущей.