Umstieg von Arduino auf AVR

Wechseln zu: Navigation, Suche

von newgenertion

Dieser Artikel soll eine kleine Hilfestellung für alle sein, die aktuell mit einem Arduino-Board arbeiten, sich aber mehr für die Materie interessieren und auf richtige Mikrocontroller-Programmierung umsteigen wollen.

Um den Umstieg zu erleichtern werden hier einige kleine, aber hilfreiche Schritte aufgezeigt. Diese Anleitung ist nur für die Arduinos mit einem 8bit-AVR als Prozessor (Uno, Mega, Leonardo, ...) gedacht und nicht für 32bit-Mikrocontroller (Due, ...).

Der Mikrocontroller wird in dieser Anleitung in C programmiert. Grund dafür ist die Verteilung von Programmiersprachen und deren Schwierigkeit zu erlernen. Auf Mikrocontrollern gibt es hauptsächlich Assembler und C, wobei auch andere Sprachen im Kommen sind, so zum Beispiel C++ (das Arduino-Framework ist in C++ geschrieben).

Trotzdem soll sich hier auf C beschränkt werden, weil

  1. C++ schwerer zu Beherrschen ist als C
  2. Die meisten Arduino-Sketches eher in C geschrieben sind, mit Ausnahme der Arduino-Libraries und derer Objekte
  3. Assembler nochmal eine ganz andere Sprache ist


Voraussetzungen

C-Kenntnisse

Wer schon C programmieren kann - damit ist mehr als if-else- und Copy&Paste-Programmierung gemeint - kann diesen Punkt selbstverständlich überspringen. Allen anderen kann ich nur wärmstens empfehlen, ein C-Buch oder wenigstens ein (gutes) C-Tutorial durchzuarbeiten.

Im Artikel C stehen einige Links zu Tutorials und Einführungen zur Sprache C. Es dürfte einfacher sein, sich die C-Kenntnisse auf einem PC zu erarbeiten, da man dort viel mehr Möglichkeiten hat, sein Programm zu analysieren und auf Fehler zu reagieren.

Achtung: Tutorials, vor allem die in deutscher Sprache, sollten teilweise Hinterfragt werden. Oftmals schreibt der Autor einfach nur seine (zum Teil begrenzte) Sicht der Dinge. Es kann nicht schaden, mehr als ein Tutorial zu lesen und bei Diskrepanzen den C-Standard zu Rate zu ziehen.

Andere Vorkenntnisse

Software

  • Compiler: Man sollte entweder die Pfade zu den Executables des avr-gccs in der Arduino-Umgebung zur Umgebungsvariable PATH hinzufügen oder, vor allem wenn man die Arduino-IDE später deinstallieren möchte, eine separate Compiler-Installation vornehmen. Siehe dazu AVR-GCC.
  • Ein Terminal-Programm wie Putty oder HTerm kann nie schaden.

Verändern des Arduino-Sketches

Als Erstes sollte man sich abgewöhnen von Sketchen zu reden, damit wird man nur belächelt. Es sind Programme, Anwendungen, Applikationen, ...

Anpassung von int-Typen

Fast sämtliche Arduino-Beispiele sehen irgendwie so aus (hier ein kleines Lauflicht):

int ledPin = 13;                  // LED connected to digital pin 13

void setup()
{
    pinMode(ledPin, OUTPUT);      // sets the digital pin as output
    for(int i = 0; i < 8; i++) {
        pinMode(i, OUTPUT);       // sets the digital pin as output
    }

    digitalWrite(ledPin, HIGH);   // sets the Board-LED on
}

void loop()
{
    for(int i = 0; i < 8; i++) {
        digitalWrite(i, HIGH);    // sets the LED on pin <i> on 
        delay(100);               // waits 100 milliseconds
        digitalWrite(i, LOW);     // sets the LED on pin <i> off
    }
    delay(1000);                  // waits for a second
}
/*
Der Sketch verwendet 1006 Bytes (3%) des Programmspeicherplatzes. 
Das Maximum sind 32256 Bytes.
Globale Variablen verwenden 9 Bytes (0%) des dynamischen Speichers, 
2039 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes. 
*/

Fällt euch etwas auf? Nein? Wie groß ist ein int? Genau mindestens 16 bit. Und es ist ein Typ mit Vorzeichen.

Eine Variable vom Typ int kann auf einem AVR also Werte von -2^15 bis 2^15 - 1 annemhen. Das sind Zahlen zwischen -32768 und 32767. Und was wird in diesem Typ gespeichert?

  • Eine Variable, deren Wert sich nie ändert: int ledPin = 13;
  • Und zwei Laufvariablen von 0 bis 7.

Also beides nicht wirklich das, wofür man 16 bit Variablen braucht.

Okay, dann hat man halt Variablen mit einem zu großen Typ definiert, was macht das? Schon etwas, denn der AVR ist ein 8bit-Mikrocontroller, das bedeutet grob, dass er immer nur 8bit-Zahlen auf einmal manipulieren kann, alles größere braucht mehrere Befehle und ist somit langsamer. Mikrocontroller mögen vorzeichenlose Zahlen auch lieber, als solche mit Vorzeichen.

Man sollte also bei jeder Variable überlegen, welchen Wertebereich man benötigt und dann immer den Typen so klein wie möglich, aber so groß wie nötig nehmen.

Der C-Standard bietet Typen mit genauer Bitbreite an, dafür muss nur eine Header-Datei eingebunden werden:

#include <stdint.h>

In dieser werden dann unter anderem die folgenden Typen definiert:

Größe Vorzeichenlos Vorzeichenbehaftet
8 bit uint8_t int8_t
16 bit uint16_t int16_t
32 bit uint32_t int32_t
64 bit uint64_t int64_t

Die Nomenklatur ist eigentlich ganz einfach: [u]int[bits]_t, wobei das [u] für unsigned, also vorzeichenlos, steht und [bits] eben die Anzahl der Bits für die Variable angibt.

Der geänderte Source-Code sieht dann so aus:

#include <stdint.h>

#define LED_PIN 13                // ein Define erzeugt keinen Zusätzlichen Code, 
                                  // es erfolgt schließlich nur eine Textersetzung.
                                  // Defines immer in GROSSBUCHSTABEN

void setup()
{
    pinMode(LED_PIN, OUTPUT);     // sets the digital pin as output
    for(uint8_t i = 0; i < 8; i++) {
        pinMode(i, OUTPUT);       // sets the digital pin as output
    }

    digitalWrite(LED_PIN, HIGH);  // sets the Board-LED on
}

void loop()
{
    for(uint8_t i = 0; i < 8; i++) {
        digitalWrite(i, HIGH);    // sets the LED on pin <i> on 
        delay(100);               // waits 100 milliseconds
        digitalWrite(i, LOW);     // sets the LED on pin <i> off
    }
    delay(1000);                  // waits for a second
}
/*Der Sketch verwendet 1006 Bytes (3%) des Programmspeicherplatzes. 
Das Maximum sind 32256 Bytes.
Globale Variablen verwenden 9 Bytes (0%) des dynamischen Speichers, 
2039 Bytes für lokale Variablen verbleiben. 
Das Maximum sind 2048 Bytes. 
*/

Bei diesem Minimal-Programm hat diese Änderung wie man sieht nichts gebracht, das zeigt aber nicht, dass diese Anpassung sinnlos ist, sondern, dass der Compiler sehr gut optimiert und diese unnötig großen Variablen eliminiert.

Ein anderes Beispiel ist dieses Programm. Es macht nichts außer eine volatile-Variable hochzuzählen. (Volatile zum verbieten der Optimierungen).

#include <stdint.h>
// 64bit
volatile int64_t a;

void setup()
{
    a = 0;
}

void loop()
{
    a++;
}
/* 
Der Sketch verwendet 570 Bytes (1%) des Programmspeicherplatzes. 
Das Maximum sind 32256 Bytes.
Globale Variablen verwenden 17 Bytes (0%) des dynamischen Speichers, 
2031 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes. 
*/

Wenn man jetzt die Variable verkleinert und auf unsigned ändert:

#include <stdint.h>
// 8bit
volatile uint8_t a;

void setup()
{
    a = 0;
}

void loop()
{
    a++;
}
/* 
Der Sketch verwendet 458 Bytes (1%) des Programmspeicherplatzes. 
Das Maximum sind 32256 Bytes.
Globale Variablen verwenden 10 Bytes (0%) des dynamischen Speichers, 
2038 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes.
*/

Schon bei diesem Minimal-Programm sieht man einen kleinen Unterschied

Bei größeren Programmen mit mehreren Modulen kann der Compiler aber nicht mehr alles überblicken, deswegen lohnt sich spätestens dort diese Änderung.

Entfernen der Arduino-Libraries

Wer seinen Mikrocontroller richtig verstehen will, der sollte auch versuchen sämtliche Hardware-Ansteuerung selber zu programmieren.

Entfernen der delay()-Aufrufen

Zuerst einmal: delays sind so gut wie immer schlecht! Während der Controller im delay() wartet, kann er nichts anderes mehr tun!

Die Implementierung vom delay() in der Arduino-Bibliothek benutzt Interrupts und kann deswegen in Interrupts nicht funktionieren (Obwohl das sowieso eine sehr schlechte Idee ist). Um aber von Arduino und deren Library wegzukommen, benutzen wie eine andere Impementierung, nämlich die von der avr-libc. Diese bietet _delay_ms() und _delay_us() für taktgenaue Verzögerungen in Milli- bzw. Mikrosekunden-Bereich an. Dafür ist nur das Einbinden von <util/delay.h> nötig.

#include <stdint.h>
#include <util/delay.h>

#define LED_PIN 13                // ein Define erzeugt keinen Zusätzlichen Code, 
                                  // es erfolgt schließlich nur eine Textersetzung.
                                  // Defines immer in GROSSBUCHSTABEN

void setup()
{
    pinMode(LED_PIN, OUTPUT);     // sets the digital pin as output
    for(uint8_t i = 0; i < 8; i++) {
        pinMode(i, OUTPUT);       // sets the digital pin as output
    }

    digitalWrite(LED_PIN, HIGH);  // sets the Board-LED on
}

void loop()
{
    for(uint8_t i = 0; i < 8; i++) {
        digitalWrite(i, HIGH);    // sets the LED on pin <i> on 
        _delay_ms(100);           // waits 100 milliseconds
        digitalWrite(i, LOW);     // sets the LED on pin <i> off
    }
    _delay_ms(1000);              // waits for a second
}
/*
Der Sketch verwendet 828 Bytes (2%) des Programmspeicherplatzes. 
Das Maximum sind 32256 Bytes.
Globale Variablen verwenden 9 Bytes (0%) des dynamischen Speichers, 
2039 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes.
*/

Entfernen der I/O-Aufrufe

Dazu zählen unter anderem pinMode, digitalWrite und ditigalRead. Diese verbindet allesamt eines: Die eigenwillige Nummerierung der Pins.

Auf dem Arduino-Board sind sie zwar logisch angeordnet, aber nicht unbedingt logisch mit dem Prozessor verbunden! Deswegen muss man einmal nach seinem arduino board + Pinout googlen, dann kommen schöne Bilder, die recht Anschaulich zeigen, was womit verbunden ist. Beim Arduino Uno ist das Ganze noch recht ordentlich, beim Arduino Mega erinnert es mehr an Chaos...

Eine Seite, die viele Pinouts hat ist libraries.io. Dort sucht man sich einfach sein Board heraus und speichert sich am besten das Bild, denn das wird noch häufiger benötigt.

Um jetzt wirklich starten zu können fehlt nur noch eins: das Datenblatt des Prozessors. Auf der Arduino-Seite steht, was für ein Prozessor dort verbaut ist, nach diesem Datenblatt sollte man dann bei Google oder direkt beim Hersteller Atmel suchen. Beim Arduino Uno ist es der ATmega328p.

Im Datenblatt gibt es ein Kaptiel "I/O-Ports", wo haarklein erklärt wird, wie die Pins funktionieren und anzusteuern sind. Wichtig sind dazu vor allem drei Register:

  • PORTx - The Port x Data Register
  • DDRx - The Port x Data Direction Register
  • PINx - The Port x Input Pin Register

Wobei das x für den Port steht. Welche Ports es gibt hängt vom jeweiligen AVR ab. Der Atmega328p hat zum Beispiel vier Stück: PORTA, PORTB, PORTC, PORTD. Ein ATmega2560 hingegen hat derer elf: PORTA - PORTH und PORTJ - PORTL. Gemeinsam ist allen, dass ein Port maximal 8 Pins enthält (PXN, X=Port-Buchstabe, N=Port-Bit).

Genaueres gibt es hier:

Eine Kurzfassung folgt nun:

Um auf die I/O-Register (beziehungsweise Register allgemein) zugreifen zu können braucht man eine weiter Header-Datei:

#include <avr/io.h>

Mittels diesen Registern kann man dann jeden einzelnen Pin steuern. Die folgende Tabelle zeigt die Einstellungsmöglichkeiten:

DDRx PORTx IO-Pin-Zustand
0 0 Eingang ohne Pull-Up (Resetzustand)
0 1 Eingang mit Pull-Up
1 0 Push-Pull-Ausgang auf LOW
1 1 Push-Pull-Ausgang auf HIGH

Das übrige PINx-Register hat nun auch wieder zwei Einsatzmöglichkeiten. Wenn der Pin ein Input-Pin ist (DDxn = 0), dann gibt dieses Register den Zustand des Pins aus, eine 1 für High und eine 0 für Low. Ist der Pin jedoch als Ausgang konfiguriert, dann können neuere AVRs (praktisch alle auf Arduinos) den Pin direkt "togglen", also umschalten: ist er aktuell High, dann wird er auf Low geschalten, und umgekehrt.

Mit diesem Wissen können wir wieder unseren Code anpacken:

#include <stdint.h>
#include <avr/io.h>
#include <util/delay.h>

//Der Arduino-Pin 13 ist auf dem Arduino Uno der Pin PB5
#define LED_DDR DDRB
#define LED_PORT PORTB
#define LED_BIT PB5

void setup()
{
    LED_DDR |= (1 << LED_BIT);    // sets the digital pin as output
    
    //die 8 LEDs leigen alle auf PORTD, also diesen komplett auf Ausgang
    DDRD = 0xFF;

    LED_PORT |= (1 << LED_BIT);  // sets the Board-LED on
}

void loop()
{
    for(uint8_t i = 0; i < 8; i++) 
    {
        PORTD |= (1 << i);        // sets the LED on pin <i> on 
        _delay_ms(100);           // waits 100 milliseconds
        PORTD &= ~(1 << i);       // sets the LED on pin <i> off
    }
    _delay_ms(1000);              // waits for a second
}
/*
Der Sketch verwendet 534 Bytes (1%) des Programmspeicherplatzes. 
Das Maximum sind 32256 Bytes.
Globale Variablen verwenden 9 Bytes (0%) des dynamischen Speichers, 
2039 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes.
*/

Man sieht: der Speicherverbrauch wurde nochmal gedrückt, und schneller wurde das Programm auch noch.

Bleiben aber noch Fragen: Warum verbraucht dieses Mini-Programm immer noch so viel Flash? Un warum wird RAM verbraucht, obwohl keine einzige Variable verwendet wird?

TODO: Geschwindigkeit mittels Oszi messen (Darf gerne auch von jemand anderem gemacht werden).

main()-Funktion statt setup() & loop()

Die Arduino-IDE hat im Vergleich zu anderen IDEs eine "Gemeinheit" eingebaut. Um es dem Benutzer einfacher zu manchen, ändert diese stillschweigend den Code (fügt etwa eine main()-Funktion hinzu und das include <Arduino.h>) und zieht Code mitein, selbst wenn dieser nicht genutzt wird.

So zum Beispiel die Interrupt-Routine, die den Millisekunden-Timer für die delay()-Funktion bildet: Sowohl die Routine an sich, als auch die Konfigurierung des Interrupts und auch die generelle Erlaubnis aller ISRs geschieht automatisch, ohne das der User daran etwas ändern kann.

Das ist im Normalfall auch in Ordnung, da sich der 08/15-Arduino-Benutzer gar nicht dafür interessiert.

Bei uns ist das aber etwas anderes! Also wird das Programm an ein richtiges C-Programm angeglichen, also mit einer main()-Funktion, statt setup() und loop().

#include <stdint.h>
#include <avr/io.h>
#include <util/delay.h>

//Der Arduino-Pin 13 ist auf dem Arduino Uno der Pin PB5
#define LED_DDR DDRB
#define LED_PORT PORTB
#define LED_BIT PB5

int main(void)
{
    LED_DDR |= (1 << LED_BIT);    // sets the digital pin as output
    
    //die 8 LEDs leigen alle auf PORTD, also diesen komplett auf Ausgang
    DDRD = 0xFF;

    LED_PORT |= (1 << LED_BIT);  // sets the Board-LED on
    while(1)
    {
        for(uint8_t i = 0; i < 8; i++) 
        {
            PORTD |= (1 << i);        // sets the LED on pin <i> on 
            _delay_ms(100);           // waits 100 milliseconds
            PORTD &= ~(1 << i);       // sets the LED on pin <i> off
        }
        _delay_ms(1000);              // waits for a second
    }
}
/*
Der Sketch verwendet 222 Bytes (0%) des Programmspeicherplatzes. 
Das Maximum sind 32256 Bytes.
Globale Variablen verwenden 0 Bytes (0%) des dynamischen Speichers, 
2048 Bytes für lokale Variablen verbleiben. Das Maximum sind 2048 Bytes.
*/

Et voilà! Da haben wir es: Der Code-Verbrauch ist nochmal drastisch gesunken und vor allem: keine Varaible, kein RAM-Verbrauch!

Damit ist jeder Arduino-Code, der im Hintergrund dazukam, getilgt.

Anmerkung: Der Compiler mag shiften um Variablen nicht, also das (1 << i). Es ist wesentlich besser, wenn man so etwas schreibt:

uint8_t mask = (1 << 0);
for(uint8_t i = 0; i < 8; i++) 
{
    PORTD |= mask;            // sets the LED on pin <i> on 
    _delay_ms(100);           // waits 100 milliseconds
    PORTD &= ~mask;           // sets the LED on pin <i> off
    mask = (mask << 1);       // shift the bit to the left
}

Das ist erstens schneller und verbraucht zweitens weniger Speicher (im Beispiel nur noch 204 Bytes).

Weg von der Arduino-IDE

Auswahl der neuen IDE

So, nun ist man an dem Punkt angelangt, an welchem man sich dazu entscheiden kann (und meiner Meinung auch sollte), Abschied von der Arduino-IDE zu nehmen. Diese ist in vielerlei Hinsicht nicht optimal, sei es zum Beispiel beim highlighting von Code oder der mangelnden Konfigurationsmöglichkeit.

Es gibt zahlreiche Möglichkeiten, wie man nun weiter verfahren kann:

  • bei der Arduino-IDE bleiben
  • auf das Atmel Studio umsteigen (entweder Version 7 mit zahlreichen neuen Features, oder VErsion 4, falls es schnell und zuverlässig sein soll)
  • auf eine andere IDE (z.B. eclipse) umsteigen
  • Mittels Makefiles und einem Editor/einer IDE seiner Wahl arbeiten

Ich persönlich habe mich nach langen arbeiten mit jeder dieser Möglichkeiten (abgesehen von der Arduino-IDE, diese habe ich mehr oder weniger direkt verworfen) für die letzte, für das Arbeiten mit Makefiles, entschieden. Dort hat man völlige Kontrolle über alles: was wird wann mit welchen Option kompiliert und was wird hinzugelinkt?

Das sollte aber jeder für sich selber herausfinden. Die Liste oben ist von der Schwierigkeit her sortiert, das bedeutet, das Makefiles das anspruchsvollste sind.

Einarbeiten in die neue Umgebung

Sobald man sich auf eine IDE festgelegt hat, sollte man sich in diese erst einmal Einarbeiten. Auf diesem Punkt kann in dieser Anleitung nicht eingegangen werden, da sich mögliche Tipps oder Ähnliches ja nach IDE unterscheiden würden.

Am besten versucht man erstmal die für sich wichtigen Funktionen zu finden und mit der neuen Umgebung allgemein zurech zu kommen.

Ziel ist auf jeden Fall, das obige Programm zu kompilieren zu bekommen.

Das Programm übertragen

So weit, so gut.

Der neue Editor/Die neue IDE läuft, der Code kompiliert.

Doch wie bringt man nun den Code auf den AVR? Dazu gibt es unter Anderem zwei Möglichkeiten auf die ich hier eingehen möchte:

Was das ist wird hier nicht erklärt, dafür sind die Artikel verlinkt.

Programmer/Debugger

Wer schon einen Programmer oder gar Debugger für AVRs sein Eigen nennen kann, der sollte diesen verwenden, da damit noch einmal der Speicherplatz für den Bootloader frei wird (Dadurch kann das Programm noch einmal ~2kB größer werden) und auch die Wartezeit nach jedem Reset entfällt.

Wer noch keinen Programmer hat, der muss sich nicht unbedingt einen solchen kaufen, solange er mit den eben genannten Nachteilen leben kann.

Wer aber jetzt in die Tasche greifen will, der kann sich überlegen, ob er vielleicht nicht lieber direkt einen vollwertigen Debugger kauft. Damit kann man, wie auch am PC, ein laufendes Programm anhalten, Werte von Registern anzeigen, etc. So etwas kann sehr hilfreich sein, wenn "unerklärliche" Phänomene auftreten.

Wie man mit dem Programmer dann schließlich den AVR programmiert hängt wieder von der IDE ab. Beim Atmel Studio wird man sicherlich auf den Programming Dialog zurückgreifen, bei anderen IDEs wird wahrscheinlich ein Drittprogram wie avrdude verwendet.

Bootloader

Alle Arduinos kommen mit einem vorinstalliertem Bootloader. Mit diesem lässt sich der Prozessor auch ohne Programmer über USB direkt flashen.

Also PC-Programm bietet sich dazu avrdude an. Dieses ist ein mehr oder weniger "Universales" Brennprogramm für fast alle AVR-Typen. Es beherrscht auch die Kommunikation mit dem Arduino-Bootloader. Damit kann man dann ganz einfach sein Programm übertragen. Die Kommandozeile ist leider je nach Arduino- und Bootloader-Versio etwas anderes

# beim Arduino mega ist es 
$ avrdude -cwiring -patmega2560 -P<serial port> -b115200 -U flash:w:<file> -D
# andere Konfigurationen könnten sein (von mir ungetestet, gerne zu Vervollständigen)
$ avrdude -carduino -patmega328p -P<serial port> -b115200 -U flash:w:<file> -D
$ avrdude -carduino -patmega328p -P<serial port> -b57600 -U flash:w:<file> -D
$ avrdude -cstk500v2 -patmega328p -P<serial port> -b115200 -U flash:w:<file> -D

Arduino-Library Ersatz

Nachdem das Minimal-Programm von oben nun auf dem AVR-Board getestet wurde, geht es weiter.

Wir haben uns von Arduino verabschiedet, damit aber auch von allen Arduino-Libraries! Das bedeutet, dass selbst so banale Sachen wie Serial.println() nicht mehr existieren. Diese müssen wir nun selber schreiben.

Erstellen eigener Libraries

Hier soll nun ein Beispiel mit einer Schritt-für-Schritt-Anleitung gegeben werden. Dazu habe ich mir das UART-Modul ausgesucht.

Serielle Kommunikation mittels UART-Hardware

Das wird zwar dann die X-te UART library, aber zur Demonstration eignet sich das UART-Modul hervorragend.


TODO: Verlinkungen zu weiterführenden Artikeln, Beispiel für UART und LCD library selbst geschrieben, ...