Tout comme les autres microcontrôleur modernes, le RP2040 du Raspberry Pi Pico intègre plusieurs interfaces standards (UART, SPI, I2C…) lui permettant de communiquer facilement avec une grande variété de périphériques. Mais le RP2040 se distingue des autres microcontrôleur car il intègre des entrées/sorties programmables (Programmable Input/Output) permettant de créer vos propres interfaces ou de mettre en œuvre des interfaces spécifiques qui ne seraient pas gérées nativement par le RP2040. Un excellent exemple d’utilisation des PIO est l’interfaçage des bandes de LED NeoPixel comme nous allons le voir dans cet article.
Comment fonctionnent les PIO ?
Le RP2040 intègre 2 blocs PIO. Chaque bloc PIO est comparable à un petit processeur qui exécute le code indépendamment du CPU (Cortex-M0+). Les PIO permettent ainsi de gérer les entrées/sorties de manière déterministe, le timing étant très précis quelque soit la charge du CPU.
Chaque bloc PIO est composé de 4 machines à état (State Machine) qui peuvent exécuter indépendamment des petits programmes dont les instructions sont stockées dans une mémoire partagée (Instruction Memory). À chaque cycle d’horloge système, chaque machine à état récupère, décode et exécute une instruction. Chaque machine à état permet de manipuler les GPIO et transférer des données. Les programmes sont écrits avec un assembleur spécifique composé de 9 instructions : JMP , WAIT , IN , OUT , PUSH , PULL , MOV , IRQ , et SET.
Sur le RP2040, les 30 GPIO utilisateur (GP0-GP29) peuvent être utilisées en tant que PIO.
Exemple : génération d’un signal carré
Par soucis de simplicité notre premier programme sera écrit en MicroPython.
Nous souhaitons que le PIO génère un signal carré sur la sortie GP28.
La fréquence d’horloge du système est de 125 MHz par défaut sur le RP2040. Les machines à état fonctionnent à la fréquence le l’horloge du système par défaut.
Nous allons programmer une machine à état pour qu’elle positionne la sortie à son état haut pendant 32 cycles soit 256 ns (1 cycle = 1/125000000 Hz = 8 ns) et qu’ensuite elle positionne la sortie à son état bas pendant 32 cycles également. La fréquence du signal carré sera donc de 1/0.000000512 s = 1.953 MHz.
Détaillons le programme de la machine à état (voir la documentation de la librairie rp2 si besoin) :
wrap_target()
Spécifie l’emplacement du début de la boucle du programme.
set(pins, 1) [31]
Positionne la sortie à l’état haut (instruction exécutée en 1 cycle) et reste dans cet état pendant 31 cycles. Cette étape dure donc bien 32 cycles.
set(pins, 0) [31]
Positionne la sortie à l’état bas (instruction exécutée en 1 cycle) et reste dans cet état pendant 31 cycles. Cette étape dure donc également 32 cycles.
wrap()
Spécifie l’endroit où se situe la fin de la boucle du programme.
Voici le code écrit en MicroPython que nous allons transférer sur le Raspberry Pi Pico et qui va programmer la machine à état du PIO comme nous venons de le définir :
import time
import rp2
from machine import Pin
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def blink():
wrap_target()
set(pins, 1) [31]
set(pins, 0) [31]
wrap()
sm = rp2.StateMachine(0, blink, set_base=Pin(28))
sm.active(1)
Nous observons bien un signal carré de fréquence 1.953 MHz en sortie de la broche 34 (GP28) du Pico.
Modification de la fréquence du signal carré
Il est possible de modifier la fréquence du signal carré en jouant sur le nombre de cycle pendant lesquels la machine a état laisse la sortie à l’état haut et bas (le paramètre 31 par exemple dans cette ligne de code set(pins, 1) [31]). Mais il est également possible de modifier la fréquence de fonctionnement de la machine à état dans l’appel de la fonction rp2.StateMachine().
import time
import rp2
from machine import Pin
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW)
def blink():
wrap_target()
set(pins, 1)
set(pins, 0)
wrap()
sm = rp2.StateMachine(0, blink, freq=2500,set_base=Pin(28))
sm.active(1)
Dans l’exemple ci-dessus, l’horloge de la machine à état est configurée sur une fréquence de 2500 Hz. Le signal carré aura donc une fréquence de 1250 Hz, ce qui est bien confirmé par le relevé à l’oscilloscope.
Utilisation du PIO pour contrôler un bandeau de LED NeoPixel
Le contrôle de bandeau de LED NeoPixel est un excellent exemple de l’utilisation des PIO.
Le principe des LED NeoPixel (voir la fiche technique du composant WS2812B intégré à chaque LED NeoPixel) est d’envoyer un tableau de mots de 24 bits correspond aux couleurs de toutes les LEDs du bandeau. Chaque LED utilise le premier mot de 24 bits quelle reçoit sur sa broche DIN pour positionner sa propre couleur et transmet aux LED suivantes sur sa broche DO la suite des mots de 24 bits (son propre mot de 24 bits étant supprimé de cette liste). Une pause de 50ms (reset code) est nécessaire entre chaque envoi de tableau de mots de 24 bits.
Le codage des données à envoyer est décrit dans est le diagramme de la documentation technique du WS2812B :
Les contraintes temporelles doivent être respectées avec une précision de ±0.15us. La fréquence de l’horloge de la machine à état est réglée sur 5 MHz, ainsi la durée d’un cycle est de 0.2us. Nous allons donc approximer les durées des états hauts et bas à 0.4us et 0.8us car ils restent dans la tolérance de ±0.15us.
Programmation de la machine à état pour les LEDs NeoPixel
J’ai simplifié l’exemple WS2812 de la documentation technique du RP2040, car je le trouve inutilement compliqué.
Voici le programme de la machine à état PIO :
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
# 1 step is 0.2us (clock frequency must be set to 5MHz)
wrap_target()
# 1 step at 0 (to wait for data in low state, reset code)
out(x, 1)
# start of the cycle
# 2 step at 1
set(pins, 1) [1]
# 2 step at x
mov(pins, x) [1]
# 1 step at 0
set(pins, 0)
wrap()
La machine à état est démarrée en mode “auto-pull”, ce qui signifie qu’il n’est pas nécessaire d’exécuter la commande pull pour récupérer les données de la TX FIFO vers l’OSR.
out(x, 1)
La commande out(x,1) copie 1 bit de l’OSR vers le registre x. Pendant ce cycle (0.2us), la sortie est à l’état bas car nous avons configuré set_init=rp2.PIO.OUT_LOW et out_init=rp2.PIO.OUT_LOW. Le fait de positionner la sortie à l’état bas au début de notre programme est important car cela permet d’avoir la sortie à l’état bas en attendant le premier bit de données (reset code).
set(pins, 1) [1]
La commande set(pins, 1) [1] positionne la sortie à l’état haut pendant 2 cycles (l’instruction prend s’exécute en 1 cycle et nous avons ajouter un délai de 1 cycle avec la commande [1]. A ce stade nous ne savons pas si le bit de donnée est 0 ou 1. Mais que ce soit 0 ou 1, il faut positionner la sortie à l’état haut pendant 2 cycles.
mov(pins, x) [1]
La commande mov(pins, x) [1] positionne la sortie à l’état correspondant à la valeur de x pendant 2 cycles.
set(pins, 0)
La commande set(pins, 0) positionne ensuite la sortie à l’état bas pendant 1 cycle.
Explication en image
Voici donc une explication en image du codage d’un 0 et d’un 1 avec le programme de la machine à état :
Capture des signaux à l’oscilloscope montrant une séquence d’un bit à 0 suivi d’un bit à 1 :
Capture de la séquence de contrôle d’une LED la positionnant de couleur verte avec la luminosité à 10%. Les données envoyés sont 25 pour la composante green (luminosité de 10% -> 255*10/100 = 25). On observe bien la séquence binaire b00011001 correspondant à 25 sur le premier octet de données qui encode la composante green. Les deux octets suivants sont à 0 (b00000000), ils correspondent aux composantes red et blue.
Voici le programme complet en MicroPython qui fait clignoter un bandeau de 10 LED NeoPixel :
import array, time
import rp2
import machine
NUM_LEDS = 10
BLACK = (0, 0, 0)
RED = (255, 0, 0)
YELLOW = (255, 150, 0)
GREEN = (0, 255, 0)
CYAN = (0, 255, 255)
BLUE = (0, 0, 255)
PURPLE = (180, 0, 255)
WHITE = (255, 255, 255)
COLORS = (BLACK, RED, YELLOW, GREEN, CYAN, BLUE, PURPLE, WHITE)
BRIGHTNESS = 0.1
def setColor(color):
r = int(color[0]*BRIGHTNESS)
g = int(color[1]*BRIGHTNESS)
b = int(color[2]*BRIGHTNESS)
return (g<<16) + (r<<8) + b
@rp2.asm_pio(set_init=rp2.PIO.OUT_LOW, out_init=rp2.PIO.OUT_LOW, out_shiftdir=rp2.PIO.SHIFT_LEFT, autopull=True, pull_thresh=24)
def ws2812():
# 1 step = 0.2us (clock frequency must be set to 5MHz)
wrap_target()
# 1 step at 0 (to wait for data in low state, reset code)
out(x, 1)
# start of the cycle
# 2 step at 1
set(pins, 1) [1]
# 2 step at x
mov(pins, x) [1]
# 1 step at 0
set(pins, 0)
wrap()
sm = rp2.StateMachine(0, ws2812, freq=5_000_000, set_base=machine.Pin(12), out_base=machine.Pin(12))
sm.active(1)
ar = array.array("I", [0 for _ in range(NUM_LEDS)])
# Set all leds to green
for i in range(NUM_LEDS):
ar[i] = setColor(GREEN)
sm.put(ar, 8)
time.sleep_ms(50)
# Rotate one red led
cpt = 1
while True:
ar[cpt-1] = setColor(GREEN)
ar[cpt] = setColor(RED)
sm.put(ar, 8)
time.sleep_ms(50)
cpt = (cpt+1)%NUM_LEDS
Nous pouvons observer la sortie de la broche 12 avec un analyseur logique et confirmer que la programmation du PIO correspond bien au comportement souhaité.
Merci pour votre intérêt pour ce article. Si vous l’avez apprécié vous pouvez le noter en cliquant sur les étoiles ci-dessous ou en laissant un commentaire. Je vous signale également mon article sur le module XIAO RP2040 et le contrôle par PIO de sa LED RGB en utilisant le langage Rust.
Il est parfaitement possible d’envoyer des données par paquets de 32bits sur les sorties du Pico en C++. Si vous voulez de la vitesse, avec une horloge précise, le mode DMA doit permettre de faire ce que vous voulez avec votre Bus 8 bits en permettant de faire autre chose pendant ce temps (ou alors c’est de l’assembleur au lieu du C++ mais vous ne pourrez rien faire sur votre coeur pendant ce temps là).
Je m’en sers pour de l’acquisition de données, je pense que cela doit aussi pouvoir se faire pour envoyer un bloc de mémoire avec une adresse fixe (un port donc).
Bonjour, est ce que les PIO peuvent s’attacher à toutes les GPIO ?
Merci
Sur le RP2040, les 30 GPIO utilisateur (GP0-GP29) peuvent être utilisées.
Bonjour,
Très intéressé par votre sujet sur le PIO du RP2040.
je suis à la recherche d’une solution pour configurer 8 pins d’un PI PICO en bus 8 bits rapide.
toutes les options que j’ai trouvé pour le moment passent par des solutions logicielles très lentes ( listes, chaines, conversions)
une programmation du PIO pourrait elle solutionner cette recherche.
Merci pour votre retour et votre aide éventuelle.
Bien cordialement
Bonjour, je pense que c’est tout à fait adapté en effet !
Donnez-moi plus de détails que je puisse regarder ce qu’il est possible de faire.
Bonjour,
je reviens vers vous tardivement. merci pour votre réponse.
En fait je suis beaucoup plus habitué à la programmation des PIC de microchip en assembleur
ou il est très aisé de sortir un tableau d’octets sur un port du contrôleur avec une instruction
movwf PORTB très rapide, qui sort une valeur 8 bits du registre de travail W sur un port en une seule instruction et un cycle d’horloge.
A 40 MHz on obtient une sortie rapide des octets vers un convertisseur Numérique Analogique parallèle par exemple.
L’application a pour but de lire des tableaux d’octets stockés sur une carte SD et les appliquer sur un port parallèle du PI PICO sans passer par une manipulation logicielle très lente, comme celles utilisées pour commander un afficheur lcd 16×2 sans passer par I2C.
Je découvre le monde du RASPBERRY PI en particulier le PI, qui me semble attrayant en confort de programmation mais très lent.
Je ne me suis pas encore posé la question d’une possible de programmation du pico en assembleur et l’utilisation du PIO me semblait une piste pour pouvoir écrire une valeur 8 ou 16 bits sur un groupe de GPIO organisés en bus, à l’aide une seule instruction C+ ou PYTHON.
mais est ce possible?
merci de votre attention.
Bien cordialement
Les PIO ne semblent pas optimales pour cet usage…
Bonjour merci de votre réponse,
Je le pense également, je n’ai rien trouvé dans la littérature qui évoque ce point.
Ce qui est d’ailleurs étonnant puisse que l’ensemble des microcontrôleurs sont architecturés en système de bus. Cela doit être dû au fait que ce PI PICO vient du monde Raspberry un peu particulier je l’avoue.
Merci pour votre aide.
Bien cordialement.