Files
revpimodio2/docs/cyclic_programming.rst
T
akira 3294c5e980 docs: Update documentation for improved clarity and consistency
Revised various sections across multiple documentation files to reflect
updated methods (`run_plc` replacing manual setup with `cycleloop`) and
adjust for new default parameters (e.g., `autorefresh`). Enhanced
descriptions for timers, Cycletools usage, and new method explanations.
Removed outdated or redundant examples and updated system requirements.

Signed-off-by: Sven Sager <akira@narux.de>
2026-02-17 12:05:58 +01:00

15 KiB

<?xml version="1.0" encoding="utf-8"?> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> <head> <style type="text/css"> /* :Author: David Goodger (goodger@python.org) :Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $ :Copyright: This stylesheet has been placed in the public domain. Default cascading style sheet for the HTML output of Docutils. Despite the name, some widely supported CSS2 features are used. See https://docutils.sourceforge.io/docs/howto/html-stylesheets.html for how to customize this style sheet. */ /* used to remove borders from tables and images */ .borderless, table.borderless td, table.borderless th { border: 0 } table.borderless td, table.borderless th { /* Override padding for "table.docutils td" with "! important". The right padding separates the table cells. */ padding: 0 0.5em 0 0 ! important } .first { /* Override more specific margin styles with "! important". */ margin-top: 0 ! important } .last, .with-subtitle { margin-bottom: 0 ! important } .hidden { display: none } .subscript { vertical-align: sub; font-size: smaller } .superscript { vertical-align: super; font-size: smaller } a.toc-backref { text-decoration: none ; color: black } blockquote.epigraph { margin: 2em 5em ; } dl.docutils dd { margin-bottom: 0.5em } object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { overflow: hidden; } /* Uncomment (and remove this text!) to get bold-faced definition list terms dl.docutils dt { font-weight: bold } */ div.abstract { margin: 2em 5em } div.abstract p.topic-title { font-weight: bold ; text-align: center } div.admonition, div.attention, div.caution, div.danger, div.error, div.hint, div.important, div.note, div.tip, div.warning { margin: 2em ; border: medium outset ; padding: 1em } div.admonition p.admonition-title, div.hint p.admonition-title, div.important p.admonition-title, div.note p.admonition-title, div.tip p.admonition-title { font-weight: bold ; font-family: sans-serif } div.attention p.admonition-title, div.caution p.admonition-title, div.danger p.admonition-title, div.error p.admonition-title, div.warning p.admonition-title, .code .error { color: red ; font-weight: bold ; font-family: sans-serif } /* Uncomment (and remove this text!) to get reduced vertical space in compound paragraphs. div.compound .compound-first, div.compound .compound-middle { margin-bottom: 0.5em } div.compound .compound-last, div.compound .compound-middle { margin-top: 0.5em } */ div.dedication { margin: 2em 5em ; text-align: center ; font-style: italic } div.dedication p.topic-title { font-weight: bold ; font-style: normal } div.figure { margin-left: 2em ; margin-right: 2em } div.footer, div.header { clear: both; font-size: smaller } div.line-block { display: block ; margin-top: 1em ; margin-bottom: 1em } div.line-block div.line-block { margin-top: 0 ; margin-bottom: 0 ; margin-left: 1.5em } div.sidebar { margin: 0 0 0.5em 1em ; border: medium outset ; padding: 1em ; background-color: #ffffee ; width: 40% ; float: right ; clear: right } div.sidebar p.rubric { font-family: sans-serif ; font-size: medium } div.system-messages { margin: 5em } div.system-messages h1 { color: red } div.system-message { border: medium outset ; padding: 1em } div.system-message p.system-message-title { color: red ; font-weight: bold } div.topic { margin: 2em } h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { margin-top: 0.4em } h1.title { text-align: center } h2.subtitle { text-align: center } hr.docutils { width: 75% } img.align-left, .figure.align-left, object.align-left, table.align-left { clear: left ; float: left ; margin-right: 1em } img.align-right, .figure.align-right, object.align-right, table.align-right { clear: right ; float: right ; margin-left: 1em } img.align-center, .figure.align-center, object.align-center { display: block; margin-left: auto; margin-right: auto; } table.align-center { margin-left: auto; margin-right: auto; } .align-left { text-align: left } .align-center { clear: both ; text-align: center } .align-right { text-align: right } /* reset inner alignment in figures */ div.align-right { text-align: inherit } /* div.align-center * { */ /* text-align: left } */ .align-top { vertical-align: top } .align-middle { vertical-align: middle } .align-bottom { vertical-align: bottom } ol.simple, ul.simple { margin-bottom: 1em } ol.arabic { list-style: decimal } ol.loweralpha { list-style: lower-alpha } ol.upperalpha { list-style: upper-alpha } ol.lowerroman { list-style: lower-roman } ol.upperroman { list-style: upper-roman } p.attribution { text-align: right ; margin-left: 50% } p.caption { font-style: italic } p.credits { font-style: italic ; font-size: smaller } p.label { white-space: nowrap } p.rubric { font-weight: bold ; font-size: larger ; color: maroon ; text-align: center } p.sidebar-title { font-family: sans-serif ; font-weight: bold ; font-size: larger } p.sidebar-subtitle { font-family: sans-serif ; font-weight: bold } p.topic-title { font-weight: bold } pre.address { margin-bottom: 0 ; margin-top: 0 ; font: inherit } pre.literal-block, pre.doctest-block, pre.math, pre.code { margin-left: 2em ; margin-right: 2em } pre.code .ln { color: gray; } /* line numbers */ pre.code, code { background-color: #eeeeee } pre.code .comment, code .comment { color: #5C6576 } pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } pre.code .literal.string, code .literal.string { color: #0C5404 } pre.code .name.builtin, code .name.builtin { color: #352B84 } pre.code .deleted, code .deleted { background-color: #DEB0A1} pre.code .inserted, code .inserted { background-color: #A3D289} span.classifier { font-family: sans-serif ; font-style: oblique } span.classifier-delimiter { font-family: sans-serif ; font-weight: bold } span.interpreted { font-family: sans-serif } span.option { white-space: nowrap } span.pre { white-space: pre } span.problematic, pre.problematic { color: red } span.section-subtitle { /* font-size relative to parent (h1..h6 element) */ font-size: 80% } table.citation { border-left: solid 1px gray; margin-left: 1px } table.docinfo { margin: 2em 4em } table.docutils { margin-top: 0.5em ; margin-bottom: 0.5em } table.footnote { border-left: solid 1px black; margin-left: 1px } table.docutils td, table.docutils th, table.docinfo td, table.docinfo th { padding-left: 0.5em ; padding-right: 0.5em ; vertical-align: top } table.docutils th.field-name, table.docinfo th.docinfo-name { font-weight: bold ; text-align: left ; white-space: nowrap ; padding-left: 0 } /* "booktabs" style (no vertical lines) */ table.docutils.booktabs { border: 0px; border-top: 2px solid; border-bottom: 2px solid; border-collapse: collapse; } table.docutils.booktabs * { border: 0px; } table.docutils.booktabs th { border-bottom: thin solid; text-align: left; } h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { font-size: 100% } ul.auto-toc { list-style-type: none } </style> </head>

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 IOs
  • ct.core - System control
  • ct.device - Device access
  • ct.var - Persistent variables
  • Lifecycle 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 transition
  • revpimodio2.FALLING - True to False transition
  • revpimodio2.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 cycle
  • ct.flag5c - Every 5 cycles
  • ct.flag10c - Every 10 cycles
  • ct.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 cycles
  • ct.flank10c - True every 10 cycles
  • ct.flank15c - True every 15 cycles
  • ct.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:

  1. Input goes True
  2. Timer starts counting
  3. If input stays True for 10 cycles, output goes True
  4. 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:

  1. Input is True, output is True
  2. Input goes False
  3. Output stays True for 20 more cycles
  4. 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:

  1. Call set_tpc to trigger pulse
  2. Output is True for 5 cycles
  3. After 5 cycles, output goes False
  4. 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

  • :doc:`basics` - Core concepts and configuration

    System Message: ERROR/3 (<stdin>, line 587); backlink

    Unknown interpreted text role "doc".

  • :doc:`event_programming` - Event-driven programming

    System Message: ERROR/3 (<stdin>, line 588); backlink

    Unknown interpreted text role "doc".

  • :doc:`advanced` - Advanced topics and examples

    System Message: ERROR/3 (<stdin>, line 589); backlink

    Unknown interpreted text role "doc".

  • :doc:`api/helper` - Cycletools API reference

    System Message: ERROR/3 (<stdin>, line 590); backlink

    Unknown interpreted text role "doc".

</html>