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 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:
/
(keinesfalls höher!)
- CS wird direkt mit einem IO-Pin des Controllers verbunden
- Reset kann mit
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
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:
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:
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 zu 64 MHz. Folglich beträgt die SPI-Taktfrequenz:
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 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.
Ich betreibe gerade so ein ähnliches Display , aber mit ST7735 Controller (dieser Controller ist fast identisch).
Angesteuert wird das Display von einem PIC18F25K22, der auf 64MHz läuft (16MHz interner OC + 4xPLL).
Wobei ich dann die SPI Frequenz auf 16MHZ einstelle, also FOSC/4. Dabei gibt es zu beachten das man dann das SLRCONbits.SRLC auf 0 setzen muss! Interessanterweise wird im Datenblatt bei der SPI Schnittstelle nix dazu gesagt. Auch die Frequenz an der SPI Schnittstelle ist laut Datenblatt nicht begrenzt. Man findet nur eine kurze Erklärung dazu am Ende des Kapitels I/O. Auch kann man im SPI MODUS 0,0 als Master die Daten so schnell rausgeben, wie man das SSPBUFF beschreibt. Funktioniert in der Praxis leider nicht.
Meine Funktion sieht so aus:
void spi_out(uint8_t c)
{
PIR1bits.SSPIF=0; // Flag löschen
SSP1BUF=c;
while(PIR1bits.SSPIF==0 ); // Flag wird gesetzt, wenn Transfer fertig
}
Moin,
also dein Problem ist, dass deine Daten nicht rausgetaktet werden, wenn Du den Bus zu schnell betreibst? Hast Du mal versucht auf SSP1STATbits.BF statt auf PIR1bits.SSPIF zu prüfen? Wichtig ist dann, das SSPBUF Register auszulesen (auch wenn der Inhalt nicht verwendet wird). Das stellt sicher, dass das Flag gelöscht wird.
Gruß, Nico
Danke für den Vorschlag,
Aber !!!
Das habe ich schon Versucht mit Prüfen des SSP1STATbits.BF.
Und dann wieder den Wert vom SSP1BUF mit Return zurückgeben.
Dann kommt aber nur noch MÜLL am TFT an, weil die Daten nicht sauber raus geschickt werden.
Die Pixel werden dann Bunt und nicht Weiß , wie ich es an das TFT schicke.
Versuche noch ein wenig diese SPI Schnittstelle zu optimieren, damit auch bei nur 16MHz und dann mit 4MHz SPI, das nicht allzu lange dauert um das TFT zu beschreiben.
Rein Theoretisch müssten ja bei 4MHZ SPI Takt 250 Bytes pro Millisekunde aus dem PIC kommen ?
Weil ja 4MHz / 2 (HIGH,LOW) / 8bit / 1000(1Sekunde=1000ms) = 250 ! oder ?
Wenn Du den SPI-Bus mit 4 MHz taktest, dann werden 4 Mbit/s übertragen oder 500 kByte/s. Da wird nichts durch 2 geteilt. Bei jeder steigenden oder fallenden Flanke wird ein Bit raus geschrieben. Und die Flanken tauchen eben mit (am Beispiel) 4 MHz auf.
Ich würde sagen, dass wir uns, wenn gewünscht im Forum weiter unterhalten. Die Kommentarfunktion ist hier an der Stelle etwas ungeeignet um so ins Detail zu gehen
Dann werde ich im Forum mal eine Neues Thema mal eröffnen. Wo wir dann mal ins Detail gehen.
Ein kompletter fillScreen dauert bei mir nämlich 250ms. Was aber nicht sein kann bei 4MHz SPI ???
MFG
Ja das ist besser. Wie hast du die Zeit gemessen? Also das Display hat 230 x 320 = 76800 Pixel mit je 6,16 oder 18 Bit (Rgb). Bei 16 Bit hat man also 76800 x 16 = 1228800 Bit für ein Screen. Bei 4 MHz Takt also 4 Millionen Bit pro Sekunde macht das dann ca 300 ms
Tolles Projekt!
Mich würde interessieren wie man zu d. Font kommt. Gibt es einen Fontgenerator?
Grüße
Paul
Hallo Paul,
schau mal hier im Forum nach, da geht/ging es genau darum.
Gruß Nico