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 :
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 |
|
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.
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 exempletr_debug
,tr_info
,tr_warning
, ortr_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 :
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.
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
:
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
:
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 exempleIBMPlexMonoBold
)<SIZE>
est la taille de la police en points<BIT>
est soit7
pour une police qui ne contient que les caractères ASCII ou8
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:
#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:
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 :
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 :
#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.
É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 :
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
.