This post explains the internal design of OO Library to handle Pico PIO relative interrupts: usage and example .
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. |
Make PIO interrupts OO savvy
The core of this library is a single class, called pio_irq. it is specialised in configuring PIO interrupt handling, and routing any interrupts to the object that can handle it (handler objects).
- It knows how to inform a PIO state machine that we're interested in a particular PIO interrupt
A Pico will only handle a PIO generated interface, if interest is registered. Else it 'll be ignored. - It knows what object in your firmware has the duty to react on that interrupt on that PIO state machine. When an interrupt occurs, it will call that object.
The Pico SDK supports registering a function to handle a PIO interrupt. It's not possible to call a class method though. This library does that translation.
How does it do that?
pio_irq will register itself as handler of the interrupts that you want to handle. And it keeps a register of your handler objects.
When an interrupt occurs, it retrieves PIOand state machine from that info. Then looks in the registry if there is a handling object. If yes, it 'll call that object.
It also marks that interrupt as handled.
As developer, you have to do 3 things:
- Design a handler class that can be executed. The handler class. The only requirement is that It needs to have operator () overloaded.
- tell pio_irq what interrupt number you want to handle, and what class type will handle it. (this is done with a template. see later)
- tell the pio_irq what state machine to watch, and which of the objects in your design needs to be called when that state machine generates the interrupt.
When I use "a state machine" in this post, it means 1 particular state machine. Pico PIOs have 4 state machines. A Pico2, with 2 PIOs, has 8 state machines. The Pico2 has 3 PIOs, hence 12 state machines. pio_irq is able to manage all of them.
template <std::invocable H, uint32_t interrupt_number> class pio_irq { static void register_interrupt(uint irq_channel, PIO pio, uint sm, bool enable) // ... static bool register_handler(PIO pio, uint sm, H *handler, bool set) // ... private: // keep pointer of objects that serve the state machines // 2-D array with slot for all possible state machines: PIO0[0..3], PIO1[0..3], ... static std::array<H *, NUM_PIOS * 4> handlers_; };
The H in the template is the type of your handler class. The interrupt_number template parameter is the interrupt number the class should handle.
The template is generic. To have a working class in your firmware, you just need to bind the template to the parameters you want:
using pio_irq_manager_t = pio_irq::pio_irq<handler_t, 0>;
In that code,
- pio_irq_manager_t is the name of your manager
- handler_t is the type of your handler object
- 0 is the interrupt number that should be handled
You 'll end up with a static class. One that you don't need to create an object for. You can directly use its methods:
pio_irq_manager_t::register_interrupt(IRQ_CHAN, h.pio, h.sm, true);
pio_irq_manager_t::register_handler(h.pio, h.sm, &h, true);
When you bind the class, it automatically creates an array. That array has the same length as the number of state machines on your Pico (8 or 12). And each slot can hold a pointer to one of your handlers.
static std::array<H *, NUM_PIOS * 4> handlers_;
The logic is a bit involved. I created a post earlier on how to correctly register for a particular state machine / interrupt number. And then how to find what state machine fired that interrupt:Handle Raspberry Pico PIO "relative interrupts" in C++ .
What are the requirements for your handler class and objects?
There is a single requirement. Your class type has to be invocable. It needs to implement (overload) the operator()(). This is the C++ way to have something that can be executed.
Example:
struct stepper_handler {
inline void operator()() {
// .. your code to handle the interrupt that signals the end of doing X steps
}
};
A stepper motor design would use this mechanism to tell the business logic that the motor has moved the requested amount of steps. The logic can then tell another motor (maybe a press drill) to do its job ...
What other things does the library do?
There are some internal functions, to register the interrupts correctly, and to parse the interrupt payload and retrieve the state machine that fired it. These are not complex manipulations. But it's hard to get them right by reading the datasheet. All the investigating and testing has been done for this lib.
By extracting that into its own library, I try to simplify my stepper motor class design. And have a reusable library, helpful in other designs. The end user impact in my stepper motor library will ne 0. Because this lib will still call the same object. But instead of an embedded class managing the interrupt, it 'll be this new library.
In the end, both libs will become simpler, and more broadly usable...
bonus: only accept handler objects that can be executed?
Constraints are cheap: they have no runtime cost. Everything is evaluated at build time. Nothing is added to the data or code size. If your design fails to meet the constraint, the compilation will fail.
Code
module; #include <array> #include "hardware/pio.h" #include <concepts> #include <utility> export module pio_irq; // find for what state machine a relative interrupt was thrown uint sm_from_interrupt(const uint32_t irq_val, const uint32_t ir) { uint i; for (i = 0; i < 4; i++) { // should be sm 0 .. 3 if (irq_val & 1 << i) { // is bit set? break; } } assert(i != 4); // develop check there has to be a bit return i; } // calculate relative IRQ flag for a state machine // 10 (REL): the state machine ID (0…3) is added to the IRQ flag index, by way of // modulo-4 addition on the two LSBs inline uint relative_interrupt(const uint32_t ir, const uint sm) { uint32_t retval = ir & 0x03; // last 2 bits retval += sm; // add relative value (is sm) retval = retval % 4; // mod 4 retval |= ir & 0xfffffffc; return retval; } // utility calculates the index for the object that serves a state machine in handlers_ size_t index_for(PIO pio, uint sm) { return PIO_NUM(pio) * 4 + sm; } // utility to do math on pio_interrupt_source enum inline pio_interrupt_source interrupt_source(const pio_interrupt_source is, const uint32_t ir){ return static_cast<pio_interrupt_source>(std::to_underlying(is) + ir); } // creating non-inline wrapper for some API calls, because GCC 15.1 for RISKV complains about exposing local TU void _pio_set_irq0_source_enabled(PIO pio, pio_interrupt_source_t source, bool enabled) { pio_set_irq0_source_enabled(pio, source, enabled); } void _pio_set_irq1_source_enabled(PIO pio, pio_interrupt_source_t source, bool enabled) { pio_set_irq1_source_enabled(pio, source, enabled); } void _pio_interrupt_clear(PIO pio, uint ir) { pio_interrupt_clear(pio, ir); } export namespace pio_irq { /* PIO interrupts can only call functions without parameters. They can't call object members. This static embedded class matches interrupts to the relevant object. These handler objects have to implement the () operation. */ // guard that handler H has to support () operator. template <std::invocable H, uint32_t interrupt_number> class pio_irq { private: pio_irq() = delete; // static class. prevent instantiating. public: static void register_interrupt(uint irq_channel, PIO pio, uint sm, bool enable) { assert (irq_channel < 2); // develop check that we use 0 or 1 only uint irq_num = PIO0_IRQ_0 + 2 * PIO_NUM(pio) + irq_channel; irq_handler_t handler = nullptr; if (irq_channel == 0) { _pio_set_irq0_source_enabled(pio, interrupt_source(pis_interrupt0, relative_interrupt(interrupt_number, sm)), true); } else { _pio_set_irq1_source_enabled(pio, interrupt_source(pis_interrupt0, relative_interrupt(interrupt_number, sm)), true); } switch (PIO_NUM(pio)) { case 0: handler = interrupt_handler_PIO0; break; case 1: handler = interrupt_handler_PIO1; break; #if (NUM_PIOS > 2) // pico 2 case 2: handler = interrupt_handler_PIO2; break; #endif } irq_add_shared_handler(irq_num, handler, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY ); //Set the handler in the NVIC if (enable) { irq_set_enabled(irq_num, true); } } // if an object is currently handling the pio + sm combination, it will // be replaced and will no longer receive interrupts // return false as warning if an existing combination is replaced static bool register_handler(PIO pio, uint sm, H *handler, bool set) { size_t idx = index_for(pio, sm); H *old = handlers_[idx]; handlers_[idx] = set ? handler : nullptr; return set ? old == nullptr : true; } private: // forwards the interrupt to the surrounding class static void interrupt_handler(PIO pio) { if (pio->irq == 0U) { return; // we can't handle IRQs that don't have sm info } assert (pio->irq); // there should always be a sm uint sm = sm_from_interrupt(pio->irq, interrupt_number); // TODO: do I need to retrieve the ir, or is it this->ir? uint ir = relative_interrupt(interrupt_number, sm); _pio_interrupt_clear(pio, ir); // TODO: should I clear if there is no handler? H *handler = handlers_[index_for(pio, sm)]; if (handler != nullptr) { (*handler)(); } } static inline void interrupt_handler_PIO0() { interrupt_handler(pio0); } static inline void interrupt_handler_PIO1() { interrupt_handler(pio1); } #if (NUM_PIOS > 2) // pico 2 static inline void interrupt_handler_PIO2() { interrupt_handler(pio2); } #endif // keep pointer of objects that serve the state machines // 2-D array with slot for all possible state machines: PIO0[0..3], PIO1[0..3], ... static std::array<H *, NUM_PIOS * 4> handlers_; }; // static data member must be initialised outside of the class, or the linker will not capture it template <std::invocable H, uint32_t interrupt_number> std::array<H *, NUM_PIOS * 4> pio_irq<H, interrupt_number>::handlers_; // lib currently supports 1 (base) class without considerations. // Behaviour when more than one handler is registered for the same sm/interrupt number combination is undefined. // If you use one handler class and one interrupt number, the library guarantees that there will be no conflict. // else, take care that there's no overlap } // namespace pio_irq