Table des matières
L'objectif de cette étude est de piloter un périphérique simple, de comprendre le rôle du contrôleur de périphérique, et de faire communiquer le processeur avec ce contrôleur par l'intermédiaire d'un bus série pour obtenir le comportement désiré.
Compétences visées:
- Comprendre le principe du multiplexage matriciel.
- Analyser la documentation technique d'un composant.
- Utiliser un protocole de communication série.
- Définir une interface logicielle pour un composant matériel.
- Concevoir une application qui interagit avec du matériel.
Le matériel
Le périphérique est une matrice de 8x8 LED rouges contrôlée par un MAX7219. La programmation se fera en Python sur la pyboard.
Matrice de LED
La matrice de LED que nous utilisons comporte 64 LED rouges disposées en 8 lignes de 8 LED.
Pour contrôler individuellement ces 64 LED, il faudrait a priori 64 fils et une masse commune, ce qui ne serait pas très pratique. En observant le composant, on constate qu'il ne possède que 16 broches, dont les 8 premières apparaissent sur la photo ci-dessous :
Les LED sont en effet organisées en 8 lignes qui comportent 8 LED dont les cathodes sont reliées. On peut donc allumer n'importe quelle LED en mettant à la masse le fil des cathodes de la ligne où elle se trouve, et en alimentant le fil de sa colonne, comme indiqué sur le schéma ci-dessous :
Mais cette organisation ne permet pas d'allumer indépendamment les différentes LED. En effet, dès qu'une ligne est à la masse, toutes les LED de cette ligne dont le fil de colonne est alimenté sont allumées. Il est par exemple impossible d'allumer uniquement les LED de la diagonale, car pour cela il faudrait mettre toutes les lignes à la masse (il y a une LED de la diagonale sur chacune des lignes) et alimenter de même toutes les colonnes. Toutes les LED seraient alors allumées.
Multiplexage et MAX7219
La solution à ce problème est de ne mettre à la masse qu'une seule ligne à la fois, et à alimenter les fils de colonne en fonction des LED que l'on souhaite allumer sur cette ligne. En traitant successivement les 8 lignes de cette façon, on allume successivement les LED que l'on souhaite sur chaque ligne, et si le changement de ligne se fait suffisamment rapidement, la persistance rétinienne donne l'illusion que toutes les LED souhaitées sont allumées en permanence. Il s'agit d'une forme de multiplexage temporel : on envoie successivement différentes valeurs sur les fils des colonnes, de manière synchronisée avec la mise à la masse des différents fils de ligne.
Ce multiplexage est assuré par un composant, le MAX7219, qui est le petit rectangle noir que vous pouvez voir entre le connecteur et la matrice de LED. Ce composant va jouer le rôle de contrôleur de périphérique, en assurant d'une part le multiplexage de la matrice de LED (à une fréquence proche de 800 Hz), et d'autre part en offrant une interface série avec le processeur, ce qui va nous permettre de piloter l'allumage des LED avec seulement 3 fils (plus la masse et l'alimentation).
Description du MAX7219
Pour assurer le multiplexage des différentes lignes de LED, le MAX7219 doit mémoriser la valeur à envoyer sur chacun des 8 fils de colonne pour chacune des 8 lignes. Il dispose pour cela de 8 registres de 8 bits. Il dispose également de différents registres qui permettent de contrôler son fonctionnement : décodage des valeurs (pour afficheur 7 segments), intensité lumineuse, nombre de lignes (ou de chiffres à 7 segments), mode de fonctionnement et test.
Chacun des registres du MAX7219 se situe à une adresse qu'il faudra indiquer lorsque l'on souhaite y écrire une valeur. La table ci-dessous donne la liste des registres du MAX7219 avec leur adresse et le format des données qui s'y trouvent :
Registre | Adresse | Donnée | Description | |||||||
---|---|---|---|---|---|---|---|---|---|---|
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | |||
Digit 0 | 0x01 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | Valeur des colonnes pour la ligne 0 |
Digit 1 | 0x02 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | Valeur des colonnes pour la ligne 1 |
Digit 2 | 0x03 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | Valeur des colonnes pour la ligne 2 |
Digit 3 | 0x04 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | Valeur des colonnes pour la ligne 3 |
Digit 4 | 0x05 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | Valeur des colonnes pour la ligne 4 |
Digit 5 | 0x06 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | Valeur des colonnes pour la ligne 5 |
Digit 6 | 0x07 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | Valeur des colonnes pour la ligne 6 |
Digit 7 | 0x08 | b7 | b6 | b5 | b4 | b3 | b2 | b1 | b0 | Valeur des colonnes pour la ligne 7 |
Decode | 0x09 | d7 | d6 | d5 | d4 | d3 | d2 | d1 | d0 | di indique si la ligne i doit être décodée. 0x00 = pas de décodage, 0xFF = décodage pour toutes les lignes |
Intensity | 0x0A | X | X | X | X | i3 | i2 | i1 | i0 | Intensité lumineuse en 16e de l'intensité max |
Scan limit | 0x0B | X | X | X | X | X | n2 | n1 | n0 | Numéro du dernier chiffre à multiplexer (0 à 7) |
Shutdown | 0x0C | X | X | X | X | X | X | X | a | En marche si a = 1, éteint sinon |
Display test | 0x0F | X | X | X | X | X | X | X | t | En test si t = 1, en mode normal sinon |
Pour cette étude de laboratoire, vous mettrez le registre Decode (0x09) à 0.
Le registre Scan Limit permet de limiter le multiplexage à un nombre réduit de lignes. Par exemple, si un afficheur ne comporte que 4 chiffres, il faut limiter le multiplexage aux 4 premières lignes. Pour notre matrice de LED, le multiplexage sera configuré pour balayer les 8 lignes.
Communication série avec le MAX7219
La communication avec le MAX7219 se fait par une liaison série, c'est-à-dire que les données sont envoyées sur un seul fil, bit par bit. Il s'agit ici d'une liaison série synchrone, l'envoi de chaque bit étant indiqué par un front montant du signal d'horloge. Ainsi, le chronogramme suivant correspond à la transmission d'un octet (8 bits) de valeur 145 (128+16+1), DIN étant le signal de donnée et CLK le signal d'horloge :
Afin d'écrire dans un registre du MAX7219, il faut lui transmettre 2 octets, le premier étant l'adresse du registre dans lequel on veut écrire, le second étant la donnée à charger dans ce registre. Pour que le MAX7219 prenne en compte les deux octets et charge la donnée dans le registre, il faut présenter un front montant sur le signal CS (Chip Select). Le chronogramme suivant indique comment mettre le MAX7219 en mode test en écrivant la valeur 1 dans le registre Display test qui se trouve à l'adresse 0x0F :
L'écriture est déclenchée par le front montant du signal CS, l'adresse du registre et la donnée à écrire ayant été transmises sur les 16 fronts montants du signal CLK.
Code pour la communication série
Le code Python suivant permet d'écrire une donnée dans un registre du MAX7219 selon ce protocole série :
Pour ceux qui souhaitent connaître tous les détails, il est possible de consulter la documentation technique du constructeur (cliquez sur datasheet sur cette page).
Travail à effectuer
Connectez la pyboard à la matrice de LED comme indiqué sur la figure ci-dessous :
Le signal CLK de la matrice est connecté à la patte X1, le signal CS à la patte X2 et le signal DIN à la patte X3. L'alimentation (VCC) et la masse (GND) sont connectées aux pattes VIN et GND de la pyboard.
Expérimentation manuelle
Copiez le code fourni et collez-le dans un fichier EL2.py. Branchez la pyboard à l'ordinateur à l'aide du câble USB, et dans un terminal, placez vous dans le dossier contenant ce fichier, et tapez la commande pyboard EL2.py
.
Votre code est maintenant chargé dans la pyboard.
Connectez-vous à l'interpréteur Python de la pyboard grâce à la commande pyterm
.
- Utilisez la fonction
serialWrite
pour mettre le MAX7219 en mode test (toutes les LED s'allument à l'intensité maximale) s'il n'y est pas déjà. - Sortez le MAX7219 du mode test et mettez le en mode de fonctionnement normal.
- Écrivez dans ses registres de données pour allumer et éteindre les LED.
- Changez l'intensité lumineuse.
À la fin, quittez pyterm en tapant Ctrl-A X
(appuyez sur la touche A en maintenant la touche Ctrl enfoncée, puis appuyez sur la touche X), puis en tapant return pour fermer la demande de confirmation.
Codage de fonctions utilitaires
Dans le fichier EL2.py, codez les fonctions suivantes :
matrixOn(on)
- qui met le MAX7219 en marche si
on
est True et l'arrête sinon ; matrixTest(test)
- qui met le MAX7219 en mode test si
test
est True et en mode normal sinon ; matrixIntensity(percent)
- qui règle l'intensité lumineuse approximativement à
percent
pourcents du maximum ; matrixDecode(decode)
- qui met le MAX7219 en mode décodage si
decode
est True, et en mode sans décodage sinon ; matrixDigits(num)
- qui indique au MAX7219 de traiter les chiffres (ou lignes) jusqu'à
num
. Sinum
vaut 0, seul le premier chiffre/la première ligne est affiché, sinum
vaut 7, tous les chiffres/toutes les lignes sont affichés ; matrixLine(num, value)
- qui donne la valeur
value
au chiffre/à la lignenum
.
Chargez de nouveau le fichier EL2.py dans la pyboard avec la commande pyboard EL2.py
, puis connectez-vous à l'interpréteur avec la commande pyterm
et testez vos fonctions.
Vous venez d'écrire ce qui correspond à un pilote de périphérique pour cette matrice de LED.
Contrôle des pixels
On souhaite maintenant pouvoir allumer ou éteindre chaque pixel (LED) individuellement. Lorsque l'on change l'état d'un pixel, on ne doit pas modifier l'état des autres, et comme on ne peut pas lire le contenu des registres du MAX7219 pour connaître cet état, il est nécessaire de stocker cet état dans une variable de notre programme. Nous utiliserons pour cela un tableau de 8 octets, chaque octet contenant la valeur du registre correspondant à une ligne. Un tel tableau est généralement appelé une bitmap car sa structure donne une cartographie (map) des pixels à allumer ou éteindre. On crée ce type de tableau de la manière suivante :
Écrivez les fonctions suivantes pour manipuler :
updateDisplay(bitmap)
- qui met à jour l'affichage en chargeant les registres du MAX7219 avec les données du tableau
bitmap
; clearDisplay(bitmap)
- qui éteint tous les pixels de
bitmap
et met à jour l'affichage ; setPixel(x, y, on, bitmap)
- qui allume le pixel de coordonnées (x, y) de
bitmap
si on est True et l'éteint sinon (sans mettre à jour l'affichage) ; getPixel(x, y, bitmap)
- qui rend True si le pixel de coordonnées (x, y) est allumé dans
bitmap
et False sinon ;
Vous pouvez tester votre code à l'aide des fonctions suivantes :
Affichage d'images
Il est possible de représenter une image de 8x8 pixels par une liste de 8 chaînes de caractères comportant chacune 8 caractères. On considèrera qu'un espace correspond à un pixel éteint, et que tout autre caractère correspond à un pixel allumé. Voici deux exemples d'images codées selon ce principe :
Écrivez une fonction displayPict(pict)
qui affiche l'image pict
supposée être au bon format. La fonction retournera la bitmap créée pour afficher l'image. Testez votre fonction avec les deux images fournies en exemple.
Jeu de la vie
Le jeu de la vie définit les règles qui régissent la naissance, la survie et la mort de cellules placées sur une grille à deux dimensions :
- une cellule entourée de 3 cellules vivantes est vivante au tour suivant ;
- une cellule entourée de 2 cellules vivantes ne change pas d'état ;
- dans les autres cas, la cellule est morte au tour suivant.
On souhaite programmer le jeu de la vie pour notre afficheur en considérant qu'un pixel allumé représente une cellule vivante et qu'un pixel éteint représente une cellule morte. On considèrera que les bords gauche et droit de l'afficheur sont adjacents, de même pour les bords haut et bas. Ceci revient à travailler avec des coordonnées modulo 8 (la coordonnée qui suit 7 est 8, qui donne 0 modulo 8 et nous ramène donc de l'autre côté de l'afficheur, de même pour la coordonnée qui précède 0, qui est -1, ce qui, modulo 8, donne 7). L'opérateur Python pour le modulo est % (pourcent).
Ecrire les fonctions suivantes :
randomBitmap()
- qui rend une bitmap avec des pixels aléatoirement allumés ou éteints ;
countNeighbours(x, y, bitmap)
- qui rend le nombre de pixels allumés parmi les voisins du pixel de coordonnées (x, y) dans
bitmap
. Ne pas oublier que les bords opposés de la bitmap sont virtuellement adjacents.
Écrire une fonction lifeStep(bitmap)
qui calcule le nouvel état des cellules en appliquant les règles du jeu de la vie à bitmap
, puis qui met à jour bitmap
avec ce nouvel état et rafraîchit l'affichage.
Vous aurez pour cela besoin de travailler avec une deuxième bitmap afin de ne pas modifier l'état courant du jeu de la vie pendant que vous calculez l'état suivant des cellules.
Écrivez enfin une fonction gameOfLife()
, qui initialise le jeu à l'aide de la fonction randomBitmap()
, puis calcule l'évolution des cellules selon les règles du jeu de la vie, en attendant 200ms entre chaque étape.
Le code suivant donne quelques tests pour vérifier que votre programme respecte bien le jeu de la vie en lui faisant calculer l'évolution de structures particulières : certaines sont stables, d'autres oscillent, et enfin, certaines se déplacent sur la grille. Chacun de ces tests est conçu de façon à ce que l'affichage soit le même à la fin du test qu'au début :
Pour aller plus loin...
Cette dernière partie n'est à traiter que par les élèves qui auraient terminé toutes les questions précédentes largement avant la fin de la séance. Il est d'une difficulté largement supérieure à ce qui est attendu des élèves à l'issue de ce cours.
La pyboard est équipée d'un accéléromètre 3 axes, ce qui permet par exemple de connaître l'orientation de la carte par rapport à la verticale en mesurant l'accélération de la pesanteur. Cet accéléromètre est connecté au processeur par un bus I2C.
Le but de cet exercice est d'utiliser la capacité de cet accéléromètre à détecter les tapes sur la pyboard pour faire progresser un point lumineux sur la matrice de LED à chaque tape. Vous obtiendrez ainsi une sorte de podomètre primitif, chaque tape correspondant à un pas.
La bibliothèque MicroPython permet d'accéder aux deux bus I2C de la pyboard :
pyb.I2C(num, mode)
- rend un objet permettant de communiquer sur le bus I2C n°
num
en tant que maître du bus (mode = pyb.I2C.MASTER
) ou en tant qu'esclave (mode = pyb.I2C.SLAVE
) ; i2c.scan()
- rend la liste des périphérique connectés au bus I2C
i2c
; i2c.mem_read(data, addr, mem_addr)
- effectue une lecture à l'adresse
mem_addr
de la mémoire du périphérique I2C d'adresseaddr
sur le busi2c
. Sidata
est un entier, c'est le nombre d'octets à lire, et la méthode rend un tableau d'octets contenant les octets lus. Sidata
est un tableau d'octets (bytearray
), sa taille détermine le nombre d'octets à lire et à placer dans ce tableau ; i2c.mem_write(data, addr, mem_addr)
- effectue une écriture à l'adresse
mem_addr
dans la mémoire du périphérique I2C d'adresseaddr
sur le busi2c
.data
est la donnée à écrire (entier ou tableau d'octets).
Pour limiter la consommation de la carte, l'accéléromètre de la pyboard n'est pas alimenté en permanence. Pour l'alimenter, il faut mettre à l'état haut la patte MMA_AVDD
.
Sachant que l'accéléromètre se trouve sur le bus I2C n°1, et qu'il ne répond que lorsqu'il est alimenté, déterminez son adresse sur le bus I2C.
Revoyez le BE n°3 pour la gestion des pattes et des interruptions avec MicroPython.
L'accéléromètre dispose d'un certain nombre de registres qui se trouvent aux adresses 0x00 à 0x0A. En voici une description très sommaire et partielle, mais qui suffira pour cette étude :
Registre | Adresse | Donnée | Description | |||||||
---|---|---|---|---|---|---|---|---|---|---|
D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | |||
XOUT | 0x00 | - | A | b5 | b4 | b3 | b2 | b1 | b0 | Accélération en x sur 6 bits (-32..+31) |
YOUT | 0x01 | - | A | b5 | b4 | b3 | b2 | b1 | b0 | Accélération en y sur 6 bits (-32..+31) |
ZOUT | 0x02 | - | A | b5 | b4 | b3 | b2 | b1 | b0 | Accélération en z sur 6 bits (-32..+31) |
TILT | 0x03 | SH | A | TP | P2 | P1 | P0 | B1 | B0 | Indicateur d'événements |
INTSU | 0x06 | SHX | SHY | SHZ | G | AS | PD | PL | FB | Masque d'interruptions |
MODE | 0x07 | IAH | IPP | SCP | ASE | AWE | TON | X | MOD | Modes de fonctionnement |
SR | 0x08 | FI2 | FI1 | FI0 | AW1 | AW0 | AM2 | AM1 | AM0 | Taux d'échantillonnage de l'accélération |
PDET | 0x09 | ZDA | YDA | XDA | PD4 | PD3 | PD2 | PD1 | PD0 | Paramètres de détection des tapes |
PD | 0x0A | D7 | D6 | D5 | D4 | D3 | D2 | D1 | D0 | Filtrage de la détection des tapes |
Pour plus de détails, vous pouvez également consulter la documentation technique complète de l'accéléromètre.
L'écriture dans les registres n'est prise en compte que lorsque l'accéléromètre est en mode standby, c'est-à-dire quand le bit MOD
du registre MODE
est à 0. Pour activer la détection des tapes dans les 3 directions, il faut :
- activer la génération d'une interruption lors de la détection d'une tape en mettant à 1 le bit
PD
du registreINTSU
, les autres bits seront laissés à 0 ; - configurer l'échantillonnage des mesures à 120Hz en écrivant 0 dans le registre
SR
; - mettre à 0 les 3 bits
ZDA
,YDA
etXDA
du registrePDET
. Les autres bits de ce registre déterminent la sensibilité aux tapes, et vous pouvez commencer avec la valeur 0x0F, que vous diminuerez si la sensibilité est trop grande ; - mettre dans le registre
PD
le nombre de tests positifs successifs nécessaires à la détection d'une tape. Vous pouvez démarrer avec la valeur 0x10.
La patte sur laquelle l'accéléromètre donne le signal d'interruption est la patte MMA_INT
. Une demande d'interruption correspond à un niveau bas et la sortie est en collecteur ouvert dans cette configuration de l'accéléromètre. Vous devrez donc configurer la ligne d'interruption pour détecter les fronts descendants, et la configurer en mode pyb.Pin.PULL_UP
.
Écrivez une fonction qui initialise l'accéléromètre comme indiqué ci-dessus, installe une routine de traitement de ses interruptions (commencez par un traitement qui ne fait qu'afficher un message dans la console, à l'aide de la fonction print
) et qui met enfin l'accéléromètre en marche en mettant le bit MOD
à 1 dans le registre MODE
.
Attention : il est indispensable de lire le registre TILT
dans la routine de traitement des interruptions afin d'indiquer que la demande d'interruption a été traitée. L'accéléromètre ne générera pas d'interruption tant que la précédente n'a pas été traitée. De plus, il n'est pas possible d'allouer de la mémoire dans une routine de traitement d'interruption, vous devrez donc déclarer une variable globale (un bytearray de taille 1) qui sera utilisée pour recevoir la valeur du registre TILT
dans la routine de traitement des interruptions.
Pour réaliser le podomètre, on utilisera un compteur qui sera incrémenté pour faire progresser une LED vers la droite, puis vers le bas sur la matrice de LED.
Complétez le code de la routine de traitement des interruptions pour faire progresser la LED allumée à chaque tape sur la pyboard.