CH32V003 : alimentation par batterie

Ce que nous allons faire - Prérequis

Nous allons voir dans ce cours quelles ressources offre le CH32V003 pour les applications alimentées par batterie ("battery-operated" ou "battery-powered" en anglais).

Les prérequis de ce cours sont :

Terminologie

Commençons par régler la question de la terminologie, plutôt torturée en français :

  • Une pile est un dispositif à usage unique qui produit de l'électricité à partir d'une réaction chimique. Une pile n'est donc par définition jamais rechargeable.

  • Un accumulateur est aussi un dispositif qui produit de l'électricité à partir d'une réaction chimique, mais celle-ci est réversible. Un accumulateur est donc par définition toujours rechargeable.

  • Une batterie est une association en série de plusieurs piles ou accumulateurs. Cette association peut être explicite, par exemple quand vous mettez plusieurs piles dans un appareil, ou être invisible à l'œil nu. C'est notamment le cas des batteries de voiture : un élément de batterie au plomb produit une tension d'environ 2V, il en faut donc 6 en série pour produire 12V.

L'expression "pile rechargeable", bien qu'impropre, s'est imposée dans le langage courant, ce qui montre bien que ces finesses de langage sont jugées excessives par le grand public. Les anglophones n'ont d'ailleurs qu'un seul mot, "battery", là où nous en avons trois. Lorsqu'ils ont besoin de faire la distinction, ils parlent de "primary battery" pour les piles et "secondary battery pour les accumulateurs. Pour la clarté des explications, je ferai comme eux et utiliserai le mot "batterie" lorsque la précision n'est pas nécessaire.

Besoins des applications alimentées par batterie

Pour vous donner un bref aperçu des caractéristiques des batteries, je vous ai fait une sélection de quelques data sheets :

  • Deux batteries non-rechargeables, une pile bouton ("coin cell" en anglais) CR2032 et une pile alcaline format AA.

  • Deux batteries rechargeables, une de technologie ("chemistry" en anglais) NiMH (Nickel Metal Hydride) et l'autre au plomb ("lead-acid" en anglais).

Les courbes figurant dans ces data sheets vous donneront une idée du comportement de ces batteries en charge et dans le temps. De notre point de vue de développeur embarqué, seules ces caractéristiques nous intéressent. Ce qu'il faut en retenir :

  • La quantité d'énergie emmagasinée dans une batterie - sa capacité, "capacity" en anglais, représentée par la lettre C et exprimée en Ah (Ampère heure) - est fixe. Ainsi, on épuisera une batterie de 900mAh en lui faisant débiter un courant de 900mA pendant 1h ou 9mA pendant 100h. Cela signifie que nous devons nous préoccuper de cette finitude dans la manière dont notre micro-contrôleur utilisera l'énergie disponible :

    1. En réduisant au maximum la consommation de l'appareil,

    2. En détectant la diminution de la charge de la batterie pour signaler à l'utilisateur qu'il est temps de la remplacer ou de la recharger,

    3. En mettant en œuvre toute technique appropriée pour éviter les pertes de données ou les instabilités de fonctionnement que pourrait causer une interruption de l'alimentation.

  • Lors de son utilisation, la tension aux bornes d'une batterie diminue d'abord faiblement, puis de manière brutale lorsqu'elle arrive en fin de charge, à cause de l'augmentation de sa résistance interne (IR, internal resistance) dans cette zone de la courbe. Cette courbe caractéristique peut être utilisée par notre application pour évaluer l'état de la batterie.

  • Selon la technologie de la batterie, la data sheet indique une valeur différente pour la tension aux bornes d'une batterie considérée comme complètement déchargée (ex. 2V pour une CR2032). Cette indication peut nous être utile pour notre évaluation de l'état de la batterie.

Notez que la consommation d'un appareil ne se réduit pas à celle du micro-contrôleur. Il ne sert pas à grand chose de l'optimiser si les autres composants consomment au total 10 fois plus que lui. C'est donc une démarche globale de conception qui permettra de tenir dans le budget énergétique disponible, souvent déterminé par des facteurs tels que poids et dimensions de l'appareil et durée d'utilisation.

Réduire la consommation : modes de fonctionnement

Vous avez sans doute remarqué que notre micro-contrôleur passe souvent du temps dans des boucles à attendre la survenue d'un événement. Il consomme donc de l'énergie sans qu'elle soit utilisée à produire quelque chose d'utile. C'est pourquoi les micro-contrôleurs offrent depuis très longtemps des modes de fonctionnement basse consommation ("low-power modes" en anglais). Le principe de la chose est d'arrêter le fonctionnement de certaines parties du micro-contrôleur - en stoppant les horloges qui cadencent ces parties - en attendant qu'une interruption se produise pour relancer le fonctionnement normal.

Je vous invite à vous reporter à la section 3.3.1 System Clock Structure du manuel de référence du CH32V003 et aux suivantes pour des détails sur les horloges du CH32V003. J'en reproduis ci-dessous la figure 3-2 pour mes explications.

Il y a 4 éléments importants sur ce diagramme :

  • L'oscillateur externe à quartz repéré "HSE OSC" sur la figure. Il n'y a pas de quartz sur notre carte de développement, donc il n'est pas utilisé dans notre cas, mais un quartz est nécessaire lorsque la fréquence de fonctionnement doit être précise, par exemple pour réaliser un radio-réveil.

  • L'oscillateur RC interne 24MHz repéré "HSI RC" sur la figure. C'est celui-ci que nous avons utilisé jusqu'à maintenant. Il est moins précis qu'un oscillateur à quartz mais nous a suffi jusqu'à maintenant.

  • La PLL (Phase Locked Loop) repérée "*2" sur la figure. Une PLL est un multiplicateur de fréquence, qui dans notre cas multiplie par 2, ce qui nous permet d'obtenir une horloge principale (repérée "SYSCLK" sur la figure) de 48MHz à partir de l'oscillateur interne à 24MHz.

  • L'oscillateur RC interne 128kHz repéré "LSI RC" sur la figure, que nous avons déjà utilisé avec le watchdog indépendant (IWDG). Les lois de la physique font que plus un changement est rapide et plus il nécessite d'énergie. Il est donc important de disposer d'un oscillateur "lent" pour réduire la consommation, et c'est ce LSI qui cadencera les parties actives du micro-contrôleur lorsqu'il sera en attente d'une interruption.

Arrêter l'horloge commandant un sous-système est un moyen de l'empêcher de changer d'état et donc de consommer de l'énergie. Les fonctions RCC_APB1PeriphClockCmd() et RCC_APB2PeriphClockCmd() que vous utilisez depuis le début permettent justement d'activer individuellement l'horloge de chaque périphérique pour ajuster finement la consommation. Arrêter complètement un ou plusieurs oscillateurs permet d'agir sur la consommation de manière plus générale, plus immédiate et bien plus simple au niveau du code.

Un autre moyen encore plus radical est de couper l'alimentation électrique de certains sous-systèmes. Comme on peut le voir sur le diagramme ci-dessous, extrait du Chapter 2 Power Control (PWR) du manuel de référence du CH32V003, celui-ci est prévu pour pouvoir couper l'alimentation du CPU (coeur du micro-contrôleur) et de la plupart de ses périphériques.

La combinaison de ces possibilités nous donne les 3 modes de fonctionnement suivants :

Mode Retour à Run Horloges Alimentation Consommation Remarques
Run - Tout ON ON 7.4 mA Fonctionnement normal
Sleep Interruption CPU : OFF, autres : ON ON 4.1 mA Parfois appelé "idle mode"
Standby Interruption, Auto-Wake Up (AWU) Tout OFF sauf LSI OFF 9.4 μA Parfois appelé "deep sleep" ou "power-down mode"

La fonctionnalité Auto-Wake Up consiste en un timer spécial qui provoque la sortie du mode Standby au bout d'un temps configurable (max. 30s).

La colonne Retour à Run indique les conditions provoquant la sortie du mode low-power pour revenir au mode Run.

La colonne Alimentation indique si le régulateur de tension du "core power supply domain" est actif.

La consommation est indiquée pour une tension d'alimentation de 5V, tous périphériques actifs et fonctionnement sur l'oscillateur HSI. Les valeurs sont issues de la data sheet du CH32V003.

Interruptions et événements

Vous avez déjà utilisé des interruptions ("interrupt" en anglais) à maintes reprises et elles vous sont maintenant familières. Le but d'une interruption est de réaliser un cours traitement en réponse à des circonstances précises - ex. détection du changement d'état d'une ligne de GPIO, débordement du compteur d'un timer, etc.

Dans le cadre des dispositifs alimentés sur batterie, on a vu la nécessité de réduire la consommation en utilisant des modes low-power et qu'il faut des moyens d'en sortir pour revenir au mode normal. Les interruptions sont un moyen pour le faire, mais il se trouve que nous voulons uniquement sortir du mode low-power où nous sommes et que nous n'avons rien à faire dans la routine d'interruption. C'est pour cela qu'il existe un mécanisme comparable aux interruptions mais simplifié au maximum : les événements ("event" en anglais).

Interruption
  • But : réaliser un traitement en réponse à un stimulus.

  • Configuration :

    • EXTI_Init() avec EXTI_Mode = EXTI_Mode_Interrupt pour activer l'interruption.
    • NVIC_Init(() pour activer la routine d'interruption.
  • La routine d'interruption associée DOIT réaliser le traitement voulu ET remettre à zéro l'indicateur d'interruption.

  • Entrée en mode low-power : __WFI()

  • Sortie du mode low-power : la routine d'interruption est appelée avant de poursuivre l'exécution à la suite de l'instruction __WFI().

Événement
  • But : provoquer la sortie d'un mode low-power.

  • Configuration :

    • EXTI_Init() avec EXTI_Mode = EXTI_Mode_Event pour activer l'événement.
  • Entrée en mode low-power : __WFE()

  • Sortie du mode low-power : l'exécution reprend à la suite de l'instruction __WFE().

Le bloc EXTI (EXTernal Interrupts), malgré son nom, regroupe aussi des sources de "réveil" internes - l'Auto-Wake Up (AWU) dans le cas du CH32V003, mais bien d'autres (ex. RTC, USB, Ethernet) avec des micro-contrôleurs plus évolués comme le CH32V307.

Dans la suite, nous donnerons des exemples pour les 2 approches, interruption et événement.

Utiliser le mode sleep

Pour illustrer l'utilisation du mode sleep, nous allons attendre qu'une touche soit appuyée (interruption) pour allumer brièvement une LED. Le schéma de câblage est le suivant :

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, pour la version avec interruption :

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

#define LED_GREEN GPIO_Pin_5

__attribute__((interrupt("WCH-Interrupt-fast")))
void EXTI7_0_IRQHandler() {
	if (EXTI_GetITStatus(EXTI_Line7) != RESET) {
		// Dans notre exemple, nous n'avons rien de particulier à faire
		// pour traiter l'interruption, donc on se contente de remettre
		// à zéro l'indicateur d'interruption.
		EXTI_ClearITPendingBit(EXTI_Line7);
	}
}

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

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

	GPIO_InitTypeDef gpioInit = { 0 };

	// Configuration de la sortie de la LED
	GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	gpioInit.GPIO_Pin = LED_GREEN;
	gpioInit.GPIO_Mode = GPIO_Mode_Out_OD;
	gpioInit.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &gpioInit);

	// Configuration de l'entrée du bouton poussoir
	gpioInit.GPIO_Pin = GPIO_Pin_7;
	gpioInit.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOC, &gpioInit);

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource7);

	EXTI_InitTypeDef extiInit = { 0 };
	extiInit.EXTI_Line = EXTI_Line7;
	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 = 0;
	nvicInit.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&nvicInit);

	while (1) {
		// On active le mode sleep.
		__WFI();

		// L'interruption générée par l'appui sur le poussoir a provoqué la sortie
		// du mode sleep, on allume brièvement la LED pour le signaler.
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_RESET);
		Delay_Ms(500);
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	}
}

Ou la même chose en utilisant un événement à la place de l'interruption :

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

#define LED_GREEN GPIO_Pin_5

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

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

	GPIO_InitTypeDef gpioInit = { 0 };

	// Configuration de la sortie de la LED
	GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	gpioInit.GPIO_Pin = LED_GREEN;
	gpioInit.GPIO_Mode = GPIO_Mode_Out_OD;
	gpioInit.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &gpioInit);

	// Configuration de l'entrée du bouton poussoir
	gpioInit.GPIO_Pin = GPIO_Pin_7;
	gpioInit.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOC, &gpioInit);

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource7);

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

	while (1) {
		// On active le mode sleep.
		__WFE();

		// L'interruption générée par l'appui sur le poussoir a provoqué la sortie
		// du mode sleep, on allume brièvement la LED pour le signaler.
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_RESET);
		Delay_Ms(500);
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	}
}

Notez que tous les périphériques restent en fonction en mode sleep, il est donc possible d'utiliser n'importe quelle source d'interruption pour "réveiller" notre micro-contrôleur. L'exemple ci-dessous utilise le timer TIM2 pour réveiller le CH32V003 toutes les 5s :

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

#define LED_GREEN GPIO_Pin_5

__attribute__((interrupt("WCH-Interrupt-fast")))
void TIM2_IRQHandler() {
	if (TIM_GetITStatus(TIM2, TIM_IT_Update) != RESET) {
		// Dans notre exemple, nous n'avons rien de particulier à faire
		// pour traiter l'interruption, donc on se contente de remettre
		// à zéro l'indicateur d'interruption.
		TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
	}
}

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

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

	GPIO_InitTypeDef gpioInit = { 0 };

	// Configuration de la sortie de la LED
	GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	gpioInit.GPIO_Pin = LED_GREEN;
	gpioInit.GPIO_Mode = GPIO_Mode_Out_OD;
	gpioInit.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &gpioInit);

	// Configuration de TIM2 pour avoir une interruption toutes les 5s.
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);

	TIM_TimeBaseInitTypeDef timeBaseInit = { 0 };
	timeBaseInit.TIM_Prescaler = 65536ul - 1;
	timeBaseInit.TIM_Period = 3662 - 1;
	timeBaseInit.TIM_ClockDivision = TIM_CKD_DIV1;
	timeBaseInit.TIM_CounterMode = TIM_CounterMode_Down;
	TIM_TimeBaseInit(TIM2, &timeBaseInit);

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

	TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
	TIM_Cmd(TIM2, ENABLE);

	while (1) {
		// On active le mode sleep.
		__WFI();

		// L'interruption générée par TIM2 a provoqué la sortie du
		// mode sleep, on allume brièvement la LED pour le signaler.
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_RESET);
		Delay_Ms(500);
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	}
}

Utiliser le mode standby

Nous allons maintenant faire exactement la même chose, mais en utilisant le mode standby au lieu du mode sleep. La différence de consommation au repos est importante, mais le changement au niveau du code est minimal. Voici la version avec interruption :

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

#define LED_GREEN GPIO_Pin_5

__attribute__((interrupt("WCH-Interrupt-fast")))
void EXTI7_0_IRQHandler() {
	if (EXTI_GetITStatus(EXTI_Line7) != RESET) {
		// Dans notre exemple, nous n'avons rien de particulier à faire
		// pour traiter l'interruption, donc on se contente de remettre
		// à zéro l'indicateur d'interruption.
		EXTI_ClearITPendingBit(EXTI_Line7);
	}
}

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

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

	GPIO_InitTypeDef gpioInit = { 0 };

	// Configuration de la sortie de la LED
	GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	gpioInit.GPIO_Pin = LED_GREEN;
	gpioInit.GPIO_Mode = GPIO_Mode_Out_OD;
	gpioInit.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &gpioInit);

	// Configuration de l'entrée du bouton poussoir
	gpioInit.GPIO_Pin = GPIO_Pin_7;
	gpioInit.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOC, &gpioInit);

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource7);

	EXTI_InitTypeDef extiInit = { 0 };
	extiInit.EXTI_Line = EXTI_Line7;
	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 = 0;
	nvicInit.NVIC_IRQChannelCmd = ENABLE;
	NVIC_Init(&nvicInit);

	while (1) {
		// On active le mode standby. Le paramètre PWR_STANDBYEntry_WFI
		// indique que c'est une interruption qui en provoquera la sortie.
		PWR_EnterSTANDBYMode(PWR_STANDBYEntry_WFI);

		// L'interruption générée par l'appui sur le poussoir a provoqué la sortie
		// du mode standby, on allume brièvement la LED pour le signaler.
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_RESET);
		Delay_Ms(500);
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	}
}

Et la même chose en utilisant un événement à la place de l'interruption :

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

#define LED_GREEN GPIO_Pin_5

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

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

	GPIO_InitTypeDef gpioInit = { 0 };

	// Configuration de la sortie de la LED
	GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	gpioInit.GPIO_Pin = LED_GREEN;
	gpioInit.GPIO_Mode = GPIO_Mode_Out_OD;
	gpioInit.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &gpioInit);

	// Configuration de l'entrée du bouton poussoir
	gpioInit.GPIO_Pin = GPIO_Pin_7;
	gpioInit.GPIO_Mode = GPIO_Mode_IPU;
	GPIO_Init(GPIOC, &gpioInit);

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOC, GPIO_PinSource7);

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

	while (1) {
		// On active le mode standby. Le paramètre PWR_STANDBYEntry_WFE
		// indique que c'est un événement qui en provoquera la sortie.
		PWR_EnterSTANDBYMode(PWR_STANDBYEntry_WFE);

		// L'interruption générée par l'appui sur le poussoir a provoqué la sortie
		// du mode standby, on allume brièvement la LED pour le signaler.
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_RESET);
		Delay_Ms(500);
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	}
}

En mode standby, seul le bloc EXTI est encore alimenté, les autres périphériques ne le sont plus, donc ils ne peuvent pas servir de source d'éveil, contrairement au mode sleep. Pour sortir du mode standby à intervalles réguliers, on ne peut donc pas utiliser TIM1 ou TIM2 comme précédemment. C'est là qu'intervient l'auto-wake up.

Sources de réveil multiples

Notez qu'il est tout à fait possible de configurer plusieurs sources d'interruption pour sortir du mode low-power. Dans ce cas, __WFI ou __WFE réagira à la première qui surviendra. Il vous appartient de positionner des indicateurs dans les routines d'interruption pour savoir comment réagir lors du retour au mode Run.

Utiliser l'auto-wake up

Il existe beaucoup de situations dans lesquelles le système est à l'arrêt la plupart du temps et se réveille de temps en temps pour faire ce qu'il a à faire avant de se rendormir. C'est exactement le but de l'auto-wake up.

Voici un exemple de code qui sort du mode standby au bout d'environ 5s pour allumer brièvement notre LED avant de retourner en mode standby. Cet exemple illustre également comment minimiser la consommation en configurant les lignes de GPIO inutilisées en mode input pull-down.

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

#define LED_GREEN GPIO_Pin_5

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

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOC | RCC_APB2Periph_GPIOD, ENABLE);

	GPIO_InitTypeDef gpioInit = { 0 };

	// Pour réduire au maximum la consommation en mode standby, il faut
	// configurer toutes les lignes de GPIO inutilisées en mode input pull-down.
	// Dans notre cas, nous n'utilisons qu'une seule ligne de GPIO, donc il est
	// plus rapide de configurer ainsi toutes les lignes, puis de ne configurer
	// en sortie que la ligne utilisée pour la LED.
	gpioInit.GPIO_Pin = GPIO_Pin_All;
	gpioInit.GPIO_Mode = GPIO_Mode_IPD;
	GPIO_Init(GPIOA, &gpioInit);
	GPIO_Init(GPIOC, &gpioInit);
	GPIO_Init(GPIOD, &gpioInit);

	// Configuration de la sortie de la LED
	GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	gpioInit.GPIO_Pin = LED_GREEN;
	gpioInit.GPIO_Mode = GPIO_Mode_Out_OD;
	gpioInit.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &gpioInit);

	// Activation de l'événement AWU (auto-Wake up), conformément à la section
	// "2.3.4 Auto-wakeup (AWU)" du manuel de référence.
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

	EXTI_InitTypeDef extiInit = { 0 };
	// L'auto-wake up utilise la ligne EXTI 9.
	extiInit.EXTI_Line = EXTI_Line9;
	// Et c'est un événement, pas une interruption.
	extiInit.EXTI_Mode = EXTI_Mode_Event;
	extiInit.EXTI_Trigger = EXTI_Trigger_Falling;
	extiInit.EXTI_LineCmd = ENABLE;
	EXTI_Init(&extiInit);

	// Démarrage de l'oscillateur interne 128kHz (LSI) pour le timer Auto-Wake Up.
	RCC_LSICmd(ENABLE);
	while (RCC_GetFlagStatus(RCC_FLAG_LSIRDY) == RESET);

	// Configuration du timer Auto-Wake Up.
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
	PWR_AWU_SetPrescaler(PWR_AWU_Prescaler_61440);
	// Avec un prescaler de 61440, chaque unité de PWR_AWU_SetWindowValue()
	// représente 61440 / 128000 = 0.48s. Pour un délai d'environ 5s, la
	// valeur à utiliser est donc 11 (5.28s). Pour rappel, la valeur
	// maximum est 64.
	PWR_AWU_SetWindowValue(11 - 1);
	PWR_AutoWakeUpCmd(ENABLE);

	while (1) {
		// On active le mode standby. Le paramètre PWR_STANDBYEntry_WFE
		// indique que c'est un événement (AWU) qui en provoquera la sortie.
		PWR_EnterSTANDBYMode(PWR_STANDBYEntry_WFE);

		// On allume brièvement la LED pour signaler qu'on est sorti du
		// mode standby.
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_RESET);
		Delay_Ms(500);
		GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
	}
}

Mesurer la consommation d'un appareil

Nous avons vu que la consommation d'un appareil varie dans le temps, et c'est justement pour ça que nous utilisons les modes low-power, dans le but de maximiser la durée de vie de la batterie.

Dans la pratique, vous avez toujours un objectif d'autonomie de la batterie, lié aux exigences non-fonctionnelles du cahier des charges (NFR, Non-Functional Requirements). Vous devez par exemple tenir un an avec une pile bouton. Et évidemment, vous ne pouvez pas vous permettre d'attendre un an pour le vérifier, vous devez trouver une autre solution.

Ce qu'on fait dans ce cas, c'est établir le profil de consommation ("power profile" en anglais) de l'appareil dans différentes conditions (phases de son fonctionnement), puis on extrapole en fonction du temps que l'appareil est censé passer dans chacune de ces conditions.

On utilise pour cela un profiler, c'est-à-dire un appareil qui mesure la consommation et observe quelques lignes de GPIO. Pour les essais, le code sera modifié pour activer certaines lignes de GPIO de l'appareil en test (DUT, Device Under Test), reliées au lignes de GPIO du profiler, en fonction des différentes conditions de fonctionnement du DUT. Un logiciel permettra ensuite de tracer une courbe représentant la consommation en fonction du temps en regard de l'état des lignes de GPIO qui matérialisent les différentes conditions de fonctionnement.

Si vos futurs projets vous mènent dans cette direction, vous voudrez sans doute acquérir un Power Profiler Kit II de NORDIC Semiconductor, qui présente un excellent rapport qualité/prix et n'a absolument rien de spécifique aux produits de NORDIC.

Utiliser le Programmable Voltage Detector (PVD)

Nous avons couvert le besoin de réduire la consommation, reste maintenant à s'intéresser à la surveillance de l'état de la batterie. Le CH32V003 nous offre pour cela un Programmable Voltage Detector, en français "détecteur de tension programmable", plus commodément appelé PVD.

Le PVD compare la tension d'alimentation du CH32V003 à une référence choisie parmi 8 possibilités. Le PVD est un comparateur avec hystérésis, c'est-à-dire avec un seuil haut et un seuil bas. Son fonctionnement est illustré ci-dessous. On y voit qu'il faut que la tension d'entrée devienne supérieure au seuil haut pour que la sortie passe à l'état haut, et que la tension d'entrée devienne inférieure au seuil bas pour que la sortie passe à l'état bas. L'hystérésis est l'écart entre les seuils haut et bas.

Le trigger de Schmitt qui "nettoie" les entrées du GPIO et dont nous avons parlé dans le cours CH32V003 : GPIO et interruptions fonctionne exactement sur ce principe (l'hystérésis est indiqué dans la section "3.3.9 I/O Port Characteristics" de la data sheet). L'hystérésis du PVD est du même ordre de grandeur (150 à 200 mV). Le tableau ci-dessous regroupe les différents seuils disponibles pour le PVD ainsi que les constantes à utiliser pour le configurer.

Constante Synonyme Seuil bas Seuil haut
PWR_PVDLevel_2V9 PWR_PVDLevel_MODE0 2.7V 2.85V
PWR_PVDLevel_3V1 PWR_PVDLevel_MODE1 2.9V 3.05V
PWR_PVDLevel_3V3 PWR_PVDLevel_MODE2 3.15V 3.3V
PWR_PVDLevel_3V5 PWR_PVDLevel_MODE3 3.3V 3.5V
PWR_PVDLevel_3V7 PWR_PVDLevel_MODE4 3.5V 3.7V
PWR_PVDLevel_3V9 PWR_PVDLevel_MODE5 3.7V 3.9V
PWR_PVDLevel_4V1 PWR_PVDLevel_MODE6 3.9V 4.1V
PWR_PVDLevel_4V4 PWR_PVDLevel_MODE7 4.2V 4.4V

Le PVD peut être utilisé en polling - PWR_GetFlagStatus(PWR_FLAG_PVDO) - mais on l'utilise plutôt en interruption, déclenchée par le front de la sortie du comparateur. On peut ainsi choisir si on veut une interruption au franchissement à la baisse du seuil bas (front descendant), au franchissement à la hausse du seuil haut (front montant), voire dans les deux cas.

Le PVD peut ainsi servir à détecter si la batterie est faible et si le chargeur a été branché, le cas échéant. Le PVD peut aussi être utilisé dans les applications alimentées sur secteur pour détecter une coupure secteur et sauvegarder en EEPROM les données nécessaires au redémarrage, par exemple.

Pour pouvoir tester le PVD, il faut disposer d'une alimentation réglable et vous n'en avez pas. Je vais cependant vous expliquer comment procéder car si vous avez suivi les cours jusque là, c'est que vous avez bien accroché au développement embarqué et il est probable que vous en achetiez une un jour. :)

Notre appareil en test (DUT, souvenez-vous) sera le suivant :

Dans un premier temps, vous flasherez le code ci-dessous et vous déconnecterez votre WCH-LinkE de votre PC.

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

#define LED_RED GPIO_Pin_4
#define LED_GREEN GPIO_Pin_5

bool isBelowThreshold = false;
bool isAboveThreshold = false;

// Routine d'interruption du PVD. On remarque que le PVD a une ISR dédiée, 
// mais que son indicateur d'interruption est dans le bloc EXTI.
__attribute__((interrupt("WCH-Interrupt-fast")))
void PVD_IRQHandler() {
	if (EXTI_GetITStatus(EXTI_Line8) != RESET) {
		// Le manuel de référence dit que PVDO est à 1 si on est en dessous
		// du seuil et à 0 si on est au dessus.
		if (PWR_GetFlagStatus(PWR_FLAG_PVDO) == RESET) {
			isAboveThreshold = true;
		} else {
			isBelowThreshold = true;
		}

		EXTI_ClearITPendingBit(EXTI_Line8);
	}
}

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

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);

	GPIO_InitTypeDef gpioInit = { 0 };

	// Configuration des sorties des LED.
	GPIO_WriteBit(GPIOC, LED_RED | LED_GREEN, Bit_SET);
	gpioInit.GPIO_Pin = LED_RED | LED_GREEN;
	gpioInit.GPIO_Mode = GPIO_Mode_Out_OD;
	gpioInit.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOC, &gpioInit);

	// Configuration des interruptions du PVD.
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);

	EXTI_InitTypeDef extiInit = { 0 };
	extiInit.EXTI_Line = EXTI_Line8;
	extiInit.EXTI_Mode = EXTI_Mode_Interrupt;
	// Dans le cadre de notre exemple, on veut traiter les 2 interruptions pour
	// bien montrer le fonctionnement du PVD. Dans une application réelle, il
	// est possible que seul EXTI_Trigger_Rising ou EXTI_Trigger_Falling vous
	// intéresse.
	extiInit.EXTI_Trigger = EXTI_Trigger_Rising_Falling;
	extiInit.EXTI_LineCmd = ENABLE;
	EXTI_Init(&extiInit);

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

	// Configuration du PVD.
	RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
	PWR_PVDLevelConfig(PWR_PVDLevel_3V3);
	PWR_PVDCmd(ENABLE);

	while (1) {
		// Nous n'avons rien de spécial à faire en dehors des interruptions,
		// alors autant activer le mode sleep, ça fait une révision. :)
		__WFI();

		if (isBelowThreshold) {
			// On est descendu en dessous du seuil bas, il faut allumer la
			// LED rouge et éteindre la verte.
			isBelowThreshold = false;
			GPIO_WriteBit(GPIOC, LED_RED, Bit_RESET);
			GPIO_WriteBit(GPIOC, LED_GREEN, Bit_SET);
		}

		if (isAboveThreshold) {
			// On est monté au dessus du seuil haut, il faut allumer la LED
			// verte et éteindre la rouge.
			isAboveThreshold = false;
			GPIO_WriteBit(GPIOC, LED_RED, Bit_SET);
			GPIO_WriteBit(GPIOC, LED_GREEN, Bit_RESET);
		}
	}
}

Vous réglerez ensuite votre alimentation approximativement sur 3.3V (le seuil choisi dans le code) et vous la connecterez aux broches VCC et GND de votre carte de développement (rappel : elle doit être déconnectée de votre PC au préalable). Ensuite, vous baisserez la tension de votre alimentation en dessous de 3.15V et vous verrez la LED rouge s'allumer et la verte s'éteindre. Augmentez maintenant la tension au dessus de 3.3V et vous verrez la LED verte s'allumer et la rouge s'éteindre.

Pour finir, notez que d'autres fabricants de micro-contrôleurs utilisent le terme de Low-Voltage Detector (LVD) à la place de PVD, mais on parle bien du même type de dispositif.

Ce que nous avons appris

Vous savez maintenant :

  • Ce à quoi vous devez veiller quand vous créez un dispositif alimenté par batterie.

  • Ce que sont les modes low-power et comment les utiliser.

  • Les différences entre interruption et événement.

  • Comment mesurer la consommation d'un appareil.

  • Ce qu'est un programmable voltage detector et comment l'utiliser.


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