Aller au contenu

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

7segment

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.

Arduino UNO click SHIELD

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.

Timing

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.

Bit Banging

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 :

Clock

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 */
pour qu’elle devienne
#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
#include "seg7.hpp"
#include <cmath> // if you need rounding functions such as `roundf`

// ----- Define static member -----

SPI_HandleTypeDef Seg7::hspi_{};

// ----- Constructor/Destructor -----

Seg7::Seg7(clickId id) {
    // TODO: Assign private attributes according to the id

    // Enable clocks
    __HAL_RCC_GPIOA_CLK_ENABLE();
    __HAL_RCC_GPIOC_CLK_ENABLE();
    __HAL_RCC_GPIOF_CLK_ENABLE();

    // Configure GPIO for Pwm, Latch and Reset
    GPIO_InitTypeDef gpio_init_structure;
    HAL_GPIO_WritePin(pwmPort_, pwmPin_, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(latchPort_, latchPin_, GPIO_PIN_RESET);
    HAL_GPIO_WritePin(resetPort_, resetPin_, GPIO_PIN_SET);

    gpio_init_structure.Mode  = GPIO_MODE_OUTPUT_PP;
    gpio_init_structure.Pull  = GPIO_PULLUP;
    gpio_init_structure.Speed = GPIO_SPEED_FREQ_HIGH;
    gpio_init_structure.Pin   = pwmPin_;
    HAL_GPIO_Init(pwmPort_, &gpio_init_structure);
    gpio_init_structure.Pin = resetPin_;
    HAL_GPIO_Init(resetPort_, &gpio_init_structure);
    gpio_init_structure.Pin = latchPin_;
    HAL_GPIO_Init(latchPort_, &gpio_init_structure);

    // SPI must only be configured once
    static bool spi_initialized = false;
    if (!spi_initialized) {
        spi_initialized = true;
        __HAL_RCC_SPI1_CLK_ENABLE();
        // Configure SPI as alternate function
        gpio_init_structure.Mode      = GPIO_MODE_AF_PP;
        gpio_init_structure.Speed     = GPIO_SPEED_FREQ_HIGH;
        gpio_init_structure.Pull      = GPIO_PULLUP;
        gpio_init_structure.Alternate = GPIO_AF5_SPI1;
        gpio_init_structure.Pin       = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
        HAL_GPIO_Init(GPIOA, &gpio_init_structure);

        // Configure SPI
        hspi_.Instance               = SPI1;
        hspi_.Init.Mode              = SPI_MODE_MASTER;
        hspi_.Init.Direction         = SPI_DIRECTION_2LINES;
        hspi_.Init.DataSize          = SPI_DATASIZE_8BIT;
        hspi_.Init.CLKPolarity       = SPI_POLARITY_LOW;
        hspi_.Init.CLKPhase          = SPI_PHASE_1EDGE;
        hspi_.Init.NSS               = SPI_NSS_SOFT;
        hspi_.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32;  // 3.125 MHz
        hspi_.Init.FirstBit          = SPI_FIRSTBIT_MSB;
        HAL_SPI_Init(&hspi_);
    }
}

Seg7::~Seg7() {}

// ----- Public methods -----

void Seg7::PrintPattern(uint16_t pattern) {
    uint8_t buffer[2];
    // TODO: assign values to the buffer
    HAL_SPI_Transmit(&hspi_, &buffer[0], 2, 1000); // TODO: Replace 1000 by a named constant
    // Latch
    // TODO: Implement the latch
}

void Seg7::Print(int i) {
    // TODO: Implement
}

void Seg7::Print(float f) {
    // TODO: Implement
}

void Seg7::PrintHex(int i) {
    // TODO:: Implement
}

void Seg7::SwitchOn() {
    HAL_GPIO_WritePin(pwmPort_, pwmPin_, GPIO_PIN_SET);
}

void Seg7::SwitchOff() {
    HAL_GPIO_WritePin(pwmPort_, pwmPin_, GPIO_PIN_RESET);
}

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

PWM

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 :

PWM

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.

Table5

Ç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
TIM_HandleTypeDef PWM::htim5_ = {};

PWM::PWM(Pin pin) : pin_(pin) {
    InitTimer5();
    __HAL_RCC_GPIOF_CLK_ENABLE();
    GPIO_InitTypeDef gpio_init_structure = {};
    gpio_init_structure.Mode             = GPIO_MODE_AF_PP;
    gpio_init_structure.Pull             = GPIO_PULLUP;
    gpio_init_structure.Speed            = GPIO_SPEED_FREQ_HIGH;
    gpio_init_structure.Alternate        = channelConfig[pin].alternate;
    gpio_init_structure.Pin              = channelConfig[pin].pin;
    HAL_GPIO_Init(GPIOF, &gpio_init_structure);

    TIM_OC_InitTypeDef sConfigOC = {};
    sConfigOC.OCMode             = TIM_OCMODE_PWM1;
    sConfigOC.Pulse              = 0;
    HAL_TIM_PWM_ConfigChannel(&htim5_, &sConfigOC, channelConfig[pin].channel);
}

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
HAL_StatusTypeDef PWM::InitTimer5() {
    static bool is_initialized = false;
    if (is_initialized) return HAL_OK;

    __HAL_RCC_TIM5_CLK_ENABLE();

    RCC_ClkInitTypeDef clkconfig;
    uint32_t latency;
    HAL_RCC_GetClockConfig(&clkconfig, &latency);

    uint32_t APB1Freq      = HAL_RCC_GetPCLK1Freq();
    uint32_t APB1Prescaler = clkconfig.APB1CLKDivider;
    uint32_t timclock;
    if (APB1Prescaler == RCC_HCLK_DIV1) {
        timclock = APB1Freq;
    } else {
        timclock = 2UL * APB1Freq;
    }

    uint32_t prescalerValue =
        (uint32_t)((timclock / kPwmFreq / kResolution) - 1U);
    uint32_t p = kResolution - 1U;

    htim5_.Instance           = TIM5;
    htim5_.Init.Period        = p;
    htim5_.Init.Prescaler     = prescalerValue;
    htim5_.Init.ClockDivision = 0;
    htim5_.Init.CounterMode   = TIM_COUNTERMODE_UP;

    HAL_StatusTypeDef status = HAL_TIM_PWM_Init(&htim5_);

    is_initialized = true;
    return status;
}

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 SetBrightnessqui 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
void Joystick::Poll() {
    uint32_t now           = HAL_GetTick();
    JOYState_TypeDef value = BSP_JOY_GetState();
    for (int i = 0; i < kNumButtons; i++) {
        if (buttons_[i] != nullptr) {
            buttons_[i]->Update(
                now, value == kJoyButtons[i] ? GPIO_PIN_SET : GPIO_PIN_RESET);
        }
    }
}

À 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)


  1. Mesuré avec un analyseur logique Saleae Logic Pro 16.