CH32V003 : informations système

Ce que nous allons faire - Prérequis

Nous allons voir comment obtenir des informations sur le micro-contrôleur (modèle, taille de mémoire flash, identifiant unique).

Les prérequis de ce cours sont :

Modèle de micro-contrôleur

Il arrive parfois que des versions successives d'un même appareil utilisent des micro-contrôleurs différents d'une même gamme. Dans ce cas, on souhaite généralement par commodité ne gérer qu'une seule version du firmware, notamment pour faciliter les mises à jour. Les micro-contrôleurs étant très proches, il est possible de gérer les différences en détectant le modèle à l'utilisation.

Les SDK de tous les micro-contrôleurs des séries CH32 offrent une fonction DBGMCU_GetCHIPID() qui lit un emplacement mémoire contenant un code décrivant le modèle de micro-contrôleur. Le second quartet de ce code (celui à la position 161) n'est pas pertinent et doit donc être masqué. Il est possible qu'il soit réservé à la révision (stepping en anglais) du modèle de micro-contrôleur, le cas échéant, mais comme ce code n'est pas documenté, impossible d'en être sûr.

Dans cette feuille de calcul, j'ai répertorié pour chaque microcontrôleur CH32 disponible à ce jour l'adresse à laquelle se trouve le code en question et les différentes valeurs qu'il peut prendre (issues du fichier ch32v00x_dbgmcu.c du SDK, ou son équivalent pour les autres modèles).

On constate que la valeur de ce code commence par les 3 derniers chiffres de la référence (ex. 003 pour le CH32V003), à l'exception de quelques modèles, notamment les plus anciens (CH32F103 et CH32V103).

Identifiant unique et mémoire flash

Comme nous le verrons dans un autre cours, il est possible d'utiliser l'espace libre à la fin de la mémoire flash interne du micro-contrôleur pour stocker des données applicatives au lieu de recourir à une EEPROM ou une mémoire flash externes. Il peut donc être nécessaire de connaître à l'exécution la taille de la mémoire flash pour savoir où elle se termine.

Ce besoin peut découler du cas évoqué précédemment d'utilisation de différents modèles de micro-contrôleurs pour des versions d'un même appareil, ou encore parce que certains modèles (ex. CH32V307) permettent de configurer la répartition entre mémoire flash et RAM.

D'autre part, notre firmware peut avoir besoin d'identifier chaque appareil de manière unique, par exemple pour autoriser certaines fonctionnalités par un code d'activation (ex. sur les oscilloscopes). C'est pourquoi la plupart des micro-contrôleurs disposent d'un numéro d'identification unique inscrit à la fabrication.

Ces 2 informations, identifiant unique et taille de la mémoire flash, sont accessibles dans des registres décrits dans le manuel de référence du CH32V003, chapitre 15 Electronic Signature (ESIG).

Carte mémoire et memory-mapped I/O

Jusqu'à maintenant, nous avons toujours utilisé les fonctions du SDK de WCH pour manipuler les périphériques. Celles-ci présentent le gros avantage d'être plus explicites que de manipuler directement les registres décrits dans le manuel de référence tout en peu pénalisantes sur le plan de la taille du code et du temps d'exécution.

Vous avez peut-être eu la curiosité de "descendre" en faisant un Ctrl-clic gauche sur leur nom et vous avez vu des choses du genre SPIx->DATAR ou *(uint32_t *) 0x1FFFF7C4;. Nous allons maintenant voir en détail à quoi elles correspondent car nous allons avoir besoin de faire pareil. Il n'y a en effet aucune fonction de prévue pour accéder à la taille de la mémoire flash interne ou à l'identifiant unique du micro-contrôleur.

Pour commencer, je vous invite à vous reporter à la section 1.2 "Memory Map" de la data sheet du CH32V003, reproduite ci-dessous :

On y voit que le CH32V003 dispose d'un espace mémoire de 4 Go, du fait que les adresses qu'il manipule sont sur 32 bits. La carte mémoire ci-dessus décrit comment est utilisé cet espace mémoire.

On y voit tout d'abord, sur la figure centrale, qu'on trouve dans le même espace mémoire à la fois la mémoire flash, la RAM et les périphériques. On parle pour ces derniers de "memory-mapped I/O" car par le passé, certains micro-processeurs (ex. 8080, Z80, 8086) avaient des espaces d'adressage distinct pour la mémoire et les entrées-sorties. Les memory-mapped I/O ont l'avantage de simplifier le jeu d'instruction (pas besoin d'instructions spéciales pour les entrées-sorties) et les contrôleurs de DMA (un seul espace mémoire à gérer).

Sur la figure de gauche, on voit qu'il y a différents types de mémoire flash pour différents usages. Sur la figure de droite, on voit que chaque périphérique dispose d'un bloc d'adresses dédié. C'est dans ces blocs qu'on retrouve les registres décrits dans le manuel de référence.

On voit par exemple que le bloc mémoire correspondant au périphérique SPI est situé à l'adresse 0x40013000 et on trouve dans le manuel de référence du CH32V003 à la section 14.3 "Register Description" la liste des registres du périphérique et leurs adresses.

Pour accéder à un registre connaissant son adresse, il faut indiquer au compilateur que ce nombre correspond à une adresse pas un "cast" (forçage de type). Par exemple, si on veut accéder au registre DATAR du périphérique SPI, décrit dans le manuel sous le nom R16_SPI_DATAR pour signifier qu'il a une taille de 16 bits (R16_) et est relatif au périphérique SPI (SPI_), nous devrons écrire (*(volatile int16_t *) 0x4001300C). Le int16_t * indique que le nombre 0x4001300C est l'adresse d'un nombre de 16 bits et le * devant le case permet d'accéder à sa valeur.

Le mot-clé volatile est ici obligatoire puisque, par définition, le rôle d'un périphérique est de permettre des échanges de données et qu'il est donc impératif de ne PAS optimiser les accès et de bien faire la lecture ou l'écriture à chaque exécution. volatile n'est par contre pas utile pour accéder à une valeur constante comme la taille de la mémoire flash interne ou l'identifiant unique.

Utilisation de structures pour l'adressage

Lorsqu'un périphérique comporte plusieurs instances, comme le GPIO qui comporte plusieurs ports identiques à l'adresse de leurs registres près, définir des constantes est fastidieux et complexifie énormément le code. Il est beaucoup plus efficace de travailler avec des offsets à partir d'une adresse de base. C'est précisément ce que permettent les structures en C.

Par exemple, si vous voulez lire l'état des lignes de GPIO du port C, vous utiliserez GPIO_ReadInputData(GPIOC). Vous verrez que GPIOC est défini de la manière suivante dans ch32v00x.h :

#define GPIOC ((GPIO_TypeDef *) GPIOC_BASE)

Toujours dans ch32v00x.h, vous verrez que GPIOC_BASE est l'adresse de base du bloc de registres du port C et que GPIO_TypeDef est défini ainsi :

// Dans core_riscv.h :
#define __IO volatile

// Dans ch32v00x.h :
typedef struct {
                          // Position par rapport au début de la structure :
    __IO uint32_t CFGLR;  // + 0 octets
    uint32_t RESERVED0;   // + 4 octets
    __IO uint32_t INDR;   // + 8 octets
    __IO uint32_t OUTDR;  // + 12 octets
    __IO uint32_t BSHR;   // + 16 octets
    __IO uint32_t BCR;    // + 20 octets
    __IO uint32_t LCKR;   // + 24 octets
} GPIO_TypeDef;

Enfin, vous trouverez la définition de GPIO_ReadInputData() dans ch32v00x_gpio.c :

uint16_t GPIO_ReadInputData(GPIO_TypeDef *GPIOx) {
    return ((uint16_t) GPIOx->INDR);
}

Ainsi, lorsque vous écrivez GPIO_ReadInputData(GPIOC), vous récupérez simplement le demi-mot situé à l'adresse GPIOC_BASE + 8. Vous conviendrez que GPIOC->INDR est beaucoup plus facile à lire et à comprendre que *(uint32_t *) (GPIOC_BASE + 8)... :)

Code d'exemple

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

#include <debug.h>

// La capacité de la mémoire flash est exprimée en kilo-octets sur 16 bits.
#define R16_ESIG_FLACAP (*(uint16_t *) 0x1ffff7e0)

// L'identifiant unique est contenu dans un tableau de 3 mots de 32 bits.
#define R32_ESIG_UNIID ((uint32_t *) 0x1ffff7e8)

// Retourne la référence ("part number" en anglais) du micro-contrôleur.
const char *getPartNumber() {
	// Voir ch32v00x_dbgmcu.c pour la signification du ChipID.
	switch (DBGMCU_GetCHIPID() & 0xffffff0f) {
	case 0x00300500:
		return "CH32V003F4P6";
	case 0x00310500:
		return "CH32V003F4U6";
	case 0x00320500:
		return "CH32V003A4M6";
	case 0x00330500:
		return "CH32V003J4M6";
	}

	return "Unknown MCU";
}

int main() {
	SystemCoreClockUpdate();
	Delay_Init();
	USART_Printf_Init(115200);

	// On affiche en boucle les informations pour vous donner le temps
	// d'ouvrir une fenêtre Terminal pour les visualiser.
	while (1) {
		Delay_Ms(5000);
		printf("ChipID: %08x\r\n", DBGMCU_GetCHIPID());
		printf("Part number: %s\r\n", getPartNumber());
		printf("Flash capacity: %dK\r\n", R16_ESIG_FLACAP);
		printf("Unique ID: %08x%08x%08x\r\n", R32_ESIG_UNIID[2], R32_ESIG_UNIID[1], R32_ESIG_UNIID[0]);
	}
}

Et voici le résultat de son exécution sur 3 modèles de CH32V003 :

ChipID: 00300500
Part number: CH32V003F4P6
Flash capacity: 16K
Unique ID: ffffffff7fc4bc4817afabcd
ChipID: 00310500
Part number: CH32V003F4U6
Flash capacity: 16K
Unique ID: ffffffff078abc559f68abcd
ChipID: 00320500
Part number: CH32V003A4M6
Flash capacity: 16K
Unique ID: ffffffff522fbc68e9faabcd

On constate que le mot de poids le plus fort de l'identifiant n'est pas utilisé (il vaut toujours 0xffffffff) et que le demi-mot de poids le plus faible vaut toujours 0xabcd. En conséquence, on pourra se contenter des deux premiers mots pour identifier nos micro-contrôleurs.

Ce que nous avons appris

Vous savez maintenant :

  • Comment déterminer le modèle de votre micro-contrôleur, la taille de sa mémoire flash, ainsi que son identifiant unique.

  • A quoi peuvent servir ces informations.


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