Eclipse


Préparer un projet audio avec Arduino

Je vais tâcher ici de vous donner quelques informations de base à connaître lorsque vous décidez de vous lancer dans un projet Arduino traitant avec de l’audio.

Mais attention, je ne parle pas de projet “audio-réactif”, comme on en voit en majorité sur le net, dont le but est de faire réagir le matériel en fonction d’une intensité sonore ou d’une fréquence particulière, ni même de “vu-meter” (visualisateur de fréquence type équaliseur). Mon but ici est de présenter les éléments importants à maîtriser dans le cas où vous souhaiteriez enregistrer réellement du son, c’est-à-dire capturer le signal sonore dans son intégralité en vue de traitement ultérieur : re-lecture, analyse spectrale, manipulation en Big Data pour créer votre propre Shazam... ;-)

Ainsi, sans avoir la prétention de vous donner un cours d’électronique avancée ou de traitement du signal (je n’en ai pas les capacités), je vais vous livrer tout ce que j’ai appris en tant que néophyte après m’être plongé tête baissée dans ce type de projet sans trop de connaissances de ces domaines.

Passage obligé : la théorie

Même si vous me voyez venir, je ne vais pas me lancer dans une explication des Transformées de Fourier (ou Fast Fourier Transform - FFT utilisées en électronique). Cependant, il est intéressant de comprendre leur concept de base : tout signal (ici sonore) peut être décomposé en signaux sinusoïdaux élémentaires, d’amplitudes et de fréquences différentes :

Comme on le voit ici, il s’agit donc d’ondes oscillant avec une amplitude variable autour de 0. Or, nos traitements numériques ne peuvent s’appuyer que sur des valeurs positives : nous allons donc devoir commencer par adapter les signaux captés pour pouvoir les traiter proprement, sans risquer de perdre la moitié des informations.

Pour ce faire, il va falloir décaler l’amplitude du signal de sorte que les oscillations aient une valeur minimum de 0. Ceci est possible en ajoutant une composante continue (pour créer un biais, ou “offset”), ayant pour valeur la moitié de l’amplitude max du signal : si la source émet une onde à partir d’une tension de 5V, le signal oscillera entre +2.5V et -2.5V. L’ajout d’une composante continue de 2.5V le fera alors osciller entre 0V et 5V.

La théorie est simple, n’est-ce pas? Mais voyons ce que cela implique au niveau matériel… Les choses se compliquent.

Un peu d’électronique

Pour additionner nos tensions (alternatives et continues), nous avons besoins d’un pont diviseur. Composé de deux résistances en série, il permet de faire entrer une tension de chaque côté et d’en restituer l’addition entre les deux résistances : nous avons d’ores-et-déjà réussi à décaler notre signal alternatif! Mais on ne peut pas s’arrêter là… Comme vous le constatez, son amplitude a été divisée de moitié, à cause des résistances. Petit aparté : nous venons de toucher du doigt la notion de Gain : c’est notamment la valeur de ces résistances qui fera varier le gain (la puissance) du signal en sortie. Il faut donc ajouter à notre montage un Amplificateur Opérationnel (AO) pour retrouver l’amplitude d’origine. Mais ce dernier ne fonctionnant qu’avec des tensions alternatives, il faut bloquer la composante continue. Pour ce faire, il faut ajouter un condensateur.

Un autre exemple avec un micro :

Voici donc le rôle de ce montage de base que vous retrouverez régulièrement lorsqu’il s’agit de traiter des signaux audio. Vous voyez donc que plusieurs composants électroniques entre en jeu : il est alors évident que les caractéristiques de chacun vont avoir un impact sur la qualité du signal restitué. C’est ce dont nous allons traiter par la suite.

Choix du matériel

Le premier élément à choisir est le micro lui-même, qui fera la captation du son. Sauf besoin particulier (nous l’aborderons un peu plus loin dans cet article), les modules à électrets les plus répandus suffisent amplement.

Cependant, ils peuvent s’avérer très sensibles au bruit électrique du circuit qui les entoure. Par exemple, s’ils sont alimentés via un adaptateur secteur, un signal de “souffle” peut apparaître même en silence complet. De même, sur un Arduino, la sortie 3.3V est connue pour être plus stable que la 5V car plus régulée, mais l’idéal reste de prévoir l’alimentation du micro par pile simple.

Ensuite, l’Amplificateur Opérationnel joue un rôle primordial dans la préservation de la qualité du signal. Prenons pour commencer l’exemple d’un circuit très répandu : le Grove Sound sensor de SeeedStudio.

Il est équipé d’un AO LM358 dont l’inconvénient majeur est d’induire une perte de tension d’environ 1.5V par rapport à la tension d’entrée. Ce qui veut dire que par exemple, pour une tension d’entrée de 5V, la valeur maximale lue sur l’ADC de l’Arduino sera de 750 au lieu de 1024, comme expliqué ici.

Il vaudra mieux donc s’orienter vers un AO possédant une vraie caractéristique “Rail-to-Rail”, c’est à dire fournissant une tension de sortie égale à la tension d’entrée. L’exemple le plus courant est le MAX4466, disponible sur ce circuit d’Adafruit :

Cerise sur le gâteau, ce montage dispose d’un gain ajustable, autrement dit un potentiomètre variable à l’arrière.

Cependant, il faut garder en tête l’usage qui va être fait de ce micro : s’il s’agit de capter par exemple une musique, pas de souci, il suffira de régler le gain en fonction du volume de la source. Mais dans le cas d’une captation de son ambiant, ou environnemental, la difficulté réside dans l’impossibilité de connaître le volume du son qui sera capté. En effet, il n’est pas si évident de pouvoir bien capter à la fois un son faible ou éloigné et un son fort ou proche. Dans ce cas, si le gain est réglé au maximum pour capter les sons faibles, un effet de “clipping” (écrêtage) peut apparaître sur les sons forts, c’est-à-dire qu’ils seront trop amplifiés pour être captés entièrement :

L’idéal est donc de se diriger vers des montages disposant de gain automatiques, capable de passer d’un gain fort dans une ambiance calme à un gain faible dans un environnement bruyant. Cela se trouve également facilement grâce à l’AO MAX9814, disponible sur ce module Adafruit :

En plus de ce réglage automatique, il est possible de configurer un gain par défaut (40dB, 50dB ou 60dB) selon le montage.

Enfin, qui dit réglage automatique, dit réactivité pour adapter le gain. Pour cela, il faut influer sur le ratio Attack:Release (broche “AR” sur le montage), qui détermine la vitesse avec laquelle le gain est diminué (“Attack”) pour atteindre le seuil nécessaire puis augmenté (“Release”) pour revenir en état initial :

J'ai essayé ci-dessous d'illustrer ce concept avec les images d'un oscilloscope :

Pour aller plus loin...

Il reste encore bien des paramètres à prendre en compte au niveau du matériel pour obtenir une captation de bonne qualité.

Pour en citer quelques-uns, on peut commencer par la topologie des micros. En effet, une prise de son stéréo se révèle également bien plus efficace : deux micros permettent de réduire les sources de bruit (ou en tout cas de le soustraire par différentiel) mais également de pallier à l’aspect directif de certains micros.

Cette page explique par exemple comment un couple de micro ORTF permet de reproduire un son entendu par les oreilles humaines, en tenant compte de la distance entre les micros et de leur orientation. Il existe évidemment bien d’autres possibilités selon les situations.

Au niveau logiciel

Le programme à mettre en place joue également un rôle important dans la qualité du signal enregistré. Le défi principal est d’arriver à l’enregistrer de manière continue, sans être interrompu par d’autres tâches. Imaginons que vous vouliez enregistrer votre son dans un fichier WAV (sur une carte SD) : il faut être en mesure de continuer l’enregistrement du son le temps de l’écriture des données sur la carte afin de rester fidèle au signal d’origine et ne pas le hachurer.

Utilité des interruptions

Pour cette raison, il est préférable d’utiliser les interruptions, disponibles sur tous les micro-contrôleurs, notamment ceux équipant les Arduino (AVR ou ARM). Ceci nous permettra de lire à intervalles réguliers les valeurs captées par le micro sans pour autant bloquer les processus d’écriture du fichier WAV (et inversement). Mais nous ne pouvons pas pour autant nous contenter d’écrire sur dans le fichier chaque valeur lue : en effet, le temps d’écriture dans un fichier n’étant pas négligeable, nous perdrions trop d’informations du signal. Pour pallier à ceci, il faut utiliser un tampon, dans lequel nous allons stocker par exemple 512 valeurs, avant de lancer l’écriture sur la carte SD. Mais le problème reste à peu près le même... La solution optimale est d’utiliser 2 tampons en parallèle : une fois que le premier est rempli, on lance l’écriture sur la carte SD tandis que le second se rempli en arrière-plan. Une autre technique similaire est celle du “circular buffer”. Sur les plateformes ARM, il est également possible d’utiliser le Direct Memory Access.

Enfin, les micro-contrôleurs Arduino étant cadencés à plusieurs MHz et un extrait audio ne nécessitant qu’un échantillonage de l’ordre du kHz, nous utiliserons un “prescaler”, c’est-à-dire un diviseur pour limiter la fréquence de déclenchement des interrupts.

Comprendre le convertisseur analogique-numérique

Puisque nous voulons enregistrer un signal analogique, nous allons devoir utiliser la fonction “ADC“ qui permet de convertir une valeur analogique en numérique. Il est à ce stade très important de jeter un oeil à la spécification de l’ATmega2560 : elle indique (page 279) qu’une conversion analogique-digital dure 13 cycles d’horloge. Voyons ce que cela implique pour nous.

Comme évoqué plus haut, le micro-contrôleur de l’Arduino Mega 2560 est cadencé à 16MHz. Commençons donc par réduire la fréquence des interrupts par un prescaler de 32 :

16 MHz / 32 = 500 kHz

Cependant, pour prendre en compte le temps de conversion analogique-numérique, il faut encore diviser cette fréquence :

500 kHz / 13 ≈ 38 kHz 

Nous obtenons donc ici une fréquence d’échantillonage finale de 38 kHz, proche de ce qui se pratique sur un CD audio (44,1 kHz). Pour cet exemple je me suis inspiré de cet Instructable. Vous pourrez donc adapter ce réglage à vos besoins.

Enfin, les échantillons relevés par l’ADC de l’Arduino sont codés sur 10 bits, comme présenté sur cette page. Or la plupart des formats audio le sont sur 8 bits. Il faut donc diminuer la précision des données acquises (“down-sample”) pour respecter les standards. Pour ce faire, il faut paramétrer l’ADC pour ne lire que les 8 bits ADCH via la commande : ADLAR=1

Prendre en compte l’offset

Vous vous souvenez de l’offset abordé en début d’article? Vous remarquerez que sur le dernier montage d’Adafruit, il est indiqué “DC offset : 1.25V”. Ce qui signifie que dans un silence complet, les valeurs retournées ne seront pas de 0 mais d’une valeur légèrement supérieure (dépendant du la tension d’entrée).

Ceci convient très bien à l’enregistrement de fichier WAV 8 bits, qui sont codés uniquement à partir de valeurs positives (unsigned int). Les logiciels de lecture (type Audacity) le savent et s’adaptent en fonction.

Cependant, à des fins de traitements du signal plus poussés, il peut s’avérer nécessaire de posséder à la fois les valeurs positives et négatives. Pour cela, il faudra penser à soustraire la valeur de l’offset (donc du silence) des valeurs mesurées.

A vous de jouer!

Vous avez dorénavant les éléments les plus importants pour vous lancer dans un projet de traitement de signal audio. J’espère que cela vous aura été utile!


Fichier(s) joint(s) :



DevOps ok? Voici DevEE!

Pour comprendre comment envisager la collaboration entre les développeurs et les électroniciens, je vous invite à aller lire cet article publié sur le blog de Valtech : DevOps ok? Voici DevEE!


Fichier(s) joint(s) :



Arduino Ethernet avec module ENC28J60

J'ai récemment acquis des cartes Ethernet de chez Velleman, dédiées à l'Arduino : VMA04

Elles ont le mérite d'êtres moins chères, de par leur fabrication et aussi par l'absence de slot SD. Mais il n'est pas si simple de trouver une bonne librairie pour l'utiliser, celle disponible dans la distribution Arduino ne fonctionnant qu'avec un module W5100.

Après de longues recherches, mon choix s'est orienté vers UIPEthernet, présentée ici : Arduino et enc28j60 : EtherCard ou UIPEthernet

Et pour la faire fonctionner avec un Arduino Mega, il faut modifier quelque peu le code comme expliqué dans la page liée, mais avec les valeurs suivantes (dans le fichier fichier utility/Enc28J60Network.h) :

#define ENC28J60_CONTROL_CS     10
#define SPI_MOSI        MOSI
#define SPI_MISO        MISO
#define SPI_SCK         SCK
#define SPI_SS          SS

Hope this helps!


Fichier(s) joint(s) :



Un client CoAP simple en C

CoAP est un protocole de communication dédié aux objets connectés : basé sur UDP et REST, il permet de diminuer au maximum le contenu des messages pour économiser les ressources des objets lors des transferts de données.

Nous allons voir ici comment implémenter votre propre client, à l'aide de la spécification du protocole. Le but est d'être capable d'envoyer, de la manière la plus simple possible, des données à un serveur CoAP exposant des services. Niveau matériel, un Arduino Mega avec un shield Ethernet communique avec un serveur Java.

Commençons par la structure des messages. Ce chapitre de la spécification nous explique comment les structurer :

8 bits 8 bits 8 bits 8 bits
Ver T TKL Code Message ID
Token
Options
Terminator Payload

Je vous laisse vous référer à la spécification pour la signification des champs. Un peu plus en détails, les Options suivent un format propre :

8 bits 8 bits 8 bits 8 bits
Ver T TKL Code Message ID
Token
Delta Length Value
Terminator Payload

Ici nous allons construire un message contenant :

  • version : 1
  • type : Request
  • TKL : 0 (pas de Token)
  • Code : POST
  • vers le service : "/sensors"
  • contenu : "test"

Voici ce que cela donne :

8 bits 8 bits 8 bits 8 bits
1 Request 0 POST Message ID
 
Uri-Path 7 URI segment "sensors"
Terminator "test"

Chaque Option possède un code "Option Number" propre. Par exemple, "Uri-Path" qui permet d'indiquer un segment de l'adresse du service, a pour code 11. Le Delta se calcule alors en additionnant le code de l'option avec celles des précédentes :

8 bits 8 bits 8 bits 8 bits
1 Request 0 POST Message ID
 
0+11 7 "sensors"
Terminator "test"

Si l'on veut spécifier plusieurs options du même type, il n'est pas nécessaire d'additionner le code. Par exemple, pour pointer sur le service "/sensors/temp" :

8 bits 8 bits 8 bits 8 bits
1 Request 0 POST Message ID
 
0+11 7 "sensors"
0+11+0 4 "temp"
Terminator "test"

En revanche, pour une autre Option, par exemple "Max-Age" qui a le code 14 :

8 bits 8 bits 8 bits 8 bits
1 Request 0 POST Message ID
 
0+11 7 "sensors"
0+11+14 2 12
Terminator "test"

Revenons à notre exemple et transformons tout en valeurs selon la spec et notre cas :

8 bits 8 bits 8 bits 8 bits
1 1 0 0.02 52942
 
11 7 "sensors"
Terminator "test"

Ce qui nous donne, en binaire en respectant la longueur des champs (pour les champs les plus simples) :

8 bits 8 bits 8 bits 8 bits
01 01 00000 00000010 1100111011001110
 
1011 0111 "sensors"
11 11 1111 "test"

Enfin, pour se faciliter l'envoi via le contrôleur Ethernet, dont la librairie est prévue pour traiter des "char" sur 8 bits, il suffit de transformer le résultat en concaténant les bits par 8 :

8 bits 8 bits 8 bits 8 bits
0x50 0x02 0xCE 0xCE
 
0xB7 "sensors"
0xFF "test"

En pratique, un petit exemple de code Arduino pour envoyer nos données :

#include <SPI.h>
#include <Ethernet.h>
#include <EthernetUdp.h>

char coapacket[18];
EthernetUDP udp;

void loop() {
  coapacket[0] = 0x50;
  coapacket[1] = 0x02;
  coapacket[2] = 0xce;
  coapacket[3] = 0xce;
  coapacket[4] = 0xB7;
  coapacket[5] = 's';
  coapacket[6] = 'e';
  coapacket[7] = 'n';
  coapacket[8] = 's';
  coapacket[9] = 'o';
  coapacket[10] = 'r';
  coapacket[11] = 's';
  coapacket[12] = 0xff;
  coapacket[13] = 't';
  coapacket[14] = 'e';
  coapacket[15] = 's';
  coapacket[16] = 't';
  coapacket[17] = '\0';
 
  udp.beginPacket(serverIP, 5683);
  udp.write(coapacket);
  udp.endPacket();
}

Voilà tout!


Fichier(s) joint(s) :



Arduino : créer une horloge synchronisée

Dans cet article, nous allons voir comment afficher sur un écran la date et l'heure courantes, avec la possibilité de se synchroniser sur un serveur de temps public (NTP - Network Time Protocol), comme le font tous les PC ou téléphones.

Matériel

Pour cet exemple, il faut vous munir :

  • d'un arduino (ici un DUE)
  • un shield Ethernet
  • un module RTC (Real Time Clock, ici un Gravitech)
  • un écran LCD

Voici un schéma du montage :

L'écran est branché, de manière classique, sur des broches numériques. Le module RTC quant à lui est intégré sur le bus I2C par les broches SDA1 et SCL1.

Logiciel

Nous allons utiliser les librairies :

  • Ethernet
  • Wire1 (pour le bus I2C)
  • LiquidCrystal
  • Time (pour faciliter la décomposition de la date à partir d'un timestamp)

Données importantes :

Selon la spécification du constructeur, l'adresse de communication du module RTC sur le bus I2C est 0x68. Le serveur NTP choisi est celui de l'Observatoire de Paris (ntp-p1.obspm.fr, IP: 145.238.203.14). Le dialogue avec un serveur NTP se fait par échange de paquets par le protocole UDP.

C'est parti!

1. Configuration d'une date par défaut sur le module RTC

Commençons par un premier échange avec ce module :

const int I2C_address = 0x68;
...
Wire1.begin();        // join i2c bus (address optional for master)
Wire1.beginTransmission(I2C_address);// Début de transaction I2C
Wire1.write(0); // Arrête l'oscillateur
Wire1.write(0); // sec
Wire1.write(0x11); // min
Wire1.write(0x20); // heure
Wire1.write(6); // jour de la semaine
Wire1.write(0x20); // jour
Wire1.write(9); // mois
Wire1.write(0x14); // annee
Wire1.write(0); // Redémarre l'oscillateur
Wire1.endTransmission(); // Fin de transaction I2C

Ici nous définissons la date "20/09/2014 20:11:00". Remarquez que les valeurs transmises sont au format hexadécimal.

2. Lecture de la date sur le module RTC

Interrogeons notre module pour récupérer la date :

void getTime(char* tempData) {
 byte i = -1;
 byte error;
 
 Wire1.beginTransmission(I2C_address); // Début de transaction I2C
 Wire1.write(0); // Demande les info à partir de l'adresse 0 (soit toutes les info)
 error = Wire1.endTransmission(); // Fin de transaction I2C
 
 if(error==0) {
  Wire1.requestFrom(I2C_address, 7); // Récupère les info (7 octets = 7 valeurs correspondant à l'heure et à la date courante)
 
  while(Wire1.available())        
  {
   tempData[++i] = Wire1.read(); // receive a byte as character
  }
 } else {
  // le composant RTC n'etait pas joignable, on relance le bus I2C pour la prochaine fois
  Wire1.begin();
 }
}

Cette méthode nous permet de récupérer, dans un tableau de 7 char (tempData), les données du module.

3. Affichage de la date sur l'écran

void displayTime()
{
 char tempchar[7] = {'\0'};
 // recuperation de la date
 getTime(tempchar);
 // affichage
 if(tempchar[0]!='\0') {
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.print(tempchar[4],HEX);
  lcd.write("/");
  lcd.print(tempchar[5],HEX);
  lcd.write("/20");
  lcd.print(tempchar[6],HEX);
  lcd.setCursor(0, 1);
  lcd.print(tempchar[2],HEX);
  lcd.write(":");
  lcd.print(tempchar[1],HEX);
  lcd.write(":");
  lcd.print(tempchar[0],HEX);
 }
}

Avec cette méthode, on envoie à l'écran chaque caractère (toujours au format hexadecimal) retourné par l'appel au module RTC.

Il ne vous reste plus qu'à placer l'appel à cette méthode dans une boucle toutes les secondes pour voir défiler le temps. Essayez même de couper l'alimentation du module RTC : l'affichage de l'heure se fige, jusqu'à nouvelle alimentation du module. Elle sera alors mise à jour avec la date qui a continué à défiler grâce à la pile du module.

4. Interrogation du serveur de temps

Le shield Ethernet va nous permettre de contacter le serveur public et de récupérer les informations de date "de référence".

IPAddress timeServer(145, 238, 203, 14); // ntp-p1.obspm.fr (IP: 145.238.203.14)
const int NTP_PACKET_SIZE= 48; // NTP time stamp is in the first 48 bytes of the message
uint8_t packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
/* Set this to the offset (in seconds) to your local time
   GMT - 2 */
const long timeZoneOffset = -7200L;  
// A UDP instance to let us send and receive packets over UDP
EthernetUDP udpClient;
EthernetClient client;

/**
* Démarrage des librairies
*/
Ethernet.begin(mac);
udpClient.begin(8888); // port UDP local


/**
* Envoi de la requete 
*/
// set all bytes in the buffer to 0
memset(packetBuffer, 0, NTP_PACKET_SIZE);
// Initialize values needed to form NTP request
// (see URL above for details on the packets)
packetBuffer[0] = 0b11100011;   // LI, Version, Mode
packetBuffer[1] = 0;     // Stratum, or type of clock
packetBuffer[2] = 6;     // Polling Interval
packetBuffer[3] = 0xEC;  // Peer Clock Precision
// 8 bytes of zero for Root Delay & Root Dispersion
packetBuffer[12]  = 49;
packetBuffer[13]  = 0x4E;
packetBuffer[14]  = 49;
packetBuffer[15]  = 52;

// all NTP fields have been given values, now
// you can send a packet requesting a timestamp:
udpClient.beginPacket(timeServer, 123); //NTP requests are to port 123
udpClient.write(packetBuffer,NTP_PACKET_SIZE);
udpClient.endPacket();


/**
* Parcours de la réponse du serveur
*/
unsigned long epoch = 0;
if ( udpClient.parsePacket() ) {
 // We've received a packet, read the data from it
 udpClient.read(packetBuffer,NTP_PACKET_SIZE);  // read the packet into the buffer

 //the timestamp starts at byte 40 of the received packet and is four bytes,
 // or two words, long. First, esxtract the two words:

 unsigned long highWord = word(packetBuffer[40], packetBuffer[41]);
 unsigned long lowWord = word(packetBuffer[42], packetBuffer[43]);
 // combine the four bytes (two words) into a long integer
 // this is NTP time (seconds since Jan 1 1900):
 unsigned long secsSince1900 = highWord << 16 | lowWord;

 // now convert NTP time into everyday time:
 // Unix time starts on Jan 1 1970. In seconds, that's 2208988800:
 const unsigned long seventyYears = 2208988800UL + timeZoneOffset;
 // subtract seventy years:
 epoch = secsSince1900 - seventyYears;
}


/**
* Configuration du module RTC avec la nouvelle date
*/
setTime(epoch); // on affecte le timestamp récupéré à la librairie Time
Wire1.beginTransmission(I2C_address);// Début de transaction I2C
Wire1.write(0); // Arrête l'oscillateur
String strval = String(second(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // sec
strval = String(minute(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // min
strval = String(hour(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // heure
Wire1.write(weekday()-1); // jour de la semaine
strval = String(day(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // jour
strval = String(month(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // mois
strval = String(year(), DEC);
Wire1.write(strtol(strval.c_str(),NULL, HEX)); // annee
Wire1.write(0); // Redémarre l'oscillateur
Wire1.endTransmission(); // Fin de transaction I2C

La librairie Time nous permet de décomposer facilement les données de la date. Mais le serveur NTP nous a envoyé la date au format décimal "20/09/2014 20:11:00". Il faut donc convertir les données au format hexa avant de configurer le module RTC.

Ainsi, après l'appel à ce code (à découper en méthodes pour plus de clarté), l'affichage de la date sera automatiquement actualisé avec la date de référence.

Have fun!

Sources :


Fichier(s) joint(s) :