CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
Étude de laboratoire n°2 (Entrées-sorties)

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 :

RegistreAdresseDonnéeDescription
  D7D6D5D4D3D2D1D0 
Digit 00x01b7b6b5b4b3b2b1b0Valeur des colonnes pour la ligne 0
Digit 10x02b7b6b5b4b3b2b1b0Valeur des colonnes pour la ligne 1
Digit 20x03b7b6b5b4b3b2b1b0Valeur des colonnes pour la ligne 2
Digit 30x04b7b6b5b4b3b2b1b0Valeur des colonnes pour la ligne 3
Digit 40x05b7b6b5b4b3b2b1b0Valeur des colonnes pour la ligne 4
Digit 50x06b7b6b5b4b3b2b1b0Valeur des colonnes pour la ligne 5
Digit 60x07b7b6b5b4b3b2b1b0Valeur des colonnes pour la ligne 6
Digit 70x08b7b6b5b4b3b2b1b0Valeur des colonnes pour la ligne 7
Decode0x09d7d6d5d4d3d2d1d0di indique si la ligne i doit être décodée. 0x00 = pas de décodage, 0xFF = décodage pour toutes les lignes
Intensity0x0AXXXXi3i2i1i0Intensité lumineuse en 16e de l'intensité max
Scan limit0x0BXXXXXn2n1n0Numéro du dernier chiffre à multiplexer (0 à 7)
Shutdown0x0CXXXXXXXaEn marche si a = 1, éteint sinon
Display test0x0FXXXXXXXtEn 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.


Afficheur à 7 segments et point décimal

Le MAX7219 est capable de piloter des afficheurs comportant jusqu'à 8 chiffres comportant 7 segments et un point décimal, comme indiqué ci-contre. Dans ce cas, on place généralement dans chaque registre de donnée la valeur numérique que l'on souhaite afficher, et le MAX7219 doit alors décoder cette valeur pour allumer les segments adéquats. Par exemple, si un registre contient la valeur 1, il faut allumer les segments B et C. S'il contient la valeur 0, il faut allumer les segments A, B, C, D, E et F. C'est ce qui est fait quand le bit du registre Decode correspondant à un chiffre est à 1. Lorsqu'il est à 0, chaque bit du registre Digit pilote un segment (ou un point de notre matrice).

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.

Pour mettre le MAX7219 en mode test, il faut mettre à 1 le bit t du registre Display test qui se trouve à l'adresse 0x0F. On fera donc :
serialWrite(0x0F, 1)

Pour mettre le MAX7219 en marche, il faut mettre à 1 le bit a du registre Shutdown qui se trouve à l'adresse 0x0C. On fera donc :
serialWrite(0x0C, 1)
Il faut également le sortir du mode test en mettant à 0 le bit t du registre Display test.

Pour allumer et éteindre les LEDs de la matrice, il faut écrire dans les registres Digit 0 à Digit 7, mais pour que l'affichage corresponde à ce que vous attendez, il est nécessaire :

  • de désactiver le décodage prévu pour les afficheurs 7 segments en écrivant 0 dans le registre Decode (adresse 0x09)
  • d'activer le multiplexage pour les 8 colonnes de LEDs de la matrice en écrivant 7 dans le registre Scan limit (adresse 0x0B).

À 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. Si num vaut 0, seul le premier chiffre/la première ligne est affiché, si numvaut 7, tous les chiffres/toutes les lignes sont affichés ;
matrixLine(num, value)
qui donne la valeur value au chiffre/à la ligne num.

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 ;

Chaque octet du tableau bitmap correspond à une ligne, et donc à l'ordonnée y des pixels. L'abscisse x correspond à la position d'un bit dans cet octet. Pour changer la valeur d'un bit individuel dans un octet, on utilise un masque, c'est-à-dire un octet dont le seul bit à 1 se trouve à l'emplacement du bit que l'on souhaite modifier. Par exemple, pour changer la valeur du 3e bit d'un octet en partant de la droite, on utilisera le masque 0B00000100 (le préfixe 0B indique un entier codé en binaire).

  • pour mettre ce bit à 1 dans un octet a, il suffit de faire le OU bit à bit (opérateur | en Python) de a et du masque : a | 0B00000100 donne un octet de même valeur que a mais avec le 3e bit à 1. On utilise le fait que le OU d'un bit avec 0 donne ce même bit, et que le OU de n'importe quelle valeur avec 1 donne 1 ;
  • pour mettre ce bit à 0 dans un octet a, il faut faire le ET bit à bit (opérateur & en Python) de cet octet et du complément du masque : a & 0b11111011 donne un octet de même valeur que a mais avec le 3e bit à 0. On utilise le fait que le ET d'un bit avec 1 donne ce même bit, et que le ET de n'importe quelle valeur avec 0 donne 0. Le complément s'obtient en Python avec l'opérateur ~ (tilde). On écrira donc : a & ~0B00000100

Pour obtenir le masque correspondant au i e bit d'un octet en partant de la droite, il suffit d'élever 2 à la puissance i, ce qui s'écrit en Python 2**i.

Dernier point : le pixel de coordonnées (x, y) correspond au xe bit de l'octet de la ligne y, mais en comptant les bits à partir de la gauche. Il faudra donc penser, pour calculer le masque, à convertir x en une position comptée en partant de la droite.

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.

Pour obtenir une valeur aléatoire, vous pouvez utiliser la fonction os.urandom(n) qui rend n octets générés aléatoirement par un dispositif matériel sur la pyboard. Pour tirer à pile ou face, il suffit de générer un seul octet aléatoire et de tester s'il est supérieur à 127 :

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

Pour attendre x millisecondes, utilisez pyb.delay(x)

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'adresse addr sur le bus i2c. Si data est un entier, c'est le nombre d'octets à lire, et la méthode rend un tableau d'octets contenant les octets lus. Si data 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'adresse addr sur le bus i2c. 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 :

RegistreAdresseDonnéeDescription
  D7D6D5D4D3D2D1D0 
XOUT0x00-Ab5b4b3b2b1b0Accélération en x sur 6 bits (-32..+31)
YOUT0x01-Ab5b4b3b2b1b0Accélération en y sur 6 bits (-32..+31)
ZOUT0x02-Ab5b4b3b2b1b0Accélération en z sur 6 bits (-32..+31)
TILT0x03SHATPP2P1P0B1B0Indicateur d'événements
INTSU0x06SHXSHYSHZGASPDPLFBMasque d'interruptions
MODE0x07IAHIPPSCPASEAWETONXMODModes de fonctionnement
SR0x08FI2FI1FI0AW1AW0AM2AM1AM0Taux d'échantillonnage de l'accélération
PDET0x09ZDAYDAXDAPD4PD3PD2PD1PD0Paramètres de détection des tapes
PD0x0AD7D6D5D4D3D2D1D0Filtrage 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 registre INTSU, 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 et XDA du registre PDET. 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.