Grafik-Display‎ > ‎

4 GrafikDisplay

http://sites.schaltungen.at/arduino-uno-r3/grafik-display-1/4-grafikdisplay

http://www.linksammlung.info/

http://www.schaltungen.at/

                                                                                             Wels, am 2016-06-12

BITTE 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


4. GRAFIKDISPLAY
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 library

http://playground.arduino.cc/Code/LCD12864




Dieser 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
   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.html


Graphic ST7565 Positive LCD (128x64) with RGB backlight + extras - ST7565

https://www.adafruit.com/product/250

Command                       Command Code    Funktion
Display 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 dis
tance);     Mit forward wird die Turtle um die in distance angegebene Anzahl von Pixeln rückwärtsbewegt          .
turtle.right (byte ang
1e);      Mit right drehen Sie die Turtle um den mit angle übergebenen Winkel in Grad nach rechts              .
turtle.left(byte ang
1e);        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