Comment configurer un réseau adressable de type I2C ?

Objectifs

  • Comprendre le protocole I2C
  • Visualiser les différents signaux nécessaires à la transmission d’une information

Pré-requis

Matériel

  • Carte STMicroelectronics Nucléo L476RG
  • Kit Microchip PKSERIAL-I2C1

Protocole I2C

Transmission par bus

Le protocole de communication I2C (Inter-Integrated Circuit, conçu par Philips) permet d’établir une liaison de transmission de données série synchrone entre un maître et plusieurs esclaves. La liaison se fait à l’aide de 2 fils (voir schéma suivant – vu du maître) :

  • SCL : signal d’horloge, imposé par le maître (Serial Clock Line)
  • SDA : données pouvant aller du maître à l’esclave ou inversement (Serial Data Line)

Ce protocole est souvent utilisé dans les systèmes embarqués pour faire communiquer de manière numérique des capteurs ou des organes de ce système entre eux sous la forme d’un seul et même bus. Ce réseau de terrain a pour avantages d’être rapide (car synchrone) et de n’utiliser que 2 fils. Il permet de transmettre des informations binaires sous forme de paquet (souvent 1 octet à chaque fois) en Half-Duplex.

Contrairement au protocole SPI, le maître n’a pas besoin d’un fil supplémentaire par esclave pour lui adresser un message.

Transmission par adressage direct

Mais alors, comment se passe une communication en I2C s’il n’y a pas de moyen direct de sélectionner l’esclave auquel discuter ? De même, comment fait un esclave s’il a besoin de communiquer au maître vu qu’il n’y a qu’un seul fil de données ?

Il s’agit en fait d’un moyen de transmission par adressage. Le destinataire du message fait partie du message transmis depuis le maître à l’ensemble du réseau. Seul le destinataire concerné traite l’information. On parle alors de l’adresse du noeud. Chaque noeud du réseau a une adresse différente pour distinguer les messages transmis. Cette adresse peut être sur 7 ou 10 bits (ce sont deux normes différentes – la plupart du temps sur 7 bits).

Il existe alors 3 types de message :

  • des commandes, où le maître envoie des données à un esclave, dans un registre particulier (ou un ensemble de registres)
  • des requêtes, où le maître demande des données à un esclave, depuis un registre particulier (ou un ensemble de registres)
  • des réponses, où l’esclave envoie au maître les données demandées, depuis un registre particulier (ou un ensemble de registres)

La transmission est dirigée par le maître dans tous les cas.

Voici à quoi ressemblent les trames sur un bus I2C :

M = Maitre, E = Esclave, ADR = Adresse (sur 7 bits), W = Write = ‘0’ (1 bit), R = Read = ‘1’ (1 bit), CMD/REG = Commande ou Registre (adresse sur 8 bits du premier registre sur l’esclave à modifier), A = Acquittement (1 bit), S = Start bit (1 bit), P = Stop bit (1 bit)

On peut noter sur la précédente figure que chaque “zone” de la trame est identifiée et normalisée, en nombre de bits.

Acquittement

Il est également intéressant de voir que toutes ces “zones” d’un message sont acquittées par l’esclave (ou le maître, selon la direction de l’échange). Cela permet d’être sûr qu’au moins un esclave à compris que le message lui était destiné. Sinon, au bout d’un certain temps (paramétrable sur la plupart des communications I2C), le bus se met en erreur et le maître sait que le message n’a pas trouvé de destinataire.

Start / Stop bits

Les communications I2C sont également délimitées par des bits spécifiques, normalisés dans le protocole. Il s’agit des bits START et STOP (nommés S et P dans la trame précédente). Ils servent à annoncer à l’ensemble des noeuds du réseau qu’un message va commencer et qu’il va falloir se mettre à l’écoute du bus (START bit). Dans l’autre cas, il sert à libérer le bus pour un nouveau message (STOP bit).

Esclaves I2C

La plupart des noeuds I2C sont des capteurs intégrant plusieurs informations : accéléromètres 3 axes, gyroscope 3 axes… Ils sont souvent paramétrables et intègrent souvent une partie “microcontroleur” pour gèrer tous ces paramètres et les données associées.

Leur structure interne est donc proche de celle d’un microcontroleur “classique” avec des registres particuliers où sont gérés à la fois les divers paramètres modifiables du capteur et les données.

TC1321 – Convertisseur Numérique-Analogique I2C

TC1321 : DAC (Digital to Analog Converter) I2C – sur 10 bits

Dans cet exemple (TC1321), ce composant a pour adresse : 0b1001000. Il possède 2 types de registres : un de configuration et un de données, respectivement aux adresses 0x01 et 0x00 (en hexadécimal – présence du h) – TABLE 4-2.

On peut également voir, dans les tableaux TABLE 4-3 et TABLE 4-4 de la documentation, l’utilité de chacun des bits de ces registres.

Par exemple, pour passer le DAC en mode normal, il faudra transmettre une commande depuis le maître :

  • l’adresse du composant – ici 0b1001000, suivi du bit W = ‘0’ (pour envoyer un ordre)
  • l’adresse du registre que l’on souhaite modifier – ici 0x01 (RWCR)
  • la valeur que l’on souhaite imposer – ici 0x01, pour forcer le mode Normal du capteur.

Si on veut vérifier que la donnée a bien été transmise, on pourra alors envoyer une requête depuis le maître qui contiendra :

  • l’adresse du composant – ici 0b1001000, suivi du bit W = ‘0’ (pour envoyer un ordre)
  • l’adresse du registre que l’on souhaite lire – ici 0x01 (RWCR)

Puis, le maître cadencera la réponse de l’esclave en transmettant :

  • l’adresse du composant – ici 0b1001000, suivi du bit R = ‘1’ (pour recevoir des données)

L’octet suivant sera alors transmis par l’esclave sur le bus de donnée. Le maître pourra alors la récupérer.

Vitesse de transmission

Attention ! Les vitesses de transfert des liaisons I2C sont normalisées :

  • Standard Mode : 100 kHz / 100 kbps
  • Fast Mode : 400 kHz / 400 kbps
  • High-Speed Mode : 3200 kHz / 3200 kpbs

La transmission de l’ensemble des messages (commande, requête ou réponse) est cadencée par le maître à l’aide d’une horloge.

I2C et MBED

Cablage d’un noeud I2C avec la carte Nucléo

La première étape est de câbler physiquement le maître et l’esclave. Il faut repèrer sur la carte Nucléo les broches nommées I2C. La carte Nucléo est capable de gérer 2 réseaux I2C différents, en tant que maître – via les bus I2C1 et I2C2. Elle peut également devenir l’esclave sur 2 réseaux différents (via les mêmes broches que précédemment).

Configuration du mode I2C de la carte Nucleo L476RG

L’ensemble des classes et des fonctions permettant de réaliser des opérations avec les modules du microcontroleur se trouvent dans la bibliothèque “mbed.h (ou “mbed-os.h”). Il est donc indispensable d’avoir au préalable inclut cette bibliothèque :

#include "mbed.h"

Déclaration des entrées/sorties

La première étape est la déclaration des broches utilisées pour la liaison I2C. Il faut la placer après la déclaration des ressources externes (bibliothèques) et avant toute définition de fonction (voir tutoriel Tester mon premier programme sur Nucléo 0 – Premier programme – /* Déclaration des entrées/sorties */).

I2C my_i2c(I2C_SDA, I2C_SCL);

On déclare, à l’aide du constructeur de la classe I2C, le nom de la liaison dans le reste du programme (ici la variable my_i2c) et les broches SDA et SCL à utiliser sur la carte (il s’agit ici des broches D15 et D14 des connecteurs Arduino).

Paramétrage de la liaison I2C

Avec le compilateur MBED, il est possible de changer la fréquence de transmission. Pour cela, il faut utiliser la fonction suivante de la classe I2C, qui prend en paramètre la fréquence en Hz :

my_i2c.frequency(100000);     

Attention : Le composant I2C esclave doit être capable de recevoir les données à la fréquence d’horloge imposée, c’est souvent lui qui est limitant ! Il faut aussi bien penser à ajouter les résistances de tirage à \(V_{DD}\) précaunisées dans la documentation technique du composant.

Les différentes trames I2C

Nous allons voir ici les différentes fonctions et leurs paramètres permettant d’envoyer des trames de données sur une liaison I2C.

Il faut savoir que la carte Nucléo permet de gérer une grande partie du protocole I2C seule, à savoir les bits de start et de stop ainsi que les bits d’acquittement.

Trame de commande

La fonction write de la classe I2C permet de transmettre une trame de commande (ou données) depuis le maître vers l’esclave.

Cette fonction attend 3 paramètres :

  • l’adresse du composant – sur 7 bits (Attention, il faut la décaler d’un bit à gauche – compatibilité avec MBED)
  • l’adresse d’un tableau de données, du bon nombre de cases – ATTENTION : la première donnée correspond souvent à l’adresse du premier registre à modifier
  • le nombre de données à transmettre
my_i2c.write(adresse + R/W, tableau_donnees, nb_donnees);

Par exemple, si on souhaite modifier les registres des adresses 0x10, 0x11 et 0x12 avec les données 0x20, 0x34 et 0x56, du composant portant l’adresse 0b0010010, il faudra écrire :

char    data[4];
 
data[0]   = 0x10;
data[1]   = 0x20;
data[2]   = 0x34;
data[3]   = 0x56;
 
my_i2c.write(0b0010010 << 1, data, 4);

ATTENTION : si les adresses des registres ne sont pas contigues, il faudra alors découper l’envoi des données en plusieurs trames différentes.

Trame de requête

Une trame de requête est une trame de commande sans donnée. Elle contient uniquement l’adresse du composant et l’adresse du registre dont on veut ensuite pouvoir récupérer la donnée.

On peut alors réutiliser la commande write, de la classe I2C, de la façon suivante :

char ad = 0b000 0000;
char regi = 0x01;
my_i2c.write(ad << 1, ®i, 1);

Par exemple, si on veut s’adresser au composant 0x78, pour pouvoir récupérer le contenu des 5 registres consécutifs à partir de l’adresse 0x05, on pourra utiliser la suite d’instructions suivante :

    char    cmd;
    cmd    = 0x05;
    i2c.write( 0x78 << 1, &cmd, 1 );

Trame de réponse

Une trame de réponse est cadencée par le maître, mais la (ou les) donnée(s) est(sont) transmise(s) par l’esclave.

La fonction read permet de faire cette étape de récupération. Elle attend 3 paramètres :

  • l’adresse du composant – sur 7 bits
  • l’adresse d’un tableau de données du bon nombre de cases
  • le nombre de données à récupérer
char ad = 0b000 0000;
char data[10];
my_i2c.read(ad << 1, data, 10);

Après l’appel à cette fonction, le tableau data contiendra 10 octets, correspondant aux 10 registres à partir de l’adresse du premier registre passé en paramètre dans la trame de requête.

Par exemple, si on veut s’adresser au composant 0x78, pour pouvoir récupérer le contenu des 5 registres consécutifs à partir de l’adresse 0x05, on pourra utiliser la suite d’instructions suivante :

char    v[5];
char    cmd;
 
cmd    = 0x05;
 
i2c.write( 0x78 << 1, &cmd, 1 );  // requete
i2c.read( 0x78 << 1, v, 5 );      // reponse

Condition de RESTART

La plupart des composants I2C peuvent retourner une donnée suite à une trame de requête “classique” et une trame de réponse “classique”
(voir paragraphes précédents). Certains ne le peuvent pas.

Une commande supplémentaire de redémarrage (signifiant que la requête et la réponse sont nécessairement successives) est alors possible de la façon suivante :

i2c.write( 0x78 << 1, &cmd, 1, true );  //  no "STOP condition" generated 
i2c.read( 0x78 << 1, v, 5 );           //  "START condition" for this transfer becomes a "repeated-START" condition

Le quatrième paramètre de la fonction write permet de spécifier si on veut remplacer le bit de STOP par un bit de RESTART. Ainsi, l’enchainement précédent aura le même effet que l’envoi d’une requête puis l’attente d’une réponse de la part du capteur.

Acquittement

Il est également possible de vérifier que les trames sont bien transmises en vérifiant qu’elles sont acquittées par l’esclave.
Pour cela, les fonctions write et read de la classe I2C renvoie un entier : valant ‘0’ si l’ensemble de la trame est acquittée, une valeur positive sinon.

char    v[5];
char    cmd;
 
cmd    = 0x05;
 
int k1 = i2c.write( 0x78 << 1, &cmd, 1 );  // requete
int k2 = i2c.read( 0x78 << 1, v, 5 );      // reponse

printf("ACQUITTEMENT WRITE = %d \r\n", k1);
printf("ACQUITTEMENT READ = %d \r\n", k2);

L’exemple ci-dessus affichera dans une console série l’information d’acquittement des deux trames indépendamment.

Exemple de transmission

Voici un exemple permettant de tester une liaison I2C. Etant donné que chaque trame doit être acquittée par un esclave, il est nécessaire ici de câbler au moins un composant de type I2C.

On pourra par exemple utiliser le kit de Microchip PKSERIAL-I2C1 qui intègre 5 composants différents (documentation technique) : 24LC02B (mémoire série), MCP9801 (Capteur de Temperature), MCP3221 (ADC – Analog to Digital Converter – 12 bits), TC1321 (DAC 10 bits), MCP23008 (Extension d’entrées-sorties).

Dans l’exemple suivant, on va mettre à jour la donnée du DAC TC1321 à intervalle régulier.

#include "mbed.h"

#define  TC1321_AD    0b1001000
#define  TC1321_DATA    0x00
#define  TC1321_CONFIG  0x01

I2C my_i2c(I2C_SDA, I2C_SCL);

char data[3];

int main(){
    int i;
    my_i2c.frequency(100000);  
    /* Initialisation du DAC en mode normal */
    data[0] = TC1321_CONFIG;
    data[1] = 0;
    my_i2c.write(TC1321_AD << 1, data, 2);

    /* Vérification du registre de configuration */
    data[0] = TC1321_CONFIG;
    data[1] = 0;
    my_i2c.write(TC1321_AD << 1, data, 1);
    my_i2c.read(TC1321_AD << 1, data, 1);
    if((data[0] & 0x01) == 0){ printf("CONFIG OK"); }

    while(1){
        if(i == 1023) i = 0;
        else     i++;
        /* Initialisation du DAC en mode normal */
        data[0] = TC1321_DATA;
        data[1] = i >> 2;
        data[2] = i << 6;
        my_i2c.write(TC1321_AD << 1, data, 3);        
        wait(0.1);
    }
}    

Procédure de mise en oeuvre

  • Alimenter le composant I2C à contrôler (3.3V ou 5V à vérifier – si 5V attention à la carte Nucléo !!)
  • Connecter les broches SDA et SCL de la carte Nucléo au composant à contrôler
  • Vérifier la présence des résistances de tirage à 3.3V (ordre de grandeur : 560 Ohms)
  • Tester l’envoi d’une donnée de commande (il existe en général sur ces composants un registre de numéro d’identification) :
    • Spécifier la bonne adresse du composant
    • Spécifier la bonne adresse du registre interne
    • Transmettre la trame via la fonction write
    • Vérifier qu’un acquittement a été retourné à la carte Nucléo
  • Tester l’envoi d’une requête et d’une récupération de données sur un registre particulier
    • Transmettre la trame de requête via la fonction write (attention, il se peut que le composant n’est pas besoin de passage par un stop, dans ce cas, il faut ajouter le dernier paramètre repeated=true)
    • Vérifier qu’un acquittement a été retourné à la carte Nucléo
    • Vérifier que la donné reçue est correcte (le fabricant du composant fourni souvent les valeurs par défaut des registres)
  • Ecrire une bibliothèque spécifique pour votre composant

Tutoriel lié

Applications

| MInE | Prototyper | Prototyper avec Nucleo et MBED |

Nucléo – Configurer un réseau adressable de type I2C / 3