Język C dla mikrokontrolerów 8051. Sterowanie modułem wyświetlacza LCD.

Zacznijmy od jakiegoś użytecznego programu. Czegoś, co przyda nam się w przyszłości tak, aby nie marnować cennego czasu. Proponuję na początek podłączenie wyświetlacza LCD w trybie 4 bity. Rozbierzemy na kawałeczki poszczególne fragmenty programu analizując jego kod. To najlepsza droga do zrozumienia całości.

Początek to tradycyjnie już deklaracje. Wybieramy model pamięci (#pragma SMALL) , dołączamy właściwą definicję rejestrów mikroprocesora (#include <reg51.h>), określamy, które bity sterują wyświetlaczem (sbit), jakie definicje znaków zapiszemy do wyświetlacza itp. Zwróćmy uwagę na troszkę inny niż w innych językach programowania, opis bitu portu. Separatorem pomiędzy nazwą portu a numerem bitu nie jest znak kropki, lecz umowny symbol potęgi. Słowo kluczowe sbit umieszcza naszą definicję w rejestrze funkcji specjalnych SFR, w obszarze adresowania bitowego.
Przyjrzyjmy się zawartości tablicy z definicjami znaków użytkownika CGRom. Zauważmy, że jej deklaracja zawiera wiele istotnych informacji dla naszego programu praktycznie w jednej linijce:
- określony zostaje typ elementów tablicy, w tym przypadku char (czyli element jednobajtowy),
- tablicę zostaje umieszczona w pamięci programu mikrokontrolera (słowo code),
- tablica otrzymuje nazwę symboliczną (CGRom) i określona zostaje liczba jej elementów (65),
- elementom tablicy nadawana jest wartość stała.
Pierwszą z funkcji, którą napotkamy analizując program, jest Delay. Jest to zwykła pętla, która absorbuje mikrokontroler na około 1 milisekundę. Tak stanie się tylko wtedy, gdy użyjemy rezonatora kwarcowego 7,3728MHz. Jeśli stosujemy inny kwarc, procedura wymaga zmiany. Zmieni się bowiem czas potrzebny na wykonanie pętli. Słowo void przed definicją funkcji mówi nam, że funkcja nie zwraca sobą żadnych parametrów jako rezultat działania. I tutaj słowo wyjaśnienia.  Funkcja w języku C może zwracać tylko jedną wartość. Nie może zwrócić ich kilku tak, jak procedura języka Pascal poprzez „var”. Jeśli zachodzi potrzeba aby funkcja zwracała więcej niż jeden wynik działania, można to zrobić na przykład przekazując wskaźnik do parametru. Wówczas obliczenia wykonywane są bezpośrednio na zmiennych źródłowych. Inną metodą jest przekazanie wskaźnika do listy parametrów lub samej listy. Działania wewnątrz funkcji, o ile nie są wykonywane na wskaźnikach, wykonywane są lokalnie i dotyczą wyłącznie zmiennych zawartych pomiędzy nawiasami klamrowymi {}, oznaczającymi jej początek i koniec.
Parametr unsigned int k, to po prostu liczba milisekund do „odczekania”. Słowo unsigned oznacza, że liczba k jest dwubajtową liczbą  bez znaku, czyli przyjmuje wartości tylko i wyłącznie dodatnie. Ciało funkcji zawiera również deklaracje zmiennych pomocniczych j oraz k. Posłużą nam one do budowy pętli for.  Ciekawy jest w języku C jej zapis:  for (j = 0; j < k; j++).Co oznacza: wstaw do zmiennej j liczbę 0 (j = 0;), następnie dopóki j jest mniejsze od k (j < k;) zwiększaj wartość j o 1 (j++) wykonując działanie opisane za nawiasem, w tym przypadku jest to następna pętla for.
Zwróćmy uwagę, że o ile w RC-51, przed deklaracją typu int, musimy użyć słowa unsigned, o ile ma to być liczba bez znaku, o tyle słowo to jest zbędne w przypadku typu char.Domyślnie kompilator zakłada, że mamy do czynienia z liczbą unsigned char. Uwaga: jest to cecha kompilatora RC-51 firmy Raisonance. Używając innego, można się spotkać z innymi rozwiązaniami (np. firma Keil stosuje typ uchar). Oczywiście można przed typem char dopisać słowo unsigned. Wówczas jest to zgodne ze specyfikacją ANSI C i prawdopodobnie będzie działać tak samo, bez względu na typ użytego kompilatora.
Po Delay znajduje się funkcja WriteByteToLcd. Tak samo, jak Delay jest to funkcja typu void. Jej zadaniem jest zapis jednobajtowej liczby X do rejestru wyświetlacza. Funkcja dokonuje podziału bajtu na połówki i zapisuje je w bezpieczny sposób – wykorzystując tylko bity b4..b7 i nie uszkadzając zawartości b0..b3 - do portu PORT.  Być może podział bajtu to nie jest właściwe słowo - przyjrzyjmy się metodzie.
Dzięki funkcji sumie bitowej OR, cztery najstarsze bity portu PORT, w tym przypadku zadeklarowanego jako P2, ustawiane są na „1”. Zapis PORT |= 0xF0 można bowiem przekształcić na równoważny mu PORT = PORT | 0xF0. Właściwy zapis zmiennej do portu następuje poprzez funkcję AND, jednak po uprzednim ustawieniu dolnej połówki zapisywanego bajtu, na wartość „1” (X | 0x0F). Zapis PORT &= (X | 0x0F) można rozłożyć na następujący szereg działań:

- X = X | 0x0F        //dolne 4 bity portu X przyjmują wartość „1”
- PORT = PORT & temp  //dolne 4 bity portu PORT pozostają nienaruszone
                      //górne przyjmują wartość X

Nawias jest konieczny, ponieważ suma bitowa OR musi być wykonana przed iloczynem zapisującym połówkę bajtu do portu mikrokontrolera. W identyczny sposób postępujemy z dolną połówką bajtu z tym, że wykonywane jest przesunięcie w lewo o 4 pozycje, bitów zmiennej X (X <<= 4, to znaczy X = X << 4).
Funkcja zapisując dane do LCD, nie testuje stanu flagi busy wyświetlacza zakładając, że po 1 milisekundzie wszystkie operacje wykonywane przez kontroler wyświetlacza, zostaną zakończone. Wywołanie Delay(1) wprowadza konieczne opóźnienie.
Kolejnymi są funkcje o nieco przydługich nazwach WriteToLcdCtrlRegister i LcdWrite. Ich zadaniem jest zapis parametru X do LCD przy odpowiedniej kombinacji sygnałów sterujących. Obie funkcje korzystają z omówionej wcześniej WriteByteToLcd.Ponieważ określiliśmy w deklaracjach, że zmienne LcdReg, LcdRead i LcdEnable to są bity portu wyjściowego mikrokontrolera, więc podstawienie wartości „1” lub „0” jest odpowiednikiem rozkazów asemblera SETB i CLR. Prawidłowy (i szczególnie użyteczny, gdy mamy do czynienia z dużą liczbą zmiennych) jest również zapis LcdReg = LcdRead = 0; Kompilator podzieli go na pojedyncze operacje SETB i CLR. Zapis w takiej postaci nie ma więc wpływu na wielkość generowanego kodu wynikowego, poprawia jednak czytelność programu.
Dalsze funkcje są bardzo podobne. Na pewną nowość natkniemy się dopiero w GotoXY. Jej rolą jest obliczenie i ustawienie właściwego dla pozycji kursora x, y adresu zapisu bajtów do wyświetlacza. Wykorzystuje ona polecenie switch do rozpatrzenia poszczególnych możliwych wartości y i wyboru odpowiedniej akcji.

switch(y)
{
    case 0:
        x += 0x80;
        break;
    case 1:
        x += 0xC0;
        break;
        case 2:
        x += 0x94;
        break;
    case 3:
        x += 0xD4;
}

Ze switch ściśle związane są case i break. Case funkcjonuje tak, jak etykieta danego warunku, break przerywa rozpatrywanie warunków. Jeśli nie użyjemy polecenia break, wówczas nastąpi przejście do następnego warunku i jego rozpatrzenie. W naszym przypadku jest to tylko strata czasu. Jedynym zadaniem funkcji GotoXY jest bowiem zwiększenie wartości x tak, aby wskazywała pożądany adres – nie ma potrzeby dalszego analizowania zmiennej y. Jeśli nasz y będzie miał wartość 0, wówczas do wartości x zostanie dodana liczba 80H. Jeśli 1, to C0H, jeśli 2 to 94H i tak dalej.
Kilka słów komentarza. Przechodzenie od jednego przypadku do drugiego budzi trochę wątpliwości. Niewątpliwie zaletą jest to, że można definiować wiele różnych warunków dla jednej akcji. Jednak taka konstrukcja programu, która umożliwia przechodzenie od warunku do warunku, jest bardzo podatna na „rozsypanie” się podczas jej modyfikacji. Z wyjątkiem wielu etykiet dla pojedynczej akcji, przechodzenie przez przypadki, powinno być stosowane bardzo oszczędnie i zawsze opatrzone komentarzem. Do dobrego stylu programowania należy wstawianie break po ostatniej instrukcji ostatniego przypadku, mimo że nie jest to konieczne. Pewnego dnia, gdy dopiszesz na końcu jakiś inny przypadek, ta odrobina zapobiegliwości może cię uratować.
Funkcja WriteTextXY wykorzystuje wskaźnik. Jest to wskaźnik do elementu typu char. Jego definicję możemy bardzo łatwo odróżnić po symbolu *. Zauważmy, że do funkcji jako parametr nie jest przekazywany tekst do wyświetlenia, a jedynie wskazanie (adres) do miejsca w pamięci RAM (lub ROM), gdzie ten tekst został umieszczony. Wskaźnik ma rozmiar tylko 2 bajtów nie tak jak tekst, który może zająć znacznie więcej. Operacja S++; przesuwa wskazanie na następny znak w łańcuchu.  Kompilator sam dab o to, aby zwiększanie wskaźnika powodowało wskazanie na następny znak. Nie musisz przejmować się liczbą bajtów inkrementacji o ile wskaźnik ma przypisane wskazanie do określonego typu elementu. Pętla while kończy się, gdy wskaźnik S pokaże znak o kodzie 0. Znak ten umieszczany jest przez kompilator zawsze na końcu tekstu. Równocześnie z S zwiększana jest współrzędna x. Jeśli wartość x przekroczy maksymalną ilość znaków w wierszu, następuje zwiększenie y i przejście do następnej linii na wyświetlaczu LCD.  Podobnie funkcjonuje DefineSpecialCharacters jest jednak prostsza, bo nie oblicza żadnych wartości x i y a jedynie przesuwa wskazania na następny bajt tablicy. Również i tutaj w pętli while napisać można warunek while (*ptr), ponieważ pojawienie się znaku o kodzie 0, while traktuje jako warunek końca (0 jest równoważne false). Jednak dla większej czytelności programu i wyraźnego zaakcentowania w jaki sposób kończy się tablica definicji, został użyty zapis while (*ptr != 0). W funkcji DefineSpecialCharactersznaleźć możemy jeszcze dwie nowe konstrukcje, których nie używaliśmy wcześniej.
Pierwsza z nich to słowo kluczowe void objęte nawiasami jako parametr funkcji. Oznacza to tylko tyle, że lista parametrów funkcji jest pusta. Druga, to przypisanie w wywołaniu funkcji, w programie głównym main(), wskaźnikowi ptr adresu tablicy CGRom. Jednoargumentowy operator & tym razem nie oznacza iloczynu logicznego, czy bitowego – podaje nam adres tablicy CGRom w pamięci mikrokontrolera.
Jak już wspomniałem przy okazji omawiania przerwań, program główny w C jest to funkcja o nazwie main. W programach pisanych dla mikrokontrolerów najczęściej jest ona typu void z pustą listą parametrów (również void). Należy jednak pamiętać o tym, że program napisany dla mikrokontrolera nigdy nie może się skończyć. Nawet jeśli zrobił on już swoje i nie ma żadnych dalszych funkcji do realizacji, to funkcję main należy zakończyć pętlą nieskończoną taką, jak: while(1) albo for(;;). Mikrokontroler nie posiada bowiem żadnego systemu operacyjnego takiego jak DOS, który po zakończeniu pracy programu przejmie kontrolę. Najprawdopodobniej nasz program po zakończeniu pracy napotka na przykład tablicę danych umieszczoną za kodem, którą potraktuje jak rozkazy języka asembler i zrobi coś zupełnie nieprzewidywalnego.

Pliki nagłówkowe (.h)

Rozważmy teraz pewną możliwość. Mamy już napisany program w języku C, mamy w nim pewne funkcje, o których wiadomo, że przydadzą nam się również w wielu innych przypadkach. Chociażby nasz pierwszy program sterujący wyświetlaczem – czy użyjemy go tylko do wyświetlania symbolu „piłeczki” odbijającej się na ekranie LCD? Z całą pewnością przyda się na również do innych zastosowań. Pojawia się więc pytanie : czy można z takiego programu zrobić swego rodzaju bibliotekę funkcji, którą będzie można dołączyć do własnego programu i używać zawsze wtedy, gdy jest potrzebna? A co z modyfikacją pewnych parametrów procedur, czy zawsze trzeba mieć dostęp do źródła programu i szukać nierzadko wśród kilkuset linii programu tego parametru, który ma być zmieniony? Odpowiedzią na tak postawione pytania są tak zwane pliki nagłówkowe (z angielskiego header files), które umożliwiają podzielenie programu na mniejsze fragmenty oraz mogą zawierać definicje stałych i zmiennych używanych zarówno przez program główny, jak i przez biblioteki funkcji. W języku C, zbiory nagłówkowe, wyróżnia rozszerzenie nazwy .H (na przykład lcd4bit.h). Tak może wyglądać plik nagłówkowy utworzony dla biblioteki funkcji wyświetlacza LCD z poprzedniego przykładu.

// port,do którego dołączono wyświetlacz LCD
#definePORT         P2
// bity sterujące LCD
sbit LcdEnable =   PORT^0;
sbit LcdRead =     PORT^3;
sbit LcdReg =       PORT^2;
// opóźnienie około k*1 milisekundy dla kwarcu 7,3728 MHz
void Delay (unsigned int k);
// zapis bajtu do lcd
void WriteByteToLcd(char X);
// zapis bajtu do rejestru kontrolnego LCD
void WriteToLcdCtrlRegister(char X);
// zapis bajtu do pamięci obrazu
void LcdWrite(char X);
// czyszczenie ekranu LCD
void LcdClrScr(void);
// inicjalizacja wyświetlacza LCD w trybie 4 bity
void LcdInitialize(void);
// ustawia kursor na współrzędnych x,y
void GotoXY(char x, char y);
// wyświetla tekst na współrzędnych x, y
void WriteTextXY(char x, char y, char *S)
// wyświetla tekst od miejsca, w którym znajduje się kursor
void WriteText(char *S);
// definiowanie znaków z tablicy wskazywanej przez ptr
void DefineSpecialCharacters(char *ptr);

Jak widać jest to bardzo krótki zbiór tekstowy zawierający podstawowe definicje zmiennych i funkcji. Zbiór ten dołączamy do programu głównego przy pomocy dyrektywy #include, podobnie jak postępowaliśmy z definicją rejestrów mikrokontrolera (notabene też jest to zbiór nagłówkowy). W takiej sytuacji, program źródłowy naszej biblioteki nie może zawierać funkcji main(). Funkcja ta zostanie zdefiniowana w naszym programie głównym.
Tworząc pliki nagłówkowe trzeba zachować ostrożność. Można bowiem samemu stworzyć pewien „bałagan” polegający na tym, że maleńki nawet program będzie miał dostęp do wielu danych, których w praktyce nie potrzebuje. Będzie to rzutować również na rozmiar ostatecznego kodu wynikowego oraz utrudni swego rodzaju zachowanie porządku w deklaracjach zmiennych i funkcji. Więcej o plikach nagłówkowych w następnym opisywanym przykładzie programu w języku C. Aby wykorzystać bowiem mechanizm tworzenia plików nagłówkowych, musimy się nauczyć jak tworzyć project files i do czego one służą.

 

Jacek Bogusz
j.bogusz@easy-soft.net.pl

http://www.tomaszbogusz.blox.pl/

ZałącznikWielkość
Źródło programu z artykułu (obsluga-lcd-4bit.zip)2.14 KB

Dodaj nowy komentarz

Zawartość pola nie będzie udostępniana publicznie.