PLC Book Part IV — Industrial Implementation & Modern Systems Chapter 15
Chapter 15 Part IV · Industrial Implementation & Modern Systems Advanced ⏱ 75 min read ✦ 12 PLC Programs

15

ControlLogix Controllers

For fourteen chapters we’ve used SLC-500 style addressing — N7:0, B3:0/0, T4:0.DN — because that scheme is the easiest place for a beginner to start. But the modern Rockwell platform is ControlLogix, and ControlLogix doesn’t work that way. Tags replace files. UDTs (User-Defined Types) replace fixed memory layouts. The project organises into Tasks → Programs → Routines instead of one big linear program. And FBD (Function Block Diagram) joins ladder as a first-class language. This chapter is the bridge: a six-part deep-dive into memory and project organization, bit-level programming with named tags, timers and counters with TIMER and COUNTER structures, math/compare/move in tag form, and the FBD language that opens the door to advanced process control. Twelve programs reimplement familiar patterns in ControlLogix syntax — and add new ones that only ControlLogix makes possible.

What you’ll be able to do after this chapter

Your goals for this chapter:

  • Read and write tag-based addressing fluently — Motor1.Run, Recipe[2].Temperature, FillTimer.DN.
  • Define User-Defined Types (UDTs) and use them to package related data into reusable structures.
  • Organise a project into Tasks, Programs, and Routines, and choose appropriate scopes for tags.
  • Use ControlLogix bit instructions (XIC, XIO, OTE, OTL, OTU, ONS) with named tags.
  • Configure TON, TOF, and RTO timers using TIMER tags and the .PRE/.ACC/.EN/.TT/.DN members.
  • Configure CTU and CTD counters using COUNTER tags and reset them with RES.
  • Apply math, comparison, and move instructions including the powerful CPT for compound expressions.
  • Read and write Function Block Diagram (FBD) programs, including PIDE blocks for enhanced PID control.
  • Choose between Ladder, FBD, ST (Structured Text), and SFC (Sequential Function Chart) for different problem types.
  • Migrate an existing SLC-500 program to ControlLogix and recognise the conversion patterns.

Key Concepts & Terms

ControlLogix · Logix5000 · Studio 5000 Tag · Tag-based addressing Atomic data type BOOL · SINT · INT · DINT · LINT REAL · LREAL · STRING UDT · User-Defined Type Structured tagMember Array · Index Controller scope · Program scope Task · Program · Routine Continuous · Periodic · Event task Main routine · Subroutine · JSR XIC · XIO · OTE OTL · OTU · ONS TIMER tag · TON · TOF · RTO .PRE · .ACC · .EN · .TT · .DN COUNTER tag · CTU · CTD · RES .CU · .CD · .OV · .UN ADD · SUB · MUL · DIV · CPT MOV · COP · CLR · FLL EQU · NEQ · GRT · LES · GEQ · LEQ · LIM FBD · Function Block Diagram Sheet · Block · Connection PIDE · Enhanced PID ST · Structured Text SFC · Sequential Function Chart Migration from SLC-500
Section 15.1 · Part 1 of 6

Memory and Project Organization

The single biggest conceptual leap from SLC-500 to ControlLogix isn’t a new instruction or a new language — it’s a completely different model of how memory works. SLC organises memory into fixed files (B3 for bits, T4 for timers, F8 for floats, and so on). ControlLogix organises memory into named tags of arbitrary types, including types you define yourself. Once you internalise this, everything else follows.

From files to tags — the conceptual leap

In SLC-500, the address N7:50 means “word 50 of integer file N7.” The address tells you where the data lives in memory. If the programmer uses N7:50 for tank temperature today, that’s a comment they wrote — the controller has no idea. Six months later when someone reorganises memory, every reference to N7:50 still points to the same physical location, but the meaning has silently changed.

In ControlLogix, the address is TankTemperature. The name itself carries the meaning. The compiler decides where in memory it actually lives, and that decision can change between downloads without breaking your program. Every reference is to the named tag, not to a physical location. Tags name what the data is, not where it is.

SLC-500 vs CONTROLLOGIX — TWO MODELS OF MEMORY Same logical data, two completely different ways of addressing it. SLC-500 — file-based Fixed files. Address = location. B3:0/0 → “Motor 1 run” (we hope!) N7:50 → “Tank temperature” T4:5.DN → “Fill timer done” F8:10 → “Pressure setpoint” Compiler sees: numbers, not meanings Re-arrange memory? Comments lie. ControlLogix — tag-based Named tags. Address = meaning. Motor1.Run → a BOOL TankTemperature → a REAL FillTimer.DN → TIMER member PressureSetpoint → a REAL Compiler sees: names with types Memory is the compiler’s problem.

Figure 15.1 — Two memory models comparedSLC-500 addresses identify where data lives; ControlLogix tags identify what data is. The leap from one to the other reshapes how you write, read, and maintain programs. Every example after this point in the book will use ControlLogix tag syntax.

Atomic data types

Every ControlLogix tag has a defined data type. The atomic types — the basic building blocks — are:

TypeSizeRangeUse for
BOOL1 bit0 or 1Single bits — motor run, sensor active, alarm latch
SINT8-bit signed integer−128 to +127Small counts, byte communication
INT16-bit signed integer−32 768 to +32 767Most integer data, raw analog values
DINT32-bit signed integer±2.1 billionDefault integer type — counters, indexes, big numbers
LINT64-bit signed integer±9 quintillionTimestamps (microseconds), big counters
REAL32-bit IEEE float~7 sig digitsEngineering values — temperatures, pressures, scaled signals
LREAL64-bit IEEE float~15 sig digitsHigh-precision math, GPS coordinates
STRINGvariableup to 82 charsRecipe names, batch IDs, text messages

Default to DINT for integer counters and indexes

ControlLogix processes 32-bit operations as fast as 16-bit operations on its native CPU, so there’s no speed reason to choose INT over DINT. Use DINT unless you have a specific reason — usually fitting into a particular memory layout for SCADA tag mapping or a data structure that explicitly uses INT.

UDTs — your own data types

The killer feature of ControlLogix is the User-Defined Type (UDT). Instead of scattering ten related variables across ten different tags, you bundle them into one structured type. Every motor in your plant, for example, has the same essential data: a Run command, a Stop command, a Fault status, a Speed setpoint, an actual Speed feedback, and accumulated Hours. Define one Motor UDT containing those six members; declare ten Motor tags (Pump1, Pump2, Conveyor, Mixer, ...); and now every motor in your plant has identical structure that SCADA can map with a single faceplate.

UDTs are the gateway to maintainable code

If you’ve ever inherited an SLC-500 program with hundreds of motors scattered across B3:0/0 through B3:31/15, integers in N7:50, N7:51, N7:52 with comments like “// Motor 5 speed setpoint, do NOT change without consulting John (left company 2018)” — you know exactly why UDTs matter. UDTs make data self-documenting: Pump3.Speed means what it says, you don’t have to look up which integer file element it lives in, and “John” never needs to be consulted again. Worked Example 1 builds a Motor UDT and uses it for three pumps.

Arrays — collections of identical things

An array is a numbered collection of tags of the same type. Declare Recipe as an array of 20 RecipeData UDTs, and you have Recipe[0] through Recipe[19] — a recipe library you can index into with Recipe[CurrentRecipeNumber]. Arrays are how ControlLogix handles recipes, alarm tables, sequence steps, anything where the count is fixed but the index varies. Worked Example 2 uses an array to store and retrieve a recipe.

Project organization — Tasks, Programs, Routines

An SLC project is one big linear program with subroutines. A ControlLogix project is structured: a Controller contains Tasks, each Task contains Programs, each Program contains Routines. Routines are where the actual ladder/FBD code lives. The hierarchy lets you organise complex projects, run different parts at different rates, and isolate scope.

CONTROLLOGIX PROJECT ORGANIZATION Controller → Tasks → Programs → Routines. Each level adds organization. CONTROLLER PlantA_Logix5571 TASK — Continuous main control loop TASK — Periodic 100ms PID loops, fast logic TASK — Event e-stop response PROGRAM Conveyors PROGRAM Mixers PROGRAM Packaging ROUTINE MainRoutine ROUTINE Filler_TON ROUTINE Capper_FBD Routines hold the actual ladder/FBD/ST code

Figure 15.2 — ControlLogix project hierarchyController is the top of the tree. Tasks define when and how often code runs (continuous, periodic, event-driven). Programs group routines that share program-scope tags. Routines are the actual code, and can be written in ladder, FBD, ST (Structured Text), or SFC (Sequential Function Chart) — even within the same Program.

Task types

  • Continuous task — runs as fast as the controller can manage, in the background. There’s exactly one continuous task per controller. Most general logic lives here.
  • Periodic task — runs at a fixed interval (e.g. every 100 ms). Use for PID loops, motion, anything that needs deterministic timing. Periodic tasks pre-empt the continuous task.
  • Event task — runs in response to a trigger (an input transition, a network message, a motion event). Use for fastest possible response to specific external events, like an emergency-stop circuit.

Tag scope — controller vs program

  • Controller-scope tags are visible everywhere in the project. Use for I/O references, communication tags, anything that crosses program boundaries.
  • Program-scope tags are visible only within the program that owns them. Use for internal state — local timers, intermediate variables, working values. Two programs can each have their own FillTimer without conflict because each is local to its own program.
Section 15.2 · Part 2 of 6

Bit-Level Programming

The bit instructions you’ve used since Chapter 5 — XIC, XIO, OTE, OTL, OTU, ONS — work identically in ControlLogix. What changes is the addressing. Instead of B3:0/0, you write Motor1.Run. Instead of B3:0/3, you write Motor1.Fault. The logic is the same; the readability improves dramatically.

The bit instructions, refreshed for ControlLogix

InstructionSLC syntaxControlLogix syntaxMeaning
XICI:1/0 contactStartButton contactTrue when bit is 1
XIOI:1/1 N.C. contactStopButton N.C. contactTrue when bit is 0
OTEO:2/0 coilMotor.Run coilSets bit to match rung state
OTLO:2/0 L-coilAlarm.Active L-coilLatches bit ON; stays until OTU clears
OTUO:2/0 U-coilAlarm.Active U-coilUnlatches (clears) bit
ONSplus storage bitplus a BOOL storage tagTrue for one scan on rising edge

Tag-based addressing in practice

A start/stop seal-in circuit in SLC syntax was:

| ─[ XIC I:1/0 ]──┬──[ XIO I:1/1 ]──( OTE O:2/0 )─|
|                 |
| ─[ XIC O:2/0 ]──┘   ← seal-in

The same circuit in ControlLogix syntax becomes:

| ─[ XIC StartButton ]──┬──[ XIO StopButton ]──( OTE Motor.Run )─|
|                       |
| ─[ XIC Motor.Run ]────┘   ← seal-in

Read it aloud: “Start button OR motor running, AND not stop button, energise motor run.” The intent is in the words. The tags are self-documenting. This is what tag-based addressing buys you. Worked Example 3 builds this rung formally.

The ONS instruction with a BOOL storage tag

The one-shot pattern from Chapter 5 still applies: ONS detects the rising edge of its input rung condition and produces one scan of TRUE output. In ControlLogix, the storage bit must be a BOOL tag dedicated to this ONS — never share storage between ONS instructions. Each ONS gets its own storage BOOL.

| ─[ XIC PartSensor ]──[ ONS PartSensor_OS ]──( CTU PartCount )─|

Each rising edge of PartSensor produces one increment of PartCount, regardless of how long PartSensor remains true. PartSensor_OS is a BOOL storage tag declared specifically for this ONS — and named to make its purpose obvious. Worked Example 4 puts this pattern to work.

Section 15.3 · Part 3 of 6

Programming Timers

Chapter 7 covered TON, TOF, and RTO with SLC’s T4 file syntax. The timer instructions themselves haven’t changed, but in ControlLogix each timer is a TIMER tag with named members — easier to read, easier to write, and properly typed.

The TIMER data type

A TIMER tag has these members, each accessible as TimerName.Member:

MemberTypeMeaning
.PREDINTPreset — the target time in milliseconds
.ACCDINTAccumulator — elapsed time so far in milliseconds
.ENBOOLEnable — true while the rung is true
.TTBOOLTimer Timing — true while ACC is counting toward PRE
.DNBOOLDone — true when ACC reaches PRE

Where SLC used T4:5.DN, ControlLogix uses FillTimer.DN — same idea, descriptive name. Where SLC used T4:5.ACC, ControlLogix uses FillTimer.ACC. The dotted-member syntax is identical to SLC’s, but the timer name carries meaning.

The three timer types in ControlLogix

TypeBehaviourCommon use
TON (Timer-On-Delay)Rung true → ACC counts up to PRE → DN goes true. Rung false → ACC resets to 0.Delay before action (“wait 5 s before opening valve”)
TOF (Timer-Off-Delay)Rung true → DN immediately true, ACC = 0. Rung false → ACC counts up to PRE → DN goes false.Delay after action (“keep fan on 10 s after switch off”)
RTO (Retentive Timer-On)Like TON but ACC retains its value when rung goes false. RES instruction needed to clear.Cumulative timing (“total motor run-hours”)

Time-base in ControlLogix is always 1 ms

SLC let you choose 1 ms or 10 ms or 1 s time-bases for timers. ControlLogix simplifies this: all timers tick at 1 ms. PRE values are in milliseconds. To time 5 seconds, set PRE = 5000. To time 30 minutes, PRE = 1800000. The simplification eliminates a whole category of “wait, what’s the time-base?” bugs.

Worked Examples 5 and 6 implement a staged pump startup using TON cascades and a conveyor stop-delay using TOF.

Section 15.4 · Part 4 of 6

Programming Counters

Counters in ControlLogix follow the same pattern as timers: each counter is a COUNTER tag with named members. The instructions CTU (count up) and CTD (count down) are unchanged from Chapter 8; only the addressing has matured.

The COUNTER data type

MemberTypeMeaning
.PREDINTPreset — the target count
.ACCDINTAccumulator — current count
.CUBOOLCount Up Enable — true while CTU rung is true
.CDBOOLCount Down Enable — true while CTD rung is true
.DNBOOLDone — true when ACC ≥ PRE
.OVBOOLOverflow — true when ACC exceeds DINT max (very rare)
.UNBOOLUnderflow — true when ACC below DINT min

CTU, CTD, and RES

  • CTU (Count Up). Each rising edge of the rung input increments the counter’s ACC by 1. Use for counting parts, events, alarms.
  • CTD (Count Down). Each rising edge of the rung input decrements ACC by 1. Use for “remaining” counters or for letting an operator manually subtract rejects from a count.
  • RES (Reset). Sets the counter’s ACC back to 0 and clears all status bits. Important: CTU and CTD do not wrap around or auto-reset — once ACC reaches PRE, .DN stays true and ACC keeps incrementing past PRE on further counts. You must explicitly RES the counter to start a new batch.

CTU and CTD share one COUNTER tag — they don’t fight each other

If you have a CTU on one rung and a CTD on another, and they both reference the same COUNTER tag, the counter goes up when CTU pulses and down when CTD pulses. ACC is a single shared value. This is exactly the pattern you want for “good count up, reject count down” applications: net production = good − rejects, automatic.

Worked Examples 7 and 8 use CTU and CTD for a bottle filler with reject subtraction, and CTU+RES for cumulative production tracking.

Section 15.5 · Part 5 of 6

Math, Comparison, and Move Instructions

Chapters 10 and 11 covered data manipulation and math instructions in SLC syntax. ControlLogix offers the same instructions plus a few notable additions, all working on tags rather than file addresses.

Math instructions

The basics — ADD, SUB, MUL, DIV, MOD, SQR, NEG, ABS — work as expected, taking two source operands and a destination tag. ControlLogix adds a more powerful instruction:

CPT — the compute instruction for compound expressions

Where SLC needed a chain of separate ADD/MUL/DIV rungs to evaluate a compound formula, ControlLogix’s CPT (Compute) instruction takes a single freeform expression and evaluates it. To compute the total cost of a recipe (cost = qty × unit_price × (1 + tax_rate)), one CPT rung does the whole job:

CPT  Destination: TotalCost
     Expression:  Qty * UnitPrice * (1.0 + TaxRate)

CPT supports the standard arithmetic operators (+ − × / mod), comparison operators (= ≠ < ≤ > ≥), boolean operators (AND OR NOT XOR), and trigonometric/logarithmic functions (SIN, COS, TAN, ASIN, ACOS, ATAN, LN, LOG, SQR, ABS, FRD, TOD). It’s the most flexible math instruction in ladder, and it’s hard to overstate how much typing it saves on complex calculations. Worked Example 9 builds a CPT-based recipe cost calculation.

Comparison instructions

InstructionLogicExample
EQUSource A = Source BRecipe number matches stored ID
NEQSource A ≠ Source BDetect a setpoint change
GRTSource A > Source BTank above high level
LESSource A < Source BPressure below low limit
GEQSource A ≥ Source BTemperature reached at-temp
LEQSource A ≤ Source BReject parts below tolerance
LIMLow ≤ Source ≤ HighValue in acceptable band
MEQMasked equality (bit-pattern)Status word matches mask

Move and copy instructions

  • MOV (Move) — copy one source tag’s value into one destination tag. Type-converts as needed.
  • COP (Copy File) — copy a block of consecutive elements from source to destination. The mainstay for moving array data.
  • CLR (Clear) — zero out a tag (or the whole array, with COP from a zeroed source).
  • FLL (Fill) — fill a destination array with a single source value (initialise or reset all elements).
  • BSL / BSR / FFL / FFU / LFL / LFU — shift register and FIFO/LIFO instructions, same as Chapter 12.

Worked Example 10 uses LIM to classify a temperature reading into named zones (cold/cool/normal/warm/hot) for an HMI display.

Section 15.6 · Part 6 of 6

Function Block Diagram (FBD) Programming

For all of this book up to now, ladder logic has been our only programming language. ControlLogix offers four — Ladder, FBD, Structured Text (ST), and Sequential Function Chart (SFC) — and each suits a different problem class. FBD (Function Block Diagram) is especially valuable for continuous-process control because the visual flow of data from one block to the next mirrors the way process engineers already think about loops.

What FBD looks like

An FBD program is a sheet populated with blocks connected by wires. Each block has inputs on its left edge and outputs on its right; wires carry values from outputs to inputs. Data flows left-to-right across the sheet. There are no rungs, no rails, no XIC contacts — the visual primitive is the data wire, not the power flow.

SAME LOGIC, TWO LANGUAGES — LADDER vs FBD “Run = (Start OR Run) AND NOT Stop AND NOT Fault” — pick the language that suits your team and problem. LADDER LOGIC Power flow, contacts, coils Start Stop Fault Run Run Power flows left to right. FUNCTION BLOCK DIAGRAM Data flow, blocks, wires Start Run Stop Fault OR 2-in NOT NOT AND 3-in Run (seal-in via feedback) Data flows left to right.

Figure 15.3 — Same logic, two languagesThe seal-in motor circuit on the left is the ladder we’ve used since Chapter 5. The same logic on the right uses an OR block fed with Start and Run, two NOT blocks for Stop and Fault, and a 3-input AND that drives the Run output. Both languages compile to the same controller behaviour. Pick whichever language fits your team and problem class — and within one project you can mix freely.

When to use FBD vs Ladder

Problem classBetter inWhy
Discrete logic, interlocks, motor controlLadderDecades of electrician familiarity; intuitive XIC/XIO/OTE
Continuous-process control (PID, ratio, cascade)FBDData flow matches process engineering’s natural picture
Mathematical algorithms, signal conditioningFBD or STVisual blocks for algebra; ST for compact expressions
Sequential operations (batch recipes)SFCState-step structure makes sequences visible
Tight repetitive code (loops, table lookups)STCompact text; FOR/WHILE/IF constructs

The PIDE block — enhanced PID for FBD

The plain PID instruction works in ladder. ControlLogix adds PIDE (Enhanced PID) as a function block specifically designed for FBD. PIDE has dozens of inputs and outputs covering autotune, gain scheduling, feedforward, ratio control, anti-windup, alarming, and bumpless mode transfer — features that would be a tangle of separate rungs in ladder, but read cleanly as labelled wires going into a single PIDE block. For any non-trivial process loop, PIDE in FBD is better than PID in ladder. Worked Example 11 builds a temperature loop with PIDE.

Mixing languages within one project is normal practice

A typical ControlLogix plant will have ladder routines for motor control and discrete logic, FBD routines for every PID loop and analog signal-conditioning chain, SFC routines for batch recipe sequences, and the occasional ST routine for repetitive table-driven calculations. Each Routine declares its own language, and Routines call each other freely via JSR. There is no language war — pick whichever language makes this particular Routine easiest to read.

Worked PLC Programs

Twelve Programs — Two per Part

Two programs for each of the six parts: a UDT-based motor controller and a recipe array (Memory); a tag-based seal-in and an ONS counter (Bit-Level); staged TON startup and TOF stop-delay (Timers); bottle CTU+CTD and total CTU+RES (Counters); CPT cost calculation and LIM zone classifier (Math); and the FBD finale — a PIDE temperature loop and a multi-block interlock network.

01

PLC Program · UDT & structured tags

Motor UDT — Three Pumps Sharing One Type Definition

UDT · Structures

The problem: a water-treatment plant has three transfer pumps. Each pump has identical data: a Run command, a Stop command, a Fault status, a Speed command (0–100 %), an actual Speed feedback, and accumulated Run-Hours. Without UDTs, you’d declare 18 separate tags (Pump1_Run, Pump1_Stop, … Pump3_Hours), and SCADA would need 18 individual bindings per pump faceplate. With a UDT, one Motor type definition gives you three identical structured tags — and SCADA needs only one faceplate template.

Step 1 — Define the Motor UDT

Open the Studio 5000 Data Types section and create a User-Defined Type called Motor with these members:

UDT DEFINITION — Motor TYPE Motor : Run : BOOL ; // run command Stop : BOOL ; // stop command Fault : BOOL ; // fault latch SpeedCmd : REAL ; // commanded speed (%) SpeedFbk : REAL ; // actual speed (%) Hours : DINT ; // run-hour totaliser END_TYPE

Step 2 — Declare three Motor tags

In the controller-scope tag list, declare three tags of type Motor:

Pump1     :  Motor
Pump2     :  Motor
Pump3     :  Motor

Each pump now has all six members accessible: Pump1.Run, Pump1.Stop, Pump1.Fault, Pump1.SpeedCmd, Pump1.SpeedFbk, Pump1.Hours — and identically for Pump2 and Pump3. Three pumps, one type, eighteen logical tags accessible by name.

Step 3 — Write logic against the structure

The seal-in start/stop circuit for Pump1 reads exactly as English:

| ─[ XIC StartButton1 ]──┬──[ XIO Pump1.Stop ]──[ XIO Pump1.Fault ]──( OTE Pump1.Run )─|
|                        |
| ─[ XIC Pump1.Run ]─────┘   ← seal-in

For Pump2 and Pump3, copy and paste this rung structure, then change the references — Studio 5000’s tag-replace tool turns Pump1.X into Pump2.X in one operation. The structure is the same; the instances differ only in name.

What we learned: a UDT bundles related data into a single named type. Once defined, you can declare any number of instances and access their members via dot syntax (Pump1.Run, Pump1.SpeedFbk). UDTs make code self-documenting, eliminate the “which N7 element was that, again?” problem, and dramatically simplify SCADA integration — one HMI faceplate template binds to the structure once and re-uses across every pump in the plant. Use UDTs for any conceptual unit that recurs: motors, valves, sensors, alarm definitions, recipes, sequence steps. Once you start, you won’t go back.
02

PLC Program · Arrays & recipe lookup

Recipe Array — 20-Slot Recipe Library Indexed by Selection Number

Array · Index

The problem: a batch process supports 20 different recipes. Each recipe specifies a target temperature, a target pressure, a target mix-time (in seconds), and a flag for whether to add catalyst. The operator selects a recipe number on the HMI; the PLC must load that recipe’s parameters into the active-recipe tags so the rest of the program can use them. Without arrays, you’d have 80 separate tags and 20 lookup rungs; with arrays, you have one indexed access.

Step 1 — Define a Recipe UDT

TYPE  Recipe :
   Name           : STRING ;       // recipe name
   Temperature    : REAL ;         // target temp (°C)
   Pressure       : REAL ;         // target pressure (bar)
   MixTime        : DINT ;         // mix duration (s)
   AddCatalyst    : BOOL ;         // catalyst flag
END_TYPE

Step 2 — Declare the array of 20 Recipes

RecipeLibrary  : Recipe[20]    // 20-element array of Recipe UDTs
ActiveRecipe   : Recipe        // single Recipe to receive selection
SelectedNumber : DINT          // operator's selection (0..19)

The library now contains RecipeLibrary[0] through RecipeLibrary[19]. Each is itself a Recipe UDT, so members are accessible as RecipeLibrary[5].Temperature, RecipeLibrary[5].MixTime, and so on. Two-level addressing — array index, then UDT member.

Step 3 — Copy the selected recipe into ActiveRecipe

The whole load operation is a single COP rung:

000 — On Load button rising edge: COP one Recipe element into ActiveRecipe — Load btn LoadRecipe ONS COP — Copy Recipe RecipeLibrary[SelectedNumber] → ActiveRecipe (1 element)

A single COP rung does the whole job. The source RecipeLibrary[SelectedNumber] evaluates the index at runtime — if SelectedNumber = 5, the COP copies RecipeLibrary[5] into ActiveRecipe. Once loaded, the rest of the program reads ActiveRecipe.Temperature, ActiveRecipe.MixTime, etc.

Always validate the index before using it

An out-of-range array index in ControlLogix is a major fault.

  • If SelectedNumber arrives as 25 (out of range for a 20-element array), the COP throws a major fault and the controller stops scanning your code. Your plant goes down.
  • Always validate: IF SelectedNumber >= 0 AND SelectedNumber < 20 before using it as an index. Use a LIM instruction.
  • Better: clamp the value to the valid range with two LIM-and-MOV rungs at the entry point so downstream code can trust the range.
  • Best: gate the SelectedNumber input from the HMI with a similar range check so an operator typo can’t crash the controller in the first place.
What we learned: arrays are how ControlLogix handles collections of identical things. Combined with UDTs, an array of structures gives you a fully self-documenting recipe library, alarm table, sequence-step list, or motor catalogue — accessible by index. The COP instruction with an array reference and a single-element destination performs the whole “load record N” operation in one rung. Always range-check the index before using it; an out-of-range index is a major fault that takes the controller down. The pattern (UDT for the row type, array for the table, single COP for the load) is the universal recipe-library structure in ControlLogix.
03

PLC Program · Tag-based bit logic

Start/Stop Seal-In with Fault Lockout — All Tag-Named

XIC · XIO · OTE · Seal-in

The problem: a conveyor motor must start when the operator presses Start, stop when they press Stop, and refuse to start (or trip out if running) when its drive reports a Fault. This is the classic seal-in — built dozens of times across this book — but here we’ll write it in pure ControlLogix tag syntax to compare directly with what we’d have written in Chapter 5 for the SLC.

Tag declarations

Before writing the rung, declare the tags. In Studio 5000’s tag editor:

StartButton    : BOOL    // input — operator's Start (alias to Local:1:I.Data.0)
StopButton     : BOOL    // input — operator's Stop  (alias to Local:1:I.Data.1)
ConveyorFault  : BOOL    // input — drive fault       (alias to Local:1:I.Data.2)
ConveyorRun    : BOOL    // output — motor contactor (alias to Local:2:O.Data.0)

Aliasing tags to physical I/O addresses (Local:1:I.Data.0) is the ControlLogix way of saying “StartButton is the bit at slot 1, terminal 0 of input module 1″. Ladder code only references the named tag; the alias declares once where the physical bit lives.

Ladder Diagram (Single Rung)

000 — Start OR (already running), AND Stop NOT pressed AND no Fault → Run — Start StartButton Seal-in ConveyorRun Stop NC StopButton No Fault ConveyorFault ConveyorRun motor

Read this rung as English: “Start button OR conveyor already running, AND stop button NOT pressed, AND no conveyor fault → energise conveyor run.” Compare against the SLC version of the same rung — same logic, but every condition reads as a meaningful name. This is the readability win of tag-based addressing.

What we learned: the seal-in pattern is identical to the SLC version we built in Chapter 5 — XIC parallel for start-OR-running, XIO series for stop and fault interlocks, OTE coil for the motor. What changes is everything around the logic: tag declarations replace file allocation, alias-to-physical-IO replaces direct file addressing, and every reference reads as a meaningful name. The logic is the same; the maintenance burden over five years is dramatically lower. When you inherit this rung in 2030, you’ll know what every contact is for without consulting any documentation.
04

PLC Program · One-shot edge detection

ONS-Triggered Cycle Counter — Counting Sensor Pulses

ONS · CTU

The problem: a photoelectric sensor pulses each time a part passes on a conveyor. We need a running count of parts produced. Without ONS, the CTU would increment every scan that the sensor was active — possibly many times for a single part, depending on scan rate and sensor dwell. ONS converts the long sensor signal into a clean one-scan rising-edge pulse, so each part counts exactly once.

Tag declarations

PartSensor       : BOOL      // input — photo eye
PartSensor_OS    : BOOL      // ONS storage bit (must be unique per ONS)
PartCount        : COUNTER   // count-up counter
ResetCount       : BOOL      // operator reset button

Ladder Diagram (Two Rungs)

000 — Each rising edge of PartSensor: increment PartCount by exactly 1 — Sensor PartSensor ONS PartSensor_OS CTU — Count Up Counter: PartCount Preset: 100000 (large) 001 — Operator reset: clear the counter to start a new shift — Reset ResetCount RES PartCount

Two rungs. Rung 0: each rising edge of PartSensor (caught by ONS using PartSensor_OS as its private storage bit) increments PartCount by exactly one — even if the sensor dwell lasts dozens of scans. Rung 1: when the operator presses ResetCount, RES clears PartCount.ACC back to zero. The current count is read elsewhere as PartCount.ACC.

Each ONS needs its own unique storage bit

Sharing ONS storage between two ONS instructions silently breaks both.

  • Two ONS instructions both using SharedOSBit will fight each other — when one fires it changes the bit, throwing off the edge detection of the other.
  • Naming convention helps: PartSensor_OS for the ONS that watches PartSensor; StartButton_OS for the ONS that watches StartButton. The _OS suffix tells anyone reading the code that this BOOL is dedicated to its associated ONS.
  • Studio 5000 won’t warn you about shared storage bits. The bug is silent — only when you watch the counter increment incorrectly during testing do you discover the conflict.
What we learned: the ONS instruction converts a long input signal into a clean one-scan rising-edge pulse — exactly what counters need to count discrete events. Each ONS gets its own dedicated BOOL storage tag, named with an _OS suffix to make the relationship obvious. The CTU+ONS combination is the canonical “count parts” pattern; the operator-driven RES resets the count for a new shift or batch. Read the count elsewhere as PartCount.ACC; check whether you’ve reached a target with PartCount.DN (true when ACC ≥ PRE).
05

PLC Program · TON cascade for staged sequence

Three-Pump Staged Startup with 5-Second Stagger

TON · Cascade

The problem: three pumps share a common power feed. Starting all three simultaneously would draw enormous inrush current and trip the upstream breaker. We need to start Pump1 immediately on Start command, Pump2 five seconds after Pump1 is running, and Pump3 five seconds after Pump2. Each TON’s .DN bit triggers the next pump and the next timer in a cascade.

Tag declarations

SystemStart    : BOOL     // operator's start command
Pump1Run       : BOOL     // pump 1 contactor
Pump2Run       : BOOL     // pump 2 contactor
Pump3Run       : BOOL     // pump 3 contactor
Stagger12      : TIMER    // delay between pump 1 → pump 2
Stagger23      : TIMER    // delay between pump 2 → pump 3

The cascade pattern

  1. SystemStart energises Pump1Run. Immediate. As soon as the operator presses Start.
  2. Pump1Run starts the Stagger12 timer. TON with PRE = 5000 (5 seconds in milliseconds).
  3. Stagger12.DN energises Pump2Run. Five seconds after Pump1 started.
  4. Pump2Run starts the Stagger23 timer. TON with PRE = 5000.
  5. Stagger23.DN energises Pump3Run. Ten seconds total after Pump1 started.

Ladder Diagram (Five Rungs)

000 — SystemStart immediately runs Pump 1 — SystemStart Pump1Run 001 — Pump 1 running starts the 5-second stagger to Pump 2 — Pump1Run TON — 5-Second Stagger 1→2 Stagger12 Preset 5000 ms 002 — After stagger expires, energise Pump 2 — Stagger12.DN Pump2Run 003 — Pump 2 running starts the 5-second stagger to Pump 3 — Pump2Run TON — 5-Second Stagger 2→3 Stagger23 Preset 5000 ms 004 — After second stagger, energise Pump 3 — . Stagger23.DN Pump3Run

Five rungs forming a cascade. Each pump’s Run output drives the next stagger timer; each stagger timer’s .DN drives the next pump. The pattern is “Pump1 NOW → wait 5 s → Pump2 → wait 5 s → Pump3” — staggering inrush across 10 seconds total instead of slamming the breaker with three simultaneous starts. The whole cascade collapses cleanly when SystemStart goes false: Pump1Run drops, the timers reset, Pump2Run and Pump3Run drop too.

What we learned: a TON cascade is the simplest way to stagger startup of multiple devices. Each timer’s .DN bit drives the next stage. The pattern extends naturally: four pumps, five conveyors, six valves — same cascade, more rungs. The whole sequence aborts cleanly when the kicker (SystemStart) drops, because TONs reset their .ACC to zero when their rung goes false. Use this pattern any time you need “do A, then five seconds later do B, then ten seconds later do C” — it’s reliable, easy to read, easy to extend.
06

PLC Program · TOF off-delay

Conveyor Stop Delay — TOF Keeps Belt Running 10 s After Last Part

TOF

The problem: a conveyor’s “presence” sensor goes true while parts are on the belt and false during gaps. We don’t want the conveyor to start and stop with every gap — that destroys the motor and produces inconsistent throughput. Instead, the conveyor should run while the sensor is active, and continue running for 10 seconds after the sensor clears. If a new part arrives within those 10 seconds, the timer resets and the conveyor keeps running. TOF is the natural fit.

Tag declarations

PartPresent    : BOOL     // input — presence sensor on belt
StopDelay      : TIMER    // 10-second TOF
ConveyorRun    : BOOL     // output — conveyor motor

Ladder Diagram (Two Rungs)

000 — PartPresent true: TOF.DN immediately true, ACC = 0. PartPresent false: ACC counts up to 10 s, then DN goes false — Sensor PartPresent TOF — 10-Second Stop Delay StopDelay Preset 10000 ms 001 — Conveyor runs while StopDelay.DN is true (= sensor active OR within 10 s of last activation) — Done StopDelay.DN ConveyorRun

Two rungs. Rung 0: TOF whose rung is the PartPresent sensor — DN bit is true while sensor is active or while still timing the 10-second post-activation period. Rung 1: ConveyorRun follows StopDelay.DN. Result: belt runs whenever a part is sensed; belt continues for 10 s after the last part clears; new part within those 10 s extends the run indefinitely.

What we learned: TOF is the inverse of TON. While the rung is true, .DN is immediately true and ACC = 0. When the rung goes false, ACC counts up to PRE; .DN stays true throughout that count, then goes false when ACC reaches PRE. The “stay-on-after” pattern is what TOF was made for. Use it for fans that should run a few minutes after a heating element switches off, lights that should stay on briefly after motion stops, or — as here — conveyors that should bridge gaps in part flow without stop-start cycling. The whole pattern is one TOF and one output rung.
07

PLC Program · CTU + CTD sharing one COUNTER

Bottle Filler — Good Count Up, Reject Count Down on the Same Counter

CTU · CTD · Net

The problem: a bottle filler counts each bottle that passes the discharge sensor. Some bottles fail QC and the operator presses a Reject button to subtract them from the count. We want the displayed count to be net production — good bottles minus rejects — not just gross throughput. The natural ControlLogix solution is one COUNTER tag accessed by both CTU and CTD on different rungs.

Tag declarations

BottleSensor      : BOOL      // discharge sensor
BottleSensor_OS   : BOOL      // ONS storage
RejectButton      : BOOL      // operator reject button
RejectButton_OS   : BOOL      // ONS storage
GoodCount         : COUNTER   // shared counter for CTU and CTD

Ladder Diagram (Two Rungs)

000 — Each bottle: rising edge of sensor → increment GoodCount — Sensor BottleSensor ONS BottleSensor_OS CTU — Count Up GoodCount Preset 100000 001 — Each Reject press: rising edge → decrement the same GoodCount — Reject RejectButton ONS RejectButton_OS CTD — Count Down GoodCount (same tag)

Two rungs share one COUNTER tag. Rung 0’s CTU increments GoodCount on each bottle sensor pulse. Rung 1’s CTD decrements the very same GoodCount on each Reject press. The accumulator GoodCount.ACC tracks net production automatically — no separate “good” and “reject” tags, no subtraction rung. SCADA reads GoodCount.ACC for the live net count.

What we learned: CTU and CTD can share the same COUNTER tag — one rung increments, another decrements, and the accumulator reflects the running difference. This is the cleanest way to track net quantities (good minus rejects, deposits minus withdrawals, things in minus things out). Both rungs need their own ONS storage bits (BottleSensor_OS and RejectButton_OS — never share). The pattern uses one COUNTER, two rungs, and produces a self-maintaining net total.
08

PLC Program · CTU + RES for shift totals

Total Production Counter with Shift-Change Reset and Per-Batch Auto-Reset

CTU · RES · Dual Counters

The problem: production tracking needs two counters. TotalProduction accumulates parts across the entire shift — only the operator’s “Shift Change” button resets it. BatchCount tracks parts within a single batch and auto-resets when each batch completes (when ACC reaches the batch size). Two CTUs sharing the same input edge but with different reset logic.

Tag declarations

PartSensor          : BOOL      // discharge sensor
PartSensor_OS       : BOOL      // ONS storage
TotalProduction     : COUNTER   // shift total (operator-reset)
BatchCount          : COUNTER   // batch total (auto-reset)
ShiftChange         : BOOL      // operator shift-change button
BatchSize           : DINT      // configurable batch quantity

Ladder Diagram (Four Rungs)

000 — Each part: increment TotalProduction (the shift counter) — PartSensor ONS PartSensor_OS CTU — TotalProduction Preset 999999 (large) 001 — Same edge: increment BatchCount (the batch counter) — PartSensor ONS PartSensor_OS2 CTU — BatchCount Preset = BatchSize 002 — Auto-reset BatchCount when it reaches BatchSize: ready for the next batch — BatchCount.DN RES BatchCount 003 — Operator-driven shift-change reset of the shift total — ShiftChange RES TotalProduction

Four rungs. Rungs 0 and 1 increment both counters on the same sensor edge — but with different ONS storage bits (PartSensor_OS for Total, PartSensor_OS2 for Batch). Rung 2 auto-resets BatchCount when it reaches BatchSize (BatchCount.DN drives RES BatchCount on the very next scan). Rung 3 resets TotalProduction only on the operator’s shift-change command. Two counters, two reset rules, one sensor input.

Two CTUs on the same input need two ONS storage bits

Reusing one ONS storage bit between two rungs silently breaks both increments.

  • If both CTU rungs share PartSensor_OS, only the first rung scanned per part will see the rising edge — by the time the second rung executes, the storage bit has already flipped to true and the second rung sees no edge.
  • Each CTU rung that watches the same source signal needs its own dedicated _OS bit. Suffix them: PartSensor_OS, PartSensor_OS2, PartSensor_OS3.
What we learned: the CTU + RES pair gives you a counter that can either auto-reset (driven by .DN) for repeating batch operations, or operator-reset (driven by an external command) for shift totals or production milestones. Combine both patterns and a single sensor input drives multiple counters at multiple “lifetimes” — batch (auto-reset), shift (operator-reset), grand total (rarely or never reset). Counters in ControlLogix don’t auto-roll-over — once .ACC reaches .PRE, you must explicitly RES, otherwise .ACC keeps incrementing past .PRE forever.
09

PLC Program · CPT compound expression

Recipe Cost Calculation — One CPT Replaces Three SLC Rungs

CPT · Compound math

The problem: the recipe-management system needs to compute total cost for the active batch as cost = qty × unit_price × (1 + tax_rate). In SLC syntax, that’s three rungs (MUL, MUL, ADD with intermediate variables). In ControlLogix, the CPT instruction takes the whole formula as one expression and computes the result in a single rung.

Tag declarations

Qty           : REAL    // batch quantity (kg)
UnitPrice     : REAL    // price per unit ($/kg)
TaxRate       : REAL    // tax rate (0.05 for 5 %)
TotalCost     : REAL    // computed total ($)
CalcCost      : BOOL    // trigger condition (e.g. recipe loaded)

Ladder Diagram (One Rung)

000 — Single CPT rung evaluates the entire compound expression — Trigger CalcCost CPT — Compute Destination: TotalCost Expression: Qty * UnitPrice * (1.0 + TaxRate)

One rung. The CPT instruction evaluates the entire compound expression and writes the result to TotalCost. Compare against the SLC equivalent: three separate MUL/ADD rungs with two intermediate float tags. The CPT version is shorter, doesn’t pollute the tag list with intermediate variables, and reads as the actual formula.

What CPT can do

CPT supports any expression that combines:

  • Arithmetic operators+, , ×, /, MOD (modulo)
  • Comparison operators=, , <, , >, (return 1 or 0 as integers)
  • Boolean operatorsAND, OR, NOT, XOR
  • FunctionsSIN, COS, TAN, ASIN, ACOS, ATAN, LN, LOG, SQR, ABS, FRD (BCD→decimal), TOD (decimal→BCD)
  • Bit operationsAND, OR, XOR, NOT, ** (exponent)
  • Parentheses for grouping

Almost any formula a process engineer would write on paper can go directly into a CPT expression. This dramatically reduces the rung count for analog-heavy programs.

What we learned: the CPT (Compute) instruction takes a freeform expression and evaluates it in one rung — replacing the chain of separate ADD/SUB/MUL/DIV rungs that SLC required for compound formulas. This is one of the biggest rung-count reductions ControlLogix offers over SLC, especially for analog-heavy code (signal scaling, engineering-unit conversions, recipe arithmetic). The expression syntax accepts standard arithmetic, comparison, boolean, and trigonometric/logarithmic functions, with parentheses for grouping. For any non-trivial formula, prefer CPT over a chain of basic-math rungs — the result is shorter, cleaner, and reads as the actual mathematics.
10

PLC Program · LIM-based zone classification

Temperature Zone Classifier — Cold / Cool / Normal / Warm / Hot Lamps

LIM · Banded compare

The problem: a process temperature reading drives a five-lamp HMI display showing which “zone” the temperature is currently in. Below 0 °C is Cold; 0–15 is Cool; 15–25 is Normal; 25–40 is Warm; above 40 is Hot. The LIM (limit) instruction is built for exactly this — three operands (Low, Test, High) and the rung is true only when Low ≤ Test ≤ High.

Tag declarations

Temperature   : REAL    // current process temp (°C)
ZoneCold      : BOOL    // lamp: below 0
ZoneCool      : BOOL    // lamp: 0 to 15
ZoneNormal    : BOOL    // lamp: 15 to 25
ZoneWarm      : BOOL    // lamp: 25 to 40
ZoneHot       : BOOL    // lamp: above 40

Ladder Diagram (Five Rungs)

000 — Cold zone: temperature below 0 °C — LES — Less Than Temperature < 0.0 ZoneCold 001 — Cool zone: 0 ≤ Temp < 15 — LIM — Limit 0.0 ≤ Temperature < 15.0 ZoneCool 002 — Normal zone: 15 ≤ Temp < 25 (the band you actually want to be in) — LIM — Limit 15.0 ≤ Temperature < 25.0 ZoneNormal 003 — Warm zone: 25 ≤ Temp ≤ 40 — LIM — Limit 25.0 ≤ Temperature ≤ 40.0 ZoneWarm 004 — Hot zone: temperature above 40 °C — GRT — Greater Than Temperature > 40.0 ZoneHot Five mutually-exclusive zones drive five independent lamp outputs.

Five rungs, one zone each. The two end zones use LES and GRT (open-ended on one side); the three middle zones use LIM with both bounds. Each rung is independent — no dependency on any other rung — and exactly one of the five outputs is true at any moment because the bounds are mutually exclusive. The HMI reads all five booleans and lights the corresponding zone lamp.

What we learned: the LIM instruction tests whether a value falls within an inclusive band (Low ≤ Test ≤ High). For zone classification — temperature ranges, pressure bands, speed brackets — LIM is exactly the right tool. The pattern uses one LIM rung per zone with mutually exclusive bounds, so exactly one output is true at any moment. Combine LES/GRT for the open-ended end zones (below the lowest band, above the highest) since LIM requires both bounds. The whole pattern scales naturally: 10 zones, 20 zones — same structure, more rungs.
11

PLC Program · FBD with PIDE block

Enhanced PID Loop in Function Block Diagram

FBD · PIDE

The problem: a heat-exchanger temperature control loop needs autotune support, anti-windup protection, and bumpless mode transfer between manual and auto. Implementing all this in ladder using the basic PID instruction would be a tangle of separate rungs. PIDE (Enhanced PID) in FBD wraps every feature into a single block with named input and output pins — read like an instrument datasheet rather than a ladder program.

The PIDE block on an FBD sheet

FBD SHEET — PIDE Temperature Loop Wires carry data left-to-right. The PIDE block configures itself from its named input pins. TempPV scaled from RTD (°C) TempSP setpoint from operator AutoBit auto/manual mode select FFwd feed-forward (load comp) CVMin output lower limit (0 %) CVMax output upper limit (100 %) PIDE Enhanced PID PV SP AutoMan FF CVMin CVMax Kp · Ki · Kd autotune anti-windup bumpless alarms CV CVEU PVHIAlarm PVLOAlarm DevHI SteamValve CV_EngUnits HighTempAlarm LowTempAlarm SP_Deviation Six output pins. Five wires. One block. Read like an instrument datasheet.

Why PIDE in FBD beats PID in ladder

  • Visual data flow. Every signal feeding the loop appears as a labelled wire on the left edge of the block. Every signal coming out appears as a labelled wire on the right. Reading the loop is reading a wiring diagram — exactly the mental model process engineers already have.
  • Built-in autotune. PIDE includes step-response and relay-feedback autotune algorithms; trigger them from the AutoTuneRequest input pin and the block computes good starting Kp/Ki/Kd values automatically. The basic PID instruction has no autotune.
  • Built-in anti-windup. Configurable strategies (clamp, back-calculate, conditional integration) prevent integral windup when the actuator saturates. In ladder PID, you’d implement this with extra logic.
  • Bumpless mode transfer. Switching between Auto and Manual doesn’t bump the output — PIDE backs the integral term to make the transition smooth. In ladder PID this requires careful manual coding.
  • Alarm outputs built in. PV-high, PV-low, deviation alarms are output pins of the block. Wire them to your alarm system and you’re done — no separate comparison rungs needed.
What we learned: for any non-trivial PID loop, PIDE in FBD is dramatically more powerful than the basic PID instruction in ladder. The block exposes every feature — gains, autotune, feedforward, anti-windup, bumpless transfer, deviation alarms — as named input/output pins. The FBD sheet shows the entire loop as a single visual diagram that reads like an instrument datasheet. The standard ControlLogix recipe: ladder for discrete logic, FBD with PIDE for every analog process loop. You can have routines of each language in the same Program, calling each other freely via JSR.
12

PLC Program · FBD interlock network

Multi-Permissive Motor Run Interlock — AND/OR/NOT in FBD

FBD · Boolean blocks

The problem: a motor must only run when all these are true: door is closed, pressure is OK, temperature is OK, and the e-stop is not pressed. The natural way to express this in ladder is a chain of XIC/XIO contacts in series. The natural way to express it in FBD is a 4-input AND block. Both work; FBD makes the boolean structure visually obvious in a way that ladder doesn’t.

The FBD sheet

FBD SHEET — Motor Run Permissive Four permissive conditions feed an AND block; one EStop feeds a NOT then ANDs in. One output: MotorRunPermissive. DoorClosed Pressure_OK Temperature_OK OperatorReady EStop_Pressed NOT AND 5-input MotorRun Permissive Read it like English: “All four permissives AND not E-stop → motor may run.”

Same logic in ladder, for comparison

| ─[ XIC DoorClosed ]──[ XIC Pressure_OK ]──[ XIC Temperature_OK ]──┐
|                                                                  |
| ┌────────────────────────────────────────────────────────────────┘
| └─[ XIC OperatorReady ]──[ XIO EStop_Pressed ]──( OTE MotorRunPermissive )─|

Both produce identical controller behaviour. The ladder version uses a series of XIC contacts (with one XIO for the negated EStop). The FBD version uses a single AND block with a NOT gate on the EStop input. Pick whichever language your team reads more fluently — and within one ControlLogix project, you can use both, freely mixed routine by routine.

When to choose FBD for boolean logic

For a four-condition AND, ladder and FBD are both perfectly readable; choose by team preference. Where FBD pulls ahead is when the boolean structure is genuinely complex — nested ANDs and ORs, voting logic (“any 2 of 3”), priority chains. In ladder, complex boolean expressions become tangled rungs with branches. In FBD, you draw the boolean tree directly: AND blocks for ANDs, OR blocks for ORs, NOT blocks for negations, and the visual structure mirrors the logic structure exactly. The more complex the boolean, the bigger the FBD readability win.

What we learned: FBD’s boolean blocks (AND, OR, NOT, XOR) build up complex permissives as a visual tree. For a simple “all conditions true” interlock, ladder and FBD are roughly equivalent. For nested boolean expressions, voting logic, or priority chains, FBD’s visual tree is dramatically clearer than the equivalent branched ladder. Mixed-language projects are normal: use ladder for motor seal-ins and discrete sequencing, FBD for analog loops (PIDE) and complex booleans, ST for table-driven calculations, SFC for batch recipes. Each Routine declares its language; routines call each other freely via JSR.

Common Mistakes

The traps engineers fall into when migrating from SLC to ControlLogix

  • Designing UDTs that are too granular or too monolithic. Too granular (one UDT per BOOL) gives you no structure benefit. Too monolithic (50 members in one UDT) becomes hard to bind in SCADA and confuses readers. Aim for “one UDT per real-world thing”: Motor, Valve, Sensor, Recipe, AlarmDef.
  • Confusing controller scope with program scope. Putting all tags at controller scope sacrifices encapsulation; putting all tags at program scope sacrifices cross-program communication. Rule of thumb: I/O references and inter-program data at controller scope, internal state and working values at program scope.
  • Forgetting the explicit RES on counters. Counters do not auto-roll-over when ACC reaches PRE — they keep incrementing past PRE forever. If you want a fresh count for each batch, you must wire BatchCount.DN to a RES BatchCount rung. Otherwise the counter saturates after the first batch and never counts again.
  • Sharing ONS storage bits across multiple ONS instructions. Each ONS needs its own dedicated BOOL. Sharing causes one ONS to “eat” the edge meant for the other. Studio 5000 won’t warn you; the bug is silent and only visible when you watch the counters increment incorrectly.
  • Not validating array indexes. An out-of-range array index throws a major fault that stops the controller. Always range-check the index before using it: IF Idx >= 0 AND Idx < ArraySize. For HMI-supplied indexes, clamp at the entry point so downstream code can trust the range.
  • Tag-name conflicts between scopes. A tag named Run at program scope shadows any controller-scope tag of the same name within that program. This is occasionally what you want but more often a surprise. Use distinctive program-scope names (e.g. prefixed with the program name) to avoid accidental shadowing.
  • Assuming SLC time-base behaviour. SLC let you choose 1 ms / 10 ms / 1 s time-bases for timers. ControlLogix is always 1 ms. To time 5 seconds, the preset is 5000 — not 5, not 50, not 500. A migrated SLC program with PRE = 5 (intending 5 seconds at 1 s base) becomes a 5 ms timer in ControlLogix — usually invisible until something obviously breaks.
  • Ignoring FBD execution order. Within an FBD sheet, blocks execute in the order Studio 5000 chooses based on data dependencies (and sometimes user-set order). When two blocks depend on each other (feedback loops), execution order determines which sees stale vs fresh data. Set explicit execution order for any FBD sheet with feedback paths.
  • Using the basic PID instruction in ladder where PIDE in FBD is appropriate. The basic PID works fine for trivial loops. For anything needing autotune, anti-windup, bumpless transfer, feedforward, or alarm outputs, PIDE in FBD is dramatically easier to write and maintain. Default to PIDE for any non-trivial process loop.
  • Treating a migrated SLC program as “done” after addressing translation. A direct conversion from SLC files to ControlLogix tags works but doesn’t capture ControlLogix’s value. The real migration value comes from refactoring: building UDTs for repeating things, splitting one big routine into multiple by function, replacing chains of math rungs with CPT, putting analog loops into FBD with PIDE. Direct translation is just the starting point.

Quick Recap

The takeaways from Chapter 15

  • Tags name what data is, not where it lives. The compiler decides memory layout. Motor1.Run beats B3:0/0 on every dimension that matters: readability, maintainability, refactoring safety, SCADA integration.
  • Atomic types are BOOL, SINT, INT, DINT, LINT, REAL, LREAL, STRING. Default to DINT for integer counters and indexes; the controller is 32-bit native and there’s no speed advantage to INT.
  • UDTs bundle related data into reusable structures. Define one Motor UDT with six members; declare ten Motor instances; SCADA binds one faceplate template to all ten. The single biggest organizing tool ControlLogix offers.
  • Arrays are fixed-size collections of identical things. Combine with UDTs for recipe libraries, alarm tables, motor catalogues. Always range-check the index.
  • Projects organise as Controller → Tasks → Programs → Routines. Tasks define when (continuous, periodic, event); Programs group related routines; Routines hold actual code. Each Routine declares its own language (Ladder, FBD, ST, SFC).
  • Tag scope: controller scope is global; program scope is local. I/O and cross-program data at controller scope; internal state and working values at program scope.
  • Bit instructions stay XIC, XIO, OTE, OTL, OTU, ONS — but with named tags. ONS needs a unique BOOL storage tag; never share between ONS instructions.
  • TIMER and COUNTER are structures with named members. FillTimer.DN, PartCount.ACC. Time-base is always 1 ms in ControlLogix; preset values in milliseconds.
  • CTU and CTD can share one COUNTER. One increments, the other decrements; the accumulator tracks the running difference automatically. Counters never auto-reset; use RES.
  • CPT replaces chains of basic-math rungs. Compound expressions with arithmetic, comparison, boolean, trig, and log functions in one rung. The biggest rung-count reduction over SLC.
  • FBD complements ladder; doesn’t replace it. Discrete logic in ladder, analog loops in FBD with PIDE, batch sequences in SFC, repetitive table calculations in ST. Mix freely.
  • PIDE in FBD beats PID in ladder for any non-trivial process loop. Built-in autotune, anti-windup, bumpless transfer, feedforward, and alarm outputs — all as named pins on a single block.

Review Questions

Test Your Understanding

Click any question to reveal the answer. Twelve questions covering memory, project organization, bit instructions, timers, counters, math, and FBD.

1What is the single biggest conceptual difference between SLC-500 file-based addressing and ControlLogix tag-based addressing?+

SLC addresses identify where data lives in fixed memory files (N7:50 = word 50 of integer file 7). ControlLogix tags identify what data is by name (TankTemperature). The compiler decides where the tag actually lives in memory, and that decision can change between downloads without breaking your program. The name carries the meaning; the location is the compiler’s problem. This single change makes ControlLogix programs dramatically more readable and maintainable.

2Define a Motor UDT with members appropriate for a typical industrial motor. Why is this better than ten loose tags?+

Typical Motor UDT: Run : BOOL, Stop : BOOL, Fault : BOOL, SpeedCmd : REAL, SpeedFbk : REAL, Hours : DINT. Better than loose tags because: (1) self-documentingPump3.SpeedCmd means what it says without comments; (2) SCADA binds once — one motor faceplate template binds to the structure and re-uses across every motor in the plant; (3) refactoring is safe — adding a member to the UDT propagates to every instance automatically; (4) scope of mistakes shrinks — you can’t accidentally use Pump3’s Hours value as Pump5’s SpeedCmd because they’re different members of different structures.

3When should a tag be controller-scope vs program-scope?+

Controller scope for: I/O references (always), tags accessed by multiple programs, communication tags read/written by SCADA or peer PLCs, alarm structures published to the historian. Program scope for: internal state local to one program, working/intermediate values, timers and counters used only within one routine, anything that wouldn’t make sense outside the program. Rule of thumb: if two programs need it, controller scope; otherwise program scope. Default to program scope unless you have a reason — it keeps the controller-scope tag list manageable.

4Three ONS instructions watching three different sensors share the same storage BOOL. What goes wrong, and how do you fix it?+

The first ONS to scan sees the rising edge and flips the storage bit to true. The next two ONS instructions, scanning later in the same scan, find the storage bit already true and produce no output. Only one rising edge per scan reaches one ONS; the others lose their edges silently. Studio 5000 does not warn about shared storage bits. Fix: declare a unique storage BOOL for each ONS instruction. Naming convention: SensorA_OS for the ONS watching SensorA; SensorB_OS for the ONS watching SensorB. The _OS suffix advertises the relationship.

5A migrated SLC program had a TON with PRE = 30 at 1-second time-base. What does that timer become in ControlLogix, and what’s the fix?+

ControlLogix timers are always 1 ms time-base, so PRE = 30 becomes a 30-millisecond timer instead of a 30-second timer — 1000× faster than intended. The behaviour is wildly different: the timer reaches DN before the operator can release the start button. Fix: multiply every migrated SLC preset by the original time-base (1, 10, or 1000) to get the correct millisecond value. PRE 30 at 1 s base becomes PRE 30000 in ControlLogix; PRE 50 at 10 ms base becomes PRE 500. Audit every migrated timer’s preset against the original time-base before commissioning.

6Explain how CTU and CTD sharing a single COUNTER tag implements net production tracking. Why is this better than two separate counters?+

One CTU rung increments the COUNTER on each good-part edge; one CTD rung decrements the same COUNTER on each reject edge. The accumulator tracks net production automatically — good count minus rejects, in real time. Better than two separate counters because: (1) the running net is always immediately readable from GoodCount.ACC with no subtraction rung; (2) the SCADA binding is one tag, not three (good, reject, computed-net); (3) the logic for “is this good or reject” stays in two simple rungs rather than scattered through subtraction logic.

7A counter has hit its preset and .DN is true. Operations want it to start counting again from zero. What rung do you add?+

A rung that drives a RES instruction targeting that counter. Trigger options depend on intent: (1) auto-reset — drive RES from Counter.DN itself; the counter resets on the very next scan after reaching preset and starts counting again immediately; (2) operator-reset — drive RES from a button press; the counter holds at preset until the operator clicks; (3) conditional reset — drive RES from some plant condition (e.g. shift-end signal). Counters never auto-roll-over in ControlLogix — they keep incrementing past PRE once .DN is true, so explicit RES is mandatory whenever you want the counter to start fresh.

8Express “TotalCost = Quantity × UnitPrice × (1 + TaxRate)” in ControlLogix. Why is CPT a better choice than separate ADD/MUL rungs?+

One CPT rung: Destination: TotalCost, Expression: Quantity * UnitPrice * (1.0 + TaxRate). Better than separate rungs because: (1) one rung instead of three, with no intermediate tags polluting the tag list; (2) the rung reads as the actual formula a process engineer would write on paper; (3) refactoring is local — change the formula in one place rather than across three rungs; (4) CPT supports the full set of arithmetic, comparison, boolean, and trig/log operators in one expression — capabilities that would require multiple rungs and intermediates with basic instructions. For analog-heavy code, CPT is the biggest rung-count win ControlLogix offers over SLC.

9When should you choose FBD over Ladder, and when should you choose Ladder over FBD?+

FBD wins for: continuous-process control loops (PIDE far surpasses PID in ladder), analog signal-conditioning chains (visual data flow matches the algebra), complex boolean expressions with nested AND/OR/NOT (visual tree mirrors logic structure), and any problem where data flow is the dominant pattern. Ladder wins for: motor seal-ins and discrete contactor logic (decades of electrician familiarity, intuitive XIC/XIO/OTE), interlocks expressible as simple series chains, fault and shutdown logic that needs to be auditable by maintenance staff. Mix freely — different routines can use different languages within the same program; pick whatever language makes this particular routine easiest to read.

10What does PIDE in FBD provide that the basic PID in ladder doesn’t?+

Five major features: (1) built-in autotune with step-response and relay-feedback algorithms — trigger from an input pin, get good Kp/Ki/Kd values; (2) built-in anti-windup with configurable strategies (clamp, back-calculate, conditional integration); (3) bumpless mode transfer between Auto and Manual without output jumps; (4) built-in alarm outputs (PV-high, PV-low, deviation) as output pins, no separate compare rungs needed; (5) feedforward input for known load disturbances. All exposed as named input/output pins on one block, readable like an instrument datasheet. For any non-trivial loop, PIDE in FBD is dramatically easier to write and maintain than the basic PID instruction in ladder.

11An array reference uses an HMI-supplied index. What’s the safest way to use that index?+

Always validate the index before using it as an array subscript. Two-step pattern: (1) at the entry point where the HMI writes the index, clamp it to the valid range using LIM and conditional MOV — if HMI sends 25 to a 20-element array, clamp to 19. (2) Before the array access itself, verify with another LIM rung that the index is still in range, and skip the access (or trigger an error path) if not. Never trust externally-supplied indexes raw — an out-of-range array reference throws a major fault that stops the controller’s entire scan, taking the plant down. Defence in depth at the HMI boundary AND at the array access prevents this.

12You inherit an SLC-500 program and need to migrate it to ControlLogix. What’s the difference between a “translation” migration and a “refactoring” migration?+

A translation migration converts each SLC address to an equivalent ControlLogix tag (N7:50N7_50) and copies every rung verbatim. The result works but captures none of ControlLogix’s value: still file-style names, still no UDTs, still long chains of math rungs, still everything in one big routine. A refactoring migration uses the translation as a starting point and then: builds UDTs for repeating things (motors, valves, sensors), gives tags meaningful names (TankTemperature not N7_50), splits monolithic routines into multiple routines by function, replaces chains of basic-math rungs with CPT, moves analog loops to FBD with PIDE, and validates HMI-supplied array indexes. Translation gets you running on ControlLogix; refactoring gets you the benefits. Budget twice the time for refactoring beyond translation — and get a long-term maintainability win that’s worth several times the effort.

Migrating from SLC to ControlLogix, or designing a new ControlLogix project from scratch?

If you’re sketching out UDT structures for a complex plant, planning a Studio 5000 project organization, deciding which routines should be FBD vs Ladder, or trying to figure out why your migrated SLC timers tick 1000× faster than they used to — happy to help.

Get in Touch →