1. Problem Description
On Siemens S7-300 and S7-400 controllers programmed in STEP 7 V5.x, a programming error such as an invalid indirect pointer, an out-of-range DB number, or a typed access violation against a non-instantiated DB triggers the operating system to call OB 121 (Programming Error Organization Block). The CPU enters STOP if OB 121 is not loaded, or executes OB 121 and continues the cycle if the OB is present.
Unlike a peripheral-access fault (OB 122) or a time-of-day fault (OB 80-87), OB 121 only supplies a small set of temporary local variables. These variables identify the type and number of the block in which the error occurred - but only when the error is reproducible on a static call. In production programs that use indirect and dynamically-parameterized calls, the populated values are frequently useless: the runtime system clears the registers before control is transferred to the user code in OB 121, or the block identifier refers to the calling FB while the fault is actually inside a deeply-nested FC, SFC, or instance DB. The engineer sees OB 121 fire, looks at the diagnostic buffer, and cannot determine which user block produced the bad address.
The symptom on the field is well known:
- CPU stays in RUN but a SF LED lights, the diagnostic buffer shows
Event ID 0x2521 / 0x2571(programming error), andOB 121is listed as the response. - Cycle time stays within limits; no other OBs fire.
- Reproduction is impossible at the desk because the fault only happens during specific runtime sequences (recipe change, dynamic multi-instance selection, batch indexing).
- Online monitoring in STEP 7 shows valid data in OB121_BLK_TYPE / OB121_BLK_NUM that points to the wrong block.
The objective is to determine, at the moment OB 121 begins executing, which user block was the most recent active code path when the indirect-access error was raised.
2. OB 121 Architecture and Temporary Variables
According to the Siemens Industry Online Support entry 109742272 - Programming error OB (OB 121) for S7-300/S7-400, OB 121 is called by the CPU operating system when a programming error is detected during execution of the user program. The OB must be loaded on the CPU; otherwise the CPU transitions to STOP with diagnostic buffer entry "STOP because programming error OB (OB 121) not loaded".
OB 121 publishes the following temporary (L-stack) variables at the start of its priority class:
| Variable | Type | Description | Reliability on S7-300 |
|---|---|---|---|
| OB121_EV_CLASS | BYTE | Event class / identifier (B#16#35 = outgoing, B#16#39 = incoming programming error) | Reliable |
| OB121_SW_FLT | BYTE | Software fault code (B#16#22, B#16#23, B#16#24, B#16#25, B#16#26, B#16#27, B#16#28, B#16#29, B#16#30, B#16#31, B#16#32, B#16#33, B#16#34, B#16#35) | Reliable |
| OB121_FLT_REG | WORD | Fault register - source-dependent (accumulator-2 or status word bits) | Conditional - not always meaningful for indirect addressing errors |
| OB121_REQ_OB | BYTE | Number of the OB that was being executed when the fault occurred | Reliable - typically 1 (OB 1) or the cyclic-interrupt OB |
| OB121_BLK_TYPE | BYTE | Block type where the fault was detected (B#16#08 = OB, B#16#0A = DB, B#16#0C = FC, B#16#0E = FB, B#16#10 = SDB) | UNRELIABLE on S7-300 with indirect addressing |
| OB121_BLK_NUM | INT | Block number where the fault was detected | UNRELIABLE on S7-300 with indirect addressing |
| OB121_PRG_ADDR | WORD | Program address (relative to start of block) of the faulty instruction | Conditional - resolves only for static, fully-qualified addresses |
| OB121_DATE_TIME | DATE_AND_TIME | Date and time OB 121 was called | Reliable (CPU real-time clock) |
The S7-300 CPU series (CPU 312 through CPU 319) does not maintain the same level of source-code cross-reference as the S7-400 when the error arises from an indirect memory operation, a register-indexed pointer, or a pointer-to-DB parameter. The CPU only knows the absolute address that faulted, not the symbolic block and instance. On the S7-400 (CPU 41x and CPU 41xH), the situation improves with several firmware versions because the operating system records a more complete context, but engineers continue to report cases where the block identifier is blank or wrong when a multi-instance FB uses ANY/POINTER parameters.
3. Root Cause: Why the Standard Diagnostics Fail
Three conditions in combination defeat the standard OB 121 variable block identification on the S7-300:
-
Indirect addressing does not carry a block tag. Instructions like
L DBW[MD 200],AUF DB[MW 100], orCALL FB [MD 50], DB [MD 54]use runtime-computed numbers. The CPU can only flag the address register or status word that the decoder used; it cannot roll the call stack back to the symbolic origin. - The L-stack is shallow on S7-300. The CPU's interrupt acceptance frame on S7-300 is limited. The OS captures the topmost block number, but on a static call chain the topmost block is the one whose call instruction itself failed. If the failing instruction is inside a sub-FC, the captured value is the FC, not the calling FB that produced the bad parameter - so the error appears in the wrong layer.
- Multi-instance FBs share a parent DI. When a multi-instance FB calls an FC that opens DB[AR1,P#0.0] and the offset is wrong, the OS reports the FC (or DB) number but cannot resolve the parent FB's instance name. Engineers tracing the error are forced to read disassembly of the STL, which is rarely useful in a running plant.
The only way to recover the calling context on the S7-300 is to record the block identity at the source, i.e. inside the block itself, before the indirect operation executes.
4. Solution Approach 1: Block-Marker Trap (Last-Caller Register)
The reliable engineering pattern is a block-marker trap: at the entry of every FB, FC, and multi-instance block that performs indirect access, write a small marker code into a global memory word (or a structured DB) that OB 121 can read. The marker uniquely identifies the block (type + number) and optionally the segment / network number so the engineer can narrow the search further.
4.1 Marker Encoding Scheme
Use a 16-bit WORD (two bytes). Define the high byte as the block type code and the low byte as the block number modulo 250. For block numbers that exceed 250 (FBlist, FClist), or to add a segment index, extend to a DWORD or to a small DB with a fixed layout.
| Block type | High byte (B#16#) | Example block | Encoded marker (hex) | Decimal representation |
|---|---|---|---|---|
| OB | B#16#01 | OB 1 | W#16#0101 | 257 |
| OB (cyclic) | B#16#01 | OB 35 | W#16#0123 | 291 |
| FB | B#16#02 | FB 100 | W#16#0264 | 612 |
| FB (multi-instance) | B#16#02 | FB 303 | W#16#012F | 303 (use concatenation: 0x02 + 0x2F = 0x022F = 559) |
| FC | B#16#03 | FC 17 | W#16#0311 | 785 |
| DB (instance) | B#16#04 | DB 200 | W#16#04C8 | 1224 |
| DB (global) | B#16#05 | DB 500 | W#16#05C8 | 1480 |
| SFB | B#16#06 | SFB 0 | W#16#0600 | 1536 |
| SFC | B#16#07 | SFC 20 | W#16#0714 | 1812 |
For block numbers larger than 250, split the marker into a WORD plus a separate bit/byte: bit-mask high bits in MW + low 8 bits in the same word. In practice most S7-300 programs use FBs and FCs below 250, so the WORD is sufficient.
4.2 The Marker Write
Insert the marker write in Network 1 / Segment 1 of every block that performs indirect access. Use STL or LAD. The write must be the first executable instruction, before the indirect call, before AUF, and before L DBW[xxx].
STL example - inside FB 303, segment 1:
NETWORK 1 / SEGMENT 1 - BLOCK MARKER
L B#16#02 // FB type code
T MB 200 // store in marker byte 200 (high byte)
L 303 // FB number
T MB 201 // store in marker byte 201 (low byte)
NETWORK 2 - PARAMETER VALIDATION
L #iIndex // indirect index input
L 0
<I // is it negative?
SPB ERR // yes -> jump to error handling
L #iIndex
L 100
>I // greater than array size?
SPB ERR // yes -> error
JU NRM
ERR: L 0
T #iIndex // safe to zero
BEA // block end (OB 121 will NOT fire here)
NRM: ...
LAD example - in the temporary local variables interface:
Network 1: Block marker
MOVE B#16#02 // FB code
MB200
MOVE 303 // FB number
MB201
4.3 The Marker Read in OB 121
OB 121 is loaded and edited exactly like any other block. In STEP 7 V5.x, double-click OB 121 in the project tree, choose STL or LAD, and read the marker at the very top. Then jump to error handling that copies the marker to the diagnostic buffer, sends a telegram, or increments a counter.
STL inside OB 121, Network 1:
NETWORK 1 - READ BLOCK MARKER
L MW 200 // last-caller marker
T MW 300 // copy to diagnostic word
L MB 201 // low byte = block number
T MB 301
L MB 200 // high byte = block type code
T MB 302
NETWORK 2 - CLASSIFY THE TYPE
L MB 302
L B#16#02
==I
JC FB_DETECTED
L MB 302
L B#16#03
==I
JC FC_DETECTED
JU UNKNOWN
FB_DETECTED: S M 310.0 // "FB fault" flag
JU DIAG_STORE
FC_DETECTED: S M 310.1 // "FC fault" flag
JU DIAG_STORE
UNKNOWN: S M 310.7
DIAG_STORE: CALL SFC 20 // BLKMOV into diagnostic DB
srcblk := P#M 200.0 BYTE 8
dstblk := P#DB300.DBX 0.0 BYTE 8
RET_VAL:= MW 304
NETWORK 3 - CLEAR FAULT REGISTERS
L 0
T OB121_FLT_REG // user has read it; clear
L 0
T OB121_BLK_NUM // ditto
The above pattern stores the last-caller into DB 300 (a dedicated diagnostic DB), so the engineer can inspect it online in STEP 7 with a watch table without disturbing the cyclic scan.
5. Solution Approach 2: OB121_FLT_REG Analysis
For static-call errors that do happen to leave valid values in OB121_BLK_TYPE and OB121_BLK_NUM, use the standard temporary variables directly. The Siemens KB 109742272 lists the fault codes and the corresponding meaning. The useful ones for block identification are:
| OB121_SW_FLT | Fault class | What OB121_FLT_REG / OB121_BLK_TYPE / OB121_BLK_NUM represent |
|---|---|---|
| B#16#22 | I/O area access error | FLT_REG: faulty input/output area ID; BLK_TYPE/BLK_NUM: relevant block |
| B#16#23 | Timeout during TP/TR/TO | Not relevant for block ID |
| B#16#24 | Nested error during DB access | FLT_REG: address that failed; BLK_TYPE/BLK_NUM: the DB or its caller |
| B#16#25 | Nested error during FB / multi-instance | FLT_REG: status word; BLK_TYPE/BLK_NUM: parent FB |
| B#16#26 | DB not loaded / wrong type | FLT_REG: address; BLK_NUM: the DB number that was missing or wrong |
| B#16#27 | Wrong number of the FBs / FCs | BLK_NUM: the called but missing block |
| B#16#28 | Load/transfer to a non-instantiated instance | BLK_NUM: the requested instance DB |
| B#16#29 | Pointer error in L / T / OPN DB | FLT_REG: address register or status word |
| B#16#30 | DB loaded as instance of wrong FB | BLK_TYPE/BLK_NUM: the FB that did not match |
| B#16#31 | Wrong type of FB / FC / DB | BLK_TYPE: identifier of the wrong type; BLK_NUM: the conflicting number |
| B#16#32 | Pointer parameter error (any) | FLT_REG: error bit-field of the ANY |
For these fault classes, a small ladder network in OB 121 that simply copies OB121_BLK_TYPE to MB 302 and OB121_BLK_NUM to MW 304 yields enough information without any block marker. This is the path the TIA Portal F1-Help suggests when you search for OB121_FLT_REG.
6. Solution Approach 3: Segment-Level Debuggable Marker
The original case (from the user) added a second marker dimension: the segment index within the block. This is done by writing the active segment number into a separate marker byte after each potentially failing indirect access, or - more compactly - into a local variable that OB 121 does not see, so the engineer can correlate the marker in MB 201 with the segment number when reviewing the code offline.
// In every block, declare a STAT variable
VAR STAT
sSeg : INT ; // current segment (1, 2, 3, ...)
END_VAR
// Network N (the one that does the indirect access)
L 17 // segment number 17
T #sSeg // store
L DBW[#iIndex] // indirect access (the one that fails)
T MW 120 // result
When OB 121 fires, the engineer reads MB 200 (type) / MB 201 (block number) and then opens the block, jumps to the segment indicated in the variable. With this technique the search is reduced from "every indirect access in every block" to "one segment in one block".
7. Implementation Procedure in STEP 7 V5.x
-
Create a diagnostic DB. Insert a new DB (e.g. DB 300) and define a data structure with at least:
lastBlockType : BYTE,lastBlockNum : INT,lastSegment : INT,faultCounter : DINT,lastFaultCode : BYTE,lastDateTime : DATE_AND_TIME. Mark DB 300 as non-retain (no check in the properties). - Edit OB 121. In the project's Blocks container, double-click OB 121. If OB 121 does not yet exist, right-click the Blocks folder and choose Insert New Object > Organization Block, select Programming error OB, and confirm.
- Write the read-and-classify logic as in section 4.3 above. Save and download OB 121 to the CPU.
- Annotate every suspect block. For each FB and FC that contains an indirect access, insert a block-marker write as in section 4.2. Save and download.
- Build a watch table. In SIMATIC Manager, right-click the Blocks folder, choose Insert New Object > VAT. Add the diagnostic DB and the marker MW 200 / MW 201. Open the watch table online (Monitor/Modify).
- Trigger the fault and observe. Reproduce the original runtime sequence. When the CPU executes OB 121 the watch table freezes on the last-caller marker. The block number in MB 201 is your starting point.
- Fix and verify. Add the missing range check, the wrong-number handling, or the missing DB. Remove the marker writes after commissioning if they cause measurable cycle-time impact, or guard them with a debug bit.
8. Optional: Diagnostic Buffer and SFC 17 / SFC 18
OB 121 entries already populate the diagnostic buffer. To extract them programmatically, use SFC 17 "ALARM_S" and SFC 18 "ALARM_SQ" only if you are prepared to configure the message texts. The simpler path is SFC 6 "RD_SINFO" which reads the start and current event information of the OB that is currently active:
// Inside OB 121, network 5
CALL SFC 6 // RD_SINFO
ret_val := MW 308
top_inf := P#DB300.DBX 50.0 BYTE 32
start_inf:= P#DB300.DBX 82.0 BYTE 32
After the call, the diagnostic DB contains 32 bytes of start-event information (block type, block number, OB number, address of the fault) that survives the OB exit and is available in the watch table for the operator to copy into the maintenance log.
9. Verification Steps
- From the offline source, count the number of blocks you have annotated. The count must match the number of active blocks (loaded on the CPU) that perform indirect access. Open the cross-reference (Ctrl+Shift+F4) in STEP 7 V5.x and verify.
- Force a known fault. Open an FC that writes a marker, and call it with a deliberately bad pointer (e.g.
L 0thenL DBW[MW 0]with no DB open). The watch table should show MB 200 = B#16#03 (FC code), MB 201 = the FC number, and DB300.lastBlockNum filled. - Check the diagnostic buffer: CPU > Diagnostic Buffer in STEP 7. The last entry should be
Programming error OB 121with the block you forced, not the OB itself. - Confirm that the cycle time before and after enabling the marker trap is within the project's reserve (the two moves per block are < 5 microseconds each on a CPU 315-2 PN/DP).
- Force a multi-instance failure. Open a multi-instance FB, replace the inner instance DB number with a non-existent one, and call. The marker should reflect the parent FB, not the missing instance.
10. Comparison with S7-400 and TIA Portal
| Aspect | S7-300 + STEP 7 V5.x | S7-400 + STEP 7 V5.x | S7-300/400 with TIA Portal V15+ |
|---|---|---|---|
| OB 121 temp variables reliable for indirect errors | Often empty / wrong | Improved from CPU 414 onward | Same L-stack semantics; no improvement |
| Diagnostic buffer entry quality | OK - lists OB 121 response | Adds cross-reference to FB/DB | Same as V5.x |
| GetDiag / SFC 187 use in OB 121 | Not supported on S7-300 | Supported from CPU 414-3 PN/DP V6.0+ | Same as S7-400 |
| Block-marker trap pattern | Required | Optional but faster than GetDiag in some cases | Required for legacy blocks |
| STEP 7 Professional / TIA supports online marker reading in OB | Yes | Yes | Yes |
If the project is being migrated to TIA Portal, the same OB 121 structure applies - TIA Portal does not magically fill OB121_BLK_TYPE/BLK_NUM with the originating instance on the S7-300. The block-marker trap remains the only reliable way to identify the calling context.
11. Common Pitfalls
- Placing the marker write behind a conditional jump. The marker is only updated on the path that the jump selects. If the indirect access is in the unselected path, the marker still shows the last block from the other path. Place the marker write unconditionally as the first statement.
- Re-using the marker bytes for other purposes. Many projects use MW 200 for HMI handshake. Reserve a separate MW (e.g. MW 280-290) and document it in the project.
- Forgetting instance DBs. A multi-instance FB does not call its instance DB explicitly. The marker must be set in the FB, and the parent FB's number must be encoded, not the instance DB number.
-
Removing the trap after the fault is found. The trap is cheap and the next indirect access regression will require it again. Either leave it on guarded by a debug bit, or wrap it in
IF #DEBUG_MODE THENwith aEND_IFin SCL. - Mis-reading OB121_FLT_REG as the block number. The FLT_REG is a status word that contains bit-level error information; it is not the block number. The block number is in OB121_BLK_NUM, which is a different temporary variable.
12. Field-Proven Caveats
- On CPU 315-2 PN/DP with firmware V2.x, OB 121 is called but the L-stack is partially overwritten by the second instruction in the same network. Always read OB121_BLK_TYPE and OB121_BLK_NUM in network 1 of OB 121, before any CALL or L-stack-consuming instruction.
- On CPU 317-2 PN/DP, multi-instance FBs share the DI register; the parent FB number is recoverable from the AR2 register's DI offset only in a few firmware versions. The block-marker trap bypasses this fragility.
- For plants with safety-integrated CPUs (CPU 315F-2 PN/DP), OB 121 is also called for safety-program errors but with a separate priority. Use OB 123 (for fail-safe blocks) and a parallel trap. The two OBs are independent; treat them as separate debug entry points.
- For S7-400H (redundant) systems, the diagnostic buffer is mirrored between both CPUs. Use only the active CPU's OB 121 marker. The standby CPU's OB 121 is not executed; it remains in the buffer of the standby for post-failover analysis.
13. Glossary
| Term | Definition |
|---|---|
| OB 121 | Programming error OB; called when the CPU detects a faulty instruction such as a wrong DB number, an undefined FC/FB, or an indirect addressing error. |
| Indirect addressing | Address calculated at runtime: e.g. L DBW[MW 100], AUF DB[MD 200]. |
| Multi-instance FB | FB whose instance DB is contained in the instance DB of a parent FB. Common in modular code. |
| Block-marker trap | A pattern in which the block under inspection writes a type+number code into a global MW at the start of its code, so OB 121 can read it. |
| Last-caller | The block that most recently updated the marker, i.e. the block in which the fault occurred. |
| SF LED | System Fault indicator on the CPU front panel. Lights for OB 121, OB 122, OB 80, and similar error events. |
| Diagnostic buffer | Ring buffer in the CPU that records all system events with a timestamp. Visible via STEP 7's online diagnostics or via the Web server (S7-300 PN/DP and all S7-400). |
14. Related Siemens Documentation
For further reading, the following official Siemens resources cover OB 121 and the diagnostic interface:
- Programming error OB (OB 121) for S7-300/S7-400 (SIOS entry 109742272)
- S7-300 CPU 31xC and CPU 31x operating instructions (SIOS entry 13008499)
- S7-400 CPU 41x operating instructions (SIOS entry 33266799)
- STEP 7 V5.x Online Help: keyword OB 121
- SIMATIC S7-300/400 Standard Software - System and Standard Functions (Siemens manual, 6ES7398-8FA10-8AB0)
What is the most reliable temporary variable in OB 121 on an S7-300?
OB121_EV_CLASS (event class), OB121_SW_FLT (software fault code), OB121_REQ_OB (OB that was active), and OB121_DATE_TIME are always reliable. OB121_BLK_TYPE and OB121_BLK_NUM are not reliable when the fault comes from an indirect address; OB121_PRG_ADDR only resolves for static, fully-qualified addresses.
How do I know which FB or FC was the last active block before OB 121 fired?
Use a block-marker trap: in the first network of every FB/FC that performs indirect access, write the block type code (B#16#02 for FB, B#16#03 for FC, B#16#01 for OB) into MB 200 and the block number into MB 201. In OB 121 network 1, read MW 200 and copy it to a diagnostic DB before any other instruction touches the L-stack.
Does TIA Portal fix the OB121_BLK_NUM issue for S7-300 CPUs?
No. TIA Portal uses the same CPU firmware semantics as STEP 7 V5.x for S7-300 and S7-400. The L-stack contents of OB 121 are not improved by the programming environment. The block-marker trap pattern works identically in both STEP 7 V5.x and TIA Portal V15+.
Can I read the L-stack of the calling block from inside OB 121?
No. The L-stack is private to the OB's priority class. The calling block's temp variables are no longer accessible once the OS switches to OB 121. The marker pattern is the only way to make the calling context visible at OB 121 entry.
What is the difference between OB 121 and OB 122?
OB 121 is the programming error OB (faulty instruction, missing DB, wrong type, indirect addressing error). OB 122 is the I/O access error OB (peripheral read/write to a missing or faulty module). Both share the same priority class on most S7-300 CPUs; on S7-400, OB 122 has a higher priority than OB 121.
Why does my watch table in STEP 7 show the wrong block number in MB 201?
Either another block (system FB, SFC, or HMI handshake) is overwriting MB 200/201 between the trap write and OB 121 entry, or the failing block is not annotated. Move the marker MW to a reserved range (e.g. MW 280) and verify with the cross-reference that only the suspect blocks write it.