Uwaga! Ta strona wysyła Ci "ciasteczko".
Użytkowników online:
1
Kursy>Kurs AVR-GCC>Kurs AVR-GCC, cz.5
printer_icon

Kurs AVR-GCC cz.5

29.03.2010 ABXYZ

Ostatnio omawiane były tablice i funkcje, a jeszcze wcześniej: zmienne, pętle i instrukcje warunkowe. W tej części kursu tematem przewodnim będzie tekst i działania na tekście. Napiszę też kilka zdań na temat preprocesora języka C. Kolejnym omawianym zagadnieniem będzie podział kodu źródłowego programu na oddzielnie kompilowane pliki. W części praktycznej będziemy bawić się alfanumerycznym wyświetlaczem LCD, przyłączymy do AVRa termometr cyfrowy ds18b20, a dalej połączymy AVRa z komputerem PC poprzez port szeregowy RS232.

Programy z tekstem

Dotąd w przykładach z kursu używane były jedynie zmienne liczbowe. A co z tekstem ? Oczywiście tekst przechowuję się w pamięci komputera również w postaci liczb. Po prostu małym i wielkim literom alfabetu, cyfrom oraz wszystkim innym znakom przyporządkowuje się kolejne liczby z pewnego zakresu. Zwykle jeden znak zajmuje w pamięci komputera jeden bajt (osiem bitów), najczęściej używanym bywa kodowanie ASCII lub jego rozszerzenia.

kody ascii
Tablica kodów ASCII. Literom alfabetu, cyfrom oraz wszystkim innym znakom przyporządkowuje się kolejne liczby. Kliknij w obrazek, żeby obejrzeć całość.

Inaczej inż jest w wielu innych językach programowania w C nie przewidziano specjalnego typu zmiennej przeznaczonego do przechowywania tekstu. W języku C do przechowywania tekstu wykorzystuje się tablice typu char. Aby zapamiętać tekst kolejne pola tablicy wypełniane są kodami ASCII znaków tworzących tekst.

obrazek
Tablica wypełniona kodami ASCII kolejnych liter tworzących napis "Siemka!". W języku C tekst przechowuje się po prostu w tablicach typu char.

W kodzie źródłowym programu można posługiwać się stałymi znakowymi i stałymi napisowymi. Stała znakowa ma postać znaku objętego pojedynczymi cudzysłowami i posiada wartość liczbową kodu ASCII tego znaku.

int jeden_znak ; char jakis_napis[7]; /* W zmiennej jeden_znak znajdzie się wartość 65(kod ASCII znaku A) */ jeden_znak = 'A'; /* Zapisujemy tekst do tablicy znak po znaku */ jakis_napis[0] = 'S'; jakis_napis[1] = 'i'; jakis_napis[2] = 'e'; jakis_napis[3] = 'm'; jakis_napis[4] = 'k'; jakis_napis[5] = 'a'; jakis_napis[6] = '!';

Stałe napisowe tworzy się obejmując fragment tekstu parą podwójnych cudzysłowów. Definiując tablicę znakową można ją jednocześnie zainicjować stałą napisową. Tym sposobem tablica, w momencie jej tworzenie, zostanie wypełniona kodami ASCII kolejnych znaków tworzących napis.

/*Tworzona tablica zostanie wypełniona ciągiem znaków */ char jakis_napis[] = "Siemka!"; /* Zawartość tablicy: jakis_napis[0] = 'S' jakis_napis[1] = 'i' jakis_napis[2] = 'e' jakis_napis[3] = 'm' jakis_napis[4] = 'k' jakis_napis[5] = 'a' jakis_napis[6] = '!' jakis_napis[7] = 0x0 */

W przykładzie wyżej, w tablicy za ostatnim znakiem napisu, kompilator dodatkowo wstawi bajt o wartości zero. Znak o kodzie zero pełni tu rolę znacznika końca ciągu znaków. Jest zasadą w języku C że ciągi znaków kończą się znakiem o kodzie równym zero. Tekst może mięć dowolną długość, aby się tylko zmieścił w  tablicy wzraz z ograniczającym go bajtem zero.

Jeżeli w stałej napisowej potrzeba wstawić znak podwójnego cudzysłowu, to należy go poprzedzić znakiem backslash (\"). A jeśli chcemy wstawić sam znak backslash, to należy wpisać dwa znaki backslash (\\). Są to tzw. sekwencje specjalne zaczynające się od znaku backslash, dalej jeszcze będę o nich pisał.

/* Do tablicy zapisany zostanie ciąg znaków: abcdef"gh\i\jklmnop"qrs'tuv'wxyz */ char jakis_napis[] = "abcdef\"gh\\i\\jklmnop\"qrs'tuv'wxyz";

Jeśli jakaś funkcja oczekuje jako argumentu tablicy typu char, to jako argument, zamiast nazwy tablicy, można wstawić stałą napisową.

/* Definicja tablicy */ char tablica[]="KURS AVR-GCC"; /* Definicja przykładowej funkcji, która jak argumentu oczekuje tablicy typu char */ void funkcja(char tablica[]) { } int main(void) { /* Wywołanie funkcji */ funkcja(tablica); /* Jako argument można wstawić stałą napisową */ funkcja("KURS AVR-GCC");

W języku C brakuje również operatorów przeznaczonych do działań na tekście, takie operacje jak porównywanie czy łączenie napisów pozostaje zaprogramować samemu. Nie jest to nic specjalnie trudnego, oto kilka przykładów prostych operacji na tekstach:

Wykorzystując instrukcję pętli można porównywać dwa ciągi znaków.

/* Przyrównanie ciągu znaków */ unsigned char i; char str1[]= "KURS AVR-GCC"; char str2[]= "KURS AVR-GCC"; for(i=0; str1[i]==str2[i] && str1[i]; i++); /* Jeśli warunek spełniony, to porównywane ciągi znaków róźnią się */ if(str1[i] || str2[i])

Podobne używając instrukcji pętli można połączyć dwa lub więcej napisów w jeden tekst.

/* Łączenie ciągów znaków */ unsigned char i,j; char str1[]= "KURS"; char str2[]= " AVR-GCC"; char str3[]= " cz.5"; char buffer[18]; /* Łączy trzy ciągi znaków . Całość zostanie zapisana w tablicy 'buffer[]' */ for(i=0,j=0; buffer[j]=str1[i]; i++,j++); for(i=0 ; buffer[j]=str2[i]; i++,j++); for(i=0 ; buffer[j]=str3[i]; i++,j++);

A tak z pomocą instrukcji pętli for można wyznaczyć długość ciągu znaków zakończonego zerem.

/* Obliczanie długości ciągu znaków */ char s[] = "KURS AVR-GCC"; unsigned char i; /* Zmienna 'i' zawierać będzie długość ciągu znaków w tablicy 's[]' . Bajt o wartości zero na końcu ciągu nie jest liczony. */ for(i=0; s[i]; i++);

Jeżeli zamierzamy na wyświetlaczu alfanumerycznym pokazać wartość zmiennej liczbowej, to koniecznym będzie zamienić wartość liczbową na ciąg znaków. Kawałek kodu poniżej zmienia 16-bitową liczbę całkowitą bez znaku na odpowiadający jej ciąg cyfr (kodów ASCII cyfr). Wartość liczbowa w zmiennej 'a' jest cyklicznie w pętli dzielona przez 10, dopóki nie stanie się zerem - dzielenie całkowite. Obliczana w każdej iteracji pętli reszta z dzielenia stanowi cyfrę stojącą na kolejnej pozycji w liczbie, idąc w kierunku od cyfry najmniej znaczącej do najbardziej znaczącej. Reszta z dzielenia (liczba z zakresu 0..9) zmieniana jest na kod ASCII cyfry przez dodanie do niej wartości kodu ASCII cyfry zero (48).

/* Zmiana liczby na ciąg znaków ASCII */ signed char i; unsigned int a ; char buffer[6]; a = 65535; /* Wypełnia tablicę 'buffer[]' kodami ASCII cyfr skaładających się na liczbę w zmiennej 'a' */ for(i=4,buffer[5]=0; a; a/=10,i--) buffer[i]= a%10 + '0'; for(; i>=0; i--) buffer[i] = ' ';

Opisane działania na tekstach można zrealizować również wykorzystując funkcje z biblioteki standardowej języka C.

Do przekształcenia wartości liczbowych na ciągi znaków i w ogóle do formowania komunikatów tekstowy użyteczne mogą być standardowe funkcje printf() i sprintf(); aby móc z nich skorzystać należy gdzieś na początku pliku wstawić polecenie:

#include <stdio.h>

Funkcje printf i sprintf różnią się od siebie tym, że sprintf zapisuje dane do tablicy, zaś printf do standardowego wyścia. Na kilku prostych przykładach wyjaśnię działanie funkcji sprintf.

char buf[32]; int t = 21; int p = 1013; int v = 10; /* W tablicy 'buf' zostanie zapisany ciąg znaków: 'Temperatura powietrza: 21°C' */ sprintf(buf,"Temp. powietrza: %d°C",t); /* W tablicy 'buf' zostanie zapisany ciąg znaków: 'T:21°C, P:1013hPa, V:10m/s' */ sprintf(buf,"T:%d°C, P:%dhPa, V:%dm/s",t,p,v);

Pierwszym argumentem funkcji sprintf() jest tablica znakowa, miejsce w pamięci, gdzie zostanie zapisany ciąg znaków zakończony zerem - tworzony komunikat. Drugim argumentem sprintf() jest ciąg znaków zawierający format komunikatu. Format zawiera stałą treść komunikatu wraz z tzw. "specyfikacjami przekształceń". Specyfikacje przekształceń są to takie "znaczki-krzaczki":) w rodzaju: %d, %4d, %u, %x, %6.1f i podobnie wyglądające, które zostaną w wyjściowym komunikacie zastąpione jakąś treścią. A czym ? W naszym przykładzie, w miejscu pierwszego wystąpienia specyfikacji %d zostanie wstawiona wartość trzeciego argumentu funkcji sprintf, czyli wartość zmiennej 't' wypisana w postaci liczby dziesiętnej. Tak samo następne występujące w formacie specyfikacje zostaną zmienione wartościami kolejnych argumentów funkcji sprintf. Funkcją sprintf nie ma ustalonej liczby argumentów, więc w formacie komunikatu można umieszczać dowolną ilość specyfikacji przekształceń. Na temat funkcji o zmiennej liczbie argumentów napiszę jak się nadarzy okazja.

Specyfikacje przekształcenia zaczynają się od znaku procenta % i kończą znakiem przekształcenia. Na przykład: %d -zastąpione zostanie liczbą całkowitą; %u-liczbą całkowitą bez znaku; o-liczbą ósemkową; %x-liczbą szesnastkową; c-jednym znakiem; $s-ciągiem znaków; %f-liczbą niecałkowitą(zmiennoprzecinkową) w postaci: część całkowita, kropka, część ułamkowa (np.: 12.345). Aby wypisać sam znak procent % wstawia się dwa znaki procent %%. Następne przykłady przekształceń: %6d -liczba całkowita zajmująca co najmniej 6 znaków (na przykład: [][][]123); %6.2f -liczba zmiennopozycyjna o długości 6 znaków i z dwiema cyframi po przecinku (na przykład: []12.34)

char buf[32]; double l = 12.3456; int valve = 80; /* W tablicy 'buf' zostanie zapisany ciąg znaków: 'Valve: 80%' */ sprintf(buf,"Valve%6d%%",valve); /* W tablicy 'buf' zostanie zapisany ciąg znaków: 'Length= 12.34' */ sprintf(buf,"Length=%6.2f",l);

Opisałem tu jedynie część możliwości funkcji sprintf(), po resztę odsyłam do podręcznika języka C i do dokumentacji biblioteki AVRLibc.

Ale uwaga, użycie w programie dla 8-bitowego mikrokontrolera dość rozbudowanych funkcji sprintf, printf skutkuje znaczącym wzrostem wielkości kodu wynikowego. W AVRLibC domyślnie, celem oszczędzania pamięci, funkcje printf, sprintf nie obsługują liczb zmiennoprzecinkowych. Aby włączyć obsługę liczb zmiennoprzecinkowych uruchamiamy program MFile, wczytujemy plik makefile naszego projektu i w menu "Makefile->printf()options" zaznaczamy opcję "floating point".

obrazek
Okno programu MFile. Opcja "floating point" włącza dla funkcji printf obsługę zmiennych zmiennopozycyjnych.

Kto wcześniej uczył się języka C pisząc programy na komputer PC, ten na pewno pamięta funkcję printf, jej używa się najczęściej aby coś napisać na ekranie tekstowym. Funkcja printf różni się od omawianej wcześniej sprintf tym, że wysyła formatowany komunikat do tzw. standardowego sterumienia wyjściowego (stdout); funkcja sprintf zapisuje wynik do tablicy znakowej.

obrazek
Funkcja printf wysyła dane do standardowego wyjścia. Domyślnie na komputerze PC standardowe wyście to ekran tekstowy.

W chwili uruchomienia programu tworzone są strumienie danych: standardowe wejście(stdin), standardowe wyjście(stdout) i standardowe wyjście dla komunikatów o błędach(stderr) W przypadku programów uruchamianych na komputerze PC standardowy strumień wyjściowy kierowany jest domyślnie na ekran monitora, ale może być też przekierowany do innego urządzenia jak drukarka lub plikiem na dysku; standardowe wejście domyślnie połączone jest z klawiaturą komputera PC. W przypadku programów uruchamianych na mikrokontrolerze, gdy brak monitora i klawiatury, można powiązać standardowe strumienie danych (stdin i stdout) z portem szeregowym uC. Praktycznie wszystkie mikrokontrolery posiadają wbudowane układy transmisji szeregowej. Można podłączyć przewodem uC z komputerem PC poprzez port szeregowy RS232C i komunikować się z  mikrokontrolerem używając monitora i klawiatury.

W stałych znakowych i napisowych można umieszczać tzw. sekwencje specjalne. Sekwencje specjalne zaczynają się od znaku backslash "\", przykładowo sekwencja "\n" to znak nowego wiersza (LF Line Feed, kod ASCII 0x0A). Jeśli tekst składa się z wielu wierszy, to każdy wiersz zakończony jest znakiem nowego wiersza.

W przykładzie poniżej uruchomiłem programik, który instrukcją printf wypisuje trzy linijki tekstu. Dalej wypisany tekst pokazany jest w postać kodów ASCII. Czerwonym kolorem podkreśliłem sekwencje specjalne \n w tekście i odpowiadające im kody ASCII 0x0A.

Newline
Znak nowego wiersza '\n' kodowany jest bajtem o wartości 0x0A. Kliknij w obrazek, żeby obejrzeć całość

Programik ten został skompilowany i uruchomiony w systemie Linux, ale jeśli ten sam programik skompilujemy w produkcie o nazwie "Windows", to znak nowej linii \n kodowany będzie z użyciem dwóch bajtów: 0x0D,0x0A.

Newlinewine
W Windowsie znak nowego wiersza '\n' kodowany jest z użyciem dwóch bajtów: 0x0D, 0x0A. Kliknij w obrazek, żeby obejrzeć całość

Preprocesor języka C

Nie należ mylić poleceń preprocesora z instrukcjami programu, polecenia preprocesora zaczynają się znakiem hash "#". Preprocesor przystępuje do działania jeszcze przed właściwą kompilacją i automatycznie edytuje tekst źródłowy programu. Preprocesor potrafi wykonywać kilka rodzajów prostych ale użytecznych operacji na tekście. W programach najczęściej można spotkać polecenia #include i #define. Polecenie #include, w miejscu jego wystąpienia, wkleja zawartość innego wskazanego pliku tekstowego. Jeżeli nazwa dołączanego pliku jest objęta parą cudzysłowów, wtedy plik poszukiwany jest w katalogu projektu.

#include "plik.h"

Jeżeli nazwa wklejanego pliku objęta jest parą znaków <>, wtedy plik poszukiwany jest w katalogu, gdzie znajdują się standardowe pliki nagłówkowe.

#include <stdio.h>

Zwykle nazwy plików dołączanym poleceniem #include posiadają rozszerzenie .h i nazywane są plikami nagłówkowymi. A co zawierają dołączane pliki? Mogą zawierać dowolny kod w języku C, zwykle zawierają deklaracje funkcji i różne makrodefinicje.

W najprostszym sposobie użycia polecenie #define (makrodefinicja) zastępuje w tekście źródłowym programu każde wystąpienie wskazanej nazwy na inny podany ciąg znaków - podobnie jak działa opcja "Zmień" typowego edytora tekstu.

#define NAZWA zastępujący ciąg znaków

Zastępujący ciąg znaków rozciąga się do końca linii, aby go kontynuować w kolejnych liniach tekstu, należy każdą przedłużaną linię zakończyć znakiem backslash \ .

#define NAZWA zastępujący\ ciąg znaków

Polecenie #define działa od miejsca wystąpienia do końca pliku albo do wystąpienia polecenia #undef NAZWA, a zawartość stałych napisowych jest przy zastępowaniu pomijana. Przyjęło się, że nazwy w makrodefinicji pisane są wielkimi literami aby się odróżniały od zmiennych i stałych programu.

Dla pokazania jak działa polecenie #define skompilowałem niewielki programik z opcją kompilatora -E. Przy wywołaniu kompilatora GCC z opcją -E, proces tłumaczenia kodu źródłowego programu zatrzymuje się po przejściu preprocesora i w wyniku otrzymujemy plik z tekstem programu przetworzonym tylko przez preprocesor. Warto zapamiętać tę opcję, może się przydać przy szukaniu błędów.

obrazek
Efekt działania polecenia #define. Programik w pliku zabawa.c skompilowałem z opcją kompilatora -E. W wyniku kompilator GCC zwrócił plik zabawa.txt zawierający tekst źródłowy programu przetworzony jedynie przez preprocesor.

A teraz praktyczny przykład wykorzystania makrodefinicji. Przypuśćmy, że jest dioda LED, która ma coś sygnalizować, przyłączona do jednego z portów we/wy AVRa. A po całym programie rozsiane są instrukcje włączające lub wyłączające diodę LED w rodzaju:

PORTD |= (1<<PD0); PORTD &= ~(1<<PD0);

W instrukcjach tych bezpośrednio wskazano nazwę portu i numer bitu. Taki sposób pisania programu nie jest dobry. Bo jeżeli zdecydujemy się na zmiany w projekcie i na nowym schemacie dioda LED będzie przyłączona do innego wyprowadzenia niż poprzednio, to wtedy trzeba będzie w całym programie wszystkie instrukcje sterujące diodą LED odszukać i zmodyfikować. Lepiej odrazu pisać programy w taki sposób, aby w przyszłości, przy zmianie schematu, modyfikacja programu nie nastręczała wielkich problemów. I właśnie do tego celu może się przydać prepreprocesor. W przykładowym programie poniżej można wskazać port, do którego przyłączono diodę LED, edytując makrodefinicje na początku programu. Poleceniem #define zdefiniowany trzy nazwy: SET_OUT_LED, SET_LED, CLR_LED, które preprocesor zastąpi odpowiednim kodem. Nazwy te mogą być używane jak instrukcje programu, SET_LED włącza diodę LED, CLR_LED - wyłącza, SET_OUT_LED ustawia port, do którego przyłączono diodę LED, jako wyjście.

#include <avr/io.h> /* Początkowo diodę LED przyłączono do wyprowadzenia PD0 */ /* #define SET_OUT_LED DDRD |= (1<<PD0) #define SET_LED PORTD |= (1<<PD0) #define CLR_LED PORTD &= ~(1<<PD0) */ /* Schemat się zmienił, aktualnie dioda LED przyłączona jest do wyprowadzenia PB3 */ #define SET_OUT_LED DDRB |= (1<<PB3) #define SET_LED PORTB |= (1<<PB3) #define CLR_LED PORTB &= ~(1<<PB3) int main(void) { /* Ustawia PB3 jako wyjście */ /* Preprocesor zastąpi SET_OUT_LED instrukcją DDRB |= (1<<PB3) */ SET_OUT_LED; /* Jakiś kawałek kodu */ /* Zapala diodę LED */ /* Preprocesor zastąpi SET_LED instrukcją PORTB |= (1<<PB3) */ SET_LED; /* Jakiś kawałek kodu */ /* Gasi diodę LED */ /* Preprocesor zastąpi CLR_LED instrukcją PORTB &= ~(1<<PB3) */ CLR_LED; /* Dalsze instrukcje programu */

Istnieje możliwość tworzenia makrodefinicji z argumentami. Listę argumentów makra umieszcza się między parą nawiasów okrągłych (), podobnie jak w definicji funkcji. W przykładzie poniżej utworzone zostało użyteczne makro _BV(numer_bitu). W tekście programu każde wystąpienie _BV(numer_bitu) preprocesor zastąpi wartością (1<<numer_bitu), czyli jedynką przesuniętą w lewo o ilość pozycji podaną jako argument makra.

#include <avr/io.h> /* Makrodefinicja z argumentem */ #define _BV(bit) (1 << (bit)) int main(void) { /* Preprocesor zastąpi _BV(3) wyrażeniem (1 << (3))*/ PORTB |= _BV(3);

Nie ma potrzeby samemu definiować _BV(bit), jest już takie makro zdefiniowane w jednym z plików dołączanych poleceniem:

#include <avr/io.h>

Z pomocą instrukcji preprocesora można wskazać kompilatorowi, które fragmenty kodu programu mają być wzięte pod uwagę w procesie kompilacje, a które fragmenty kodu mają być pominięte. Nazywa się to kompilacją warunkową i używane są do tego celu polecenia: #if, #elif, #else, #endif oraz #ifdef, #ifndef. Z poleceń tych tworzy się konstrukcje w rodzaju:

#if WARUNEK_1 /* Fragment kodu włączany do porgramu jeśli spełniony jest WARUNEK_1*/ #elif WARUNEK_2 /* Fragment kodu włączany do porgramu jeśli spełniony jest WARUNEK_2*/ #else /* Fragment kodu włączany do programu jeśli żaden w warunków nie został spełniony*/ #endif

Gdzie #elif i #else nie muszą wystąpić. I tak, jeśli spełniony jest warunek stojący zaraz po #if lub #elif, wtedy następne linie kodu, aż do wystąpienia #endif, #elif lub #else, są włączane do programu. Jeśli żaden z warunków nie jest spełniony, wtedy częścią programu staje się fragment kodu między #else i #endif. W przykładzie poniżej zdefiniowano nazwę F_CPU z przypisaną częstotliwość pracy mikrokontrolera; zależnie od częstotliwości preprocesor włącza do programu jeden z trzech fragmentów kodu.

//#define F_CPU 1000000UL #define F_CPU 4000000UL int main() { #if F_CPU <= 1000000 /* Fragment kodu włączany do programu jeżeli F_CPU <= 1MHz */ #elif 1000000 < F_CPU && F_CPU <= 8000000 /* Fragment kodu włączany do programu jeżeli 1MHz < F_CPU <= 8MHz */ #else /* Fragment kodu włączany do programu jeżeli F_CPU > 8MHz*/ #endif

W warunku po #if i #elif można wstawiać wyrażenie defined(NAZWA), wyrażenie to przyjmuje wartość logiczną PRAWDA, jeżeli dana nazwa została wcześnie zdefiniowana poleceniem #define; w przeciwnym przypadku wyrażenie to posiada wartość logiczną FAŁSZ.

#define MICRO intmain() { #if defined(MICRO) /* Ten fragment kodu zostanie włączony do programu, bo wcześniej zdefiniowano nazwę MICRO */ #else /* Ten fragment kodu byłby włączony do programu, gdyby nie zdefiniowano nazwy MICRO */ #endif

Istnieją także polecenia #ifdef i #ifndef. Kod występujący po #ifdef NAZWA jest włączany do programu, jeżeli wcześniej zdefiniowano nazwę poleceniem #define NAZWA. Natomiast kod #ifndef NAZWA jest włączany do programu, jeżeli nie zdefiniowano wcześniej nazwy poleceniem #define NAZWA. Polecenie #if z warunkiem defined(NAZWA) można zastąpić #ifdef NAZWA.

Aby zapobiec sytuacji, że jakiś plik mógłby być włączony poleceniem #include wielokrotnie, treść dołączanego pliku umieszcza się między parą poleceń #ifndef i #endif.

/* Zawartość przykładowego pliku dołączanego poleceniem #include "plik.h"*/ /* Jeżeli wcześniej nazwa PLIK została zdefiniowana poleceniem #define PLIK, to dalsza część pliku nie zostanie włączona do programu */ #ifndef PLIK /* Definicja nazwy PLIK, aby zapobiec wielokrotnemu dołączaniu plik.h */ #define PLIK /* Deklaracje funkcji, makrodefinicje itp.*/ #endif /* KONIEC PLIKU */

Podział kodu źródłowego programu na osobno kompilowane pliki

Dotychczas, we wszytkich programikach naszego kursu, całość kodu źródłowego umieszczana była w jednym pliku, jak w poniższym przykładzie.

//----------------------------------------------------------- // plik "main.c" //----------------------------------------------------------- #include <avr/io.h> /* Definicje zmiennych globalnych */ char tytul[]="KURS AVR-GCC, cz.5"; int czesc = 5; /* Definicje kilku funkcji */ void funkcja_1(int a, int b) { } int funkcja_2(void) { funkcja_4(tytul); funkcja_1(czesc, 1); } double funkcja_3(double x) { funkcja_1(2, 6); funkcja_2(); } char funkcja_4(char s[]) { } /* Główna funkcja programu */ int main(void) { funkcja_1(czesc, 6); funkcja_2(); funkcja_3(3.14); funkcja_4(tytul); }

Ale istnieje możliwość podziału kodu źródłowego programu na mniejsze fragmenty umieszczane w osobno kompilowanych plikach. Przykładowo możemy podzielić nasz programik w następujący sposób: funkcja_1 i funkcja_2 do pliku "file1.c"; funkcja_3 i funkcja_4 do pliku "file2.c"; zmienne globalne i funkcja main do pliku "main.c"

//----------------------------------------------------------- // plik "file1.c" //----------------------------------------------------------- #include <avr/io.h> /* Deklaracje zmiennych i funkcji zdefiniowanych poza plikiem "file1.c" */ extern char tytul[]; extern int czesc; char funkcja_4(char s[]); void funkcja_1(int a, int b) { } int funkcja_2(void) { funkcja_4(tytul); funkcja_1(czesc,1); } //----------------------------------------------------------- // plik "file2.c" //----------------------------------------------------------- /* Deklaracje funkcji zdefiniowanych poza plikiem "file2.c" */ void funkcja_1(int , int ); int funkcja_2(void); double funkcja_3(double x) { funkcja_1(2,6); funkcja_2(); } char funkcja_4(char s[]) { return 0; } //----------------------------------------------------------- // plik "main.c" //----------------------------------------------------------- #include <avr/io.h> /* Definicje zmiennych globalnych */ char tytul[]="KURS AVR-GCC, cz.5"; int czesc = 5; /* Deklaracje funkcji zdefiniowanych poza plikem "main.c" */ void funkcja_1(int , int ); int funkcja_2(void); double funkcja_3(double); char funkcja_4(char []); /* Główna funkcja programu */ int main(void) { funkcja_1(czesc,6); funkcja_2(); funkcja_3(3.14); funkcja_4(tytul); }

Aby mieć możliwość użycia funkcji zdefiniowanej w innym pliku, należy gdzieś wcześniej w programie umieścić deklarację tej funkcji. Deklaracja funkcji wygląda prawie tak samo jak pierwsza linia definicji funkcji zakończona średnikiem.

typ nazwa_funkcja( typ_arg_1, typ_arg_2, ... );

Przypominam, że definicja oznacza tworzenie funkcji(zmiennej), natomiast deklaracja jedynie informuje kompilator jakiego typu wartość funkcja zwraca i jakich oczekuje argumentów. Zatem definicja funkcji jest jednocześnie jej deklaracją, ale deklaracja nie definiuje(tworzy) funkcji. I jak wcześniej pisałem, żeby mieć możliwość użycia funkcji zdefiniowanej(utworzonej) w oddzielnie kompilowanym pliku, należy wcześniej przed użyciem tę funkcje zdeklarować - właśnie, żeby poinformować kompilator jakiego typu wartość funkcja zwraca i jakich oczekuje argumentów.

Zmienne mogą być definiowane wewnątrz funkcji(zmienne lokalne) albo poza wszystkimi funkcjami(zmienne globalne), o tym pisałem w poprzedniej części kursu. Zmienne definiowane wewnątrz funkcji tworzone są w chwili wywołania funkcji i przestają istnieć w  momencie powrotu z funkcji, przechowywane dane są tracone. Przy każdym wywołaniu funkcji zmienne tworzone są od nowa. Jeśli zależy nam żeby zmienna deklarowane w funkcji istniała przez cały okres działania programu i dane w zmiennej nie były tracone po wyjściu z funkcji, to należy deklaracje zmiennej poprzedzić słówkiem static; takie zmienne nazywa się statycznymi. Zmienne deklarowane na początku pliku, poza wszystkimi funkcjami pliku istnieją przez cały czas działania programu i są dostępne we wszystkich funkcjach w pliku. Aby mieć dostęp do zmiennej globalnej zdefiniowanej w osobno kompilowanym pliku, należy przed użyciem wcześniej tę zmienną zdeklarować; dodatkowo przed taką deklaracją należy wstawić słówko extern.

extern typ_zmiennej nazwa_zmiennej;

Z kolei żeby ograniczyć zasięg widoczności takiej zmiennej jedynie do pliku, w którym została zdefiniowana, poprzeda się deklaracje zmienej słówkiem static.

static typ_zmiennej nazwa_zmiennej;

Podobnie jeśli definicję funkcji poprzedzimy słówkiem static, funkcja ta będzie dostępna tylko w tym jednym pliku, w którym została zdefiniowana

Zwykle deklaracje funkcji umieszcza się w osobnych plikach, tzw. plikach nagłówkowych z rozszerzeniem .h I wtedy, aby móc wykorzystać funkcję zdefiniowaną w innym pliku, dołącza się plik nagłówkowy zawierający definicję tej funkcji, wstawiając gdzieś na początku instrukcję preprocesora #include "nazwa.h"

//---------------------------------------------------------- // plik "file1.c" //---------------------------------------------------------- #include <avr/io.h> /* Dołącza plik zawierający deklaracje funkcji i zmiennych zdefiniowanych poza plikiem "file1.c" */ #include "rozne.h" void funkcja_1(int a, int b) { } int funkcja_2(void) { funkcja_4(tytul); funkcja_1(czesc,1); } //---------------------------------------------------------- // plik "file2.c" //---------------------------------------------------------- /* Dołącza plik zawierający deklaracje funkcji i zmiennych zdefiniowanych poza plikiem "file2.c" */ #include "rozne.h" double funkcja_3(double x) { funkcja_1(2,6); funkcja_2(); return 0; } char funkcja_4(char s[]) { } //---------------------------------------------------------- // plik "rozne.h" //---------------------------------------------------------- // Deklaracje kilku funkcji i zmniennych /* Poniższe polecenia preprocesora zapobiegną przypadkowi, w którym "rozne.h" mógłby być kilkakrotnie włączony do jednedo pliku */ #ifndef ROZNE_FUNKCJE #define ROZNE_FUNKCJE extern char tytul[]; extern int czesc; void funkcja_1(int, int); int funkcja_2(void); double funkcja_3(double); char funkcja_4(char []); #endif //--------------------------------------------------------- // plik "main.c" //--------------------------------------------------------- #include <avr/io.h> /* Dołącza plik zawierający deklaracje funkcji i zmiennych zdefiniowanych poza plikiem "main.c" */ #include "rozne.h" /* Definicje zmiennych globalnych */ char tytul[]="KURS AVR-GCC, cz.5"; int czesc = 5; /* Definicje funkcji */ static void funkcja(void) { } /* Główna funkcja programu */ int main(void) { funkcja_1(czesc,6); funkcja_2(); funkcja_3(3.14); funkcja_4(tytul); }

Aby skompilować nasz pokrojony programik, potrzebujemy utworzyć w katalogu projektu odpowiedni plik makefile. Robimy to w podobny sposób, jak poprzednio, z pomocą programu MFile. Wpierw klikamy w menu opcję Makfile->Main file name.. i w  okienku które się pojawi wpisujemy nazwę pliku "main" (wpisujemy nazwę pliku bez rozszerzenia .c) Dodatkowo należy wybrać z menu opcję Makefile->C/C++source files(s) i w okienku wpisać nazwy plików "file1.c" i "file2.c" (tym razem należy wpisać nazwy plików wraz z rozszerzeniami .c )

mfile
Okno programu MFile. Wpisujemy nazyw wszytkich plków *.c projektu. Kliknij w obrazek, żeby zobaczyć całość.

Przyjrzyjmy się teraz przebiegowi kompilacji naszego pokrojonego programiku. Program make wywołuje kompilator avr-gcc cztery razy. Wpierw wszystkie pliki projektu z rozszerzeniem .c (main.c, file1.c, file2.c) są pojedynczo kompilowane, w  wyniku powstają trzy plik pośrednie: main.o, file1.o, file2.o Za czwartym razem avr-gcc uruchamiany jest żeby połączyć wymienione pliki pośrednie w całość; tworzony jest plik wynikowy main.elf Na koniec uruchamiany jest program narzędziowy avr-objdump, który tworzy z pliku wynikowego main.elf pliki dla programatora main.hex i main.epp. Warto zauważyć, że przy kolejnej próbie kompilacji projektu program make kompiluje jedynie te plik .c, które zostały w międzyczasie edytowanie.

avrgcc
Etapy kompilacja projekty złożnego z kilku plików *.c Kliknij w obrazek, żeby obejrzeć całość.

Wyświetlacz alfanumeryczny LCD.

Dla uruchamiania przykładowych programików będzie potrzebny wyświetlacz alfanumeryczny LCD, moduł z układem HD44780. Ale bez obaw, takie wyświetlacze są łatwo dostępne i  nie kosztują drogo. Ja przyłączyłem do AVRa moduł widoczny na fotografii poniżej, na którym można wyświetlić dwie linie tekstu po szesnaście znaków, moduły 2X16 są chyba najczęściej spotykane. Do tego celu można również wykorzystać wyświetlacze o dwudziestu lub czterdziestu znakach w linii, lecz w takim przypadku pewnie potrzebne będą drobne modyfikacje w kodzie przykładów.

obrazek
Moduł wyświetlacza LCD 2X16 wykorzystany przy uruchomieniu przykładów.

W kursie nie będę szczegółowo tłumaczy jak programować tego typu wyświetlacz, jest to temat na osobny artykuł, który w przyszłości napiszę i umieszczę na stronie w dziale artykuły. W zamian przygotowałem zestaw gotowych funkcji do obsługi wyświetlaczy LCD ze sterownikiem HD44780. W przykładach, w których będzie wykorzystywany wyświetlacz, trzeba będzie skopiować do katalogu projektu plik: hd44780.c, hd44780.h Plik hd44780.c zawiera definicje funkcji do obsługi wyświetlaczy, zaś w pliku hd44780.h znajdują się deklaracje tych funkcji, makrodefinicje przypisujące sygnały wyświetlacza do wybranych wyprowadzeń AVRa oraz kilka przydatnych makroinstrukcji. Dalej w tekści, przy opisie uruchamianych przykładów, objśnię jak tych funkcji używać.

Poniżej na schemacie pokazane jest w jaki sposób przyłączyłem wyświetlacz do AVRa atmega16.

obrazek
Schemat 5.1 Schemat przyłączenia wyświetlacza LCD(hd44780) do AVRa ATMEGA16.

Dla sterowania wyświetlaczem potrzebne jest siedem linii we/wy mikrokontrolera: trzy linie sterujące: RS, RW, E i cztery linie danych D4,D5,D5,D7. Będziemy programować wyświetlacz w trybie 4-bitowym (tylko cztery linie danych), wyprowadzenia wyświetlacza D0, D1, D2 ,D3 nie będą wykorzystywane. Ja przyłączyłem wyświetlacz do portów PA0..PA6 uC atmega16, ale nic nie stoi na przeszkodzie aby wykorzystać dowolne siedem linii we/wy AVRa. W tym celu należy zmodyfikować makrodefinicje w poniższym fragmencie pliku hd44780.h

/* RS */ #define SET_OUT_LCD_RS DDRA |= _BV(PA0) #define SET_LCD_RS PORTA |= _BV(PA0) #define CLR_LCD_RS PORTA &= ~_BV(PA0) /* RW */ #define SET_OUT_LCD_RW DDRA |= _BV(PA1) #define SET_LCD_RW PORTA |= _BV(PA1) #define CLR_LCD_RW PORTA &= ~_BV(PA1) /* E */ #define SET_OUT_LCD_E DDRA |= _BV(PA2) #define SET_LCD_E PORTA |= _BV(PA2) #define CLR_LCD_E PORTA &= ~_BV(PA2) /* D4 */ #define SET_OUT_LCD_D4 DDRA |= _BV(PA3) #define SET_IN_LCD_D4 DDRA &= ~_BV(PA3) #define SET_LCD_D4 PORTA |= _BV(PA3) #define CLR_LCD_D4 PORTA &= ~_BV(PA3) #define IS_SET_LCD_D4 PINA & _BV(PA3) /* D5 */ #define SET_OUT_LCD_D5 DDRA |= _BV(PA4) #define SET_IN_LCD_D5 DDRA &= ~_BV(PA4) #define SET_LCD_D5 PORTA |= _BV(PA4) #define CLR_LCD_D5 PORTA &= ~_BV(PA4) #define IS_SET_LCD_D5 PINA & _BV(PA4) /* D6 */ #define SET_OUT_LCD_D6 DDRA |= _BV(PA5) #define SET_IN_LCD_D6 DDRA &= ~_BV(PA5) #define SET_LCD_D6 PORTA |= _BV(PA5) #define CLR_LCD_D6 PORTA &= ~_BV(PA5) #define IS_SET_LCD_D6 PINA & _BV(PA5) /* D7 */ #define SET_OUT_LCD_D7 DDRA |= _BV(PA6) #define SET_IN_LCD_D7 DDRA &= ~_BV(PA6) #define SET_LCD_D7 PORTA |= _BV(PA6) #define CLR_LCD_D7 PORTA &= ~_BV(PA6) #define IS_SET_LCD_D7 PINA & _BV(PA6)

Przykładowo, jeżeli linia sygnału D7 wyświetlacza ma być przyłącza do portu PD0 AVRa, wtedy należy zmodyfikować w poniższym fragmęcie pliku hd44780.h to, co zaznaczone jest na czerwono, czyli nazwę portu (A,B,C,D) i numer bitu.

/* D7 */ #define SET_OUT_LCD_D7 DDRD |= _BV(PD0) #define SET_IN_LCD_D7 DDRD &= ~_BV(PD0) #define SET_LCD_D7 PORTD |= _BV(PD0) #define CLR_LCD_D7 PORTD &= ~_BV(PD0) #define IS_SET_LCD_D7 PIND & _BV(PD0)

Termometr cyfrowy DS18B20

Obok wyświetlacza i przycisków przyłączymy do AVRa także termometr cyfrowy DS18B20. Wszystkie potrzebne informacje na temat układu DS18B20 można znaleźć w jego karcie katalogowej ds18b20.pdf

obrazek
Termometr cyfrowy ds18b20

Układ scalony DS18B20 jest czujnikiem cyfrowym z interfejsem 1-wire, mikrokontroler komunikuje się z DS18B20 wykorzystując tylko jedną linię we/wy.

obrazek
Schemat 5.2 Schemat przyłączenia termometru cyfrowego ds18b20 do AVRa ATMEGA16.

W tej części kursu jeszcze nie będę tłumaczył jak działa magistrala 1-wire i jak programować układ ds18b20, w zamian przygotowałem gotowy zestaw funkcji - minimum kodu do odczytu wartości temperatury z ds18b20. W przykładach, w których będzie wykorzystywany termometr ds18b20 należy do katalogu projektu skopiować pliki: ds18b20.h i ds18b20.c. Ja przyłączyłem układ ds18b20 do wyprowadzenia PD7 AVRa atmega16. Ale można wykorzystać dowolny port AVRa, w tym celu należy zmodyfikować makrodefinicje w poniższym fragmencie pliku ds18b20.h Wystarczy odpowiednio zmienić, zaznaczone kolorem czerwony, nazwę portu(A,B,C,D) i numer bitu (0..7).

/* DS18B20 przyłączony do portu PD7 AVRa */ #define SET_ONEWIRE_PORT PORTD |= _BV(7) #define CLR_ONEWIRE_PORT PORTD &= ~_BV(7) #define IS_SET_ONEWIRE_PIN PIND & _BV(7) #define SET_OUT_ONEWIRE_DDR DDRD |= _BV(7) #define SET_IN_ONEWIRE_DDR DDRD &= ~_BV(7)

Sprzężenie AVRa z komputerem PC poprzez port szeregowy

Kolejny schemat przedstawia sposób połączenia portu szeregowego AVRa atmega16 z interfejsem RS232C komputera PC.

schemat
Schemat 5.3 Schemat połączenia portu szeregowego AVRa atmega16 z interfejsem rs232c komputera PC. Kliknij w obrazek, żeby powiększyć.

Potrzebne są: złącze DB-9 żeńskie, przewód trójżyłowy (około 60cm), układ scalony MAX232 i kilka kondensatorów jak na schemacie. Do połączenia portu szeregowego mikrokontrolera z portem szeregowym rs232 komputera PC konieczny jest konwerter napięć RS232C<=>TTL - na przykład układ scalony MAX232. Ja umieściłem MAX232 i współpracujące z nim kondensatory na osobnej płytce.

obrazek
Przewód łączący AVRa z komputerem PC i na osobnej płytce układ MAX232 wraz ze współpracującymi z nim kondensatorami.

Niektóre komputery, te nowsze, mogą nie posiadać portu rs232c. Brak ten można obejść stosując adapter, przejściówkę z USB na RS232. Taka przejściówka kosztuje niewiele, a  może być bardzo użyteczna, szczególnie do laptoka :)

Przykłady do uruchomienia

Przygotowałem cztery przykładowe programiki do uruchamiania jako ćwiczenia. Starałem się maksymalnie uprościć kod przykładów, zgodnie z zasadą: dobry przykład to krótki przykład :) Ala zalecam przed przystąpieniem do uruchamiania przykładów chociaż przejrzeć artykuł, wtedy nikt nie powinien mieć trudności ze zrozumieniem jak działają.

Przykład pierwszy - Powitanie

W pierwszym przykładzie program wypisuje na ekranie wyświetlacza kilka słów powitania.

obrazek
Animacja pokazuje efekt działania programu "Powitanie"

Żeby skompilować program, należy do katalogu projektu skopiować trzy zamieszczone poniżej pliki: main.c, hd44780.h, hd44780.c W pliku "main.c" znajduje się kod naszego przykładu.

/*
   Plik main.c

   KURS AVR-GCC cz.5
   Wyświetlacz alfanumeryczny LCD HD44780
   (schemat i opis działania w artykule)   
   
   układ atmega16 (1MHz)
*/

#include <avr/io.h>
#include <util/delay.h>
/* Wstawia w tym miejscu zawartość 
pliku hd44780.h*/
#include "hd44780.h"


int main(void)
{
    /* Napisy przechowujemy w tablicach */
    char str1[] = "KURS";
    char str2[] = " AVR-GCC";
    char str3[] = "cz.5";

    /* Funkcja inicjalizuje wyświetlacz*/
    lcd_init();
    /* Włącza wyświetlanie */
    LCD_DISPLAY(LCDDISPLAY);

    while(1)
    {    
        /* Czyści cały ekran */
        LCD_CLEAR;            

        /* Ustawia kursor w pozycji: 
        pierwszy wiersz, szósta kolumna */
        LCD_LOCATE(5,0);

        /* Wysyła do wyświetlacza jeden znak*/
        LCD_WRITE_DATA('S');
        _delay_ms(200);

        LCD_WRITE_DATA('i');
        _delay_ms(200);

        LCD_WRITE_DATA('e');
        _delay_ms(200);

        LCD_WRITE_DATA('m');
        _delay_ms(200);    

        LCD_WRITE_DATA('k');
        _delay_ms(200);

        LCD_WRITE_DATA('a');
        _delay_ms(200);

        LCD_WRITE_DATA('!');
        _delay_ms(2000);    

        LCD_CLEAR;

        LCD_LOCATE(2,0);

        /* Funkcja lcd_puts wysyła do 
        wyświetlacza ciąg  znaków */
        lcd_puts(str1);
        _delay_ms(800);    

        lcd_puts(str2);
        _delay_ms(800);    

        LCD_LOCATE(6,1);
        lcd_puts(str3);    

        _delay_ms(2000);
        LCD_CLEAR; 

        LCD_LOCATE(1,0);
        /* Jako argumentu funkcji można 
           wstawić stałą napisową */
        lcd_puts("Programy");

        _delay_ms(800);

        LCD_LOCATE(4,1);
        lcd_puts("z tekstem:)");
        
        /* Czeka 2.5 sek. */
        _delay_ms(2500);    
        
    }
    return 0;
}
Listing 5.1 Powitanie

Plik hd44780.c zawiera zestaw funkcji do obsługi wyświetlacza.

/*
  Plik hd44780.c

  Definicje kilku funkcji do obsługi alfanumerycznego
  wyświetlacza LCD HD44780
*/


#include<avr/io.h>
#include<util/delay.h>
#include "hd44780.h"

/*--------------------------------------------------------*/
/* Zapis danej lub instrukcji */

void WriteToLCD (unsigned char v,unsigned char  rs)
{
    unsigned char bf;

    SET_OUT_LCD_D4;
    SET_OUT_LCD_D5;
    SET_OUT_LCD_D6;
    SET_OUT_LCD_D7;

    if(v&0x10) SET_LCD_D4; else CLR_LCD_D4;
    if(v&0x20) SET_LCD_D5; else CLR_LCD_D5;
    if(v&0x40) SET_LCD_D6; else CLR_LCD_D6;
    if(v&0x80) SET_LCD_D7; else CLR_LCD_D7;
 
    CLR_LCD_E;
    if(rs) SET_LCD_RS;else CLR_LCD_RS;
    CLR_LCD_RW;

    LCD_NOP;
    SET_LCD_E;
    LCD_NOP; 
    CLR_LCD_E;
    LCD_NOP;
 
    if(v&0x01) SET_LCD_D4; else CLR_LCD_D4;
    if(v&0x02) SET_LCD_D5; else CLR_LCD_D5;
    if(v&0x04) SET_LCD_D6; else CLR_LCD_D6;
    if(v&0x08) SET_LCD_D7; else CLR_LCD_D7;
 
    LCD_NOP;
    SET_LCD_E;
    LCD_NOP; 
    CLR_LCD_E;
    LCD_NOP;
 
    SET_IN_LCD_D4;
    SET_IN_LCD_D5;
    SET_IN_LCD_D6;
    SET_IN_LCD_D7;

    CLR_LCD_RS;
    SET_LCD_RW;
    SET_LCD_D7;


/* Przydałby się pełny odczyt */
    do
    {
        LCD_NOP;
        SET_LCD_E;
        LCD_NOP;
        bf = IS_SET_LCD_D7;
        CLR_LCD_E;
        LCD_NOP;
        SET_LCD_E;
        LCD_NOP;
        LCD_NOP;
        CLR_LCD_E;
        
    }while( bf );
}


/*--------------------------------------------------------*/
/* Funkcja odczytuje adres i flage zajetosci */

unsigned char ReadAddressLCD ( void)
{
    unsigned char g = 0 ;

    CLR_LCD_RS;
    SET_LCD_RW; 

    SET_IN_LCD_D4;
    SET_IN_LCD_D5;
    SET_IN_LCD_D6;
    SET_IN_LCD_D7;

    LCD_NOP;
    SET_LCD_E;
    LCD_NOP;

    if(IS_SET_LCD_D4) g+=16;
    if(IS_SET_LCD_D4) g+=32;
    if(IS_SET_LCD_D4) g+=64;
    if(IS_SET_LCD_D4) g+=128;
 
    CLR_LCD_E;
    LCD_NOP;
    SET_LCD_E;  
    LCD_NOP;
  
    if(IS_SET_LCD_D4) g+=8;
    if(IS_SET_LCD_D4) g+=4;
    if(IS_SET_LCD_D4) g+=2;
    if(IS_SET_LCD_D4) g+=1;
  
    CLR_LCD_E; 

    return  g ;
}


/*---------------------------------------------------------*/
/* Inicjalizacja wyświetlacza */

void lcd_init(void)
{
    _delay_ms(31);    
   
    SET_OUT_LCD_RS;
    SET_OUT_LCD_RW;
    SET_OUT_LCD_E;
    SET_OUT_LCD_D4;
    SET_OUT_LCD_D5;
    SET_OUT_LCD_D6;
    SET_OUT_LCD_D7;

    CLR_LCD_E;
    CLR_LCD_RS;
    CLR_LCD_RW;
    SET_LCD_D4;
    SET_LCD_D5;
    CLR_LCD_D6;
    CLR_LCD_D7;        
  
    LCD_NOP;
    SET_LCD_E;
    LCD_NOP; 
    CLR_LCD_E;
    LCD_NOP;
    _delay_ms(10);

    LCD_NOP;
    SET_LCD_E;
    LCD_NOP; 
    CLR_LCD_E;
    LCD_NOP;
    _delay_ms(2);

    LCD_NOP;
    SET_LCD_E;
    LCD_NOP; 
    CLR_LCD_E;
    LCD_NOP;
    _delay_ms(2);

    CLR_LCD_D4;
    LCD_NOP;
    SET_LCD_E;
    LCD_NOP; 
    CLR_LCD_E;
    LCD_NOP;
    _delay_us(80);

    WriteToLCD (0x28 , LCDCOMMAND) ;
    LCD_DISPLAY(0) ;
    LCD_CLEAR ;
    LCD_ENTRY_MODE(LCDINCREMENT) ;
}


/*--------------------------------------------------------*/
/* Wyswietla tekst na aktualnej pozycji kursora */

void lcd_puts(char *str)
{
    unsigned char i =0;

    while( str[i])
        LCD_WRITE_DATA(str[i++]) ;
}
Listing 5.2 Zestaw funkcje do obsług wyświetlacza LCD (hd44780)

W pliku hd44780.h znajdują się deklaracje funkcji do obsługi wyświetlacza, makrodefinicje przypisujące sygnały wyświetlacza do wybranych wyprowadzeń AVRa oraz kilka przydatnych makroinstrukcji.

/*
Plik hd44780.h
*/

#ifndef LCD_HD44780
#define LCD_HD44780

/* RS */
#define SET_OUT_LCD_RS  DDRA  |=  _BV(PA0)
#define SET_LCD_RS      PORTA |=  _BV(PA0)
#define CLR_LCD_RS      PORTA &= ~_BV(PA0)

/* RW */
#define SET_OUT_LCD_RW  DDRA  |=  _BV(PA1)
#define SET_LCD_RW      PORTA |=  _BV(PA1)
#define CLR_LCD_RW      PORTA &= ~_BV(PA1)

/* E */
#define SET_OUT_LCD_E   DDRA  |=  _BV(PA2)
#define SET_LCD_E       PORTA |=  _BV(PA2)
#define CLR_LCD_E       PORTA &= ~_BV(PA2)

/* D4 */
#define SET_OUT_LCD_D4  DDRA  |=  _BV(PA3)
#define SET_IN_LCD_D4   DDRA  &= ~_BV(PA3)
#define SET_LCD_D4      PORTA |=  _BV(PA3)
#define CLR_LCD_D4      PORTA &= ~_BV(PA3)
#define IS_SET_LCD_D4   PINA  &   _BV(PA3)

/* D5 */
#define SET_OUT_LCD_D5  DDRA  |=  _BV(PA4)
#define SET_IN_LCD_D5   DDRA  &= ~_BV(PA4)
#define SET_LCD_D5      PORTA |=  _BV(PA4)
#define CLR_LCD_D5      PORTA &= ~_BV(PA4)
#define IS_SET_LCD_D5   PINA  &   _BV(PA4)

/* D6 */
#define SET_OUT_LCD_D6  DDRA  |=  _BV(PA5)
#define SET_IN_LCD_D6   DDRA  &= ~_BV(PA5)
#define SET_LCD_D6      PORTA |=  _BV(PA5)
#define CLR_LCD_D6      PORTA &= ~_BV(PA5)
#define IS_SET_LCD_D6   PINA  &   _BV(PA5)

/* D7 */
#define SET_OUT_LCD_D7  DDRA  |=  _BV(PA6)
#define SET_IN_LCD_D7   DDRA  &= ~_BV(PA6)
#define SET_LCD_D7      PORTA |=  _BV(PA6)
#define CLR_LCD_D7      PORTA &= ~_BV(PA6)
#define IS_SET_LCD_D7   PINA  &   _BV(PA6)


#define LCD_NOP asm volatile("nop\n\t""nop\n\t" "nop\n\t" "nop\n\t" ::);



#define LCDCOMMAND 0
#define LCDDATA    1

#define LCD_LOCATE(x,y)  WriteToLCD(0x80|((x)+((y)*0x40)), LCDCOMMAND)

#define LCD_CLEAR              WriteToLCD(0x01, LCDCOMMAND)
#define LCD_HOME               WriteToLCD(0x02, LCDCOMMAND)

/* IDS */

#define LCDINCREMENT           0x02
#define LCDDECREMENT           0x00
#define LCDDISPLAYSHIFT        0x01

#define LCD_ENTRY_MODE(IDS)    WriteToLCD(0x04|(IDS), LCDCOMMAND)

/* BCD */
#define LCDDISPLAY             0x04
#define LCDCURSOR              0x02
#define LCDBLINK               0x01

#define LCD_DISPLAY(DCB)       WriteToLCD(0x08|(DCB), LCDCOMMAND)

/* RL */
#define LCDLEFT                0x00
#define LCDRIGHT               0x04

#define LCD_SHIFT_DISPLAY(RL)  WriteToLCD(0x18|(RL), LCDCOMMAND)
#define LCD_SHIFT_CURSOR(RL)   WriteToLCD(0x10|(RL), LCDCOMMAND)

#define LCD_CGRAM_ADDRESS(A)   WriteToLCD(0x40|((A)&0x3f), LCDCOMMAND)
#define LCD_DDRAM_ADDRESS(A)   WriteToLCD(0x80|((A)&0x7f), LCDCOMMAND)

#define LCD_WRITE_DATA(D)      WriteToLCD((D),LCDDATA)


void lcd_init(void);
void WriteToLCD(unsigned char v,unsigned char rs);
unsigned char ReadAddressLCD(void);
void lcd_puts(char *str);

#endif
Listing 5.3

Następnie należy utworzyć w katalogu projektu odpowiedni plik Makefile, z pomocą programu Mfile, tak jak robiliśmy to przy poprzednich przykładach. Dodatkowo należy wybrać z menu programu Mfile opcję Makefile->C/C++source files(s) i w okienku wpisać nazwę pliku: hd44780.c (nazwę pliku trzeba wpisać wraz z rozszerzeniem .c)

mfile
Okno programu MFile. Wpisujemy nazwy wszytkich plików *.c projektu. Kliknij w obrazek, żeby zobaczyć całość.

A jeszcze musimy wpisać w pliku Makefile częstotliwość sygnału taktującego procesor; częstotliwość wpisujemy ręcznie, gdyż brak takiej opcji w menu. Aby móc edytować treść tworzonego pliku Makefile trzeba zaznaczyć w menu "Makefile" opcję "Enable Editing of Makefile"

Odnajdujemy w nowo tworzonym pliku Makefile fragment zaczynającym się od:

#Processor frequency

I wpisujemy częstotliwość sygnału taktującego procesor

F_CPU = 1000000
obrazek
Okno programu MFile. Wpisujemy ręcznie częstotliwość pracy mikrokontrolera.

Przypominam. Jeżeli w programie wykorzystywane są funkcje _delay_ms lub _delay_us, to należy dodać informację o częstotliwość sygnału taktującego procesor, inaczej funkcje te nie będą działać prawidłowo. Informację tę można przekazać umieszczając na początku każdego pliku z kodem makrodefinicję

#define F_CPU 1000000UL

Jednak znacznie wygodniej jest wspiać wczęstotliwość pracy procesora w pliku Makefile, raz dla wszytkich plików programu, tak, jak w naszym przykładzie. Ale jest to tylko informacja dla kompilatora, żeby faktycznie zmienić częstotliwość zegara z jakim uC atmega pracuje, trzeba zaprogramować tzw fusebity.

Celem tego przykładu jest pokazanie w jaki sposób napisać cokolwiek na ekranie wyświetlacza z użyciem funkcji zapisanych w pliku hd44780.c Pierwsza rzecz to należy gdzieś na początku pliku programu wstawić polecenie preprocesora #include dołączające plik hd44780.h zawierający deklaracje funkcji zdefiniowanych w pliku hd44780.c

#include "hd44780.h"

W kolejnym kroku, zanim zaczniemy pisać na wyświetlaczu, trzeba wywołać funkcję lcd_init, funkcja ta realizuje procedurę programowej inicjalizacji wyświetlacza i następnie przełącza interfejs wyświetlacza do trybu 4-bitowego. Po wykonaniu funkcji lcd_init wyświetlacz zostaje wygaszony. Włączyć wyświetlanie można z pomocą makroinstrukcji LCD_DISPLAY.

/* Funkcja inicjalizuje wyświetlacz*/ lcd_init(); /* Włącza wyświetlanie */ LCD_DISPLAY(LCDDISPLAY);

Z pomocą makra LCD_LOCATE wskazujemy pozycje na ekranie wyświetlacza (kolumnę i wiersz), gdzie zamierzamy coś napisać. Kolumny i wiersze liczone są zaczynając od zera.

/* szósta kolumna, pierwszy wiersz */ LCD_LOCATE(5,0);

Pojedyncze znaki można wysyłać do wyświetlacza wykorzystując makro LCD_WRITE_DATA.

/* Wysyła do wyświetlacza jeden znak*/ LCD_WRITE_DATA('S');

Po wykonaniu LCD_WRITE_DATA numer kolumny zwiększa się o jeden, więc jeśli wyślemy kilka znaków jeden za drugim, to zobaczymy na ekranie wyświetlacza napis. Napisy (ciągi znaków zakończone zerem) można wysyłać do wyświetlacza funkcją lcd_puts. Funkcja lcd_puts po prostu wysyła do wyświetlacza znak po znaku cały napis wywołując w pętli makro LCD_WRITE_DATA. Jako argument funkcji lcd_puts wstawia się nazwę tabeli z tekstem lub stałą napisową.

/* Funkcja lcd_puts wysyła do wyświetlacza ciąg znaków */ lcd_puts(str1); /* Jako argumentu funkcji można wstawić stałą napisową */ lcd_puts("Programy");

Makro LCD_CLEAR czyści cały ekran wyświetlacza i ustawia aktualny numer kolumny i wiersza na 0.

/* Czyści cały ekran */ LCD_CLEAR;

I jeszcze kursor, tzw. znak zachęty, zachęcający do prowadzania danych z klawiaturki. Kursor ma postać poziomej kreski wyświetlanej pod kratką, gdzie ma zostać wpisany kolejny znak. Kursor można pokazać uruchamiając makro LCD_DISPLAY.

/* Włącza wyświetlanie i kursor */ LCD_DISPLAY(LCDDISPLAY|LCDCURSOR);

W następnej części kursu pokażę jeszcze jak uzyskać na ekranie wyświetlacza polskie znaki z "ogonkami" ąćęńłóśźż oraz przedstawię kilka innych, ciekawych możliwości jakie oferują tego typu wyświetlacze.

Przykład drugi - Licznik owiec

Po prostu licznik. Licząc owce lub gwiazdy na niebie lub cokolwiek innego nie trudno o pomyłkę, zatem jak widać taki licznik może być szalenie użyteczny:) Obok wyświetlacza dołączyłem do AVRa trzy przyciski, pierwszy przycisk zwiększa licznik o jeden, drugi przycisk zmniejsza licznik o jeden, a trzeci przycisk zeruje licznik. Przyciski przyłączone są do portów PB0..PB2 AVRa.

obrazek
Animacja pokazuje sposób działa program "Licznik owiec". Pierwszy przycisk zwiększa licznik o jeden, drugi zmniejsza o jeden, a trzeci przycisk zeruje licznik.

Działanie programu jest bardzo proste, jest w pamięci zmienna całkowita (32 bity) - licznik. Każdorazowo przy zmianie stanu licznika wartość liczbowa w zmiennej zmieniana jest na ciąg znaków (kodów ASCII), który jest wysyłany do wyświetlacza.

Aby skompilować program, należy do katalogu projektu, obok pliku main.c, skopiować pliki: hd44780.h, hd44780.c i oczywiście stworzyć odpowiedni plik Makefile - tak jak w poprzednim przykładzie.

/*
   Plik "main.c"

   KURS AVR-GCC cz.5 (przykład nr. 2)
   Licznik owiec :)
   (schemat i opis działania w artykule)   
   
   uC atmega16 (1MHz)
*/

#include <avr/io.h>
#include <util/delay.h>
/* Dołącza deklaracje funkcji obsługujących 
   wyświetlacz */
#include "hd44780.h"


/* ZMIENNE GLOBALNE */

/* W tablicy będą formowane komunikaty 
   wysyłane do wyświetlacza */
unsigned char str1[17]="----------------";


/* DEFINICJE FUNKCJI */

/* Funkcja aktualizuje zawartość ekranu */
static void lcd(unsigned long int a)
{
    signed char i;
   
/* Zamiana 32 bitowej liczby bez znaku 
   na ciąg znaków ASCII */    
    for(i=12; i>=3; a/=10 ,i--) 
                str1[i] = a % 10 +'0';

/* Ustawia kursor w pierwszej kolumnie
   pierwszego wersza */    
    LCD_LOCATE(0,0);

/* Wysyła do wyświetlacza ciąg znaków z 
   tablicy str1 */
    lcd_puts(str1);
}


/* GŁÓWNA FUNKCJA */
int main(void)
{

  /* Zmienna przechowuje stan licznika */    
  unsigned long int n=0;

  /* PB0,PB2  wejściami z podciągnięciem do VCC */
  DDRB  = 0x00;
  PORTB = 0x07;
  
  /* Programowa inicjalizacja wyświetlacza */
  lcd_init();
  /* Włącza wyświetlanie */
  LCD_DISPLAY(LCDDISPLAY);  
  /* Czyści  ekran */
  LCD_CLEAR;            
  /* Wyświetla początkowy stan licznika */
  lcd(n);
  

  /* Główna pętla  */
  while(1)
  {
     /* Jeśli pierwszy przycisk wciśnięty */
     if(!(PINB & 0x01))
     {  
/* Czas na wygaśnięcie drgań styków przycisku*/     
        _delay_ms(80);
    /* Oczekuje na zwolnienie przycisku*/
        while(!(PINB & 0x01)) {}
/* Czas na wygaśnięcie drgań styków przycisku*/  
    _delay_ms(80);
        
    /* Zwiększa licznik o 1 */
        n++;
    /* Aktualizuje zawartość ekranu */
     lcd(n);
    }

    /* Jeśli drugi przycisk wciśnięty */
    else if(!(PINB & 0x02))
    {
        _delay_ms(80);
        while(!(PINB & 0x02)) {}
        _delay_ms(80);

    /* Zmniejsza licznik o 1 */
    n--;
    /* Aktualizuje zawartość ekranu */
     lcd(n);
    }
    /* Jeśli trzeci przycisk wciśnięty */
    else if(!(PINB & 0x04))
    {
        _delay_ms(80);
        while(!(PINB & 0x04)) {}
        _delay_ms(80);
        
    /* Zeruje licznik */
    n=0;
    /* Aktualizuje zawartość ekranu */
     lcd(n);
    }
  }

}
Listing 5.4 Licznik owiec

Przykład trzeci - termometr cyfrowy

W tym przykładzie, obok wyświetlacza LCD, dołączyłem do AVRa scalony termometr cyfrowy DS18B20. AVR komunikuje się z układem DS18B20 poprzez szeregową magistralę 1-wire. Termometr DS18B20 jest inteligentnym czujnikiem cyfrowym, w wyniku pomiaru otrzymujemy gotową wartość liczbową - temperaturę wyskalowaną w stopniach Celsjusza. Jak pisałem wcześniej, w tej części kursu