http://sites.schaltungen.at/arduino-uno-r3/grafik-display-1/4-grafikdisplayWels, am 2016-06-12BITTE nützen Sie doch rechts OBEN das Suchfeld [ ] [ Diese Site durchsuchen]DIN A3 oder DIN A4 quer ausdrucken ********************************************************************************** DIN A4 ausdrucken *********************************************************
C:\Programme (x86)\Arduino\arduino.exe
\examples\01.Basic\Blink\Blink.ino
4 GrafikDisplay 4.01 Constellation - wie Sterne am Himmel 4.02 Scope - ein einfaches Oszilloskop 4.03 Blick in die Library III - die Grundlagen des Grafikdisplays 4.04 Nikolaus - ich mal das Haus vom Nikolaus 4.05 Analysis - Mathe-Unterricht mit Arduino 4.06 SmartHome - Hausautomatisierung auf dem Display 4.07 Blick in die Library IV - Linien, Texte und Bilder 4.08 AnalogWatch - Digitaluhr mit Ziffernblatt 4.09 Geometrie - auf zu neuen Formen 4.10 VU-Meter - der klassische Austeuerungsmesser 4.11 Calendar - analoge Uhr mit erweiterten Funktionen 4.12 HardwarePlayground - die Hardware voll im Griff 4.13 Navigation - ein Menü mit reichlich Auswahl 4.14 Blick in die Library V - die Library ist komplett 4.15 turtleGraphics - der kleine Zeichenroboter 4.16 Flappy Bird - Spielspaß zum Abschluss 5 Projekte Im vorangegangenen Teil haben Sie das Display als Textdisplay setzt. Die Library hat es Ihnen einfach gemacht, Texte und Symbole a das Display zu schreiben, In diesem Kapitel gehen Sie ein Stück weiter, indem Sie geometrische Figuren und ganze Zeichnungen auf.d« Display bringen. Doch die Fähigkeit, Text auf dem Display auszugeben, bleibt erhalten. Am Ende erhalten Sie eine vollständige und universal einsetzbare Library für alle Ihre zukünftigen Projekte. 4.1 Constellation - wie Sterne am Himmel Um Grafiken auf dem Display auszugeben, benutzen Sie im ersten Schritt schon den Video-Buffer und geben erste zufällig gesetzte Pixel aus.
Anleitung:
FRANZIS Grafik-Display auf ARDUINO UNO R3 aufstecken.
Mit USB Verbindungskabel mit PC verbinden.
Rechner > Geräte-Manager > Anschlüsse (COM & LPT) > Arduino Uno (COM5)
MENU > Datei > Voreinstellungen > Sketchbook-Speicherort: C:\User\fritz\Documents\Arduino
MENU > Werkzeuge > Platine: "Arduino Uno"
MENU > Werkzeuge > Port: "COM5 (Arduino Uno)" MENU > Datei > Sketchbook > DisplayBeispiele > constellation.ino
Hackerl = Verifizieren
Pfeil = Hochladen ORDNER constellation | constellation.ino | Display.cpp | Display.h | #include "Display.h" #include "SPI.h" Display lcd = Display(); void setup() { lcd.init(20); lcd.clearVideoBuffer(); } void loop() { byte x = random(0, 128) ; byte y = random(0, 64) ; lcd.drawPixel(x, y); lcd.show(); } Abb.4.1: Zufällige Punkte auf dem Display Zunächst stellt sich wieder die Frage: Wieso wird ein Video-Buffer (Grafikspeicher] benötigt? Die Antwort ist ähnlich wie beim Textdisplay: Sie wollen eine Grafik Stück für Stück aufbauen, ohne dabei den schon angezeigten Inhalt zu ersetzen. Sie können die Register des Displays-alerdings nicht auslesen. Deswegen benötigen Sie einen eigenen auslesbaren und beschreibbaren Videospeicher, der sich dann auf das Display übertragen lässt. Seite 48 001 #include "Display.h" 002 #include "SPI.h" 003 004 Display lcd = Display(); 005 006 void setup() { 007 lcd.init(0): 008 lcd.clearVideoBuffer(); 009 } Das Programm beginnt mit der üblichen Initialisierung des Displays, diesmal klassisch als Instanz von Display und nicht, wie im vorangegangenen Kapitel, von TextDisplay. Im nächsten Schritt wird der Video-Buffer mit dem neuen clear-Befehl gelöscht, um eventuelle Rückstände aus älteren Programmen zu entfernen. Ziel des Prögramms soll sein. zufällige Pixel auf das Display zu zeichnen. Die bereits aktivierten Pixel sollen natürlich nicht gelöscht werden, weshalb der Video-Buffer genutzt wird. 001 void loop() { 002 byte x = random(0, 128) ; 003 byte y = random(0, 64) ; 004 005 lcd.drawPixel(x, y); 006 lcd.show(); 007 } In der loop-Routine definieren Sie zunächst zwei Bytes, die mit dem random-Befehl mit einer Zahl im Bereich von 0 bis 128 bzw. 0 bis 64 gefüllt werden. Nun können Sie mithilfe des Befehls drawPixel(x, y) einen Punkt an die zufällig gesetzten Koordinaten in den Video-Buffer eintragen. Damit das neue Bild auf dem Display erscheint, muss noch der Buffer mit dem Befehl lcd.show ( ) auf das Display übertragen werden. Das ist bereits der gesamte Quelltext für den ersten Versuch. Das Programm läuft nach dem Upload in einer Loop durch und füllt das Display nach und nach mit zufälligen Punkten, bis das Display voll ist und man keine Veränderung mehr erkennen kann. Ein delay-Befehl ist dabei nicht nötig, da der gesamte Vorgang (das Schreiben per Und-Verknüpfung in den Buffer und die Ausgabe des kompletten Buffers zeilenweise auf das Display) eine gewisse Zeit in Anspruch nimmt.
Seite 49
4.2 Scope - ein einfaches Oszilloskop Im zweiten Beispiel zum Grafikdisplay geht es schon um eine praktisch nutzbare Anwendung, es handelt sich nämlich um ein einfaches Oszilloskop. Als Eingang dient der analoge pin-A0. Nach dem Aufspielen auf Ihren ARDUINO-Controller können Sie die Funktionalität testen, indem Sie mithilfe eines Widerstands eine Verbindung zu einem GND-Pin herstellen. Das anfängliche Schwingen der Linie, erzeugt durch die 50Hz Einstreuungen aus der Netzleitung, verschwindet und Sie sehen einen eindeutigen Nullpegel am unteren Rand des Displays. Mit einer Verbindung zum 5,0V pin lässt sich dagegen eine konstante Linie etwas oberhalb der Displaymitte konstruieren. Damit Sie aber auch ein definiertes Signal sehen können, gibt der digital pin-3 ein Testsignal aus. Bei diesem Testsignal handelt es sich um ein Rechtecksignal, das durch den Timer2 erzeugt wird.
Hackerl = Verifizieren
Pfeil = Hochladen ORDNER scope | scope.ino | Display.cpp | Display.h | #include "Display.h" #include "SPI.h" /****************Scope Settings*****************/ word store = 200; // Zeige Messspeicher für X ms an byte focus = 2; // Pixelbreite der Signaldarstellung byte yPos = 10; // Signalverschiebung in y-Richtung byte time = 2; // Zeitbasis - Prescaler ADC - Werte von 0-7 / Prescaler(1, 2, 4, 8, 16, 32, 64, 128) byte volts = 5; // Signalstärke über Division des ADC Wertes (/2^volts) Display lcd = Display(); byte values[128] = {}; void setup() { //Testpoint pinMode(3, OUTPUT); TCCR2A = _BV(COM2B1) | _BV(WGM21) | _BV(WGM20); TCCR2B = 0<<CS22 | (1<<CS21) | (1<<CS20); OCR2B = 128; ADCSRA &= ~7; ADCSRA |= time; lcd.init(20); pinMode(A0, INPUT); } void loop() { delay(store); lcd.clearVideoBuffer(); for (byte x=0; x<128; x++) { values[x] = (analogRead(A0) >> volts) + yPos; } for (byte x=0; x<128; x++) { for (byte p=0; p<focus; p++) { lcd.drawPixel(x, 64-values[x] - p + 1); } } lcd.show(); } Abb. 4.2: Das Rechtecksignal auf dem Display Sie können das Oszilloskop natürlich auch für typische Alltagssituationen im Labor verwenden. Dazu sollten Sie sich die Einstellungsmöglichkeiten genauer ansehen. Im Quelltext finden Sie direkt zu Beginn eine ganze Reihe wichtiger Parameter, die Sie verändern können. Die meisten dieser Einstellungen sind Ihnen geläufig, wenn Sie schon einmal mit einem Oszilloskop gearbeitet haben. In den darüber stehenden Kommentaren sind die Parameter aber auch erklärt. Das Array values wird benutzt, um die gemessenen Werte zwischenzuspeichern, bis sie analysiert und ausgeben werden. Seite 50 001 #include "Display.h" 002 #include "SPI.h" 003 004 /****************Scope Settings*****************/ 005 // Zeige Messspeicher für X ms an 006 word store = 200; 007 008 // Pixelbreite der Signaldarstellung 009 byte focus = 2; 010 011 // Signalverschiebung in y-Richtung 012 byte yPos = 10; 013 014 // Zeitbasis - Prescaler ADC - Werte von 0-7 / Prescaler (1, 2, 4, 8, 16, 32, 64, 128) 015 byte time = 2; 016 017 // Signalstärke über Division des ADC Wertes (/2hoch volts) 018 byte volts = 5: 019 020 Display lcd = Display( ); 021 byte values[128) = { }; In der Setup-Routine geht es vor allem um die Programmierung des bereits erwähnten Testsignals. Eine Besonderheit bei dieser Verwendung des Timers2 ist, dass für die Generierung des Signals keine Rechenzeit in Anspruch genommen wird. Hier wird ein Timer so eingestellt, dass nach einer bestimmten Zeit automatisch der Portzustand getoggelt, also umgeschaltet wird. Hier werden die Timerregister direkt programmiert. Genauere Informationen dazu liefert das Datenblatt des ATmega328. 001 void setup() { 002 / / Testpoint 003 pinMode(3, OUTPUT); 004 TCCR2A = _BV(COM2B1) | _BV (WGM21) | _BV(WGM20); 005 TCCR2B = 0<<CS22 | (<<CS21) | (1<<CS20); 006 OCR2B = 128; 007 008 ADCSRA &= ~7; 009 ADCSRA I= time; 010 lcd.init(0); 011 pinMode(A0, INPUT); 012 } Vor der Initialisierung des Displays findet noch die Aktivierung des Analog-Digital-Wandlers statt. Seite 51 Zusätzlich wird der Faktor für den internen Frequenzteiler des ADC gesetzt. Verglichen mit einem analogen Oszilloskop, wird damit die Zeitbasis eingestellt. Diese Verwendung des ADC bringt im Gegensatz zur Arduino-Funktion readAnalog() einen entscheidenden Geschwindigkeitsvorteil mit dem nun auch NF-Signale auf unserem Oszilloskop sichtbar gemacht werden können. 001 void loop() { 002 delay(store); 003 lcd.clearVideoBuffer(); 004 for (byte x=0; x<128; x++) { 005 values[x] = (analogRead(A0) >> volts) + yPos; 006 } 007 for (byte x=0; x<128; x++) { 008 for (byte p=0; p<focus; p++) { 009 lcd.drawPixel(x, 64-values[x] - p + 1); 010 } 011 } 012 lcd.show(); 013 } Wichtig für die Ausgabe auf dem Display ist vor allem die loop-Routine. Die erste for-Schleife sorgt dafür, dass 128 Werte direkt hintereinander gemessen und zwischengespeichert werden. Eine schrittweise Erfassung der Messdaten und direkte Ausgabe würde zu viel Zeit in Anspruch nehmen. Diese Werte werden nach den am Anfang des Quelltextes gesetzten Parametern verändert. Zunächst wird also das gemessene Byte um die in der Variable volts definierten Stellen nach rechts verschoben. Bei dem max. messbaren Wert des ADC, nämlich 1024 für eine Spannung von 5,0 V, kommt als Ergebnis der Wert 32 heraus. Das gilt natürlich nur für die Standardeinstellung von volts = 5. Dadurch verliert das Oszilloskop zwar an Genauigkeit, allerdings erlaubt die Darstellung so oder so nur eine Auflösung von 64 Pixeln. Deswegen werden die unteren Werte, die keine große Bedeutung haben, abgeschnitten. Der neue Wert wird schließlich noch durch eine Addition mit der yPos-Konstanten angepasst, die für eine Verschiebung auf der Y-Achse zuständig ist. Nach dem Zwischenspeichern der 128 Werte folgt die Ausgabe in den nächsten for-Schleifen. Die äußere Schleife geht dabei die X-Positionen des Displays durch. Die innere for-Schleife ist für den Fokus zuständig. Der Standartwert von focus = 2 bedeutet, dass zwei Pixel für die Darstellung eines Werts [Breite) verwendet werden. Seite 52 In der darauffolgenden Zeile wird der Punkt mit der Funktion drawPixel ( ) in den Video-Buffer geschrieben. Der X-Wert läuft also insgesamt von links nach rechts durch, der Y-Wert wird anhand der gemessenen Werte bestimmt und auf die Skala des Displays umgerechnet. Dadurch, dass die Punkte kontinuierlich auf das Display geschrieben werden, erscheinen die Werte als Linien und Kurven. Das lcd.show( ) am Ende der loop-Funktion überträgt nun noch der Inhalt des Video-Buffers auf das Display, bevor die Prozedur von vorn beginnt. 4.3 Blick in die Library III - die Grundlagen des Grafikdisplays Wie bereits erwähnt wurde, ist die Library für die grafischen Darstellungen eine Weiterentwicklung der Bibliothek aus dem Kapitel „Blick in die Library I" Die meisten Funktionen wurden bereits behandelt und nur neue Funktionen werden hier genauer betrachtet. Als Referenz-Library wird die aus dem Beispiel „Scope" herangezogen. 001 [...] 002 void clearVideoBuffer(void); 003 void drawPixel(byte x, byte y); 004 void show(); 005 006 private: 007 byte videoBuffer [1024]; 008 } ; Es gibt 3 neue Funktionen, wie man durch einen Vergleich mit der Header-Dateien aus dem Beispiel „Analoginput" gut erkennen kann. Außerdem gibt es eine neue Privatvariable, den videoBuffer. Die Funktionen selbst finden Sie wie gewohnt in der Datei Display.cpp 001 void Display::drawPixel(byte x, byte y) { 002 if ((x >= 128) II (y >= 64)) { 003 return; 004 } 005 videoBuffer[x + (y/8) * 128] |= _BV((y%8)); 006 } Seite 53 Obwohl die drawPixel -Funktion nicht die erste neue Funktion in der Klasse ist, wird sie an dieser Stelle als Erstes erklärt, da sie grundlegend für die andern Funktionen ist. Mit ihr wird ein Pixel an einer bestimmten Stelle auf dem Display aktiviert, wie man im Beispiel „Constellation" gut sehen konnte. Die Koordinaten des Pixels werden als Parameter übergeben. Eine If-Abfrage überprüft zunächst, ob der Punkt im zulässigen Bereich oder außerhalb des Displays liegt. Ist ein Punkt unzulässig, wird die Funktion sofort mit dem Return-Befehl beendet. Ist alles in Ordnung, geht es mit der nächsten Zeile weiter, die das entsprechende Pixel in den Buffer einträgt. Das Setzen des Pixels im Buffer sieht auf den ersten Blick kompliziert aus, lässt sich allerdings ganz einfach erklären. Dazu muss man allerdings verstehen, wie der Video-Buffer aufgebaut ist. Bei der Textdisplay-Library gab es ebenfalls einen Buffer, der für den Text zuständig war. Das entsprechende Array war allerdings zweidimensional. Hier ist der Buffer eindimensional und hat eine Größe von 1.024 Positionen - 1.024 Stellen, weil es 8 Zeilen mit je 128 Spalten gibt [8 x 128 =1.024]. Das Display wurde schließlich zu Beginn in Pages (Zeilen) und Collums [Spalten) aufgeteilt. Jede dieser 1.024 Stellen beinhaltet nun 8 Pixel und kann somit über ein Byte gesteuert werden. Wollen Sie z. B. das Pixel oben links aktivieren, müssen Sie an die Funktion die Parameter 0,0 übergeben. Zur Berechnung der richtigen Position des Bytes im Video-Buffer kommt folgende Formel zum Einsatz: Position = x + [y/8] x 128 Setzt man für x und y jeweils 0 ein, kommt 0 heraus. Das zu setzende Pixel wird also in Byte 0 [erstes Byte) abgebildet. Dieses Byte im Video-Buffer beinhaltet allerdings die Information von 8 Pixeln. Es muss also noch berechnet werden, welche Stelle des Bytes aktiviert werden muss. Dafür ist die Funktion Byte = _BV (y%8) zuständig. Hierbei handelt es sich zunächst wieder um eine Modulo-Rechnung, deren Ergebnis der Funktion _BV übergeben wird. Es wird also der Rest ermittelt, der übrig bleibt, wenn y durch 8 geteilt wird (0 / 8 = 0 mit Rest 0). Bei _BV handelt es sich um eine Funktion, die das als Parameter übergebene Bit in einem leeren Byte aktiviert. Mit anderen Worten wird in diesem Beispiel mit y = 0 das Byte 0b00000001 erstellt, also das Bit an der Stelle 0 aktiviert. Seite 54 Dieses Byte wird nun aber nicht einfach in die Position 0 des Video-Buffers eingetragen, sondern mit dem Bytewert aus dem Video-Buffer oder-verknüpft [Operator |=.] Das Resultat ist, dass ein bereits an dieser Stelle aktiviertes Pixel nicht überschrieben, sondern einfach das neue Pixel dazu geschrieben wird. Abb. 4.3: Die Pixel werden ODER verknüpft Was ist aber, wenn Sie ein Pixel an der untersten, rechten Ecke aktivieren möchten? Dann müssten Sie an die Funktion die Werte x=127 / y=63 übergeben. Würden Sie mit einem Taschenrechner die obere Formel ausrechnen, käme 1.135 heraus - ein Wert, der außerhalb des Arrays liegt. Der Controller führt jedoch eine ganzzahlige Division durch, er kennt also keine Nachkommastellen. Mit diesem neuen Wissen kommen Sie zu dem Ergebnis, dass dem Array an der Stelle 1023 ein neuer Wert zugeordnet wird. Sein Inhalt wird im zweiten Teil berechnet. 63%8 ergibt 7. Mit der Funktion _BV wird also das Bit 7 aktiviert. Das Byte lautet also 0b10000000 Mit diesen beiden Beispielen wurde verdeutlicht, wie der Buffer beschrieben wird. Sie wissen bereits, dass das LSB [Least Signifikant Bit = niederwertigstes Bit] dem obersten Pixel einer Stelle entspricht. Dagegen steht das MSB (Most Signifikant Bit = höchstwertiges Bit) für das unterste Pixel. Anhand der oberen Berechnungen ist das passende Byte aber schnell erstellt. 001 void Display::show() { 002 short position=0; 003 for (byte page=0; page<8; page++) { 004 setPageAddress(page); 005 setCol umnAddress (0): 006 for (byte column=0; column<128; column++) { 007 writeData(videoBuffer[positioh++]); 008 } 009 } Seite 55 Nachdem das ein oder andere Byte in den Buffer geschrieben wurde, muss das gesamte Array noch auf das Display übertragen werden. Die Funktion ähnelt dabei sehr der writeTextBuffer( ) -Funktion aus der Textdisplay-Library. Seite für Seite (also für jede der 8 Pages) und Zeile für Zeile (alle 128 Spalten) werden mit den for-Schleifen durchwandert und der Buffer-Inhalt wird per writeData wie gewohnt auf das Display übertragen. 001 void Display::clearVideoBuffer() { 002 for(word i=0; i<1024; i++) { 003 videoBuffer[i] = 0; 004 } 005 } Ebenfalls nichts Neues ist die clearVideoBuffer-Funktion. Die Methode dient dazu, alle Stellen des Video-Buffers zu löschen, indem eine Null eingetragen wird. Im Grunde ist sie also vergleichbar mit der clear-Funktion aus der Textdisplay-Library, nur dass diesmal ein eindimensionales Array als Buffer verwendet wird. 4.4 Nikolaus - ich mal das Haus vom Nikolaus Viele werden das alte Spiel aus Kindertagen wahrscheinlich noch kennen. Ziel ist es, ein einfaches Haus zu malen, allerdings ohne den Stift abzusetzen. ORDNER nikolaus | nikolaus.ino | Display.cpp | Display.h | #include "Display.h" #include "SPI.h" Display lcd = Display(); void setup() { lcd.init(20); } void loop() { lcd.clearVideoBuffer(); lcd.drawLine(54, 54, 54, 34); lcd.show(); delay(1000); lcd.drawLine(54, 34, 64, 24); lcd.show(); delay(1000); lcd.drawLine(64, 24, 74, 34); lcd.show(); delay(1000); lcd.drawLine(74, 34, 54, 34); lcd.show(); delay(1000); lcd.drawLine(54, 34, 74, 54); lcd.show(); delay(1000); lcd.drawLine(74, 54, 74, 34); lcd.show(); delay(1000); lcd.drawLine(74, 34, 54, 54); lcd.show(); delay(1000); lcd.drawLine(54, 54, 74, 54); lcd.show(); delay(1000); } Abb. 4.4: Das Haus vom Nikolaus Seite 56 Dabei spricht man mit jeder Linie eine Silbe des folgenden Satzes aus: „Das ist das Haus vom Ni-ko-laus". Anhand dieses einfachen Kinderspiels lernen Sie in diesem Kapitel eine neue Funktion der Library kennen. 001 #include "Display.h" 002 #include "SPI.h" 003 004 Display lcd = Display(); 005 006 void setup() { 007 lcd.init(0);
008 }
Die ersten Zeilen des Programms kennen Sie mittlerweile wahrscheinlich auswendig. Auch die setup-Routine besteht in diesem Fall nur aus der Initialisierung des Displays auf altbekannte Weise. In der loop-Funktion schließlich wird zunächst einmal der Video-Buffer mithilfe des clearVideoBuffer -Befehls bereinigt. Danach folgt direkt der neue Befehl lcd.drawLine( ) 001 void loop() { 002 lcd.clearVideoBuffer(): 003 lcd.drawLine(54, 54, 54, 34); 004 lcd.show(); 005 [ ... ] Wie Sie sicher schon vermutet haben, dient dieser Befehl dazu, eine Linie auf das Display zu zeichnen. Dazu müssen Sie natürlich auch die passenden Parameter angeben, nämlich die Start- und die Endposition der Linie. Genaugenommen sieht die Funktion dann etwa so aus: drawLine(x-Startwert, y-Startwert, x-Endwert, y-Endwert); In einem ersten Test können Sie eine Linie von der Position 54,54 zur Position 54,34 zeichnen. Vergessen Sie nicht, den Buffer mit der Funktion 1cd.Show( ) auf das Display zu übertragen. Sie sehen einen ersten vertikalen Strich auf dem Display und damit die erste Wand des Nikolaushauses. Seite 57 001 [ ... ] 002 delay(1000); 003 lcd.drawLine(54, 34, 64, 24); 004 lcd.show ( ) ; 005 delay (1000 ) ; 006 [ ... ] Nun können Sie fortfahren, indem Sie zunächst einmal einen Wartebefehl (delay) mit einer Wartezeit von 1.000 ms einfügen. Nach dem Wartebefehl folgt eine weitere Linie von der alten Position 54,34 bis zur Position 64,24 und die Übertragung des neuen Inhalts auf das Display. Sie sollten auch hier einen Wartebefehl einfügen, damit Sie die neue Teilzeichnung sehen können, bevor die loop-Funktion von vorn beginnt. Die nächsten Zeilen sehen ähnlich aus. Es wird immer zunächst eine Linie gezeichnet, das Ergebnis mit der show-Funktion auf das Display übertragen und das Programm kurzzeitig mit einem Wartebefehl angehalten. Damit Sie nicht selbst ausprobieren müssen, welche Koordinaten zu dem gewünschten Haus führen, finden Sie hier alle Werte in der Tabelle aufgelistet. Die ersten beiden Werte haben Sie unter Umständen schon in Ihrem Programm realisiert. Mit den anderen können Sie dann auf gleiche Weise verfahren: Start: Start: Ende: Ende: X-Position Y-Position X-Position Y-Position 1. 54 54 54 34 2. 54 34 64 24 3. 64 24 74 34 4. 74 34 54 34 5. 54 34 74 54 6. 74 54 74 34 7. 74 34 54 54 8. 54 54 74 54 Nach dem Upload können Sie dabei zusehen, wie sich das Haus Schritt für Schritt aufbaut. Das ist natürlich nur ein Beispiel. Sie können nach Belieben eine neue Zeichnung mit eigenen Linienkoordinaten entwickeln. Seite 58 4.5 Analysis - Mathe-Unterricht mit Arduino Im vorangegangen Beispiel haben Sie einfache Linien gezeichnet. Nun geht es schon um etwas komplexere Gebilde. Ziel ist, eine mathematische Parabelfunktion auf das Display zu zeichnen. Das hört sich vielleicht kompliziert an, ist aber im Detail gar nicht so komplex. ORDNER analysis | analysis.ino | Display.cpp | Display.h | Front.h #include "Display.h" #include "SPI.h" Display lcd = Display(); void setup() { lcd.init(20); lcd.clearVideoBuffer(); double a = -1; double b = 3; double c = 0; short factorX = 10; short factorY = 5; double codomain = 10; //Zeichne Koordinatensystem lcd.drawLine(0, 31, 127, 31); lcd.drawLine(63, 0, 63, 63); lcd.drawString(5, 15, "-x^2+3x"); for (double x=-1.0 * codomain; x<codomain; x=x+0.1) { int yValue = 31 - (((a*x*x) + (b*x) + c) * factorY) ; int xValue = x*factorX + 63; if (yValue >= 0 && yValue < 64) { lcd.drawPixel(xValue, yValue); lcd.show(); } } } void loop() { } Abb. 4.5: Eine umgedrehte und verschobene Parabel Der gesamte Quelltext befindet sich in diesem Programm wieder allein in der Setup-Routine, damit das Programm nicht als Dauerschleife ausgeführt wird. In dieser Setup-Routine wird zunächst einmal das Display initialisiert und schließlich der Video-Buffer gelöscht. Anschließend müssen Sie eine ganze Palette von Variablen definieren, im Folgenden kurz aufgelistet: 001 void setup() { 002 [...] 003 double a = -1; 004 double b = 3; 005 double c = 0; 006 short factorX = 10; 007 short factorY = 5; 008 double codomain = 10; 009 [...] Welche Bedeutungen die Variablen haben, wird zu einem späteren Zeitpunkt erklärt. Zunächst einmal zeichnen Sie nur das Koordinatensystem mit der aus dem vorangegangenen Kapitel bekannten Funktion drawLine 001 // Zeichne Koordinatensystem 002 lcd.drawLine(0, 31, 127, 31); 003 lcd.drawLine(63, 0, 63, 63); 004 lcd.drawString(5, 15, "-x^2+3x"); Seite 59 Außerdem finden Sie eine neue Funktion in der letzten Zeile des abgebildeten Auszugs, nämlich die Funktion drawString. Sie erlaubt Ihnen, Texte an beliebigen Stellen auf dem Display zu platzieren. Die ersten beiden Parameter enthalten die X- und die Y-Koordinate und als dritter Parameter wird der Text übergeben. In diesem Programm wird die Funktion als mathematischer Ausdruck in den oberen linken Bereich des Koordinatensystems geschrieben. Im Unterschied zur Darstellung von Texten in vorherigen Beispielen arbeitet die Funktion drawString voll grafisch, d.h. sie sind bei der Darstellung von Zeichen nicht mehr an das Raster mit 8 Zeilen gebunden. Was nun folgt, ist der entscheidende Teil des Programms. In der folgenden for-Schleife werden nämlich nacheinander anhand der vorgegeben Formel Werte berechnet und an die entsprechenden Positionen ein Punkt gesetzt. So wird nach und nach die Parabel aufgebaut. 001 for (double x=-1.0 * codomain; x<codomain; x=x+0.1) { 002 int yValue = 31 - (((a*x*x) + (b*x) + c) * factorY) ; 003 int xValue = x*factorX + 63; 004 if (yValue >= 0 && yValue < 64) { 005 lcd.drawPi xel (xValue, yValue); 006 lcd.show(); 007 } 008 } Interessant ist an dieser Stelle auch die if-Abfrage, die verhindert, dass Werte außerhalb des Displays gezeichnet werden. Das geschieht durch einen einfachen Vergleich der Y-Werte mit den Grenzen des Displays. Die Variable codomain gibt den Wertebereich der Funktion an, also den Teil der Funktion, der abgebildet werden soll. In diesem Beispiel ist ein Wertebereich von -10.0 bis +10.0 gewählt. Dabei wird der gesamte Bereich in der for-Schleife in 0.1er-Schritten durchlaufen. Die Variablen factorx und factory sind für die Skalierung auf dem Display verantwortlich. Mit den eingestellten Faktoren wird die Funktion vergrößert abgebildet und ist damit auf dem Display gut sichtbar. Sie können auch eine andere Parabel eingeben und zeichnen lassen. Dabei sollten Sie nicht zu komplizierte Funktionen verwenden. Als ein einfaches Beispiel können Sie die Standartparabel f(x] = x2 verwenden. Dazu müssen Sie die Variable a = 1 und b = 0 setzen. C bleibt in diesem Bespiel 0. Seite 60 Nun stimmt der Text am Rand zwar nicht mehr, aber Sie können ihn einfach anpassen. Mit den drei Parametern können Sie nach Belieben experimentieren. Sogar eine einfache Linie durch den Ursprung ist möglich, indem Sie a = 0 und c = 0 setzen. Die Steigung wird dann durch den b-Parameter bestimmt. Somit lassen sich die wichtigsten mathematischen Funktionen einfach auf dem Display plotten. 4.6 SmartHome - Hausautomatisierung auf dem Display In dem folgenden Programm werden Sie eine ganze Reihe neuer Dinge kennenlernen. Zum einen werden Sie die beiden Buttons zum ersten Mal selbst in einem Programm verwenden und zum anderen lernen Sie, wie Sie auch komplexe Bilder schnell und unkompliziert auf dem Display darstellen können. ORDNER smartHome | smartHome.ino | Display.cpp | Display.h | Front.h | Home.h | #include "Display.h" #include "SPI.h" #include "Home.h" #define LCD_BUTTON_LEFT A4 #define LCD_BUTTON_RIGHT A5 Display lcd = Display(); void setup() { lcd.init(20); pinMode(LCD_BUTTON_LEFT, INPUT_PULLUP); pinMode(LCD_BUTTON_RIGHT, INPUT_PULLUP); } void loop() { lcd.clearVideoBuffer(); lcd.drawBitmap(0, 0, 128, 64, home); drawOutdoorLightingOn(!digitalRead(LCD_BUTTON_LEFT)); drawFrontDoorOpen(!digitalRead(LCD_BUTTON_RIGHT)); lcd.show(); } void drawOutdoorLightingOn(boolean isOn) { if(isOn) { //Front lcd.drawLine(6, 22, 8, 23); lcd.drawLine(5, 25, 7, 25); lcd.drawLine(6, 28, 8, 27); //Back lcd.drawLine(94, 39, 96, 38); lcd.drawLine(95, 41, 97, 41); lcd.drawLine(94, 43, 96, 44); } else { } } void drawFrontDoorOpen(boolean isOpen) { if(isOpen) { lcd.drawLine(14, 38, 14, 30); } else { lcd.drawLine(14, 38, 23, 37); } } Abb. 4.6: Das Haus aus dem Beispiel Wenn Sie in den smartHome-Sketchordner sehen, werden Sie feststellen, dass sich darin mehr Dateien als in den vorangegangenen Beispielen befinden. Unter anderem finden Sie die Bitmap-Datei eines Hauses. Dieses Haus soll zunächst einmal auf das Display übertragen werden. Eine Möglichkeit wäre sicher, die Zeichnung Pixel für Pixel in den Editor auf der Mosaic-Seite zu übertragen. Allerdings würde das bei der Komplexität der Zeichnung sehr lange dauern. Deshalb wird an dieser Stelle eine einfachere Möglichkeit vorgestellt. Vielleicht ist Ihnen der Button auf der Mosaic-Seite http://tiny.sys-tems/article/mosaic.html schon aufgefallen, der mit „Datei auswählen•• beschriftet ist. Es ist nämlich möglich, Bitmap-Dateien auf die Seite hochzuladen und automatisch in Hex-Code formatieren zu lassen. Seite 61 Dabei sollten Sie allerdings bedenken, dass sich nicht alle Bitmap-Dateien so einfach konvertieren lassen. Sie müssen darauf achten, dass es sich um ein monochromes (einfarbiges) Bitmap handelt. Am leichtesten ist es, sich selbst mit einem Malprogramm wie Microsoft Paint ein solches Bitmap zu erstellen. Eine Alternative ist z.B. das Zeichenprogramm Gimp. Die Bilder in diesem Text basieren aber auf Paint. Als ersten Versuch könnte man z.B. das Haus des Nikolaus nachzeichnen. Dazu stellen Sie zunächst einmal die Auflösung des Bildes auf die Auflösung des Displays ein, also 128 x 64 Pixel. Das legen Sie entweder unter den Einstellungen fest oder Sie ändern die weiße, zu bemalende Fläche entsprechend. Nun können Sie sich auf dem Bild austoben. Wenn Sie die Datei speichern wollen, müssen Sie im Untermenü als Dateityp „Monochrom-Bitmap" auswählen. Abb. 47: So Speichern Sie das Bild in MS Paint richtig Diese gespeicherte Datei können Sie nun auf der Mosaic-Seite hochladen. Als Ausgabe bekommen Sie wieder die Bildinformation als Hex-Zahlen. Diese Hex-Zahlen kopieren Sie vorzugsweise in eine eigene Datei. Dabei gehen Sie wie folgt vor. Öffnen Sie ein neues Projekt auf Basis des HelloWorldDIY-Beispiels. In der oberen Leiste finden Sie, neben den Library-Dateien auch die Datei Bitmap.h. In dieser Datei finden Sie eine leere Konstante mit dem Namen home. 001 const PROGMEM byte home [1] = { Zwischen die geschweiften Klammern können Sie nun Ihren Hex-Code einfügen und das Projekt schließlich unter anderem Namen abspeichern. Seite 62 ORDNER Hello World DIY | Hello World DIY.ino | Bitmap.h | Display.cpp | Display.h | Front.h | TextDisplay.cpp | TextDisplay.h | Abb. 4.8: So fügen Sie den Hex-Code ein Die Bitmap-Datei muss in Ihrem Programm durch #include „Bitmap.h" eingebunden werden. In die Setup-Routine fügen Sie außerdem nach der Initialisierung des Displays die unten stehenden Zeilen ein: 001 #include "Display.h" 002 #include "SPI.h" 003 #include "Bitmap.h" 004 005 Display lcd = Display(); 006 007 void setup() { 008 lcd.init(0); 009 lcd.clearVideoBuffer(); 010 lcd.drawBitmap(0, 0, 128, 64, home); 011 lcd.show(); 012 } Die Parameter der drawlBitmap-Funktion sind schnell erklärt: Die ersten beiden Zahlen (hier jeweils 0] geben die Startposition auf dem Display an und die nächsten beiden Zahlen die Breite und Höhe des zu zeichnenden Bitmaps. Als letzter Parameter wird die von Ihnen in der Bitmap.h-Datei bearbeitete Konstante home übergeben. Wenn Sie dieses kleine Programm laden, erscheint Ihre Zeichnung auf dem Display. Seite 63 Tipp: Die Funktion erlaubt es Ihnen, auch mehrere kleine Bitmops an verschiedene Positionen im Display zu übertragen und auf diese Weise ein Gesamtbild zu erzeugen. Das war der 1. Teil des Beispiels, der dazu dient, den grundsätzlichen Vorgang zum Konvertieren von Bitmops zu erklären. Im 2. Teil smartHome (Hausautomatisierung) wird demonstriert, wie Sie eine intuitive Visualisierung von Aktoren und Sensoren in einem Haus darstellen können. Dazu wird die in dem Beispielordner integrierte Datei Home.h benutzt, um das Haus zu zeichnen. Wenn Sie dieses Beispiel also selbst entwickeln möchten, sollten Sie diese Datei in Ihren Sktechordner kopieren und per inlcude einbinden. Als nächsten Schritt erweitern Sie Ihr Programm um die Definition der beiden Buttons zwischen den include-Befehlen und der Display-Instanziierung: 001 #include "Display.h" 002 #include "SPI.h" 003 #include "Home.h" 004 005 #define LCD_BUTTON_LEFT A4 006 #define LCD_BUTTON_RIGHT A5 007 008 Display lcd - Display( ); 009 010 void setup() ) {
011 [ ... ]
Nun geht es um die setup-Routine. Verschieben Sie die dort eingefügten clearVideoBuffer-, drawBitmap- und show-Befehle in die loop-Routine und fügen die pinMode-Einstellungen für die Buttons ein: 001 void setup() ) { 002 lcd.init(0); 003 pinMode(LCD_BUTTON_LEFT, INPUT_PULLUP); 004 pinMode(LCD_BUTTON_RIGHT, INPUT_PULLUP); 005 } Seite 64 Wie man sieht, werden die Buttons mit internem Pull-up betrieben. Dadurch ist der Zustand bei Tastendruck LOW. Das ist wichtig, wenn Sie in der loop-Routine die zwei neuen Funktionen drawOutdoorLighting und drawFrontDoorOpen, wie unten abgebildet, einfügen: 001 void loop() { 002 lcd.clearVideoBuffer(); 003 lcd.drawBitmap(0, 0, 128, 64, home); 004 drawOutdoorLightingOn(!digitalRead(LCD_BUTTON_LEFT)); 005 drawFrontDoorOpen(!digitalRead(LCD_BUTTON_RIGHT)); 006 lcd.showO; 007 } Der durch digital Read ermittelte Wert der Buttons wird nämlich durch das Ausrufezeichen invertiert. drawOutdoorLightingOn und drawFrontDoorOpen sind übrigens keine Funktionen der Library, sondern werden von Ihnen selbst implementiert. Sie bekommen als Parameter den Zustand der Buttons übergeben und zeichnen dann entsprechend das dazu passende Bild. Die erste Funktion soll ein aktives Außenlicht zeichnen. Das wird dargestellt durch Linien, die von den Lichtern wegzeigen. Die zweite Funktion zeigt an, ob die Außentür offen oder geschlossen ist. Das wird ebenfalls durch eine Linie dargestellt, die ihre Position verändert. Beide Funktionen werden hier im Quelltext vorgestellt. 001 void drawOutdoorLightingOn(boolean isOn) { 002 if(is0n) { 003 // Front 004 lcd.drawLine(6, 22. 8, 23); 005 lcd.drawLine(5, 25. 7, 25); 006 lcd.drawLine(6, 28, 8. 27); 007 008 //Back 009 lcd.drawLine(94, 39, 96, 38); 010 lcd.drawLine(95. 41, 97. 41); 011 lcd.drawLine(94, 43, 96, 44); 012 } else { 013 } 014 } 015 016 void drawFrontDoorOpen(boolean isOpen) { 017 if(isOpen) { 018 lcd.drawLine(14, 38, 14, 30); 019 020 } else { 021 lcd.drawLine(14, 38, 23, 37); 022 } 023 } Seite 65 Laden Sie dieses Programm nun auf den Controller. Durch Drücken der beiden Taster wird deutlich, was genau mit den Funktionen gemeint ist. Es erscheint eine Tür auf dem Display, die im Normalzustand offen ist. Drücken Sie auf den rechten Button, schließt sich diese Tür. Mit Druck auf den linken Button werden die beiden Außenlichter am linken und am rechten Rand des Hauses aktiviert. Sie haben also gerade die Grundlage für eine intuitive Visualisierung für eine Hausautomatisierung geschaffen. 4.7 Blick in die Library IV - Linien, Texte und Bilder Beim letzten „Blick in die Library" wurden die Grundlagen des Grafikdisplays vorgestellt, also vor allem, wie einzelne Pixel aktiviert und schließlich auf das Display übertragen werden. In diesem Kapitel werden die 3 neu dazugekommenen Funktionen genauer erklärt. Es geht also um die Fumktionen drawLine, drawString und drawBitmap. 001 void Display::drawLine(byte x0, byte y0 byte xl, byte yl) { 002 short dx = abs(xl-x0), sx = (x0<xl) ? 1 : -1; 003 short dy = abs(yl-y0), sy = (y0<y1) ? 1 : -1; 004 short err = (dx>dy ? dx : -dy)/2, e2; 005 006 while(true) { 007 drawPixel(x0, y0): 008 if (x0==xl && y0==y1) { 009 break; 010 } 011 e2 = err; 012 if (e2 >-dx) { 013 err -= dy; 014 x0 += sx; 015 } 016 if (e2 < dy) { 017 err dx; 018 y0 sy; 019 } 020 } 021 } Seite 66 Bei der ersten Funktion drawLine kommt ein spezieller Linienalgorithmus zum Einsatz. Der Algorithmus wurde nach seinem Erfinder Bresenham-Linienalgorithmus genannt. Kurz gesagt, hilft der Algorithmus bei der Berechnung einer Linie, die eigentlich zwischen zwei Displaypixeln verläuft, wie es bei schrägen Linien oft der Fall ist. Der Algorithmus entscheidet dann, ob das obere oder das untere Pixel für die Linie verwendet werden soll. Dadurch werden die Rundungsfehler, die automatisch bei Displays mit begrenzter Pixelzahl entstehen, minimiert. Abb. 4.9: So wird eine Line auf dem Display dargestellt In der Funktion passiert Folgendes: Als Parameter werden die Anfangs-und Endkoordinaten an die Funktion übergeben. Dann werden die beiden Differenzen zwischen den X- und Y-Werten ermittelt, um daraus schließlich einen Fehlerfaktor zu bestimmen. Nun wird die Linie in der while-Schleife gezeichnet, wobei der Fehlerfaktor ausschlaggebend ist für die Entscheidung, welches zweier möglicher Pixel verwendet werden soll. Die ermittelten Koordinaten werden in den Anfangskoordinaten gespeichert und mit drawPixel in den Buffer geschrieben. Wenn die veränderten Anfangskoordinaten schließlich den Endkoordinaten entsprechen, ist die Linie komplett in den Buffer gespeichert und die while-Schleife wird mit dem break-Befehl verlassen. Das Ergebnis kann nun mit der show-Routine aufs Display übertragen werden. Seite 67 Hier wurde auf eine genaue Analyse des Linienalgorithmus verzichtet. Für alle, die sich für den genauen Ablauf interessieren, seien die folgenden Wikipedia-Artikel empfohlen: http://en.wikipedia.org/wiki/Bresenharn%27s_line_algorithm http://de.wikipedia.org/wiki/Bresenharn-Algorithmus http://de.wikipedia.org/wiki/Rosterung_von_Linien Die 2. Funktion, drawString, ermöglicht Ihnen das, was Sie in den Kapiteln zum Textdisplay bereits kennengelernt haben, nämlich die Ausgabe von Texten auf dem Display. Diesmal gibt es allerdings kleine Unterschiede, denn Sie können den Text frei auf dem Display platzieren. Dazu übergeben Sie einfach die Koordinaten und den Text an die Funktion. Damit der Text überhaupt auf dem Display angezeigt werden kann, brauchen Sie natürlich wieder den Schriftsatz, und tatsächlich ist der von Ihnen in den Kapiteln zum Textdisplay verwendete Schriftsatz in der Font-Datei ebenfalls in die Klasse eingebunden. 001 void Display::drawString(byte x, byte y, const char *c) { 002 if(y > HEIGHT) { 003 return; 004 } 005 byte page — y/8; 006 do { 007 char character = c[0]; 008 if (! character) 009 return; 010 011 for (byte i =0; i<CHARACTER_WIDTH; i++ ) { 012 word charBuffer = pgm_read_ byte(font7x5+(character*6)+i) << y%8; 013 videoBuffer[x+(page*WIDTH)] |= charBuffer; 014 videoBuffer[x+(WIDTH*(page+1))] |= charBuffer >> 8; 015 x++; 016 } 017 } 018 while (c++); 019 } In der Funktion passiert Folgendes. Zunächst wird überprüft, ob der Text überhaupt auf einer zulässigen Höhe kleiner als die Displayhöhe geschrieben werden soll. In der do-while-Schleife werden nun, falls das aktuelle Zeichen kein leeres Zeichen ist, in einer for-Schleife alle 6 Bytes des aktuellen Zeichens in die berechneten Positionen des Video-Buffers übertragen, also ganz ähnlich wie Sie es bei der writeText - Buffer-Funktion des Textdisplays kennengelernt haben. Seite 68 Der Unterschied zum Textdisplay ist jedoch, dass der Text frei positionierbar ist und nicht ausschließlich im Raster der 8 Pages ausgegeben werden muss. Das wird erreicht, indem das 8 Pixel hohe Zeichenfragment in einem 16 Pixel großen Puffer (charBuffer) an die richtige Stelle eingefügt wird. Dieser Puffer wird daraufhin in zwei Bytes aufgeteilt und an die richtigen Stellen im Video-Buffer kopiert. 001 void Display::drawBitmap(byte x, byte y, byte width, byte height, const byte *bitmap) { 002 for (byte j=0; j<height; j++) { 003 for (byte 1=0; i<width; i++) { 004 if (pgm_read_byte(bitmap + i + (j/8)*width) &_BV(j708)) { 005 drawPixel(x+i, y+j); 006 } 007 } 008 } 009 } Die letzte in diesem Kapitel erklärte Funktion zum Grafikdisplay ist die drawBitmap-Funktion. Als Parameter werden eine Startkoordinate mit X- und Y-Wert, die Breite und Höhe sowie die Bitmap-Konstante übergeben. Da das Bitmap bereits als byte-Array vorliegt, müssen die einzelnen Werte nur noch in den Video-Buffer übertragen werden. Das geschieht einfach durch den Aufruf der Funktion drawPixel(). Die beiden for-Schleifen gehen dabei einfach die gesamte Höhe und Breite des Bitmaps durch, um alle Pixel zu erfassen. 4.8 AnalogWatch - Digitaluhr mit Ziffernblatt Bei diesem praktischen Beispiel wird eine digitale Uhr mit analogem Erscheinungsbild programmiert. Das Stellen dieser Uhr funktioniert ganz praktisch über den Serial Monitor der Arduino-Software. Diesmal wird der Quelltext des Programms besonders komplex und enthält viele Verweise auf äußere Ressourcen. Deswegen ist es empfehlenswert, das Beispiel nicht komplett selbst zu programmieren, sondern vielmehr zu versuchen, die Schritte des Programms mit diesem Text zu verstehen. Seite 69 Aus diesem Grund wird hier Stück für Stück der Quelltext vorgestellt und die entsprechenden Passagen werden inhaltlich erklärt. ORDNER analogWatch | analogWatch.ino | Display.cpp | Display.h | Front.h | Time.cpp | Time.h | Watch.h | #include "Display.h" #include "SPI.h" #include "Time.h" #include "watch.h" char inputBuffer[20]; Display lcd = Display(); void setup() { Serial.begin(9600); Serial.println("Set Time (Format: HHMMSS)"); lcd.init(20); } void loop() { int i = 0; if (Serial.available() > 0) { memset(inputBuffer, 0, sizeof(inputBuffer)); delay(50); while (Serial.available() > 0 && i < sizeof(inputBuffer) - 1) { inputBuffer[i] = Serial.read(); i++; } char hours[3] = {inputBuffer[0], inputBuffer[1], 0}; char minutes[3] = {inputBuffer[2], inputBuffer[3], 0}; char seconds[3] = {inputBuffer[4], inputBuffer[5], 0}; setTime(atoi(hours), atoi(minutes), atoi(seconds), 0, 0, 0); } lcd.clearVideoBuffer(); lcd.drawBitmap(0, 0, 128, 64, watch); lcd.drawLine(63, 32, 63+(int)(17*cos((hour()%12*30+minute()/2.0-90) * M_PI / 180.0)), 32+(int)(17*sin((hour()%12*30+minute()/2.0-90) * M_PI / 180.0))); lcd.drawLine(63, 32, 63+(int)(25*cos(((minute()*6)-90) * M_PI / 180.0)), 32+(int)(25*sin(((minute()*6)-90) * M_PI / 180.0))); lcd.drawLine(63, 32, 63+(int)(27*cos(((second()*6)-90) * M_PI / 180.0)), 32+(int)(27*sin(((second()*6)-90) * M_PI / 180.0))); lcd.show(); } Abb. 4.10: Die analoge Uhr Um zu verstehen, worum es geht, sollten Sie zunächst das Programm auf den Controller laden. Es erscheint eine analoge Uhr mit einigen Verzierungen rundherum. Die Zeiger stehen auf 12 Uhr. Sie können die Uhrzeit aber einstellen, indem Sie den Serial Monitor in der Arduino-Programmierumgebung öffnen. Dort wird Ihnen ein vom Mikrocontroller gesendeter Text angezeigt, der einen Hinweis auf das zu sendende Format enthält. Sie müssen die aktuelle Zeit nämlich eingeben, indem Sie zuerst zwei Stellen für die Stunde, dann zwei Stellen für die Minuten und schließlich zwei Stellen für die Sekunden eintippen und senden, jeweils ohne Leerzeichen oder andere Trennungszeichen. Beträgt Ihre aktuelle Uhrzeit beispielsweise 09:32:14, tippen Sie einfach „093214" in das Senden-Textfeld des Terminals ein. Sobald Sie Enter drücken, zeigt die Uhr die neue Zeit an und läuft von dort aus automatisch weiter. Doch nun zum Quelltext: 001 #include "Display.h" 002 #include "SPI.h" 003 #include "Time.h" 004 #include "watch.h" watch.h - 128x64 Bitmap analogWatch usw. usw.
http://tiny.systems/article/mosaic.html
12864 Graphics LCD libraryhttp://playground.arduino.cc/Code/LCD12864Dieser erste Teil enthält in diesem Programm ein paar kleine Erweiterungen. Zum einen ist die watch.h-Datei mit eingebunden. Diese enthält das Muster [die Bildinformation], das rund um die Uhr angezeigt wird, sowie die vier Striche auf dem Ziffernblatt, die zur Orientierung dienen, und den Punkt in der Mitte, der die Achsen der Zeiger andeutet. Die Zeiger selbst müssen zur Laufzeit berechnet werden und sind deswegen nicht Teil des Hintergrundbilds. Seite 70 Eine Library, die ebenfalls zum ersten Mal in diesem Sketch verwendet wird, ist die Time-Library von Michael Margolis. Diese Library kann man sich von der Arduino-Playground-Seite herunterladen, wir haben sie aber praktischerweise auch unter www.buch.cd für Sie bereitgestellt, um Versionskonflikten und anderen Fehlern aus dem Weg zu gehen. Diese Library erleichtert den allgemeinen Zugang zu zeitrelevanten Funktionen und wird hier nicht im Detail erklärt. Wichtig ist an dieser Stelle nur, dass Sie die setTime-Funktion nutzen, um die Uhrzeit einzustellen, und mit den Funktionen hour( ), minute( ) und second( ) jederzeit die aktuelle Stunde, Minute oder Sekunde abfragen können. 001 char inputBuffer[15] = { } : 002 Display lcd = Display(); Ein weiterer wichtiger Punkt in diesem Programm ist die Initialisierung des inputBuffers. Sie ist hier nötig, um die relevanten Informationen aus der seriellen Kommunikation auslesen zu können. 001 void setup() ) { 002 Serial.begin(9600); 003 Serial.println("Set Time (Format: HHMMSS)"); 004 lcd.init(0); 005 } In der setup-Routine wird die serielle Schnittstelle mit einer Baudrate von 9600 initialisiert. Außerdem wird der Text gesendet, den Sie beim öffnen des Serial Monitors zu sehen bekommen. Da beim Öffnen des Arduino-Terminals der Controller automatisch resetet wird, sehen Sie diesen Text immer. Den letzten Befehl aus diesem Abschnitt kennen Sie bereits. 001 void loop() { 002 int i = 0; 003 004 if (Serial.available() > 0) { 005 memset(inputBuffer, 0, sizeof(inputBuffer)); 006 delay(50); 007 while (Serial.available() > 0 && i < sizeof(inputBuffer) - 1) { 008 inputBuffer[i] = Serial.read(); 009 i++; 010 }
011 012 char hours[3] - { inputBuffer[0], inputBuffer[1], 0}; 013 char minutes[3] - { inputBuffer[2], inputBuffer[3], 0}; 014 char seconds[3] - { inputBuffer[4], inputBuffer[5], 0); 015 016 setTime(atoi(hours), atoi(minutes), atoi(seconds), 0, 0, 0); 017 } Seite 71 In der loop-Routine geht es zunächst um das Stellen der Uhr. Dazu wird abgefragt, ob serielle Daten vorhanden sind. Nur falls etwas empfangen wurde, wird der folgende Teil ausgeführt, der die Daten in den Buffer legt. Mit dem Befehl memset wird der gesamte Speicherbereich von inputBuffer auf 0 gesetzt, also gelöscht. Wenn alle seriellen Daten empfangen sind [das wird mit dem delay sichergestellt) und auf den inputBuffer aufgeteilt wurden, werden an die 3 char-Variablen hour, minute und second die gefilterten Werte übergeben und mit 0 für String-Ende terminiert. Der Code ist speziell auf den Serial Monitor der Arduino-Oberfläche zugeschnitten, die den gesamten Text erst nach einem Enter abschickt. Mit diesen Werten kann man nun die interne Uhr aus der Time Library mithilfe der setTime-Funktion einstellen. Die atoi -Funktion formatiert Zeichenketten in kompatible Integer-Werte. Die letzten 3 Nullen, die als Parameter angegeben werden, stehen für Tag, Monat und Jahr. Da diese Werte für die analoge Uhr keine Rolle spielen, werden einfach Nullen übergeben. Allerdings werden diese Parameter in einem späteren Beispiel noch relevant werden. 001 cd.clearVideoBuffer(); 002 lcd.drawBitmap(0, 0, 128, 64, watch); 003 004 lcd.drawLine(63, 32, 63+(int)(17*cos((hour()%12*30+mi nute()/2.0-90) * M_PI / 180.0)), 005 32+(int)(17*sin((hour( )%12*30+minute0/2.0-90) * M_PI / 180.0))); 006 lcd.drawLine(63, 32, 63+(int) (25*cos(((minute()*6)-90) * M_PI / 180.0)), 007 32+(int) (25*sin(((minute()*6)-90) * M_PI / 180.0))); 008 lcd.drawLine(63, 32, 63+(int) (27*cos(((second()*6)-90) * M_PI / 180.0)), 009 32+(int) (27*sin(((second()*6)-90) * M_PI / 180.0))); 010 lcd.show(); 011 } Seite 72 Während es in dem ersten Teil der loop-Schleife nur um das Stellen der Uhr geht, ist dieser Teil für das Anzeigen der Uhrzeit relevant. Die Zeit wird im Hintergrund von der Library ständig berechnet und kann mit den entsprechenden Funktionen permanent abgefragt werden. Zunächst einmal wird jedoch der Video-Buffer komplett gelöscht und das Hintergrundbild neu in den Buffer übertragen. Nun können die Zeiger eingezeichnet werden. Dazu benutzen Sie wieder die drawLine-Funktion. Die Startwerte sind jedes Mal dieselben, denn die Zeiger gehen logischerweise von der Uhrenmitte aus. Die Endwerte sind allerdings etwas komplizierter zu berechnen. Nehmen Sie als Beispiel den Sekundenzeiger. Der X-Wert dieses Zeigers wird berechnet aus: 63 + (int) (27 • cos(((second() • 6) - 90) • M_PI / 180.0)) Für den Y-Wert gilt ein ähnlicher Ausdruck, nämlich: 32 + (int) (27 • sin (((second() • 6) - 90) • M_PI / 180.0)) Das sieht ziemlich kompliziert aus. Etwas einfacher wird es, wenn Sie eine vereinfachte Form betrachten und sich die Funktionen mit dem Einheitskreis verdeutlichen: 63 + 27 • cos(a) Für den Y-Wert gilt dann: 63 + 27 • sin(a) Seite 73 Abb. 4.11: Sinus und Cosinus im Einheitskreis a ist jeweils der Winkel in Grad. Also wird bei einem Winkel von a = 0° der Cosinusterm zu 1 und der Sinusterm zu 0. Der X-Wert würde also den Wert 63 + 27 = 90 annehmen und der Y-Wert ergäbe 63 + 0 = 63. Das würde bedeuten, dass der Zeiger nach rechts auf die drei-Uhr-Stellung zeigen würde, und zwar mit der Länge von 27 Pixeln von der Uhrmitte aus gesehen, die bei 63,32 liegt. Bei einem Winkel von a = 90° wird der Cos-Wert 0 und damit der X-Wert 63, während der Sinusausdruck 1 ergibt, wodurch der Y-Wert 90 wird. Der Zeiger zeigt auf die 6-Uhr-Stellung. Bei einem Wert von 180° wird der Cosinusterm -1 und der Sinuswert wieder 0. Dadurch ergibt sich für den X-Wert 63 - 27, während der Y-Wert erneut 63 wird. Der Zeiger zeigt also nach links auf die 9-Uhr-Stellung. Kurz gesagt nehmen Cosinus- und Sinusterme also Werte von -1 bis 1 an. Diese Werte werden mit der Länge des Zeigers, also der 27, multipliziert und zu der Uhrenmitte [also der 63,32) addiert. Die tatsächliche Länge des Zeigers bleibt immer 27 Pixel, ganz nach den Winkelsätzen im rechtwinkligen Dreieck. Seite 74 Nun muss also nur dafür gesorgt werden, dass der Winkel a der Funktionen immer passend zur aktuellen Sekunde ist. Dazu gibt es den Term ((second() • 6) - 90) Für die 30. Sekunde ergibt sich in diesem Rechenbeispiel a= 30 x 6 - 90 = 180 - 90 also a = 90°. Das stimmt mit dem oben bestimmten Winkel überein. Bei der 45. Sekunde beträgt der Winkel a =180°, passt also auch. Kurz gesagt, werden hier alle Werte von a = 0 bis 360° durch die Multiplikation der Sekunde x6 abgebildet, denn für den Maximalwert gilt: 60 Sekunden x 6 ergibt a = 360. Die -90 dienen dazu, den Wertebereich so zu verschieben, dass 0°, also auch 12 Uhr, oben liegt. Abb. 4.12: Das Uhrenblatt wird gedreht Aber wozu wird der Winkel noch mit M_PI / 180,0 multipliziert? Das liegt ganz einfach daran, dass die Winkelfunktion von Arduino als Parameter Radiantwerte verlangt. Mit der Multiplikation mit Pi / 180 wird der Winkel somit transformatiert. Der Minutenzeiger funktioniert auf ganz ähnliche Weise. Die einzigen Unterschiede liegen darin, dass der Zeiger selbst etwas kürzer ist und dass er natürlich von dem Minutenwert abhängt. Seite 75 Bei dem Stundenzeiger wird die Berechnung wieder etwas schwieriger. Betrachten Sie nur den Teil, der in diesem Fall anders ist: (hour()%12 • 30 + minute() / 2,0 - 90) Bei dem 1. Teil, also hour()%12 , handelt es sich wieder um eine Modulo-Division. In diesem Fall wird alsb der Rest ermittelt, der bei einer Division der aktuellen Stunde durch 12 entsteht. Angenommen es sei 6 Uhr, dann ist der Modulo ebenfalls 6. Wenn es 18 Uhr wäre, bliebe bei der Division ebenfalls ein Rest von 6 übrig. Sie ahnen also vielleicht, wozu das gut ist. Die analoge Uhr unterscheidet nämlich nicht zwischen 6 und 18 Uhr, die Time-Library allerdings schon. Deshalb kann man mit diesem einfachen Trick eine 24-Stunden-Uhr auf einer 12-Stunden-Skala abbilden. Die 12 Stunden werden mit 30 multipliziert und ergeben somit einen Wertebereich von 0-330, jeweils in 30er-Schritten. Wenn das wieder mit den Sinus- und Cosinus-Funktionen auf das Ziffernblatt projiziert wird, kann man die Stunden gut erkennen. Doch es gibt noch einen Term, der zu den Stunden addiert wird, nämlich minute() / 2,0. Wenn Sie eine gewöhnliche analoge Uhr mit mechanischem Uhrwerk betrachten, werden Sie feststellen, dass der Stundenzeiger nicht beim Wechsel der Stunde plötzlich um einen Wert weiter springt, sondern sich permanent langsam mit dem Minutenzeiger mit dreht. Damit Ihre analoge Uhr das auch kann, müssen Sie also die Stellung des Minutenzeigers ebenfalls mit einberechnen. Dafür ist der zweite Term zuständig, der nämlich Werte von 0 bis 30 annimmt, einfach dadurch, dass die Minuten durch 2 geteilt werden. Das Maximum von fast 30° wird also dann erreicht, wenn der Minutenwert bei fast 60 liegt, also beispielsweise bei 12.59. In diesem Fall ist der Stundenzeiger also schon fast 30° weiter, also 1/12 der 360°. Damit zeigt der Stundenzeiger schon fast auf die nächste Stunde, genauso wie bei einer analogen Uhr. Abschließend wird das Gesamtergebnis mit der show-Funktion auf dem Display ausgegeben. Alles in allem ergibt das eine einfache, aber schöne analoge Uhr, die man sich auch gut in den Schrank stellen kann. Nur um die Stromversorgung müssen Sie sich Gedanken machen. 4.9 Geometrie - auf zu neuen Formen Bei diesem Versuch geht es darum, verschiedene geometrische Figuren auf das Display zu zeichnen. Seite 76 Als kleine Übung können Sie versuchen, die ersten Elemente selbst auf das Display zu bringen. Dazu lernen Sie auch eine neue Funktion kennen, die es Ihnen ermöglicht, Ellipsen und Kreise zu zeichnen. ORDNER geometrie | geometrie.ino | Display.cpp | Display.h | Front.h | #include "Display.h" #include "SPI.h" Display lcd = Display(); void setup() { lcd.init(20); //Kreuz lcd.drawLine(10, 10, 30, 30); lcd.drawLine(10, 30, 30, 10); //Kreis lcd.drawEllipse(40, 20, 10, 10); //Dreieck lcd.drawLine(50, 30, 70, 30); lcd.drawLine(60, 10, 70, 30); lcd.drawLine(50, 30, 60, 10); //Viereck lcd.drawLine(70, 10, 90, 10); lcd.drawLine(70, 30, 90, 30); lcd.drawLine(70, 10, 70, 30); lcd.drawLine(90, 10, 90, 30); lcd.show(); for (byte x=0; x<Display::WIDTH; x+=5) { lcd.drawLine(0, Display::HEIGHT-x, Display::WIDTH-2*x, Display::HEIGHT); lcd.show(); delay(300); } for (byte x=0; x<16; x+=2) { lcd.drawEllipse(100-x, 30+x, 20+x, 10); lcd.show(); delay(300); } } void loop() {} Abb. 4.13: Verschiedene geometrische Figuren Das Beispiel zeichnet nach dem Upload 4 geometrische Figuren in die oberen Bereiche des Displays: ein X, einen Kreis, ein Dreieck und ein Viereck. Sie können versuchen, das entsprechende Programm selbst zu schreiben. Die meisten Figuren bestehen aus einfachen Linien, deren Start- und Endpunkte Sie berechnen müssen. Nur beim Kreis kommt eine neue Funktion zum Einsatz, die Funktion drawEllipse. Ein Kreis ist nämlich eine Ellipse, nur in besonders symmetrischer Form. Sie können das mit dem Unterschied zwischen Rechteck und Quadrat vergleichen. Ein Quadrat ist zwar ein Rechteck, hat aber die zusätzliche Eigenschaft, dass alle vier Seiten gleich lang sind. Bei einem Kreis ist das ähnlich. Er ist so definiert, dass alle Punkte vom Mittelpunkt gleich weit entfernt sind. Eine Ellipse kann aber auch ungleichmäßiger sein, als hätte sich jemand daraufgesetzt. Die Funktion drawEllipse selbst benötigt vier Parameter. Die ersten beiden Parameter geben den Mittelpunkt an. Die beiden letzten Parameter bestimmen den Radius. Wenn also beide Stellen des Radius gleich, z. B. 10,10 sind, ergibt das einen Kreis, der von dem im ersten Parameter angegebenen Mittelpunkt aus einen Radius von 10 hat. Ist der Radius unterschiedlich, z. B. 10,12, ergibt sich eine Ellipse, die auf der Y-Achse etwas gestreckt ist. Seite 77 001 #include "Display. h" 002.#include "SPI.h" 003 004 Display lcd = Display( ) ; 005 006 void setup() 1 007 lcd.init(0); 008 009 // Kreuz 010 lcd.drawLine(10, 10, 30, 30); 011 lcd.drawLine(10, 30, 30, 10); 012 013 // Kreis 014 lcd.drawEllipse(40, 20, 10, 10); 015 016 // Dreieck 017 lcd.drawLine(50, 30, 70, 30); 018 lcd.drawLine(60, 10, 70, 30); 019 lcd.drawLine(50, 30, 60, 10); 020 021 // Viereck 022 lcd.drawLine(70, 10, 90, 10); 023 lcd.drawLine(70, 30, 90, 30); 024 lcd.drawLine(70, 10, 70, 30); 025 lcd.drawLine(90, 10, 90, 30); 026 017 lcd.show(); 028 [ ... ] Falls Sie sich nicht selbst an der Gestaltung der Elemente versuchen möchten, können Sie auch direkt das Beispiel ausprobieren. Es enthält nämlich noch weitere Darstellungen geometrischer Figuren, die in zwei verschiedenen for-Schleifen generiert werden. 001 for (byte x=0; x<Display::WIDTH; x+=5) { 002 lcd.drawLine(0, Display::HEIGHT-x, Displa y::WIDTH-2*x, Display::HEIGHT); 003 lcd.show(); 004 delay(300); 005 } Die 1. for-Schleife generiert eine Reihe von Linien, angefangen am linken oberen Bildschirmrand hin zur unteren rechten Ecke. Durch den Versatz der Linien ergibt sich das Bild einer gekrümmten Ebene. Ein kleines Delay nach jedem show-Befehl sorgt dafür, dass man den Aufbau gut beobachten kann. Seite 78 001 for (byte x=0; x<16; x+-2) { 002 lcd.drawEllipse(100-x, 30+x, 20+x, 10); 003 lcd.show(); 004 delay(300); 005 } Die 2. for-Schleife zeichnet eine Reihe von kleiner werdenden Ellipsen an den rechten Rand. Diesmal handelt es sich nicht um Kreise, wie man gut erkennen kann. Außerdem wird deutlich, wie genau die Ellipsen aufgespannt werden, wenn der Y-Wert variiert wird. 4.10 VU-Meter - der klassische Austeuerungsmesser Ein VU-Meter ist ein Messgerät, das vor allem in der Tontechnik zum Einsatz kommt. Es handelt sich dabei um ein Spannungsmessgerät, meist mit Zeiger, das zur Beurteilung der Aussteuerung eingesetzt wird. Moderne Varianten des VU-Meters sind beispielsweise LED- oder Balkenanzeigen, die neben dem aktuellen Pegel auch den Maximalpegel für eine Zeitperiode anzeigen. Das hier vorgestellte VU-Meter simuliert ein klassisches analoges VU-Meter auf Basis eines Drehspulenmesswerks. Am anlogen Eingang pin-A0 kann eine Spannung bis maximal 5,0 V angelegt werden, die dann durch einen Zeiger auf der Skala angezeigt wird. Mit passender Verstärkerschaltung können Sie so z.B. ein analoges Soundsignal anlegen und die aktuellen Pegel auf dem Display übersichtlich ablesen. Das Messgerät eignet sich auch bestens als Voltmeter zur Überprüfung Ihrer Akkus oder Batterien. ORDNER vuMeter | vuMeter.ino | Display.cpp | Display.h | Font.h | #include "Display.h" #include "SPI.h" Display lcd = Display(); void setup() { lcd.init(20); } void loop() { lcd.clearVideoBuffer(); drawBackground(); byte value = (analogRead(A0) >> 3); lcd.drawLine(64, 90, value, 17); lcd.show(); delay(100); } void drawBackground() { lcd.drawEllipse(64, 50, 100, 50); lcd.drawEllipse(64, 70, 100, 50); lcd.drawLine(0, 32, 64, 100); lcd.drawLine(127, 32, 64, 100); lcd.drawString(58, 40, "VU"); //Skala lcd.drawLine(7, 25, 11, 28); lcd.drawString(2, 17, "0"); lcd.drawLine(28, 19, 29, 23); lcd.drawString(25, 11, "1"); lcd.drawLine(51, 16, 52, 20); lcd.drawString(49, 8, "2"); lcd.drawLine(76, 16, 75, 20); lcd.drawString(74, 8, "3"); lcd.drawLine(99, 19, 98, 23); lcd.drawString(98, 11, "4"); lcd.drawLine(120, 25, 118, 27); lcd.drawString(121, 17, "5"); } Abb. 4.14: Das VU-Meter Das Programm enthält viele der zuvor beschriebenen Funktionen zum Grafikdisplay. Mithilfe von Funktionen wie drawElipse und drawLine wird hier zunächst einmal eine eindeutige Skala auf das Display gezeichnet. Seite 79 Die Funktion drawString sorgt für die richtige Beschriftung an geeigneten Stellen. Der Zeiger selbst wird ebenfalls durch die drawLine-Funktion dargestellt. Der End-X-Wert des Zeigers ist abhängig vom gemessenen Wert. Hier werden die Funktionen nacheinander vorgestellt. 001 #include “Display.h 002 include "SPI.h" 003 004 Display lcd = Display(); 005 006 void setup() { 007 lcd.init(0); 008 } Am ersten Teil des Quelltexts ändert sich auch dieses Mal nichts. In der setup-Routine wird das Display wie gewohnt initialisiert. Der Hauptteil des Programms befindet sich in der loop-Funktion. 001 void loop() { 002 lcd.clearVideoBuffer( ); 003 drawBackground(); 004 byte value = (analogRead(A0) >> 3); 005 lcd.drawLine(64, 90, value, 17); 006 lcd.show(); 007 delay(100); 008 } Hier wird zunächst einmal der Video-Buffer gelöscht. Als Nächstes wird eine Funktion aufgerufen, die nicht Teil der Library, sondern Teil dieses Hauptprogramms ist und weiter unten gezeigt wird. Wie der Name der Funktion schon erahnen lässt, wird hier der Hintergrund, also hauptsächlich die Skala, gezeichnet. Was folgt, ist das Einlesen des aktuellen Pegelwerts am Analog pin-A0. Der Wert wird binär um drei Stellen verschoben und dadurch die Auflösung der Messung von 10-bit auf 7-bit reduziert. Im Ergebnis erhalten Sie Messwerte von 0 bis 127. Der errechnete value-Wert bestimmt nun den End-X-Wert des Zeigers, der in der nächsten Zeile mit dem drawLine-Befehl in den Video-Buffer übertragen wird. Der End-Y-Wert ändert sich nicht, wodurch sich die Länge des Zeigers genau genommen mit Veränderung des Pegels ebenfalls ändert. Wer möchte, kann an dieser Stelle mit Winkelfunktionen auch den Y-Wert anpassen. Seite 80 Was Sie vielleicht stutzig machen könnte, ist der Start-Y-Wert des Zeigers. Doch dazu gleich mehr. In der loop-Funktion wird der Buffer-Inhalt mit dem show-Befehl auf das Display übertragen und ein kleiner Wartebefehl ausgeführt. Der Wartebefehl simuliert die Trägheit einer solchen Anzeige und sorgt dafür, dass der Zeiger sich gleichmäßiger bewegt. Interessant ist nun noch die drawBackground-Funktion. 001 void drawBackground() { 002 lcd.drawEllipse(64, 50, 100, 50); 003 lcd.drawEllipse(64, 70, 100, 50); 004 lcd.drawLine(0, 32, 64, 100); 005 lcd.drawLine(127, 32, 64, 100); 006 lcd.drawString(58, 40, "VU"); 007 // Skala 008 lcd.drawLine(7, 25, 11, 28); 009 lcd.drawString(2, 17, "0"); 010 lcd.drawLine(28, 19, 29, 23): 011 lcd.drawString(25, 11, "1"); 012 lcd.drawLine(51, 16, 52. 20); 013 lcd.drawString(49, 8, "2"); 014 lcd.drawLine(76, 16, 75, 20); 015 lcd.drawString(74, 8, "3"); 016 lcd.drawLine(99, 19, 98. 23): 017 lcd.drawString(98, 11. "4"); 018 lcd.drawLine(120, 25, 118, 27); 019 lcd.drawString(121, 17, "5"); 020 } Diese Funktion enthält eine Reihe einzelner Elemente, die insgesamt die Skala nebst Beschriftungen ergeben. Doch betrachtet man die ersten beiden Ellipsezeichnungen, sieht man, dass die Zeichnungen eindeutig über die Dimensionen des Displays hinausragen. Vielleicht ist Ihnen das bereits beim Zeichnen des Zeigers aufgefallen, denn der maximale Y-Wert des Displays liegt bei 63, während der Zeiger bei Position 90 beginnt und auch die beiden Kreise sowie die danach gezeichneten beiden Linien überragen das Display bei Weitem. Das ist legitim, denn beim Zeichnen der einzelnen Elemente werden immer die Grenzen des Video-Buffers überprüft. Dadurch kann im Display-RAM auch kein Überlauf passieren. Das ist in diesem Fall besonders praktisch, denn es ermöglicht Ihnen, mit bekannten Elementen, wie dem Zeichnen eines Kreises, auch nicht definierte Elemente wie das Zeichnen eines Halbkreises zu realisieren. Seite 81 Der Zeiger wird in diesem Fall auch plastischer, indem der Drehpunkt an eine virtuelle Stelle unterhalb des Displays verlagert ist. Dadurch wirkt die gesamte Darstellung gleich um einiges realistischer. Tipp Achtung, dieser Spezialfall funktioniert nur in die positiven Richtungen von x und y (also in der Ausrichtung nur nach unten und nach rechts). Zusätzlich sollten Sie bei der Übergabe von Positionsvariablen auf den Überlauf bei 255 achten. Erzeugte Überläufe in Ihrem eigenen Programmcode kann die Library nicht erkennen. 4.11 Calendar - analoge Uhr mit erweiterten Funktionen Dieses Projekt erinnert auf den ersten Blick an die analoge Uhr, enthält aber einige Neuerungen. Diesmal werden nämlich neben der aktuellen Uhrzeit auch der Wochentag und das Datum angezeigt - also noch zwei Gründe mehr, sich dieses Projekt in den Schrank zu stellen. ORDNER calendar | calendar.ino | Display.cpp | Display.h | Font.h | Time.cpp | Time.h | background.h | #include "Display.h" #include "SPI.h" #include "Time.h" #include "background.h" char inputBuffer[20]; Display lcd = Display(); char* daysS[]={"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"}; char* monthS[]={"Januar", "Februar", "Maerz", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"}; void setup() { Serial.begin(9600); Serial.println("Set Date (Format: HHMMSSDDMMYYYY)"); lcd.init(20); } void loop() { int i = 0; if (Serial.available() > 0) { memset(inputBuffer, 0, sizeof(inputBuffer)); delay(50); while (Serial.available() > 0 && i < sizeof(inputBuffer) - 1) { inputBuffer[i] = Serial.read(); i++; } inputBuffer[i] = 0; char hours[3] = {inputBuffer[0], inputBuffer[1], 0}; char minutes[3] = {inputBuffer[2], inputBuffer[3], 0}; char seconds[3] = {inputBuffer[4], inputBuffer[5], 0}; char day[3] = {inputBuffer[6], inputBuffer[7], 0}; char month[3] = {inputBuffer[8], inputBuffer[9], 0}; char year[5] = {inputBuffer[10], inputBuffer[11], inputBuffer[12], inputBuffer[13], 0}; setTime(atoi(hours), atoi(minutes), atoi(seconds), atoi(day), atoi(month), atoi(year)); } lcd.clearVideoBuffer(); lcd.drawBitmap(0, 0, 128, 64, watch); lcd.drawLine(31, 32, 31+(int)(17*cos((hour()%12*30+minute()/2.0-90) * M_PI / 180.0)), 32+(int)(17*sin((hour()%12*30+minute()/2.0-90) * M_PI / 180.0))); lcd.drawLine(31, 32, 31+(int)(25*cos(((minute()*6)-90) * M_PI / 180.0)), 32+(int)(25*sin(((minute()*6)-90) * M_PI / 180.0))); lcd.drawLine(31, 32, 31+(int)(27*cos(((second()*6)-90) * M_PI / 180.0)), 32+(int)(27*sin(((second()*6)-90) * M_PI / 180.0))); char buffer[50]; sprintf(buffer, "%d.", day()); lcd.drawString(65, 12, daysS[weekday()-1]); lcd.drawString(65, 22, buffer); lcd.drawString(65, 32, monthS[month()-1]); sprintf(buffer, "%d", year()); lcd.drawString(65, 42, buffer); lcd.show(); } Abb. 4.15: Die analoge Uhr mit Kalenderfunktion Was den Quelltext angeht, kann man sich viel aus dem analogWatch-Projekt abgucken. Wer versuchen möchte, das Projekt weitgehend selbst zur erarbeiten, kann die analoge Uhr als Vorlage nehmen und anpassen. Sie sollten allerdings beachten, dass der Hintergrund sich gegenüber der anlogen Uhr geändert hat. Deswegen sollten Sie die background.h -Datei aus dem Beispielordner verwenden und den include-Befehl anpassen. In dem Programm selbst gibt es natürlich auch einige Veränderungen. Zunächst wird die Größe des inputBuffers auf 20 Zeilen erweitert. Danach werden die Strings zu den Wochentagen und den Monaten wie unten abgebildet in zwei char-Arrays definiert. Seite 82 Falls Sie den Kalender in eine andere Sprache übersetzen möchten, können Sie an dieser Stelle andere Strings einfügen. Nur die Reihenfolge sollte nicht verändert werden. 001 char* daysS[]=1"Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"); 002 char* monthS[]=I"Januar", "Februar", "März", "April", "Mai", "Juni", 003 "Juli", "August", "September", "Oktober", "November", "Dezember"}; Als Nächstes passen Sie den gesendeten Text in der setup-Routine an. Diesmal muss der Benutzer nämlich nicht nur Stunde, Minute und Sekunde, sondern auch Tag, Monat und Jahr angeben. Die Ausgabe beim Öffnen des seriellen Monitors lautet also: 001 Serial.println("Set Date (Format: HHMMSSDDMMYYYY)"); In der loop-Routine ändert sich insgesamt nicht so viel, wie man meinen könnte. Die erste If-Abfrage zu serial.Avaiable bleibt komplett identisch. Das Auslesen des inputBuffers muss aber natürlich um die neuen Informationen erweitert werden. Deswegen müssen Sie nach char second [3] [...] noch folgende Zeilen einfügen: 001 char day[3] — (inputBuffer[6], inputBuffer[7], 01; 002 char month[3] — (inputBuffer[8], inputBuffer[9], 01; 003 char year[5] = linputBuffer[10], inputBuffer[11], inputBuffer[12], inputBuffer[13], 01; In dem Aufruf der setTime-Funktion werden die Parameter, bei denen Ivorher nur eine Null übergeben wurde, durch die neuen Werte ersetzt: 001 setTime(atoi (hours), atoi (minutes), atoi (seconds), atoi (day), atoi (month), atoi (year)); Damit wäre das Stellen der Uhr inklusive Datum bereits realisiert. Nun müssen Sie sich noch um die Anzeige kümmern. Das neue Design besteht aus zwei Fenstern in einen großen Rahmen. Das linke Fenster zeigt die Uhrzeit an, während im rechten Fenster der Wochentag und das Datum angezeigt werden. Das bedeutet also, dass Sie Ihre frühere Uhr nach links verschieben. Das ist aber ganz einfach, denn Sie müssen lediglich die X-Werte der drei drawLine-Befehle anpassen. Seite 83 Für die Start-X-Werte bedeutet das, dass aus der 63 jeweils 31 wird. Für den End-X-Wert gilt das Gleiche, also wird aus 63 + [int] [...] jeweils 31+ [int)[...]. Schon ist die komplette Uhr nach links in das Fenster verschoben. Um sicher zu gehen, können Sie diese Veränderungen erst einmal testen, indem Sie das Programm uploaden. Nun geht es an die Realisierung der Datumsanzeige. Dazu sollten Sie zunächst einmal einen neuen Buffer definieren. Dieser ist nur für die Integer-Werte zuständig, die Sie ausgeben wollen. Die Strings für Wochentag und Monat haben Sie schließlich bereits am Anfang definiert. 001 char buffer[50]; 002 lcd.drawString(65, 12, daysS[weekday()-1]); 003 sprintf(buffer, "%d.", day()); 004 lcd.drawString(65, 22, buffer); 005 lcd.drawString(65, 32, monthS[month()-1]); 006 sprintf(buffer, "%d", year()); 007 lcd.drawString(65, 42, buffer); Der Wochentag wird auch mit dem ersten drawString ausgegeben. Die Abfrage des Wochentags ist in der Time-Library durch die weekday( )-Funktion möglich. Diese Funktion gibt einen Wert von 1-7 aus, wobei Sonntag die 1 ist. In Ihrem days-char-Array hat der Sonntag die Position 0, da ein Array immer mit 0 beginnt. Also ziehen Sie von dem weekdays-Rückgabewert einfach eins ab und rufen die entsprechende Stelle aus dem Array auf, um sie mit der drawString-Methode auszugeben. Als Nächstes soll der Tag im Monat gefolgt von einem Punkt in der nächsten Zeile ausgegeben werden. Dazu wird aber der Buffer benötigt, denn es soll eine Integer-Zahl ausgegeben werden. Der bekannte sprintf-Befehl mit entsprechendem Platzhalter hilft an dieser Stelle. Mit day( ) kann der Tag einfach abgefragt werden. Mit drawString wird der Buffer schließlich wieder übertragen. Es folgt die Ausgabe des Monats nach dem gleichen Prinzip wie bei Weekdays und die Ausgabe des Jahrs nach dem Prinzip des Tages. Schon haben Sie alle Werte zusammen und können das Gesamtergebnis mit lcd.show ( ) auf das Display übertragen. Das upgeloadete Programm kann sich sehen lassen, allerdings stimmen Uhrzeit und Datum natürlich noch nicht. Also öffnen Sie wieder das Terminal der Arduino-Oberfläche und übertragen die aktuellen Werte im bekannten Format. Für 09:32:14 am 03.04.2015 wäre das also 09321403042015. Schon stimmen die Daten überein. Seite 84 4.12 HardwarePlayground - die Hardware voll im Griff In diesem Kapitel wird, ähnlich wie in den Kapiteln „Blick in die Library" ein besonderes Thema behandelt, nämlich die Hardware selbst. Sie haben das Display nun schon in vielen Versuchen ausprobiert und angesteuert, aber wie das alles genau funktioniert und was das Display noch alles kann, erfahren Sie hier. So können Sie mit dem Display auf einer ganz anderen Ebene experimentieren. Sehen Sie sich das Display einmal genauer an. Mit einem Schaltplan geht das am besten. ORDNER HardwarePlayground | HardwarePlayground.ino | Display.cpp | Display.h | Font.h | #include "Display.h" #include "SPI.h" #define LCD_BUTTON_LEFT A4 #define LCD_BUTTON_RIGHT A5 #define LCD_BACKLIGHT 12 Display lcd = Display(); void setup() { pinMode(LCD_BUTTON_LEFT, INPUT_PULLUP); pinMode(LCD_BUTTON_RIGHT, INPUT_PULLUP); pinMode(LCD_BACKLIGHT, OUTPUT); digitalWrite(LCD_BACKLIGHT, HIGH); lcd.initSoftSPI(20); lcd.clearVideoBuffer(); lcd.drawString(5, 10, "Hardware Playground"); lcd.drawLine(0, 20, 127, 20); lcd.show(); } void loop() { waitForInput(); digitalWrite(LCD_BACKLIGHT, LOW); waitForInput(); digitalWrite(LCD_BACKLIGHT, HIGH); lcd.writeCommand(0xA5); //alle Pixel an waitForInput(); lcd.writeCommand(0xA4); //alle Pixel im Normalmodus waitForInput(); lcd.writeCommand(0xA7); //Anzeige invertiert waitForInput(); lcd.writeCommand(0xA6); //Anzeige nichtinvertiert waitForInput(); lcd.writeCommand(0xC0); //Anzeige horizontal gespiegelt waitForInput(); lcd.writeCommand(0xC8); //Anzeige nicht horizontal gespiegelt waitForInput(); lcd.writeCommand(0xA1); //Anzeige vertikal gespiegelt lcd.show(); //nur wirksam nach neuschreiben des Display RAM waitForInput(); lcd.writeCommand(0xA0); //Anzeige nicht vertikal gespiegelt lcd.show(); //nur wirksam nach neuschreiben des Display RAM } void waitForInput() { delay(300); while (digitalRead(LCD_BUTTON_LEFT) && digitalRead(LCD_BUTTON_RIGHT)) {} } Abb. 4.15.1: Textausgabe Abb. 4.16: Der Schaltplan des Display-Shields mit LDC-Display DXDCG12864-4330 Neben dem Display und den vielen Anschlüssen befindet sich noch ein IC auf dem Shield. Dabei handelt es sich um einen HCF4050, eine schnelle Variante des nicht invertierenden Pegelwandlers 4050. Er schützt das Display vor zu hohen Spannungen und wandelt die 5-V-Signale des Ardu-ino in 3,3V-Signale um. Das Display selbst arbeitet ebenfalls mit 3,3V. Interessant ist ein Blick auf das Display selbst. Ziemlich viele Anschlüsse müssen da verdrahtet werden. Seite 85 Bei genauer Betrachtung fällt auf, dass die meisten der Anschlüsse auf „High", also auf 3,3 V liegen. Der Grund dafür ist die Betriebsart des LCD. Neben einem seriellen Betrieb ist es möglich, ST7565-basierte LCDs auch im parallelen Modus zu betreiben. Ein Vorteil dieser Methode ist, dass der interne Speicher des LCD nicht nur beschreibbar, sondern auch lesbar ist. Der Nachteil dieser Betriebsart liegt jedoch auf der Hand: 21 Leitungen müssten hierzu angesteuert werden. Damit Ihnen für Ihre eigenen Projekte nicht die Pins am Arduino ausgehen, wird das LCD im seriellen Modus betrieben. Dazu sind lediglich sechs Leitungen notwendig. Dem seriellen Betrieb ist es übrigens geschuldet, dass man in der Library einen Videospeicher anlegen muss, denn ein Auslesen des LCD-RAM ist damit nicht möglich. https://www.adafruit.com/product/250 Abb. 4.17: Die Druckvorlage des LCD-Shield-Platine 53x46mm (Leiterbahnseite unten) ST7565 basiertes LCD-Shield für ARDUINO UNO R3 Seite 86 Die Anschlüsse im Detail: Anschluss Funktion AO Steuerleitung Kommando/Daten RST Hardwarereset CS ChipSelect - LCD-Aktivierung SDA Datenleitung der SPI-Schnittstelle LED Hintergrundbeleuchtung SCL Clock-Leitung der SPI-Schnittstelle S1 Taster S1 S2 Taster S2 3V, GND Stromversorgung Bevor Sie das Beispiel HardwarePlayground in den Controller laden, müssen Sie einen minimalen Umbau am Shield vornehmen. Auf der unteren Seite befindet sich ein Jumper (JP1 auf 2-3), der auf den linken beiden Kontakten (von oben gesehen) eines dreifachen Steckers steckt. Steckt Richtung JP4 Dieser Jumper wird für das kommende Beispiel auf die rechten beiden Kontakte (von oben gesehen) umgesteckt. Dazu nehmen Sie kurz das Display vom Arduino ab und stecken den JP1 auf 1-2 Richtung JP3 der langen Stiftleiste o o o o o o o 1 2 3 o o [o o]o o o JP1 von unten o
JP3 JP4
Seite 87 Mit dieser Jumpereinstellung haben Sie die dauerhafte Displaybeleuchtung auf einen Ausgangsport am Arduino gelegt. Nun können Sie das Beispiel uploaden, aber natürlich nicht, ohne vorher den Kontrast anzupassen. Das machen Sie diesmal im Aufruf der lcd.initSoftSPI -Methode, denn eine Standard-init-Methode gibt es in diesem Programm nicht. Nach dem Upload sollten Sie den unterstrichenen Schriftzug HardwarePlayground auf dem Bildschirm sehen. Durch einen Druck auf einen der beiden Hardwarebuttons können Sie verschiedenste Veränderungen der Darstellung vornehmen. Was steckt hinter diesen Veränderungen? Ein Blick in den Quelltext verrät, dass hier, bis auf die init-Funktion, keine neuen Funktionen aufgerufen werden. Vielmehr werden im Hauptprogramm einfache Kommandos an das Display gesendet. 001 void loop( ) { 002 waitForInput ( ) ; 003 [...] 004 lcd.writeCommand(0xA5); // alle Pixel an 005 waitForInput(); 006 lcd.writeCommand(0xA4); // alle Pixel im Normalmodus 007 waitForInput(); 008 lcd.writeCommand(0xA7); // Anzeige invertiert 009 waitForInput(); 010 lcd.writeCommand(0xA6); // Anzeige nichtinvertiert 011 waitForInput(); 012 lcd.writeCommand(0xC0); //Anzeige horizontal gespiegelt 013 waitForinput(); 014 lcd.writeCommand(0xC8): // Anzeige nicht horizontal gespiegelt 015 waitForInput(); 016 lcd.writeCommand(0xA1); // Anzeige vertikal gespiegelt 017 lcd.show(); //nur wirksam nach neuschreiben des Display RAM 018 waitForInput(); 019 lcd.writeCommand(0xA0); // Anzeige nicht vertikal gespiegelt 020 lcd.show(); // nur wirksam nach neuschreiben des Display RAM 021 } Seite 88 Im ersten Kapitel „Blick in die Library" wurde die init-Routine analysiert und die einzelnen Kommandos wurden betrachtet. Vielleicht haben Sie hier bereits geahnt, dass man mit den Kommandos noch eine ganze Menge mehr machen kann. Weiter unten finden Sie einen Auszug der Steuerbefehle des Displaycontrollers. Sie sind der Übersicht halber als Binärzahlen angegeben. Das hat den Grund, dass ein einzelnes oder manchmal sogar mehrere Bits über die Funktion des Kommandos entscheiden, also ähnlich wie bei Parametern in Funktionen. Sieht man sich z.B. die erste Funktion Display ON/OFF an, erkennt man, dass das letzte Bit entscheidet, ob das LCD aktiviert oder deaktiviert wird. Die folgende Tabelle zeigt einen Auszug der Steuerbefehle des Displaycontrollers ST7565r. kommuniziert wird per SPI über einen st7565r Displaycontroller. http://www.ladyada.net/learn/lcd/st7565.htmlGraphic ST7565 Positive LCD (128x64) with RGB backlight + extras - ST7565https://www.adafruit.com/product/250Command Command Code FunktionDisplay ON/OFF 0b1010111i LCD aktivieren (1 ein, 0 aus) Page address set Ob10110iii Zeilenadresse einstellen [0-7) Column address set upper bit Ob0001iiii Spaltenadresse einstellen ihöherwertige Bits] Column address set lower bit Ob0000iiii Spaltenadresse einstellen (niederwertige Bits) ADC select Ob1010000i Anzeige horizontal umdrehen (1 ein, 0 aus] Display normal/reverse Ob1010011i Anzeige invertieren (1 ein, 0 aus) Display all points ON/OFF Ob1010010i Alle LCD-Pixel setzen (1 ein, 0 aus] LCD bias set Ob1010001i LCD-Treiberspannung setzen (0 1/9bias, 11/7bias) Reset 0b11100010 Interne Komponenten zurücksetzen Common output mode select Ob1100i000 Anzeige vertikal umdrehen (1 ein, 0 aus) Power control set Ob00101ijk LCD-Booster, Regulator, Follower (1 ein, 0 aus] VO voltage regulator Ob00100iii LCD Spannungsregler (0-7) Electronic volume mode set Ob10000001 Kontrastspannung ändern Electronic volume register set Ob00iiiiii Kontrastspannung setzen (0-63) Booster ratio mode set 0b11111000 Boosterrate ändern Booster ratio register set Ob000000ii Boosterrate setzen (00 2x,3x,4x; 01 5x; 116x) Quelle: Datasheet: ST7565r Tabelle 16 Seite 50 Seite 89 Wenn Sie einen der Befehle testen möchten, können Sie ihn entweder in Hex-Code umrechnen oder direkt den Binärcode in folgender Form übergeben: 001 lcd.writeCommand(0b10101110); // Display Ausschalten Das ist zwar länger, aber für Tests eventuell praktischer, da Sie immer schnell das wichtige Parameterbit ändern können, ohne alles in Hex-Code umzurechnen. In dem Beispielprogramm sind bereits einige interessante Einstellungen vorgestellt, die Sie nacheinander durchgehen können. Die interessanten Fragen sind nun, wieso Sie diesmal die Hintergrundbeleuchtung an- und ausschalten können, warum eine andere Initialisierungsmethode verwendet wurde und weshalb Sie den Jumper umstecken mussten. Die Antworten auf alle drei Fragen hängen unmittelbar zusammen. In allen vorangegangenen Beispielen haben Sie das Display über die Hardware-SPI angesteuert. Das bedeutet, dass der Mikrocontroller, also z. B. der Atmega328P im Arduino Uno, von sich aus eine SPI-Schnittstelle über festgelegte Pins bereitstellt. Sie können eine SPI-Schnittstelle aber auch über reine Software realisieren. Durch den Aufruf der initSoft-SPI -Methode entscheiden Sie sich dafür, die SPI-Kommutation über die Software aufzubauen. Die Variable isSoftSPI in der Library ist dafür ausschlaggebend. Zudem werden für die SPI-Kommunikation relevante Pins als Konstanten definiert und als Ausgänge geschaltet. 001 [Auszug aus der Library] 002 [...] 003 const int LCD_A0 = 8; 004 const int LCD_RST = 9; 005 const int LCD_CS = 10; 006 const int LCD_SDA = 11; 007 const int LCD_SCL = 13; 008 009 [...] 010 011 void Display::initSoftSPl(byte contrast) { 012 isSoftSPl = true; 013 pinMode(LCD_SDA, OUTPUT); 014 pinMode(LCD_SCL, OUTPUT); 015 pinMode(LCD_AO, OUTPUT): 016 pinMode(LCD_RST, OUTPUT); 017 pinMode(LCD_CS, OUTPUT); 018 [...] 019 } Seite 90 Beim Senden von Kommandos muss man den veränderten Kommunikationsweg ebenfalls beachten: 001 void Display::writeCommand(byte command) { 002 digitalWrite(LCD_AO, LOW); 003 if (isSoftSPl) { 004 shift0ut(LCD_SDA, LCD_SCL, MSBFIRST, command); 005 } else { 006 SPI.transfer(command); 007 } 008 digitalWrite(LCD_A0, HIGH); 009 } Wenn die Variable isSoftSPI true ist, wird das Kommando über den shiftOut-Befehl übermittelt. Wenn nicht, wird die Standard-SPI.transfer-Methode verwendet. Gleiches geschieht auch bei der writeData-Methode. Doch wozu die Umstellung von Hardware- auf Software-SPI? Durch die Verwendung der Software-SPI ist ein Pin frei geworden, nämlich der Digi-tal-Pin12. Dieser lässt sich über den Jumper, den Sie umgesteckt haben, auf den Steuer-Pin der Displayhintergrundbeleuchtung legen. Dadurch können Sie durch Schalten des Pins 12 die Hintergrundbeleuchtung aktivieren oder deaktivieren. Das geschieht auch beim ersten Durchgang, wenn Sie die Hardwarebuttons betätigen. 001 [...] 002 #define LCD_BACKLIGHT 12 003 [...] 004 005 void loop() { 006 waitForinput(); 007 digitalWrite(LCD_BACKLIGHT, LOW); 008 waitForInput(): 009 digitalWrite(LCD_BACKLIGHT, HIGH); 010 [...] 011 } Seite 91 Kurz gesagt, bekommen Sie nun mit der Library auch die Möglichkeit, die Hintergrundbeleuchtung zu schalten. Das kann nützlich sein, wenn Sie beispielsweise eine Uhr für den Nachttisch programmieren, die nur auf Knopfdruck leuchtet. Alles, was Sie dazu tun müssen, ist, den Jumper umzustecken und auf SoftwareSPl umzuschalten. An dem Großteil Ihres Quelltextes müssen Sie also nichts verändern. Vergessen Sie aber nicht, den Jumper für die nächsten Beispiele wieder auf die ursprüngliche Position umzustecken. 4.13 Navigation - ein Menü mit reichlich Auswahl In dem folgenden Beispiel wird ein Menü vorgestellt, über das Sie die digitalen pins-D0 bis D7 ein- und ausschalteten können. Mit dem linken Taster auf dem Displayshield navigieren Sie durch das Menü, während der rechte Taster den ausgewählten pin umschaltet. Außerdem lernen Sie einen neuen Befehl kennen, den invertRectangle-Befehl, der eine übersichtliche Navigation durch das Menü ermöglicht. ORDNER navigation | navigation.ino | Display.cpp | Display.h | Font.h | #include "Display.h" #include "SPI.h" #define LCD_BUTTON_LEFT A4 #define LCD_BUTTON_RIGHT A5 Display lcd = Display(); byte position = 0; void setup() { DDRD = B11111111; pinMode(LCD_BUTTON_LEFT, INPUT_PULLUP); pinMode(LCD_BUTTON_RIGHT, INPUT_PULLUP); lcd.init(20); lcd.drawString(1, 1, "Navigation"); lcd.invertRectangle(0, 0, 60, 8); lcd.drawLine(60, 8, 127, 8); lcd.drawString(5, 55, "Auswahl"); lcd.invertRectangle(0, 54, 50, 63); lcd.drawString(85, 55, "Toggle"); lcd.invertRectangle(77, 54, 127, 63); for (byte x=0; x<8; x++) { char buffer[50]; sprintf(buffer, "PIN %d", x); if (x<4) { lcd.drawString(10, 16+8*x, buffer); } else { lcd.drawString(90, 8+8*(x-3), buffer); } } invertItem(position); lcd.show(); } void invertItem(byte position) { if (position<4) { lcd.invertRectangle(8, 7+8*(position+1), 40, 7+8*(position+2)); } else { lcd.invertRectangle(88, 7+8*(position-3), 120, 7+8*(position-2)); } } void loop() { if (!digitalRead(LCD_BUTTON_LEFT)) { invertItem(position); position++; position=position&7; invertItem(position); lcd.show(); delay(200); } if (!digitalRead(LCD_BUTTON_RIGHT)) { PIND = bit(position); delay(200); } } Abb. 4.19: Das Navigationsmenü Da dieses und die folgenden Programme sehr lang sind, werden immer nur einzelne Quelltextabschnitte gezeigt und erklärt. Die Grundlagen zum Verständnis des Programms haben Sie bereits kennengelernt. Deswegen wird es Ihnen auch möglich sein, den Quelltext im Ganzen nachzuvollziehen. 001 void setup() { 002 [...] 003 lcd.drawString(1, 1, "Navigation"); 004 lcd.invertRectangle(0, 0, 60, 8); 005 lcd.drawLine(60, 8, 127, 8); 006 lcd.drawString(5. 55, "Auswahl"); 007 lcd.invertRectangle(0, 54, 50. 63); 008 lcd.drawString(85, 55, "Toggle"); 009 lcd.invertRectangle(77. 54, 127, 63); Seite 92 In den nicht abgebildeten Teilen finden Sie vor allem die übliche Initialisierung des Displays und die Voreinstellungen zu den Buttons sowie die Definition der Pins als Ausgänge. Der hier vorgestellte Quelltext zeigt die Setup-Routine. Diese Zeilen sind sowohl für den oberen Schriftzug als auch für die untere Schrift über den beiden Hardwarebuttons zuständig. Der Text über den Buttons klärt über die jeweilige Funktion auf. In diesem Programmteil wird zum ersten Mal der invertRectangle-Befehl benutzt, doch was bewirkt dieser Befehl? Wenn Sie das Programm auf den Controller geladen haben, werden Sie sehen, dass sich die eben vorgestellten Elemente auf dem Display von der in anderen Beispielen gewählten Darstellung unterscheiden. Es befindet sich nämlich nicht dunkler Text auf hellem Hintergrund, sondern genau andersherum helle Schrift auf dunklem Hintergrund. Im Grunde wurde ein ausgewähltes Rechteck invertiert, wie der Name der Funktion auch vermuten lässt. In diesem Fall handelt es sich mehr um eine Designfrage, doch wenn es um die Navigation durch das Menü geht, bekommt die Funktion auch einen praktischen Nutzen. Zunächst werden allerdings noch in der Setup-Routine die einzelnen Menüpunkte, also die Pinbezeichnungen, mithilfe einer for-Schleife generiert. Am Ende wird der erste Menüpunkt markiert, denn dieser ist bei Programmstart ausgewählt. Die Markierung übernimmt die Funktion invertItem weiter unten im Programm. 001 void invertItem(byte position) { 002 if (position<4) { 003 lcd.invertRectangle(8, 7+8*(position+1), 40, 7+8*(position+2)); 004 } 005 else { 006 lcd.invertRectangle(88, 7+8*(position-3), 120, 7+8*(position-2)); 007 } 008 } Diese Funktion ermöglichtes, anhand der Position des Items, also einem Wert zwischen 0 und 7, ein Rechteck um diesen Menüpunkt zu berechnen und dieses Rechteck schließlich zu invertieren. Seite 93 Als Letztes fehlt noch das Auswählen von Elementen in der loop-Schleife. 001 void loop( ) { 002 if (!digitalRead(LCD_BUTTON_LEFT)) { 003 invertltem(position); 004 position++; 005 position=position&7; 006 invertItem(position); ()07 008 lcd.show(); 009 delay(200); 010 011 if (!digitalRead(LCD_BUTTON_RIGHT)) { 012 PIND = bit(position); 013 delay(200); 014 } 015 } Wird der linke Auswahl-Button gedrückt, wird zunächst das aktuelle Element invertiert. D.h. das Element das eben markiert war, wird nun wieder invertiert und ist deswegen nicht mehr markiert. Dann springt die aktuelle Position um einen Zähler weiter und das neue Element wird markiert. Ein kurzes Delay sorgt für eine Entprellung, ermöglicht es aber dennoch, mit Gedrückthalten durch das Menü zu scrollen. Wird der rechte Togglel-Button gedrückt, verändert sich der Zustand des Pins. Man könnte nun z.B. eine Reihe von LEDs mit Vorwiderständen anschließen und mittels Menü an- und ausschalten. Über eine entsprechende Schaltung könnte man so sogar die gesamte Wohnungsbeleuchtung bequem über das Menü steuern. 4.14 Blick in die Library V - die Library ist komplett Alle Funktionen sind nun komplett und lassen sich über das DIY-Beispiel nutzen. In diesem Kapitel werden die beiden letzten neuen Funktionen analysiert, nämlich drawEllipse und invertRectangle. Die neue Initialisierungsfunktion initSoftSPI wurde bereits im Kapitel „HardwarePlayground" betrachtet. Seite 94 001 void Display::drawEllipse(byte x, byte y, byte a, byte b) { 002 int dx — 0, dy = b; // im I. Quadranten von links oben nach rechts unten 003 long a2 = a*a, b2 — b*b; 004 long err — b2-(2*b-1)*a2, e2; // Fehler im 1. Schritt 005 006 do { 007 drawPixel(x+dx, y+dy); // I. Quadrant 008 drawPixel(x-dx, y+dy); // II. Quadrant 009 drawPixel(x-dx, y-dy); // III. Quadrant 010 drawPixel(x+dx, y-dy); // IV. Quadrant 011 012 e2 = 2*err; 013 if (e2 < (2*dx+1)*b2) { 014 dx++; 015 err +- (2*dx+1)*b2; 016 } 017 if (e2 >- (2*dy-1)*a2) { 018 dy- - ; 019 err -= (2*dy-1)*a2; 020 }
021 }
022 while (dy >= 0);
023 024 while (dx++ < a) i // fehlerhafter Abbruch bei flachen Ellipsen (b=1) 025 drawPixel(x+dx, y); // -> Spitze der Ellipse vollenden 026 drawPixel(x-dx, y); 027 } 028 } Bei der Funktion drawEllipse kommt erneut ein Algorithmus nach Bresenham zum Einsatz. Wenn Sie den Algorithmus genau verstehen wollen, hilft der folgende Wikipedia-Artikel: de.wikipedia.org/wiki/Bresenham-Algorithmus Die Funktion hat 4 Parameter. Die Parameter x und y definieren den Mittelpunkt des Kreises. a und b geben den Radius an. Nun wird, ähnlich wie beim Linienalgorithmus, eine Fehlerkonstante berechnet. Die while-Schleife beinhaltet das Zeichnen des Kreises. Der Kreis selbst ist in vier Quadranten aufgeteilt. Seite 95 Das bedeutet, dass der Kreis nicht in einem gezeichnet wird, sondern sich jedes Viertel des Kreises mit jedem Durchgang ein Stück aufbaut. Abb. 4.20: So wird der Kreis gezeichnet Anhand der Fehlerkonstante wird entschieden, welche neuen Punkte gezeichnet werden. Entscheidend sind die Differenzkonstanten dx und dy. Über die Konstante dy wird auch entschieden, ob der Kreis vollständig ist. Ein Sonderfall muss noch getrennt behandelt werden. Wenn die Ellipse sehr flach verläuft, kann die oben beschriebene Funktion nicht genutzt werden. In diesem Fall kommt die untere while-Schleife zum Einsatz. 001 void Display::invertRectangle(byte xl, byte yl, byte x2, byte y2) { 002 for (byte x=x1: x<=x2; x++) { 003 for (byte y=y1; y<=y2; y++) { 004 videoBuffer[x + (y/8) * 128] ^= _BV((y%8)); 005 } 006 } 007 } Die invertRectangle-Funktion invertiert die Pixel in einem durch Parameter definierten Rechteck, wie es in dem Beispiel Navigation gezeigt wurde. Die Funktion dahinter ist recht einfach. In zwei for-Schleifen, die die Breite und Höhe des Rechtecks ablaufen, werden die Bytes des Video-Buffers mit dem Bytewert der aktuellen Y-Position exklusiv-oder verknüpft [Operator ^=]. Seite 96 Der Exklusiv-oder-, auch XOR genannt, unterscheidet sich von dem Oder-Operator dadurch, dass immer dann, wenn beide bits wahr sind, das Ergebnis nicht wahr lautet. Durch diese XOR-Verknüpfung werden die bits des Video-Buffers nacheinander invertiert. Abb. 4.21: Die Invertierung der Pixel mittels XOR 4.15 turtleGraphics - der kleine Zeichenroboter Bei der Bildbeschreibungssprache Turtle-Grafik handelt es sich um eine einfache Programmiersprache, bei der man sich vorstellt, dass eine Schildkröte [engl. Turtle] mit einem Stift über ein Blatt Papier läuft und dabei Linien zeichnet. Man hat die Möglichkeit, die kleine Schildkröte mit ein paar einfachen Befehlen zu steuern und dadurch ein Muster oder eine Zeichnung aufs Display zu bringen. Einige Programmiersprachen wie z.B. LOGO, die zu Lernzwecken oft an Schulen verwendet werden, basieren auf diesem Prinzip. In diesem Beispiel wurde die ursprüngliche Turtle-Sprache mit einer neuen API für das Display verbunden. ORDNER turtleGraphics | turtleGraphics.ino | Display.cpp | Display.h | Font.h | Turtle.cpp | Turtle.h | #include "Turtle.h" #include "SPI.h" Turtle turtle = Turtle(); void setup() { turtle.init(20); turtle.back(32); tree(14.0); } void loop() { } void tree(double length) { if (length < 4.55) { return; } turtle.forward(length); turtle.left(30); tree(length * 0.85); turtle.right(60); tree(length * 0.85); turtle.left(30); turtle.back(length); return; } Abb. 4.22: Der gezeichnete Baum Sie können durch einige wenige Befehle die Turtle über das Display bewegen. Die aufwendigen Berechnungen der Positionen sind komplett in die Library verlagert. Seite 97 Alle Befehle, die Sie benötigen, sind in der folgenden Tabelle zusammengefasst. Befehl Bedeutung turtle.init(byte contrast) Mit diesem Befehl wird die Turtle sowie das Display am Anfang eines Programms initialisiert . turtle.forward(byte distance); Mit forward wird die Turtle um die in distance angegebene Anzahl von Pixeln vorwärtsbewegt . turtle.back(byte distance); Mit forward wird die Turtle um die in distance angegebene Anzahl von Pixeln rückwärtsbewegt . turtle.right (byte ang1e); Mit right drehen Sie die Turtle um den mit angle übergebenen Winkel in Grad nach rechts . turtle.left(byte ang1e); Mit left drehen Sie die Turtle um den mit angle übergebenen Winkel in Grad nach links . turtle. home( ); Mit dem home-Befehl bewegen Sie die Turtle zurück auf die Startposition in der Mitte des Displays . turtl e.clean( ) ; Mit clean können Sie alle zuvor gezeichneten Linien löschen. Die Turtle behalt Ihre aktuelle Position. turtle.cleanscreen( ); Mit clean können Sie alle zuvor gezeichneten Linien löschen und die Turtle wird auf die Home-Position zurückgesetzt. turtle.penup(); Mit diesem Befehl wird der virtuelle Stift der Turtle angehoben. Das bedeutet, dass alle nachfolgenden Bewegungsbefehle keine Linie auf dem Display hinterlassen. turtl e .pendown( ) ; Mit diesem Befehl wird der virtuelle Stift wieder gesenkt. Das heißt, dass ab dann die Bewegungen wieder nachgezeichnet werden. Seite 98 Mit diesen Funktionen lässt sich eine ganze Menge verschiedener Zeichnungen realisieren. 001 void tree(double length) { 002 if (length < 4.55) { 003 return; 004 } 005 turtle.forward(length); 006 turtle.left(30); 007 tree(length * 0.85); 008 turtle.right(60); 009 tree(length * 0.85); 010 turtle.left(30); 011 turtle.back(length); 012 return; 013 } Der oben abgebildete Quelltext ist eines der möglichen Beispiele. Die Befehle der Turtle sind in einer eigenen Routine mit dem Namen Tree untergebracht. In dem Beispiel aus dem Internet wird diese Routine aufgerufen. Das Ergebnis ist ein Baum, der auf das Display gezeichnet wird. Der Baum entsteht durch Rekursion, also eine Funktion, die sich selbst aufruft. Damit wird bis zu einer bestimmten Tiefe die Verästelung des Baums realisiert. Eine Besonderheit in diesem Beispiel sind die Rundungsfehler, die dazu führen, dass der Baum an bestimmten Stellen mehr Fülle erlangt. Durch die Multiplikation mit einer Kommazahl entstehen diese Rundungsfehler automatisch. Ein ebenfalls interessantes Turtle-Programm ist der automatische Kunst-Creator. Mit gerade mal zwei random-Befehlen malt die Turtle völlig autonom ganze Kunstwerke auf das Display - sofern man abstrakte Kunst zu schätzen weiß. Mit einem kleinen delay kann man sogar dabei zusehen: 001 void loop() { 002 turtle.forward(random(20)); 003 turtle.left(random(360)); 004 delay(1000); 005 } Seite 99 4.16 Flappy Bird - Spielspaß zum Abschluss Auch hier können nur einige wenige Elemente des Quelltextes genauer betrachtet werden. Sie haben alle Programmiergrundlagen und die nötigen Algorithmen kennengelernt, um dieses oder ein ähnliches Programm auch selbst zu erstellen oder zumindest den Quelltext nachvollziehen zu können. In diesem Abschlussbeispiel steht jedoch in erster Linie der Spaß im Vordergrund. Bei dem Programm handelt sich um ein kleines Geschicklichkeitsspiel für Smartphones, das sich vor allem Anfang 2014 großer Popularität erfreute. Der Entwickler Dong Nguyen entschied sich allerdings schon im Februar des gleichen Jahres dazu, das Spiel wieder aus den App-Stores zu nehmen. Dennoch gibt es aktuell etliche Klone, sodass das Spielprinzip bis heute populär ist. ORDNER flappyBirdClone | flappyBirdClone.ino | Display.cpp | Display.h | Flappy.h | Font.h | #include "Display.h" #include "SPI.h" #include "Flappy.h" #define GATE 25 #define GROUND 111//128-1-16 #define PIPE_WIDTH 16 #define PIPE_BORDER_HEIGHT 4 #define PIPE_BORDER_WIDTH 1 #define PIPE_REFLECTION 2 #define LCD_BUTTON_LEFT A4 Display lcd = Display(); void setup() { lcd.init(20); // Display initialisieren pinMode(LCD_BUTTON_LEFT, INPUT); digitalWrite(LCD_BUTTON_LEFT, HIGH); lcd.clearVideoBuffer(); drawGround(); drawPipe(60, 50, true); drawPipe(40, 10, false); lcd.drawBitmap(16, 40, 16, 16, flappy); lcd.show(); } void loop() { byte gateHeight = GATE; double speed = 0; double posY = 20; double time = 1; byte score = 0; boolean alive = false; delay(2000); while (digitalRead(LCD_BUTTON_LEFT)) { } alive = true; while(alive) { long gate = random(25, 80); gateHeight--; //lvl for (byte y=1; y<80; y++) { lcd.clearVideoBuffer(); drawPipe(gate+gateHeight, y, true); drawPipe(gate-gateHeight, y, false); drawGround(); if (y == 60) { score++; } if (( y > 60 && y < 80) && score > 0) { lcd.drawBitmap(16, 34, 16, 8, flappyFont+16*(score/10)); lcd.drawBitmap(16, 24, 16, 8, flappyFont+16*(score%10)); } time = time + 0.01; if (!digitalRead(LCD_BUTTON_LEFT)) { speed= 5; time=1; } posY -= speed * time; speed -= 1.0 * time; // 1.0 lcd.drawBitmap(posY, 40, 16, 16, flappy); //Collision if (posY > GROUND -16 || (y < 70 && y > 40 && posY > gate+gateHeight-16) || (y < 70 && y > 40 && posY < gate-gateHeight)) { alive = false; lcd.clearVideoBuffer(); drawPipe(gate+gateHeight, y, true); drawPipe(gate-gateHeight, y, false); drawGround(); lcd.drawBitmap(posY, 40, 16, 16, flappyRIP); lcd.drawBitmap(16, 34, 16, 8, flappyFont+16*(score/10)); lcd.drawBitmap(16, 24, 16, 8, flappyFont+16*(score%10)); lcd.show(); break; } lcd.show(); delay(50); } } } void drawGround() { lcd.drawLine(GROUND, 0, GROUND, Display::HEIGHT); lcd.drawLine(GROUND+3, 0, GROUND+3, Display::HEIGHT); } void drawPipe(byte x, byte y, boolean ground) { byte pipeBorderX = ground ? x+PIPE_BORDER_HEIGHT : x-PIPE_BORDER_HEIGHT; // xposition Rand byte yPipeWidth = y-PIPE_WIDTH < 0 ? 0 : y-PIPE_WIDTH; lcd.drawLine(x, y, x, yPipeWidth); // Röhreneingang lcd.drawLine(pipeBorderX, y, pipeBorderX, yPipeWidth); // Röhrenrand lcd.drawLine(x, y, ground ? x+PIPE_BORDER_HEIGHT : x-PIPE_BORDER_HEIGHT, y); // vorderes Seitenteil Röhrenrand lcd.drawLine(x, y-PIPE_REFLECTION, ground ? x+PIPE_BORDER_HEIGHT : x-PIPE_BORDER_HEIGHT, y-PIPE_REFLECTION); // vorderes Seitenteil Röhrenrand Reflektion lcd.drawLine(x, y-PIPE_WIDTH, ground ? x+PIPE_BORDER_HEIGHT : x-PIPE_BORDER_HEIGHT, y-PIPE_WIDTH); // hinteres Seitenteil Röhrenrand lcd.drawLine(pipeBorderX, y-PIPE_BORDER_WIDTH, ground ? GROUND : 0, y-PIPE_BORDER_WIDTH); // Vorderseite lcd.drawLine(pipeBorderX, y-PIPE_WIDTH+PIPE_BORDER_WIDTH, ground ? GROUND : 0, y-PIPE_WIDTH+PIPE_BORDER_WIDTH); // Hinterseite lcd.drawLine(pipeBorderX, y-PIPE_REFLECTION-PIPE_BORDER_WIDTH, ground ? GROUND : 0, y-PIPE_REFLECTION-PIPE_BORDER_WIDTH); // Reflektionslinie } Abb. 4.23: Floppy Birds auf dem Display Ziel des Spiels ist, mit einen kleinen Vogel zwischen zwei Rohren hindurchzufliegen. Die Steuerung kann man sich leicht merken, denn man benötigt nur den oberen Hardwarebutton, mit dem man dem Vogel einen kurzen Aufschwung geben kann. Dennoch ist eine Menge Geschicklichkeit gefragt, denn drückt man zu lange, stößt der Vogel oben am Kanalrohr an, reicht der Schwung hingegen nicht, stürzt der Vogel ab. Zudem erhöht sich der Schwierigkeitsgrad mit jedem Hindernis, da die Lücke zwischen den beiden Röhren immer kleiner wird. Seite 100 Wenn der Vogel scheitert, wird die Anzahl der überwundenen Hürden angezeigt. Mit erneutem Druck auf die Taste kann man von vorn starten. Im Folgenden ist der Quelltext des FlappyBirdClone in Kurzform dargestellt. 001 for (byte y-1; y<80; y++) { 002 [...] 003 004 if (y == 60) { 005 score++; 006 } 007 008 if (( y > 60 && y < 80) && score > 0) { 009 [...] 010 } 011 012 time - time + 0.01; 013 if (!digitalRead(LCD_BUTTON_LEFT)) { 014 speed- 5; 015 time-1; 016 } 017 018 posY -= speed * time; 019 speed -=1.0 * time; // 1.0 020 lcd.drawBitmap(posY, 40. 16. 16. flappy); 021 022 // Collision // todo +- bei gate I023 if (posY > GROUND -16 || (y < 70 && y > 40 && posY > gate+gateHeight-16) || (y < 70 && y > 40 && posY < gate-gateHeight)) { 024 [...] 025 } 026 lcd.show(); 027 } 028 } Der wichtigste Teil des Programms befindet sich in dieser for-Schleife. Ein Durchgang in dieser Schleife entspricht einer Schwierigkeitsstufe, also einer Überwindung der Rohre. Ein Durchlauf beinhaltet 80 Positionen (64 Stellen auf dem Display + 16 Stellen für ein Rohr), wobei einige Positionen in if -Abfragen besonders behandelt werden. Seite 101 Die erste if-Abfrage if (y == 60) ist erfüllt, wenn der Vogel ein Hindernis passiert hat. Die Variable score, die den Spielstand angibt, wird dann automatisch um eins erhöht. Die zweite if -Anweisung if ( ( y > 60 && y < 80) && score > 0) wird ausgeführt, wenn der Vogel gerade ein Hindernis passiert hat. In diesem Fall wird für einen kurzen Zeitraum der Punktestand angezeigt. Vor der nächsten if-Abfrage wird die Variable time erhöht. Sie bildet die Zeitbasis zur Berechnung der aktuellen Fallgeschwindigkeit und y-Position: 001 posY -= speed * time; 002 speed -= 1.0 * time; // 1.0 003 lcd.drawBitmap(posY, 40, 16, 16, flappy); Eine Schwierigkeit des Spiels besteht nämlich darin, dass der Vogel mit der Zeit immer schneller sinkt. Das ist in dem oben abgebildeten Teil realisiert. Mit der if-Abfrage if ( !digital Read( LCD_BUTTON_LEFT)), die durch das Drücken des Buttons wahr wird, kann man das Sinken aufhalten, indem man dem Vogel einerseits einen Speed-boost nach oben verschafft und andererseits die Zeitvariable auf eins zurücksetzt. Die letzte if-Abfrage hat eine Reihe von Bedingungen, die ODER-verknüpft sind. Der Quelltext wird also ausgeführt, wenn eine der Bedingungen wahr wird. Es geht hierbei um den Fall der Kollision des Vogels. Wenn man die einzelnen Bedingungen übersichtlicher schreibt, wird das deutlicher: 001 if ( 002 posY > GROUND -16 003 || 004 (y < 70 && y > 40 && posY > gate+gateHeight-16) 005 || 006 (y < 70 && y > 40 && posY < gate-gateHeight) 007 ) Es gibt also genau drei Möglichkeiten, das Spiel zu verlieren. Der erste Fall tritt ein, wenn der Vogel den Boden berührt. Im zweiten Fall stößt der Vogel oben an einem Rohr an und im dritten berührt der Vogel das Rohr unten. Kommt es zu einer dieser Kollisionen, wird das Spiel beendet. Seite 102 Das Programm ist ein nettes Geschicklichkeitsspiel für zwischendurch und zugleich ein Anlass für eine Menge Frust. Es macht dennoch sehr viel Spaß, im direkten Vergleich gegen Freunde anzutreten. Damit Sie den Controller mit dem Display auch mobil nutzen können, gibt es die Möglichkeit, eine Batterie anzuschließen. Dabei sollten Sie allerdings immer darauf achten, die Stromversorgung nach dem Spielen wieder zu trennen, denn das Arduino Board ist mit ca. 70mA ein ziemlich energiehungriger Verbraucher. Seite 103
5. BEFEHLSLISTE
5.1 API Grafikdisplay 128 x64 dots
API-Funktion Parameter Erklärung init byte contrast (0-63] Initialisierung des Displays mit übergebendem Kontrastwert (Hardware SPI], Hintergrundbeleuchtung nicht schaltbar, maximale Übertragungsgeschwindigkeit initSoftSPl byte contrast (0-63) Initialisierung des Displays mit übergebenden-Kontrastwert (Software SPI), Hintergrundbeleuchtung [Pin 12] schaltbar writeCommand byte command Führt ein ST7565-Kommando aus writeData byte data Überträgt das angegebene Byte auf das Display resetRamAddress - Setzt den RAMzeiger auf Zeile 0 Spalte 0 setPageAddress byte pageAddress (0-7) Setzt den RAMzeiger in die angegebene Zeile setColumnAddress byte columnAddress [0-127] Setzt den RAMzeiger in die angegebene Spalte clearDisplayRAM - Löscht den gesamten Inhalt des Displays clearVideoBuffer - Löscht den gesamten Inhalt des Videopuffers drawPixel byte x, byte y [x-, y-Position] Zeichnet einen Punkt an die gegebene Position [Änderungen beziehen sich auf den Videopuffer] drawLine byte xl, byte yl [Punkt 1) byte x2, byte y2 [Punkt 2] Zeichnet eine Linie zwischen den beiden Punkten (xl, yl] und (x2, y2] (Änderungen beziehen sich auf den Videopuffer] drawString byte x, byte y [x-, y-Position], Zeichenkette Zeichnet eine Zeichenkette an eine beliebige Position (x, y] [Änderungen beziehen sich auf den Videopuffer] drawBitmap byte x, byte y, byte width, byte height, const byte bitmap[] Zeichnet eine Bitmap an Position x, y mit der Länge (width) und der Breite (height) (Änderungen beziehen sich auf den Videopuffer] drawEllipse byte x, byte y, byte a, byte b Zeichnet eine Ellipse/einen Kreis an Position [x,y] mit dem Radius [a, b] (Änderungen beziehen sich auf den Videopuffer) invertRectangle byte xl, byte yl, byte x2, byte y2 Invertiert die Anzeige in dem angegebenen Rechteck mit der Diagonalen (xl, yl], (x2, y2] (Änderungen beziehen sich auf den Videopuffer) Show - Überträgt den Inhalt des Videopuffers auf das Display
Seite 104 5.2 API Textdisplay 22x 8 Zeichen API-Funktion Parameter Erklärung init byte contrast [0-63] Initialisierung des Displays mit übergebendem Kontrastwert (Hardware SPI], Hintergrundbeleuchtung nicht schaltbar, maximale Übertragungsgeschwindigkeit println Zeichenkette Gibt eine Zeichenkette in der aktuellen Zeile aus [mit anschließendem Zeilenumbruch] print Zeichenkette Gibt eine Zeichenkette in der aktuellen Zeile aus clear - Löscht den internen Textpuffer und setzt die aktuelle Position auf Anfang getLastCharPosition byte line [0-7] Ermittelt die Position des letzten Zeichens in einer angegebenen Zeile writeTextBuffer - Stellt den Inhalt des Textpuffers auf dem Display dar Seite 105
ENDE
Quelle:
Autoren: 2015 Thomas Baum & Fabian Kainka
HANDBUCH zu FRANZIS MAKER KIT Grafikdisplays programmieren
ISBN: 3-645-65278-0
ST7565 basiertes LCD-Shield für ARDUINO UNO R3 DIN A4 ausdrucken ********************************************************* Impressum: Fritz Prenninger, Haidestr. 11A, A-4600 Wels, Ober-Österreich, mailto:[email protected] ENDE |
Grafik-Display >