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.
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:
-
Instance DB / FB type mismatch. The
CONT_CFFB imported from the library registers itself internally as FB41. The user-createdDB41was, 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 encountersCONT_C.DB41(...)the binding between the symbolic nameCONT_Cand the instance DB number is broken, so the compiler cannot resolve the multi-instance access and rejects the call. -
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 astemp := 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 |
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:
- Open the project in SIMATIC Manager and delete the hand-created DB41.
- Re-open the library and re-import
CONT_Cinto the S7 program; the wizard will offer to create the instance DB — accept. - 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.
- 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;
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
-
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_Cinto Blocks and let the dialog create the instance DB. Note both the FB number and the DB number. -
Set the call interval. The user is calling from OB35, so
CYCLEmust equal the OB35 period. Configure OB35 in HW Config → CPU Properties → Cyclic Interrupts to20 msand passCYCLE := T#20MS(the same value the user already used — it is correct). -
Pick the PV source. Decide whether you will use the peripheral input (
PV_PERwithPVPER_ON = TRUE) or a pre-scaled REAL (PV_INwithPVPER_ON = FALSE). For a dissolved-oxygen loop on a 4-wire transmitter, peripheral mode is usually the cleanest. - 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).
-
Wire the output to the actuator. If the actuator is a 4–20 mA output, scale
LMNto a peripheral WORD with the block’sLMN_FAC/LMN_OFFand write toPQW336:PQW336 := DB41.LMN_PER; -
Initial values. Set
COM_RST := FALSE,MAN_ON := FALSE, andINT_HOLD := FALSEfor normal automatic operation. PulseCOM_RSTfor one OB35 cycle to clear the integrator on cold start. - 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
- Open the project online, then in Blocks → right-click DB41 → Monitor/Modify confirm:
-
SP_INTreflects the setpoint you pass. -
PVtracks the scaled input. WithPVPER_ON := TRUEand a 4–20 mA source, it should sit betweenPV_LLMandPV_HLM. -
ER≈SP_INT − PV. -
LMNsettles to a value betweenLMN_LLMandLMN_HLM.
-
- Step the loop into manual by setting
MAN_ON := TRUEfrom a VAT/PLC watch table; driveDB41.MANto 50% and verify thatPQW336follows. Return to auto withMAN_ON := FALSE. - Step the setpoint in steps of 10% and observe the response on
PVin the trend. WithGAIN = 1.43,TI = 4 s,TD = 1 syou should see a critically-damped response within ~20 s for a first-order plant with a 2–5 s time constant. - Confirm
QLMN_HLM/QLMN_LLMonly go TRUE at the saturation rails — persistent TRUE indicates thatGAINis too high or the limits are too narrow for the plant. - To capture the issue that prompted this article, add a one-shot pulse on
COM_RSTin 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_Cout 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_CtoPID_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; passingT#1Swhile 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%byLMN_OFF/LMN_FACmismatch. -
Use
DISVfor feedforward. Wiring a flow-based disturbance intoDISVlets the FB41 substract it from the manipulated variable directly, decoupling the integrator from the disturbance. - Capture
PV,ER, andLMNin 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.
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
- Siemens: Standard PID Control (FB41 / FB42 / FB43) reference manual
- Siemens: SCL Programming Manual (S7-300/400)
- Siemens Support: Calling SFB41 / PID in SCL — is not working
- Siemens: PID Self-Tuner (FB58 / FB59) overview
- Siemens: S7-300 CPU 31x/31xP, 3x7-2PN/DP Operating Instructions (OB35 description)
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.