Il peut arriver que tous les objets d’une même classe aient envie de partager une information de telle sorte que lorsqu’un des
objets modifie cette information, les autres en soient automatiquement avertis. Cette information mise en commun est également
représentée par un attribut, mais qui possède la particularité d’être un attribut de la classe même valeur pour tous les
objets et non plus un attribut d’un objet puisque chaque objet possède normalement sa propre valeur.
Entrées sorties tout ou rien sur pcDuino - GPIO
Dans ce chapitre nous allons nous consacrer à l'étude des entrées sorties digitales de la carte pcDuino. Dans cette
première étape, notre intention sera de récupérer une impulsion provoquée par un bouton poussoir et d'activer une sortie de la
carte afin qu'une led puisse s'éclairer. Vous voyez que notre démarche est très modeste. L'objectif est, comme les projets
précédents, de créer une classe qui va encapsuler toute la démarche nécessaire pour que l'utilisation des entrées-sorties tout ou
rien de la carte pcDuino soit extrêmement simple à utiliser. La mise en place sera ainsi intuitive et très rapide à
expérimenter.
Mise en oeuvre du codage
Dans un premier temps, vous pouvez réaliser des expériences afin de bien maîtriser cette notion de fichiers et de flot. Une fois
que vous aurez compris tous les mécanisme en jeu, vous pouvez coder votre classe GPIO et respecter l'algorithme prévu pour
élaborer votre projet.
#include <iostream>
#include <unistd.h>
using namespace std;
#include "gpio.h"
int main()
{
GPIO bouton(Entree, 6);
GPIO led(Sortie, 3);
led.activer(true);
while (!bouton.etat()) usleep(100000);
return 0;
}
Conclusion sur la programmation orientée objet
Dans le fichier principal, vous remarquez que cette classe GPIO est très très simple à utiliser. Il suffit de déclarer
les objets relatifs avec des noms évocateurs comme bouton et led. Toutes les phases délicates se situent
dans le constructeur et le destructeur, mais ces deux méthodes particulières ne se voient pas
puisqu'elles sont appelées implicitement. Du coup le code correspondant à l'algorithme de fonctionnement est très très court et
rapide à construire. Je me répète, mais la programmation modulaire comme l'est la programmation orientée objet nous donne à la fois
une conception intuitive et très rapide à mettre en oeuvre. Cela facilite le développement d'applications.
Gestion d'éclairage d'une pièce
Dans ce chapitre, nous allons juste utiliser les compétences que nous venons d'acquérir pour le pilotage de ces entrées-sorties tout
ou rien. Différents scenarii seront proposer sans que nous ayions besoins de restructurer la classe GPIO. Tout porte sur la
gestion d'un éclairage d'une pièce. Toutefois, nous profiterons de cette étude pour mettre en oeuvre des processus multi-thread.
Utilisation d'un télérupteur
Pour cet éclairage, nous avons besoin cette fois-ci de deux boutons poussoirs. Le premier permet successivement d'allumer et
d'éteindre la lumière à chaque appui, à l'image d'un télérupteur. Le deuxième bouton, comme précédemment, nous fait quitter
le programme.
Diagramme de déploiement
L'ossature générale du diagramme de déploiement est très similaire au précédent. Juste un élément physique est rajouté pour la
partie télérupteur. Les composants logiciels demeurent les mêmes. Il faut juste changer le scénario dans le fichier main.cpp
pour respecter ce cas d'utilisation.
Diagramme des classes
La classe que nous avons élaborée précédemment reste d'actualité. Nous avons passé du temps à concevoir cette classe GPIO.
Maintenant, comme nous l'avons évoqué précédemment, il est très facile de changer de scénario pour s'adapter à cette nouvelle
situation. Grâce à la programmation objet, nous pouvons concevoir du code très rapidement avec une fonctionnalité performante.
Lorsque nous devons mettre au point des systèmes embarqués avec une gestion des entrées sorties, il est souvent souhaitable
d'intégrer la notion de threads. Je rappelle que les threads permettent d'avoir un système multi-tâches à l'intérieur d'un
même processus programme, c'est-à-dire que plusieurs tâches s'exécutent simultanément. L'intérêt de cette pratique
est de pouvoir séparer les traitements relatifs à la capture des informations venant des entrées fréquence de capture assez
élevé et le traitement associé pour générer les informations utiles pour activer les sorties fréquence de
rafraîchissement beaucoup plus lente.
Utilisation d'un va et vient
Pour cette application, les deux poussoirs sont maintenant utilisés pour la gestion de l'éclairage. Il est donc nécessaire de
prévoir un élément supplémentaire afin de pouvoir quitter l'application proprement sans passer par une intervention brutale
de l'extérieur à l'aide de la commande kill. C'est pour cela que nous prévoyons l'utilisation du clavier, et plus
précisément la touche . Vu ces conditions, nous sommes obligés de passer par une programmation concurrente
qui utilise les threads.
Diagramme de déploiement
L'ossature générale du diagramme de déploiement est très similaire au précédent. Juste un élément physique est rajouté pour la
partie télérupteur. Les composants logiciels demeurent les mêmes. Il faut juste changer le scénario dans le fichier main.cpp
pour respecter ce cas d'utilisation.
Diagramme des classes
La classe que nous avons élaborée précédemment reste d'actualité. Nous avons passé du temps à concevoir cette classe GPIO.
Maintenant, comme nous l'avons évoqué précédemment, il est très facile de changer de scénario pour s'adapter à cette nouvelle
situation. Grâce à la programmation objet, nous pouvons concevoir du code très rapidement avec une fonctionnalité performante.
La pratique des threads en C++11
Depuis C++11, il est possible d'implémenter le multi-tâches à l'aide de la classe thread. Lorsque vous devez créer
une nouvelle tâche à partir de cette classe, vous devez spécifier, soit une fonction, soit un foncteur ou
soit une expression lambda qui sera automatiquement exécutée lors de la création de l'objet correspondant. La fin de la
tâche correspondra tout simplement à la fin de la fonction de rappel ou foncteur ou lambda.Lorsque le
programme principal lance plusieurs threads, il est souhaitable qu'il attende que chacune des tâches soient terminées avant de
clôturer définitivement le programme. Pour savoir si une tâche est bien terminée, il suffit de faire appel à la méthode join()
de la classe thread. C'est une méthode qui bloque l'exécution mise en attente du programme principal
jusqu'à que la tâche correspondante soit effectivement terminée.Lorsqu'un système possède plusieurs threads,
il faut éviter que chacune des tâches soit énergivore en terme d'utilisation du processeur. Dès que la partie de code souhaitée
par une tâche est exécutée, il est tout de suite nécessaire de mettre en attente le thread correspondant grâce à la fonction sleep_for()
afin que les autres tâches puissent avoir la main le plus rapidement possible, afin d'augmenter la notion de
simultanéité.
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
using namespace std::this_thread;
using namespace std::chrono;
#include "gpio.h"
bool fin = false; // Variable globale
void VaEtVient()
{
GPIO BP8(Entree, 8);
GPIO BP9(Entree, 9);
GPIO lampe(Sortie, 3);
milliseconds attente(100);
bool etatPrecedent = false;
while (!fin)
{
if ((BP8.etat() || BP9.etat()) && !etatPrecedent) lampe.activer(!lampe.etat());
etatPrecedent = BP8.etat() || BP9.etat();
sleep_for(attente);
}
}
int main()
{
thread eclairage(VaEtVient);
cout << "Programme d'éclairage en fonctionnement..." << endl;
cin.get(); // Appui sur la touche "Entrée"
fin = true;
cout << "Demande d'interruption du programme" << endl;
eclairage.join();
return 0;
}
Allumage automatique d'une pièce en fonction du nombre de personnes présentes
Nous allons profiter de ces nouvelles compétences pour que notre système embarqué soit capable de maîtriser l'allumage automatique
d'une pièce. Pour que cela fonctionne de façon optimisée, deux capteurs seront placés côte à côte au niveau de la porte pour
contrôler le passage des personnes. Deux capteurs sont nécessaires pour vérifier la direction que prend chacune de ces personnes,
soit elles rentrent dans la pièce, soit elles en sortent. Nous pouvons ainsi à tout moment comptabiliser le nombre de personnes
présentes. Du moment qu'il existe au moins une personne dans la pièce, la pièce est alors éclairée.
Allumage automatique d'une pièce
Pour cette application, les deux poussoirs représentent maintenant les deux capteurs fixés sur le chambranle de la porte.
Diagramme de déploiement
Les composants logiciels demeurent les mêmes. Il faut juste changer le scénario dans le fichier main.cpp pour respecter ce
cas d'utilisation.
Diagramme des classes
La classe que nous avons élaborée précédemment reste d'actualité. Nous avons passé du temps à concevoir cette classe GPIO.
Maintenant, comme nous l'avons évoqué précédemment, il est très facile de changer de scénario pour s'adapter à cette nouvelle
situation. Grâce à la programmation objet, nous pouvons concevoir du code très rapidement avec une fonctionnalité performante.
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
using namespace std::this_thread;
using namespace std::chrono;
#include "gpio.h"
bool fin = false; // Variable globale
void detecteurs() // Fonction utilisée par la tâche d'éclairage
{
GPIO exterieur(Entree, 8);
GPIO interieur(Entree, 9);
GPIO lampe(Sortie, 3);
milliseconds attente(100);
int compteur = 0;
enum CapteurPorte {Inactif, Exterieur, Seuil, Interieur};
CapteurPorte avant=Inactif, maintenant;
while (!fin)
{
// Mesure l'état des capteurs de porte
bool ex = exterieur.etat();
bool in = interieur.etat();
// Valeur de maintenant
if (ex && !in) maintenant = Exterieur;
else if (ex && in) maintenant = Seuil;
else if (!ex && in) maintenant = Interieur;
else maintenant = Inactif;
// Comptage ou décomptage si une personne rentre ou sort de la pièce
if (avant==Seuil && maintenant==Interieur) { cout << "Compteur = " << ++compteur << endl; }
if (avant==Seuil && maintenant==Exterieur) { cout << "Compteur = " << --compteur << endl; }
// Gestion de l'éclairage
lampe.activer(compteur>0);
sleep_for(attente);
avant = maintenant;
}
}
int main()
{
thread eclairage(detecteurs);
cout << "Programme d'éclairage en fonctionnement..." << endl;
cin.get(); // Attente de l'appui sur la touche "Entrée" du clavier
fin = true;
cout << "Demande d'interruption du programme..." << endl;
eclairage.join();
return 0;
}
Entrées analogiques - Capteur de température
Maintenant que nous connaissons bien les entrées-sorties numériques, nous allons nous intéresser cette fois-ci aux entrées
analogiques. Dans le projet qui va suivre, nous connecterons un capteur dont la tension délivrée est proportionnelle à la
température mesuré, à raison de 10mv par degré celcius. Ce capteur est capable de mesurer des températures de 0°
à 100° celcius. Nous effectuerons deux mesures, une au lancement du programme, et une deuxième lorsque l'utilisateur
décidera de quitter l'application.
Caractéristiques des broches E/S numériques et analogiques du pcDuino
GPIO : 18 E/S en 3.3V.
Entrées Analogiques : 2 x 6 bits en 0-2V et 4 x 12 bits en 0-3.3V.
Sorties analogiques : 6 x PWM 2 broches « fast » en 520Hz – 8 bits et 4 broches « slow » en 5Hz – 20
niveaux.
Communication Sérielles : I2C, SPI, UART.
Nous retrouvons la plupart des possibilités de l'Arduino à quelques adaptation près.
Câblage du capteur sur une des entrées analogiquesDiagramme des cas d'utilisation
Nous conservons le projet précédent auquel nous rajoutons toutes les fonctionnalités liées à la mesure de la température.
Diagramme de déploiement
Dans ce diagramme, vous voyez que nous conservons les éléments du projet précédent auquel nous rajoutons notre capteur de
température sur la broche 2 de la connexion analogique. Nous prenons cette entrée puisqu'elle possède une résolution sur 12
bits les deux premières entrées ADC0 et ADC1 sont seulement sur 6 bits. Par ailleurs, dans le projet, nous
rajoutons deux fichiers supplémentaires adc.h et adc.cpp qui permettent de définir complètement la classe ADC.
Diagramme des classes
La classe ADC possède trois attributs :
L'attribut broche qui nous permet d'être en relation avec le fichier représentant la broche utilisée en
lecture seule. C'est grâce à ce fichier que vous pourrez récupérer la valeur de la tension délivrée par le capteur de
température. Contrairement à la classe GPIO, vous n'avez pas besoin de passer par une phase de configuration puisque
cette broche spécifique est toujours une entrée.
L'attribut convertisseur est un attribut de classe. En effet, quel que soit le capteur que nous utiliserons, ce
convertisseur analogique-numérique possède une résolution de 12bits soit 4096 valeurs possibles pour une
tension maximale de 3.3V, soit 0.806 mV par pas de conversion.
L'attribut proportion permet de régler le taux de conversion suivant le capteur utilisé. Ici par exemple, le
capteur de température est un capteur linéaire qui délivre une tension de 10 mV par degré celcius.
Cette classe ADC possède trois méthodes :
Le constructeur : Lorsque vous devez créer un nouvel objet de type ADC, vous précisez bien entendu la broche
de connexion ainsi que la proportion correspondant au type de capteur connecté. Là aussi votre objectif dans l'élaboration de ce
constructeur sera d'ouvrir le fichier qui permet de lire l'état de la broche utilisée. Ce fichier sera associé à l'attribut broche
et ainsi cette dernière sera ensuite accessible instantanément.
Le destructeur : la phase de destruction est toujours l'inverse de ce qui se passe en phase de construction. Ainsi,
il serait judicieux de fermer le fichier associé à l'attribut broche qui a été ouvert dans le
constructeur.
La méthode lire() : Cette méthode toute simple nous renvoie directement la valeur calculée valeur réelle,
lue sur la broche correspondante, en tenant compte de la conversion et de la proportion donnée en phase de construction.
Mise en oeuvre du codage
Dans un premier temps, vous pouvez réaliser des expériences afin de bien maîtriser cette notion de fichiers et de flots. Une fois
que vous aurez compris tous les mécanismes en jeu, vous pouvez coder votre classe ADC et respecter l'algorithme prévu pour
élaborer votre projet.
Durant ce projet, nous allons fusionner deux projets anciens sur lesquels vous avez travaillé. L'objectif est maintenant
d'afficher en temps réel toutes les deux secondes la température de la pièce jusqu'à ce que nous arrêtions le
dispositif. Suivant la température, nous l'afficherons avec des couleurs différentes. Enfin, il est préférable que l'affichage soit
direct sans défilement ni clignotement.
Diagramme des cas d'utilisation
Diagramme de déploiementDiagramme des classes
Aux classes précédentes nous rajoutons toutes les classes concernant la gestion globale de l'afficheur. Nous en profitons pour
rajouter une nouvelle méthode à la classe Afficheur qui permet de modifier une page déjà existante cette
fonctionnalité n'étant pas encore implémentée. Dans ce projet, l'afficheur affichera systématiquement qu'une seule page.
Il serait donc judicieux de travailler constamment avec la même page afin de modifier les valeurs des températures ainsi que la
gestion des couleurs.
Mise en oeuvre du codage
Ce projet est constitué de pas mal de fichiers puisque nous avons besoin de l'ensemble des classes correspondantes aux
entrées-sorties avec également les classes s'occupant de la gestion de l'afficheur.
using namespace std;
#include "gpio.h"
#include "adc.h"
#include "afficheur.h"
int main()
{
GPIO bouton(Entree, 8);
ADC temperature(2, 0.1);
Afficheur lcd;
ostringstream degre;
degre << fixed << setprecision(1);
double valeur;
Page page("");
page.setDefilement(false);
page.setTemps(2);
lcd.ajouter(page);
if (lcd.etat())
{
while (!bouton.etat())
{
valeur = temperature.lire();
degre.seekp(0);
degre << valeur;
page.changerMessage(degre.str());
if (valeur<23.0) page.setCouleur(Vert);
else if (valeur>=23.0 && valeur<25.0) page.setCouleur(Orange);
else page.setCouleur(Rouge);
lcd.modifier(page);
for (int i=0; i<20; i++)
{
usleep(100000);
if (bouton.etat()) break;
}
}
lcd.neRienAfficher();
}
return 0;
}
Utilisation des threads pour la gestion des entrées et des sorties
Lorsque nous devons mettre au point des systèmes embarqués avec une gestion des entrées sorties, il est souvent souhaitable
d'intégrer la notion de threads. Je rappelle que les threads permettent d'avoir un système multi-tâches à l'intérieur d'un même
processus programme, c'est-à-dire que plusieurs tâches s'exécutent simultanément. L'intérêt de cette pratique est
de pouvoir séparer les traitements relatifs à la capture des informations venant des entrées fréquence de capture assez
élevé et le traitement associé pour générer les informations utiles pour activer les sorties fréquence de
rafraîchissement beaucoup plus lente.
Objectif du projet
Nous introduisons ces différentes notions au travers d'un projet qui permet d'afficher la température ambiante toute les cinq
secondes, en même temps que nous faisons clignoter une LED avec une fréquence fixée cette fois-ci à la seconde. Pour terminer le
programme, nous devons appuyer sur un bouton la fréquence de consultation est imposée au dizièmes de seconde.
Diagramme de déploiementDiagramme des classes
La pratique des threads en C++11
Depuis C++11, il est possible d'implémenter le multi-tâches à l'aide de la classe thread. Lorsque vous devez créer une
nouvelle tâche à partir de cette classe, vous devez spécifier, soit une fonction, soit un foncteur ou soit
une expression lambda qui sera automatiquement exécutée lors de la création de l'objet correspondant. La fin de la
tâche correspondra tout simplement à la fin de la fonction de rappel ou foncteur ou lambda. Il est souvent pratique
et concis d'utiliser un lambda spécification du C++11 pour mettre en oeuvre le traitement relatif à la
tâche souhaitée.
La syntaxe d'un lambda est la suivante : [&] ( ) { }. Elle est composée de trois parties :
[&] : La première, délimitée par des crochets, correspond à la capture des variables environnants. Lorsque
nous plaçons un &, nous souhaitons nous connecter à toutes les variables extérieures au thread par référence.
( ) : La deuxième, délimitée par des parenthèses, permet de spécifier des paramètres ou pas,
comme nous le faisons naturellement avec une fonction ou une méthode classique.
{ } : La troisième, délimitée par des accolades, permet de spécifier le corps de la fonction, correspondant au
traitement à réaliser pour le thread.
Lorsque le programme principal lance plusieurs threads, il est souhaitable qu'il attende que chacune des tâches
soient terminées avant de clôturer définitivement le programme. Pour savoir si une tâche est bien terminée, il suffit de faire
appel à la méthode join() de la classe thread. C'est une méthode qui bloque l'exécution mise en attente
du programme principal jusqu'à que la tâche correspondante soit effectivement terminée.Lorsqu'un système possède
plusieurs threads, il faut éviter que chacune des tâches soit énergivore en terme d'utilisation du processeur. Dès que la partie de
code souhaitée par une tâche est exécutée, il est tout de suite nécessaire de mettre en attente le thread correspondant grâce à la
fonction sleep_for() afin que les autres tâches puissent avoir la main le plus rapidement possible, afin d'augmenter
la notion de simultanéité.Mise en oeuvre du codage
Pour ce projet, vous allez mettre en oeuvre trois threads différents : un thread quitter qui permet de gérer la clôture du
programme à l'aide du bouton poussoir, un thread clignoter qui s'occupe uniquement de l'affichage de la led et un thread afficher
qui nous renseigne sur la température ambiante avec un rafraîchissement toutes les 5s.
L'avantage des threads, c'est d'avoir un traitement bien séparé des différentes activités. C'est particulièrement adapté lorsque
nous utilisons des fréquences de rafraîchissement totalement différentes. À ce sujet, je vous invite à réaliser différents tests en
changeant les fréquences. Par exemple, changer la fréquence de clignotement de la led. Prévoyer un affichage des températures
toutes les trois secondes. Analysez le comportement de l'action sur le bouton poussoir lorsque la fréquence de rafraîchissement est
plus lente, par exemple, la demi-seconde ou la seconde. Enfin, plutôt que d'utiliser un bouton poussoir, le programme doit
s'arrêter lorsque l'utilisateur appuie sur le bouton du clavier.
Gestion du chauffage en fonction du nombre de personnes présentes dans la pièce
Nous allons profiter de ces nouvelles compétences pour que notre système embarqué soit capable de maîtriser la gestion de
chauffage d'une pièce. Pour que cela fonctionne de façon optimisée, deux capteurs seront placés côte à côte au niveau de la porte
pour contrôler le passage des personnes. Deux capteurs sont nécessaires pour vérifier la direction que prend chacune de ces
personnes, soit elles rentrent dans la pièce, soit elles en sortent. Nous pouvons ainsi à tout moment comptabiliser le nombre de
personnes présentes. Du moment qu'il existe au moins une personne dans la pièce, le chauffage est en fonctionnement. Toutefois,
bien entendu, si la température atteint un certain seuil, le chauffage peut être coupé.
Diagramme des cas d'utilisation
Diagramme de déploiementDiagramme des classes
Mise en oeuvre du codage
Pour ce projet, vous allez mettre en oeuvre trois threads différents : un thread quitter qui permet de clôturer du
programme lorsque l'utilisateur appuie sur la touche , un thread porte qui gère le comptage des
personnes et le thread chauffage qui active le radiateur suivant le nombre de personnes et la température ambiante de la
pièce.