What is Programmable I/O on Raspberry Pi Pico?

By Ben Everard. Posted

Like many microcontrollers, RP2040 has hardware support for some digital communications protocols such as I2C, SPI, and UART. This means that if you want to communicate with something via I2C, you can simply send the raw data to the I2C peripheral and it will control the pins to handle the specific timing requirements for this protocol. However, what happens if you need something a little unusual such as WS2812B LEDs, or more I2C or SPI buses than are available? Pico (and RP2040) has a little trick up its sleeve: Programmable I/O.

On most microcontrollers, you have to ‘bit-bang’, which means using the main processing core to turn the pins on and off directly. This can work, but it can cause timing problems (especially if you use interrupts), and can take a lot of processing resources that you may need for other things. Pico has a little trick up its sleeve: Programmable I/O (PIO). As well as the two main Cortex-M0+ processing cores, there are two PIO blocks that each have four state machines. These are really stripped-down processing cores that can be used to handle data coming in or out of the microcontroller, and offload some of the processing requirement for implementing communications protocols.

For each simple processor (called a state machine), there are two First-In-First-Out (FIFO) structures: one for data coming in and one for data going out. These are used to link the state machine to everything else. Essentially, they are just a type of memory where the data is read out in the same order it’s written in – for this reason, they’re often called queues because they work in the same way as a queue of people. The first piece of data in (or first person in the queue) is the first to get out the other end. When you need to send data via a PIO state machine, you push it to the FIFO, and when your state machine is ready for the next chunk of data, it pulls it. This way, your PIO state machine and program running on the main core don’t have to be perfectly in-sync. The FIFOs are only four words (of 32 bits) long, but you can link these with direct memory access (DMA) to send larger amounts of data to the PIO state machine without needing to constantly write it from your main program. The FIFOs link to the PIO state machine via the input shift register and the output shift register. There are also two scratch registers called X and Y. These are for storing temporary data. The processor cores are simple state machines that have nine instructions:

  • IN shifts 1–32 bits at a time into the input shift register from somewhere (such as a set of pins or a scratch register)

  • OUT shifts 1–32 bits from the output shift register to somewhere

  • PUSH sends data to the RX FIFO

  • PULL gets data from the TX FIFO

  • MOV moves data from a source to destination

  • IRQ sets or clears the interrupt flag

  • SET writes data to destination

  • WAIT pauses until a particular action happens

  • JMP moves to a different point in the code

Within these there are options to change the behaviour and locations of the data. Take a look at the data sheet for a full overview.

In addition, there are a couple of features to make life a little easier:

  • Side-setting is where you can set one or more pins at the same time that another instruction runs

  • Delays can be added to any instruction (the number of clock cycle delays in square brackets)

Let’s have a look at a really simple example, a square wave: .program squarewave_wrap

	set pins, 1 [1]
	set pins, 0 [1]

This uses an outer wrap loop. The wrap_target label is a special case because we don’t need to do an explicit jump to it: we can use .wrap to jump straight to .wrap_target. Unlike an explicit jump instruction, this doesn’t take a clock cycle, so in this case we’re producing a perfect 50% square wave. The .wrap and .wrap_target labels can be omitted. Each instruction also has a delay of a single clock cycle.

The value of pins is set when we create the PIO program, so this can create a square wave on any I/O.

Let’s take a look at a more useful example, an SPI transmitter. This just controls the data-sending line for SPI:

.program spi_tx_fast
.side_set 1

	out pins, 1 side 0
	jmp loop side 1

This shows the power of the side-set. In this case, the side-set pin is the SCL (clock) line of SPI, and the out pin is the SDA. The out instruction takes a value from the FIFO and sets the pin to that value (either 1 or 0). The next instruction triggers the side-set on the clock pin, which causes the receiver to read the data line. Notice that the second instruction is a jump, not the wrap as in the previous example, so it does take a clock cycle to complete.

The out instruction gets a bit of data from the output shift register, and this program would quickly empty this register without a pull instruction to get data into it. However, the pull instruction would take one cycle which would complicate the timings. Fortunately, we can enable autopull when we create a PIO program, and this will continuously refill the output shift register as needed as long as data is available in the FIFO. Should the FIFO be empty, the PIO state machine will stall and wait for more data.

The speed of this SPI transmit is set up the clock speed of the PIO state machine, and this is configurable when you start the PIO.

This has been a whistle-stop tour to show off the ideas behind PIO. If you want to take a look at a simple PIO example in MicroPython, head to here.

You can get your very own Pico by getting a paper copy of HackSpace mag issue 39 or subscribing to HackSpace magazine from just £10. If you're in the UK head here. For delivery elsewhere in the world, take a look here.


From HackSpace magazine store


Subscribe to our newsletter