Piszemy własną bibliotekę dla wyświetlacza LCD(HD44780) na AVR atmega88PA

Uruchomiliśmy już wyświetlacz LCD ze sterownikiem HD44780 wykorzystując do tego gotową bibliotekę w artykule Jak zaprogramować wyświetlacz LCD (HD44780) na AVR ATTINY84A . Polecam dzisiejsze ćwiczenie rozpocząć od tamtego ćwiczenia żeby nie mieć wątpliwości że podłączenie jest dobre i wszystko działa jak należy. Dzięki temu będziemy mogli wykluczyć potencjalne błędy związane z budową i skupić się na programowaniu.

Do tej pory wykorzystywałem mikrokontroler attiny84a, nadszedł czas by zacząć się oswajać z innymi typami. Dzisiaj wykorzystamy atmega88PA, mikrokontroler ten ma więcej pinów i ogólnie lepsze parametry więc można na nim zrobić trochę więcej. Podpięcie pinów będzie wyglądać trochę inaczej.

Podpięcie LCD 4×20 do mikrokontrolera atmega88PA

Przypomnę schematy wyświetlacza i mikrokontrolera, w przypadku wyświetlacza 4×20 podłączenie wygląda tak samo jak w 2×16, przynajmniej w moim przypadku (rzuć okiem na dokumentacje swojego wyświetlacza). Zasilanie i potencjometr podpinamy tak jak poprzednio.

Podpinam piny mikrokontrolera do następujących pinów wyświetlacza:

RS – PC4
RW – PC5
E – PB0
D4 – PC0
D5 – PC1
D6 – PC2
D7 – PC3

Podpinamy cztery piny danych D4 – D7, resztę zostawiamy wolną. Będziemy przesyłać dane po połowie bajta.

Zaczynamy programowanie wyświetlacza

Mamy tutaj 7 linii podpiętych plus 4 nie wykorzystane. Linie RS, RW i E służą do sterowania a D0 D7 służą do przesyłania bajtów danych (8 linit na 8 bitów) danych, ale my wykorzystamy tylko D4-D7 czyli będzie przesyłać bajty po połowie. Już za chwilę omówimy sobie zagadnienie dokładniej.

RW służy do ustawiania trybu zapisu lub odczytu. Możemy podłączyć RW do GND dzięki czemu uzyskamy na tym pinie zawsze stan niski, ustawiając w ten sposób na stałe tryb zapisu, co może okazać się wystarczające do obsługi wyświetlacza, ale dzięki podpięciu RW do jednego z pinów mikrokontrolera możemy zaprogramować bibliotekę tak że obsługa wyświetlacza LCD z modułem HD44780 będzie znacznie szybsza. Wynika to z tego że możemy wysłać informację i odczekać określoną ilość czasu aż moduł HD44780 przetworzy dane lub możemy wcześniej wysłać zapytanie czy już przetworzył i zazwyczaj szybciej wysłać kolejną informację.

Linia RS będzie głównie służyła do określania czy przesyłamy kod znaku czy komendę. Linia E będzie służyła do sterowania przesyłanymi bitami.

Definicja portów i pinów

Prace zaczniemy standardowo od utworzenia pliku nagłówkowego gdzie będziemy trzymać wszelkie definicje zmienne w zależności od podłączenia i sprzętu. Dzięki temu, gdy weźmiemy inny AVR i podepniemy inne piny lub inny wyświetlacz z LCD zmieniamy konfigurację w jednym miejscu i od razu powinno działać. Można by tutaj użyć makra, czy wtedy będzie to łatwiejsze do zrozumienia? Na pewno mniej kodu, w przyszłych artykułach pewnie zaczniemy korzystać. Tworzymy plik lcd.h

Plik obsługi wyświetlacza

Mamy już podpięte porty i piny to teraz można rozpocząć pisanie funkcji, a będzie ich tutaj kilka potrzebnych. Przede wszystkim potrzebujemy funkcji do przesyłania danych do wyświetlacza i jego inicjalizacji. Zacznijmy od utworzenia pliku lcd.c i umieszczenia w nim nagłówków. Będziemy korzystać z opóźnień.

Podstawowa funkcja jakiej będziemy potrzebować służy do przesłania połowy bajta danych. Zgodnie z założeniem jak wspomniałem wyżej będziemy przesyłać bajty po połowie. Stwórzmy zatem funkcję lcdSendHalfByte(uint8_t data), która będzie pobierała bajt danych i ustawiała odpowiednie stany na pinach mikrokontrolera.

Nie ma tutaj chyba za wiele do tłumaczenia ponad to co w komentarzach. Tworzę cztery warunki i porównuję kolejno pierwsze cztery bity z bajtu. Dalej jeśli jest 1 to ustawiam stan wysoki na przypisanym pinie a jeśli 0 to stan niski.

Teraz jeszcze zanim przejdziemy do funkcji, dzięki której wyślemy cały bajt danych napiszmy małą funkcję ustawiającą linie D4-D7 na wyjście. W przyszłości będziemy używać tych linii także do odbioru danych więc dobrze mieć funkcję ustawiającą kierunek.

Mamy już funkcję kierunku i przesyłania połowy bajta, więc możemy przejść do poskładania tego w przesyłanie całego bajta.

Kolejnym krokiem będzie powiedzenie wyświetlaczowi czy przesyłamy bajt danych związanych z kodem znaku czy komendą. Tą informacją sterujemy za pomocą lini RS. W zależności od tego czy jest to komenda ustawiamy stan niski lub stan wysoki dla kodu znaku.

Od razu utworzymy sobie funkcję wywołującą komendę czyszczenia wyświetlacza. Musimy przekazać komendę z kodem czyszczenia wyśweitlacza 0x01 będzie to po prostu jedynka na pierwszym bicie 0000001. Tutaj poczekamy nieco dłużej ponieważ wyświetlacz potrzebuje trochę czasu żeby się wyczyścić. Później trochę to usprawnimy.

Inicjalizacja wyświetlacza

Nadszedł najtrudniejszy i zarazem najważniejszy moment czyli inicjalizacja wyświetlacza. Opis inicjalizacji możemy znaleźć w dokumentacji HD44780 i to co tutaj piszę nie jest jakąś tajemną wiedzą, wręcz przeciwnie bardzo łatwo dostępną. W dokumentacji możemy na przykład znaleźć takie strony

Żeby zobaczyć jakiekolwiek działanie wyświetlacza pozostaje nam już tylko jedna funkcja do napisania, która pobierze tablicę znaków i wyśle do naszego wyświetlacza. Będzie to bardzo prosta funkcja ponieważ wszystko co potrzebujemy mamy już praktycznie napisane.

Funkcja po prostu pobiera tablicę kodów znaków i przesyła je w pętli za pomocą wcześniej napisanej funkcji lcdWrite. Po przesłaniu kodu znaku za pomocą inkrementacji przechodzimy do następnego znaku.

Pierwszy „Hello world”

Mamy już wszystko co potrzebne do wyświetlenia jakiegokolwiek napisu na wyświetlaczu. Przechodzimy teraz do pliku głównego naszego programu main.c i dodajemy kod, który napisaliśmy. Później w funkcji głównej inicjalizujemy wyświetlacz i przesyłamy jakąś tablicę znaków którą chcemy wyświetlić.

Mamy już podstawy, na jeśli wszystko zrobiłeś dobrze to teraz na wyświetlaczu masz napis Hello World!. To już coś ale przed nami wciąż dużo pracy do wykonania.

Po pierwsze trzeba usprawnić program żeby wykorzystywał linię RW i odczyt busyFlag czyli zamiast czekać dłuższy czas aż wyświetlacz obsłuży dane to pytać go czy już obsłużył dzięki czemu będzie działać znacznie szybciej.

Sterowanie kursorem, ponieważ na ten moment obsługa kolejnych linijek lub dłuższych tekstów może okazać się problematyczna. Warto napisać funkcję do przechodzenia do następnej linijki a także do wypełniania spacją odpowiednich przestrzeni żeby zrobić np wcięcie.

No i wisienką na torcie będzie dodawanie własnych znaków do pamięci wyświetlacza, aby był w stanie wyświetlić chociażby polskie znaki. Z drugiej strony jakby to dobrze rozegrać mając do dyspozycji praktycznie każdy piksel można stworzyć np prostą grę. Wystarczy trochę pomyśleć i można zrobić alternatywę dinozaura z chrome lub angry birds na wyświetlacz lcd, mikrokontroler i jeden microswitch.

Odczyt flagi zajętości

Jak już wyżej wspomniałem, wyświetlacz możemy podłączyć o obsłużyć na co najmniej dwa sposoby. W pierwszym nie wykorzystujemy linii RW tylko podpinamy ją pod GND i tylko wysyłamy dane. Przy permanentnym ustawieniu na zapis po wysłaniu komendy za każdym razem odczekujemy chwilę żeby mieć pewność że moduł wyświetlacza zakończył pracę i dopiero wtedy podejmujemy dalsze działania. Przy wykorzystaniu odczytu po prostu w pętli odpytujemy moduł czy jeszcze pracuje i od razu jak zwróci informację że już skończył przechodzimy do dalszych działań.

Zanim dojdziemy do napisania funkcji pobierającej flagę zajętości musimy przebrnąć przez kilka etapów, analogicznie jak przy zapisie. Najpierw ustawiamy linie D4-D7 na wejścia, dalej piszemy funkcję do pobrania połowy bajtu a dalej całego bajtu.

Zacznijmy od ustawienia pinów na wejścia, gdzieś na górze naszego pliku lcd.c

Dalej analogicznie jak wcześniej tworzymy funkcję do odczytu połowy bajta.

Jako że nie używamy jeszcze makra dodajemy do pliku nagłówkowego lcd.h cztery definicje użyte w powyższej funkcji. Koniecznie zgodnie z portami do których są podpięte wymienione piny.

Teraz analogicznie jak wcześniej tworzymy funkcję do odczytu całego bajta.

Na tym etapie odczyt bajta danych mamy już opanowany pozostaje tylko odczytać flagę zajętości. W zasadzie jest on potrzebna do odczekania aż wyświetlacz będzie gotowy na kolejne akcje. Napiszmy funkcję, która będzie w pętli odczytywać flagę zajętości aż się nie zwolni.

I teraz skoro już mamy funkcję czekającą należy podmienić miejsca gdzie czekamy określoną ilość czasu na tę funkcję. (poza inicjalizacją gdzie wyświetlacz nie jest jeszcze gotowy do przesyłania tej informacji). Umieszczamy np przy wysyłaniu kolejnych bajtów.

I to tyle na ten moment w temacie odczytywania danych z wyświetlacza.

Ustawianie kursora na wyświetlaczu ze sterownikiem HD44780

Na ten moment nasz wyświetlacz powinien działać całkiem fajnie gdy chcemy wyświetlić bardzo krótki napis jednak przy próbie wyświetlenia dłuższego napisu, np takiego, który nie mieści się w pierwszej linijce może powstać problem. Spróbowałem na moim wyświetlaczu 4×20 wyświetlić dłuższy tekst. Taki oto kod wprowadziłem do mojej funkcji głównej programu w pliku main.c

Efekt był dość nieoczekiwany. Co bardziej wnikliwi powinni zaobserwować sytuację, w której po zapełnieniu pierwszej linijki, kursor przeskakuje do trzeciej a później wraca do drugiej.

Napiszemy teraz kilka funkcji, które pomogą nam ustawić kursor dokładnie tam gdzie tego chcemy. Zacznijmy może od najprostszej, czyli ustawienia kursora na początku w dowolnym momencie.

Jak ustawić kursor na początku?

Jest to niezwykle proste na tym etapie, ponieważ wystarczy przesłać do naszego wyświetlacza tylko odpowiednią komendę. Funkcja do ustawiania kursora na początku będzie wyglądać w sposób następujący

Komenda ustawiająca kursor na początku ma kod 0x02, generalnie w bajcie chodzi o ustawienie bitów na odpowiednie komendy i tym sposobem możemy przesłać ze dwie lub trzy komendy na raz. To by mogło całkiem sporo wyjaśniać co się działo przy inicjalizacji. Te kody niewiele nam mówią i prawdę mówiąc jak rozpracowywałem ten temat miałem z nimi pewien problem, w części źródeł nie było opisane co jest co, więc myślę że dobrym pomysłem będzie dopisanie definicji komend do pliku nagłówkowego.

Komendy wyświetlacza HD44780

Przejdźmy teraz do pliku lcd.h gdzie definiowaliśmy między innymi porty i piny a następnie dopiszmy tam nasze komendy wyświetlacza. Pierwsze z nich już doskonale znamy. Jedną z nich czyścimy wyświetlacz a drugą ustawiamy kursor na początku. Będzie jeszcze kilka komend między innymi do określenia czy kursor ma być widoczny co ma migać, jak przesunąć kursor i kilka innych określających jak ma się zachować nasz wyświetlacz. Część z nich używamy razem tak jak miało to miejsce przy inicjalizacji. Za chwilę zaktualizujemy sobie tamten kod. Poniżej kody poszczególnych komend. Mam nadzieję że większość z nich da się skojarzyć po nazwie definicji.

Jak widać możliwości tutaj mamy całkiem sporo. Spróbujmy sobie może teraz przypomnieć miejsce do tej pory niezbyt objaśnione przy inicjalizacji wyświetlacza gdzie wykorzystaliśmy część z powyższych kodów. Na ten moment kod powinien wyglądać tak w tamtym miejscu.

Spróbujmy teraz nieco zmodyfikować ten kod aby był bardziej czytelny i bardziej było wiadomo o co chodzi.

Jak ustawić kursor na wyświetlaczu LCD 4×20?

W końcu nadszedł moment na napisanie funkcji dzięki, której będziemy w stanie umieścić kursor w dowolnym miejscu. Po pierwsze musimy znać adresy początku każdej z linii wyświetlacza. W tym miejscu wyjaśni się dlaczego na moim wyświetlaczu 4×20 po zapełnieniu 1linii przeskakuje do trzeciej a później wraca do drugiej. Teraz w pliku nagłówkowym zdefiniujmy te adresy.

Przy okazji powyższego warto pamiętać że 0x14 w to nie 14 a 4 + 16 czyli 20. Ten adres by wyjaśniał przejście do trzeciej a nie drugiej linii.

Skoro mamy już kody linii, które trzeba wysłać, to wystarczy dodać do tego pozycję w linii i otrzymamy pozycję. Napiszmy teraz bardzo podstawową funkcję kontroli pozycji kursora.

No i mamy to. Oczywiście tę bibliotekę dało by się znacznie usprawnić i rozbudować na przykład tak żeby obsługiwała przy okazji inne typy wyświetlaczy. Pewnie za jakiś czas to zrobię i wrzucę na gita do pobrania, a tymczasem zachęcam do usprawnień we własnym zakresie 🙂

Obsługa własnych znaków wyświetlacza z modułem HD44780 za pomocą mikrokontrolera AVR Atmega88PA

Miałem w tym miejscu napisać że została nam do obsłużenia ostatnia kwestia ale jak się okaże pozostaje ich jeszcze kilka. W każdym razie skupimy się teraz na dodaniu własnego znaku. Niestety jednocześnie możemy dodać ich tylko 8 do wyświetlacza, ale chyba nic nie stoi na przeszkodzie żeby te znaki podmienić według potrzeb np w innych komunikatach wyświetlacza.

Zasada tworzenia takiego znaku nie jest zbyt skomplikowana. Mamy zazwyczaj macierz pikseli 5×8 czyli osiem rzędów po 5px. I w tym zakresie możemy zaprojektować dowolny znak, który możemy zapisać do wyświetlacza a później go wykorzystywać wedle uznania. Aby zapisać taki znak będziemy potrzebowali przesłać 8 bajtów danych gdzie znaczenie będą miały ostatnie 5 bitów. W tym przypadku jedynka będzie oznaczała zapalony bit a 0 zgaszony.

Dobrze będzie jeszcze opracować pobieranie pozycji kursora żeby pobrać pozycję, wysłać znak i postawić kursor tam gdzie stał. W tym przykładzie spróbujemy sobie zrobić Polski znak „ą”, którego raczej nie uświadczymy wśród standardowych znaków wyświetlacza LCD. Poniżej rozpisałem poszczególne bajty znaku, które będziemy musieli przesłać.

000 00000 – 0
000 00000 – 0
000 01110 – 14
000 00001 – 1
000 01111 – 15
000 10001 – 17
000 01111 – 15
000 00010 – 2

Chcielibyśmy pobrać współrzędne kursora, aby to zrobić potrzebowalibyśmy funkcji, która zwracałaby dwie wartości, albo dwóch funkcji. Ewentualnie mogła by być to tablica dwuelementowa. Jednak jest to idealny moment na stworzenie struktury Pozycja zawierającej rekord i pozycję w rekordzie dla kursora. Zacznijmy więc od zdefiniowania struktury Position w pliku nagłówkowym lcd.h

Teraz gdy już mamy strukturę napiszemy funkcję, która zwróci nam pozycję kursora. Do tej pory już ustaliliśmy całkiem sporo, a między innymi adresy początku każdej linii, bo adresy znaków w takim wyświetlaczu idą po kolei z różnicą pomiędzy rekordami. Jak wcześniej ustaliliśmy po pierwszy rekordzie jest trzeci a po trzecim jest drugi. Dlatego odczytujemy adres kursora i korygujemy jego współrzędne o adresy początku linii. W ten sposób uzyskamy nr linii i pozycję w tej linii. Patrz kod poniżej.

Teraz to mam już wszystko co potrzeba do stworzenia znaku, oprócz oczywiście najważniejszej funkcji, która to robi. Będzie to funkcja pobierająca tablicę bajtów znaku (tak jak rozpisałem to powyżej) oraz pozycję w pamięci wyświetlacza. Mamy do dyspozycji 64 bajty pamięci czyli 8 pozycji.

Na początku sprawdzam czy przypadkiem nie wychodzimy poza zakres pozycji znaku w pamięci. Później pobieram pozycję kursora i zapisuję ją do zmiennej. Dalej wysyłam komendę z pozycją w pamięci po czym przesyłam po kolei 8 bajtów znaku. Na koniec ustawiam pozycję kursora tam gdzie był i mamy to.

Na koniec wypadałoby jeszcze pokazać przykład z użyciem tego, więc przechodzimy do funkcji głównej programu w main.c i dodajemy sobie dwie linijki kodu.

No i mamy to na wyświetlaczu. W tym miejscu miałem zakończyć ale pomyślałem że warto jeszcze napisać funkcję wyświetlającą liczby, ponieważ używam tego wyświetlacza głównie do odczytu wartości z jakichś czujników itd, w sumie głównie do wyświetlania liczb mi służy.

Jak wyświetlić liczbę na wyświetlaczu LCD z modułem HD44780?

Spróbujemy jeszcze na koniec napisać prostą funkcję analogiczną do lcdStr(char *str) tylko że pobierającą i wyświetlającą liczby całkowite. Do napisania funkcji wyświetlającej liczby zmienno przecinkowe zapraszam już do pracy samodzielnej w ramach ćwiczenia.

Poszedłem trochę na łatwiznę i wykorzystałem funkcję wbudowaną itoa() pobierającą trzy argumenty. Pierwszy argument to liczba, drugi to adres tablicy bufora, do której chcemy załadować znaki i trzeci jakiej postaci chcemy liczbę. Możemy podać 10 czyli liczby dziesiętne a możemy też dać 16 dla szesnastkowych.

Wykorzystanie w funkcji głównej programu jest niezwykle proste i analogiczne do tego jak wyświetlamy po prostu łańcuch znaków.

Koniec

To tyle na dzisiaj. Chyba z tydzień rozpracowywałem ten temat 🙂 Oczywiście tę bibliotekę można by znacznie ulepszyć. Wypadałoby na przykład dodać obsługę innych typów wyświetlaczy jak te 2×16. Można by też znacznie zoptymalizować ten kod i sprawić by wyświetlał się tylko potrzebny kod, albo też obsłużyć podpięcie linii RW do GND bez tych wszystkich funkcji odczytu. Myślę jednak że rozpracowaliśmy tutaj wszystko co najważniejsze.

No i na koniec co udało się uzyskać pisząc ten artykuł.

Jeśli masz jakieś sugestie pisz śmiało.

Ćwiczenie

A jeśli chcesz sobie poćwiczyć to dobry moment żeby do tego projektu zapiąć jeszcze buzzer i jakiś microswitch, wykorzystać timery i napisać jakąś grę. Chociażby przesuwające się przeszkody i ich przeskakiwanie a po przegranej dowolny dźwięk z buzzera.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *