Die Arduino IDE ist recht brauchbar. Überhaupt bin ich von dem Arduino Universum recht begeistert. Es gibt günstige Mikrokontroller Boards, die IDE ist kostenlos, es gibt eine Menge günstiger Hardware (Sensoren, Displays,….) mit entsprechenden Bibliotheken, es gibt eine recht große Community (viele Tutorien, Beispiele, Foren,..) und so kann man leicht versuchen seine Projekte umzusetzen.
Leicht wird allerdings vergessen, dass es sich hierbei um einen Mikrokontoller handelt – er ist für bestimmte Zwecke gebaut und für die ist er auch super einsetzbar. Die IDE Sprache ist quasi C – und man hat mit den ganzen Registern und der Hardware ja nix zu tun! Super!
Aber verschenkt man dadurch nicht Möglichkeiten? Kann man so das Potenzial des kleinen Mikrokontrollers wirklich ausnutzen?
Sehen wir uns mal ein Beispiel an:
Aufgabe einen Ausgang (den 13er PIN) so schnell wie möglich ein und ausschalten (alle folgenden Programme sind auf einem Arduino Uno Klon getestet):
int LED = 13; //Der Pin mit der internen LED ist 13 void setup() { pinMode(LED,OUTPUT); //Der LED-Pin wird als Ausgang festgelegt } void loop() { digitalWrite(LED,HIGH); //Der Ausgang LED wird HIGH gesetzt digitalWrite(LED,LOW); //Der Ausgang LED wird LOW }
Dafür brauchen wir nicht viel Code und das Oszi-Bild zeigt:
Das Programm macht also, was es soll. Es schreibt ein Rechtecksignal mit eine Periodenlänge von 11.4 µs (meistens!). Man sieht links, dass die Rechecke nicht „präzise“ sind (das Oszi mittelt über mehrere Bilder). D.h. die Periodenlänge ist meistens 11.4µs aber manchmal eben doch nicht (man spricht hier auch von „Jitter“). Woher kommt das? Der Mikrokontroller tut eben zwischendurch auch andere Dinge (z.B. hat er interne Timer, die, wenn sie abgelaufen sind Aktionen (sogenannte Interrupts) auslösen – damit werden z.B. Zeiten gemessen). Das brauchen wir aber nicht – wir wollen ja nur unseren Ausgang setzen und rücksetzen. Also schalten wir die Interrupts aus (ACHTUNG: Wie beschrieben funktioniert dann Verschiedenes, wie die Zeitmessung nicht mehr!, also Achtung wenn man das in einem größeren Programm macht):
int LED = 13; //Der Pin mit der internen LED ist 13 void setup() { pinMode(LED,OUTPUT); //Der LED-Pin wird als Ausgang festgelegt noInterrupts(); //Alle Interrupts werden ausgeschaltet } void loop() { digitalWrite(LED,HIGH); //Der Ausgang LED wird HIGH gesetzt digitalWrite(LED,LOW); //Der Ausgang LED wird LOW }
Und siehe da:
Das Rechteck ist nun sauber! (auch über längere Zeiten, ich hab das entsprechende Bild nicht hier her getan)
Jetzt habe ich aber noch einen kleinen „Fehler“ in das Programm eingebaut, der den Ablauf etwas verzögert!
Habt Ihr ihn gefunden?
Die Variable LED ist tatsächlich eine Variable, d.h. sie könnte sich im Programmablauf ändern. Der Mikrokontroller schaut also bei jedem digitalWrite was da jetzt im Speicher steht, wohin er schreiben muß. Tatsächlich brauchen wir hier aber eine Konstante (hier ersetzt der Compiler vor dem Upload jedes LED durch 13 d.h. im Programm im Mikrokontroller kommt die Variable LED gar nicht mehr vor – macht nicht viel Unterschied, aber wenn wir es ändern:
int const LED = 13; //Der Pin mit der internen LED ist 13 void setup() { pinMode(LED,OUTPUT); //Der LED-Pin wird als Ausgang festgelegt noInterrupts(); //Alle Interrupts werden ausgeschaltet } void loop() { digitalWrite(LED,HIGH); //Der Ausgang LED wird HIGH gesetzt digitalWrite(LED,LOW); //Der Ausgang LED wird LOW }
kommt das raus:
Ein sauberes Rechteck, dass jetzt etwas schneller ist. Die Periodendauer ist 10.68µs mit einer High-Zeit von 5.12 µs und einer Low-Zeit von 5.56 µs. Ist also eine Frequenz von 93.6 kHz.
Moment! Der Arduino hat eine Taktfrequenz von 16 MHz und das Schnellste was wir zusammenbringen sind nicht mal 100 kHz? – das kann doch nicht sein, oder?
Warum ist das so? – naja, die kleinen Befehle digitalWrite() sind in Wahrheit wohl gar nicht so klein. Und wir wissen ja nicht so genau, was der Compiler daraus macht und was dann der Mikrokontroller zu tun hat – aber offensichtlich eine ganze Menge. Sonst würde er nicht so lange brauchen.
Um schneller zu werden, müssen wir mal kurz überlegen, was eigentlich intern im Mikrokontoller passiert. Dort gibt es sogenannte Register, deren Inhalt z.B. auf die PINs übertragen wird. Wenn wir ins Datenblatt für den ATmega328 (das ist der Mikrokontroller auf dem Arduino Uno Board) schauen, finden wir dort, dass das Register mit dem Namen PORTB dafür zuständig ist. Das Register ist für die PINS 8-13 zuständig. Wenn wir hier das sechste Bit auf 1 setzten, wird auch der Ausgang high gesetzt. Das probieren wir gleich mal aus:
int const LED = 13; //Der Pin mit der internen LED ist 13 void setup() { pinMode(LED,OUTPUT); //Der LED-Pin wird als Ausgang festgelegt noInterrupts(); //Alle Interrupts werden ausgeschaltet } void loop() { PORTB = B00100000; //Der Ausgang LED wird HIGH gesetzt PORTB = B00000000; //Der Ausgang LED wird LOW }
Zu beachten ist hier, dass wir auch die anderen BITs in dem Register verändern (also auch andere Ausgänge). Das könnte stören! Bei unserem einfachen Beispiel tut es das aber nicht.
Als Ergebnis erhalten wir:
Hurra! Durch diese Änderung im Programm sind wir um mehr als den Faktor 10 schneller geworden und sind jetzt schon bei über 1 MHz!
Geht es noch schneller?
Wo liegt eigentlich jetzt das Problem? Bei einer Taktfrequenz von 16 MHz ist ein Taktzyklus 62.5 ns lang. Das ist auch ziemlich genau die Zeit, die das High-Signal dauert. Das Low Signal ist deutlich länger – hier wird der Ausgang zuerst auf LOW gesetzt und dann wird im Programm zum Anfang gesprungen – das Springen vom Ende der Schleife zum Anfang dürfte also relativ lange dauern. Wenn wir das Programm etwas modifizieren und am Ende mit einer GoTo-Anweisung zurückspringen (ja, ich weiß, bei manchen ist das GoTo verpönt, weil es schlechter Stil sei!) dann:
int const LED = 13; void setup() { pinMode(LED,OUTPUT); //Der LED-Pin wird als Ausgang festgelegt noInterrupts(); //Alle Interrupts werden ausgeschaltet } void loop() { start: PORTB = B00100000; //PIN13 wird HIGH gesetzt (und alle anderen im Register auf LOW) PORTB = B00000000; //PIN13 wird LOW (und alle anderen im Register!) goto start; }
sieht das plötzlich so aus:
Jetzt sind wir schon bei 4 MHz!
Wir brauchen einen Taktzyklus für das schreiben des High Signals, einen für das Low und dann zwei für den Rücksprung. Und das dürfte auch so ziemlich das Ende der Fahnenstange sein. Schneller können wir wohl auf diese Art nicht mehr werden, ohne Zähler einzusetzen.
Womit haben wir das erreicht?
- Wir haben statt des digitalWrite() direkt auf das entsprechende Register geschrieben.
- Wir haben ein GoTo statt dem „normalen“ Rücksprung verwendet.
Was ist der Preis?
- Das Programm ist schwerer lesbar (ein digitaWrite() liest sich viel leichter als Bits und Adressen).
- Man muß höllisch aufpassen, wenn man direkt in Register schreibt (denn man ändert eventuell auch andere Dinge (Eingänge, Ausgänge,….)
- Bei einem anderen Mikrokontroller können die Register anders sein und das Programm muß geändert werden, damit es verwendbar bleibt.
Welche Lösung ist besser?
Wie so oft: Das kann man so nicht sagen. Wenn man z.B. eine LED blinken lassen will, sind irgendwelche µs und ns ziemlich egal. Dann ist es besser ein gut lesbares und „sicheres“ Programm zu haben.
Wenn man aber recht schnell irgendwelche Signale erzeugen oder verarbeiten will, dann kann das helfen! Es bleibt also an Euch zu entscheiden, was in welchem Fall besser ist! – aber es gut, wenn man beide Möglichkeiten kennt!
Ich hoffe, der Artikel war verständlich und ist nachvollziehbar!
Bei Anregungen oder Fragen benutzt bitte die Kommentarfunktion.
Liebe Grüße
Euer DIYDonkey