Suche
Teil 5: Grafik, Marke Eigenbau

Mikrocontroller für das IoT: Wir bauen ein UI für den STM32

Tam Hanna

@Shutterstock / BSVIT

Ein vollgrafisches Display macht aus Nutzersicht einen immensen Unterschied. Ein auf Zeichen basierendes LCD wirkt „billig“, während die Anzeige einiger kleiner Animationen in der Post-iPhone-Welt auf Seiten der Kundschaft Faszination und Freude auslöst. Im fünften Teil unserer Serie über Microcontroller für das IoT wollen wir uns deshalb die Umsetzung vollgrafischer User Interfaces anschauen.

Serie: Mikrocontroller für das Internet der Dinge

Mikrocontroller für das IoT: Grafik, Marke Eigenbau

Dank der Verfügbarkeit von preiswerten Displays fallen auch die Preise grafischer Module. Aus Entwicklersicht ist die Nutzung im Grunde genommen immer gleich – die am Bildschirm anzuzeigenden Informationen liegen in Form eines Bitmaps vor, das vom Controller ausgegeben wird. Auch wenn die verschiedenen Displaymodule Eigenheiten aufweisen – wer ein LCD programmieren kann, kommt mit allen anderen auch zurecht. Da unser in den letzten beiden Heften verwendetes Evaluationsboard ein Farbdisplay mitbringt, wollen wir die STM32F429-Planare nochmals einspannen. Die hier gezeigten Verfahren lassen sich – im Großen und Ganzen – auch auf andere Displays und Controller portieren. So nutze ich die weiter unten besprochene Routine von Schaum zur Darstellung gestrichelter Linien auf einem MSP430 – der Controller liefert die anzuzeigenden Bitmaps an ein per SPI angeschlossenes Mini-OLED.

Das LCD im Mittelpunkt

Da es in diesem Tutorial nur um die Demonstration von Techniken zum Erstellen von Grafiken im Allgemeinen geht, wollen wir die in Eclipse Magazin 1.16 beschriebene Minimal-Toolchain nutzen: Ein Echtzeitbetriebssystem ist hier nicht erforderlich. Stattdessen nutzen wir die von STM bereitgestellte HAL, die unsere Arbeit mit diversen, für den Zugriff auf das LCD vorgesehenen Methoden unterstützt. Achten Sie darauf, das vom Generator erzeugte Projektskelett um die unter STM32Cube_FW_F4_V1.9.0\Drivers\BSP\Components und unter STM32Cube_FW_F4_V1.9.0\Drivers\BSP\STM32F429I-Discovery liegenden Inhalte zu ergänzen und die Debugger-Konfiguration korrekt einzustellen.

Die im Rahmen der Erstellung des Projektskeletts erzeugte Blinkroutine ist für uns an dieser Stelle nicht weiter relevant: Ersetzen Sie den Inhalt von main() durch den Code in Listing 1.

#include "stmhal/stm32f429i_discovery.h"
#include "stmhal/stm32f429i_discovery_lcd.h"
int main(int argc, char* argv[])
{

  HAL_Init();

  BSP_LCD_Init();
  BSP_LCD_LayerDefaultInit(0, (uint32_t) LCD_FRAME_BUFFER);
  BSP_LCD_SetLayerVisible(0, ENABLE);
  BSP_LCD_SelectLayer(0);
  BSP_LCD_Clear(LCD_COLOR_BLUE);
  BSP_LCD_DisplayOn();
}

Nach dem Einbinden der im vorigen Schritt importierten HAL aktivieren wir die Abstraktionsbibliothek und werfen die diversen Taktgeneratoren an. Als Nächstes folgt die eigentliche Einrichtung des Displays: Neben der Aktivierung der ersten Displayebene leeren wir den Framebuffer und starten den für die Hintergrundbeleuchtung zuständigen Inverter.

Der als Bildschirmspeicher dienende Bereich wird in der Fachsprache als Framebuffer bezeichnet. Im Fall unseres Displays deckt er ein 320 x 240 x 32 Bit großes Feld ab, was zu einer Gesamtgröße von rund 300 KB führt. Seine Deklaration erfolgt in der HAL über ein #define, also #define LCD_FRAME_BUFFER ((uint32_t)0xD0000000).

Da findet sich auch eine Gruppe von Deklarationen, die uns weitere Informationen über den Aufbau dieses Speicherbereichs liefert. Bei der Analyse eines unbekannten Systems ist es ratsam, mit der Betrachtung von markanten Systemzuständen zu beginnen – ein Kommunikationsprotokoll lässt sich mit dem unverdächtigen AGALAPANZER testen (A*A*A*), während Farbgrafiksysteme Informationen über ihren inneren Aufbau bei Betrachtung von „reinen“ Farbzuständen offenbaren:

#define LCD_COLOR_BLUE     0xFF0000FF

#define LCD_COLOR_GREEN    0xFF00FF00

#define LCD_COLOR_RED      0xFFFF0000

#define LCD_COLOR_WHITE    0xFFFFFFFF

#define LCD_COLOR_BLACK    0xFF000000

Der mit unserem Display verbundene LCD-Controller unterstützt Transparenzen. Daraus folgt, dass Farbinformationen neben den eigentlichen additiven Kanaldaten auch eine Sichtbarkeitsstärke mitbringen müssen – da der Controller von Haus aus im Format ARGP arbeitet, entfallen die ersten acht Bit auf diese Rolle. Das Nicht- oder Falschsetzen des Transparenzwerts ist eine weit verbreitete Falle: Funktioniert ein LCD-Controller nicht nach Plan, sollte dies als Erstes überprüft werden.

Unser Displaycontroller arbeitet mit mehreren Ebenen: Zwecks effizienterer Ressourcenauslastung liegt sein Framebuffer im externen SDRAM der MCU. STM spendiert uns in der CUBE-Bibliothek eine Methode zum direkten Setzen eines Pixels, die wir nach kurzer Besprechung dankbar übernehmen wollen (Listing 2).

 
void BSP_LCD_DrawPixel(uint16_t Xpos, uint16_t Ypos, uint32_t RGB_Code)

{

*(__IO uint32_t*) (LtdcHandler.LayerCfg[ActiveLayer].FBStartAdress + (4*(Ypos*BSP_LCD_GetXSize() + Xpos))) = RGB_Code;

}

Der Term LtdcHandler.LayerCfg[ActiveLayer].FBStartAdress liefert uns die Startadresse der gerade aktiven Ebene. Danach ermitteln wir die eigentliche Zieladresse für das Einschreiben des 32-Bit-Worts: Die Zeilen werden durch Multiplikation mit der Breite neutralisiert, während die Spaltenadresse durch direkte Addition in den mit 4 zu multiplizierenden Adresswert wandert. Am Ende dieser Analyse steht das Setzen des Farbwerts durch direktes Einschreiben.

Damit sind wir fürs Erste startbereit. Führen Sie das Programm auf dem Evaluationsboard aus, um sich am einfarbig leuchtenden Display zu erfreuen.

Zeichnen einer Linie

Nach diesen einführenden Überlegungen wollen wir mit ersten grafischen Gehversuchen beginnen. Das Zeichnen von Linien ist in vielerlei Hinsicht elementar: Rechtecke, Diagramme und andere Elemente lassen sich ohne Linien nur schwer realisieren.

Es zahlt sich aus, bei der Implementierung grafischer Operationen auf Effizienz zu achten. Ein einzelnes Rechteck besteht aus mindestens vier Aufrufen der Linienoperation; bei der Erzeugung komplexerer GUI-Elemente treten dann aber exponentielle Wachstumsprozesse auf.

Die grundlegende Implementierung anhand von y = kx + d funktioniert nur so lange, wie die Steigung der Linie nicht zu hoch gerät: Eine perfekt senkrechte Linie (Delta x von null) würde aus null oder einem Punkt bestehen. Wer dieses Problem durch bedarfsorientiertes Austauschen der Zeichenrichtung löst, ist nicht wirklich weiter: Multiplikationen mit Nichtzweierpotenzen sind vergleichsweise teure Rechenoperationen, denen man als Entwickler von performancekritischem Code nach Möglichkeit aus dem Weg gehen sollte.

Für das Zeichnen von nicht mit Antialiasing geglätteten Linien hat sich der im Jahre 1962 entwickelte Algorithmus von Bresenham als Standardwerkzeug etabliert. Er beginnt mit der Festlegung, dass eine Linie aus jeden Punkten besteht, die den minimalen Abstand zur Ideallinie aufweisen (Abb. 1).

Abb. 1: Der Punkt kann entweder über oder unter dem Idealort liegen

Im Rahmen des eigentlichen Zeichenprozesses wird eine Entscheidungsvariable (hier d) bei jedem Durchlauf aktualisiert. Je nach Wert wird entweder x, oder x und y gleichermaßen inkrementiert und der errechnete Punkt gesetzt. Im nächsten Schritt inkrementieren wir die Koordinaten nach den gegebenen Vorschriften, womit der Vorgang wieder von vorne beginnen kann. Damit können wir uns an die Implementierung der Zeichenroutine heranwagen. LineBresenham präsentiert sich in Listing 3.

void LineBresenham(int _x1, int _y1,int _x2, int _y2,long int color)

{

   int dx=_x2-_x1;

   int dy=_y2-_y1;

   int inc1=2*dy;

   int inc2=2*(dy-dx);

   int d=inc1-dx;



   int x, y;

   x=_x1;

   y=_y1;

   BSP_LCD_DrawPixel(x,y,color);



   while(x<=_x2)

   {

   if(d<0)

   {

   d=d+inc1;
 
   }

    else

   {

     d=d+inc2;

      y++;

    }

    x++;

     BSP_LCD_DrawPixel(x,y,color);

    }

  }

Unsere hier beschriebene Methode geht aus Gründen der Einfachheit davon aus, dass die zu zeichnenden Linien eine Steigung von 0 bis 45 Grad aufweisen und dass der Wert von x2 größer ist als der von x1. Will man mit beliebigen Koordinaten aufrufen, so müssen die Koordinaten vor dem Aktivieren des eigentlichen Zeichenprozesses umgeworfen werden.

Aus Gründen der Vollständigkeit sei noch auf den hier beschriebenen Algorithmus von Xiaolin Wu hingewiesen, der eine Möglichkeit zum schnellen Zeichnen geglätteter Linien bietet.

Linie mit Pause

Bei der Arbeit mit monochromen Displays ist Rasterisation ein wirksames Mittel zur Steigerung der Informationsdichte: Verschiedene Punktfolgen erlauben die Unterscheidung der Geraden. Die im vorigen Abschnitt vorgestellte Liniengleichung y = kx + d erlaubt das Berechnen von Koordinaten in der Mitte der Linie. LineBresenham kann so nur in jenen Bereichen aufgerufen werden, die von der Linie tatsächlich berührt werden sollen. LineDotter ist eine von mir erzeugte Implementierung dieses von Schaum nicht weiterverfolgten Konzepts, die Linien mit zwei Längensegmenten erzeugt. _seg1 gibt dabei die aktive Länge des ersten Linienstücks an, während _seg2 die darauffolgende „Totzeit“ beschreibt. _seg3 und _seg4 kümmern sich um das andere Segment, woraufhin die Bearbeitung wieder mit dem von _seg1 und _seg2 beschriebenen Streckenstück weitergeht (Abb. 2).

Abb. 2: Ein gestricheltes Linienstück besteht aus einzelnen Segmenten

LineDotter beginnt mit der Konversion der Absolutbeträge in die jeweiligen X- und Y-Differenzwerte. Diese sind von der Steigung der Linie abhängig: Je steiler, desto geringer die X-Anteile. Wir lösen dieses Problem durch das Ermitteln der Gesamtlänge der zu zeichnenden Linie und dem Berechnen der insgesamt in X- und Y-Richtung zurückzulegenden Strecken. Im nächsten Schritt folgt die Ermittlung des Anteils eines Liniensegments an der Gesamtlänge: Wenn ein Strich beispielsweise zehn Prozent der Gesamtlinie ausmacht, so entsteht ein Faktor von 0.1. Das Ergebnis einer Multiplikation dieses Werts mit der Gesamt-X- und Gesamt-Y-Strecke ist in Listing 4 zu sehen.

void LineDotter(int _x1, int _y1,int _x2, int _y2, int _seg1, int _seg2, int _seg3, int _seg4, long int color)

{

volatile float d=sqrtf((_x2-_x1)*(_x2-_x1)+(_y2-_y1)*(_y2-_y1));

float xpers1= (_seg1/d)*(_x2-_x1);

float xpers2= (_seg2/d)*(_x2-_x1);

float xpers3= (_seg3/d)*(_x2-_x1);

float xpers4= (_seg4/d)*(_x2-_x1);

float ypers1= (_seg1/d)*(_y2-_y1);

float ypers2= (_seg2/d)*(_y2-_y1);

float ypers3= (_seg3/d)*(_y2-_y1);

float ypers4= (_seg4/d)*(_y2-_y1);

Das eigentliche Zeichnen erfolgt in einem Zustandsautomat, der die im vorigen Schritt besprochene LineBresenham-Methode zur Realisierung der Geradensegmente einspannt. Als Laufvariable dient dabei die X-Koordinate: Wenn x den oberen Grenzwert x2 überschreitet, so endet die Schleife wie in Listing 5.

float x=_x1;

float y=_y1;

int whichSegment=0;

while(x<_x2)

{

   switch(whichSegment)

   {

    case 0:

     LineBresenham(x, y, x+xpers1, y+ypers1, LCD_COLOR_DARKBLUE);

     x+=xpers1;

     y+=ypers1;

     break;

    case 1:

     x+=xpers2;

     y+=ypers2;

     break;

    case 2:

     LineBresenham(x, y, x+xpers3, y+ypers3, LCD_COLOR_ORANGE);

     x+=xpers3;

     y+=ypers3;

     break;

    case 3:

     x+=xpers4;

     y+=ypers4;

     break;


    }



   whichSegment=(++whichSegment)%4;

  }

}

Zwecks höherer Zeichengenauigkeit habe ich die Ermittlung der schon zurückgelegten Strecke mit floats realisiert. In praktischem Code wäre hier die Nutzung von „Fixkommaarithmetik“ sinnvoller – im Internet finden sich fertige Headerdateien, die die notwendige Logik mitbringen.

Bei der Arbeit mit grafischen Systemen ist man als Entwickler immer wieder mit Grenzfällen konfrontiert. Ein gutes Beispiel dafür liefert folgende Variante von main, die zu dem in Abbildung 3 gezeigten Schirmbild führt:

int

main(int argc, char* argv[])

{

...

LineDotter(0,0,150,50,20,10,10,2,LCD_COLOR_CYAN);

BSP_LCD_DisplayOn();

}

Abb. 3: Wo ist der zweite Abstand?

Ab einem gewissen Steigungsgrad werden kleine Abstände kleiner als 1 px: Sie fallen bei der Darstellung unter den Tisch. Dieses Problem lässt sich durch eine Deckelung der Untergrenze der xpers– und ypers-Variable beheben – ein Verfahren, das die „mathematische Genauigkeit“ des Zeichenprozesses verschlechtert.

Aufmerksame Beobachter stellen bei genauerer Betrachtung von LineDotter fest, dass die Farbe der Liniensegmente durch zwei Konstanten festgelegt wird. Diese Vorgehensweise ist beim Debugging sinnvoll: Scheuen Sie sich nicht, eine Routine während der Entwicklung mit „Signalfarben“ arbeiten zu lassen. Im Fall von LineDotter lässt sich das Verhalten des Zustandsautomaten anhand der Betrachtung der Farben ermitteln – langwierige Umwege in den Debugger entfallen.

Linie, aber schnell

Komplett horizontale und/oder vertikale Linien sind ein Sonderfall: Die Nutzung von Bresenhams Algorithmus kann hier zugunsten einer einfachen Addition in den Hintergrund treten. Auf Mikroprozessoren mit leistungsfähigem Speichersubsystem gibt es eine noch schnellere Möglichkeit, die eine als DMA-Controller bezeichnete Funktion ausnutzt. Im Fall der von STM mitgelieferten Bibliothek liegt die diesbezügliche Logik in zwei Methoden, die nach einer in beiden Fällen identischen Adressberechnung zur Aktivierung der FillBuffer-Methode übergehen (Listing 6).

void BSP_LCD_DrawHLine(uint16_t Xpos, uint16_t Ypos, uint16_t Length)
{
  uint32_t xaddress = (LtdcHandler.LayerCfg[ActiveLayer].FBStartAdress) + 4*(BSP_LCD_GetXSize()*Ypos + Xpos);

  FillBuffer(ActiveLayer, (uint32_t *)xaddress, Length, 1, 0, DrawProp[ActiveLayer].TextColor);
}

void BSP_LCD_DrawVLine(. . .)
{
  . . .
  
  FillBuffer(ActiveLayer, (uint32_t *)xaddress, 1, Length, (BSP_LCD_GetXSize() - 1), DrawProp[ActiveLayer].TextColor);
}

Der DMA-Controller des im STM32F429 verwendeten ARM-Cores ist sehr leistungsfähig: Er ist sogar zur Berücksichtigung von Offsets befähigt. Die hier aus Platzgründen nicht abgedruckte Methode FillBuffer nutzt dies aus. Die gewünschte Farbe wandert ins Quellregister, dessen Inhalt von der Speicherzugriffslogik sodann automatisch an die für das Zeichnen der Linie erforderlichen Adressen kopiert wird.

DMA-Controller gelten traditionsgemäß als schwierig zu programmieren: Die Routinen für vertikale und horizontale Linien sind zudem nur dann nützlich, wenn sie von den Nutzern auch wirklich aufgerufen werden. Das manuelle Prüfen von Start- und Endkoordinaten in der normalen DrawLine-Methode wäre eine Methode zur Beschleunigung, die aber auf Kosten der Geschwindigkeit „normaler“ Linien geht – in der Praxis ist ein Profiling Run hilfreich, der Informationen über die optimale Vorgehensweise liefert.

Algorithmus fällt vom Himmel
In der Computergrafik trifft man immer wieder auf Algorithmen, deren Verständnis einige Zeit in Anspruch nimmt. Die praktische Erfahrung des Autors lehrt, dass sich tiefergehende Beschäftigung mit der dahinterstehenden Mathematik nur in den seltensten Fällen lohnt – insbesondere dann nicht, wenn die Implementierung funktioniert.

Mach den Kreis

Als nächste Aufgabe wollen wir uns der Realisierung von Kreisen zuwenden. Das naive Programmieren eines Kreises anhand trigonometrischer Formeln ist zum Scheitern verurteilt: Floating-Point-Berechnungen zählen in der Welt der Mikroprozessoren zu den langsamsten Operationen. Ein erster Weg zur Beschleunigung der Arbeit besteht darin, das zu zeichnende Objekt auf Symmetrien zu untersuchen. Im Fall eines Kreises gibt es acht: Abbildung 4 zeigt, welche.

Abb. 4: Kreise sind extrem symmetrisch

Das Zeichnen von Kreissegmenten lässt sich mit einem an Bresenham angelehnten Verfahren realisieren. ST Microelectronics liefert in der HAL eine sehr übersichtliche Implementierung aus, wie in Listing 7 zu sehen.

void BSP_LCD_DrawCircle(uint16_t Xpos, uint16_t Ypos, uint16_t Radius)
{
  int32_t  d;/* Decision Variable */ 
  uint32_t  curx;/* Current X Value */
  uint32_t  cury;/* Current Y Value */ 
  
  d = 3 - (Radius >> 1);
  curx = 0;
  cury = Radius;
  
  while (curx >= cury)
  {
    BSP_LCD_DrawPixel((Xpos + curx), (Ypos - cury), DrawProp[ActiveLayer].TextColor);
    BSP_LCD_DrawPixel((Xpos - curx), (Ypos - cury), DrawProp[ActiveLayer].TextColor);
    BSP_LCD_DrawPixel((Xpos + cury), (Ypos - curx), DrawProp[ActiveLayer].TextColor);
    BSP_LCD_DrawPixel((Xpos - cury), (Ypos - curx), DrawProp[ActiveLayer].TextColor);
    BSP_LCD_DrawPixel((Xpos + curx), (Ypos + cury), DrawProp[ActiveLayer].TextColor);
    BSP_LCD_DrawPixel((Xpos - curx), (Ypos + cury), DrawProp[ActiveLayer].TextColor);
    BSP_LCD_DrawPixel((Xpos + cury), (Ypos + curx), DrawProp[ActiveLayer].TextColor);
    BSP_LCD_DrawPixel((Xpos - cury), (Ypos + curx), DrawProp[ActiveLayer].TextColor);   

    if (d > 0)
    { 
      d += (curx >> 2) + 6;
    }
    else
    {
      d += ((curx - cury) >> 2) + 10;
      cury--;
    }
    curx++;
  } 
}

Das MIT bietet eine Seite mit genauen mathematischen Erklärungen und einer noch stärker optimierten Version des Kreiszeichners an – wer in seiner Applikation häufiger Kreise zeichnet, sollte dem Code eine Chance geben.

Lass uns malen!

In der Anfangszeit der Computertechnik lagen Schriftarten als Bitmaps vor: Die in Windows ab 3.1 ausgelieferten TrueType-Schriftarten waren Vektordaten, die erst bei der Darstellung in Bitmaps umgewandelt wurden. Auch wenn es mit Freetype eine auf (leistungsstarken) MCUs lauffähige und quelloffene Variante eines TrueType-Renderers gibt, sind Entwickler von Embedded-Systemen im Allgemeinen mit vorkonvertierten Schriftarten besser bedient.

Unter folgender URL steht das Produkt GLCD Font Creator zum Download bereit, das sich im Bereich der Konverter zu einer Art Goldstandard entwickelt hat. Installieren Sie die Software auf ihrer Workstation und klicken Sie auf File | New Fojnt | Import an Existing System font. Wählen Sie im nächsten Schritt die gewünschte Schriftart samt Größe aus und geben Sie danach den zu konvertierenden Zeichenbereich an. Die von Haus aus voreingestellte Zone von 32 bis 127 deckt die Bedürfnisse des normalen ASCII-Codes ab. Klicken Sie danach auf Export for GLCD, um einen Dialog mit dem in Pascal, C und Basic angebotenen Code zu öffnen. Das generierte Array wandert per Cut and Paste in Richtung des Eclipse-Projekts – falls Sie Comic Sans MS verwendet haben, sehen die ersten Zeilen des Felds so aus:

const unsigned short Comic_Sans_MS12x16[] = {

0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Code for char

0x02, 0x00, 0x00, 0xFC, 0x0B, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Code for char !

GLCD Font Creator kommt auch mit Schriftarten zurecht, bei denen die einzelnen Zeichen verschiedene Abstände aufweisen. In diesem Fall – Comic Sans MS gehört zu dieser Gruppe – gibt das erste Byte die Breite des Zeichens in Pixeln an. Die eigentlichen Fontdaten folgen danach. Für unseren Font sind pro Spalte zwei Bytes erforderlich, die die zu setzenden und zu löschenden Bits beschreiben.

Die von GLCD Font Creator generierten Arrays sind statischer Natur. Dies ist für Entwickler insofern erfreulich, als sie auf den meisten MCUs in den Programmspeicher kopiert werden können und so kein (oder nur sehr wenig) RAM verbrauchen. Dabei sollten Lizenzbedingungen beachtet werden. Das „Einfach-so-ausliefern“ von auf der Workstation des Entwicklers befindlichen Fonts ist ein sicheres Rezept für Rechtsstreitigkeiten. Achten Sie darauf, dass der Anbieter der Schriftart das Weiterverbreiten des Fonts explizit erlaubt.

Zeichnen, zur Zweiten

Nach der Abklärung dieser Fakten beginnen wir mit der Realisierung der Zeichenroutine. Sie nimmt neben dem anzuzeigenden String (nullterminiert!) auch ein Koordinatenpaar und eine Wunschfarbe entgegen – die eigentliche Abarbeitung beginnt mit einer While-Schleife, die alle angelieferten Zeichen nacheinander abarbeitet:

 

void DrawWithGLCDFont(char what[],int _x1, int _y1, long int color)
{
  int currentX=_x1;
  int i=0;

  while(what[i]!=0)
  {

GLCD Font Creator generiert eindimensionale Arrays: Jedes Zeichen ist im Fall der hier verwendeten Bitmapvariante von Comic Sans MS 25 Bytes breit. Da unser Font auf den Zeichenbereich 32 bis 127 beschränkt ist, lässt sich der Beginn der Daten der darzustellenden Glyphe aus der Formel (what[i]-32)*25 ableiten.

Das erste an dieser Stelle liegende Byte ist der weiter oben besprochene Längenindikator, der beim Zeichnen keine Rolle spielt und somit durch eine ++-Operation entfernt wird:

 

long int indexInArray=(what[i]-32)*25;

//Wegoptimieren blockieren zwecks einfacherem Debugging

volatile char numOfFieldsToProcess=Comic_Sans_MS12x16[indexInArray]*2;

indexInArray++; //LEN Indikator nach Astoria

Für das eigentliche Zeichnen der Glyphen durchlaufen wir das Datenarray Schritt für Schritt: Dank der Sprungweite von 2 können wir die X-Koordinate bequem bei jedem Durchlauf der äußeren For-Schleife inkrementieren.

Im nächsten Schritt sammeln wir die zu setzenden und zu löschenden Bits in der Variable wordAtHand. Die <<-Operatoren repräsentieren einen Schiebebefehl, der den Inhalt der Variablen und der Konstante im Register um die angegebene Anzahl von Plätzen „weiterschiebt“. Zu guter Letzt durchlaufen wir den Inhalt von wordAtHand und setzen die angewiesenen Bits abwechselnd. Als „Diskriminator“ dient dabei eine immer weiter nach rechts geschobene 1, die per And-Operator mit dem Inhalt von wordAtHand verbunden wird und so entweder Null oder einen beliebigen Wert größer oder gleich 1 liefert (Listing 8).

 

  for(int j=0;j<numOfFieldsToProcess;j+=2)

  {

     short int wordAtHand=((Comic_Sans_MS12x16[indexInArray+j+1])<<8) + Comic_Sans_MS12x16[indexInArray+j];

     for(int y=0;y<=15;y++)

     {

       if(!((wordAtHand)&(1<<y)))

        {

         BSP_LCD_DrawPixel(currentX, _y1+y,LCD_COLOR_DARKMAGENTA);

      }

     }

    currentX++;

   }

   i++;

   currentX++;
    
   }

}

Die soeben realisierte Zeichenmethode ist aus Performancegründen insofern suboptimal, als die Darstellung der Glyphen durch mehrfaches Aufrufen von setPixel erfolgt. Praktischer Code sollte auf die weiter oben erwähnte DMA Engine zurückgreifen, die den Transfer bei korrekter Parametrierung automatisch erledigt.

Dies erklärt, warum das Setzen der Textfarbe in den meisten Grafikstacks eine vom eigentlichen Zeichnen unabhängige Operation darstellt. DMA-Transfers kopieren Daten stupide von A nach B; es ist den Engines nicht möglich, die einzelnen Bits bzw. Bytes zu bearbeiten. Aus diesem Grund muss das API den zum Zeichnen verwendeten Glyphenbaum beim Setzen einer neuen Textfarbe neu generieren – nur dann steht er auf Abruf bereit.

Mit weniger Speicher …

STMs 32F429 bringt sehr viel Arbeitsspeicher mit. Es ist kein Problem, mehrere Kopien des Framebuffers gleichzeitig vorzuhalten. Auf kleineren MCUs ist dies mitunter nicht möglich: Die insbesondere in Maker-Kreisen weit verbreiteten AVRs müssen oft mit 2 oder 4 KB RAM auskommen. Das Anbinden eines externen Arbeitsspeichers ist dabei nur in den wenigsten Fällen sinnvoll. Ein externes RAM ist im Allgemeinen komplexer und teurer als die Aufrüstung der MCU (Stichwort Platinenlayout). Bei ausreichender Rechenleistung bietet sich die Methode der dynamischen Inhaltsgeneration an. Der Controller analysiert dabei die gerade aktuelle Speicheradresse und berechnet den benötigten Farbwert dynamisch. Dies lässt sich am einfachsten mit der im vorigen Abschnitt besprochenen Textzeichenmethode realisieren: Die Ermittlung der Farbwerte aus dem (im Codespeicher liegenden) Datenarray lässt sich mit geringem Aufwand zur Laufzeit realisieren.

Besonders attraktive Resultate entstehen durch Kombination der dynamischen Berechnung mit einem klassischen Framebuffer. Die MCU entscheidet dabei anhand der Adresse, wie die Farbinformationen entstehen – Abbildung 5 demonstriert die damit erreichbaren Effekte.

Abb. 5: Das Diagramm entsteht per Framebuffer, während die Legende als Text im Arbeitsspeicher liegt

Fazit

Grafik ist faszinierend: So ist der Erfolg des iPhones unter anderem auf die hohe Qualität der Benutzerschnittstelle zurückzuführen. Wer seine Hardware mit einem attraktiven GUI ausstattet und beim Display nicht auf flimmernde Billigware zurückgreift, ist der Konkurrenz um einen Schritt voraus. Die hier besprochenen Methoden stellen einen soliden Grundstock dar, der bei Bedarf rasch um weitere Tricks erweitert werden kann.

Achten Sie bei aller Freude an der Erzeugung eigener Grafikstacks allerdings immer darauf, dass eine Vielzahl von Beratungsunternehmen preiswerte Embedded-GUI-Stacks anbieten. Deren Nutzung macht aus ökonomischer Sicht Sinn: Die auf den ersten Blick einfach klingende Aufgabe zur Erstellung eines kleinen Window-Managers artet in der Praxis rasch in echte Arbeit aus.

Verwandte Themen:

Geschrieben von
Tam Hanna
Tam Hanna
Tam Hanna befasst sich seit der Zeit des Palm IIIc mit der Programmierung und Anwendung von Handcomputern. Er entwickelt Programme für diverse Plattformen, betreibt Onlinenewsdienste zum Thema und steht unter tamhan@tamoggemon.com für Fragen, Trainings und Vorträge gern zur Verfügung.
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.