Aller au contenu

TP01 : Prise en main et station météo

Dans ce TP, après une rapide prise en main de la cible, nous réalisons une station météo qui affiche différentes valeurs environnementales sur l’écran de la cible.

Photo par Kaffeebart sur Unsplash

Objectifs du TP

Ce TP a pour but de vous familiariser avec l’environnement de développement et de réaliser un premier projet qui met en œuvre le bus de communication I2C.

À la fin de ce TP, les étudiants :

  • auront installé leur PC pour réaliser les TPs;
  • sauront utiliser les outils de développement;
  • connaîtront les règles de style à appliquer pour les TPs;
  • auront pris connaissance du système d’exploitation Mbed OS;
  • sauront utiliser le CI/CD de gitlab pour vérifier le code;
  • auront réalisé un test unitaire qui tourne sur une cible;
  • auront mis en œuvre le concept de programmation orientée objet et auront implémenté des classes en C++;
  • connaîtront les différents bus de communications pour les périphériques externes (I2C, SPI, UART);
  • auront rédigé un journal de travail et déposé le PDF sur Teams.

Les livrables sont :

  • un projet git nommé “weather_station” dans votre groupe sur gitlab.forge.hefr.ch avec le code du TP et le code du programme de test;
  • un release avec le tag “tp01” doit être créé sur votre dépôt.
  • une configuration CI/CD de gitlab pour valider votre TP;
  • un rapport de travail déposé sur Teams.

Temps accordé : 4 périodes de 45 minutes en classe + travail personnel à la maison

Délai

Les dates de rendu des TPs sont indiquées sur le plan de cours.

Prise en main de l’environnement de développement

Pour les TPs du cours Module Acquisition et traitement de données : Internet des Objets, nous utilisons la même cible que pour les TPs du cours Architecture des ordinateurs. Nous utilisons PlatformIO avec Visual Code Studio. Par contre, nous remplaçons la bibliothèque STM32CubeF4 par le système d’exploitation Mbed OS.

Pour faire connaissance avec ce système d’exploitation, commençons par implémenter le fameux “Hello World” de l’embarqué : faire clignoter une LED!

Dans votre groupe, créez un nouveau projet weather_station sur la base du modèle mis à disposition dans le groupe embsys :

template

Attention

  • Créez le projet dans votre groupe de travail et pas dans le groupe des templates.
  • Evitez d’avoir des espaces, des lettres accentuées ou des caractères spéciaux dans le path du projet.
  • Assurez-vous d’avoir au moins 3GB de libre sur votre disque dur / ssd.

“Clonez” le projet sur votre machine, puis étudiez le fichier platformio.ini et comprenez les différentes sections de ce fichier.

Pour faire clignoter la LED bleue, remplacez le fichier src/main.cpp par le code ci-dessous :

#include "mbed.h"

int main()
{
    DigitalOut led(LED4);
    while (true) {
        led = !led;
        ThisThread::sleep_for(500ms);
    }
}

Compilez le code et programmez la cible. Si tout se passe bien, la LED bleue devrait clignoter. Si cela n’est pas le cas, vous devez trouver et corriger le problème avant de poursuivre.

Notez les points importants suivants :

  • Vous n’avez pas besoin de configurer le “clock” principal de la cible; Mbed OS l’a fait pour vous.
  • Vous n’avez pas besoin d’enclencher l’horloge du GPIO pour utiliser la LED; là aussi, Mbed OS l’a fait pour vous.
  • Vous n’utilisez plus le GPIO avec des fonctions qui utilisent des ports et des pins, mais vous utilisez un objet de type DigitalOut pour contrôler la LED.
  • Vous pouvez mettre en pause votre programme avec ThisThread::sleep_for sans avoir eu à configurer le systick ni les timers.

Vous constaterez que l’API de Mbed OS est en C++, ce qui permet une grande simplicité d’utilisation. Observez en particulier l’instruction led = !led. Pour réaliser cette abstraction, Mbed OS fait appel à la surcharge d’opérateur disponible en C++ :

1
2
3
4
5
6
7
8
class DigitalOut {

public:
    ...
    DigitalOut &operator= (int value)...
    operator int() ...
    ...
};

La ligne 5 du code ci-dessus surcharge l’opérateur d’assignation (=) ce qui permet de contrôler la LED en faisant led = 0 ou led = 1. Notez que la construction de la cible est telle que led = 0 allume la LED alors que led = 1 l’éteint. Ceci est dû au fait que le courant est “puisé” par le GPIO et non “fourni” par le GPIO.

LED

La bonne pratique consiste à définir des constantes (avec constexpr) pour représenter les états de la LED :

static constexpr int kLedOn  = 0;
static constexpr int kLedOff = 1;

Note

Pour les constantes, utilisez de préférence les préfixes static et constexpr. Vous vous assurez ainsi que la constante est sauvegardée dans les variables statiques et calculée à la compilation.

La ligne 6 surcharge l’opérateur de conversion en entier (int) ce qui permet de lire un GPIO en faisant int v = led.

l’instruction led = !led de notre code combine ces deux opérateurs et ajoute l’opérateur logique (!) qui inverse la valeur de l’argument pour simplement inverser la valeur de la LED.

À faire

Étudiez l’API de DigitalOut et familiarisez-vous avec les fonctions disponibles.

Rappel sur les règles de style à adopter

Important

Respectez les règles de style imposées pour ce cours!

Logging avec les printf

Le debugging dans le développement d’applications pour les systèmes embarqués est une part importante du processus de développement, comme pour tout développement d’applications. Sur les systèmes embarqués, les fonctionnalités de debugging sont souvent un peu plus limitées en comparaison d’autres systèmes comme les applications web ou mobiles. Dans ce sens, les capacités de logging c’est-à-dire d’affichage de messages (souvent sur la console) sont importantes. Il est donc important que vous compreniez les outils de logging mis à disposition par Mbed OS.

Une bonne introduction à l’utilisation pour le logging avec printf et des macros associées est donnée sur Debugging using printf. Le principe de base des macros est de catégoriser les instructions de logging par niveau de sévérité, comme les niveaux DEBUG, WARNING ou ERROR. Pour ce faire, le système doit :

  • Définir les niveaux de sévérité dans la définition des macros.
  • Spécifier le niveau de sévérité à utiliser pour la compilation et l’exécution du programme.

Les macros utilisent ainsi le niveau de sévérité dans des conditions #if, qui enclenchent ou non l’affichage et contrôlent le format de l’information qui sera affichée sur la console. Cela permet au développeur de complètement contrôler les informations qui seront affichées sur la console au moment de l’exécution du programme.

Le mécanisme décrit ci-dessus est réalisé dans la librairie mbed-trace. Cette librairie est intégrée dans la librairie Mbed OS et, afin de pouvoir l’utiliser, le développeur doit :

  • Activer ou déactiver le logging pour l’application (dans le fichier “mbed_app.json” qui définit l’ensemble des paramètres de configuration de l’application).
  • Définir le niveau de logging requis pour compiler l’application (dans le même fichier).

mbed_app.json

"config": {    
    "trace-level": {
    "help": "Options are TRACE_LEVEL_ERROR,TRACE_LEVEL_WARN,TRACE_LEVEL_INFO,TRACE_LEVEL_DEBUG",
    "macro_name": "MBED_TRACE_MAX_LEVEL",
    "value": "TRACE_LEVEL_INFO"
    }
"target_overrides": {
    "*": {
      "mbed-trace.enable": false,
      "trace-level": "TRACE_LEVEL_DEBUG"
   }
}
  • Inclure le fichier mbed_trace.h dans tout fichier qui utilise le logging.
  • Définir également un symbole TRACE_GROUP dans tout fichier qui utilise le logging afin de permettre à l’utilisateur d’identifier l’origine du logging.

main.cpp

#include "mbed_trace.h"
#if defined(MBED_CONF_MBED_TRACE_ENABLE)
#define TRACE_GROUP  "main"
#endif // MBED_CONF_MBED_TRACE_ENABLE
  • Initialiser la librairie dans la fonction main

main.cpp

int main() {
   ...
#if defined(MBED_CONF_MBED_TRACE_ENABLE)
   mbed_trace_init();
#endif
   ...
}
  • Lorsque le développeur souhaite afficher un message de logging, il peut ensuite utiliser les macros tr_<level> dans son code (par exemple tr_debug, tr_info, tr_warning, or tr_error).
  • Le développeur peut également choisir la routine de logging à utiliser pour son programme (par défaut printf) et filtrer les messages affichés au moment de l’exécution en appliquant des filtres de groupe (voir mbed-trace pour plus de détails).

En conclusion, l’utilisation du logging pour le debugging est très importante pour le développement de logiciels embarqués. L’utilisation de la librairie mbed-trace offre toute la flexibilité requise afin de déterminer comment le logging doit être activé et configuré au moment de la compilation et comment les messages peuvent être filtrés au moment de l’exécution. Vous devez donc réaliser le logging nécessaire dans vos applications en utilisant cette librairie.

Lecture et affichage des données environnementales

Pour mesurer la température, nous utilisons une nouvelle “Click-Board” avec le capteur de température, d’humidité et de pression atmosphérique BME280. Vous trouverez la documentation technique pour ce capteur au chapitre Documentation/Datasheets de ce site.

Configurez votre cible comme illustré ci-dessous :

Hardware

Installation du capteur "WeatherClick/BME280"

Ce capteur utilise le bus I2C (Inter-Integrated Circuit). Il s’agit d’un protocole inventé par Philips Semiconductors en 1982 et qui est très utilisé dans les systèmes embarqués. Le bus I2C n’a besoin que de deux fils (en plus de la masse) pour communiquer. Sur le bus, chaque périphérique est identifié par une adresse unique transmise également sur ces deux fils. Prenez le temps de parcourir la page Wikipedia pour plus de détails sur le protocole I2C.

I2C

Questions

  • Quelle est la vitesse de transmission du bus I2C ?
  • Quels sont les avantages et les inconvénients du bus I2C par rapport au bus SPI ?
  • Est-ce que le bus I2C est synchrone ou asynchrone ?
  • Est-ce que le bus I2C fonctionne en “simplex”, “full-duplex” ou en “half-duplex” ?

Mbed OS nous permet d’utiliser le bus I2C avec la classe I2C.

Pilote pour le Weather Click

Pour le pilote du capteur Weather Click, nous avons adapté la bibliothèque BME280. Afin de pouvoir utiliser ce pilote modifié dans votre projet, ajoutez manuellement la ligne https://github.com/heia-fr/embsys-bme280.git# suivi de la version souhaitée à votre fichier platformio.ini. La version actuelle est la v0.1.2 :

platformio.ini
lib_deps =
    https://github.com/heia-fr/embsys-target-disco-heiafr.git#v0.4.0    
    https://github.com/heia-fr/embsys-bme280.git#v0.1.2

Attention

La bibliothèque “BME280” n’est pas “thread safe”.

Intégration du Weather Click dans votre application

Afin d’obtenir les données de capteurs dans votre application, il suffit d’instancier un objet de la classe BME280, selon le code suivant :

// other includes

#include "bme280.hpp"

... 

int main()
{
    ...
    // initialize WeatherClick
    static constexpr PinName SCL = ...;
    static constexpr PinName SDA = ...;
    static constexpr char slaveAddress = ...;

    BME280 bme280(SDA, SCL, slaveAddress);
    if (bme280.initialize()) {
        tr_debug("WeatherClick initialized properly");
    }
    ...

Afin d’initialiser les constantes SCL, SDA et slaveAddress correctement, vous devez consulter les datasheets correspondantes. Après avoir initialisé l’instance de BME280 correctement, vous pouvez lire les valeurs et les afficher en utilisant une macro tr_debug ou tr_info.

Pilote pour l’écran LCD

Pour le pilote de l’écran LCD, nous utilisons la même base que pour le cours “Architecture des ordinateurs, à savoir la bibliothèque GFX de Adafruit. Nous avons adapté cette bibliothèque à l’écran de la cible ainsi qu’a Mbed OS. Pour installer ce pilote, ajoutez manuellement la ligne https://github.com/heia-fr/embsys-disco-gfx.git# suivi de la version souhaitée à votre fichier platformio.ini. La version actuelle est la v0.1.2 :

platformio.ini
lib_deps =
    https://github.com/heia-fr/embsys-target-disco-heiafr.git#v0.4.0
    https://github.com/heia-fr/embsys-disco-gfx.git#v0.1.2

Attention

La bibliothèque “GFX” n’est pas “thread safe”.

Le pilote est fourni avec beaucoup de polices d’écriture. Le nom du fichier de la police est structuré comme ceci:

<FONT-NAME><SIZE>pt<BITS>b.h
  • <FONT-NAME> est le nom de la police sans espace (par exemple IBMPlexMonoBold)
  • <SIZE> est la taille de la police en points
  • <BIT> est soit 7 pour une police qui ne contient que les caractères ASCII ou 8 pour une police avec les caractères latin-1 (ou ISO 8859-1).

Avec la bibliothèque GFX de Adafruit, la taille totale d’une police ne peut pas dépasser 64KiByte. C’est pourquoi certaines grosses polices ne sont disponibles qu’en ASCII.

Voici un petit programme qui démontre les fonctionnalités de base du pilote:

demo_lcd.cpp
#include "Fonts/IBMPlexSansBold24pt8b.h"
#include "Fonts/IBMPlexSansMedium12pt8b.h"

#include "disco_lcd_gfx.h"
#include "mbed.h"

void printCenter(DiscoLcdGFX& lcd, const char* text, int16_t x, int16_t y)
{
    int16_t x1, y1;
    uint16_t w, h;
    lcd.getTextBounds(text, 0, 0, &x1, &y1, &w, &h);
    lcd.setCursor(x - w / 2, y);
    lcd.write(text);
}

int main()
{
    DiscoLcdGFX lcd;
    lcd.fillScreen(LCD_COLOR_BLACK);

    lcd.setFont(&IBMPlexSansBold24pt8b);
    lcd.fillCircle(150, 70, 60, LCD_COLOR_DARKRED);
    for (int i = 5; i < lcd.height(); i+=25) {
        lcd.drawLine(0, i, lcd.width(), i, LCD_COLOR_DARKBLUE);
        lcd.drawLine(i, 0, i, lcd.width(), LCD_COLOR_DARKBLUE);
    }
    lcd.setTextColor(LCD_COLOR_YELLOW);
    printCenter(lcd, "36.5\260C", lcd.width() / 2, 60);

    lcd.setFont(&IBMPlexSansMedium12pt8b);
    lcd.setTextColor(LCD_COLOR_WHITE);
    printCenter(lcd, "Haute \351cole", lcd.width() / 2, 100);
    printCenter(lcd, "d'ing\351nierie", lcd.width() / 2, 125);
    printCenter(lcd, "et d'architecture", lcd.width() / 2, 150);
    printCenter(lcd, "de Fribourg", lcd.width() / 2, 175);

    while (true) {
        ThisThread::sleep_for(Kernel::wait_for_u32_forever);
    }
}

Et le résultat obtenu:

Démo de l’écran

Classe pour l’écran de la station météo

L’illustration suivante montre le genre d’affichage que nous souhaitons réaliser sur notre application :

Affichage des valeurs

Réalisez une classe nommée LCDDisplay (dans le dossier “lib/lcd_display” de votre projet) qui se charge d’afficher les bons éléments aux bons endroits. La déclaration de la classe que vous devez réaliser est donnée ci-dessous :

lib/lcd_display/lcd_display.hpp
#pragma once

#include <mbed.h>

#include "disco_lcd_gfx.h"

class LCDDisplay {
public:
    // The constructor allocates a new screen and initialises it.
    LCDDisplay();

    // Initialize the screen and print labels.
    void init();

    // Delete the previous value and print the new temperature
    // at the correct position.
    void printTemperature(double t);

    // Delete the previous value and print the new humidity
    // at the correct position.
    void printHumidity(double h);

    // Delete the previous value and print the new preasure
    // at the correct position.
    void printPressure(double p);

private:       
    // TODO: add methods and attributes if needed

    // Set the font used for the next print operations
    void setFont(const GFXfont *f);      

    // Print text centered at position (x,y)
    void printCenter(const char* text, int16_t x, int16_t y);

    // Display a filled rectangle with clearColor covering a text
    // centered at position (x,y).
    // Use the bounding box ot the text to compute the size and the
    // position of the rectangle.
    void clearCenterText(
        const char* text,
        int16_t x, int16_t y,
        uint16_t clearColor = LCD_COLOR_BLACK);

    DiscoLcdGFX _lcd;
};

Si cela est nécessaire, vous pouvez adapter ou modifier cette déclaration. Vous devez réaliser la classe dans le fichier “lcd_display.cpp”.

Autres interfaces de communication

En plus du SPI et du I2C, les systèmes embarqués utilisent également des interfaces de communication comme le UART ou le CAN.

L’interface UART (Universal asynchronous receiver-transmitter) est un protocole de communication asynchrone (comme son nom l’indique) qui permet de communiquer sur de plus longues distances, mais avec un débit de données plus limité. Cette interface est souvent utilisée pour communiquer avec des modems, des modules WiFi ou des récepteurs GPS.

UART

Étudiez les caractéristiques de l’interface UART sur Wikipedia et répondez aux questions suivantes :

Questions

  • Quelle distance peut-on atteindre avec cette interface si on utilise la couche physique RS-485 ?
  • Quels sont les avantages et les inconvénients l’interface UART par rapport au bus I2C et SPI ?
  • Est-ce que l’interface UART fonctionne en “simplex”, “full-duplex” ou en “half-duplex” ?
  • Quelle classe de Mbed OS permet d’utilise l’interface UART ?

Mini projet

Combinez maintenant la lecture des données environnementales et le module d’affichage sur l’écran (classe LCDDisplay) pour créer un mini-projet qui affiche les données environnemtales. Votre programme devrait ressembler à ça :

// TODO : MODIFY HEADER based on template

#include "mbed.h"

#include "bme280.hpp"
#include "lcd_display.hpp"

#include "mbed_trace.h"
#if defined(MBED_CONF_MBED_TRACE_ENABLE)
#define TRACE_GROUP  "main"
#endif // MBED_CONF_MBED_TRACE_ENABLE

int main()
{
#if defined(MBED_CONF_MBED_TRACE_ENABLE)
    mbed_trace_init();
#endif
    tr_debug("Program started");

    // create and initialize the WeatherClick
    static constexpr PinName SCL = ...;
    static constexpr PinName SDA = ...;
    static constexpr char slaveAddress = ...;

    BME280 bme280(SDA, SCL, slaveAddress);
    if (bme280.initialize()) {
        tr_debug("WeatherClick initialized properly");
    }

    // create the LCD Display
    LCDDisplay lcd;

    // define refresh interval
    static constexpr std::chrono::milliseconds refreshInterval = 5000ms;

    DigitalOut led(LED4);
    while (true) {
        led = !led;

        // update LCD display with environmental data
        ...

        ThisThread::sleep_for(refreshInterval);
    }
}
  • Si vous voulez, vous pouvez aussi faire clignoter la LED bleue lors de chaque mesure de température comme dans le code ci-dessus.
  • Vérifier que les valeurs affichées sur l’écran LCD sont plausibles.
  • Vérifier avec la console (PlatformIO Serial Monitor ou un autre serial monitor) que les messages de debug s’affichent correctement.
  • Touchez le capteur avec votre doigt pour voir si la température change.

Tests et validations

Comme vous l’avez appris au cours “Architecture des ordinateurs”, implémentez des tests unitaires pour vérifier que votre code fonctionne correctement. Vous pouvez simplement vérifier que la température ambiante soit “plausible” (par exemple entre 10 et 40°C) et que lors de deux mesures consécutives, la différence de température ne soit pas trop importante. Le test doit effectuer des mesures pendant 5 minutes à un intervalle de 10 secondes.

Vous avez déjà implémenté des tests unitaires lors des travaux pratiques du cours “Architecture des ordinateurs”. Vous trouverez un rappel du fonctionnement des tests unitaires de PlatformIO sur la page Unit Testing.

Implémentez également le CI/CD et configurer votre projet pour que les tests unitaires tournent automatiquement dans l’infrastructure CI/CD de l’école. Pour ce faire, ajoutez un fichier .gitlab-ci.yml comme ceci à votre projet :

.gitlab-ci.yml
image: python:3.9

variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
  PLATFORMIO_CACHE_DIR: "$CI_PROJECT_DIR/.platformio-cache"

cache:
  paths:
    - .pip-cache/
    - .platformio-cache/

stages:
 - check
 - test

before_script:
  - pip install -U platformio

check-job:
  stage: check
  script:
    - pio run -e DISCO_F
    - pio check -e DISCO_F --skip-packages
    - pio test --without-uploading --without-testing

test-job:
  stage: test
  tags:
    - embsys-bme
  script:
    - pio test -e DISCO_F --test-port /dev/ttyACM0

Grâce au tag embsys-bme, vous envoyez votre code sur un des 6 runners équipé du capteur WeatherClick.

À ne pas oublier

  • 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 des tests unitaires.
  • Utilisez des assertions dans votre code pour le documenter et le rendre plus robuste.

Rapport de travail

  • Rédigez un rapport (journal de travail) avec les indications suivantes :
  • Une page de titre avec au minimum :
    • le nom et le logo officiel de l’école
    • le nom du cours : Module Acquisition et traitement de données : Internet des Objets
    • le titre de votre document : Travail Pratique 1 : Prise en main et station météo
    • le numéro de votre groupe
    • les noms des auteurs (vous) avec la classe dans laquelle vous êtes
    • la date à laquelle vous avez terminé le rapport
    • éventuellement la version du rapport
  • Une introduction pour poser le contexte
  • Un résumé des notions que vous avez apprises pendant ce TP en précisant si c’est
    • non acquis
    • acquis, mais encore à exercer
    • parfaitement acquis
  • Un résumé des points qui vous semblent importants et que vous devez retenir
  • Les réponses aux questions.
  • Si des explications sur le code sont demandées ou nécessaires, les parties du code source bien formaté et avec du “syntax highlighting” de votre code source.
  • Une conclusion par laquelle vous donnez vos impressions sur le TP, ce que vous avez aimé, ce que vous avez moins aimé, et éventuellement des suggestions pour des changements. Indiquez également le nombre d’heures que vous avez passées, par personne, en dehors des heures de TP en classe.

Important

Vous utiliserez le même dépôt pour le rendu de tous vos travaux pratiques. Lors des corrections des travaux pratiques, si des défauts sont identifiés dans votre solution, ceux-ci seront signalés sous forme d‘“issues” et vous devrez les corriger lors du travail pratique suivant.

Déposez votre rapport dans le devoir correspondant sur Teams, avec le nom report01.pdf.