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. C++ langsamer compiliert als C (nur relevant für große Projekte)
  3. Die meisten Arduino-Sketches eher in C geschrieben sind, mit Ausnahme der Arduino-Libraries und derer Objekte
  4. 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.

Antrieb

Man sollte sich überlegen, warum man sich vom Arduino lösen will. Dazu gibt es im wesentlichen 2 Gründe:

  • Trennung von der Arduino-IDE
  • Trennung von der Arduino-Hardware und der IDE in logischer Konsequenz

Trennung von der Arduino-IDE

Es gibt Situationen die Arduino-Hardware zu behalten und sich von der Arduino-IDE trennen. Die gängige Ursache sind Fehlkäufe: Der Arduino (zumeist Uno) ist nun einmal da und „muss weg“. Die IDE ist entsetzlich träge und bietet nur wenig zielführende Hilfestellung. Solange man fertige „Libraries“ zu einem funktionierenden Etwas zusammenklicken kan ist man mit der IDE einigermaßen bedient. Damit kann man aber nichts erfinden! Sobald man aber etwas neues programmieren will oder muss, ist das Datenblatt des Mikrocontrollers der bessere Freund als jede Hilfestellung in den diversen Arduino-Foren.

Trennung von der Arduino-Hardware

Hierbei gibt es mehrere Gründe:

  • Der Arduino ist zu groß oder sperrig und passt nicht in das Zielgerät
  • Für die angestrebte Anwendung gibt es keinen Arduino
  • Die Beschaltung des Mikrocontrollers ist unpassend (bspw. falscher Quarz) und frisst zu viel Strom
  • Die Beschaltung stört beim Mikrocontroller-Lernprozess

Ein Arduino-Board ist zum Verständnis des Zusammenhangs zwischen Software, Mikrocontroller (und seiner Peripherie) und der umgebenden Schaltung nur wenig geeignet. Denn der entscheidende Teil, das Verständnis der Mikrocontroller-Peripherie und dessen nutzbringender Einsatz wird wegabstrahiert und nur für wenige häufige Fälle brauchbar. Daher wird man zum nackten Mikrocontroller greifen. In der Entwicklungsphase, für solche die es nur in SMD gibt, gibt es aus China oder von Olimex preiswerte Adaptierungen auf Pins für ein Steckbrett.

Dazu muss man sich auch um das Programmierinterface Gedanken machen, entweder per ISP und/oder mittels gesondertem Porgrammiergerät. Das ist immer ein Teil des Problems! Nur AVRs mit USB bringen von Haus aus einen USB-Urlader mit. Das ist aber nicht derselbe wie der Arduino-Urlader (namens Caterina)!

Ein wichtiger Grund ist die zu hohe Stromaufnahme von Arduino-Boards, die einen sinnvollen Batteriebetrieb ausschließt. Hierzu muss man dem Datenblatt noch sämtliche Maßnahmen zur Senkung der Stromaufnahme entlocken. Eine realistische Ruhestromaufnahme für eine Schaltung, die auf Tasten wartet (ohne Timerfunktion) liegt unter 1 µA. Ist ein Timer involviert (bspw. zur zyklischen Abfrage eines Analogeingangs) kommt man mittels Watchdog auf unter 10 µA. Die Außenbeschaltung von Arduinos frisst vor allem durch den Querstrom des Spannungsreglers; der Arduino Uno zusätzlich vom LM358 und dem zweiten Mikrocontroller.

Verändern des Arduino-Sketches

Als Erstes sollte man sich abgewöhnen von Sketchen (Skizzen) zu reden, damit wird man nur belächelt. Denn damit meint man etwas unfertiges, so als ob man nie fertig werden wolle. Es sind Programme, Anwendungen, Applikationen. Wenn man den Arduino oder den Mikrocontroller in einem Gerät einbaut und damit für den Endanwender unsichtbar macht spricht man von Firmware.

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 annehmen. 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.

Hinweis: Der Compiler kann bei der Laufvariable i die Größe von 16 auf 8 Bit reduzieren via Code-Optimierung, und kann den Speicherplatz für die Konstante ledPin wegoptimieren (wenn man static const davorschreibt). Der GCC tut das in der Regel nicht, da dieser wie jeder Compiler von einer Mindestintelligenz des Programmierers ausgeht.

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

/* Anmerkung: #define ist auch in C-Programmen mittlerweile altmodisch
und eine Quelle schwer auffindbarer Probleme!
Integer-Konstanten kann man mit jedem noch so alten C-Compiler mittels enum festlegen:
enum{
 LED_PIN = 13
};
*/

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 Implementierung, 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 weitere 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 geschaltet, 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
//Hier muss #define stehen, da DDRB und PORTB keine Integerzahlen sind.
#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).

Immer noch C++

Weg von der Arduino-IDE wird häufig als „C statt C++“ missverstanden. Genau genommen ist das oben angegebene Programm immer noch C++. Nicht wegen der //-Kommentare (die sind schon lange in C erlaubt) sondern wegen der uint8_t-Definition in der for-Anweisung. Das ist IMHO eine GNU-Erweiterung von gcc, ansonsten C++-Syntax.

Um tatsächlich C (und nicht C++) zu kompilieren ist die Dateiendung auf .c zu ändern. Am Speicherverbrauch des fertigen Programms ändert das nichts.

Immer noch untergeschobener Kode

Das Verschwinden des RAM-Verbrauchs bedeutet nicht, dass noch Kode vom Compiler bzw. Linker eingebaut wird. Dieser ist im Allgemeinen (aber nicht in diesem Beispiel) notwendig um Interrupttabellen zu initialisieren und eine C/C++-konforme Laufzeitumgebung zur Verfügung zu stellen (sprich: alle statischen Variablen zu initialisieren). Der zusätzliche Kode bewegt sich je nach Länge der Interrupttabelle und initialisierter Daten bei 50..200 Byte.

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 langem 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.

Editoren

Beim Editor steht man ebenfalls vor der Qual der Wahl. Daher die folgenden Tipps nur für Anfänger:

In der Vergangenheit gab es WinAVR (letzte Version 2010) mit eingebautem Programmer's Notepad. Für Windows. Der geht heutzutage (2023) immer noch! Allerdings muss man den gcc erneuern, wenn man moderne C++-Features nutzen möchte oder einen neueren, damals unbekannten AVR-Typ verwendet. Gut ist, dass die Menü-Konfiguration der Voreinstellung bereits zum Compilieren und Programmieren des Mikrocontrollers taugt ohne eine Kommandozeile bemühen zu müssen. Fehlerfrei ist Programmer's Notepad nicht: Das Hantieren mit dem Ausgabefenster erfordert den Griff zur Maus, da ein Fokuswechsel per Tastatur nicht vorgesehen ist. Auch gibt es Probleme mit der UTF-8-Unterstützung.

Unter Windows weit verbreitet ist Notepad++. Auch dieser SciTE-basierte Editor ist dem Programmer's Notepad ähnlich, kann mehr, muss ihm aber ein Ausgabefenster per Plugin „NppExec“ unterschieben und dieses so konfigurieren, dass man beim Anklicken einer Fehlermeldung auch tatsächlich zur fehlerhaften Kodezeile kommt. Gewöhnungsbedürftig aber auch genial ist, dass das Ausgabefenster auch zur Eingabe (von Kommandos und Abfragen) geeignet ist. Fehlerfrei ist auch Notepad++ nicht: Trotz mehrfacher Tickets zur Fehlerbereinigung funktionieren im Suchen-und-Ersetzen-Dialog die Hotkeys nicht erwartungsgemäß.

Eine IDE (etwa Visual Studio) als Editor zu verwenden macht IMHO quälend viel Aufwand um alles rundum lauffähig hinzubekommen. Außerdem sind diese notorisch langsam beim Bildaufbau. Das macht daher nur Sinn wenn man ohnehin eine IDE benutzt.

Unter Linux habe ich den im Double Commander eingebauten für am praktischsten befunden. Der Aufruf von make in der Kommandozeile ist unter Linux weniger störend. Als Anfänger sollte man nicht dem Expertenrat folgen, vi zu benutzen. Auch nicht nano. Außer man hat keine GUI verfügbar, etwa im Terminalprogramm. Da bevorzuge ich joe, aber nur weil ich die WordStar-Tastenkodes von Turbo Pascal auswendig kenne. Eine handliche Lösung, um von Fehlermeldungen zur Editor-Kodezeile zu springen ist mir nicht bekannt. (Ich ist jemand, der Linux nur gelegentlich benutzt.)

Grundsätzlich sollten Quelltexte nur in UTF-8 erstellt werden, und sinnvollerweise mit "\n" (Newline) als Zeilenende. Was unter Linux Standard ist, ist auch unter Windows 11 Notepad inzwischen verdaulich und speicherbar. Die Editor-Schriftart sollte (faktisch: muss) auf einen diktengleichen Font eingestellt werden. Allenfalls Tab-Weite und Einrückstil verbleiben als ewiges Streitthema. Wissen sollte man dazu, dass die Standard-Tabweite in einem Terminal 8 Zeichen beträgt.

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 in jedem Fall, das obige Programm kompiliert zu bekommen. Nach Möglichkeit ohne Warnungen (warnings). Warnungen kann man zwar beim Compileraufruf oder mittels Pragmas unterdrücken, besser jedoch ist es, ohne solchen Schindluder auszukommen.

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

Die meisten Arduinos kommen mit einem vorinstalliertem Bootloader. Clones werden auch gerne mal ohne verkauft. Mit dem Bootloader lässt sich der Prozessor auch ohne Programmer über USB direkt flashen.

Als PC-Programm bietet sich dazu avrdude an. Dieses ist ein mehr oder weniger universelles 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-Version 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, ...