Chciałbyś pozbyć się blokowania procesora przy obsłudze LCD? A może zmieniać wyświetlane znaki z procedury obsługi przerwania? Żaden problem 🙂 Zapraszam do artykułu, w którym opisuję to na przykładzie wyświetlacza ze sterownikiem HD44780.
Pisząc ten artykuł zakładam, że potrafisz już obsługiwać LCD, ponieważ tutaj będę jedynie rozwijał ten temat. Niżej prezentowany kod, napisany jest pod wyświetlacze ze sterownikiem HD44780, które opisywałem tutaj. Na początku zaznaczę, że nie jest to zagadnienie podstawowe i niezbędne. W wielu wypadkach zwykła obsługa wyświetlaczy jest wystarczająca i nie ma potrzeby stosowania buforowania, ale jeśli dobrze opanowałeś obsługę LCD to ten temat może być dla Ciebie fajną ciekawostką i dać nowe spojrzenie na to zagadnienie. Otóż, do obsługi wyświetlacza można podejść w zupełnie inny sposób.
Buforowanie wyświetlaczy
Jak wiesz, operacje wykonywane przez sterownik HD44780 są bardzo czasochłonne. W standardowym podejściu, gdy wysyłamy coś do sterownika LCD, a następnie wprowadzamy opóźnienie czy to funkcją delay() czy przez oczekiwanie na wyzerowanie flagi zajętości (ang. busy flag) marnujemy bardzo dużo czasu procesora, który w tym czasie mógłby wykonać mnóstwo innych instrukcji. Jak się już pewnie domyślasz, buforowanie pozwala zlikwidować lub chociaż zminimalizować ten problem. Przy obsłudze bardziej zaawansowanych wyświetlaczy jest on niemal niezbędny. Wykorzystywane są wtedy dwa bufory do przechowywania obrazu (ang. double buffering). Gdy na jednym buforze wykonywane są operacje związane z rysowaniem to równolegle dane z drugiego bufora przesyłane są bardzo szybko do wyświetlacza, np. przy pomocy jakiegoś DMA. Co prawda zużywamy na to trochę pamięci, ale dzięki temu nie rysujemy obrazu bezpośrednio na wyświetlaczu, co korzystnie wpływa na płynność animacji i zapobiega powstawaniu artefaktów. Tutaj będzie wyglądało to trochę podobnie – całe „rysowanie”, czyli w tym wypadku dodawanie znaków, będziemy przeprowadzali na buforze, a następnie, co jakiś czas przesyłali to do wyświetlacza. Jak wiadomo dostęp do pamięci RAM mikrokontrolera jest bardzo szybki, więc takie operacje na buforze będziemy mogli wykonywać nawet w procedurach obsługi przerwań. W przypadku standardowej obsługi lcd nie mogliśmy sobie na to pozwolić. Stosując bufory możemy w dowolnej chwili dodać szybko określoną ilość znaków i przejść dalej do wykonywania innych czynności, a wysyłanie do wyświetlacza będzie odbywać się w tle. Wysyłaniem zajmiemy się potem, na początku stwórzmy bufor i funkcje do jego obsługi.
Obsługa bufora
Bufor będzie miał postać tablicy. Aby bardziej przypominał wyświetlacz, niech będzie to tablica dwuwymiarowa: mamy w niej ilość wierszy, a w każdym wierszu ilość kolumn odpowiadające wielkości wyświetlacza:
1 |
char lcd_buf[LCD_ROWS][LCD_COLS]; |
Makra LCD_ROWS i LCD_COLS pochodzą z biblioteki do obsługi LCD. Oprócz bufora przydadzą się jeszcze zmienne do przechowywania współrzędnych kursora, odpowiadające polu wyświetlacza, w którym chcemy coś wpisać.
1 |
uint8_t lcd_buf_x, lcd_buf_y; |
Zmienna lcd_buf_x odpowiada numerowi kolumny, a lcd_buf_y numerowi wiersza LCD. Ustawienie kursora w określonej pozycji będzie odbywało się poprzez przypisanie tym zmiennym odpowiednich wartości. Stwórzmy w tym celu funkcję:
1 2 3 4 5 |
void buf_locate(uint8_t x, uint8_t y) { lcd_buf_x = x; lcd_buf_y = y; } |
Teraz przejdźmy do funkcji, która umożliwi dodawanie znaków do bufora:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void buf_char(char c) { if (lcd_buf_x < LCD_COLS && lcd_buf_y < LCD_ROWS) { lcd_buf[lcd_buf_y][lcd_buf_x] = c; lcd_buf_x++; if (lcd_buf_x == LCD_COLS) { lcd_buf_x = 0; lcd_buf_y++; if (lcd_buf_y == LCD_ROWS) lcd_buf_y = 0; } } } |
Na samym początku sprawdzamy poprawność współrzędnych kursora. Następnie dodajemy znak do bufora i inkrementujemy współrzędną x. Jeśli dojdziemy do końca wiersza to ustawiamy kursor na początek kolejnego. A jeżeli jesteśmy już na ostatnim polu wyświetlacza to ustawiamy go na sam początek.
Stwórzmy jeszcze funkcję, która umożliwi czyszczenie bufora, czyli wpisanie w każdym elemencie tablicy znaku spacji:
1 2 3 4 5 6 7 8 9 10 11 |
void buf_clear(void) { for(uint8_t y=0; y<LCD_ROWS; y++) { for(uint8_t x=0; x<LCD_COLS; x++) { lcd_buf[y][x]=' '; } } lcd_buf_x=0; lcd_buf_y=0; } |
Jak widzisz, powyższe funkcje odpowiadają tym z obsługi wyświetlacza. Jedyną różnicą jest to, że działają na buforze zamiast na lcd. Na ich podstawie możemy już bez problemu tworzyć kolejne: np. funkcja do dodawania stringów:
1 2 3 4 5 |
void buf_str(char *text) { while(*text) buf_char(*text++); } |
Jedyne co nam zostało, to stworzenie mechanizmu, który umożliwi przesłanie zawartości bufora do pamięci wyświetlacza 🙂
Odświeżanie
Odświeżanie wyświetlacza można zrealizować na wiele sposobów. Od Twojej aplikacji zależeć będzie, jaki okażę się optymalny. Ja przedstawię tutaj dwa przykładowe i opiszę ich plusy i minusy:
Sposób pierwszy
Najzwyklejsze odświeżanie, czyli po prostu co jakiś czas przesyłamy do wyświetlacza wszystkie znaki z bufora. Przy wysyłaniu, można jeszcze dodatkowo sprawdzać czy znak, który jest w buforze różni się od tego, który jest aktualnie na wyświetlaczu. Bez sensu byłoby go wysyłać ponownie. Niestety coś kosztem czegoś. Aby móc sobie na to pozwolić konieczne jest stworzenie dodatkowej tablicy, która będzie przechowywała aktualnie wyświetlane znaki. Po wysłaniu znaku do wyświetlacza, będziemy go także dodawać do tej tablicy.
1 |
char lcd_buf_old[LCD_ROWS][LCD_COLS]; |
Przejdźmy do funkcji:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
void lcd_refresh(void) { static uint8_t locate_flag = 0; // informuje o konieczności przestawienia kursora lcd for(uint8_t y=0; y<LCD_ROWS; y++) { lcd_locate( 0, y ); for(uint8_t x=0; x<LCD_COLS; x++) { if( lcd_buf[y][x] != lcd_buf_old[y][x] ) { if( !locate_flag ) lcd_locate( x, y ); lcd_char( lcd_buf[y][x] ); lcd_buf_old[y][x] = lcd_buf[y][x]; locate_flag = 1; } else locate_flag = 0; } } } |
W funkcji, za pomocą pętli for przechodzimy po kolejnych elementach tablicy i sprawdzamy czy dany znak, różni się od tego, który jest aktualnie wyświetlany na tej pozycji. Jeśli jest inny to go wysyłamy za pomocą funkcji lcd_char() i od razu aktualizujemy tablicę lcd_buf_old. Wyjaśnię jeszcze o co chodzi z tą zmienną locate_flag. Po wysłaniu znaku do wyświetlacza sterownik automatycznie inkrementuje pozycję swojego kursora, więc nie ma potrzeby za każdym razem go ręcznie ustawiać za pomocą funkcji lcd_locate(). Jednak może zdarzyć się taka sytuacja, że wyślemy znak i sterownik przestawi swój kursor o jedną pozycję, a my w to miejsce nie będziemy chcieli nic wysyłać, ponieważ znak znajdujący się w buforze jest identyczny jak ten, który jest w tym miejscu na wyświetlaczu. W takim wypadku, przy kolejnym znaku, który będziemy chcieli wysłać, musimy już sami zadbać o ustawienie kursora na lcd i zmienna locate_flag służy właśnie do przechowywania tej informacji. Chcemy, aby jej stan był zapamiętywany pomiędzy kolejnymi wywołaniami funkcji, dlatego konieczne jest jej zadeklarowanie jako zmiennej statycznej. Funkcja lcd_locate() wywoływana jest także przy każdym obiegu pętli odpowiadającej wierszom wyświetlacza. Jest to konieczne, ze względu na strukturę pamięci DDRAM w sterowniku HD44780. W przypadku wyświetlaczy 4×20, gdy kursor jest np. na ostatniej pozycji pierwszego wiersza, to następnym polem na które automatycznie przeskakuje jest pierwsza pozycja wiersza trzeciego. Przyporządkowanie pól wyświetlacza do adresów DDRAM przedstawiałem tutaj.
Mamy już wszystko 🙂 Wystarczy już tylko ustawić timer i np. co 50ms wywoływać funkcję lcd_refresh() w pętli głównej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
int main(void) { NVIC_Config(); TIMERS_Config(); LCD_Init(); buf_locate(3,1); buf_str("BUFOROWANIE LCD"); buf_locate(1,2); buf_str("www.EmbeddedDev.pl"); while(1) { if(timer_flag) { lcd_refresh(); timer_flag=0; } } } |
Konfiguracji liczników i kontrolera przerwań nie będę tutaj opisywał, ponieważ to zupełnie inne zagadnienie, ale jeśli będziesz miał z tym kłopot to możesz skorzystać z mojego kodu – całkowite kody źródłowe z tego przykładu wraz z ciałami funkcji konfigurującymi timer i kontroler przerwań znajdziesz na moim githubie.
W tym sposobie niestety nie pozbyliśmy się opóźnień występujących przy zapisie danych do sterownika, ale w wielu wypadkach przy odstępach np. 50ms możemy sobie pozwolić na takie operacje.
Sposób drugi
Zacznę od przedstawienia funkcji, a następnie przejdę do jej opisu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
void lcd_refresh(void) { static uint8_t locate_flag = 0; // informuje o konieczności przestawienia kursora lcd static uint8_t x = 0, y = 0; if( lcd_readFlag()!= 0x80 ) { if (x == LCD_COLS) { x = 0; y++; if (y == LCD_ROWS) y = 0; lcd_locate(0,y); return; } if( lcd_buf[y][x] != lcd_buf_old[y][x] ) { if(!locate_flag) { lcd_locate(x,y); locate_flag=1; return; } lcd_char( lcd_buf[y][x] ); lcd_buf_old[y][x]=lcd_buf[y][x]; } else { locate_flag=0; } x++; } } |
Założeniem jest, że funkcję wywołujemy w głównej pętli programu, a więc przy każdym jej obiegu odczytujemy flagę zajętości i sprawdzamy czy sterownik lcd jest wolny. Jeżeli warunek jest spełniony, to wraz z kolejnymi obiegami pętli głównej przechodzimy przez bufor element po elemencie (samodzielnie inkrementując indeksy x i y, bez pętli for) i podobnie jak w poprzednim sposobie sprawdzamy czy dany znak różni się od tego, który jest aktualnie na wyświetlaczu. Jeśli tak to go wysyłamy. Natomiast w przypadku gdy sterownik wyświetlacza jest zajęty to nie czekamy, aż będzie gotowy tylko wychodzimy z funkcji i zajmujemy się wykonywaniem innych zadań. Z funkcji wychodzimy także w przypadku, gdy zmieniamy położenie kursora na lcd za pomocą funkcji lcd_locate(), ponieważ wiemy, że po jej wywołaniu sterownik będzie zajęty, więc nie chcemy mu w tej chwili wysyłać znaku. Zrobimy to przy którymś z kolejnych obiegów pętli głównej, gdy sterownik będzie wolny. Pozostała część kodu tej funkcji jest taka sama jak w poprzednim sposobie, więc pominę jej opis. Poniżej główna funkcja programu:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
int main(void) { TIMERS_Config(); LCD_Init(); buf_locate(3,1); buf_str("BUFOROWANIE LCD"); buf_locate(1,2); buf_str("www.EmbeddedDev.pl"); while(1) { lcd_refresh(); } } |
Dzięki takiej obsłudze możemy usunąć poniższą linię z funkcji lcd_write_byte():
1 |
while( lcd_readFlag() & 0x80 ); |
I w ten oto sposób pozbywamy się opóźnień 🙂 Zadanie wykonane! 😀 Należy jednak pamiętać, że zmiana wielu znaków na wyświetlaczu zostanie wykonana dopiero po wielu obiegach pętli głównej. W przypadku gdy będziemy w niej wykonywać długotrwałe operacje to może pojawić się efekt widocznego odrysowywania kolejnych znaków na lcd. Jak mówiłem, optymalny sposób będzie można wybrać w zależności od projektu. Oprócz sposobów opisywanych tutaj rozważyć można jeszcze zrealizowanie odświeżania w obsłudze przerwania od timera. Całkowity kod do tego przykładu także znajdziesz na moim githubie.
Na tym temacie na razie kończę artykuły dotyczące obsługi wyświetlaczy. Mam nadzieję, że podobał Ci się ten wpis i dowiedziałeś się czegoś nowego. Zachęcam do przeczytania innych 🙂
Zostaw komentarz!
Podobał Ci się wpis? Masz jakieś pytania lub uwagi? Zostaw mi proszę komentarz, a jeśli uważasz artykuł za wartościowy – podaj go dalej 🙂 Fajnie by było gdyby trafił do większej ilości osób. Pozdro i do następnego! 😉