Resolving CONT_C FB41 PID SCL Compilation Errors in STEP 7

David Krause14 min read
S7-300SiemensTroubleshooting
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Problem Description: CONT_C FB41 SCL Compilation Failures

The CONT_C block (FB41) from the Siemens Standard Library → PID Control Blocks is the most widely deployed continuous PID controller on S7-300 and S7-400 systems programmed with STEP 7 (Classic). When the block is dropped from the library into an S7 program, STEP 7 renames the FB to CONT_C while internally registering it as FB41. The instance data block that the library wizard creates is, by default, also named CONT_C with a system-assigned DB number.

Engineers writing the controller in SCL (Structured Control Language) frequently encounter one or more of the following compiler diagnostics when they first wire the call:

  • "The instruction is unknown."
  • "The instruction is not valid."
  • "An operator is missing."
  • Generic "Invalid data type" or "Identifier expected" messages on the output side of the call.

The errors typically point at the out-block of the FB41 call, although the real defect is almost always in how the call is written and how the instance DB is bound to the function block type.

Symptoms in this case: The user imported CONT_C from the standard library, created DB41 manually, and tried to invoke the block as CONT_C.DB41(...). SCL flagged the output list := DB41.LMN; := DB41.LMN_PER; ... as invalid, and after the user removed those lines the controller stopped updating LMN / LMN_PER even though the analog input PIW336 changed in real time.

Root Cause Analysis

Two distinct, but related, defects produce this symptom pattern in SCL:

  1. Instance DB / FB type mismatch. The CONT_C FFB imported from the library registers itself internally as FB41. The user-created DB41 was, however, declared as a shared data block and then associated with the standard FB41 signature by hand (or by a separate wizard pass). When SCL then encounters CONT_C.DB41(...) the binding between the symbolic name CONT_C and the instance DB number is broken, so the compiler cannot resolve the multi-instance access and rejects the call.
  2. Useless output self-assignments. Statements of the form := DB41.LMN; (with nothing on the left-hand side) are not legal SCL syntax. Even when the left side is supplied, an assignment such as temp := DB41.LMN; from a function or another block merely copies the output of the instance to a temporary, with no effect on the process. These phantom assignments either fail to compile or compile to a no-op sequence that the user mistakes for "the output is not updating".

The SCL call is purely a call operator. The instance DB is the storage for the FB's static variables (the integrator, derivative, GAIN, TI, TD, the working setpoint, etc.). The output values are accessed by reading the instance DB symbols directly, not by listing them in a comma-separated output list as if they were a structured return record.

Affected Versions and Tooling

Component Affected Versions Notes
STEP 7 (Classic) SIMATIC Manager V5.3 – V5.6 (incl. SP) Library import path identical across all versions
SCL Compiler SCL V5.3 – V5.6 Call operator & multi-instance DB binding unchanged
CPU families S7-300, S7-400, S7-400H, WinAC FB41 runs in OB1 / OB35 / OB100; cycle must be honored
Library Standard Library → PID Control Blocks → Blocks (FB41 … FB43) FB41 = CONT_C, FB42 = CONT_S, FB43 = PULSEGEN
Firmware compatibility: FB41 is a software function block, not a CPU instruction. It is delivered as STL source inside the FBD/LAD library. The SCL call pattern in this article applies to any S7-300/400 CPU firmware (V2.x through V4.x for S7-400; all current S7-300 CPUs) that can be configured under STEP 7 V5.x.

Solution 1 — Correct the Instance DB / FB Association

STEP 7 must understand that the instance DB you call from SCL is bound to the same FB type that the SCL call operator names. Pick one of the two equivalent formulations:

Option A — call by FB number (recommended):

FB41.DB41(COM_RST := FALSE,
          MAN_ON   := FALSE,
          PVPER_ON := TRUE,
          P_SEL    := TRUE,
          I_SEL    := TRUE,
          INT_HOLD := FALSE,
          I_ITL_ON := FALSE,
          D_SEL    := TRUE,
          CYCLE    := T#20MS,
          SP_INT   := DB1.set_point_oxig_tanque,
          PV_IN    := 0.0,
          PV_PER   := PIW336,
          MAN      := 0.0,
          GAIN     := 1.43,
          TI       := T#4S,
          TD       := T#1S,
          TM_LAG   := T#2S,
          DEADB_W  := 0.0,
          LMN_HLM  := 100.0,
          LMN_LLM  := 0.0,
          PV_FAC   := 1.0,
          PV_OFF   := 0.0,
          LMN_FAC  := 1.0,
          LMN_OFF  := 0.0,
          I_ITLVAL := 0.0,
          DISV     := 0.0);

PQW336 := DB41.LMN_PER;   // use the output you actually wired

Option B — call by the symbolic name CONT_C: this is only valid if the FB imported from the library is still named CONT_C in your program (i.e. you have not renamed it, and the instance DB is bound to that FB, not to a hand-rolled FB41). If the original FFB was renamed, rename it back to CONT_C in the Properties → General → Name dialog, recompile, and let the wizard re-bind the instance.

The most reliable workflow:

  1. Open the project in SIMATIC Manager and delete the hand-created DB41.
  2. Re-open the library and re-import CONT_C into the S7 program; the wizard will offer to create the instance DB — accept.
  3. Note the DB number the wizard assigned (often DB41, but it may auto-increment). This is now the only DB whose internal structure matches the imported FB.
  4. From SCL, call FB<num>.DB<num>(...) using both numbers — the FB number from the FFB header and the DB number the wizard just assigned.

Solution 2 — Remove the Invalid Output Assignment Block

The original SCL source contained a trailing list:

:= DB41.LMN;        // OUT: REAL
:= DB41.LMN_PER;    // OUT: WORD
:= DB41.QLMN_HLM;   // OUT: BOOL
:= DB41.QLMN_LLM;   // OUT: BOOL
:= DB41.LMN_P;      // OUT: REAL
:= DB41.LMN_I;      // OUT: REAL
:= DB41.LMN_D;      // OUT: REAL
:= DB41.PV;         // OUT: REAL
:= DB41.ER;         // OUT: REAL

These lines are illegal SCL because the assignment operator := requires a target variable on the left. They are also semantically useless: in STEP 7 the instance DB itself is the output of an FB. After the call, every OUT/STAT variable is updated inside the instance DB. Read them from there directly.

Correct SCL pattern for reading a CONT_C output:

// Inside the same block (function / FB / OB):
lm_real  := DB41.LMN;        // manipulated variable, REAL
lm_perc  := DB41.LMN_PER;    // manipulated variable, peripheral WORD
high_lim := DB41.QLMN_HLM;   // high-limit reached
low_lim  := DB41.QLMN_LLM;   // low-limit reached
p_term   := DB41.LMN_P;
i_term   := DB41.LMN_I;
d_term   := DB41.LMN_D;
pv_proc  := DB41.PV;
err_sig  := DB41.ER;
Why outputs look "dead": when only the call and a read of DB41.LMN_PER remain, but PVPER_ON is left at FALSE and the block is in manual (MAN_ON = TRUE) or in reset (COM_RST pulsed), the integrator, derivative, and LMN remain frozen. The next section covers enabling the analog path.

Solution 3 — Enable PV_PER and Apply Correct Scaling

The original code reads PIW336 into PV_PER (the peripheral WORD input) but left PVPER_ON := FALSE. With PVPER_ON = FALSE, CONT_C ignores PV_PER entirely and expects the engineering-unit process variable on PV_IN. That is why PIW336 updates visibly in the watch table while PV, ER, and LMN stay at zero.

To use the analog input directly, set:

FB41.DB41(
  PVPER_ON := TRUE,
  PV_PER   := PIW336,
  PV_FAC   := ,
  PV_OFF   := ,
  ...);

For a 0–27648 raw range scaled to 0–100 % with the built-in 1:1 path (PV_FAC = 1.0, PV_OFF = 0.0) the block does the conversion internally. For a custom engineering range, compute:

PV_FAC = (EU_max - EU_min) / 27648.0;
PV_OFF = EU_min;
//   e.g. 0..20 mg/L:  PV_FAC = 20.0/27648.0, PV_OFF = 0.0

If you would rather scale the raw value yourself in SCL and feed a REAL to PV_IN, keep PVPER_ON := FALSE:

DB41.PV_IN := INT_TO_REAL(WORD_TO_INT(PIW336)) * (20.0/27648.0);

Parameter Mapping Reference

Input / Output Type Default / Typical Engineering Meaning
COM_RST BOOL FALSE Complete restart: clears integrator and derivative
MAN_ON BOOL FALSE TRUE → manual mode; LMN follows MAN
PVPER_ON BOOL FALSE TRUE → process variable from PV_PER (peripheral)
P_SEL / I_SEL / D_SEL BOOL TRUE Enable P / I / D branches
INT_HOLD BOOL FALSE TRUE → integrator output frozen
I_ITL_ON BOOL FALSE TRUE → initialize integrator to I_ITLVAL
CYCLE TIME T#1S Sampling time; must match OB35 call interval if used
SP_INT REAL 0.0 Setpoint in engineering units
PV_IN / PV_PER REAL / WORD 0.0 / W#16#0 Process variable (select with PVPER_ON)
MAN REAL 0.0 Manual manipulated variable
GAIN REAL 1.0 Proportional gain
TI / TD / TM_LAG TIME T#0S Reset / derivative / derivative lag times
DEADB_W REAL 0.0 Deadband width on the error
LMN_HLM / LMN_LLM REAL 100.0 / 0.0 Manipulated-variable high / low limits
PV_FAC / PV_OFF REAL 1.0 / 0.0 Process-variable scale factor / offset
LMN_FAC / LMN_OFF REAL 1.0 / 0.0 Manipulated-variable scale factor / offset
I_ITLVAL / DISV REAL 0.0 Integrator initial value / disturbance variable
LMN REAL (out) Manipulated variable, engineering units
LMN_PER WORD (out) Manipulated variable, peripheral format (0–27648)
QLMN_HLM / QLMN_LLM BOOL (out) Limit reached flags
LMN_P / LMN_I / LMN_D REAL (out) P, I, D branch contributions
PV / ER REAL (out) Scaled PV / error (SP−PV)

Step-by-Step: Rebuilding a Working CONT_C Call in SCL

  1. Create the FB41 instance correctly. In SIMATIC Manager open the program, navigate to Blocks, right-click → Insert New Object → FB, accept the wizard and choose Instance DB. Or, from the Standard Library → PID Control Blocks, drag CONT_C into Blocks and let the dialog create the instance DB. Note both the FB number and the DB number.
  2. Set the call interval. The user is calling from OB35, so CYCLE must equal the OB35 period. Configure OB35 in HW Config → CPU Properties → Cyclic Interrupts to 20 ms and pass CYCLE := T#20MS (the same value the user already used — it is correct).
  3. Pick the PV source. Decide whether you will use the peripheral input (PV_PER with PVPER_ON = TRUE) or a pre-scaled REAL (PV_IN with PVPER_ON = FALSE). For a dissolved-oxygen loop on a 4-wire transmitter, peripheral mode is usually the cleanest.
  4. Write the SCL call. Use the canonical call below as a template. Only assign the inputs you need; everything else takes the FB’s default (which is the same as the original library source).
  5. Wire the output to the actuator. If the actuator is a 4–20 mA output, scale LMN to a peripheral WORD with the block’s LMN_FAC/LMN_OFF and write to PQW336: PQW336 := DB41.LMN_PER;
  6. Initial values. Set COM_RST := FALSE, MAN_ON := FALSE, and INT_HOLD := FALSE for normal automatic operation. Pulse COM_RST for one OB35 cycle to clear the integrator on cold start.
  7. Compile and download. From the SCL editor, File → Compile; resolve any remaining errors (most are unused-variable warnings and can be filtered). Download both the FB41, its instance DB, and the SCL source block to the CPU.

Reference: Minimal Working SCL Function

FUNCTION FC100 : VOID
VAR_TEMP
    info : DWORD;
END_VAR
BEGIN
    // --- PID call (FB41 / CONT_C) ---------------------------------
    FB41.DB41(
        CYCLE    := T#20MS,                  // matches OB35
        SP_INT   := DB1.set_point_oxig_tanque,
        PV_PER   := PIW336,                   // raw 0..27648
        PVPER_ON := TRUE,                     // use peripheral PV
        PV_FAC   := 2.0E-4,                   // (20 - 0) / 27648
        PV_OFF   := 0.0,
        GAIN     := 1.43,
        TI       := T#4S,
        TD       := T#1S,
        TM_LAG   := T#2S,
        LMN_HLM  := 100.0,
        LMN_LLM  := 0.0,
        LMN_FAC  := 276.48,                   // 100% -> 27648
        LMN_OFF  := 0.0);

    // --- Drive the actuator --------------------------------------
    PQW336 := DB41.LMN_PER;

    // --- Optional: forward diagnostic flags -----------------------
    DB1.oxig_high := DB41.QLMN_HLM;
    DB1.oxig_low  := DB41.QLMN_LLM;
    DB1.oxig_pv   := DB41.PV;
    DB1.oxig_err  := DB41.ER;
END_FUNCTION

Verification & Commissioning

  1. Open the project online, then in Blocks → right-click DB41 → Monitor/Modify confirm:
    • SP_INT reflects the setpoint you pass.
    • PV tracks the scaled input. With PVPER_ON := TRUE and a 4–20 mA source, it should sit between PV_LLM and PV_HLM.
    • ERSP_INT − PV.
    • LMN settles to a value between LMN_LLM and LMN_HLM.
  2. Step the loop into manual by setting MAN_ON := TRUE from a VAT/PLC watch table; drive DB41.MAN to 50% and verify that PQW336 follows. Return to auto with MAN_ON := FALSE.
  3. Step the setpoint in steps of 10% and observe the response on PV in the trend. With GAIN = 1.43, TI = 4 s, TD = 1 s you should see a critically-damped response within ~20 s for a first-order plant with a 2–5 s time constant.
  4. Confirm QLMN_HLM / QLMN_LLM only go TRUE at the saturation rails — persistent TRUE indicates that GAIN is too high or the limits are too narrow for the plant.
  5. To capture the issue that prompted this article, add a one-shot pulse on COM_RST in OB100 (cold restart) so the integrator is deterministic after a download.

Troubleshooting Matrix

Symptom Likely Cause Fix
SCL compile error “instruction is unknown” Instance DB not bound to the imported FB Re-import CONT_C, let wizard create DB, call FB<n>.DB<n>
SCL compile error on output list (:= DB41.LMN) No LHS target; output re-assignments are not valid SCL Delete the output list; read DB41.LMN into a local variable
PIW336 changes but LMN / PV stay at 0 PVPER_ON := FALSE while using PV_PER Set PVPER_ON := TRUE or scale the value into PV_IN
LMN_PER = 0 / actuator dead Block in manual (MAN_ON = TRUE) or reset Set MAN_ON := FALSE, COM_RST := FALSE
Oscillation around SP GAIN too high, TI too short Reduce GAIN by 30%, double TI
Slow response, large steady-state error I_SEL := FALSE or TI >> plant time constant Set I_SEL := TRUE, reduce TI
QLMN_HLM sticky, no output above limit LMN_HLM too low for the plant range Raise LMN_HLM to the actuator physical range
CPU goes STOP with OB35 time error CYCLE shorter than OB35 period Set CYCLE = OB35 configured period
LMN_PER is negative (0x8000) Wiring of the peripheral module reversed Swap analog output channel or check HW Config channel

Common Pitfalls and Best Practices

  • Never edit the library FB in place. Always copy CONT_C out of the Standard Library into your project Blocks folder before calling it. Editing the original FFB will break every project that depends on it.
  • Do not rename the FB unless you also rename every call. If you rename CONT_C to PID_OXIG, all SCL calls must be updated; the number (FB41) is fixed at first generation and stays fixed, but the symbol is what SCL resolves at compile time.
  • Match CYCLE to the calling OB. The block internally scales integral / derivative contributions to CYCLE; passing T#1S while calling from a 20 ms OB35 effectively divides the controller gain by 50. This is the single most common cause of "the PID is dead slow" complaints.
  • Keep LMN_HLM and LMN_LLM consistent with the actuator. A 0–100% range matched to a 4–20 mA pneumatic positioner prevents saturation when the controller is forced to LMN = 100% by LMN_OFF/LMN_FAC mismatch.
  • Use DISV for feedforward. Wiring a flow-based disturbance into DISV lets the FB41 substract it from the manipulated variable directly, decoupling the integrator from the disturbance.
  • Capture PV, ER, and LMN in a trace for tuning — STEP 7's Commissioning → PID Control tool can auto-tune GAIN/TI/TD from a step response, but only if the loop is in auto and the load is stationary.
Safety warning: the PID controller will drive the output to LMN_HLM whenever the error demands it. If the actuator downstream of PQW336 is a pneumatically-actuated valve in a closed system, wire a hardwired high-pressure interlock in series with the I/P transducer — do not rely on the software high-limit alone for overpressure protection.

Related Documentation

Why does SCL say “instruction is unknown” when I call CONT_C.DB41?

The CONT_C FFB in your S7 program is registered internally as FB41, but the DB41 you hand-created is not bound to it. Re-import CONT_C from the Standard Library so the wizard generates a matching instance DB, then call FB41.DB<num>(...) with both the FB and DB numbers STEP 7 assigns.

What is the difference between calling CONT_C.DB41 and FB41.DB41 in SCL?

They are equivalent only when the FB’s symbolic name is still CONT_C. If you renamed the FFB, the symbolic form no longer resolves. The numeric form FB41.DB41 works regardless of the FB’s name and is the safer choice for long-term maintainability.

How do I read LMN, LMN_PER, and PV from CONT_C?

After the call, read them from the instance DB. Use DB41.LMN (REAL, engineering units), DB41.LMN_PER (WORD, 0–27648), DB41.PV (scaled process variable) and DB41.ER (setpoint minus process variable). Do not list them in a comma-separated output block on the right-hand side of the call.

Why does my output stay at zero even though PIW336 changes?

You are passing the raw value to PV_PER while PVPER_ON is FALSE. Set PVPER_ON := TRUE when the process variable comes from a peripheral input, or pre-scale the value and write it to PV_IN with PVPER_ON := FALSE.

What value should I use for the CYCLE parameter?

Pass the exact sampling period of the OB that calls CONT_C. If OB35 is configured for 20 ms in HW Config, set CYCLE := T#20MS. Mismatching CYCLE and the calling OB period will scale the integrator and derivative by the ratio, which is the most common cause of a “dead” or “wildly oscillating” loop.

Back to blog