LCD 2×16 i AVR Atmega16A

Po dość długiej przerwie (sesja) szarpnąłem się wreszcie na napisanie jakiegoś artykułu, ponieważ ostatnio rozpocząłem prace z mikrokontrolerem (uC) to postanowiłem dzielić się różnymi artykułami – a nuż się komuś to przyda. Zakładam już, że każdy zna podstawy programowania w C i posiada odpowiedni soft.

Jednym z podstawowych projektów opartych na uC jest wyświetlacz ciekłokrystaliczny działający zazwyczaj na sterowniku HD47780 lub również na jego klonach KS0066, KS0065. W swoim artykule będę używał wyświetlacza oznaczonego WC1602D, link do noty katalogowej.


Zdjęcia przedstawiają ów wyświetlacz, na drugim można zauważyć, że przylutowałem piny, ułatwiają połączenie wyświetlacza zarówno z płytką stykową, jak i z późniejszym układem docelowym.

Schemat przedstawiający moje połączenie LCD z uC.

Teraz gdybyśmy podpięli zasilanie, na wyświetlaczu powinniśmy zobaczyć rząd czarnych prostokątów, jest to normalne zachowanie i oznacza, że wyświetlacz nie został zainicjowany, ale działa.

Teraz pokrótce omówię wyprowadzenia na wyświetlaczu:

DB0 – DB7 – Szyna danych, tutaj będziemy ustawiać bajt, który ma zostać odebrany przez wyświetlacz
EEnable Signal, informujemy wyświetlacz, że chcemy mu coś wysłać, zostanie to dokładniej omówione
RWRead/Write, w zależności od ustawionego stanu wysyłamy dane do wyświetlacza (0) lub z niego odbieramy (1)
RSRegister Select, w zależności od ustawionego stanu wysyłamy albo dane (1) albo instrukcje (0)
V0Contrast, możemy sterować kontrastem, wpiąłem do masy żeby uzyskać maksymalny
Vdd – Napięcie +5V
Vss – Masa
A – Napięcie +5V dla podświetlenia
K – Masa dla podświetlenia

Programowanie

Przyszedł czas żeby trochę pokodzić, a najlepiej zacząć od początku, czyli od inicjacji wyświetlacza. Nie jest to takie straszne jak brzmi, jest to po prostu sekwencja komend wysyłanych do wyświetlacza żeby wiedział jak ma pracować. Schemat jest przedstawiony w dokumentacji dlatego nie będę przepisywał całości, a jedynie przedstawię i opiszę swój kod.

Zdefiniowałem sobie na początku przydatne wyrażenia:

Do wyświetlacza wysyłam następujące dane:

Dobra dobra, ale jak to wysłać? Żeby zrozumieć ideę wysyłania danych do wyświetlacza, warto zapoznać się z wykresem czasowym dostępnym w dokumentacji, przedstawię go również poniżej. Polecam to zrobić, ja na początku olałem wykresy, przez co długo zastanawiałem się jak dokładnie działa wysyłanie.

Na pierwszy rzut oka nie wygląda to zachęcająco, ale postaram się ułatwić przyswojenie. Po lewej stronie mamy podpisane linie, których wyświetlacz używa do komunikacji. Wykresy są intuicyjne, górka – 1, dołek – 0, jeżeli jest jednocześnie 1 i 0 znaczy, że może być albo jedno albo drugie. Bajt który wysyłamy jest oznaczony jako „Valid data„. Przyjrzyj się innym wykresom nad naszym bajtem, linia RS może być w stanie wysokim lub niskim, zależy co wysyłamy, R/W ma stan niski, co oznacza, że w tym przypadku wysyłamy do wyświetlacza. Najbardziej nam zależy na interpretacji sygnału Enable, trzeba zauważyć, że swój żywot zaczyna przed wystawienie danych na szynę, a kończy kiedy te dane są już wystawione, czyli odczytuje zboczem opadającym. Zatem możemy stworzyć algorytm postępowania:

  1. Ustaw stan wysoki na linii Enable
  2. Ustaw bajt, który masz zamiar wysłać na liniach DB0-DB7
  3. Ustaw stan niski na linii Enable (dodatkowo przed tym dodaję opóźnienie 500 μs, bez tego napisy są ucinane)

Na podstawie tego tworzymy nasze funkcje do wysyłania instrukcji i danych:

Chyba nie muszę wyjaśniać jak one działają, zbieramy to do kupy i tworzymy funkcję inicjującą wyświetlacz:

Opóźnienia wstawione są zgodnie z zaleceniami w nocie katalogowej. Polecam również zrobić sobie funkcję inicjującą sam kontroler, tzn. wyjścia, które będą przez nas używane:

I ostatecznie funkcja main:

W efekcie kod jaki powinniśmy uzyskać:

Wyświetlacz odwdzięcza się nam współpracą:

Tips’n’Tricks

Pomimo, że wyświetlacz posiada możliwość wyświetlenia tylko 16 znaków, nie oznacza to, że nie może w jednej linijce pomieścić więcej, brzmi trochę sprzecznie, ale zaraz wyjaśnię. 1 wiersz jest w stanie pomieścić 40 znaków, te które są niewyświetlone czekają w pamięci aż „przesuniemy” ekran. Aby dowiedzieć się jak to zrobić przeszukujemy notę katalogową i trafiamy na:

S/C – Wybieramy czy chcemy przesuwać kursor (0) czy cały wyświetlacz (1)
R/L – Wybieramy kierunek przesuwu, lewo (0) lub prawo (1)

Czyli np. chcemy przesunąć cały wyświetlacz w prawo, wysyłamy do wyświetlacza odpowiednią komendę:

Każde wywołanie tej komendy spowoduje przesunięcie wyświetlacza w lewo o jedną pozycję.

Przejście do nowej linii

Możemy to wykonać podając odpowiednią instrukcję. Wysłanie do wyświetlacz instrukcji 0x80 nakazuje mu ustawienie kursora w pozycji początkowej (00), 0x81 ustawi na pozycję 01, 0x82 na 02 itd. Jeżeli mamy 40 znaków w wierszu, ostatnia pozycja będzie miała indeks 39 (0x27) oraz będzie adresowana: 0x80+0x27 = 0xA7, czyli kolejny znak (0xA8) będzie już pierwszym znakiem w drugiej linii.

Definiowanie własnych znaków

Wbrew pozorom jest to banalna sprawa. Najpierw musimy wymyślić jakiś znak mieszczący się w wymiarach 5×8 pikseli, może być to serduszko, strzałeczka, kółeczko, cokolwiek co chcesz. Następnie każdy z 8 wierszy musimy zapisać jako liczbę 5-cio bitową. Na przykład stwórzmy strzałkę, najpierw zakodujmy ją:

Zapiszemy to w postaci tablicy:

Wprowadzanie danych do generatora znaków odbywa się poprzez ustawienie wpisywania do odpowiedniego adresu, tak jak w poprzednim przykładzie wpisywaliśmy do DDRAM (pod adres 0x80), tak tutaj będziemy wpisywać do CGRAM (pod adres 0x40).

Musimy pamiętać, że każdy wiersz jest zapisywany pod oddzielnym adresem, tzn. pierwszy wiersz pierwszego znaku jest pod adresem 0x40, drugi wiersz – 0x41, itd.. W efekcie czego możemy zdefiniować maksymalnie 8 własnych znaków, ponieważ mamy na to 64 bajty (0x80 – 0x40), a jeden znak zajmuje 8 bajtów, 64/8 = 8.

Ponieważ po wprowadzaniu danych do CGRAM musimy wrócić do DDRAM (żeby móc wpisywać na wyświetlacz), polecam zdefiniowanie znaków na samym początku naszego programu. A na przyszłość, żeby ułatwić sobie życie, podrzucam funkcję do definiowania znaków:

I przykładowe wywołanie:

Dzięki argumentowi id funkcja sama oblicza adres od którego rozpocznie wpisywanie danych.

Na razie to tyle, mam nadzieję, że mój artykuł pomógł w czymś osobom wkraczającym w piękny świat programowania mikrokontrolerów.

[guest@itachi.pl:~]$