Stromsparen am Arduino II. - Der Sleep-Modus

Um in Projekten, die auf dem Arduino basieren, Strom zu sparen, um eine möglichst lange Batterielaufzeit zu ermöglichen wurden im ersten Teil schon einige Möglichkeiten beschrieben. In diesem Beitrag geht es um den sogenannten "Sleep-Modus". Dies bezeichnet den Zustand des Mikrocontrollers, in dem verschiedene Funktionsblöcke wie Timer, UART, AD-Wandler etc. deaktiviert sind.

Hinweis: Hier wir der Sleep-Modus (auch damit verbundene Strom-Messungen) mit einem Arduino-Board vorgenommen, welches zur Entwicklung von Prototypen vorgesehen ist. Es soll also nur das Einspar-Potential aufgezeigt werden. In Projekten, die wirklich mit jedem mA rechnen müssen, sind daher diese Development-Boards nicht geeignet und man sollte nach besser optimierten Lösungen suchen, die von sich aus schon weniger Strom verbrauchen.

Einsatz

Der Sleep-Modus ist vor allem bei Anwendungen sinnvoll, die zum einen im mobilen Einsatz sind, d.h. von einer Batterie oder Akku abhängen und zum Anderen, die nicht permanent die Rechenleistung des Mikrocontrollers benötigen, z.B. Erfassung von Sensordaten über eine längere Zeit in bestimmten Intervallen. Dabei werden meist relativ wenig Daten in langen Zeitabständen aufgezeichnet, gespeichert oder übertragen. Zwischendurch gibt es für den Mikrocontroller meist nichts zu tun, daher kann man ihn während dieser Zeiten schlafen legen, denn dies kann eine Menge Strom einsparen.

Funktionsweise

Nach der Initialisierung setup() des Mikrocontrollers und evtl. angeschlossener externer Module (LCD, SD-Karte, o.ä.) geht der Prozessor in eine Endlosschleife loop(), in welcher die verschiedenen Aufgaben zyklisch abgearbeitet werden. Wenn jedoch gerade nichts zu tun ist geht der Prozessor schlafen. Ein Interrupt weckt ihn, der Interrupt wird ausgeführt und er setzt seine Arbeit in der Hauptschleife fort. Das heißt aber auch, dass vor dem Eintritt in den Sleep-Mode die jeweiligen Interrupts freigegeben werden müssen und auch die entsprechenden Interruptroutinen bereitgestellt werden müssen. Sonst gibt es im wahrsten Sinne des Wortes ein böses Erwachen oder einen endlosen Dornröschenschlaf, der nur durch ein Reset des Mikrocontrollers beendet werden kann.
Bevor man den Sleep-Modus aktiviert, sollte man möglichst alle Module abschalten, welche nicht gebraucht werden, um wertvollen Strom zu sparen. Dazu zählen:

Typen des Sleep-Modus

Der Atmel ATmega328P auf dem Arduino Uno verfügt über 6 verschiedene Typen des Sleep-Modus. Je effektiver der Schlafmodus ist, desto weniger Komponenten sind aktiv, wie die folgende Tabelle aus dem Datenblatt des ATmega328P zeigt:

Tabelle der Sleep-Modi des Atmel ATmega328P
Abb.: Tabelle der Sleep-Modi des Atmel ATmega328P

Idle

Wenn keine Berechnungen erfolgen sollen, Timer, UART, ADC etc. aber aktiv bleiben sollen, ist dieser Modus geeignet. CPU und Flash stoppen, der Stromverbrauch sinkt auf ca. 0,35 mA. Aus diesem Modus kann jeder Interrupt die CPU wieder wecken. Sie ist nach 6 Takten wieder voll einsatzfähig.

ADC Noise Reduction

Hier wird zusätzlich zum Idle Mode der Takt für die IO-Komponenten abgeschaltet. Nur noch der AD-Wandler, die externen Interrupts, das TWI und der Watchdog sind funktionsfähig. Der UART, Timer0 und Timer1 sind nicht mehr nutzbar.

Power-save

Hier werden fast alle Oszillatoren mit Ausnahme von Timer2 gestoppt. Dieser Timer könnte bei Bedarf mit einem externen 32,768kHz-Quarz betrieben werden. Ist er entsprechend konfiguriert, dann bleibt er beim Einschalten des Power-save Modus aktiv.

Standby

In diesem Modus werden alle Takte des Controllers gestoppt, nur der Hauptoszillator läuft weiter. Das kostet zwar etwas Energie, aber dadurch ist die CPU nach dem Aufwachen nach nur sechs Takten wieder bereit. Weil alle Oszillatoren gestoppt sind, funktionieren nur noch die asynchronen Komponenten.

Power-down

Dies ist sozusagen der Winterschlaf des Mikrocontrollers. Es werden alle Oszillatoren gestoppt (außer dem Watchdog-Takt, falls aktiviert). Nur asynchrone Komponenten funktionieren jetzt noch und können den schlafenden Controller wecken.

Verwendete Bauteile

Aufwecken

Aufwecken durch externen Interrupt

In diesem Beispiel wird gezeigt, wie man den Arduino über einen externen Interrupt weckt. Der Atmega168 besitzt zwei Interrupts INT0 (liegt an Pin 2) und INT1 (liegt an Pin 3), wobei hier Pin 2 als Trigger mit einem angeschlossenen Taster verwendet wird. Im Sketch muss noch eine ISR (Interrupt service routine) implementiert werden, so dass der Mikrocontroller bei einem LOW-Signal des Tasters wieder aufwacht. Es gibt für externe Interrupts auch noch drei weitere Trigger-Modi (CHANGE, RISING und FALLING), die hier nicht benutzt werden.

Aufbau

Aufbau zum Aufwecken durch externen Interrupt

Sketch

Der folgende Sketch verwendet für den Sleep-Mode die Library sleep.h, die standardmäßig schon in der Arduino-IDE vorhanden ist.
Der Sketch lässt eine LED für eine gewisse Zeit leuchten (default: 5 Sekunden). Sobald der Trigger (hier: Taster) ausgelöst wird, wird der LED-Timer wieder zurückgesetzt und die Leuchtdauer verlängert sich. Läuft der LED-Timer aus, so erlischt die LED und der Arduino fährt in den Power-down-Modus. Wird nun der Trigger wieder ausgelöst, so erwacht der Mikrocontroller, der LED-Timer wird zurückgesetzt und die LED leuchtet erneut.

#include <avr/sleep.h>

#define PIN_TRIGGER  2
#define PIN_LED      8

#define LIGHT_TIME   5 // in s
#define lmillis() ((long)millis())

bool isLedLit;
byte tiltStatus = LOW, lastTiltStatus = LOW;
int ledTimer = 0;
long lastAction = 0;

void isrAwake(void)
{
    detachInterrupt(0);
}

void enterSleepMode(void)
{
    attachInterrupt(digitalPinToInterrupt(PIN_TRIGGER), isrAwake, LOW);
    set_sleep_mode(SLEEP_MODE_PWR_DOWN);
    sleep_enable();
    sleep_mode();
    sleep_disable();
}

void lightLED()
{
    digitalWrite(PIN_LED, HIGH);
    isLedLit = true;
    ledTimer = LIGHT_TIME;
}

void setup()
{
    pinMode(PIN_TRIGGER, INPUT_PULLUP);
    pinMode(PIN_LED, OUTPUT);

    pinMode(13, OUTPUT);
    digitalWrite(13, LOW);

    lightLED();
    lastAction = lmillis();
}

void loop()
{
    if (isLedLit && ledTimer <= 0) {
        digitalWrite(PIN_LED, LOW);
        isLedLit = false;
        enterSleepMode();
    }

    tiltStatus = digitalRead(PIN_TRIGGER);
    if (tiltStatus != lastTiltStatus && ledTimer < LIGHT_TIME) {
        lastTiltStatus = tiltStatus;
        lightLED();
    }

    if (ledTimer > 0 && lmillis() - lastAction >= 0) {
        lastAction = lmillis() + 1000;
        ledTimer--;
    }
}

Nachdem statt der USB-Spannungsversorgung des PCs eine externe 5V-Spannungsversorgung mit einem 18650 Li-Ionen-Akku über den Pin Vin und GND angelegt wurde, konnte der Unterschied in der Stromaufnahme deutlich gemessen werden:

Will man beispielsweise zu Debug-Zwecken durch Aufrufe von Serial.print() Ausgaben auf der seriellen Konsole erzeugen, sollte man immer nach jeder Ausgabe eine kleine Verzögerung von etwa 100ms einbauen, damit die serielle Kommunikation abgeschlossen ist, bevor der Mikrocontroller schlafen geht!

Aufwecken durch den Watchdog

Der Funktionalität des Watchdog-Timers wurde schon in einem früheren Beitrag besprochen. Wir verwenden ihn hier als Trigger zum Aufwecken des Mikrocontrollers aus dem Sleep-Mode.
Der Watchdog-Timer (WDT) auf dem Arduino hat einen eigenen internen 128kHz-Oszillator, der es dem WDT ermöglicht, sogar im niedrigsten Leistungsmodus Power-down zu arbeiten. Allerdings kann der Prozessor bei dieser Art des Aufweckens nur maximal 8 Sekunden am Stück in den Schlaf geschickt werden (Minimum sind 16ms). Durch Iteration kann dies jedoch je nach Anwendung zu enormer Stromersparnis führen.
Der WDT bietet 3 Möglichkeiten:

WDTON WDE WDIE Modus Aktion bei Timeout
1 0 0 gestoppt keine
1 0 1 Interrupt-Modus Interrupt
1 1 0 Systemreset-Modus Reset
1 1 1 Interrupt- und Systemreset-Modus Interrupt, anschließend Reset
0 x x Systemreset-Modus Reset
Tabelle mit den zu setzenden Bits des Prescalers für die Dauer des WDT
Abb.: Tabelle mit den zu setzenden Bits des Prescalers für die Dauer des WDT

In diesem Beispiel wird alle 2 Sekunden ein WDT-Interrupt ausgelöst, der die LED umschaltet und dann den Mikrocontroller wieder schlafen legt. Hier muss die globale Variable toggle als volatile deklariert werden.

#include <avr/sleep.h>
#include <avr/power.h>
#include <avr/wdt.h>

#define LED_PIN 8

volatile bool toggle = true;

// Watchdog imer Interrupt Service Routine
ISR(WDT_vect)
{
    toggle = true;
}

void enterSleepMode(void)
{
    set_sleep_mode(SLEEP_MODE_PWR_SAVE);
    sleep_enable();
    power_adc_disable();    // disable analog inputs
    power_spi_disable();
    power_timer0_disable();
    power_timer2_disable();
    power_twi_disable();
    sleep_mode();
    sleep_disable();
    power_all_enable(); // enable everything
}

void setup()
{
    pinMode(LED_PIN, OUTPUT);

    // setup of the WDT
    MCUSR &= ~(1 << WDRF); // remove reset flag
    WDTCSR |= (1 << WDCE) | (1 << WDE); // set WDCE, access prescaler
    WDTCSR = 1 << WDP0 | 1 << WDP1 | 1 << WDP2; // set prescaler bits to to 2s
    WDTCSR |= 1 << WDIE; // access WDT interrupt
}

void loop()
{
    if (toggle) {
        toggle = false;
        digitalWrite(LED_PIN, !digitalRead(LED_PIN));
        enterSleepMode();
    }
}

Aufwecken durch einen Timer

Ähnlich dem Watchdog kann auch ein normaler Timer verwendet werden, den Sleep-Mode zu beenden.

#include <avr/sleep.h>
#include <avr/power.h>

#define LED_PIN 8

volatile bool toggle = true;

ISR(TIMER1_OVF_vect)
{
    toggle = true;
}

void enterSleepMode(void)
{
    set_sleep_mode(SLEEP_MODE_IDLE);
    sleep_enable();
    power_adc_disable();
    power_spi_disable();
    power_timer0_disable();
    power_timer2_disable();
    power_twi_disable();
    sleep_mode();
    sleep_disable();
    power_all_enable();
}

void setup()
{
    pinMode(LED_PIN, OUTPUT);

    // configure timer1
    TCCR1A = 0x00;
    TCNT1 = 0x0000; // reset timer
    TCCR1B = 0x05;  // prescaler to 1024
    TIMSK1 = 0x01;  // enable timer interrupt
}

void loop()
{
    if (toggle)  {
        toggle = false;
        digitalWrite(LED_PIN, ! digitalRead(LED_PIN));
        enterSleepMode();
    }
}

Sleep-Modus mit "LowPower.h"

Eine leichtgewichtige Library für das Versetzen des Arduinos in den Tiefschlaf ist die LowPower. Der folgende Beispiel-Sketch lässt zunächst eine LED an Pin 8 für eine Sekunde aufleuchten, dann wieder erlöschen und versetzt den Arduino für 8 Sekunden in Sleep-Mode. Danach wacht der Arduino wieder auf und der loop() beginnt von vorne.

#include <LowPower.h>

#define PIN_LED 8

void setup()
{
    pinMode(PIN_LED, OUTPUT);
}

void loop()
{
    digitalWrite(PIN_LED, HIGH);
    delay(1000);
    digitalWrite(PIN_LED, LOW);

    LowPower.idle(SLEEP_8S, ADC_OFF, TIMER2_OFF, TIMER1_OFF, TIMER0_OFF, SPI_OFF, USART0_OFF, TWI_OFF);
}

Weiterführende Links

zurück