mardi 19 mars 2019

Arduino, ESP8266 et ESP32 une librairie timers



 Arduino, ESP8266 et ESP32

 une librairie timers


Sur ARDUINO la gestion du temps est pour le moins spartiate. Les développeurs utilisent souvent millis et lorsque l'on a plusieurs timers à gérer le code devient vite sac de nouilles.

Pourquoi ne pas bâtir une librairie qui permettent de gérer un ou plusieurs timers ?

Ou pourrait résumer l'interface à celle-ci :
  • créer  le timer
  • armer le timer
  • consulter le timer
  • le timer pourrait aussi appeler une fonction fournie par l'utilisateur lorsque le temps est écoulé.
Des choses existent déjà :
Oui, mais ces librairies monopolisent un timer pour chaque utilisation. Ce n'est pas suffisant si l'on a besoin de plus de deux timers, surtout sur un ARDUINO NANO qui n'en possède que deux.

Sur ESP8266 et ESP32 la librairie le kit de développement dispose déjà de timers modernes qui correspondent exactement au besoin.
Mais comme toujours dans le monde Espressif, chaque microcontrôleur a une interface logicielle différente. Pourquoi simplifier la vie des développeurs quand on peut s'en dispenser ?

On pourrait tout simplement exploiter les timers ESP8266 et ESP32 pour les intégrer dans la librairie. L'interface serait donc identique pour ARDUINO, ESP8266 et ESP32.

1. Le besoin

L'idée de base serait de reprendre l'idée de TimerOne ou Timer2, c'est à dire accrocher une routine d'interruption au vecteur TIMER1 ou TIMER2. A partir de cette routine gérer plusieurs instances purement logicielles ne devrait pas être bien complexe.

J'ai donc déterré une idée que j'avais eu à il y a dix ans : porter l'interface de timers du kernel LINUX :
  • void init_timer(struct timer_list * timer);
  • void setup_timer(struct timer_list timer, void (function)(unsigned long), unsigned long data );
  • int add_timer(struct timer_list* timer);
  • void mod_timer(struct timer_list* timer, unsigned long expires);
  • int del_timer(struct timer_list* timer);
init_timer crée un timer
setup_timer permet de fournir une fonction utilisateur
add_timer ajoute le timer au système
mod_timer arme le timer
del_timer le détruit

A l'époque ce portage avait été fait sur MSP430. Un ARDUINO sera parfaitement à l'aise. Le code de la routine d'interruption est très légère.

Un timer est un élément de liste chaînable, et nous pourrons en créer autant que nécessaire.

Un petit exemple de ce que cela pourrait donner :

#include <kernel_timers.h>

#define TIMEOUT          (HZ*5)

struct timer_list timer;

void setup()
{
  Serial.begin(115200);
  // initialize timers system with TIMER1
  timer_init(1);
  init_timer(&timer);
  add_timer(&timer);
  Serial.println("\nStart the timer for 5 seconds");
  mod_timer(&timer, jiffies + TIMEOUT);
}

void loop()
{
  if (timer.expires != 0 && timer.expires == jiffies) {
    Serial.println("Timer has elapsed");
    mod_timer(&timer, 0);
  }
} 

HZ représente la fréquence de d'actualisation des timers (l'interruption).
jiffies représente le temps courant.
Donc jiffies + (5 * HZ) représente le temps courant + 5 secondes.

Une explication des jiffies sous WIKIPEDIA : https://en.wikipedia.org/wiki/Jiffy_(time)

Notre librairie aura une valeur de HZ de 200. Cela nous donnera une résolution de 5 millisecondes pour nos timers.

La fonction d'initialisation de la librairie accepte un paramètre timer. Il s'agit du numéro de timer hardware :
  • sur Atmega328 vous avez le choix entre 1 et 2
  • sur Atmega2560 vous avez le choix entre 1,2, 3, 4 et 5
  • sur ESP8266 et ESP32 ce paramètre est ignoré

2. La librairie

La librairie est disponible ici :
https://bitbucket.org/henri_bachetti/kernel_timers.git

Cette page vous donne toutes les informations nécessaires :
https://riton-duino.blogspot.com/p/migration-sous-bitbucket.html

Quelques  exemples d'utilisation sont fournis.

Tous les exemples tournent sur ARDUINO UNO, MEGA, ESP8266 et ESP32.


Cordialement

Henri BACHETTI

samedi 16 mars 2019

Arduino, ESP8266, ESP32 et STM32 : une librairie console / logger



Arduino, ESP8266, ESP32 et STM32

une librairie console / logger


Dans cet article je vais vous présenter un développement récent, une librairie composée de deux parties :
  • une partie affichage sur la console, appelée moniteur série dans le monde ARDUINO
  • un logger permettant d'afficher ou d'enregistrer des messages de log.
Cette librairie doit être utilisable sur les plateformes suivants :
  • ARDUINO
  • ESP8266
  • ESP32
  • STM32

1. Les notions de base

Dans un logiciel, lorsque l'on affiche des messages sur une console, ceux-ci peuvent être de différents type :
  • messages à destination de l'utilisateur
  • messages d'erreur, warning, erreur critique
  • messages destinés à déverminer le logiciel (logging)
Ces messages sont affichés sur un terminal. Le moniteur série de l'IDE ARDUINO en est un, mais ce n'est pas le seul possible. Il en existe d'autres :
  • minicom, picocom, kermit sous Linux
  • teraterm sous Windows
  • etc.
Classiquement dans un logiciel en C, si l'application ne dispose pas d'un écran graphique, les messages à destination de l'utilisateur sont affichés via la sortie standard stdout à l'aide de fonctions telles que putchar, puts, printf, etc.

Les messages d'erreur sont affichés via la sortie standard stderr à l'aide de fonctions telles que fprintf, perror, etc.

En langage C, les messages destinés à déverminer le logiciel utilisent des mécanismes peu standardisés. On appelle ces mécanismes des loggers. Sous Linux,on utilise généralement syslog.
Un logger peut diriger les messages à afficher vers la console, un fichier ou une liaison Ethernet.

Un langage moderne comme PYTHON possède un système de logging évolué permettant la création de logs hautement personnalisables :
  • activation dynamique
  • affichage sur la console
  • création de fichiers de logs
  • création de fichiers de logs avec rotation
  • envoi vers une socket
  • envoi vers une base de données 
  • etc.
Un fichier de log, en français "fichier journal", est un fichier texte où l'on enregistre des messages pouvant être consultés ultérieurement :
  • messages de diagnostic
    • traces de debug
    • erreurs
    • problèmes de communication
  • événements
    • démarrage
    • pannes
    • défauts
  • tentatives d'accès  à un serveur WEB
  • etc.
Sur les plateformes ARDUINO on ne dispose pas de ces facilités, à moins d'installer une librairie.
Sur ESP8266 ou ESP32 une sortie de logs est prévue mais elle ne permet pas de créer des fichiers de logs.

La librairie présentée ici se propose de remédier partiellement à ces manques.

2. Les différentes architectures

Les différentes plateformes testées sont :
  • ARDUINO UNO
  • ARDUINO MEGA
  • ESP8266 NodeMCU (ESP12E) 
  • ESP32 VROOM
Je vous propose un tableau pour résumer les différentes possibilités en matière de sortie sur ligne série :

Carte Serial Serial1 Serial2 Serial3 Soft Serial
UNO NANO Oui


Oui
MEGA Oui Oui Oui Oui Oui
ESP8266 Oui Oui (1)

Oui
ESP32 Oui Oui (2) Oui
Oui
STM32 Oui (3) (3) (3)

(1) limité à la ligne TX
(2) toutes les cartes ne possèdent pas les pins appropriées
(3) Tout dépend de la carte utilisée

2.1. Le HardwareSerial

Le HardwareSerial repose sur un périphérique natif du microcontrôleur, un UART ou USART. L'envoi et la réception des bits à la bonne cadence sont assurés par l'UART.
Dans le tableau ci-dessus les lignes série Hard correspondent aux colonnes Serial à Serial3.

2.2. Le SoftwareSerial

Le SoftwareSerial repose sur une émulation logicielle d'UART. L'envoi et la réception des bits à la bonne cadence sont assurés par du code. Il faut donc installer une librairie.

ARDUINO : https://github.com/PaulStoffregen/SoftwareSerial.git
ESP8266 :  installée par défaut dans le package support
ESP32 : https://github.com/akshaybaweja/SoftwareSerial.git

La librairie SoftwareSerial pour ESP32 doit être installée dans le répertoire où se trouve le package support ESP32 :
Sous Linux :
/home/username/.arduino15/packages/esp32/hardware/esp32/1.0.1/libraries/
Sous Windows :
C:\Users\username\AppData\Local\Arduino15\packages\esp32\hardware\esp32\1.0.1/libraries/

2.3. L'ARDUINO

La sortie standard existe mais il faut lui affecter un flux. On procède comme ceci :

static FILE uartout = {0} ;

static int console_putchar (char c, FILE *stream)
{
  if (c == '\n') Seiral.write('\r');
  Serial.write(c);
  return 0;
}


void setup(void)
{
  fdev_setup_stream (&uartout, console_putchar, NULL, _FDEV_SETUP_WRITE);
  stdout = &uartout;

}

Ensuite les fonctions standard putchar, printf et puts sont parfaitement utilisables.

L'ARDUINO ne possède aucun mécanisme de sortie de logs. Il faut ajouter une librairie.

Cette page explique comment rendre le logging dépendant d'une directive de compilation :
https://arduino103.blogspot.com/2012/09/define-debug-une-methode-de-debugging.html

Cette page donne explique comment sortir des logs sur un SoftwareSerial :
https://wolfgang-ziegler.com/blog/serial-debugging-on-arduino

Celles-ci permettent d'afficher des messages sur Serial uniquement :
https://github.com/ptlug/Arduino-logging
https://github.com/thijse/Arduino-Log

Celle-ci permet d'afficher des messages sur HardwareSerial ou SoftwareSerial :
https://github.com/mrRobot62/Arduino-logging-library

Une autre librairie beaucoup plus élaborée, permettant d'activer les logs dynamiquement :
https://github.com/JoaoLopesF/SerialDebug.git

2.4. L'ESP8266

Sur ESP8266 la sortie standard s'active comme ceci :

void setup(void)
{
  Serial.setDebugOutput(true);
}

Malheureusement cela ne fonctionne pas pour la sortie Serial1 ou Serial2. Cela fonctionnait pourtant en version 2.2.0 mais cela ne fonctionne plus en 2.5.0.
L'utilisation n'est pas possible avec un SoftwareSerial.

La librairie standard de l'ESP8266 possède une sortie de DEBUG activable par l'IDE ARDUINO :
https://arduino-esp8266.readthedocs.io/en/latest/Troubleshooting/debugging.html

Cette sortie est utilisable par le développeur d'applications :

  DEBUG_ESP_PORT.printf("output from \n", "DEBUG_ESP_PORT");

Cette sortie, lorsqu'elle est redirigée vers Serial1, a un petit défaut. L'affichage d'un '\n' provoque un saut à la ligne suivante mais pas de retour chariot.
En utilisant Serial le comportement est correct.

2.5. L'ESP32

Sur ESP32 la sortie standard s'active comme ceci :

void setup(void)
{
  Serial.setDebugOutput(true);
}

Comme pour l'ESP8266 cela ne fonctionne pas pour la sortie Serial1 ou Serial2.
L'utilisation n'est pas possible avec un SoftwareSerial.

L'ESP32 possède également sa propre librairie de logging :
https://docs.espressif.com/projects/esp-idf/en/latest/api-reference/system/log.html

Cette sortie est utilisable aussi par le développeur d'applications :

#include <esp_log.h>

  static const char *TAG = "example";
  esp_log_level_set(TAG, ESP_LOG_VERBOSE);
  ESP_LOGI(TAG, "output from %s\n", "ESP32_LOG");


Avant la compilation il faudra sélectionner un niveau de log par défaut : voir Outils/Core DebugLevel.

Cette sortie, sur Serial ou lorsqu'elle est redirigée vers Serial2, fonctionne correctement.

3. Le bilan

En résumé que faut-il faire ?
En premier lieu réussir à rediriger la sortie standard vers la sortie série de notre choix, et ceci quelle que soit la plateforme.

On peut utiliser ensuite une des librairie de logging citées au chapitre 2 ou développer une solution.

3.1. La sortie console standard


3.1.1. L'ARDUINO
Cela va être très simple. La sortie standard va être redirigée vers la console choisie à l'aide de la méthode décrite au chapitre 2.
Comme nous écrivons nous-même notre fonction de redirection, elle peut faire un travail très simple :

static FILE uartout = {0} ;

static int console_putchar (char c, FILE *stream)
{
  if (c == '\n') console->write('\r');
  console->write(c);
  return 0;
}


Tout passera donc par cette fonction.

3.1.2. L'ESP8266 et l'ESP32

Nous avons constaté certains défauts de la librairie standard :
  • la redirection de la sortie standard vers Serial1 ou Serial2 ne fonctionne pas
  • la redirection de la sortie standard vers SoftwareSerial n'existe pas
  • la redirection de la sortie log vers Serial1 ne fonctionne pas sur l'ESP8266
  • la redirection de la sortie log vers SoftwareSerial n'existe pas
Comme nous ne pouvons pas intervenir dans la librairie nous allons redéfinir printf, puts et putchar dans un fichier console.h :

#define printf console_printf
#define puts console_puts
#define putchar console_putc


Tout passera donc par ces fonctions. Par contre pour que cela marche il faudra inclure le fichier console.h dans chaque source.

3.1. Le logger

Si l'on a besoin d'un logger ayant les mêmes fonctionnalités que celui de l'ESP32 il faut le développer. L'avantage est qu'il aura une API identique sur les plateformes ARDUINO, ESP8266 et ESP32

4. Développement d'un logger

Peu d'amateurs ARDUINO savent qu'il existe des librairies de logging.

Lorsque l'on veut déverminer son sketch on ajoute généralement des lignes de Serial.println().

Ensuite lorsque le logiciel fonctionne, en principe on retire ces lignes. C'est dommage car on peut fort bien en avoir besoin par la suite pour corriger un bug ou faire une évolution.

4.1. Définir des macros

Une première astuce consiste à définir des macros :

#define LOGS_ACTIVE

#ifdef LOGS_ACTIVE
#define log_print       Serial.print
#define log_println     Serial.println
#else
#define log_print
#define log_println
#endif

void setup()
{
  Serial.begin(115200);
  log_println("***** BOOT *****");
}

void loop()
{
}


Ainsi, au lieu de retirer les lignes d'affichage de logs, on met simplement la définition de LOGS_ACTIVE en commentaire et les logs ne seront plus affichés.

Comment cela fonctionne t-il ? Le préprocesseur évalue ces lignes de la manière suivante :

  log_println("***** BOOT *****");

Si LOG_ACTIVE est défini, lorsque le mot log_println est rencontré il est remplacé par Serial.println. La ligne devient  :

  Serial.println("***** BOOT *****");

Si LOG_ACTIVE nest pas défini, lorsque le mot log_println est rencontré il est remplacé par du vide. La ligne devient  :

  ("***** BOOT *****");

Cette ligne ne comporte pas d'erreur de syntaxe, mais comme elle n'a pas d'utilité, elle ne sera pas compilée. L'effet sera identique à celui obtenu en supprimant la ligne.

4.2. Activation dynamique

Ensuite on peut ajouter la possibilité d'activer ou de désactiver les logs de manière dynamique sur plusieurs niveaux :
  • messages permanents
  • messages de debug
  • messages d'erreur
#include <SoftwareSerial.h>

#if defined ESP8266 || defined(ESP32)
SoftwareSerial swSer(14, 12, false, 256);
#else
SoftwareSerial softSerial(10, 11);
#endif

#define LOGS_ACTIVE

#define LOG_DEBUG     2
#define LOG_ERROR     1

#define log_port Serial

#ifdef LOGS_ACTIVE
int log_level;

void log_setLevel(int level) {
  log_level = level;
}

void log_always(const char *msg)
{
  log_port.println(msg);
}

void log_debug(const char *msg)
{
  if (log_level >= LOG_DEBUG) {
    log_port.println(msg);
  }
}

void log_error(const char *msg)
{
  if (log_level >= LOG_ERROR) {
    log_port.println(msg);
  }
}

#else
#define log_setLevel
#define log_always
#define log_debug
#define log_error
#endif

void setup()
{
  log_port.begin(115200);
  log_always("***** BOOT *****");
  log_setLevel(LOG_DEBUG);
}

void loop()
{
  log_debug("This is a DEBUG information");
  log_error("This is an ERROR information");
  delay(2000);
}


Avec ce code il est facile d'activer les logs sur trois niveaux :
  • log_setLevel(0);                          // voir seulement les messages permanents
  • log_setLevel(LOG_ERROR);    // voir seulement les messages d'erreur
  • log_setLevel(LOG_DEBUG);    // voir tous les messages
Il est également possible d'utiliser une ligne série différente de Serial :
  • #define log_port Serial              // Serial
  • #define log_port Serial1            // Serial1
  • #define log_port Serial2            // Serial2
  • #define log_port Serial3            // Serial3
  • #define log_port sofSerial         // SoftwareSerial
Si l'on travaille en C++ il est également possible d'instancier un objet logger par fichier source et activer ou non les logs pour chacun d'eux.

Et il reste encore la possibilité de mettre la définition de LOGS_ACTIVE en commentaire pour supprimer les logs.

4.3. Expression du besoin

Notre logger, contrairement à la console sera développé sous forme de classe. Il sera instancié soit de manière unique soit dans chaque fichier source de l'application :

Logger LOGGER("test");
// ou
static Logger LOGGER("test");

Il sera possible d'instancier plusieurs loggers bien entendu. Par exemple les logs d'accès d'un serveur WEB seront enregistrés dans des fichiers. Les logs de debug seront simplement affichés.

Les fonctions de sortie de logs de la librairie accepteront des arguments variables, comme printf() ou les fonction de logs ESP32 :

void Logger::debug(const char *format, ...);

Nous allons factoriser tout cela et développer une fonction du type vprintf :

void Logger::vprintf(const char *format, va_list ap);

Les fonctions de logging utiliseront toutes cette fonction et donc tout passera par elle.

Les logs pourront être redirigés vers différents supports physiques. Chaque support sera géré par une classe Handler :
  • handler Stream (console standard, HardwareSerial ou SoftwareSerial)
  • handler de fichier sur SPIFFS
  • handler de fichier sur µSD
Bien entendu il est possible d'écrire un handler personnalisé (socket, syslog, MYSQL, etc.)
La librairie fournit un handler pour créer des fichiers dans le système de fichiers SPIFFS de l'ESP8266 et ESP32
Le logger possédera différentes classes de formatage de message :
  • formatage simple (formateur par défaut)
  • formatage indiquant la source du message
  • formatage avec horodatage par timestamp en milliseconde
  • formatage avec horodatage par date et heure
Bien entendu il est possible d'écrire un formateur personnalisé
Dans le cas où les logs sont générés dans des fichiers, la récupération de ceux-ci sera confiée à un serveur FTP ou HTTP.

4.4. L'application

La première des choses à faire sera, dans l'application, d'initialiser la ou les lignes série choisies :

  Serial.begin(115200);

Ensuite nous passerons l'adresse de cette instance du type Stream aux fonctions d'initialisation de la librairie. La console et le ou les loggers pourront fonctionner avec deux lignes série différentes, ou rediriger les messages vers un fichier.

Cette adresse sera mémorisée par la fonction console_init() dans une variable du type Stream :

Stream *console_output;

Elle sera aussi mémorisée par le constructeur de l'instance du logger ou du handler choisi.

Un exemple avec un logger sur Serial et un logger sur SD :

Logger LOGGER("http");
LogNameFormatter formatter(&LOGGER);
StreamLogHandler streamHandler(&formatter, &Serial);
Logger ACCESS("access");
LogTimeDateFormatter logFormatter(&ACCESS);
SdFatFileLogHandler logHandler(&logFormatter, &SD, "/access.log", 10000, 5);
void setup()
{
  Serial.begin(115200);
  console_init(&Serial);
  printf("ESP32 SDFAT logging WebServer demo\n");
  LOGGER.init(&Serial);  LOGGER.setLevel(DEBUG);
  formatter.setHandler(&streamHandler);  LOGGER.setFormatter(&formatter);
  ACCESS.init(&Serial);
  ACCESS.setLevel(INFO);
  logFormatter.setHandler(&streamHandler);  ACCESS.setFormatter(&logFormatter);
  if (SD.begin(SS)) {
    log_info("SD Card initialized.");
    logFormatter.setHandler(&logHandler);    log_info(F("SdFatFileLogHandler installed"));
  }
}


Dans cet exemple, on passe au formateur logFormatter le handler streamHandler, donc de type Stream.

Si la SD est montée avec succès, le handler est changé pour que les messages du logger ACCESS soient enregistrés. Sinon on continue en affichant sur le handler streamHandler.

Afin de simplifier les choses, comme cela correspond à la majeure partie des utilisations, le logger pourra être créé avec l'adresse d'un Stream en paramètres. Il pourra créer ainsi lui-même son formateur et son hander par défaut.

Un exemple avec un seul logger sur Serial :

Logger LOGGER("test");

void setup()
{
  Serial.begin(115200);
  console_init(&Serial);
  printf("Basic logging demo\n");
  LOGGER.init(&Serial);
  LOGGER.setLevel(DEBUG);
}


5. La librairie

La librairie est disponible ici :
https://bitbucket.org/henri_bachetti/mpp-console-logger.git

Cette page vous donne toutes les informations nécessaires :
https://riton-duino.blogspot.com/p/migration-sous-bitbucket.html

De nombreux exemples d'utilisation sont fournis.

Certains exemples, comme leur nom l'indique, sont dédiés :
  • ESP32-sdfat : logs sur µSD.
  • ESP32-sdfat-http-server : WebServer avec logs sur µSD
  • ESP32-spiffs : logs sur SPIFFS.
  • ESP32-spiffs-http-server : WebServer avec logs sur SPIFFS
  • ESP8266-spiffs : logs sur SPIFFS.
Les serveurs WEB des exemples permettent de visualiser les logs et de les télécharger.

Les exemple suivants sont utilisables sur une UNO, NANO ou MINI :
  • console
  • basic-logger
  • basic-logger-handler
  • name-log-formatter
  • timedate-log-formatter
  • timestamp-log-formatter
  • soft-serial-logger
Les exemple suivants sont utilisables sur une MEGA  ou un STM32 :
  • tous les exemples précédents
  • dual-serial-logger
Les exemple suivants sont utilisables sur un ESP8266 :
  • tous les exemples précédents
  • ESP-spiffs
Les exemple suivants sont utilisables sur un ESP32 :
  • tous les exemples précédents
  • ESP32-sdfat
  • ESP32-sdfat-http-server
  • ESP32-spiffs-http-server
Exemple basic-logger sur une UNO :
Le sketch utilise 4478 octets (13%) de FLASH et 418 octets (20%) de RAM.

Exemple soft-serial-logger sur une UNO :
Le sketch utilise 6846 octets (21%) de FLASH et 659 octets (32%) de RAM.

Cordialement
Henri

6. Mises à jour

18/03/2019 : exemples de serveurs WEB
23/03/2019 : ajout du STM32

vendredi 8 mars 2019

Arduino: sauvegarde de données en EEPROM en cas de coupure d'alimentation


Arduino: sauvegarde de données en EEPROM

en cas de coupure d'alimentation



Dans cet article nous allons décrire une méthode de sauvegarde de données en EEPROM en cas de coupure brutale de l'alimentation d'un ARDUINO.

1. Principe

La méthode repose sur le maintien de l'alimentation par un condensateur ayant suffisamment de capacité pour permettre l'écriture de la quantité de données à sauvegarder. La détection de la coupe d'alimentation est réalisée en appliquant une partie de la tension d'alimentation sur une pin digitale du microcontrôleur :
Le condensateur C1 se charge à travers une diode si le 5V est présent.
L'entrée digitale D2 reçoit une partie de la tension d'alimentation à travers un pont diviseur de tension.

L'entrée D2 va pouvoir générer une interruption qui sera traitée rapidement par le logiciel.
Un ARDUINO UNO possède deux pins pouvant générer une interruption : D2 et D3.
Un ARDUINO MEGA en possède 6 : 2, 3, 18, 19, 20, 21

La diode D1 empêche le condensateur de se décharger dans le pont diviseur.

Lors de la mise sous tension la valeur de la tension à ses bornes va croître rapidement et approcher les 5V. Cette tension va dépendre de la diode utilisée.
La chute de tension provoquée par la diode, dite VF pour "forward voltage" peut être plus ou moins élevée. De plus elle dépend du courant traversant la diode.
Mesurons celle-ci pour quelques diodes courantes, avec un courant de 30mA, la consommation typique d'un ARDUINO UNO :
  • 1N4001 : 0.7V
  • BAT46 : 0.5V
  • 1N5819 : 0.3V
On voit tout de suite que la 1N5819 provoque une chute de tension nettement inférieure et permettra d'assurer 4.7V d'alimentation à l'ARDUINO.

Ceci nous amène à expliquer l'utilité du pont diviseur de tension.
Nous avons 5V en amont de la diode et 4.7V en aval de celle-ci et donc aussi 4.7V sur la broche VCC de l'ARDUINO.
Il est assez peu recommandé d'appliquer sur une entrée d'un microcontrôleur une tension supérieure à celle de l'alimentation. Même s'il y a peu de chances que cette différence de tension provoque des dégâts, car elle est faible, elle pourrait entraîner une surconsommation.
Avec ce pont diviseur, la tension sur l'entrée est ramenée à :
Vd2 = 5V * (R1 + R2) / R2 = 5V / (100K + 1M) * 1M = 4.54V

Cette tension est suffisante pour être vue comme un niveau haut par l'entrée D2.

2. Fonctionnement

Lors de la mise sous tension de l'alimentation, le condensateur C1 se charge à 4.7V et cette tension alimente l'ARDUINO.
Le microcontrôleur voit 4.5V sur son entrée D2.

Lorsque l'alimentation disparaît, le condensateur C1 se décharge dans l'ARDUINO et décroît lentement (plus ou moins rapidement suivant la taille du condensateur), jusqu'à atteindre une valeur ne permettant plus le fonctionnement du microcontrôleur.
Le microcontrôleur voit rapidement 0V sur son entrée D2. C'est la capture de cet événement qui va déclencher la sauvegarde de nos précieuses données dans l'EEPROM.
Dans le sketch décrit plus bas, on verra que cette information est traitée sous interruption.

3. Calculs

Arrivés à ce point, nous avons quelques inconnues :
  • quelle est la tension minimale de fonctionnement de l'ARDUINO ?
  • quel temps va prendre la sauvegarde de nos données ?
  • quelle valeur donner au condensateur C1 ?

3.1. Tension minimale

Comment connaître la tension minimale de fonctionnement de l'ARDUINO ?
L'idée est de charger un sketch faisant clignoter la LED D13 à 10Hz. Ensuite on alimente par la broche 5V et on débranche le cordon USB.
La LED clignote toujours. Diminuons la tension doucement jusqu'à ce que la LED ne clignote plus.

Le résultat obtenu : la LED clignote encore à 3V. J'obtiens le même résultat avec un autre multimètre.
Cet exemplaire de UNO est exceptionnel !

On ne peut pas considérer que cette valeur sera vérifiée sur un nombre important de cartes.
J'ai déjà eu entre les mains une carte NANO que j'avais alimenté par erreur en 3.7V et qui fonctionnait aléatoirement.

Nous allons plutôt adopter une valeur de 4V.

3.2. Durée de sauvegarde

Nous allons parler des données à sauvegarder mais par seulement.
En effet il serait bien de s'assurer que les données de l'EEPROM soient effectivement nos données, et pas des valeurs au hasard, comme celles qui sont présentes dans l'EEPROM d'un ARDUINO n'ayant jamais servi.
Lors de la relecture, il va falloir vérifier que nos données soient valides.

Une approche simple consiste à écrire un nombre dit "magic" au débutde nos données.
A la relecture, ce nombre magique sera comparé à sa valeur supposée.

On peut faire encore mieux : un CRC16 par exemple (voir en fin de document).

Il serait intéressant d'enregistrer aussi la date et l'heure de la sauvegarde, si le système possède un moyen quelconque d'obtenir cette information :
  • circuit RTC
  • liaison avec un ordinateur ou un serveur NTP
  • etc.
Nous avons déjà une bonne idée du contenu. Nous allons l'organiser en structure :

#define MAGIC     0xDEADBEEF
#define EEPROM_ADDR 0

struct eeprom_data
{
  time_t timeStamp;
  char data[160];
  unsigned long magic;
};

struct eeprom_data eepromData;


Ensuite nous allons écrire une fonction de sauvegarde et l'appeler dans le code pour mesurer son temps d'exécution :

void powerLoss()void powerLoss()
{
  unsigned long start, stop;
  start = micros();
  EEPROM.put(EEPROM_ADDR + offsetof(struct eeprom_data, magic), 0);
  EEPROM.put(EEPROM_ADDR, eepromData);
  EEPROM.put(EEPROM_ADDR + offsetof(struct eeprom_data, magic), MAGIC);
  stop = micros();
  Serial.print("LOSS POWER: ");
  Serial.print(stop - start);
  Serial.println("µs");
}

Le résultat obtenu : 620µs.
La quantité de données sauvegardée est de 160 + 4 + 4 = 168 octets.

La sauvegarde des données d'heure, date et magic (2 fois) dure donc 620µs / 168 * 10 = 37µs
La sauvegarde des 160 octets de données prend le reste, c'est à dire 583µs.

Il sera donc facile de faire un calcul en fonction de la vraie quantité de données à sauvegarder.

3.3. Valeur du condensateur

Lorsque l'alimentation va disparaître le condensateur va se décharger comme ceci :


Cela va très vite semble t-il mais pas d'affolement, tout dépend de la consommation de la carte et de la taille du condensateur.

Tout d'abord intéressons-nous à la valeur que représente notre tension minimale de fonctionnement par rapport à la tension nominale :

rapport = Vmin / Vmax  = 0.85 = 85%

Pour rappel on appelle constante de temps d'un circuit RC le temps que met un condensateur pour se charger à 63% (ou à se décharger de 63%) à travers une résistance.
Le temps de charge ne nous intéresse pas car la charge se fait pratiquement sans résistance.
Seule la décharge nous intéresse. On peut dire que la constante de temps définit le temps au bout duquel le condensateur aura perdu 63% de sa tension. Il nous restera donc 37%.

Dans un circuit RC composé d'une résistance de 1KΩ et d'un condensateur de 1µF le condensateur se déchargera donc de 63% de la tension appliquée à l'ensemble du circuit au bout de :
t = R * C = 0.000001F * 1000Ω = 0.001s

Or 37% ne nous intéresse pas, nous voulons 85%.
Sur la courbe ci-dessus ces 85% sont atteints au bout d'un temps égal à environ 0,15 * T.

Pour vérifier cette valeur il va nous falloir résoudre une équation différentielle.

Uc(t) = U(exp(-t / RC))

A l'aide d'un tableur on peut tracer une courbe plus détaillée du début de la décharge :


On voit qu'avec 1000Ω et 1000µF (RC = 1s) on a bien 0.15s à 85% d'alimentation.

Quelle est notre résistance ?
Dans notre cas, il s'agit d'un ARDUINO qui consomme environ 25mA s'il est alimenté par sa broche 5V :

R = U / I = 5V / 0.025A = 200Ω

Pour un montage consommant plus, il faudra recalculer cette valeur.

En première approximation pour assurer nos 620µs de maintien de l'alimentation il nous faudrait un condensateur de :

C = T / R / 0.15 = 0.000620s / 200 / 0.15 = 0,00002066 = 20µF.

Il nous faut donc tracer une courbe de la tension du condensateur (Uc) en fonction du temps (t) et de la constante RC.

En modifiant les valeur de R et C on obtient ceci :


On voit qu'avec 200Ω et 20µF on atteint bien notre objectif de 600µs à 85% d'alimentation.

C'était la théorie, mais que dit la pratique ?

4. Les essais

L'heure est donc venue d'essayer de voir si cela fonctionne.

Voici le petit sketch utilisé pour les tests :

#include <time.h>
#include <EEPROM.h>

#define MAGIC       0xDEADBEEF
#define EEPROM_ADDR 0

struct __attribute__ ((packed)) eeprom_data
{
  unsigned long magic;
  time_t timeStamp;
  char data[160];
};

struct eeprom_data eepromData;

bool backup;

void powerLoss()
{
  unsigned long start, stop;
  start = micros();
  EEPROM.put(EEPROM_ADDR + offsetof(struct eeprom_data, magic), 0);
  EEPROM.put(EEPROM_ADDR, eepromData);
  EEPROM.put(EEPROM_ADDR + offsetof(struct eeprom_data, magic), MAGIC);
  stop = micros();
  Serial.print("LOSS POWER: ");
  Serial.print(stop - start);
  Serial.println("µs");
}

void setup()
{
  struct eeprom_data data;
  time_t t;

  Serial.begin(115200);
  set_system_time(getTimeFromAnywhere());
  time(&t);
  Serial.println(ctime(&t));
  attachInterrupt(digitalPinToInterrupt(2), powerLoss, FALLING);
  EEPROM.get(EEPROM_ADDR, data);
  if (data.magic != MAGIC) {
    Serial.println("No Backup available");
  }
  else {
    memcpy(&eepromData, &data, sizeof(data));
    Serial.println("Valid Backup data has been found in the EEPROM: ");
    Serial.print("MAGIC: "); Serial.println(data.magic, HEX);
    Serial.print("DATE: "); Serial.println(ctime(&eepromData.timeStamp));
    Serial.print("DATA: "); Serial.println(eepromData.data);
  }
}

void loop()
{
  time(&eepromData.timeStamp);
  strcpy(eepromData.data, "MY-PRECIOUS-DATA");
  Serial.println("Zzzzzzzzzzzzz");
  delay(30000);
}

// get time from RTC, NTP, etc.
time_t getTimeFromAnywhere(void)
{
  char s_month[5];
  struct tm t = {0};
  int s, m, h, d, y;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";

  sscanf(__DATE__, "%s %d %d", s_month, &d, &y);
  sscanf(__TIME__, "%d:%d:%d", &h, &m, &s);
  t.tm_sec = s;
  t.tm_min = m;
  t.tm_hour = h;
  t.tm_mday = d;
  t.tm_mon = (strstr(month_names, s_month) - month_names) / 3;
  t.tm_year = y - 1900;
  t.tm_isdst = -1;
  return mktime(&t);
}

Dans la fonction loop(), les données - une structure en RAM - sont mises à jour régulièrement, mais elles restent en RAM.

Dans la fonction setup() :

  attachInterrupt(digitalPinToInterrupt(2), powerLoss, FALLING);

Cet appel permet d'accrocher la fonction powerLoss() au vecteur d'interruption de la pin D2. Cette fonction sera appelée y compris pendant l'exécution de la routine delay(30000).

Dans la fonction powerLoss le nombre magique est mis à zéro au départ. Cela permet de s'assurer que la fonction aura bien le temps d'écrire l'intégralité des données plus le nombre magique.
Si celui-ci est relu et qu'il vaut zéro, cela voudra dire que

Dans la fonction setup(), la validité des données en EEPROM est vérifiée le nombre magic de l'EEPROM est comparé au nombre magic 0xDEADBEEF. Si le nombre correspond, les données sont recopiées dans la structure eepromData.

Passons à la manipulation proprement dite.

Le montage est donc alimenté en 5V en amont de la diode avec une alimentation de labo.

La manipulation est la suivante :
  • brancher l'alimentation
  • installer un condensateur de 22µF
  • charger le sketch
  • débrancher l'USB
  • couper l'alimentation
  • rebrancher l'USB
  • l'ARDUINO affiche le contenu de l'EEPROM
  • le contenu est correct
  • brancher l'alimentation
  • installer un condensateur de 10µF
  • modifier les données à sauvegarder (le nombre magic par exemple)
  • recharger le sketch
  • débrancher l'USB
  • couper l'alimentation
  • rebrancher l'USB
  • l'ARDUINO affiche le contenu de l'EEPROM
  • le contenu est correct
  • etc.

Avec 4.7µF cela marche encore, mais pas en dessous. Avec un condensateur de 2.2µF, la sauvegarde n'a pas eu lieu. La réserve d'énergie n'est pas assez importante.

Par rapport aux 20µF calculés cela peut paraître étrange que 4.7µF soient suffisants.

Il faut dire que la marge de sécurité prise en adoptant une tension minimale de fonctionnement de 4V est importante, et que la carte utilisée fonctionne encore à 3V. Mais cette marge est obligatoire, car rien ne permet d'affirmer qu'une autre carte fonctionnerait de la même façon dans les mêmes conditions.

5. Alimenter par VIN

Bien entendu, on pourrait faire la même manipulation en alimentant la carte par sa broche VIN.
Cela complique un peu les choses car la tension de chute (drop-out) du régulateur 5V va avoir une influence. Certains régulateurs ont un drop-out de 1V, d'autre plutôt 2V.
Ensuite, il est difficile de dire comment va se comporter le régulateur quand la tension va chuter.

Mais pour assurer la sauvegarde on peut prendre une bonne marge de sécurité, et surtout tester ensuite.

En admettant qu'il faut au minimum 7V sur VIN pour assurer un bon 5V à l'ARDUINO, le pont diviseur doit être recalculé :

Avec 470KΩ et 1MΩ j'obtiens :
Vd2 = 7V * (R1 + R2) / R2 = 7V / (470K + 1M) * 1M = 4.65V
 il n'est même pas nécessaire de refaire les calculs. Le résultat doit être le même, et le test le confirme.

La solution fonctionnera aussi avec du 12V sur VIN, probablement avec un condensateur encore plus petit.

Le pont diviseur doit être recalculé :

Avec 750KΩ et 470KΩ j'obtiens :
Vd2 = 12V * (R1 + R2) / R2 = 12V / (750K + 470K) * 470K = 4.62V

Dans cette configuration, le montage fonctionne presque avec 2.2µF. Seuls les deux derniers octets n'ont pas été écrits lors de la sauvegarde !
Cela ne semble pas anormal. Partant d'une tension plus haute, l'alimentation de l'ARDUINO reste à une valeur élevée plus longtemps.

Comme nous l'avons déjà dit, cette valeur de 4.7µF est une valeur qui convient pour l'exemplaire de carte testée. Par sécurité, nous adopterons donc la valeur de condensateur calculée : 20µF.

Comme il faut toujours choisir une marge de sécurité par rapport au calcul, la valeur de 47µF sera retenue.

Cette valeur a été calculée pour pouvoir sauvegarder 160 octets. Pour sauvegarder plus de données, il suffira d'augmenter la valeur proportionnellement.

6. Choix des composants

Bien entendu la diode a une importance capitale. Avec une 1N5819 l'exemplaire d'ARDUINO UNO que je viens de tester est capable de sauvegarder 160 octets en 620µs avec un condensateur de 4.7µF.
Avec une 1N4001 la valeur du condensateur monte à 33µF pour que la sauvegarde fonctionne, soit 6 fois plus de capacité.
Si l'on prend en compte la valeur calculée, c'est à dire 20µF avec la 1N5919, il faudrait augmenter la valeur du condensateur à 120µF si l'on adoptait une 1N4001.

Le condensateur sera choisi en fonction de la tension maximale présente à ses bornes, avec une marge de sécurité d'environ 25% :
  • 5V : condensateur de 6.3V
  • 7V : condensateur de 10V
  • 12V : condensateur de 16V
Comme il s'agit de condensateurs de forte valeur, un condensateur électrolytique conviendra parfaitement.

Vous aurez du mal à trouver des condensateurs de 10µF en dessous de 16V. Ce n'est pas catastrophique, remplacer un condensateur de 10µF 6.3V introuvable par un 22µF 6.3V ou 10V est envisageable. Qui peut le plus peut le moins.

Parlons de la taille. Un condensateur électrolytique de 47µF 10V a un diamètre de 5mm et fait 11mm de hauteur :

Les modèles récents font 6.3mm x 5mm. Votre réserve d'énergie tient peu de place !

Pour une valeur inférieure à 1µF, un condensateur polyester peut être envisagé. Au delà de 1µF ils occupent un volume important et sont chers (à peu près 10 fois plus chers à capacité égale) :


Jusqu'à quelques µF les condensateurs céramiques multi-couche (MLCC) peuvent être utilisés :

Parlons des résistances du pont diviseur. Celles-ci peuvent avoir une valeur élevée si le montage est destiné à fonctionner en basse consommation. Avec deux résistances de 1MΩ et 100KΩ, la consommation du pont diviseur sera de 4.5µA.

On peut remplacer R2 par une diode zener de 4.2V si des variations de tension d'alimentation sont prévisibles :
On peut utiliser une BZX55C4V2 par exemple. Elle doit normalement être polarisée à l'aide d'un courant de 2.5 à 5mA. Mais pour notre utilisation non critique, on se contentera d'1mA :
  • 5V : R1 vaudra 330Ω
  • 7V : R1 vaudra 1KΩ
  • 12V : R1 vaudra 2.7KΩ
L'inconvénient de la zener : elle n'est pas adaptée à la basse consommation. Hors de question de l'utiliser dans une solution alimentée par batterie.

7. Améliorons avec un CRC

En adoptant un nombre magique sur 32 bits comme vérification d'intégrité des données, il y a une chance d'erreur sur 4294967295, c'est à dire que le nombre magique peut avoir la bonne valeur tout à fait par hasard. Les données pourraient avoir dans ce cas des valeurs incertaines.

C'est une bonne sécurité basique mais l'intégrité des données n'est pas assurée pour autant. En effet le nombre magique peut parfaitement être présent et correct mais rien ne prouve que les données soient valides.

Une autre approche consiste à utiliser une somme de contrôle (CheckSum).
On fait la somme de tous les octets de données pour fabriquer le nombre magique. Cela apporte plus de sécurité mais le simple fait d'inverser deux octets de données ne modifiera pas la somme finale.

Nous allons plutôt utiliser un CRC (Cyclical Redundancy Check)
Si l'on inverse deux octets de données, le CRC ne sera pas le même.

Ce sketch utilise un CRC :

#include <time.h>
#include <EEPROM.h>

#define EEPROM_ADDR 0

struct __attribute__ ((packed)) eeprom_data
{
  time_t timeStamp;
  char data[160];
  unsigned long crc;
};

struct eeprom_data eepromData;

#define DATA_SIZE (sizeof(eepromData) - sizeof(eepromData.crc))

void powerLoss()
{
  unsigned long start, stop;
  start = micros();
  EEPROM.put(EEPROM_ADDR, eepromData);
  eepromData.crc = Crc32((unsigned char *)&eepromData, DATA_SIZE);
  EEPROM.put(EEPROM_ADDR + offsetof(struct eeprom_data, crc), eepromData.crc);
  stop = micros();
  Serial.print("LOSS POWER: ");
  Serial.print(stop - start);
  Serial.println("µs");
}

void setup()
{
  struct eeprom_data data;
  time_t t;

  Serial.begin(115200);
  set_system_time(getTimeFromAnywhere());
  time(&t);
  Serial.println(ctime(&t));
  attachInterrupt(digitalPinToInterrupt(2), powerLoss, FALLING);
  EEPROM.get(EEPROM_ADDR, data);
  if (Crc32((unsigned char *)&data, DATA_SIZE) != data.crc) {
    Serial.println("No Backup available");
  }
  else {
    Serial.println("Valid Backup data has been found in the EEPROM: ");
    memcpy(&eepromData, &data, sizeof(data));
  }
  Serial.print("CRC: 0x"); Serial.println(data.crc, HEX);
  Serial.print("DATE: "); Serial.println(ctime(&eepromData.timeStamp));
  Serial.print("DATA: "); Serial.println(eepromData.data);
}

void loop()
{
  time(&eepromData.timeStamp);
  strcpy(eepromData.data, "MY-PRECIOUS-DATA");
  Serial.println("Zzzzzzzzzzzzz");
  delay(30000);
}

unsigned long Crc32(const unsigned char *buf, int length)
{
  const unsigned long crc_table[16] = {
    0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac,
    0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c,
    0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c,
    0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c
  };

  unsigned long crc = 0L;

  for (int index = 0 ; index < length  ; ++index) {
    crc = crc_table[(crc ^ buf[index]) & 0x0f] ^ (crc >> 4);
    crc = crc_table[(crc ^ (buf[index] >> 4)) & 0x0f] ^ (crc >> 4);
    crc = ~crc;
  }
  return crc;
}

// get time from RTC, NTP, etc.
time_t getTimeFromAnywhere(void)
{
  char s_month[5];
  struct tm t = {0};
  int s, m, h, d, y;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";

  sscanf(__DATE__, "%s %d %d", s_month, &d, &y);
  sscanf(__TIME__, "%d:%d:%d", &h, &m, &s);
  t.tm_sec = s;
  t.tm_min = m;
  t.tm_hour = h;
  t.tm_mday = d;
  t.tm_mon = (strstr(month_names, s_month) - month_names) / 3;
  t.tm_year = y - 1900;
  t.tm_isdst = -1;
  return mktime(&t);
}


ATTENTION: le calcul du CRC prend du temps. Dans l'exemple précédent la sauvegarde des données prenait 620µs. Avec l'ajout du calcul du CRC, nous passons à 980µs !

Nous augmenterons donc la capacité du condensateur.


8. Gestion de versions

Pour des projets plus complexes il arrive que l'on doive gérer la version des structures de données, parce qu'elles évoluent en fonction des versions du logiciel.

Pour illustrer le principe  je propose un exemple fictif avec deux catégories de données :
  • des données de configuration
  • des données de mesure
Les données de configuration sont stockées en RAM mais sauvegardées à chaque fois qu'elles sont modifiées. Dans l'exemple ces données sont entrées via le terminal :
  • un message de bienvenue
  • une adresse IP
  • un numéro de port
Dans une première version, les données adresse IP et port étaient absentes, elles ont été ajoutées dans la deuxième version du logiciel.

Lors d'un démarrage, la fonction setup() lit la configuration à partir de l'EEPROM.
Si la version de la configuration est égale à 1 la configuration en RAM est mise à jour et sauvegardée en EEPROM avec une adresse IP et un numéro de port par défaut.
Si la version de la configuration est égale à 2 elle est laissée telle quelle.

Les données de mesure peuvent être diverses (vitesse du vent, débit d'eau, température d'un ballon, etc.).

Ces données sont stockées en RAM et sont traitées au fil de l'eau.On peut imaginer beaucoup de choses :
  • calcul de moyenne
  • détection de défaut
  • mise à jour d'une page WEB
  • etc.
Les données de mesures sont sauvegardées uniquement en cas de coupure d'alimentation, car on ne désire pas les sauvegarder à chaque fois qu'elles sont modifiées. Par contre on désire les retrouver après une coupure d'alimentation pour pouvoir continuer nos traitements.

Dans l'exemple ces données sont des nombres aléatoires stockés dans un buffer tampon. Toutes les dix secondes un nouveau numéro est tiré et stocké à la fin du buffer après avoir décalé les précédents pour éliminer le plus ancien.

Dans une première version, il y avait un seul tableau de données, un deuxième tableau a été ajouté dans la deuxième version du logiciel.

Lors d'un démarrage, la fonction setup() lit les données à partir de l'EEPROM.
Si la version de la configuration est égale à 1 les données en RAM du deuxième tableau sont effacées. En effet si on utilisait telles quelles des données de l'EEPROM à partir d'un emplacement n'ayant jamais été initialisé auparavant, on ne serait absolument pas certain du contenu.
Si la version de la configuration est égale à 2 les données sont laissées telle quelles.

Un petit interpréteur de commandes permet d'effectuer certaines actions :
  • welcome=XXXXXXXXX : permet de modifier le message de bienvenue
  • ip=XXX.XXX.XXX.XXX : permet de modifier l'adresse IP
  • port=XXX : permet de modifier le numéro de port
  • drop : permet d'effacer la configuration en EEPROM
  • date : affiche la date
  • conf : affiche la configuration
  • data : affiche les données
  • v1 : enregistre dans l'EEPROM une configuration des données dans leur version 1
La configuration par défaut est :
Message : "WELCOME TO THE ARDUINO WORLD"
Adresse : "192.168.1.1"
Port : 8000

Pour faire marcher tout cela il faut bien sûr le montage décrit plus haut (diode, condensateur, pont diviseur), alimenté en 5V par une alimentation de labo. Le cordon USB peut être laissé branché.

Quelques manipulations :
  • brancher l'alimentation
  • entrer : drop
  • redémarrer la carte (bouton reset)
  • le terminal affiche "No data available "
  • la configuration est celle par défaut
  • les données sont vides
  • entrer : welcome=NOUVEAU MESSAGE DE BIENVENUE 
  • entrer : ip=192.168.1.50
  • entrer : port=9000
  • laisser tourner le logiciel quelques minutes
  • entrer : data
  • le logiciel affiche les données
  • couper l'alimentation
  • les données sont sauvegardées
  • redémarrer la carte
  • le terminal affiche "Valid configuration has been found in the EEPROM "
  • le terminal affiche "Valid data has been found in the EEPROM"
  • la configuration est celle précédement enregistrée
  • les données sont celles ayant été enregistrées lors de la coupure
  • entrer : v1
  • le terminal affiche "CONFIGURATION BACK TO V1"
  • redémarrer la carte
  • le terminal affiche "Valid configuration has been found in the EEPROM"
  • le terminal affiche "V1: Upgrading configuration, using default IP/port "
  • la configuration est celle par défaut (adresse IP + port)
  • le message de bienvenue est celui entré précédement
  • les données du premier tableau sont conservées
  • les données du deuxième tableau sont vides
Voici le sketch :

#include <time.h>
#include <EEPROM.h>

#define CONFIG_ADDR     0
#define DATA_ADDR           256

#define MAGIC                     0xDEADBEEF
#define WELCOME_MAX    80
#define SPEED_MAX           50
#define CMD_MAX               WELCOME_MAX+10
#define IPADDR_MAX          16

#define DEFAULT_WELCOME "WELCOME TO THE ARDUINO WORLD"
#define NULL_IPADDR     "           "
#define DEFAULT_IPADDR  "192.168.1.1"
#define DEFAULT_PORT    8000

struct __attribute__ ((packed)) config_data
{
  unsigned long magic;
  unsigned version;
  time_t timeStamp;
  char welcomeMessage[WELCOME_MAX];
  char ipAddr[IPADDR_MAX];
  uint16_t port;
};

struct config_data configData;

struct __attribute__ ((packed)) measured_data
{
  unsigned long magic;
  unsigned version;
  unsigned speedA[SPEED_MAX];
  unsigned speedB[SPEED_MAX];
};

struct measured_data measuredData;

void eraseConfig(void)
{
  memset(&configData, 0, sizeof(configData));
  EEPROM.put(CONFIG_ADDR, configData);
}

void printConfiguration(void)
{
  Serial.print("DATE: "); Serial.println(ctime(&configData.timeStamp));
  Serial.print("VERSION: "); Serial.println(configData.version);
  Serial.print("WELCOME: "); Serial.println(configData.welcomeMessage);
  Serial.print("IP: "); Serial.println(configData.ipAddr);
  Serial.print("PORT: "); Serial.println(configData.port);
}

void saveConfiguration(void)
{
  configData.magic = 0L;
  time(&configData.timeStamp);
  EEPROM.put(CONFIG_ADDR, configData);
  EEPROM.put(CONFIG_ADDR + offsetof(struct config_data, magic), MAGIC);
}

void printData(void)
{
  Serial.println("SPEEDA:");
  for (int i = 0 ; i < SPEED_MAX ; i++) {
    Serial.print(measuredData.speedA[i]);
    Serial.print(" ");
  }
  Serial.println("\nSPEEDB:");
  for (int i = 0 ; i < SPEED_MAX ; i++) {
    Serial.print(measuredData.speedB[i]);
    Serial.print(" ");
  }
  Serial.println();
}
void saveData(void)
{
  measuredData.magic = 0L;
  EEPROM.put(DATA_ADDR, measuredData);
  EEPROM.put(DATA_ADDR + offsetof(struct measured_data, magic), MAGIC);
}

void powerLoss()
{
  unsigned long start, stop;
  start = micros();
  saveData();
  stop = micros();
  Serial.print("LOSS POWER: ");
  Serial.print(stop - start);
  Serial.println("µs");
}

void setup()
{
  struct config_data cdata;
  struct measured_data mdata;
  time_t t;

  Serial.begin(115200);
  set_system_time(getTimeFromAnywhere());
  time(&t);
  Serial.println(ctime(&t));
  attachInterrupt(digitalPinToInterrupt(2), powerLoss, FALLING);
  EEPROM.get(CONFIG_ADDR, cdata);
  if (cdata.magic != MAGIC) {
    Serial.println("No Backup available, using defaults");
    configData.version = 2;
    strcpy(configData.welcomeMessage, DEFAULT_WELCOME);
    strcpy(configData.ipAddr, DEFAULT_IPADDR);
    configData.port = DEFAULT_PORT;
    saveConfiguration();
    Serial.println("CONFIGURATION SAVED");
  }
  else {
    memcpy(&configData, &cdata, sizeof(cdata));
    Serial.println("Valid configuration has been found in the EEPROM: ");
  }
  switch (configData.version) {
    case 1:
      Serial.println("V1: Upgrading configuration, using default IP/port");
      configData.version = 2;
      strcpy(configData.ipAddr, DEFAULT_IPADDR);
      configData.port = DEFAULT_PORT;
      saveConfiguration();
      Serial.println("CONFIGURATION SAVED");
      break;
    case 2:
      break;
    default:
      break;
  }
  printConfiguration();

  EEPROM.get(DATA_ADDR, mdata);
  if (mdata.magic != MAGIC) {
    Serial.println("No data available");
    measuredData.version = 2;
    memset(&measuredData, 0, sizeof(measuredData));
  }
  else {
    memcpy(&measuredData, &mdata, sizeof(mdata));
    Serial.println("Valid data has been found in the EEPROM: ");
  }
  switch (measuredData.version) {
    case 1:
      Serial.println("V1: Upgrading data");
      measuredData.version = 2;
      memset(&measuredData.speedB, 0, sizeof(measuredData.speedB));
      break;
    case 2:
      break;
    default:
      break;
  }
  printData();
}

void loop()
{
  static unsigned long start;

  commandShell();
  unsigned long now  = millis();
  unsigned long elapsed = now - start;
  if (elapsed >= 10000) {
    // shift data to the left
    memcpy(measuredData.speedA, measuredData.speedA + 1, SPEED_MAX * sizeof(unsigned));
    // new data at right
    measuredData.speedA[SPEED_MAX - 1] = random(3000, 5000);
    // shift data to the left
    memcpy(measuredData.speedB, measuredData.speedB + 1, SPEED_MAX * sizeof(unsigned));
    // new data at right
    measuredData.speedB[SPEED_MAX - 1] = random(2500, 5000);
    start = millis();
  }
}

// get commands from serial line, keyboard, touchscreen, radio, etc.
void commandShell(void)
{
  static char buf[CMD_MAX];
  static int i;
  time_t t;

  if (Serial.available()) {
    int c = Serial.read();
    if (c != -1) {
      if (c == '\n') {
        if (!strncmp(buf, "welcome=", 8)) {
          strncpy(configData.welcomeMessage, buf + 8, WELCOME_MAX);
          saveConfiguration();
          Serial.println("WELCOME MESSAGE SAVED");
          printConfiguration();
        }
        else if (!strncmp(buf, "ip=", 3)) {
          strncpy(configData.ipAddr, buf + 3, IPADDR_MAX);
          saveConfiguration();
          Serial.println("IPADDR SAVED");
          printConfiguration();
        }
        else if (!strncmp(buf, "port=", 5)) {
          configData.port = atoi(buf + 5);
          saveConfiguration();
          Serial.println("PORT SAVED");
          printConfiguration();
        }
        else if (!strcmp(buf, "drop")) {
          eraseConfig();
          Serial.println("CONFIGURATION ERASED");
        }
        else if (!strcmp(buf, "date")) {
          time(&t);
          Serial.println(ctime(&t));
        }
        else if (!strcmp(buf, "conf")) {
          printConfiguration();
        }
        else if (!strcmp(buf, "data")) {
          printData();
        }
        else if (!strcmp(buf, "v1")) {
          configData.version = 1;
          configData.port = 0;
          strcpy(configData.ipAddr, NULL_IPADDR);
          saveConfiguration();
          Serial.println("CONFIGURATION BACK TO V1");
          printConfiguration();
          measuredData.version = 1;
          memset(&measuredData.speedB, 0, sizeof(measuredData.speedB));
          saveData();
          Serial.println("DATA BACK TO V1");
          printData();
        }
        else {
          Serial.print(buf);
          Serial.println(": WHAT'S UP DOC ???");
        }
        i = 0;
        return;
      }
      if (c != '\r') {
        if (i < WELCOME_MAX - 1) {
          buf[i++] = c;
          buf[i] = '\0';
        }
      }
    }
  }
}

// get time from RTC, NTP, etc.
time_t getTimeFromAnywhere(void)
{
  char s_month[5];
  struct tm t = {0};
  int s, m, h, d, y;
  static const char month_names[] = "JanFebMarAprMayJunJulAugSepOctNovDec";

  sscanf(__DATE__, "%s %d %d", s_month, &d, &y);
  sscanf(__TIME__, "%d:%d:%d", &h, &m, &s);
  t.tm_sec = s;
  t.tm_min = m;
  t.tm_hour = h;
  t.tm_mday = d;
  t.tm_mon = (strstr(month_names, s_month) - month_names) / 3;
  t.tm_year = y - 1900;
  t.tm_isdst = -1;
  return mktime(&t);
}


9. Conclusion

J'espère que cette petite récréation vous aura amusé.

Il existe des condensateur électrolytiques de gros volume, jusqu'aux super-condensateurs de 10 Farads. Deux condensateurs de 10F 2.7V en série permettraient à un ARDUINO UNO de tenir beaucoup plus longtemps sans alimentation. Si avec 20µF le temps de maintien d'alimentation est de 620µs, avec 10F on obtiendra :

620µs / 20µF * 10F = 310 secondes soit 5 minutes (je l'ai testé).

Cela permettrait de sauvegarder une grosse quantité de données. Si 160 octets sont sauvegardés en 583µs, pour 5 minutes on aura :

q = 160 / 583µs * (5*60*1000000) = 82.332.761 !

Mais on ne parle pas de MégaOctet dans le monde ARDUINO, c'est l'usage.

Cordialement
Henri

10. Mises à jour

09/03/2019 : 7. Améliorons avec un CRC
                    8. Gestion de version

mercredi 6 mars 2019

Un éclairage d'escalier à LEDs



Un éclairage d'escalier à LEDs


Aujourd'hui nous allons jouer avec un petit montage domotique afin d'éclairer automatiquement les marches d'un escalier avec des rubans de LEDs.
L'automatisation sera confiée à un ARDUINO équipé de deux capteurs de mouvement.

1. L'installation

Comme on le voit sur la photo ci-dessus les rubans de LEDs sont fixés sous les nez de marche.
En bas et en haut de l'escalier, un détecteur de mouvement infrarouge est installé afin de détecter l'approche d'une personne.
Le boîtier électronique de commande est installé sous l'escalier par exemple, ou tout autre endroit à votre convenance. La proximité d'une source d'alimentation en 230V est primordiale.


2. Principe et fonctionnement

Lorsque une personne est détectée, le microcontrôleur allume le ruban de LEDs de la première marche, puis des suivants avec un petit délai entre chaque marche.
Bien entendu la séquence d'allumage se fait du bas vers le haut si le détecteur du bas est activé, et se fait du haut vers le bas si le détecteur du haut est activé.

En outre chaque ruban est allumé progressivement. On appelle cela un effet "dimming".

Chaque ruban commence à s'allumer lorsque le précédent a atteint un certain pourcentage de luminosité, afin de produire un effet cascade très fluide.

L'extinction est confiée à un timer. Elle se fait dans le même ordre que l'allumage avec un effet de dimming inversé bien sûr.

Le temps d'allumage et d'extinction peuvent être différents.

2.1. Parlons sécurité

Il est hors de question d'allumer l'éclairage sur détection d'une personne en bas de l'escalier et de l'éteindre sur détection de la même personne en haut.

Rien ne garantit qu'il s'agisse de la même personne. Un capteur de passage ne détecte pas le sens de passage.
De plus une autre personne peut très bien descendre, et donc être détectée par le capteur du haut, alors que la première personne est en train de monter, et éteindre l'éclairage à ce moment-là n'est pas une bonne idée, car s'il fait nuit les deux personnes se retrouvent dans le noir.
C'est pour cette raison que l'extinction est confiée à un timer.

2.2. Cas particuliers

Lors des tests il faut absolument que certains cas fonctionnent :
  • deux personnes se suivent
    • la première personne monte
    • l'éclairage s'allume
    • l'éclairage commence à s'éteindre au bout de 10 secondes
    • pendant l'extinction une deuxième personne monte
    • l'éclairage se rallume
    • l'éclairage commence à s'éteindre au bout de 10 secondes
  • deux personnes se croisent
    • la première personne monte
    • l'éclairage s'allume
    • l'éclairage commence à s'éteindre au bout de 10 secondes
    • pendant l'extinction une deuxième personne descend
    • l'éclairage se rallume
    • l'éclairage commence à s'éteindre au bout de 10 secondes

3. Quelques calculs

Nous allons baser nos calculs sur un escalier à 15 marches, correspondant à une hauteur d'étage de 2.70m et une hauteur de marche de 18cm.

Une personne met environ 8 secondes à monter les 15 marches.

Le temps s'écoulant entre le début de l'allumage du premier ruban et le début de l'allumage du dernier est fixé à 5 secondes.

L'intervalle entre chaque début d'allumage sera donc :

t = 5s / 15 = 0.33s

Comme le microcontrôleur va piloter les 15 rubans en même temps, le délai entre chaque changement d'état sera de :

t = 0.33 / 15 = 0.022s

Et comme nous allons lui faire piloter un seul ruban à un instant donné, nous allons encore diviser :

t = 0.022s / 15 =  0.0015s

La durée de l'effet dimming ne doit pas être trop courte, pour être visible. Fixons-la à 1.5 seconde.
Afin de ne pas allumer les LEDs à pleine puissance, et éviter d'aveugler les personnes, nous allons nous limiter à une luminosité de 50%.

L'effet dimming est produit par une modulation de largeur d'impulsion PWM. Le nombre de valeurs possibles sur un ARDUINO est de 255.
Comme nous nous contentons de 50% de luminosité nous allons exploiter seulement 127 valeurs.

Combien de pas faudra t-il compter entre le début de l'allumage d'un ruban et le début de l'allumage du suivant ?

p = 0.022s / 0.0015s = 15 pas

Au bout de 15 pas, le ruban aura atteint une luminosité de :

100% / 255 * 15 = 12%

Ces calculs ne tiennent pas compte des temps de traitement. Il faudra ajuster.

5. Choix matériels

Pour bénéficier de 15 sorties PWM un ARDUINO UNO ou NANO ne suffira pas. La MEGA2560 possède 15 sorties PWM. Il n'y en a pas une de plus, mais en général un nombre de 15 marches est suffisant.
Au pire, avec 16 marches, la première marche du bas peut se passer d'éclairage.

Les commandes de puissance PWM seront confiées à des transistors MOSFET. L'IRLD024 en boîtier DIP est un MOSFET "logic level" et supporte 2A. Sa résistance RDSon est de 100mΩ. C'est peu en regard de la résistance des rubans de LEDs.

Le microcontrôleur sera endormi pendant les périodes d'inactivité, ce qui nous permettra d'abaisser au maximum sa consommation. De 60mA en mode éveillé nous devrions passer à 7mA en mode sommeil.

Les deux capteurs PIR sont reliés aux entrées D20 et D21. Ces entrées nous permettront de réveiller le microcontrôleur.
Pour rappel, les pins permettant d'interrompre la MEGA2560 sont : 2, 3, 18, 19, 20 et 21.

5. Le schéma


Le schéma est réalisé à l'aide de KICAD.

Pour récupérer le projet voir plus bas :  9. Téléchargements

Pour habiller le PCB j'ai choisi un boîtier rail DIN de 160mm :

L'espace étant restreint la carte utilisée sera une MEGA2560 core INHAOS :
L'alimentation de l'ARDUINO est assurée par une MeanWell IRM-03-5 (3W / 5V) intégrée sur le PCB :


1W suffiraient amplement mais les alimentations 1W ne sont pas très faciles à trouver.

L'alimentation 12V des LEDs n'est pas intégrée dans le boîtier pour des raisons de taille. Son choix dépend de plus de la luminosité attendue pour notre éclairage (voir plus loin).

Les entrées pour les deux capteurs PIR comportent des résistances de pull-down. Cela permet de faire des essais avec des boutons-poussoirs.
Deux LEDs permettent de visualiser l'état des alimentations 5V et 12V.

6. L'alimentation 12V

L'alimentation 12V devra être dimensionnée en fonction des LEDs utilisée.
Il est bien entendu possible d'utiliser des LEDs 5V ou 12V, mais les rubans de LED 5V commandable en PWM sont rares.


On peut citer les rubans de LEDs 2835, 5050, 5630 ou 5730.

6.1. Sous 12V

Par exemple un ruban de 1m de LEDs 5730 12V consomme environ 5W par mètre. Si la luminosité est maximale, l'escalier complet consommera :

5W * 15 = 75W

C'est énorme. Deux ou trois petite ampoules à LED de 10W au total suffiraient.

Bien sûr nous allons l'alimenter en PWM pour réduire cette puissance.
Si nous choisissons une puissance maximale de 10W pour l'escalier complet, le PWM aura un rapport de 100 / 75 * 10 = 13%
Nous aurons 255 / 100 * 13 = 34 valeurs possibles pour notre luminosité.

6.2. Examen du ruban

Si on examine le ruban, on s'aperçoit qu'il est composé de sections de 5cm de long formées de 3 LEDs et une résistance de 390Ω en série.
Une LED blanche produit une chute de tension de 3V.

Le courant sous 12V sera de (12V - 3V) / 390Ω = 23mA
Comme nous avons 20 sections par mètre cela donne 460mA par mètre, donc 460mA * 12V = 5.5W.

6.3. Sous 9V

Nous pouvons sous-alimenter le ruban pour diminuer la puissance.
Le courant sous 9V sera de (9V - 3V) / 390Ω = 15mA
Cela donne 300mA par mètre, donc 300mA * 12V = 3.7W.
Si la luminosité est maximale, l'escalier complet consommera :

3.7W * 15 = 55W

Si nous choisissons une puissance maximale de 10W pour l'escalier complet, le PWM aura un rapport de 100 / 55 * 10 = 18%
Nous aurons 255 / 100 * 18 = 46 valeurs possibles pour notre luminosité. L'effet de dimming sera plus fluide.

6.4. Ruban court

Pour diminuer la puissance il est possible bien sûr d'utiliser seulement 10cm de ruban, ou 2 fois 5cm, c'est à dire 6 LEDs.

Le courant sous 12V sera de (12V - 3V) / 390Ω = 23mA
Comme nous avons seulement 2 sections cela donne 46mA, donc 46mA * 12V = 0.55W.

Si la luminosité est maximale, l'escalier complet consommera :

0.55W * 15 = 8.25W

Nous pourrons ainsi profiter de notre PWM à 100% et l'effet de dimming sera maximal.

C'est la solution que j'ai choisi, ceci afin de limiter la puissance totale.

6.5. Choix de l'alimentation

En résumé le choix de l'alimentation va dépendre de la luminosité voulue.

L'alimentation sera de préférence une alimentation à découpage de qualité.
N'oubliez pas qu'elle va rester sous tension 24H / 24. Économiser quelques euros sur ce composant n'est pas conseillé.

Une MeanWell me semble un bon choix :


Personnellement j'ai opté pour une MeanWell HDR-15-12 montée à côté du dimmer sur le rail DIN :

On trouve des modèles de toutes puissance entre 6W et plus de 100W.

Il est bon de prévoir une marge de puissance confortable, car ces alimentation coupent leur sortie brutalement en cas de dépassement du courant maximal.

Le câblage des rubans de LEDs est réalisé à l'aide de fil de section 0.25 mm2. La résistance de ce câble est de 75Ω / Km. La longueur maximale entre le tableau électrique et le ruban de LEDs la plus éloigné est de 8m - donc 16m aller / retour -  ce qui donne 75 / 1000 *16 = 1.2Ω, ce qui est très faible si l'on compare à la résistance intégrée aux rubans de LEDs (1 résistance de 390Ω pour 5cm et 3 LEDs).

Si les rubans font 10cm, leur résistance équivalente est de 390Ω / 2 = 195Ω). La chute de tension dans les câbles est faible : 0.6%

Si les rubans font un mètre, leur résistance équivalente est de 390Ω / 20 = 19.5Ω). La chute de tension dans les câbles reste acceptable : 6%.

7. L'IDE ARDUINO

Une seule librairie est à prévoir :

https://github.com/thomasfredericks/Bounce2.git

Cette librairie n'est utile que dans le cas où l'on connecte des boutons-poussoirs sur les entrées prévues pour les capteurs PIR

Pour récupérer le projet, voir plus bas : 9. Téléchargements

Le sketch est composé de deux parties :
  • deux classes
    • DimmingLed
    • Dimmer
  • l'application
Les rubans de LEDs sont définis comme suit avec leurs pins respectives :

DimmingLed leds[] = {DimmingLed(2), DimmingLed(3), DimmingLed(4),
                     DimmingLed(5), DimmingLed(6), DimmingLed(7), DimmingLed(8),
                     DimmingLed(9), DimmingLed(10), DimmingLed(11), DimmingLed(12),
                     DimmingLed(13), DimmingLed(44),
                     DimmingLed(45), DimmingLed(46)
                    };
int _nLeds = sizeof(leds) / sizeof(leds[0]);


Le dimmer se définit comme suit :

#define DIMMER_DELAY  1950
#define OFF_DELAY     10000

// leds: DimmingLed array
// n: 15 LEDs
// delay: 1.95ms
// onTrigger: 25%
// offTrigger: 3%
// luminosity: 50%
Dimmer dimmer(leds, _nLeds, DIMMER_DELAY, 25, 3, 50);


leds : la liste de LEDs

n: leur nombre

delay : le délai entre chaque changement d'état

onTrigger : c'est le paramètre qui va déterminer le seuil de luminosité à attendre en % pour commencer à allumer la LED suivante.

offTrigger : c'est le paramètre qui va déterminer le seuil de perte de luminosité à attendre en % pour commencer à éteidre la LED suivante.

luminosity : la luminosité maximale

Dans l'exemple, j'ai choisi une durée d'allumage de 5s entre la première LED et la dernière. Le temps d'extinction est de 3s avec très peu d'effet de cascade, c'est à dire que toutes les LEDs arrivent à 0% de luminosité presque simultanément.

La valeur de DIMMER_DELAY détermine le temps de montée de l'escalier, ici 10 secondes.

8. Conclusion

Ce petit montage est très agréable à l'utilisation.
Je le teste actuellement sur une plaque breadboard en attendant de réaliser la carte définitive.

9. Téléchargements

Le projet est disponible ici : https://bitbucket.org/henri_bachetti/led-stairs-lighting.git

Cette page vous donne toutes les informations nécessaires :
https://riton-duino.blogspot.com/p/migration-sous-bitbucket.html


Cordialement
Henri

10. Mises à jour

07/03/2019 : ajout dossier complet KICAD
                     mode low-power
                     remplacement connecteurs LEDs par NS25-W2K
                     remplacement connecteurs PIR par NS25-W3K
                     remplacement connecteurs secteur par bornier à vis
                     ajout 4. Choix matériels