Kurs AVR-GCC cz.3
W poprzedniej części kursu objaśniałem jak programować równoległe porty we/wy układów AVR, bo porty równoległe to najprostszy i podstawowy sposób komunikacji mikrokontrolera z otoczeniem. W tej i dwóch kolejnych częściach kursu postaram się przedstawić podstawy języka C, oczywiście w dużym skrócie, gdyż trudno byłoby opisać szczegółowo całość języka C w trzech krótkich artykułach. Ale nie ma się czego obawiać, na szczęście język C nie jest zbyt obszerny, szybko można opanować jego postawy, które pozwolą samodzielnie pisać programy.
Tematem tej części kursu będą: zmienne i stałe liczbowe, operatory oraz instrukcje sterujące. Wpierw omówię kolejno wymienione tematy, a dalej, jako ćwiczenie, uruchomimy kilka przykładowych programów.
Zmienne i typy danych
Zakładam, że nie ma potrzeby objaśniać czym są zmienne w programie, wiadomo, zmienne przechowują dane. Każda zmienna posiada własną nazwę (identyfikator) oraz przypisany jeden z dostępnych w języku C typów. Typ zmiennej określa rodzaj przechowywanej danej, np. znak, liczba całkowita, liczba rzeczywista oraz wielkość obszaru pamięci zajmowanego przez zmienną, np. 8, 16, 32 bity.
Podstawowe typy danych w języku C to:- char - jeden znak (8-bitowa liczba całkowita);
- int - liczba całkowita;
- short int - liczba całkowita krótka;
- long int - liczba całkowita długa;
- long long int - liczba całkowita bardzo długa;
- float - liczba rzeczywista (typ zmiennopozycyjny);
- double - typ zmiennopozycyjny podwójnej precyzji;
- long double - typ zmiennopozycyjny rozszerzonej precyzji.
W języku C należy każdą zmienną przed użyciem zdefiniować, definicję zmiennej można rozumieć jako tworzenie zmiennej. Zmienne definiuje się wpisując w linii typ zmiennej, a po nim nazwę zmiennej lub nazwy kilku zmiennych rozdzielone przecinkami; definicję kończymy średnikiem. Definiując zmienną można jednocześnie ją zainicjować umieszczając zaraz po nazwie zmiennej znak "=", a po nim wartość początkową dla zmiennej. Znaczenie ma miejsce definicji zmiennych, dokładnie objaśnię to przy temacie funkcji. W programach z tej części kursu będziemy definiować zmienne na początku funkcji main. Przykłady:
Nazwy zmiennych mogą składać się z liter, z cyfr i znaku "_", ale nie mogą zaczynać się od cyfry; wielkość liter ma znaczenie, np. "temp" i "Temp" to dwie różne zmienne. Nie można używać w nazwach zmiennych polskich liter: ąćęłńóśźż ĄĆĘŁŃÓŚŹŻ.
W nazwach typów: short int, long int, long long int można pominąć słówko int, np. typ long oznacza typ long int. Dodając przed nazwę typu całkowitego słówko signed(unsigned) informujemy kompilator, że powinien traktować wartości w zmiennej jako liczby ze znakiem(bez znaku). Liczby ze znakiem (signed) zapisywane są na bitach w kodzie uzupełnień do dwóch U2(two's complement).
Zależnie od używanego kompilatora języka C, rozmiary poszczególnych typów zmiennych mogą się różnić. Przykładowo na procesorach 16-bitowych typ całkowity int zwykle ma rozmiar 16 bitów, na procesorach 32-bitowych - 32 bity. W przypadku mikroprocesorów 8-bitowych typ int ma zwykle rozmiar 16 bitów, tak też jest w AVR-GCC; zgodnie ze standardem języka C typ int nie może mieć mniej niż 16 bitów. W AVR-GCC zmienne wszystkich typów zmiennopozycyjnych: float, double, long double kodowane są na 32 bitach, czyli faktycznie jest dostępny jedynie typ zmiennopozycyjny pojedynczej precyzji (zgodny ze standardem IEEE754). Zapewne wynika to z faktu, że operacje na liczbach zmiennopozycyjnych większej precyzji byłyby zbyt dużym obciążeniem dla 8-bitowych mikroprocesorów. W tabeli poniżej wypisałem rozmiary podstawowych typów zmiennych w kompilatorze AVR-GCC.
Typ | Rozmiar (bitów) | Wartość minimalna | Wartość maksymalna |
---|---|---|---|
char | 8 | ||
signed char | 8 | -128 | 127 |
unsigned char | 8 | 0 | 255 |
short int | 16 | -32768 | 32767 |
unsigned short int | 16 | 0 | 65535 |
int | 16 | -32768 | 32767 |
unsigned int | 16 | 0 | 65535 |
long int | 32 | -231 | 231-1 |
unsigned long int | 32 | 0 | 232-1 |
long long int | 64 | -263 | 263-1 |
unsigned long long int | 64 | 0 | 264-1 |
float | 32 | ±1.18·10-38 | ±3.4·1038 |
double | 32 | ±1.18·10-38 | ±3.4·1038 |
long double | 32 | ±1.18·10-38 | ±3.4·1038 |
W języku C nie istnieje specjalny typ zmiennych dla wartości logicznych, zwyczajnie jeśli wartość jakiejś zmiennej równa się zeru, to zmienna posiada wartość logiczną FAŁSZ, w przeciwnym przypadku zmienna posiada wartość logiczną PRAWDA. Przykład:
Ogólna zasada. W języku C jeśli wartość liczbowa stałej, zmiennej lub dowolnego wyrażenia jest różna od zera, wtedy stała, zmienna, wyrażenie ma wartość logiczną PRAWDA, w przeciwnym przypadku ma wartość logiczną FAŁSZ.
Chwilowo tyle informacji o zmiennych wystarczy.
Stałe liczbowe
W programie można używać stałych liczbowych całkowitych w postaci dziesiętnej, szesnastkowej i ósemkowej; można też używać stałych typu zmiennopozycyjnego. Postać szesnastkową tworzymy wstawiając przed liczbą parę znaków 0x lub 0X (np. 0xFF); stałe ósemkowe rozpoczynają się od cyfry 0 (np. 077); a stałe całkowite w systemie dziesiątkowym piszemy zwyczajnie (np. 123). Stałe zmiennopozycyjne zawierają dziesiętną kropkę (np. 3.14), mogą też zawierać wykładnik (np. 123.45E3= 123450)
Domyślnie stałe całkowite są typu int, jeśli wartość stałej nie mieści się w typie int, wtedy stała otrzymuje typ long int lub long long int. Można nadawać stałym typ long int dopisując na ich końcu literę L lub l; a dopisując litery LL lub ll, typ long long int. Podobnie dopisując na końcu stałych całkowitych literę U lub u można nadać stałym typ bez znaku. Stałe zmiennopozycyjne są domyślnie typu double. Dopisując na końcu stałych zmiennopozycyjnych literkę F lub f można nadać im typ float; a dopisując literkę L lub l można nadać typ long double. Przykłady:
Rejestry I/O
A czym są nazwy rejestrów I/O, jak np.: PORTB, PINB, DDRB ? Przecież język C nic nie wie o rejestrach mikrokontrolerów AVR. W poprzedniej części napisałem kilka słów na temat preprocesora języka C, który może wykonywać różne operacje na tekście źródłowym programu jeszcze przed właściwą kompilacją. To właśnie preprocesor, przed kompilacją, zmienia w tekście programu nazwy rejestrów I/O na właściwy kod w języku C; na przykład instrukcja:
zostanie zmieniona przez preprocesor na kod:
Ale tylko, jeśli dołączy się do kodu programu pliki z definicjami rejestrów I/O, wstawiając gdzieś na początku programu linię:
A co oznacza ten fragment kodu ? Rejestry I/O układów AVR ulokowane są w przestrzeni adresowej pamięci danych, zaczynając od adresu 0x20, rejestr PORTB mieści się pod numerem 0x18 względem adresu 0x20 (patrz datasheet AVR-a) W AVR-GCC dostęp do rejestrów I/O jest za pośrednictwem wskaźnika do zmiennej typu uint8_t (unsigned char). Jeśli ostatnie dwa zdania są niezrozumiałe, to absolutnie proszę się tym nie przejmować i czytać dalej. Z tego trzeba zapamiętać, że rejestry IO, jak na przykład: PORTB, PINB, DDRB, posiadają typ "unsigned char".
Kodowanie U2.
Liczby całkowite ze znakiem (signed) zapisywane są w pamięci w kodzie uzupełnień do dwóch U2 (two's complement), liczby bez znaku (unsigned) w naturalnym kodzie binarnym NKB.
Proszę spojrzeć na wzory i tablice.
bity | wartości w kodzie NKB | wartości w kodzie U2 |
---|---|---|
0000 | 0 | 0 |
0001 | 1 | 1 |
0010 | 2 | 2 |
0011 | 3 | 3 |
0100 | 4 | 4 |
0101 | 5 | 5 |
0110 | 6 | 6 |
0111 | 7 | 7 |
1000 | 8 | -8 |
1001 | 9 | -7 |
1010 | 10 | -6 |
1011 | 11 | -5 |
1100 | 12 | -4 |
1101 | 13 | -3 |
1110 | 14 | -2 |
1111 | 15 | -1 |
bity | wartości w kodzie NKB | wartości w kodzie U2 |
---|---|---|
0000 0000 | 0 | 0 |
0000 0001 | 1 | 1 |
0000 0010 | 2 | 2 |
0000 0011 | 3 | 3 |
: | : | : |
0111 1101 | 125 | 125 |
0111 1110 | 126 | 126 |
0111 1111 | 127 | 127 |
1000 0000 | 128 | -128 |
1000 0001 | 129 | -127 |
1000 0010 | 130 | -126 |
: | : | : |
1111 1101 | 253 | -3 |
1111 1110 | 254 | -2 |
1111 1111 | 255 | -1 |
W naturalnym kodzie binarnym na n bitach można zapisać wartości z zakresu [0,2n-1]; czyli dla 4 bitów [0,15], dla 8 bitów [0,255], dla 16 bitów [0,65535].
Natomiast w kodzie U2 na n bitach można zapisać wartości z zakresu [-2n-1,2n-1-1]; czyli dla 4 bitów [-8,7], dla 8 bitów [-128 ,127], dla 16 bitów [-32768,32767].
W kodzie U2 najstarszy(pierwszy od lewej) bit liczby mówi o znaku, dla zera i wartości dodatnich najstarszy bit jest zerem, dla wartości ujemnych - jedynką. Aby zmienić znak liczby zapisanej w kodzie U2 można "odwrócić" wartości wszystkich bitów liczby (jedynki zmienić na zera, a zera na jedynki) i do uzyskanej wartości dodać jeden.
Przy dodawania/odejmowania liczb zapisanych w kodzie U2 przeprowadza się jednakowe operacje na bitach jak przy dodawaniu/odejmowaniu liczb zapisanych w NKB.
Operatory przypisania
Nową wartość można zapisać w zmiennej używając operatora przypisania "="; instrukcję przypisania należy zakończyć średnikiem. Przykłady:
Ta sama zmienna może stać po obu stronach operatora przypisania, przykład:
Nowicjuszom w programowaniu taki zapis może się wydać dziwny. Ale jest to instrukcja przypisania a nie równanie; w tym przykładzie w zmiennej h zostanie zapisana nowa wartość, która jest równa aktualnej wartości tej zmiennej powiększonej o 10. Tutaj lepiej byłoby posłużyć się operatorem przypisania +=, przykład:
Obok operatora += są do dyspozycji podobnie działające operatory przypisania: -=, *=, /=, %=, ^=, |=, &=, <<=, >>=.
Operatory arytmetyczne
W języku C istnieją operatory arytmetyczne:
+ | dodawania |
- | odejmowania |
* | mnożenia |
/ | dzielenia |
% | dzielenia modulo (reszta z dzielenia całkowitego) |
Jeśli oba argumenty operatora dzielenia / będą typu całkowitego, wtedy wynik operacji dzielenia będzie typu całkowitego i część ułamkowa wyniku będzie tracona. Resztę z dzielenia liczb całkowitych można wyliczyć posługując się operatorem % (dzielenia modulo). Inaczej jest, jeśli dzielna lub dzielnik albo oba argumenty operatora dzielenia / będą typu zmiennopozycyjnego (float, double), wtedy wynik dzielenia także będzie typu zmiennopozycyjnego. Przykłady:
Istnieją jeszcze jednoargumentowe operatory + , -, służące do zmiany znaku liczb; jednoargumentowy plus właściwie nic nie robi, jest tylko do pary :).
Operatory zwiększania i zmiejszania.
W języku C istnieją operatory zwiększające lub zmniejszające o jeden wartości zmiennych.
++ | zwiększ |
-- | zmniejsz |
Sposób działania tych operatorów zależy od tego, czy ++(--) znajdują się po lewej, czy po prawej stronie zmiennej. Jeśli operator ++(--) stoi po lewej stronie zmiennej, wartość zmiennej zwiększa(zmniejsza) się o jeden przed użyciem wartości zmiennej. Jeśli operator ++(--) stoi po prawej stronie zmiennej, wartość zmiennej zwiększa(zmniejsza) się o jeden po użyciu wartości tej zmiennej.
Przykłady:
Operatory logiczne, relacji i porównania
W języku C dostępne są operatory relacji:
> | większy |
>= | większy równy |
< | mniejszy |
<= | mniejszy równy |
Operatory przyrównania:
== | równy |
!= | różny |
Operatory logiczne:
&& | AND |
|| | OR |
! | operator negacji |
Wynikiem działania operatorów: relacji, przyrównania i logicznych są wartości liczbowe: 0 - gdy fałsz, 1 - gdy prawda. W języku C wartość liczbowa 0 oznacza jednocześnie wartość logiczną FAŁSZ, a wartość 1 i każda inna wartość liczbowa różna od zera oznacza wartość logiczną PRAWDA
Opisane w tym punkcie operatory zwykle wykorzystuje się w instrukcjach z warunkiem, jak np. if-else, while, for.
Szczególnie należy uważać, by zamiast operatora przyrównania == nie wpisać operatora przypisania = .
Operatory bitowe
W języku C istnieje sześć operatorów bitowych:
| | bitowe OR |
& | bitowe AND |
^ | bitowe XOR |
>> | przesunięcie w prawo |
<< | przesunięcie w lewo |
~ | dopełnienie jednykowe |
Operacje bitowe działają na wartościach całkowitych i służą do manipulowania bitami, więc są szczególnie użyteczne przy programowaniu sprzętu - przy zabawie z bitami rejestrów I/O.
Operatory bitowe & i | mogą się mylić z opisanymi wcześniej operatorami logicznymi && i ||, dlatego szczegółowo, na przykładach, wyjaśnię różnice w działaniu operatorów bitowych i logicznych. W poniższych przykładach liczby wypisane są w postaci dwójkowej.
Proszę zauważyć że, przesuwając bity liczby o jedną pozycję w lewo mnożymy wartość liczby razy dwa; a przesuwając o jedno pozycję w prawo, dzielimy liczbę przez dwa. Jeśli trzeba mnożyć(dzielić) liczby całkowite razy(przez) 2,4,8,2n, to można przesuwać bity. A po co? Przecież są operatory arytmetyczne mnożenia i dzielenia: *, /. Na przykład, żeby zoptymalizować program pod kątem szybkości. Przesuwanie bitów liczby jest o wiele prostszą operacją w porównaniu z mnożeniem i dzieleniem. Dla 8 bitowych mikroprocesorów operacje mnożenia i dzielenia mogą być znaczącym obciążeniem.
W liczbach ze znakiem(signed) najstarszy bit decyduje o znaku, więc w operacji przesuwania bitów w prawo liczb ze znakiem najstarszy bit pozostanie bez zmian. W przypadku przesuwania w prawo liczb bez znaku(unsigned) najstarszy bit otrzymuje wartość 0. Przykład:
Trójargumentowy operator warunkowy.
Trójargumentowy operator warunkowy ?: ma postać:
I działa w następujący sposób: Jeśli wartość wyrażenia wyr1 różni się od zera (logiczna wartość PRAWDA), wtedy zwraca wartość wyrażenia wyr2 (całość jest równa wartości wyr2), w przeciwnym razie zwraca wartość wyr3. Przykłady:
W przykładach warunek umieszczony jest w okrągłych nawiasach, podobnie jak w instrukcji if-else, nie jest to konieczne, ale poprawia czytelność kodu.
Priorytety i łączność operatorów
Priorytety i łączność operatorów decydują o kolejności wykonywania operacji przy obliczaniu wartości wyrażeń Przykładowo, operacje mnożenia i dzielenia mają wyższy priorytet niż dodawanie i odejmowanie (tak, jak w matematyce), a operatory przypisania mają niższy priorytet niż większość operatorów; więc w poniższym przykładzie wpierw wykona się mnożenie, następnie dodawanie i na końcu obliczona wartość wyrażenia zostanie zapisana w zmiennej 'wynik'.
Kolejnością wykonywanych działań można sterować używając okrągłych nawiasów (jak w matematyce).
W tabelce poniżej zestawione zostały wszystkie operatory języka C według malejących priorytetów. Operatory w jednym wierszu tabelki mają ten sam piorytet.
Uwaga, w języku C jeden symbol (np.: +, -, *, &) może oznaczać dwa różne operatory, zależnie od kontekstu. Czytając tabelkę należy wiedzieć, że jednoargumentowe operatory zmiany znaku: + , - posiadają wyższy priorytet niż dwuargumentowe operatory dodawania i odejmowania. Podobnie jednoargumentowe operatory & (adres obiektu) i * (dostęp za pośrednictwem wskaźnika) mają wyższy priorytet niż dwuargumentowe operatory & (bitowe AND) i * (mnożenie).
Operatory | Łączność |
---|---|
() [] -> . | lewostronna |
! ~ ++ -- + - * & (typ) sizeof | prawostronna |
* / % | lewostronna |
+ - | lewostronna |
<< >> | lewostronna |
<= < > >= | lewostronna |
== != | lewostronna |
& | lewostronna |
^ | lewostronna |
| | lewostronna |
&& | lewostronna |
|| | lewostronna |
?: | lewostronna |
= += -= *= /= %= ^= &= |= <<= >>= | prawostronna |
, | lewostronna |
Prawostronna łączność operatorów jednoargumentowych mówi, że argument stoi po prawej stronie operatora. Przykład:
Lewostronna (prawostronna) łączność dwuargumentowych operatorów oznacza, że jeżeli w wyrażeniu występuje więcej niż jeden operatorów o jednakowych priorytetach, wtedy wpierw wykonywany jest ten najbardziej z lewej(prawej). Na przykład operatory mnożenia(*), dzielenia(/) i operator obliczania reszty z dzielenia(%) mają jednakowy priorytet i są lewostronnie łączne, więc wyrażenie poniżej:
jest równoznaczne wyrażeniu:
Z kolei operatory przypisania są prawostronnie łączne i na przykład instrukcja w postaci:
jest równoważna instrukcji
Warto z tego zapamiętać, że całe wyrażenie przypisania, jak np:
też posiada wartość liczbową, równą wartości argumentu stojącego po prawej stronie operatora przypisania.
Instrukcje wyboru
Instrukcję wyboru if-else używaliśmy już w przykładach z poprzedniej części kursu, if-else ma postać:
Instrukcja if-else sprawdza czy warunek jest spełniony, tzn. czy wartość wyrażenia w nawiasach okrągłych po słówku if jest różna od zera. Jeśli tak, to zostanie wykonana instrukja znajdująca się zaraz za nawiasem ")"; w przeciwnym razie zostanie wykonana instrukcja po słówku else, część else można pominąć. Obejmując fragment kodu parą klamrowych nawiasów "{","}" tworzymy blok instrukcji, blok w "if-else" jest traktowany jako pojedyncza instrukcja. Przykład:
Jeśli potrzebna jest instrukcja if-else, ale z więcej niż dwoma wariantami kodu, to można użyć dwóch lub więcej instrukcji if-else zagnieżdżonych kaskadowo, jak w przykładzie poniżej.
Kolejna instrukcja wyboru switch ma postać:
Instrukcja switch działa w następujący sposób: Jeśli wyrażenie w nawiasach okrągłych, po słówku switch, jest równe wartości stałej po którymś wystąpieniu słówka case, wtedy wykonują się instrukcje wypisane po dwukropku, aż do wystąpienia instrukcji break, która kończy działanie całej instrukcji switch. Jeśli brak instrukcji break, wtedy wykonane zostaną instrukcje kolejnego wariantu, aż do momentu napotkania instrukcji break lub nawiasu klamrowego } kończącego całą instrukcję switch. Jeśli wartość wyrażenia nie pasuje(nie równa się) do żadnej stałej, wtedy wykonuają się instrukcje po słówku default, część default można w instrukcji switch pominąć. Przykład:
Kolejny przykład użycia switch:
Instrukcje pętli
W języku C pętle tworzy się instrukcjami:
- while,
- do-while,
- for.
Instrukcja while ma postać:
I działa w następujący sposób:
- Oblicza wartość wyrażenia w nawiasach okrągłych.
- Jeśli wartość liczbowa wyrażenia jest różna od zera, wykonuje instrukcje wewnątrz pętli, po czym przechodzi do punkut 1; w przeciwnym przypadku działanie instrukcji pętli jest zakończone.
Instrukcja do-while ma postać:
I działa w następujący sposób:
- Wykonuje instrukcje wewnątrz pętli.
- Oblicza wyrażenie w nawisach okrągłych. Jeśli wartość liczbowa wyrażenia jest różna od zera, przechodzi do punkut 1; w przeciwnym przypadku działanie instrukcji pętli jest zakończone.
Przykłady:
Instrukcja for ma postać:
I działa w następujący sposób:
- Oblicza wyrażenie_1.
- Oblicza wyrażenie_2.
- Jeśli wartość liczbowa wyrażenia_2 jest różna od zera (wartość logiczna PRAWDA), wykonuje instrukcję w pętli i przechodzi do punktu 4; w przeciwnym przypadku działanie instrukcji pętli jest zakończone.
- Oblicza wyrażenie_3 i przechodzi do punktu 2.
Na przykład, jeśli jest potrzeba, żeby jakiś fragment kodu wykonał się określoną ilość razy, można wtedy zbudować pętlę z użyciem instrukcji for.
W tym przykładzie użyto zmiennej "i" jako licznika iteracji pętli; przykład działa w następujący sposób:
- Zapisuje do zmiennej "i" wartość 0.
- Sprawdza czy wartość w zmiennej "i" jest mniejsza od 7; jeśli tak, przechodzi do następnego punktu, w przeciwnym wypadku kończy działanie pętli.
- Wykonuje instrukcje w pętli.
- Zwiększa wartość w zmiennej "i" o jeden.
- Przechodzi do punktu 2.
Kolejny przykład z instrukcją for. Instrukcje w pętli będą wykonywane dopóki wartość zmiennej "i" będzie mniejsza od wartości zmiennej "n" i jednocześnie bit numer 2 rejestru PINC będzie miał wartość 1. Zmienna "i" pracuje jako licznik iteracji pętli.
Działanie instrukcji pętli: while, do-while i for można wcześniej zakończyć używając instrukcji break, po "break" wstawiamy średnik. Przykład:
Istnieje jeszcze instrukcja continue, która powoduje pominięcie dalszych instrukcji w pętli i przejście do początku kolejnej iteracji pętli; po continue wstawiamy średnik.
Z wielokrotnie zagnieżdżonej pętli, jak w przykładzie poniżej, poręcznie jest "wyskoczyć" wykorzystują instrukcję goto. Przykład:
Instrukcja goto ma postać:
Po napotkaniu instrukcji goto następuje skok do miejsca w programie oznaczonego etykietą, etykietę powinna kończyć się dwukropkiem.
Z pomocą instrukcji goto można skakać w obrębie całej funkcji, a nawet tworzyć pętlę. Jednak nadużywanie goto prowadzi to powstania nieczytelnych, zagmatwanych kodów, więc zaleca się używania instrukcji goto tylko w tych sytuacjach, gdy w inny sposób nie da się tego napisać. Teoretycznie zawsze można się obejść bez użycia goto.
Przykładowe programy
Przygotowałem kilka przykładowych programów, bardzo prostych i chyba zabawnych :) . Proponuje, jako ćwiczenie i zabawę, uruchomić wszystkie.
Jak poprzednio, wszystkie te przykładowe programy napisane są według jednego, prostego schematu: Całość algorytmu zapisana jest w funkcji 'main', wpierw wykonują się instrukcje inicjujące (definicja zmiennych, konfiguracja portów we/wy itp.), a następnie program przechodzi do wykonania instrukcji umieszczonych w nieskończonej pętli - nazwę ją główną pętlą programu.
Schematy połączeń
W poprzedniej części kursu przykłady uruchamiane były na układzie atmaga8, teraz, dla odmiany, będziemy wykorzystywać mikrokontroler atmega16.
Jak widać na ilustracji poniżej, wybierając układ atmega16, mamy do dyspozycji cztery 8 bitowe porty we/wy (A,B,C,D). Ale w fabrycznie nowym atmega16 domyślnie jest włączony interfejs JTAG i w tej konfiguracji nie można wykorzystywać pinów PC2-PC5 jako cyfrowych wejść/wyjść.
Podobnie jak w poprzednio, będziemy bawić się przyłączając do portów mikrokontrolera diody LED, przyciski, przełączniki, buzzer.
Poprzednio mikrokontroler oraz wszystkie wykorzystywane elementy elektroniczne umieszczałem na płytce stykowej, lecz montowanie za każdym razem wszystkiego od nowa przy zmianie schematu okazało się być uciążliwe, więc zdecydowałem dalej układać na płytce stykowej jedynie mikrokontroler i ewentualnie inne układy scalone współpracujące z mikrokontrolerem. A na osobnych, niewielkich kawałkach płytki drukowanej umieściłem takie często wykorzystywane części, jak:
- osiem diod LED;
- siedmiosegmentowy wskaźnik LED;
- cztery miniaturowe przyciski monostabilne;
- buzer(z generatorem);
- dwa przełączniki typu piano dip switch 8;
- stabilizator napięcia 5V.
A w dalszej części kursu, na osobnych płytkach umieszczane będą klawiatura, różnego typu wyświetlacze oraz inne ciekawsze podzespoły.
Kliknij w obrazek, aby powiększyć.
Przykład pierwszy. 4 bitowy kalkulator.
Program wykonuje podstawowe operacje arytmetyczne na czterobitowych liczbach ze znakiem. Warto uruchomić ten programik żeby poćwiczyć kodowanie U2, jeśli ktoś jeszcze nie jest w tym biegły.
Pierwsza liczba odczytywana jest z linii PD0..PD3, druga z PD4..PD7, wynik wyświetlany jest na ośmiu diodach LED przyłączonych do portu A. Rodzaj operacji wybiera się na liniach PB0..P3: 1-dodawanie, 2-odejmowanie, 3-mnożenie, 4-dzielenie całkowite, 5-obliczanie reszty z dzielenia liczb całkowitych.
Dla wygody, do wejść mikrokontrolera podłączyłem przełączniki typu piano_dip_switch, które zwierają poszczególne wejścia uC z GND; oczywiście można zwierać wejścia uC na płytce stykowej do masy zwyczajnie, przewodami. Osiem diod LED podłączone zostały do wyprowadzeń portu A atmega16 na sposób: VCC->R->LED->PAx, czyli świecą się gdy odpowiedni bit w rejestrze PORTA ma wartość 0.
/* KURS AVRGCC, przykład 031 4 bitowy kalkulator Progam wykonuje operacje artymetyczne (+,-,*,/) na 4 bitowych liczbach ze znakiem. Układ ATmega16 Wejścia: PD..PD3 - pierwsza liczba, PD4..PD7 - druga liczba, PB0..PB3 - wybór operacji Wyjścia PA0..PA7 - wynik operacji Do PA0..PA7 podłączone są diody LED na sposób: VCC->R->LED->PAx */ #include <avr/io.h> int main(void) { /* Definicja zmiennych */ signed char a,b; /* Wszystkie linie portu A wyjściami */ DDRA = 0xFF; /* Wszystkie linie portu D wejściami */ DDRD = 0x00; PORTD = 0XFF; /* PB0..PB3 wejściami z podciągnięciem do Vcc */ DDRB = 0x00; PORTB = 0X0F; /* Główna pętla programu */ while(1) { /* W zmiennej 'a' zapisuje bity 0..3 z rejestru PIND */ a = PIND & 0x0f; /* w zmiennej 'b' bity 4..7 */ b = PIND >> 4; /* Rozszerza 4 bitowe liczby ze znakiem do 8 bitów. Jeśli bit nr 3 zmiennej 'a' ma wartość 1, czyli odczytano liczbę ujemną,to bity 4..7 zmiennej 'a' też ustawiane są na wartość 1 */ if(a & 0x08) a |= 0xf0; if(b & 0x08) b |= 0xf0; /* Wybiera rodzaj operacji odczytując bity 0..3 portu B */ switch(PINB & 0x0f) { case 1: //dodaje //PORTA = a+b; // PAx->R->LED->GND PORTA = ~(a+b); // VCC->R->LED->PAx break; case 2: //odejmuje //PORTA = a-b; PORTA = ~(a-b); break; case 3: //mnoży //PORTA = a*b; PORTA = ~(a*b); break; case 4: //dzieli //PORTA = a/b; PORTA = ~(a/b); break; case 5: //oblicza resztę z dzielenia //PORTA = a%b; PORTA = ~(a%b); break; default: // inne //PORTA = 0; PORTA = ~(0); } } }
Przykład 2. Sygnalizacja świetlna
Po prostu sygnalizacja świetlna. Świecą się kolejno światła: zielone, żółte, czerwone, czerwone i żółte , i ponownie zielone; a w przypadku podania na wyprowadzenie PD0 napięcia GND, światło żółte pulsujące.
/* KURS AVRGCC przykład 032 Sygnalizacja świetlna ATmega16 1MHz Wyjścia: PA0 - czerwone, PA1 - zółte, PA2 - zielone Do PA0..PA2 podłączone są diody LED na sposób: VCC->R->LED->PAx wejścia PD0 - GND na PD0 włącza pulsujące żółte */ #define F_CPU 1000000L #include <avr/io.h> #include <util/delay.h> int main(void) { /* Deklaracja zmiennych */ unsigned char i; /* PA0..PA2 - wyjścia */ DDRA = 0x07; PORTA = 0x07; /* PD0 wejście */ DDRD = 0x00; PORTD = 0x01; /* Główna pętla programu */ while(1) { /* Cztery fazy: zielone, żółte, czerwone, czerwone_żółte */ for (i= 1;i <= 4; i++) { /* Jeśli na PD0 GND */ if(!(PIND & 0x01)) i = 0; /* Wybór jednego z 5 wariantów */ switch(i) { case 1: // Jeśli i=1, zapala się zielone PORTA |= 0x03; // ustawia bity nr. 0,1 PORTA &= ~0x04; // kasuje bit nr. 2 _delay_ms(6000); // czeka 6 sekund break; case 2: // Jeśli i=2, zółte PORTA |= 0x05; // ustawia bity nr. 2,0 PORTA &= ~0x02; // kasuje bit nr. 1 _delay_ms(6000); // 6 sek break; case 3: // Jeśli i=3, czerwone PORTA |= 0x06; // ustawia bity nr. 2,1 PORTA &= ~0x01; // kasuje bit nr. 0 _delay_ms(6000); break; case 4: // Jeśli i=4, czerowne,zółte PORTA |= 0x01; // ustawia bit nr. 0 PORTA &= ~0x03; // kasuje bity nr. 0,1 _delay_ms(6000); break; default: //w innym razie, żółte pulsujące PORTA |= 0x07; // ustawia bity nr. 0,1,2 PORTA ^= 0x02; // odwraca bit nr. 1 _delay_ms(600); // 0.6 sek PORTA ^= 0x02; // odwraca bit nr. 1 _delay_ms(600); // 0.6 sek } } } }
Przykład 3. Elektroniczna kość do gry
Elektroniczna kość do gry. Po wciśnięciu i zwolnieniu przycisku na siedmio-segmentowym wyświetlaczu LED pokazują się cyfry: program liczy od 1 do losowo wybranej liczby z zakresu 1..6, animacja poniżej.
/* Kurs AVRGCC, przykład 033 Elektroniczna kość do gry ATmega16 1MHz Wyjścia: PA0..PA7 - wyświetlacz siedmiosegmentowy LED VCC->LED->R->PAx PB4 - buzzer z generatorem Wejścia: PC0 - przycisk zwierający do GND */ #define F_CPU 1000000L #include <avr/io.h> #include <util/delay.h> /* Instrukcje zaczynają się znakiem hash "#", to polecenia preprocesora. Preprocesor języka C przeprowadza rozmaite operacje na tekście programu jeszcze przed rozpoczęciem właściwej kompilacji programu. Przykładowo pierwsze z poniższych poleceń zmienia w tekście programu wszystkie wystąpienia ciągu znaków LED_1 na 0x06, identycznie jak opcja "replace" w edytorze teksu. */ // -- A -- // | | // F B // | | // -- G -- // | | // E C // | | // -- D -- #define LED_1 0x06; // 0000 0110 #define LED_2 0x5b; // 0101 1011 #define LED_3 0x4f; // 0100 1111 #define LED_4 0x66; // 0110 0110 #define LED_5 0x6d; // 0110 1101 #define LED_6 0x7d; // 0111 1101 int main(void) { /* Definicja zmiennych */ unsigned char i,n,l; /* Port A - wyjścia */ DDRA = 0xFF; PORTA = 0xFF; /* PC0 - wejście z podciągnięciem do VCC */ DDRC = 0x00; PORTC = 0X01; /* PB4 - Wyjście */ DDRB = 0x10; PORTB = 0x00; /* Główna pętla programu*/ while(1) { /* Czeka na wciśnięcie przycisku */ /* Zmienna 'l' posłuży do uzyskania liczb losowych */ while(PINC & 0x01) ++l; /* Czas na wygaśnięcie drgań styków przycisku */ _delay_ms(140); /* Czeka na zwolnienie przycisku*/ while(!(PINC & 0x01)) l+=2; /* Czas na wygaśnięcie drgań styków przycisku */ _delay_ms(140); /* Pozyskanie liczby losowej z zakresu 1..6 */ n = l % 6 + 1; /* Liczy od 1 do n */ for(i=1; i <= n; i++) { /* Wybór jednej z sześciu możliwości */ switch(i) { case 1: /* Wyświetla cyfrę 1. Uwaga, poszczególne segmenty LED wyświetlacza świecą się przy niskim stanie napięcia na portach uC, dlatego użyto tu operatora "~" */ PORTA = ~LED_1 break; case 2: PORTA = ~LED_2; // wyświetli 2 break; case 3: PORTA = ~LED_3; // 3 break; case 4: PORTA = ~LED_4; // 4 break; case 5: PORTA = ~LED_5; // 5 break; case 6: PORTA = ~LED_6; // 6 } /* Krótki sygnał dzwiękowy */ PORTB |= 0X10; // Włącza buzzer _delay_ms(50); PORTB &= ~0X10; // Wyłącza buzzer _delay_ms(450); } } }
Przykład 4. Kogut policyjny
Program steruje natężeniem świecenia dwóch kolorowych żaróweczek, powstaje efekt przypominający światło policyjnego koguta, animacja poniżej. Tym razem, dla większego efektu, zamiast diód LED, zastosowałem żaróweczki od latarki (4,5V 0.3A) przyłączone do portów we/wy mikrokontrolera za pośrednictwem układu scalonego ULN2803A. Żarówki zasilane są napięciem impulsowym (PWM), program zmieniając szerokość impulsu steruje natężeniem świecenia żaróweczek. Programik nie działa całkiem zgodnie z oczekiwaniem, lampki nie gasną całkowicie, gdy powinny, po prostu mikroprocesor atmega 1MHz jest zbyt wolny od tego kodu. Sygnał PWM lepiej generować sprzętowo wykorzystując układy czasowe mikrokrokotrolera albo pisać w asemblerze, oczywiście będziemy się tym tematem w dalszej części kursu.
/* Kurs AVRGCC, przykład 034 Kogut policyjny Do linii PA0,PA1, za pośrednictwem układu ULN2803A, przyłączone są dwie kolorowe żaróweczki (4,5V 0.3A). Żarówki zasilane są napięciem impulsowym (ok 1KHz), program, sterując szerokością impulsów zmienia płynnie natężeniem światła żaróweczek. ATmega16 1MHz Wyjścia: PA0,PA1 */ #define F_CPU 1000000L #include <avr/io.h> #include <util/delay.h> int main(void) { /* Definicja zmiennych */ int t; /* P0,P1 - wyjścia */ DDRA = 0x03; PORTA = 0x00; /* Główna pętla */ while(1) { /* Pierwsza lampka stopniowo rozjaśnia się, druga stopniowo gaśnie */ for(t=0; t<=768; t+=2) { PORTA |= 0x01; // ustawia bit nr 0 PORTA &= ~0x02; // kasuje bit nr 1 _delay_us(t); // opóźnienie w mikrosekundach PORTA &= ~0x01; // kasuje bit nr 0 PORTA |= 0x02; // ustawia bit nr1 _delay_us(768-t); } /* Pierwsza lampka stopniowo gaśnie, druga stopniowo rozjaśnia się */ for(t=768; t>=0; t-=2) { PORTA |= 0x01; PORTA &= ~0x02; _delay_us(t); PORTA &= ~0x01; PORTA |= 0x02; _delay_us(768-t); } } }
Przykład 5. Kluczyk - zabawka zręcznościowa
Zabawa polega na prowadzeniu klucza nawleczonego na pętlę z drutu, trzeba tak przeprowadzić klucz na drugi koniec pętli by nie dotknął ani razu pętli. Wciśnięcie przycisku rozpoczyna zabawę. Każde zwarcie klucza z pętlą sygnalizowanie jest krótkim dzwiękiem z buzzera i przygaśnięciem jednej diody LED. Początkowo świecą się cztery diody LED, można popełnić cztery błędy, piąte zwarcie kończy zabawę co sygnalizowane jest długim dziwiękiem przerywanym.
Pętlę wykonałem z kawałka miedzianego drutu o średnicy ok 2,5mm, klucz też, prawdziwe klucze raczej się nie nadają. Klucz przyłączyłem giętkim przewodem do masy, a pętlę do wyprowadzenia PB0 mikrokontrolera skonfigurowanego jako wejście. Dodatkowo pętla została podciągnięta przez rezystor 1k do napięcia zasilania. Przy zetknięciu klucza z pętlą na wyprowadzeniu PB0 powinno pojawić się napięcie GND i wartość bitu nr. 0 odczytana z rejestru PORTB będzie 0. Przycisk rozpoczynający grę przyłączony został między GND a wyprowadzenie PB1 skonfigurowane jako wejście z wewnętrzny podciągnięciem do VCC, czyli przy wciśniętym przycisku wartość bitu nr. 1 odczytana z rejestru PORTB będzie 0. Diody LED zostały przyłączone do wyprowadzeń PD3..PD0 na sposób: VCC->R->LED->PDx, czyli świecą się gdy odpowiedni bit w rejestrze PORTD ma wartość 0. Buzzer został przyłączony poprzez tranzystor npn do wyprowadzenia PC0 i działa, gdy w rejestrze PORTC bit nr. 0 zosanie ustawiony na wartość 1.
/* Kurs AVRGCC Przykład 035 Klucz - zabawka zręcznościowa Zabawa polega na prowadzeniu klucza nawleczonego na pętlę z drutu, trzeba tak przeprowadzić klucz na drugi koniec pętli by nie dotknął ani razu pętli. Układ ATmega16 1MHz wejścia: PB0 - pętla z drutu Dodatkowo pętla ma być podciągnięta przez rezystor 1k do Vcc; a klucz - połączony z GND. PB1 - przycisk wyjścia: PC0 - buzzer PD0..PD3 - diody LED przyłączone na sposób: VCC->R->LED->PDx */ #define F_CPU 1000000L #include <avr/io.h> #include <util/delay.h> int main(void) { /* Definicja zmiennych */ unsigned char m ; /* Konfiguracja portów we/wy */ DDRB = 0x00; PORTB = 0x02; DDRC = 0x01; PORTC = 0x00; DDRD = 0x0F; PORTD = 0x0F; /* Głóna pętla programu */ while(1) { /* Oczekiwanie na wciśnięcie przycisku, wciśnięcie przycisku rozpoczyna zabawę*/ while(PINB & 0X02); /* Początkowa wartość zmiennej 'm' dwójkowo 0001 0000 */ m = 0x10; /* Kasując bity nr 0..3 rejestru PORTD zapala wszystkie 4 diody LED */ PORTD &= 0XF0; /* Pętla wykonuje się dopóki m będzie różne od 0 */ while(m) { /* Jeśli nastąpi zwarcie klucza z pętlą */ if(!(PINB & 0X01)) { /* Krótki sygnał dzwiękowy */ PORTC |= 0x01; // włącza buzzer _delay_ms(100); // czeka 0.1s PORTC &= ~0x01; // wyłącza buzzer /* Dodatkowy czas na odsunięcie klucza */ _delay_ms(300); /* Za każdym przejściem przesuwa bity w zmiennej 'm' o jedną pozycje w prawo */ m>>=1; /* 0001 0000 początkowa wartość zmiennej 'm' */ /* 0000 1000 wartość 'm' po pierwszym przejściu */ /* 0000 0100 po drugim przejściu */ /* 0000 0010 po trzecim przejściu */ /* 0000 0001 po drugim przejściu */ /* 0000 0000 po piątym przejściu */ /* Za każdym przejściem gasi jedną diodę LED, w kolejności od czwartej do pierwszej. Diody LED przyłączone są na sposób: VCC->R->LED->PDx i gasną, gdy odpowiedni bit w PORTD ma wartość 1 */ PORTD |= m; } } /* Długi dzwięk przerywany sygnalizuje koniec zabawy */ for(m=0; m<12; m++) { PORTC |= 0x01; // włącza buzzer _delay_ms(50); // czeka 0.05s PORTC &= ~0x01; // wyłącza buzzer _delay_ms(50); // czeka 0.05s } } }
Myślę, że zamieszczone w tej części przykładowe programy są na tyle proste, iż nie ma potrzeby objaśniać ich działania linia po linii; i że wystarczą animacje oraz komentarze dołączone w kodzie. Ale jeśli jest inaczej, to proszę dać znać co jest niezrozumiałe, wtedy dopiszę objaśnienia.
W następnej części
Tematem następnej części kursu będą tablice i funkcje - czyli dalszy ciąg podstaw języka C.
Kurs AVR-GCC cz.2 | Kurs AVR-GCC cz.4