I made an Object Oriented library that supports handling PIO interrupts with C++ objects.
Goal: let a PIO state machine call the very C++ object that can manage (or watch) that particular event.
For who is this library? It's most useful in
A typical design is to have your own little object (a handler) for each state machine + interrupt combination. There is only a single requirement for the handler class(es): The () operator has to be available (overloaded). |
Let's imagine that you have 7 stepper motors. They are kept running by PIO state machines, and those state machines are set up to send an interrupt when a particular motor has done its steps. PIO will raise an interrupt, with info about what state machine and PIO has raised it. This library analyses that info, will determine what object has to handle the event, and will call that object when this happens. PIO relative interrupts aren't that straightforward. And aren't OO savvy. This lib solves the riddle of understanding what state machine threw what interrupt on what PIO. And it will call the very object that you registered for that event. Reusable and tested. With low run time costs. |
Write a handler class
You have to write a class that will react on the PIO interrupt. In the stepper motor scenario, this could be the same class that controls all aspects of a stepper motor. In my example here, it is a very small class that will count how many times it got invoked, and prints that:
struct dummy_handler { dummy_handler(PIO pio, uint sm) : pio(pio), sm(sm), counter(0) {} PIO pio; uint sm; inline void operator()() { counter += 1; printf("PIO %d SM %d: %d\n", PIO_NUM(pio), sm, counter); } private: volatile uint counter; };
This is obviously a demo design. I have given it attributes to hold state machine and PIO. That's not a requirement, just used to print meaningful info.
The only mandatory part is the void operator()().
Define the interrupt manager for your design
Easy :). We just need to create a type that knows what handler type and what interrupt number we're servicing.
using pio_irq_manager_t = pio_irq::pio_irq<handler_t, IRQ_NUM>;
That's really all there is to it. You don't have to (in fact: can't) create an object of this class. You can directly use it.
Example:
// ...
pio_irq_manager_t::register_interrupt(0, h.pio, h.sm, true);
pio_irq_manager_t::register_handler(h.pio, h.sm, &h, true);
// ...
Real Example
I wrote a simple PIO program, that just fires an interrupt 0 every second. It's Raspberry's Pico PIO blink example. Instead of blinking, it shoots the interrupt after it completes a loop (line 11).
.program run
pull block
out y, 32
.wrap_target
mov x, y [1]
lp1:
jmp x-- lp1 ; Delay for (x + 1) cycles, x is a 32 bit number
mov x, y [1]
lp2:
jmp x-- lp2 ; Delay for the same number of cycles again
irq 0 rel ; 20250824 jc throw a relative interrupt
.wrap ; run forever!
I use a Pico2 with 3 PIOs. Because this is a demo, I use each available state machine: 3 PIOs x 4 state machines is 12 state machines. So I instantiate 12 handler objects. I use an array to store them, making the code easier. But that's not needed. You could just create 12 individual object if that suits your design better:
const uint32_t IRQ_NUM = 0U;
using handler_t = dummy_handler::dummy_handler;
std::array<handler_t, 4 * NUM_PIOS> handlers {{
{pio0, 0}, {pio0, 1}, {pio0, 2}, {pio0, 3},
{pio1, 0}, {pio1, 1}, {pio1, 2}, {pio1, 3},
{pio2, 0}, {pio2, 1}, {pio2, 2}, {pio2, 3}
}};
We use the library to enable the interrupt for each of the handlers. In your design; you do that after programming the PIO(s). I left that out of the snippet below, but the attached project has that code.
using pio_irq_manager_t = pio_irq::pio_irq<handler_t, IRQ_NUM>; int main() { // ... program the PIO first // ... for (auto &h: handlers) { pio_irq_manager_t::register_interrupt(0, h.pio, h.sm, true); }
You can see the advantage of placing all handlers in a container here. They are stored in an organised way, and we can register all interrupts in a simple loop.
Then we register the handlers to the library. Once all is enabled, the operator()() will be called when a PIO state machine arrives at line 11 of its little program.
for (auto &h: handlers) { pio_irq_manager_t::register_handler(h.pio, h.sm, &h, true); } // ... enable each state machine // ...
As soon as the SMs are enabled and the handlers registered, the mechanism is active. You can see a snippet of the output here.
Each registered object gets called when its state machine fired interrupt 0. And executes the code of operator()().In this little, that is: increase counter, print message:
inline void operator()() {
counter += 1;
printf("PIO %d SM %d: %d\n", PIO_NUM(pio), sm, counter);
}
The attached archive has the code of the library and the example. It's a VSCode Pico Extension project, configured for Pico2 / RISCV. You can alter the settings with the Pico extension.
If you select a Pico1, it 'll use the 8 state machines of PIO 0 and 1 only (because the Pico1 has 1 PIO less than the Pico2. Try it!
pio_relative_interrupt_20250826.zip
In the next post, I'll review the library's design and code: OO Library to handle Pico PIO relative interrupts: library design