Cyclic Programming
Cyclic programming executes a function at regular intervals, similar to PLC programming.
When to Use Cyclic Programming
Cyclic programming is ideal for:
Deterministic timing requirements
Traditional PLC-style logic
State machines
Time-critical control
Continuous monitoring and control
Advantages:
Predictable timing
Simple mental model
Easy to reason about program flow
Natural for control systems
Considerations:
Consumes CPU even when idle
Cycle time affects responsiveness
Must keep cycle logic fast
Basic Structure
Simple Cycle Loop
import revpimodio2
def main_cycle(ct: revpimodio2.Cycletools):
"""Execute each cycle."""
if ct.io.start_button.value:
ct.io.motor.value = True
if ct.io.stop_button.value:
ct.io.motor.value = False
revpimodio2.run_plc(main_cycle)
# .run_plc is a shortcut for:
# rpi = revpimodio2.RevPiModIO(autorefresh=True)
# rpi.handlesignalend()
# rpi.cycleloop(main_cycle)
The main_cycle function is called repeatedly at the configured cycle time (typically 20-50ms).
Info: rpi.handlesignalend()
Understanding Cycle Time
The cycle time determines execution frequency:
Core 1: 40ms (25 Hz)
Core3/Connect: 20ms (50 Hz)
NetIO: 50ms (20 Hz)
Adjust cycle time to match your needs:
revpimodio2.run_plc(main_cycle, cycletime=100) # 100ms = 10 Hz
Important: Faster cycle times consume more CPU but will detect fast changes of input values.
Cycletools Object
The Cycletools object is passed to your cycle function, providing access to:
ct.io- All IOsct.core- System controlct.device- Device accessct.var- Persistent variablesLifecycle flags (
first,last)Timing flags (
flag5c,flank10c, etc.)Timer functions (
set_tonc,get_tofc, etc.)Change detection (
changed)
Initialization and Cleanup
Use ct.first and ct.last for setup and teardown:
def main_cycle(ct: revpimodio2.Cycletools):
if ct.first:
# Initialize on first cycle
ct.var.counter = 0
ct.var.state = "IDLE"
print("System started")
# Main logic runs every cycle
ct.var.counter += 1
if ct.last:
# Cleanup before exit
ct.io.motor.value = False
print(f"Total cycles: {ct.var.counter}")
revpimodio2.run_plc(main_cycle)
Persistent Variables
Use ct.var to store variables that persist between cycles:
def main_cycle(ct):
if ct.first:
ct.var.counter = 0
ct.var.state = "IDLE"
ct.var.accumulator = 0.0
# Variables persist between cycles
ct.var.counter += 1
ct.var.accumulator += ct.io.sensor.value
# Access variables later
average = ct.var.accumulator / ct.var.counter
Variables defined in ct.var maintain their values across all cycle executions.
Change Detection
Detect input changes efficiently without storing previous values:
def main_cycle(ct: revpimodio2.Cycletools):
# Detect any change
if ct.changed(ct.io.sensor):
print(f"Sensor changed to: {ct.io.sensor.value}")
# Detect rising edge (button press)
if ct.changed(ct.io.button, edge=revpimodio2.RISING):
print("Button pressed!")
# Detect falling edge (button release)
if ct.changed(ct.io.button, edge=revpimodio2.FALLING):
print("Button released!")
rpi = revpimodio2.RevPiModIO(autorefresh=True)
rpi.handlesignalend()
rpi.cycleloop(main_cycle)
Edge types:
revpimodio2.RISING- False to True transitionrevpimodio2.FALLING- True to False transitionrevpimodio2.BOTH- Any change (default)
Timing Flags
Built-in timing flags provide periodic execution without manual counting.
Toggle Flags
Toggle flags alternate between True/False at regular intervals:
def main_cycle(ct):
# Blink LED - flag5c alternates every cycle
ct.io.blink_led.value = ct.flag1c
# Different blink rates
ct.io.fast_blink.value = ct.flag5c # Every 5 cycles
ct.io.slow_blink.value = ct.flag20c # Every 20 cycles
Available toggle flags:
ct.flag1c- Every cyclect.flag5c- Every 5 cyclesct.flag10c- Every 10 cyclesct.flag20c- Every 20 cycles
Flank Flags
Flank flags are True for exactly one cycle at regular intervals:
def main_cycle(ct):
# Execute task every 10 cycles
if ct.flank10c:
print(f"Runtime: {ct.runtime:.3f}s")
# Execute task every 20 cycles
if ct.flank20c:
temp = ct.io.temperature.value
print(f"Temperature: {temp}°C")
Available flank flags:
ct.flank5c- True every 5 cyclesct.flank10c- True every 10 cyclesct.flank15c- True every 15 cyclesct.flank20c- True every 20 cycles
Timers
RevPiModIO provides three timer types based on PLC standards. All timers are specified in cycle counts or milliseconds.
On-Delay Timer (TON/TONC)
Output becomes True only after input is continuously True for specified cycles (use ton with milliseconds value instead of cycles):
def main_cycle(ct):
# Input: sensor value
ct.set_tonc("delay", 10)
# Output goes high after input is high for 10 cycles
if ct.get_tonc("delay"):
ct.io.output.value = True
else:
ct.io.output.value = False
How it works:
Input goes True
Timer starts counting
If input stays True for 10 cycles, output goes True
If input goes False before 10 cycles, timer resets
Use cases:
Button debouncing
Startup delays
Confirming sustained conditions
Off-Delay Timer (TOF/TOFC)
Output stays True for specified cycles or milliseconds after input goes False (use tof with milliseconds value instead of cycles):
def main_cycle(ct):
# Input: button value
ct.set_tofc("motor_coast", 20)
# Motor continues for 20 cycles after button release
ct.io.motor.value = ct.get_tofc("motor_coast")
How it works:
Input is True, output is True
Input goes False
Output stays True for 20 more cycles
After 20 cycles, output goes False
Use cases:
Motor coast-down
Relay hold-in
Graceful shutdowns
Pulse Timer (TP/TPC)
Generates a one-shot pulse of specified duration (use tp with milliseconds value instead of cycles):
def main_cycle(ct):
# Trigger pulse on button press
if ct.changed(ct.io.trigger, edge=revpimodio2.RISING):
ct.set_tpc("pulse", 5)
# Output is True for 5 cycles
ct.io.pulse_output.value = ct.get_tpc("pulse")
How it works:
Call
set_tpcto trigger pulseOutput is True for 5 cycles
After 5 cycles, output goes False
Additional triggers during pulse are ignored
Use cases:
One-shot operations
Acknowledgment pulses
Retriggerable delays
State Machines
State machines implement complex control logic with distinct operational modes.
Simple State Machine
def traffic_light(ct: revpimodio2.Cycletools):
"""Traffic light controller."""
if ct.first:
ct.var.state = "GREEN"
if ct.var.state == "GREEN":
ct.io.green_led.value = True
ct.io.yellow_led.value = False
ct.io.red_led.value = False
# After 2 seconds, go to yellow
ct.set_ton("green_time", 2000)
if ct.get_ton("green_time"):
ct.var.state = "YELLOW"
elif ct.var.state == "YELLOW":
ct.io.green_led.value = False
ct.io.yellow_led.value = True
ct.set_ton("yellow_time", 500)
if ct.get_ton("yellow_time"):
ct.var.state = "RED"
elif ct.var.state == "RED":
ct.io.yellow_led.value = False
ct.io.red_led.value = True
ct.set_ton("red_time", 3000)
if ct.get_ton("red_time"):
ct.var.state = "GREEN"
rpi = revpimodio2.RevPiModIO(autorefresh=True)
rpi.handlesignalend()
rpi.cycleloop(traffic_light)
Complex State Machine
def machine_controller(ct: revpimodio2.Cycletools):
"""Multi-state machine controller."""
if ct.first:
ct.var.state = "IDLE"
ct.var.production_count = 0
# State: IDLE - Ready to start
if ct.var.state == "IDLE":
ct.io.motor.value = False
ct.io.green_led.value = True
ct.io.red_led.value = False
if ct.changed(ct.io.start_button, edge=revpimodio2.RISING):
ct.var.state = "STARTING"
print("Starting...")
# State: STARTING - Startup sequence
elif ct.var.state == "STARTING":
ct.io.yellow_led.value = True
# 2-second startup delay
ct.set_ton("startup", 2000)
if ct.get_ton("startup"):
ct.var.state = "RUNNING"
print("Running")
# State: RUNNING - Normal operation
elif ct.var.state == "RUNNING":
ct.io.motor.value = True
ct.io.yellow_led.value = False
ct.io.green_led.value = ct.flag5c # Blink
# Count production
if ct.changed(ct.io.sensor, edge=revpimodio2.RISING):
ct.var.production_count += 1
# Check for stop
if ct.io.stop_button.value:
ct.var.state = "STOPPING"
# Check for error
if ct.io.error_sensor.value:
ct.var.state = "ERROR"
# State: STOPPING - Controlled shutdown
elif ct.var.state == "STOPPING":
# Coast motor for 1 second
ct.set_tof("coast", 1000)
ct.io.motor.value = ct.get_tof("coast")
if not ct.io.motor.value:
ct.var.state = "IDLE"
print("Stopped")
# State: ERROR - Fault condition
elif ct.var.state == "ERROR":
ct.io.motor.value = False
ct.io.red_led.value = ct.flag5c # Blink red
if ct.changed(ct.io.ack_button, edge=revpimodio2.RISING):
if not ct.io.error_sensor.value:
ct.var.state = "IDLE"
print("Error cleared")
if ct.last:
print(f"Total production: {ct.var.production_count}")
revpimodio.run_plc(machine_controller)
Practical Examples
Temperature Control
Temperature monitoring with hysteresis control:
def temperature_monitor(ct: revpimodio2.Cycletools):
"""Monitor temperature and control cooling."""
if ct.first:
ct.var.cooling_active = False
temp = ct.io.temperature.value
# Hysteresis: ON at 75°C, OFF at 65°C
if temp > 75 and not ct.var.cooling_active:
ct.io.cooling_fan.value = True
ct.var.cooling_active = True
print(f"Cooling ON: {temp}°C")
elif temp < 65 and ct.var.cooling_active:
ct.io.cooling_fan.value = False
ct.var.cooling_active = False
print(f"Cooling OFF: {temp}°C")
# Warning if too hot
if temp > 85:
ct.core.a1green.value = False
ct.core.a1red.value = ct.flag5c # Blink
else:
ct.core.a1green.value = ct.flag5c # Blink
ct.core.a1red.value = False
# Emergency shutdown
if temp > 95:
ct.io.emergency_shutdown.value = True
revpimodio2.run_plc(temperature_monitor)
Production Counter
Count production items with start/stop control:
def production_counter(ct: revpimodio2.Cycletools):
"""Track production count."""
if ct.first:
ct.var.total_count = 0
ct.var.running = False
# Start/stop control
if ct.changed(ct.io.start_button, edge=revpimodio2.RISING):
ct.var.running = True
if ct.changed(ct.io.stop_button, edge=revpimodio2.RISING):
ct.var.running = False
# Count items
if ct.var.running:
if ct.changed(ct.io.item_sensor, edge=revpimodio2.RISING):
ct.var.total_count += 1
ct.set_tpc("count_pulse", 5) # Pulse LED
print(f"Item #{ct.var.total_count}")
ct.io.count_led.value = ct.get_tpc("count_pulse")
# Reset counter
if ct.changed(ct.io.reset_button, edge=revpimodio2.RISING):
print(f"Final count: {ct.var.total_count}")
ct.var.total_count = 0
revpimodio2.run_plc(production_counter)
Best Practices
Keep Cycle Logic Fast
Minimize processing time in each cycle:
def optimized_cycle(ct):
# Heavy work only when needed
if ct.flank100c:
heavy_calculation()
# Keep cycle logic minimal
ct.io.output.value = ct.io.input.value
Guidelines:
Avoid blocking operations (network, file I/O)
Use flank flags for expensive operations or even Threads
Keep cycle time ≥20ms for stability
Use Appropriate Cycle Time
Match cycle time to application requirements:
# Fast control (motion, high-speed counting)
rpi.cycletime = 20 # 50 Hz
# Normal control (most applications)
rpi.cycletime = 50 # 20 Hz
# Slow monitoring (temperature, status)
rpi.cycletime = 100 # 10 Hz
Handle Errors Safely
Always implement safe failure modes:
def safe_cycle(ct):
try:
value = ct.io.sensor.value
ct.io.output.value = process(value)
except Exception as e:
print(f"Error: {e}")
ct.io.output.value = False # Safe state
Initialize Properly
Use ct.first for all initialization:
def main_cycle(ct):
if ct.first:
# Initialize all variables
ct.var.counter = 0
ct.var.state = "IDLE"
ct.var.last_value = 0
# Set initial outputs
ct.io.motor.value = False
See Also
Basics - Core concepts and configuration
Event Programming - Event-driven programming
Advanced - Advanced topics and examples
Helper Classes - Cycletools API reference