Aller au contenu

TP02 - Bouton-poussoir, assertions, horloge – avec solutions

Objectifs du TP

Ce TP a pour but de vous familiariser avec les entrées/sorties (boutons-poussoirs), les assertions, les timers et la programmation en C++. À la fin de ce TP, les étudiants :

  • connaîtront les différentes techniques pour connecter un bouton-poussoir à un microcontrôleur;
  • sauront lire l’état du joystick de la cible;
  • sauront utiliser les assertions pour rendre le code plus robuste;
  • sauront utiliser les timers du microcontrôleur pour faire des actions répétitives et pour mesurer des temps;
  • sauront utiliser les timers pour lire l’état des boutons-poussoirs;
  • auront réalisé plusieurs mini-projets sur la cible;
  • auront appliqué les bonnes pratiques en écrivant des tests unitaires et en validant systématiquement leur code avec le CI/CD.

Les livrables sont :

  • un projet git (tp02) 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.

Préparation

Avec le TP précédent, vous avez appris à créer un projet pour PlatformIO à partir de zéro. Dès maintenant, le projet dans lequel vous avez été invité à travailler est déjà configuré pour vous avec un code de base. Vous pouvez donc simplement cloner ce projet sur votre PC et ouvrir le dossier avec Visual Studio Code et PlatformIO.

Les boutons-poussoirs

Le bouton carré bleu à côté de l’écran est un joystick avec des boutons-poussoirs dans les 4 directions, et un bouton-poussoir central. À la page 28 du User Manual du Discovery kit with STM32F412ZG MCU nous lisons que l’état logique d’un bouton est à 1 quand le bouton est pressé. Cette indication est importante, car c’est souvent l’inverse. Pour comprendre pourquoi, voyons comment nous pouvons connecter un bouton à un microcontrôleur.

Un bouton-poussoir classique a deux connecteurs, et quand on presse le bouton, on relie ces deux connecteurs. On peut bien imaginer que l’on connecte un des deux connecteurs au microcontrôleur, mais que faire du deuxième connecteur ?

button1

On pourrait connecter ce deuxième connecteur au plus de l’alimentation (Vdd) :

button2

On a alors bien une tension quand le bouton est pressé, mais quand il est relâché, la patte du microcontrôleur est flottante et son état n’est pas défini.

On pourrait faire comme sur le schéma ci-dessous :

button short circuit

On a bien une valeur de 1 quand le bouton est lâché et un 0 quand il est pressé, mais le problème c’est que quand on presse le bouton, on fait un court-circuit!

La solution c’est simplement d’ajouter une résistance :

button with pull up

Si la résistance est placée vers le plus de l’alimentation comme dans la figure ci-dessus, on dit que c’est une Pull up, car elle tire la tension vers le haut quand le bouton n’est pas pressé et son niveau logique est à 1. Quand le bouton est pressé, la patte est reliée à Vss et le niveau logique passe à 0. Un petit courant est consommé quand le bouton est pressé, mais on choisit une résistance suffisamment grande. Avec une tension Vdd de \(3.3V\) et une résistance de pull up standard \(R\) de \(100\mathrm{k}\Omega\) le courant consommé quand on presse le bouton est de

\[I = \frac{U}{R} = \frac{3.3}{100 \times 10^3}A = 33\mu A\]

Une autre possibilité consiste à mettre la résistance du côté de Vss et à tirer la tension vers le bas quand le bouton n’est pas pressé. On appelle alors cette résistance Pull Down :

button with pull down

Dans ce cas, le niveau logique de la patte est à zéro quand le bouton est relâché et à un quand il est pressé. Ce comportement semble plus logique que le précédent et on pourrait croire que c’est cette construction qui est la plus souvent utilisée, mais avec certains types de semi-conducteurs, la résistance de pull down ne peut pas être aussi grande que dans une construction de type pull up et c’est pourquoi la construction avec une résistance de pull up est la plus souvent utilisée.

Revenons maintenant à notre joystick. Comment est-il connecté au microcontrôleur ? Le schéma à la page 40 nous donne la réponse :

joystick

Le schéma confirme la construction de type pull down mais le [N/A] comme valeur indique que les résistances ne sont pas installées. Il faudra donc configurer des résistances internes.

Information

Pour votre information, et selon le Datasheet, les résistances internes de notre microcontrôleur valent environ \(40 \mathrm{k}\Omega\).

Pour ce TP, nous n’utiliserons que le bouton central et le BSP (Board Support Package) nous permet de l’initialiser avec la fonction suivante :

BSP_PB_Init(BUTTON_WAKEUP, BUTTON_MODE_GPIO);
On peut ensuite lire l’état du bouton avec la fonction :

BSP_PB_GetState(BUTTON_WAKEUP)

Cette fonction retourne GPIO_PIN_RESET si le signal est à zéro et GPIO_PIN_SET si le signal est à un. Comme le joystick est câblé en pull down, la valeur GPIO_PIN_SET représente le bouton pressé et GPIO_PIN_RESETreprésente le bouton relâché.

Maintenant que nous avons terminé notre analyse du matériel, nous pouvons réaliser un premier programme.

Premier programme avec un bouton

Écrivez un programme simulant un bouton qui enclenche une lampe dans une chambre. Il faut presser le bouton pour enclencher la lampe et le presser une seconde fois pour la déclencher. Utilisez le bouton central du joystick pour le bouton et la LED bleue pour simuler la lampe.

Mettez en place les tests unitaires avec juste une fonction main vide afin de vous assurer que le pipeline fonctionne correctement.

Le problème des rebonds

Vous avez peut-être observé qu’une implémentation naïve de l’exercice ci-dessus ne réagit pas toujours bien aux pressions sur le bouton. Si c’est le cas, c’est probablement dû à un problème de rebond du bouton.

En théorie, un bouton-poussoir avec une résistance de pull down devrait se comporter comme ça :

ideal switch

Mais dans la pratique, quand on presse le bouton, le signal ressemble plutôt à ça :

Bouncing

Ce phénomène s’appelle des rebonds (en anglais bouncing). On peut gérer ce problème de différentes manières :

  • avec un condensateur qui amorti les rebonds (solution hardware bon marché)
  • avec un circuit spécialisé (solution hardware performante)
  • en introduisant des délais dans le code (solution software)

Vous trouverez beaucoup de détails concernant les rebonds dans l’article A Guide to Debouncing. Vous y apprendrez entre autres que le temps moyen d’un rebond est d’environ 1.6 milliseconde et que le maximum observé est de 6.2 millisecondes. Dans la pratique, un délai de 10 ou 20 millisecondes fait très bien l’affaire. Vous apprendrez dans la suite de ce TP comment introduire un délai précis dans un programme.

Les connexions des LEDs

Nous avons donné les schémas électroniques des boutons et prenons aussi le temps d’expliquer comment les LEDs sont connectées aux microcontrôleurs, ou plus précisément aux ports des GPIOs des microcontrôleurs.

Une première manière de connecter une LED au port d’un GPIO est de laisser le GPIO fournir le courant pour la LED comme indiqué sur le schéma suivant :

sourcing led

Notez la présence de la résistance qui limite le courant qui peut passer par la LED. Dès que le port du GPIO passe à 1, la tension de la pin correspondante est mise à \(3.3 \mathrm{V}\), le courant traverse la résistance et la LED s’allume. Pour calculer la valeur de la résistance \(\mathrm{R_1}\), il faut connaître les caractéristiques de la LED. Ces caractéristiques varient d’un modèle à l’autre, mais une LED standard rouge est capable de consommer au maximum \(20 \mathrm{mA}\) et la tension aux bornes de la LED est de \(2 \mathrm{V}\). La résistance sera aussi traversée par un courant de \(20 \mathrm{mA}\) et la tension à ses bornes sera de \(3.3 - 2 = 1.3 \mathrm{V}\). La valeur minimale de la résistance est donc de

\[R_1 = \frac{U}{I} = \frac{1.3}{0.02} = 65 \Omega\]

Dans la pratique, on n’a pas besoin de faire briller la LED au maximum et on choisira plutôt une résistance entre \(150 \Omega\) et \(620 \Omega\).

Une autre manière de connecter une LED au GPIO est de laisser le GPIO puiser le courant :

sinking led

Avec cette construction, le courant est fourni par VDD (\(3.3 \mathrm{V}\)) et il faut mettre le port du GPIO à 0 (zéro) pour créer une différence de potentiel et ainsi allumer la LED. On utilise souvent cette construction, car certains GPIOs sont capables de puiser plus de courant que d’en fournir.

Le calcul de \(\mathrm{R_1}\) est le même que pour la construction précédente.

Un GPIO est limité quant au courant qu’il est capable de fournir (ou de puiser). Si votre circuit doit contrôler beaucoup de LEDs ou contrôler des LEDs plus gourmandes, on utilise généralement un circuit de puissance entre le GPIO et les LEDs. Ça peut être un simple transistor ou un circuit intégré de type ULN2003 :

ULN2003

Question

Comment sont configurés les quatre LEDs de votre cible ? Est-ce que le GPIO fournit ou puise le courant ? Quelles sont les résistances ? Sont-elles toutes les mêmes ? Sinon, pourquoi pas ?

Les assertions

Les assertions sont importantes pour assurer la fiabilité des programmes. Selon les directives de la NASA, chaque procédure devrait avoir au moins deux assertions. Consultez la règle 5 de l’article que j’ai posté sur dev.to à ce sujet.

Étudiez les fichiers du dossier lib/assert fournis.

lib/assert/assert.h
/**
 ******************************************************************************
 * @file        : assert.h
 * @brief       : Assertions
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 27. July 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Assertions
 ******************************************************************************
 */

#pragma once

#include <stdint.h>

#include "app_conf.h"

#ifdef __cplusplus
extern "C" {
#endif

#ifdef CHECK_ASSERT
#define assert(expr) ((expr) ? (void)0U : assert_failed((uint8_t*)__FILE__, __LINE__))
void assert_failed(uint8_t* file, uint32_t line);
#else
#define assert(expr) ((void)0U)
#endif /* CHECK_ASSERT */

#ifdef __cplusplus
}
#endif

lib/assert/assert.c
/**
 ******************************************************************************
 * @file        : assert.c
 * @brief       : Assertions
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 27. July 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Assertions
 ******************************************************************************
 */

#include "assert.h"

#include <stdio.h>

#include "f412disco_ado.h"

void assert_failed(uint8_t* file, uint32_t line) {
    printf("Assertion failed at %s, line %d\n", file, line);
    BSP_LED_Init(LED_ORANGE);
    while (1) {
        BSP_LED_Toggle(LED_ORANGE);
        HAL_Delay(50);
    }
}

La fonction assert est implémentée à l’aide d’une macro et ça permet de supprimer toute trace des assertions lorsqu’on souhaite (pour de questions de performance) de supprimer les tests des assertions.

Lorsqu’une assertion n’est pas satisfaite, la méthode assert_failed sera appelée. Dans l’implémentation ci-dessus, cette méthode affiche le nom du fichier et la ligne de code à laquelle se trouve l’assertion et fait clignoter rapidement la LED orange dans une boucle infinie.

Étudiez le fichier app_conf.h dans le dossier include de votre projet et notez comment les assertions sont activées :

include/app_conf.h
/**
 ******************************************************************************
 * @file        : app_conf.h
 * @brief       : App Configuration
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 10. August 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * App Configuration
 ******************************************************************************
 */

#pragma once

// Enable assertion checks
#define CHECK_ASSERT

Assurez-vous de bien comprendre ces codes et n’hésitez pas à demander des explications à l’enseignant.

Testez ce mécanisme en écrivant un petit programme avec un assert(true) et un assert(false) et vérifiez que tout fonctionne.

N’hésitez pas à mettre des assertions dans votre code pour en améliorer la fiabilité

Un meilleur tracing

Dans le TP précédent, nous avons simplement utilisé des printf pour afficher des informations sur la console. C’était un bon départ, mais il serait très pratique de pouvoir choisir le niveau de détails qu’on souhaite avoir sur cette même console. En production, nous avons besoin des messages d’erreurs importants tandis que pendant le développement, nous souhaitons avoir des traces plus précises pour nous permettre de déboguer notre code.

Étudiez les fichiers tracing.h et tracing.c fournis dans le dossier lib/tracing.

lib/tracing/tracing.h
/**
 ******************************************************************************
 * @file        : tracing.h
 * @brief       : Tracing
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 27. July 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Tracing
 ******************************************************************************
 */

#pragma once

#include <stdio.h>

#include "app_conf.h"
#include "f412disco_ado.h"

#ifdef __cplusplus
extern "C" {
#endif

#define TRACE_LEVEL_DEBUG 0
#define TRACE_LEVEL_INFO 1
#define TRACE_LEVEL_WARNING 2
#define TRACE_LEVEL_ERROR 3
#define TRACE_LEVEL_FATAL 4

void trace(int level, const char* fmt, ...);

#if TRACE_LEVEL <= TRACE_LEVEL_DEBUG
#define tr_debug(fmt, ...) trace(TRACE_LEVEL_DEBUG, fmt, ##__VA_ARGS__)
#else
#define tr_debug(fmt, ...) ((void)0U)
#endif

#if TRACE_LEVEL <= TRACE_LEVEL_INFO
#define tr_info(fmt, ...) trace(TRACE_LEVEL_INFO, fmt, ##__VA_ARGS__)
#else
#define tr_info(fmt, ...) ((void)0U)
#endif

#if TRACE_LEVEL <= TRACE_LEVEL_WARNING
#define tr_warn(fmt, ...) trace(TRACE_LEVEL_WARNING, fmt, ##__VA_ARGS__)
#else
#define tr_warn(fmt, ...) ((void)0U)
#endif

#if TRACE_LEVEL <= TRACE_LEVEL_ERROR
#define tr_error(fmt, ...) trace(TRACE_LEVEL_ERROR, fmt, ##__VA_ARGS__)
#else
#define tr_error(fmt, ...) ((void)0U)
#endif

#define tr_fatal(fmt, ...) trace(TRACE_LEVEL_FATAL, fmt, ##__VA_ARGS__)

#ifdef __cplusplus
}
#endif

lib/tracing/tracing.c
/**
 ******************************************************************************
 * @file        : tracing.c
 * @brief       : Tracing
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 27. July 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Tracing
 ******************************************************************************
 */

#include "tracing.h"

#include <stdarg.h>
#include <stdio.h>

#include "error_handler.h"
#include "f412disco_ado.h"

static char* level_name[] = {
    [TRACE_LEVEL_DEBUG]   = "DEBUG",
    [TRACE_LEVEL_INFO]    = "INFO",
    [TRACE_LEVEL_WARNING] = "WARN",
    [TRACE_LEVEL_ERROR]   = "ERROR",
    [TRACE_LEVEL_FATAL]   = "FATAL",
};

static void __trace(int level, const char* fmt, va_list args) {
    printf("%-6s: ", level_name[level]);
    vprintf(fmt, args);
    printf("\n");
    if (level >= TRACE_LEVEL_FATAL) {
        ErrorHandler();
    }
}

void trace(int level, const char* fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    __trace(level, fmt, ap);
    va_end(ap);
}

Ce système définit 5 niveaux de tracing :

  • TRACE_LEVEL_DEBUG
  • TRACE_LEVEL_INFO
  • TRACE_LEVEL_WARNING
  • TRACE_LEVEL_ERROR
  • TRACE_LEVEL_FATAL

Vous pouvez maintenant utiliser tr_debug, tr_info, tr_warn, tr_error ou tr_fatal à la place des printf. Notez que tr_fatal appelle la méthode ErrorHandler qui fait clignoter la LED rouge dans une boucle infinie.

Vous pouvez régler le niveau de tracing en définissant le symbole TRACE_LEVEL dans le fichier include/app_conf.h. Par exemple :

#define TRACE_LEVEL TRACE_LEVEL_INFO

Expérimentez le tracing et n’hésitez pas à l’utiliser dans votre code.

Une version orientée objet du bouton

La première classe que vous allez réaliser implémente le comportement d’un bouton, sans aucun lien avec un matériel donné. Cette classe aura l’avantage d’être facilement testable avec des tests unitaires. Voici le schéma de la classe :

classe Led

Et en voici un extrait de l’interface

class Button {
   private:
    // TODO: Add private members if needed

   public:
    explicit Button(uint32_t long_press = 1000, uint32_t repeated_press = 200);
    void Update(uint32_t tick, uint32_t value);
    virtual void OnPress() {}
    virtual void OnLongPress(int repetition) { (void)repetition; }
    virtual void OnRelease() {}
};

Le constructeur prend deux paramètres :

  • long_press : Le temps (en millisecondes) qu’il faut presser le bouton pour déclencher le premier évènement OnLongPress;
  • repeated_press : Le temps (en millisecondes) entre les appels successifs à OnLongPress tant que le bouton est pressé.

Les 3 méthodes OnPress, OnLongPress et OnRelease sont virtuelles et peuvent être redéfinies dans des classes filles. OnPress est appelé dès que le bouton est pressé, OnRelease est appelé dès que le bouton est relâché, et OnLongPress est appelé tant que le bouton reste pressé. Le paramètre repetition indique le nombre de fois (en commençant par 0) que la méthode a été appelée.

Note

Notez que c’est la méthode Update() qui déclenche l’appel de ces méthodes.

Implémentez cette classe dans les fichiers lib/button/button.hpp et lib/button/button.cpp.

Si cppcheck indique qu’une fonction n’est pas utilisée, vous pouvez supprimer le message d’erreur avec le commentaire // cppcheck-suppress unusedFunction. Par exemple :

// cppcheck-suppress unusedFunction
void OnPress() override {
    tr_debug("Button pressed");
    green_led_->On();
}

Pour tester votre classe, utilisez les trois fichiers suivants :

test/test_main.cpp
/**
 ******************************************************************************
 * @file        : test_main.cpp
 * @brief       : Tests runner
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 11. August 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Tests runner
 ******************************************************************************
 */

#include "f412disco_ado.h"
#include "stm32412g_discovery.h"
#include "stm32f4xx_hal.h"
#include "system_clock.h"
#include "test_button.hpp"
#include "unity.h"

void setUp(void) {}

void tearDown(void) {}

int main(void) {
    HAL_Init();
    SystemClock_Config();
    HAL_Delay(2000);  // Mandatory waiting for 2 seconds...
    UNITY_BEGIN();    // Mandatory call to initialize test framework
    RUN_TEST(test_button_pressed);
    RUN_TEST(test_button_long_pressed);
    UNITY_END();  // Mandatory call to finalize test framework

    while (1) {
        asm("nop");
    }
}

test/test_button.hpp
/**
 ******************************************************************************
 * @file        : test_buttons.hpp
 * @brief       : Button tests
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 11. August 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Button tests
 ******************************************************************************
 */

#pragma once

void test_button_pressed(void);
void test_button_long_pressed(void);

test/test_button.cpp
/**
 ******************************************************************************
 * @file        : test_buttons.cpp
 * @brief       : Button tests
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 11. August 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Button tests
 ******************************************************************************
 */

#include "test_button.hpp"

#include "button.hpp"
#include "unity.h"

class MySimpleButton : public Button {
   public:
    explicit MySimpleButton(uint32_t long_press     = 1000,
                            uint32_t repeated_press = 200)
        : Button(long_press, repeated_press),
          press_count_{0},
          long_press_count_{0},
          repetition_{0} {}
    void OnPress() override { press_count_++; }
    void OnLongPress(int repetition) override {
        long_press_count_++;
        repetition_ = repetition;
    }
    int press_count_;
    int long_press_count_;
    int repetition_;
};

void test_button_pressed(void) {
    MySimpleButton b(1000, 200);
    uint32_t time = 10;
    b.Update(0, GPIO_PIN_RESET);
    TEST_ASSERT_EQUAL(0, b.press_count_);
    // Test 20 press
    for (int count = 0; count < 20; count++) {
        b.Update(time += 10, GPIO_PIN_SET);
        TEST_ASSERT_EQUAL(count + 1, b.press_count_);
        b.Update(time += 10, GPIO_PIN_RESET);
        TEST_ASSERT_EQUAL(count + 1, b.press_count_);
    }
}

void test_button_long_pressed(void) {
    MySimpleButton b(1000, 200);
    uint32_t time = 10;
    b.Update(0, GPIO_PIN_RESET);
    TEST_ASSERT_EQUAL(0, b.long_press_count_);
    // Repeat 20 times
    for (int count = 0; count < 20; count += 2) {
        // Test 20 quick press
        for (int i = 0; i < 20; i++) {
            b.Update(time += 50, GPIO_PIN_SET);
            TEST_ASSERT_EQUAL(count, b.long_press_count_);
            b.Update(time += 50, GPIO_PIN_RESET);
            TEST_ASSERT_EQUAL(count, b.long_press_count_);
        }
        // keep pressed for 900 ms
        b.Update(time += 10, GPIO_PIN_SET);
        for (int i = 0; i < 90; i++) {
            b.Update(time += 10, GPIO_PIN_SET);
            TEST_ASSERT_EQUAL(count, b.long_press_count_);
        }
        // keep pressed for 200 ms more
        b.Update(time += 200, GPIO_PIN_SET);
        TEST_ASSERT_EQUAL(count + 1, b.long_press_count_);
        TEST_ASSERT_EQUAL(0, b.repetition_);

        // keep pressed for 10 ms more
        b.Update(time += 10, GPIO_PIN_SET);
        TEST_ASSERT_EQUAL(count + 1, b.long_press_count_);
        TEST_ASSERT_EQUAL(0, b.repetition_);

        // keep pressed for 300 ms more
        b.Update(time += 300, GPIO_PIN_SET);
        TEST_ASSERT_EQUAL(count + 2, b.long_press_count_);
        TEST_ASSERT_EQUAL(1, b.repetition_);
        // release
        b.Update(time += 10, GPIO_PIN_RESET);
        TEST_ASSERT_EQUAL(count + 2, b.long_press_count_);
        TEST_ASSERT_EQUAL(1, b.repetition_);
    }
}

Dès que la classe de base de votre bouton fonctionne, nous pouvons l’étendre avec une classe dérivée connectée au matériel.

Complétez l’interface button.hpp avec le code suivant :

class WakeUpButton : public Button {
   public:
    WakeUpButton();
    void Poll();
};

et le fichier button.cpp avec :

WakeUpButton::WakeUpButton() : Button() {
    BSP_PB_Init(BUTTON_WAKEUP, BUTTON_MODE_GPIO);
    Poll();
}

void WakeUpButton::Poll() {
    Update(HAL_GetTick(), BSP_PB_GetState(BUTTON_WAKEUP));
}

Et c’est tout! Il vous suffit d’appeler régulièrement la méthode Poll() de votre bouton pour qu’il gère correctement les évènements.

Le poller

Si vous avez un grand nombre de boutons à gérer, ça devient fastidieux d’appeler la méthode Poll() de tout ces boutons. C’est pourquoi nous décidons d’utiliser un poller pour nous simplifier la tâche.

L’implémentation vous est fournie avec les deux fichiers du dossier lib/poller.

lib/poller/poller.hpp
/**
 ******************************************************************************
 * @file        : poller.hpp
 * @brief       : Poller
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 10. August 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Poller
 ******************************************************************************
 */

#pragma once

#include <vector>

class Poller {
   public:
    Poller() { Add(this); }
    virtual ~Poller() { Del(this); }

    virtual void Poll() = 0;

    static void PollAll();

   private:
    static void Add(Poller* p);
    static void Del(Poller* p);
    static std::vector<Poller*> pollers_;
};

lib/poller/poller.cpp
/**
 ******************************************************************************
 * @file        : poller.cpp
 * @brief       : Poller
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 10. August 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Poller
 ******************************************************************************
 */

#include "poller.hpp"

#include <stdio.h>

#include <vector>

#include "assert.h"

std::vector<Poller*> Poller::pollers_;

void Poller::Add(Poller* p) { pollers_.push_back(p); }
void Poller::Del(Poller* p) {
    int len = pollers_.size();
    for (std::vector<Poller*>::iterator it = pollers_.begin();
         it != pollers_.end();) {
        if (*it == p) {
            it = pollers_.erase(it);
        } else {
            ++it;
        }
    }
    assert((int)pollers_.size() == len - 1);
}

void Poller::PollAll() {
    for (Poller* p : pollers_) {
        p->Poll();
    }
}

Chaque nouvelle instance d’un Poller est automatiquement ajoutée à l’attribut statique pollers_ et la méthode PollAll appelle simplement la méthode Poll de chaque poller.

Pour utiliser cette fonctionnalité avec le bouton, il suffit de faire en sorte que WakeUpButton hérite également de Poller :

class WakeUpButton : public Poller, Button {
   ...

Button implémente déjà la méthode Poll, donc c’est tout ce que vous avez à faire.

Écrivez maintenant un des tests unitaires pour le Poller :

test/test_poller.hpp
/**
 ******************************************************************************
 * @file        : test_poller.hpp
 * @brief       : Poller Tests
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 11. August 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Poller Tests
 ******************************************************************************
 */

#pragma once

void test_poller(void);

test/test_poller.cpp
/**
 ******************************************************************************
 * @file        : test_poller.cpp
 * @brief       : Poller Tests
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 11. August 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Poller Tests
 ******************************************************************************
 */

#include "test_poller.hpp"

#include "poller.hpp"
#include "unity.h"

class MyPoller : public Poller {
   public:
    MyPoller() : Poller(), polls_{0} {};
    void Poll() override {
        polls_++;
        total_polls_++;
    }
    int polls_;
    static int total_polls_;
};
int MyPoller::total_polls_ = 0;

void test_poller(void) {
    // TODO: Implement unit test.
}

N’oubliez pas d’ajouter l’appel de la fonction de test dans test/test_main.cpp :

int main(void) {
    ...
    RUN_TEST(test_poller);
    ...
}

Pour tester l’ajout et la suppression de poller, utilisez des instances créées dynamiquement (avec new et delete) :

    MyPoller* poller1 = new MyPoller();
    ...
    delete (poller1);
    ...

Pour la dernière étape du TP, nous allons automatiser l’appel à tous les pollers à l’aide d’un timer. Mais avant d’implémenter ça, nous devons étudier comment le microcontrôleur gère le temps.

La notion du temps

Le microcontrôleur dispose de plusieurs timers qui permettent de donner une notion précise du temps qui passe à notre programme. Sans forcément le savoir, vous avez déjà utilisé un timer dans le TP précédent. En effet, la fonction HAL_Delay() utilise un timer (en occurrence le timer 6) pour attendre un nombre donné de millisecondes.

Un timer est composé d’un compteur qui démarre à une valeur minimum (Min), qui est incrémenté automatiquement avec une horloge, et qui est remis à la valeur Min dès qu’il atteint la valeur maximum (Max). En plus de revenir à la valeur Min, le timer génère une interruption que le microprocesseur peut utiliser pour effectuer des tâches de manière périodique. On peut régler la période (T) du timer en modifiant les valeurs de Min et de Max ou en modifiant la vitesse de l’horloge qui cadence l’incrémentation du compteur.

Timer

Le stm32f412 possède 17 timers : douze timers 16-bit, deux timers 32-bit, deux timers pour les watchdogs et un timer SysTick. La valeur Min de ces timers est de zéro et la valeur Max dépend de la taille du compteur (entre 1 à 65‘536 pour les compteurs 16 bits et entre 1 et 4‘294‘967‘296 pour les compteurs 32 bits.)

Lisez le chapitre 3.23 du datasheet pour plus de détail concernant les caractéristiques des différents timers.

Comme mentionné plus haut, le timer 6 est déjà utilisé par notre système pour, entre autres, implémenter la fonction HAL_Delay(). Nous utiliserons aussi le timer 5 pour implémenter un PWM dans les TPs suivants. Pour ce TP, nous utilisons le timer 3. Il s’agit d’un timer à usage général (General-purpose timer) de 16 bits.

La base de temps pour les timers est donnée par le RCC (Reset and Clock Control). Dans notre configuration, l’horloge pour les timers (APB1 Timer Clocks) est de 100MHz. Cette horloge est produite à partir de l’horloge système HLCK. Comme on le voit dans la figure ci-dessous, le signal HCLK est divisé par \(2\) avec le APB1 Prescaler et remultiplié par \(2\).

Clock with div 2

Le APB1 Prescaler peut être configuré par l’utilisateur final. Si on configure ce prescaler pour faire une division par \(1\), on remarque que le facteur multiplicateur est aussi passé à \(\times 1\).

Clock with div 1

Notez que cette configuration n’est pas valide, car le signal APB1 peripheral clock est trop rapide.

Si on on configure le prescaler pour faire une division par \(4\), on remarque le que le facteur multiplicateur est repassé à \(\times 2\) (et non \(\times 4\)).

Clock with div 4

Le facteur multiplicateur reste à \(\times 2\) si le prescaler fait une division par \(8\) ou par \(16\)

On ne peut pas demander au système la valeur du APB1 Timer Clocks, mais on peut demander le HCLK, le APB1 Prescaler et le APB1 peripheral clock. On peut donc calculer le APB1 Timer Clocks comme suit :

\[ \mathrm{APB1 Timer Clocks}= \begin{cases} \mathrm{APB1 peripheral clock} ,& \mathsf{si}\, \mathrm{APB1 Prescaler} = \div 1\\ \mathrm{APB1 peripheral clock} \times 2,& \mathsf{sinon} \end{cases} \]

Nous implémenterons cette formule générale dans notre code, mais revenons à note cas concret où le APB1 Timer Clocks est à 100 MHz. À cette fréquence, un compteur sur 16 bits fait le tour complet en :

\[ \frac{2^{16}}{100 \cdot 10^{6}} = 655.36 \cdot 10^{-6} \mathsf{s} = 655.36 \mathsf{\mu s} \]

Question

Quelle est cette durée si on utilise un timer sur 32 bits au lieu de 16?

Solution
\[ \frac{2^{32}}{100 \cdot 10^{6}} = 42.94967296 \mathsf{s} \]

Si on souhaite utiliser ce timer 16 bit pour faire clignoter une LED, on doit impérativement ralentir l’horloge pour obtenir une période de l’ordre de la seconde.

Pour ralentir un timer, on utilise un autre prescaler, le PSC Prescaler :

Prescaler

Ce prescaler va de \(1\) à \(65536\) et avec la valeur maximum, on peut faire faire en sorte qu’un tour complet du timer prenne environ 43 secondes.

Question

Avec une horloge APB1 Timer Clocks à 100 MHz, comment configurer le prescaler et la valeur max pour obtenir une période de 1 seconde ?

Solution

on peut utiliser les combinaisons suivantes :

  • Un prescaler à 10‘000 (ce qui fait du 10KHz à l’entrée du compteur) et une valeur max à 10‘000 également.
  • Un prescaler à 5‘000 et une valeur max à 20‘000 (ou l’inverse).
  • Un prescaler à 1‘600 et une valeur max à 62‘000 (ou l’inverse).

La règle c’est que le produit des deux doit faire \(100'000'000\) et que les deux nombres doivent être \(\leq 2^{16}\).

On pourrait penser que l’idéal consiste à choisir le prescaler le plus petit possible afin d’obtenir la meilleure résolution, mais ce n’est pas la solution. Dans l’exemple ci dessus, le prescaler le plus petit serait de \(1'526\) et le max serait donc de \(65'530.7994757536\), ce qui pose un problème car max doit être entier!

Pour utiliser un timer (par exemple le timer 3) avec STM32Cube, on fait les opérations suivantes :

  1. On enclenche le clock du timer dans RCC :
    __HAL_RCC_TIM3_CLK_ENABLE();
    
  2. On récupère les paramètres de l’horloge du système (APB1 peripheral clock et APB1 Prescaler) :
    RCC_ClkInitTypeDef clkconfig;
    uint32_t lLatency; // not used here
    HAL_RCC_GetClockConfig(&clkconfig, &latency);
    uint32_t APB1Freq = HAL_RCC_GetPCLK1Freq()
    uint32_t APB1Prescaler = clkconfig.APB1CLKDivider;
    
  3. On calcule la fréquence de l’horloge du timer (timclock) en fonction de l’horloge du système et du prescaler :
    uint32_t timclock;
    if (APB1Prescaler == RCC_HCLK_DIV1) {
        timclock = APB1Freq;
    } else {
        timclock = 2UL * APB1Freq;
    }
    
  4. On calcule la valeur du prescaler (prescalerValue). Supposons que nous souhaitons configurer le prescaler pour avoir une fréquence d’entrée de prescaler_freq:
    uint32_t prescalerValue = (uint32_t)((timclock / prescaler_freq) - 1U);
    assert(prescalerValue <= 0xffff);
    
  5. On initialise le timer. Supposons que nous souhaitons une fréquence d’incrémentation de frequency :
    TIM_HandleTypeDef htim3;
    
    htim3.Instance = TIM3;
    uint32_t p = (prescaler_freq / frequency) - 1U;
    assert(p <= 0xffff);
    htim3.Init.Period            = p;
    htim3.Init.Prescaler         = prescalerValue;
    htim3.Init.ClockDivision     = 0;
    htim3.Init.CounterMode       = TIM_COUNTERMODE_UP;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
    
    HAL_StatusTypeDef status = HAL_TIM_Base_Init(&htim3);
    if (status != HAL_OK) {
        return status;
    }
    
  6. On enregistre un callback qui sera appelé lorsque le timer reviendra à zéro :
    status = HAL_TIM_RegisterCallback(
        &htim3, HAL_TIM_PERIOD_ELAPSED_CB_ID, callback);
    if (status != HAL_OK) {
        return status;
    }
    
  7. On démarre de timer en mode interruption (IT) pour qu’il appelle bien le callback :
    status = HAL_TIM_Base_Start_IT(&htim3);
    if (status != HAL_OK) {
        return status;
    }
    
  8. On active les interruptions du timer 3 :
    HAL_NVIC_EnableIRQ(TIM3_IRQn);
    

On définit encore la fonction callback :

void callback(TIM_HandleTypeDef* htim) {
    (void)htim;
    ...
}

Et pour que le système d’interruptions fonctionne, il faut encore définir une fonction TIM3_IRQHandler avec une signature C qui appelle la fonction HAL_TIM_IRQHandler :

extern "C" {
void TIM3_IRQHandler(void) { HAL_TIM_IRQHandler(&htim3); }
}

Votre fonction callback sera appelée périodiquement et si vous le souhaitez, vous pourrez en tout temps obtenir la valeur du compteur du timer 3 avec __HAL_TIM_GET_COUNTER(&htim3);.

Note

Le fait de préfixer un identificateur avec un underscore (_) indique parfois qu’il s’agit d’une méthode interne (ou privée) qui ne fait pas partie de l’API officiel. C’est le cas par exemple en Python ou en JavaScript. Avec STM32Cube il n’en est rien et les deux _ en préfix de __HAL_TIM_GET_COUNTER indiquent juste qu’il s’agit d’une macro et non d’une fonction.

Polling périodique

Vous avez probablement réalisé le premier exercice de ce TP en implémentant une boucle sans fin qui lit l’état des boutons et qui modifie l’état des LEDs en conséquence. Cette technique s’appelle polling continue. C’est une technique simple à mettre en œuvre, mais le problème est que le système peut difficilement être étendu pour faire d’autres choses. De plus, en lisant l’état du bouton trop rapidement, on risque d’être confronté au problème des rebonds expliqués précédemment.

Maintenant que nous savons que notre système possède des timers, nous pouvons les utiliser pour régulièrement demander l’état du bouton et de réagir en cas de changement. Nous n’interrogeons donc plus le bouton constamment, mais à intervalles réguliers.

Pour la fréquence du polling, les paramètres suivants sont à prendre en compte

  • La fréquence doit être suffisamment lente pour éviter les problèmes de rebonds. La période de polling doit être plus grande que le temps d’instabilité du signal du bouton
  • La fréquence doit être suffisamment rapide pour assurer une bonne réactivité du système.
  • La fréquence doit être suffisamment lente pour ne pas trop charger le système et le laisser faire d’autres tâches.

Dans la pratique, une fréquence de 100 Hz fait très bien l’affaire.

Cette technique s’appelle le polling périodique.

Intégrons maintenant cette technique au poller que nous avons fait précédemment.

lib/poller/poller.hpp
/**
 ******************************************************************************
 * @file        : poller.hpp
 * @brief       : Poller
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 10. August 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Poller
 ******************************************************************************
 */

#pragma once

#include <vector>

#include "stm32f4xx_hal.h"

class Poller {
   public:
    Poller() { Add(this); }
    virtual ~Poller() { Del(this); }

    HAL_StatusTypeDef static InitTimer(uint32_t prescaler, uint32_t frequency);
    virtual void Poll() = 0;

    static void PollAll();

   private:
    static void Add(Poller* p);
    static void Del(Poller* p);
    static std::vector<Poller*> pollers_;
};

lib/poller/poller.cpp
/**
 ******************************************************************************
 * @file        : poller.cpp
 * @brief       : Poller
 * @author      : Jacques Supcik <jacques.supcik@hefr.ch>
 * @date        : 10. August 2022
 ******************************************************************************
 * @copyright   : Copyright (c) 2022 HEIA-FR / ISC
 *                Haute école d'ingénierie et d'architecture de Fribourg
 *                Informatique et Systèmes de Communication
 * @attention   : SPDX-License-Identifier: MIT OR Apache-2.0
 ******************************************************************************
 * @details
 * Poller
 ******************************************************************************
 */

#include "poller.hpp"

#include <stdio.h>

#include <vector>

#include "assert.h"

std::vector<Poller*> Poller::pollers_;
static TIM_HandleTypeDef htim3;

void callback(TIM_HandleTypeDef* htim) {
    (void)htim;
    Poller::PollAll();
}

/**
 * Initializes the timer used for polling.
 *
 * @param prescaler The prescaler value for the timer.
 * @param frequency The frequency of the timer.
 *
 * @returns HAL_OK if the timer was successfully initialized.
 */
HAL_StatusTypeDef Poller::InitTimer(uint32_t prescaler_freq,
                                    uint32_t frequency) {
    RCC_ClkInitTypeDef clkconfig;
    uint32_t latency;
    HAL_StatusTypeDef status;

    __HAL_RCC_TIM3_CLK_ENABLE();
    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 / prescaler_freq) - 1U);
    assert(prescalerValue <= 0xffff);
    htim3.Instance = TIM3;

    /* Initialize TIMx peripheral as follow:
    + Period = [(TIM3CLK/1000) - 1]. to have a 1 s time base.
    + Prescaler = (uwTimclock/1000 - 1) to have a 1kHz counter clock.
    + ClockDivision = 0
    + Counter direction = Up
    */

    uint32_t p = (prescaler_freq / frequency) - 1U;
    assert(p <= 0xffff);
    htim3.Init.Period            = p;
    htim3.Init.Prescaler         = prescalerValue;
    htim3.Init.ClockDivision     = 0;
    htim3.Init.CounterMode       = TIM_COUNTERMODE_UP;
    htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;

    status = HAL_TIM_Base_Init(&htim3);
    if (status != HAL_OK) {
        return status;
    }
    status = HAL_TIM_RegisterCallback(
        &htim3, HAL_TIM_PERIOD_ELAPSED_CB_ID, callback);
    if (status != HAL_OK) {
        return status;
    }
    /* Start the TIM time Base generation in interrupt mode */
    status = HAL_TIM_Base_Start_IT(&htim3);
    if (status != HAL_OK) {
        return status;
    }
    /* Enable the TIM3 global Interrupt */
    HAL_NVIC_EnableIRQ(TIM3_IRQn);

    return status;
}

/**
 * Adds a poller to the list of pollers.
 *
 * @param p The poller to add.
 *
 * @returns None
 */
void Poller::Add(Poller* p) { pollers_.push_back(p); }

/**
 * Removes a poller from the poller list.
 *
 * @param p The poller to remove.
 *
 * @returns None
 */
void Poller::Del(Poller* p) {
    int len = pollers_.size();
    for (std::vector<Poller*>::iterator it = pollers_.begin();
         it != pollers_.end();) {
        if (*it == p) {
            it = pollers_.erase(it);
        } else {
            ++it;
        }
    }
    assert((int)pollers_.size() == len - 1);
}

/**
 * Polls all registered Pollers.
 *
 * @returns None
 */
void Poller::PollAll() {
    for (Poller* p : pollers_) {
        p->Poll();
    }
}

extern "C" {

/**
 * Interrupt handler for TIM3.
 *
 * @returns None
 */
void TIM3_IRQHandler(void) { HAL_TIM_IRQHandler(&htim3); }

}

Modifiez votre programme principal (main) pour appeler Poller::InitTimer() et laisser le timer faire le nécessaire pour gérer le bouton.

Après avoir initialisé le timer, le programme principal peut maintenant faire autre chose (par exemple faire clignoter une autre LED). S’il ne doit rien faire, il peut alors « tourner dans le vide » :

int main(void)
{
    ...
    while (1) {
        asm volatile("nop");
    }
}

L’instruction asm volatile("nop"); évite que la passe d’optimisation du compilateur supprime cette boucle qu’il pourrait considérer comme inutile.

Mini projet

Écrivez un programme qui utilise le polling périodique pour enclencher et déclencher la LED bleue à chaque pression du bouton central du joystick. Faites clignoter la LED verte pendant toute l’exécution du programme.

À ne pas oublier

Gardez toujours en têtes les bonnes pratiques ainsi que les dix commandements du bon programmeur.

  • Prenez du temps pour la conception sur papier avant de coder.
  • Choisissez de bons noms pour les classes, les méthodes et les variables.
  • Faites des git commit régulièrement avec de bons commentaires, utilisez des branches et faites des merge requests.
  • 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 (tp02-x) avec le nom report.pdf (le chemin complet vers votre rapport est donc /docs/report.pdf)