Programming Traffic Lights on Omron CJ1M: Compare and Timer Logic

James Nishida18 min read
HMI ProgrammingOmronTutorial / How-to
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Overview: Traffic Light Sequencing on the CJ1M

A traffic-light intersection is one of the canonical training problems for any PLC platform because it forces the programmer to combine three fundamental concepts: timed sequencing (when does each phase end?), logical comparison (which phase am I in now?), and output interlocks (only one direction of green may ever be on at a time). This article walks through a complete, two-crossroad implementation on the Omron CJ1M family of compact modular PLCs, focusing on the techniques most often asked about by first-time programmers:

  • How to drive outputs from TIM/TIMX timer contacts without leaving lamps latched on at power-up.
  • How to use the CMP instruction and the IEC-style comparison contacts (=, <, >) as a clean state-machine driver.
  • How to read the built-in real-time clock on the CJ1M for schedules that must follow wall-clock time.
  • How to coordinate two independent intersections with different phase lengths.

The reference ladder is written in CX-Programmer notation. Any CJ1M CPU from the CJ1M-CPU11/12/13 (low-end, 5K-step program capacity) up to the CJ1M-CPU21/22/23 (high-end, up to 20K steps, built-in Ethernet/IP on the CPU22/23) is sufficient. The logic uses only standard instruction set features so it ports to the CP1, CP1E, and CS1 platforms with almost no changes.

Safety disclaimer: A traffic-light program is also a safety-related program. Real intersections require redundant controllers, conflict-monitoring (MMU), fault outputs, and conformance to NEMA TS-2, EN 12675, or your local authority's standard. The ladder below is a training artifact; do not deploy it to control real vehicles.

Hardware Reference: CJ1M Platform Summary

Before writing code, anchor the memory model so every operand used later is unambiguous. The relevant facts for the CJ1M series are summarized in the table below (from the CJ1M Catalog (CSM_4919) and the W472 CJ1M CPU Unit Operation Manual):

Parameter CJ1M-CPU11/12/13 CJ1M-CPU21/22/23
Program capacity 5,120 steps 10,240 / 20,480 steps
I/O bits (CIO area) 160 pts (CPU1x) to 320 pts 320 pts to 640 pts
Work bits (W) 1,600 4,800
Holding bits (HR) 1,600 1,600
Auxiliary bits (AR) 960 (read-only A000-A447, R/W A448-A959) 960
DM area (D) 32K words 32K words
Timer/Counter numbers 4,096 (T0-T4095 / C0-C4095) 4,096
Built-in real-time clock Yes (battery-backed) Yes (battery-backed)
Built-in serial port RS-232C (Peripheral) + RS-422A/485 on CPU12/13 RS-232C + RS-422A/485 (CPU23 only)
Built-in Ethernet/IP No Yes on CPU22/23

The ladder examples below use these default area conventions (override per project):

  • Inputs: 0.00 = Start PB, 0.01 = Stop PB, 0.02 = Manual/auto selector.
  • Outputs: 100.00 = X-dir red 1, 100.01 = X-dir yellow 1, 100.02 = X-dir green 1; 100.03 = Y-dir red 1, 100.04 = Y-dir yellow 1, 100.05 = Y-dir green 1. The second crossroad uses 101.00-101.05.
  • Working bits: W0.00-W0.03 = state bits; W1.00-W1.01 = phase-done flags.
  • Timers: T0000-T0011.

Phase Model and Timing Requirements

The original specification uses two intersections with different cycle lengths. The cleanest implementation treats each intersection as an independent state machine and synchronizes them only through their phase boundaries, not through a shared clock. The required phase lengths are:

Direction Green Yellow Red Total cycle
Crossroad 1 - X (horizontal) 20 s 10 s 30 s 60 s
Crossroad 1 - Y (vertical) 20 s 10 s 30 s 60 s
Crossroad 2 - Horizontal 35 s 5 s 20 s 60 s
Crossroad 2 - Vertical 15 s 5 s 40 s 60 s

Both intersections happen to have a 60-second cycle, which is convenient: one P_1s clock pulse from A200.06 plus a single ring counter lets every timer compare against the same elapsed-time word. If you scale to a 90-second or 120-second cycle later, only the comparison constants change.

Root Cause of the "All Yellow and Red On At Power-Up" Fault

The most common mistake with a first ladder is leaving output coils energized by their own contact. Look at this classic anti-pattern, which is exactly what produces the symptom described in the source post:

|  START_PB  T0000_done   RED_LAMP_1 |
|--| |------| / |---------( )--------|
|                                  |
|  RED_LAMP_1                     |
|--| |----------------------------|

If the rung is scanned before the timer has been started (or if the timer is never started), the normally-closed T0000_done contact passes power, the lamp energizes, and on the next scan its own contact seals it in. The lamp is now permanently on until a Stop button breaks the seal-in path. When you write a program that intentionally relies on seal-in, you must also write an explicit off-rung. The fix is to never latch an output coil with its own contact; instead, drive every lamp from a comparison that is true only during its intended phase window.

A second common cause is PLC power-up with the outputs in their last state. By default, CJ1M outputs hold their previous state on power loss unless the IOM Hold Bit (A500.12) is cleared. After a brief power blip, the start rung was never re-triggered but the previous outputs are still there. Either reset outputs on first scan (use P_First_Cycle, A200.11) or, better, drive every output from a stateless comparison so the initial condition is meaningless.

Best practice: Place P_First_Cycle (A200.11) on a one-shot rung that clears all work bits, all timer PVs (via BCD CLR to the SV), and forces the state register to a defined value. This makes the program deterministic on every power-up, not just on the first commissioning.

Two Implementation Styles: Direct Ladder Compare vs. CMP Instruction

The source post specifically asked for guidance comparing the two common Omron ways to express "is the current time inside this phase?":

Style A: Direct comparison contacts (IEC 61131-3 style in CX-Programmer)

CX-Programmer 9.x and later lets you place =, <>, <, <=, >, >= contacts directly on the bus. These compile to CMP(020) under the hood but are far easier to read:

|    P_1s     |
|--| |--------|          ; A200.06 1-second pulse
|    C0,0 ---| |
|    (CNT 0  SV=60)       ; ring counter, 0 to 59
|   elapsed_time = C0 PV

|    elapsed_time >= 0   elapsed_time < 20   X_GREEN_1 |
|--|-------------------|-------------------( )---------|

|    elapsed_time >= 20  elapsed_time < 30   X_YELLOW_1 |
|--|-------------------|-------------------( )----------|

|    elapsed_time >= 30  elapsed_time < 60   X_RED_1 |
|--|-------------------|-------------------( )--------|

The Y-direction lamps use identical rungs with the offset added. Because the three phase rungs are mutually exclusive (their comparison windows never overlap), no interlock rung is required - the comparison itself guarantees only one lamp is on at a time.

Style B: CMP(020) instruction with result flags

The classic Omron way is to call CMP(020) once per cycle and read its result from the auxiliary area:

|  ALWAYS_ON       |
|--| |--------------|
|                  CMP(020)         ; Instruction
|                  S1 = elapsed_time
|                  S2 = 20
|                  ; Result flags:
|                  ;   P_GT  = A200.05 if S1>S2
|                  ;   P_EQ  = A200.06 if S1=S2
|                  ;   P_LT  = A200.07 if S1<S2

|  P_LT_flag  X_GREEN_1  |
|--| |---------( )--------|

CMP(020) is destructive - it overwrites the three flags every scan. Save the flags into work bits if you need them later in the cycle. The IEC contacts in Style A do this automatically; the legacy form requires you to add latches or use the @ differentiation suffix.

Style C: State machine with single integer register

For intersections beyond a single set of three lamps, the state-machine approach scales much better. Encode each phase as a number, drive an integer state_word, and use one compare per output:

State value Meaning Duration
0 All-red safety 3 s
1 X green, Y red 20 s
2 X yellow, Y red 10 s
3 X red, Y green 20 s
4 X red, Y yellow 10 s
5 All-red safety 3 s
|  state_word = 0  |    ; On entry to all-red state, start timer
|  T0000 |
|--| |------------|----(TON T0000, SV=30)----|  ; 30 * 0.1s = 3 s

|  T0000_done  state_word := 1 |
|--| |----------|----(MOV #1 state_word)----|

This is the approach recommended by the senior-programmer comment in the source thread - the state is the truth of the system, not the timer contacts. Compare the state, not the time.

Building the 1-Second Tick and the Elapsed-Time Counter

Every ladder above depends on a clean 1-second pulse. The CJ1M generates this from auxiliary bit A200.06 (P_1s). To convert that pulse into elapsed seconds for the comparison, use a reversible counter as a 0-59 ring:

|  P_1s    C0,0 (reset coil)|
|--| |----| >|---| |-------(RES)-------|
|                              |
|  P_1s    C0,0 (count up)    |
|--| |----| <|----------------(CNT 0, SV=60)|
|                              |
|  C0 PV --> elapsed_time (W2)|
|--| |----|MOV(021) C0 PV W2|--|

When the counter reaches 60, the reset coil fires from the >= contact and the counter rolls back to zero on the next count pulse. W2 now contains a value that grows 0, 1, 2, ... 59, 0, 1, ... and is exactly the elapsed-time register every comparison references.

Why not use TIM? You could use TIM 0 SV=60 for a single 60-second phase, but to drive multiple phases from one shared clock you need either a counter ring (as above) or a BCD/binary up-counter with reset. Using P_1s from the PLC's clock generator is more accurate than relying on the I/O scan time of the timer's rung, and it survives minor program edits without re-tuning.

Two-Crossroad Coordination

The two intersections have different phase lengths but both run a 60-second cycle. They are coordinated by offsetting the second counter's reset phase:

|  P_1s                            |
|--| |----------------------------|
|  C1,0 (reset)                   |
|--|>|------------------(RES)----|  ; Reset C1 when PV hits 60
|  C1,0 (count)                   |
|--|<|------------------(CNT 1, SV=60)|
|                                  |
|  C1 PV --> elapsed_time_2 (W3)  |
|--| |----(MOV C1_PV W3)--------|

Use the second intersection's elapsed time W3 with its own comparison constants:

|  W3 >= 0   W3 < 35   H2_GREEN  |
|--|---------|---------( )--------|
|  W3 >= 35  W3 < 40   H2_YELLOW |
|--|---------|---------( )---------|
|  W3 >= 40  W3 < 60   H2_RED    |
|--|---------|---------( )---------|

If you need crossroad 2 to start 30 seconds after crossroad 1 (a common real-traffic requirement), preload counter C1 with 30 instead of 0 at start:

|  P_First_Cycle |
|--| |-----------|
|  C1 SV := 60 - 30  ; = 30, so first reset happens after 30 ticks
|--(MOV #30 C1_SV)--|

Use CX-Programmer's counter preset edit (see W446 CX-Programmer Operation Manual) to view and modify the SV on-line.

Adding Real-Time Clock Functionality

The source author specifically asked about using the real-time clock on the CJ1M. The CJ1M CPU has a battery-backed clock accessed through the AR area and special instructions. The relevant addresses (per the W342 CS/CJ Series Programming Reference Manual) are:

Address Meaning Encoding
A351.00 30-second clock bit 0.5 s on / 0.5 s off
A351.01 30-minute clock bit 0.5 min on / 0.5 min off
A351.02-A351.07 Day-of-week, hour, minute, second flags BCD via @READ instruction
D9000-D9002 Current calendar (year/month/day, hour/min/sec) BCD, refreshed each scan on CPU21+

For a traffic-light application the most useful operations are:

Reading the current hour for night mode

|  ALWAYS_ON              |
|--| |--------------------|
|  READ(007) C1 --> D100 |
|--(BCD_TO_BIN D100 W10)--|  ; Hour, minute, second packed in D100
|                          |
|  W10 >= 22              |  ; 22:00 - 06:00 = night flash
|  W10 < 6     W12_night |
|--|-----------|--------( )---|

Scheduling a "school zone" flash

|  W10 = 7    W11_flash  |
|--|----|-----( )---------|
|  W10 = 8    W11_flash  |
|--|----|-----( )---------|

Where W11_flash becomes the override that forces the yellow lamps to blink at 0.5 s intervals using P_0_5s from A200.08:

|  W11_flash   P_0_5s    X_YELLOW_1  |
|--|---------|---------|----( )--------|

Date arithmetic (e.g. "skip the schedule on holidays") is done with the CADD(730) and CSUB(731) instructions. The W472 manual describes these in detail.

Clock accuracy: The CJ1M's RTC drifts roughly ±2 minutes per month at 25 °C and more at temperature extremes. If the schedule must be accurate to the minute, use an NTP-capable Ethernet module such as the CJ1W-ETN21 or sync daily from an external master.

Step-by-Step Commissioning Procedure

  1. Create the CX-Programmer project. File → New, select Device Type = CJ1M-H, CPU type matching your hardware. CX-Programmer 9.65 or later supports the latest CJ1M firmware.
  2. Configure the I/O table offline (or online with auto-detect). The CJ1M-CPU12/13/22/23 has 10 built-in inputs and 6 built-in outputs at fixed addresses CIO 0 and CIO 100 respectively.
  3. Set the PLC's clock. Use CX-Programmer → PLC → Clock → Set, or send the @SET(049) values from the HMI.
  4. Download and switch to Monitor mode. The CJ1M will go into Program mode briefly, then run.
  5. Force W0.00 (start) and observe outputs. Confirm only one green, one yellow, or one red lamp per direction is on at any moment, and that no lamp stays on across phase boundaries.
  6. Verify the timing. Use CX-Programmer's Data Trace (Alt+F7) or the CJ1M's built-in trace buffer to capture the first 60 seconds. Confirm each phase lasts exactly the programmed duration.
  7. Test the stop button. Confirm all outputs drop within one scan when stop is pressed, regardless of which phase the PLC is in.
  8. Power-cycle the PLC. Confirm outputs come up in a defined state (recommended: all red for 3 s) and never stuck on.

Verification: Signals to Confirm Correct Operation

Test Expected result Pass criterion
Power-up, no Start pressed All lamps off (or all red if you choose that as default) No yellow, no green
Press Start at second 0 X-green on immediately 100.02 = 1 within 1 scan
Wait 20 s X-yellow comes on, X-green goes off 100.02 = 0, 100.01 = 1 at t = 20 ±0.1 s
Wait further 10 s X-red comes on, X-yellow goes off 100.01 = 0, 100.00 = 1 at t = 30 ±0.1 s
Press Stop at any time All lamps off within 1 scan All 6 outputs = 0 within 20 ms
Simulate power loss at t = 17 s After restart, sequence resumes from t = 0 or remains paused depending on IOM Hold setting No lamp latched on indefinitely
Test Expected result Pass criterion
Crossroad 2 simultaneous start H2 green on at t = 0; H2 yellow at t = 35 s; H2 red at t = 40 s 101.01, 101.02, 101.00 transition at the right times
Crossroad 2 with 30 s offset H2 starts in state "yellow" at t = 0 101.01 = 1 at t = 0; sequence reaches steady state by t = 30 s
Real-time clock: hour = 23 Night-flash pattern active Yellow lamps blink at 1 Hz; greens and reds off

Troubleshooting Matrix

Symptom Likely cause Diagnostic Fix
All yellow + red on at power-up, never recovers Output sealed in by its own contact, or stop rung never clears it Watch the output bit in Monitor mode - does it stay on when you force it off? Replace seal-in rung with comparison-driven output
Stop button only turns off some lamps Stop rung only breaks the green seal-in; yellow/red seal-ins remain Force Stop = 1 in monitor; list which outputs drop Add a single stop rung that clears all work bits and timer PVs in one scan
Lamps flicker at 10 Hz Timer rung is being scanned faster than the timer's 0.1 s base, or SV is in wrong units Read T0 PV; confirm SV was entered as 200 (not 2000) for 20.0 s Use TIMX with explicit SV, or use P_1s counter as shown above
Phase skips (e.g., goes straight from green to red) Yellow phase timer is missing or SV = 0 Compare ladder to phase table; check T001 SV Add the missing rung with correct SV
Clock reads 2001-01-01 00:00:00 Backup battery dead or PLC never had its clock set Check BAT LED; check D9000 in monitor Replace CJ1W-BAT01 battery (3-year life); re-set clock via CX-Programmer
Outputs change but lights don't respond Wrong CIO addresses mapped to physical terminals; CIO 100.00 may be on a CJ1M-PA high-density output module Cross-reference I/O table in CX-Programmer against the wiring diagram Move symbol addresses or rewire to match
Green lights on both directions simultaneously (conflict) State machine entered illegal state due to power glitch Watch state_word; check whether it ever takes a value outside the defined set Add validity check: if state_word > 5 then state_word := 0

Porting Notes: From CJ1M to CP1E / CP1L / NX

The same patterns work on every modern Omron platform. Key differences:

  • CP1E: No P_1s in the same place - it is at A200.06 on CP1E-N as well, but confirm in the W516 CP1E CPU Unit Operation Manual. IEC comparison contacts are supported.
  • CP1L: Adds built-in Ethernet on M-type CPUs. The state-machine pattern ports unchanged.
  • NJ/NX series (Sysmac Studio): Move from ladder to Structured Text (ST). A CASE statement on the state variable replaces all the comparison contacts:
CASE state_word OF
  0: X_LAMP := RED;   Y_LAMP := RED;   T#3s_timer(IN:=TRUE, PT:=T#3s);
  1: X_LAMP := GREEN; Y_LAMP := RED;   T#20s_timer(IN:=TRUE, PT:=T#20s);
  2: X_LAMP := YELLOW;Y_LAMP := RED;   T#10s_timer(IN:=TRUE, PT:=T#10s);
  3: X_LAMP := RED;   Y_LAMP := GREEN; T#20s_timer(IN:=TRUE, PT:=T#20s);
  4: X_LAMP := RED;   Y_LAMP := YELLOW;T#10s_timer(IN:=TRUE, PT:=T#10s);
END_CASE;

The Sysmac NJ101-1000 has the same state-machine idiom but the timers are IEC 61131-3 objects instead of TIM/TIMX instructions. The W500 Sysmac NJ-series CPU Unit Software User's Manual documents the conversion.

Common Pitfalls and Field-Validated Caveats

  • Don't use the SET/RESET instruction for traffic-light outputs. It is tempting because each lamp is essentially a bistate, but a SET is sticky - it survives the condition that set it and only clears on a matching RESET. If the comparison ever fails to reset for any reason (programming error, communication glitch, watchdog), the lamp stays on indefinitely. The latched-coil failure mode is exactly the symptom described in the source post.
  • Avoid more than one coil per output bit. If two rungs write to 100.02, the last rung in the scan wins. This is the most common cause of "I added a fault indicator and now my green lamp flashes." Use OUT once, in the comparison rung, and never anywhere else.
  • Do not feed the timer's own contact back into its start. This is the classic "the timer never resets" trap. If the timer's done bit drives a coil that is on the same rung as the timer's start, the rung oscillates.
  • Watch the scan time when adding a second crossroad. The CJ1M-CPU11 in particular has a scan time around 0.7 ms for 1 K steps. Adding the second intersection's logic and clock arithmetic can push it past 2 ms, which is still fine for traffic lights but starts to be noticeable on a 0.5 s night flash. Profile with CX-Programmer → PLC → Scan Time.
  • Use a retentive timer (TIML or PV-retentive TIMX) only if you genuinely want the timer to survive a power loss. For a traffic light, you almost always want the timer to reset on power-up - the program should re-establish its starting condition, not resume mid-phase.

Recommended CX-Programmer Project Layout

  1. Section 0 - Initialization: First-scan reset, clock read, default state.
  2. Section 1 - Clock & counters: P_1s driver, C0 ring counter, C1 second-intersection counter.
  3. Section 2 - Intersection 1 outputs: six rungs, one per lamp, comparison-driven.
  4. Section 3 - Intersection 2 outputs: six more rungs with offset constants.
  5. Section 4 - Schedule overrides: night mode, school zone, manual flash.
  6. Section 5 - Diagnostics: lamp-on-too-long alarm, conflict alarm, deadman timer.

This layout maps directly to the Tasks/Programs structure on the CJ1M. Use Task 0: Cyclic Task, every 10 ms for sections 0-5; the entire program fits in the standard cyclic task and runs every scan.

Further Optimization with High-Speed Counter

If the intersection needs to react to vehicle detection (induction loops, radar, push-buttons for pedestrians), the CJ1M-CPU21+ has built-in high-speed counter inputs on 0.00-0.03. Use PRV(881) to read the count at a fixed interval and add a "green extension" phase: if a vehicle is detected during the last 5 s of green, do not transition to yellow yet. The pattern is identical to the basic state machine, with one extra state added before the yellow phase.

Source: Traffic light coordination theory is well documented - see Wikipedia: Traffic light control and coordination for the higher-level concepts of coordination, offsets, and splits. The PLC implementation here is the lowest level of that hierarchy.

FAQ

Why do all my yellow and red lamps come on the moment I press Start?

This is a seal-in (latching) problem. Your Stop rung is only breaking the seal-in path for the green lamp; the yellow and red seal-ins were established earlier and have no off-path. Replace every latched coil with a direct comparison-driven output that is only true during its phase window, and the lamp will turn off automatically when the phase ends.

Should I use TIM/TIMX or a counter driven by P_1s for the phase clock?

For a single set of three phases, either works. For multi-intersection projects where several phases share the same time base, the counter driven by A200.06 (P_1s) is easier to scale: one register drives every comparison on every intersection. Use TIMX (binary SV) rather than the legacy BCD TIM to avoid surprises when entering constants.

What is the difference between CMP(020) and the IEC =, <, > contacts in CX-Programmer?

They produce the same result. The IEC contacts are easier to read because each one is a single contact on the bus; CMP(020) is a separate instruction that writes its result into the auxiliary bits P_GT, P_EQ, P_LT and those flags must be saved to work bits if you need them later in the cycle. For simple "is elapsed time >= 20?" checks, prefer the IEC contact.

How do I use the CJ1M's real-time clock for a daily schedule?

Read the calendar from D9000-D9002 (BCD year/month/day, hour/minute/second) using the READ(007) instruction at the start of each scan, convert to binary, and compare the hour and minute against your schedule constants. Use W11 as a work bit for the "night mode" state and drive your output logic from that bit instead of the time directly.

Can I move this program to a CP1E, CP1L, or NX102?

Yes. The state-machine pattern and the comparison-driven output rungs are platform-agnostic. CP1E and CP1L accept the same ladder; on NX (Sysmac Studio), port the state machine to a CASE statement in Structured Text and the outputs to ST assignments. The auxiliary bits A200.06 and A200.11 are present on every modern Omron controller, though their addresses are identical so no change is required.

Back to blog