I. Introduction▲
I2C est le sigle d'Inter-Integrated Circuit. À la fin des années 1970, la division des semi-conducteurs de chez Philips (maintenant devenue NXP) avait vu la nécessité de simplifier et standardiser les échanges de données entre les différents circuits intégrés dans leurs produits. Leur solution fut le bus I2C, elle réduisait le nombre de lignes nécessaires à seulement deux lignes, SDA - Serial DAta, et SCL - Serial CLock.
Dans ce tutoriel, nous allons décrire l'architecture physique du bus I2C, le protocole de communication série et comment communiquer en I2C entre une carte Arduino et un capteur de température DS1621.
II. Le bus I2C▲
C'est donc maintenant chez le fabricant de semi-conducteurs NXP que vous trouverez les spécifications de la norme I2C.
À l'origine, la communication était limitée à la vitesse de transfert de 100 kbit/s, et cela suffisait dans la majorité des cas. Pour des débits plus rapides, de nouvelles spécifications sont nées. D'abord un Fast Mode à 400 kbit/s, puis un Fast Mode plus (FM+) à 1 Mbit/s. Depuis 1998, il y a une version High Speed à 3,4 Mbit/s. Le débit maximal possible via un bus I2C est spécifié dans l'Ultra Fast mode à 5 Mbit/s, mais avec un fonctionnement un peu particulier.
Caractéristiques principales du bus I2C :
- seulement deux lignes (bidirectionnelles) sont nécessaires, une pour transporter les données - SDA -, une autre pour l'horloge de synchronisation - SCK - (1 bit échangé à chaque « coup » d'horloge) ;
- transmission synchrone. Pas besoin de spécifier une vitesse de transfert comme pour la liaison RS232. Ici, le périphérique maître (master) génère le signal d'horloge qui synchronise et cadence les échanges ;
- la relation entre les périphériques du bus est de type maître-esclave (master/slave). Le maître est à l'initiative de la transmission et s'adresse à un esclave (ou tous les esclaves) ;
- chaque périphérique sur le bus I2C est adressable, avec une adresse unique pour chaque périphérique du bus ;
- l'I2C gère le fonctionnement multimaître (multi-master), plusieurs périphériques maîtres peuvent prendre simultanément le contrôle du bus (système d'arbitrage des maîtres et gestion des collisions).
III. Pourquoi utiliser des périphériques I2C ?▲
Il y a des milliers de composants qui utilisent une interface I2C, et les cartes de la famille Arduino peuvent toutes les contrôler. Les applications sont multiples : horloges temps réel, potentiomètres numériques, capteurs de température, boussoles numériques, mémoires, circuits radio FM, cartes d'extension d'entrées-sorties, contrôleurs d'afficheur LCD, accéléromètres, amplificateurs et bien d'autres encore. Vous pouvez connecter plusieurs composants adressables sur un même bus à tout moment (jusqu'à 112 périphériques I2C adressables sur un même bus en théorie).
Quelques composants et modules bien utiles avec une interface I2CÂ :
Composants I2C compatibles Arduino |
Description |
Le capteur de température et thermostat DS1621 (Maxim Integrated), objet de ce tutoriel. |
|
Mémoire morte EEPROM - 256 Kbit (Réf. 24LC256 Microchip) |
|
Module Horloge temps réel (Real Time Clock) à base de DS1307 (Maxim Integrated). |
|
Module de conversion Numérique-Analogique (Digital-to-Analog Converter) à base de MCP4725 (Microchip). |
|
Module d'interface I2C pour écran LCD |
|
Circuit d'extension Entrée-Sortie à base de PCF8575 (NXP) |
|
IV. I2C et Arduino▲
En ce qui concerne l'architecture matérielle, le câblage est très simple. Ceux qui disposent de l'Arduino Uno ou d'une carte compatible utiliseront les connecteurs A4 pour SDA (les données) et A5 pour SCL (l'horloge) :
Si vous utilisez une carte Arduino Mega, SDA est sur le connecteur 20 et SCL sur le connecteur 21. Si vous voulez connecter un shield Arduino avec une interface I2C, vérifiez bien sa compatibilité avec l'Arduino Mega. Pour les autres types de cartes, il faudra consulter la fiche technique (data sheet) ou se renseigner sur le site de la communauté Arduino. Finalement, si vous interfacez directement le microcontrôleur ATmega328-PU dans son boîtier DIP, vous utiliserez les pattes 27 pour SDA et 28 pour SCL.
L'architecture du bus est simple, ici avec deux périphériques sur le bus I2C :
Avec un seul équipement I2C connecté à l'Arduino, les résistances de tirage au plus (pull-up resistors) ne sont pas (normalement) requises, car le microcontrôleur ATmega328 de l'Arduino les intègre déjà . Si plusieurs équipements sont connectés au bus, utilisez deux résistances de 10 kΩ chacune. Comme souvent, ce sont les tests que vous effectuerez en prototypant un circuit sur une plaquette de câblage rapide que vous jugerez de leur nécessité. Parfois, dans les fiches techniques (datasheets), vous trouverez d'autres valeurs de résistance, du 4,7 kΩ par exemple. Dans ce cas, suivez les recommandations du constructeur.
La distance maximale maître-esclave pour un bus I2C est d'environ un mètre et dépend de plusieurs facteurs comme la capacité électrique du bus ou le débit de transmission. Cette distance peut être sensiblement augmentée en passant par des interfaces spécifiques (un i2c-bus extender amplifie le courant débité par les broches SDA et SCL, ce qui augmente la portée du signal).
Chaque équipement peut être connecté au bus dans n'importe quel ordre, et certains équipements peuvent même passer du statut de maître à esclave et inversement. Dans ce tutoriel, la carte Arduino sera dans la situation du maître et le composant connecté au bus I2C sera en situation d'esclave. De l'Arduino, on peut « écrire » sur la ligne pour envoyer des données vers un composant, ou « lire » la ligne pour récupérer les données retournées par celui-ci.
Se pose maintenant la question de savoir comment différencier les composants connectés sur le bus. En fait, chacun d'entre eux doit posséder une adresse unique, fixée par le constructeur et parfois configurable. Cette adresse sera utilisée pour écrire ou lire les données vers le composant souhaité.
Pour communiquer en suivant le protocole I2C, on inclura une bibliothèque Arduino dans les croquis, ici nommée Wire, avec un #include <Wire.h>
. Puis on l'initialisera avec Wire.begin
(
) à l'intérieur du void
setup
(
).
V. Exemple d'application : le capteur de température DS1621▲
V-A. Présentation du composant▲
Le DS1621 de Maxim Integrated apporte à la fois des fonctionnalités de thermomètre et de thermostat dans une puce au format DIP 8 broches, bien pratique pour faire un prototype sur une plaquette de câblage sans soudure.
Ce composant intègre toute la chaîne de traitement, de l'acquisition de la température en analogique jusqu'à la conversion numérique en mot de 16 bits. Il suffit alors d'aller consulter les registres internes selon le protocole I2C pour récupérer la température. On peut aussi configurer le DS1621 comme un thermostat avec une sortie Tout qui peut changer d'état logique lorsque la température passe en deçà d'un seuil bas (TL), ou au-delà d'un seuil haut (TH).
V-B. Branchements▲
Relier le DS1621 à une Arduino Uno ne présente pas de difficultés. Une plaquette de câblage rapide et quelques fils suffiront. On rappelle que les résistances pull-up ne sont pas indispensables ici puisqu'intégrées à la carte Arduino.
Les broches SDA et SDL du DS1621 sont connectées respectivement aux connecteurs A4 et A5 de l'Arduino Uno. On alimente le composant avec les broches GND et VDD reliées respectivement aux connecteurs GND et 5V de l'Arduino Uno.
On rappelle que l'adresse d'un composant I2C est codée avec sept bits (A0 à A6). Les bits de poids faible A0, A1 et A2 peuvent être configurés matériellement, par exemple en reliant les trois broches A0, A1 et A2 à la référence électrique GND comme sur le schéma du montage ci-dessus, on les met au niveau logique bas, A0 = A1 = A2 =0. Les bits A3 à A6 sont fixés par le fabricant du DS1621, ici A3=1, A4=0, A5=0 et A6=1.
Ce qui donne pour l'adresse du composant : 100 1000 = 48Hex
Les bits A0, A1 et A2 pouvant être configurés matériellement, le fabricant offre la possibilité d'adresser huit DS1621 sur un même bus.
V-C. Comment envoyer des données▲
« Écrire » sur la ligne pour envoyer des données de l'Arduino (maître) vers un composant I2C (esclave) requiert deux choses : l'adresse du composant qui doit être unique sur le bus, et le ou les octets de données à envoyer. Dans le cas de notre DS1621, l'adresse du capteur numérique est fixée à 48 en hexadécimal.
Par exemple, pour lancer une conversion de température, on procède en trois temps :
Wire.beginTransmission
(
0x48
); // adresse du DS1621
L'adresse est alors écrite sur la ligne de données SDA du bus, ce qui, en quelque sorte, va notifier le composant concerné que des données vont lui être adressées…
Wire.write
(
0xEE
); // 0xEE est la commande Start Convert T
L'octet de données (ici, la commande Start Convert [0xEE] qui permet de lancer la conversion de température) est ensuite envoyé au DS1621 qui l'attend à bras ouverts. Les autres composants sur le bus vont bien sûr ignorer cette donnée. Notez que vous pouvez n'effectuer qu'une seule opération à la fois sur le bus. Quand l'opération d'écriture sur la ligne est terminée, il faut « couper la transmission » afin de libérer la ligne pour l'opération de lecture/écriture suivante :
Wire.endTransmission
(
);
Pour être plus précis et si on veut s'assurer de la bonne transmission, la méthode Wire.endTransmission() peut aussi retourner un code qui indique le statut de la transmission (si égal à 0, la transmission s'est correctement déroulée).
Envoyer des données selon le protocole I2C - niveau physique
La transmission commence par une condition START (tandis que la ligne SCL est à l'état HIGH, la ligne SDA bascule à l'état LOW). S'ensuivent les 7 bits de l'adresse du composant (Slave Address), puis un bit R/W qui vaut ici 0 pour indiquer une opération d'écriture sur la ligne SDA. Le composant adressé doit envoyer un bit d'acquittement (ACK pour Acknowledgement, selon les spécifications le composant esclave doit abaisser la ligne SDA pour acquitter de la bonne réception). Chaque octet de donnée écrit sur la ligne par le maître doit être acquitté par l'esclave. La transmission s'arrête en finissant par une condition STOP (tandis que la ligne SCL est à l'état HIGH, la ligne SDA bascule à l'état HIGH).
La bibliothèque Wire, comme souvent avec les bibliothèques Arduino, vous offre une surcouche logicielle qui permet de vous affranchir des méandres bas niveau du protocole I2C, mais il est toujours bien d'avoir un aperçu de ce qui se dissimule sous le capot.
Par exemple, si on revient au lancement d'une conversion de température du DS1621, il faut envoyer la commande Start Convert [0XEE], soit le chronogramme suivant d'après le constructeur :
Address Byte = 0x48 (bits A0, A1 et A2 mis à zéro) et Command Byte=0xEE.
En effet, la trame correspondante relevée après essai à l'oscilloscope (avec un module de décodage des trames I2C) sur les lignes SDA et SCL donne :
On voit que le codage des bits utilisé est de type NRZ (Non Retour à Zéro), c'est-à -dire un niveau HIGH (5 V) pour coder un « 1 », et un niveau LOW (0 V) pour coder un « 0 ». Pour l'échantillonnage, le niveau HIGH ou LOW sur la ligne SDA doit être maintenu stable pendant le niveau HIGH de l'horloge de la ligne SCL.
V-D. Comment recevoir des données▲
Une fois la conversion de température terminée, la valeur de température est stockée dans un format bien précis dans un registre du DS1621. L'Arduino doit donc préparer le DS1621 à pointer vers ce registre avant de requérir les deux octets correspondants.
On commence par demander au composant I2C d'envoyer ses données en lui faisant pointer vers le registre souhaité. Pour cela, on envoie la commande Read Temperature [0xAA] :
Wire.beginTransmission
(
0x48
);
Wire.write
(
0xAA
); // 0xAA = commande 'Read Temperature'
Wire.endTransmission
(
); // condition STOP
À noter que le protocole I2C propose une condition particulière nommée RESTART pour signifier le début d'une nouvelle trame dès la fin de la trame précédente sans passer par une condition de STOP pour libérer le bus. Comme la commande Read Temperature doit être suivie d'une opération de lecture sur le bus, on écrira plutôt :
Wire.beginTransmission
(
0x48
);
Wire.write
(
0xAA
); // 0xAA = commande 'Read Temperature'
Wire.endTransmission
(
false
); // condition RESTART
À partir de là , le composant I2C retournera l'état de son registre sous forme d'octets de données. L'Arduino doit alors renseigner le nombre d'octets requis.
Wire.requestFrom
(
0x48
, 2
); // Deux octets sont requis
Cette instruction est immédiatement suivie par la lecture consécutive des deux octets retournés par le DS1621, par exemple :
if
(
2
<=
Wire.available
(
)) {
// si deux octets disponibles
TemperatureMSB =
Wire.read
(
); // lire l'octet de poids fort
TemperatureLSB =
Wire.read
(
); // lire l'octet de poids faible
}
Il n'est pas nécessaire d'utiliser Wire.endTransmission
(
) après la lecture sur la ligne. Une fois que les données demandées sont récupérées dans leur variable respective, il ne reste plus qu'à les traiter.
Recevoir des données selon le protocole I2C - niveau physique
La transmission commence par une condition START (tandis que la ligne SCL est à l'état HIGH, la ligne SDA bascule à l'état LOW). S'ensuivent 7 bits de l'adresse du composant (Slave Address), puis un bit R/W qui vaut ici 1 pour indiquer une opération de lecture sur la ligne SDA. Le composant adressé doit envoyer un bit d'acquittement (ACK pour Acknowledgement, selon les spécifications le composant esclave doit abaisser la ligne SDA pour acquitter de la bonne réception). Chaque octet de donnée retourné sur la ligne par l'esclave doit être acquitté par le maître. La transmission s'arrête en finissant par une condition STOP (tandis que la ligne SCL est à l'état HIGH, la ligne SDA bascule à l'état HIGH).
Par exemple, pour récupérer les deux octets de poids fort et de poids faible (MSBYTE et LSBYTE) contenant la valeur de température acquise par le DS1621, le chronogramme selon le constructeur est le suivant :
Address Byte = 0x48 (A0, A1 et A2 mis à zéro), Command Byte = 0xAA (Read Temperature).
Un exemple de trame correspondante relevée à l'oscilloscope donne :
Les deux octets écrits sur la ligne par le DS1621 et récupérés grâce à une opération de lecture par l'Arduino sont 0X13 et 0x80. Ce qui correspond à une température de 19,5 °C comme nous le verrons plus tard.
V-E. Le code de démonstration▲
Pour cette démonstration, le programme devra faire une acquisition de température, au coup par coup, disons toutes les cinq secondes. Le résultat sera affiché dans le Terminal Série.
La structure du croquis Arduino est la suivante :
# include <Wire.h> // La bibliothèque Wire gère l'I2C
#define A0_DS1621 0
#define A1_DS1621 0
#define A2_DS1621 0
#define ADDRESS_DS1621 (0x48 | A2_DS1621<<2 | A1_DS1621<<1 | A0_DS1621)
#define ONESHOT 1 // bit 1SHOT=1 si conversion au coup par coup
#define POL 0 // bit POL, non utilisé ici
#define NVB 0 // bit NVB, non utilisé ici
#define TLF 0 // bit TLF, non utilisé ici
#define THF 0 // bit THF, non utilisé ici
/* Configuration du registre */
#define REGISTER_CONFIG (THF<<6 | TLF<<5 | NVB<<4 | POL<<1 | ONESHOT)
#define DONE_MASK 0x80 // Masque pour bit DONE
/* Commandes du DS1621 */
#define READ_TEMPERATURE 0xAA
#define ACCESS_CONFIG 0xAC
#define START_CONVERT 0xEE
#define STOP_CONVERT 0x22
byte endConversion =
0
;
byte temperatureMSB =
0
;
byte temperatureLSB =
0
;
float
temperature;
void
setup
(
){
// Initialisation des ressources
Serial.begin
(
9600
); // Initialisation Terminal Série
Wire.begin
(
); // Initialisation I2C
/* Configuration du DS1621 */
}
// fin setup
void
loop
(
){
// boucle infinie
/* Lancement de la conversion */
/* Attendre la fin de la conversion */
/* Récupérer les deux octets de température */
/* Faire les calculs et afficher la température en degrés Celcius */
delay
(
5000
); // Attendre 5 s avant de recommencer
}
Si vous câblez différemment les broches A0, A1 et A2, il vous suffit de modifier les déclarations dans le code.
Commençons par configurer le DS1621 dans le setup :
/* Configuration du DS1621 */
Wire.beginTransmission
(
ADDRESS_DS1621);
Wire.write
(
STOP_CONVERT);
Wire.endTransmission
(
);
Wire.beginTransmission
(
ADDRESS_DS1621);
Wire.write
(
ACCESS_CONFIG); // Accès au registre de configuration
Wire.write
(
REGISTER_CONFIG); // écriture dans le registre de configuration
Wire.endTransmission
(
);
On stoppe toute conversion en premier lieu. La configuration est non volatile et il se peut qu'une configuration précédente avec des conversions de température en continu soit encore mémorisée.
Pour configurer le DS1621 en mode de conversion « au coup par coup », il faut écrire dans son registre de configuration au format suivant :
Le mode de conversion « au coup par coup » est activé en mettant le bit 1SHOT à 1. Pour notre démonstration, les autres bits peuvent être laissés à zéro.
Dans la boucle loop
(
), on commence par lancer une conversion de température :
/* Lancement de la conversion */
Wire.beginTransmission
(
ADDRESS_DS1621);
Wire.write
(
START_CONVERT);
Wire.endTransmission
(
);
La conversion de température n'est pas instantanée. Le DS1621 prévient qu'elle est terminée lorsque le bit DONE de son registre de configuration est égal à 1 :
/* Attendre la fin de la conversion */
do
{
Wire.beginTransmission
(
ADDRESS_DS1621);
Wire.write
(
ACCESS_CONFIG);
Wire.endTransmission
(
false
); // Condition RESTART
Wire.requestFrom
(
ADDRESS_DS1621, 1
); // Un octet est requis
if
(
1
<=
Wire.available
(
)) endConversion =
Wire.read
(
) &
DONE_MASK;
}
while
(!
endConversion);
Après cela, on peut récupérer la valeur de la température :
/* Récupérer les deux octets de température */
Wire.beginTransmission
(
ADDRESS_DS1621);
Wire.write
(
READ_TEMPERATURE);
Wire.endTransmission
(
false
); // Condition RESTART
Wire.requestFrom
(
ADDRESS_DS1621, 2
); // Deux octets sont requis
if
(
2
<=
Wire.available
(
)) {
temperatureMSB =
Wire.read
(
); // Octet de poids fort
temperatureLSB =
Wire.read
(
); // Octet de poids faible
}
Les deux octets de poids fort et de poids faible récupérés représentent la température dans un format bien documenté par le constructeur :
Le premier octet de poids fort est la partie entière de la température en degrés Celcius avec la méthode du complément à deux.
Dans cette représentation, le bit de poids fort est le bit de signe et vaut 1 pour les entiers négatifs. Pour retrouver la valeur absolue de départ, il faut alors faire l'opération : 256-x.
Par exemple, si l'octet de poids fort vaut 1110 0111, soit 231 en décimal : le bit de poids fort est à 1, c'est donc une valeur négative que l'on veut représenter. On réalise alors l'opération 256-231=25, et on en déduit la température qui est de -25 °C.
Le deuxième octet de poids faible vaut soit 0000 0000, soit 1000 0000. Dans ce deuxième cas, il y a un demi-degré à prendre en compte dans la partie fractionnaire de la température.
On peut maintenant faire les calculs :
/* Faire les calculs et afficher la température en degrés Celcius */
temperature =
(
float
) temperatureMSB;
if
(
temperatureLSB &
0x80
) temperature +=
0
.5
; // 1/2 °C à prendre en compte
if
(
temperatureMSB &
0x80
) temperature -=
256
; // Température négative
Serial.print
(
"
Temperature =
"
);
Serial.print
(
temperature);
Serial.println
(
"
degres Celcius
"
);
Le code complet :
#include <Wire.h>
#define A0_DS1621 0
#define A1_DS1621 0
#define A2_DS1621 0
#define ADDRESS_DS1621 (0x48 | A2_DS1621<<2 | A1_DS1621<<1 | A0_DS1621)
#define ONESHOT 1 // bit 1SHOT=1 si conversion au coup par coup
#define POL 0 // bit POL, non utilisé ici
#define NVB 0 // bit NVB, non utilisé ici
#define TLF 0 // bit TLF, non utilisé ici
#define THF 0 // bit THF, non utilisé ici
/* Configuration du registre */
#define REGISTER_CONFIG (THF<<6 | TLF<<5 | NVB<<4 | POL<<1 | ONESHOT)
#define DONE_MASK 0x80 // Masque pour bit DONE
/* Commandes du DS1621 */
#define READ_TEMPERATURE 0xAA
#define ACCESS_CONFIG 0xAC
#define START_CONVERT 0xEE
#define STOP_CONVERT 0x22
byte endConversion =
0
;
byte temperatureMSB =
0
;
byte temperatureLSB =
0
;
float
temperature;
void
setup
(
) {
Serial.begin
(
9600
); // Initialisation Terminal Série
Wire.begin
(
); // Initialisation I2C
/* Configuration du DS1621 */
Wire.beginTransmission
(
ADDRESS_DS1621);
Wire.write
(
STOP_CONVERT);
Wire.endTransmission
(
);
Wire.beginTransmission
(
ADDRESS_DS1621);
Wire.write
(
ACCESS_CONFIG); // Accès au registre de configuration
Wire.write
(
REGISTER_CONFIG); // écriture dans le registre de configuration
Wire.endTransmission
(
);
}
void
loop
(
) {
/* Lancement de la conversion */
Wire.beginTransmission
(
ADDRESS_DS1621);
Wire.write
(
START_CONVERT);
Wire.endTransmission
(
);
/* Attendre la fin de la conversion */
do
{
Wire.beginTransmission
(
ADDRESS_DS1621);
Wire.write
(
ACCESS_CONFIG);
Wire.endTransmission
(
false
); // Condition RESTART
Wire.requestFrom
(
ADDRESS_DS1621, 1
); // Un octet est requis
if
(
1
<=
Wire.available
(
)) endConversion =
Wire.read
(
) &
DONE_MASK;
}
while
(!
endConversion);
/* Récupérer les deux octets de température */
Wire.beginTransmission
(
ADDRESS_DS1621);
Wire.write
(
READ_TEMPERATURE);
Wire.endTransmission
(
false
); // Condition RESTART
Wire.requestFrom
(
ADDRESS_DS1621, 2
); // Deux octets sont requis
if
(
2
<=
Wire.available
(
)) {
temperatureMSB =
Wire.read
(
); // Octet de poids fort
temperatureLSB =
Wire.read
(
); // Octet de poids faible
}
/* Faire les calculs et afficher la température en degrés Celcius */
temperature =
(
float
) temperatureMSB;
if
(
temperatureLSB &
0x80
) temperature +=
0
.5
; // 1/2 °C à prendre en compte
if
(
temperatureMSB &
0x80
) temperature -=
256
; // Température négative
Serial.print
(
"
Temperature =
"
);
Serial.print
(
temperature);
Serial.println
(
"
degres Celcius
"
);
delay
(
5000
); // Attendre 5 s avant de recommencer
}
Et le résultat dans le Terminal Série :
VI. Conclusion▲
La configuration d'un composant I2C et l'accès à ses différents registres nécessitent impérativement une lecture attentive et approfondie de la documentation fournie (la fameuse datasheet), souvent en anglais. Mais nous espérons vous avoir fait découvrir les principes de fonctionnement du bus I2C et que vous pourrez désormais interfacer plus facilement les composants utilisant ce bus dans vos projets Arduino.
Do It Yourself...
VII. Sitographie▲
-
Références à la norme I2C sur le site de NXP Semiconductors :
- http://www.i2c-bus.org/
- Bibliothèque Arduino de gestion du bus I2C : Wire Library
- Datasheet DS1621 chez Maxim Integrated : DS1621 Digital Thermometer and Thermostat
-
Quelques fabricants de semi-conducteurs avec un catalogue de composants I2C bien fourni :
VIII. Remerciements▲
Je remercie sevyc64, deusyss, ram-0000 et Auteur pour leur aide dans l'amélioration de cet article.
Je remercie également Claude Leloup pour sa relecture orthographique.