TP 7 — Le bus I²C : température, accéléromètre et gyroscope

Objectifs

  • comprendre le bus I²C : deux fils, des adresses, des registres ;

  • trouver les composants présents sur un bus, et vérifier leur identité ;

  • lire le capteur de température de la carte et convertir sa mesure ;

  • configurer et lire une centrale inertielle (accéléromètre et gyroscope).

Durée indicative : deux séances de 3 h. Prérequis : TP 1 — Allumer une LED, lire un bouton.

Ce qu’il faut savoir

Le bus I²C

L’I²C relie un maître (le microcontrôleur) à plusieurs esclaves (les capteurs) par deux fils seulement : SCL, l’horloge, que le maître impose, et SDA, les données, dans les deux sens. Les deux lignes sont tirées au +3,3 V par des résistances, et chaque composant ne sait que les mettre à 0 : c’est ce qui permet à plusieurs composants de partager les mêmes fils.

Figure made with TikZ

Le bus I²C2 : deux fils partagés, tirés au +3,3 V.

Chaque esclave a une adresse sur 7 bits. Une transaction commence par l’adresse : seul le composant qui la reconnaît répond, en acquittant (ACK). La plupart des capteurs se lisent comme une petite mémoire : on écrit le numéro d’un registre, puis on lit un ou plusieurs octets à partir de lui. La HAL fournit les deux opérations :

HAL_I2C_Mem_Read (&hi2c2, adresse << 1, registre, I2C_MEMADD_SIZE_8BIT,
                  tampon, nombre_d_octets, delai_ms);
HAL_I2C_Mem_Write(&hi2c2, adresse << 1, registre, I2C_MEMADD_SIZE_8BIT,
                  tampon, nombre_d_octets, delai_ms);

Figure made with TikZ

Une lecture de registre (HAL_I2C_Mem_Read) : écrire le numéro du registre, puis lire.

Important

La HAL attend l’adresse décalée d’un bit à gauche : l’adresse 7 bits 0x48 se passe 0x48 << 1, soit 0x90. Oublier le décalage est l’erreur la plus fréquente avec l’I²C.

Les capteurs de la carte

Composant

Bus et broches

Adresse 7 bits

Rôle

TMP102 (U3)

I2C2 : SCL PB10, SDA PB11

0x48

capteur de température, soudé sur la carte

LSM6DSOX

I2C3 : SCL PA8, SDA PC9

0x6A ou 0x6B

centrale inertielle (accéléromètre et gyroscope), sur un petit module relié au connecteur J9

Sur un robot complet, d’autres composants partagent le bus I2C2, comme le capteur de distance avant. Seul le TMP102 est toujours présent.

Étape 1 — Configurer l’I²C2 et chercher les composants

  1. Créez un projet tp07_i2c comme au TP 1 (STM32H750VBTx, SYS → Debug : Serial Wire, horloge par défaut).

  2. Dans Connectivity → I2C2, choisissez I2C. Vérifiez que CubeMX a placé SCL et SDA sur PB10 et PB11 (sinon, cliquez sur ces broches et choisissez I2C2_SCL et I2C2_SDA). Gardez la vitesse Standard Mode (100 kHz).

  3. Générez le code.

  4. Écrivez un balayage du bus : pour chaque adresse de 1 à 127, demandez si un composant répond, et gardez la liste de ceux qui répondent :

    if (HAL_I2C_IsDeviceReady(&hi2c2, adresse << 1, 2, 5) == HAL_OK) {
        /* un composant a acquitté à cette adresse */
    }
    

    Affichez le nombre de composants trouvés sur les LED de façade, ou lisez la liste dans le débogueur.

Questions

  1. Quelles adresses répondent ? Retrouvez-les dans le tableau ci-dessus.

  2. Que se passe-t-il si l’on oublie le décalage << 1 ?

Solution de l’étape 1
#define I2C_TIMEOUT_MS 5u

volatile uint8_t  g_found[16];       /* adresses qui répondent */
volatile uint32_t g_found_count;

static void i2c2_scan(void)
{
    g_found_count = 0u;
    for (uint16_t a = 1u; a < 128u; a++)
    {
        if (HAL_I2C_IsDeviceReady(&hi2c2, (uint16_t)(a << 1), 2u, I2C_TIMEOUT_MS) == HAL_OK)
        {
            if (g_found_count < 16u) { g_found[g_found_count] = (uint8_t)a; }
            g_found_count++;
        }
    }
}

appelée une fois dans USER CODE BEGIN 2. Le tableau se lit dans Live Expressions du débogueur.

Ce qui circule sur le bus. Pour chaque adresse, le contrôleur I²C émet une condition de départ (SDA descend pendant que SCL est haut), puis l’octet d’adresse : les 7 bits de l’adresse et un 8e bit, 0 pour une écriture. Au 9e coup d’horloge, il relâche SDA : si un composant reconnaît son adresse, il tire SDA à 0, c’est l”acquittement (ACK). Sinon, SDA reste à 1, tirée par les résistances de rappel : c’est un NACK. Le contrôleur termine par une condition d”arrêt. HAL_I2C_IsDeviceReady fait exactement cela (deux fois, d’après son troisième argument), et renvoie HAL_OK en cas d’acquittement.

Le délai de 5 ms évite de rester bloqué si le bus est en défaut (une ligne collée à 0 par exemple). Le balayage complet prend 127 × 2 tentatives × environ 0,1 ms à 100 kHz : quelques dizaines de millisecondes.

Réponses aux questions 1 et 2

1. Les adresses. Sur une carte seule : 0x48, le TMP102 (sa broche d’adresse ADD0 est reliée à la masse ; reliée à V+, il répondrait en 0x49). Sur un robot complet, on voit aussi 0x29, le capteur de distance avant VL53L5CX, branché sur le même bus I2C2. La centrale inertielle, elle, est sur un autre bus (I2C3) : invisible ici.

2. Sans le décalage. Sur le bus, l’adresse occupe les 7 bits de poids fort de l’octet, le bit de poids faible indiquant le sens (lecture ou écriture). La HAL attend l’octet tel qu’il part sur le fil, donc l’adresse déjà décalée : 0x48 << 1 = 0x90. Sans le décalage, on demande 0x48 lui-même, que le contrôleur envoie comme l’adresse 0x24 : personne ne répond… ou un autre composant, par hasard, et on lit n’importe quoi. C’est l’erreur la plus fréquente avec l’I²C sur STM32 : les fiches techniques donnent l’adresse sur 7 bits, la HAL la veut sur 8.

Étape 2 — Vérifier qu’on parle bien au TMP102

Qu’un composant acquitte à l’adresse 0x48 ne prouve pas que ce soit un TMP102. Pour le vérifier, lisez un registre dont la valeur est connue : le registre de configuration (numéro 0x01), qui vaut 0x60A0 au démarrage, d’après la fiche technique du TMP102.

  1. Lisez 2 octets à partir du registre 0x01, et reconstituez la valeur sur 16 bits : le premier octet reçu est l’octet de poids fort.

  2. Comparez-la à 0x60A0.

Solution de l’étape 2
#define TMP102_ADDR    (0x48u << 1)
#define TMP102_TEMP    0x00u
#define TMP102_CONFIG  0x01u

/* Lit un registre 16 bits du TMP102, octet de poids fort en premier. */
static int tmp102_read16(uint8_t reg, uint16_t *out)
{
    uint8_t b[2];

    if (HAL_I2C_Mem_Read(&hi2c2, TMP102_ADDR, reg, I2C_MEMADD_SIZE_8BIT,
                         b, 2u, I2C_TIMEOUT_MS) != HAL_OK)
    {
        return 0;
    }
    *out = (uint16_t)(((uint16_t)b[0] << 8) | b[1]);
    return 1;
}

puis, dans USER CODE BEGIN 2 :

uint16_t c;
if (tmp102_read16(TMP102_CONFIG, &c)) { g_tmp_config = c; }   /* attendu : 0x60A0 */

HAL_I2C_Mem_Read enchaîne deux transactions, comme le demande le TMP102 : une écriture du numéro de registre (le « pointeur » du composant), puis, après un nouveau départ (repeated start), une lecture de deux octets. Le TMP102 envoie l’octet de poids fort en premier (big-endian) : b[0] << 8 | b[1] reconstitue la valeur.

Pourquoi 0x60A0 prouve l’identité. Ce motif précis (12 bits de résolution, mode de comparaison, 4 Hz de conversion) est la valeur documentée au démarrage. Un autre composant à la même adresse n’aurait aucune raison de la donner. On parle de vérification d’identité : la plupart des capteurs ont un registre fait pour cela (WHO_AM_I pour la centrale, à l’étape 4).

Étape 3 — Lire la température

Le registre de température (numéro 0x00) contient une mesure sur 12 bits, cadrée à gauche dans deux octets, en complément à deux (les températures négatives sont des nombres négatifs). Un pas vaut 0,0625 °C.

  1. Lisez 2 octets à partir du registre 0x00.

  2. Reconstituez la mesure : assemblez les deux octets, décalez de 4 bits à droite, puis étendez le signe si le bit 11 est à 1.

  3. Convertissez en millièmes de degré, avec des entiers : un pas vaut 62,5 m°C.

  4. Lisez la température toutes les secondes. Posez le doigt sur le TMP102 (repère U3, près du microcontrôleur) et regardez-la monter.

Questions

  1. Quelle température lisez-vous ? Est-ce la température de la pièce ? Pourquoi ?

  2. Le doigt fait-il monter la mesure tout de suite ? De combien, et en combien de temps ?

  3. Pourquoi faut-il étendre le signe ? Que lirait-on à −10 °C sans le faire ?

Solution de l’étape 3
/* 12 bits cadrés à gauche, complément à deux, 62,5 m°C par pas. */
static int tmp102_temp_mC(int32_t *out)
{
    uint16_t raw16;
    int32_t  raw;

    if (!tmp102_read16(TMP102_TEMP, &raw16)) { return 0; }
    raw = (int32_t)(raw16 >> 4);              /* les 12 bits utiles */
    if (raw & 0x800) { raw -= 4096; }         /* extension de signe */
    *out = (raw * 625) / 10;                  /* × 62,5 */
    return 1;
}

et dans la boucle :

int32_t t;

if (tmp102_temp_mC(&t)) { g_temp_mC = t; }
HAL_Delay(1000);

Les trois opérations, sur un exemple. À 25 °C, le TMP102 renvoie 0x19 0x00 :

assemblage   : 0x1900 = 0001 1001 0000 0000
décalage >>4 : 0x190  = 400                 (les 4 bits de droite sont toujours 0)
bit 11       : 0 → positif, rien à faire
conversion   : 400 × 62,5 = 25 000 m°C = 25,000 °C

Le × 625 / 10. Multiplier par 62,5 sans virgule : on multiplie par 625, puis on divise par 10. Le résultat est exact, car 400 × 625 = 250 000 est divisible par 10 ; dans le cas général, l’erreur d’arrondi est de 0,1 m°C, bien en dessous de la résolution du capteur.

Réponses aux questions 3 à 5

3. La température lue. Vers 28 °C au repos sur la carte de référence (28,6 °C à la mise en service), soit plusieurs degrés au-dessus de la pièce. Le TMP102 mesure sa propre température, c’est-à-dire celle du circuit imprimé sur lequel il est soudé : ses pattes sont reliées thermiquement au cuivre de la carte, que le microcontrôleur, les régulateurs et les LED réchauffent. C’est une température de carte, très utile pour surveiller l’électronique (et piloter un ventilateur, TP 4), pas une mesure de l’air ambiant.

4. Le doigt. La mesure monte de quelques dixièmes de degré en quelques secondes (+0,5 °C relevé), pas tout de suite : la chaleur doit traverser le boîtier, et le doigt chauffe aussi le cuivre autour, qui en absorbe une partie. C’est l”inertie thermique : un capteur de température mesure toujours avec retard. La mesure redescend ensuite lentement quand on retire le doigt.

5. L’extension de signe. La mesure est un nombre signé sur 12 bits, rangé dans un int32_t de 32 bits. Pour un nombre négatif, le bit 11 (le signe sur 12 bits) vaut 1, mais les bits 12 à 31 du int32_t valent 0 : le processeur voit un grand nombre positif. À −10 °C, la mesure vaut −160, codée 0xF60 sur 12 bits : sans extension, on lit 3 936 pas, soit +246 °C. Retrancher 4 096 (212) quand le bit 11 est à 1 recopie le signe sur les bits de gauche : 3 936 − 4 096 = −160.

Étape 4 — La centrale inertielle

Cette étape demande le module de centrale inertielle branché sur J9.

  1. Activez I2C3 dans CubeMX, sur PA8 (SCL) et PC9 (SDA), en Standard Mode. Régénérez le code.

  2. L’adresse. Selon le câblage de sa broche SA0, le LSM6DSOX répond en 0x6A ou en 0x6B. Essayez les deux avec HAL_I2C_IsDeviceReady.

  3. L’identité. Lisez le registre WHO_AM_I (numéro 0x0F) : il doit valoir 0x6C.

  4. La configuration. Au démarrage, les capteurs sont éteints. Écrivez :

    Registre

    Numéro

    Valeur

    Effet

    CTRL3_C

    0x12

    0x44

    mise à jour des données par blocs (BDU), adresse qui s’incrémente à chaque octet lu

    CTRL1_XL

    0x10

    0x48

    accéléromètre à 104 Hz, pleine échelle ±4 g

    CTRL2_G

    0x11

    0x40

    gyroscope à 104 Hz, pleine échelle ±250 °/s

  5. La lecture. Lisez 6 octets à partir de OUTX_L_A (0x28) pour l’accéléromètre, et 6 octets à partir de OUTX_L_G (0x22) pour le gyroscope : trois axes X, Y, Z de 16 bits chacun, octet de poids faible en premier, en complément à deux.

  6. Les unités. D’après la fiche technique : 0,122 mg par pas pour l’accéléromètre à ±4 g, 8,75 m°/s par pas pour le gyroscope à ±250 °/s.

Questions

  1. Carte posée à plat et immobile, que vaut l’accélération sur chaque axe ? Lequel mesure la gravité ? Avec quel signe ? Qu’en déduire sur l’orientation du module ?

  2. Immobile, le gyroscope devrait lire zéro. Que lit-il ? Comment corriger ce décalage, appelé biais ?

  3. Pourquoi le réglage BDU est-il important quand on lit les deux octets d’une mesure en deux fois ?

Solution de l’étape 4
#define IMU_WHO_AM_I  0x0Fu
#define IMU_CTRL1_XL  0x10u
#define IMU_CTRL2_G   0x11u
#define IMU_CTRL3_C   0x12u
#define IMU_OUTX_L_G  0x22u
#define IMU_OUTX_L_A  0x28u

static int imu_init(void)
{
    static const uint16_t addrs[2] = { 0x6Au << 1, 0x6Bu << 1 };
    uint8_t v;

    g_imu_addr = 0u;
    for (uint32_t k = 0u; k < 2u; k++)           /* SA0 à 0 ou à 1 */
    {
        if (HAL_I2C_IsDeviceReady(&hi2c3, addrs[k], 2u, I2C_TIMEOUT_MS) == HAL_OK)
        {
            g_imu_addr = addrs[k];
            break;
        }
    }
    if (g_imu_addr == 0u) { return 0; }

    if (HAL_I2C_Mem_Read(&hi2c3, g_imu_addr, IMU_WHO_AM_I, I2C_MEMADD_SIZE_8BIT,
                         &v, 1u, I2C_TIMEOUT_MS) != HAL_OK) { return 0; }
    g_who_am_i = v;
    if (v != 0x6Cu) { return 0; }                 /* ce n'est pas un LSM6DSOX */

    v = 0x44u;   /* CTRL3_C : BDU + IF_INC, en premier */
    HAL_I2C_Mem_Write(&hi2c3, g_imu_addr, IMU_CTRL3_C, I2C_MEMADD_SIZE_8BIT, &v, 1u, I2C_TIMEOUT_MS);
    v = 0x48u;   /* CTRL1_XL : 104 Hz, ±4 g */
    HAL_I2C_Mem_Write(&hi2c3, g_imu_addr, IMU_CTRL1_XL, I2C_MEMADD_SIZE_8BIT, &v, 1u, I2C_TIMEOUT_MS);
    v = 0x40u;   /* CTRL2_G : 104 Hz, ±250 °/s */
    HAL_I2C_Mem_Write(&hi2c3, g_imu_addr, IMU_CTRL2_G, I2C_MEMADD_SIZE_8BIT, &v, 1u, I2C_TIMEOUT_MS);
    return 1;
}

/* Trois axes de 16 bits, octet de poids faible en premier. */
static int imu_read3(uint8_t reg, int16_t out[3])
{
    uint8_t b[6];

    if (HAL_I2C_Mem_Read(&hi2c3, g_imu_addr, reg, I2C_MEMADD_SIZE_8BIT,
                         b, 6u, I2C_TIMEOUT_MS) != HAL_OK) { return 0; }
    for (uint32_t k = 0u; k < 3u; k++)
    {
        out[k] = (int16_t)(((uint16_t)b[2u * k + 1u] << 8) | b[2u * k]);
    }
    return 1;
}

et dans la boucle, dix fois par seconde :

int16_t a[3], g[3];

if ((g_imu_addr != 0u) && imu_read3(IMU_OUTX_L_A, a) && imu_read3(IMU_OUTX_L_G, g))
{
    for (uint32_t k = 0u; k < 3u; k++)
    {
        g_acc_mg[k]    = ((int32_t)a[k] * 122) / 1000;   /* 0,122 mg / pas */
        g_gyro_mdps[k] = ((int32_t)g[k] * 875) / 100;    /* 8,75 m°/s / pas */
    }
}
HAL_Delay(100);

Décoder 0x48 (CTRL1_XL). En binaire, 0100 1000 : les 4 bits de gauche (0100) choisissent la fréquence, 104 Hz ; les deux suivants (10) la pleine échelle, ±4 g. La fiche technique du LSM6DSOX donne ces tableaux de bits, registre par registre.

Lire 6 octets d’un coup marche grâce au bit IF_INC de CTRL3_C : après chaque octet lu, le capteur passe tout seul au registre suivant (OUTX_L, OUTX_H, OUTY_L…). Une seule transaction I²C pour trois axes : plus rapide, et les trois valeurs viennent du même échantillon.

Ici, l’ordre des octets est inversé par rapport au TMP102 : poids faible en premier (little-endian). Chaque composant a sa convention : on la lit dans sa fiche technique, on ne la devine jamais.

Le transtypage en int16_t fait de lui-même l’extension de signe : les 16 bits assemblés sont interprétés comme un nombre signé, ce qui est exactement le format du capteur.

Réponses aux questions 6 à 8

6. La gravité. Un axe lit environ ±1 000 mg, les deux autres presque 0. Un accéléromètre au repos ne mesure pas zéro : il mesure la réaction du support, qui l’empêche de tomber, dirigée vers le haut. L’axe qui pointe vers le haut lit +1 g, celui qui pointe vers le bas −1 g. Sur RatBot, le module est monté à l’envers : son axe Z pointe vers le bas, et lit environ −1 000 mg. C’est ainsi qu’on vérifie le montage d’une centrale, et le firmware du robot (comme son modèle 3D) en tient compte.

7. Le biais. Immobile, le gyroscope lit quelques dizaines à quelques centaines de m°/s, différents sur chaque axe : c’est son biais (offset), propre à chaque puce, et qui varie avec la température. Il paraît faible, mais il s”intègre : 0,1 °/s de biais donne 6° d’erreur de cap par minute. On le corrige en faisant la moyenne des mesures pendant une ou deux secondes, carte immobile, puis en la soustrayant de chaque mesure. Le firmware du robot le fait à chaque démarrage, et le remesure dès que le robot est à l’arrêt.

8. BDU. Une mesure de 16 bits est dans deux registres de 8 bits, lus l’un après l’autre. Le capteur produit 104 échantillons par seconde : si un nouvel échantillon arrive entre la lecture de l’octet bas et celle de l’octet haut, on assemble deux moitiés d’échantillons différents. Près d’un passage d’octet (de 0x00FF à 0x0100, par exemple), l’erreur est énorme : 0x01FF, ou 0x0000 — un faux pic. Avec BDU (Block Data Update), le capteur ne met à jour les deux registres qu’une fois les deux octets lus. Le défaut est rare, donc difficile à trouver : il vaut mieux ne jamais l’avoir.

Tout le code de la correction

main_user_code.c — compilé sans avertissement contre la HAL du STM32H7.

Ce n’est pas un main.c complet : ce sont les extraits à placer dans les blocs USER CODE du main.c généré par CubeMX, chacun sous le commentaire qui indique son bloc. La boucle while (1) contient une variante par étape, encadrée par #if 0 / #if 1 : n’en activer qu’une à la fois. La configuration CubeMX attendue est rappelée en tête du fichier, et les réponses aux questions à la fin.

Exercices supplémentaires

Ces exercices n’ont pas de correction.

  • Alerte de température : le TMP102 a une sortie ALERT reliée à PE15. Réglez ses seuils (registres 0x02 et 0x03) pour qu’elle s’active au-dessus de 30 °C, et allumez une LED quand elle passe à 0.

  • Niveau à bulle : les quatre LED indiquent de quel côté la carte penche, d’après l’accéléromètre.

  • Détecteur de choc : une LED s’allume (et le buzzer sonne) quand l’accélération dépasse 2 g sur un axe horizontal.

  • Cap : intégrez la vitesse de rotation du gyroscope autour de l’axe vertical pour obtenir un angle, et observez sa dérive en quelques minutes.

Après le TP

Pour rendre la carte au robot, voir Remettre la carte en état RatBot.