Introduction au PIO (Programmable Input Output) du RP2040

4.9
(18)

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.

Schéma d’un bloc PIO (crédit : fiche technique RP2040)

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.

Copie 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 :

Représentation du positionnement de l’état de la sortie en fonction de la valeur du bit x (0 ou 1) avec la représentation du code de la machine à état positionnant cette sortie

Capture des signaux à l’oscilloscope montrant une séquence d’un bit à 0 suivi d’un bit à 1 :

Séquence d’un bit à 0 (état haut pendant 0.4us, état bas pendant 0.8us) suivi d’un bit à 1 (état haut pendant 0.8us, état bas pendant 0.4us)

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.

Séquence complète pour une allumer une LED en vert avec une luminosité de 10%
Vu de cette séquence avec un analyseur logique (0x19 = 25 = b00011001)

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é.

Vu de la séquence dans laquelle la 6ème LED est de couleur rouge et les autres de couleur verte

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.

Votre avis compte !

Note moyenne : 4.9 / 5. Nombre de votes : 18

Pas encore de vote pour ce tutoriel

Désolé si cet article ne vous a pas intéressé

Merci de commenter afin que je puisse l’améliorer.

Dites-moi comment améliorer cette page.