Mikrocontroller stromsparend programmieren

Aus BraLUG-Wiki

Wechseln zu: Navigation, Suche


Inhaltsverzeichnis

Motivation

Nicht erst seitdem der Begriffe "Green IT" aktuell wurde, kümmern sich Hard­- und Softwareentwickler um die Minimierung des Energieverbrauchs ihrer Produk­te. Viele Einsatzszenarien erfordern den effizienten Umgang mit der zur Verfügung stehenden Energie. Geräte wie eine elektronische Armbanduhr oder Fernbedienung sollen nicht jeden Tag an ein Ladegerät angeschlossen werden. Ein Mobiltelefon soll in der Hosen­ oder Handtasche Platz finden. Unbemannte Messstationen, Signalbo­jen etc. müssen monatelang oder, im Extremfall, über Jahre permanent und war­tungsfrei funktionieren. Nicht immer steht eine Steckdose zur Stromversorgung in unmittelbarer Reichweite zur Verfügung.

Mit modernen Batterien und Akkumulatoren (Akkus), vielfach in Verbindung mit sogenannten "Energy Harvesting"-Energiequellen (z.B. Photovoltaik), stehen Tech­nologien zur Verfügung, die die Konzeption und den Aufbau von platz­- und ge­wichtssparenden Geräten ermöglichen. Dabei reicht es nicht aus, "Ultra­ Low Power"-­Hardware nur einfach einzusetzen. Vielmehr muss auch die Firmware die zur Verfügung gestellten Möglichkeiten nutzen.

Der Autor betreibt eine Außenstation mit diversen Sensoren zur Wetterbeobach­tung, welche drahtlos an eine Hauptstation angebunden ist. Die Energieversorgung sollte dabei unabhängig von stationären Stromquellen gelöst werden. Um dies leis­ten zu können, setzte er sich mit den Begriffen "Ultra­ Low Power" und „energieeffi­ziente Programmierung“ intensiv auseinander.

"Ultra Low Power"-Hardware

Grundsätzlich werden die elektrischen Eigenschaften von Halbleiterschaltungen, und damit auch deren Stromverbrauch, durch die eingesetzten Technologien und den damit gegebenen physikalischen Gesetzmäßigkeiten bestimmt. Die Auswahl der geeigneten elektronischen Bauteile für ein konkretes Produkt ist eine verantwortungsvolle Aufgabe. Diese wird meist dem Hardwareentwickler überlassen, der diese Zusammenhänge kennen sollte. Einige dieser Eigenschaften sind aber auch für den Softwareentwickler interessant.

Viele moderne MCUs bieten die Möglichkeit die interne CPU und weitere interne Peripherie­-Komponenten (z.B. ADC, DMA, WDT) unabhängig voneinander, teil­weise oder vollständig abzuschalten. Dazu sind softwareseitig meist nur entspre­chende Steuerbits in den dafür vorgesehenen MCU­-Registern (siehe Datenblätter) zu manipulieren. Durch vorher konfigurierte interne und externe Ereignisse kann dieser Ruhezustand jederzeit wieder verlassen werden. Ähnliches gilt auch für viele externe Komponenten und Baugruppen diverser Hersteller.

Dabei wird der Stromverbrauch teilweise drastisch gesenkt. Im Fall einer ATmega8L­-MCU reduziert sich der Bedarf beispielsweise von 3mA im Aktiv­-Modus auf unter 1μA bei der Abschaltung der CPU und aller weiteren inter­nen Komponenten. Weitere Stromeinsparungen können durch die Absenkung der CPU­-Taktfrequenz (z.B. ATmega8L mit 3V Versorgungsspannung: 8MHz, 6mA → 4MHz, 3mA) und die Minimierung der Zugriffe auf die verschiedenen Speicherbe­reiche (RAM, FLASH, EEPROM etc.) außerhalb der CPU erzielt werden.

Stromeffiziente Firmware entwickeln

Der überwiegende Teil, der durch eine MCU abzuarbeitenden Aufgaben lässt sich mit dem EVA-­Prinzip beschreiben. Es tritt ein Ereignis (Signal, Timer etc.) ein, auf welches der Prozessor entsprechend reagieren muss (Daten ermitteln, Berechnun­gen etc.). Die Verarbeitung endet mit einer geeigneten Ausgabe des Ergebnisses. Eine ausreichend schnelle MCU vorausgesetzt, wird man feststellen, dass zwischen diesen „EVA­-Zyklen“ Wartezeiten entstehen. Der Prozessor und weitere nicht benö­tigte Peripherie kann abgeschaltet und damit in einen der zur Verfügung stehenden stromsparenden Zustände versetzt werden. Dies bedeutet, kurze Verarbeitungszei­ten innerhalb der MCU verlängert deren Wartezeiten und reduziert damit den Strombedarf des Gesamtsystems.

Für ihre Entwicklungsumgebung „Code Composer Studio“ bietet die Firma Texas Instruments das Analysetool „ULPAdvisor“ an. Dieses Werkzeug ist auf die Ana­lyse von C-­Quelltexten für die hauseigene MCU­-Familie MSP430 zugeschnitten. Die zugrunde liegenden Kriterien sind aber ohne weiteres verallgemeinerbar und damit auch auf vergleichbare Hardwareprodukte anderer Hersteller anwendbar. Die fol­genden Empfehlungen beziehen sich deshalb auf die implementierten Analysekrite­rien des „ULPAdvisor“.

Nutze die Stromspar­-Modi der MCU, wann immer es geht

In einem der „Low Power“­-Betriebsmodi verbraucht eine MCU am wenigsten Strom. Deshalb sollte einer dieser Zustände immer Ausgangs­ und Endpunkt einer jeden Verarbeitung sein. Die Realisierung dieser wichtigen Empfehlung erfordert bereits in der Konzeptionsphase eine gewissenhafte Planung der Softwarestruktur:

ungünstig besser

Low power pap bad.png

Low power pap good.png

Damit unterscheidet sich die Steuerung der Software von den üblichen Konzepten. Statt ständig aktiv Zustände abzufragen, wartet man auf deren Auftreten und akti­viert erst dann die notwendigen Komponenten.

Hinweis: Als Schlafmodus ist jeweils derjenige zu wählen, der ein Wecken durch den oder die zu erwartenden Interrupt-­Ereignisse zulässt. Welche das sind, ist in den entsprechenden Datenblättern der jeweiligen MCU-­Hersteller zu finden.

Verwende Interrupts statt „Flag­-Polling“

Viele der internen Peripherie­-Baugruppen einer MCU spiegeln ihren momentan Zustand mit Hilfe von Status­Bits (Flags) wider. Stößt man beispielsweise auf einem ATmega8 eine A/D­-Wandlung an, wird das Ende der Messung mit einem Low­-Wert des ADSC-­Bit des ADCSRA­-Register gemeldet. Das Löschen dieses Flags könnte man mit einer while-­Schleife im Programm abfragen. Dabei ist aber die CPU ständig aktiv und verbraucht viel Strom.

ungünstig besser
uint16_t  adc_read(uint8_t channel) 
{
   ADMUX |= (channel & 0x1F);
   ADCSRA |= (1<<ADSC);
   while (ADCSRA & (1<<ADSC)) {}
   return ADCW; 
}
uint16_t  adc_read(uint8_t channel) 
{
  ADMUX |= (channel & 0x1F);
  set_sleep_mode(SLEEP_MODE_ADC);
  ADCSRA |= (1<<ADSC)|(1<<ADIE);
  sleep_mode();
  return ADCW; 
}

ISR(ADC_vect)
{
}

Besser ist es, vor Anstoß der A/D-­Messung den ADC-­Interrupt zu aktivieren. Jetzt kann man die MCU in den „ADC Noise Reduction“­Schlafmodus versetzen (Stromverbrauch ca. 0,3mA) oder währenddessen andere Aufgaben erledigen las­sen, um die Verarbeitungszeit insgesamt zu verkürzen. Ist die A/D­-Wandlung been­det, wird automatisch die ADC­-Interrupt-­Routine gestartet. Es können nun die er­mittelten ADC­-Werte verarbeitet werden. Ähnliches ist auch mit vielen anderen Komponenten diverser MCU möglich. Für Einzelheiten wird auf die jeweiligen Da­tenblätter verwiesen.

Verwende Timer statt Pausenschleifen

Viele Softwareentwickler realisieren zeitlich definierte Programmpausen mit while­-, until­- oder for­-Schleifen. Dabei muss die CPU der MCU aber aktiv sein und ständig die entsprechenden Schleifenbefehle ausführen. Zielführender ist es, einen der internen MCU­-Timer so zu konfigurieren, dass nach der gewünschten Pausendauer ein Interrupt ausgeführt wird. Während der Pause schaltet man in einen der möglichen Schlafmodi.

ungünstig besser
void long_delay(uint16_t ms) 
{
    for(; ms>0; ms--) _delay_ms(1);
}

int main(void) 
{
    DDRC |= (1<<PC1);
    while(1) 
    {
       PORTC ^= (1<<PC1);
       long_delay_ms(500);
    }
}
int main(void) 
{
    DDRC |= (1<<PC1);
    TCCR1B |= (1 << WGM12);
    TIMSK |= (1 << OCIE1A);
    OCR1A   = 7812;
    sei();
    while(1) 
    {
        // was anderes machen...
        // … oder Sleep-Mode...
    }
}

ISR(TIMER1_COMPA_vect) 
{
    PORTC ^= (1<<PC1);
}

Hinweis: Bei sehr kurzen Pausen muss man abwägen, ob sich das Umschalten lohnt, da sowohl hierfür als auch das Wecken ebenfalls Rechenzeit benötigt wird.

Vermeide Funktionsaufrufe innerhalb von Interrupt­-Routinen

Aus den bisherigen Punkten ist erkennbar, dass Interrupt-­Routinen eine zentrale Rolle bei der Steuerung stromeffizienter Firmware spielen. Durch sie sollen vor al­lem schnelle Reaktionszeiten auf interne und externe Zustände realisiert werden. Dazu müssen Interrupt­-Routinen möglichst kurz sein und dürfen sich nicht über­lappen. Im Idealfall setzt man dort nur Flags, auf deren Zustände später im Haupt­programm entsprechend reagiert wird.

ungünstig besser
int main(void) 
{
    // INT0 konfigurieren...
    while(1) 
    {
        // ...mache etwas
    }
}

ISR(INT0_vect) 
{
    printf(“Signal an INT0-Pin!\n“);
}
volatile uint8_t flag=0;

int main(void) 
{
    // INT0 kongigurieren
    while(1) {
        if (flag) 
        {
            printf(“Signal an INT0-Pin!\n“);  
            flag=0;
        }
        // ...mache etwas
    }
}

ISR(INT0_vect) 
{
    flag=1;
}

Hinweis: Diese Flags sollten als volatile Variablen deklariert sein.

Vermeide rechenintensive Operationen

Auf MCUs ohne FPU sollten Floating­-Point­-Berechnungen vermieden werden, da hierfür sehr langer Maschinencode vom Compiler generiert wird. Dessen Abarbei­tung verkürzt die Verweildauer in einem der stromsparenden Modi. Gleiches gilt auch für Ganzzahlmultiplikationen ohne Hardware­-Multiplizierer, Modulo­-/Divi­sions­-Operationen und viele weitere mathematische Funktionen. Jede Verwendung im Programmcode sollte deshalb kritisch überdacht und auf ein Minimum be­schränkt werden. Für einige MCU­-Plattformen existieren speziell angepasste Ma­thematik­-Bibliotheken. Weiterhin sind beispielsweise Festkomma­-Arithmetik und Lookup­-Tabellen ebenfalls geeignete Mittel zur Code-­Reduzierung.

Aus diesen Gründen sollte man außerdem die Verwendung von „Universal“­-Funk­tionen (z.B. printf(), sprintf() und String­-Funktionen) aus den C-­Standard­-Bibliothe­ken vermeiden. Besser ist hier eine Neu­-Implementierung mit genau dem Funkti­onsumfang, der in dem konkreten Anwendungsfall tatsächlich benötigt wird.

Verwende, wenn vorhanden, DMA

Steht in der MCU ein DMA­-Controller zur Verfügung, ist dieser zum Transfer großer Datenmengen zwischen zwei Speicherbereichen (z.B. anstatt der C­-Funktion memcpy()) oder interner Peripherie und Speicher (z.B. Senden/Empfangen über die serieller Schnittstelle) zu verwenden. Während des DMA­-Betriebs kann die CPU abgeschaltet und damit insgesamt Strom gespart werden.

ungünstig besser
#define LEN 100
char str1[LEN], str2[LEN];
uin8_t i;

...

for (i=LEN; i>0; i--) 
{
    str2[i-1]=str1[i-1];
}

...

memcpy(str2, str1, LEN);

...

#define LEN 100
char str1[LEN], str2[LEN];

...

DMA0DA = (unsigned int)str2;
DMA0SA = (unsigned int)str1;
DMA0SZ = LEN;
DMA0CTL |= DMAEN;
DMA0CTL |= DMAREQ;      
while (!(DMA0CTL & DMAIFG)) ;

...

(Beispiel MSP430Fxxxx: große Variable kopieren)

Hinweis: die while-Schleife sollte konsequenterweise natürlich durch den entsprechenden DMA-Interrupt ersetzt werden.

Benutze, wenn möglich, lokale statt globale Variablen

Viele Compiler versuchen bei der Übersetzung lokale Funktionsvariablen freien Re­gistern der CPU zuzuordnen. Der daraus resultierende Maschinencode ist kürzer und damit schneller. Es entfällt das Umkopieren vom Speicher in die CPU­-Register.

ungünstig besser
uint8_t i;

...

void do_it(void) 
{
    for (i=0; i<10; i++) 
    {
        Printf(“%d\n“, i);
    }
}
void do_it(void) 
{
    uint8_t i;
    for (i=0; i<10; i++) 
    {
        Printf(“%d\n“, i);
    }
}

Verwende „call by reference“ bei großen Variablen

Wenn Funktionsparameter über den Zeiger auf ihre (globale) Speicheradresse refe­renziert werden („call by reference“), generiert der Compiler keinen Maschinencode zum Umkopieren der entsprechenden Speicherinhalte in die CPU­-Register bzw. den Stack. Es reduziert sich die aktive Zeit der CPU. Diese Empfehlung steht im Widerspruch zur vorhergehenden Regel, weil hierfür global deklarierte Variablen Voraussetzung sind. Hier muss der Softwareentwickler entscheiden bzw. experimentieren, welche der beiden Empfehlungen zum ge­wünschten Ergebnis führt. Bei kleineren Variablengrößen ist meist „pass by value“ vorzuziehen.

ungünstig besser
void call_by_value(int z) 
{
    printf(“Antwort %d“, z);
}

int answer = 42;

...

pass_by_value(answer);

...

void call_by_reference(int *z) 
{
    printf(“Anwort %d“, *z);
}

int answer = 42;

...

pass_by_reference(&answer);

...

Verwende „const“ und „static“ bei Variablen­-Deklarationen

Wenn lokale Variablen in einer Funktion als "static" deklariert sind, werden sie nur einmal erzeugt und stehen während der gesamten Lebensdauer der Anwendung zur Verfügung. Dies reduziert den Maschinencode um den Teil, welcher sonst bei jedem Funktionsaufruf zur Reservierung und Initialisierung des entsprechenden Speicherbereiches ausgeführt werden muss.

Für Variablen, die mit dem Typ-­Qualifizierer „const“ deklariert sind, wird kein Ma­schinencode zum Umkopieren vom Programmspeicher in den dynamischen Speicherbereich erzeugt. Der Inhalt dieser Variablen wird direkt aus dem Pro­grammspeicherbereich gelesen, ist aber damit auch nicht veränderbar.

ungünstig besser
void do_it(void) 
{
    uint8_t answer=42;
    uint8_t zahl=23;
    printf(“%d statt %d?\n“, zahl, answer);
}

...

for (i=10; i>0; i--) 
{
    do_it();
}
void do_it(void) 
{
    const uint8_t answer=42;
    static uint8_t zahl=23;
    printf(“%d statt %d?\n“, zahl, answer);
}

...

for (i=10; i>0; i--) 
{
    do_it();
}

In beiden Fällen wird, durch die Reduzierung des auszuführenden Maschinenco­des, die Dauer des stromverbrauchenden Aktiv-­Modus der CPU verringert.

Verwende den "ausreichenden" Variablentyp

Jede Rechnung mit vorzeichenbehafteten Variablen erzeugt zusätzlichen Maschi­nencode zur Überprüfung der Wertegrenzen, der die Aktivität der CPU verlängert. Aus diesem Grund sollte jede Variablendeklarationen nach ihrem voraussichtlichen Wertebereich kritisch beurteilt werden. Feld­-Indizes können beispielsweise in C nur positive Werte annehmen.

ungünstig besser
#define LEN 100
char a[LEN];
int i;

...

for (i=0; i<LEN; i++) 
{
    printf(a[i]);
}
#define LEN 100
char a[LEN];
uint8_t i;

...

for (i=0; i<LEN; i++) 
{
    printf(a[i]);
}

Hinweis: Ähnliches gilt auch für Variablen, deren Größe die Registerbreite der CPU überschreiten.

Verwende Bitmasken statt Bitfelder

Programmquelltext mit Zugriffen auf deklarierte Bitfelder ist zwar lesbarer, aber der daraus resultierende Maschinencode meist ineffizient. Besonders der Zugriff auf einzelne Bits eines Registers oder Variablen über Bitmasken kann vom Compi­ler meist zu einem Maschinenbefehl zusammengefasst werden. Ergebnis ist ein ins­gesamt kürzerer Maschinencode.

ungünstig besser
struct status_t 
{
    unsigned s1    : 1;
    unsigned s2    : 1;
    unsigned s3    : 1
    unsigned res   : 5;
} status; 

...

status.s1 = 1;
status.s2 = 1;
if (status.s2) status.s3=0;

...
#define S1   1
#define S2   2
#define S3   4

uint8_t status;

...

status |= S1 | S2;
if (status & S2) status &= ~S3;

...

Zähle in bedingten Schleifen rückwärts statt vorwärts

Diese Empfehlung mag auf den ersten Blick etwas ungewöhnlich erscheinen. Aber für jedes Inkrementieren einer Zählervariable in einer for-­Schleife muss der Compi­ler in der Mehrzahl der Fälle einen zusätzlichen Maschinenbefehl einfügen. Es muss bei jedem Durchlauf die, vom Softwareentwickler festgelegte, obere Grenze der Schleife abgefragt werden, die eventuell eine Weiterverzweigung bedingen könnte. Viele Maschinencode­-Befehlssätze kennen aber einen Befehl „Verzweige, wenn Er­gebnis gleich 0“. Besonders bei Schleifen mit vielen Durchläufen macht sich diese Reduzierung der Programmgröße hinsichtlich des Stromverbrauchs positiv bemerk­bar.

ungünstig besser
uint16_t i;

...

for (i=0; i<10000; i++) 
{
    // mache etwas...
}

...
uint16_t i;

...

for (i=10000-1; i=0; i--) 
{
    // mache etwas...
}

...

Wie lange reicht eine Batterie-Ladung?

Nachdem nun die wichtigsten verallgemeinerbaren Empfehlungen zur Implemen­tierung stromsparender Firmware besprochen wurden, stellt sich die abschließende Frage nach der einzusetzenden Energiequelle. Wichtigstes Kriterium ist dabei die Dauer des geplanten wartungsfreien Betriebs des Gerätes. Vernachlässigt man da­bei die Lebensdauer der eingesetzten elektronischen Bauelemente, wird das War­tungsintervall bei autarken Baugruppen durch die Kapazität der Spannungsversor­gung bestimmt. Wünschenswert wäre eine permanente Stromversorgung über die gesamte Lebensdauer des Gerätes. Dem gegenüber stehen aber die Grenzen, die durch das Gerätedesign (z.B. Gewicht, Größe und Aussehen) bestimmt werden.

Heißt „permanent“ aber wirklich immer „für immer und ewig“? Die tatsächliche Nutzungsdauer elektronischer Geräte ist auf Grund verschiedener Faktoren be­grenzt. Spätestens nach ca. 10 Jahren wird man in der Regel über den Ersatz nach­denken, da Abnutzungserscheinungen sowie technologische Weiterentwicklung zu weit fortgeschritten sind. Preiswerte Elektronik aus der Massenproduktion wird man in der Regel nicht länger wie 2­-3 Jahre nutzen. In diesem Zusammenhang soll­te auch der Begriff „geplante Obzoleszenz“ erwähnt werden. Eine ausreichende Stromversorgung über mehrere Wochen, Monate oder Jahre aus ein und der selben Batterie kann dabei, je nach Anwendungsfall, ausreichend und zumutbar sein.

Beispielrechnung: Batterielebensdauer

Der maßgebliche Kennwert zur Angabe des „Energiegehaltes“ einer Batterie ist ihre Kapazität. Diese wird in Amperestunden angeben. Hier einige ausgewählte Kenn­zahlen häufig eingesetzter Primärbatterie­-Typen:

AA (2x) CR2032 CR123A
Nennspannung 2x1,5V=3V 3V 3V
Kapazität 2500mAh 220mAh 1500mAh
Kapazität bis 2,55V 1250mAh 220mAh 1500mAh
Selbstentladung (21°C) 3% 1% 1%


Beispielrechnung:

  • Stromverbrauch bei 3V Versorgungsspannung:
    • Aktiv-Modus: 30mA
    • Schlaf-Modus: 1μA (erst mal vernachlässigbar...)
  • 2 AA-Batterien bis 2,55V → 1250mAh
    • 30mA permanent: 1250mAh/30mA=41,6h
  • Verhältnis Aktiv-/Schlaf-Modus: 0,5s/60s
    • 1h=60min → 60*0,5s=30s → 0,83% einer Stunde
  • Resultierende Batterielebensdauer:
    • 41.6h/0,0083=5012h → 209d → 6,97 Monate

Was wäre wenn?

Unter den gleichen Rahmenbedingungen würde eine CR2032-Batterie ca. 1,23 Monate und eine CR123A über 8 Monate reichen. Könnte man die Stromaufnahme im Aktiv-Modus um 50% senken, würde sich die Batterielebensdauer auf über ein Jahr verlängern. Halbiert man die Aktiv-Zeit der Schaltung (bzw. verdoppelt man die Zeit im Schlaf-Modus), verlängert sich die Lebensdauer der Batterie ebenfalls auf über ein Jahr.

Die „10-Jahre-Batterie“

Wieviel Strom darf permanent entnommen werden, wenn eine Batterie 10 Jahre ausreichen soll?

  • 2xAA → 9,8μA
  • CR2032 → 2,2μA
  • CR123A → 15,4μA

Kontakt

Uwe

'Persönliche Werkzeuge