Umstieg von Arduino auf AVR

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
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

C-Buch

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 ihn nur wärmstens empfehlen.

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.

Was auch noch wichtig ist, ist ein Verständnis über Binär-, Dezimal- und Hexadezimalzahlen, sowie über Bitmanipulation.

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++) {
        DDRD |= (1 << i);         // sets the LED on pin <i> on 
        _delay_ms(100);           // waits 100 milliseconds
        DDRD &= ~(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.

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

Erklärung bezüglich Library-Setup etc.

Weg von der Arduino-IDE

TODO: Umstieg von der Aruino-IDE auf ATMEL Studio 7 mit der Sketch Funktion oder eine beliebige andere IDE. Verlinkungen zu weiterführenden Artikeln, Beispiel für UART und LCD library selbst geschrieben, ...