← Resources
Platforms·11 min read·

FB vs FC in TIA Portal: which block to use, and why

An FC has no memory; an FB keeps its data in an instance DB. What that means for timers, edges, outputs and retain, with SCL for S7-1200 and S7-1500.

By the LADX engineering team

Short answer

An FC (function) has no memory of its own: its Temp data lasts only for the call, so every parameter must be supplied each time. An FB (function block) keeps its inputs, outputs, in-outs and Static data in an instance data block, so values survive from one cycle to the next. Use an FB for anything that remembers, such as timers, edges and sequences, and an FC for calculations.

A function (FC) interface with Input, Output, InOut, Temp and Return sections, whose Temp data lasts only for the call, beside a function block (FB) whose Input, Output, InOut and Static sections, including a TON_TIME timer and an R_TRIG edge as multi-instances, are kept in its instance data block between cycles
Where an FC and an FB keep their data

Siemens gives you two kinds of code block for your own logic, and the difference between them comes down to one word: memory. A function block (FB) has it. A function (FC) does not. Nearly every FB-or-FC question, from "why does my edge fire every scan" to "why does my output keep dropping out", comes back to that.

The rules below are taken from Siemens' TIA Portal V21 help, and the examples are in SCL. The same rules apply in LAD and FBD; only the notation changes. If SCL is new to you, Structured Text for ladder programmers covers the syntax.

The short answer

Use an FB when the block has to remember something between calls: a timer, an edge, a sequence step, a count, a latch. Use an FC when the result depends only on what you pass in this time: a scaling calculation, a conversion, a limit check.

FC (function)FB (function block)
Memory of its ownNone. Siemens calls FCs code blocks without memoryAn instance data block for each call
Temp sectionValid for this call onlyValid for this call only
Static sectionNot availableKept in the instance between cycles
Parameters at the callAll of them must be suppliedMostly optional: an unassigned one uses the value stored in the instance. In-outs are required on optimised blocks
An output you never writeComes back as the data type's default (FALSE for Bool); structured outputs are not pre-assigned at allKeeps its last value in the instance
Return valueYes, and an SCL expression can use itNo
Timers and edges insideOnly with a separate instance DB or storage in a global DBAs multi-instances in Static
Retentive dataNone of its own; keep it in a global DBPer tag on optimised blocks, whole instance on standard ones
Siemens TIA Portal - "Function Block" and "Function" Differences, from Liam BeePrivacyWatch on YouTube

Why an FC forgets

An FC has nowhere to put a value between calls. Its local working data lives in the Temp section, and Siemens' block interface reference is blunt about it: temporary local data is retained for only one cycle, so you must write it in the same cycle you read it.

What you get if you read Temp before writing it depends on the block's access setting. In a block with optimised access, an unwritten Temp tag reads as the default for its type: FALSE for a Bool, 0 for an Int. In a block with standard access it holds whatever happened to be there, which Siemens describes as random, so you must initialise it first.

Here is the classic mistake: a parts counter written as an FC, with its edge memory in Temp.

scl
FUNCTION "Count_Parts" : Void
VAR_INPUT
  PartSensor : Bool;
END_VAR
VAR_IN_OUT
  Count : DInt;
END_VAR
VAR_TEMP
  LastSensor : Bool;   // lives for this call only
END_VAR
BEGIN
  IF #PartSensor AND NOT #LastSensor THEN
    #Count := #Count + 1;
  END_IF;
  #LastSensor := #PartSensor;   // written, then thrown away
END_FUNCTION

It compiles and it reads correctly. In an optimised block, #LastSensor is FALSE at the start of every call, so the condition is true on every scan the sensor is covered and the count climbs by one per scan rather than one per part. In a standard-access block the result depends on what was left on the stack. Either way the edge never works, because the previous state was never kept. A one-shot needs a storage bit for exactly this reason.

The fix is not a cleverer FC. It is giving the edge somewhere to live.

The same machine as an FB

Here is a conveyor with a start/stop latch, a part counter and a jam timer, written as an FB. The edge and the timer are declared in the Static section, which is VAR in the textual interface. That makes them multi-instances: their data sits inside this FB's own instance DB.

scl
FUNCTION_BLOCK "Conveyor"
VAR_INPUT
  Start : Bool;        // momentary push button
  Stop : Bool;         // normally closed: TRUE while healthy
  PartSensor : Bool;
END_VAR
VAR_OUTPUT
  Run : Bool;
  Jammed : Bool;
  Parts : DInt;
END_VAR
VAR
  Running : Bool;
  JamLatch : Bool;
  Count : DInt;
  PartEdge : R_TRIG;     // multi-instance: edge memory in this instance
  JamTimer : TON_TIME;   // multi-instance IEC timer
END_VAR
BEGIN
  // a part sitting on the sensor for 5 s while running is a jam
  #JamTimer(IN := #Running AND #PartSensor, PT := T#5s);
  IF #JamTimer.Q THEN
    #JamLatch := TRUE;             // held until the next start
  ELSIF #Start THEN
    #JamLatch := FALSE;
  END_IF;

  #Running := (#Start OR #Running) AND #Stop AND NOT #JamLatch;

  // one count per part, not one per scan
  #PartEdge(CLK := #PartSensor);
  IF #PartEdge.Q THEN
    #Count := #Count + 1;
  END_IF;

  #Run := #Running;
  #Jammed := #JamLatch;
  #Parts := #Count;
END_FUNCTION_BLOCK

The latch and the jam flag are Static tags rather than outputs read back, because Siemens' general rule for block parameters is that inputs are only read and outputs are only written. Calling it twice from OB1 gives two independent conveyors:

scl
"Conveyor_1_DB"(Start := "C1_Start", Stop := "C1_Stop", PartSensor := "C1_Part",
                Run => "C1_Motor", Jammed => "C1_Jam");
"Conveyor_2_DB"(Start := "C2_Start", Stop := "C2_Stop", PartSensor := "C2_Part",
                Run => "C2_Motor", Jammed => "C2_Jam");

Each call has its own instance DB, so conveyor 2's timer, edge and count are separate from conveyor 1's. Point both calls at the same DB and they fight over one timer and one count.

Single, multi and parameter instances

Siemens defines three places an FB's instance can live. You choose in the Call options dialog that appears when you drop an FB into a block, or by declaring the instance yourself.

InstanceWhere the data livesUse it for
Single instanceIts own instance DBTop-level units called from an OB
Multi-instanceThe Static section of the calling FBTimers, edges and sub-blocks inside an FB, with fewer DBs
ARRAY of multi-instancesAn array in the caller's Static section, indexed by a variableIdentical units processed in a loop
Parameter instancePassed in as an InOut at run timeChoosing which instance to process while the program runs

Multi-instances are the normal way to put several units under one parent:

scl
FUNCTION_BLOCK "Line"
VAR_INPUT
  Start : Bool;
  Stop : Bool;
END_VAR
VAR
  Conveyors : Array[1..4] of "Conveyor";   // four instances, one DB
END_VAR
VAR_TEMP
  i : Int;
END_VAR
BEGIN
  FOR #i := 1 TO 4 DO
    #Conveyors[#i](Start := #Start, Stop := #Stop,
                   PartSensor := "Line_IO".PartSensor[#i]);
  END_FOR;
END_FUNCTION_BLOCK

The loop counter is in Temp, which is fine: it is written before it is read on every call. Parameter instances have conditions worth knowing before you design around them. Passing an instance through a DB_ANY tag needs S7-1200 firmware V4.2 or S7-1500 V2.0 or later, and Siemens notes it does not work for instances of system blocks from the Instructions task card. NULL can never be passed.

Timers and edges always need an instance

IEC timers and R_TRIG are function blocks too. Siemens' TON reference says every call must be assigned an IEC timer in which its instance data is stored, and gives two places for it: an instance DB of type IEC_TIMER, or a tag of type TON_TIME in the Static section of a block. R_TRIG compares CLK with an edge memory bit kept in its instance in the same way. TON, TOF and RTO covers how the timers themselves behave.

That is why a timer inside an FC is awkward. An FC has no Static section, so each timer needs a single-instance DB of its own, or a timer held in a global DB; Siemens' programming recommendations show a global DB holding an ARRAY of TON. Both work, and both put the memory outside the block, which undoes much of the point of reusing it.

Two traps from the same Siemens pages:

  • Do not drive a multi-instance timer's IN from outside the call. TON starts on a rising edge of IN compared against the value stored in its instance. Write IN somewhere else first and the call sees no new edge, so the timer never starts. Pass IN in the call.
  • Do not reinitialise a running timer. Loading with reinitialisation, loading a snapshot, or forcing the instance's actual values disrupts the timer. Siemens marks this as a danger: put the plant in a safe state first.

The FC output trap

Siemens' parameter-assignment rules for FCs say that an output not written in the function returns the predefined value of its data type, and that structured outputs are not pre-assigned at all. They name two ways it happens: a jump that skips the assignment, and Set or Reset instructions, which write nothing when the RLO is 0.

So this does not latch anything:

scl
FUNCTION "Level_Alarm" : Void
VAR_INPUT
  Level : Real;
  HighLimit : Real;
END_VAR
VAR_OUTPUT
  Alarm : Bool;
END_VAR
BEGIN
  IF #Level > #HighLimit THEN
    #Alarm := TRUE;     // only written on this path
  END_IF;
END_FUNCTION

On every call where the level is below the limit, Alarm is not written and comes back FALSE. If you wanted a latched alarm, an FC cannot hold it: the latch needs memory, so it belongs in an FB's Static section or a global DB. If you wanted a plain comparison, write the output on every path:

scl
#Alarm := #Level > #HighLimit;

In-outs behave differently. An in-out the FC does not write keeps its old value, because it is read before the call and written back after it. That is why the parts counter above passed Count as an in-out. It works, but the memory is now the caller's problem.

Retained state after a power cut

An FB's data can be made retentive; an FC has none of its own to retain. How you set it depends on the block's access mode, according to Siemens' retentivity pages:

  • Optimised access. Each tag in the interface can be Retentive, Non-retentive or Set in IDB. Set in IDB leaves the choice to the instance DB, where one checkbox applies to every tag marked that way. A structure is retentive or not as a whole.
  • Standard access. Retentivity is set for the whole instance in the instance DB, not tag by tag.
  • Textual interface. VAR RETAIN makes a section retentive and VAR DB_SPECIFIC defers the choice to the instance DB. Without an attribute the section is non-retentive.

In-outs of structured types such as ARRAY, STRUCT or STRING are always non-retentive. For an FC, keep anything that must survive a restart in a global DB and set retentivity there. Treat retain as a design decision rather than a default: a retained run latch can restart a motor at power-up, which is the trade-off retentive memory is about.

When an FC is the right choice

Siemens' own examples of what FCs are for are returning function values, such as mathematical functions, and technological functions built from bit logic. In practice that means:

  • Scaling and unit conversion.
  • Limit tests and validity checks that produce a result on every call.
  • Packing and unpacking bits and words.
  • Anything you want to use inside an SCL expression.

An FC with a return value can be called in an expression. The function value has the same name as the function:

scl
FUNCTION "Scale" : Real
VAR_INPUT
  Raw : Int;
  RawLo : Int;
  RawHi : Int;
  EngLo : Real;
  EngHi : Real;
END_VAR
BEGIN
  IF #RawHi = #RawLo THEN
    #Scale := #EngLo;    // no span: avoid dividing by zero
    RETURN;
  END_IF;
  #Scale := #EngLo + INT_TO_REAL(#Raw - #RawLo) * (#EngHi - #EngLo)
            / INT_TO_REAL(#RawHi - #RawLo);
END_FUNCTION

And from any block:

scl
"Tank".Level_m := "Scale"(Raw := "AI_Level", RawLo := "Cal".RawLo,
                           RawHi := "Cal".RawHi, EngLo := 0.0, EngHi := 4.0);

Two rules from Siemens' SCL call reference apply here: every parameter of an FC must be supplied, and in SCL you cannot use EN to call an FC conditionally, so wrap the call in an IF instead. Analog scaling covers the arithmetic itself.

Coming from Rockwell

If you think in Studio 5000, the closest match for an FB is an Add-On Instruction: each use carries its own data, a backing tag there and an instance here. An FC is closer to a subroutine called with parameters, except that it cannot keep anything between calls unless you hand it storage. The bigger adjustment is timers: a Rockwell timer is a tag whose bits you read, while an S7 IEC timer is a block call with an instance of its own. Siemens and Allen-Bradley thinking goes further into the differences.

What goes wrong, and how to recognise it

SymptomLikely causeFix
A count or one-shot fires on every scan the input is onEdge memory kept in Temp, usually in an FCR_TRIG as a multi-instance in an FB, or a bit in a global DB
Two machines interfere, counts double, one start runs bothTwo calls share one instance DBOne instance per call
An FC output drops back to FALSEOutput not written on every path, or written with S/R while the RLO is 0Assign every output on every path; put latches in an FB
Temp values look randomStandard-access block reading Temp before writing itInitialise Temp first, or use optimised access
A multi-instance timer never startsIN written outside the call, so the timer sees no new edgePass IN in the call
Values reset after a power cycleTags non-retentiveSet retentivity in the interface or instance DB, deliberately
A timer misbehaves after a downloadThe download reinitialised the instance while it was timingDownload with reinitialisation only when the plant is safe

Seeing the instance problem from the other side

LADX Convert shows the same rule when it turns ladder into Siemens SCL. Open one of its example programs that uses a timer and choose Siemens SCL as the output: the result declares the timer as a TON instance in a VAR section, and a one-shot becomes an R_TRIG instance, with a note in the report that this is state the ladder did not spell out. In a TIA Portal block's textual interface, VAR is the Static section, which only an FB has. So code with timers or one-shots lands in an FB, or needs instance DBs of its own. Treat the output as a starting point and read the report; you can also try the logic first in the free ladder editor.

Sources and further reading

Common questions

Can an FC call an FB in TIA Portal?
Yes, but the FB still needs somewhere to keep its data, and an FC has no Static section to hold a multi-instance. Give the call its own single-instance DB, or hand the FC an instance as a parameter. Siemens documents which parameter types an FC can forward to an FB for each CPU family.
Does an FB use more memory than an FC?
An FB uses an instance DB for each call, sized by the parameters and Static tags in its interface, and Siemens notes the maximum instance DB size depends on the CPU. An FC keeps nothing of its own between calls. Multi-instances cut the number of DBs, not the amount of data stored.
What is the difference between a global DB and an instance DB?
A global DB is a data area any block can read and write. An instance DB belongs to one FB call: its structure is derived from that FB's interface and can only be changed there. Use global DBs for data several blocks share, and instance DBs for a block's own working memory.
Should I use optimised or standard block access?
On S7-1200 and S7-1500, optimised access stores tags by symbolic name with no fixed address, while standard access keeps a fixed layout compatible with S7-300 and S7-400. Optimised blocks also let you set retentivity tag by tag. Standard access mainly matters when something reads the data by absolute offset.
Can two calls share one instance DB?
Nothing stops you, but both calls then read and write the same Static data, timers and edges, so one machine's state leaks into the other. Make one instance per controlled thing the rule. The exceptions are deliberate and documented, such as Siemens' MB_CLIENT examples that sequence several requests through one instance.

Keep reading