CH32V003 : Protocole SPI et mémoire flash

Ce que nous allons faire - Prérequis

Nous allons voir dans ce cours :

  • ce qu'est une mémoire flash

  • comment utiliser une mémoire flash SPI

Les prérequis de ce cours sont :

Les mémoires flash

Il y a 2 technologies de mémoire flash : NAND flash, utilisée dans les disques durs, et NOR flash, utilisée pour stocker le firmware des micro-contrôleurs. La W25Q32 que nous allons utiliser ici est une NOR flash et nous ne parlerons ici que de cette technologie lorsque nous utiliserons le terme "mémoire flash".

Nous avons déjà utilisé une autre mémoire non-volatile précédemment, l'EEPROM I2C 24C256, et vous pourrez constater que EEPROM et mémoire flash partagent beaucoup de caractéristiques communes : nombre de cycles d'écriture limité, durée de rétention limitée, modes de lecture et d'écriture.

Par contre, les mémoires flash sont beaucoup plus rapides (un ordre de grandeur) que les EEPROM, à tel point que beaucoup de mémoires flash permettent d'exécuter directement le code qu'elles contiennent (XIP, eXecute In Place).

La capacité des mémoires flash est aussi beaucoup plus importante que celle des EEPROM : de 2 à 256Mo pour une mémoire flash, et de 16 octets à 128Ko pour une EEPROM. Du fait de leur faible capacité, les EEPROM utilisent généralement le protocole I2C, dont la faible vitesse (100 ou 400kHz) n'est pas un problème. Par contre, la capacité des mémoires flash exige un protocole plus rapide, donc SPI ou ses évolutions Dual-SPI (transmission des bits 2 par 2, au lieu de 1 par 1 avec SPI) ou Quad-SPI (transmission des bits 4 par 4).

Une autre différence est qu'une mémoire flash doit être effacée avant de procéder à l'écriture, ce dont nous étions dispensés avec notre EEPROM. La granularité d'écriture est différente de celle de l'effacement. Dans le cas de la W25Q32, on peut écrire des blocs d'un maximum de 256 octets alors que l'effacement se fait par blocs de 4Ko, 32Ko, 64Ko, ou l'ensemble de la mémoire flash.

En général, les EEPROM sont utilisées pour stocker des données de configuration et les mémoires flash soit du code, soit des données volumineuses, comme par exemple les mesures d'un système d'acquisition de données. On utilise aussi des mémoires flash de 128 ou 256Mo sur certains SBC (Single Board Computer, ordinateur monocarte, ex. le célèbre Raspberry Pi) pour stocker le chargeur du système d'exploitation (le fameux U-Boot).

Le matériel

Notre module mémoire flash est équipé d'une W25Q32 dont vous trouverez la data sheet ici, dont la capacité est de 4Mo. Elle peut fonctionner en mode SPI, dual-SPI ou quand-SPI, mais le CH32V003 ne permet d'utiliser que SPI, nous utiliserons ce mode.

La W25Q32 a une tension d'alimentation de 3.3V. Vous devrez donc prendre garde à connecter votre carte de développement à la broche 3.3V du WCH-LinkE (au lieu de 5V) afin de ne pas l'endommager. Le câblage devra être effectué de la manière suivante :

Je vous invite à prendre connaissance des sections suivantes de la data sheet de la W25Q32, qui décrivent les commandes dont nous aurons besoin pour utiliser notre mémoire flash :

  • 7.2.6 Read Data (03h), p. 28

  • 7.2.1 Write Enable (06h), p.24

  • 7.2.15 Sector Erase (20h) p. 37
    7.2.16 32KB Block Erase (52h), p. 38
    7.2.17 64KB Block Erase (D8h), p. 39
    7.2.18 Chip Erase (C7h / 60h), p. 40

  • 6.1.1 Erase/Write In Progress (BUSY), p. 13
    7.2.4 Read Status Register-1 (05h), p. 25

  • 7.2.13 Page Program (02h), p.35

Remarquez en particulier sur les chronogrammes qu'on fonctionne soit en mode SPI 0, soit en mode SPI 3, MSB en premier, et que le signal CS# doit rester à l'état bas pendant toute la durée d'exécution de chaque commande et revenir à l'état haut entre 2 commandes successives.

Le code

Vous avez maintenant toutes les informations nécessaires pour écrire un petit programme de test des fonctions d'effacement, de lecture et d'écriture de mémoire flash, programme qui enverra le résultat de chaque étape des tests sur l'UART du CH32V003, à visualiser dans une vue Terminal comme nous l'avons déjà fait. Notez que pour afficher correctement les caractères accentués, il faut choisir UTF-8 comme encodage lors de la connexion de la vue Terminal au CH32V003.

Créez un nouveau projet et modifiez system_ch32v00x.c comme vous en avez maintenant l'habitude, puis remplacez le contenu du fichier main.c par le code suivant :

#include <debug.h>
// Pour memset().
#include <string.h>

/*
 * Les fonctions ci-dessous fournissent une abstraction de la communication SPI.
 * =============================================================================
 *
 * L'intérêt de ce découpage est qu'en cas de portage sur un autre
 * micro-contrôleur, seules ces fonctions devront être adaptées,
 * les fonctions implémentant les commandes de la mémoire flash
 * restant inchangées, a fortiori le code de l'application qui les
 * utilise.
 */

void configureSPI() {
    // Initialisation du GPIO.
	GPIO_InitTypeDef gpioInit = { 0 };

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

	// On configure MISO
	gpioInit.GPIO_Pin = GPIO_Pin_7;
	gpioInit.GPIO_Mode = GPIO_Mode_IN_FLOATING;
	GPIO_Init(GPIOC, &gpioInit);

	// On configure SCLK et MOSI
	gpioInit.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6;
	gpioInit.GPIO_Mode = GPIO_Mode_AF_PP;
	gpioInit.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &gpioInit);

	// On configure CS#
	GPIO_WriteBit(GPIOC, GPIO_Pin_4, Bit_SET);
	gpioInit.GPIO_Pin = GPIO_Pin_4;
	gpioInit.GPIO_Mode = GPIO_Mode_Out_PP;
	GPIO_Init(GPIOC, &gpioInit);

	SPI_InitTypeDef  spiInit = { 0 };

	spiInit.SPI_Mode = SPI_Mode_Master;
	spiInit.SPI_DataSize = SPI_DataSize_8b;
	spiInit.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
	// La W25Q32 fonctionne en mode SPI 0 ou 3, MSB en premier.
	spiInit.SPI_CPOL = SPI_CPOL_Low;
	spiInit.SPI_CPHA = SPI_CPHA_1Edge;
	spiInit.SPI_FirstBit = SPI_FirstBit_MSB;
	// Correspond à 48MHz / 64 = 750 kHz, ce qui est raisonnable avec un câblage volant.
	spiInit.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_64;
	// On veut gérer CS# nous-mêmes.
	spiInit.SPI_NSS = SPI_NSS_Soft;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
	SPI_Init(SPI1, &spiInit);
	SPI_Cmd(SPI1, ENABLE);
}

void beginTransaction() {
	// On met CS# à l'état bas (actif).
	GPIO_WriteBit(GPIOC, GPIO_Pin_4, Bit_RESET);
}

void endTransaction() {
	// On attend la fin de la transmission le cas échéant.
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) != RESET);

	// On remet CS# à l'état haut (inactif).
	GPIO_WriteBit(GPIOC, GPIO_Pin_4, Bit_SET);
}

void writeByte(uint8_t b) {
	// On attend que le périphérique SPI soit prêt à émettre
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);

	// On envoie l'octet.
	SPI_I2S_SendData(SPI1, b);
}

uint8_t readByte() {
	// On attend que le périphérique SPI soit prêt à émettre
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);

	// On envoie une valeur quelconque pour que l'esclave puisse envoyer son octet.
	SPI_I2S_SendData(SPI1, 0);

	// On attend la fin de la transmission.
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) != RESET);

	// Et on récupère l'octet reçu.
	return SPI_I2S_ReceiveData(SPI1);
}

/*
 * Les fonctions ci-dessous implémentent les commandes de la mémoire flash.
 * ========================================================================
 *
 * L'intérêt est que le code de l'application n'a pas besoin de s'occuper
 * des détails d'implémentation, il ne manipule que des opérations de
 * haut niveau, ce qui rend le code de l'application plus lisible et
 * facile à maintenir.
 *
 * Remarque : ce code fonctionne sans modification sur les mémoires flash
 * W25Q16JV, W25Q32JV, W25Q64JV et W25Q128JV.
 */

// L'intérêt d'utiliser un type pour les commandes de la mémoire flash
// est qu'il permet de documenter les arguments de la fonction qui les
// utilise (c'est plus parlant que uint8_t) et que l'IDE est capable
// d'optimiser la saisie des valeurs et la navigation dans les sources.
typedef enum {
	// data sheet p. 24
	CMD_WriteEnable = 0x06,
	// data sheet p. 25
	CMD_ReadStatus1 = 0x05,
	// L'intérêt d'utiliser "Fast read" plutôt que "Read data" est que
	// "Fast read" peut aller si besoin jusqu'à la fréquence d'horloge
	// maximale supportée par la mémoire flash, tout en fonctionnant
	// aussi bien que "Read data" aux fréquences d'horloge plus basses.
	// data sheet p. 29
	CMD_FastRead = 0x0b,
	// data sheet p. 35
	CMD_PageProgram = 0x02,
	// data sheet p. 37
	CMD_SectorErase = 0x20,
	// data sheet p. 38
	CMD_BlockErase_32k = 0x52,
	// data sheet p. 39
	CMD_BlockErase_64k = 0xd8,
	// data sheet p. 40
	CMD_ChipErase = 0xc7,
} W25Q32Command;

void sendCommand(W25Q32Command cmd) {
	writeByte(cmd);
}

void sendCommandAndAddress(W25Q32Command cmd, uint32_t addr) {
	writeByte(cmd);
	// Les adresses de la mémoire flash font 24 bits.
	writeByte(addr >> 16);
	writeByte(addr >> 8);
	writeByte(addr);
}

/**
 * Attend la fin de l'opération d'effacement ou d'écriture en cours.
 */
void flashBusyWait() {
	// On lit l'octet de poids faible du registre d'état.
	beginTransaction();
	sendCommand(CMD_ReadStatus1);
	uint8_t status;

	// Le registre d'état peut être lu en continu
	// (data sheet p. 25 "7.2.4 Read status register").
	do {
		status = readByte();

		// Le bit 0 du registre d'état est à 1 quand
		// un effacement ou une écriture est en cours
		// (data sheet p.13 "6.1.1 Erase/Write in progress").
	} while (status & 1);

	endTransaction();
}

/**
 * Efface un bloc d'octets dans la mémoire flash.
 *
 * IMPORTANT : par souci de simplicité, on suppose que l'adresse
 * de destination est alignée sur une frontière de bloc.
 *
 * @param count Nombre d'octets à effacer.
 * @param addr Adresse de destination dans la mémoire flash.
 */
void flashErase(uint32_t count, uint32_t addr) {
	beginTransaction();
	// Avant tout effacement, il faut envoyer CMD_WriteEnable.
	sendCommand(CMD_WriteEnable);
	endTransaction();

	// Ensuite, on efface ce qui a besoin de l'être.
	beginTransaction();

	if (count <= 4096) {
		sendCommandAndAddress(CMD_SectorErase, addr);
	} else if (count <= 32768) {
		sendCommandAndAddress(CMD_BlockErase_32k, addr);
	} else if (count <= 65536) {
		sendCommandAndAddress(CMD_BlockErase_64k, addr);
	} else {
		// Ceci est un exemple. Selon l'utilisation qui est faite
		// de la mémoire flash, il pourrait être plus pertinent
		// de combiner différents effacements de blocs afin de
		// conserver le contenu de la fin de la mémoire flash si
		// l'application l'exigeait.
		sendCommand(CMD_ChipErase);
	}

	endTransaction();

	// On attend que l'effacement soit terminé.
	flashBusyWait();
}

/**
 * Programme un bloc d'octets dans la mémoire flash. Il faut appeler
 * flashErase() avant d'utiliser flashProgram().
 *
 * IMPORTANT : par souci de simplicité, on suppose que l'adresse
 * de destination est alignée sur une frontière de bloc.
 *
 * @param buffer Tableau d'octets à programmer.
 * @param count Nombre d'octets à programmer.
 * @param addr Adresse de destination dans la mémoire flash.
 */
void flashProgram(const uint8_t *buffer, uint32_t count, uint32_t addr) {
	uint32_t offset = 0;

	while (count > 0) {
		// CMD_PageProgram ne peut pas écrire plus de 256 octets à la fois.
		uint32_t n = (count > 256) ? 256 : count;

		// Avant toute écriture, il faut envoyer CMD_WriteEnable.
		beginTransaction();
		sendCommand(CMD_WriteEnable);
		endTransaction();

		// On envoie la commande de programmation de page.
		beginTransaction();
		sendCommandAndAddress(CMD_PageProgram, addr);

		for (uint32_t i = 0; i < n; i++) {
			writeByte(buffer[offset++]);
		}

		endTransaction();

		// On attend que l'écriture soit terminée.
		flashBusyWait();

		addr += n;
		count -= n;
	}
}

/**
 * Lit un bloc d'octets de la mémoire flash.
 *
 * @param buffer Tampon de destination.
 * @param count Nombre d'octets à lire.
 * @param addr Adresse source dans la mémoire flash.
 */
void flashRead(uint8_t *buffer, uint32_t count, uint32_t addr) {
	// On envoie la commande de lecture.
	beginTransaction();
	sendCommandAndAddress(CMD_FastRead, addr);

	// On envoie un octet bidon pour laisser à la mémoire flash
	// le temps de se préparer et on attend que le transfert
	// soit complètement terminé. Voir data sheet p. 29.
	readByte();

	// On lit les octets un par un.
	for (uint32_t i = 0; i < count; i++) {
		buffer[i] = readByte();
	}

	endTransaction();
}

/*
 * Utilitaire d'affichage hexadécimal.
 * ===================================
 */
void dump(const uint8_t *buffer, uint32_t count) {
	for (uint32_t i = 0; i < count; i++) {
		if ((i % 16) == 0) {
			printf("\r\n%04x:", (int) i);
		}

		printf(" %02x", (int) buffer[i]);
	}

	printf("\r\n");
}

int main() {
	// Nécessaire pour le calcul des délais.
	SystemCoreClockUpdate();
	Delay_Init();

	// Nécessaire pour pouvoir utiliser printf().
	USART_Printf_Init(115200);

	// On configure le périphérique SPI et les lignes de GPIO.
	configureSPI();

	while (1) {
		uint8_t buffer[256];

		// On efface la mémoire flash.
		printf("Effacement de la mémoire flash.\r\n");
		flashErase(sizeof(buffer), 0);

		// On relit le contenu de la mémoire flash.
		flashRead(buffer, sizeof(buffer), 0);

		// On affiche le résultat.
		printf("Contenu de la mémoire :");
		dump(buffer, sizeof(buffer));
		Delay_Ms(2000);

		// On initialise le buffer avec les données à programmer.
		uint8_t data = 0;

		for (uint32_t i = 0; i < sizeof(buffer); i++) {
			buffer[i] = data++;
		}

		// On programme la mémoire flash.
		printf("Programmation de la mémoire flash.\r\n");
		flashProgram(buffer, sizeof(buffer), 0);

		// On initialise le buffer à 0 pour effacer les anciennes valeurs,
		// ce qui prouvera qu'on a reçu correctement les nouvelles.
		memset(buffer, 0, sizeof(buffer));

		// On relit le contenu de la mémoire flash.
		flashRead(buffer, sizeof(buffer), 0);

		// On affiche le résultat.
		printf("Contenu de la mémoire :");
		dump(buffer, sizeof(buffer));
		Delay_Ms(2000);
	}
}

Notez que ce code peut être utilisé sans modification avec plusieurs modèles de mémoire flash du fabricant Winbond (W25Q16JV, W25Q32JV, W25Q64JV et W25Q128JV), et moyennant des modifications mineures avec d'autres modèles.

Ce que nous avons appris

Vous savez maintenant :

  • Ce qu'est une mémoire flash.

  • Quelles sont les différences entre EEPROM et mémoire flash.

  • Comment utiliser une mémoire flash SPI avec le CH32V003.


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