One unique feature of the new RP2040 microcontroller from the Raspberry Pi Foundation is its Programmable I/O (PIO) peripheral. The focus of PIO is serial communication. Most microcontrollers have hardware support for popular serial protocols such as I²C, SPI, and USART. However, this hardware support is always limited both in the number of serial interfaces and the types of serial interfaces that can be used. If you have a non-standard serial interface you need to support, you may have to resort to bit-banging I/O pins to implement it, tying up the microcontroller core to get the timing right. PIO aims to solve this problem by providing a highly configurable, programmable I/O peripheral that will take care of the bit-banging and provide simple input and output FIFO queues to the microcontroller core.
The RP2040 features two PIO blocks, and each has four state machines. Each state machine has its own input and output FIFO queues for communicating with the ARM cores, and can operate on any GPIO pins.
Each PIO block has memory to hold 32 instructions, with all four state machines reading from the same memory. Hence you can create four instances of the same serial interface on four sets of pins using one PIO block. The instruction set for the state machines consists of just nine instructions, but with flexible parameters aimed at facilitating well-timed I/O.
Programs for the PIO block are written in the PIO’s assembly language, those programs can be embedded in either MicroPython or C/C++ programs. Example code for the Pico is provided in the Raspberry Pi Pico Python SDK and the Raspberry Pi Pico C/C++ SDK, with full documentation of PIO assembly in the RP2040 Datasheet (These datasheets also supply the tables used in this article). Adafruit has provided an introduction to PIO in CircuitPython in their Learning System. Although support for the Raspberry Pi Pico has been added to the Arduino IDE, there is no support yet for programming the PIO blocks and few libraries support it.
Let’s look at one example application of the PIO that demonstrates what it can do. WS2812 LEDs, also known as Neopixels, use a one-wire serial interface where the lengths of the pulses are used to indicate ones and zeros.
I’ve never encountered a microcontroller with hardware WS2812 support and because the timing of pulses must be precise, the PIO’s ability to support these LEDs is welcome. We will look at just the PIO assembly code to get a sense of what it is like. There is much more code not shown that configures the PIO and its default behaviors.
Starting at line 20, we see an out instruction. In this case the instruction pulls 1 bit from the output shift register and sends it to the x scratchpad register. There is an output shift register (OSR) and an input shift register (ISR) for each state machine. These registers keep track of how many bits have been shifted in or out and can be configured to automatically move data to or from the FIFO queues. There are also two scratchpad registers called x and y that are typically used for counters. We also see two additional parameters in this line. The side parameter indicates a constant bit or bits that are to be sent to a previously specified side-set pin or pins concurrent with the execution of the instruction. In this case we are setting a single pin low. The number in brackets is a delay. It instructs the state machine to wait an additional two cycles before moving to the next instruction. The length of a PIO instruction cycle is determined by the state machine’s clock divider, which is set when the state machine is configured.
As you can see, a lot can happen in one instruction. Note that because of the way the WS2812 protocol works, the data from the output shift register is never sent to a pin. In more conventional serial interfaces, the out instruction would shift data to a pin (or pins) and the side parameter would set something like a clock signal on another pin.
Continuing with the code, line 21 performs a jump based on the bit value that was moved into x. At the same time, it sets the side-set pin high and delays for an additional cycle. If the bit moved into x is a zero, the program jumps to line 25 where the output is set to zero for five cycles, making the pulse short. If the bit is one then the program proceeds to line 23 where the output is kept high for five cycles, making the pulse long, before jumping back to the beginning. The directives .wrap and .wrap_target tell the state machine where the code ends and should restart. It is a special zero-cycle jump.
So what kind of interfaces are possible with PIO? For starters, they don’t really have to be serial. You can specify that the out instruction shifts data to a contiguous set of up to 32 pins, and likewise for in. (Note only 30 pins are available in the RP2040’s current package, and only 26 are broken out on the Pico.) The datasheet notes that you can implement a parallel bus to communicate with 8080 or 6800 microprocessors. Makers have used output pins connected to a resistor ladder to generate VGA output from PIO. A library available through the Arduino IDE charlieplexes LEDs through the PIO. But if you just want more I²C, SPI, or UART interfaces, the PIO can do that too. If an I/O task is simple, repetitive and would tie up the processor, it is a candidate for off-loading to a PIO block.