Le attiny85 est un tout petit microcontrôleur de Atmel, disponible en boitier DIP8. Il a donc juste 6 GPIO. Il coute moins de 2€. Bref c'est un microcontrôleur assez chouette pour de petits projets, ou en complément pour décharger un microcontrôleur principal.
Ce n'est pas l'objet de cet article, mais il est possible de le programmer avec l'IDE Arduino. Nous utiliserons l'IDE (surtout le framework Arduino) ici par souci d'accessibilité. Toutesfois ce que nous allons voir ici ne dépend absolument pas du framework Arduino et est donc utilisable en dehors de celui-ci (programmer un microcontrôleur atmel en dehors du framework Arduino pourrait faire l'objet d'un article futur...).
Nous allons donc explorer, en partie, comment réduire la consommation électrique d'un attiny85. L'idée que j'ai derrière la tête est de faire un mottage minimal avec un attiny85 (sur batterie) avec un transmetteur ASK 433Mhz.
Pour cela, on va partir d'un programme simple qui fait clignoter une led 3 fois toute les 10 secondes, et au fur à mesure on va essayer de réduire la consommation. Voici le programme de départ :
int led = 4;
void blink(){
bool state = HIGH;
for(int i = 0; i<2*3; i++){
digitalWrite(led, state);
state = !state;
delay(100);
}
}
void setup() {
pinMode(led, OUTPUT);
}
void loop() {
blink();
delay(10000);
}
Avec ce programme de base, en utilisant l'horloge interne et avec une alimentation en 3.3v, on a les consommations suivantes en fonction de la fréquence :
- 1MHz, 1.20mA;
- 8MHz, 4.30mA;
- 16MHz, 7.66A;
À retenir : on peut réduire la fréquence pour réduire la consommation. Par la suite nous limiterons les tests au 8MHz. En effet l'application que je vise nécessite une fréquence de 8MHz. Mais si cela suffit pour l'application visée et que limiter la consommation électrique est important autant partir sur de 1MHz (rajoutons tout de même que lorsque le CPU est en veille, son horloge est éteinte donc la fréquence de celle-ci importe peu... sauf lors des courts réveils provoqués par le watchdog avant une nouvelle mise en veille, voir la suite...).
Avant de dormir, on coupe ce qui ne sert à rien...
La première chose à faire pour réduire la consommation est de désactiver tous les périphériques qui ne servent pas. Notons qu'il est possible de couper un périphérique tout le temps ou alors seulement au moment du passage en veille (sleep mode, on y vient plus bas).
ADC
Le premier périphérique que l'on peut éteindre est le convertisseur analogique numérique (si on ne l'utilise pas). Pour ça il faut mettre à zéro le bit ADEN
du registre ADCSRA
(voir section 17.13.2 de la datasheet). Pour cela on ajoute dans le setup
:
ADCSRA &= ~_BV(ADEN);
À savoir : La macro _BV
décale un '1
' du nombre de bit indiqué. Elle permet donc de convertir un numéro de bit en 1
sur ce bit. Elle est définie dans la avr-libc.
En désactivant le convertisseur, on mesure une consommation de 4.05mA. L'ADC consomme donc ~25mA (toujours avec une alimentation a 3.3v).
Notons que si on utilise le ADC, il est tout de même possible de l'éteindre juste avant de passer le CPU en veille (et de le réactiver ensuite).
Analog Comparator
On peut ensuite désactiver le comparateur analogique. Il faut cette fois inscrire un 1
sur le bit ACD
du registre ACSR
(Section 16.2.2 de la datasheet) :
ACSR |= _BV(ACD);
On gagne alors ~0.5mA
et on arrive à environ ~4.00mA (toujours en 3.3v
). À noter : on peut aussi désactiver que le comparateur sans désactiver le convertisseur (on gagne bien ~0.5mA
).
BOD, Watchdog et Internal voltage reference
Quelques autres périphériques sont encore désactivables, cf section 7.4 de la datasheet.
Le BOD ou Brown-out Detector surveille la tension d'alimentation et reset le microcontrôleur si elle passe en dessous d'un certain seuil. Cela permet d'éviter que le CPU continue de tourner alors que l'alimentation n'est pas nécessaire pour garantir son bon fonctionnement. Il est désactivable avec les fuses, pour le moment je ne vais pas m'étendre sur le sujet. Par défaut celui-ci n'est pas activé par la configuration des fuses faite par l'IDE Arduino.
Le watchdog on va y venir, mais en gros on en a besoin donc on le garde ! L'internal voltage reference va être désactivé automatiquement si le BOD, l'analog compartor et le ADC sont éteints.
Sleep avec reveil par le watchdog
On peut éteindre certains périphériques, ok. Mais si vraiment on veut consommer moins... on va devoir éteindre le CPU lui-même ! et donc utiliser les différents "sleep mode" disponibles.
Avant de faire faire dodo au CPU, on va prévoir ce qui va le réveiller ! Rassurez-vous dans tout les cas un "reset manuel" redémarre le CPU, mais en général dans une application on à besoin d'un réveil automatique. Plusieurs méthodes de réveil sont possibles, nous allons simplement voir (pour le moment) le réveil avec le watchdog c'est-à-dire un réveil au bout d'une durée fixe déterminée.
Configuration du watchdog
Le watchdog est un périphérique assez classique sur les microcontrôleurs. Il permet soit de déclencher un "reset logiciel" au bout d'un certain laps de temps, soit d'appeler une interruption à intervalle régulier. L'usage premier est donc une sécurité, il peut enclencher un reset automatique pour éviter que le microcontrôleur reste "bloqué". Ici nous allons l'utiliser simplement pour réveiller le cpu à intervalle régulier, avec une interruption.
Voyons donc comment configurer le watchdog pour déclencher une interruption régulière (et donc réveiller le cpu). Toutes les options de configuration du watchdog sont documentées dans la section 16.2.2 de la datasheet. N'hésitez surtout pas à vous plonger dans cette documentation de référence ! (c'est d'ailleurs vrai pour tout ce que présente ici).
Le watchdog se configure avec les registres :
MCUSR
(MCU Status Register),WDTCR
(Watchdog Timer Control Register).
Sur MCUSR
seul le bit WDRF
(Watchdog Reset Flag) est utile ici. Ce bit passe à 1
lorsqu’un reset a été provoqué par le watchdog. Il repasse à 0
par un reset manuel, ou si on le met à 0
"à la main" depuis le programme. C'est donc surtout utile pour détecter un éventuel reset par le watchdog.
C'est donc surtout avec WDTCR
que l'on configure le watchdog. Voici rapidement a quoi correspond chacun des de ces bits :
WDIF
(Watchdog Timeout Interrupt Flag) : passe à1
après un déclenchement du watchdog en mode "interruption".WDIE
(Watchdog Timeout Interrupt Enable) : si a1
alors alors le watchdog déclenche une interruption (et non un reset). Attention, il repasse automatiquement à0
à chaque déclenchement du watchdog.WDCE
(Watchdog Change Enable) : doit être forcé à1
pour autoriser un passage a0
deWDE
.WDE
(Watchdog Enable) : permet d'activer ou non le watchdog.WDP[3:0]
Watchdog Timer Prescaler 3, 2, 1, and 0) : configure la fréquence de déclenchement du watchdog.
La table 8.3 présente les différentes configurations de WDP[3:0]
et les fréquences de déclenchement associé, la voici reproduite :
Notons que le temps indiqué est relativement approximatif. En effet je mesure 4.80 secondes et non 4 avec l'avant-dernière configuration. C'est simplement que l'oscillateur interne n'est pas très précis.
De tout ça on tire deux fonctions. L'une pour activer le watchdog pour un temps donné, et pour qu'il déclenche une interruption :
volatile uint8_t wdt_count = 0;
void watchdog_start_interrupt(uint8_t wd_prescaler) {
if(wd_prescaler > 9) wd_prescaler = 9;
byte _prescaler = wd_prescaler & 0x7;
if (wd_prescaler > 7 ) _prescaler |= _BV(WDP3);
// ^ fourth bit of the prescaler is somewhere else in the register...
// set new watchdog timer prescaler value
WDTCR = _prescaler;
// start timed sequence (and activate interrupt)
WDTCR |= _BV(WDIE) | _BV(WDCE) | _BV(WDE);
}
// Watchdog Interrupt Service / is executed when watchdog timed out
ISR(WDT_vect) {
wdt_count++;
WDTCR |= _BV(WDIE); // next watchdog will go to interrupt (not reset)
}
Et enfin une pour désactiver celui-ci :
/* Turn off WDT */
void watchdog_stop() {
WDTCR |= _BV(WDCE) | _BV(WDE);
WDTCR = 0x00;
}
Et donc avec ça on peut modifier le programme de départ pour ne plus utiliser le delay(10000)
, mais simplement une attente du watchdog :
#include <avr/io.h>
#include <avr/wdt.h>
// Incremented by watchdog interrupt
volatile uint8_t wdt_count = 0;
int led = 4;
void blink(uint8_t flash){
bool state = HIGH;
for(int i = 0; i<2*flash; i++){
digitalWrite(led, state);
state = !state;
delay(120);
}
}
void setup() {
ADCSRA &= ~_BV(ADEN); // switch ADC OFF
ACSR |= _BV(ACD); // switch Analog Compartaror OFF
// initialize the LED pin as an output.
pinMode(led, OUTPUT);
}
void loop() {
blink(3);
wdt_count = 0;
watchdog_start_interrupt(6); // prescale of 6 ~= 1sec
while(wdt_count < 10); // Wait 10 watchdog interupts (~10secs)
watchdog_stop();
}
Aller op au dodo !
Il ne nous reste plus qu'a ajouter dans le while(wdt_count < 10)
une mise en veille du microcontrôleur !
Les différents sleep modes sont décrits en section 7.1 de la datasheet. Pour les utiliser nous allons passer par les fonctions et macro définies dans avr/sleep.h
de la avr-libc.
Bref, c'est très simple, on n'a besoin que de deux fonctions/macros :
set_sleep_mode()
permet de définir le sleep mode utilisé,sleep_mode()
enclenche le sleep_mode.
Pour le attiny85 trois sleep mode sont possibles : SLEEP_MODE_IDLE
, SLEEP_MODE_ADC
, SLEEP_MODE_PWR_DOWN
. En fonction du mode choisi, différentes fonctionnalités (de réveil) restent actives. Ces différentes fonctionnalités sont résumées dans le tableau 7.1 de la datasheet, reproduit ici :
On ajoute donc une configuration du sleep mode choisi dans le setup
, et un appel a sleep_mode()
dans la boucle "d'attente" du watchdog pour activer la veille. On arrive donc au programme suivant :
#include <avr/io.h>
#include <avr/wdt.h>
#include <avr/sleep.h>
// Incremented by watchdog interrupt
volatile uint8_t wdt_count;
int led = 4;
void blink(uint8_t flash){
bool state = HIGH;
for(int i = 0; i<2*flash; i++){
digitalWrite(led, state);
state = !state;
delay(120);
}
}
void setup() {
ADCSRA &= ~_BV(ADEN); // switch ADC OFF
ACSR |= _BV(ACD); // switch Analog Compartaror OFF
// Configure attiny85 sleep mode
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
// Reset watchdog interrupt counter
wdt_count = 255; //max value
// Configure pin modes
pinMode(led, OUTPUT);
}
void loop() {
blink(3);
wdt_count = 0;
watchdog_start_interrupt(6); // prescale of 6 ~= 1sec
while(wdt_count < 10){ // Wait 10 watchdog interupts (~10secs)
sleep_mode(); // Make CPU sleep until next WDT interrupt
}
watchdog_stop();
}
/* Turn off WDT */
void watchdog_stop() {
WDTCR |= _BV(WDCE) | _BV(WDE);
WDTCR = 0x00;
}
/* Turn onn WDT (with interupt) */
void watchdog_start_interrupt(uint8_t wd_prescaler) {
if(wd_prescaler > 9) wd_prescaler = 9;
byte _prescaler = wd_prescaler & 0x7;
if (wd_prescaler > 7 ) _prescaler |= _BV(WDP3);
// ^ fourth bit of the prescaler is somewhere else in the register...
// set new watchdog timer prescaler value
WDTCR = _prescaler;
// start timed sequence
WDTCR |= _BV(WDIE) | _BV(WDCE) | _BV(WDE);
}
// Watchdog Interrupt Service / is executed when watchdog timed out
ISR(WDT_vect) {
wdt_count++;
WDTCR |= _BV(WDIE); // Watchdog goes to interrupt not reset
}
Et la, en fonction du mode de veille :
- en idle on mesure
~1.75mA
, ~675µA
en mode ADC noise reduction,- et enfin on tombe à
~4.5µA
en mode power down !
On arrive donc a une consommation très faible. Si on imagine un réveil de 1 seconde toutes les 60, on peut tenir plus de 3ans sur une batterie de 2Ah ! (sans compter tout ce qui va consommer a côté du microcontrôleur : capteurs, émetteur radio ...).
Conclusions
Nous avons donc vu comment réduire la consommation d'un attiny85. Un certain nombre de petits détails ont été laissés de côté pour ne pas surcharger cet article. Je recommande donc encore une fois la lecture des documentations de références pour aller un peu plus loin sur le sujet !
Sur ce c'est à mon tour de passer en sleep mode ! ;)