Mikrocontroller stromsparend programmieren
Aus BraLUG-Wiki
...derzeit in Arbeit!
Motivation
Nicht erst seitdem der Begriffe "Green IT" aktuell wurde, kümmern sich Hard- und Softwareentwickler um die Minimierung des Energieverbrauchs ihrer Produkte. 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, Signalbojen etc. müssen monatelang oder, im Extremfall, über Jahre permanent und wartungsfrei 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 Technologien zur Verfügung, die die Konzeption und den Aufbau von platz und gewichtssparenden 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 Wetterbeobachtung, welche drahtlos an eine Hauptstation angebunden ist. Die Energieversorgung sollte dabei unabhängig von stationären Stromquellen gelöst werden. Um dies leisten zu können, setzte er sich mit den Begriffen "Ultra Low Power" und „energieeffiziente 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, teilweise oder vollständig abzuschalten. Dazu sind softwareseitig meist nur entsprechende Steuerbits in den dafür vorgesehenen MCURegistern (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 internen 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 Speicherbereiche (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, Berechnungen 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 Verarbeitungszeiten 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 Analyse 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 folgenden Empfehlungen beziehen sich deshalb auf die implementierten Analysekriterien 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:
Tabelle mit Bilder...
Damit unterscheidet sich die Steuerung der Software von den üblichen Konzepten. Statt ständig aktiv Zustände abzufragen, wartet man auf deren Auftreten und aktiviert erst dann die notwendigen Komponenten.
Hinweis: Als Schlafmodus ist jeweils derjenige zu wählen, der ein Wecken durch den oder die zu erwartenden InterruptEreignisse zulässt14. Welche das sind, ist in den entsprechenden Datenblättern der jeweiligen MCUHersteller zu finden.
Verwende Interrupts statt „Flag-Polling“
Viele der internen Peripherie-Baugruppen einer MCU spiegeln ihren momentan Zustand mit Hilfe von StatusBits (Flags) wider. Stößt man beispielsweise auf einem ATmega815 eine A/DWandlung an, wird das Ende der Messung mit einem Low-Wert des ADSCBit 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 16 versetzen (Stromverbrauch ca. 0,3mA) oder währenddessen andere Aufgaben erledigen lassen, um die Verarbeitungszeit insgesamt zu verkürzen. Ist die A/D-Wandlung beendet, wird automatisch die ADC-Interrupt-Routine gestartet. Es können nun die ermittelten ADC-Werte verarbeitet werden. Ähnliches ist auch mit vielen anderen Komponenten diverser MCU möglich. Für Einzelheiten wird auf die jeweiligen Datenblä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 allem schnelle Reaktionszeiten auf interne und externe Zustände realisiert werden. Dazu müssen Interrupt-Routinen möglichst kurz sein und dürfen sich nicht überlappen. Im Idealfall setzt man dort nur Flags, auf deren Zustände später im Hauptprogramm 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
...
Verwende, wenn vorhanden, DMA
...
Benutze, wenn möglich, lokale statt globale Variablen
...
Verwende „call by reference“ bei großen Variablen
...
Verwende „const“ und „static“ bei Variablen-Deklarationen
...
Verwende keine vorzeichenbehafteten Variablen, wenn nicht erforderlich
...
Verwende Bitmasken statt Bitfelder
...
Zähle in bedingten Schleifen rückwärts statt vorwärts
...
Wie lange reicht eine Batterie-Ladung?
...
Ein konkretes (und "lebendes") Beispiel...