Tuesday, February 25, 2014

Retain or not to retain? That is the question.

When should a variable be marked retain?

  • A configuration parameter, that you want to remember the user value after a warm start.
  • A event or cycle log value, i.e., Who, What, When, Why, How.
  • An internal state value, if you want to be able to restore the program to the previous state.

When should a variable be NOT retained?

  • Values coming in from the outside every second.
  • Values going out every second. (90%)
  • Internal states, that will be reset by the warm start logic.
  • temporary variables: loop counters, intermediate results.
See the post on Cold/Warm start for code example.

Monday, February 24, 2014

Contract Hour - Once a day summing

Contract Hour How do you do a daily summary, at whatever the user wants for contract hour? Similar code can be used to do Hourly Summing.
FUNCTION CHOURTIME:
  • Returns CHOUR_NOT when you are not in contract hour.
  • Returns CHOUR_JUST when you are in contract hour and need to sum. i.e., first second of hour.
  • Returns CHOUR_DONE when you are in contract hour and have already done a sum.
  • Variables: ContractHourStatus should be a non-retained global and initialized to CHOUR_NOT at warm boot.
  • ContractHour should be a retained global and initialized to a default value at cold boot. Should be called once a second.
NSEC := ANY_TO_DINT(DateTime);
Hour := ANY_TO_UDINT(MOD ( NSEC / 3600, 24) );

IF  (ContractHour = Hour) THEN
    IF (ContractHourStatus = CHOUR_NOT) THEN
        ChourTime:= CHOUR_JUST;
    ELSE 
        ChourTime:= CHOUR_DONE;
    END_IF; 
ELSE 
    ChourTime:= CHOUR_NOT;
END_IF; 

Variables-Local

NameTypeDirAttrRetained
NSECDINTVarR/WNo
HOURUDINTVarR/WNo
ContractHourUDINTVarInputReadNo
ContractHourStatUDINTVarInputR/WNo
ChourTimeUDINTVarOutputWNo

Defined words (#define in C++)


Defined WordsValue
CHOUR_NOT0
CHOUR_JUST1
CHOUR_DONE2

Cold/Warm boot.

Example Main Program

(* setup initial retain values - first time.
   They will be read back from config file after a first time.*) 
 
IF (Cold = FALSE) THEN
    Enable := FALSE;
    HiHiLimit := 100.0;
    HiLimit := 90.0;

    ReturnStatus := Event_Clear();
    ReturnStatus := Event_Log(State_Cold, 0, 0.0);
    Cold := TRUE;
END_IF;

(* setup non-retain variables after a warm start *)
IF (Warm = False) THEN
    (* for each input clear the state and set the output to ON *)
    FOR pos := 1 TO ANY_TO_DINT(Input_Size) DO
        State[pos] := State_OK;
        Output[pos] := 1;
    END_FOR;

    ReturnStatus := Event_Log(State_Warm, 0, 0.0);
    Warm := TRUE;
END_IF;

Variables

NameTypeDirAttrRetained
ColdBoolVarR/WYes
WarmBoolVarR/WNo

Defined words (#define in C++)

Defined WordsValue
State_Unused0
State_OK1
State_HiHi2
State_Cold3
State_Warm4

Log an Event

Here is a function to log a single event:

(* Datetime is a udint (uin32) that reads the system clock 0.9.0 as an ISaGRAF I/O *)
EVENT_LOG FUNCTION (Code, Input, Value)

(* CODE - an integer to describe the event (hosts like integers) *)
(* Input - which input caused the event *)
(* Value - the value of the input that caused the event *)
(* Stamp - Date/Time stamp of when the event occured *)

(* Move all data down by one row – start at bottom, filling from above *)
  FOR pos := ANY_TO_DINT(Event_Log_Size) TO 2 BY -1 DO
    Event_Code[pos]  := Event_Code[pos - 1];
    Event_Input[pos] := Event_Input[pos - 1];
    Event_Value[pos] := Event_Value[pos - 1];
    Event_Stamp[pos] := Event_Stamp[pos - 1];
  END_FOR;

(* place new log in first row *)
  Event_Code[1]  := Code;
  Event_Input[1] := Input;
  Event_Value[1] := Value;
  Event_Stamp[1] := DateTime;
  Event_Log := TRUE;    (* all functions must return a value *)

Here is the initialization code for cold boot

(* ************ at cold boot - clear the log ************** *)
  FOR pos := 1 TO ANY_TO_DINT(Event_Log_Size) DO
    Event_Code[pos]  := 0;
    Event_Input[pos] := 0;
    Event_Value[pos] := 0.0;
    Event_Stamp[pos] := 0;
  END_FOR;

Timers - Pulse a DO and Log cycle time.

Method 1 - Assumes a 1 second cycle

(* init timers to zero at warm start *)
    Valve_Open_Timer = 0;

(* Open a Valve with a Pulse *)
    Valve := 1;
    Valve_Open_Timer = 1;

(* Pulse DO Timer -- if Timer is zero not active *)
IF Valve_Open_Timer <= DO_Time_Delay AND Valve_Open_Timer > 0 THEN
    Valve_Open_Timer := Valve_Open_Timer + 1;
    IF Valve_Open_Timer = DO_Time_Delay THEN
        Valve := 0;

        Valve_Open_Timer := 0;

    END_IF;
END_IF;

Example 2 - use system DateTime (seconds since 1/1/1970)

(* Datetime is a udint (uin32) that reads the system clock 0.9.0 as an ISaGRAF I/O *)
(* INIT clock at start of cycle *)
 StartCycleTime := DateTime;

(* Terminate clock at end of cycle *)
 CycleTime := DateTime - StartCycleTime;

State Machines

What is a State Machine?

A state machine is a why to keep track of where you are in the middle of a process. The example from current G4 is the plunger app. Its states are:
  1. Fail
  2. Start of Closed - waiting for plunger to fall
  3. waiting for the right conditions to open the valve.
  4. Start of Open - waiting for plunger to arrive.
  5. Plunger never arrived - Use blow valve to assist (not longer used due to EPA)
  6. Plunger Arrived - 1 sec state to record values at time of arrival.
  7. Afterflow - waiting for the right conditions to close the valve.

 How do I implement a state machine?

  • An integer variable is usually used to keep track of the current state. Optionally the state can ALSO be keep in a string for ease of displaying on the front panel or by host systems.
  • Expanding on its use in plunger, it is now common to add a Disabled state. This gives the host only one place to look for overall status.
  • Usually the state is not retained, but some processes need to know what happened at power down, so that recovery can be adjusted.
  • In it's simplest form one switch statement chooses which code to execute by the value of the current state.
  • More complex programs can use up to three switch statements
    1. Change of state at the beginning  to initialize a state
    2. Normal operation
    3. Change of state at the end to cleanup a state.

Example: 

IF Old_State <> Curr_State THEN 
    (* When state changes - Initialize new State *)
    CASE Curr_State of
       State_Open: 
            Statusbool := OpenInit();
       State_Close:
            StatusBool := CloseInit();
       State_Disabled:
            (* do nothing *)
       ELSE  (* Bad value reset Close *)
            Curr_State := State_Disabled;
    END_CASE;
    Old_State :=  Curr_State;
END_IF;
    CASE Curr_State of
    State_Open: 
         Statusbool := Open();
    State_Close:
         StatusBool := Close();
END_CASE;
CASE Old_State of     (* When state changes - Terminate new State *)
    State_Open: 
        Statusbool := OpenTerm();
    State_Close:
        StatusBool := CloseTerm();
END_CASE;
(* ********************* *)
OpenInit
    Open_Valve = TRUE;
(* ********************* *)
OPEN
    IF timetoclose THEN
        Curr_State = State_Close;
    END_IF;
(* ********************* *)
OpenTerm
    (* Record open time *)

Best Programming Practices

  1. Always use a UDINT variable named "NumRegisters" to set the size of the register (indirect) array (app.253.x)
  2. Always use a String variable named "PartNumber" to set the program's part number that is also used to select the INI file in PCCU. For example for Well Test: "2509032-001"
    1. Don't use numbers that start with 1,000,000 or 2,000,000 - those are for ABB.
    2. Don't do this will developing. Wait until you are ready to do a custom INI.
  3. INI Files:
    1. Always lock the INI index of all variables using the "Address" field of each variable, using a type code as part of the address.
    2. Use the program: "Gen INI G4.exe" to generate the first pass of a custom INI.
    3. Include Auto tree only in expert mode, if desired, normally only in the developer's INI: xxxxxxx-255.ini.
    4. Never include the "Symbol Table" or "System Variables" portion of the trees.
    5. Always include IEC pgm Command/Status page: (Msg Reads, IEC state, Start/Stop, Resource, Rescan, ...)
    6. Write protect items that the user should not change using command: rwa:1;
    7. Define unused columns with a blank comment and a write protect.
    8. Use Rowloop command for repeating lines.
  4. Package Files:
    1. Make both XFC and XRC packages if you have both kinds of meters.
    2. Only include the IEC pgm. Not ISAGRAF runtime.
  5. Always set the ISaGRAF resources for the project as follows:
    1. Embed Symbol Table = True
    2. Embedded Table Table Type = Complete
    3. Check Array Index = True
    4. Target = TOTALFLOW
    5. Memory for Retain = "\tfData\iec-%\2509xxx-rrr" (replace with correct part number)
    6. Cycle Time = 1000 ms (1 sec)

Topics to be covered?

What topics should we cover in this blog? Here is my list:
  • Good Coding Practices ✓
  • Contract Hour - summing daily or hourly. ✓
  • Startup: Cold/Warm/Luke Warm? ✓
  • State Machines - When and How. ✓
  • Timers ✓
  • Event Log/Cycle Log ✓
  • Retain or not to retain? That is the question. ✓
  • Input/Output - indirect guide. ✓
  • Steps to start a new project.
  • INI tricks and tips.
  • Deployment: Box to Box, Multiple copies.
  • Your suggestions?