TP01 : Prise en main et thermomètre numérique
Dans ce TP, après une rapide prise en main de la cible, nous réalisons un thermomètre qui affiche la température ambiante sur l’afficheur 7-segments.
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 plusieurs bus de communication (I2C et SPI).
À 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”;
- 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 dans le dépôt git.
Les livrables sont :
- un projet git (tp01) 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é : 4 périodes de 45 minutes en classe + travail personnel à la maison
Délai
Le TPs doit être rendu au plus tard 6 jours après la deuxième séance en classe à 23:59.
Prise en main de l’environnement de développement
Pour les TPs du cours Informatique Embarquée, nous utilisons la même cible que pour les TPs du cours Architecture des ordinateurs et nous utilisons également PlatformIO avec Visual Code Studio. Par contre, nous remplaçons la bibliothèque LibOpenCM3 par le système d’exploitation Mbed OS 6.
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 TP01 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.
“Clonez” le projet sur votre machine.
Attention
- 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.
- Étudiez le fichier
platformio.ini
et notez les différences par rapport à celui du que nous utilisions avec la librairie LibOpenCM3. - Étudiez également les autres fichiers du modèle.
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 comme vous le faisiez avec la librairie LibOpenCM3. Si tout se passe bien, la LED bleue devrait clignoter.
Notez les changements par rapport à LibOpenCM3 :
- Vous n’avez pas besoin de configurer le “clock” principal de la cible; Mbed OS 6 l’a fait pour vous.
- Vous n’avez pas besoin d’enclencher l’horloge du GPIO pour utiliser la LED; là aussi, Mbed OS 6 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 est en C++ et ça 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 calculée à la compilation et vous limitez sa portée.
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.
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.
YYY.cpp
#include "mbed_trace.h"
#if defined(MBED_CONF_MBED_TRACE_ENABLE)
#define TRACE_GROUP "YYY"
#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.
Utilisation du 7-segments
Dans le cours “Architecture des ordinateurs”, vous avez utilisé le 7-segments en manipulant directement les signaux du registre à décalage. Cette technique s’appelle
“bit-banging” et est très simple à réaliser. Nous allons continuer de faire de la sorte pour manipuler les signaux reset
et latch
du 7-segments,
mais pour les données, nous allons utiliser l’interface de communication SPI.
L’interface de communication SPI permet une communication synchrone et full-duplex entre un contrôleur et un périphérique externe. L’interface utilise 4 signaux :
- SCK (Serial ClocK) : le signal d’horloge émis par le contrôleur.
- SDO (Serial Data Out) : le signal de données émis par le contrôleur. Ce signal était parfois nommé MOSI (Master Out Slave In), mais les termes de master ou de slave ne sont plus politiquement corrects. La dénomination COPI (Controller Out Peripheral In) est parfois également utilisée.
- SDI (Serial Data In) : le signal de données reçues par le contrôleur. L’ancien nom était MISO (Master In Slave Out). La dénomination CIPO (Controller In Peripheral Out) est également utilisée.
- CS (Chip Select) : le signal de sélection du périphérique externe. Son ancien nom était SS (Slave Select).
Un contrôleur peut communiquer avec plusieurs périphériques, mais il a besoin d’un signal Chip Select par périphérique. Le microcontrôleur STM32F412 possède 5 contrôleurs SPI qui peuvent communiquer avec une horloge de maximum 50 MHz.
À faire
Consultez la page Wikipedia pour plus d’information sur l’interface SPI et expliquez les concepts de “polarité” et de “phase” d’horloge dans votre journal de travail.
Pour écrire dans le registre à décalage du 7-segments avec cette interface de communication, nous devons nous assurer d’utiliser les bons paramètres (vitesse d’horloge, polarité et phase).
Le code suivant fait clignoter le 7-segments :
int main()
{
SPI display(PA_7, PA_6, PA_5);
DigitalOut brightness(PF_3);
DigitalOut reset(PC_4);
DigitalOut latch(PA_15);
display.format(8, 0);
display.frequency(1000000);
bool state = 0;
brightness = 1;
reset = 1;
latch = 0;
while (true) {
if (state) {
display.write(0xFF);
display.write(0x00);
} else {
display.write(0x00);
display.write(0xFF);
}
latch = 1;
latch = 0;
state = 1 - state;
ThisThread::sleep_for(500ms);
}
}
À faire
Étudiez le code ainsi que l’API de Mbed pour le SPI.
À faire
Implémentez une classe SevenSegments
avec les caractéristiques suivantes :
- On doit pouvoir instancier un objet de
SevenSegments
en précisant le “slot” dans lequel il est installé (gauche ou droite). - On doit pouvoir enclencher ou déclencher l’affichage avec les méthodes
SwitchOn
etSwitchOff
. - On doit pouvoir afficher un entier (
int
) compris entre -9 et 99 avec la méthodePrint
. Si l’entier n’est pas entre -9 et 99, on affiche “–“. Lors de l’affichage de nombres entiers, le point décimal n’est jamais affiché. - On doit également pouvoir afficher un nombre à virgule (
double
) compris entre -9.5 et 99.5 (non compris) avec la méthodePrint
. Le point décimal est toujours présent pour bien montrer que le nombre est à virgule. Voici quelques exemples d’affichage :- -9.5 →
-.-
- -9.4 →
-9.
- -4.8 →
-5.
- 0 →
0.0
- 0.39 →
0.4
- 7.21 →
7.2
- 12.3 →
12.
- 99.43 →
99.
- 99.5 →
-.-
- -9.5 →
- Surchargez l’opérateur d’assignation pour afficher un nombre (entier ou à virgule) avec une simple affectation (
display = 42
). - Implémentez le code dans les fichiers
lib/seven_segments/seven_segments.hpp
etlib/seven_segments/seven_segments.cpp
. - Testez et validez votre code.
Note
Notez qu’avec LibOpenCM3, vous pouviez faire varier l’intensité de l’affichage avec le PWM. Mbed OS permet aussi de simplement contrôler un
PWM, mais le timer pour le PWM du signal utilisé pour la luminosité (TIM5
) est réservé pour d’autres fonctionnalités de Mbed OS.
C’est le prix à payer pour bénéficier des avantages d’un système tel que MBed OS; les auteurs font parfois des choix qui ne sont
pas compatibles avec les spécificités d’un système donné.
Important
Respectez les règles de style imposées pour ce cours!
Mesure et lecture de la température
Pour mesurer la température, nous utilisons une nouvelle “Click-Board” avec le capteur de température TMP102
. 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 et et 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.
Voici les opérations en pseudo-code qui permettent de lire la température du capteur :
- Créer un instance de la classe `I2C` avec les pins corrects de la cible
- Configurer la fréquence (400 KHz)
- Ecrire le numéro du registre du thermomètre sur le bus I2C (méthode `write`) :
- Utiliser l'adresse correspondante au module TMP102 (lire la documentation
technique du TMP102)
- Ecrire le numéro correspondant au registre de température sur le bus I2C
(lire la documentation du TMP102)
- Lire la température sur le bus I2C (méthode `read`):
Utiliser l'adresse correspondante au module TMP102
Lire les 16 bits de données du registre de température
- Convertir les 16 bits de données en un `double` qui représente la température
en °C (lire la documentation du TMP102)
À faire
Implémentez une classe ClickThermo
avec les caractéristiques suivantes :
- On doit pouvoir instancier un objet de
ClickThermo
. - On doit pouvoir lire la température avec la méthode
GetTemperature
qui retourne undouble
représentant la température en °C. - On doit également pouvoir lire la température en convertissant l’objet en
double
(surcharge de l’opérateurdouble
). - Implémentez le code dans les fichiers
lib/click_thermo/click_thermo.hpp
etlib/click_thermo/click_thermo.cpp
. - Testez et validez votre code.
Autres interfaces de communications
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 le module d’affichage sur le 7-segments et le module de lecture de température pour créer un mini-projet qui affiche la température ambiante. Votre programme doit ressembler à ça :
// TODO : Add header
#include "mbed.h"
#include "mbed_trace.h"
#include "click_thermo.hpp"
#include "seven_segments.hpp"
#include ...// TODO (add include if needed)
#define TRACE_GROUP "MAIN"
int main()
{
mbed_trace_init();
tr_info("Starting program");
SevenSegments display{...}; // TODO : LEFT or RIGHT slot
ClickThermo thermo;
while (true) {
double t = thermo;
tr_info("Temperature: %f", t);
display = t;
ThisThread::sleep_for(500ms);
}
}
- Si vous voulez, vous pouvez aussi faire clignoter la LED bleue lors de chaque mesure de température.
- Vérifier avec la console (PlatformIO Serial Monitor) que la température soit plausible.
- 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.
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-thermo
script:
- pio test -e DISCO_F --test-port /dev/ttyACM0
Grâce au tag embsys-thermo
, vous envoyez votre code sur un des 6 runners équipé du capteur Thermo 3.
À 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 des 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) avec les indications suivantes :
- Une page de titre avec au minimum :
- le nom et le logo officiel de l’école
- le nom du cours : Informatique Embarquée
- le titre de votre document : Travail Pratique 1 : Prise en main et thermomètre numérique
- 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.
- Le 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
Déposez votre rapport dans un dossier /docs
de votre dépôt git (tp01) avec le nom report01.pdf
(le chemin complet vers votre
rapport est donc /docs/report01.pdf
)