Programmierung von Mikrocontrollern Energieeffizient per Software

Mittlerweile gibt es ein reichhaltiges Angebot an energieeffizienten Mikrocontrollern. Falsche Programmierung kann jedoch dazu führen, dass Systeme, die solche Bausteine nutzen, nicht so energieeffizient arbeiten, wie sie könnten oder sollten. Durch geschickte Verwendung von Peripheriekomponenten und die richtige Anwendung der Programmiersprache C lässt sich einiges an Energie einsparen.

Der wichtigste Schritt in der energieeffizienten Programmierung besteht darin, das genaue Systemverhalten und die Anforderungen an das System zu kennen. Dazu gehören vor allem die zur Verfügung stehende Energie (Batterie, Akku, Energy-Harvester, etc.) und die Stromaufnahme in den verschiedenen Modi auch über die Lebensdauer des Systems, also das Stromprofil.

Bild 1 zeigt das Stromprofil einer einfachen Anwendung. Die Realität wird sich wahrscheinlich komplexer darstellen, da vermutlich mehr Aufgaben zu erledigen sind. Neben der durchschnittlichen Stromaufnahme sollten die maximalen Ströme nicht vernachlässigt werden, da zum Beispiel der Innenwiderstand von Batterien im Laufe der Zeit zunimmt und Stromspitzen zum Ende der Batterielaufzeit einen stärkeren Spannungsabfall verursachen, was eventuell zu Ausfällen im System führt.

Eine weitere Herausforderung ist es, aus einer Vielzahl von Angaben über die Stromaufnahme die Richtige zu finden. Zwei Gruppen von Strömen werden üblicherweise angeben: Ströme im Aktivmodus und im Ruhezustand. Bei ersteren beziehen sich die Angaben auf die Ausführung eines Programms aus dem Flash, FRAM, ROM oder RAM und beinhalten meist keine Peripheriekomponenten.

Dabei ist zu beachten, dass zumindest die nötigen Oszillatoren in der angegebenen Stromaufnahme enthalten sind. Diese Ströme sind abhängig von der Taktfrequenz und meist in μA/MHz angegeben. Die größten Stromverbraucher im Mikrocontroller sind typischerweise Zugriffe auf den Flash- beziehungsweise FRAM-Speicher. Daher sollten Entwickler die Speichergröße des Mikrocontrollers optimal wählen.

Ströme im Ruhezustand sind typischerweise Leckströme, die im Ruhezustand ohne Taktung auftreten. Leckströme kommen durch die sehr hohen, aber nicht unendlichen Widerstände abgeschalteter CMOS-Transistoren zustande. Eine Möglichkeit, diese Leckströme weiter zu reduzieren, ist das so genannte Power-Gating, wobei der Mikrocontroller einige seiner Teilbereiche komplett von der Versorgung trennt. In diesen Teilen werden dabei sämtliche Zustände gelöscht und sind bei Wiederaktivierung neu zu initialisieren.

Ruheströme sind sehr stark von der Umgebungstemperatur abhängig und steigen mit der Temperatur an. Je nach Anwendung ist der Aktivstrom oder der Ruhestrom dominant und sollte für die Auswahl die größere Rolle spielen. Ein einfaches Beispiel ist eine Aktivitätsanzeige mittels LED. Nehmen wir an die LED ist für 1 ms pro Sekunde aktiv. Daraus gibt sich ein Tastverhältnis von 1000:1. Benötigt die LED 1 mA, lässt sich mit dem Tastverhältnis der durchschnittliche Diodenstrom berechnen: Id = 1 mA/1000 = 1 µA. Die LED-Aktivitätsanzeige trägt also 1 μA zum durchschnittlichen Strom bei.

Der Aufwand im Mikrocontroller kann hier vernachlässigt werden, da dessen Aktivität, um einen Port ein- oder auszuschalten, nur im Mikrosekundenbereich liegt. Diese Methode lässt sich natürlich auf alle Aktivitäten einzeln anwenden. Der gesamte Durchschnittsstrom ist demzufolge die Summe aller Einzelströme.

Verwirrung bei der Strommessung

Leider führt jeder Hersteller seine Strommessungen bei Mikrocontrollern anders durch, zudem werden die Ergebnisse oft nicht genau dokumentiert. So finden sich einfache while(1)-Schleifen, kleine Algorithmen wie Bubble-Sort oder eine Mischung verschiedener Befehle. Manche Hersteller propagieren Primzahlenberechnungen. Speziell bei der letzten Methode stellt sich die Frage, wie oft Mikrocontroller zur Primzahlenberechnung benutzt werden.

Weiterhin sind neben der CPU auch noch Peripherieblöcke sowie externe Komponenten aktiv, die ebenfalls zur Stromaufnahme beitragen. Der Kernpunkt ist, dass sich die Stromangaben nicht sehr gut vergleichen lassen und dass sie oft auch nicht direkt auf die eigene Applikation anwendbar sind. Am besten ist es, ein Programm zu verwenden, das die Applikation am genauesten widerspiegelt, und dabei den Strom zu messen.

Energieeffiziente Mikrocontroller bieten eine Vielzahl von Low-Power-, Stand-by- und Sleep-Modi. Ein Entwickler sollte die Struktur des Programms auf diese Betriebsarten ausrichten und auch so oft wie möglich nutzen. Im Extremfall findet die Programmausführung nur noch in Interrupt-Routinen statt, das Hauptprogramm führt nur das generelle System-Setup nach dem Reset aus und geht dann in einen Low-Power-Modus.

Die Low-Power-Betriebsarten beziehen sich auf eine Kombination von Clock-Gating (Takt abschalten) bis hin zum Power-Gating (Spannung abschalten), um die Stromaufnahme zu minimieren. Welche dieser Betriebsarten ein Entwickler wie oft verwenden sollte, hängt direkt mit den Aufwachzeiten zusammen. Generell gilt die Faustregel: Je niedriger die Stromaufnahme im Low-Power-Modus, umso länger die Aufwachzeit. Hier muss man schon die ersten Kompromisse treffen und die am besten zutreffenden Betriebsarten verwenden, denn auch Aufwachen kostet Zeit und damit Energie.

Nutzt ein Entwickler sehr kurze Interrupt-Routinen, sollte er mehr auf die Aufwachzeiten achten. Zugriffe auf den Speicher, speziell auf Flash, benötigen einen recht hohen Strom und sind zu minimieren. Mikrocontroller mit einer höheren Codedichte sind hier sehr hilfreich. Breite Bussysteme (32 Bit oder mehr) sind nicht immer ein Vorteil, wenn man hauptsächlich mit 8-Bit- oder 16-Bit-Daten arbeitet.

Optimiert der Entwickler Variablen im CPU-Register, so spart er Zugriffe auf das RAM und somit Strom. Optimierte Compiler erledigen diese Aufgaben meistens zuverlässig, aber falls es auf das letzte Mikroampere ankommt, sollte der Entwickler hier durch entsprechende Compiler-Direktiven eingreifen. Mikrocontroller starten typischerweise mit auf Eingang konfigurierten Portmodulen.

Da Ultra-Low-Power-Mikrocontroller auf die minimale Stromaufnahme optimiert werden, haben die Ports einen sehr hohen Eingangswiderstand. Das macht sie anfällig für Störsignale, zum Beispiel für das 50-Hz-Netzbrummen. Die Ports könnten dadurch immer zwischen zwei Zuständen schalten, deshalb sollte der Entwickler unbenutzte Ports besser als Ausgänge konfigurieren.

Für Warteschleifen Timer verwenden

Software-Warteschleifen kosten Speicherzugriffe, um die Instruk-tionen zu holen, und CPU-Rechen-leistung. Bei einer Timer-gesteuerten Warteschleife kann das System die Taktversorgung für die CPU und den Speicher abschalten. Läuft der Timer ab, werden CPU und Speicher wieder getaktet und können weiterarbeiten. Ein weiterer Nebeneffekt ist, dass man deterministisches Zeitverhalten erreicht, da die Zeitschleife nicht vom CPU-Takt beziehungsweise von CPU-Prioritäten abhängt.

Listing 1: Warteschleife
// Software Warteschleife
void main(void)
{
P1DIR |= BIT0; //Set P1.0 to output direction

for (;;)
{
P1OUT ^= BIT0; // Toggle LED on port 1.0

i = 50000; // Delay
do (i--);
while (i != 0);
}
}
// Warteschleife mit Timer
void main(void)
{
P1DIR |= BIT0; //Set P1.0 to output direction
TA0CCTL0 = CCIE; // CCR0 interrupt enabled
TA0CCR0 = 50000;
TA0CTL = TASSEL_2 + MC_1 + TACLR; // SMCLK, upmode, clear TAR

__bis_SR_register(LPM0_bits + GIE); // Enter LPM0, enable interrupts
}

// Timer0 A0 interrupt service routine
#pragma vector=TIMER0_A0_VECTOR
__interrupt void TIMER0_A0_ISR(void)
{
P1OUT ^= BIT0; // Toggle LED on port 1.0
}

Bei sehr kleinen Warteschleifen im Bereich kleiner zwanzig CPU-Takte sind Software-Warteschleifen aller-dings effizienter. Im Codebeispiel in Listing 1 sind eine softwarebasierte Warteschleife und dieselbe Funktion mit Timer und Interrupt realisiert. Dieses Beispiel und alle nachfolgenden beziehen sich auf die Mikrocontroller der Serie »MSP430F5xxx« von Texas Instruments, allerdings sind die Tipps im Prinzip auch für andere Bausteine gültig.

Die Warteschleife mit Timer benötigt nur etwa 65 µA, die Software-Warteschleife jedoch etwa 220 µA. Im Regelfall sollte man Variablen so lokal wie möglich machen. Eine Variable, die nur innerhalb einer Funktion oder Schleife verwendet wird, sollte auch nur innerhalb dieser deklariert sein. Der Compiler wird immer versuchen, CPU-Register für lokale Variablen zu benutzen, während er globale Variablen meistens im RAM-Bereich allokiert.

Dies vermeidet häufige Zugriffe auf das RAM. Manchmal helfen globale Variablen aber dabei, Parameterübergaben in Funktionen zu minimieren, und können in bestimmten Fällen effizienter sein. Die Peripherie des MSP430 kann Interrupts auslösen, sodass die CPU in Low-Power-Modi auf Ereignisse warten kann. Ein Beispiel hierzu ist der Empfang von Daten über die seriellen Schnittstellen.

Die CPU oder der DMA-Controller wird aufgeweckt, wenn der Empfangspuffer voll ist. Aber auch beim Senden von Daten über die seriellen Schnittstellen lohnt sich die Verwendung von Interrupts und Low-Power-Betriebsarten, wenn die Übertragungsrate im Vergleich zum CPU-Takt niedrig ist. Ein weiterer Vorteil des MSP430 sind die autarken Peripheriekomponenten, die Teilaufgaben auch ohne CPU-Aktivität ausführen können. Dazu zählen unter anderem serielle Schnittstellen, DMA-Controller und Analog/Digital-Wandler.

Oft sind mehrere A/D-Wandlungen nötig, deren Ergebnisse dann ein weiterführender Algorithmus verarbeitet. Dazu zählen zum Beispiel Filterfunktionen oder auch nur ein einfache Durchschnittsbildung. Im MSP430 hat der A/D-Wandler einen Datenpuffer, der bis zu 32 Werte zwischenspeichern kann. Will man den Durchschnitt bilden, kann man natürlich jeden Wert einzeln auf ein CPU-Register addieren.

Die bessere Methode ist es, alle 32 A/D-Wandlungen abzuwarten und die CPU nur kurz aufzuwecken, um die Additionen durchzuführen. Die anschließende Teilung durch 32 kann man durch ein Rechtsschieben (Shift) um fünf Stellen sehr effektiv lösen. Für Speichertransferfunktionen wie »memcpy« oder »strcpy« sollte man den DMA-Controller verwenden. Dieser führt die Kopie selbstständig ohne CPU durch.

Die Vorteile sind vielfältig, da weder Instruktionszugriffe noch Berechnungen mit der CPU durchgeführt werden, sondern der DMA-Controller autark arbeitet und mit sehr viel kleinerem Strom auskommt. Wiederkehrende Transfers lassen sich noch effizienter ausführen, da bei Wiederholungen sogar die Neuinitialisierung des DMA-Controllers entfällt.