Grafik-Display am STM32F4-Discovery

Das kleine Grafikdisplay wird bereits sehr erfolgreich mit den größeren AVRs eingesetzt. ARM Controller rufen mit ihrer Leistungsfähigkeit geradezu nach grafischen Displays. Dieser Abschnitt soll kurz umreißen wie man ein solches Display, unter Nutzung verfügbarer Bibliotheken, effizient einsetzt.

Bitte beachten Sie, dass dieser Abschnitt des Tutorials auf die Kenntnisse der vorangegangenen Abschnitte aufbaut. Gern können wir dieses Anwendungsbeispiel auch gemeinsam in einem unserer ARM-Aufsteiger-Seminare entwickeln.

Benötigte Hardware und Software

Natürlich benötigen wir zunächst ein Grafik-Display für unser STM32F4-Discovery. Dazu bietet sich für den versierten Bastler der Bausatz an oder, wer es nicht so mit dem Löten hat, die bestückte Variante.

Dieser Tutorialabschnitt wendet sich vor allem an die Nutzer eines STM32 Einsteigersets. Daher sollten folgende Hard- und Softwarekomponenten verfügbar sein:

Das Grafik-Display Add-On

Das GLCD-Add-On basiert auf dem myAVR-Steckverbinder. Dieser verfügt über 20 Pins. Dabei sind 18 Pins für IO-Leitungen frei verfügbar. Das Add-On kann somit an alle myAVR-Systemboards als auch an die ARM-Erweiterungsboards für das mySTM32F4D und das mySTM32F0D angeschlossen werden.

Die derzeitige Version des mySTM32 Erweiterungsboards verfügt über keine festen Verbindungen zur Erweiterungsbuchse. Diese kann der Anwender selbst frei wählen und entweder per Patchkabel (rechtes Bild) oder mit festen Lötverbindungen realisieren (linkes Bild).

Die folgenden Bilder zeigen die Realisierung mit Lötverbindungen.

Vorbereitungen

Legen Sie ein neues SiSy-Projekt mit dem Vorgehensmodell ARM und ARM Framework sowie ein Klassendiagramm mit der Zielsprache ARM C++ an. Beachten Sie die Einstellungen für die Zielplattform STM32F4-Discovery. Laden Sie bitte die Diagrammvorlage für eine ARM C++ Applikation.

Das Programmgerüst sollte übersetzt (kompilieren, linken) und in den Programmspeicher des Controllers übertragen werden (brennen).

Damit steht die Verbindung zum Controller und wir können mit der Arbeit beginnen.

Die Bibliotheken

Bis hier war es das allgemeine Prozedere, um eine STM32 Anwendung anzulegen. Die aktuellen Treiber für das Grafikdisplay holen wir uns ebenfalls aus dem SiSy-LibStore. Dazu ziehen wir aus der Objektbibliothek das Element LibStore. Daraufhin öffnet sich der SiSy-LibStore-Dialog. Als Suchbegriff geben wir GLCD ein.

Wählen sie die angebotene Komponente aus und importieren Sie diese in das Diagramm. Beachten Sie, dass Sie den Baustein für den STM32F4 importieren.

Erste Schritte

Das Paket wird jetzt als verfügbare Komponente im Klassendiagramm angezeigt. Wählen sie auf der Komponente rechte Maustaste - in neuem Fenster öffnen. Jetzt können wir uns die innere Struktur der Komponente ansehen.

Die Klasse GraficLcd besteht aus einer Reihe Attributen, unter anderem vom Typ DigitalOut, welche die einzelnen Steuer- und Datenleitungen abstrahieren. Diese sind öffentlich, damit das Display vom Anwender konfiguriert werden kann. Die für uns wichtigsten Operationen, um später das Spiel zu realisieren, können wir auch schon erkennen.

  • init()
  • light()
  • clear()
  • setPos()
  • write()
  • circle()
  • line()
  • rect()

Um das Display zu verwenden, muss eine Referenz der Klasse GraficDisplay in das Klassendiagramm der Anwendung gezogen werden. Diese ist mit einer Aggregation an die Applikations-Klasse anzubinden. Benutzen Sie als Rollenbezeichner den namen +lcd.

Für die Initialisierung des Displays wählen wir die Operation onStart der Klasse Application aus. Zuerst sind die einzelnen Steuer- und Datenleitungen zu konfigurieren. Danach kann der eigentliche Displaycontroller initialisiert werden. Vergleichen Sie dazu die Pinbelegung des GLCD-Add-On und des STM32F4 Discovery.

/// <sequence show="hide" text="config LDC_lines">
lcd.linePs.config (GPIOB,BIT15);
lcd.lineC86.config(GPIOB,BIT14);
lcd.lineLight.config(GPIOB,BIT13);
lcd.lineE.config(GPIOB,BIT12);
lcd.lineRw.config(GPIOB,BIT11);
lcd.lineRs.config(GPIOB,BIT9);
lcd.lineRes.config(GPIOB,BIT4);	
lcd.lineCs.config(GPIOB,BIT8);	//B7
lcd.dataPort.config(GPIOE,0xFF00);	// E8..E15
/// </sequence>
lcd.init();
lcd.clear();
lcd.light();

Zu guter Letzt schließen wir die Initialisierung mit einem clear des Displayinhaltes (falls noch Schrott vom letzten Versuch zu sehen ist) und dem Einschalten der Hintergrundbeleuchtung ab. Die beiden Kommentar-Tags

 /// <sequence show="hide" ... > 

und

 /// </sequence> 

sorgen dafür, dass im Sequenzdiagrammgenerator dieser Teil zugeklappt wird.

Die Ausgabe von Texten erfolgt an einer beliebigen grafischen Position. Dazu benutzen wir die Operation setPos. Der Cursor wird bei der Ausgabe des Textes mit der Operation write automatisch weiter gestellt. Deshalb setzt sich jede weitere Ausgabe an jeweils der letzten Cursorposition fort.

lcd.setPos(40, 20);          // Cursor positionieren
lcd.write("Hallo mySTM32");  // Textausgabe
lcd.write("!!!");            // den Text fortsetzen

Erstellen Sie die Anwendung und übertragen Sie diese in den Programmspeicher des Controllers. Testen sie das Programm!

… Punkt, Linie, Rechteck, Kreis

lcd.line(10,10,50,50);
lcd.rect(20,10,40,20);
lcd.circle(30,30,10,true);

Erstellen Sie die Anwednung und übertragen Sie diese in den Programmspeicher des Controllers. Testen sie das Programm!

Eine kleine Anwendung

Es soll eine kleine Spielanwendung, in Anlehnung alter Spieleklassiker erstellt werden. Wir nennen unser Projekt SoloSquash. Bei diesem Spiel ist mit einem Schläger zu verhindern, dass ein Ball ein Tor trifft. Der Ball muss gegen die gegenüberliegenden Wände (Spielfeld/Display-Grenzen) gespielt werden. An den Wänden prallt der Ball ab. Trifft der Ball das Tor, ist das Spiel vorbei. Den Schläger wollen wir mit dem Lagesensor auf dem STM32F4 Board steuern. Im Folgenden ein kleines Storyboard für das Spielszenario.

Aus der textuellen und bildlichen Aufgabenstellung lassen sich folgende Klassenkandidaten ableiten:

  • SoloSquash, Spiel, Anwendung (Synonyme)
  • Schläger
  • Tor
  • Ball
  • Bewegungssensor
  • Display

Falls Sie die oben beschriebenen Arbeitschritte noch nicht ausgeführt haben legen Sie ein neues SiSy-Projekt mit dem Namen SoloSquash an. Importieren Sie das aktuelle ARM-Framework für den STM32F4 aus dem SiSy-LibStore in das neue Projekt. Erstellen Sie ein Klassendiagramm mit dem Namen SoloSquash. Wählen Sie als Einstellungen für die Zielplattform ARM C++, STM32F4 Discovery, ST-Link V2. Laden Sie aus dem LibStore das Grundgerüst für eine ARM Applikaton und die Komponente für das GLCD.

Aus den Klassenkandidaten leiten wir die Fachklassen für unser Klassenmodell ab. Einige der Klassen sind durch die verwendeten Vorlagen bereits verfügbar. So zum Beispiel die Klassen für die Applikation, den Bewegungssensor und das Grafikdisplay. Wir ergänzen unser Klassenmodell als erstes um die Fachklassen Schlaeger, Ball und Tor. Die aggregieren wir in der Anwendungsklasse Application des Spiels.

Der nächste Schritt soll erst einmal sein, dass alle Spielelemente an einer bestimmten Position angezeigt werden. Dabei soll der Ball als Kreis, der Schläger als Rechteck und das Tor als Linie angezeigt werden. Daraus ergeben sich für die Klassen eine Reihe von Attributen, die jeweils die Position und Größe der Elemente definieren. Zusätzlich geben wir den Klassen noch eine Operation zum Anzeigen. Ergänzen Sie das Klassendiagramm wie folgt:

Jetzt müssen die Operationen mit Leben erfüllt werden. Ergänzen Sie die angegebenen Operationen wie folgt:

Application::onStart()
/// <sequence show="hide" text="config LDC_lines">
lcd.linePs.config (		GPIOB,BIT15);
lcd.lineC86.config(		GPIOB,BIT14);
lcd.lineLight.config(	GPIOB,BIT13);
lcd.lineE.config(		GPIOB,BIT12);
lcd.lineRw.config(		GPIOB,BIT11);
lcd.lineRs.config(		GPIOB,BIT9);
lcd.lineRes.config(		GPIOB,BIT4);	
lcd.lineCs.config(		GPIOB,BIT8);	//B7
lcd.dataPort.config(GPIOE,0xFF00);	// E8..E15
/// </sequence>
lcd.init();
 
/// <sequence show="hide" text="Intro anzeigen">
lcd.clear();
lcd.light();
lcd.setPos(35, 20);
lcd.write("Willkommen");
lcd.setPos(55, 28);
lcd.write("zu");
waitMs(500);
lcd.setPos(16, 32);
lcd.fontStyle=LCD_FONT_WIDE; // LCD_FONT_FIXED, LCD_FONT_NORMAL
lcd.write("SoloSquash!");
waitMs(1000);
lcd.line(20,55,100,55);
waitMs(500);
lcd.rect(60,50,70,54,true);
waitMs(500);
lcd.circle(50,50,3,true);
/// </sequence>
 
waitMs(2000);
lcd.clear();

Das Display wird initialisiert und ein Begrüßungsbildschirm angezeigt. Dieser bleibt 2 Sekunden stehen. Danach wird das Display gelöscht und das Spiel kann beginnen.

Application::onWork()
tor.show();
schlaeger.show();
ball.show();
waitMs(10);    // vorläufig bis wir eine bessere Idee haben ;-)

Die Operation onWork wird aus der Mainloop der Applikation aufgerufen. Hier können wir alles unterbringen was keine strengen Zeitkriterien erfüllen muss. Das Aufrufen der Wartefunktion waitMs, um einfach nur Rechenzeit zu verbraten, ist jedoch nicht wirklich sexy und sollte im weiteren Verlauf eleganter gelöst werden.

Schlaeger::show()
#define posY 55
app.lcd.rect(posX,posY,posX+breite,posY+hoehe,true);

Der Schläger wird an einer festen Y-Position angezeigt. Die X-Position, Breite und Höhe des Schlägers sind Attribute. Es ist natürlich auch denkbar die Y-Position als Attribut zu realisieren.

Ball::show()
app.lcd.circle(posX,posY,radius,true);

Ball und Tor bedürfen wohl vorerst keiner weiteren Erläuterung.

Tor::show()
app.lcd.line(1,63,127,63);

Erstellen Sie die Anwendung und übertragen Sie diese in den Programmspeicher des Controllers. Testen sie das Programm!

WOW! OK, weiter im Text 8-)

Die Bewegung des Balls soll der Einfachheit halber zunächst nur diagonal erfolgen. Falls Sie Spaß an der Idee finden können Sie gern das Spiel verfeinern und ausbauen. Bei näherer Betrachtung kommt die Bewegung in einer Spielanimation dadurch zustande, dass dem Auge durch eine Folge von Einzelbildern die Bewegung „vorgegaukelt“ wird. Der Ball vollzieht auf dem Display eine schrittweise Änderung (step) seiner Position in X-Richtung und in Y-Richtung. An den den Displaygrenzen als „Spielfeldrändern“ (64×128 Pixel) muss eine Richtungsänderung (abprallen) realisiert werden.

Zunächst ergänzen wir die Klasse Ball mit der Operation move() und die Attribute stepX sowie stepY.

Ergänzen Sie die folgende Operation mit dem Quellcode für die Ballbewegung.

Ball::move()
// ein Schritt
posX+=stepX;
posY+=stepY;
 
// oben checken
if (posY <= radius) 
	stepY = +1;
 
// links checken
if (posX <= radius)
	stepX = +1;
 
// rechts checken
if (posX >= 127-radius)
	stepX = -1;
 
// unten checken
if (posY >= 63-radius)
	stepY = -1;

Damit das auch zu sehen ist, muss die Operation onWork um die Nachricht ball.move() erweitert werden.

Application::onWork()
tor.show();
schlaeger.show();
ball.move();
ball.show();
waitMs(10);    // vorläufig bis wir eine bessere Idee haben ;-)

Übersetzen Sie das Programm. Korrigieren sie ggf. Schreibfehler und übertragen Sie das Programm auf den Controller.

Beim Testen der Anwendung fällt sofort auf, dass der Ball sich zwar bewegt, aber er hinterlässt eine unschöne „Schleifspur“. Wir haben vergessen den Ball an der alten Position verschwinden zu lassen. Dafür bietet das Display verschiedene Modi, um Pixel auf dem Display zu manipulieren.

  • SET … Pixel setzen
  • CLR … Pixel löschen
  • XOR … Pixel invertieren

Diese Möglichkeiten sind in der Treiber-Klasse für das Display als Definitionen hinterlegt.

  • LCD_MODE_SET
  • LCD_MODE_CLR
  • LCD_MODE_XOR

Die von uns verwendete Operation circle benötigt mindestens drei Parameter (x,y,radius). Zusätzlich können als optionale Parameter die Füllung und der Pixelmodus angegeben werden. Daraus ergibt sich folgende Signatur der Operation:

 GraficLcd::cirle( uint8_t x, uint_8_t y, uint8_t radius, bool fill=false, mode=LCD_MODE_SET); 

Wir ändern den Code für die Ballbewegung wie folgt:

// alten Ball löschen
app.lcd.circle(posX,posY,radius,true,LCD_MODE_CLR);
 
// ein Schritt
posX+=stepX;
posY+=stepY;
 
// oben checken
if (posY <= radius) 
	stepY = +1;
 
// links checken
if (posX <= radius)
	stepX = +1;
 
// rechts checken
if (posX >= 127-radius)
	stepX = -1;
 
// unten checken, hier ist später das Spiel aus "GAME OVER"
if (posY >= 63-radius)
	stepY = -1;

Übersetzen Sie das Programm. Korrigieremn sie ggf. Schreibfehler und übertragen Sie das Programm auf den Controller.

Damit ist die Ballbewegung vorerst realisiert. Es fehlt nur noch das geforderte Systemverhalten, wenn der Ball das Tor berührt. Dann soll das Spiel ja zu Ende sein. Es macht jedoch noch nicht wirklich viel Sinn, bevor wir das Tor nicht mit dem Schläger verteidigen können.

Für die Steuerung des Schlägers soll der Bewegungssensor genutzt werden. Dieser wurde im Tutorial bereits besprochen. Es ist eine Instanz des Sensors anzulegen und wir müssen ermitteln, welche Sensorwerte für die Steuerung des Schlägers in Frage kommen. Als Erstes sollten wir uns Überlegen, in welcher Beziehung steht der Schläger mit dem Sensor. Dafür kommen in der UML folgende Beziehungstypen in Betracht:

  • der Schläger kennt den Sensor = gerichtete Assoziation
  • der Schläger hat den Sensor = Aggregation
  • der Schläger ist der Sensor = Generalisierung

Alle drei Varianten sind durchaus machbar. Wir arbeiten mit dem aus objektorientierter Sicht mutigsten Entwurf für das Problem weiter, der Generalisierung. Suchen Sie im Framework das Paket EvalBoard_Stm32f4Discovery und ziehen sie das Paket und die Klasse F4dMotionSensor in das Diagramm. Ergänzen Sie das Klassendiagramm wie folgt (Paket EvalBoard, Generalisierung zur Sensorklasse):

Jetzt können wir die Sensordaten ermitteln und am besten gleich mal auf dem Display ausgeben. Diesen Code realisieren wir in der Operation show() des Schlägers.

#define posY 55
app.lcd.rect(posX,posY,posX+breite,posY+hoehe,true);
 
// Testdaten ermitteln
String txt;
s32 x,y,z;
this->getAcc(x,y,z);
txt.format("x=%d    ",x);
app.lcd.setPos(10,10);
app.lcd.write(txt);
txt.format("y=%d    ",y);
app.lcd.setPos(10,20);
app.lcd.write(txt);
txt.format("z=%d    ",z);
app.lcd.setPos(10,30);
app.lcd.write(txt);

Übersetzen Sie das Programm, korrigieren sie ggf. Schreibfehler und testen Sie das System. Beobachten Sie die Veränderung der Sensordaten auf dem Display bei der gewünschten Steuerbewegung.

Der Test sollte ergeben, dass die Y-Koordinate die besten Werte für die Schlägersteuerung liefert. Im Ruhezustand liegt der Y-Wert etwa bei 50 bis 70. Neigt man das Board etwa 45° nach rechts folgt der Wert bis etwa 600. Wenn das Board nach links geneigt wird, liegt der Y-Wert bei 45° etwa bei -600. Der Schläger sollte seine Position natürlich nicht um bis zu 600 Pixel verändern. Aber wenn wir den Wert durch 100 teilen, erhalten wir einen sehr netten Intervall von -6 bis +6. Im Ruhezustand ist der Wert dann 0. Was will man mehr. Ändern Sie die Operation show des Schlägers wie folgt ab:

#define posY 55
s32 x,y,z;
this->getAcc(x,y,z);
app.lcd.rect(posX,posY,posX+breite,posY+hoehe,true,LCD_MODE_CLR);
posX += y/100;
if ( posX <= 0 )
   posX = 0;
if ( posX >= 127-breite )
   posX = 127-breite;
app.lcd.rect(posX,posY,posX+breite,posY+hoehe,true,LCD_MODE_SET);

Übersetzen Sie das Programm, korrigieren sie ggf. Schreibfehler und testen Sie das System. Jetzt sollte sich der Schläger gefühlvoll über die Neigung des Boards bewegen lassen.

Die grundlegenden Algorithmen sind gelöst. Es müssen noch Lösungen für das Auftreffen des Balls auf den Schläger und für das GAME-OVER Szenario sowie eine elegantere Lösung für das Timing gefunden werden. Wenden wir uns zuerst der Trefferauswertung zu. Der Ball verfügt über eine X- und Y-Position sowie über einen Radius. Diese Werte sollen als Parameter einer Nachricht an den Schläger gesendet werden. Dieser antwortet mit true oder false. Die Nachricht nennen wir am besten gleich treffer. Erweitern Sie die Klasse Schlaeger, wie in der folgenden Darstellung gezeigt, um die Operation treffer.

Die Quellcodes für die Operationen treffer des Schlägers und move des Balls sind wie folgt zu ergänzen:

Schlaeger::treffer()
if ( x >= posX &&  x <= posX+breite && y + radius >= 55 )
	return true;
Ball::move()
// alten ball löschen
app.lcd.circle(posX,posY,radius,true,LCD_MODE_CLR);
// ein Schritt
posX+=stepX;
posY+=stepY;
// oben checken
if (posY <= radius) 
	stepY = +1;
// links checken
if (posX <= radius)
	stepX = +1;
// rechts checken
if (posX >= 127-radius)
	stepX = -1;
// Schläger checken
if (app.schlaeger.treffer(posX,posY,radius))
	stepY=-1;
// unten checken, Game Over?
if (posY >= 63-radius)
	stepY = -1;

Übersetzen und Übertragen Sie das Programm auf den Controller. Testen sie, ob der Schläger den Ball jetzt abwehren kann. Das „Game Over“-Szenario planen wir wie folgt zu realisieren: Die Applikation erhält ein Attribut gameOver. Sobald dieses Attribut den Wert true annimmt, wird das Spiel beendet und das Spielende angezeigt. Zusätzlich bereiten wir noch das elegantere Timing vor. Ergänzen Sie das Klassendiagramm wie folgt:

Die Klasse Application wurde durch die Attribute gameOver, refresh und speed sowie die Operationen onTimer10ms und onTimer1s ergänzt. Zuerst passen wir den Quellcode im Ball an.

Ball::move()
// alten ball löschen
app.lcd.circle(posX,posY,radius,true,LCD_MODE_CLR);
// ein Schritt
posX+=stepX;
posY+=stepY;
// oben checken
if (posY <= radius) 
	stepY = +1;
// links checken
if (posX <= radius)
	stepX = +1;
// rechts checken
if (posX >= 127-radius)
	stepX = -1;
// Schläger checken
if (app.schlaeger.treffer(posX,posY,radius))
	stepY=-1;
// unten checken, Game Over !!!!
if (posY >= 63-radius)
	app.gameOver=true;

Danach bietet es sich an, erst mal die einfacheren Codes der Timer-Ereignisse zu realisieren.

Application::onTimer10ms()
refresh++;
Application::onTimer1s()
speed--;
if (speed<=0)
   speed=0;

Zum Schluss passen wir die Operation onWork an.

Application::onWork()
if (refresh>=speed/10)
{
	tor.show();
	schlaeger.show();
	ball.move();
	ball.show();
	refresh=0;
}
 
if (gameOver)
{
	lcd.clear();
	lcd.setPos(16, 32);
	lcd.fontStyle=LCD_FONT_WIDE;
	lcd.write("GAME OVER!");
	while (true) 
	{
		lcd.light(false);
		waitMs(1000);
		lcd.light(true);
		waitMs(1000);
	}
}

Das Attribut refresh ist ein Up-Counter, der alle 10 Millisekunden um eins erhöht wird. Das Attribut speed ist ein Down-Counter, der pro Sekunde um eins verringert wird. Beide zusammen bewirken, dass die Spieldynamik langsam zunimmt und damit auch der Schwierigkeitsgrad. Zusätzlich ist das böse waitMs(10) aus unserem Spielzyklus und damit theoretisch Rechenzeit frei für weitere Funktionen. Bei gameOver == true wird der Bildschirm gelöscht und die Ausschrift „GAME OVER!“ angezeigt. Das Display fängt an zu blinken und nur noch ein RESET kann das System aus der „End-Schleife“ holen.

Übersetzen und übertragen Sie das Programm auf den Controller.

Viel Spaß beim Spielen und beim Weiterentwickeln!

Projektarchiv

Videozusammenfassung

Seminarhinweise