CH32V003 : Protocole SPI et matrice de LED

Ce que nous allons faire - Prérequis

Nous allons voir dans ce cours :

  • ce qu'est le protocole SPI, comment il fonctionne et à quoi il sert

  • comment utiliser un périphérique SPI simple, le MAX7219 intégré à une matrice de LED

Les prérequis de ce cours sont :

  • Avoir suivi le cours CH32V003 : premiers pas.

  • Disposer d'un module d'affichage à matrice de LED 8x8 basé sur le MAX7219 tel que recommandé dans les pré-requis.

Présentation de SPI

Un périphérique SPI est un dispositif de communication série synchrone full-duplex, c'est-à-dire dans laquelle les bits des données sont transférés un par un à la suite, au rythme d'une horloge commune à l'émetteur et au récepteur, et dans laquelle émission et réception ont lieu simultanément.

Avec SPI, un maître (le micro-contrôleur) communique avec un esclave (ex. un module d'affichage, un capteur de température à thermocouple, ou encore un autre micro-contrôleur). Le protocole SPI fonctionne à l'aide de 4 signaux :

  • SCLK (Serial ClocK) est le signal d'horloge. Les impulsions ne sont émises que pendant la transmission. Entre 2 transmissions, la ligne est à un niveau fixe configurable appelé "polarité".

  • MOSI (Master Out Slave In) est la ligne de transmission du maître vers l'esclave.

  • MISO (Master In Slave Out) est la ligne de transmission de l'esclave vers le maître. Si l'esclave ne fait que recevoir, elle est inutilisée et les données aléatoires reçues sont simplement ignorées (on peut aussi désactiver le registre de réception du maître).

  • NSS (Not Slave Select = sélection d'esclave active à l'état bas), parfois appelé /CS ou CS# (Not Chip Select). Ce signal indique à l'esclave que le maître communique avec lui et doit rester à l'état bas pendant toute la communication. Si plusieurs esclaves sont reliés au même maître, il doit y avoir un signal NSS distinct pour chaque esclave, les autres lignes étant communes.

Un périphérique SPI est très configurable, comme nous le verrons bientôt. Outre les caractéristiques de l'horloge, on peut choisir si les données sont transmises en commençant par le bit de poids le plus fort (MSB - Most Significant Bit - first) ou par celui de poids le plus faible (LSB - Least Significant Bit - first).

On peut en outre transmettre des mots de plusieurs octets (ex. 16 ou 24 bits), il suffit de les envoyer les uns après les autres en laissant NSS à l'état bas pendant tout ce temps.

Voici comment se déroule concrètement une communication SPI dans la configuration la plus courante, le mode 0 :

Deux paramètres de configuration de l'horloge définissent les différents modes de fonctionnement de SPI, la polarité (notée CPOL), c'est-à-dire le niveau logique de repos de l'horloge, et la phase (notée CPHA), qui définit si l'esclave échantillonne l'état de MOSI sur le premier (leading edge) ou le second (trailing edge) front du signal d'horloge. Ça donne les 4 combinaisons suivantes :

Comparaison SPI / I2C / UART

Comme I2C, SPI est limité à de très courtes distances, normalement le circuit imprimé où se trouve le micro-contrôleur. UART peut quant à lui être utilisé sur une distance de plus d'un kilomètre en utilisant une interface RS-485 et un taux de transmission réduit de 9600 Bd.

SPI a besoin de 4 signaux alors que I2C et UART n'en utilisent que 2.

SPI peut atteindre des vitesses très élevées. Si le circuit imprimé est bien conçu et le micro-contrôleur suffisamment rapide, rien n'empêche d'utiliser une horloge de 100 MHz ou plus. I2C est la plupart du temps limité à 100 ou 400 kHz et UART peut atteindre au mieux quelques MBd avec une liaison très courte.

SPI et UART fonctionnent en full-duplex, I2C en half-duplex.

SPI et UART n'ont aucun moyen de savoir si un esclave est connecté, sauf à le gérer au niveau logiciel au moyen d'une convention avec l'esclave. I2C intègre des bits d'acquittement qui permettent de savoir si l'esclave a reçu la donnée.

UART (protocole multi-processeur à 9 bits) et I2C utilisent des adresses pour sélectionner le destinataire de la communication. SPI doit utiliser une ligne de sélection (NSS) par esclave connecté.

La gestion de la communication est complexe avec I2C, très simple avec SPI (simple registre à décalage), intermédiaire avec UART.

Avec SPI et I2C, le maître fournit un signal d'horloge à l'esclave. Avec UART, les deux doivent être configurés individuellement pour utiliser le même taux de transmission.

UART et I2C transfèrent des octets, SPI peut transférer des mots d'une taille multiple de 8 bits.

Le module matrice de LED 8x8

Le module matrice de LED se compose de 2 éléments, la matrice de LED proprement dite comportant 8 rangées de 8 LED, et un circuit intégré pour le piloter, le MAX7219. Ce module est conçu pour pouvoir être assemblé en cascade à d'autres pour former des matrices plus longue, par exemple 8x128 LED, un peu comme les affichages à l'arrière des véhicules d'urgence qui interviennent sur les autoroutes, ou les affichages à LED de certains commerces ou panneaux d'informations municipales. Dans cet exercice, nous n'utiliserons qu'un seul module, mais vous devrez faire attention à vous connecter au pin header marqué "->IN".

Le MAX7219 est normalement prévu pour piloter des affichages à LED comportant un maximum de 8 chiffres à 7 segments comme celui ci-dessous. Or, un afficheur 7 segments comporte 8 LED (les 7 segments formant le chiffre, nommés de A à G, plus le point décimal, nommé DP), donc il convient naturellement pour notre matrice 8x8.

Vous trouverez la data sheet du MAX7219 ici et nous allons parcourir ensemble ses principaux éléments, à commencer par son diagramme fonctionnel (page 5) reproduit ci-dessous.

On y trouve un registre à décalage de 16 bits alimenté par DIN et CLK, et on voit que les bits 8 à 11 du registre forment l'adresse d'une petite mémoire interne de 16 octets dont 8 correspondent à l'état des segments de chaque chiffre et 5 à des registres de configuration du fonctionnement du MAX7219. On voit également qu'une petite ROM permet de traduire le contenu des registres de segments en chiffres, mais qu'elle peut être mise hors circuit (by-pass) pour commander les segments directement.

En haut de la page 6, on trouve les chronogrammes (timing diagram) décrivant la communication entre le micro-contrôleur et le MAX7219, dont nous allons pouvoir déduire les paramètres de configuration du périphérique SPI du CH32V003.

Toujours page 6, la figure "Serial-Data Format (16 Bits)" nous confirme ce que nous avions vu du registre à décalage du MAX7219. Page 7, nous trouvons la liste des registres du MAX7219 et leurs adresses, suivie de la description des différents registres de configuration. Enfin, en bas de la page 8, les figures "No-Decode Mode Data Bits and Corresponding Segment Lines" nous montrent comment les segments sont représentés dans les registres correspondants.

Enfin, pour pouvoir déterminer quelles valeurs nous devons envoyer au MAX7219 pour commander nos LED, nous avons besoin de savoir comment la matrice de LED est reliée au MAX7219 :

Le code

Nous avons maintenant toutes les informations nécessaires pour écrire un petit programme qui va faire défiler quelques lettres sur notre matrice de LED.

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>

/**
 * Adresses des registres du MAX7219.
 *
 * Remarque : les adresses 13 et 14 ne sont pas utilisées.
 */
typedef enum {
	MAX7219_NoOp = 0,        /**< Utilisé lorsque plusieurs MAX7219 sont chaînés. */
	MAX7219_Digit0,          /**< Segments du digit 0. */
	MAX7219_Digit1,          /**< Segments du digit 1. */
	MAX7219_Digit2,          /**< Segments du digit 2. */
	MAX7219_Digit3,          /**< Segments du digit 3. */
	MAX7219_Digit4,          /**< Segments du digit 4. */
	MAX7219_Digit5,          /**< Segments du digit 5. */
	MAX7219_Digit6,          /**< Segments du digit 6. */
	MAX7219_Digit7,          /**< Segments du digit 7. */
	MAX7219_Decode,          /**< Configuration du décodage. Utile seulement avec des afficheurs 7 segments. */
	MAX7219_Intensity,       /**< Intensité de l'affichage, de 0 à 15. */
	MAX7219_ScanLimit,       /**< Nombre de digits utilisés - 1, de 0 à 7. */
	MAX7219_Shutdown,        /**< Mode de fonctionnement : shutdown ou normal. */
	MAX7219_DisplayTest = 15,/**< Test de l'affichage. Allume tous les segments de tous les digits. */
} MAX7219Register;

#define MATRIX_WIDTH 8

// Matrice complètement éteinte
const uint8_t ALL_OFF[] = {
	0, 0, 0, 0, 0, 0, 0, 0,
};

// Matrice complètement allumée
const uint8_t ALL_ON[] = {
	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
};

// Lettre A majuscule
const uint8_t LETTER_A[] = {
	0x41, 0x41, 0x41, 0x3e, 0x22, 0x14, 0x14, 0x08,
};

// Lettre B majuscule
const uint8_t LETTER_B[] = {
	0x3f, 0x41, 0x41, 0x41, 0x3f, 0x11, 0x11, 0x0f,
};

// Lettre C majuscule
const uint8_t LETTER_C[] = {
	0x3e, 0x41, 0x01, 0x01, 0x01, 0x01, 0x41, 0x3e,
};

/**
 * Ecrit un octet dans un registre du MAX7219.
 *
 * @param reg Adresse du registre.
 * @param data Valeur à écrire dans le registre.
 */
void writeByte(MAX7219Register reg, uint8_t data) {
	// On met CS# à l'état bas (actif).
	GPIO_WriteBit(GPIOC, GPIO_Pin_4, Bit_RESET);

	// On envoie l'adresse du registre.
	SPI_I2S_SendData(SPI1, reg);

	// On attend que le périphérique SPI soit prêt à envoyer un autre octet.
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);

	// On envoie la valeur du registre.
	SPI_I2S_SendData(SPI1, data);

	// On attend la fin de la transmission. Indispensable car CS# 
	// DOIT rester à l'état bas pendant TOUTE la communication.
	while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_BSY) != RESET);

	// On remet CS# à l'état haut (inactif).
	// C'est à ce moment que le MAX7219 charge la valeur dans son registre.
	GPIO_WriteBit(GPIOC, GPIO_Pin_4, Bit_SET);
}

/**
 * Ecrit une image sur la matrice gérée par le MAX7219.
 *
 * @param image Tableau de 8 octets décrivant l'image.
 */
void writeImage(const uint8_t *image) {
	for (int i = 0; i < MATRIX_WIDTH; i++) {
		writeByte(MAX7219_Digit0 + i, image[i]);
	}
}

int main() {
	// == Initialisation des fonctions de Delay_Ms() et Delay_Us().
	SystemCoreClockUpdate();
	Delay_Init();

	// == Initialisation du GPIO.
	GPIO_InitTypeDef gpioInit = { 0 };

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

	// On n'a pas besoin de MISO (le MAX7219 n'envoie pas de données au CH32V003)
	//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);

	// == Initialisation du périphérique SPI.
	SPI_InitTypeDef spiInit = { 0 };

	spiInit.SPI_Mode = SPI_Mode_Master;
	spiInit.SPI_DataSize = SPI_DataSize_8b;
	// Nous n'avons pas besoin de la lecture avec le MAX7219, mais en mode
	// half-duplex, SPI_I2S_FLAG_BSY n'est pas mis à jour correctement,
	// donc on doit utiliser le mode full-duplex.
	spiInit.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
	// Le MAX7219 fonctionne en mode SPI 0 (CPOL = 0, CPHA = 0), 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);

	// == Configuration du MAX7219
	writeByte(MAX7219_Decode, 0x00);      // Pas de décodage, on veut commander les segments directement.
	writeByte(MAX7219_Intensity, 0x07);   // Intensité moyenne (7 / 15).
	writeByte(MAX7219_ScanLimit, 0x07);   // On utilise les 8 digits.
	writeByte(MAX7219_Shutdown, 0x01);    // Mode normal (au lieu de shutdown).
	writeByte(MAX7219_DisplayTest, 0x00); // Mode normal (au lieu de test de l'affichage).

	while (1) {
		// On fait défiler les différents caractères.
		writeImage(LETTER_A);
		Delay_Ms(1000);
		writeImage(LETTER_B);
		Delay_Ms(1000);
		writeImage(LETTER_C);
		Delay_Ms(1000);
		writeImage(ALL_ON);
		Delay_Ms(1000);
		writeImage(ALL_OFF);
		Delay_Ms(1000);
	}
}

Ce que nous avons appris

Vous savez maintenant :

  • Le principe de fonctionnement de SPI.

  • Quelles sont les différences entre SPI, I2C et UART.

  • Comment utiliser le périphérique SPI du CH32V003.

  • Comment utiliser le MAX7219 pour piloter une matrice de LED 8x8.


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