Wyświetlacz graficzny LCD z kontrolerem KS0108

 KS0108 Driver C Library 

Wyświetlacz graficzny LCD ze sterownikiem KS0107/KS0108 – sterowanie w języku C od podstaw.

W najróżniejszych urządzeniach zbudowanych w oparciu o mikrokontrolery jako urządzenie wyjściowe wykorzystywane są wyświetlacze LCD. Najczęściej są to wyświetlacze alfanumeryczne ze sterownikiem HD44780 ze względu na stosunkowo niską cenę oraz proste sterowanie. Jednak ich możliwości są niewielkie w porównaniu do wyświetlaczy graficznych. Najtańsze wyświetlacze graficzne zbudowane są w oparciu o sterownik KS0107/KS0108 (HD61202/HD61203) nie posiadający generatora znaków. Cena wyświetlacza o rozdzielczości 128x64 punktów jest zbliżona do trzykrotności ceny zwykłego wyświetlacza alfanumerycznego 16x2, przy wiele większych możliwościach prezentacji danych.

Wyświetlacz ten pozwala na wyświetlenie ośmiu linii po 21 znaków (typowa czcionka 5x7) co daje łączną liczbę 168 znaków. Brak wbudowanego generatora znaków wydaje się być poważną wadą wyświetlacza, jednak w prosty sposób można tą wadę wyeliminować poprzez programową generację znaków realizowaną przez docelowy mikrokontroler. Wszystkie niezbędne funkcje realizujące wyświetlanie tekstu wraz z tablicą znaków zapisaną w pamięci programu zajmują około 1KB pamięci.

Wyświetlacz JM12864A
Wszystkie prezentowane tutaj procedury zostały napisane dla wyświetlacza JM12864A, jednak powinny działać z innymi wyświetlaczami o rozdzielczości 128x64 piksele zawierającymi sterownik KS0108. Funkcje poszczególnych wyprowadzeń wyświetlacza JM12864A przedstawia poniższy rysunek :


 

Do sterowania wyświetlaczem wymagane jest 13 wyprowadzeń mikrokontrolera. Nie jest możliwa transmisja 4-bitowa, jak w przypadku wyświetlaczy alfanumerycznych ze sterownikiem HD44780.

Organizacja pamięci wyświetlacza JM12864A
Wyświetlacz posiada 1KB pamięci RAM (8192 bity). Organizacja pamięci i jej odwzorowanie na ekranie wyświetlacza przedstawia rysunek


Poszczególne adresy pamięci wyświetlacza nie odpowiadają typowym współrzędnym ekranu. Adres Y odpowiada współrzędnej x ekranu (oś pozioma) natomiast adres X określa numer strony (czyli jest podzieloną przez 8 współrzędną y ekranu). Adres Z określa, która linia pamięci będzie odwzorowana w najwyższej linii wyświetlacza. Zapis danych do pamięci odbywa się po 8 bitów, najmłodszy bit odpowiada pikselowi położonemu w linii najwyższej w obrębie danej strony a najstarszy bit odpowiada pikselowi położonemu w linii najniższej.
 

Rozkazy sterownika KS0107/0108
Sterownik KS0107/0108 posiada 7 podstawowych rozkazów :

1. Display On/Off



D = 0 – wyświetlanie zawartości pamięci RAM na ekranie wyłączone
D = 1 – wyświetlanie zawartości pamięci RAM na ekranie włączone
Instrukcja nie wpływa na zawartość pamięci RAM.

2. Set Address (Y address)



Ustawienie adresu Y wyświetlacza (czyli współrzędnej x ekranu) z zakresu 0-63. Adres jest automatycznie inkrementowany po każdej operacji odczytu, bądź zapisu danych.

3.Set Page (X address)



Ustawienie adresu X (wybór strony) wyświetlacza z zakresu 0-7. Wszelkie operacje zapisu i odczytu danych wykonywane są na bieżącej stronie do momentu wybrania nowej strony.

4. Display Start Line (Z address)



Adres Z określa która linia obrazu zawartego w pamięci RAM ma zostać wyświetlona jako pierwsza na ekranie.

5. Status read



Odczyt bajtu statusu kontrolera.
• BUSY
BUSY = 1 – kontroler wykonuje operację i nie przyjmuje kolejnych instrukcji
BUSY = 0 – kontroler jest gotowy do przyjęcia kolejnej instrukcji
• ON/OFF
ON/OFF = 1 – wyświetlacz jest włączony
ON/OFF = 1 – wyświetlacz jest wyłączony
• RESET
RESET = 1 – trwa inicjalizacja wyświetlacza, żadne instrukcje za wyjątkiem instrukcji odczytu bajtu statusu nie są akceptowane
RESET = 0 – inicjalizacja zakończona, kontroler w stanie normalnej pracy

6. Write Dislpay Data



Zapis danej do pamięci RAM wyświetlacza. Po wykonaniu instrukcji następuje automatyczna inkrementacja adresu Y.

7. Read Display Data



Odczyt danej z pamięci RAM wyświetlacza. Po wykonaniu instrukcji następuje automatyczna inkrementacja adresu Y.

Przykładowa bitmapa wyświetlona na wyświetlaczu JM12864A (sterownik KS0108)

atmel ks0108 lcd

 

Komunikacja mikrokontrolera z wyświetlaczem.
Interfejs wyświetlacza zajmuje 13 wyprowadzeń mikrokontrolera. Szyna danych zajmuje cały dowolnie wybrany port, natomiast sygnały sterujące zajmują 5 wyprowadzeń drugiego dowolnie wybranego portu. Przypisanie sygnałów sterujących poszczególnym wyprowadzeniom jest dowolne. Schemat połączeń zawarty jest w pliku schemat.pdf Napięcie ujemne konieczne do zasilania wyświetlacza wytwarzane jest przez układ MAX232 (w przypadku zastosowania wyświetlacza z wbudowanym generatorem napięcia ujemnego można ten układ pominąć).

W kodzie programu umieszczamy definicje odpowiednich sygnałów sterujących :

// port szyny danych
#define LCD_DATA_PORT PORTA
#define LCD_DATA_PIN PINA
#define LCD_DATA_DDR DDRA
// port sygnałów sterujących
#define LCD_CTRL_PORT PORTC
#define LCD_CTRL_PIN PINC
#define LCD_CTRL_DDR DDRC
// sygnały sterujące
#define LCD_CS1P PC6
#define LCD_CS2P PC5
#define LCD_EN PC0
#define LCD_RW PC1
#define LCD_RS PC2

Następnie zdefiniujmy kilka podstawowych makroinstrukcji kontrolujących stan sygnałów sterujących :

// makroinstrukcje ustawienia stanu na linii CS1
#define SET_CS1() (LCD_CTRL_PORT |= (1 << LCD_CS1P))
#define CLR_CS1() (LCD_CTRL_PORT &= ~(1 << LCD_CS1P))
// makroinstrukcje ustawienia stanu na linii CS2
#define SET_CS2() (LCD_CTRL_PORT |= (1 << LCD_CS2P))
#define CLR_CS2() (LCD_CTRL_PORT &= ~(1 << LCD_CS2P))
// makroinstrukcje ustawienia stanu na linii EN
#define SET_EN() (LCD_CTRL_PORT |= (1 << LCD_EN))
#define CLR_EN() (LCD_CTRL_PORT &= ~(1 << LCD_EN))
// makroinstrukcje ustawienia stanu na linii RW
#define SET_RW() (LCD_CTRL_PORT |= (1 << LCD_RW))
#define CLR_RW() (LCD_CTRL_PORT &= ~(1 << LCD_RW))
// makroinstrukcje ustawienia stanu na linii RS
#define SET_RS() (LCD_CTRL_PORT |= (1 << LCD_RS))
#define CLR_RS() (LCD_CTRL_PORT &= ~(1 << LCD_RS))
// makroinstrukcje ustawiajace odpowiednią kombinację sygnałów CS1 i CS2
#define LCD_CS0() CLR_CS1();SET_CS2();
#define LCD_CS1() SET_CS1();CLR_CS2();
#define LCD_NOCS() SET_CS1();SET_CS2();

Ponieważ niemożliwy jest odczyt z wyświetlacza aktualnej pozycji kursora należy zadeklarować globalne zmienne przechowujące współrzędne kursora :

unsigned char lcd_x, lcd_y;

Funkcje pomocnicze
Pierwszą funkcją będzie funkcja konfigurująca porty wykorzystywane do komunikacji z wyświetlaczem w tryb wyjściowy. Wyświetlacz LM12864A nie wymaga dodatkowej inicjalizacji jak to miało miejsce w wyświetlaczu alfanumerycznym.

void lcdInit(void)
{
LCD_DATA_DDR = 0xFF;
LCD_CTRL_DDR = 0xFF;
}

Kolejna funkcja wprowadza opóźnienie wymagane przez sterownik wyświetlacza :

void delay(void)
{
asm("nop");asm("nop");
}

Dokumentacja sterownika określa minimalny czas trwania stanu na linii EN na 450ns. Tak więc dwie instrukcje nop łącznie z wywołaniem i powrotem z funkcji zapewniają spełnienie tego warunku. Przeprowadzone przeze mnie eksperymenty wykazały że w rzeczywistości dodawanie opóźnień jest zbędne. Wyświetlacz poprawnie współpracował z mikrokontrolerem taktowanym sygnałem o częstotliwości 16MHz bez żadnych dodatkowych opóźnień. Niemniej jednak, aby mieć pewność poprawnego działania należy spełnić wymogi czasowe transmisji zalecane przez producenta kontrolera.

Sprawdzanie zajętości kontrolera
Ponieważ kontroler wyświetlacza do czasu zakończenia wykonywania poprzedniej instrukcji ignoruje kolejne instrukcje, należy przed zapisaniem instrukcji sprawdzić flagę zajętości kontrolera. W tym celu zdefiniujmy następującą funkcję :

void lcdWait(void)
{
LCD_DATA_DDR = 0x00; // ustawienie portu danych w tryb wejściowy
CLR_RS(); // niski stan na linii RS -> odczyt rejestru statusu
SET_RW(); // wysoki stan na linii RW -> odczyt z wyświetlacza
do { //pętla
delay(); // opóźnienie
SET_EN(); // ustaw linię EN
delay(); // opóźnienie
CLR_EN(); // wyzeruj linię EN
} while((LCD_DATA_PIN & DISPLAY_STATUS_BUSY)); // powtarzaj do
// wyzerowania flagi BUSY
}

Zapis instrukcji
Zapis instrukcji odbywa się przy niskim stanie linii RS i RW. Opadające zbocze na linii E zatrzaskuje dane w rejestrze wejściowym.

void lcdWriteCmd(unsigned char x)
{
lcdWait();
CLR_RS();
CLR_RW();
LCD_DATA_DDR = 0xFF;
LCD_DATA_PORT = x;
SET_EN();
delay();
CLR_EN();
}

Zapis danych
Dane zapisywane są przy wysokim stanie linii RS i niskim stanie linii RW. Opadające zbocze na linii E zatrzaskuje dane w rejestrze wejściowym. W zależności od aktualnej wartości adresu Y odpowiednio ustawiana jest kombinacja sygnałów CS1 i CS2.

void lcdWriteData(unsigned char data)
{
if(lcd_x < 64) // jeśli współrzędna x wyświetlacza < 64
{LCD_CS0()} // to zapisujemy do pierwszego kontrolera
else // w przeciwnym razie
{LCD_CS1()} // zapisujemy do drugiego kontrolera
lcdWait(); // oczekiwanie na gotowość kontrolera
SET_RS(); // wysoki stan na linii RS -> dane
CLR_RW(); // niski stan na linii RW -> zapis
LCD_DATA_DDR = 0xFF; // port danych -> wyjście
LCD_DATA_PORT = data; // wystawienie na port danej
SET_EN(); // wysoki stan na linii EN
delay(); // opóźnienie
CLR_EN(); // niski stan na linii EN
lcd_x++; // zwiększenie współrzędnej x wyświetlacza (pomocniczej)
if(lcd_x > 127) // jesli koniec ekranu
lcd_x = 0; // to wyzeruj współrzędną x
LCD_NOCS();
}

Odczyt danych
Dane odczytywane są przy wysokim stanie linii RS i RW. Aby poprawnie odczytać dane z wyświetlacza należy operację odczytu wykonać dwukrotnie.

unsigned char lcdReadData(void)
{
unsigned char data;
if(lcd_x < 64) // jeśli współrzędna x wyświetlacza < 64
{LCD_CS0()} // to odczytujemy z pierwszego kontrolera
else // w przeciwnym razie
{LCD_CS1()} // odczytujemy z drugiego kontrolera
lcdWait(); // oczekiwanie na gotowość kontrolera
SET_RS(); // wysoki stan na linii RS -> dane
SET_RW(); // wysoki stan na linii RW -> odczyt
SET_EN(); // wysoki stan na linii EN
delay(); // opóźnienie
LCD_DATA_DDR = 0x00; // ustawienie portu danych w tryb wejsciowy
data = LCD_DATA_PIN; // odczyt danych z portu
CLR_EN(); // niski stan na linii EN
lcd_x++; // zwiększenie współrzędnej x wyświetlacza
if(lcd_x > 127) // jesli koniec ekranu
lcd_x = 0; // to wyzeruj współrzędną x
LCD_NOCS();
return data;
}

Ustawienie współrzędnych wyświetlania

void lcdGoTo(unsigned char x, unsigned char y)
{
lcd_x = x; // przypisanie współrzędym globalnym nowych wartości
lcd_y = y;
if(lcd_x > 63) // jeśli współrzędna pozioma jest większa od 64 to
{
LCD_CS1(); // uaktywnienie drugiego kontrolera
lcdWriteCmd(DISPLAY_SET_X | lcd_y); // zapis współrzędnej pionowej
lcdWriteCmd(DISPLAY_SET_Y | (lcd_x - 64)); // zapis współrzędnej poziomej
}
else // w przeciwnym razie
{
LCD_CS0(); // uatywnienie pierwszego kontrolera
lcdWriteCmd(DISPLAY_SET_X | lcd_y); // zapis współrzędnej pionowej
lcdWriteCmd(DISPLAY_SET_Y | lcd_x); // zapis współrzędnej poziomej
LCD_CS1(); // uaktywnienie drugiego kontrolera
lcdWriteCmd(DISPLAY_SET_X | lcd_y); // zapis współrzędnej pionowej
lcdWriteCmd(DISPLAY_SET_Y | 0 ); // wyzerowanie współrzędnej poziomej
}
LCD_CS0(); // uaktywnienie pierwszego kontrolera
lcdWriteCmd(DISPLAY_START_LINE | 0); //
LCD_CS1(); // uaktywnienie drugiego kontrolera
lcdWriteCmd(DISPLAY_START_LINE | 0);
LCD_NOCS();
}

Włączenie wyświetlacza
Włączenie wyświetlacza następuje po wykonaniu instrukcji Display On. Instrukcję należy zapisać do każdego kontrolera oddzielnie :

void lcdOn(void)
{
LCD_CS0(); // aktywny pierwszy kontroler
lcdWriteCmd(DISPLAY_ON_CMD | ON); // włączenie wyświetlania danych
LCD_CS1(); // aktywny drugi kontroler
lcdWriteCmd(DISPLAY_ON_CMD | ON); // włączenie wyświetlania danych
LCD_NOCS(); //
}

Wyłączenie wyświetlacza
Wyłaczenie wyświetlacza następuje po wykonaniu instrukcji Display Off

void lcdOff(void)
{
LCD_CS0(); // aktywny pierwszy kontroler
lcdWriteCmd(DISPLAY_ON_CMD | OFF); // wyłączenie wyświetlania danych
LCD_CS1(); // aktywny drugi kontroler
lcdWriteCmd(DISPLAY_ON_CMD | OFF); // wyłączenie wyświetlania danych
LCD_NOCS(); //
}

Czyszczenie wyświetlacza
Czyszczenie zawartości wyświetlacza polega na wypełnieniu pamięci danych ekranu bajtami o wartości zero :

void lcdCls(void)
{
unsigned char x, y; // pomocnicze zmienne
for (y = 0; y < 8; y++) // 8-krotne powtórzenie pętli
{
lcdGoTo(0,y); // ustawienie współrzędnej y wyświetlacza
for (x = 0; x < 128; x++) // 128-krotne powtórzenie pętli
lcdWriteData(0x00); // zapis do wyświetlacza
}
lcdGoTo(0,0); // ustawienie początkowych współrzędnych
}

 

Tryb tekstowy
W tej części artykułu zajmiemy się wyświetlaniem tekstu. Ponieważ wyświetlacze zbudowane w oparciu o kontroler KS0108 nie posiadają generatora znaków, wyświetlanie tekstu wymaga zdefiniowania własnej tablicy czcionek, która będzie przechowywana w pamięci programu mikrokontrolera.
Każdy znak jest określony pięcioma bajtami danych (typowa czcionka 5x8 pikseli). Kolejność bajtów w tablicy odpowiada kolejności poszczególnych pionowych fragmentów znaku. Wygląd litery A przedstawiony jest na rysunku


Do projektowania własnych czcionek można wykorzystać program ze strony http://radzio.dxp.pl/font/

Wyświetlenie znaku
Wyświetlenie znaku polega na zapisie do wyświetlacza 5 kolejnych bajtów tworzących dany znak. Ponieważ nie ma sensu marnować miejsca w pamięci programu na znaki ‘niewyświetlane’ (czyli pierwszych 31 znaków tablicy ASCII) pierwszy znak w tablicy (spacja) ma indeks 0, podczas gdy w tablicy ASCII ten znak posiada kod 32. Należy więc od kodu znaku przekazanego do funkcji jako parametr odjąć liczbę 32.

void lcdWriteChar(char x)
{
char i;
x -= 32; // konwersja kodu znaku
for(i = 0; i < 5; i++)
lcdWriteData(pgm_read_byte(font5x7 + (5 * x) + i)); // zapis do wyświetlacza
//5 kolejnych bajtów tworzących znak
lcdWriteData(0); // odstęp między znakami
}


Wyświetlanie ciągu znaków
Parametrem funkcji jest wskaźnik do typowego dla języka C ciągu znaków zakończonego zerem.

void lcdWriteString(char * s)
{
while(*s) // dopóki znak wskazywany przez s jest różny od zera
lcdWriteChar(*s++); // zapis znaku
}

Wyświetlanie ciągu znaków z pamięci programu
Ponieważ przechowywanie w pamięci RAM stałych napisów powoduje znaczne zużycie stosunkowo niewielkiej pamięci, wygodniej jest niezmienne ciągi znaków przechowywać w pamięci programu. Ze względu na specyficzny odczyt pamięci programu w kompilatorze avr-gcc do tego celu służy oddzielna funkcja.

void lcdWriteStringPgm(prog_char * s)
{
char c; // pomocnicza zmienna
while(c = pgm_read_byte(s++)) // dopóki znak wskazywany przez s jest różny od
// zera
lcdWriteChar(c); // zapis znaku
}

Ustawienie współrzędnych tekstowych
W trybie tekstowym wygodniej posługiwać się współrzędnymi odpowiadającymi położeniu znaków na wyświetlaczu. Ponieważ każdy znak składa się z sześciu pionowych "części" to zamiana współrzędnych tekstowych na graficzne sprowadza się do pomnożenia przez 6 poziomej współrzędnej tekstowej.

void lcdLocate(unsigned char x, unsigned char y)
{
lcdGoTo(x * 6, y); // zapis
}

 

Tryb graficzny
Przejdziemy teraz do procedur umożliwiających wyświetlanie grafiki.


Zapalanie piksela
Włączenie piksela polega na ustawieniu odpowiadającego mu bitu w pamięci wyświetlacza. Ponieważ możemy odczytywać i zapisywać tylko cały bajt procedura włączenia piksela przebiega następująco :
- ustawiamy współrzędne : poziomą oraz podzieloną przez 8 pionową (ponieważ dostęp do pamięci odbywa się całymi bajtami)
- odczytujemy z wyświetlacza aktualny stan pikseli (operację odczytu należy wykonać dwukrotnie)
- modyfikujemy odczytany bajt poprzez wykonanie na nim operacji sumy logicznej z bitem o wartości 1 przesuniętym w lewo o resztę z dzielenia współrzędnej pionowej przez 8 (położenie bitu w obrębie wybranej strony)
- zapisujemy tak zmodyfikowany bajt danych pod odpowiedni adres pamięci wyświetlacza
void lcdSetPixel(unsigned char x, unsigned char y)
{
char temp; // zmienna pomocnicza
lcdGoTo(x, y/8); // ustawienie współrzędnych
temp = lcdReadData(); //
temp = lcdReadData(); // podwójny odczyt danych
lcdGoTo(x, y/8); // ponowne ustawienie współrzędnych
lcdWriteData(temp | (1 << (y % 8))); // zapis odpowiednio zmodyfikowanej wartości
}

Gaszenie piksela
Procedura gaszenia piksela przebiega podobnie jak jego zapalanie. Jedyną różnicą jest sposób modyfikacji odczytanego aktualnego stanu pikseli : zamiast sumy logicznej wykonywany jest iloczyn logiczny z zanegowaną wartością przesunięcia w lewo bitu o wartości 1 o resztę z dzielenia przez 8 współrzędnej pionowej.

void lcdClrPixel(unsigned char x, unsigned char y)
{
char temp;
lcdGoTo(x, y/8);
temp = lcdReadData();
temp = lcdReadData();
lcdGoTo(x, y/8);
lcdWriteData(temp & (0xFF - (1 << (y % 8))));
}

 

 

Ostatnia modyfikacja strony : 24 maja 2008 10:12:12
(c) Radosław Kwiecień
Polityka prywatności