Membres statiques – Membres de classes

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.

Liste des travaux pratiques

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.

Caractéristiques des broches E/S numériques et analogiques du pcDuino

Nous retrouvons la plupart des possibilités de l'Arduino à quelques adaptation près.

Le brochage du pcDuino avec l'alimentation et les entrées analogiques

Le brochage des entrées-sorties tout-ou-rien

Caractéristiques électriques : Ces broches fonctionnent en 3,3V et pas en 5V et ne peuvent fournir que quelques mA... donc attention, prudence... !
Principe d'accès aux broches E/S

Il est possible de gérer les broches E/S comme de simples fichiers c'est un principe général sous Gnu/Linux.... Le fichier de chaque broche est alors de la forme gpioXX correspond au numéro de la broche. Il existe deux types de fichier de nature différentes :

  1. Les fichiers fixant le mode entrée ou sortie sont placés dans le répertoire : /sys/devices/virtual/misc/gpio/mode/

  2. Les fichiers contenant/définissant l'état des broches sont placés dans le répertoire : /sys/devices/virtual/misc/gpio/pin/

Fixer le sens d'une broche

Pour fixer le sens d'une broche, c'est simple : il suffit d'ouvrir le fichier et d'écrire la valeur voulue sachant :

Diagramme des cas d'utilisation

Après toutes ces considérations techniques qui nous ont permis de comprendre les différents fonctionnements, nous allons maintenant mettre en oeuvre un petit programme qui permet, dès le départ, d'activer une sortie du pcDuino qui est connectée à une simple led. Cette led reste affichée indéfiniment jusqu'à ce que l'utilisateur appuie sur un bouton poussoir. Cette action provoque l'arrêt du programme en même temps que l'extinction de la led.

Diagramme de déploiement

Comme précédemment, le diagramme de déploiement nous permet de bien voir les différents systèmes mis en jeu. Vous devez mettre en oeuvre du cross-développement pour créer votre programme qui sera automatiquement déployé sur le système embarqué pcDuino à l'aide des trois fichiers nécessaires à l'élaboration du projet. La classe GPIO sera développée dans les deux fichiers gpio.h et gpio.cpp. Vous placerez les fonctionnalités attendues dans le fichier principal main.cpp.

Diagramme des classes

D'après ce diagramme vous remarquez que la classe GPIO est composée de deux attributs :

  1. D'une part l'attribut broche qui nous permet d'être en relation avec le fichier représentant la broche utilisée en lecture et en écriture. C'est dans ce fichier que vous allez proposer la valeur 1 ou 0 suivant l'activation que vous souhaitez soumettre broche configurée en sortie, et c'est toujours dans ce fichier que vous pouvez récupérer l'état de l'activation broche configurée en entrée.
  2. D'autre part l'attribut mode, issu de l'énumération MODE, qui permet de fixer une fois pour toute si la broche concernée doit être considérée comme une entrée ou comme une sortie une fois que le sens de la broche est fixé, il ne doit plus être possible de le changer.

Cette classe GPIO possède uniquement quatre méthodes qui d'un point de vue utilisation sont très très simples à manipuler. Leurs noms sont très évocateurs et intuitifs. Toute la partie délicate à traiter se situe à l'intérieur des méthodes. Le développeur qui va utiliser cette classe pourra réaliser des algorithmes sophistiqués sans qu'il se rende compte de la difficulté interne. Toute la problématique est ainsi encapsulée à l'intérieur de la classe. C'est ce qui fait l'intérêt de la programmation objet.

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.

gpio.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt

SOURCES += main.cpp gpio.cpp
HEADERS += gpio.h

target.path = /home/ubuntu/Public
INSTALLS += target
gpio.h
#ifndef GPIO_H
#define GPIO_H

#include <fstream>
using namespace std;

enum MODE {Entree, Sortie};

class GPIO
{
  fstream broche;
  const MODE mode;
public:
  GPIO(MODE m, unsigned numero);
  ~GPIO();
  bool etat();
  void activer(bool action);
};

#endif // GPIO_H
gpio.cpp
#include "gpio.h"

GPIO::GPIO(MODE m, unsigned numero) : mode(m)
{
  string localisation = "/sys/devices/virtual/misc/gpio/mode/gpio";
  localisation += '0' + numero;
  ofstream modeGPIO(localisation.c_str());
  modeGPIO << (mode==Sortie ? '1' : '0');
  modeGPIO.close();
  localisation = "/sys/devices/virtual/misc/gpio/pin/gpio";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

GPIO::~GPIO()
{
  if (mode==Sortie) broche << '0';
  broche.close();
}

void GPIO::activer(bool action)
{
  if (mode==Sortie) {
    broche << (action ? '1' : '0');
    broche.flush();
  }
}

bool GPIO::etat()
{
  broche.seekg(0);
  return broche.peek()=='1';
}
main.cpp
#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.

Mise en oeuvre du codage
gpio.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt

SOURCES += main.cpp gpio.cpp
HEADERS += gpio.h

target.path = /home/ubuntu/Public
INSTALLS += target
gpio.h
#ifndef GPIO_H
#define GPIO_H

#include <fstream>
using namespace std;

enum MODE {Entree, Sortie};

class GPIO
{
  fstream broche;
  const MODE mode;
public:
  GPIO(MODE m, unsigned numero);
  ~GPIO();
  bool etat();
  void activer(bool action);
};

#endif // GPIO_H
gpio.cpp
#include "gpio.h"

GPIO::GPIO(MODE m, unsigned numero) : mode(m)
{
  string localisation = "/sys/devices/virtual/misc/gpio/mode/gpio";
  localisation += '0' + numero;
  ofstream modeGPIO(localisation.c_str());
  modeGPIO << (mode==Sortie ? '1' : '0');
  modeGPIO.close();
  localisation = "/sys/devices/virtual/misc/gpio/pin/gpio";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

GPIO::~GPIO()
{
  if (mode==Sortie) broche << '0';
  broche.close();
}

void GPIO::activer(bool action)
{
  if (mode==Sortie) {
    broche << (action ? '1' : '0');
    broche.flush();
  }
}

bool GPIO::etat()
{
  broche.seekg(0);
  return broche.peek()=='1';
}
main.cpp
#include <unistd.h>
#include "gpio.h"

int main()
{   
  GPIO commande(Entree, 8);
  GPIO fin(Entree, 9);
  GPIO lampe(Sortie, 3);
  bool etatPrecedent = commande.etat();

  while (!fin.etat())
  {
    if (commande.etat() && !etatPrecedent) lampe.activer(!lampe.etat());
    etatPrecedent = commande.etat();
    usleep(100000);
  }

  return 0;
}

Programmation concurrente - système multi-tâches

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é.

Mise en oeuvre du codage
gpio.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt
LIBS += -pthread

SOURCES += main.cpp gpio.cpp
HEADERS += gpio.h

target.path = /home/ubuntu/Public
INSTALLS += target
gpio.h
#ifndef GPIO_H
#define GPIO_H

#include <fstream>
using namespace std;

enum MODE {Entree, Sortie};

class GPIO
{
  fstream broche;
  const MODE mode;
public:
  GPIO(MODE m, unsigned numero);
  ~GPIO();
  bool etat();
  void activer(bool action);
};

#endif // GPIO_H
gpio.cpp
#include "gpio.h"

GPIO::GPIO(MODE m, unsigned numero) : mode(m)
{
  string localisation = "/sys/devices/virtual/misc/gpio/mode/gpio";
  localisation += '0' + numero;
  ofstream modeGPIO(localisation.c_str());
  modeGPIO << (mode==Sortie ? '1' : '0');
  modeGPIO.close();
  localisation = "/sys/devices/virtual/misc/gpio/pin/gpio";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

GPIO::~GPIO()
{
  if (mode==Sortie) broche << '0';
  broche.close();
}

void GPIO::activer(bool action)
{
  if (mode==Sortie) {
    broche << (action ? '1' : '0');
    broche.flush();
  }
}

bool GPIO::etat()
{
  broche.seekg(0);
  return broche.peek()=='1';
}
main.cpp
#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.

Mise en oeuvre du codage
gpio.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt
LIBS += -pthread

SOURCES += main.cpp gpio.cpp
HEADERS += gpio.h

target.path = /home/ubuntu/Public
INSTALLS += target
gpio.h
#ifndef GPIO_H
#define GPIO_H

#include <fstream>
using namespace std;

enum MODE {Entree, Sortie};

class GPIO
{
  fstream broche;
  const MODE mode;
public:
  GPIO(MODE m, unsigned numero);
  ~GPIO();
  bool etat();
  void activer(bool action);
};

#endif // GPIO_H
gpio.cpp
#include "gpio.h"

GPIO::GPIO(MODE m, unsigned numero) : mode(m)
{
  string localisation = "/sys/devices/virtual/misc/gpio/mode/gpio";
  localisation += '0' + numero;
  ofstream modeGPIO(localisation.c_str());
  modeGPIO << (mode==Sortie ? '1' : '0');
  modeGPIO.close();
  localisation = "/sys/devices/virtual/misc/gpio/pin/gpio";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

GPIO::~GPIO()
{
  if (mode==Sortie) broche << '0';
  broche.close();
}

void GPIO::activer(bool action)
{
  if (mode==Sortie) {
    broche << (action ? '1' : '0');
    broche.flush();
  }
}

bool GPIO::etat()
{
  broche.seekg(0);
  return broche.peek()=='1';
}
main.cpp
#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 à 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

Nous retrouvons la plupart des possibilités de l'Arduino à quelques adaptation près.

Câblage du capteur sur une des entrées analogiques

Diagramme 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 :

  1. 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.
  2. 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.
  3. 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 :

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.

gpio.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt

SOURCES += main.cpp gpio.cpp adc.cpp
HEADERS += gpio.h adc.h

target.path = /home/ubuntu/Public
INSTALLS += target
gpio.h
#ifndef GPIO_H
#define GPIO_H

#include <fstream>
using namespace std;

enum MODE {Entree, Sortie};

class GPIO
{
  fstream broche;
  const MODE mode;
public:
  GPIO(MODE m, unsigned numero);
  ~GPIO();
  bool etat();
  void activer(bool action);
};

#endif // GPIO_H
gpio.cpp
#include "gpio.h"

GPIO::GPIO(MODE m, unsigned numero) : mode(m)
{
  string localisation = "/sys/devices/virtual/misc/gpio/mode/gpio";
  localisation += '0' + numero;
  ofstream modeGPIO(localisation.c_str());
  modeGPIO << (mode==Sortie ? '1' : '0');
  modeGPIO.close();
  localisation = "/sys/devices/virtual/misc/gpio/pin/gpio";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

GPIO::~GPIO()
{
  if (mode==Sortie) broche << '0';
  broche.close();
}

void GPIO::activer(bool action)
{
  if (mode==Sortie) {
    broche << (action ? '1' : '0');
    broche.flush();
  }
}

bool GPIO::etat()
{
  broche.seekg(0);
  return broche.peek()=='1';
}
adc.h
#ifndef ADC_H
#define ADC_H

#include <fstream>
using namespace std;

class ADC
{
  ifstream broche;
  static const double convertisseur; // 0.806 mV par unité
  const double proportion;
public:
  ADC(unsigned numero, double proportion);
  ~ADC();
  double lire();
};

#endif // ADC_H
adc.cpp
#include "adc.h"
#include <stdlib.h>

const double ADC::convertisseur = 0.806;  // 0.806 mV par unité

ADC::ADC(unsigned numero, double proportion) : proportion(convertisseur*proportion)
{
  string localisation = "/proc/adc";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

ADC::~ADC()
{
  broche.close();
}

double ADC::lire()
{
  string texte;
  broche.seekg(0);
  broche >> texte;
  string numerique = texte.substr(5);
  return atoi(numerique.c_str()) * proportion;
}
main.cpp
#include <iostream>
#include <unistd.h>
using namespace std;

#include "gpio.h"
#include "adc.h"

int main()
{
  GPIO bouton(Entree, 6);
  GPIO led(Sortie, 3);
  ADC temperature(2, 0.1);
  led.activer(true);
  cout << "Température = " << temperature.lire() << endl;
  while (!bouton.etat()) usleep(100000);
  cout << "Température = " << temperature.lire() << endl;
  return 0;
}

Entrées analogiques - Afficher température

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éploiement

Diagramme 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.

temperature.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt

SOURCES += main.cpp gpio.cpp adc.cpp afficheur.cpp page.cpp serialib.cpp
HEADERS += gpio.h adc.h afficheur.h page.h serialib.h

target.path = /home/ubuntu/Public
INSTALLS += target
gpio.h
#ifndef GPIO_H
#define GPIO_H

#include <fstream>
using namespace std;

enum MODE {Entree, Sortie};

class GPIO
{
  fstream broche;
  const MODE mode;
public:
  GPIO(MODE m, unsigned numero);
  ~GPIO();
  bool etat();
  void activer(bool action);
};

#endif // GPIO_H
gpio.cpp
#include "gpio.h"

GPIO::GPIO(MODE m, unsigned numero) : mode(m)
{
  string localisation = "/sys/devices/virtual/misc/gpio/mode/gpio";
  localisation += '0' + numero;
  ofstream modeGPIO(localisation.c_str());
  modeGPIO << (mode==Sortie ? '1' : '0');
  modeGPIO.close();
  localisation = "/sys/devices/virtual/misc/gpio/pin/gpio";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

GPIO::~GPIO()
{
  if (mode==Sortie) broche << '0';
  broche.close();
}

void GPIO::activer(bool action)
{
  if (mode==Sortie) {
    broche << (action ? '1' : '0');
    broche.flush();
  }
}

bool GPIO::etat()
{
  broche.seekg(0);
  return broche.peek()=='1';
}
adc.h
#ifndef ADC_H
#define ADC_H

#include <fstream>
using namespace std;

class ADC
{
  ifstream broche;
  static const double convertisseur; // 0.806 mV par unité
  const double proportion;
public:
  ADC(unsigned numero, double proportion);
  ~ADC();
  double lire();
};

#endif // ADC_H
adc.cpp
#include "adc.h"
#include <stdlib.h>

const double ADC::convertisseur = 0.806;  // 0.806 mV par unité

ADC::ADC(unsigned numero, double proportion) : proportion(convertisseur*proportion)
{
  string localisation = "/proc/adc";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

ADC::~ADC()
{
  broche.close();
}

double ADC::lire()
{
  string texte;
  broche.seekg(0);
  broche >> texte;
  string numerique = texte.substr(5);
  return atoi(numerique.c_str()) * proportion;
}
page.h
#ifndef PAGE_H
#define PAGE_H

#include <string>
using namespace std;

enum Couleur {Vert, Orange, Rouge, ArcEnCiel};

class Page
{
  string numero;
  string message;
  string defilement = "<FE>";
  string clignotement = "<MA>";
  string temps = "<WC>";
  string couleur = "<CE>";
public:
  Page(const string &texte);
  void setNumero(unsigned page);
  void setDefilement(bool actif);
  void setClignotement(bool actif);
  void setTemps(unsigned secondes);
  void setCouleur(Couleur c);
  void changerMessage(const string &texte);
  string trame() const;
  bool vide() const;
};

#endif // PAGE_H
page.cpp
#include "page.h"

Page::Page(const string &texte) : message(texte) {}

void Page::setNumero(unsigned page)
{
  numero = "<L1><P";
  numero += 'A' + page - 1;
  numero += '>';
}

void Page::setDefilement(bool actif)
{
  defilement = "<F";
  defilement += actif ? "E>" : "A>";
}

void Page::setClignotement(bool actif)
{
  clignotement = "<M";
  clignotement += actif ? "B>" : "A>";
}

void Page::setTemps(unsigned secondes)
{
  temps = "<W";
  temps += 'A'+secondes;
  temps += '>';
}

void Page::setCouleur(unsigned c)
{
  char coul;
  switch (c) {
    default:
    case Vert      : coul='E'; break;
    case Orange    : coul='H'; break;
    case Rouge     : coul='B'; break;
    case ArcEnCiel : coul='S'; break;
  }
  couleur = "<C";
  couleur += coul;
  couleur += '>';
}

void Page::changerMessage(const string &texte) { message = texte; }

string Page::trame() const
{
  return numero+defilement+clignotement+temps+defilement+couleur+message;
}

bool Page::vide() const { return message.empty(); }
afficheur.h
#ifndef AFFICHEUR_H
#define AFFICHEUR_H

#include "serialib.h"
#include "page.h"
#include <string>
#include <vector>
using namespace std;

class Afficheur
{
  serialib usb;
  bool connexion;
  vector<Page> pages;
public:
  Afficheur();
  ~Afficheur();
  bool etat() const  { return connexion; } 
  void ajouter(Page &page);
void modifier(Page &page);
 void enleverDernierePage(); void neRienAfficher(); private: void choisirNombrePage(unsigned nombre); void envoyerTrame(const string &commande); string checksum(string texte); }; #endif // AFFICHEUR_H
afficheur.cpp
#include "afficheur.h"

Afficheur::Afficheur()
{
  connexion = usb.Open("/dev/ttyUSB0", 9600) == 1;
  Page page("");
  ajouter(page);
}

Afficheur::~Afficheur()
{
  usb.Close();
}

void Afficheur::ajouter(Page &page)
{
  if (pages.size()==5) return;
  if (pages.size()==1 && pages[0].vide()) {
    pages.clear();
    page.setNumero(1);
    pages.push_back(page);
    envoyerTrame(page.trame());
  }
  else {
    unsigned nombrePage = pages.size()+1;
    page.setNumero(nombrePage);
    pages.push_back(page);
    choisirNombrePage(nombrePage);
    envoyerTrame(page.trame());
  }
}

void Afficheur::modifier(Page &page)
{
  envoyerTrame(page.trame());
}
void Afficheur::enleverDernierePage() { if (pages.size()>1) { pages.pop_back(); choisirNombrePage(pages.size()); } else if (!pages[0].vide()) { pages[0].changerMessage(""); envoyerTrame(pages[0].trame()); } } void Afficheur::neRienAfficher() { pages.clear(); Page page(""); ajouter(page); } void Afficheur::choisirNombrePage(unsigned nombre) { string commande = "<TA>00000000009900000000"; for (int i=0; i<nombre; i++) commande += 'A'+i; envoyerTrame(commande); } void Afficheur::envoyerTrame(const string &commande) { string trame = "<ID01>"; trame += commande; trame += checksum(commande); trame += "<E>"; usb.WriteString(trame.c_str()); usleep(100000); // Attente de 1/10ème de seconde entre chaque trame } string Afficheur::checksum(string texte) { char calcul = 0; for (char octet : texte) calcul^=octet; char bas = calcul & 0b00001111; char haut = (calcul & 0b11110000) >> 4; bas = bas < 10 ? bas+=0x30 :bas+=0x41-10; haut = haut < 10 ? haut+=0x30 : haut+=0x41-10; return {haut, bas}; }
main.cpp
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éploiement

Diagramme 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.

gpio.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt
LIBS += -pthread

SOURCES += main.cpp gpio.cpp adc.cpp
HEADERS += gpio.h adc.h

target.path = /home/ubuntu/Public
INSTALLS += target
gpio.h
#ifndef GPIO_H
#define GPIO_H

#include <fstream>
using namespace std;

enum MODE {Entree, Sortie};

class GPIO
{
  fstream broche;
  const MODE mode;
public:
  GPIO(MODE m, unsigned numero);
  ~GPIO();
  bool etat();
  void activer(bool action);
};

#endif // GPIO_H
gpio.cpp
#include "gpio.h"

GPIO::GPIO(MODE m, unsigned numero) : mode(m)
{
  string localisation = "/sys/devices/virtual/misc/gpio/mode/gpio";
  localisation += '0' + numero;
  ofstream modeGPIO(localisation.c_str());
  modeGPIO << (mode==Sortie ? '1' : '0');
  modeGPIO.close();
  localisation = "/sys/devices/virtual/misc/gpio/pin/gpio";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

GPIO::~GPIO()
{
  if (mode==Sortie) broche << '0';
  broche.close();
}

void GPIO::activer(bool action)
{
  if (mode==Sortie) {
    broche << (action ? '1' : '0');
    broche.flush();
  }
}

bool GPIO::etat()
{
  broche.seekg(0);
  return broche.peek()=='1';
}
adc.h
#ifndef ADC_H
#define ADC_H

#include <fstream>
using namespace std;

class ADC
{
  ifstream broche;
  static const double convertisseur; // 0.806 mV par unité
  const double proportion;
public:
  ADC(unsigned numero, double proportion);
  ~ADC();
  double lire();
};

#endif // ADC_H
adc.cpp
#include "adc.h"
#include <stdlib.h>

const double ADC::convertisseur = 0.806;  // 0.806 mV par unité

ADC::ADC(unsigned numero, double proportion) : proportion(convertisseur*proportion)
{
  string localisation = "/proc/adc";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

ADC::~ADC()
{
  broche.close();
}

double ADC::lire()
{
  string texte;
  broche.seekg(0);
  broche >> texte;
  string numerique = texte.substr(5);
  return atoi(numerique.c_str()) * proportion;
}
main.cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
using namespace std::this_thread;
using namespace std::chrono;

#include "gpio.h"
#include "adc.h"

int main()
{   
  bool fin = false;

  thread quitter([&](){
    GPIO bouton(Entree, 8);
    milliseconds attente(100);
    while (!bouton.etat()) sleep_for(attente);
    fin = true;
    cout << "Demande d'interruption du programme..." << endl;
  });

  thread clignoter([&](){
    GPIO led(Sortie, 3);
    seconds attente(1);
    bool sortie = false;
    while (!fin) {
      sortie = !sortie;
      led.activer(sortie);
      sleep_for(attente);
    }
  });

  thread afficher([&](){
    ADC temperature(2, 0.1);
    seconds attente(5);
    while (!fin) {
      cout << "Température = " << temperature.lire() << endl;
      sleep_for(attente);
    }
  });

  quitter.join();
  clignoter.join();
  afficher.join();

  cout << "Programme terminé !" << endl;

  return 0;
}
Conclusion et modification du programme principal

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.

main.cpp
#include <iostream>
#include <thread>
#include <chrono>
using namespace std;
using namespace std::this_thread;
using namespace std::chrono;

#include "gpio.h"
#include "adc.h"

int main()
{   
  bool fin = false;

  thread quitter([&](){
//    GPIO bouton(Entree, 6);
//    milliseconds attente(100);
//    while (!bouton.etat()) sleep_for(attente);
    cin.get();
    fin = true;
    cout << "Demande d'interruption du programme..." << endl;
  });

  thread clignoter([&](){
    GPIO led(Sortie, 3);
    milliseconds attente(500);
    bool sortie = false;
    while (!fin) {
      sortie = !sortie;
      led.activer(sortie);
      sleep_for(attente);
    }
  });

  thread afficher([&](){
    ADC temperature(2, 0.1);
    seconds attente(3);
    while (!fin) {
      cout << "Température = " << temperature.lire() << endl;
      sleep_for(attente);
    }
  });

  quitter.join();
  clignoter.join();
  afficher.join();

  cout << "Programme terminé !" << endl;

  return 0;
}

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éploiement

Diagramme 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.

gpio.pro
TEMPLATE = app
CONFIG += console c++11
CONFIG -= app_bundle qt
LIBS += -pthread

SOURCES += main.cpp gpio.cpp adc.cpp
HEADERS += gpio.h adc.h

TARGET = chauffage
target.path = /home/ubuntu/Public
INSTALLS += target
gpio.h
#ifndef GPIO_H
#define GPIO_H

#include <fstream>
using namespace std;

enum MODE {Entree, Sortie};

class GPIO
{
  fstream broche;
  const MODE mode;
public:
  GPIO(MODE m, unsigned numero);
  ~GPIO();
  bool etat();
  void activer(bool action);
};

#endif // GPIO_H
gpio.cpp
#include "gpio.h"

GPIO::GPIO(MODE m, unsigned numero) : mode(m)
{
  string localisation = "/sys/devices/virtual/misc/gpio/mode/gpio";
  localisation += '0' + numero;
  ofstream modeGPIO(localisation.c_str());
  modeGPIO << (mode==Sortie ? '1' : '0');
  modeGPIO.close();
  localisation = "/sys/devices/virtual/misc/gpio/pin/gpio";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

GPIO::~GPIO()
{
  if (mode==Sortie) broche << '0';
  broche.close();
}

void GPIO::activer(bool action)
{
  if (mode==Sortie) {
    broche << (action ? '1' : '0');
    broche.flush();
  }
}

bool GPIO::etat()
{
  broche.seekg(0);
  return broche.peek()=='1';
}
adc.h
#ifndef ADC_H
#define ADC_H

#include <fstream>
using namespace std;

class ADC
{
  ifstream broche;
  static const double convertisseur; // 0.806 mV par unité
  const double proportion;
public:
  ADC(unsigned numero, double proportion);
  ~ADC();
  double lire();
};

#endif // ADC_H
adc.cpp
#include "adc.h"
#include <stdlib.h>

const double ADC::convertisseur = 0.806;  // 0.806 mV par unité

ADC::ADC(unsigned numero, double proportion) : proportion(convertisseur*proportion)
{
  string localisation = "/proc/adc";
  localisation += '0' + numero;
  broche.open(localisation.c_str());
}

ADC::~ADC()
{
  broche.close();
}

double ADC::lire()
{
  string texte;
  broche.seekg(0);
  broche >> texte;
  string numerique = texte.substr(5);
  return atoi(numerique.c_str()) * proportion;
}
main.cpp
#include <iostream>
#include <thread>
#include <chrono>
#include <iostream>
using namespace std;
using namespace std::this_thread;
using namespace std::chrono;

#include "gpio.h"
#include "adc.h"

int main()
{   
  bool fin = false;
  int compteur = 0;

  thread quitter([&](){
    cin.get();
    fin = true;
    cout << "Demande d'interruption du programme..." << endl;
  });

  thread porte([&](){
    GPIO exterieur(Entree, 8);
    GPIO interieur(Entree, 9);
    milliseconds attente(500);
    enum CapteurPorte {Inactif, Exterieur, Seuil, Interieur};
    CapteurPorte avant=Inactif, maintenant;

    while (!fin) {
      bool ex = exterieur.etat();
      bool in = interieur.etat();

      if (ex && !in)      maintenant = Exterieur;
      else if (ex && in)  maintenant = Seuil;
      else if (!ex && in) maintenant = Interieur;
      else                maintenant = Inactif;

      if (avant==Seuil && maintenant==Interieur) { cout << "Compteur = " << ++compteur << endl; }
      if (avant==Seuil && maintenant==Exterieur) { cout << "Compteur = " << --compteur << endl; }

      sleep_for(attente);
      avant = maintenant;
    }
  });

  thread chauffage([&](){
    const double MAXI = 24;
    GPIO radiateur(Sortie, 3);
    ADC temperature(2, 0.1);
    seconds attente(5);
    while (!fin) {
      cout << "Température = " << temperature.lire() << endl;
      radiateur.activer(compteur>0 && temperature.lire()<MAXI);
      sleep_for(attente);
    }
  });

  quitter.join();
  porte.join();
  chauffage.join();

  cout << "Programme terminé !" << endl;

  return 0;
}