mercredi 29 janvier 2020

ESP32-CAM: un web server minimaliste


Lors du premier essai de ma ESP32-CAM, j'ai utilisé l'exemple officiel "CameraWebServer", conçu par l'équipe d'Espressif et fourni avec l'IDE Arduino.

Cet exemple a visiblement été créé pour en mettre plein la vue: il créé une page web sophistiquée qui comporte une grande quantité de boutons et de glissières permettant à l'utilisateur de régler tous les paramètres de l'image, d'activer la reconnaissance faciale, etc.  Il s'agit d'un programme assez complexe (1400 lignes de code réparties en 4 fichiers) et souvent nébuleux (les commentaires sont rares!).

Lorsque j'ai voulu apporter quelques modifications à la page web générée par cet exemple, j'ai eu la surprise de constater que son code html a été compressé (gzip) et présenté par une longue suite d'octets dans le fichier camera_index.h: vraiment pas pratique si vous désirez apporter des modifications!


Owen (alias easytarget) a apporté une solution à ce problème: dans sa version améliorée de l'exemple CameraWebServer, le code html de la page web est directement accessible dans le fichier "camera_index_ov2640.h": c'est beaucoup plus facile de comprendre comment la page web est générée, et d'y apporter des modifications. Owen a pris soin de préserver toutes les fonctionnalités de l'exemple officiel, et en a même ajouté quelques-unes (contrôle du flash et de la LED indicatrice, par exemple).

La version easytarget est donc devenue mon nouveau point de départ. Mais le programme demeure lourd: qu'est-ce qui fait quoi, exactement? Pas facile de s'y retrouver.

Je me suis donc donné comme mission de produire un sketch qui ne fera qu'une seule chose: afficher dans une page web l'image vidéo de l'ESP32-CAM. Ça a donné le sketch de 225 lignes présenté ci-dessous.

Le sketch

Dans ma version simplifiée, la page web générée par l'ESP32-CAM ne présente rien d'autre que l'image vidéo: l'utilisateur ne dispose d'aucun bouton qui lui permettrait de modifier le contraste, la luminosité, etc.

Je me suis également permis, très égoïstement de ne conserver que ce qui concerne le modèle d'ESP32-CAM que je possède (AI Thinker). Certaines modifications devront être apportées au sketch si vous désirez l'utiliser sur un autre modèle.

La routine "stream_handler()" s'occupe du "stream server": elle gère l'affichage en direct de l'image vidéo. La version officielle comportait certaines instructions liées à la reconnaissance faciale, qui ont été retirées.

La routine "web_handler()" construit la page web. Cette modeste page web est logée dans un tableau de 175 caractères.

La routine "startCameraServer()" démarre le web server et le stream server.

setUp() initialise la caméra et établit la connexion au réseau WiFi, et loop() n'a rien à faire puisque tout sera géré par stream_handler() et web_handler().

-
-

Utilisation

Au démarrage, l'adresse IP de la caméra est affichée dans le moniteur série.


On colle cette adresse dans un navigateur web pour accéder en direct à la vidéo.



À lire également

Première utilisation de l'ESP32-CAM avec l'IDE Arduino ,  enregistrer des photos sur la carte microSDles LEDs de l'ESP32-CAM.

Yves Pelletier (TwitterFacebook)


dimanche 26 janvier 2020

Robot Raspberry Pi


Le robot que nous allons assembler dans ce tutoriel est une plate-forme mobile dont le mouvement est contrôlé par un Rasbperry Pi. Pour l'instant, le robot ne comportera pas de capteurs qui lui permettraient de prendre des décisions de façon autonome: il sera plutôt contrôlé par des commandes  que nous lui procurerons par SSH à partir d'un autre ordinateur ou d'un téléphone. Bien entendu, des capteurs pourront être ajoutés au robot dans une étape subséquente.

 Matériel
  • Un Raspberry Pi capable de communiquer en WiFi. N'importe quel modèle fera l'affaire, j'en ai même profité pour mettre à contribution mon antique Raspberry Pi 1 modèle B (muni d'une petite clé WiFi). Il faut que la communication SSH soit activée dans le logiciel "Configuration du Raspberry Pi".


  • Une plate-forme mobile munie de deux roues, chaque roue étant contrôlée par son propre moteur à courant continu. C'est relativement facile de s'en procurer une toute faite. Si vous préférez la construire vous-même, assurez-vous d'utiliser des moteurs munis d'une boîte d'engrenages, sinon les roues tourneront beaucoup trop vite.

  • Un pilote de moteur: il s'agit d'un petit circuit qui sert d'intermédiaire entre le Raspberry Pi et les moteurs, car le courant nécessaire pour faire tourner un moteur est beaucoup trop intense pour les délicates broches GPIO du Raspberry Pi. Dans ce tuto, j'utilise le module L298N photographié ci-contre, mais il existe plusieurs autres possibilités­. Si vous préférez utiliser le L293D, par exemple, vous trouverez des informations utiles dans cet article concernant le contrôle de moteurs à courant continu avec un Rasperry Pi. Le script en python fourni un peu plus loin dans le présent billet devrait fonctionner correctement peu importe le pilote de moteur que vous utiliserez.
  • Une source d'alimentation pour le Raspberry Pi. J'ai utilisé un chargeur mobile pour téléphone ("USB power bank") qui a rempli son rôle à la perfection. 

  • Une source d'alimentation pour les moteurs: on évite bien des problèmes potentiels en utilisant une source d'alimentation dédiée aux moteurs, distincte de celle qui alimente le Raspberry Pi. Pour mes tests, j'ai utilisé un accumulateur NiMH de 9 V, qui n'est certainement pas le meilleur choix (avec sa capacité de 175 mAh, mon robot n'ira pas très loin). Une batterie LiPo ou Li-Ion de 7,4 V ou 6 piles de format AA auraient été des choix plus appropriés.
Schéma du circuit

Le module L298N comporte des broches d'alimentations (GND et +12), 6 entrées qui permettent au Raspberry Pi de contrôler les moteurs (ENA, IN1, IN2, IN3, IN4, ENB), et 4 sorties vers les moteurs (OUT1, OUT2, OUT3, OUT4). Il y a également une sortie 5 V, que je n'ai pas utilisée.
  • Broche +12 du L298N: Borne positive de l'alimentation des moteurs (12 V est un maximum).
  • Broche GND du L298N: Borne négative de l'alimentation des moteurs et une broche GND du Raspberry Pi.
  • OUT1 et OUT2 du L298N branchées à un des moteurs.
  • OUT3 et OUT4 du L298N branchées à l'autre moteur
  • ENA du L298N broche BCM 25 (BOARD 22) du Raspberry Pi
  • IN1 du L298N  broche BCM 23 (BOARD 16) du Raspberry Pi
  • IN2 du L298N broche BCM 12 (BOARD 18) du Raspberry Pi
  • IN3 du L298N broche BCM 10 (BOARD 19) du Raspberry Pi
  • IN4 du L298N broche BCM 9 (BOARD 21) du Raspberry Pi
  • ENB du L298N broche BCM 11 (BOARD 23) du Raspberry Pi


Script en Python

Pour contrôler le robot, j'ai installé dans le Raspberry Pi le script en Python ci-dessous, et je l'ai exécuté par SSH au moyen d'un téléphone ou d'un autre ordinateur.

Les commandes sont des chiffres de 0 à 9, dont la disposition respecte une certaine logique si vous utilisez un pavé numérique:
    numeric-keypad
  • 5: Arrêt. Les deux moteurs sont à l'arrêt (broche "enable" au niveau logique bas).
  • 8: Marche avant en ligne droite: les deux moteurs tournent dans le même sens.
  • 2: Marche arrière en ligne droite: les deux moteurs tournent dans le même sens.
  • 4: Rotation vers la gauche: les deux moteurs tournent en sens contraire
  • 6: Rotation vers la droite: les deux moteurs tournent en sens contraire
  • 7: Virage à gauche en avançant: le moteur de droite tourne, mais pas celui de gauche
  • 9: Virage à droite en avançant: le moteur de gauche tourne, mais pas celui de droite
  • 1: Virage à gauche en reculant: le moteur de droite tourne, mais pas celui de gauche
  • 3: Virage à droite en reculant: le moteur de gauche tourne, mais pas celui de droite
  • 0: Arrêt du programme
-
-


Pour terminer, une courte vidéo du véhicule en action:



À lire également:

En utilisant un Arduino plutôt qu'un Raspberry Pi, j'avais fait un robot qui se déplace selon une trajectoire pré-programmée, un robot téléguidé, un robot suiveur de ligne, un robot éviteur d'obstacles...


Yves Pelletier (TwitterFacebook)

samedi 18 janvier 2020

ESP32-CAM: première utilisation avec l'IDE Arduino


L'ESP32-CAM est carte offerte à prix très modique (moins de 10 euros) qui comporte un microcontrôleur ESP32 et une caméra OV2640. Une des caractéristiques les plus intéressantes de l'ESP32 étant la possibilité de communiquer en WiFi, une utilisation évidente de l'ESP-32 CAM consiste à transmettre en direct des images vidéo par WiFi (caméra de surveillance, etc.).

En plus d'une caméra OV2640, le module est équipé d'un lecteur de cartes micro SD qui pourra éventuellement servir à stocker des images ou des séquences vidéo.

Si vous êtes déjà familier avec les modules ESP-32 conventionnels, vous remarquerez toutefois que le module ESP32-CAM ne comporte aucun connecteur USB: pour programmer le microcontrôleur, vous devez utiliser un convertisseur USB-Série fonctionnant à un niveau logique de 3,3 V.

De plus, puisque la caméra accapare un certain nombre d'entrées/sorties du microcontrôleur, les broches GPIO disponibles sont beaucoup moins nombreuses que sur un module ESP-32 conventionnel (il n'y en a que 8, et 6 d'entre elles sont déjà connectées au lecteur de carte SD).

Le module que j'ai reçu était déjà assemblé: nul besoin de souder soi-même les connecteurs, ni même de connecter la caméra.

Dans ce court tutoriel, je vous explique comment, dans les minutes suivant la réception de mon module ESP32-CAM, j'ai pu utiliser un exemple fourni avec l'IDE Arduino pour diffuser en direct sur une page web la vidéo captée par la caméra.

Connexions de l'ESP32-CAM au convertisseur USB-série

Pour programmer mon module ESP32-CAM, j'ai utilisé un convertisseur USB-série qui fonctionne à un niveau logique de 3,3 V.


Pour la phase de programmation, les connexions étaient les suivantes:

  • Sortie 5 V du convertisseur USB-série:  Entrée 5 V de l'ESP32-CAM
  • Broche GND du convertisseur USB-série: Broche GND de l'ESP32-CAM
  • Broche TXD du convertisseur USB-série: Broche UDR de l'ESP32-CAM
  • Broche RXD du convertisseur USB-série: Broche UDT de l'ESP32-CAM
  • Broche IO0 branchée à GND



Le module est donc alimenté en 5 V pendant la programmation (je n'ai pas testé, mais j'ai lu sur des forums de discussion que certaines personnes ont eu des problèmes en tentant de programmer sous une alimentation de 3,3 V).

L'information transite de l'ordinateur vers le microcontrôleur (TXD / UDR) et du microcontrôleur vers l'ordinateur (RXD / UDT) à un niveau logique de 3,3 V (si votre convertisseur USB-série est muni d'un commutateur permettant de sélectionner le niveau logique, veillez à le régler à 3,3 V).

Pour mettre l'ESP32-CAM en mode programmation, il faut que la broche IO0 (ou GPIO 0) soit reliée à la masse (GND); on appuie ensuite sur le bouton "reset". Pour exécuter le programme, on débranche la broche IO0 et on appuie à nouveau sur le bouton "reset".

Après quelques heures de développement sur cette carte, j'ai bricolé un rudimentaire support en carton qui permet de maintenir la caméra à un angle pertinent tout en laissant le bouton "reset" facilement accessible. De plus, j'ai placé un interrupteur entre la broche IO0 et la masse pour éviter de devoir sans cesse débrancher et rebrancher un fil.





Réglages de l'IDE Arduino

Les cartes de la famille ESP32 ne sont pas incluses par défaut avec l'IDE Arduino; il faut les installer en passant par le "Gestionnaire de cartes". Si ce n'est pas déjà fait, vous trouverez la marche à suivre détaillée dans ce précédant billet.


Parmi les nombreuses cartes ESP32 disponibles, vous sélectionnez "AI Thinker ESP32-CAM". (À vrai dire, rien sur mon module ou sur la description offerte par le vendeur ne mentionne "AI Thinker", mais ce choix a fonctionné correctement.)

Vous ouvrez ensuite l'exemple "CameraWebServer" (Menu Fichier - Exemples - ESP32 - Camera - CameraWebServer).


Par défaut, le sketch est réglé pour être utilisé avec le modèle WROVER: utilisez plutôt la ligne 14 "#define CAMERA_MODEL_AI_THINKER".

De plus, écrivez les paramètres de votre réseau WiFi aux lignes 18 et 19.


Après vous être assuré que la broche IO0 est reliée à la masse GND et appuyé sur le bouton "reset", vous pouvez maintenant verser le sketch dans votre ESP32.


Si tout se passe bien, vous devriez voir, au bout de quelques dizaines de secondes, un message indiquant la réussite du téléversement.



Pour démarrer le programme, vous débranchez le fil reliant la broche IO0 à la masse et appuyez sur le bouton "reset".

Le moniteur série affiche l'adresse IP qui permettra de visionner l'image dans un navigateur web.


On copie cette adresse dans le navigateur web de notre choix. Dans la page web qui s'affiche, il s'agit de cliquer sur le bouton "Start Stream" pour que l'image captée par le module ESP32-CAM s'affiche à l'écran. Plusieurs contrôles situés à la gauche de la fenêtre permettent de modifier les caractéristiques de l'image.




En ce qui me concerne, cette première exploration du module ESP32-CAM s'est effectué sans le moindre pépin. J'espère bien, dans un proche avenir, apprendre à personnaliser les programmes pour, par exemple, enregistrer des images sur une carte SD, etc.

À lire également

D'autres articles sur l'ESP32-CAM:  un web server minimaliste , enregistrer des photos sur la carte microSDles LEDs de l'ESP32-CAM.

Mon article sur sur le module caméra du Raspberry Pi, et la liste des articles impliquant l'ESP32.

Yves Pelletier (TwitterFacebook)

dimanche 12 janvier 2020

Analyse d'une communication I2C


Tout comme nous l'avons déjà fait pour les communications UART et SPI, nous allons aujourd'hui décrire les principales caractéristiques d'une communication I2C , et observer une transaction I2C typique au moyen d'un analyseur logique.

Mis au point par la compagnie Philips en 1982,  I2C est un protocole de communication série qui permet à un microcontrôleur d'interagir avec une grande quantité de périphériques en utilisant uniquement deux lignes de transmission (on l'appelle d'ailleurs parfois "Two Wire Interface"). Le nom " I2C " est l'acronyme de "inter-integrated circuit"; donc "IIC" apparemment transformé en "I2C" pour faire plus original.

Il s'agit d'une communication série synchrone bidirectionnelle half-duplex...

"Série" car les bits qui forment un octet sont transmis un à la suite de l'autre sur la même ligne de transmission (par opposition à une communication parallèle, dans laquelle chaque bit serait transmis sur sa propre ligne). UART et SPI sont deux autres exemples de communication série.

"Synchrone" car un signal d'horloge sert de référence à tous les appareils impliqués dans la communication. Ce signal d'horloge est géré par le "maître" et partagé sur la ligne SCL ("serial clock").

"Bi-directionnelle" car l'information peut être transmise du maître à l'esclave et de l'esclave vers le maître.

"Half-duplex" car, puisque qu'une seule ligne de transmission est utilisée pour les deux directions (la ligne SDA "serial data"), les informations ne peuvent circuler que dans une direction à la fois.

Puisqu'il peut y avoir un très grand nombre d'esclaves, chaque esclave dispose d'une adresse unique, constituée de 7 bits (ce qui donne une possibilité théorique de 128 esclaves). La présence de plusieurs maîtres est également possible.

Quelques exemples de périphériques  I2

Le nombre de capteurs et d'afficheurs qui peuvent communiquer avec l'Arduino par I2C est assez impressionnant: écran OLED SH1106,  accéléromètre MMA7455capteur de lumière TSL2561récepteur radio FM RDA5807convertisseur analogique-numérique PCF8591,  accéléromètre/gyro MPU-6050, capteur de pression atmosphérique BMP180magnétomètre/boussole HMC5883L, horloge temps réel DS1307, mémoire EEPROM, etc.

Drain ouvert 

Tous les appareils  I2C  sont connectés à la ligne SDA et à la ligne SCL par une sortie à drain ouvert (ou à collecteur ouvert, si vous préférez la terminologie des transistors bipolaires). Le circuit est schématisé ci-contre. Il s'agit d'un circuit inverseur, puisque sa sortie est au niveau logique '0' lorsque l'entrée est au niveau logique '1', et sa sortie est au niveau '1' lorsque l'entrée est au niveau '0'.

Lorsque l'entrée (la grille du transistor) est à 0 V, le trajet source-drain ne conduit pas le courant, aucun courant ne traverse la résistance et la sortie se trouve à un potentiel de 5 V. Lorsque l'entrée est à 5 V, le trajet source-drain devient conducteur et relie la sortie à la masse (GND).

Imaginez plusieurs circuits de ce genre dont la sortie est reliée à la même ligne (SDA ou SCL): il s'agit qu'un seul de ces circuits relie la ligne de transmission à la masse pour qu'elle se retrouve au niveau logique '0', même si tous les autres circuits tentent de la maintenir au niveau logique '1'.  Nous verrons un peu plus loin que cette caractéristique est utilisée, entre autres,  pour permettre à un esclave d'envoyer un acquittement (aknowledge): pendant que le maître maintient la ligne SDA à '1', l'esclave l'abaisse à '0' pour indiquer qu'il a reçu l'information.

La résistance pull-up doit être assez grande pour limiter le courant circulant à travers les lignes SDA et SCL. Par contre, plus elle est grande, plus la transition entre le niveau logique '0' et le niveau logique '1' sera longue, ce qui limite la vitesse de transmission du signal. Plus le bus  I2C comporte un grand nombre d'appareils, plus la capacité est grande , ce qui nous oblige à diminuer la valeur de la résistance pull-up.

Lorsqu'il n'y a aucune communication  I2C , les lignes SDA et SCL sont au niveau logique '1', puisque c'est dans cet état qu'aucun courant ne circule à travers la résistance pull-up.

Observation d'une communication  I2

J'ai profité de mes récentes expérimentations avec une mémoire EEPROM 24LC01B branchée à une carte Arduino pour observer une communication  I2C  typique au moyen d'un analyseur logique et du logiciel Sigrok Pulseview. La communication  I2C  étant, comme d'habitude, gérée par la bibliothèque Wire.


L'EEPROM 24LC01B était branché à l'Arduino de la façon suivante:
  • Les broches 1, 2 et 3 du 24LC01B ne sont pas branchées. 
  • Broche 4 du 24LC01B branchée à la masse (GND).
  • Broche 5 du 24LC01B (SDA) branchée à la broche A4 de l'Arduino Uno, ainsi qu'à 5 V par l'entremise d'une résistance de tirage de 4,7 kΩ.
  • Broche 6 du 24LC01B  (SCL) branchée à la broche A5 de l'Arduino Uno, ainsi qu'à 5 V par l'entremise d'une résistance de tirage de de 4,7 kΩ.
  • Broche 7 du 24LC01B (WP) reliée à la masse (GND).
  • Broche 8 du 24LC01B reliée à 5 V.

Première communication: écriture d'une valeur en mémoire

Dans un premier temps, j'ai exécuté un sketch qui écrit la valeur "44" à l'adresse "63" de l'EEPROM (dont l'adresse I2C est 0x50) .


Et j'ai obtenu ce chronogramme sur Seegrok Pulseview, qui montre l'émission de 3 octets consécutifs.

La vue d'ensemble est jolie, mais c'est tout petit: on va zoomer un peu...


Première constatation: avant l'établissement de la communication I2C, les deux signaux (SCL et SDA) étaient au niveau logique '1'.

Le maître envoie ensuite un bit "start", qui consiste à mettre la ligne SDA au niveau '0' pendant que la ligne SCL demeure au niveau '1'.

Le maître envoie ensuite un octet constitué des 7 bits de l'adresse I2C du périphérique (ici 50 en hexadécimal, ou 1010000 en binaire), et d'un huitième bit nommé R/W. Le bit R/W prend la valeur '0' si le maître s'apprête à envoyer de l'information à l'esclave, et la valeur '1' si le maître demande à l'esclave de lui envoyer de l'information. Ici, ce 8e bit est à 0: les prochains octets seront donc générés par le maître et non par l'esclave.  Chaque bit est lu sur la ligne SDA au moment où la ligne SCL passe de 0 à 1 (j'ai ajouté des points noirs pour indiquer le moment de la lecture de chacun des 8 bits).

Pour terminer l'envoi de ce premier octet, le maître laisse la ligne SDA à 1, et l'esclave force cette ligne à '0' pour indiquer qu'il a bien reçu l'information et qu'il est prêt à recevoir les données (c'est le signal d'acquittement ACK, symbolisé par le "A" rose à l'extrême droite du chronogramme). Si d'autres esclaves sont présents, ils ne tiennent pas compte du reste de cette communication, puisque l'adresse énoncée n'est pas la leur.


Suite à cette confirmation que l'esclave est prêt à recevoir les données, le maître envoie un deuxième octet, qui est l'adresse de la mémoire dont on désire modifier le contenu. Il s'agit ici de 63, qui se traduit par 00111111 en binaire. À la fin, l'esclave met la ligne SDA à '0' pour indiquer qu'il a bien reçu l'information (acquittement).

Le maître envoie ensuite un troisième octet qui contient la valeur qui désire enregistrer en mémoire. Il s'agit ici de 44, qui se traduit par 00101100 en binaire (ou par 0x2C en hexadécimal, tel qu'indiqué par le logiciel PulseView sur le chronogramme ci-dessus).

C'est suivi par un dernier signal d'acquittement de la part de l'esclave et, finalement, le maître clôt la transaction par le signal "stop" (identifié par le P à l'extrême droite du chronogramme), qui consiste à faire passer la ligne SDA de '0' à '1' pendant que le signal SCL demeure à '1'.

La communication étant terminée, les deux lignes (SDA et SCL) demeurent à '1' en attendant la prochaine transaction.

Remarque: la fréquence par défaut de la communication I2C est de 100 kHz. Au besoin, j'aurais pu modifier cette fréquence en utilisant Wire.setClock() dans mon sketch. Remarquons en passant que cette fréquence est environ 10 fois plus lente que celle d'une communication SPI typique.



Deuxième communication: lecture d'une valeur en mémoire

Dans un deuxième temps, j'ai exécuté un sketch qui lit la valeur se trouvant à l'adresse "63" de l'EEPROM:


Cette fois, la transaction implique 4 octets: le maître doit d'abord indiquer à l'esclave qu'il lui envoie de l'information (l'adresse dont il veut connaître le contenu), puis lui indiquer qu'il attend la réponse.


Faisons un zoom sur chaque octet:


Encore une fois, le signal "Start", qui consiste à faire passer la ligne SDA de '1' à '0' pendant que la ligne SCL demeure à '1'.

Ensuite, l'envoi des 7 bits de l'adresse I2C de l'esclave (1010000), suivi du bit R/W qui est de '0' car le maître devra d'abord envoyer de l'information à l'esclave.

Cette partie de la transaction se termine par l'émission du signal d'acquittement par l'esclave.


Dans un deuxième temps, le maître envoie sur la ligne SDA l'adresse de la mémoire EEPROM dont il désire connaître le contenu (00111111 qui correspond à 63 en décimal). Si à la réception de cette adresse, l'esclave génère le bit d'acquittement, et le maître clôt la transaction par un bit "stop" (la ligne SDA passe de '0' à '1' pendant que SCL reste à '1').


Le 3e octet permet au maître d'aviser l'esclave que c'est maintenant à lui, l'esclave, d'envoyer l'information (qui sera le contenu de l'adresse qu'on lui a précédemment communiqué). Ce troisième octet est identique au premier, sauf qu'il se termine par '1' plutôt que par '0'. Le bit R/W de '1' indique que le prochain échange s'effectuera de l'esclave vers le maître.


Le 4e octet est généré par l'esclave. 00101100 correspond à 44 en décimal, qui est la valeur précédemment enregistrée à l'adresse 63.

Cette fois, c'est le maître qui est responsable d'émettre un signal d'acquittement. S'il le fait, l'esclave lui enverra une autre donnée (le contenu de l'adresse 64). Sinon (signal "N" de non-acquittement), l'esclave cesse d'envoyer des données, et le maître clôt la transaction en faisant passer la ligne SDA de '0' à '1' pendant que SCL est maintenu à '1'.

En cas de mauvaise adresse

Qu'arrive-t-il si le maître envoie une instruction à une adresse qui ne correspond à aucun esclave connecté? Pour vérifier, j'ai modifié mon sketch pour qu'il tente d'écrire une valeur à un esclave (inexistant) dont l'adresse serait 0x60 plutôt que 0x50.


Cette fois, la transaction se limite à un seul octet. Le maître envoie un message constitué des 7 bits de l'adresse i2c 1100000 (qui ne correspond à aucun esclave connecté) et du bit R/W de 0. N'ayant détecté aucun signal d'acquittement, le maître met fin à la transaction.

Avantages/inconvénients

Une communication I2C est moins rapide qu'une communication SPI, entre autres parce que la fréquence de l'horloge est plus faible, parce que l'information ne peut circuler que dans une direction à la fois, parce qu'une partie de la communication consiste à transmettre l'adresse de l'esclave concerné, etc.  Par contre on peut brancher un grand nombre de périphériques en utilisant seulement 2 broches du microcontrôleur.

À lire également

Des articles similaire ont été publiés concernant la communication SPI et la communication UART.


Yves Pelletier (TwitterFacebook)

vendredi 10 janvier 2020

Utiliser une mémoire EEPROM I2C (Arduino)


Après avoir exploré l'utilisation de la mémoire EEPROM interne de l'Arduino,  voyons maintenant comment utiliser une mémoire EEPROM externe communiquant en I2C .



Je rappelle que le sigle EEPROM signifie "Electrically-Erasable Programmable Read-Only Memory". Il s'agit d'une mémoire non-volatile: les informations persistent après que l'EEPROM ait été mis hors-tension.

Une mémoire EEPROM externe: pourquoi?

Dans le dernier article, nous avons mentionné quelques utilisations pertinentes d'une mémoire EEPROM, comme sauvegarder les préférences d'un utilisateur ou les paramètres de calibration d'un capteur. On peut toutefois se demander à quoi pourrait bien servir une mémoire EEPROM externe alors que l'Arduino dispose déjà de sa propre mémoire EEPROM interne.

Quelques possibilités:
  • ajouter de la mémoire supplémentaire, si l'espace disponible dans l'EEPROM interne est insuffisant.
  • augmenter la durée de vie de l'EEPROM (la fiche technique des circuits intégrés que j'ai testés pour cet article garantit une durée de vie de l'ordre du million de réécritures, alors que celle de  l'EEPROM interne de l'Arduino est plutôt de 100 000 réécritures).
  • utilisation de l'EEPROM externe comme une clé de sécurité amovible qui serait nécessaire pour faire fonctionner l'Arduino, ou qui donnerait à son utilisateur des privilèges d'administrateur.
Les modèles d'EEPROM I2C testés

Pour les besoins de cet article, j'ai fait l'essai de 3 mémoires EEPROM  I2C:
Avec un peu de chance, les informations que vous trouverez ici vous serons également utiles pour d'autres modèles d'EEPROM  I2C.

Il est important de mentionner que la capacité des mémoires EEPROM I2C externes est donnée en kilobits et non en kilooctets. Ainsi, une mémoire EEPROM de 1K comporte 1024 bits (128 octets) et contient donc 8 fois moins d'information que la mémoire EEPROM interne de l'Arduino qui, elle, est de 1 kilooctet!

Connexions de l'EEPROM à l'Arduino

Les EEPROM I2C prennent la forme d'un circuit intégré de 8 broches.



Les 3 modèles de mémoire EEPROM que j'ai testés se branchent à l'Arduino de la même façon:


  • Les broches 1, 2 et 3 du CI ne sont pas branchées. Pour le modèle 24C64A, on peut relier certaines d'entre elles à 5 V pour modifier l'adresse I2C. Pour les deux autres modèles, ces 3 broches ne servent à rien!
  • La broche 4 du CI doit être branchée à la masse (GND).
  • La broche 5 du CI est responsable de l'échange des données (SDA). On la branche à la broche A4 de l'Arduino Uno, ainsi qu'à 5 V par l'entremise d'une résistance de tirage de quelques kiloohms (j'ai utilisé 4,7 kΩ).
  • La broche 6 du CI reçoit le signal d'horloge (SCL). On la branche à la broche A5 de l'Arduino Uno, ainsi qu'à 5 V par l'entremise d'une résistance de tirage de quelques kiloohms (j'ai utilisé 4,7  kΩ).
  • La broche 7 du CI (WP) doit être reliée à la masse (GND) pour qu'il soit possible d'écrire dans l'EEPROM. Si vous désirez ensuite empêcher que l'information de l'EEPROM puisse être modifiée, vous pouvez brancher cette broche à 5 V.
  • La broche 8 du CI doit être reliée à 5 V.

Exemples de sketch

Même si mes 3 modèles d'EEPROM se branchent exactement de la même façon et qu'ils ont la même adresse I2C (0x50), j'ai dû écrire un sketch légèrement différent pour chaque modèle!

Dans chaque cas, j'ai écrit un premier sketch  qui écrit la valeur "100" à l'adresse numéro 5 de l'EEPROM, et un deuxième sketch qui lit la valeur enregistrée à chaque adresse de l'EEPROM.

Si l'EEPROM n'a jamais été utilisé auparavant, l'exécution du sketch de lecture affichera la valeur maximale de 255 pour chaque adresse:


Le sketch d'écriture enregistre la valeur "100" à l'adresse 5 de l'EEPROM.


On pourra ensuite exécuter à nouveau le sketch de lecture pour vérifier que l'adresse 5 contient maintenant la valeur 100:


Dans tous les cas, j'ai utilisé la bibliothèque Wire intégrée à l'IDE Arduino pour gérer la communication I2C.

Exemples de sketch pour le 24LC01B

Le 24LC01B (fiche technique) comporte 128 octets numérotés de 0 à 127. Chacun de ces 128 emplacements de mémoire peut contenir une valeur située entre 0 et 255.

Le sketch ci-dessous écrit la valeur "100" à l'adresse "5" de l'EEPROM. Cette écriture ne s'exécute qu'une seule fois, au démarrage du programme. L'essentiel du travail s'effectue dans la routine writebyte():

  • On débute la transmission en indiquant l'adresse I2C du 24LC01B:  Wire.beginTransmission(0x50);
  • On envoie au 24LC01B l'adresse de l'octet dont on désire modifier la valeur: Wire.write(5);
  • On envoie au 24LC01B la valeur qu'on désire enregistrer à cette adresse: Wire.write(100);
  • On clôt la transmission: Wire.endTransmission();

---
---

Le sketch ci-dessous affiche dans le moniteur série la valeur enregistrée à chaque adresse de l'EEPROM. L'essentiel du travail s'effectue dans la routine readbyte():
  • On débute la transmission en indiquant l'adresse I2C du 24LC01B:  Wire.beginTransmission(0x50);
  • On envoie au 24LC01B l'adresse de l'octet dont on désire lire la valeur: Wire.write(5);
  • On clôt la transmission: Wire.endTransmission();
  • On demande au 24LC01B de nous envoyer l'information: Wire.requestFrom(0x50, 1);
  • On récupère l'information reçue: if (Wire.available()) {lecture = Wire.read();}
---
---


Exemples de sketch pour le 24LC16B

Le 24LC16B comporte 8 blocs de 256 octets (donc un total de 2048 octets). Puisque 11 bits sont nécessaires pour exprimer les 2048 adresses, 3 de ces bits (ceux de poids fort) doivent être ajoutés à la fin de l'adresse I2C .

Nous devons donc faire appel aux opérations bit à bit...

Plutôt que simplement écrire Wire.beginTransmission(adressei2c), nous écrivons:

Wire.beginTransmission((int)(adressei2c | adresseMem >> 8));

L'expression "adressei2c | adresseMem >> 8" ajoute les bits de poids fort de l'adresse mémoire à la fin de l'adresse  I2C du 24LC16B.

Et plutôt que simplement écrire Wire.write(adresseMem), nous écrivons:

Wire.write((int)(adresseMem & 0b11111111));

L'expression "adresseMem & 0b11111111" ne conserve que les 8 bits de poids faible de l'adresse mémoire (ceux de poids forts ayant déjà été transmis en même temps que l'adresse  I2C.

Voici donc le sketch qui écrit la valeur 100 à l'adresse 5 du 24LC16B:

---
---

...et voici le sketch qui lit chacune des 2048 valeurs enregistrées dans le 24LC16B:

---
---

Exemples de sketch pour le 24C64A

Le 24C64A comporte 8192 octets, et deux octets sont nécessaires pour écrire une adresse. Plutôt qu'envoyer une partie de l'adresse mémoire en même temps que l'adresse I2C , on sépare les 16 bits de l'adresse mémoire en deux octets:

Wire.write((int)(adresseMem >> 8));   // bits de poids fort de l'adresse mémoire
Wire.write((int)(adresseMem & 0xFF)); // bits de poids faible de l'adresse mémoire

Voici le sketch qui écrit la valeur 100 à l'adresse 5 du 24C64A:

---
---

...et voici celui qui lit chacune des 8192 valeurs enregistrées dans le 24C64A:

---
---


Yves Pelletier (TwitterFacebook)


mardi 7 janvier 2020

Utiliser la mémoire EEPROM interne de l'Arduino


Les cartes Arduino disposent d'une mémoire EEPROM ("Electrically-Erasable Programmable Read-Only Memory"): il s'agit d'un endroit où vous pouvez stocker des informations qui demeureront disponibles même après que l'Arduino ait été mis hors tension pendant un certain temps, ou après que vous ayez téléversé un nouveau sketch dans l'Arduino.

La mémoire EEPROM pourrait servir, par exemple, à conserver en mémoire un numéro permettant d'identifier de façon unique une carte Arduino,  les préférences de l'utilisateur, les paramètres de calibration d'un capteur, etc.

Attention, cependant, à une contrainte importante: le nombre d'écritures sur une même adresse de la mémoire EEPROM est limitée à environ 100 000. Ça peut sembler énorme à première vue, mais tout dépend de l'utilisation que vous en faites... Si vous écrivez continuellement une nouvelle information une fois par heure, la mémoire EEPROM devrait théoriquement pouvoir tenir le coup pendant un peu plus de 11 ans. Mais cette durée de vie passe à environ 2 mois si l'information est réécrite une fois par minute, et à environ une journée si l'information est mise à jour à chaque seconde!  Précisons toutefois que cette estimation de 100 000 écritures est généralement considérée comme extrêmement sévère: dans la vraie vie, l'EEPROM de votre Arduino demeurera probablement fonctionnel beaucoup plus longtemps.

La mémoire EEPROM interne de la carte Arduino Uno est de 1 ko, ce qui signifie qu'on dispose de 1024 adresses (numérotées de 0 à 1023) pouvant chacune stocker un octet (donc un nombre situé entre 0 et 255). La taille disponible dépend du modèle de carte: elle est de 4 ko pour l'Arduino Mega, par exemple.

La bibliothèque EEPROM

Pour écrire ou lire l'information sur la mémoire EEPROM interne de l'Arduino, nous utiliserons la bibliothèque EEPROM. Aucune installation n'est nécessaire, puisqu'elle est présente par défaut dans l'IDE Arduino. Il est toutefois important de déclarer cette bibliothèque au début de votre sketch:


La bibliothèque est accompagnée d'une bonne quantité d'exemples fort instructifs, que vous trouverez dans le menu "Fichier / Exemples".



Lire un octet: la méthode EEPROM.read()

Pour lire l'une des 1024 valeurs stockées dans l'EEPROM, on utilise la méthode EEPROM read:

EEPROM.read(adresse)

...où l'adresse passée en paramètre est un nombre situé entre 0 et 1023 (s'il s'agit d'une carte Arduino Uno). Ainsi, pour lire la valeur enregistrée à l'adresse 55, on pourrait écrire:

valeur = EEPROM.read(55);

À titre d'exemple, je vous suggère de jeter un oeil sur l'exemple intitulé "EEPROM_read()", accessible  dans l'IDE Arduino par le menu "Fichier / Exemples / EEPROM" . Ce sketch affiche dans le moniteur série la valeur stockée à chaque adresse de la mémoire EEPROM. Si rien n'a jamais été enregistré dans votre mémoire EEPROM, chacune des adresse contient initialement la valeur 255.


Écrire un octet: la méthode EEPROM.write()

Pour écrire une information dans l'EEPROM, on peut utiliser la méthode EEPROM.write():

EEPROM.write(adresse, valeur)

...où "adresse" est l'adresse à laquelle nous désirons écrire l'information (c'est un nombre entre 0 et 1023 pour l'Arduino Uno), et "valeur" est l'information que nous désirons enregistrer à cet endroit (un nombre entre 0 et 255).

Par exemple, l'instruction suivante enregistrera le nombre "23" à l'adresse numéro 4 de l'EEPROM:

EEPROM.write(4,23);

Si, après avoir exécuté cette instruction, j'exécute à nouveau l'exemple "EEPROM_read()". je constate que l'adresse 4 contient maintenant le nombre 23, plutôt que le nombre 255 qu'il contenait auparavant.


Vous pourriez en principe pouvoir faire tout ce que vous voulez en vous limitant à l'utilisation des méthodes EEPROM.write() et EEPROM.read(). La bibliothèque EEPROM met toutefois à notre disposition quelques méthodes supplémentaires, afin de nous simplifier la vie.

Écrire un octet, mais seulement si nécessaire:  la méthode EEPROM.update()

Comme je le mentionnais au tout début de cet article, le nombre total d'écritures d'une mémoire EEPROM est limité. Il serait donc dommage de réduire inutilement la durée de vie d'une mémoire EEPROM en y écrivant une valeur identique à celle qui s'y trouvait déjà.

La méthode EEPROM.update() est donc identique à la méthode EEPROM.write(), sauf que la nouvelle valeur sera écrite uniquement si elle est différente de la valeur déjà présente à cette adresse.

EEPROM.update(adresse, valeur)

Par exemple, l'instruction EEPROM.update(22,144) inscrira la valeur "144" à l'adresse "22", mais seulement si la valeur déjà stockée à l'adresse 22 est différente de 144.

Personnellement, je ne vois aucune raison valable de ne pas utiliser EEPROM.update() plutôt qu'EEPROM.write() chaque fois que vous désirez écrire une valeur en mémoire.

Écrire une variable de n'importe quel type: EEPROM.put()

Les 3 méthodes que nous avons explorées jusqu'à maintenant impliquent l'enregistrement ou la lecture d'un octet: un nombre à 8 bits dont la valeur se situe entre 0 et 255.

Imaginez que vous désirez stocker en mémoire EEPROM l'état d'une entrée analogique de l'Arduino: il s'agit d'une valeur à 10 bits pouvant aller de 0 à 1023, ce qui est trop grand pour être exprimé par un seul octet.

Pour contourner ce problème, vous pourriez diviser votre mesure par 4 avant de la stocker en mémoire (ce qui aurait pour effet négatif de diminuer la résolution de votre mesure), ou encore utiliser les opérateurs bit à bit pour séparer manuellement le nombre à 10 bits en deux octets différents.

Mais rien de tout ça ne sera nécessaire si vous utilisez la méthode EEPROM.put(), spécialement conçue pour stocker facilement une variable de n'importe quel type (int, long, float, etc.) ou même une structure (struct) constituée de plusieurs types différents.

La syntaxe est similaire à celle d'EEPROM.write():

EEPROM.put(adresse, valeur)

Dans l'exemple ci-dessous, j'enregistre une variable de type "long" contenant le nombre 123456789 à l'adresse 4 de l'EEPROM:

Lire une variable de n'importe quel type: la méthode EEPROM.get()

Le complément de la méthode EEPROM.put() est EEPROM.get(), qui nous permet de lire sur l'EEPROM la valeur d'une variable de n'importe quel type.

La syntaxe est:

EEPROM.get(adresse, valeur)

Par exemple, dans le sketch ci-dessous, je récupère la valeur de la variable de type "long" précédemment enregistrée à l'adresse 4, et je l'affiche dans le moniteur série.

Lors de l'exécution de ce sketch, le moniteur série a affiché 123456789, puisque j'avais précédemment enregistré cette valeur grâce à la méthode EEPROM.put().


Mais attention! Puisque tout à été fait de façon automatique par les méthodes EEPROM.put() et EEPROM.get(), il serait dangereux d'oublier que notre variable de type "long" est un nombre à 32 bits. Elle ne peut donc pas avoir été enregistré uniquement à l'adresse 4, qui ne peut contenir que 8 bits­.

Pour en avoir le coeur net, j'utilise encore une fois l'exemple EEPROM_read, fourni avec l'IDE Arduino,  qui présente dans le moniteur série le contenu de toutes les adresses:


Comme vous pouvez le constater, lorsque j'ai demandé de stocker la variable de type long à l'adresse 4 par la méthode EEPROM.put(), 4 octets ont été modifiés: ceux situés aux adresses 4, 5, 6 et 7.

L'adresse 4 contient le nombre décimal 21, ou 00010101 en binaire.
L'adresse 5 contient le nombre décimal 205, soit 11001101 en binaire.
L'adresse 6 contient le nombre décimal 91, ou 01011011 en binaire.
L'adresse 7 contient le nombre décimal 7, soit 00000111 en binaire.

En mettant ces 4 octets bout à bout pour obtenir un nombre à 32 bits, en commençant par l'adresse 7 et en terminant par l'adresse 4, nous obtenons: 00000111010110111100110100010101 , qui correspond à la valeur décimale 123456789.

Il aurait donc été catastrophique, après avoir stocké notre variable à l'adresse 4, d'en enregistrer une autre à l'adresse 5, en oubliant que cette adresse est déjà occupée!

Utiliser l'EEPROM comme un tableau d'octets: l'objet EEPROM[]

Un objet EEPROM[] vous permet d'écrire et lire des octets comme si la mémoire EEPROM était un tableau d'octets.  Ainsi, l'expression "valeur = EEPROM[5]" aura le même effet que l'expression "valeur = EEPROM.read(5)", et l'expression "EEPROM[7] = 123" aura le même effet que l'expression "EEPROM.update(7, 123)".

Connaître la taille de l'EEPROM grâce à EEPROM.length()

Tel que précisé un peu plus haut, la taille de la mémoire EEPROM n'est pas la même pour tous les modèles d'Arduino. La méthode EEPROM.length() retourne cette taille (en octets), ce qui permet à votre sketch d'utiliser la totalité de la mémoire disponible, peu importe le modèle de carte utilisé.


Yves Pelletier   (TwitterFacebook)