Your prototype reports an event. Which job actually finished?

Two functions work on their own, then behave strangely when you combine them. Perhaps a pulse reader and a display driver both notify the main program that something happened. Before blaming the processor's speed, check whether those notifications still mean what the application thinks they mean.

On an RP2040 prototype, PIO is a small hardware sequencer that can do work independently of the Python loop. An interrupt flag lets it ask the software for attention. The important point is that a flag is a signal chosen by the program. Its number does not, by itself, prove which physical device finished a job.

We ran a compact experiment on our bench that makes this visible: one PIO state machine raises two different flags, and a Python callback prints them. It is a useful reference when integrating separately working modules into a test fixture or an interactive device.

Follow the one machine that is actually running

The test creates only StateMachine(0). Its program waits, sets flag 0, waits again, sets flag 1 and repeats. Python starts the state machine, waits five seconds, then stops it. No second state machine needs to be running for the second flag to appear.

It is tempting to describe these as "the interrupts of state machines 0 and 1." That wording leads to the wrong conclusion: irq(1) sets flag 1; it does not start state machine 1. The MicroPython rp2 reference describes irq() as a flag operation.

A minimal version of the structure looks like this:

import rp2
import time

@rp2.asm_pio()
def two_flags():
    wrap_target()
    nop() [31]
    irq(0)          # set flag 0
    nop() [31]
    irq(1)          # set flag 1, from the same state machine
    wrap()

sm = rp2.StateMachine(0, two_flags, freq=2000)
sm.active(1)
time.sleep(5)
sm.active(0)

Our run used several delayed nop() instructions between flags to slow the output down; the structure is the same. freq=2000 requests a 2,000 Hz state-machine clock, and each nop() [31] costs the instruction plus its delay. A code comment that described the frequency as the system clock divided by 2,000 was wrong, so do not copy that into a timing calculation.

Read the printed number in the right context

The handler is registered on the PIO block, not separately on each state machine, with rp2.PIO(0).irq(handler). Inside the handler, pio.irq().flags() returns the status, which our handler printed in decimal and as a twelve-bit binary string.

The console alternated between 256 and 512. Those are bit masks: 256 selects bit 8 and 512 selects bit 9, counting from zero. They are not counts of interrupts or device identification numbers. In this experiment, they correspond to the two flags raised by the same running program.

The register excerpt labels those positions SM0 and SM1. Keep the label and the behavior separate. MicroPython's v1.23.0 rp2_pio.c exposes the corresponding PIO masks as 0x100 and 0x200 and records the masked interrupt status for this callback. Those names do not establish that two independent jobs completed.

That distinction matters once a second feature joins the prototype. Write down what each flag means in your application, which program may raise it and which handler consumes it. A flag might mean "a sample is ready"; it should not quietly mean both "a sample is ready" and "an output sequence has finished." This is the convention we recommend; we have not run a second experiment to demonstrate it.

Keep notification and response separate

There is a real integration question behind this exercise. In a September 2024 MicroPython discussion, a developer reported that two PIO programs worked separately but conflicted once combined. The author later reported understanding the interrupt setup, and replies clarified callback behavior. The thread is good evidence of the confusion; its proposed documentation edits are not the API specification.

The rp2.PIO API uses a scheduled handler by default (hard=False). Our test relied on that default and printed inside the callback. The console showed the handler doing something visible on MicroPython v1.23.0 on a Pico, but a scrolling terminal cannot tell you the delay between the hardware event and the Python response. It is a functional record, not a latency or maximum-event-rate measurement.

Do not switch a print-heavy handler to a hard interrupt as a quick performance fix. Hard handlers have restrictions, including on memory allocation; follow MicroPython's interrupt-handler guidance and keep the immediate work short. Deciding how to record pending work is part of the design. A flag alone does not preserve a history of every event or its data.

For a prototype that must keep every sample, specify where samples are buffered and what happens when the application falls behind. For one that only needs the latest state, merging repeated notifications may be acceptable. These are different requirements, and a five-second flag demonstration validates neither under a real workload.

Before combining the next feature

Start with a simple event map, one row per event:

Event meaning Producing program Flag Receiving handler
Sample ready Pulse reader 0 Store sample
Frame sent Display output 1 Queue next frame

The rows are an illustration; fill them in from your own design. Then exercise one producer at a time and confirm the intended action runs. Add the second producer and repeat while the display, storage or communications are busy. Check both the response time and whether events or data are lost.

The useful outcome is not a terminal full of numbers. It is an application where every notification has an agreed meaning and the device still responds correctly when features run together.