Program Structure - CircuitPython

The examples in this section illustrate several common approaches to structuring whole programs in CircuitPython. These are abstracted templates which will apply to any compatible microcontroller. These examples explore the tradeoffs which emerge when designing the control flow of the entire program.

Embedded microcontroller programs typically feature one or more of these processes:

  1. Sequential logic in which the program proceeds through a series of states. The activity typically includes performing actions then waiting for time to pass, sensors to trigger, or data to become available. This is often the algorithmic programming mode which students encounter first in computer science classes.

  2. Real-time synchronous computation. This often takes the form of sampling sensor input or generating output signals at regular intervals. Each input/output operation must take place precisely on schedule. The constant rate simplifies filtering and trajectory generation.

  3. Real-time asynchronous computation. This often involves processing user interface inputs or communication channels as data becomes available. Frequently the task requires these events to be handled immediately with minimal delay (low latency), so the task is classified as real-time.

Many practical systems involve a mixture of all these kinds of activity. An essential design question becomes choosing a program structure which most conveniently expresses a program given a particular balance of requirements.

An essential constraint is that CircuitPython system only supports one thread of execution in Python. (The Arduino has the same constraint in C++.) Parallel activity has to be constructed by managing a single chain of execution using some form of non-preemptive multitasking. Please note that the system itself does use various forms of background processing, e.g. to play out audio samples, but this happens at the underlying system level and not from Python.

The following sections walk through several idiomatic program structures and discuss the tradeoffs of each.

Related Pages

Event Loop

The simplest program structure example is a script which executes a series of start-up actions then enters an infinite loop. This form is used by many of the introductory examples. More detailed code discussion appears below the code sample.

Tradeoffs

Advantages.

  1. This form is well-suited for simple programs which are either entirely real-time or entirely sequential. Most of the samples are entirely real-time, and the infinite loop iterates as quickly as possible, polling all inputs and immediately computing any new outputs, with a minimum of modes or states.

  2. This form also minimizes memory usage, since all initialization expressions can be released as soon as they execute and no memory is used for function definitions. This can occasionally be a concern on tiny systems.

Disadvantages.

  1. Mixing sequential and real-time activity requires coding sequenced logic as a state machine. Typically the sequential state is captured in variables and the logic expressed as conditionals through which execution passes on every cycle. This is addressed in the State Machine example.

Template Code

Direct download: event_loop_template.py.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# event_loop_template.py

# Program structure example. For discussion, please see
# https://courses.ideate.cmu.edu/16-223/f2021/text/code/structure.html

# ----------------------------------------------------------------
# Import any needed standard Python modules.
import time

# Import any needed microcontroller or board-specific library modules.
import board, digitalio

# ----------------------------------------------------------------
# Initialize hardware.

# Some boards include a onboard LED on an I/O pin.
# led = digitalio.DigitalInOut(board.D13)
# led.direction = digitalio.Direction.OUTPUT

# ----------------------------------------------------------------
# Initialize global variables for the main loop.

# Specify the integer time stamp for a future event.
next_blink_time = time.monotonic_ns()

# ----------------------------------------------------------------
# Enter the main event loop.
while True:

    # Read the current integer clock.
    now = time.monotonic_ns()
    
    # Poll time stamps to decide if specific timed events should occur.
    if now >= next_blink_time:

        # Advance the time stamp to the next event time.
        next_blink_time += 1000000000 # one second in nanosecond units

        # Perform the timed action
        # led.value = not led.value
    
    # Other timed events could go here, including sensor processing, program
    # logic, or other outputs.

    # At the end, control continues on without delay to the next iteration of
    # 'while True'.

Code Notes

Comments. Any text appearing on a line after a # is a comment ignored by Python. It is conventional to write a few descriptive comments at the start of a program to guide human readers. It can also help readability to divide program sections with long separator bar comments.

Imports. The base Python language includes a basic set of data types and operators, but much of the useful functionality needs to be made available by importing the named modules into the program. CircuitPython includes many of these modules compiled directly within the firmware, so the import is very fast. Other additional library modules need to be loaded into memory from the virtual drive and take a little time. It is also common to divide a program into multiple .py files on the virtual drive and import them as modules from the main code.py script.

Some internal CircuitPython modules implement a subset of standard Python, including time, math, array, sys, struct, etc. Others are specific to microcontrollers, including analogio, board, digitalio, audiopwmio, etc. The full list of internal modules can be listed from the REPL using help('modules').

Hardware initialization. Usually the microcontroller hardware needs to be configured for the specific attached circuit. This is commonly done as soon as possible after startup. With CircuitPython this involves creating and configuring global objects to represent and control the hardware.

Global initialization. In this style of program, any data values held over from one loop iteration to the next needs to be held in a global variable defined at the top level of the script (i.e. not within a function or loop).

Main event loop. Typically microcontroller programs need to run forever, so the program enters some form of infinite loop. It is called an event loop because each cycle of the loop involves polling inputs to detect events and applying logic to evaluate whether to change internal state or change outputs.

Regulating time. The simplest examples regulate the execution tempo by slowing the entire main loop iteration with a delay (e.g. time.sleep(0.1)) specified in seconds. This doesn’t scale to multiple simultaneous tempos, so the example instead includes logic which uses the integer monotonic clock to decide when events should occur.

Trinket note: Some of the very smallest CircuitPython implementations (e.g. Adafruit Trinket) don’t support large integers, so the clock calculation requires slightly different code, but the concept remains the same.

Top level event loop. This particular example executes the main loop at top level (outside any functions). A more general discussion appears of this under Functions and Classes, but for this style it has the following implications:

  1. A Python script executes top-to-bottom, so any auxiliary function definitions must be placed in the code before they are used by top-level code.

  2. All state variables are global, so any assignments within functions will need global declarations, a frequent source of errors.

  3. This form is considered poor practice for general Python programming since it does not scale up easily to more complex programs.

State Machine

The next example extends the event loop structure to include sequential logic coded as a state machine. The key observation is that in a sequential program, the location of execution represents a specific state of the system, both physically and conceptually. Within the interpreter, that location is represented as a program pointer, but that value is not directly available. If that state can instead be directly represented in a variable, the sequential logic can then be expressed as rules which operate on the variable to recreate the same flow of control but without ever pausing. This also creates new opportunities for control flow, since it becomes possible for the rules to jump arbitrarily between states.

Tradeoffs

Template Code

Full direct download: event_loop_template.py.

The prologue of the program is very similar to the previous example, so only the event loop is shown to highlight the branching conditions of the state machine.

33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# The label or index representing the state machine program pointer.
current_state = 'start'

# ----------------------------------------------------------------
# Enter the main event loop.
while True:

    # Read the current integer clock.
    now = time.monotonic_ns()

    # Poll time stamps to decide if specific timed events should occur.
    if now >= next_blink_time:

        # Advance the time stamp to the next event time.
        next_blink_time += 1000000000 # one second in nanosecond units

        # Perform the timed action
        # led.value = not led.value

    # Evaluate the state machine logic.  Only one clause executes per cycle.
    if current_state == 'start':
        pass

    elif current_state == 'blinking':
        pass

    elif current_state == 'waiting':
        pass

    # At the end, control continues on without delay to the next iteration of 'while True'.

Code Notes

Background Event Loop

The next example shows a different solution in which sequential logic is still represented as a sequential program, but all real-time activity is moved within input/output and delay functions. The expectation is that the sequential logic spends the majority of time blocking on new events (i.e. pausing), then quickly applying the logic computations. The functions which block are extended to include polling of real-time processes while they wait.

Tradeoffs

Template Code

Code Notes

Functions and Classes

In practice, only the simplest Python programs are coded as a single top-level script. More typically programs are structured as functions, classes, and modules. Each of these divisions promotes modular design for clarity of data flow, software interfaces, and reusable code. This can both simplify the code and reduce programming mistakes. E.g. it is more legible to capture the state variables of a task into an object (an instance of a class) to keep all the related code in one place.

By and large the design for modularity is independent from the essential control flow architecuture. But the two work together; an event loop involves multiple inputs and state machines quickly becomes unmanagable unless the logic is expressed as classes and objects. This is especially true if there is any level of repetition, since the same code can be quickly applied to multiple devices by creating multiple object instances.