Artykuł pochodzi ze strony XYZ HOBBY ROBOT (
xyz.isgreat.org
)
Kurs AVR-GCC cz.2
30.11.2008 ABXYZ
W pierwszym odcinku kursu
zainstalowaliśmy pakiet programów
WinAVR, nauczyliśmy się kompilować kody
źródłowe programów oraz ładować
programy do pamięci flash
mikrokontrolera AVR. W tej części kursu
zaczniemy pisać proste programy w języku
C, poznamy w jaki sposób programować
równoległe porty wejścia/wyjścia układów
AVR. Zaczniemy od naprawdę prostych przykładów :), zupełnie od
zera. Jeśli i tak coś w tekście będzie niezrozumiałe, to nie naleŜy
szybko się zniechęcać, w trakcie dalszej lektury kursu powinno się
wyjaśnić.
Równoległe porty wejścia/wyjścia
Układ atmega8 w obudowie DIP28, na którym będziemy uruchamiać
przykładowe programy, posiada 28 wyprowadzeń, z których 23
mogą słuŜyć jako uniwersalne binarne wejścia/wyjścia. Linie we/wy
atmega8 podzielone są na trzy grupy, nazwane: PORTB, PORTC i
PORTD. Mikrokontrolery AVR w obudowach DIP40(atmega16,
atmega32) posiadają cztery porty: A,B,C,D; natomiast attiny2313
w obudowie DIP20, posiada: PORTA (tylko PA0..PA2), PORTB
i PORTD(PD0..PD6).
PORTB układu atemga8 posiada osiem linii we/wy (PB0..PB7), ale
jeśli zamierzamy podłączyć do mikrokontrolera rezonator, to linie
PB6 i PB7 odpadają. Z kolei wyprowadzenia: 17(PB3), 18(PB4)
i 19(PB4) wykorzystywanie są przy programowaniu szeregowym
ISP pamięci flash mikrokontrolera. PORTC uC atmaga8 posiada 7
linii we/wy (PC0..PC6), ale wyprowadzenie 1(PC6) normalnie pełni
rolę wejścia sygnału reset mikroprocesora. Aby wykorzystywać
wyprowadzenie 1(PC6) atmeg8 jako kolejną linię portu we/wy,
potrzeba zaprogramować fuse-bit RSTDISBL (na temat fuse-bitów
będę pisał), lecz w ten sposób pozbawimy się moŜliwości
programowania szeregowego ISP pamięci FLASH mikrokontrolera.
PORTD atmega8 posiada 8 linii we/wy (PD0..PD7).
Rozkład wyprowadzeń mikrokontrolera ATmega8
1 z 19
KaŜda z wymienionych linii we/wy układu atmega8 moŜe zostać
indywidualnie skonfigurowana jako binarne wejście lub wyjście.
Domyślnie wszystkie ustawione są wejściami z wyjątkiem
wyprowadzenia 1 (RESET|PC6), które jest normalnie w atmega8
wejściem sygnału RESET mikroprocesora.
Program kontroluje układy peryferyjne mikrokontrolera AVR
poprzez "rejestry I/O", 64 8-bitowe rejestry I/O znajdują się
w przestrzeni adresowej pamięci danych - program moŜe zapisywać
do "rejestrów I/O" lub odczytywać z nich jakby korzystał z pamięci
RAM. W zbiorze instrukcji języka maszynowego uC AVR istnieją teŜ
specjalne rozkazy słuŜące do odczytu(zapisu) zawartości rejestrów
I/O, takŜe rozkazy do manipulowani poszczególnymi bitami
rejestrów. W jaki sposób moŜna w AVR-GCC odczytywać i
zapisywać rejestry I/O pokaŜę za chwilę, przy objaśnianiu
przykładowych programów.
Z kaŜdym z równoległych portów we/wy: A,B,C,D powiązane są po
trzy rejestry I/O, o nazwach: DDRx, PORTx, PINx, gdzie x to
oczywiście litery A,B,C,D. Stan poszczególnych bitów rejestrów
DDRx (Port Data Direction Register) decyduje czy odpowiadające im
linie są wejściami, czy wyjściami (0-wejście, 1-wyjście).
Jeśli dana linia we/wy pracuje jako wyjście, wtedy ustawiając na
wartość 1 odpowiadający tej linii bit w rejestrze PORTx (Port Data
Register), wymuszamy na wyprowadzeniu stan wysoki napięcia,
a ustawiając wartość bitu na 0, oczywiście stan niski.
Jeśli linię we/wy skonfigurowano jako wejście, poziom napięcia na
wyprowadzeniu, niski czy wysoki, sprawdza się odczytując wartość
odpowiadającego tej linii bitu w rejestrze PINx (Port Input Pins
Address), oczywiście wartość 0 oznacza stan niski, 1 stan wysoki.
Dodatkowo, gdy linia jest wejściem i odpowiadający tej linii bit w
rejestrze PORTx ma wartość 1, wtedy wyprowadzenie jest
wewnętrznie podciągnięta do napięcia zasilania.
DDRx.n PORTx.n
Px.n
0
0
wejście
0
1
wejście z podciągnięciem do VCC
1
0/1
wyjście
Konfiguracja równoległych portów we/wy mikrokontrolerów AVR
A teraz przykład konfiguracji portu B, patrzymy na ilustrację
poniŜej.
2 z 19
Przykład konfiguracji portu B
WyŜej, na ilustracji widać, Ŝe cztery mniej znaczące bity rejestru
DDRB mają wartość "0", a pozostałe cztery wartość "1", więc linie
PB0..PB3 będą wejściami, a linie PB4..PB7 będą wyjściami. Bity
numer 0 i 1 w rejestrze PORTB ustawione zostały na wartość 1,
więc linie PB0 i PB1 pracują jako wejścia z wewnętrznym
podciągnięciem do napięcia zasilania. Stan wejść (PB0..PB3)
sprawdzamy odczytując bity 0..3 rejestru PINB, natomiast
zmieniając wartości bitów 4..7 rejestru PORTB moŜna wymusić
oczekiwany stan napięcia(wysoki lub niski) na wyprowadzeniach
PB4..PB7.
Schematy połączeń
Oprócz mikro_kontrolera AVR, dla uruchomienia przykładowych
programików, potrzebne będą: osiem diod LED, cztery miniaturowe
przyciski monostabilne oraz buzzer z generatorem. Diody LED
przyłączone będą do portu D, przyciski do linii PC0..PC3, a buzzer
do PB1. KaŜda z ośmiu diod LED, wraz z rezystorem ograniczającym
prąd, przyłączona jest między wyprowadzenie portu a masę, więc
będzie się świecić, kiedy do odpowiedniego bitu rejestru wpiszemy
wartość "1".
Przyciski przyłączone są do linii we/wy portu C w taki sposób, Ŝe
przy wciśnięciu zwierają dane wyprowadzenie układu z masą, więc
przy wciśniętym przycisku z rejestru PINC odczytamy wartość
odpowiedniego bitu "0", a przy zwolnionym przycisku, odczytamy
"1". Linie we/wy z przyłączonymi w ten sposób przyciskami naleŜy
skonfigurować jako wejścia z podciągnięciem do VCC.
Sposób przyłączenia przycisku do portu we/wy uC AVR
Dla przejrzystości schemat połączeń rozdzieliłem na dwa. Pierwszy
schemat przedstawia sposób przyłączenia do atmega8 zasilania,
resetu oraz programatora ISP, na drugim schemacie widać sposób
podłączenie do portów we/wy mikro_kontrolera diod LED,
przycisków i buzzera.
3 z 19
Schemat przedstawia sposób przyłączenia do atmega8 zasilania, resetu oraz programatora
ISP. Kliknij w obrazek, Ŝeby powiększyć.
Schemat przedstawia sposób przyłączenia diod LED, przycisków i buzzera do portów
we/wy uC Kliknij w obrazek, Ŝeby powiększyć.
Ja zestawiłem wszystkie części na małej płytce stykowej.
Gotowy układ zestawiony na płytce stykowej.Kliknij w obrazek, Ŝeby powiększyć.
Szkielet prostego programu dla avr-gcc
Wszystkie przykładowe programy z tej części kursy będą wyglądać
podobnie, poniŜej znajduje się szkielet prostego programu dla
AVR-GCC.
/* Szkielet prostego programu dla avr-gcc */
4 z 19
#define F_CPU 1000000L
#include <avr/io.h>
#include <util/delay.h>
int
main(
(
(
(
void
)
)
)
)
{
{
{
{
/* Tutaj wpisujemy instrukcje naszego programu */
for
(;;)
(;;)
(;;)
(;;)
{
{
{
{
/* Instrukcje moŜna umieścić w nieskończonej pętli */
}
}
}
}
}
}
}
}
Szkielet prostego programu dla avr-gcc
Pierwsza linia to komentarz.
/* Szkielet prostego programu dla avr-gcc */
Komentarze są pomijanie przez kompilator, zwykle objaśniają
mniej oczywiste fragmenty kodu. GCC pozwala wstawiać
komentarze na dwa sposoby. Pierwszy sposób to objęcie treści
komentarza parą ograniczników: '/*' , '*/'; w ten sposób utworzony
komentarz moŜe zawierać wiele linii tekstu.
/*
Komentarz blokowy,
moŜe zawierać wiele
linii tekstu
*/
Drugi sposób to umieszczenie przed treścią komentarza dwóch
znaków slash '//' , tak utworzony komentarz rozciąga tylko do
końca linii.
// Komentarz liniowy
A czy GCC pozwala zakomentowywać inne komentarze ? Proszę to
sprawdzić samemu :)
Kolejne trzy linijki to polecenia preprocesora.
#define F_CPU 1000000L
#include <avr/io.h>
#include <util/delay.h>
Polecenia preprocesora zaczynają się znakiem hash "#".
Preprocesor języka C przeprowadza rozmaite operacje na tekście
programu jeszcze przed rozpoczęciem "właściwej" kompilacji
programu. Na przykład pierwsze z tych trzech poleceń zmienia w
tekście programu wszystkie wystąpienia ciągu znaków "F_CPU" na
"1000000L", jak opcja "replace" w edytorze teksu. Liczba ta jest
częstotliwością taktowania mikrokontrolera podaną w Hz. Nowe
układy atmega skonfigurowane są do pracy z wewnętrznym
oscylatorem RC 1Mz, dla przykładowych programików z tej części
kursu nie ma potrzeby tego zmieniać. Inne częstotliwości
taktowania mikrokontrolera atmaga moŜna wybrać programując
odpowiednie fuse-bity, napiszę o tym w dalszej części kursu. Drugie
polecenie dołącza (wkleja) do tekstu programu, w miejscu jego
wystąpienia, zawartość pliku "include/avr/io.h"; podobnie trzecie
polecenie dołącza zawartość pliku "include/util/delay.h" A co
takiego zawierają te pliki? Są to pliki tekstowe, więc ich zawartość
5 z 19
moŜna przejrzeć w dowolnym edytorze tekstu. Plik "include/avr
/io.h" zawiera definicje rejestrów, bitów i przerwań; zaś plik
"include/util/delay.h" zawiera funkcje wstrzymujących działanie
programu na zadany okres czasu. Temat preprocesora zostanie
dalej w kursie szczegółowo omówiony.
Następny fragment listingu to początek "głównej" funkcji programu.
int
main(
(
(
(
void
)
)
)
)
{
{
{
{
/* Tutaj wpisujemy instrukcje naszego programu */
}
}
}
}
Funkcja w języku C to najprościej mówiąc wydzielony z programu
fragment kodu, wykonujący jakieś konkretne zadanie.
Odpowiednikiem funkcji z C w innych językach programowania są
procedura i podprogram. KaŜda funkcja posada własną nazwę,
nadaną jej przy tworzeniu, posługując się nazwą funkcji moŜna
wielokrotnie, dowolnym miejscu programu, uruchamiać blok
instrukcji zawarty w funkcji, tak jak by to była pojedyncza
instrukcja. Programy podzielone są zwykle na wiele mniejszych
funkcji, dzięki temu są bardziej czytelne i oszczędza się pamięć.
Funkcje moŜna tworzyć (definiować) samemu, a takŜe korzysta się
z gotowych funkcji z bibliotek dostarczonych wraz z kompilatorem.
KaŜdy program w C musi zawierać funkcję o nazwie "main" (funkcja
główna), poniewaŜ pierwsza instrukcja funkcji "main" jest
jednocześnie pierwszą instrukcją całego programu. Przykładowe
programy w tej części kursu będą zawierać jedynie funkcję "main",
cały nasz kod będzie umieszczany w głównej funkcji, między
nawiasami klamrowymi "{","}".
W naszych prostych, przykładowych programach będzie moŜna
wyróŜnić dwie sekcje, pierwsza to instrukcje wykonywane raz
jeden, natychmiast po starcie programu, a druga sekcja to blok
instrukcji wykonywany wielokrotnie w nieskończonej pętli.
Nieskończoną pętle w programie moŜna zbudować uŜywając
instrukcji "for" lub "while", w taki sposób, jak w przykładzie poniŜej.
W obu przypadkach instrukcje we wnętrzu pętli objęte są parą
nawiasów klamrowych "{","}", podobnie jak zawartość całej funkcji
głównej "main". Instrukcje tworzące pętle: "for" i "while" omówię
szczegółowo w dalszej części kursu, ale juŜ teraz będziemy je
wykorzystywać w naszych przykładowych programach.
int
main(
(
(
(
void
)
)
)
)
{
{
{
{
/* Instrukcje wykonywane raz jeden po starcie programu */
/* Pętla nieskończona utworzona instrukcją "for" */
for
(;;)
(;;)
(;;)
(;;)
{
{
{
{
/* Instrukcje w nieskończonej pętli */
}
}
}
}
}
}
}
}
/* Pętla nieskończona utworzona instrukcją "while" */
while
(1)
(1)
(1)
(1)
{
{
{
{
/* Instrukcje w nieskończonej pętli */
}
}
}
}
Programujemy porty we/wy
6 z 19
Jak pisałem, zaczniemy od naprawdę prostych przykładów. Pierwszy
program będzie powodował miganie ośmiu diod LED przyłączonych
do wyprowadzeń skojarzonych z portem D. Diody mają świecić się
na przemian, raz parzyste, raz nieparzyste. Tak, jak pokazuje to
animacja poniŜej.
/* przykład 2.1 "leds.c" */
/* 8 diod LED przłączonych do portu D */
/* ATmega 1MHz */
#define F_CPU 1000000L
#include <avr/io.h>
#include <util/delay.h>
int
main(
(
(
(
void
)
)
)
)
{
{
{
{
/* Wszystkie linie portu D będą wyjściami */
DDRD =
=
=
=
0xFF
;
;
;
;
/* 0xFF binarnie 1111 1111 */
/* Początek nieskończonej pętli */
while
(
(
(
(
1
)
)
)
)
{
{
{
{
PORTD =
=
=
=
0xaa
;
;
;
;
/* 0xaa binarnie 1010 1010 */
/* opóźnienie 0.33 sek. */
_delay_ms(
(
(
(
330
);
);
);
);
PORTD =
=
=
=
0x55
;
;
;
;
/* 0x55 binarnie 0101 0101 */
/* opóźnienie 0.33 sek. */
_delay_ms(
(
(
(
330
);
);
);
);
}
}
}
}
}
}
}
}
Listing 2.1
Efekt działania programu z listingu 2.1
Pierwszą instrukcją (będę tłumaczył zaczynając od wnętrza funkcji
głównej "main") ładujemy do rejestru DDRD wartość 0xff (postać
szesnastkowa liczby), w ten sposób konfigurujemy wszystkie linie
portu D jako wyjścia.
/* Wszystkie linie portu D będą wyjściami */
DDRD =
=
=
=
0xFF
;
;
;
;
/* 0xFF binarnie 1111 1111 */
UŜywając nazwy rejestru i operatora przypisania - znaku "=",
moŜna zapisać(odczytać) całą 8-bitową zawartość rejestru I/O,
instrukcję przypisania naleŜy zakończyć znakiem średnika, inaczej
kompilacja zakończy się z błędem. NaleŜy teŜ pamiętać o
dołączeniu do kodu plików z definicjami rejestrów I/O, wstawiając
gdzieś na początku programu linię:
#include <avr/io.h>
7 z 19
Inaczej kompilator nie rozpozna nazw rejestrów I/0 i przerwie
kompilację wysyłając komunikat o wielu błędach.
W tekście programu moŜna umieszczać stałe liczby (tzw. literały) w
postaci dziesiętnej, szesnastkowej oraz ósemkowej, natomiast
standard języka C nie przewiduje moŜliwości zapisywania stałych w
postaci dwójkowej. Postać szesnastkową liczby stałej tworzymy
wstawiając przed liczbą parę znaków '0x' lub '0X' (np. 0xFF), stałe
ósemkowe rozpoczynają się od cyfry "0" (np. 077), liczby w
systemie dziesiątkowym wpisujemy zwyczajnie (np. 123).
Kolejne dwie linie to początek nieskończonej pętli utowrzonej
instrukcją "while". Wykonanie bloku instrukcji po "while(1)",
objętych parą nawiasów klamrowych "{","}", będzie powtarzane, aŜ
do momentu odłączenia zasilania lub resetu mikroprocesora
/* Początek nieskończonej pętli */
while
(1)
(1)
(1)
(1)
{
{
{
{
Pierwsza instrukcja w pętli zapisuje do rejestru PORTD wartość
0xAA ( binarnie 1010 1010), zaświecą się diody LED przyłączone do
wyprowadzeń: PD1,PD3,PD5 i PD7; pozostałe diody będą
wygaszone.
PORTD =
=
=
=
0xaa
;
;
;
;
/* 0xaa binarnie 1010 1010 */
Kolejna linijka wprowadza w tym miejscu programu opóźnienie ok
0.33 sek. Na razie nie będę szczegółowo objaśniał tej instrukcji, bo
nie jest dobrze tłumaczyć zbyt wielu rzeczach na raz. Będziemy
wstawiać tę linię do naszych przykładów tam, gdzie potrzebne
będzie opóźnienie, w kolejnej części kursu dokładnie wyjaśnię
działanie tej instrukcji. Długość opóźnienia wpisujemy tam gdzie
jest liczba 330, w tysięcznych częściach sekundy.
/* opóźnienie 0.33 sek. */
_delay_ms(
(
(
(
330
);
);
);
);
W następnej linii do rejestru PORTD ładowana jest wartość 0x55(
dwójkowo 0101 0101), tym razem zaświecą się diody LED
przyłączone do wyprowadzeń PD0,PD2,PD4 i PD6. I ponownie
działanie programu zostanie wstrzymane na okres ok 0.33 sek.
PORTD =
=
=
=
0x55
;
;
;
;
/* 0x55 binarnie 0101 0101 */
/* opóźnienie 0.33 sek. */
_delay_ms(
(
(
(
330
);
);
);
);
Dalej, pierwszy nawias klamrowy kończy blok instrukcji
wykonywanych w pętli, kolejny nawias oznacza koniec funkcji
głównej "main".
}
}
}
}
}
}
}
}
W pierwszym przykładzie zapisywana była cała zawartość rejestru
PORTD, w kolejnym programie pokaŜę jak ustawiać i kasować
poszczególne bity rejestrów.
8 z 19
Zanim zacznę objaśniać program, to omówię operacje bitowe.
Operacje bitowe działają na wartościach całkowitych i słuŜą do
manipulowania bitami. W języku C jest sześć operatorów
bitowych:|,&,^,<<,>>,~. Operatory te są szczególnie uŜyteczne
przy zabawie z bitami rejestrów, więc juŜ teraz wyjaśnię ich
działanie na przykładach. Oczywiście w przykładach liczby
przedstawione są w postaci dwójkowej.
operator "|" - bitowa alternatywa (OR)
0 1 0 1 0 1 0 1
|
0 0 1 1 0 0 1 1
=
0 1 1 1 0 1 1 1
operator "&" - bitowa koniunkcja (AND)
0 1 0 1 0 1 0 1
&
0 0 1 1 0 0 1 1
=
0 0 0 1 0 0 0 1
operator "^" - bitowa alternatywa wykluczająca (XOR)
0 1 0 1 0 1 0 1
^
0 0 1 1 0 0 1 1
=
0 1 1 0 0 1 1 0
operator "<<" - przesunięcie w lewo
1 0 0 1 1 0 0 1 << 3 = 1 1 0 0 1 0 0 0
operator ">>" - przesunięcie w prawo
1 0 0 1 1 0 0 1 >> 5 = 0 0 0 0 0 1 0 0
operator "~" - dopełnienie jedynkowe
~1 0 0 1 1 0 0 1 = 0 1 1 0 0 1 1 0
Drugi programik róŜni się niewiele od pierwszego, więc omówię
tylko te linie gdzie, gdzie moŜna znaleźć coś nowego.
/* przykład 2.2 "leds2.c" */
/* 8 diod LED przłączonych do portu D */
/* ATmega 1MHz */
#define F_CPU 1000000L
#include <avr/io.h>
#include <util/delay.h>
int
main(
(
(
(
void
)
)
)
)
{
{
{
{
/* Wszystkie linie portu D będą wyjściami */
DDRD =
=
=
=
0xFF
;
;
;
;
/* Początek nieskończonej pętli */
for
(;;)
(;;)
(;;)
(;;)
{
{
{
{
PORTD =
=
=
=
0x0f
;
;
;
;
/* Ładuje do PORTD wartość 0x0f*/
/* opóźnienie 1 sek. */
9 z 19
_delay_ms(
(
(
(
1000
);
);
);
);
PORTD |=
|=
|=
|=
0xf0
;
;
;
;
/* ustawia bity nr. 4..7 */
_delay_ms(
(
(
(
1000
);
);
);
);
PORTD &=
&=
&=
&=
0xaa
;
;
;
;
/* zeruje bity nr. 0,2,4,6 */
_delay_ms(
(
(
(
1000
);
);
);
);
PORTD ^=
^=
^=
^=
0x0f
;
;
;
;
/* "odwraca" bity nr. 0..3 */
_delay_ms(
(
(
(
1000
);
);
);
);
PORTD =
=
=
=
0x00
;
;
;
;
/* opóźnienie 2 sek. */
_delay_ms(
(
(
(
2000
);
);
);
);
}
}
}
}
}
}
}
}
Listing 2.2
Efekt działania programu z listingu 2.2
Wybrane bity rejestru moŜna ustawić na wartość "1", bez zmiany
wartości pozostałych bitów rejestru, uŜywając operatora przypisania
"|=", instrukcja przypisania kończy się średnikem.
PORTD |=
|=
|=
|=
0xf0
;
;
;
;
/* ustawia bity nr. 0..3 */
Na zawartości rejestru PORTD wykonywana jest operacja bitowa OR
z wartością stałej znajdującej się po prawej stronie operatora "|=",
a następnie wynik operacji zapisywany jest do rejestru PORTD.
PORTD 0 0 0 0 1 1 1 1
bitowe OR
0xF0 1 1 1 1 0 0 0 0
=
1 1 1 1 1 1 1 1 -> PORTD
W kodach źródłówych programów spotyka się następujący sposób
ustawiania wybranych bitów rejestru.
PORTC |= (1<<3)|(1<<5)|(1<<7);
W tym przykładzie, zostaną ustawione bity numer: 3,5,7 rejestru
PORTC (bity w rejestrze numerowane są zaczynając od zera),
pozostałe bity pozostaną niezmienione. UŜyto operatorów bitowych:
"|"- bitowe OR, "<<" - przesunięcie w lewo.
(1<<3) = 0000 1000
bitowe OR
(1<<5) = 0010 0000
bitowe OR
(1<<7) = 1000 0000
-------------
1010 1000
10 z 19
Wracamy do naszego programu. Wybrane bity rejestru moŜna
ustawiać na wartość "0", bez zmiany pozostałych bitów rejestru,
uŜywając instrukcji przypisania "&="
PORTD &=
&=
&=
&=
0xaa
;
;
;
;
/* zeruje bity nr. 0,2,4,6 */
Na zawartości rejestru PORTD wykonywana jest operacja bitowa
AND z wartością stałej znajdującej się po prawej stronie operatora
"&=", a następnie wynik operacji zapisywany jest do rejestru
PORTD.
PORTD 1 1 1 1 1 1 1 1
bitowe AND
0xAA 1 0 1 0 1 0 1 0
=
1 0 1 0 1 0 1 0 -> PORTD
Wybrane bity rejestru moŜna "odwrócić", tzn. zmieniać wartość bitu
"1" na "0", a "0" na "1", bez zmiany pozostałych bitów rejestru,
uŜywając instrukcji przypisania "^="
PORTD ^=
^=
^=
^=
0x0f
;
;
;
;
/* "odwraca" bity nr. 0..3 */
Na zawartości rejestru PORTD wykonywana jest operacja bitowa
XOR z wartością stałej znajdującej się po prawej stronie operatora
"^=", a następnie wynik operacji zapisywany jest do rejestru
PORTD.
PORTD 1 0 1 0 1 0 1 0
bitowe XOR
0x0f 0 0 0 0 1 1 1 1
=
1 0 1 0 0 1 0 1 -> PORTD
Trzeci przykładowy program pokazuje jak odczytać całą zawrtość
rejestru
/* przykład 2.3 "leds3.c" */
/* 8 diod LED przłączonych do portu D */
/* 4 przyciski przyłączone do PC0..PC3 */
/* ATmega 1MHz */
#define F_CPU 1000000L
#include <avr/io.h>
#include <util/delay.h>
int
main(
(
(
(
void
)
)
)
)
{
{
{
{
/* Wszystkie linie portu D będą wyjściami */
DDRD =
=
=
=
0xff
;
;
;
;
/* Linie portu C będą wejściami z podciągnięciem do VCC */
DDRC =
=
=
=
0x00
;
;
;
;
PORTC =
=
=
=
0xff
;
;
;
;
11 z 19
/* Początek nieskończonej pętli */
for
(;;)
(;;)
(;;)
(;;)
{
{
{
{
/* Przepisanie zawartości PINC do PORTD */
PORTD =
=
=
= PINC;
;
;
;
//PORTD = ~PINC & 0x0f;
}
}
}
}
}
}
}
}
Listing 2.3
Efekt działania programu z listingu 2.3
Wpierw linie we/wy portu C zostają skonfigurowane jako wejścia
podciągnięte do VCC, a linie portu D wyjścia. Dalej, w
nieskończonej pętli, z uŜyciem operatora przypisania "=",
zawartość rejestru PINC jest bezpośrednio przepisywana do rejestru
PORTD.
/* Przepisanie zawartości PINC do PORTD */
PORTD =
=
=
= PINC;
;
;
;
Normalnie cztery pierwsze diody LED przyłączone do linii PD0..PD3
będą się świecić, wciśnięcie któregoś z przycisków powoduje
wygaszenie odpowiedniej diody LED.
W kolejnych przykładach potrzebna będzie instrukcja "if-else"
wykorzystywana do podejmowania decyzji.
if
(
(
(
( wartość_logiczna PRAWDA/FAŁSZ )
)
)
)
/* Instrukcja wykonywana jeśli PRAWDA */
else
/* Instrukcja wykonywana jeśli FAŁSZ */
Część "else" , czyli instrukcje wykonywaną gdy FAŁSZ moŜna
pominąć.
if
(
(
(
( wartość_logiczna PRAWDA/FAŁSZ )
)
)
)
/* Instrukcja wykonywana jeśli PRAWDA */
Obejmując fragment kodu parą klamrowych nawiasów "{","}"
tworzymy tzw. blok instrukcji, blok w "if-else" jest traktowany jako
pojedyncza instrukcja.
if
(
(
(
( wartość_logiczna PRAWDA/FAŁSZ )
)
)
)
{
{
{
{
/* Blok instrukcji wykonywany jeśli PRAWDA */
12 z 19
}
}
}
}
else
{
{
{
{
/* Blok instrukcji wykonywany jeśli FAŁSZ */
}
}
}
}
Chcąc sprawdzić czy jeden lub grupa bitów jednocześnie w rejestrze
I/0 ma wartość "1" moŜna to zrobić z uŜyciem instrukcji "if" w
następujący sposób:
if
(
(
(
(REJESTR_I0 &
&
&
&
MASKA
)
)
)
)
{
{
{
{
/* Blok instrukcji wykonywany jeśli warunek spełniony */
}
}
}
}
Gdzie "REJESTR_IO" to nazwa rejestru, a "MASKA" to stała wartość
z ustawionymi tymi bitami, które potrzeba testować
Przykłady:
/* Jeśli bit nr. 3 rejestru PINC ma wartość "1" */
if
(
(
(
(PINC &
&
&
&
0x08
)
)
)
){
{
{
{}
}
}
}
/* Jeśli bity 0 lub 1 w PIND mają wartość "1" */
if
(
(
(
(PIND &
&
&
&
0x03
)
)
)
){
{
{
{}
}
}
}
Na zawartości rejestru wykonywana jest operacja bitowa AND z
wartością stałej stojącej po prawej stronie operatora "&". I jeśli
wynik tej operacji będzie róŜnił się od zera, to całe wyraŜenie w
nawiasach okrągłych będzie uznane jako wartość logiczną
"PRAWDA" i wykonane zostaną instrukcje objęte nawiasami
klamrowymi po "if"; w przeciwnym przypadku instrukcje te zostaną
pominięte. W języku C, jeŜeli jakieś wyraŜenie po obliczeniu jest
róŜne od zera, to ma wartość logiczną "PRAWDA", a jeŜeli
wyraŜenie jest równe zero, to ma wartość logiczną "FAŁSZ".
A po co na wartości odczytanej z rejestru wykonywana jest bitową
operację AND? śeby przysłonić te bity rejestru, których wartości
nas nie interesują. Przykład:
Interesuje na stan bitu nr 3 rejestru
REJESTR_I0 0 1 0 1 1
1
1
1 1 1 1 (0x5f)
bitowe AND
0 0 0 0 1
1
1
1 0 0 0 (0x08)
-----------------------------
= 0 0 0 0 1
1
1
1 0 0 0 (0x08)
REJESTR_I0 0 1 0 0 0
0
0
0 1 0 1 (0x45)
bitowe AND
0 0 0 0 1
1
1
1 0 0 0 (0x08)
-----------------------------
= 0 0 0 0 0
0
0
0 0 0 0 (0x00)
Jednak na schemacie przyciski przyłączono do portów we/wy uC w
taki sposób, Ŝe przy wciśniętym przycisku odczytujemy z rejestru
wartość bitu "0", a nie "1".Tak moŜna sprawdzić czy jeden lub
grupa bitów w rejestrze I/0 ma wartość "0":
if
(!(
(!(
(!(
(!(REJESTR_IO &
&
&
&
MASKA
))
))
))
))
{
{
{
{
/* Blok instrukcji wykonywany jeśli warunek spełniony */
}
}
}
}
13 z 19
Przykłady:
/* Jeśli bit nr. 0 rejestru PINC ma wartość "0" */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x01
))
))
))
)){
{
{
{}
}
}
}
/* Jeśli bit nr. 1 rejestru PINC ma wartość "0" */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x02
))
))
))
)){
{
{
{}
}
}
}
/* Jeśli bit nr. 2 rejestru PINC ma wartość "0" */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x04
))
))
))
)){
{
{
{}
}
}
}
/* Jeśli bit nr. 3 rejestru PINC ma wartość "0" */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x08
))
))
))
)){
{
{
{}
}
}
}
Tutaj bitowy iloczyn zawartości rejestru i stałej został dodatkowo
wzięty w okrągłe nawiasy i wartość logiczna całości została
zanegowana z uŜyciem operatora wykrzyknik "!". Znak wykrzyknik
"!" jest w języku C logicznym operatorem negacji, zmienia wartość
logiczną PRAWDA na FAŁSZ, A wartość FAŁSZ na PRAWDA. A jak
pisałem przed chwilą, jeŜeli jakieś wyraŜenie po obliczeniu jest
większe od zera to ma wartość logiczną "PRAWDA", a jeŜeli równe
zero to ma wartość logiczną "FAŁSZ".
A jeśli potrzeba, aby blok instrukcji wykonywał się wielokrotnie w
pętli, dopóki jeden lub grupa bitów jednocześnie w rejestrze I/0 ma
wartość "1", to moŜna to zrobić z uŜyciem instrukcji "while":
while
(
(
(
(REJESTR_I0 &
&
&
&
MASKA
)
)
)
)
{
{
{
{
/* Blok instrukcji wykonywany jeśli warunek spełniony */
}
}
}
}
A jeśli potrzeba, aby blok instrukcji wykonywał się wielokrotnie w
pętli, dopóki jeden lub grupa bitów jednocześnie w rejestrze ma
wartość "0", na przykład gdy wciśnięty jest przycisk:
while
(!(
(!(
(!(
(!(REJESTR_IO &
&
&
&
MASKA
))
))
))
))
{
{
{
{
/* Blok instrukcji wykonywany jeśli warunek spełniony */
}
}
}
}
Gdzie, jak poprzednio, "REJESTR_IO" to nazwa rejestru, a "MASKA"
to stała wartość z ustawionymi tymi bitami, które potrzeba testować
Instrukcje sterujące programem w języku C będą głównym
tematem następnej, trzeciej części kursu.
A to kolejny przykładowy program, działa w następujący sposób:
Jeśli jest wciśnięty pierwszy przycisk (przyłączony do PC0), to
święcą się diody LED przyłączone do PD4..PD7, pozostałe LED są
zgaszone. Jeśli wciśnięty jest drugi przycisk ( przyłączony PC1), to
świecą się cztery diody podłączone do PD0..PD3, pozostałe LED
gasną.
/* przykład 2.4 "leds4.c" */
/* 8 diod LED przłączonych do portu D */
/* 2 przyciski przyłączone do PC0,PC1 */
/* ATmega 1MHz */
#include <avr/io.h>
int
main(
(
(
(
void
)
)
)
)
{
{
{
{
/* Wszystkie linie portu D będą wyjściami */
14 z 19
DDRD =
=
=
=
0xff
;
;
;
;
/* linie PC0,PC1 będą wejściami z podciągnięciem do VCC */
DDRC =
=
=
=
0x00
;
;
;
;
PORTC =
=
=
=
0x03
;
;
;
;
/* Początek nieskończonej pętli */
while
(
(
(
(
1
)
)
)
)
{
{
{
{
/* Jeśli pierwszy przycisk wciśnięty */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x01
))
))
))
)) PORTD =
=
=
=
0xf0
;
;
;
;
/* Jeśli drugi przycisk wciśnięty */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x02
))
))
))
)) PORTD =
=
=
=
0x0f
;
;
;
;
}
}
}
}
}
}
}
}
Listing 2.4
Efekt działania programu z listingu 2.4
Omówię tylko te fragmenty listingu, w których jest coś nowego.
Linia programu poniŜej testuje czy został wciśnięty przycisk
przyłączony do PC0. Sprawdzane jest czy odpowiadający
przyciskowi bit w rejestrze PINC ma wartość "0", jeśli tak to
wykonuje się instrukcję wpisującą do rejestru PORTD wartość 0xf0.
/* Jeśli pierwszy przycisk wciśnięty */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x01
))
))
))
)) PORTD =
=
=
=
0xf0
;
;
;
;
Podobnie w kolejnej linii programu sprawdzany jest stan drugiego
przycisku, podpietego do PC1; jeŜeli jest wciśnięty, to do rejestru
PORTD ładowana jest wartość 0x0f;
/* Jeśli drugi przycisk wciśnięty */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x02
))
))
))
)) PORTD =
=
=
=
0x0f
;
;
;
;
Odczytując stan przycisków podłączonych do portów uC napotyka
się kłopotliwy efekt drgających styków przycisku. W momentach
wciśnięcia i zwolnienia przycisku dostajemy na wejściu uC serie
impulsów, co "źle" napisany program moŜe błędnie zinterpretować
jako wielokrotne wciśnięcie i zwolnienie przycisku.
15 z 19
Drgania na stykach przycisku
Prostym i skutecznym sposobem ominięcia problemu drgających
styków przycisku jest przeczekanie drgań. Po wykryciu wciśnięcia
lub zwolnienia przycisku program moŜe wstrzymać dalsze działanie
na okres nieco dłuŜszy, niŜ spodziewany czas zaniku drgań styków.
PokaŜe to na kolejnym przykładzie, listing poniŜej.
Kolejny program będzie zliczał kaŜde wciśnięcie przycisku
przyłączonego do PC0, a aktualny stan tego licznika będzie
wyświetlany na ośmiu diodach LEd przyłączonych do portu D.
/* przykład 2.5 "leds5.c" */
/* 8 diod LED przłączonych do portu D */
/* przycisk przyłączony do PC0 */
/* ATmega 1MHz */
#define F_CPU 1000000L
#include <avr/io.h>
#include <util/delay.h>
int
main(
(
(
(
void
)
)
)
)
{
{
{
{
/* Wszystkie linie portu D będą wyjściami */
DDRD =
=
=
=
0xff
;
;
;
;
/* Linia PC0 będzie wejściem z podciągnięciem do VCC */
DDRC =
=
=
=
0x00
;
;
;
;
PORTC =
=
=
=
0x01
;
;
;
;
/* Początek nieskończonej pętli */
while
(
(
(
(
1
)
)
)
)
{
{
{
{
/* Jeśli pierwszy przycisk wciśnięty */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x01
))
))
))
))
{
{
{
{
/* Zwiększenie stanu licznika o 1 */
PORTD +=
+=
+=
+=
1
;
;
;
;
/* opóŜnienie aŜ drgania na stykach ustaną */
_delay_ms(
(
(
(
80
);
);
);
);
/* oczekiwanie na zwolnienie przycisku */
while
(!(
(!(
(!(
(!(PINC &
&
&
&
0x01
))
))
))
)) {}
{}
{}
{}
/* opóŜnienie aŜ drgania na stykach ustaną */
_delay_ms(
(
(
(
80
);
);
);
);
}
}
}
}
}
}
}
}
}
}
}
}
Listing 2.5
16 z 19
Efekt działania programu z listingu 2.5
Tutaj, w pętli sprawdzany jest stan przycisku, jeśli jest wciśnięty, to
wykonywany jest blok instrukcji po "if" objęty parą nawiasów
klamrowych, w przeciwnym przypadku stan przycisku testowany
jest ponownie.
/* Jeśli pierwszy przycisk wciśnięty */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x01
))
))
))
))
{
{
{
{
To kolejna postać operatora przypisania "+=", jak moŜna się
domyśleć, do rejestru PORTD zapisywana jest aktualna jego
zawartość powiększona o wartość stałej stojącej po prawej stronie
operatora "+=", w tym przypadku o 1.
/* Zwiększenie stanu licznika o 1 */
PORTD +=
+=
+=
+=
1
;
;
;
;
Wstrzymanie działania programu na spodziewany czas wygaśnięcia
drgań styków przycisku, czas wstrzymania programu dobrałem
doświadczalnie.
/* OpóŜnienie aŜ drgania na stykach ustaną */
_delay_ms(
(
(
(
80
);
);
);
);
Po ustaniu drgań styków, w następnej instrukcji, program testuje
czy przycisk został zwolniony. W tym miejscu program zostanie
uwięziony w wewnętrznej (zagnieŜdŜonej) pętli, wykonanej teŜ z
pomocą "while", do momentu zwolnienia przycisku. Po "while()",
między nawiasami klamrowymi, jest pusto, program w pętli będzi
"robił nic", zatrzma się w tym miejscu do chwili zwolnienia
przycisku.
/* oczekiwanie na zwolnienie przycisku */
while
(!(
(!(
(!(
(!(PINC &
&
&
&
0x01
))
))
))
)) {}
{}
{}
{}
Ponowne wstrzymanie działania programu, aby wygasły drgania
styków przycisku.
/* OpóŜnienie aŜ drgania na stykach ustaną */
_delay_ms(
(
(
(
80
);
);
);
);
Nawias klamrowy zamyka nieskończoną pętlę
}
}
}
}
17 z 19
I to juŜ wszystko w tej części kursu. Na zadanie domowe proponuje
uruchomić program z listingu poniŜej i samodzielnie przanalizować
jego działanie. Jeśli znajdzie się coś w tym kodzie, o czym nie było
jeszcze mowy, to porszę poszukać odpowiedzi swojej ksiąŜce do
języka C bądź w Internecie.
/* przykład 2.6 "leds6.c */
/* 8 diod LED przłączonych do portu D */
/* 2 przycisk przyłączone do PC0,PC1 */
/* Buzzer z generatorem przyłączony do PB1*/
/* ATmega 1MHz */
#define F_CPU 1000000L
#include <avr/io.h>
#include <util/delay.h>
int
main(
(
(
(
void
)
)
)
)
{
{
{
{
/* Wszystkie linie portu D będą wyjściami */
DDRD =
=
=
=
0xff
;
;
;
;
/* PB1 wyjście - buzzer z generatorem */
DDRB =
=
=
=
0x02
;
;
;
;
/* PC0,PC1 będą wejściami z podciągnięciem do VCC */
DDRC =
=
=
=
0x00
;
;
;
;
PORTC =
=
=
=
0x03
;
;
;
;
/* Początek nieskończonej pętli */
while
(
(
(
(
1
)
)
)
)
{
{
{
{
/* Jeśli pierwszy przycisk wciśnięty */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x01
))
))
))
))
{
{
{
{
if
(!
(!
(!
(!PORTD)
)
)
)
{
{
{
{
/* Krótki sygnał dźwiękowy */
PORTB |=
|=
|=
|=
0x02
;
;
;
;
_delay_ms(
(
(
(
100
);
);
);
);
PORTB &=
&=
&=
&= ~
~
~
~
0x02
;
;
;
;
}
}
}
}
else
/* Wygasza jedną diodę LED */
PORTD >>=
>>=
>>=
>>=
1
;
;
;
;
/* OpóŜnienie, aŜ drgania na stykach przycisku ustaną */
_delay_ms(
(
(
(
80
);
);
);
);
/* Oczekiwanie na zwolnienie przycisku */
while
(!(
(!(
(!(
(!(PINC &
&
&
&
0x01
))
))
))
)) {}
{}
{}
{}
/* OpóŜnienie, aŜ drgania na stykach przycisku ustaną */
_delay_ms(
(
(
(
80
);
);
);
);
}
}
}
}
/* Jeśli drugi przycisk wciśnięty */
if
(!(
(!(
(!(
(!(PINC &
&
&
&
0x02
))
))
))
))
{
{
{
{
if
(
(
(
(PORTD &
&
&
&
0X80
)
)
)
)
{
{
{
{
/* Krótki sygnał dźwiękowy */
PORTB |=
|=
|=
|=
0x02
;
;
;
;
_delay_ms(
(
(
(
100
);
);
);
);
PORTB &=
&=
&=
&= ~
~
~
~
0x02
;
;
;
;
}
}
}
}
else
{
{
{
{
/* Zapala jedną diodę LED */
PORTD <<=
<<=
<<=
<<=
1
;
;
;
;
PORTD |=
|=
|=
|=
1
;
;
;
;
}
}
}
}
/* Opóźnienie, aŜ drgania na stykach przycisku ustaną */
_delay_ms(
(
(
(
80
);
);
);
);
/* Oczekiwanie na zwolnienie przycisku */
while
(!(
(!(
(!(
(!(PINC &
&
&
&
0x02
))
))
))
)) {}
{}
{}
{}
/* OpóŜnienie, aŜ drgania na stykach przycisku ustaną */
_delay_ms(
(
(
(
80
);
);
);
);
18 z 19
}
}
}
}
}
}
}
}
}
}
}
}
Efekt działania programu z listingu 2.6
W następnej części kursu...
Tematem kolejnej części kursu będą: zmienne i stałe liczbowe,
operatory oraz instrukcje sterujące programem. Dalej będziemy się
bawić diodami LED, brzęczykiem i przyciskami.
Kurs AVR-GCC cz.3
© 2009 ABXYZ Wszelkie prawa zastrzeŜone
19 z 19