Skip to content

Programmable IO on the Raspberry Pi Pico with zeptoforth

tabemann edited this page May 18, 2022 · 6 revisions

Introduction

Programmable input/output (PIO) on the RP2040 (i.e. the Raspberry Pi Pico) as interfaced with by the pio module provides a means to input to or output from pins in a very high-speed fashion at some speed up to the system clock of 125 MHz. There are two PIO peripherals, PIO0 and PIO1, each of which contain four state machines.

PIO's may have up to 32 PIO instructions in their memory, which are 16 bits in size each. PIO state machines may be set to wrap from a top instruction to a bottom instruction automatically, unless a jmp instruction is executed at the top address. Instructions may be loaded into a PIO's instruction memory with pio-instr-mem!. Instructions may also be fed into a state machine to be executed immediately with sm-instr!. The address to execute PIO instructions at may be set for a state machine with sm-addr!

Up to four PIO state machines may be enabled, disabled, or reset at a time with sm-enable, sm-disable, or sm-restart respectively. These take a bitset of four bits where the position of each bit corresponds to the index of the state machine to enable, disable, or restart.

Each PIO state machine has four 32-bit registers, an input shift register (ISR), an output shift register (OSR), an X register, and a Y register. They also have a 5-bit program counter (PC). These are all initialized to zero.

Each PIO state machine has an RX FIFO and a TX FIFO of four 32-bit values each. Note that the RX FIFO and TX FIFO on a PIO state machine may be joined into a single unidirectional FIFO consisting of eight 32-bit values. The RX FIFO for a state machine may be pushed to from a state machine's ISR register, which is 32-bits in size. The TX FIFO for a state machine may be pulled from to a state machine's OSR register, which is also 32-bits in size.

PIO state machines may automatically pull from its TX FIFO after a threshold number of bits have been shifted out of its OSR register. They may also automatically push to its RX FIFO after a threshold number of bits have been shifted into its ISR register.

The clock divider for a state machine is set with sm-clkdiv!, which takes a fractional component (from 0 to 255) and an integral component (from 0 to 65536) to divide the system clock by for the clock rate of the state machine in question. Note that if the integral clock divisor is 0 it is treated as 65536, and in those cases the fractional clock divisor must be 0.

PIO state machines may either have an optional delay associated with each PIO instruction, or may have sideset enabled, where they may set the state of up to five output pins each cycle simultaneous with whatever other operations they are carrying out. sm-delay-enable is used to enable delay mode and sm-sideset-enable is to enable sideset mode.

PIO assembler words compile PIO instructions to here as 16 bits per instruction. There are two different basic types of PIO instructions - instructions without an associated delay or sideset, and instructions with an associated delay or sideset. The latter kind of instruction is marked with an + in its assembling word.

Examples

Here is an example of PIO's in action to implement a fading blinky using the LED onboard the Raspberry Pi Pico:

interrupt import
pio import
task import
systick import

Here are our initial imports.

\ The initial setup
create pio-init
1 SET_PINDIRS set,
0 SET_PINS set,

Here we assemble the initial PIO instructions to be sent to the PIO state machine we will be using on initialization. These instructions configure the pin directions, i.e. 1 for pin 25, which is pin 0 relative to the base SET pin of 25, and set the initial state of said pin to low.

\ The PIO code
create pio-code
PULL_BLOCK PULL_NOT_EMPTY pull,
32 OUT_X out,
1 SET_PINS set,
3 COND_X1- jmp,
PULL_BLOCK PULL_NOT_EMPTY pull,
32 OUT_X out,
0 SET_PINS set,
7 COND_X1- jmp,

Here we assemble the main PIO program to execute. This program will first pull a value from the TX FIFO for the PIO state machine in use into the state machine's OSR register, which will be fed in by the PIO0 IRQ0 interrupt service routine and then write all 32-bit bits of the OSR register into the X register. Then it will set pin 25, i.e. pin 0 relative to the base SET pin of 25, high. Finally it will count down the value of X in a tight loop until it reaches zero. Then it repeates the whole process except that instead of setting pin 25 high it will set pin 25 low. Once the final loop exits, the state machine will wrap around to the first instruction, using the wrap parameters that will be set later.

\ The blinker state
variable blinker-state

\ The blinker maximum input shade
variable blinker-max-input-shade

\ The blinker maximum shade
variable blinker-max-shade

\ The blinker shading
variable blinker-shade

\ The blinker shade step delay in 100 us increments
variable blinker-step-delay

Here are our variables for controlling the shading process.

\ The blinker shade conversion routine
: convert-shade ( i -- shade ) 0 swap 2dup 2dup f* 0,01 f* d+ nip ;

This implements our polynomial, 0.01x^2 + x, for converting our shading index into a shading brightness value, and then returns the value as a single-cell integer.

\ PIO interrupt handler
: handle-pio ( -- )
  blinker-state @ not if
    blinker-shade @ 0 PIO0 sm-txf!
  else
    blinker-max-shade @ blinker-shade @ - 0 PIO0 sm-txf!
  then
  blinker-state @ not blinker-state !
  PIO0_IRQ0 NVIC_ICPR_CLRPEND!
;

This is our interrupt handler for feeding shading values into the PIO from a CPU and automatically alternating between on and off values quickly, so as to produce an illusion to the eye of the LED being a shade of brightness rather than completely on or completely off even though it is really blinking on and off very fast.

\ Set the shading
: blinker-shade! ( i -- )
  blinker-max-input-shade @ convert-shade blinker-max-shade !
  convert-shade blinker-shade !
;

This word sets the parameters for controlling the shading process, specifically the maximum shade value and the current shade value, while invoking convert-shade to apply the shading polynomial to them.

\ The blinker shading task loop
: blinker-shade-loop ( -- )
  begin
    blinker-max-input-shade @ 0 ?do
      blinker-shade!
      systick-counter linker-step-delay @ current-task delay
    loop
    0 blinker-max-input-shade @ ?do	
      blinker-shade!
      systick-counter linker-step-delay @ current-task delay
    -1 +loop
  again
;

This word implements the main loop of a task which linearly alternately increases and decreases the shading value while waiting a delay between each time the shading value is changed.

\ Init blinker
: init-blinker ( -- )
  true blinker-state !
  500 blinker-max-input-shade !
  25 blinker-step-delay !
  0 blinker-shade!
  %0001 PIO0 sm-disable
  %0001 PIO0 sm-restart
  0 758 0 PIO0 sm-clkdiv!
  25 1 0 PIO0 sm-set-pins!
  0 7 0 PIO0 sm-wrap!
  on 0 PIO0 sm-out-sticky!
  pio-init 2 0 PIO0 sm-instr!
  pio-code 8 PIO0 pio-instr-mem!
  0 0 PIO0 sm-addr!
  blinker-shade @ 0 PIO0 sm-txf!
  ['] handle-pio PIO0_IRQ0 16 + vector!
  0 INT_SM_TXNFULL IRQ0 PIO0 pio-interrupt-enable
  PIO0_IRQ0 NVIC_ISER_SETENA!
  %0001 PIO0 sm-enable
  0 ['] blinker-shade-loop 420 128 512 spawn run
;

Here is the meat of configuring the PIO to carry out the shading of the LED on the Raspberry Pi Pico. First we set some variables to configure the shading process. Then we disable and reset state machine 0 of PIO0, i.e. bit 0 as represented by %0001. Then we set its clock divisor relative to the system clock to 758. We then set the SET pin base to GPIO pin 25 and the SET pin count to 1. Afterwards we set wrapping for the state machine to have a bottom instruction of 0 and a top instruction of 7; note that wrapping only occurs from the top instruction when its JMP instruction does not branch. After that we set outputs to be sticky for the state machine. Once the state machine is configured we feed in two initialization instructions with sm-instr!, load the whole PIO program with pio-instr-mem!, and add the initial blinker shading to the state machine's TX FIFO. Finally, we set up the PIO0_IRQ0 interrupt vector, enable the PIO0 state machine 0 TX FIFO not-full interrupt, enable the interrupt PIO0_IRQ0, enable the state machine, and start the task to carry out the shading changes for the LED.