niedziela, 29 września 2019

ILI9341 - kolorowy wyświetlacz w rozmiarze 2.8"

W moich zbiorach pojawił się wyświetlacz ILI9341 w rozmiarze 2.8 ". Przygodę z kolorowym LCD zaczynałam od wymiaru 1.8 ". Człowiekowi zawsze mało i w imię tej zasady wspinam się z rozmiarem powoli w górę pomimo, że kompleksów żadnych nie mam :) . Kolejnym  wyświetlaczem jaki będę poznawał będzie zapewne w wymiarze 3.5 ". W zasadzie bibliotekę dla PIC32MM i ATSAM już poczyniłem dla mniejszego brata 2.2 ". Biblioteka powinna działać zatem i na 2.8 " bez żadnych zmian. Dodatkowo wyświetlacz 2.8" obsługuje dotyk ale to temat na oddzielny artykuł. W tym artykule chciałbym pokazać podstawy sterowania wyświetlaczem LCD czyli jak w ogóle zacząć cokolwiek wyświetlać na LCD z rodziny ILIxxxx.


Rewers wyświetlacza jaki posiadam wygląda jak poniżej :


A poniżej opis jego wyprowadzeń :



Podstawowe parametry wyświetlacza :


Type: touch panel
Size:2.8" 
Serial SPI
Display area:6.5(W) x 4.5(H)cm/2.55 x 1.77''
Driver element: TFT active matrix
Pixel arrangement: RGB vertical stripe
Driver IC: ILI9341
Backlight: White LED
Viewing Direction: 6 o'clock
Color Depth: 262K/65K
Resolution (dots): 240*320 Dots
5V compatible, use with 3.3V or 5V logic
Need at least 4 IOs from your MCU

Dokumentacja dla wyświetlacza jest uniwersalna i obejmuje chyba wszystkie modele LCD wyprodukowane w China Company :) Chińczyk zaoszczędził na papierze :). Nie zdziwmy się zatem widząc w dokumentacji obrazek całkiem innego LCD niż nabyliśmy. Ale na szczęście adresy rejestrów i cała reszta pi razy oko się zgadza.

Wyświetlacze tego typu określane są jako  LCD z  buforem ramki. Są to najprostsze LCD . Ich zaletą jest niska cena , wadą mała szybkość. Cały ciężar tworzenia obrazu jest tutaj po stronie MCU. Spotykane interfejsy wymiany danych to SPI, 8-bit i 16-bit. Powiedzmy sobie szczerze SPI jest w przypadku LCD o większych rozdzielczościach za wolne. Transfer jaki możemy uzyskać w tego typu LCD przy bardzo sprzyjających wiatrach to ok. 2 MB/s w praktyce uzyskujemy ok 1MB/s. Maksymalna prędkość zegara SPI w zapisie danych jaki możemy zastosować w przypadku ILI9341 to 10MHz

Przyjrzyjmy się jak wygląda komunikacja z wyświetlaczem LCD. Posłużymy się wykresem z chińskiego datasheet :


Na bazie tego wykresu możemy wydumać jak wynika komunikacja z LCD. Przekonwertujmy najpierw pinologię, SDA z wykresu to u nas SDI, SCL to SCK , D/CX to u nas DC i CSX to CS.

Struktura przesyłanych danych w uproszczeniu wygląda następująco ,najpierw wysyłamy komendę/polecenie potem dane. Podczas wysyłania komendy pin DC musi być w stanie niskim a podczas wysyłania danych w stanie wysokim. W obu przypadkach pin CS musi być w stanie niskim. Dodatkowo widzimy, że pomiędzy wysłaniem komendy/polecenia a danymi pojawia się zwłoka realizowana na  pinie CS w postaci stanu wysokiego. Z praktyki jednak wiem, że pin CS możemy na sztywno połączyć do masy. Zwłoka powstanie nam automatycznie w procesie realizacji komunikacji SPI.

Na razie nie wiemy nic o dostępnych komendach ale już na tym etapie możemy napisać funkcję realizującą wysłanie komendy i będzie ona bardzo prosta.

// Write a command to Display
void sendCmd(uint8_t data){
   DC_ON;       
//Set the DC pin to low
   SPI_send(data); 
   DC_OFF; 
//Set the DC pin to high
   }

Możemy również napisać funkcję do wysłania danych. Mając na uwadze, że dane reprezentowane są jako 16 bitowe, wysyłamy je dzieląc na starszy i młodszy bajt. Realizuje nam to poniższa funkcja :

// Write a data to Display
void sendData16(uint16_t data){
    unsigned char data1 = data>>8;
    unsigned char data2 = data&0xff;
    DC_OFF;
//Set the DC pin to high
    SPI_send(data1);
    SPI_send(data2);
}

Mając powyższe dwie funkcję możemy już rozmawiać z naszym LCD. Ale musimy wiedzieć jeszcze o czym rozmawiać. Rozmowa z LCD sprowadza się w uproszczeniu na dwóch zdaniach. Ustaw współrzędne piksela ,który chcesz zmienić, zmień ten piksel na wybrany kolor. Te dwa zdania muszą być ubrane w odpowiednie komendy i dane.
Teraz pora na grzebanie w dokumentacji czyli chińskim datasheet. Ból głowy murowany ale ostatecznie nie jest tak, źle. Próbujemy się dowiedzieć jakie numerki komend odpowiadają za ustawienie współrzędnych piksela  X i Y. Poszukiwania zaczynamy od spisu treści w którym znajdujemy rozdział nr 8 Command. Tam widzimy w sumie ciurkiem trzy komendy , które intuicyjnie podejrzewamy, że się nam przydadzą , poniżej wycinek ze spisu treści :

8.2.20. Column Address Set (2Ah) ........................................................ 110
8.2.21. Page Address Set (2Bh)............................................................. 112
8.2.22. Memory Write (2Ch) ................................................................... 114

Komenda Column Address Set  to nasz poszukiwany X, a Page Address Set reprezentuje Y. Kolejne dwie komendy Memory Write i Color Set również się nam przydadzą. Najpierw zaglądamy do opisu Column Address Set :


Z powyższego opisu musimy wykontycypować jak reprezentowana jest komenda i dana a w zasadzie dane do ustawienia współrzędnej X. Z opisu wynika, że X występuje w parze tzn X0 - SC[15:0] i X1 - EC[15:0]. Gdzie SC to Start Columna a EC to End Column. Punkt X0 opisany jest słowem 16 bitowym rozbitym na dwa bajty SC7...SC0 i SC15...SC8 analogicznie X1. Po grzyba jednak zamiast jednego X-a trzeba podawać dwa parametry ? Domyślam się,   że podając np X0 = 0 a X1 = 5, możemy potem ciurkiem tylko nadawać kolor piksela a driver w LCD będzie nam automatycznie za każdym przesłaniem danej o kolorze inkrementował adres pamięci GRAM  w LCD i w efekcie będzie rysowana linia od X0 do X1. Czyli w naszym przypadku linia o długości 6 pikseli. Gdyby nie było takiej możliwości to byśmy musieli 6 razy podawać nowe współrzędne X co wydłużyłoby czas operacji rysowania od punktu X0 do X1. W opisie w dokumentacji jest inne tłumaczenie dla X0 i X1 ,mianowicie określamy tym zakres dostępnej dla MCU pamięci GRAM modułu (This command is used to define area of frame memory where MCU can access).
Jeśli jesteśmy zainteresowani mimo wszystko operować tylko jednym pikselem to X0=X1 i w takiej postaci dane musimy przesłać. Komenda informująca kontroler LCD o tym , że odwołujemy się do X-a to 0x2A

Zaglądamy do opisu kolejnej komendy czyli Page Address Set :


Komenda reprezentuje nasz Y i tak samo jak w przypadku X-a mamy tutaj Y0 - SP[15:0] i X1 - EP[15:0]. Gdzie SP to Start Page a EP to End Page. Wszytko co napisałem w przypadku komendy dla X odnosi się i dla Y. Kod komendy to 0x2B
W opisie obu powyższych komend znajdziemy schemat blokowy , który pokazuje nam jak wygląda przepływ danych dla ustawienia piksela/pikseli :


Najpierw wysyłamy komendę 0x2A , która jest przyporządkowana dla X-a. Po komendzie przesyłamy cztery bajty dwa pierwsze dla X0 a dwa kolejne dla X1. Jeśli odnosimy się do jednego piksela to X0=X1. Następnie wysyłamy komendę 0x2B dla ustawienia Y i cztery bajty analogicznie jak dla X-a. Potem pojawia nam się nieznana jeszcze komenda RAMWR o kodzie 0x2C po której przesyłamy dane 16-bitowe o kolorze piksela.

I to już koniec operacji na pikselu. Nie wygląda to strasznie a wręcz prosto. Zerknijmy na komendę  RAMWR :

 
Widzimy, że jest to komenda która inicjuje transfer do pamięci RAM czyli pamięci obrazu naszego LCD. Po tej komendzie dane jakie będą przesyłane trafią do pamięci obrazu.

Ogarnijmy powyższe rozważania w postaci przykładowego kodu . Poniżej funkcje ustawiające X i Y dla jednego piksela :


#define ILI9341_CMD_COLUMN_ADDRES_SET  0x2A
#define ILI9341_CMD_PAGE_ADDRESS_SET  0x2B
#define  ILI9341_CMD_MEMORY_WRITE  0x2C

void setX(uint16_t x0,uint16_t x1)
{
    sendCmd(ILI9341_CMD_COLUMN_ADDRESS_SET);
    sendData16(x0);
    sendData16(x1);
}

void setY(uint16_t y0,uint16_t y1)
{
    sendCmd(ILI9341_CMD_PAGE_ADDRESS_SET);
    sendData16(y0);
    sendData16(y1);
}

void setXY(uint16_t x0, uint16_t y0)
{
    setX(x0, x0);
    setY(y0, y0);
    sendCmd(ILI9341_CMD_MEMORY_WRITE);
}

void pixel(uint16_t x0, uint16_t y0, uint16_t color)
{
    if ((x0 > 239) || (y0 > 319)) return; // Nothing if out of the screen   
        setXY(x0, y0);
    sendData16(color); //send data color
}

Powyższe funkcje zrealizują nam ustawienie jednego piksela na ekranie LCD o wybranym kolorze.

Kolor kodujemy na 16 bitach. Taka reprezentacja koloru umożliwia nam przesłanie koloru z palety barw w ilości 65.536. Ale chyba kogoś porąbało kto jest w stanie tyle kolorów ogarnąć. Wyświetlacz potrafi kodować kolor maksymalnie na 18 bitach i wtedy mamy 262 tys kolorków ale to chyba jakieś nieporozumienie aby taką ilość kolorów był w stanie zaprezentować w sumie tani chiński wyświetlacz. My skupiamy się na wersji 16-bitowej bardziej realistycznej . Format jakim się tutaj posługujemy to RGB565, oznacza to, że kolor RED i BLUE są reprezentowane przez skalę RGB na 5-ciu bitach a kolor GREEN na 6 bitach. A czemu kolor zielony został bardziej doceniony w tym formacie ? Ano podobno jesteśmy na ten kolor najbardziej wyczuleni. Stąd wniosek , że nasz organizm preferuje ten kolor :). Dlatego w dowód uznania kolor ten otrzymał 1 bit więcej. Fizycznie format wygląda jak poniżej :



Powyższe 16-bitów rozbijane jest na dwa pojedyńcze bajty i tak przesyłane do pamięci kontrolera LCD. Funkcja wysyłająca kolor/dane sendData16() przyjmuje jako argument całe 16 bitów w jednym kawałku ,które wewnętrznie rozbija na dwa pojedyńcze bajty.
W świecie obrazków króluje najczęściej format 24-bitowy kodowania kolorów, dlatego najczęściej będziemy dokonywać konwersji 24-bity (RGB888) na 16-bity (RGB565) służy do tego poniższa definicja :

#define RGB565(r,g,b) (((uint16_t)r & 0x00F8) << 8) | (((uint16_t)g & 0x00FC) << 3) | (((uint16_t)b & 0x00F8) >> 3)

używamy tego tak :

#define WHITE                   RGB565(255, 255, 255)
#define BLACK                  RGB565(  0,   0,   0)
#define YELLOW               RGB565(255, 255,   0)

Jaj będzie zatem wyglądać wyświetlenie jednego piksela np o współrzędnych X=50, Y=20 w kolorze  YELLOW za pomocą prezentowanej wyżej funkcji :


pixel(50, 20, YELLOW) ;

Teraz zajmiemy się inicjalizacją wyświetlacza ILI9341. W necie jest dostępny dokument pod tym linkiem w którym są podane przykłady inicjalizacji. Szkoda tylko , że na stronie producenta firmy ILITEK nie ma tych dokumentów tylko są rozsiane po różnych dziwnych stronach.Typowa funkcja inicjalizacyjna jest nudna jak flaki z olejem, musimy w niej przesłać do LCD , sekwencje różnych dziwnych ustawień, które po rozszyfrowaniu stają cokolwiek bardziej lub mniej zrozumiałe. Myślę jednak, że szkoda życia na rozgryzanie ustawień np. związanych z gamma korekcją etc. Po prostu zastosować podane ustawienia i cieszyć się , że działa :) . Zwrócić tylko uwagę czy jesteśmy w 16-bitach czy 18-tu jeśli chodzi o kodowanie  kolorów. Nie będę tutaj rozszyfrowywał poszczególnych ustawień ale przedstawię dosyć ciekawe podejście do funkcji inicjalizującej opierającej się na niestandardowej definicji tablicy z danymi i poruszaniem się po niej za pomocą wskaźników. Pomysł zaczerpnąłem od programistów  z Silicon Labs. Przedstawiona koncepcja umożliwia wyraźne zaoszczędzenie  pamięci Flash w stosunku do standardowego podejścia , które także  pokażę.

Najpierw klasyczna postać funkcji inicjalizującej czyli tak jak to się robi standardowo :

void ili9341_init(void)
{
DC_OFF; // set pin 1
RESET_OFF ; // set pin 1
_delay_ms(1);
RESET_ON ; // set pin 0
_delay_ms(10);
RESET_OFF ; // set pin 1
_delay_ms(120);
   


sendCmd(ILI9341_CMD_POWER_ON_SEQ_CONTROL); //0xCB
sendData(0x39);
sendData(0x2C);
sendData(0x00);
sendData(0x34);
sendData(0x02);

sendCmd(ILI9341_CMD_POWER_CONTROL_B); //0xCF
sendData(0x00);
sendData(0xC1);
sendData(0x30);

sendCmd(ILI9341_CMD_DRIVER_TIMING_CONTROL_A); //0xE8
sendData(0x85);
sendData(0x00);
sendData(0x78);

sendCmd(ILI9341_CMD_DRIVER_TIMING_CONTROL_B); //0xEA
sendData(0x00);
sendData(0x00);

sendCmd(0xED);
sendData(0x64);
sendData(0x03);
sendData(0x12);
sendData(0X81);

sendCmd(ILI9341_CMD_PUMP_RATIO_CONTROL); //0xF7
sendData(0x20);

sendCmd(ILI9341_CMD_POWER_CONTROL_1); //0xC0
sendData(0x1B);

sendCmd(ILI9341_CMD_POWER_CONTROL_2); //0xC1
sendData(0x10);

sendCmd(ILI9341_CMD_VCOM_CONTROL_1); //0xC5
sendData(0x2D);
sendData(0x33);

sendCmd(ILI9341_CMD_VCOM_CONTROL_2); //0xC7
sendData(0xCF);

sendCmd(ILI9341_CMD_MEMORY_ACCESS_CONTROL); //0x36
sendData(0x48);

sendCmd(ILI9341_CMD_COLMOD_PIXEL_FORMAT_SET); //0x3A
sendData(0x55);

sendCmd(ILI9341_CMD_FRAME_RATE_CONTROL_NORMAL); //0xB1
sendData(0x00);
sendData(0x1D);

sendCmd(ILI9341_CMD_DISPLAY_FUNCTION_CONTROL); //0xB6
sendData(0x08);
sendData(0x82);
sendData(0x27);

sendCmd(ILI9341_CMD_ENABLE_3_GAMMA_CONTROL); //0xF2
sendData(0x00);

sendCmd(ILI9341_CMD_GAMMA_SET); //0x26
sendData(0x1);

sendCmd(ILI9341_CMD_POSITIVE_GAMMA_CORRECTION); //0xE0
sendData(0x0F);
sendData(0x31);
sendData(0x2B);
sendData(0x0C);
sendData(0x0E);
sendData(0x08);
sendData(0x4E);
sendData(0xF1);
sendData(0x37);
sendData(0x07);
sendData(0x10);
sendData(0x03);
sendData(0x0E);
sendData(0x09);
sendData(0x00);

sendCmd(ILI9341_CMD_NEGATIVE_GAMMA_CORRECTION); //0xE1
sendData(0x00);
sendData(0x0E);
sendData(0x14);
sendData(0x03);
sendData(0x11);
sendData(0x07);
sendData(0x31);
sendData(0xC1);
sendData(0x48);
sendData(0x08);
sendData(0x0F);
sendData(0x0C);
sendData(0x31);
sendData(0x36);
sendData(0x0F);

sendCmd(ILI9341_CMD_SLEEP_OUT); //0x11
_delay_ms(120);
sendCmd(ILI9341_CMD_DISPLAY_ON); //0x29

setOrientation(PORTRAIT); //0x00
bg(bgcolor); //BLACK
}

Teraz zobaczmy jak wygląda zajętość pamięci Flash z uaktywnioną powyższą funkcją :


Widzimy , że cały program zajmuje nam 9.2 KB pamięci Flash. Oczywiście to nie jest tylko sama obsługa LCD ,program robi jeszcze milion innych rzeczy ale to jest nieistotne. Zrobiliśmy tylko migawkę zajętości pamięci Flash z aktywną funkcją inicjalizacji. Migawka ta jest nam potrzebna aby pokazać jak zmieni się zajętość pamięci po zastosowaniu pewnej bardzo ciekawj konstrukcji w języku.C.

Teraz pokazuję inny bardzo ciekawy sposób inicjalizacji . Wszystkie dane przesyłane do LCD umieścimy w definicji tablicy ale uwaga !!! w definicji czyli fizycznie ta tablica w pamięci nie zaistnieje dopóki nie zostanie wykorzystana w funkcji inicjalizującej. Jak wygląda taka definicja pokazuje poniżej :

#define ILI9341_CONFIGURATION_DATA_ARRAY { \
        6, ILI9341_CMD_POWER_ON_SEQ_CONTROL, 0x39, 0x2C, 0x00, 0x34, 0x02, \
        4, ILI9341_CMD_POWER_CONTROL_B, 0x00, 0xC1, 0x30, \
        4, ILI9341_CMD_DRIVER_TIMING_CONTROL_A, 0x85, 0x00, 0x78, \
        3, ILI9341_CMD_DRIVER_TIMING_CONTROL_B, 0x00, 0x00, \
        5, 0xED, 0x64, 0x03, 0x12, 0x81, \
        2, ILI9341_CMD_PUMP_RATIO_CONTROL, 0x20 , \
        2, ILI9341_CMD_POWER_CONTROL_1, 0x1B, \
        2, ILI9341_CMD_POWER_CONTROL_2, 0x10, \
        3, ILI9341_CMD_VCOM_CONTROL_1, 0x2D, 0x33, \
        2, ILI9341_CMD_VCOM_CONTROL_2, 0xCF, \
        2, ILI9341_CMD_MEMORY_ACCESS_CONTROL, 0x48,\
        2, ILI9341_CMD_COLMOD_PIXEL_FORMAT_SET, 0x55, \
        3, ILI9341_CMD_FRAME_RATE_CONTROL_NORMAL, 0x00, 0x1D,\
        4, ILI9341_CMD_DISPLAY_FUNCTION_CONTROL, 0x08, 0x82, 0x27,\
        2, ILI9341_CMD_ENABLE_3_GAMMA_CONTROL, 0x00, \
        2, ILI9341_CMD_GAMMA_SET, 0x01, \
       16,ILI9341_CMD_POSITIVE_GAMMA_CORRECTION, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, 0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, 0x0E, 0x09, 0x00, \
      16,ILI9341_CMD_NEGATIVE_GAMMA_CORRECTION, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, 0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, 0x31, 0x36, 0x0F, \
        0x00 \
 }

Konstrukcja powyższa wygląda następująco najpierw podajemy cyfrę reprezentującą ilość wysyłanych bajtów potem jest kod komendy i potem ciurkiem dane. wiersze danych są przedzielone odwrotnymi sleashami. Ostatnim elementem tej definicji tablicy jest 0x00, które będzie nam potrzebne do wykrycia końca tablicy.

A poniżej funkcja inicjalizująca wykorzystująca powyższą definicje tablicy i za pomocą wskaźników jeździmy po niej we wszystkie możliwe strony :) :

void ili9341_init(void)
{
     uint8_t Lcd_Conf_Array[] = ILI9341_CONFIGURATION_DATA_ARRAY;
     uint8_t x=0;
     uint16_t place = 0;
   
    DC_OFF;
    RESET_OFF ;
    _delay_ms(1);
    RESET_ON ;
    _delay_ms(10);
    RESET_OFF ;
    _delay_ms(120);
           
    while( *(Lcd_Conf_Array + place) != 0x00 )
    {
        sendCmd(*(Lcd_Conf_Array + place + 1));
            for(x=0; x < *(Lcd_Conf_Array + place)-1 ; x++){
               
                sendData(*(Lcd_Conf_Array + place + 2 + x));
                }
             
        place += *(Lcd_Conf_Array + place) + 1;
    }
       
      sendCmd(ILI9341_CMD_SLEEP_OUT);
      _delay_ms(120);
      sendCmd(ILI9341_CMD_DISPLAY_ON);
      setOrientation(PORTRAIT);
      bg(bgcolor);
}



Cała magia wykorzystania definicji tablicy w funkcji inicjalizującej jest w pierwszym wierszu tej funkcji gdzie przypisujemy do powołanej tablicy Lcd_Conf_Array[] naszą definicje tablicy ILI9341_CONFIGURATION_DATA_ARRAY . Jaką korzyść otrzymujemy w tej konstrukcji ? przedstawia zrzut po kompilacji programu :


Jak widzimy teraz program zajmuje 9 KB czyli o ok 200 bajtów mniej w stosunku do standardowej funkcji inicjalizującej. Funkcja robi dokładnie to samo co standardowa funkcja czyli inicjalizuje LCD ale wykorzystuje dane zawarte w definicji tablicy.

Taka praktyczna mała uwaga dotycząca odczytania i zrozumienia zapisu za pomocą konstrukcji z użyciem wskaźnikia. Weźmy np sekwencję :

*(Lcd_Conf_Array + place + 1)

Analizę zapisów z użyciem wskaźnika zaczynamy zawsze od prawej skrajnej  strony. Czyli od 1 :). Jeden to jeden i wiemy co to oznacza po prostu 1 :), do 1 dodajemy wartość zmiennej place. Na początku zmienna ta ma wartość 0 więc na razie mamy ....0 + 1 . Docieramy wreszczcie do Lcd_Conf_Array . Wiemy , że jest to nazwa tablicy do której przypisaliśmy definicję tablicy z danymi. Sama nazwa tablicy oznacza adres na pierwszy jej element. Ale adres a nie wartość elementu tablicy. Innymi słowy Lcd_Conf_Array = &Lcd_Conf_Array[0]. Zatem mamy taką sumę (Adres Tablicy Lcd_Conf_Array + 0 + 1). Czyli suma będzie adresem drugiego elementu tablicy Lcd_Conf_Array. A  drugim elementem tablicy jest konkretnie komenda ILI9341_CMD_POWER_ON_SEQ_CONTROL (przy place = 0). Czyli zapis (Adres Tablicy Lcd_Conf_Array + 0 + 1) jest adresem tego drugiego elementu tablicy. Ostatnim elementem naszej analizy ku zrozumieniu zapisu opartego na wskaźniku jest dodanie znaczka "*". Ten magiczny znaczek  pobiera nam wartość zmiennej znajdującej się pod adresem określonym z prawej strony znaczka :) . Nasz adres z prawej strony to (Adres Tablicy Lcd_Conf_Array + 0 + 1). Czyli zapis *(Adres Tablicy Lcd_Conf_Array + 0 + 1) = ILI9341_CMD_POWER_ON_SEQ_CONTROL.

Wskaźniki to fundament jezyka C i są jego nieodłącznym atrybutem ale jednocześnie to największe przekleństwo tego języka . Analiza kodu opisanego na bogato wskaźnikami jest nielada wyzwaniem i rozszyfrowanie co poeta miał na myśli zajmie więcej czasu niż pisanie kodu bez użycia wskaźników.

ILI9341 w rozmiarze 2.8" jest już konkretnym wyświetlaczem . Możemy na nim prezentować wykresy zdjęcia, menu etc. Wszystko będzie z pewnością ładnie i czytelnie prezentowane. W porównaniu do rozmiaru 2.2" jest znacząca różnica . Wyświetlane treści są czytelne z większych odległości, nie trzeba zbliżać ślepiów aby odczytać tekst.



Pozdrawiam
picmajster.blog@gmail.com


Linki :

Brak komentarzy:

Prześlij komentarz