TP03 - 7-Segments et Codeur Rotatif
Objectifs du TP
Ce TP a pour but de vous familiariser avec les afficheurs 7 segments, les codeurs rotatifs et la programmation orientée objet en C++.
À la fin de ce TP, les étudiants :
- sauront utiliser un afficheur 7 segments pour représenter des nombres entiers;
- sauront utiliser un codeur rotatif;
- auront mis en œuvre le concept de programmation orientée objet et auront implémenté plusieurs classes en C++;
- sauront retrouver des informations dans les datasheets de la cible;
- réutiliseront les concepts appris dans les TPs précédents;
- réutiliseront du code précédent pour faire des applications plus complexes;
- auront réalisé un nouveau mini-projet sur la cible;
- auront appliqué les bonnes pratiques en utilisant des assertions, en écrivant des tests unitaires et en validant systématiquement leur code avec le CI/CD.
Les livrables sont :
- un projet git (tp03) dans votre groupe sur gitlab.forge.hefr.ch avec le code du TP et le code du programme de test;
- une configuration CI/CD de gitlab pour valider votre TP;
- un journal de travail déposé sur gitlab.
Temps accordé : 2 fois 4 périodes de 45 minutes en classe + travail personnel à la maison
Délais
Un premier rapport intermédiaire doit être rendu au plus tard 6 jours après la première séance en classe. Ce rapport doit déjà comporter tous les chapitres du rapport final.
La version finale du code (fonctionnel et testé) et du rapport doit être rendue au plus tard 6 jours après la deuxième séance en classe.
Analyse
L’afficheur 7-segments
Étudions tout d’abord comment un afficheur 7-segments fonctionne. Cette partie du travail s’appelle l’analyse et la plupart des projets commencent par ça.
Les registres à décalage
Comme on le voit sur le schéma du “7seg click”, les 7-segments sont pilotés avec des circuits intégrés de type 74HC595.
Information utile
Le “datasheet” de ce circuit intégré est disponible dans les documents de ce cours, mais si vous cherchez des “datasheets” pour d’autres circuits, vous les trouverez certainement sur le site octopart.
Le 74HC595 est un registre à décalage. On écrit 8 bits en série (input) et le circuit les sort en parallèle (output). Chaque bit contrôle un segment de l’affichage et un dernier bit contrôle le point (decimal point) en bas à droite du digit.
Un tel registre à décalage est contrôlé par 3 signaux principaux :
- L’horloge (Clock) cadence l’envoi de données. Un bit est poussé dans le registre à décalage à chaque flanc montant de l’horloge.
- Les données (Serial) sont les bits envoyés au registre à décalage. On utilise le flanc descendant de l’horloge pour mettre la bonne valeur sur ce signal. Le signal doit être stable lors du flanc montant de l’horloge.
- Pendant l’envoi des bits dans le registre à décalage, les sorties de ce dernier ne reflètent pas encore les nouvelles valeurs. C’est seulement sur le flanc montant du Latch que le registre à décalage met à jour les sorties. Autrement dit, les sorties du registre à décalage conservent leur état tant que le Latch n’a pas été activé.
Les signaux (pins) de contrôle
La luminosité de l’affichage est contrôlée par la pin PWM. Nous verrons plus tard comment l’utiliser.
Pour l’instant, nous la mettons à 0 pour éteindre l’affichage et à 1 pour l’allumer.
La pin MR# (Master Reset) permet de réinitialiser le chip.
Les bits sont envoyés en série sur le signal SDI et cadencés par l’horloge sur le signal SCK.
Dès que les 16 bits sont envoyés (il y a deux registres à décalage de 8 bits reliés en série),
on peut les transférer vers les sorties en activant le signal LATCH.
Le signal SDO permet de lire les valeurs qui sortent du registre à décalage, mais nous
n’en avons pas besoin ici.
Nous pilotons les signaux MR# et LATCH avec les GPIOs de notre microcontrôleur
et les signaux SDI et SCK avec le contrôleur SPI.
Les connexions avec la cible
Nous devons maintenant déterminer les pins du STM32F412 correspondant à ces signaux. Nous avons besoin pour cela de trois documents :
- Le “datasheet” des “7seg click” (voir ci-dessus)
- Le “datasheet” de l‘“Arduino UNO click SHIELD”
- Le “datasheet” de la cible (Discovery kit with STM32F412ZG MCU / UM2032)
Déterminons par exemple à quelle pin est connecté le MR# du “click board” 1 (celui de gauche).
On voit sur le schéma du “7seg click” que le MR# correspond au signal RST du “click board”.
Dans le schéma du “Arduino UNO click SHIELD” ci-dessous, on voit que le signal RST du “click board” de gauche
correspond au signal PC3.
Ce PC3 n’est pas très intuitif, car il ne correspond pas aux noms des pins du
connecteur, mais les “datasheets” ne sont pas toujours parfaits et nous devons vivre avec. On voit que le
PC3 correspond à la pin 4 du connecteur HD1. HD1 est un connecteur avec 6 pins et le seul connecteur avec 6 pins
est celui avec les signaux A0 à A5. La pin 4 de ce connecteur correspond à A3, donc le signal MR# du
“click board” de gauche correspond à A3 sur le “Arduino UNO click SHIELD”.
Le chapitre 6.15 du “datasheet” de notre cible (UM2032) explique comment est connecté le “Arduino UNO click SHIELD”.
On voit dans le tableau 5 que la pin A3 correspond au GPIO PC4 (Port C, Pin 4). On voit bien que le PC3 que
nous avons vu au paragraphe précédent était déroutant, car c’est bien le PC4 et non le PC3 que nous devons utiliser.
Pour résumer, nous savons maintenant que le MR# du “click board” 1 correspond au GPIO PC4 (Port C, Pin 4).
À faire
Refaites cet exercice pour tous les signaux, et ce, pour le socle de gauche et pour le socle de droite. Complétez le tableau ci-dessous.
| Signal | Click | GPIO (socle de gauche) | GPIO (socle de droite) |
|---|---|---|---|
MR# |
RST |
PC4 |
|
SCK |
SCK |
||
SDO |
MISO |
||
SDI |
MOSI |
||
PWM |
PWM |
||
LATCH |
CS |
Pour info
Voici encore la correspondance pour les autres signaux des “Click Boards”
| Click | GPIO (socle de gauche) | GPIO (socle de droite) |
|---|---|---|
AN |
PA1 |
PC1 |
INT |
PG13 |
PF4 |
TX |
PG14 |
PG14 (idem) |
RX |
PG9 |
PG9 (idem) |
SCL |
PB10 |
PB10 (idem) |
SDA |
PB9 |
PB9 (idem) |
Le timing
Nous savons maintenant précisément comment est connecté le “7seg click” à notre cible, mais nous devons encore nous assurer que les contraintes de temps sont respectées. C’est une étape très importante pour garantir la fiabilité de notre système.
Notre microcontrôleur est très rapide et le 74HC595 a quelques contraintes temporelles concernant les signaux d’entrée.
On voit dans le “datasheet” du circuit intégré que, par exemple, la largeur d’une impulsion (tw) pour le signal MR# doit
être d’au moins 16 ns à 4.5 V et d’au moins 80 ns à 2 V. Nous ne connaissons pas les valeurs pour 3.3 V qui est la tension
que nous utilisons, mais nous pouvons garder un peu de marge et utiliser les valeurs pour 2 V.
En manipulant les signaux avec la procédure HAL_GPIO_WritePin :
HAL_GPIO_WritePin(<PORT>, <PIN>, GPIO_PIN_SET);
HAL_GPIO_WritePin(<PORT>, <PIN>, GPIO_PIN_RESET);
nous mesurons1 que sur notre système cadencé à 100 MHz, la période dure 230 ns et que l’impulsion la plus courte que nous puissions produire dure environ 90 ns.

On respecte
donc les contraintes du registre à décalage. Si ça n’avait pas été le cas, nous aurions dû ajouter
des boucles de délais (avec des instructions asm volatile ("nop");).
Info
Sur notre microcontrôleur cadencé à 100 MHz, le temps pour une instruction nop est de \(\frac{1000}{100} = 10\mathrm{\ ns}\).
Dans la datasheet, on lit aussi les vitesses maximales de l’horloge :

Nous prenons un peu de marge et cadençons notre système avec une horloge inférieure à 4 MHz.
Le Codeur rotatif
Le codeur rotatif est un dispositif mécanique qui permet de générer des signaux électriques en fonction de la rotation d’un axe.
En plus du codeur rotatif, le “Click Board” contient 16 LEDs placées en cercle autour de l’axe de rotation du codeur qui permettent de faire des animations visuelles.
Pour contrôler ces LEDs, le “Click Board” utilise le même registre à décalage que pour l’afficheur 7 segments. Cette information est utile pour la conception de notre driver. Nous y reviendrons plus tard.
Lorsqu’on tourne le bouton du codeur rotatif, deux signaux “A” et “B” permettent de déterminer le sens de rotation. La figure ci-dessous montre comment ces signaux se comportent :

Dans les positions stables (quand on entend un “click”), les signaux “A” et “B” sont soit les deux à “0”, soit les deux à “1”. Entre les positions stables, il y a les positions intermédiaires où les signaux “A” et “B” sont décalés et c’est ce qui nous permet de déterminer le sens de rotation. Si on arrive sur la position 11 (“A” et “B” sont à “1”) et que la position précédente était “10” (“A” à “1” et “B” à “0”), ça veut dire qu’on tourne dans le sens horaire. Si par contre la position précédente était “01” (“A” à “0” et “B” à “1”), ça veut dire qu’on tourne dans le sens antihoraire. On peut faire la même réflexion pour la position stable “00”.
À faire
Dessinez une machine d’états pour détecter le sens de rotation du codeur rotatif. Dessinez aussi la table de transition correspondante.
Astuce
Pour dessiner des machines d’états, vous pouvez utiliser draw.io ou mermaid. Vous pouvez utiliser la version en ligne ou installer une extension pour vscode. Pour draw.io, il y a aussi une extension vscode.
La figure ci-dessous montre ce qui se passe lorsque nous tournons le codeur. À première vue, le signal semble correspondre à ce que nous attendons :

Mais en “zoomant” sur le flanc du signal, on remarque un problème :

Le flanc n’est pas franc, mais oscille pendant environ 10 microsecondes avant de se stabiliser.

Comme vu lors du dernier TP, la technique de polling périodique nous permet de résoudre ce problème de “rebond”. La fréquence du polling doit être plus petite que 100 kHz. Cette fréquence doit aussi être suffisamment grande pour détecter tous les changements, même si on tourne le codeur rotatif rapidement. Dans la pratique, une fréquence de polling de 1 kHz fonctionne très bien.
En plus des deux signaux “A” et “B” qui permettent de déterminer le sens de rotation du codeur rotatif, nous avons également un signal “SW” qui est activé lorsqu’on presse sur l’axe du codeur. Nous voyons sur le schéma que les deux signaux “A” et “B” sont déjà pourvus de résistances de “Pull Down”, mais que “SW” n’en a pas. Nous devrons alors en ajouter une interne au microcontrôleur.
Trouvez maintenant les correspondances entre les pins et les ports GPIO du microcontrôleur pour les signaux du codeur rotatif. Complétez le tableau ci-dessous.
| Signal | GPIO (socle de gauche - Click 1) | GPIO (socle de droite - Click 2) |
|---|---|---|
ENCA_OUT |
||
ENCB_OUT |
||
SW |
||
LATCH |
||
SCK |
||
SDO |
||
SDI |
Voici un petit programme de test pour le codeur rotatif.
Conception
Nous avons maintenant terminé l’analyse de notre système et nous pouvons passer à la conception des pilotes. Nous décidons d’utiliser une approche orientée objet.
Design orienté objet
Nous avons vu que le “7seg click” et le “Rotary click” utilisent tous les deux le même registre à décalage pour piloter les sorties.
Il est donc intéressant de créer une classe ShiftRegister pour représenter ce registre à décalage et de l’utiliser dans les classes Seg7 et Rotary.
Nous allons donc créer trois classes :
ShiftRegisterpour représenter le registre à décalageSeg7pour représenter l’afficheur 7 segmentsRotarypour représenter le codeur rotatif
La question est maintenant de déterminer les liens entre ces classes :
- Est-ce que
Seg7etRotaryhéritent deShiftRegister? Autrement dit, est-ce qu’un affichage 7 segments est un registre à décalage avec quelque chose en plus ? - Est-ce que
Seg7etRotarycontiennent un objet de typeShiftRegister(composition) ? Autrement dit, est-ce qu’un affichage 7 segments a un registre à décalage ?
Il n’y a pas de bonne ou de mauvaise réponse, mais il est important d’y réfléchir et de faire un choix.
Nous avons déjà utilisé l’héritage dans le TP précédent pour les boutons. Choisissons donc la composition pour le ShiftRegister.
Comme on peut presser sur le bouton du codeur rotatif, on peut dire que le Rotary est un bouton et on peut donc utiliser l’héritage de la classe Button pour le Rotary.
Voici le diagramme de classes correspondant à notre conception :
classDiagram
Seg7 *-- ShiftRegister
Rotary *-- ShiftRegister
Button <|-- Rotary
Poller <|-- Rotary
class ShiftRegister {
Reset()
SendData(buffer, len)
Latch()
}
class Button {
Update()
OnPress()*
OnRelease()*
OnLongPress(repetition)*
}
class Poller {
Poll()*
PollAll()$
Add(poller)$
Delete(poller)$
}
class Rotary {
OnRotate(direction)
OnPress()
OnLongPress(repeated)
}
class Seg7 {
PrintPattern(pattern)
Print(i)
Print(f)
PrintHex(i)
SwitchOn()
SwitchOff()
}
Envoi des données par SPI
Pour envoyer les données au registre à décalage, nous avons le choix entre deux méthodes :
- Bit Banging : nous utilisons les GPIOs pour générer les signaux
SCKetSDIet nous envoyons les bits un par un. Cette méthode est simple à implémenter, mais elle est lente et consomme beaucoup de ressources du microcontrôleur. - SPI : nous utilisons le contrôleur SPI du microcontrôleur pour envoyer les données. Cette méthode est plus rapide et consomme moins de ressources, mais elle est un peu plus complexe à implémenter. C’est cependant la méthode que nous allons utiliser dans ce TP.
Attention
Pour utiliser le contrôleur SPI, il est impératif de l’activer dans la configuration de la cible.
Ceci se fait en modifiant le fichier stm32f4xx_hal_conf.h qui se trouve dans le dossier ./include/
de votre projet PlatformIO. Il vous faudra changer la ligne
/* #define HAL_SPI_MODULE_ENABLED */
#define HAL_SPI_MODULE_ENABLED
Réalisation
Vous avez été invité à rejoindre un projet gitlab pour ce TP. Commencez par cloner ce projet sur votre machine et ouvrez-le dans VSCode.
Étudiez le code des fichiers suivants :
lib/gpio/gpio.hpp: Contient la classeGpiopour manipuler les GPIOs du microcontrôleur.lib/gpio/gpio.cpp: Contient l’implémentation de la classeGpio.lib/arduino_shield/arduino_shield.hpp: Contient la classeArduinoShieldpour récupérer les pins du “Arduino UNO click SHIELD”.lib/arduino_shield/arduino_shield.cpp: Contient l’implémentation de la classeArduinoShield.lib/shift_reg/shift_reg.hpp: Contient la classeShiftRegisterpour manipuler le registre à décalage.lib/shift_reg/shift_reg.cpp: Contient l’implémentation de la classeShiftRegister.lib/seg7/seg7.hpp: Contient la définition de la classeSeg7pour manipuler l’afficheur 7 segments.lib/rotary/rotary.hpp: Contient la définition de la classeRotarypour manipuler le codeur rotatif.
Notez en particulier :
- La surcharge des opérateurs dans la classe
Gpio. - L’utilisation du pattern Singleton pour la classe
ArduinoShield - L’utilisation du tableau
pinsConfigdans la classeArduinoShield. - L’utilisation du SPI pour envoyer les données au registre à décalage (voir le chapitre 66 du document UM1725 : “User manual Description of STM32F4 HAL and low-layer drivers”)
- La méthode
SendDatade la classeShiftRegister.
Question
Pour utiliser un GPIO en sortie (output) il ne faut pas oublier de le configurer, car par défaut, tous les GPIOs sont en entrée (input). À votre avis, est-ce préférable de configurer le GPIO en sortie avant de lui définir sa valeur (0 ou 1) ou est-ce préférable de définir la valeur du GPIO avant de le configurer en sortie ? Justifiez votre réponse.
Les Alternate Functions (AF)
Par défaut, les pattes physiques d’un microcontrôleur sont associées à un GPIO, mais on peut aussi les associer à
d’autres fonctions. Par exemple, on peut associer la pin PA5 à la fonction SPI1_SCK pour en faire l’horloge du SPI1 ou la pin PA2 à la fonction TIM5_CH3 pour faire un PWM sur cette pin. Ces autres fonctions sont appelées “Alternate
Functions” (AF) et sont définies dans la table 11 aux pages 67 à 73 du
Data Sheet du STM32F412.
- Étudiez l’utilisation des Alternate Functions dans la méthode
ArduinoShield::MspInitSpi. - Étudiez l’initialisation du SPI dans la fonction
main. Étudiez en particulier la valeur duBaudRatePrescaleret vérifiez que la fréquence du SPI est adaptée à notre registre à décalage.
Le driver pour le 7-segments
Commencez par implémenter la classe Seg7 qui vous permettra de piloter l’afficheur 7 segments. Le header file seg7.hpp est déjà fourni et vous n’avez pas à le modifier.
Vous devez par contre implémenter les méthodes de la classe dans le fichier seg7.cpp.
Modifiez le fichier main.cpp en commentant les lignes qui concernent le codeur rotatif. Implémentez ensuite un programme de test pour l’afficheur 7 segments.
Vous pouvez par exemple afficher le nombre 42 sur l’afficheur.

Écrivez des tests unitaires pour la classe Seg7. Ces tests doivent valider que des nombres tels que 42, -7, 3.1 ou -.3 sont correctement transformés en patterns pour l’afficheur 7 segments.
Note
Quand vous écrirez vos tests unitaires, vous devrez peut-être modifier la définition de la classe Seg7 en y ajoutant des méthodes publiques. N’hésitez pas à le faire et documentez bien ces modifications dans votre journal de travail.
Le driver pour le codeur rotatif
Implémentez ensuite la classe Rotary qui vous permettra de piloter le codeur rotatif. Le header file rotary.hpp est déjà fourni et vous avez juste à ajouter des membres privés si nécessaire (spoiler : vous en aurez besoin).
Le codeur rotatif hérite de la classe Poller et tout comme pour le bouton, c’est l’interruption du timer qui appelle la méthode PollAll de la classe Poller. Cette méthode appelle ensuite la méthode Poll du codeur rotatif, qui appelle la méthode OnRotate dès qu’une rotation est détectée. Tout ça se passe dans l’interruption du timer et vous n’avez pas à vous en occuper, mais vous devez faire attention à ne pas bloquer le système dans la méthode OnRotate.
Règle de développement pour les interruptions
Une routine de traitement d’interruption (ISR) doit terminer le plus rapidement possible.
Dans le mini-projet, nous souhaitons modifier le 7-segments et l’état des 16 LEDs du codeur rotatif dans la méthode OnRotate. Ces opérations prennent du temps et ne doivent pas être faites dans l’interruption. Nous allons donc utiliser un flag pour signaler à la boucle principale que le codeur rotatif a été tourné et que nous devons mettre à jour l’afficheur 7 segments et les LEDs. Étudiez le code du fichier main.cpp pour voir comment ce flag est utilisé. Notez aussi le type de ce flag : std::atomic<bool> ; ce type est nécessaire pour que le flag soit correctement manipulé dans l’interruption et dans la boucle principale (variable partagée dans une section critique).
Écrivez des tests unitaires pour la classe Rotary. Ces tests doivent valider que le codeur rotatif détecte correctement les rotations dans les deux sens et que le bouton du codeur rotatif détecte correctement les pressions courtes et longues.
Note
Quand vous écrirez vos tests unitaires, vous devrez peut-être modifier la définition de la classe Rotary en y ajoutant des méthodes publiques. N’hésitez pas à le faire et documentez bien ces modifications dans votre journal de travail.
Le projet
Comme mini-projet, nous aimerions implémenter un compteur avec les spécifications suivantes :
- Compter en décimal entre 0 et 99.
- On incrémente et décrémente le compteur avec le codeur rotatif.
- Si on est à 99 et qu’on essaye d’incrémenter, on reste à 99.
- Si on est à 0 et qu’on essaye de décrémenter, on reste à 0.
- En pressant sur le bouton du codeur rotatif, on change l’animation des LEDs du codeur rotatif.
- En pressant sur le bouton du codeur rotatif pendant plus d’une seconde, on réinitialise le compteur à 0.
À ne pas oublier
Gardez toujours en tête les bonnes pratiques ainsi que les dix commandements du bon programmeur.
- Choisissez de bons noms pour les classes, les méthodes et les variables.
- Implémentez les bibliothèques avec un haut niveau d’abstraction pour pouvoir réutiliser les méthodes dans d’autres projets.
- Faites des “git commit” régulièrement avec de bons commentaires.
- Configurez le CI/CD de gitlab et testez automatiquement le plus de choses possible.
- Implémentez beaucoup de tests unitaires.
- Utilisez des assertions dans votre code pour le documenter et le rendre plus robuste.
- Votre code doit être rendu dans la branche
mainde votre dépôt gitlab. - Les fichiers binaires (
.elfet.bin) ne doivent pas être dans le dépôt, mais ils doivent être générés automatiquement par le CI/CD et disponibles dans la release.
Journal de travail
Rédigez un rapport (journal de travail) selon le même modèle que pour le premier TP. Votre rapport doit être disponible sous le nom “report.pdf” dans la release v1.0.x de votre dépôt sur GitLab.
Évaluation
L’évaluation de ce TP se fait selon les mêmes critères que pour le premier TP.