ESP32 et Atmega32u4 (Arduino micro) via I2C

Dans cet article, nous allons échanger des données entre le ESP32 et le  Atmega32u4 (Arduino micro) via un lien I2C. Le ESP32 sera programmé dans PlatformIO dans le cadre Arduino alors que le Atmega32u4 sera programmé sous Atmel studio avec les librairies C de base. Pour les rôles, le ESP32 sera le maître et le Atmega32U4 sera l’esclave.

La fonctionnalité sera validée par un bouton et une DEL sur chacun des deux modules. Ainsi, lorsque l’on appuiera sur le bouton du ESP32, la DEL du Atmega32 sera allumée et vise-versa. 

Finalement, nous utiliserons l’analyseur logique (Digital Discovery) afin de visualiser le protocole I2C entre les deux modules.

Débutons par le montage. Voici le schéma :

Notez que le ESP32 n’est pas tolérant au 5V sur les GPIO et que le Atmega32u4 supporte les niveaux logiques du 3V3. Une approche simple consiste donc d’utiliser des résistances tire-haut à 3V3.

Lorsque l’on appuiera sur le bouton SW1, la valeur transmise du ESP32 au Atmega32u4 sera 1 et 0 lorsqu’il ne sera pas appuyé.

1- Programmation de la transmission dans le ESP32

La librairie Wire dans arduino permet d’utiliser le lien I2C sans difficulté. 

Dans la fonction setup, nous débutons par l’initialisation du I2C. Par défaut, pour le ESP32, les broches SCL et SDA sont respectivement GPIO22 et GPIO21. L’appel de la méthode begin() se fait donc sans paramètre.

void setup() 
{
 Wire.begin(); 
}

Ensuite dans loop, la lecture du bouton conserve l’état dans une variable (pour utilisation future) qui sera transmise sur le lien I2C. Pour la transmission, il suffit de démarrer la transmission en précisant l’adresse du module visé (ici nous utiliserons 8 pour le Atmega32u4). Ensuite, l’envoi de la donnée se fait avec la méthode write, qui reçoit en paramètre la donnée à transmettre. Finalement, la transmission est terminée à l’aide de la méthode endTransmission, qui va émettre la condition stop sur le I2C. Le délai de 100ms est seulement pour envoyer moins de données durant l’exemple.

uint8_t stateOut = 0;

void loop() 
{
 Wire.beginTransmission(8); // L'adresse du atmega32u4 sera 8
 Wire.write(stateOut);       
 Wire.endTransmission();    

 // Lecture du bouton vers une variable.
if(digitalRead(23)==0)
   stateOut =1;
 else
   stateOut =0;

  delay(100);
}
2 – Programmation de la réception dans le Atmega32u4

Pour simplifier la réception, nous utiliserons l’interruption du TWI (I2C chez Atmel). L’interruption est déclenchée lorsque l’adresse du module est détectée en réception, si une donnée est reçue ou si une donnée est transmise. Les définitions sont dans la librairie twi.h dans les fichiers avr.

Pour la réception, nous allons utiliser un tableau de 16 octets même si dans la première application, nous transmettons un seul octet. Nous débutons donc par déclarer nos variables :

#define _I2C_RX_BUFFER_MAX_SIZE 16

volatile uint8_t _i2c_rxBuffer[_I2C_RX_BUFFER_MAX_SIZE];
volatile uint8_t _i2c_rxBuffer_indexIn = 0;
volatile uint8_t _i2c_newRxData = 0;

Ensuite, dans le traitement de l’interruption, nous allons faire le traitement en fonction du registre d’état. Si l’adresse esclave est reçue et que la commande est un Write, nous initialisons l’index de réception et nous nous assurons que le module va envoyer un ACK après la réception des données.

Si c’est une donnée qui est reçue, elle est placée en mémoire et l’index est augmenté. Si l’index arrive au dernier élément du tableau de réception, la configuration passe à la génération d’un NACK pour le prochain élément reçu. Un indicateur est utilisé afin de savoir qu’une nouvelle donnée est reçue.

ISR(TWI_vect)
{
switch ((TWSR & 0xF8))
{
// Adresse du module reçu et W
case TW_SR_SLA_ACK :
_i2c_rxBuffer_indexIn = 0;
// prépare pour réception de la donnée
TWCR |=(1<<TWEA);
break;
// Donnée reçue
case TW_SR_DATA_ACK:
// place en mémoire si le buffer n'est pas plein
if(_i2c_rxBuffer_indexIn < _I2C_RX_BUFFER_MAX_SIZE)
_i2c_rxBuffer[_i2c_rxBuffer_indexIn++] = TWDR;

// Si le buffer est au dernier élément, générer un NACK au prochain.
if(_i2c_rxBuffer_indexIn >= _I2C_RX_BUFFER_MAX_SIZE-1)
TWCR &= ~(1<<TWEA);
else
TWCR |=(1<<TWEA);

_i2c_newRxData = 1;
break;

default :
TWCR |=(1<<TWEA);
}
// Dans tous les cas, relâche le flag du ISR
TWCR |= (1<<TWIE) | (1<<TWINT) | (1<<TWEN);
}

Pour l’initialisation c’est simple, car le mode esclave ne configure par la vitesse. Il faut simplement s’assurer que le l’horloge interne est au moins 16x plus rapide que le SCL. Par contre dans le mode esclave, il faut configurer l’adresse du module.

void i2cInitSlaveMode(uint8_t add )
{
TWAR = (add<<1) ; // Place l'adresse du slave + Ne répondra pas sur l'adresse générale
TWCR = (1<<TWIE) | (1<<TWEA) | (1<<TWINT) | (1<<TWEN); // active le ISR et le I2c et le ACK
sei();
}

Afin de détecter les nouvelles données reçues à l’extérieur de la librairie, nous utiliserons les deux fonctions suivantes :

void i2cClearNewDataFlag()
{
cli();
_i2c_newRxData = 0;
sei();
}
uint8_t i2cNewDataAvailable()
{
return _i2c_newRxData;
}

Finalement, une fonction permet de lire un octet dans le tableau de réception.

uint8_t i2cReadBuffer(uint8_t add)
{
if(add < _I2C_RX_BUFFER_MAX_SIZE)
return _i2c_rxBuffer[add];
return _i2c_rxBuffer[0];
}

La fonction main est très petite. Elle regarde si une nouvelle donnée est reçue. Si c’est le cas, elle va lire le premier octet en mémoire, elle efface l’indication qu’une donnée est reçue, puis elle change l’état de la DEL.

int main(void)
{
i2cInitSlaveMode(8);
DDRC |= (1<<7);
while (1)
{
if(i2cNewDataAvailable())
{
stateIn = i2cReadBuffer(0);
i2cClearNewDataFlag();
if(stateIn & 1)
PORTC |= (1<<7);
else
PORTC &= ~(1<<7);
}
_delay_ms(1);
}
}
3- Ajout de la lecture du slave dans le ESP32

Maintenant que le ESP32 peut envoyer des données vers le Atmega32u4, nous allons ajouter le sens inverse.  

Pour déclencher la lecture de l’esclave, il faut utiliser la commande Wire.requestFrom(#adresse, #nombre d’octets à lire). Ensuite la lecture se fait avec la fonction Wire.read() qui retourne 1 octet reçu à la fois.

delayMicroseconds(10);
Wire.requestFrom(8, 1);  // Lire 1 octet du module #8
stateIn = Wire.read();
4- Ajout de la transmission dans le atmega32u4

Pour que le atmega32u4 réponde à la demande de lecture du maître, il faut ajouter le support pour la condition où l’on reçoit l’adresse du module et la commande Read. Lorsque l’adresse est reçue avec la commande Read, la première étape consiste à remettre l’index de transmission à 0 et à envoyer la première donnée. Ensuite, pour chaque réception d’un ACK par le maître, transmettre une autre donnée. L’arrêt se fait sur réception d’un NACK. Sinon, si la lecture dépasse le tableau de transmission, il va transmettre des 0. Voici le code pour cette section.

// Adresse reçue avec demande de lecture
case TW_ST_SLA_ACK :
case TW_ST_ARB_LOST_SLA_ACK :
_i2c_txBuffer_indexOut = 0; // au début on clear l'index
case TW_ST_DATA_ACK :
// Transmission de la donnée
if(_i2c_txBuffer_indexOut < _I2C_TX_BUFFER_MAX_SIZE)
TWDR = _i2c_txBuffer[_i2c_txBuffer_indexOut++];
else
TWDR = 0;
break;
// Dernière donnée NACK, on fait rien
case TW_ST_DATA_NACK :
break;
5- Lectures avec l’analyseur logique

Voici ce que l’on pourra observé sur l’analyseur logique avec les signaux SDA et SCL. La première séquence est lorsque le ESP32 transmet vers le Atmega32u4 et la seconde, est lorsque le ESP32 va lire le Atmega32u4.

Si aucun bouton n’est appuyé

Lors qu’aucun bouton n'est appuyé

Si le bouton du ESP32 est appuyé

Si le bouton du Atmega32u4 est appuyé

 

6- Codes complets

Voici donc le code complet pour le ESP32

#include <Arduino.h>
#include <Wire.h>

void setup() 
{
 Wire.begin(); 
 pinMode(15,OUTPUT);
}

uint8_t stateOut = 0;
uint8_t stateIn = 0;

void loop() 
{
 Wire.beginTransmission(8);
 Wire.write(stateOut);             // sends value byte  
 Wire.endTransmission();     // stop transmitting  
 if(digitalRead(23)==0)
   stateOut =1;
 else
   stateOut =0;
 delayMicroseconds(10);
 Wire.requestFrom(8, 1);    // request 6 bytes from slave device #2
 stateIn = Wire.read();
 digitalWrite(15,stateIn);
 delay(100);
}

Le code complet pour le fichier i2cSlave.c

#include "i2c.h"
#include <avr/io.h>
#include <util/twi.h> // pour les defines des états
#include <avr/interrupt.h>

#define _I2C_TX_BUFFER_MAX_SIZE 16
volatile uint8_t _i2c_txBuffer[_I2C_TX_BUFFER_MAX_SIZE];
volatile uint8_t _i2c_txBuffer_indexOut = 0;

#define _I2C_RX_BUFFER_MAX_SIZE 16
volatile uint8_t _i2c_rxBuffer[_I2C_RX_BUFFER_MAX_SIZE];
volatile uint8_t _i2c_rxBuffer_indexIn = 0;
volatile uint8_t _i2c_newRxData = 0;

ISR(TWI_vect)
{
switch ((TWSR & 0xF8))
{
// Adresse du module reçu et W
case TW_SR_SLA_ACK :
_i2c_rxBuffer_indexIn = 0;
// prépare pour réception de la donnée
TWCR |=(1<<TWEA);
break;
// Donnée reçue
case TW_SR_DATA_ACK:
// place en mémoire si le buffer n'est pas plein
if(_i2c_rxBuffer_indexIn < _I2C_RX_BUFFER_MAX_SIZE)
_i2c_rxBuffer[_i2c_rxBuffer_indexIn++] = TWDR;

// le buffer est full, arrêter la réception, sinon prépare pour la prochaine réception
if(_i2c_rxBuffer_indexIn >= _I2C_RX_BUFFER_MAX_SIZE)
TWCR &= ~(1<<TWEA);
else
TWCR |=(1<<TWEA);

_i2c_newRxData = 1;
break;
// Adresse reçue avec demande de lecture
case TW_ST_SLA_ACK :
case TW_ST_ARB_LOST_SLA_ACK :
_i2c_txBuffer_indexOut = 0;
case TW_ST_DATA_ACK :
// Transmission de la première donnée
if(_i2c_txBuffer_indexOut < _I2C_TX_BUFFER_MAX_SIZE)
TWDR = _i2c_txBuffer[_i2c_txBuffer_indexOut++];
else
TCDR = 0;
break;
// Dernière donnée NACK, on fait rien
case TW_ST_DATA_NACK :
break;
default :
TWCR |=(1<<TWEA);
}
// Dans tous les cas, relâche le flag du ISR
TWCR |= (1<<TWIE) | (1<<TWINT) | (1<<TWEN);
}

void i2cWriteBuffer(uint8_t add, uint8_t data)
{
if(add < _I2C_TX_BUFFER_MAX_SIZE)
_i2c_txBuffer[add] = data;
}

uint8_t i2cReadBuffer(uint8_t add)
{
if(add < _I2C_RX_BUFFER_MAX_SIZE)
return _i2c_rxBuffer[add];
return _i2c_rxBuffer[0];
}

void i2cClearNewDataFlag()
{
cli();
_i2c_newRxData = 0;
sei();
}
uint8_t i2cNewDataAvailable()
{
return _i2c_newRxData;
}

void i2cInitSlaveMode(uint8_t add )
{
TWAR = (add<<1) ; // Place l'adresse du slave + Ne répondra pas sur l'adresse générale
TWCR = (1<<TWIE) | (1<<TWEA) | (1<<TWINT) | (1<<TWEN); // active le ISR et le I2c et le ACK
sei();
}

Le code complet pour le fichier main.c

#define F_CPU 16000000
#include <avr/io.h>
#include <util/delay.h>
#include "i2cSlave.h"

uint8_t stateIn = 0;
uint8_t stateOut = 0;

int main(void)
{
i2cInitSlaveMode(8);
DDRC |= (1<<7);
while (1)
{
if(PINB & (1<<4))
stateOut = 0;
else
stateOut = 1;
i2cWriteBuffer(0,stateOut);
if(i2cNewDataAvailable())
{
stateIn = i2cReadBuffer(0);
i2cClearNewDataFlag();
if(stateIn & 1)
PORTC |= (1<<7);
else
PORTC &= ~(1<<7);
}
_delay_ms(1);
}
}

N’hésitez pas à donner vos commentaires et à poser vos questions.

Laisser un commentaire

Votre adresse courriel ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire le pourriel. En savoir plus sur comment les données de vos commentaires sont utilisées.