Bare-Metal STM32 : Utilisation du bus I2C en mode maître-émetteur-récepteur

En tant que l’un des bus les plus populaires aujourd’hui pour la communication embarquée et inter-cartes au sein des systèmes, il y a de fortes chances que vous finissiez par l’utiliser avec un système embarqué. I2C offre une variété de vitesses tout en ne nécessitant que deux fils (horloge et données), ce qui le rend beaucoup plus facile à gérer que les alternatives, telles que SPI. Dans la famille de microcontrôleurs STM32, vous trouverez au moins un périphérique I2C sur chaque appareil.

En tant que support semi-duplex partagé, I2C utilise une conception d’appel et de réponse assez simple, où un appareil contrôle l’horloge, et d’autres appareils attendent et écoutent simplement jusqu’à ce que leur adresse fixe soit envoyée sur le bus I2C. Si la configuration d’un périphérique STM32 I2C comporte quelques étapes, il est assez indolore à utiliser par la suite, comme nous le verrons dans cet article.

Étapes de base

En supposant que les dispositifs de réception tels que les capteurs sont correctement câblés avec les résistances de rappel requises en place, nous pouvons ensuite commencer à configurer le périphérique I2C du MCU. Nous utiliserons le STM32F042 comme MCU cible, mais les autres familles STM32 sont assez similaires du point de vue I2C. Nous utiliserons également des références de périphériques et de registres de style CMSIS.

Tout d’abord, nous définissons les broches GPIO que nous souhaitons utiliser pour le périphérique I2C, activant le mode de fonction alternative (AF) approprié. Ceci est documenté dans la fiche technique du MCU cible. Pour le MCU STM23F042, la broche SCL (horloge) standard est sur PA11, avec AF 5. SDA (données) se trouve sur PA12, avec le même AF. Pour cela, nous devons définir les bits appropriés dans le registre GPIO_AFRH (registre de fonction alternatif haut) :

GPIO_AFRH sur STM32F042 avec des valeurs AF.

En sélectionnant AF 5 pour les broches 11 & 12 (AFSEL11 & AFSEL12), ces broches sont ensuite connectées en interne au premier périphérique I2C (I2C1). Ceci est similaire à ce que nous avons fait dans un article précédent sur l’UART. Nous devons également activer le mode AF pour la broche dans GPIO_MODER :

Disposition GPIO_MODER STM32F0x2 (RM0091, 8.4.4).

Tout cela se fait à l’aide du code suivant :


uint8_t pin = 11;                // Repeat for pin 12
uint8_t pin2 = pin * 2;
GPIOA->MODER &= ~(0x3 << pin2); 
GPIOA->MODER |= (0x2 << pin2);   // Set AF mode.

// Set AF mode in appropriate (high/low) register.
if (pin < 8) { 
    uint8_t pin4 = pin * 4; 
    GPIOA->AFR[0] &= ~(0xF << pin4); 
    GPIOA->AFR[0] |= (af << pin4); 
} 
else { 
    uint8_t pin4 = (pin - 8) * 4; 
    GPIOA->AFR[1] &= ~(0xF << pin4); 
    GPIOA->AFR[1] |= (af << pin4);
}


Notez que nous voulons que les broches SCL et SDA soient toutes deux configurées dans les registres GPIO pour être dans un état flottant sans pullup ou pulldown, et en configuration de drain ouvert. Cela correspond aux propriétés du bus I2C, qui est conçu pour être à drain ouvert. En fait, cela signifie que les pull-ups sur les lignes de bus maintiennent le signal haut, à moins qu’il ne soit abaissé par un périphérique maître ou esclave sur le bus.

L’horloge du premier périphérique I2C est activée dans RCC_APB1ENR (activer le registre) avec :

RCC->APB1ENR |= RCC_APB1ENR_I2C1EN;

Certains microcontrôleurs STM32F0 n’ont qu’un seul périphérique I2C (STM32F03x et F04x), tandis que les autres en ont deux. Quoi qu’il en soit, si le périphérique I2C existe, après avoir défini son bit d’activation d’horloge dans ce registre, nous pouvons maintenant passer à la configuration du périphérique I2C lui-même en tant que maître.

Configuration de l’horloge

Avant de faire quoi que ce soit d’autre avec le périphérique I2C, nous devons nous assurer qu’il est dans un état désactivé :

I2C1->CR1 &= ~I2C_CR1_PE;

Les réglages de l’horloge sont définis dans I2C_ TIMINGR:

Disposition I2C_TIMINGR, selon RM0091 (26.7.5)
Disposition I2C_TIMINGR, selon RM0091 (26.7.5)

Le manuel de référence répertorie un certain nombre de tableaux avec des paramètres de synchronisation, en fonction de l’horloge I2C, par exemple pour une vitesse d’horloge I2C de 8 MHz sur STM32F0 :

Tableau d'exemple de configuration IC2_TIMINGR.  Source : RM0091, 26.4.10.
Tableau d’exemple de configuration IC2_TIMINGR. Source : RM0091, 26.4.10.

Cette table peut être convertie en un tableau de valeurs prêt à l’emploi pour configurer le périphérique I2C en mettant ces valeurs dans le bon ordre pour l’insertion dans I2C_TIMINGR, par exemple pour STM32F0 :


uint32_t i2c_timings_4[4];
uint32_t i2c_timings_8[4];
uint32_t i2c_timings_16[4];
uint32_t i2c_timings_48[4];
uint32_t i2c_timings_54[4];

i2c_timings_4[0] = 0x004091F3;
i2c_timings_4[1] = 0x00400D10;
i2c_timings_4[2] = 0x00100002;
i2c_timings_4[3] = 0x00000001;
i2c_timings_8[0] = 0x1042C3C7;
i2c_timings_8[1] = 0x10420F13;
i2c_timings_8[2] = 0x00310309;
i2c_timings_8[3] = 0x00100306;
i2c_timings_16[0] = 0x3042C3C7;
i2c_timings_16[1] = 0x30420F13;
i2c_timings_16[2] = 0x10320309;
i2c_timings_16[3] = 0x00200204;
i2c_timings_48[0] = 0xB042C3C7;
i2c_timings_48[1] = 0xB0420F13;
i2c_timings_48[2] = 0x50330309;
i2c_timings_48[3] = 0x50100103;
i2c_timings_54[0] = 0xD0417BFF;
i2c_timings_54[1] = 0x40D32A31;
i2c_timings_54[2] = 0x10A60D20;
i2c_timings_54[3] = 0x00900916;


Les autres options disponibles ici consistent à laisser les outils fournis par STMicroelectronic (par exemple CubeMX) calculer les valeurs pour vous, ou à utiliser les informations du manuel de référence pour les calculer vous-même. À ce stade, l’implémentation I2C du framework Nodate pour STM32 utilise à la fois, avec les valeurs prédéfinies comme ci-dessus pour STM32F0, et des valeurs calculées dynamiquement pour les autres familles.

L’avantage du calcul dynamique des valeurs de synchronisation est qu’il ne repose pas sur des vitesses d’horloge I2C prédéfinies. Comme inconvénient, il y a le retard supplémentaire impliqué dans le calcul de ces valeurs, plutôt que de les lire directement à partir d’une table. L’approche qui fonctionne le mieux dépend probablement des exigences du projet.

Avec le I2C_TIMINGR registre ainsi configuré, on peut activer le périphérique :

I2C1->CR1 |= I2C_CR1_PE;

Écrire des données

Avec le périphérique I2C prêt et en attente, nous pouvons commencer à envoyer des données. Tout comme avec un USART, cela se fait en écrivant dans un registre de transmission (TX) et en attendant que la transmission se termine. Les étapes à suivre ici sont couvertes dans le diagramme de flux utile fourni dans le manuel de référence :

Organigramme du transmetteur maître, reproduit à partir du RM0091.
Organigramme du transmetteur maître, reproduit à partir du RM0091.

Il convient de noter ici qu’avec certaines vérifications, comme pour I2C_ISR_TC (transfert terminé), l’idée n’est pas de vérifier une fois et d’être terminé, mais plutôt d’attendre avec un délai d’attente.

Pour un simple transfert de 1 octet, nous définirions I2C_CR2 comme tel :


I2C1->CR2 |= (slaveID << 1) | I2C_CR2_AUTOEND | (uint32_t) (1 << 16) | I2C_CR2_START;


Cela démarrerait le transfert pour un total de 1 octet (décalé à gauche vers la position NBYTES dans le registre I2C_CR2), ciblant le 7 bits slaveID, avec la condition d’arrêt I2C générée automatiquement. Une fois le transfert effectué (NBYTES transféré), le STOP est généré, ce qui définit un indicateur dans I2C_ISR appelé STOPF.

Lorsque nous savons que nous avons fini de transférer des données, nous devons attendre que cet indicateur soit défini, puis effacer l’indicateur dans I2C_ICR et effacer le registre I2C_CR2 :


instance.regs->ICR |= I2C_ICR_STOPCF;
instance.regs->CR2 = 0x0;


Ceci termine un transfert de données de base. Pour transférer plus d’un octet, bouclez simplement la même procédure, en écrivant un seul octet dans I2C_TXDR chaque cycle et en attendant I2C_ISR_TXIS à régler (avec temporisation requise). Pour transférer plus de 255 octets, réglez I2C_CR2_RELOAD à la place de I2C_CR2_AUTOEND dans I2C_CR2 permettra de transférer un nouveau lot de 255 octets ou moins.

Lecture de données

Lors de la lecture de données à partir d’un périphérique, assurez-vous que les interruptions sont désactivées (à l’aide NVIC_DisableIRQ). Généralement, une demande de lecture est envoyée au dispositif par le microcontrôleur, le dispositif répondant en envoyant le contenu du registre demandé en réponse. Par exemple, si un capteur MEMS BME280 est envoyé 0xd0 comme seule charge utile, il répondra en renvoyant son ID (fixe) tel que programmé dans ce registre en usine.

L’organigramme de base pour la réception depuis un appareil se présente comme suit :

Organigramme du récepteur maître pour STM32F0.  Source : RM0091.
Organigramme du récepteur maître pour STM32F0. Source : RM0091.

L’idée de base ici est la même que pour la transmission de données. Nous configurons I2C_CR2 de la même manière qu’avant. Les principales différences ici sont que nous attendons que le drapeau I2C_ISR_RXNE soit désactivé, après quoi nous pouvons lire le contenu à un octet de I2C_RXDR dans notre tampon.

Tout comme pour l’écriture de données, après avoir lu NBYTES, nous devons attendre que l’indicateur I2C_ISR_STOPF soit défini, puis l’effacer via le registre I2C_ICR et effacer le registre I2C_CR2.

Lectures basées sur les interruptions

La configuration des interruptions avec I2C nous oblige à activer les interruptions pour le périphérique I2C en question. Cela doit être fait avec le périphérique dans un état désactivé. Après cela, nous pouvons activer l’interruption :


NVIC_SetPriority(I2C1_IRQn, 0);
NVIC_EnableIRQ(I2C1_IRQn);


Les interruptions sont ensuite activées sur le périphérique en définissant le bit de configuration :


I2C1->CR1 |= I2C_CR1_RXIE;


Assurez-vous que le gestionnaire d’interruption (ISR) nommé de manière appropriée est implémenté avec le nom spécifié dans le code de démarrage :


volatile uint8_t i2c_rxb = 0;

void I2C1_IRQHandler(void) {
    // Verify interrupt status.
    if ((I2C->ISR & I2C_ISR_RXNE) == I2C_ISR_RXNE) {
        // Read byte (which clears RXNE flag).
        i2c_rxb = instance.regs->RXDR;
    }
}


N’oubliez pas d’ajouter le extern "C" { } bloquer autour du gestionnaire si vous utilisez un langage autre que C pour empêcher la modification du nom de la fonction.

Avec ce code en place, chaque fois que le tampon de lecture reçoit un octet, l’ISR sera appelé et nous pourrons le copier dans un tampon, ou ailleurs.

Utilisation multi-appareils

Comme on peut le supposer à ce stade, l’utilisation de plusieurs appareils à partir d’un seul émetteur-récepteur à microcontrôleur nécessite uniquement que l’identifiant d’appareil correct soit envoyé avant toute charge utile. C’est également là qu’il est essentiel d’effacer le registre I2C_CR2 et de le définir correctement lors du prochain cycle d’émission ou de réception, afin d’éviter toute confusion dans les identifiants d’appareils.

Selon l’implémentation du code (par exemple avec un RTOS multithread), il est possible que des lectures et des écritures conflictuelles se produisent. Il est essentiel dans ce cas que les écritures et lectures I2C soient coordonnées afin qu’aucune donnée ou commande ne soit perdue ou envoyée au mauvais appareil.

Emballer

L’utilisation d’I2C sur STM32 n’est pas très compliquée, une fois l’obstacle de la configuration de l’horloge franchi. C’est un sujet qui mérite peut-être son propre article, ainsi que des sujets avancés relatifs à I2C tels que l’étirement de l’horloge et le filtrage du bruit. Par défaut, le périphérique I2C sur les MCU STM32 a un filtre de bruit activé sur ses entrées I2C, mais ceux-ci peuvent également être configurés davantage.

Aussi simple que soit la lecture et l’écriture de base avec I2C, il reste encore tout un terrier de lapin à explorer, également lorsqu’il s’agit d’implémenter votre propre appareil I2C sur STM32. Restez à l’écoute pour d’autres articles sur ces sujets.