TP03 - 7-Segments et PWM – avec solutions
Objectifs du TP
Ce TP a pour but de vous familiariser avec les afficheurs 7 segments, les PWM (Pulse Width Modulation) 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;
- connaîtrons les concepts de PWM (Pulse Width Modulation);
- 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 rendu au plus tard 6 jours après la deuxième séance en classe.
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.
Tip
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.
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 “data sheet” 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)
A 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 |
Solution
Signal | Click | GPIO (socle de gauche) | GPIO (socle de droite) |
---|---|---|---|
MR# |
RST |
PC4 |
PC3 |
SCK |
SCK |
PA5 |
PA5 (idem) |
SDO |
MISO |
PA6 |
PA6 (idem) |
SDI |
MOSI |
PA7 |
PA7 (idem) |
PWM |
PWM |
PF3 |
PF10 |
LATCH |
CS |
PA15 |
PB8 |
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 |
PG19 |
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 soient 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 230ns 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é à 100MHz, le temps pour une instruction nop
est de \(\frac{1000}{100} = 10\mathrm{\ ns}\).
Dans la datasheet, on lit aussi les vitesses maximums de l’horloge :
Nous prenons un peu de marge et cadençons notre système avec une horloge inférieure à 4MHz.
Le driver en programmation orientée objet
Nous avons maintenant terminé l’analyse de notre système et nous pouvons passer à la conception du pilote. Nous décidons d’utiliser une approche orientée objet.
Attention
Pour que le programme compile, il est impératif d’activer le contrôleur SPI 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
Créez un dossier seg7
dans le dossier lib
du projet que vous avez créé pour ce TP.
Dans ce dossier, créez le fichier seg7.hpp
avec le contenu suivant :
#pragma once
#include <stdint.h>
#include "stm32f4xx_hal.h"
class Seg7 {
public:
enum clickId { kClick1, kClick2 };
explicit Seg7(clickId id = kClick1);
~Seg7();
void PrintPattern(uint16_t pattern);
void Print(int i);
void Print(float f);
void PrintHex(int i);
void SwitchOn();
void SwitchOff();
private:
GPIO_TypeDef* pwmPort_;
GPIO_TypeDef* resetPort_;
GPIO_TypeDef* latchPort_;
uint32_t pwmPin_;
uint32_t resetPin_;
uint32_t latchPin_;
static SPI_HandleTypeDef hspi_;
};
Ce fichier définit l’interface de la classe Seg7
avec les méthodes publiques,
les attributs (privés) et les méthodes privées.
Pour la réalisation, créez le fichier seg7.cpp
avec le contenu suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
|
Explication du code:
La ligne 6 implémente l’attribut statique Seg7::hspi_
. Les habitués de Java ont tendance à oublier
cette ligne, mais sans elle, le linker vous donnerait une erreur de type :
undefined reference to `Seg7::hspi_'
Apprenez à reconnaître cette erreur et à savoir comment la corriger.
Les lignes 14 à 16 enclenchent les horloges des périphériques. En effet, pour économiser
l’énergie, les périphériques sont tous déclenchés par défaut. Vous devez donc les enclencher
en activant leur horloge. Ces lignes enclenchent les horloges des GPIOA
, GPIOC
et GPIOF
Notez que vous devrez peut-être encore activer d’autres GPIO en fonction de l’id
choisi.
Vous pouvez aussi appeler les fonctions __HAL_RCC_<DEVICE>_CLK_ENABLE
plusieurs fois;
ça ne sert à rien, mais ça ne dérange pas non plus.
La ligne 19 déclare une variable gpio_init_structure
de type GPIO_InitTypeDef
. Cette
variable est utilisée pour configurer les GPIOs,
Les lignes 20 à 22 initialisent les sorties des pins des GPIOs. Le pwm et le latch à 0 et le reset à 1.
Les lignes 24 à 26 définissent la configuration des GPIOs en sortie (output) dans la variable gpio_init_structure
.
Les lignes 27 à 32 configurent les pins des GPIOs avec les paramètres spécifiées dans la variable gpio_init_structure
pour les signaux pwm, latch et reset
Question
Pourquoi avoir défini la valeur des sorties (lignes 20 à 22) avant de les avoir configurées (lignes 24 à 32) ? Aurait-on pu faire l’inverse ? Expliquez votre réponse.
Les lignes 35 à 57 initialisent le SPI
. Comme il n’y a qu’un seul SPI pour les deux slots,
l’attribut hspi_
est statique et il ne faut donc l’initialiser qu’une seule fois.
On aurait pû enrober la structure pour le SPI dans un singleton, mais pour simplifier,
nous avons décider d’utiliser une variable statique (spi_initialized
) qui empêche
les initialisations multiples du SPI.
La ligne 37 active l’horloge du SPI1.
Les lignes 39 à 44 configurent les pins utilisés par le SPI avec les Alternate Functions (AF)
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.
Nous voyons dans cette table que c’est AF5
qui configure les pins PA5
, PA6
et PA7
en mode SPI.
Les lignes 47 à 56 configurent le SPI. Notez la valeur du BaudRatePrescaler
. Ce paramètre ne peux être qu’une
puissance de deux, et avec 32, on obtient \(\frac{100\, \mathsf{MHz}}{32} = 3.125 \, \mathsf{MHz}\).
Cette valeur nous convient car plus tôt, on avait défini qu’elle devait être en dessous de 4 MHz.
Avec un prescaler de 16 on aurait une fréquence potentiellement trop rapide et avec un prescaler de 64
on ralentirait inutilement le système. 32 est donc la valuer idéale pour note affichage.
Pour tester votre 7-segments, écrivez le programme suivant dans le fichier src/main.cpp
:
#include "f412disco_ado.h"
#include "seg7.hpp"
int main(void) {
DiscoAdoInit();
Seg7 display(Seg7::clickId::kClick1);
display.SwitchOn();
display.Print(42);
while (1) {
asm volatile("nop");
}
}
Vous devriez voir le chiffre 42 sur l’affichage 7-segments :
Le PWM
La classe actuelle permet d’enclencher ou de déclencher le 7-segments en écrivant
un “1” ou un “0” dans le registre du GPIO qui correspond à la pin PWM
.
Avec un vrai PWM (Pulse Width Modulation) on peut faire varier la luminosité de notre
affichage 7-segments.
Un PWM est Un signal dont on fait varier la largeur de l’impulsion tout en gardant une fréquence fixe. C’est le rapport entre le temps passé à “0” et celui passé à “1” qui varie. Ce rapport s’appelle le duty cycle. L’illustration ci-dessous montre un signal avec un duty cycle de 50%, 75% ou 25% :
Appliqué à une LED, un signal avec 75% de duty cycle sera plus lumineux qu’avec 25%. A part pour les LEDs, les PWMs sont utilisés pour contrôler des servos ou pour réguler la vitesse de moteurs.
Pour produire un PWM, nous utilisons un timer et nous ajoutons un paramètre que nous nommons Compare dans la figure ci-dessous :
Tant que le compteur du timer est en dessous du Compare, le signal est à un et dès qu’il dépasse le Compare, il est à zéro. Dans la figure ci-dessus, T représente la période et P (Pulse) représente la largeur de l’impulsion du PWM. Comme pour n’importe quel autre timer de notre microcontrôleur, le compteur revient à zéro dès qu’il atteint une valeur donnée. Cette valeur est appelée Resolution dans l’image car elle est en effet liée à la résolution du PWM. Si cette valeur vaut 1000, le PWM aura une résolution de 0.1% et si elle vaut 10, la résolution sera de 10%.
Notre microcontrôleur nous impose quelques restrictions quant au choix du timer à utiliser pour
une pin donnée. Lors de l’exercice précédent, vous aurez
probablement découvert que le signal PWM
du click board de gauche est sur le port PF3
et pour
le click board de droite c’est PF10
. Comme on peut le voir dans le tableau 5 du data sheet
de la cible (UM2032), la fonction associée à PF3
est TIM5_CH1
et celle associée à PF10
est
TIM5_CH4
.
Ça signifie que pour émettre un signal PWM sur cette pin, nous devons utiliser le timer 5 et le canal 1 pour le click board de gauche et le canal 4 pour celui de droite.
Note
Le timer 5 sera aussi utilisé plus tard lorsque nous mettrons en œuvre l’écran LCD, mais ça sera aussi dans le contexte d’un PWM et c’est tout à fait compatible avec ce que nous faisons ici.
Nous continuons en orienté objet pour le PWM et nous pouvons commencer par déclarer l’interface de
la classe PWM
dans un fichier lib/pwm/pwm.hpp
:
#pragma once
#include "stm32f4xx_hal.h"
class PWM {
public:
enum Pin { kPF3, kPF5, kPF10 };
explicit PWM(Pin pin);
~PWM();
void SetDutyCycle(float duty_cycle);
HAL_StatusTypeDef Start();
HAL_StatusTypeDef Stop();
private:
static TIM_HandleTypeDef htim5_;
static HAL_StatusTypeDef InitTimer5();
Pin pin_;
};
Toutes les instances des pwms utilisent le timer 5 et c’est pourquoi l’attribut
htim5_
ainsi que la méthode InitTimer5
sont statiques.
Le constructeur d’un objet PWM
prend comme argument la pin que nous souhaitons contrôler. Cette pin
est dans un enum
à l’intérieur de la classe pour en limiter la portée. Si on a besoin d’un PWM sur la
pin PF3
, on pourra instancier un objet pwm
avec
pwm = new PWM(PWM::kPF3);
Pour simplifier l’implémentation, nous fixons la fréquence du pwm à 50 KHz, mais nous aurions très bien pu définir cette fréquence dans le constructeur.
Pourquoi avoir chosi 50 KHz
On doit choisir une fréquence suffisamment rapide (> 100 Hz), car sinon, on verra la LED clignoter et ce n’est pas souhaitable. Selon le type d’appareil, si on choisit une fréquence trop rapide, l’appareil ne recevrait pas assez de courant et ne fonctionnerait pas correctement. On choisit donc la fréquence la plus faible possible qui évite le scintillement. Mais parfois, l’enclenchement et le déclenchement d’un appareil produit un léger bruit et si on choisit une fréquence de pwm dans la plage des fréquences audibles (entre 20 Hz et 20 kHz) alors on risque d’entendre le pwm. Avec 50 KHz, même votre chien ne l’entendra pas. Et si vraiment ça dérange votre chat, alors vous pouvez monter la fréquence jusqu’à 70 KHz.
On peut changer à tout moment le duty cycle du pwm avec la méthode SetDutyCycle
.
Pour l’implémentation (dans le fichier pwm.cpp
), commencez par inclure les bibliothèques nécessaires
à votre programme comme d’habitude. Pour configurer le GPIO, le canal du timer et l’alternate function correspondante, nous
définissons le tableau de constantes suivant :
constexpr static struct {
GPIO_TypeDef* port;
uint32_t pin;
uint32_t channel;
uint8_t alternate;
} channelConfig[] = {
[PWM::kPF3] = {GPIOF, GPIO_PIN_3, TIM_CHANNEL_1, GPIO_AF2_TIM5},
[PWM::kPF5] = {GPIOF, GPIO_PIN_5, TIM_CHANNEL_3, GPIO_AF2_TIM5},
[PWM::kPF10] = {GPIOF, GPIO_PIN_10, TIM_CHANNEL_4, GPIO_AF2_TIM5},
};
Nous pouvons dès lors utiliser ce tableau dans le constructeur pour configurer le GPIO et le Timer :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
Nous avons vu que l’attribut htim5_
est statique. En C++, on doit encore
instancier cet attribut et c’est la ligne 1 du code ci-dessus qui s’en charge.
Après avoir enclenché l’horloge du GPIOF
(ligne 5), les lignes 6 à 12 configurent la
pin du PWM avec l’alternate function permettant de lier cette pin au timer 5.
Les lignes 14 à 17 configurent le canal correspondant.
la ligne 4 configure le timer 5 utilisé par le PWM. Voici l’implémentation
de la méthode statique InitTimer5
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
La variable is_initialized
permet de s’assurer que l’initialisation ne
soit faite qu’une seule fois.
A la ligne 5, on enclenche l’horloge du timer 5.
Les lignes 7 à 18 récupèrent la fréquence (avant les pre-scalers) des timers.
La ligne 20 calcule la valeur du pre-scaler en fonction de la fréquence souhaitée (kPwmFreq = 50000UL
)
et de la résolution souhaitée (kResolution = 1000UL
). La résolution correspond à la valeur maximum du
compteur du timer
Question
Avec un horloge de base de 100 MHz pour les timers (APB1 Timer Clocks), quel est la valeur effective de prescalerValue
?
Aurait-on pû prendre un résolution de 100 à la place de 1000 ?
Aurait-on pû prendre un résolution de 10000 à la place de 1000 ?
Les lignes 24 à 28 configurent le timer 5.
Pour le déstructeur du PWM
vous pouvez simplement désinitialiser le GPIO:
PWM::~PWM() { HAL_GPIO_DeInit(GPIOF, channelConfig[pin_].pin); }
Implémentez maintenant les méthodes manquantes :
// Configure the duty cycle between 0% and 100%.
void PWM::SetDutyCycle(float duty_cycle) {
// TODO: compute pulse width based on the duty_cycle and kResolution
// TODO: set the compare value with `__HAL_TIM_SET_COMPARE`
}
HAL_StatusTypeDef PWM::Start() {
// TODO: Start the pwm using `HAL_TIM_PWM_Start`
}
HAL_StatusTypeDef PWM::Stop() {
// TODO: Stop the pwm using `HAL_TIM_PWM_Stop`
}
A faire
Modifiez le code du 7-segments pour contrôler la luminosité avec le PWM.
Dans l’interface, utilisez simplement un objet de type PWM
.
class Seg7 {
...
private:
PWM* pwm_;
...
};
Modifiez le constructeur de Seg7
pour initialiser le PWM.
Remplacez les méthodes SwitchOn
et SwitchOff
par une méthode SetBrightness
qui permet de régler
l’intensité lumineuse entre 0 et 100%. Vous pouvez aussi conserver les méthodes SwitchOn
et SwitchOff
si vous voulez, mais dans ce cas, faites en sorte que ces méthodes appellent SetBrightness
avec 100%, respectivement 0%.
Le joystick
Lors du TP précédent, vous avez fait une classe pour les boutons. Nous pouvons maintenant
les grouper dans une classe Joystick
.
Mais contrairement aux boutons, nous n’avons qu’un seul joystick et ça ne fait pas de sens de permettre
d’en instancier plusieurs. Nous avons deux manières d’adresser ce problème :
- Implémenter le “design pattern” du “Singleton”
- Implémenter la classe avec tous les attributs et toutes les méthodes en “static”
Le singleton a l’avantage de permettre au constructeur d’initialiser le joystick dès qu’on fait l’instanciation. Avec des méthodes statique, l’utilisateur doit explicitement appeler une méthode d’initialisation avant de pouvoir utiliser les autres méthodes.
Nous choisissons donc d’implémenter le joystick avec un singleton. Voici tout d’abord l’interface de cette classe :
#include "button.hpp"
#include "poller.hpp"
constexpr int kNumButtons = 5;
class Joystick : public Poller {
public:
enum ButtonId {
kButtonUp = 0,
kButtonDown = 1,
kButtonLeft = 2,
kButtonRight = 3,
kButtonSelect = 4,
};
static Joystick* GetInstance();
void AddButton(enum ButtonId buttonId, Button* button);
void DelButton(enum ButtonId buttonId);
void Poll() override;
private:
Button* buttons_[kNumButtons];
Joystick();
Joystick(Joystick const&);
void operator=(Joystick const&);
};
Les méthodes AddButton
et DelButton
permettent d’attacher respectivement
de détacher un objet d’une classe dérivée de Button
qui implément le
comportement souhaité dans les méthodes virtuelles (OnPress
,
OnLongPress
et OnRelease
).
Notez que le constructeur est privé et ne peut donc être appelé que
depuis une fonction statique (GetInstance
) de la classe.
Pour l’implémentation, la méthode GetInstance
crée un objet et
le sauve dans une variable statique. On s’assure ainsi que j’objet
n’est créé qu’une seule fois.
Joystick* Joystick::GetInstance() {
static Joystick* instance = new Joystick;
return instance;
}
Pour le constructeur, on initialise le joystick (avec la procédure BSP_JOY_Init(JOY_MODE_GPIO
fournie par STM32Cube
et qui se trouve dans le fichier stm32412g_discovery.h
) et on assigne nullptr
aux 5 boutons :
Joystick::Joystick() {
BSP_JOY_Init(JOY_MODE_GPIO);
for (int i = 0; i < kNumButtons; i++) {
buttons_[i] = nullptr;
}
}
Les méthodes AddButton
et DelButton
sont très simples :
void Joystick::AddButton(enum ButtonId buttonId, Button* button) {
buttons_[buttonId] = button;
}
void Joystick::DelButton(enum ButtonId buttonId) {
buttons_[buttonId] = nullptr;
}
Dans la déclaration de la classe, nous avons écrit que le joystick
était un Poller
. Nous devons donc implémenter la méthode Poll
:
1 2 3 4 5 6 7 8 9 10 |
|
À la ligne 3, la fonction BSP_JOY_GetState
(fournie par STM32Cube) nous indique quel bouton
est pressé. Elle retourne JOY_SEL
si c’est le bouton central, JOY_DOWN
si c’est le bouton
du bas, JOY_LEFT
si c’est celui de gauche, JOY_RIGHT
si c’est celui de droite, JOY_UP
si c’est celui du haut et JOY_NONE
si aucun bouton n’est pressé. Ces constantes sont
définies dans le fichier Drivers/BSP/STM32412G-Discovery/stm32412g_discovery.h
.
Pour faire correspondre les constantes définies par STM32Cube avec notre Joystick::ButtonId
,
nous définissons le tableau kJoyButtons
au début du fichier joystick.cpp
:
static constexpr JOYState_TypeDef kJoyButtons[] = {
[Joystick::kButtonUp] = JOY_DOWN,
[Joystick::kButtonDown] = JOY_UP,
[Joystick::kButtonLeft] = JOY_RIGHT,
[Joystick::kButtonRight] = JOY_LEFT,
[Joystick::kButtonSelect] = JOY_SEL,
};
Notez que les directions Up/Down et Left/Right sont inversées! En effet, la cible a été faite pour que le joystick soit en haut à droite de l’écran, mais avec la carte d’extension que nous utilisons, nous devons retourner la cible.
Comme le Joystick
est un Poller
, l’appel de la méthode Poll
se fera “automatiquement”
en réaction à l’interruption du timer.
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 le compteur en pressant sur le bouton de droite.
- Si on est à 99 et qu’on essaye d’incrémenter, on reste à 99.
- On décrémente le compteur en pressant sur le bouton de gauche.
- Si on est à 0 et qu’on essaye de décrémenter, on reste à 0.
- En pressant le bouton vers le haut, on augmente la luminosité.
- En pressant le bouton vers le bas, on diminue la luminosité.
- En pressant sur le bouton central pendant au moins une seconde, on remet le compteur à zéro.
- Optionnel : Si on garde le doigt sur le bouton de droite ou de gauche pendant plus d’une seconde, alors le compteur s’incrémente ou se décrémente automatiquement chaque dixième de seconde.
- Optionnel : Si on garde le doigt sur le bouton du haut ou du bas pendant plus d’une seconde, alors la luminosité augmente ou diminue tant que le bouton est pressé.
À ne pas oublier
Gardez toujours en têtes 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 possibles.
- Implémentez beaucoup de tests unitaires.
- Utilisez des assertions dans votre code pour le documenter et le rendre plus robuste.
Journal de travail
Rédigez un rapport (journal de travail) selon le même modèle que pour le premier TP.
Déposez votre rapport dans un dossier /docs
de votre dépôt git (tp03-x) avec le nom report.pdf
(le chemin complet vers votre
rapport est donc /docs/report.pdf
)