Table of Contents
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.
diagram taken from the RP2040 Datasheet
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
diagram 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 one, 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
In-depth: Raspberry Pi Pico's PIO - programmable I/O! by stacksmashing
Pico Micropython Examples by Raspberry Pi
Stepper motor example on Youtube by Tinker Tech Trove and the code on Github
Seeedstudio: Programmable I/O with Raspberry Pi Pico by Jonathan Tan _seeedstudio.com
Serial Connection between Raspberry Pi and Raspberry Pico bySebastian _medium.com
License
This manuals is made by Wolfgang Spahn 2021.
Except where otherwise noted, content on this wiki is licensed under the following license: Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.