Most people open TIA Portal and start dragging contacts. But a Siemens CPU has opinions about when your code runs — a startup pass, a main cycle, and interrupts that barge in mid-scan. Miss that and your logic is correct yet your machine misbehaves. We'll learn it by guarding a railway level crossing, where "when" is the whole job.
Before a single rung executes, the CPU runs OB100 (Startup) once. Then it loops forever through three steps. The catch: that loop takes a few milliseconds, and the time wobbles with how much code runs — so anything that needs exact timing, or a reaction faster than one loop, cannot live here alone.
🚆 Problem 1 — too slow to wait. A train trips the approach sensor. If that wait sits in OB1, the CPU might be busy at the far end of its scan. At line speed, even a few milliseconds of "I'll get to it" is metres of track.
🔁 Problem 2 — must be exact. The warning lights must flash at a precise, regulated rate. If their blink depends on a scan time that drifts from 4 ms to 9 ms, the cadence drifts too. Unacceptable for a signal.
Siemens' answer is Organization Blocks — extra entry points the operating system calls on its own schedule, at higher priority than OB1. Let's watch three of them run a crossing.
Below is the crossing up top and a logic-analyzer view of the CPU itself underneath — exactly what an engineer watches in TIA. OB1 runs the gate sequence in the gaps. OB30 fires on an exact 500 ms drumbeat to flash the lamps. Hit 🚆 Send a train and OB40 slams in the instant the sensor trips — preempting whatever OB1 was doing. Watch the lanes: higher priority always cuts the line.
👀 The lamps blink on the yellow OB30 ticks — rock-steady, because the OS fires that block on a clock, not on your scan. When the train hits, the red OB40 bar appears on top of the petrol OB1 bar: OB1 was literally paused mid-instruction so the crossing could react now, not next cycle.
A rail line has many crossings, all behaving identically. You don't copy-paste the logic — you write it once as a Function Block, FB_Crossing, then call it twice. Each call gets its own instance DB: a private notebook holding that crossing's state. Same code, separate memory. Send a train through one and watch only its notebook change.
// written ONCE — the behaviour of any crossing FUNCTION_BLOCK "FB_Crossing" VAR gateState : Int; stepTimer : Time; lampPhase : Bool; trainPresent : Bool; END_VAR // called TWICE — each call carves out its own instance DB "Main_St"(); // instance DB10 — Hauptstraße crossing "Bahnhof"(); // instance DB11 — Bahnhofweg crossing
This is the whole point of an FB: the code is shared, the data is not. Fix a bug in FB_Crossing once and every crossing on the line inherits the fix — yet each keeps its own gateState, timer and train flag. An FC couldn't do this: it has no instance DB, so it remembers nothing between scans.
Here's the thing that trips up everyone arriving from older Siemens PLCs. On the S7-1200/1500, data blocks are optimized by default: variables have no fixed byte address — you reach them by name only, and the compiler packs them for speed. Flip a DB to standard (the old way) and every variable gets a frozen byte.bit offset you can address absolutely… until you add one variable and the whole map shifts. Toggle it.
✅ Optimized — no offsets, symbolic only ("Main_St".gateState). Faster, no wasted bytes, and you can add variables without shifting anything or forcing a re-init. Siemens' default and recommended path.
⚠️ Standard — fixed offsets let you use absolute addresses like %DB10.DBX4.0 and tools that need them — but BOOLs waste bytes, and inserting a variable renumbers everything below it. That's how a working HMI tag silently starts reading the wrong value.
1 · Your warning lamps must flash at a dead-steady 1 Hz no matter how heavy the program gets. Where does that logic belong?
2 · You add one BOOL to the top of a standard DB that an HMI reads by absolute address %DB10.DBX4.0. After download, what happens?