CH32V003 : I2C et EEPROM

Ce que nous allons faire - Prérequis

Nous allons voir dans ce cours :

  • comment utiliser l'interface I2C du CH32V003

  • ce qu'est une EEPROM et à quoi elle sert

  • comment utiliser une EEPROM I2C

Les prérequis de ce cours sont :

  • Avoir suivi le cours CH32V003 : GPIO et interruptions.

  • Disposer d'un module EEPROM I2C 24C256 tel que recommandé dans les pré-requis, un bouton poussoir et un condensateur de 100nF.

  • Avoir lu la page Wikipedia sur le protocole I2C afin d'avoir une idée générale du sujet. Vous n'avez pas besoin de chercher à tout comprendre, ça viendra naturellement avec les manipulations.

Le matériel

Avant de commencer, vérifiez que les jumpers du module EEPROM sont dans la même position que sur la photo. Ça affecte l'adresse I2C de l'EEPROM (A0 et A1) et sa protection en écriture (WP). Notez que le jumper A2 n'est pas utilisé avec cette EEPROM, mais il pourrait l'être si vous remplaciez la 24C256 par une autre EEPROM de capacité plus faible (ex. la 24C02).

Ensuite, réalisez le câblage décrit ci-dessous. Nous utiliserons un bouton poussoir pour déclencher les lectures et écritures de l'EEPROM afin d'avoir le temps de suivre ce qui se passe.

Les EEPROM

Les EEPROM sont des mémoires non-volatiles, c'est-à-dire conservant leur contenu même hors tension.

Leur nom signifie "Electrically Erasable Programmable Read-Only Memory". Ça peut sembler bizarre (comment une mémoire à lecture seule peut-elle être effaçable et programmable ?), mais ce nom s'explique par l'histoire de l'évolution des technologies des mémoires. Malgré tout, ce nom a le mérite de mettre en relief plusieurs caractéristiques des EEPROM :

  • Elles sont conçues pour être plus souvent lues que programmées.

  • La rapidité d'accès n'est pas la priorité. Ce qui est intéressant, c'est qu'elles sont non-volatiles. Et par le fait, l'écriture est d'une grande lenteur pour une mémoire, la data sheet indique 10ms !

  • Elles offrent une certaine souplesse d'utilisation : elles sont effaçables électriquement alors que leurs ancêtres les EPROM (avec un seul E) devaient être soumises à des ultra-violets avant d'être de nouveau programmables et ce au moyen d'un programmateur spécial.

  • Cette souplesse permet de les utiliser en lecture et en écriture directement par le micro-contrôleur.

Ces caractéristiques font des EEPROM des supports idéaux pour stocker des données de configuration, par exemple.

Les EEPROM, comme les mémoires flash, ont une durée de vie limitée et ce doit être pris en compte dans certaines applications. Cependant, la data sheet de la 24C256 nous indique que son endurance est de 100000 cycles d'écriture, donc on a largement de quoi voir venir dans la majorité des cas.

Une autre caractéristique intéressante est la durée de rétention des données. Pour la 24C256, elle est de 40 ans, donc là encore, il y a de la marge.

De nos jours, la plupart des périphériques ont une interface de communication avec le micro-contrôleur de type série, soit I2C, soit SPI et ses variantes. Le charme de la chose est que l'interfaçage de ces périphériques nécessite très peu de lignes de GPIO, une ressource dont on manque toujours (il existe d'ailleurs des "I/O expanders", I2C et SPI, pour cette raison, par exemple les PCF8574A, MCP23S17, etc).

Enfin, notez que contrairement aux mémoires flash, le contenu des EEPROM est écrit octet par octet. Leur utilisation est donc beaucoup plus souple pour manipuler de petits blocs de données comme des structures contenant des options de configuration.

Lecture et écriture d'un octet

Nous allons commencer par les 2 opérations les plus simples, la lecture et l'écriture d'un octet. Ce sera l'occasion pour vous de voir des séquences complètes de transactions I2C et de voir le rôle déterminant que les conditions start et stop et les bits ACK et NAK jouent dans le séquencement des opérations.

Vous verrez en particulier qu'on peut avoir 2 conditions de start dans une même transaction pour changer de sens : un premier start suivi de l'adresse I2C de l'EEPROM et du bit d'écriture pour envoyer l'adresse mémoire de la donnée à lire, puis un second start suivi de l'adresse I2C de l'EEPROM et du bit de lecture pour attendre le résultat. L'absence de stop avant le second start n'est PAS une erreur.

Le moment est venu pour vous de créer un nouveau projet, modifier system_ch32v00x.c comme vous en avez maintenant l'habitude, puis de remplacer le contenu du fichier main.c par le code suivant :

#include <ch32v00x.h>
#include <stdbool.h>
#include <debug.h>

// L'interface I2C du CH32V003 peut fonctionner au maximum à 400kHz
// et la 24C256 1MHz, donc allons-y pour 400kHz.
#define I2C_CLOCK_FREQ 400000
// On assigne l'adresse I2C 1 au CH32V003.
#define I2C_HOST_ADDR 1

// L'adresse I2C de la 24C256 est comprise entre 0x50 et 0x53.
// Avec la position par défaut des jumpers de la carte, c'est 0x50.
#define EEPROM_I2C_ADDR 0x50

volatile bool buttonPressed = false;

__attribute__((interrupt("WCH-Interrupt-fast")))
void EXTI7_0_IRQHandler() {
	if (EXTI_GetITStatus(EXTI_Line2) != RESET) {
		buttonPressed = true;
		EXTI_ClearITPendingBit(EXTI_Line2);
	}
}

// -- IMPORTANT
// Reportez-vous à la figure 8 page 11 de la data sheet
// de la 24C256 et à son paragraphe "BYTE WRITE" page 10
// pour suivre les explications des commentaires.
void eepromWriteByte(uint16_t dataAddress, uint8_t value) {
	// On s'assure que le bus I2C soit disponible.
	while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) != RESET);

	// On génère la condition de start de la transaction
	I2C_GenerateSTART(I2C1, ENABLE);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

	// On envoie l'adresse de l'EEPROM et on signale que c'est une
	// écriture (I2C_Direction_Transmitter).
	// Dans le protocole I2C, l'adresse commence au 2ème bit (le bit
	// de poids le plus faible indique si on lit ou écrit l'esclave)
	// donc il faut penser à la décaler d'une position à gauche.
	I2C_Send7bitAddress(I2C1, EEPROM_I2C_ADDR << 1, I2C_Direction_Transmitter);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

	// On envoie l'octet de poids fort de l'adresse de la donnée à écrire.
	I2C_SendData(I2C1, dataAddress >> 8);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

	// On envoie l'octet de poids faible de l'adresse de la donnée à écrire.
	I2C_SendData(I2C1, dataAddress);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

	// On envoie la valeur à écrire.
	I2C_SendData(I2C1, value);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

	// On génère la condition de stop de la transaction.
	I2C_GenerateSTOP(I2C1, ENABLE);
}

// -- IMPORTANT
// Reportez-vous à la figure 11 page 12 de la data sheet
// de la 24C256 et à son paragraphe "RANDOM READ" page 11
// pour suivre les explications des commentaires.
uint8_t eepromReadByte(uint16_t dataAddress) {
	uint8_t result = 0;

	// On s'assure que le bus I2C soit disponible.
	while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) != RESET);

	// On génère la première condition de start, celle qui précède
	// l'envoi de l'adresse de la donnée à lire.
	I2C_GenerateSTART(I2C1, ENABLE);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

	// On envoie l'adresse de l'EEPROM et on signale que c'est une
	// écriture (I2C_Direction_Transmitter).
	// Dans le protocole I2C, l'adresse commence au 2ème bit (le bit
	// de poids le plus faible indique si on lit ou écrit l'esclave)
	// donc il faut penser à la décaler d'une position à gauche.
	I2C_Send7bitAddress(I2C1, EEPROM_I2C_ADDR << 1, I2C_Direction_Transmitter);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

	// On envoie l'octet de poids fort de l'adresse de la donnée à lire.
	I2C_SendData(I2C1, dataAddress >> 8);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

	// On envoie l'octet de poids faible de l'adresse de la donnée à lire.
	I2C_SendData(I2C1, dataAddress);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

	// On génère la seconde condition de start, celle qui précède
	// la réception de la donnée à lire.
	I2C_GenerateSTART(I2C1, ENABLE);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

	// On envoie l'adresse de l'EEPROM et on signale que c'est une
	// lecture (I2C_Direction_Receiver).
	I2C_Send7bitAddress(I2C1, EEPROM_I2C_ADDR << 1, I2C_Direction_Receiver);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

	// On attend que la réponse soit arrivée.
	while (I2C_GetFlagStatus(I2C1, I2C_FLAG_RXNE) == RESET);

	// On envoie un NAK (voir figure 11 page 12 de la data sheet de la 24C256).
	I2C_AcknowledgeConfig(I2C1, DISABLE);

	// On récupère le résultat.
	result = I2C_ReceiveData(I2C1);
	
	// On reconfigure un ACK par défaut pour les transactions suivantes.
	I2C_AcknowledgeConfig(I2C1, ENABLE);

	// On génère la condition de stop de la transaction.
	I2C_GenerateSTOP(I2C1, ENABLE);

	return result;
}

int main() {
	USART_Printf_Init(115200);

	// Les interruptions externes et le port I2C sont des alternate functions.
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

	// -- On configure les lignes de GPIO.
	GPIO_InitTypeDef gpioInit = { 0 };
	// PD2 = bouton poussoir
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
	gpioInit.GPIO_Pin = GPIO_Pin_2;
	gpioInit.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOD, &gpioInit);
	// PC1 = SDA
	// PC2 = SCL
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
	gpioInit.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
	// Attention au mode, I2C est une alternate function
	gpioInit.GPIO_Mode = GPIO_Mode_AF_OD;
	gpioInit.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &gpioInit);

	// -- On configure l'interruption du bouton poussoir.
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOD, GPIO_PinSource2);

	EXTI_InitTypeDef extiInit = { 0 };
	extiInit.EXTI_Line = EXTI_Line2;
	extiInit.EXTI_Mode = EXTI_Mode_Interrupt;
	extiInit.EXTI_Trigger = EXTI_Trigger_Falling;
	extiInit.EXTI_LineCmd = ENABLE;
	EXTI_Init(&extiInit);

	NVIC_InitTypeDef nvicInit = { 0 };
	nvicInit.NVIC_IRQChannel = EXTI7_0_IRQn;
	nvicInit.NVIC_IRQChannelPreemptionPriority = 0;
	nvicInit.NVIC_IRQChannelSubPriority = 1;
	nvicInit.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&nvicInit);

	// -- On configure et active l'interface I2C.
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
	I2C_InitTypeDef i2cInit = { 0 };
	i2cInit.I2C_ClockSpeed = I2C_CLOCK_FREQ;
	i2cInit.I2C_Mode = I2C_Mode_I2C;
	i2cInit.I2C_DutyCycle = I2C_DutyCycle_2;
	// Dans le protocole I2C, l'adresse commence au 2ème bit (le bit
	// de poids le plus faible indique si on lit ou écrit l'esclave)
	// donc il faut penser à la décaler d'une position à gauche.
	i2cInit.I2C_OwnAddress1 = I2C_HOST_ADDR << 1;
	i2cInit.I2C_Ack = I2C_Ack_Enable;
	i2cInit.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	I2C_Init(I2C1, &i2cInit);
	I2C_Cmd(I2C1, ENABLE);

	// On utilisera toujours la même adresse dans cet exercice.
	uint16_t dataAddress = 0;

	while (1) {
		// A chaque fois qu'on appuie sur le bouton poussoir,
		if (buttonPressed) {
			buttonPressed = false;
			// on lit le contenu de l'EEPROM,
			uint8_t value = eepromReadByte(dataAddress);
			// on l'affiche,
			printf("value = %d\r\n", (int) value);
			// et on l'incrémente.
			eepromWriteByte(dataAddress, value + 1);
		}
	}
}

Le plus important dans cet exercice est de faire le lien entre la documentation (chronogrammes) et le code.

Lecture et écriture de blocs d'octets

Évidemment, les données de configuration d'une application comportent plus d'un octet, donc ce qui va nous intéresser, c'est de lire et écrire des blocs de d'octets plutôt que de les traiter un par un.

Remplacez maintenant le contenu du fichier main.c par le code suivant :

#include <ch32v00x.h>
#include <stdbool.h>
#include <debug.h>

// L'interface I2C du CH32V003 peut fonctionner au maximum à 400kHz
// et la 24C256 aussi, donc allons-y pour 400kHz.
#define I2C_CLOCK_FREQ 400000
// On assigne l'adresse I2C 1 au CH32V003.
#define I2C_HOST_ADDR 1

// L'adresse I2C de la 24C256 est comprise entre 0x50 et 0x53.
// Avec la position par défaut des jumpers de la carte, c'est 0x50.
#define EEPROM_I2C_ADDR 0x50
// La taille d'une page de l'EEPROM est de 64 octets,
// c'est-à-dire 6 bits d'adresse.
#define EEPROM_PAGE_BITS 6
// La 24C256 a une capacité de 32Ko
#define EEPROM_SIZE 32768u

volatile bool buttonPressed = false;

__attribute__((interrupt("WCH-Interrupt-fast")))
void EXTI7_0_IRQHandler() {
	if (EXTI_GetITStatus(EXTI_Line2) != RESET) {
		buttonPressed = true;
		EXTI_ClearITPendingBit(EXTI_Line2);
	}
}

// -- IMPORTANT
// Reportez-vous à la figure 9 page 12 de la data sheet de
// la 24C256 et à son paragraphe "PAGE WRITE" page 10 pour
// suivre les explications des commentaires.
void eepromPageWrite(uint16_t startAddress, const uint8_t *pageBuffer, uint16_t count) {
	// Si on écrit plus d'octets que la page cible ne peut en contenir,
	// le pointeur d'écriture revient AU DEBUT DE LA PAGE, ce qui est
	// contre-intuitif. Pour avoir un comportement moins surprenant
	// (cf. https://fr.wikipedia.org/wiki/Principe_de_moindre_surprise),
	// on va limiter l'écriture à la fin de la page. On recalcule pour
	// cela le nombre d'octets à écrire de façon à s'arrêter à la fin
	// de la page. C'est pour la même raison que la fonction s'appelle
	// eepromPageWrite() et non eepromWriteBlock ou eepromSequentialWrite,
	// car ces derniers noms ne font pas référence à la limite de page.
	uint16_t pageAddress = startAddress >> EEPROM_PAGE_BITS;

	if (pageAddress != ((startAddress + count - 1) >> EEPROM_PAGE_BITS)) {
		count = ((pageAddress + 1) << EEPROM_PAGE_BITS) - startAddress;
	}

	// On s'assure que le bus I2C soit disponible.
	while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) != RESET);

	// On génère la condition de start de la transaction
	I2C_GenerateSTART(I2C1, ENABLE);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

	// On envoie l'adresse de l'EEPROM et on signale que c'est une
	// écriture (I2C_Direction_Transmitter).
	I2C_Send7bitAddress(I2C1, EEPROM_I2C_ADDR << 1, I2C_Direction_Transmitter);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

	// On envoie l'octet de poids fort de l'adresse de la donnée à écrire.
	I2C_SendData(I2C1, startAddress >> 8);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

	// On envoie l'octet de poids faible de l'adresse de la donnée à écrire.
	I2C_SendData(I2C1, startAddress);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));


	for (; count; count--) {
		// On envoie un octet.
		I2C_SendData(I2C1, *pageBuffer);
		while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));
		// Et on passe au suivant.
		pageBuffer++;
	}

	// On génère la condition de stop de la transaction.
	I2C_GenerateSTOP(I2C1, ENABLE);
}

// -- IMPORTANT
// Reportez-vous à la figure 12 page 12 de la data sheet de
// la 24C256 et à son paragraphe "SEQUENTIAL READ" page 11
// pour suivre les explications des commentaires.
void eepromSequentialRead(uint16_t startAddress, uint8_t *pageBuffer, uint16_t count) {
	// Si une lecture séquentielle dépasse la fin de l'EEPROM,
	// elle continue en repartant du premier octet (adresse 0).
	// Pour avoir un comportement moins surprenant (cf.
	// https://fr.wikipedia.org/wiki/Principe_de_moindre_surprise),
	// on va limiter l'écriture à la fin de la mémoire. On recalcule
	// pour cela le nombre d'octets à lire de façon à ne plus déborder.

	if ((startAddress + count) >= EEPROM_SIZE) {
		count = EEPROM_SIZE - startAddress;
	}

	// On s'assure que le bus I2C soit disponible.
	while (I2C_GetFlagStatus(I2C1, I2C_FLAG_BUSY) != RESET);

	// On génère la première condition de start, celle qui précède
	// l'envoi de l'adresse de la donnée à lire.
	I2C_GenerateSTART(I2C1, ENABLE);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

	// On envoie l'adresse de l'EEPROM et on signale que c'est une
	// écriture (I2C_Direction_Transmitter).
	I2C_Send7bitAddress(I2C1, EEPROM_I2C_ADDR << 1, I2C_Direction_Transmitter);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_TRANSMITTER_MODE_SELECTED));

	// On envoie l'octet de poids fort de l'adresse de la donnée à lire.
	I2C_SendData(I2C1, startAddress >> 8);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

	// On envoie l'octet de poids faible de l'adresse de la donnée à lire.
	I2C_SendData(I2C1, startAddress);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_BYTE_TRANSMITTED));

	// On génère la seconde condition de start, celle qui précède
	// la réception des données à lire.
	I2C_GenerateSTART(I2C1, ENABLE);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_MODE_SELECT));

	// On envoie l'adresse de l'EEPROM et on signale que c'est une
	// lecture (I2C_Direction_Receiver).
	I2C_Send7bitAddress(I2C1, EEPROM_I2C_ADDR << 1, I2C_Direction_Receiver);
	while (!I2C_CheckEvent(I2C1, I2C_EVENT_MASTER_RECEIVER_MODE_SELECTED));

	// On reçoit normalement (avec un ACK) les (count - 1) premiers octets.
	for (count--; count; count--) {
		// On attend que la réponse soit arrivée.
		while (I2C_GetFlagStatus(I2C1, I2C_FLAG_RXNE) == RESET);
		// On stocke l'octet reçu.
		*pageBuffer = I2C_ReceiveData(I2C1);
		// Et on passe au suivant.
		pageBuffer++;
	}

	// On attend que le dernier octet soit arrivé.
	while (I2C_GetFlagStatus(I2C1, I2C_FLAG_RXNE) == RESET);
	// On envoie un NAK (voir figure 12 page 12 de la data sheet de la 24C256).
	I2C_AcknowledgeConfig(I2C1, DISABLE);
	// On stocke l'octet reçu.
	*pageBuffer = I2C_ReceiveData(I2C1);
	// On reconfigure un ACK par défaut pour les transactions suivantes.
	I2C_AcknowledgeConfig(I2C1, ENABLE);

	// On génère la condition de stop de la transaction.
	I2C_GenerateSTOP(I2C1, ENABLE);
}

int main() {
	USART_Printf_Init(115200);

	// Les interruptions externes et le port I2C sont des alternate functions.
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

	// -- On configure les lignes de GPIO.
	GPIO_InitTypeDef gpioInit = { 0 };
	// PD2 = bouton poussoir
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOD, ENABLE);
	gpioInit.GPIO_Pin = GPIO_Pin_2;
	gpioInit.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOD, &gpioInit);
	// PC1 = SDA
	// PC2 = SCL
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);
	gpioInit.GPIO_Pin = GPIO_Pin_1 | GPIO_Pin_2;
	// Attention au mode, I2C est une alternate function
	gpioInit.GPIO_Mode = GPIO_Mode_AF_OD;
	gpioInit.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &gpioInit);

	// -- On configure l'interruption du bouton poussoir.
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOD, GPIO_PinSource2);

	EXTI_InitTypeDef extiInit = { 0 };
	extiInit.EXTI_Line = EXTI_Line2;
	extiInit.EXTI_Mode = EXTI_Mode_Interrupt;
	extiInit.EXTI_Trigger = EXTI_Trigger_Falling;
	extiInit.EXTI_LineCmd = ENABLE;
	EXTI_Init(&extiInit);

	NVIC_InitTypeDef nvicInit = { 0 };
	nvicInit.NVIC_IRQChannel = EXTI7_0_IRQn;
	nvicInit.NVIC_IRQChannelPreemptionPriority = 0;
	nvicInit.NVIC_IRQChannelSubPriority = 1;
	nvicInit.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&nvicInit);

	// -- On configure et active l'interface I2C.
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_I2C1, ENABLE);
	I2C_InitTypeDef i2cInit = { 0 };
	i2cInit.I2C_ClockSpeed = I2C_CLOCK_FREQ;
	i2cInit.I2C_Mode = I2C_Mode_I2C;
	i2cInit.I2C_DutyCycle = I2C_DutyCycle_2;
	// Dans le protocole I2C, l'adresse commence au 2ème bit (le bit
	// de poids le plus faible indique si on lit ou écrit l'esclave)
	// donc il faut penser à la décaler d'une position à gauche.
	i2cInit.I2C_OwnAddress1 = I2C_HOST_ADDR << 1;
	i2cInit.I2C_Ack = I2C_Ack_Enable;
	i2cInit.I2C_AcknowledgedAddress = I2C_AcknowledgedAddress_7bit;
	I2C_Init(I2C1, &i2cInit);
	I2C_Cmd(I2C1, ENABLE);

	// On utilisera toujours la même adresse dans cet exercice.
	uint16_t dataAddress = 0;
	uint8_t testBuffer[16];

	while (1) {
		// A chaque fois qu'on appuie sur le bouton poussoir,
		if (buttonPressed) {
			buttonPressed = false;
			// on lit le contenu de l'EEPROM,
			eepromSequentialRead(dataAddress, testBuffer, sizeof(testBuffer));

			printf("buffer:");

			for (int i = 0; i < sizeof(testBuffer); i++) {
				// on l'affiche,
				printf(" %02x", testBuffer[i]);
				// on l'incrémente non uniformément pour le fun,
				testBuffer[i] += i + 1;
			}

			printf("\r\n");

			// et on écrit les nouvelles valeurs.
			eepromPageWrite(dataAddress, testBuffer, sizeof(testBuffer));
		}
	}
}

Vous pourrez constater que le déroulé de ces opérations avec le paramètre count = 1 est exactement le même que dans les versions mono-octet. Nous avons donc gagné en généricité avec cette nouvelle version - qui peut le plus peut le moins.

Ce que nous avons appris

Vous savez maintenant :

  • Comment fonctionne le protocole I2C.

  • Ce qu'est une EEPROM et à quoi elle sert.

  • Analyser la documentation d'un périphérique I2C pour coder les transactions qu'elle décrit.

  • Lire et écrire des données dans une EEPROM.


Copyright (c) 2024 Vincent DEFERT - Tous droits réservés