Еще для серии S7-300 и S7-400 под Step 7 классических версий предлагаемых разработчику таймеров вполне хватало — это и стандартные таймеры IEC, реализованные в виде функциональных блоков, и таймеры S5 (которые, к слову, до сих пор существуют для серии S7-1500). Однако в ряде случаев разработчик не применял стандартные инструменты и реализовывал собственные таймеры, чаще всего — в виде функций. Такие таймеры-функции необходимы были при «айтишном» подходе к программированию, в котором оперировали не отдельными экземплярами функциональных блоков технологического оборудования, с соответствующей обвязкой входов и выходов, а массивами структур. Например — массив структуры типа «дискретный вход». Или массив структуры «агрегат». Такой подход к программированию имеет право на существование, поскольку позволяет серьезно экономить рабочую память CPU, но, с другой стороны, делает программный код трудночитаемым. Стороннему программисту и с простым видом программы на LAD разобраться получается далеко не сразу, а про кучи индексов, массивов и функций их обработки — и речи не идет, тут без документации к ППО (и без поллитры, разумеется) вообще никуда.

Эти массивы структур, как правило, обрабатывались в функциях. В принципе, ничто не мешало делать обработку и в функциональных блоках, но всегда вставал важный вопрос — как работать с таймерами в этих случаях? Стандартные таймеры предполагают либо номер (S5), либо экземпляр функционального блока (IEC). Речь, напоминаю, идет об обработке массивов структур для классических ПЛК Simatic, и «вкрячить» в эти структуры еще и номера таймеров, а тем более — экземпляры — либо сложно, либо просто невозможно.

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

Для 300 и 400 серии определить это время можно было двумя способами. Первый — смотреть время выполнения главного OB1 (есть соответствующая переменная в самом OB1) или циклических OB и увеличивать внутренний аккумулятор времени при каждом вызове таймера при условии подачи «истины» на вход. Не очень хороший вариант, поскольку это время отличается для OB1 и циклических OB. Второй способ — системная функция TIME_TCK, которая при каждом вызове возвращала одно-единственное значение — внутренний счетчик миллисекунд центрального процессора.

image

Таким образом, для таймера типа 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 доступен по ссылке.