Programmable IO on the Raspberry Pi Pico with zeptoforth - tabemann/zeptoforth GitHub Wiki

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 setup-prog. 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 :pio pio-init \ Set GPIO 25 to be output 1 SET_PINDIRS set,

 \ Set GPIO 25 to be low
 0 SET_PINS set,

;pio

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
:pio pio-code
  \ Pull from the TX FIFO into the OSR register even if it is not empty,
  \ blocking if the TX FIFO is empty
  PULL_BLOCK PULL_NOT_EMPTY pull,
  
  \ Move 32 bits from the OSR into the X register
  32 OUT_X out,
  
  \ Set GPIO 25 to be high
  1 SET_PINS set,
  
  \ A mark to jump to for the next instruction
  mark<
  
  \ Jump to the previous mark if X is non-zero, post-decrement
  COND_X1- jmp<
  
  \ Pull from the TX FIFO into the OSR register even if it is not empty,
  \ blocking if the TX FIFO is empty
  PULL_BLOCK PULL_NOT_EMPTY pull,
  
  \ Move 32 bits from the OSR into the X register
  32 OUT_X out,
  
  \ Set GPIO 25 to be low
  0 SET_PINS set,
  
  \ A mark to jump to for the next instruction
  mark<
  
  \ Jump to the previous mark if X is non-zero, post-decrement
  COND_X1- jmp<
;pio

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 repeats 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
      i blinker-shade!
      systick-counter blinker-step-delay @ current-task delay
    loop
    0 blinker-max-input-shade @ ?do	
      i blinker-shade!
      systick-counter blinker-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 PIO0 pins-pio-alternate
  25 1 0 PIO0 sm-set-pins!
  on 0 PIO0 sm-out-sticky!
  pio-init p-prog 0 PIO0 sm-instr!
  0 PIO0 pio-code 0 setup-prog
  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 setup-prog, 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.