Function timer for Simatic S7-1200 industrial controller

Even for the S7-300 and S7-400 series, under Step 7, the classic versions of the timers offered to the developer were quite enough - these are standard IEC timers, implemented as function blocks, and S5 timers (which, by the way, still exist for the S7- series 1500). However, in some cases, the developer did not use standard tools and implemented his own timers, most often in the form of functions. Such timers-functions were necessary with an “IT” approach to programming, in which they operated not with separate instances of the functional blocks of technological equipment, with the corresponding binding of inputs and outputs, but with arrays of structures. For example, an array of a discrete input type structure. Or an array of an aggregate structure. This approach to programming has a right to exist, since it allows you to seriously save the working memory of the CPU, but,on the other hand, makes program code hard to read. A third-party programmer and with a simple look of a program on LAD can not figure it out right away, but heaps of indices, arrays, and functions for processing them are out of the question; here, without documentation for the software (and without a half liter, of course), nowhere.

These arrays of structures were typically processed in functions. In principle, nothing prevented the processing of function blocks, but there was always an important question - how to work with timers in these cases? Standard timers assume either a number (S5) or an instance of a function block (IEC). This, I remind you, is about processing arrays of structures for classic Simatic PLCs, and to “twist” timer numbers into these structures, and even more so, instances are either difficult or simply impossible.

For this reason, we created our own timer functionality as a function. In principle, for the operation of any timer, you need to know only a few things - the state of the input, the time setting and how much time has passed since activation.

For the 300 and 400 series, there were two ways to determine this time. The first is to look at the runtime of the main OB1 (there is a corresponding variable in OB1 itself) or cyclic OBs and increase the internal time accumulator with each timer call, provided that the “truth” is input. Not a good option, since this time is different for OB1 and cyclic OBs. The second method is the TIME_TCK system function, which, with each call, returned a single value - the internal millisecond counter of the central processor.

image

Thus, for a timer of the TON type (on delay), the operation algorithm was as follows:

  • on the rising edge of the response request, reset the output and remember the current value of the system timer TIME_TCK
  • «» , ( , TIME_TCK 0 (2 ^ 31 — 1), ). , . , «», — «»
  • «»,

With the advent of the "thousandth" series, the situation has changed a bit. The fact is that the S7-1500 line inherited support for the TIME_TCK system call, and lovers of the “standing and in a hammock” approach (how else can you call a program that just does it that processes arrays of structures, while operating with creepy indexes?) calmly continue to use their achievements.

The S7-1200 series of base controllers is based on a different architecture, and it has a number of differences from the S7-1500. Including the lack of a TIME_TCK system call. In the ranks of developers who do not have sufficient flexibility of thinking, dissatisfaction has gone - it is impossible to execute copies / pastes of old programs. However, the task of determining how much time has passed since the previous call can be performed using the runtime function.

This function returns the time elapsed since its previous call, in seconds, as a double precision real number LREAL. Details are described in the help. For internal purposes, an additional MEM variable (also of the LREAL type) is required.

I will give the sources of the first approximation of the function, and I will give some notes.

Function declaration:

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

With the inputs / outputs, everything is clear: IN, Q and PT. I set the time setting in the form of a real one, it's seconds. Just wanted to (but in vain, but more on that below). Further about variables of the InOut area. Since we have a function, we don’t have a STAT area, there are no variables that retain their value during the next function call, and such variables are needed:

INPrv - to determine the positive edge of the request

MEM - auxiliary variable for the system call to work runtime

TimeACC - time accumulator , which will store the number of microseconds of the currently running delay.

The variables TimeACC, udiCycle and udiPT are specified in the UDINT format, an unsigned integer, 4 bytes. Despite the fact that I specified the time as real, and the runtime function returns real as much as double precision, I prefer to perform simple operations of summing and comparing with integer operands to save processor time. Time in my case is taken into account to the microsecond. The reason is simple - if you coarsen the time to a millisecond, then with almost empty OB1 (for example, if only one timer is called in the entire controller program and nothing more), “skips” of cycles are possible, the program sometimes runs for 250 μs. But in this case, the maximum allowable value of the time accumulator will be 4,294 seconds, almost 4,295 (2 ^ 32 - 1 = 4,294,967,295). There is nothing to be done, such “optimization” requires sacrifice.

Function text.

#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

The first two lines are the recalculation of the timer setting from the number of seconds specified in the REAL format to the number of microseconds. The time in microseconds elapsed from the previous program block call is also determined.

Further, the algorithm is as follows, and I have already given it:

  • on the rising edge of input IN, reset output Q and reset the time accumulator
  • if the “truth” continues to be input, we increase the time accumulator by the already known udiCycle value and compare it with the time setting. If the time setting is exceeded, the timer has worked, give the output “true”, otherwise, give the output “false”
  • in the case of applying false input to the IN input, reset the Q output and reset the time accumulator.

At the end of the function, in order to determine the edge of the input IN, remember its previous value. Also give the output ENO (when using a function in graphical languages, such as LAD) the value of the output Q.

We make sure that the function is working, after which it becomes interesting to evaluate its speed and, if necessary, improve (it already appears at first glance that a number of calculations go to idle and wasting CPU time in vain). To evaluate performance, I declare an array of 1000 timer data structures.

Declaration of the structure. Its fields duplicate the input and output variables of the timer function.

TYPE "typePervTONdata"
VERSION : 0.1
   STRUCT
      IN : Bool;   //  
      PT : Real;   //   
      Q : Bool;   //  
      INPrv : Bool;   //    
      MEM : LReal;   //    
      TimeACC : UDInt;   //  
   END_STRUCT;

END_TYPE

An array of structures is declared in the TortureTON global data block:

TONs : Array[0..999] of "typePervTONdata";

The following code is executed in the organizational block 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;

Announced 1000 “instances” of timers, each set a time of 10 seconds. All 1000 timers start counting the time by the value of the startton marker variable.

I launch the diagnostic functions of the controller (S7-1214C DC / DC / DC, version FW 4.4, version Step7 - V16) and watch the scan cycle time of the controller. At “idle” (when “false” arrives at the input of the timers), the entire thousand are processed on average for 36-42 milliseconds. During the countdown of ten seconds, this reading grows by about 6-8 milliseconds and sometimes rolls over for 50 ms.

We look at what can be improved in the function code. Firstly, the lines at the very beginning of the program block:

#udiCycle := LREAL_TO_UDINT(RUNTIME(#MEM) * 1000000); //     
#udiPT := REAL_TO_UDINT(#PT * 1000000); //   

They are always called, regardless of whether the timer counts the time, does not count, or has already counted. A big waste of money is to load the not very powerful CPU of the 1200 series with calculations involving double precision materials. It is reasonable to transfer both lines to the part of the code that processes the countdown (if the “truth” continues to come in). It is also necessary to duplicate the udiCycle calculation into a code that processes a positive edge at the input of the timer. This should relieve the “idle operation” of the timer when the input value is false. In practice, timers in programmable logic controllers most often work “idle”. For example, the filtering time of contact bounce is tens of milliseconds. The control pulse of a discrete output is a few hundred milliseconds, usually from 0.5 to 1.0 seconds.The time for monitoring the execution of the unit command (for example, the time the valve opens completely) is from tens of seconds to several minutes. PLC in production works 24 hours a day and 365 (and sometimes more!) Days a year. That is, most often the input of the timer is either “zero”, and the timer does not count anything, or a “unit” arrives for a long time, and the timer has already counted everything. To unload the second-stage CPU idle (the timer has already counted), it is necessary to check at the stage “the input continues to receive the truth” - if the timer has already counted all the time and set the output to true. In this case, no calculations should be performed.most often the input of the timer is either “zero”, and the timer does not count anything, or a “unit” arrives for a long time, and the timer has already counted everything. To unload the second-stage CPU idle (the timer has already counted), it is necessary to check at the stage “the input continues to receive the truth” - if the timer has already counted all the time and set the output to true. In this case, no calculations should be performed.most often the input of the timer is either “zero”, and the timer does not count anything, or a “unit” arrives for a long time, and the timer has already counted everything. To unload the second-stage CPU idle (the timer has already counted), it is necessary to check at the stage “the input continues to receive the truth” - if the timer has already counted all the time and set the output to true. In this case, no calculations should be performed.

To make these changes, it is necessary to transfer the output of the Q timer from the OUTPUT area to the IN_OUT area, and the output value will be stored in external variables (in this example, in an array of structures). After refinement, the entire function code, including the declaration, is as follows:

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

After that, the execution time improves: the idle time processing of the timers is 23 ms, with a working filtering time of 37-40 ms.

This function code does not check for an invalid value of the timer setting - a negative value (when the material is converted to an unsigned integer, the setting will be distorted) or a value greater than 4294.9 seconds (the time setting will overflow and distort). You must either control the value of the PT value in the code, or entrust the task of checking the range of the time setting (from 0 to 4294.9 seconds) to the top-level operator system. Checking the range by means of the PLC program increases the processing time to approximately 45-46 ms (and, in general, the most correct way is to set the timer time not in REAL format, but in UDINT format in milliseconds and to do nonsense).

The application project with a timer for the TIA Portal Step 7 version 16 environment is available here .

All Articles