#7 - Ventilation de garage - La programmation sous Arduino

, par Patrice Freney


Côté programmation, je suis parti de zéro pour comprendre ce que je faisais, même si beaucoup d’exemples de code sont disponibles sur Internet.
J’ai utilisé tout simplement l’application Arduino officielle.

Je ne détaillerai pas ici, mais je suis passé par plusieurs étapes d’apprentissage :

  • gestion des entrées (boutons, potentiomètre) et des sorties (module relais).
  • gestion des capteurs de températures
  • gestion de l’écran LCD

J’ai essayé de commenter le plus possible le code mis à disposition afin que vous puissiez me comprendre au maximum. Par expérience, je sais qu’il est toujours un peu compliqué de cerner le code créé par quelqu’un d’autre que soi.
Le programme est relativement simple, et les remarques s’adressent surtout aux débutants.

Quelques petites explications supplémentaires :

  • le curseur : je souhaitais voir si l’Arduino fonctionnait en permanence. J’ai donc placé un petit curseur clignotant " : " en bas de l’écran à droite. Si le microcontrôleur est planté, soit il n’y a rien, soit les " : " sont fixes.
  • la barre de progression, façon presque-K2000 (les plus vieux comprendront) : elle est utilisée comme temps d’attente avant que le relais ne change d’état. Comme expliqué dans le cahier des charges, il est déconseillé que le moteur du ventilateur s’allume et s’éteigne trop fréquemment quand les températures sont un peu trop voisines les unes des autres.
    Je disposais de 3 caractères de libres verticalement, et chaque caractère est constitué de 8 lignes de pixel, soit 24 en tout. J’ai déterminé en fonction de ces 24 lignes que 240 secondes, soit 4 minutes, étaient un temps correct. À voir à l’utilisation. Une ligne représente donc un temps d’attente de 10 secondes.

Une petite vidéo où la progression a été (très) accélérée :

Le programme, au complet :

// ************************************************************
//
// Gestion de la ventilation du garage
//
// Patrice Freney - http://www.freney.net
//
// version 1.0 du 18/10/2019
//
//
// Arduino Application version 1.8.10
//
// LiquidCrystal_I2C.h version 1.1.2
// OneWire.h version 2.3.5
// DallasTemperature.h version 3.8.0
//
// ************************************************************


// ************************************************************
// pour les variables :
//    - ve_ : entier (sauf pour les boucles)
//    - vr_ : reel
//    - vb_ : booleen
//    - vllong_ : long long
//
// ************************************************************

// ************************************************************
// affichage des informations sur écran LCD 20 x 4 caractères
//
// P : 3 x caractère de progression
// C : 1 x curseur clignotant
//
// Position ligne/colonne
// 0           12     19
// **********************
// *Temp. Ext.  24.5°C P* 0
// *Temp. Int.  26.2°C P* 1
// *Temp. Reg.  25.0°C P* 2
// *Ventilation ON     C* 3
// **********************
//
// mode normal :
// **********************
// *Temp. Ext.  24.5°C P*
// *Temp. Int.  26.2°C P*
// *Temp. Reg.  25.0°C P*
// *Ventilation ON     C*
// **********************
//
// mode ventilation forcée ON
// **********************
// *Temp. Ext.  25.2°C P*
// *Temp. Int.  25.2°C P*
// *Temp. Reg.  25.0°C P*
// *Ventilation FO.ON  C*
// **********************
//
// mode ventilation forcée OFF
// **********************
// *Temp. Ext.  25.2°C P*
// *Temp. Int.  25.2°C P*
// *Temp. Reg.  25.0°C P*
// *Ventilation FO.OFF C*
// **********************
//
// ************************************************************




// ************************************************************

// déclaration des bibliothèques
#include <LiquidCrystal_I2C.h>  // bibliothèque pour l'affichage LCD
#include <Arduino.h>
#include <Wire.h>
#include <OneWire.h> // bibliothèque du bus OneWire
#include <DallasTemperature.h> // bibliothèque du capteur de température
OneWire oneWire(2); // bus One Wire sur la pin D2 de l'arduino
DallasTemperature sensors(&oneWire); // utilisation du bus Onewire pour les capteurs
DeviceAddress sensorDeviceAddress; // vérifie la compatibilité des capteurs avec la bibliothèque


LiquidCrystal_I2C lcd(0x27, 20, 4); // définition de l'écran LCD (bus de transmission, nombre caractère par ligne, nombre de lignes)


// déclaration des variables
float vr_temp_int; // température intérieure
float vr_temp_ext; // température extérieure
float vr_temp_min; // température minimale de réglage
float vr_temp_max; // température maximale de réglage
float vr_temp_plage; // plage de température de réglage (max-min)
float vr_temp_pas; // pas de réglage
int ve_poten_valeur; // valeur brute retournée par le potentiomètre
float vr_temp_reglage; // température de réglage calculée en fonction du potentiomètre

unsigned long long vllong_current_millis; // temps qui s'écoule depuis le démarrage de l'Arduino

// nombre de millisecondes fixes entre deux changements d'état (curseur, progression et ventilation)
const unsigned long vllong_curseur_interval = 500; // 500, toutes les demi-secondes
const unsigned long vllong_progression_interval = 10000; // 10000, toutes les 10 secondes, avec 8 lignes par curseur (x3), soit 24 lignes - barre de progression
const unsigned long vllong_ventilation_interval = 240000; // 240000, toutes les 4 minutes, soit 240 secondes, pour ne pas griller le moteur de la ventilation

unsigned long vllong_curseur_previous_millis = 0; // précédente valeur de millis() pour le curseur
boolean vb_curseur_previous_etat = LOW; // précédent état du curseur

unsigned long vllong_progression_previous_millis = 0; // précédente valeur de millis() pour l'affichage de la progression
int ve_progression_numero_ligne; // numéro de ligne pour la progression, de 1 à 24, pour les trois curseurs
int ve_progression_ligne_position; // position verticale du curseur de progression
int ve_progression_caractere_choix; // choix du caractère pour afficher la progression

unsigned long vllong_ventilation_previous_millis = 0; // précédente valeur de millis() pour le ventilateur



// désignation des broches utilisées

// broche A4 utilisée pour l'afficheur LCD
// broche A5 utilisée pour l'afficheur LCD
int potentiometre = 3; // broche A3 pour le réglage de température avec le potentiomètre

// broche D2 utilisée pour le bus One Wire des sondes de température
int ecran_inter_on = 5; // broche D5 pour l'interrupteur de l'écran
int ventilation_inter_force_on = 6; // broche D6 pour l'interrupteur de la ventilation forcée sur ON
int ventilation_inter_force_off = 7; // broche D7 pour l'interrupteur de la ventilation forcée sur OFF
int ventilation_relais = 9; // broche D9 pour la commande du relais de puissance de la ventilation



// création des 8 caractères pour la barre de progression. Chaque caractère est composé de 8 pixels en hauteur x 5 pixels en largeur.
byte barre1[8] = {
 B00000,
 B00000,
 B00000,
 B00000,
 B00000,
 B00000,
 B00000,
 B11111
};

byte barre2[8] = {
 B00000,
 B00000,
 B00000,
 B00000,
 B00000,
 B00000,
 B11111,
 B11111
};

byte barre3[8] = {
 B00000,
 B00000,
 B00000,
 B00000,
 B00000,
 B11111,
 B11111,
 B11111
};

byte barre4[8] = {
 B00000,
 B00000,
 B00000,
 B00000,
 B11111,
 B11111,
 B11111,
 B11111
};

byte barre5[8] = {
 B00000,
 B00000,
 B00000,
 B11111,
 B11111,
 B11111,
 B11111,
 B11111
};

byte barre6[8] = {
 B00000,
 B00000,
 B11111,
 B11111,
 B11111,
 B11111,
 B11111,
 B11111
};

byte barre7[8] = {
 B00000,
 B11111,
 B11111,
 B11111,
 B11111,
 B11111,
 B11111,
 B11111
};

byte barre8[8] = {
 B11111,
 B11111,
 B11111,
 B11111,
 B11111,
 B11111,
 B11111,
 B11111
};




// ************************************************************
//
// au démarrage
//
// ************************************************************
void setup()
{

 // initialisation de l'écran LCD
 lcd.init(); // initialise l'écran LCD
 lcd.begin(20, 4); // initialise le LCD avec 20 colonnes x 4 lignes
 delay(10); // pause rapide pour laisser temps initialisation
 lcd.noCursor(); // désactive le curseur qui devient invisible
 lcd.clear(); // efface l'écran
 lcd.setBacklight(1);
 
 // caractères spéciaux
 lcd.createChar(1, barre1);
 lcd.createChar(2, barre2);
 lcd.createChar(3, barre3);
 lcd.createChar(4, barre4);
 lcd.createChar(5, barre5);
 lcd.createChar(6, barre6);
 lcd.createChar(7, barre7);
 lcd.createChar(8, barre8);

 // affichage textes de bienvenue. Rien d'obligatoire, mais cela permet d'afficher deux ou trois informations.
 lcd.setCursor(2, 0);
 lcd.print("Hello Pat ! ;-)");
 lcd.setCursor(0, 1);
 lcd.print("Gestion de");
 lcd.setCursor(6, 2);
 lcd.print("la ventilation");
 lcd.setCursor(0, 3);
 lcd.print("v. 1.0 du 16/10/2019");
 delay(2000); // ce n'est rien, 2 secondes dans une vie… ;-)


 // affichage textes fixes des températures
 lcd.clear(); // efface l'écran.
 lcd.setCursor(0, 0);
 lcd.print("Temp. Ext.");
 lcd.setCursor(0, 1);
 lcd.print("Temp. Int.");
 lcd.setCursor(0, 2);
 lcd.print("Temp. Reg.");
 lcd.setCursor(0, 3);
 lcd.print("Ventilation");
 lcd.setCursor(12, 3);
 lcd.print("OFF  ");


 // initialisation des variables
 vr_temp_ext = 0;
 vr_temp_min = 10;
 vr_temp_max = 30;
 vr_temp_plage = vr_temp_max - vr_temp_min; // plage de températures entre le maximum et le mininum, fixées par programmation avec les valeurs ci-dessus
 vr_temp_pas = 1024 / vr_temp_plage; // réglage du "pas" du potentiomètre, de 0 à 1024 en entrée analogique

 ve_progression_ligne_position = 0;

 
 // définition des entrées
 pinMode(potentiometre, INPUT); // potentiometre
 pinMode(ventilation_inter_force_on, INPUT); // interrupteur ventilation forcée sur ON  
 pinMode(ventilation_inter_force_off, INPUT); // interrupteur ventilation forcée sur OFF
 pinMode(ecran_inter_on, INPUT); // interrupteur LCD ON/OFF

 // capteurs de température
 sensors.begin(); // activation des capteurs
 sensors.getAddress(sensorDeviceAddress, 0); // demande l'adresse du capteur à l'index 0 du bus
 sensors.setResolution(sensorDeviceAddress, 12); // résolutions possibles: 9,10,11,12


 // définition des sorties, éteintes au démarrage
 pinMode(ventilation_relais, OUTPUT); // Relais ventilation en sortie
 digitalWrite(ventilation_relais, LOW);  // Relais ventilation sur OFF


} // setup







// ************************************************************
//
// programme principal
//
// ************************************************************
void loop()
{
 
 // demande des températures aux capteurs  
 sensors.requestTemperatures();
 vr_temp_ext = sensors.getTempCByIndex(0);
 vr_temp_int = sensors.getTempCByIndex(1);
 
 // affichage de la température extérieure
 lcd.setCursor(12, 0);
 lcd.print(vr_temp_ext, 1); // seulement une décimale
 caractere_degre_afficher(16, 0);
 
 // affichage de la température intérieure
 lcd.setCursor(12, 1);
 lcd.print(vr_temp_int, 1); // seulement une décimale
 caractere_degre_afficher(16, 1);

 unsigned long long vllong_current_millis = superMillis(); // récupérer la valeur actuelle de millis(), nombre de millisecondes depuis le démarrage de la machine


// gestion du curseur - Toutes les secondes
 if (vllong_current_millis - vllong_curseur_previous_millis >= vllong_curseur_interval)  // si vllong_curseur_interval ou plus millisecondes se sont écoulés
 {
   vllong_curseur_previous_millis = vllong_current_millis;  // garder en mémoire la valeur actuelle de millis()
   vb_curseur_previous_etat = !vb_curseur_previous_etat;  // inverser l'état du curseur
   
   // afficher ou non le curseur
   lcd.setCursor(19,3);
   if (vb_curseur_previous_etat==1) {
     lcd.print(":");
   } else {
     lcd.print(" ");
   }
 }


// gestion de la barre de progression - Toutes les 10 secondes
 if (vllong_current_millis - vllong_progression_previous_millis >= vllong_progression_interval)  // si vllong_progression_interval ou plus millisecondes se sont écoulés
 {
   vllong_progression_previous_millis = vllong_current_millis;  // garder en mémoire la valeur actuelle de millis()
   ve_progression_numero_ligne = ++ve_progression_numero_ligne; // incrémenter le compteur des lignes

   if (ve_progression_numero_ligne < 9) // position en bas du curseur, ligne 2
   {
     ve_progression_ligne_position = 2;
   }
   else if (ve_progression_numero_ligne < 17) // position centrale, ligne 1
     {
       ve_progression_ligne_position = 1;
     }
     else if (ve_progression_numero_ligne < 25) // position haute, ligne 0
       {
         ve_progression_ligne_position = 0;
       }
       else  // compteur égal à 25, remettre à zéro puis effacer la progression
       {
         progression_curseur_effacer();
         ve_progression_numero_ligne = 0;
       }
 }

 // affichage de la progression
 if (ve_progression_numero_ligne > 0) {

   switch (ve_progression_numero_ligne) {

   // curseur ligne 2
   case 1:
     ve_progression_caractere_choix = 1;
     break;
     
   case 2:
     ve_progression_caractere_choix = 2;
     break;
   
   case 3:
     ve_progression_caractere_choix = 3;
     break;

   case 4:
     ve_progression_caractere_choix = 4;
     break;

   case 5:
     ve_progression_caractere_choix = 5;
     break;

   case 6:
     ve_progression_caractere_choix = 6;
     break;
     
   case 7:
     ve_progression_caractere_choix = 7;
     break;
   
   case 8:
     ve_progression_caractere_choix = 8;
     break;

   // curseur ligne 1
   case 9:
     ve_progression_caractere_choix = 1;
     break;
     
   case 10:
     ve_progression_caractere_choix = 2;
     break;
   
   case 11:
     ve_progression_caractere_choix = 3;
     break;

   case 12:
     ve_progression_caractere_choix = 4;
     break;

   case 13:
     ve_progression_caractere_choix = 5;
     break;

   case 14:
     ve_progression_caractere_choix = 6;
     break;
     
   case 15:
     ve_progression_caractere_choix = 7;
     break;
   
   case 16:
     ve_progression_caractere_choix = 8;
     break;

     
   // curseur ligne 0
   case 17:
     ve_progression_caractere_choix = 1;
     break;
     
   case 18:
     ve_progression_caractere_choix = 2;
     break;
   
   case 19:
     ve_progression_caractere_choix = 3;
     break;

   case 20:
     ve_progression_caractere_choix = 4;
     break;

   case 21:
     ve_progression_caractere_choix = 5;
     break;

   case 22:
     ve_progression_caractere_choix = 6;
     break;
     
   case 23:
     ve_progression_caractere_choix = 7;
     break;
   
   case 24:
     ve_progression_caractere_choix = 8;
     break;

 } // switch
 
      lcd.setCursor(19, ve_progression_ligne_position);
      lcd.write(byte(ve_progression_caractere_choix));
     
 } // if ve_progression_numero_ligne



// allumage ou extinction de l'écran LCD selon l'interrupteur
 if (digitalRead(ecran_inter_on) == HIGH)
 {
   lcd.display(); // allume rapidement l'écran (le contenu affiché n'est pas modifié). N'impacte pas le contrôle du rétroéclairage.
   lcd.backlight(); // active le rétroéclairage
 }
 else
 {
    lcd.noDisplay(); // éteint rapidement l'écran (le contenu affiché n'est pas modifié). N'impacte pas le contrôle du rétroéclairage.
    lcd.noBacklight(); // désactive le rétroéclairage
 }



// affichage de la valeur de réglage du potentiomètre
 ve_poten_valeur = analogRead(potentiometre);
 vr_temp_reglage = ((((ve_poten_valeur) / (vr_temp_pas)) + (vr_temp_min)) * (10)) / (10);

 lcd.setCursor(12, 2);
 lcd.print( vr_temp_reglage, 1 );
 caractere_degre_afficher(16, 2);



// gestion de la ventilation
// choix de la ventilation : forcée ON (prioritaire), forcée OFF (secondaire), ou en fonction des températures (par default)
// l'interrupteur ventilation forcée ON/OFF est un interrupteur trois positions, dont la position neutre au centre n'active ni l'une, ni l'autre, donc pas de "forçage".

if (digitalRead(ventilation_inter_force_on) == HIGH) // ventilation forcée en fonctionnement, elle est prioritaire
 {
   lcd.setCursor(12, 3);
   lcd.print("FO.ON ");
   digitalWrite(ventilation_relais, HIGH);
   vllong_ventilation_previous_millis = vllong_current_millis + 1000; // le temps de standby de la ventilation se remet à "zéro", en ajoutant 1000 ms au temps précédent
   vllong_progression_previous_millis = vllong_current_millis + 10;  // la progression se remettra à "zéro", en ajoutant 10 ms au temps précédent
   progression_curseur_effacer();
   ve_progression_numero_ligne = 0;
 }

 else if (digitalRead(ventilation_inter_force_off) == HIGH) // ventilation forcée sur arrêt
   {
     lcd.setCursor(12, 3);
     lcd.print("FO.OFF ");
     digitalWrite(ventilation_relais, LOW);
     vllong_ventilation_previous_millis = vllong_current_millis + 1000; // le temps de standby de la ventilation se remet à "zéro", en ajoutant 1000 ms au temps précédent
     vllong_progression_previous_millis = vllong_current_millis + 10;  // la progression se remettra à "zéro", en ajoutant 10 ms au temps précédent
     progression_curseur_effacer();
     ve_progression_numero_ligne = 0;
   }

   else // pas de ventilation forcée, la ventilation est gérée en fonction des températures (voir cahier des charges)
   {
     if (vllong_current_millis - vllong_ventilation_previous_millis >= vllong_ventilation_interval)  // si vllong_ventilation_interval ou plus millisecondes se sont écoulés, le ventilateur peut être allumé ou éteint
     {
       vllong_ventilation_previous_millis = vllong_current_millis;  // garder en mémoire la valeur actuelle de millis()
       
       if((((vr_temp_int) > (vr_temp_reglage)) && ((vr_temp_int) > (vr_temp_ext))) || ((((vr_temp_int) < (vr_temp_reglage)) && ((vr_temp_ext) > (vr_temp_int))) && ((vr_temp_ext) > (0))))
       {
         ventilation_ON();
       }
        else
        {
           ventilation_OFF();
        }
     }
   }



} // loop



// ************************************************************
// déclaration des fonctions personnelles

// ************************************************************


// ************************************************************
// fonction : ventilation_ON
// paramètres : aucun
// retour : aucun
//
// description : met la ventilation en fonctionnement
// ************************************************************

void ventilation_ON()
{
 lcd.setCursor(12, 3);
 lcd.print("ON    ");
 digitalWrite(ventilation_relais, HIGH);
}



// ************************************************************
// fonction : ventilation_OFF
// paramètres : aucun
// retour : aucun
//
// description : arrête la ventilation
// ************************************************************
void ventilation_OFF()
{
 lcd.setCursor(12, 3);
 lcd.print("OFF   ");
 digitalWrite(ventilation_relais, LOW);
}



// ************************************************************
// fonction : caractere_degre_afficher
// paramètres : (x,y)
// retour : aucun
//
// description : affiche "°C" a la position (x,y) donnée
// ************************************************************

void caractere_degre_afficher (int ve_position_x, int ve_position_y)
{
 lcd.setCursor(ve_position_x, ve_position_y);
 lcd.print((char)223); // symbole "degré"
 lcd.print("C");
}



// ************************************************************
// fonction : progression_curseur_effacer
// paramètres : aucun
// retour : aucun
//
// description: efface les trois curseurs de la progression à l'aide du caractère espace
// ************************************************************
void progression_curseur_effacer()
{
   for (int i = 0; i <= 2; i++)
       {
         lcd.setCursor(19, i);
         lcd.print(" "); // caractère vide
       }
}




// ************************************************************
// fonction : superMillis
// paramètres : aucun
// retour : finalMillis
//
// description: gérer le débordement de millis() par skywodd https://www.carnetdumaker.net/articles/la-gestion-du-temps-avec-arduino/. Merci à lui.
//
// * Retourne le nombre de millisecondes depuis le démarrage du programme.
// *
// * @return Le nombre de millisecondes depuis le démarrage du programme sous la forme d'un nombre entier sur 64 bits (unsigned long long).
// *
// ************************************************************
unsigned long long superMillis() {
 static unsigned long nbRollover = 0;
 static unsigned long previousMillis = 0;
 unsigned long currentMillis = millis();
 
 if (currentMillis < previousMillis) {
    nbRollover++;
 }
 previousMillis = currentMillis;

 unsigned long long finalMillis = nbRollover;
 finalMillis <<= 32;
 finalMillis +=  currentMillis;
 return finalMillis;
}

// ************************************************************

Prochaine et dernière étape, le coût du montage