Mit einer PIO State-Machine kannst du Peripheriegeräte am Raspberry Pi Pico direkt programmieren, also ohne die CPU einzubinden. Die Grundlagen dazu will ich dir in diesem Post zeigen.
PIO State-Machine
PIO steht für Programmable I/O. Mit PIO wird die I/O direkt in sog. Zustandsautomaten (State-Machines) programmiert.
Zustandsautomaten sind ein Programmierparadigma aus der Informatik und stammen ursprünglich aus der Automatentheorie. Sie begegnen uns überall im Leben. Beispielsweise bei der Steuerung eines Toasters.
Assembler der PIO State-Machine
Die PIO Programmierung ist sehr hardwarenah, deshalb setzt man dazu Assembler ein. Der Befehlssatz von Assembler ist immer vom verwendeten Prozessor abhängig. Du kannst ein Assemblerprogramm, dass für den 8088 geschrieben wurde nicht einfach so auf den ARM-Prozessor des Raspberry Pi Laufen lassen.
Befehlssatz
Jeder Prozessor hat seinen eigenen Assembler-Befehlssatz. Das gilt auch für unsere PIO State-Machine. Ich habe ihn hier mal aufgelistet.
Befehl | Funktion |
---|---|
NOP | Nop Operation – Tut nichts, außer Zeit zu verbrauchen. |
OUT | Überträgt Daten von einem internen Register (OSR) an einen PIO Pin |
IN | Liest Daten von einem Eingangspin oder einem Statusregister und speichert sie im ISR |
PUSH | Verschiebt Daten vom ISR (in den FIFO der CPU. |
PULL | Holt Daten aus dem CPU-FIFO in das OSR |
MOV | Verschiebt Daten zwischen Registern (z.B. zwischen X, Y, ISR, OSR) |
IRQ | Setzt oder löscht ein IRQ-Signal (Interrupt Request) |
WAIT | Wartet auf eine bestimmte Bedingung (z.B. auf einen bestimmten Pegel eines Eingabepins) |
JMP | Springt zu einer bestimmten Adresse innerhalb des Programms, mit optionalen Bedingungen. |
SET | Setzt bestimmte Pins oder Statusregister auf einen gewünschten Wert. |
EXEC | Führt eine spezifizierte Anweisung sofort aus, ohne das Programmzählerregister zu ändern. |
JMP_REL | Springt relativ zur aktuellen Position im Programm. |
Register
Register dienen dazu, Daten zwischenzuspeichern, sie entsprechen den Variablen in höheren Programmiersprachen. In der Regel werden dort nur Integerwerte abgelegt. Einige Register sind PIO intern und können nur abgefragt werden. Andere brauchst du,um Zwischenwerte zu speichern.
Register | Bedeutung |
---|---|
ISR | Input Shift Register. Hier kommen Daten von den Eingabepins an. Mit PUSH können sie dann in den FIFO geschoben werden. |
OSR | Output Shift Register. Moit OUT kannst hierüber Daten an die PIO-Eingabepins ausgeben. |
X und Y | Arbeitsregister für Werte des Assemblerprogramms. |
PC | Programm-Counter. Hier hällt der Prozessor fest, welcher Befehl als nächstes ausgeführt wird. Er wird automatisch hochgezählt und kann durch den JMP Befehl verändert werden. |
FIFO | First In -First Out Register um Daten zwischen Hauptprozessor und PIO auszutauschen. |
Statusregister | Diese Register dokumentieren den aktuellen Zustand der PIO und können, z.B. mit SET manipuliert werden. |
Divider | bestimmt die Taktfrequenz der PIO |
Praxis
Nach so viel Theorie will ich dir noch den praktischen Einsatz zeigen. Dazu nehme ich einen Raspberry Pi Pico der mit Micropython geflasht ist. Das Pythonprogramm zum Blinken der onboard LED ist nichts besonderes mehr:
from machine import Pin
from utime import sleep
led = machine.Pin(25, machine.Pin.OUT)
state = False
running = True
#Endlosschleife
led.off()
while True:
state = not state # state hin- und herschalten
sleep(1.0)
if state:
print('LED on')
led.on()
# running = False
else:
print('LED off')
led.off()
In dieser Variante schaltet der Pythoncode selber die LED ein- und aus. Falls du noch etwas anderes berechnen möchtest, musst du diesen Mechanismus irgendwie an geeigneten Stellen in deinem Programmcode unterbringen oder einen zweiten Thread aufmachen.
Eleganter geht es aber, die PIO State-Machine die Arbeit machen zu lassen.
from machine import Pin
import rp2
from time import sleep
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def blink():
wrap_target()
set(pins, 1) [31]
nop() [31]
nop() [31]
nop() [31]
set(pins, 0) [31]
nop() [31]
nop() [31]
nop() [31]
wrap()
led = machine.Pin(25, machine.Pin.OUT)
sm = rp2.StateMachine(0, blink, freq=2000, set_base=led)
# StateMachine aktivieren
sm.active(1)
# Für 3 Sekunden laufen lassen.
sleep(3)
# deaktivieren
sm.active(0)
Dieses Pythonprogramm dient dazu, den Assemblercode der Methode blink()
in die PIO State-Machine zu laden. Dazu wird mit dem Decorator @rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
bekannt gemacht, dass die nachfolgende Methode Assemblercode für die PIO enthält. Der Parameter set_init=rp2.PIO.OUT_LOW sorgt dafür, dass vor dem Start der State-Machine alle Pins auf LOW gezogen werden.
Die Methode blink()
ist für ein Pythonprogramm ungewöhnlich, da sie keinen Pythoncode enthält. In der Methode wird mir wrap_target()
eine Schleife definiert. In dieser wird mit set(pins, 1) [31]
zunächst die LED eingeschaltet. Die [31] sorgt dafür, das nach dem Befehl für 31 Taktzyklen pausiert werden soll. Mittels nop()
werden dann noch weitere Wartezyklen eingefügt. Dann schaltet set(pins, 0) [31]
die LED wieder aus, gefolgt von weiteren nop() zum Warten. wrap()
definiert dann das Ende der Schleife.
Nachdem die Methode für die State-Machine definiert ist, müssen wir diese initialisieren sm = rp2.StateMachine(0, blink, freq=2000, set_base=led)
und starten sm.active(1)
Fazit
PIO ist eine interessante Möglichkeit, die I/O Hardware des Raspberry Pi Pico direkt anzusprechen. Gerade für der Treiber für Sensoren könnte damit ein höherer Datendurchsatz erreicht werden als mit einem Treiber, der in Python kodiert ist.