This is an old revision of the document!


Paspberry Pi Pico - Programmable I/O

This manual is for programming Raspberry Pi Pico's programmable in- and output pins (PIO) in MicroPython.
Here one finds all information I could find about the python PIO assemmbly language.

More and general information about the Pico one finds on this wiki here: Paspberry Pi Pico.

In chapter 3 of the RP2040 Datasheet is a detailed description of the functionality of the programmable I/Os. Most information are taken from there as well as from the Raspberry Pi Pico Python SDK and the Raspberry Pi Pico C/C++ SDK.

The SDK and APIs ducumentation:
Pico SDK
Programmable I/O (PIO) API
PIO state machine configuration


Structure

The RP2040 contains two programmable IO blocks with four state machines each, to control GPIOs and to transfer data.

Each PIO block has one instruction memory with 32 instructions. Four read ports allows all state machine simultaneously access.
Each state machine has:
- two 32-bit shift registers (OSR out, ISR in), could shift left or right
- two 32-bit scratch registers/temporary register (X,Y)
- two FIFO - “first in, first out” data buffer, one for each direction (TX/RX), they could be reconfigured in single direction (both TX or both RX)
- a clock divider
- GPIO mapping (Input, Output, Set, Sideset)
- a direct memory access (DMA) interface, 1 word per clock from system DMA
- an interrupt


diagrams taken from the RP2040 Datasheet


Import the Python Library

In Python first one has to import the library, ether the whole:

 import rp2

or just the module one needs.

 from rp2 import PIO, StateMachine, asm_pi

Instantiate the StateMachine

To instantiate a StateMachine one needs the following parameters:

state machine number use 0 to 7 to choose one of the eight state machines

name of the program for the PIO code

frequency the frequency of the state machine, should be between 1000 and 125000000

GPIO pin depending on the first on all following pins will be mapped (up to 32)
INPUT: in_base (sets input pins)

 sm = StateMachine(0, do_something, freq=1000, in_base=pin10)

OUTPUT: out_base (set output pins)

 sm = StateMachine(1, do_something, freq=1000, out_base=Pin(PIN_BASE + i))

SET: set_base (set pins)

 sm = StateMachine(0, do_something, freq=20000, set_base=Pin(10))

SIDESET: sideset_base (set sideset pins)

 sm = StateMachine(0, do_more, freq=8_000_000, sideset_base=Pin(22))

or nothing

 sm = rp2.StateMachine(0, do_less, freq=1000)

combinations are possible, too

 sm = StateMachine(0, do_something, freq=9200, sideset_base=Pin(0), out_base=pin(1))

StateMachine Commands

active can turn the state machine on and off

 sm.active(1)  #turns on the StateMachine
 sm.active(0)  #turns off the StateMachine

put will send data to the state machine's FIFO

 sm.put(value) #sends data to the TX FIFO
 sm.put(ar, 8)

get reads data from the state machine's FIFO

 sm.get() & 0xff) # reads data from the RX FIFO

exec could run arbitrary commands

 sm.exec("set(pins, 0)") #turns pin(s) on or off
 
 sm.pull(value)   #sends value to FIFO
 sm.exec("pull()"   #moves data from FIFO to OSR
 sm.exec("mov(isr, osr)")  #moves data from OSR to ISR

irq calls the interrupt handler

 sm.irq(handler) #Set the IRQ handler
 sm.irq(lambda p: print(time.ticks_ms())) #Set the IRQ handler to print the millisecond timestamp

Decorator for PIO Assembly

The decorator let MicroPython know that a method is written in PIO assembly. It has to be written right before the actual program.

@asm_pio(parameter)

parameter
- out_init (PIO.OUT_LOW, PIO.OUT_HGH)
- set_init (PIO.OUT_LOW, PIO.OUT_HGH)
- sideset_init (PIO.OUT_LOW, PIO.OUT_HGH)

- in_shiftdir (PIO.SHIFT_LEFT, PIO.SHIFT_RIGHT)
- out_shiftdir (PIO.SHIFT_LEFT, PIO.SHIFT_RIGHT)

- autopull (True, False)
- autopush (True, False) - pull_thresh (0-31)
- push_thresh (0-31)

 import rp2
 
 @rp2.asm_pio()
 @rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
 @rp2.asm_pio(sideset_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
 @rp2.asm_pio(out_shiftdir=0, autopull=True, pull_thresh=8, autopush=True, push_thresh=8, sideset_init=(rp2.PIO.OUT_LOW, rp2.PIO.OUT_HIGH), out_init=rp2.PIO.OUT_LOW)
 from rp2 import PIO, StateMachine, asm_pio
 
 @asm_pio()
 @asm_pio(set_init=PIO.OUT_LOW)
 @asm_pio(sideset_init=PIO.OUT_LOW)
 @asm_pio(sideset_init=PIO.OUT_HIGH, out_init=PIO.OUT_HIGH, out_shiftdir=PIO.SHIFT_RIGHT)
 @asm_pio(set_init=(PIO.OUT_LOW,) * 4, out_init=(PIO.OUT_LOW,) * 4, out_shiftdir=PIO.SHIFT_RIGHT, in_shiftdir=PIO.SHIFT_LEFT)

Input pins have to be set outside the PIO program

 pin17 = Pin(17, Pin.IN, Pin.PULL_UP)

Instructions

The PIO has a total of nine instructions: JMP, WAIT, IN, OUT, PUSH, PULL, MOV, IRQ, and SET. In MicroPython one can use them as followed.

jmp (condition, target)

The JMP instructions jumps to a specific point in the code.
conditions:
- not_x, not_y # jump if x,y is zero
- x_dec, y_dec # if x,y is not zero, decrease x,y by one and jump
- x_not_y # jumps if x is not y
- not_osre # jump if the output shift register is empty
- pin
target:
0-31 (5bit), or a label. Set a label with: label()

 label("loop_start")
 set(pins, 1)
 set(pins, o)
 jmp("loop_start")

PIN condition: to jumps by the setting of a specific pin one has to set the in_base pin for a StateMachine:

 sm = rp2.StateMachine(0, myProgram, in_base=pin1)

It has to be combined with the WAIT instruction to wait for an input pin getting low.

 wait(0, pin, 0)

wait(option)

wait(polarity, gpio, num)
It will waits until the specified GPIO has the specified polarity.
- polarity (0=LOW, 1=HIGH)
- num (absolute GPIO number)

 wait(0, gpio, 11)

wait(polarity, pin, num)
It does the same like before, but it uses the pin mapping.

 wait(1, pin, 0)

wait (polarity, irq, num(rel))
It will wait for an irg to be set.
- polarity (1 clears the interrupt flag, 0 won't)
- num (interrupt number, with rel the relative interrupt numbers could be used)

 wait(1, irq, rel(0))

in(source, bits)

The IN instructor shifts data from the source into the input shift register (ISR).
source:
- pins (from pins mapped as inputs)
- x, y (from scratch register)
- null (set destination to zero)
- status (from special register could e.g. indicate FIFO empty or full)
- osr (from output shift registers)
bits:
- 0-31 bits

out(destination, bits)

The OUT instructor shifts data from the output shift register (OSR) to the destination.
destination:
- pins (to pins mapped as outputs)
- pindirs (defines pin as in- or output, 0=input, 1=output)
- x, y (to scratch register x, y)
- null (set destination to zero)
- exec (for an instruction that will be executed next cycle)
- pc (shifts an instruction into the program counter)
- isr (into the input shift registers)
bits:
- 0-31 bits

push(option)

The PUSH instruction pushes data from the ISR into the RX FIFO and clears the ISR.
options:
- noblock (won't block if the FIFO is full, but will clear the ISR)
- block (will block if the FIFO is full, waits until it is enough space in the FIFO)
- iffull (will only push if the ISR is full)
- no option (like block)


pull(option)

The PULL instruction will take data from the TX FIFO and place it in the output shift register (OSR).
options:
- noblock (if there is no data in the FIFO it will ignore the command)
- block (waits until FIFO is filled)
- ifempty (pull only if OSR is empty)
In Python one can send data:

 sm.put(1234) #sends 1234 to the StateMachine's FIFO

In the StateMachine one can receive them:

 pull()      # gets data from the TX FIFO
 mov(x, osr) # copies them from the OSR to the scratch register

 

mov(destination, source)

The MOV instruction copies data from the “source” to the “destination”.
destination:
- pins (to pins mapped as outputs)
- x, y (into scratch register x, y)
- exec (for an instruction that will be executed next cycle)
- pc (shifts an instruction into the program counter)
- isr, osr (into input (ISR) and output shift registers (OSR))
source:
- pins (pins mapped as inputs)
- x, y (from scratch register x, y)
- null (set destination to zero)
- status (special register could indicate FIFO empty or full)
- isr, osr (from input (ISR) and output shift registers (OSR))
prefix: ~ or ! for inverting and :: for inverse copying

mov(y, osr) #copy the output shift register to the y scratch register
mov(osr, x) #copy the x scratch register to the output shift register 

irq(option, num(rel))

The IRQ instruction sets or clears an interrupt flag.
option
- set, nowait (default option, set flag without clear first)
- wait (wait until the flag is zero before setting it)
- clear (clears the flag)
num number of the interrupt 0-7
rel relative IRQ number
In the StateMachine

 irq(block, rel(0))

In Python:

 sm.irq(myFunction)

set (destination, data)

The SET instruction writes everything from “data” into “destination”.
destination:
- pins (pins mapped as outputs, 0=low, 1=high)
- pindirs (defines pin as in- or output pin, 0=input, 1=output)
- x, y (into scratch register x, y)
data: 0-31

 set(pins, 11)     #drive first four mapped pins: 1011 (=> dec 11)
 set(x, 31)        #store 31 in the x scratch register

Side-Set

A specific pin can be turn on (1) or off (0) in the same cycle as main command. So shifting data and toggling pin can be done in the same cycle. Up to five individual pins can be implemented.
Each Side-Set pin will reduce the delay by one.

 pull()     .side(0)
 mov(x,ors) .side(1)  [30]

Two pins:

 pull(ifempty)            .side(0x2)   [1]
 label("bitloop")
 out(pins, 1)             .side(0x0)   [1]
 in_(pins, 1)             .side(0x1)
 jmp(x_dec, "bitloop")    .side(0x1)

And in binary:

  wrap_target()
  out(pins, 9)            .side(0b10)
  out(null, 7)            .side(0b11)
  wrap()

delay

One can delay 1 to 31 cycles by putting the number in braces [] behind the command.

 set(pins, 1) [31] #drive first mapped pin high and delay 31 cycles

nop()

The NOP instruction stands for no operations. In combination with the delay function one can create delays between 1 to 31 cycles.

 nop () [31]   #delay 31 cycles

wrap()

The WRAP instruction resets the program counter and starts over again. It needs no cycle.

 wrap_target()
 set(pins, 1)
 set(pins, 0)
 wrap ()

pass

Pass can be used if the actual code is empty.

 def prog():
     pass

Examples

from rp2 import PIO, StateMachine, asm_pio
from machine import Pin
import utime

led_onboard = machine.Pin(25, machine.Pin.OUT)
led_onboard.value(1)
utime.sleep(2)
led_onboard.value(0)

@asm_pio(set_init=PIO.OUT_LOW)
def faint_led():
  set(pins, 0) [20]
  set(pins, 1)

sm1 = StateMachine(1, faint_led, freq=10000, set_base=Pin(25))

while(True):
  sm1.active(1)
  utime.sleep(1)
  sm1.active(0)
  utime.sleep(1)

The commands set(pins, 0) and set(pins, 1) turns the GPIO pin on and off.
In square brackets are numbers between 1 and 31 to pause this clock cycles
The @asm_pio descriptor above the function takes the set_init parameters.
To start and stop the state machine use the active method (1 or 0)


Knowledge