====== Paspberry Pi Pico - Programmable I/O ====== This manual is for programming [[https://www.raspberrypi.org/documentation/rp2040/getting-started/|Raspberry Pi Pico]]'s [[https://en.wikipedia.org/wiki/Programmed_input%E2%80%93output|programmable in- and output pins]] (PIO) in [[https://micropython.org/|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: [[raspberrypipico:raspberrypipico|Paspberry Pi Pico]].\\ In chapter 3 of the [[https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf|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 [[https://datasheets.raspberrypi.org/pico/raspberry-pi-pico-python-sdk.pdf|Raspberry Pi Pico Python SDK]] and the [[https://datasheets.raspberrypi.org/pico/raspberry-pi-pico-c-sdk.pdf|Raspberry Pi Pico C/C++ SDK]].\\ The SDK and APIs ducumentation:\\ [[https://github.com/raspberrypi/pico-sdk|Pico SDK]]\\ [[https://raspberrypi.github.io/pico-sdk-doxygen/group__hardware__pio.html|Programmable I/O (PIO) API]]\\ [[https://raspberrypi.github.io/pico-sdk-doxygen/group__sm__config.html|PIO state machine configuration]]\\ ---- ===== Structure ===== The RP2040 contains two **programmable IO blocks** with four **state machines** each, to control GPIOs and to transfer data.\\ {{:raspberrypipico:rp2040_diagram_for_a_single_pio_block.jpg?400|}}\\ //diagram taken from the [[https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf|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**\\ {{:raspberrypipico:rp2040_state_machine_overview.jpg?400|}}\\ //diagram taken from the [[https://datasheets.raspberrypi.org/rp2040/rp2040-datasheet.pdf|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 ===== [[https://www.youtube.com/watch?v=yYnQYF_Xa8g|In-depth: Raspberry Pi Pico's PIO - programmable I/O!]] by stacksmashing\\ [[https://github.com/raspberrypi/pico-micropython-examples/tree/master/pio|Pico Micropython Examples]] by Raspberry Pi\\ Stepper motor example on [[https://www.youtube.com/watch?v=UJ4JjeCLuaI| Youtube by Tinker Tech Trove]] and the code on [[https://github.com/tinkertechtrove/pico-pi-playing/tree/main/pio-steppers|Github]]\\ [[https://www.seeedstudio.com/blog/2021/01/25/programmable-io-with-raspberry-pi-pico/|Seeedstudio: Programmable I/O with Raspberry Pi Pico by Jonathan Tan _seeedstudio.com]]\\ [[https://medium.com/geekculture/serial-connection-between-raspberry-pi-and-raspberry-pico-d6c0ba97c7dc|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: [[http://creativecommons.org/licenses/by-nc-sa/4.0/"|Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License]].\\ Creative Commons License ----