In diesem Artikel stelle ich euch meine Bibliothek zum ILI9341-Displaycontroller vor. Warum dieser Controller? Nun es gibt beim Internetauktionshaus sehr viele Anbieter, die RGB-TFT-Displays zu sehr günstigen Preisen anbieten und diese haben des öfteren den ILI9341 als Displaycontroller verbaut. Damit diese Displays praktikabel werden ist es notwendig eine entsprechende Bibliothek zur Verfügung zu haben. Wer das Display nicht unbedingt innerhalb von einer Woche benötigt findet auf eBay problemlos Angebote für gerade einmal 6€ inklusive Versand. Und das für ein RGB-Display mit einer Auflösung von 320 \cdot 240 Pixeln.

Anschluss

Da die Ansteuerung des Displays über den SPI-Bus realisiert wird, beschränkt sich der Hardwareaufwand auf ein Minimum. Somit lohnt es sich noch nicht einmal einen Schaltplan zu zeichnen. Die nachfolgende Liste zeigt die notwendige Beschaltung:

  • \text{GND} / \text{V}_{\text{CC}} = 3\text{,}3\,\text{V} (keinesfalls höher!)
  • CS wird direkt mit einem IO-Pin des Controllers verbunden
  • Reset kann mit \text{V}_{\text{CC}} verbunden werden (oder wahlweise mit einem IO-Pin)
  • D/C wird direkt mit einem IO-Pin des Controllers verbunden
  • SDI (MOSI) dieser Pin kommt an den SPI-Ausgang eures Controllers
  • SCK dieser Pin kommt an den Clock-Ausgang des SPI-Interface eures Controllers
  • LED über einen Widerstand an \text{V}_{\text{CC}} oder schaltbar (Transistor) an einen IO-Pin (47…100 Ohm)
  • SDO (MISO) wird bei meinen Routinen nicht verwendet (offen lassen)

Ansteuerung

Wie schon eingangs erwähnt, wird das Display mit dem SPI-Bus betrieben. Es ist also notwendig, dass dieser entsprechend konfiguriert wird. Ich zeige hier eine beispielhafte Ansteuerung anhand eines PIC18F45K22. Das Beispiel bzw. die Beispielcodes sind jedoch leicht auf andere PIC-Typen übertragbar. Die gesamten Display-Routinen sind in C geschrieben und sollten somit leicht auf andere Controller-Familien übertragbar sein.

SPI-Interface

Die Beschreibung des SPI-Interfaces beim PIC18F45K22 beginnt im Datenblatt ab Seite 214. Der nachfolgende Programmcode initialisiert das SPI-Interface.

void initSPI(void)
{
    SSP1ADD = 1;
    // Clock = Fosc /(4*(SSPxADD+1)) und CKP = 0 (CLK idle is low)
    SSP1CON1 = 0b00001010;
    // Transmit occurs from idle to active (CLK))
    SSP1STATbits.CKE = 1;
    // SPI Modul nur einschalten wenn nötig
    SSP1CON1bits.SSPEN = 1;
}

Das SPI-Interface wird so konfiguriert, dass das Clock-Signal im Ruhezustand auf Low-Potential (GND) liegt und dass gültige Daten bei steigender Taktflanke an den Empfänger übertragen werden. Das ist sehr wichtig, da nur so der Displaycontroller die Daten korrekt empfangen kann, siehe Datenblatt des Controllers auf Seite 62. Des Weiteren muss sich für eine Frequenz des Taktsignals entschieden werden. In dem oben gezeigten Beispiel ist diese wie folgt gewählt:

\text{f}_{\text{CLK}} = \frac{\text{F}_{\text{osc}}}{4 \cdot (\text{SSPxADD}+1)}

Da ich das Display im Beispiel mit dem SPI1-Modul des PIC betreibe wird das ‘x’ in SSPxADD durch eine ‘1’ ersetzt. Dieses Register habe ich in der Zeile drüber mit dem Wert ‘1’ geladen. Somit ergibt sich folgende Formel zur Berechnung der Taktfrequenz des SPI-Taktes:

\text{f}_{\text{CLK}} = \frac{\text{F}_{\text{osc}}}{8}

Da die Frequenz von der Taktfrequenz des Controllers abhängig ist, müssen wir zunächst wissen wie dieser getaktet wird um die eigentliche Taktfrequenz für den SPI-Bus bestimmen zu können. Ich betreibe den PIC18F45K22 mit dem internen 16 MHz Oszillator. Zusätzlich habe ich die PLL eingeschaltet, die das Taktsignal nun noch einmal um Faktor 4 erhöht. Somit ergibt sich \text{f}_{\text{osc}} zu 64 MHz. Folglich beträgt die SPI-Taktfrequenz:

\text{f}_{\text{CLK}} = \frac{\text{F}_{\text{osc}}}{8} = \frac{64\,\text{MHz}}{8} = 8\,\text{MHz}

Zusätzlich zur Konfiguration wird noch eine Funktion benötigt, die schlussendlich Daten über das SPI-Interface aussenden kann. Der nachfolgende Code zeigt diese Funktion:

uint8_t sendSPI (uint8_t byte)
{
   SSPBUF = byte;
   while(!SSP1STATbits.BF);

   return SSPBUF;
}

Durch das Laden des auszusendenden Bytes in den Sendebuffer wird das SPI-Modul automatisch angetriggert. Es sendet nun den Inhalt des Buffers aus und setzt nach Beendigung das BF-Bit im SSPxSTAT-Register (hier x = 1). Obligatorisch wird der Buffer ausgelesen aber in dieser Anwendung nicht weiter betrachtet.

Display-Routinen

In diesem Kapitel werden nun die einzelnen Funktionen der Bibliothek beschrieben sowie Beispiele zur Anwendung gezeigt. Die aktuellste Version der Bibliothek ist in meinem Github Repository zu finden (siehe Ordner ‘Source’). Die Bibliothek ist mit JavaDoc ausgestattet, so dass sich beim Arbeiten mit den Routinen unter MPLABX direkt kleine Hilfestellungen beim Programmieren angezeigt werden, siehe:

Anwendung

Nun werden die einzelnen Funktionen der Bibliothek besprochen. Dazu werden Beispiele zur Anwendung geliefert, die den Umgang mit den Funktionen vereinfachen sollen. Bevor jedoch mit der Besprechung der Routinen begonnen wird möchte ich noch eine Grafik zeigen, die die verwendete Orientierung des Displays klar machen soll.

Das rote Koordinatenkreuz zeigt den Ursprung, sprich Adresse (0|0). Somit liegt oben rechts die Adresse (319|239). Die X-Achse geht in die horizontale und die Y-Achse in die vertikale, so dass sich ein “Breitbild” als Bildschirmfläche ergibt. Der Displaycontroller führt einen automatischen Cursorvorschub in Y-Richtung durch, aus diesem Grund ist auch die Funktion zum Zeichnen einer vertikalen-Linie deutlich schneller als die für eine horizontale Linie, da bei letzterer jedesmal die Adresse neu gesetzt werden muss, während die vertikale-Linie einfach in einem Durchlauf gezeichnet werden kann.

lcd_init 

Initialisierung des Displays

Prototyp

void lcd_init (void);

lcd_send

Übertragen von Daten (Bytes) via SPI-Bus an den Displaycontroller. Anmerkung: Diese Funktion wird nur indirekt aufgerufen!

Prototyp

void lcd_send(bool dc, uint8_t value);

Parameter

  • dc – Wahl zwischen Register- (0) oder Dateninformationen (1)
  • value – Das zu übertragende Byte

lcd_set_cursor

Setzt den Displaycursor an eine bestimmte Adresse. Anmerkung: Diese Funktion wird i.d.R. nur indirekt aufgerufen!

Prototyp

uint8_t lcd_set_cursor(uint16_t x, uint16_t y);

Parameter

  • x – X-Koordinate
  • y – Y-Koordinate

Beispiel

// Platzieren des Cursors an (0,0)
lcd_set_cursor(0,0);

lcd_set_cursor_x

Setzt den X-Displaycursor an eine bestimmte Adresse. Anmerkung: Diese Funktion wird nur indirekt aufgerufen!

Prototyp

uint8_t lcd_set_cursor_x(uint16_t x);

Parameter

  • x – X-Koordinate

lcd_set_cursor_y

Setzt den Y-Displaycursor an eine bestimmte Adresse. Anmerkung: Diese Funktion wird nur indirekt aufgerufen!

Prototyp

uint8_t lcd_set_cursor_y(uint16_t y);

Parameter

  • x – Y-Koordinate

lcd_draw_pixel

Zeichnen eines Pixels an die aktuelle Cursor-Position. Anmerkung: Diese Funktion wird i.d.R. nur indirekt aufgerufen!

Prototyp

uint8_t lcd_draw_pixel(uint16_t color);

Parameter

  • color – Vordergrundfarbe des zu zeichnenden Pixels

lcd_fill

Füllen des gesamten Displayinhaltes mit einer bestimmten Farbe. Anmerkung: Diese Funktion wird z.B. bei der Initialisierung verwendet.

Prototyp

void lcd_fill(uint16_t bg_color);

Parameter

  • color – Farbe für den Displayinhalt

Beispiel

// Fuellen des gesamten Displays mit schwarz
lcd_fill(BLACK);

lcd_draw_line

Zeichnen einer Linie beliebiger Orientierung.

Prototyp

void lcd_draw_line(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color);

Parameter

  • x0 – X-Koordinate des Startpunktes
  • y0 – Y-Koordinate des Startpunktes
  • x1 – X-Koordinate des Endpunktes
  • y1 – Y-Koordinate des Endpunktes
  • color – Vordergrundfarbe der zu zeichnenden Linie

Beispiel

// Zeichnen einer gruenen Linie von (0|0) zu (10|10)
lcd_draw_line(0,0,10,10,GREEN);

lcd_draw_ver_line

Zeichnen einer vertikalen Linie. Diese Funktion bietet einen Geschwindigkeitsvorteil im Vergleich zu lcd_draw_line.

Prototyp

void lcd_draw_ver_line(uint16_t x, uint16_t y0, uint16_t y1, uint16_t color);

Parameter

  • x – X-Koordinate der Linie
  • y0 – Y-Koordinate des Startpunktes
  • y1 – Y-Koordinate des Endpunktes
  • color – Vordergrundfarbe der zu zeichnenden Linie

Beispiel

// Zeichnen einer weißen Linie von (50|0) zu (50|10)
lcd_draw_ver_line(50,0,10,WHITE);

lcd_draw_hor_line

Zeichnen einer horizontalen Linie. Diese Funktion bietet einen Geschwindigkeitsvorteil im Vergleich zu lcd_draw_line.

Prototyp

void lcd_draw_hor_line(uint16_t y, uint16_t x0, uint16_t x1, uint16_t color);

Parameter

  • y – Y-Koordinate der Linie
  • x0 – X-Koordinate des Startpunktes
  • x1 – X-Koordinate des Endpunktes
  • color – Vordergrundfarbe der zu zeichnenden Linie

Beispiel

// Zeichnen einer weißen Linie von (0|100) zu (10|100)
lcd_draw_hor_line(100,0,10,WHITE);

lcd_draw_pixel_at

Zeichnen eines Pixels an eine bestimmte Cursor-Position.

Prototyp

void lcd_draw_pixel_at(uint16_t x, uint16_t y, uint16_t color);

Parameter

  • x – X-Koordinate des Pixels
  • y – Y-Koordinate des Pixels
  • color – Vordergrundfarbe des zu zeichnenden Pixels

Beispiel

// Zeichnen eines blauen Pixels an (10|10)
lcd_draw_pixel_at(10,10,BLUE);

lcd_fill_rect

Zeichnen eines ausgefüllten Rechteckes beliebiger Größe.

Prototyp

void lcd_fill_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color);

Parameter

  • x0 – X-Koordinate des Startpunktes
  • y0 – Y-Koordinate des Startpunktes
  • x1 – X-Koordinate des Endpunktes
  • y1 – Y-Koordinate des Endpunktes
  • color – Vordergrundfarbe des Rechteckes

Beispiel

// Zeichnen eines roten ausgefuellten Rechtecks von (10|10) zu (20|20)
lcd_fill_rect(10,10,20,20,RED);

lcd_draw_rect

Zeichnen eines nicht ausgefüllten Rechteckes beliebiger Größe.

Prototyp

void lcd_draw_rect(uint16_t x0, uint16_t y0, uint16_t x1, uint16_t y1, uint16_t color);

Parameter

  • x0 – X-Koordinate des Startpunktes
  • y0 – Y-Koordinate des Startpunktes
  • x1 – X-Koordinate des Endpunktes
  • y1 – Y-Koordinate des Endpunktes
  • color – Vordergrundfarbe des Rechteckes

Beispiel

// Zeichnen eines roten Rechtecks von (10|10) zu (20|20)
lcd_draw_rect(10,10,20,20,RED);

lcd_draw_circle

Zeichnen eines Kreises mit Mittelpunkt und Radius.

Prototyp

void lcd_draw_circle(int16_t xm, int16_t ym, int16_t r, uint16_t color);

Parameter

  • xm – X-Koordinate des Mittelpunktes
  • ym – Y-Koordinate des Mittelpunktes
  • r – Radius des Kreises
  • color – Vordergrundfarbe des Kreises

Beispiel

// Zeichnen eines grauen Kreises an (10|10) mit Radius 4
lcd_draw_circle(10,10,4,GREY);

lcd_draw_filled_circle 

Zeichnen eines ausgefüllten Kreises mit Mittelpunkt und Radius.

Prototyp

void lcd_draw_filled_circle (uint16_t xm, uint16_t ym, uint8_t r, uint16_t color);

Parameter

  • xm – X-Koordinate des Mittelpunktes
  • ym – Y-Koordinate des Mittelpunktes
  • r – Radius des Kreises
  • color – Vordergrundfarbe des Kreises

Beispiel

// Zeichnen eines ausgefuellten grauen Kreises an (10|10) mit Radius 4
lcd_draw_filled_circle(10,10,4,GREY);

lcd_draw_char 

Zeichnen eines Zeichens auf dem Display. Hinweis: Diese Funktion wird nur indirekt aufgerufen!

Prototyp

void lcd_draw_char (uint16_t x, uint16_t y, uint16_t fIndex, uint16_t fg_color, uint16_t bg_color);

Parameter

  • x – X-Koordinate (unten links des Zeichens)
  • y – Y-Koordinate (unten links des Zeichens)
  • fIndex – Index auf das Zeichen im font-Vektor
  • fg_color – Vordergrundfarbe des Zeichens
  • bg_color – Hintergrundfarbe des Zeichens

lcd_draw_string 

Zeichnen einer Zeichenkette auf dem Display. Hinweis: Der zugrunde liegende Font hat eine variable Zeichenbreite! Jeweils der erste Eintrag eines Zeichens gibt die Breite des zugehörigen Zeichens an.

Prototyp

void lcd_draw_string (uint16_t x, uint16_t y, const char *pS, uint16_t fg_color, uint16_t bg_color);

Parameter

  • x – X-Koordinate (unten links des Zeichens)
  • y – Y-Koordinate (unten links des Zeichens)
  • pS – Zeiger auf die Zeichenkette
  • fg_color – Vordergrundfarbe des Zeichens
  • bg_color – Hintergrundfarbe des Zeichens

Beispiel

// Text "Hallo Welt" blau auf weiß an (0|0)
lcd_draw_string(0,0,"Hallo Welt",WHITE,BLUE);

Vollständiges Anwendungsbeispiel

Der Download beinhaltet ein vollständiges Anwendungsbeispiel (MPLABX-Projekt). Bei diesem wurde ein PIC18F45K22 eingesetzt. Als Taktgeber wird der interne Oszillator mit 16 MHz verwendet. Zusätzlich ist die PLL eingeschaltet, so dass sich \text{F}_{\text{osc}} zu 64 MHz ergibt. Der SPI-Takt ist auf 8 MHz eingestellt. Die Pin-Belegung (Display) ist in der Datei lcd.h definiert. Zur Ansteuerung wurde das SPI1-Modul verwendet. Bitte beachten: Das Anwendungsbeispiel (Download) kann unter Umständen eine ältere Version der Routinen, im Vergleich zu denen im Github Repository, enthalten.

10 Responses

Leave a Comment