Réduire la consommation d'un AVR

Open Silicium n° 005 | janvier 2012 | Jérôme Labidurie
Creative Commons
  • Actuellement 5 sur 5 étoiles
5
Merci d'avoir participé !
Vous avez déjà noté cette page, vous ne pouvez la noter qu'une fois !
Votre note a été changée, merci de votre participation !
Qu'il soit alimenté par panneau solaire ou par piles, un montage autonome nécessite une faible consommation. Cela tombe bien car les AVR sont capables de nous impressionner dans ce domaine. Voyons ensemble comment...

1. Introduction

1.1. Pourquoi réduire la consommation ?

Dès que l'on veut créer des montages autonomes, la consommation devient très vite un facteur limitant. Dans un montage alimenté par piles, il peut être très contraignant (et cher) de devoir les changer souvent. Même si nous optons pour un panneau solaire, il sera nécessaire de consommer le moins possible si nous voulons éviter d'y adjoindre 10m².

Nous allons dans cet article découvrir quelques techniques permettant de réduire le coût énergétique d'un ATtiny45 à l'aide d'un montage simple. Il est recommandé d'avoir quelques bases de programmation en C de ces petites bêtes avant d'aller plus loin.

1.2. Présentation rapide de l'ATtiny45

L'ATtiny45 est un petit microcontrôleur 8bits de la société ATMEL. Il comporte seulement 8 pattes dont 6 sont utilisables comme entrée/sortie. Ce nombre se réduit à 5 si l'on veut conserver la fonction ISP1 puisque le reset est nécessaire dans ce cas.

Sa petite taille ne l'empêche pas de disposer de nombreuses fonctions intégrées dignes des plus grands. 4KB de flash pour stocker le code, 256B d'eeprom et 256B de RAM permettent de développer des programmes conséquents. Parmi les fonctions qu'il offre, citons celles qui vont nous intéresser :

  • Comparateur analogique qui permet de tester une tension par rapport à un seuil prédéfini. Celui-ci peut être présent sur une 2ème patte ou en interne (1,1V).
  • Watchdog qui peut déclencher un reset ou une interruption.
  • Oscillateur interne qui permet de ne pas utiliser de quartz externe.
  • Fonctions de réduction de la consommation que nous étudierons plus en détail ci-dessous.
  • Enfin, il possède une plage d'alimentation très large (1,8V-5,5V), ce qui permet de l'utiliser facilement sans trop se soucier des valeurs fournies.

Pour plus de détails, vous pouvez consulter la datasheet [DS] du composant.

1.3. Présentation d'une fonction simple servant d'exemple

Pour les besoins de cet article, nous allons utiliser un petit montage simple. La fonction à offrir sera de faire clignoter une LED pendant la nuit. Nous pouvons rattacher cela au monde réel en imaginant par exemple que nous créons une balise cardinale pour le balisage maritime.

Le circuit est reproduit dans la figure 1 ci-dessous.

Fig. 1 : Schéma électronique d'une (pseudo)balise cardinale

Une photo-résistance montée en pont diviseur de tension est branchée sur une entrée du comparateur analogique de notre Attiny. Elle nous permettra de détecter une baisse de luminosité. Quand la lumière diminue, la résistance du composant augmente, et selon la loi d'Ohm, la tension à ses bornes augmente. Le potentiomètre nous permet d'ajuster le point de détection par rapport à la référence interne.

Le reste du schéma se compose d'une simple LED associée à sa résistance pour réduire le courant la traversant.

Nous trouvons aussi l'habituel connecteur ISP pour reprogrammer l'ATtiny en situation.

2. Sleep modes des AVR

Il est bien connu que tout ordinateur passe son temps à attendre. Les µC n'échappent généralement pas à la règle. Pour réduire la consommation pendant ces temps d'attente, nous pouvons endormir le composant en ne gardant que le strict nécessaire pour le réveiller au moment opportun et lui faire exécuter sa tâche.

2.1. Les différents sleep modes

Notre Attiny45 propose 3 modes suivant les fonctions éteintes et les capacités de réveil offertes :

- Idle ;

- ADC Noise Reduction ;

- Power-down.

Notons aussi que d'autres AVR offrent d'autres modes appelés Standby, Extended Standby ou encore Power-Save. Vous pouvez vous référer à la datasheet de votre composant pour en savoir plus.

Fig. 2 : Extrait de la datasheet de l'ATtiny45 [DS] p35

2.1.1. Idle

Ce mode conserve un maximum de périphériques en fonction, il se contente principalement d'arrêter l'exécution du programme en cours et permet de le réveiller de nombreuses manières. En contrepartie, il consommera plus d'énergie que ses camarades.

2.1.2. ADC Noise Reduction

Comme son nom l'indique, le but de ce mode est de réduire le bruit électrique lors de l'utilisation du convertisseur analogique/numérique. Il arrête les horloges de la flash, du CPU et des entrées/sorties.

2.1.3. Power-down

Enfin, le mode le plus économe. S'il ne consomme presque rien, peu de conditions peuvent l'en sortir. Principalement, un changement de niveau sur INT0 (interruption) ou le déclenchement du watchdog.

2.2. Lequel choisir ?

Il est bien entendu impossible de répondre de manière générique à cette question. Tout dépend du programme, des périphériques nécessaires et des conditions de réveil. Donnons tout de même quelques pistes.

Il faut étudier précisément quels périphériques sont nécessaires à un instant donné pour ne conserver que ceux-ci. On cherchera aussi à éviter, outre les attentes actives, les longues périodes de _delai_ms(). Dans ce cas, on préférera endormir le processeur et se réveiller à l'aide d'un compteur (idle), d'une interruption extérieure (tous les modes) ou du watchdog si la durée n'a pas besoin d'être très précise (power-down)

On aura tout intérêt à varier les plaisirs en utilisant plusieurs modes au cours de notre programme. Quelques mA sont toujours bons à gagner.

Appliquons cela à notre exemple de balise.

Pour le comparateur analogique, la 1ère idée est de passer en Idle afin d'être réveillé par interruption quand le seuil est franchi. Mais la détection de nuit n'a pas besoin d'être immédiate, un délai de quelques secondes est largement acceptable. Nous pouvons donc rester en Power-down quasiment en permanence et nous faire réveiller régulièrement par le watchdog afin d'utiliser le comparateur analogique (sans interruption cette fois). Ainsi, en détournant le watchdog de sa fonction première, nous économisons beaucoup.

Pour faire clignoter notre LED, nous pouvons envisager la même solution. Mais elle présente tout de même quelques inconvénients : le watchdog ne propose que quelques valeurs de timeout (15ms, 30ms, 60ms, 120ms, 250ms, 500ms, 1s, 2s et aussi 4s et 8s sur l'ATtiny45) ; de plus, ces valeurs ne sont pas précises et augmentent quand la tension d'alimentation diminue. Notre clignotement devant être précis, il nous faut une autre solution. Nous pouvons cette fois envisager le mode Idle en nous faisant réveiller par un Timer/Counter.

2.3. Show us the code !

2.3.1. Watchdog

La bibliothèque avr-libc nous fournit un certain nombre de fonctions et macros nous facilitant la vie pour utiliser les sleep modes de nos AVR. De ce fait, un endormissement en mode Power-down du µC sera aussi simple que :

#include <avr/sleep.h>

...

    set_sleep_mode(SLEEP_MODE_PWR_DOWN);

    sleep_mode();

On s'assurera bien sur que les interruptions sont activées au préalable (via sei()) afin de pouvoir être réveillé. L'exécution continuera alors simplement à la ligne suivante. Il est aussi possible de découper le déclenchement en étapes élémentaires (via sleep_enable(), sleep_cpu()) afin d'éviter des conditions de blocage. Typiquement si l'interruption de réveil apparaît pendant la fonction sleep_mode().

Nous pouvons donc écrire les fonctions suivantes pour gérer notre endormissement :

/** set system into the sleep state

* system wakes up when watchdog is timed out

*

* @param duration watchdog timer (see wdt.h)

*/

void system_sleep(uint8_t duration)

{

   PORTB &= ~_BV(DRIVE); // Power off photoresistor

   ACSR |= _BV(ACD); // switch Analog Comparator OFF

   PRR |= _BV(PRADC); // switch off ADC clock

   wdt_enable ( duration );

   // set wdt to generate interrupt instead of reset (see p47)

   WDTCR |= _BV(WDIE);

   set_sleep_mode(SLEEP_MODE_PWR_DOWN); // sleep mode is set here

   sleep_mode();

   

   wdt_disable ();

   PORTB |= _BV(DRIVE); // Power on photoresistor

   PRR &= ~ _BV(PRADC); // switch on ADC clock

   ACSR &= ~ _BV(ACD); // switch Analog Comparator ON

   // wait for stabilization of internale voltage ref (See p44)

   _delay_ms(1);

}

/** Interrupt handler for Watchdog

*/

ISR(WDT_vect)

{

   // this is set to 0 on each interrupt, so re-force it to 1

   // set wdt to generate interrupt instead of reset (see p47)

   WDTCR |= _BV(WDIE);

}

On désactive le comparateur analogique et l'horloge associée. On active alors le watchdog configuré pour utiliser une interruption. On désalimente aussi notre photo-résistance. Lorsqu'on est réveillé, on rétablit tout cela pour être en capacité d'utiliser notre photo-résistance. Notre boucle principale pourra alors ressembler à ceci :

   while (1)

   {

      if ( ! bit_is_set (ACSR,ACO) ) {

         // ACO is not set --> dark, switch on led

         PORTB |= _BV(LED);

         system_sleep(WDTO_8S);

      }

      else {

         // ACO is set --> light, switch off led

         PORTB &= ~ _BV(LED);

         system_sleep(WDTO_8S);

      }

   }

Ainsi, le changement de luminosité sera détecté sous un délai maximum d'environ 8s et la LED sera allumée tant qu'il fera nuit. Vous trouverez le code complet dans [CODE] sous le nom ex1.c.

2.3.2. Timer0

Le Timer0 compte les cycles d'horloge sur 8bits. Son horloge peut être divisée par un prescaler. Il peut déclencher une interruption lorsqu'il déborde. Nous allons utiliser cette particularité en comptant le nombre de débordements nécessaires pour atteindre une durée voulue.

La période de débordement (overflow) du compteur (en secondes) peut être calculée ainsi : taille du compteur * prescaler / FCLK_IO. Il nous suffit alors de diviser la durée recherchée par ce nombre pour obtenir le nombre de débordements nécessaires. Nous pouvons alors écrire les fonctions suivantes pour attendre une durée voulue en restant en mode Idle la plupart du temps.

volatile uint16_t timer0_overflows; // number of timer overflows

/** sleep by going idle

* @param duration sleep time (ms)

* @param prescal value of Timer0 prescaler

*/

void idle_sleep(uint16_t duration, uint16_t prescal)

{

   // compute number of overflows needed

   uint16_t overflows = ( ( duration * F_CPU ) / (0xFF * prescal) ) / 1000;

   

   while (timer0_overflows < overflows)

   {

      set_sleep_mode(SLEEP_MODE_IDLE); // sleep mode is set here

      sleep_mode();

   }

   timer0_overflows = 0;

} // idle_sleep

/** Interrupt for Timer0

   */

ISR (TIMER0_OVF_vect) {

   timer0_overflows++;

}

Fonction que nous appellerons ainsi :

// set a timer0

PRR    &= ~ _BV(PRTIM0); // enable Timer0 module

TIMSK |=   _BV(TOIE0); // enable interrupt on overflow

TCCR0B |=   _BV(CS02);   // set prescaler to CLKio/256

idle_sleep (3000, 256); // sleep 3s

Nous pouvons alors faire clignoter notre LED à la fréquence voulue avec une précision relativement importante. On n'oubliera pas d'arrêter le comparateur analogique et la photo-résistance pendant chaque cycle de clignotement. Vous trouverez le code complet dans [CODE] sous le nom ex2.c.

3. Réduire la consommation en fonctionnement

Maintenant, attaquons-nous au moment où notre µC travaille. Voici quelques trucs pour réduire sa consommation en dehors des périodes de sommeil.

3.1. Horloge

La fréquence d'horloge a une forte influence sur le courant utilisé, il est donc inutile de fonctionner à de hautes fréquences si ce n'est pas nécessaire.

Avec la configuration usine, notre ATtiny45 fonctionne sur l'oscillateur interne à une fréquence de 8MHz. Celle-ci traverse une PLL qui la divise par 8 et offre donc une horloge système à 1MHz. Pour réduire la consommation générale, nous pouvons jouer sur cette configuration de deux manières. Configurer la PLL pour augmenter la division et ralentir l'horloge système ou changer la source d'origine. En restant sur une génération interne d'horloge, l'ATtiny45 nous offre aussi une source à 6,4MHz (mode de compatibilité avec l'ATtiny15) et une source de faible consommation à 128KHz. Avec cette dernière, nous réduisons de beaucoup notre puissance de calcul et nous perdons en précision (à titre d'exemple, sur mon Attiny à 3,9V, j'obtiens une fréquence CPU de 11,9KHz au lieu des 128/8=16KHz attendus).

Pour jouer sur ces paramètres, nous disposons des fusibles (fuses) pour sélectionner la source (CKSEL) et d'un fusible+registre pour la PLL (CKDIV8+CLKPR)

On sélectionne la clock à 128KHz en positionnant les poids faibles du fuse low. Ce qui donne avec avrdude et un programmeur de type stk500 :

$ /usr/bin/avrdude -V -p t45 -c stk500 -P /dev/ttyUSB0 -q -U lfuse:w:0b01100100:m

Nous voici donc avec une horloge principale à 128KHz qui passe dans un diviseur par 8 pour avoir une horloge système à 16KHz.

Dans le code, on se bornera à changer la définition de F_CPU (utilisé par l'avr-libc). Pour une raison que je ne m'explique pas, j'ai aussi dû supprimer le prescaler du Timer0. Le comportement était erratique en le laissant à 256. Le code est disponible sous le nom ex3.c dans [CODE].

Note

Attention, si vous changez la source d'horloge de votre ATtiny, il faut dès lors coordonner l'horloge de votre programmeur avec l'horloge système de votre ATtiny. Il est recommandé d'avoir une fréquence de programmation au moins égale à 1/5 de la fréquence de la cible. J'ai pour habitude de simplement mettre la même.

Avrdude attend une valeur en µs. Donc pour une fréquence à 1MHz (valeur usine), votre période doit être d'au moins 1/1MHz = 1µs. Pour une horloge cadencée à 16KHz (128/8), votre période devient 62,5µs.

Ce réglage peut se faire de 2 manières suivant le modèle :

  • Avec un stk500, vous pouvez positionner le paramètre sck :

$ /usr/bin/avrdude -p t45 -c stk500 -P /dev/ttyUSB0 -tuF

avrdude> parms

>>> parms

Vtarget         : 0.0 V

SCK period      : 0.1 us

avrdude> sck 62

>>> sck 62

avrdude> q

>>> q

Vous aurez remarqué que les paramètres affichés par mon clone de stk500 ne sont pas valides.

  • Si celui-ci n'est pas disponible, l'option -B en ligne de commandes sera votre amie :

$ avrdude -p t45 -B62 -c stk500 -P /dev/ttyUSB0 -U flash:w:ex3.hex

À noter aussi que mon programmeur n'est pas très stable à cette fréquence. Pour éviter de rater des programmations, on pourra flasher le programme à 1MHz et ne changer la fréquence (via le lfuse) qu'après.

3.2. Désactiver l'inutile

Les AVR nous offrent aussi un mécanisme simple pour désactiver les périphériques inutiles pour notre application. C'est très utile en fonctionnement, mais aussi dans certains sleep modes comme Idle. On pourra ainsi activer ceux-ci à la demande et pour le temps strictement nécessaire.

Cela se fait via le Power Reduction Register (PRR). Notre ATtiny45 nous permet de désactiver les Timers (0 et 1), l'USI, le convertisseur analogique/numérique. Nous pouvons aussi, via d'autres registres, désactiver le comparateur analogique (ADCSRA), le watchdog (WDTCR) ou la tension de référence interne (en désactivant l'ADC et le comparateur analogique). Il est donc important d'étudier la datasheet du composant pour traquer et désactiver toutes les fonctions inutiles pour votre application.

// Configure Power Reduction Register (See p39)

// disable Timer1, USI

PRR |= _BV(PRTIM1) | _BV(PRUSI);

// switch Analog to Digitalconverter OFF

ADCSRA &= ~_BV(ADEN);

3.3. Consommations « parasites »

Même non connectées, les entrées/sorties inutilisées du composant peuvent consommer du courant. Pour éviter cela, on s'assurera :

  • que les pins non connectées sont configurées en entrée avec la résistance de pull-up afin d'avoir un niveau bien défini.
  • que les buffers d'entrée sont désactivés.

// enable input / Pull-Up on unconnected pins (See 10.2.6 p59)

DDRB &= ~ ( _BV(DDB0)   | _BV(DDB3)   | _BV(DDB5) );

PORTB |= ( _BV(PORTB0) | _BV(PORTB3) | _BV(PORTB5) );

//disable all Digital Inputs (See p142, p125)

DIDR0 &= ~ ( _BV(ADC0D) | _BV(ADC2D) | _BV(ADC3D) | _BV(ADC1D)

            | _BV(AIN1D) | _BV(AIN0D) );

Ces modifications sont accessibles dans ex4.c.

On fera aussi attention au schéma de notre montage. Prenons par exemple la figure suivante :

Fig. 3 : 2 schémas ayant la même fonction

Dans le schéma A, le pont de résistances consomme en permanence, quel que soit l'état du µC et le besoin de mesure. Dans le schéma B, l'ATtiny45 n'alimente la photo-résistance que lorsqu'une mesure est nécessaire, cela nous prend une patte de plus, mais réduit grandement la consommation totale. On prêtera toutefois attention à ce que nos résistances ne tirent pas plus que les 40mA que peut fournir notre ATtiny (en cas de besoin en courant supérieur à 40mA, on peut adjoindre un système de transistor piloté par l'ATtiny, qui bloquera ou non le courant fort pris sur l'alimentation selon l'état de la sortie, sans augmenter le courant fourni par l'ATtiny).

4. Consommations comparées

Quelques notes sur les mesures suivantes.

L'alimentation est régulée à 3,9V, soit l'équivalent de 3 piles AA rechargeables.

Pour l'exemple 1 : loop : fonctionnement en boucle infinie. sys_sleep : passage en mode power down après chaque action.

Pour l'exemple 2 : delay_ms correspond à l'utilisation de la fonction de la libc _delay_ms() pour les temps d'attente, contrairement à idle qui passe en mode idle pour ces mêmes attentes.

  Nuit LED on Nuit LED off Jour
ex1 loop 3,46 mA Non applicable 1,39 mA
ex1 sys_sleep 3,33 mA Non applicable 4,66 µA
ex2 delay_ms 3,46 mA 1,27 mA 4,66 µA
ex2 idle 3,41 mA 0,67 mA 4,66 µA
ex3 (128KHz) 3,35 mA 0,23 mA 4,66 µA
ex4 (128KHz+PRR) 3,33 mA 0,21 mA 4,64 µA

De ces quelques mesures, nous pouvons déduire que le mode power down est effectivement très intéressant puisqu'il permet de réduire la consommation de notre AVR à seulement quelques µA. On remarque aussi qu'endormir le µC plutôt que de faire une attente active est valable sur le long terme (jusqu'à 0,6mA de gagné pour ex2). De même, utiliser l'horloge cadencée à 128KHz vaut le coup si la précision n'est pas très importante. Enfin, lorsque l'on commande la LED, celle-ci consomme tellement qu'il n'est pas possible d'obtenir une réduction très significative.

En conclusion, un petit calcul de consommation en mAH nous permet d'estimer la durée de vie de nos piles. Dans le cas le plus consommateur, elles dureront ~66 jours et dans le meilleur des cas ~278 jours. Soit un gain d'un facteur >4. Cela vaut la peine, non ?

Références

[CODE] Code complet des exemples (Licence GPLv2), http://svn.tuxfamily.org/viewvc.cgi/scrippets_scripts/trunk/avr/OpenSilicium_01/

[DS] Datasheet de l'ATtiny45, http://www.atmel.com/dyn/resources/prod_documents/doc2586.pdf

[AT1] AVR1010: Minimizing the power consumption of Atmel AVR XMEGA devices, http://www.atmel.com/dyn/resources/prod_documents/doc8267.pdf

[AT2] Innovative Techniques for Extremely Low Power Consumption with 8-bit Microcontrollers, http://www.atmel.com/dyn/resources/prod_documents/doc7903.pdf