You are on page 1of 93

Podstawy AVR-GCC

Zawartość
AVR-GCC - wstęp ..................................................................................................................... 4
AVR-GCC - dystrybucja WinAVR............................................................................................ 4
Praca z kompilatorem ......................................................................................................... 5
AVR-GCC - kompilator ............................................................................................................. 5
AVR-GCC - kompilacja prostych programów........................................................................... 6
AVR-GCC - program make........................................................................................................ 7
Plik sterujący (makefile) .................................................................................................... 8
AVR-GCC - programowanie układu ........................................................................................ 13
Programator AVRprog (AVR910) ................................................................................... 13
Programator STK200 ....................................................................................................... 14
Oprogramowanie .............................................................................................................. 15
AVR-GCC - dostęp do zasobów mikrokontrolera ................................................................... 16
Rejestry specjalne ............................................................................................................. 16
Najważniejsze funkcje zawarte w pliku "avr/io.h" .......................................................... 16
AVR-GCC - wejście i wyjście binarne .................................................................................... 18
Rejestry............................................................................................................................. 19
Praktyczny sposób dostępu do wyprowadzeń układu ...................................................... 20
Podciąganie wejścia do logicznej jedynki ........................................................................ 20
Programy przykładowe .................................................................................................... 21
AVR-GCC - port szeregowy .................................................................................................... 23
Inicjalizacja ...................................................................................................................... 24
Wysyłanie znaku .............................................................................................................. 25
Odbiór znaku .................................................................................................................... 25
Program przykładowy ...................................................................................................... 26
AVR-GCC - pamięć SRAM ..................................................................................................... 28
Operacje na zmiennych .................................................................................................... 29
Program przykładowy ...................................................................................................... 30
AVR-GCC - pamięć programu (FLASH) ................................................................................ 32
Najważniejsze funkcje zawarte w pliku avr/pgmspace.h ................................................. 33
Tworzenie stałych w pamięci programu .......................................................................... 33
Czytanie stałych z pamięci programu .............................................................................. 33
Tworzenie tablic w pamięci programu ............................................................................. 33
Czytanie wartości z tablic w pamięci programu .............................................................. 33
Tworzenie łańcucha znaków ............................................................................................ 33
Czytanie łańcucha znaków ............................................................................................... 34
Program przykładowy ...................................................................................................... 34
AVR-GCC - pamięć EEPROM ................................................................................................ 36
Najważniejsze funkcje zawarte w pliku avr/eeprom.h..................................................... 36

1
Program przykładowy ...................................................................................................... 36
AVR-GCC - obsługa przerwań ................................................................................................ 38
Program przykładowy ...................................................................................................... 39
AVR-GCC - licznik/czasomierz TIMER 0 .............................................................................. 40
Tryb licznika .................................................................................................................... 41
Tryb czasomierza ............................................................................................................. 43
AVR-GCC - licznik/czasomierz TIMER 1 .............................................................................. 45
Tryb licznika .................................................................................................................... 45
Tryb czasomierza ............................................................................................................. 46
Tryb porównywania ......................................................................................................... 47
Tryb przechwytywania ..................................................................................................... 49
Tryb PWM - modulowana szerokość impulsu ................................................................. 50
AVR-GCC - licznik/czasomierz TIMER 2 .............................................................................. 52
Tryb czasomierza ................................................................................................................. 53
Program przykładowy ...................................................................................................... 53
Tryb porównywania ............................................................................................................. 56
Program przykładowy ...................................................................................................... 56
Program przykładowy ...................................................................................................... 58
AVR-GCC - komparator analogowy........................................................................................ 58
Programy przykładowe .................................................................................................... 59
AVR-GCC - przetwornik analogowo/cyfrowy ........................................................................ 61
Programy przykładowe .................................................................................................... 63
AVR-GCC - układ Watchdog .................................................................................................. 65
Najważniejsze funkcje zawarte w pliku avr/wdt.h ........................................................... 66
Program przykładowy ...................................................................................................... 66
AVR-GCC - tryby zmniejszonego poboru mocy ..................................................................... 68
Najważniejsze funkcje zawarte w pliku avr/sleep.h ........................................................ 68
Program przykładowy ...................................................................................................... 68
AVR-GCC - opcje wywoływania narzędzi .............................................................................. 71
AVR-GCC - opis funkcji biblioteki avr-libc ............................................................................ 76
Lista plików nagłówkowych ................................................................................................ 76
avr/crc16.h ............................................................................................................................ 77
avr/delay.h ............................................................................................................................ 77
avr/eeprom.h ......................................................................................................................... 77
avr/ina90.h ............................................................................................................................ 78
avr/interrupt.h ....................................................................................................................... 78
avr/io.h.................................................................................................................................. 79
avr/io[MCU].h ...................................................................................................................... 79
avr/parity.h ........................................................................................................................... 79
avr/pgmspace.h ..................................................................................................................... 79
avr/sfr_defs.h ........................................................................................................................ 81
avr/signal.h ........................................................................................................................... 81
avr/sleep.h ............................................................................................................................ 83
2
avr/timer.h ............................................................................................................................ 84
avr/twi.h................................................................................................................................ 84
avr/wdt.h ............................................................................................................................... 84
ctype.h .................................................................................................................................. 85
errno.h .................................................................................................................................. 86
inttypes.h .............................................................................................................................. 86
math.h ................................................................................................................................... 87
setjmp.h ................................................................................................................................ 88
stdlib.h .................................................................................................................................. 89
string.h .................................................................................................................................. 90
AVR-GCC - kompilacja środowiska ze źródeł ........................................................................ 91
Pakiet binutils ................................................................................................................... 92
Pakiet gcc-core ................................................................................................................. 92
Pakiet avr-libc .................................................................................................................. 93

3
AVR-GCC - wstęp
Kilka lat temu firma Atmel wprowadziła nową rodzinę 8 bitowych mikrokontrolerów
zbudowanych w architekturze RISC (o zredukowanej liście rozkazów), z których
większość wykonuje się w pojedynczym takcie zegara, posiadające bardzo rozbudowane
peryferia i łatwo programowalne w systemie docelowym (pięć przewodów łącznie z
zasilaniem). Wydawać by się mogło, że nowa rodzina mikrokontrolerów z zupełnie nową
niekompatybilną z innymi rodzinami listą rozkazów nie zostanie dobrze przyjęta na rynku.
Jednak stało się inaczej - dzięki mądrej polityce firmy Atmel, która udostępnia za darmo
narzędzia uruchomieniowe (np. AVR Studio), wielu entuzjastów mikrokontrolerów z
łatwością może testować swoje własne konstrukcje z tymi układami. Jednak wśród tych
narzędzi brakowało czegoś w dzisiejszych czasach ważnego a mianowicie kompilatora
języka wysokiego poziomu - nie zawsze mamy tyle czasu i chęci, żeby pisać każdy
program w asemblerze (ucząc się nowej listy rozkazów i technik programowania) i później
długo go testować. W tym momencie przychodzi na myśl zastosowanie języka, który byłby
blisko związany ze sprzętem (dostęp do rejestrów, peryferii, pamięci itp.) i jednocześnie
można byłoby w nim pisać programy na dosyć wysokim poziomie abstrakcji (bez
ukierunkowywania się na konkretny sprzęt). Tutaj na myśl przychodzi język C. Jest to
najbardziej rozpowszechniony język programowania komputerów. Spotyka się jego
implementacje na najróżniejsze maszyny: od prostych mikroprocesorów po potężne
superkomputery. Jednak jeżeli posiadamy już darmowe oprogramowanie do uruchamiania
na pewno pomyślimy sobie: czy nie ma również jakiegoś darmowego kompilatora języka
C na mikrokontrolery AVR? Okazuje się, że jest i to bardzo dobry. Wywodzi się z rodziny
GCC (GNU Compiler Collection), nazywa się AVR-GCC. Pierwotnie pracował pod
systemami unixowymi jak FreeBSD czy Linux (zresztą na takich systemach ciągle są
rozwijane jego nowe wersje), ale posiadając wersje źródłowe (bez problemu dostępne w
internecie na zasadzie "otwartego źródła") każdy chętny może "przenieść" go pod swój
ulubiony system operacyjny. Jednak przeciętnego użytkownika najbardziej interesuje
szybki efekt i tu z pomocą przychodzą tzw. "dystrybucje" zawierające kompilator z całym
szeregiem narzędzi i bibliotek.

AVR-GCC - dystrybucja WinAVR


Od końca 2002 roku w internecie dostępna jest "dystrybucja" WinAVR, którą stworzył
Eric Weddington. Najnowszą wersję można pobrać ze strony
http://sourceforge.net/project/showfiles.php?group_id=68108. Jest ona przeznaczona do
pracy pod systemami MS Windows. Instalacja kompilatora odbywa się poprzez
uruchomienie programu instalacyjnego. Program instalacyjny wprowadza niezbędne
modyfikacje do systemu (np. aktualizuje domyślne ścieżki poszukiwań programów).
Właśnie ta dystrybucja została użyta do przetestowania wszystkich programów tutaj
zawartych.

4
Praca z kompilatorem

Ponieważ programy wchodzące w skład pakietu są uruchamiane i przyjmują argumenty z


linii poleceń, dobrym pomysłem - przynajmniej na początek będzie więc praca z tzw.
"wiersza poleceń". Uruchamiamy wiersz poleceń systemu operacyjnego. W systemach
Windows można to zrobić z menu "Start -> Programy -> Wiersz poleceń".
Uwaga: w systemach bazujących na MS-DOS takich jak Windows 9x "wiersz poleceń"
nazywany jest "Tryb MS-DOS".
Wydajemy polecenie:

avr-gcc -v

W rezultacie powinien się nam wyświetlić tekst zawierający m.in wersję kompilatora:

Reading specs from C:/WinAVR/bin/../lib/gcc/avr/3.4.1/specs


Configured with: ../gcc-3.4.1/configure --prefix=e:/avrdev/install --
build=mingw
32 --host=mingw32 --target=avr --enable-languages=c,c++
Thread model: single
gcc version 3.4.1

Jeśli otrzymaliśmy tekst identyczny lub podobny do powyższego to wyszystko przebiegło


zgodnie z oczekiwaniami i możemy rozpocząć normalną pracę.

AVR-GCC - kompilator
Na kompilator AVR-GCC składa się wiele programów, są to:
• avr-addr2line - tłumaczy adresy w plikach wynikowych na numery linii w plikach
źródłowych
• avr-ar - tworzy, modyfikuje i wyciąga dane z archiwów
• avr-as - asembler
• avr-cpp - preprocessor C
• avr-gcc - kompilator
• avr-ld - linker (konsolidator)
• avr-nm - wyświetla nazwy symboliczne z plików wynikowych
• avr-objcopy - kopiuje i tłumaczy pliki objektowe (może "pięknie" deasemblować)
• avr-objdump - wyświetla różne informacje z plików objektowych
• avr-ranlib - generuje indeks do archiwum
• avr-size - wyświetla listę rozmiarów sekcji
• avr-strings - wyświetla łańcuchy ("drukowalne" znaki) z pliku
• avr-strip - usuwa symbole z plików wynikowych
• avr-libc - standardowa biblioteka funkcji dla kompilatora.
Ogólne określenie kompilator obejmuje cały zestaw narzędzi, bibliotek, plików
nagłówkowych, które łącznie pozwalają przetworzyć kod programu stworzony w C i
assemblerze do wynikowej postaci binarnego kodu maszynowego ładowanego do pamięci
flash mikrokontrolera. Przetwarzanie takie obejmuje następujące etapy:
• wykonanie na tekście programu dyrektyw preprocesora (np. wstawienie plików
(dyrektywy #include), zastąpienie fragmentów odpowiednimi definicjami (dyrektywy
#define));
• kompilacja modułów C do postaci plików assemblerowych;

5
• asemblacja plików assemblerowych do postaci relokowalnego kodu maszynowego / tj.
bez przydzielonych konkretnych adresów etykiet, skoków itp.;
• konsolidacja (linkowanie) czyli połączenie przygotowanych plików relokowalnych,
dołączenie kodu wywoływanych funkcji bibliotecznych i ustawienie wszystkiego
kolejno w przestrzeni adresowej programu z przydzieleniem konkretnych wartości
adresów; wynikiem jest plik w formacie elf zawierający wszelkie potrzebne
informacje o projekcie - wynikowy kod programu, zawartość obszaru eeprom, dane
dla debuggera itd.;
• utworzenie na podstawie pliku elf potrzebnych wyjściowych plików w odpowiednich
formatach (zawartość flash i eeprom w postaci bin lub hex, plik debuggera coff dla
potrzeb symulacji);
• pobranie z pliku elf dodatkowych użytecznych informacji o projekcie, jak np. zajętość
poszczególnych obszarów pamięci.
Właściwy program avr-gcc wykonuje dwa pierwsze etapy - czyli przetworzenie kodu C na
kod assemblerowy z uwzględnieniem ustawionych opcji, optymalizacji itp. Dalej sprawą
zajmują się specjalizowane narzędzia (binutils): assembler avr-as, linker avr-ld i szereg
innych: avr-objcopy, avr-objdump, avr-ar itd.).
Oczywiście wszystko należy uruchomić w odpowiedniej kolejności, z podaniem
potrzebnych argumentów i wymaganych opcji. Tradycyjnie służy do tego bardzo
uniwersalny i wszechstronny manager procesów make.
Istnieją dwie drogi prowadzące do przygotowania działającego środowiska kompilatora:
• pobranie źródeł z internetu, kompilacja i instalacja oprogramowania
• pobranie z internetu "dystrybucji" zawierającej skompilowane i (zazwyczaj)
przetestowane programy oraz jego instalacja
Każda z tych metod ma swoje "za" i "przeciw". Pierwsza gwarantuje, że będziemy mogli
używać najnowszych wersji narzędzi i bibliotek. Ponadto możemy pracować z tymi
narzędziami pod kontrolą swojego ulubionego systemu operacyjnego. Jest to jednak
okupione znacznie dłuższą i bardziej czasochłonną metodą instalacji nie gwarantującą
zawsze pełnego sukcesu.
Stosując drugą metodę efekt osiągamy niemal natychmiastowo. Otrzymujemy środowisko
jakie posiada zapewne wiele innych osób na świecie. Okupione jest to jednak mniejszą
elastycznością w pozyskiwaniu nowych wersji programów i bibliotek. Jednak dla
praktyków, którzy chcą wykorzystywać narzędzia a nie je tworzyć jest to najlepsze
rozwiązanie.

AVR-GCC - kompilacja prostych programów


Proste programy tzn. takie, które składają się tylko z jednego pliku źródłowego można
kompilować bez użycia programu make. Zakładając, że wybraliśmy mikrokontroler
AT90S2313 a program źródłowy znajduje się w pliku o nazwie program.c. Wydajemy
polecenie:

avr-gcc -mmcu=at90s2313 program.c

powoduje ono skompilowanie ww. programu na wybrany typ mikrokontrolera - tu:


AT90S2313 i utworzenie pliku wynikowego a.out. Należy zaznaczyć, że jest to najprostsze
z możliwych wywołanie kompilatora - program wynikowy nie jest optymalizowany itp.
Niestety plik a.out nie nadaje się do zaprogramowania pamięci flash mikrokontrolera.
Należy więc z niego "wydobyć" potrzebną treść - zrobimy to za pomocą programu avr-
objcopy potrafiącym manipulować plikami wynikowymi. Wydajemy polecenie:
6
avr-objcopy -O ihex -R .eeprom a.out program.hex

opcja -O ihex powoduje, że plik wyjściowy będzie miał format Intel HEX,
opcja -R .eeprom wyłącza z przetwarzania sekcję z zawartością przeznaczoną dla pamięci
EEPROM mikrokontrolera, a.out - plik wejściowy, program.hex - plik wyjściowy. Po
wykonaniu powyższego polecenia otrzymujemy plik .hex przeznaczony dla programatora
układu.
Aby w przyszłości za każdym razem nie wypisywać z klawiatury tylu komend, można
stworzyć plik wsadowy np. o nazwie avr-build.bat z następującą treścią:

@echo off
avr-gcc -mmcu=%1 %2.c
avr-objcopy -O ihex -R .eeprom a.out %2.hex

Wtedy kompilacja programu sprowadzi się do wydania polecenia:

avr-build at90s2313 program

Uwaga! istotna jest tutaj kolejność parametrów (najpierw typ mikrokontrolera).


Nazwa pliku z programem źródłowym musi być podana bez rozszerzenia nazwy.

AVR-GCC - program make


Kompilacja programu w języku C składa się z kilku faz. Pierwszą z nich jest
wygenerowanie tzw. pliku pośredniego (object file), zazwyczaj z rozszerzeniem ".o".
Następnie pliki pośrednie modułów i głównego programu są łączone za pomocą
konsolidatora (linker) w plik wykonywalny (.elf). Dla prostych programów te dwie
operacje mogą być wykonane w jednym kroku. Jednak plik .elf nie nadaje się do
bezpośredniego zaprogramowania mikrokontrolera (na dzień dzisiejszy nie są znane
programatory mikrokontrolerów "rozumiejące" ten format plików) dlatego należy jeszcze z
niego "wydobyć" dane w formacie obsługiwanym przez popularne programatory np. Intel
HEX.
Make jest programem podejmującym decyzję, które części dużego programu muszą zostać
zrekompilowanen i wywołującym polecenia służące do tego. Aby korzystać z programu
make potrzebujemy pliku zawierającego informacje o tym jak należy postępować w
przypadku zmian w plikach źródłowych i zależnościach między nimi. Domyślnie ten plik
nosi nazwę makefile.
Gdy zmieni się zawartość któregokolwiek pliku źródłowego musi on zostać
zrekompilowany, eżeli zmieni się zawartość któregoś z plików nagłówkowych bezpiecznie
jest rekompilować wszystkie źródła zawierające ten plik.
Kiedy którykolwiek z plików wynikowych (ang. object files; np. .o) się zmieni wtedy
trzeba ponownie skonsolidować całość.
Korzystanie z make sprowadza się wiec do stworzenia pliku makefile, który pokieruje
procesem kompilacji naszego programu.
Najczęściej używane opcje programu make:

-d włącza tryb szczegółowego śledzenia


-f plik_sterujacy umożliwia stosowanie innych niż standardowe nazw
plików sterujących
-i powoduje ignorowanie błędów kompilacji (stosować z ostrożnością!)
-n powoduje wypisanie poleceń na ekran zamiast ich wykonania

7
-p powoduje wypisanie makrodefinicji i reguł transformacji
-s wyłącza wypisywanie treści polecenia przed jego wykonaniem

Opcje można ze sobą łączyć. Np.: polecenie {make -np} powoduje wypisanie wszystkich
reguł i makrodefinicji oraz ciągu poleceń jakie powinne być wykonane, aby uzyskać
żądany cel.
Jest to pomocne w sytuacji, gdy chcemy sprawdzić poprawność definicji zawartych w
pliku sterującym bez uruchamiania długotrwałej kompilacji wielu plików.

Plik sterujący (makefile)

Plik sterujący zawiera definicje relacji zależności, które mówią w jaki sposób i z jakich
elementów należy stworzyć cel (program, bibliotekę, lub plik obiektowy) i wskazują pliki,
których zmiany implikują wykonanie powtórnej kompilacji poszczególnych celów. Plik
sterujący może również zawierać zdefiniowane przez programistę reguły transformacji. W
pliku makefile znakiem komentarza jest znak # (hash) umieszczony na początku linii.
Poniżej przedstawiłem skonstruowany przezemnie plik makefile, w którym w zasadzie
wystarczy zmienić tylko nazwę programu TARGET i typ mikrokontrolera MCU.

# Nazwa pliku z funkcją main() - BEZ ROZSZERZENIA!


TARGET = program

# typ mikrokontrolera
MCU = atmega163

# Katalog z bibliotekami użytkownika


USRLIB = ../../lib

# Lista plików źródłowych bibliotek w języku C


SRCLIB =
#include $(USRLIB)/conv/sources
#include $(USRLIB)/lcd/sources
#include $(USRLIB)/i2c/sources
#include $(USRLIB)/led7seg/sources
#include $(USRLIB)/kbd/sources
#include $(USRLIB)/delay/sources
#include $(USRLIB)/pcf8583/sources
#include $(USRLIB)/uart/sources

# Lista plików źródłowych w języku C


SRC = $(TARGET).c

# Lista plików źródłowych w asemblerze (rozszerzenie S - DUŻE S !)


ASRC =

# Format pliku wyjściowego (srec, ihex)


FORMAT = ihex

# Poziom optymalizacji (0, 1, 2, 3, s)


# (Uwaga: 3 nie zawsze jest najlepszym wyborem)
OPT = s

# Dodatkowe biblioteki
#
# Minimalna wersja printf
#LDFLAGS += -Wl,-u,vfprintf -lprintf_min
#
# Zmiennoprzecinkowa wersja printf (wymaga biblioteki matematycznej)

8
#LDFLAGS += -Wl,-u,vfprintf -lprintf_flt
#
# Biblioteka matematyczna
#LDFLAGS += -lm

include $(USRLIB)/avr_make

Zauważmy, ze na końcu dyrektywą

include $(USRLIB)/avr_make

jest włączany kolejny plik będący zbiorem reguł i makrodefinicji wspólnych dla każdego
projektu.
Oto jego listing:

# Przykładowy wspólny plik włączany do makefile dla avr-gcc


#
# Wywołanie programu make z linii komend:
# make clean <> czyści projekt
# make <> kompiluje projekt
# make install <> programuje układ za pomocą avrdude
# ---------------------------------------------------------
# Programowanie układu w systemie (usunąć komentaż z odpowiedniej linii)
PROG = stk200
#PROG = stk500
#PROG = avr910
# ---------------------------------------------------------
# Konwersja ELF na COFF dla symulatora (usunąć komentaż z odpowiedniej
linii)
# AVR Studio 3.5x i VMLAB do v3.9:
# COFFOUT = coff-avr
# AVR Studio 4.x i VMLAB od v3.10:
COFFOUT = coff-ext-avr
# ---------------------------------------------------------

# Opcje kompilatora
CFLAGS += -g
CFLAGS += -funsigned-char
CFLAGS += -funsigned-bitfields
CFLAGS += -fpack-struct
CFLAGS += -fshort-enums
CFLAGS += -Wall
CFLAGS += -Wstrict-prototypes
CFLAGS += -Wa,-ahlms=$(<:.c=.lst)
CFLAGS += -I$(USRLIB)
CFLAGS += -O$(OPT)

# Opcje asemblera
ASFLAGS = -Wa,-ahlms=$(<:.asm=.lst),-gstabs

# Opcje linkera
LDFLAGS += $(TARGET).a
LDFLAGS += -Wl,-Map=$(TARGET).map,--cref

# Definicje programów i komend.

CC = avr-gcc
OBJCOPY = avr-objcopy
OBJDUMP = avr-objdump

9
AR = avr-ar
REMOVE = rm -f
COPY = cp

# ---------------------------------------------------------

HEXSIZE = avr-size --target=ihex $(TARGET).hex


ELFSIZE = avr-size $(TARGET).elf

FINISH = echo Errors: none


BEGIN = echo -------- begin --------
END = echo -------- end --------

# ---------------------------------------------------------

# Definicje plików obiektowych


OBJ = $(SRC:.c=.o) $(ASRC:.asm=.o)

# ---------------------------------------------------------

# Definicje plików z wygenerowanymi listingami


LST = $(SRC:.c=.lst) $(ASRC:.asm=.lst)

# ---------------------------------------------------------

# Definicje plików obiektowych bibliotek


OBJLIB = $(SRCLIB:.c=.o) $(ASRCLIB:.asm=.o)

# ---------------------------------------------------------

# Scala wszystkie opcje i przełączniki. Dodaje typ procesora.


ALL_CFLAGS = -mmcu=$(MCU) -I. $(CFLAGS)
ALL_ASFLAGS = -mmcu=$(MCU) -I. -x assembler-with-cpp $(ASFLAGS)

# ---------------------------------------------------------

# Domyślne wywołanie
all: begin sizebefore \
$(TARGET).a \
$(TARGET).elf \
$(TARGET).lss \
$(TARGET).hex \
$(TARGET).eep \
$(TARGET).cof \
sizeafter finished end

# ---------------------------------------------------------
# Wyświetlanie tekstów.
begin:
@$(BEGIN)

finished:
@$(FINISH)

end:
@$(END)

# ---------------------------------------------------------

# Wyświetla rozmiar kodu wynikowego


sizebefore:

10
@if [ -f $(TARGET).elf ]; then echo Size before:; $(ELFSIZE);fi

sizeafter:
@if [ -f $(TARGET).elf ]; then echo Size after:; $(ELFSIZE);fi

# ---------------------------------------------------------

# Wyświetla informację na temat wersji kompilatora


gccversion :
$(CC) --version

# ---------------------------------------------------------

# Konwersja ELF na COFF dla symulacji w AVR Studio

COFFCONVERT=$(OBJCOPY) --debugging \
--change-section-address .data-0x800000 \
--change-section-address .bss-0x800000 \
--change-section-address .noinit-0x800000 \
--change-section-address .eeprom-0x810000

%.cof: %.elf
$(COFFCONVERT) -O $(COFFOUT) $< $@

# ---------------------------------------------------------

# Tworzy pliki wynikowe (.hex, .eep) z pliku ELF.


%.hex: %.elf
$(OBJCOPY) -O ihex -R .eeprom $< $@

%.eep: %.elf
$(OBJCOPY) -j .eeprom --set-section-flags=.eeprom="alloc,load" --
change-section-lma .eeprom=0 -O ihex $< $@

# ---------------------------------------------------------

# Deasemblacja: Tworzy rozszerzony listing z pliku ELF.


%.lss: %.elf
$(OBJDUMP) -h -S $< > $@

# ---------------------------------------------------------

# Konsolidacja: tworzy plik ELF z plików objektowych.


%.elf: $(OBJ)
$(CC) -mmcu=$(MCU) $(OBJ) $(LDFLAGS) --output $@

# ---------------------------------------------------------

# Kompilacja: tworzy pliki objektowe z plików źródłowych C.


%.o : %.c
$(CC) -c $(ALL_CFLAGS) $< -o $@

# ---------------------------------------------------------

# Kompilacja: tworzy pliki asemblera z plików źródłowych C.


%.s : %.c
$(CC) -S $(ALL_CFLAGS) $< -o $@

# ---------------------------------------------------------

# Asemblacja: tworzy pliki objektowe z plików źródłowych asemblera.

11
%.o : %.asm
$(CC) -c $(ALL_ASFLAGS) $< -o $@

# ---------------------------------------------------------

# Tworzenie pliku biblioteki użytkownika dla projektu


%.a : $(OBJLIB)
$(AR) rc $@ $?

# ---------------------------------------------------------

# Czyści projekt.
clean: begin clean_list finished end

clean_list :
$(REMOVE) $(SRC:.c=.s)
$(REMOVE) $(SRCLIB:.c=.s)
$(REMOVE) $(SRCLIB:.c=.lst)
$(REMOVE) $(OBJLIB)
$(REMOVE) $(OBJ)
$(REMOVE) $(LST)
$(REMOVE) $(TARGET).a
$(REMOVE) $(TARGET).hex
$(REMOVE) $(TARGET).eep
$(REMOVE) $(TARGET).obj
$(REMOVE) $(TARGET).cof
$(REMOVE) $(TARGET).elf
$(REMOVE) $(TARGET).map
$(REMOVE) $(TARGET).a90
$(REMOVE) $(TARGET).sym
$(REMOVE) $(TARGET).lnk
$(REMOVE) $(TARGET).lss

# ---------------------------------------------------------

# Programowanie układu w systemie

program: begin install end

install:
avrdude -p $(MCU) -c $(PROG) -U flash:w:$(TARGET).hex -U
eeprom:w:$(TARGET).eep

# ---------------------------------------------------------

# Zależności
$(TARGET).a : $(CONFIG)
$(TARGET).o : $(TARGET).c $(CONFIG)

Czasem występuje potrzeba "wyczyszczenia" projektu z plików będących efektem złych


kompilacji czy też podmiany niektórych plików wchodzących w skład projektu. Jeżeli
wydamy teraz komendę

> make clean

to zobaczymy w efekcie, że wszystkie pliki z wyjątkiem źródeł i makefile zostały usunięte


- to bardzo przydatna opcja i korzystajmy z niej jeśli wydaje się nam, że program powinien
działać a nie działa zgodnie z naszymi założeniami - oczywiście nie jest to metoda na
wszystkie bolączki programisty ale czasami właśnie tu tkwi problem.

12
AVR-GCC - programowanie układu
Jeśli kompilacja projektu przebiegnie poprawnie, można plik wynikowy (ten z
rozszerzeniem .hex) wpisać do pamięci programu mikrokontrolera. Tutaj nie powinno być
problemu, gdyż opublikowano bardzo dużo układów programatorów - od bardzo prostych
składających się tylko z wtyczki do portu równoległego aż do bardziej skomplikowanych,
niekiedy zawierających wbudowany mikrokontroler sterujący jego pracą, ale za to
pozwalające na programowanie różnych funkcji (np. przeprogramowywanie tzw. fuse bits
- blokowanie programowania szeregowego, uaktywnianie wewnętrznego generatora RC,
przyśpieszanie startu mikrokontrolera po włączeniu zasilania itp.), których (zwykle) nie
można zaprogramować prostymi programatorami (tzw. szeregowymi). Jednak do
szybkiego uruchamiania oprogramowania najlepsze są właśnie te proste programatory
szeregowe (ISP - ang. in system programming - programowanie w systemie docelowym
bez wyjmowania układu z podstawki).
Firma ATMEL ustandaryzowała złącza służące do programowania układów. W chwili
obecnej są stosowane złącza 6 i 10 stykowe. Ich schematy przedstawiono na rysunku:

Programator AVRprog (AVR910)

Jest to programator szeregowy komunikujący się z komputerem za pomocą łącza RS-232,


jego program obsługi na PC wchodzi w skład AVR Studio - można go uruchomić z menu
Tools|AVR prog. Schemat przedstawiono na rysunku:

13
Opis (PDF) znajduje się na stronie WWW producenta. Niestety główną przeszkodą w jego
skonstruowaniu może być konieczność zaprogramowania mikrokontrolera AT90S1200,
który jest jego głównym elementem. Programator jest zasilany napięciem pobieranym z
systemu, w związku z czym podczas korzystania z niego nie trzeba stosować dodatkowego
zasilacza.
W sieci można znaleźć wiele ciekawych "wariacji" na temat tego programatora jedną z
nich jest AVR910 na AT90S2313.

Programator STK200

Innym rozwiązaniem programatora ISP jest STK200 również firmowany przez firmę
Atmel lecz konstrukcję tego programatora jak również starterkitu STK200 opracowała
firma Kanda. Jest to programator szeregowy komunikujący się z komputerem za pomocą
pomocą portu równoległego (LPT), obsługiwany jest przez bardzo dużą liczbę programów.
Schemat elektryczny programatora STK200 pokazano na rysunku:

14
Układ IC1 spełnia rolę separatora linii wejść/wyjść interfejsu drukarkowego Centronics od
systemu, w którym znajduje się programowany mikrokontroler. Programator jest zasilany
napięciem pobieranym z systemu, w związku z czym podczas korzystania z niego nie
trzeba stosować dodatkowego zasilacza.

Oprogramowanie

Do obsługi ww. programatorów mogą służyć programy wchodzące w skład dystrybucji


WinAVR.
Są to: uisp oraz avrdude. Ich cechą szczególną jest praca w trybie wsadowym (z konsoli).
Pozwala to jednak na proste programowanie układu np. wykorzystując program make
poprzez wydanie komendy:

make program

oczywiście pod warunkiem, że wcześniej przygotowaliśmy sobie odpowiedni plik


makefile.
Wystarczy jego na końcu dopisać odpowiednią treść, która została podana niżej.
Uzupełnienie pliku makefile dla programu avrdude:

PROG = stk200

program:
avrdude -p $(MCU) -c $(PROG) -e -m flash -i $(TARGET).hex
avrdude -p $(MCU) -c $(PROG) -m eeprom -i $(TARGET).eep

Uzupełnienie pliku makefile dla programu uisp:

PROG = stk200

program:
uisp -v -dprog=$(PROG) --erase --upload if=$(TARGET).hex
uisp -v -dprog=$(PROG) --segment=eeprom --upload if=$(TARGET).eep

15
przy czym zmienna TARGET oznacza nazwę projektu a MCU - typ mikrokotrolera.
Po szczegóły dotyczące parametrów wywołania i konfiguracji programów uisp i avrdude
odsyłam do ich dokumentacji.

AVR-GCC - dostęp do zasobów


mikrokontrolera
Tutaj zostaną przedstawione podstawowe techniki dostępu do zasobów mikrokontrolera.
Oczywiście nie są to sposoby "jedynie słuszne" i każdy może uzyskać dostęp do
wybranych zasobów w inny wybrany przez siebie sposób.
W treści bardzo często będą występowały odwołania do różnych plików nagłówkowych -
dociekliwy czytelnik może je znaleźć w katalogu [avrgcc]/avr/include. Warto tam często
zaglądać - być może w ten sposób uda się łatwiej rozwiązać jakiś problem czy też znaleźć
bardziej odpowiednienią funkcję. Niestety dystrybucyjna kompilatora o nazwie WinAVR
nie zawiera w chwili obecnej plików źródłowych bibliotek. Nowe wersje bibliotek można
jednak znaleźć w internecie pod adresem: http://savannah.nongnu.org/projects/avr-libc/.
Jako platformę do testów używałem mikrokontrolera typu ATMega32. Zostało to
podyktowane jego bogatymi zasobami wewnętrznymi, łatwością montażu (obudowa
DIL40) oraz relatywnie niską ceną.
Oczywiście większość przykładów (tych mniej zaawansowanych) można skompilować
(czasem po drobnych modyfikacjach kodu źródłowego) i przetestować na mniejszych
układach np. AT90S2313.
Rodzina AVR jest bardzo duża i każdy może użyć takiego układu jaki ma "pod ręką" -
niestety z pewnymi wyjątkami: kompilator języka C zawarty w AVR-GCC nie obsługuje
prostszych układów AVR bez wewnętrznej pamięci SRAM i posługujących się
sprzętowym stosem. Nie będzie więc można wykorzystać bardzo popularnego układu
AT90S1200 i większości ATtiny. Pełną listę układów obsługiwanych przez kompilator i
jego biblioteki można zobaczyć przeglądając plik avr/io.h z biblioteki standardowej.

Rejestry specjalne

Dostęp do urządzeń peryferyjnych wbudowanych w mikrokontrolery AVR jest możliwy


poprzez rejestry specjalne. Do realizacji dostępu używa się makr zawartych w pliku
avr/sfr_defs.h. Dzięki nim jest możliwe (tak jak w komercyjnych kompilatorach)
uzyskanie dostępu do portów poprzez przypisanie wartości np. PORTA=0x55. Taką
konwencję dostępu do zasobów będziemy stosować w programach przykładowych. Jednak
w miejsce jednego z powyższych plików w naszym projekcie powinniśmy użyć avr/io.h,
który dodatkowo włącza odpowiednie definicje rejestrów w zależności od kontrolera, na
który jest kompilowany program (np. dla mikrokontrolera AT90S8515) włącza plik
avr/io8515.h. Typ mikrokontrolera, na który ma być skompilowany program jest określany
przez parametr przekazywany z linii poleceń do kompilatora np. -mmcu=at90s8515.
Łańcuch znaków po znaku równości zawiera wybrany typ mikrokontrolera.

Najważniejsze funkcje zawarte w pliku "avr/io.h"

Poniższe instrukcje ułatwiają podstawowe operacje związane z dostępem do rejestrów


specjalnych.

16
sbi (sfr, bit )
Instrukcja ta służy do ustawiania wskazanego bitu bit w rejestrze sfr. Pozostałe bity nie są
zmieniane. Przykład:

sbi (PORTB, 3);

lub

sbi (PORTB, PB3);

Obie funkcje robią dokładnie to samo czyli ustawiają 3 bit w PORTB. Zwróćmy uwagę na
fakt, że w drugim przykładzie jako numeru bitu użyto PB3 - w ten sposób możemy
zwiększyć czytelność programu.

cbi ( sfr, bit )


Instrukcja ta służy do kasowania (ustawiania na 0) wskazanego bitu bit w rejestrze sfr.
Pozostałe bity nie są zmieniane. Przykład:

cbi (PORTB, 3);

lub

cbi (PORTB, PB3);

Obie funkcje robią dokładnie to samo czyli kasują 3 bit w rejestrze PORTB.

bit_is_set ( sfr, bit )


Zwraca wartość większą od zera gdy bit jest ustawiony, w przeciwnym wypadku 0.
Przykład:

result = bit_is_set (PINB, PINB3);

loop_until_bit_is_set ( sfr, bit )


Wstrzymuje działanie programu (wykonuje pętlę) dopóki bit jest ustawiony. Przykład:

loop_until_bit_is_set (PINB, PINB3);

subsection {bit_is_clear ( sfr, bit )


Zwraca wartość większą od zera gdy bit jest skasowany, w przeciwnym wypadku 0.
Przykład:

result = bit_is_clear (PINB, PINB3);

loop_until_bit_is_clear ( sfr, bit)


Wstrzymuje działanie programu (wykonuje pętlę) dopóki bit jest skasowany. Przykład:

loop_until_bit_is_clear (PINB, PINB3);

subsection {inb ( sfr )


Instrukcja ta służy do czytania wartości w rejestrze sfr. Przykład:

res = inb (SREG);

17
lub poprzez przypisanie:

res = SREG;
Czyta rejestr SREG i wpisuje jego wartość do res.

inw ( sfr )
Instrukcja ta służy do czytania wartości 16 bitowej w rejestrze sfr. Tymi rejestrami są np.:
ADC, ICR1, OCR1A, OCR1B, TCNT1 itp. Zgodnie ze specyfikacją kontrolerów AVR ich
8 bitowe "połówki" muszą być odczytane w odpowiedniej kolejności aby poprawnie
została przeczytana 16 bitowa wartość. Właśnie inw wyręcza nas w pamiętaniu o tej
konieczności. Przykład:

res = inw (TCNT1);

lub poprzez przypisanie:

res = TCNT1;

Czyta rejestr TCNT1 i wpisuje jego wartość do res.

outb ( sfr , val )


Instrukcja ta służy do wpisania wartości val do portu sfr. Przykład:

outb(PORTB, 0xFF);

lub poprzez przypisanie:

PORTB = 0xFF;

Ustawiają wszystkie bity w rejestrze PORTB.

outw ( sfr , val )


Instrukcja ta służy do wpisania wartości 16 bitowej val do rejestru sfr. Tymi rejestrami są
np.: ADC, ICR1, OCR1A, OCR1B, TCNT1 itp. Zgodnie ze specyfikacją kontrolerów
AVR ich 8 bitowe "połówki" muszą być wpisane w odpowiedniej kolejności aby
poprawnie została wpisana 16 bitowa wartość. Właśnie outw wyręcza nas w pamiętaniu o
tej konieczności. Przykład:

outw (0xAAAA, OCR1A);

lub poprzez przypisanie:

OCR1A = 0xAAAA;

Wpisuje 0xAAAA do rejestru OCR1A.

AVR-GCC - wejście i wyjście binarne

18
Rejestry

Tutaj sytuacja się trochę (pozornie) komplikuje. Do każdego z fizycznych portów (tzn.
tych dostępnych z zewnątrz układu) przypisane są trzy rejestry. Ale dzięki temu możemy
niemal dowolnie konfigurować każdą końcówkę układu związaną z portami, tzn. ustalać
kierunek, podciągać wejścia do zasilania, odłączać od reszty układu elektronicznego itp.

Ustalanie kierunku - DDRx

Rejestr kierunku danych (ang. Data Direction Register X, gdzie X jest oznaczeniem
znakowym portu np. DDRA jest rejestrem kierunku dla portu A). Ustalenie kierunku
odbywa się wg zasady:

• ustawienie odpowiedniego bitu - wyjście


• skasowanie odpowiedniego bitu - wejście

Przykład:

DDRB = _BV(7)|_BV(6)|_BV(5)|_BV(4);

lub

DDRB = 0xF0;

Ustawia odpowiednie końcówki portu B: 0-3 na wejścia, 4-7 na wyjścia.

Rejestr zapisu danych - PORTx

Rejestr Danych Portu X (gdzie X jest oznaczeniem znakowym portu np. PORTA jest
rejestrem danych dla portu A). Na przykład aby załadować do rejestru PORTB wartość
0xAA należy użyć następującego kodu:

PORTB = 0xAA;

Rejestr odczytu danych - PINx

Odczyt z portu X (gdzie X jest oznaczeniem znakowym portu np. PINA jest rejestrem
odczytu portu A). Odczyt z tego portu daje nam fizyczny stan na końcówkach - oczywiście
pod warunkiem wcześniejszego ustalenia kierunku poprzez wpisanie zer w odpowiednie
bity rejestru kierunku.
Przykład:

DDRA = 0x00; // ustawienie kierunku na wejście


res = PINA;

Czyta fizyczną wartość na porcie A i umieszcza ją w res.

19
Praktyczny sposób dostępu do wyprowadzeń układu

Trzy rejestry dla jednego fizycznego portu mogą być dla wielu zbyt dużą uciążliwością
szczególnie w przypadku, gdy z jakiegoś powodu musimy zmienić port, do którego
podłączamy jakieś urządzenie - wtedy w całym programie musieli byśmy dokonywać
zmian dotyczących trzech portów - łatwo więc o pomyłkę. Można jednak wykorzystać
fakt, że wszystkie opisywane tu rejestry łączy pewna zależność: w przestrzeni adresowej
układu znajdują się one "koło siebie". Załóżmy że piszemy procedury obsługi np.
wyświetlacza LCD, będzie on podłączony do jednego fizycznego portu np. PORTB.
Wystarczy umieścić w programie odpowiednie makrodefinicje a cała operacja stanie się
banalnie prosta. Przykład:

#define DDR(x) _SFR_IO8(_SFR_IO_ADDR(x)-1) // adr. r. kier. PORTx


#define PIN(x) _SFR_IO8(_SFR_IO_ADDR(x)-2) // adr. r. wej. PORTx
#define LCD_PORT PORTB // używany port
#define LCD_PORT_O LCD_PORT // rejestr wyjściowy
#define LCD_PORT_D DDR(LCD_PORT) // rejestr kierunkowy
#define LCD_PORT_I PIN(LCD_PORT) // rejestr wejściowy

Po napisaniu takich makrodefinicji w dalszej części programu już nie posługujemy się
nazwami w rodzaju: PORTB, PINB, DDRB ale: LCD_PORT_O, LCD_PORT_I,
LCD_PORT_D. Ma to również jeszcze jedną zaletę - jeśli będziemy chcieli zmienić w
programie port do którego ma być przyłączone obsługiwane urządzenie wystarczy zmiana
tylko jednej linijki np.:

#define LCD_PORT PORTB

na:

#define LCD_PORT PORTD

a dalszą zamianą zajmie się kompilator (konkretnie jego preprocesor).

Podciąganie wejścia do logicznej jedynki

Bardzo interesującą cechą układów AVR (szczególnie dla używających w swej praktyce
układów MCS-51) jest możliwość "podciągania" wejść do logicznej jedynki bez użycia
zewnętrznych rezystorów. Robi się to w ten sposób, że przy wpisanym zerze do bitu
kierunku DDRx (ustawienie bitu portu jako wejście) należy wpisać jedynkę na ten sam bit -
ale do portu PORTx. Zostanie to zilustrowane na przykładzie:

cbi(DDRB,7); // użyj linii PB7 jako wejścia


sbi(PORTB,7); // "podciągnij" do logicznej 1 linię PB7

Tutaj celowo użyto dwóch instrukcji, jednak praktycznie można użyć tylko ostatniej
"podciągającej" wejście gdy mamy pewność, że wcześniej nie był ustawiany dany bit
ustalający kierunek w pocie (DDRx) - po starcie mikrokontrolera wszystkie bity związanie
z portami fizycznymi są wyzerowane czyli porty są ustawione na odczyt danych.

20
Programy przykładowe

Po zapoznaniu z podstawowymi operacjami wejścia/wyjścia możemy napisać i


przetestować nasz pierwszy program na mikrokontroler AVR w języku C. Na początek
będzie to sterowanie diodą LED podłączoną przez rezystor 470 om między zasilanie a
wyjście PD4 przy pomocy przycisku monostabilnego podłączonego między linię PD3 a
masę.

// Sterowanie diodą LED podłączoną do linii PD4 mikrokontorlera


// za pomocą przycisku podłączonego do linii PD3 mikrokontrolera

#include <avr/io.h> // dostęp do rejestrów

int main( void ) // program główny


{
sbi(DDRD,4); // użyj linii PD4 jako
wyjścia
sbi(PORTD,3); // "podciągnij" do
logicznej 1 linię PD3

while(1) // pętla nieskończona


{
cbi(PORTD,4); // zapal diodę LED podłączoną do
linii PD4
loop_until_bit_is_clear(PIND,3); // czekaj na naciśnięcie
przycisku na PD3
sbi(PORTD,4); // zgaś diodę LED podłączoną do
linii PD4
loop_until_bit_is_clear(PIND,3); // czekaj na naciśnięcie
przycisku na PD3
}
}

Dyrektywa #include dołącza do programu pliki z definicjami portów, rejestrów i funkcji


dostępu do nich. Najlepiej, podczas pisania programu mieć zawsze otwarty gdzieś "na
boku" ten plik i inne, które on dodatkowo włącza - głównie plik z definicjami dla
mikrokontrolera, na który będzie skompilowany nasz program np. dla AT90S8515 będzie
to avr/io8515.h. Dalej znajduje się główna funkcja programu int main(void), od której
zawsze rozpoczyna się działanie programu napisanego w języku C. Instrukcja
sbi(DDRB,0) powoduje wpisanie jedynki do bitu 0 rejestru DDRB. Jest to rejestr ustalający
kierunek przepływu danych w porcie B. W efekcie możemy używać linię PB0
mikrokontrolera jako wyjścia. Kolejna instrukcja: sbi(PORTB,7) wpisuje jedynkę do bitu 7
rejestru PORTB, w tym samym czasie bit 7 portu DDRB jest wyzerowany, gdyż
mikrokontroler po starcie ma wpisane zera do wszystkich rejestrów związanych z
fizycznymi portami mikrokontrolera. W efekcie linia PB7 mikrokontrolera pracuje jako
wejście "podciągnięte" do logicznej jedynki - nie musimy używać rezystora zewnętrznego.
Kolejna instrukcja while(1) to pętla nieskończona, w której wykonywana jest reszta
programu mikrokontrolera. Każdy program na mikrokontrolery musi zawierać jakąś pętlę
nieskończoną - program nie może się bowiem zakończyć. W ostateczności może być
zakończony pustą pętlą nieskończoną. Tak będą realizowane niektóre proste programy
przykładowe. Dalej występuje instrukcja cbi(PORTB,0) powodująca wpisanie zera do bitu
0 rejestru PORTB co w efekcie powoduje pojawienie się stanu niskiego na wyprowadzeniu
PB0 mikrokontrolera i zapalenie diody LED podłączonej między linię PB0 a linię zasilania
(oczywiście przez rezystor). W następnej linii instrukcja loop_until_bit_is_clear(PINB,7)

21
powoduje zatrzymanie programu do momentu, aż na bicie 7 rejestru PINB pojawi się stan
niski. W efekcie program czeka na naciśnięcie przycisku podłączonego pomiędzy linię
PB7 mikrokontrolera a masę. Następnie instrukcja sbi(PORTB,0) wpisuje jedynkę do bitu
0 rejestru PORTB co w efekcie powoduje pojawienie się stanu wysokiego na
wyprowadzeniu PB0. W efekcie gasi diodę LED podłączoną do linii PB0. Dalej ponownie
pojawia się instrukcja loop_until_bit_is_clear(PINB,7) i pętla się zamyka - program
przechodzi ponownie do wykonywania instrukcji cbi(PORTB,0) itd.
Po zaprogramowaniu układu mikrokontrolera powinna zapalić się dioda LED podłączona
do linii PB0. Naciśnięcie przycisku podłączonego do linii PB7 powinno spowodować
lekkie zmniejszenie jasności świecenia diody (pojawia się tam fala prostokątna o
wypełnieniu 50\%). Gdy przestaniemy naciskać przycisk dioda zapali się lub zgaśnie w
zależności od tego, w którym miejscu był program w momencie zwolnienia przycisku.
Załóżmy jednak, że w przyszłości będziemy chcieli podłączyć diodę LED do innej
końcówki układu, bo np. w ten sposób uprościmy płytkę drukowaną dla układu. Podobnie
może być z przyciskiem. I co wtedy? Tutaj być może jeszcze nie będzie wielkiego
problemu, ot zmienimy wpisy w paru linijkach kodu i po kłopocie!
Jednak gdy kod źródłowy rozrośnie się powiedzmy do kilkuset a nawet kilku tysięcy linii
to tak napisany program może być bardzo trudny do późniejszej modyfikacji, a o pomyłkę
będzie bardzo łatwo. Warto więc zawczasu nabrać kilku dobrych nawyków. Po pierwsze -
korzystajmy z makrodefinicji (to nie pochłania pamięci programu mikrokontrolera!).
Program źródłowy jest większy i wygląda na bardziej skomplikowany lecz wielkość
programu wynikowego nie zmienia się w stosunku do tego z powyżsego listingu.

// Sterowanie diodą LED podłączoną do dowolnej linii mikrokontorlera


// za pomocą przycisku podłączonego do dowolnej linii mikrokontrolera

#include <avr/io.h> // dostęp do rejestrów

#define DDR(x) _SFR_IO8(_SFR_IO_ADDR(x)-1) // adr. rej. kier. PORTx


#define PIN(x) _SFR_IO8(_SFR_IO_ADDR(x)-2) // adr. rej. wej. PORTx

#define LED_PORT PORTD // port diody LED


#define LED_BIT 4 // bit diody LED
#define LED_PORT_O LED_PORT // rejestr wyjściowy
#define LED_PORT_D DDR(LED_PORT) // rejestr kierunkowy
#define LED_PORT_I PIN(LED_PORT) // rejestr wejściowy

#define KEY_PORT PORTD // port przycisku


#define KEY_BIT 3 // bit przycisku
#define KEY_PORT_O KEY_PORT // rejestr wyjściowy
#define KEY_PORT_D DDR(KEY_PORT) // rejestr kierunkowy
#define KEY_PORT_I PIN(KEY_PORT) // rejestr wejściowy

int main( void ) // program główny


{
sbi(LED_PORT_D,LED_BIT); // użyj linii jako wyjścia
sbi(KEY_PORT_O,KEY_BIT); // "podciągnij" linię do logicznej 1

while(1) // pętla nieskończona


{
cbi(LED_PORT_O,LED_BIT); // zapal diodę LED
loop_until_bit_is_clear(KEY_PORT_I,KEY_BIT); // czekaj na naciśnięcie
przycisku
sbi(LED_PORT_O,LED_BIT); // zgaś diodę LED
loop_until_bit_is_clear(KEY_PORT_I,KEY_BIT); // czekaj na naciśnięcie
przycisku

22
}
}

Makrodefinicjami:

#define DDR(x) _SFR_IO8(_SFR_IO_ADDR(x)-1)


#define PIN(x) _SFR_IO8(_SFR_IO_ADDR(x)-2)

wyliczają adresy rejestru kierunkowego i wejściowego dla podanego portu wyjściowego.


W kolejnych liniach mamy definicję:

#define LED_PORT PORTB

gdzie symbolowi LED_PORT jest przypisywana wartość znajdująca się w linii po nim
czyli PORTB. W ten sposób zmieniając napis PORTB np. na PORTD spowodujemy, że
symbol LED_PORT przyjmie wartość PORTD. Teraz w dalszej części programu będziemy
się posługiwać symbolem LED_PORT. W kolejnym wierszu programu znajduje się
definicja:

#define LED_BIT 0

przyporządkowująca symbolowi LED_BIT wartość 0. Analogicznie jak w definicji portu


możemy zmieniać tę wartość w zakresie od 0 do 7 - oczywiście, jeżeli budowa portu
używanego mikrokontrolera na to pozwala. Linie programu zawierające makrodefinicje:

#define LED_PORT_O LED_PORT

- definiuje rejestr wyjściowy - w dalszej części programu będziemy używać nazwy


LED_PORT_O

#define LED_PORT_D DDR(LED_PORT)

- definiuje rejestr kierunkowy - w dalszej części programu będziemy używać nazwy


LED_PORT_D

#define LED_PORT_I PIN(LED_PORT)

- definiuje rejestr wejściowy - w dalszej części programu będziemy używać nazwy


LED_PORT_I.
Analogiczne definicje dla klawisza (KEY_PORT, KEY_BIT) znajdziemy w kolejnych
liniach programu. Dalej znajduje się główna funkcja programu int main(void) której
"ciało" wygląda analogicznie do tej z listingu pierwszego z tą tylko różnicą, że nazwy
portów i numery bitów zastąpiono nazwami definiowanymi przez nas. Jak widać długość
listingu programu w stosunku do poprzedniego wzrosła dosyć znacznie - ale to się opłaca!
- teraz zmiana przyporządkowania urządzeń do końcówek układu wymaga zmian w
czterech miejscach. Już w tak krótkim programie zyskaliśmy 3 razy mniej zmian!

AVR-GCC - port szeregowy


Większość kontrolerów AVR posiada wbudowany układ pozwalający na przesyłanie
informacji w postaci szeregowej za pomocą linii: TXD - wyjście szeregowe i RXD -

23
wejście szeregowe. Transmisja może się odbywać w trybie full-duplex, gdyż układ ten
posiada dwa niezależne rejestry transmisyjne. Układ posiada także własny układ taktujący,
co zwalnia liczniki-czasomierze z generowania tego sygnału. Jest to znaczne rozszerzenie
możliwości układu UART w stosunku do popularnej rodziny kontrolerów 8051.
Jego główne cechy to:

• ustawianie praktycznie dowolnej prędkości transmisji z przedziału 2400 - 115000 kb/s


(dla częstotliwości zegara mikrokontrolera 1 - 8 MHz)
• duże prędkości transmisji przy małej częstotliwości zegarowej
• filtracja szumów i zakłóceń
• rozmaite detekcje błędów
• generuje trzy oddzielne przerwania:
o od zakończenia odbioru znaku,
o od zakończenia transmisji znaku,
o od opróźnienienia rejestru transmisji znaku.

Uwaga!
Ze względu na duże rozbieżności w nazewnictwie rejestrów i ich bitów kontrolujących
port szeregowy opis będzie dotyczył "klasycznych" układów AVR np. AT90S2313 i
AT90S8515 - w układach ATMega istnieją ich odpowiedniki o często rozszerzonej
funkcjonalności.

Interfejs pomiędzy AVR a PC

Inicjalizacja

Aby zrealizować transmisję danych należy zainicjować rejestry kontrolne odpowiednimi


wartościami.Po pierwsze należy ustalić prędkość transmisji. Robi się to przez wpisanie do
rejestru UBRR odpowiedniej wartości. Na przykład dla częstotliwości zegara 8MHz i
szybkości 9600 bodów wpisujemy do UBRR wartość 51 odczytaną z tabeli znajdującej się
w nocie katalogowej układu. Można też obliczyć tę wartość korzystając z zależności:

F_CPU
UBRR = ---------------- - 1
(UART_BAUD * 16)

24
gdzie:
F_CPU - częstotliwość zegara mikrokontrolera w Hz,
UART_BAUD - prędkość transmisji w b/s.
Powyższą formułę można umieścić w programie jako makrodefinicje np.

#define F_CPU 8000000 //częstotliwość zegara w Hz


#define UART_BAUD 19200 //prędkość transmisji
#define UART_CONST (F_CPU/(16ul*UART_BAUD)-1)

następnie wyliczoną przez preprocesor wartość umieszczamy w rejestrze UBRR:

UBRR = (unsigned char)UART_CONST; // ustaw prędkość transmisji

Gdy mamy ustaloną prędkość transmisji należy zezwolić na transmisję i/lub odbiór
znaków (w zależności od potrzeb). Robi się to przez ustawianie odpowiednich bitów w
rejestrze UCR (RXEN - zezwolenie na odbiór, TXEN - zezwolenie na nadawanie) np.
poprzez wpisanie takiego kodu:

UCR = _BV(RXEN)|_BV(TXEN);

W tym momencie mamy zainicjowany interfejs szeregowy - pora więc z niego skorzystać.

Wysyłanie znaku

Główną częścią układu nadajnika transmisji jest rejestr przesuwający, połączony z


rejestrem bufora UDR, do którego należy wpisać transmitowany bajt. Po stwierdzeniu że
rejestr przesuwający jest pusty, zawartość rejestru UDR jest przepisywana do rejestru
przesuwającego co rozpoczyna transmisję tego bajtu. Napiszmy więc procedurę służącą do
wysłania znaku na port szeregowy. Oto ona:

void putchar (char c)


{
UDR=c;
loop_until_bit_is_set(USR,TXC);
sbi(USR,TXC);
}

Instrukcja UDR=c umieszcza w rejestrze UDR znak do wysłania. Instrukcja


loop_until_bit_is_set(USR,TXC) powoduje oczekiwanie programu na koniec transmisji
znaku. Instrukcja sbi(USR,TXC) ustawia znacznik TXC w rejestrze USR aby można było
sprawdzać kolejny wysyłany bajt. Dociekliwy czytelnik na pewno zauważy, że powyższa
instrukcja powoduje ustawienie już i tak ustawionego bitu TXC w rejesterze USR - więc po
co? Otóż niektóre bity znaczników w mikrokontrolerach AVR mają taką właściwość, że
skasować je można m.in. przez programowe ustawienie tego bitu. Do nich m.in. należy
TXC.

Odbiór znaku

Podobnie jak w przypadku nadajnika, główną częścią części odbiorczej jest rejestr
przesuwający oraz połączony z nim rejestr bufora UDR. Jak wspomniano na początku
25
część odbiorcza jest wyposażona w układ eliminujący zakłócenia jakie mogą wystąpić
podczas transmisji bitów. Mechanizm jego działania jest prosty. Polega on na
wielokrotnym próbkowaniu stanu linii RxD. Jeśli logika sterująca stwierdzi, że co najmniej
dwie ostatnie z trzech próbek – w środku “okna” dla każdego z bitów – są identyczne, stan
linii jest uznawany za ważny i bit trafia do rejestru przesuwającego.
Gdy mamy już procedurę służącą do wysyłania danych przydała by się do niej
komplementarna - czyli odbierająca znaki oto i ona:

char getchar (void)


{
loop_until_bit_is_set(USR,RXC);
cbi(USR,RXC);
return UDR;
}

Instrukcja loop_until_bit_is_set(USR,RXC) powoduje oczekiwanie programu na koniec


odbierania znaku. Instrukcja cbi(USR,RXC) kasuje znacznik RXC aby można było
sprawdzać kolejny odbierany bajt. Instrukcja {return UDR} zwraca wartość umieszczoną
w rejestrze UDR.

Program przykładowy

Poniższy przykład będzie rozwinięciem poprzednich dwóch prostych programów o


możliwość sterowania diodą LED z portu szeregowego i dodatkowo sygnalizowaniem jej
stanu znakiem wysyłanym na port szeregowy. Do sprawdzenia działania tego programu
potrzebny będzie program emulujący terminal np. tandardowy "windowsowy"
Hyperterminal lub darmowy program Br@y++ Terminal do pobrania ze strony autora
programu: http://bray.velenje.cx/avr/terminal/.
Dla wybranego łącza szeregowego (tego, do którego mamy podłączony mikrokontroler)
ustalamy następujące parametry: prędkość transmisji 19200 b/s, 8 bitów danych, brak
parzystości, 1 bit stopu, brak sterowania przepływem. Do podłączenia komputera portu
szeregowego w mikrokontrolerze można użyć konwertera poziomów MAX232 lub jego
odpowiednika.

// Sterownie diodą LED podłączoną do dowolnej linii mikrokontrolera


// za pomocą dowolnego znaku odebranego z portu szeregowego
// mikrokontrolera i wysyłanie jej stanu na port szeregowy

#include <avr/io.h> // dostęp do rejestrów

// Zmieniając poniższe definicje można dostosować program do potrzeb

#define F_CPU 8000000ul // częstotliwość zegara w


Hz
#define UART_BAUD 19200ul // prędkość transmisji
bit/s

#define LED_PORT PORTD // port diody LED


#define LED_BIT 4 // bit diody LED

#define DDR(x) _SFR_IO8(_SFR_IO_ADDR(x)-1) // adr. rej. kier. PORTx


#define PIN(x) _SFR_IO8(_SFR_IO_ADDR(x)-2) // adr. rej. wej. PORTx

#define LED_PORT_O LED_PORT // rejestr wyjściowy


#define LED_PORT_D DDR(LED_PORT) // rejestr kierunkowy

26
#define LED_PORT_I PIN(LED_PORT) // rejestr wejściowy

#define UART_CONST (F_CPU/(16ul*UART_BAUD)-1)

// _UCR_

#ifdef UCR
#define _UCR_ UCR
#endif

#ifdef UCSRB
#define _UCR_ UCSRB
#endif

#ifdef UCSR0B
#define _UCR_ UCSR0B
#endif

// _USR_

#ifdef USR
#define _USR_ USR
#endif

#ifdef UCSRA
#define _USR_ UCSRA
#endif

#ifdef UCSR0A
#define _USR_ UCSR0A
#endif

// Definicje funkcji

// inicjalizacja portu szeregowego


void UART_init(void)
{
UBRR = (unsigned char)UART_CONST; // ustaw prędkość transmisji
_UCR_ = _BV(RXEN)|_BV(TXEN); // załącz tx, rx
}

// wysyła znak podany jako parametr na port szeregowy


void UART_putchar (char c)
{
UDR = c; // wpisz c do rejestru UDR
loop_until_bit_is_set(_USR_,TXC); // czekaj na zakończenie
transmisji
sbi(_USR_,TXC); // ustaw bit TXC w rej.
USR
}

// odbiera znak z portu szeregowego i zwraca go jako wartość funkcji


char UART_getchar (void)
{
loop_until_bit_is_set(_USR_,RXC); // czekaj na zakończenie
odbioru
cbi(_USR_,RXC); // skasuj bit RXC w rej.
USR
return UDR; // zwróć zawartość rejestru
UDR
}

27
int main(void) // program główny
{
UART_init(); // inicjalizacja portu szeregowego

sbi(LED_PORT_D,LED_BIT); // użyj linii jako wyjścia

while(1) // pętla nieskończona


{
cbi(LED_PORT_O,LED_BIT); // zapal diodę LED
UART_putchar('1'); // wyślij '1' na port szeregowy
UART_getchar(); // czekaj na znak z portu szeregowego
sbi(LED_PORT_O,LED_BIT); // zgaś diodę LED
UART_putchar('0'); // wyślij '0' na port szeregowy
UART_getchar(); // czekaj na znak z portu szeregowego
}
}

Na początku programu znajdują się makrodefinicje, które globalnie ustalają parametry


pracy układu takie jak: częstotliwość zegara (kwarcu) mikrokontrolera, prędkość transmisji
szeregowej, port i jego bit, do którego jest podłączona dioda LED. Wartości te
(wyróżnione przez podkreślenie i wytłuszczenie) w razie konieczności można zmienić. W
dalszej części znajdują się poznane już wcześniej makrodefinicje służące do określania
adresów rejestrów portów mikrokontrolera. Później spotykamy makrodefinicję wyliczającą
stałą, którą mamy załadować do rejestru UBRR aby ustawić żądaną prędkość transmisji. Po
tej sporej dawce makr widzimy definicje funkcji, które wcześniej zostały szczegółowo
opisane: UART_init(), UART_putchar() i UART_getchar(). W końcu docieramy do
głównej funkcji naszego programu: main(). Tutaj opis zostanie pominięty - komentarze
powinny wystarczyć. Po starcie programu w układzie powinna się świecić dioda
podłączona do PB0 a na terminalu ostatnim wyświetlonym znakiem powinna być "1"
(jedynka), następnie z terminala wysyłamy dowolny znak np. naciskając spację. Dioda
powinna zgasnąć a na terminalu powinien pojawić się znak "0" (zero).

AVR-GCC - pamięć SRAM


Zmienne, które definiujemy w programie bez żadnych dodatkowych atrybutów są
umieszczane przez kompilator w pamięci SRAM mikrokontrolera. Jest ona podłączona
bezpośrednio do magistrali (bez rejestrów pośredniczących jak w opisywanej w dalszej
części książki pamięci EEPROM) co znacznie przyspiesza dostęp a także nie ma potrzeby
korzystania z dodatkowych funkcji aby z niej skorzystać.
Należy podkreślić, że w przestrzeni adresowej pamięci SRAM znajdują się także 32
rejestry ogólnego przeznaczenia R0-R31 oraz 64 (większość) lub 224 (np. ATmega128)
bajty zarezerwowane dla rejestrów zintegrowanych w mikrokontrolerze układów
peryferyjnych. W związku z tym adres rzeczywistej pamięci SRAM zaczyna się dla
większości układów od adresu 96 (0x60) lub 256 (0x100) jak np. w ATmega128. Podobnie
jak w większości układów rodziny MCS-51 istniała możliwość dołączenia zewnętrznej
pamięci danych tak i w rodzinie AVR mamy taką możliwość (ale tylko niektóre układy
AVR). Istnieje tu jednak znacząca różnica - jeżeli w MCS-51 ta dodatkowa pamięć
zajmowała oddzielną przestrzeń adresową, to w AVR ta pamięć stanowi "przedłużenie"
istniejącej przestrzeni adresowej pamięci SRAM. Co więcej pamięć tą można w dowolnej
chwili programowo podłączać i odłączać od systemu (poprzez zmianę stanu bitu SRE w
rejestrze MCUCR), przez co uzyskuje się jeszcze większą elastyczność w konstruowaniu

28
urządzeń, ponieważ możemy w zasadzie w dowolnej chwili skorzystać z linii używanych
do komunikacji z pamięcią np. w celu obsłużenia innych urządzeń. W pamięci SRAM
przechowywane są wszystkie zmienne. Deklaracje często używanych całkowitych typów
danych znajdują się w pliku avr/inttypes.h. Stałe są tworzone poprzez podanie słowa
kluczowego const. Taka "zmienna" posiada atrybut tylko do odczytu i nie może być
zmieniana - jest stałą przechowywaną w pamięci SRAM.
W tabeli przedstawiono predefiniowane typy zmiennych wg standardu ISO C99 stworzone
po to, aby ułatwić pisanie programów i było łatwiejsze szybkie zorientowanie się w ilości
bajtów (bitów) zajmowanych przez daną zmienną.

Predefiniowane typy zmiennych w pliku inttypes.h

Nazwa typu danych | Długość w bajtach | Zakres wartości


int8_t | 1 | -128 ... 127
uint8_t | 1 | 0 ... 255
int16_t | 2 | -32768 ... 32767
uint16_t | 2 | 0 ... 65535
int32_t | 4 | -2147483648 ... 2147483647
uint32_t | 4 | 0 ... 4294967295
int64_t | 8 | -9,22*10^18 ... 9,22*10^18
uint64_t | 8 | 0 ... 1,844*10^19

Są też stosowane bardziej skrócone nazwy w rodzaju u08, s08, u16 itp. Jak widać są one
krótsze a równie łatwo można się zorientować ile miejsca w pamięci będą zajmowały
zmienne. Oto definicje takich typów danych:

typedef unsigned char u08; // 8 bitów bez znaku (int8_t)


typedef char s08; // 8 bitów ze znakiem (uint8_t)
typedef unsigned short u16; // 16 bitów bez znaku (int16_t)
typedef short s16; // 16 bitów ze znakiem (uint16_t)
typedef unsigned long u32; // 32 bity bez znaku (int32_t)
typedef long s32; // 32 bity ze znakiem (uint32_t)
typedef unsigned long long u64; // 64 bity bez znaku (int64_t)
typedef long long s64; // 64 bity ze znakiem (uint64_t)

Operacje na zmiennych

Tworzenie zmiennej

Tworzenie zmiennej to prosta operacja, którą można wykonać na wiele sposobów np.:

unsigned char val = 8;

lub

uint_8 val = 8;

lub

u08 val = 8;

Po wywołaniu jednej z powyższych instrukcji zostanie utworzona nowa zmienna o nazwie


val i zainicjowana wartością 8. Ta zmienna zajmie 1 bajt pamięci SRAM. Widać wyraźnie

29
korzyść
z zastosowania krótszej nazwy typu - po prostu mniej pisania a efekt taki sam!

Tworzenie zmiennej w rejestrze

Poprzez użycie słowa kluczowego register informujemy kompilator o potrzebie


umieszczenia
zmiennej w rejestrze procesora. Jest to użyteczne rozwiązanie jeżeli dana zmienna jest
bardzo często wykorzystywana i dostęp do niej ma być jak najszybszy. Należy zaznaczyć,
że tak można deklarować tylko zmienne lokalne (w obrębie funkcji).

register unsigned char val = 8;

lub

register uint_8 val = 8;

lub

register u08 val = 8;

Po wywołaniu powyższej instrukcji zostanie utworzona nowa zmienna o nazwie Val i


zainicjowana wartością 8. Ta zmienna zajmie 1 rejestr kontrolera.

Zmiana wartości zmiennej

Zmiany wartości zmiennej możemy dokonać poprzez przypisanie np.:

val = 10;

czy też poprzez inne operacje jak np. inkrementacja:

val++;

Po wywołaniu obu powyższych instrukcji zmienna o nazwie val przyjmie wartość 11.

Tworzenie stałych w pamięci SRAM

Stałe deklaruje się bez podania typu za pomocą słowa kluczowego const.

const pazdziernik = 10;

Po wywołaniu powyższej instrukcji zostanie utworzona nowa "zmienna" o nazwie


pazdziernik z przypisaną wartością 10, której nie można zmienić.

Program przykładowy

Będzie to program bardziej złożony w stosunku do poprzednich, ale przybliży nas do


"prawdziwych" projektów. Naszym celem będzie napisanie programu, który pokaże

30
zachowanie zmiennych. Komunikacja z użytkownikiem będzie zrealizowana tak jak w
poprzednim przykładzie za pomocą łącza szeregowego.
Jednak same funkcje komunikacji szeregowej powinny zostać "ukryte" aby nie zaciemniać
głównego programu. W związku z tym funkcje związane z portem szeregowym
przeniesiono do osobnych plików i umieszczono w oddzielnym katalogu (../lib), tak aby i
inne programy mogły z nich korzystać. Funkcje biblioteczne umieszczone w tym katalogu
zostały omówione tutaj.

// Testowanie zmiennych i stałych w pamięci SRAM

#include <avr/io.h> // dostęp do rejestrów


#include <stdlib.h> // zawiera m.in. deklarację funkcji
itoa
#include "global.h" // zawiera definicje typów całkowitych
#include "uart.h" // obsługa portu szeregowego

// zamiana nazw funkcji (zobacz do uart.h)


#define getchar UART_getchar
#define putstr UART_putstr

void putint(int value) // wysyła na port szeregowy tekst


przedtawiający value
{
char string[4]; // bufor na wynik funkcji itoa
itoa(value, string, 10); // konwersja value na wartość
dziesiętną
putstr(string); // wyślij string na port szeregowy
}

void puttekst(int value) // wyswietla zdefiniowany tekst z


umieszczoną liczbą
{
putstr("\n\rSpodziewamy sie wartosci ");
putint(value);
putstr(" - wyslij dowolny znak...\n\r");
}

const pazdziernik = 10;

int main( void ) // program główny


{
u08 val = 8; // deklaracja i inicjalizacja
zmiennej
register u08 val2 = 12; // deklaracja i inicjalizacja zmiennej w
rejestrze

UART_init(); // inicjalizacja portu szeregowego

puttekst(8); // spodziewamy się 8


getchar(); // czekaj na znak z portu szeregowego
putint(val); // wyświetl val

val = 130; // zmień wartość zmiennej

puttekst(130); // spodziewamy się 130


getchar(); // czekaj na znak z portu szeregowego
putint(val); // wyświetl val

puttekst(12); // spodziewamy się 12


getchar(); // czekaj na znak z portu szeregowego

31
putint(val2); // wyświetl val2

puttekst(10); // spodziewamy się 10


getchar(); // czekaj na znak z portu szeregowego
putint(pazdziernik); // wyświetl pazdziernik

pazdziernik=3; // próba zmiany wartości stałej

puttekst(3); // spodziewamy się 3


getchar(); // czekaj na znak z portu szeregowego
putint(pazdziernik); // wyświetl pazdziernik

while(1); // pętla nieskończona


}

Zauważmy, że na początku programu została dokonana "podmiana" nazw kilku funkcji


poprzez zastosowanie dyrektyw #define. Zostało to zrobione celowo aby pokazać, że w ten
sposób możemy łatwo zmieniać np. urządzenie na którym będą prezentowane komunikaty.
Np. poprzez zmianę linii:

#define putstr UART_putstr

na linię w rodzaju:

#define putstr LCD_putstr

można w prosty zmienić wyświetlanie komunikatów na ekranie terminala na ekran


wyświetlacza LCD.
W tym przykładzie już sama kompilacja nie przebiegnie zupełnie "bezboleśnie" gdyż
kompilator zauważy, że chcemy zmienić coś co z definicji jest niezmienne. Pomimo tego
powinien zostać wygenerowany plik .hex, który wpisujemy do mikrokontrolera za pomocą
programatora.
Po uruchomieniu programu na ekranie terminala pojawi się tekst w rodzaju:

Spodziewamy sie wartosci 8 - wyslij dowolny znak...

po czym wysyłamy dowolny znak np. poprzez naciśnięcie klawisza ENTER i


obserwujemy wynik... wszędzie powinniśmy otrzymać taką wartość jakiej się
spodziewaliśmy za wyjątkiem ostatniej.

AVR-GCC - pamięć programu (FLASH)


Do przechowywania stałych np. tekstów dla wyświetlaczy LCD najlepiej nadaje się
pamięć programu. Uzasadnienie tego jest proste: stałe zadeklarowane w pamięci SRAM
zajmują cenne miejsce w tej (małej) pamięci (są tam kopiowane z pamięci flash podczas
startu programu i nie są z niej usuwane) mogą być więc powodem różnych dziwnych
błędów (na domiar złego kompilator nas zwykle o nich nie informuje).
To tworzenia tego typu danych przeznaczone jest wyrażenie: __attribute__ ((progmem)).
Aby w pełni z niego skorzystać należy włączyć do projektu plik nagłówkowy
avr/pgmspace.h. Ten plik zawiera funkcje służące do czytania danych z pamięci programu.
Są tam również zdefiniowane następujące typy danych:

32
• 8 bitowy: prog_char
• 16 bitowy: prog_int
• 32 bitowy: prog_long
• 64 bitowy: prog_long_long

Najważniejsze funkcje zawarte w pliku avr/pgmspace.h

Poniższe funkcje (są one w rzeczywistości jest makroinstrukcjami) ułatwiają prawidłowe


tworzenie i odczytywanie stałych z pamięci programu.

• pgm_read_byte( addr ) - jako argument pobiera adres pamięci programu a zwraca


wartość znajdującą się pod tym adresem.
• PSTR( s ) - tworzy łańcuch znaków w pamięci programu i zwraca adres do niego.

Tworzenie stałych w pamięci programu

Tutaj stałej LINE przypisano wartość 1:

prog_char LINE = {1};

Czytanie stałych z pamięci programu

Spod adresu LINE czytana jest wartość i zapamiętana w zmiennej res:

char res = pgm_read_byte(&LINE);

Tworzenie tablic w pamięci programu

Na przykład tak:

prog_char TAB[10] = {0,1,2,3,4,5,6,7,8,9};

jest również możliwe utworzenie otwartej tablicy (bez określenia wielkości - kompilator
robi to za nas) ...

prog_char TAB[] = {0,1,2,3,4,5,6,7,8,9};

Czytanie wartości z tablic w pamięci programu

Spod adresu TEN[5] czytana jest wartość i zapamiętana w zmiennej res:

char res = pgm_read_byte(&TEN[5]);

Tworzenie łańcucha znaków

Tworzy łańcuch w pamięci programu i zwraca wskaźnik do niego w LINE1:

char *LINE1 = PSTR("Pierwsza linia na LCD");

lub taki przykład:


tworzy łańcuch w pamięci programu i zwraca wskaźnik do niego w LINE2:

33
char LINE2[] __attribute__ ((progmem)) = "to jest drugi wiersz";

inny przykład:
tworzy łańcuch w pamięci programu i zwraca wskaźnik do niego w text1.

char text1[] PROGMEM = "Tak tez mozna definiowac teksty";

Czytanie łańcucha znaków

Zmienna firstchar zawiera teraz pierwszy znak z łańcucha LINE2.

firstchar = pgm_read_byte (&LINE2[0]);

Zmienna lastchar zawiera teraz 43 znak z łańcucha LINE1 ponieważ w języku C wszystkie
tablice są indeksowane od zera.

lastchar = pgm_read_byte (LINE1+42);

Program przykładowy

Naszym celem będzie napisanie programu, który pokaże zachowanie stałych i tablic
umieszczonych w pamięci programu. Komunikacja z użytkownikiem będzie zrealizowana
tak jak w poprzednim przykładzie za pomocą łącza szeregowego.

// Testowanie stałych w pamięci programu

#include <avr/io.h> // dostęp do rejestrów


#include "uart.h" // obsługa portu szeregowego

prog_char STALA = {'1'}; // stała o wartości będącej znakiem 1


ASCII

prog_char NEWLINE[] = {'\n','\r',0}; // tablica zawiarająca znaki nowej


linii

prog_char CLEAR[] = {27,'[','H',27,'[','2','J',0}; // j.w. ale


czyszczącza ekran terminala

prog_char HOME[] = {27,'[','H',0}; // j.w. ale przestawiająca kursor na


początek

char text1[] __attribute__ ((progmem)) = "To jest pierwszy wiersz ....";

char text2[] PROGMEM = "Tak latwiej mozna definiowac teksty :-)";

char VERSION[] PROGMEM = __DATE__" "__TIME__; // w ten sposób można


pilnować wersji :-)

// wysyła na port szeregowy łańcuch umieszczony w PAMIECI PROGRAMU


// jako argument przyjmuje wskaźnik (adres pierwszego znaku) do niego
void _UART_putstr_P(const char *s)
{
register u08 c;
while ((c = pgm_read_byte(s++)))
{
UART_putchar(c);
}

34
}

int main( void ) // program główny


{
char res; // deklaracja zmiennej

UART_init(); // inicjalizacja portu szeregowego

_UART_putstr_P(CLEAR); // wyślij na port szeregowy znaki


CLEAR
res = pgm_read_byte(&STALA); // pobierz wartość stałej STALA
UART_putchar(res); // i wyświetl ją

UART_putchar(pgm_read_byte(&NEWLINE[0])); // czytaj bajt 0 z NEWLINE i


wyslij go na port szer.
UART_putchar(pgm_read_byte(&NEWLINE[1])); // czytaj bajt 1 z NEWLINE i
wyslij go na port szer.
// w konsekwencji kursor na terminalu znalazł się w
nowej linii

UART_putstr(PSTR("Funkcja putstr() nie dziala z łańcuchami


umieszczonymi w pamięci FLASH"));
_UART_putstr_P(NEWLINE); // wyślij na port szeregowy
znak nowej linii

_UART_putstr_P(PSTR("To jest tekst z pamięci FLASH z funkcji putstr_P()


- dziala !!!"));
_UART_putstr_P(NEWLINE); // wyślij na port szeregowy
znak nowej linii
UART_getchar(); // czekaj na znak z portu
szeregowego

_UART_putstr_P(text1); // wyślij tekst zdefiniowany


wszcześniej w tablicy text1
_UART_putstr_P(NEWLINE); // wyślij na port szeregowy
znak nowej linii
UART_getchar(); // czekaj na znak z portu
szeregowego

_UART_putstr_P(PSTR("A teraz przeniesiemy kursor na początek"));


UART_getchar(); // czekaj na znak z portu
szeregowego
_UART_putstr_P(HOME); // wyślij na port szeregowy znaki
HOME

_UART_putstr_P(text2); // wyślij tekst zdefiniowany


wszcześniej w tablicy text2
_UART_putstr_P(NEWLINE); // wyślij na port szeregowy
znak nowej linii
UART_getchar(); // czekaj na znak z portu
szeregowego

_UART_putstr_P(PSTR("Czyścimy ekran... "));


UART_getchar(); // czekaj na znak z portu
szeregowego
_UART_putstr_P(CLEAR); // wyślij na port szeregowy znaki
CLEAR

_UART_putstr_P(PSTR("Data kompilacji: "));


_UART_putstr_P(VERSION); // wyślij na port szeregowy
wersję programu

35
while(1); // pętla nieskończona
}

Po skompilowaniu i uruchomieniu przykładu ekran terminala powinien zostać


wyczyszczony przez wysłanie odpowiedniej sekwencji znaków ANSI zdefiniowanej w
tablicy CLEAR. Następnie na ekranie terminala ukaże się "1" (jedynka) jako efekt
wyprowadzenia na port szeregowy stałej STALA. Kolejne instrukcje powodują odczyt
poszczególnych bajtów z tablicy NEWLINE i wysyłanie ich na port szeregowy co skutkuje
przejściem kursora na terminalu do nowej linii. W kolejnej linii użyto funkcji putstr() do
wyłania tekstu umieszczonego w pamięci programu. Na terminalu powinien się pojawić
jakiś przypadkowy znak. Takie zachowanie jest dowodem na to, że do wysyłania stałych
zdefiniowanych w pamięci programu należy używać specjalnie do tego stworzonych
funkcji. Uzasadnienie tego jest proste: mikrokontrolery AVR mają architekturę harwardzką
co oznacza, że posiadają oddzielne przestrzenie adresowe dla danych oraz programu i do
dostępu do nich używają zupełnie innych technik. W następnej linii została użyta
zdefiniowana wcześniej funkcja putstr_P() do wysłania tekstu z pamięci programu - tutaj
widać efekt. W dalszej części programu znajduje się demonstracja kilku sekwencji ANSI
do sterowania terminalem. Na samym końcu znajduje się przykład wykorzystania
predefiniowanych symboli __DATE__ i __TIME__ do wpisywania ich w pamięć programu
tak aby np. mieć w przyszłości kontrolę nad wersją programu (zawierają one datę i czas
kompilacji programu w postaci tekstowej).

AVR-GCC - pamięć EEPROM


W odróżnieniu do prostych definicji zmiennych w pamięci SRAM, użycie pamięci
EEPROM, do której dostęp odbywa się przez specjalne rejestry wymaga użycia
specjalnych funkcji dla uzyskania dostępu do niej. Deklaracje tych funkcji znajdują się w
pliku nagłówkowym avr/eeprom.h.
Jednak należy uważać by nie stosować zmiennych w EEPROM, do których często
zapisywane będą dane - np. zmienna sterująca pętli. Dzieje się tak dlatego, iż nominalnie
pamięć EEPROM ma ograniczona możliwość przeprogramowywania. Producent
gwarantuje tylko 100 tysięcy operacji zapisu. Łatwo więc w tym przypadku o
przekroczenie tej liczby w dość krótkim czasie. Dlatego nie należy pochopnie używać tej
pamięci, i w żadnym wypadku nie w instrukcjach pętli!

Najważniejsze funkcje zawarte w pliku avr/eeprom.h

eeprom_write_byte ( *adres, val) - zapisuje wartość val pod adres adres.


eeprom_read_byte ( *adres ) - czyta zawartość pamięci spod adresu adres.
eeprom_read_word ( *adres ) - czyta 16 bitową zawartość pamięci spod adresu adres.
eeprom_read_block ( *buf, *adres, n) - czyta n wartości od adresu adres i zapisuje do
pamięci SRAM w *buf.
eeprom_is_ready () - zwraca 1 jeśli pamięć jest wolna lub 0 jeśli na niej jest wykonywana
jakaś operacja (trwa zapis).

Program przykładowy

Naszym celem będzie napisanie programu, który pokaże zachowanie danych


umieszczonych w wewnętrznej pamięci EEPROM. Komunikacja z użytkownikiem będzie

36
zrealizowana tak jak w poprzednich przykładach za pomocą terminala podłączonego do
łącza szeregowego. Podczas jego testowania należy zwrócić uwagę na powstały w wyniku
kompilacji plik .eep zawierający dane przeznaczone do wpisania w pamięć EEPROM
mikrokontrolera.

// Testowanie pamięci danych EEPROM

#include <avr/io.h> // dostęp do rejestrów


#include <avr/eeprom.h> // dostęp do pamięci EEPROM
#include "uart.h" // obsługa portu szeregowego

#define RADIX 10 // podstawa wyświetlania liczb

uint8_t eeprom_val __attribute__((section(".eeprom")));

char ver[] __attribute__((section(".eeprom"))) = "Wersja z " __DATE__ " "


__TIME__ "\0";

// wysyła na port szeregowy łańcuch umieszczony w PAMIECI EEPROM


// jako argument przyjmuje wskaźnik (adres pierwszego znaku) do niego
void _UART_putstr_E(uint8_t *s)
{
register uint8_t c;
while ((c = eeprom_read_byte(s++)))
{
UART_putchar(c);
}
}

int main(void) // program główny


{
char buffer[32]; // bufor dla tablicy odczytywanej z
eeprom
uint8_t val1 = 123;
uint8_t val2;

UART_init(); // inicjalizacja portu szeregowego

UART_putstr_P(PSTR("\n\r1. Odczyt z pamięci EEPROM\n\reeprom_val ->


"));
val2 = eeprom_read_byte(&eeprom_val); // odczyt z eeprom
UART_putint(val2,RADIX);

UART_putstr_P(PSTR("\n\r2. Zapis do pamięci EEPROM\n\reeprom_val <-


eeprom_val+1"));
eeprom_write_byte (&eeprom_val, ++val2); // zapis do eeprom

UART_putstr_P(PSTR("\n\r3. Odczyt z pamięci EEPROM\n\reeprom_val ->


"));
val2 = eeprom_read_byte(&eeprom_val); // odczyt z eeprom
UART_putint(val2,RADIX);

UART_putstr_P(PSTR("\n\r4. Zapis do pamięci EEPROM\n\rKomorka E2END <-


"));
UART_putint(val1,RADIX);
eeprom_write_byte((uint8_t*)E2END, val1); // zapis do eeprom

UART_putstr_P(PSTR("\n\r5. Odczyt z pamięci EEPROM\n\rKomorka E2END ->


"));

37
val2 = eeprom_read_byte((uint8_t*)E2END); // odczyt eeprom spod adresu
E2END
UART_putint(val2,RADIX);

UART_putstr_P(PSTR("\n\r6. Odczyt wersji z pamięci EEPROM za pomocą


eeprom_read_block()\n\r"));
eeprom_read_block(&buffer,&ver,32);
UART_putstr(buffer);

UART_putstr_P(PSTR("\n\r7. Odczyt wersji z pamięci EEPROM za pomocą


UART_putline_E()\n\r"));
_UART_putstr_E(ver);

while(1); // pętla nieskończona


}

jest zdefiniowana tablica o nazwie ver[] również w pamięci EEPROM zawierająca ciąg
znaków z datą i czasem kompilacji programu. Dostęp do tych zmiennych jest możliwy
przez specjalne funkcje dostępu do pamięci EEPROM. W kolejnych liniach mamy
zdefiniowaną funkcję UART_putstr_E(*s), która służy do wysyłania przez port zeregowy
łańcucha znajdującego się w pamięci EEPROM. Dalej znajduje się program główny -
funkcja main(), w której są zadeklarowane zmienne: buffer, val1 z zainicjowaną wartością
oraz val2. Po zainicjowaniu portu szeregowego przez funkcję UART_init() następuje
wypisanie tekstu:
1. Odczyt z pamięci EEPROM eeprom_val ->

przez wywołanie funkcji UART_putstr_P(). Następnie za pomocą instrukcji

val2 = eeprom_read_byte((u08*)&eeprom_val);

jest odczytywana wartość z komórki pamięci EEPROM o adresie eeprom_val,


przepisywana do zmiennej val2 i za pomocą funkcji UART_putint() wyprowadzana na port
szeregowy w postaci liczby o podstawie zdefiniowanej w RADIX. Później jest wypisywany
komunikat o zapisie do pamięci EEPROM zwiększonej o 1 wartości zmiennej eeprom_val.
Wartość ta jest zapisywana instrukcją:

eeprom_write_byte ((u08*)&eeprom_val, ++val2);

Zauważmy, że w powyższej instrukcji zostało napisane ++val2 (preinkrementacja) a nie


val2++ (postinkrementacja) ponieważ wtedy została by wpisania niezmieniona wartość a
zostałaby ona zmieniona po wykonaniu funkcji eeprom_write_byte(). Później (w trzecim
kroku) jest ponownie odczytywana wartość zmiennej eeprom_val - powinna być większa o
1 od tej w kroku 1. Najlepiej to widać restartując mikrokontroler (lub nawet lepiej poprzez
wyłączanie i ponowne włączanie układu) - nie tracimy zawartości pamięci - możemy
zliczać programowo np. ilość restartów mikrokontrolera. W kolejnych krokach mamy
przedstawione zastosowanie stałej E2END, która zawiera adres ostatniej komórki pamięci
EEPROM. Następnie mamy przykład zastosowania funkcji eeprom_read_block() oraz
zdefiniowanej na początku programu UART_putstr_E() do odczytu danych zawartych w
tablicy znajdującej się w pamięci EEPROM.

AVR-GCC - obsługa przerwań


38
Przerwania są stosowane w przypadku, gdy program ma szybko reagować na zdarzenia.
Powyższe zdarzenia mogą być generowane zarówno przez urządzenia wewnętrzne jak i
zewnętrzne. Każde z przerwań może być maskowane przez kasowanie bitów w
odpowiednich rejestrach i w rejestrze statusu.
Aby użyć funkcji obsługi przerwań należy włączyć plik avr/interrupt.h.
Funkcja, która ma służyć do obsługi przerwania musi mieć nazwę składającą się ze słowa
SIGNAL lub INTERRUPT.

SIGNAL (nazwa_uchwytu)
{
// Instrukcje tu zawarte będą wykonywane jako obsługa przerwania
}

Funkcja obsługi przerwania z nazwą SIGNAL będzie obsługiwana z wyłączoną obsługą


innych przerwań.
lub:

INTERRUPT (nazwa_uchwytu)
{
// Instrukcje tu zawarte będą wykonywane jako obsługa przerwania
}

Funkcja obsługi przerwania z nazwą INTERRUPT będzie obsługiwana z włączoną obsługą


innych przerwań - jej realizacja może być przerwana przez przerwanie o wyższym
priorytecie.
Priorytety przerwań należy sprawdzić w dokumentacji mikrokontrolera.
Rejestry systemu obsługi przerwań:

• TIFR - Znaczniki przerwania z liczników/czasomierzy


• TIMSK - Maska przerwań liczników/czasomierzy
• GIMSK - Globalna maska przerwań

Funkcje zdefiniowane w interrupt.h:

• sei() - Włącza obsługę przerwań. Makro.


• cli() - Wyłącza obsługę przerwań. Makro.
• enable_external_int(ints) - Wpisuje ints do rejestrów EIMSK lub GIMSK
• timer_enable_int(ints) - Wpisuje ints do rejestru TIMSK

Program przykładowy

Naszym celem będzie napisanie programu, który pokaże w możliwie najprostszy sposób
wykorzystanie przerwań. Komunikacja z użytkownikiem będzie zrealizowana za pomocą
przycisków podłączonych z jednej strony do masy a z drugiej do linii INT0 i INT1
mikrokontrolera oraz diod LED podłączonych do portu B.

// Testowanie przerwań zewnętrznych

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h>

SIGNAL (SIG_INTERRUPT0)

39
{
PORTD = 0x1d; // zgaś diode LED
}

SIGNAL (SIG_INTERRUPT1)
{
PORTD = 0x0d; // zapal diode LED
}

int main(void) // program główny


{
DDRD = 0x10; // bit 4 PortD jako wyjscie (LED) pozostale
jako wejścia (przyciski)
PORTD = 0x0d; // podciąganie bitów 3 i 4 PortD
(przyciski)

GIMSK = _BV(INT0)|_BV(INT1);
//włącz obsługę przerwań Int0 i Int1
MCUCR = _BV(ISC01)|_BV(ISC11);
// włącz generowanie przerwań przez
// opadające zbocze na Int0 i Int1

sei(); // włącz obsługę przerwań

while(1); // pętla nieskończona


}

W tym przykładzie została zaimplementowana obsługa dwóch przerwań zewnętrznych


INT0 i INT1 za pomocą słów SIGNAL z odpowiednimi parametrami: SIG_INTERRUPT0
dla INT0 i SIG_INTERRUPT1 dla INT1. Na początku programu głównego są ustawiane
parametry portów.
W następnej kolejności za pomocą instrukcji: outb(GIMSK, _BV(INT0)|_BV(INT1)); są
ustawiane bity INT0 i INT1 w rejestrze GIMSK (globalnej maski przerwań) co w efekcie
przygotowuje
mikrokontroler na obsługę przerwań zewnętrznych INT0 i INT1. Aby program uczynić
łatwiej przenośnym na inne kontrolery AVR proponuję zastąpić powyższą instrukcję taką:
enable_external_int(_BV(INT0)|_BV(INT1)); Kolejna instrukcja: outb(MCUCR,
_BV(ISC01)|_BV(ISC11)); powoduje, że mikrokontroler będzie generował przerwania
podczas opadającego zbocza sygnału na wejściach INT0 lub INT1. Jednak aby przerwania
były rzeczywiście obsługiwane należy zezwolić mikrokontrolerowi na ich obsługę. Robi to
kolejna instrukcja: sei();.
Dalej mamy już tylko znaną z poprzednich przykładów instrukcję while(1); czyli pętlę
nieskończoną. Wydawało by się, że program tu zakończy działanie, tak jak to było w
poprzednich przypadkach, lecz tak nie jest. Wystarczy, że na dowolne z wejść INT1 lub
INT0 podamy na krótko stan niski (np. z przycisku) a diody LED podłączone do portu B
zapalą się lub zgasną. Widać tu więc wyraźnie na czym polegają przerwania - są one
obsługiwane niezależnie od programu głównego.

AVR-GCC - licznik/czasomierz TIMER 0


Timer 0 jest to 8-bitowy licznik/czasomierz mogący zliczać impulsy w zakresie od 0 do
255. Pracując w trybie czasomierza używa wewnętrznego sygnału zegarowego, w trybie
licznika - zewnętrznego sygnału pobranego z odpowiedniej końcówki układu AVR (z
której - należy sprawdzić w dokumentacji układu). Ponadto może generować przerwania,

40
które następnie należy obsłużyć. Również program może odczytywać jego wartość w
dowolnym momencie (tzw. polling) i na tej podstawie podejmować odpowiednie działania.

Tryb licznika

W tym trybie są zliczane zmiany stanu na końcówce T0. Zmiany stanu na końcówce T0 są
synchronizowane z częstotliwością zegarową CPU.Aby te zmiany były zauważone,
minimalny czas pomiędzy tymi zmianami musi być większy od okresu zegara CPU. Stan
na wejściu T0 jest próbkowany w czasie narastającego zbocza wewnętrznego sygnału
zegarowego CPU. Aby włączyć zliczanie impulsów należy ustawić odpowiednią
kombinację bitów w rejestrze TCCR0.

Programy przykładowe

Prosty przykład na wykorzystanie licznika 0 do zliczania impulsów na wejściu T0 bez


użycia przerwań. Obsługa tego programu polega na częstym podawaniu impulsów na
wejście T0 mikrokontrolera (np. poprzez zwieranie z masą). Diody LED podłączone do
linii portu C sygnalizują binarnie ilość przepełnień licznika.

// Testowanie licznika 0 (polling)

#include <avr/io.h> // dostęp do rejestrów

uint8_t led;

int main( void )


{
DDRC = 0xFF; // PortC jako wyjścia
TCNT0 = 0xFE; // wartość początkowa T/C0
TCCR0 = _BV(CS01)|_BV(CS02);
// T/C0 zlicza opadające
// zbocza na wejściu T0
while(1)
{
loop_until_bit_is_set(TIFR,TOV0);
// ta pętla sprawdza bit przepełnienia
// w rejestrze TIFR
TCNT0 = 0xFE; // przeładuj T/C0
PORTC = ~led++; // wyślij ilość przepełnień na PortC

sbi(TIFR,TOV0);
// jeśli wpiszemy 1 do bitu TOV0
// to ten bit zostanie skasowany
// przy następnym przepełnieniu licznika 0
}
}

Na początku są ustalane kierunki danych w porcie C. Następnie jest ustawiana wartość


początkowa licznika TCNT0 na 0xFE co oznacza, że po dwóch impulsach na wejściu T0
pojawi się przepełnienie licznika (licznik 8 bitowy liczy w górę czyli 0xFE, 0xFF, 0x00) i
w konsekwencji ustawienie bitu TOV0 w rejestrze TIFR. Kolejna instrukcja konfiguruje
wejście licznika w taki sposób aby zliczanie następowało podczas opadającego zbocza na
wejściu T0. W pętli nieskończonej sprawdzany jest stan bitu TOV0. Następnie jest
przeładowywany jest licznik 0 wartością 0xFE, inkrementowana zmienna led określająca
liczbę przepełnień. Zmienna ta po zanegowaniu jest wystawiana na PORTC. Ostatnią

41
instrukcją w tej pętli jest ustawienie bitu TOV0, które w efekcie powoduje jego
skasowanie.
Bardziej złożony przykład na wykorzystanie licznika 0 do zliczania impulsów na wejściu
T0. Dzięki wykorzystaniu przerwań można całkowicie oddzielić zliczanie impulsów od
programu głównego. Obsługa tego programu polega na częstym podawaniu impulsów na
wejście T0 mikrokontrolera (np. poprzez zwieranie go z masą) i wysyłaniu dowolnego
znaku na port szeregowy z terminala. Na terminalu otrzymany wyniki działania programu.

// Testowanie licznika 0 (przerwania)

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT
#include "uart.h" // obsługa portu szeregowego

prog_char NEWLINE[] = {'\n','\r',0};


// tablica zawiarająca znaki nowej linii

volatile uint16_t licznik;


volatile uint8_t overflow;
// |
// -- volatile jest konieczne ze względu na modyfikację
// i sprawdzanie zmiennych poza procedurą obsługi przerwania

// funkcja używana do wypisywania zmiennych


void PRINT_VALUE(const char *s, int v)
{
UART_putstr_P(s); // wyświetl tekst s z pamięci programu
UART_putint(v,10); // wyświetl wartość v
UART_putstr_P(NEWLINE);
// dopisz znaki końca wiersza
}

SIGNAL (SIG_OVERFLOW0)
{
overflow++; // inkrementuj licznik przepełnień
}

int main(void)
{
UART_init(); // inicjalizacja portu szeregowego
DDRB = 0xFF; // wszystkie linie PORTB jako wyjścia
// nawet linia PB0 - wejście T0
PORTB = 0xFF; // wszystkie linie PORTB w stan wysoki
TIMSK = _BV(TOIE0); // włącz obsługę przerwań T/C0
TCNT0 = 0x00; // wartość początkowa T/C0
TCCR0 = _BV(CS01)|_BV(CS02);
// wyzwalanie z T0 opad. zboczem

sei(); // włącz obsługę przerwań

while(1) // pętla nieskończona


{
licznik=(overflow<<8)|TCNT0;
// oblicz wartość

PRINT_VALUE(PSTR("overflow="),overflow);
// overvlow na UART
PRINT_VALUE(PSTR("TCNT0="),TCNT0);
// TCNT0 na UART

42
PRINT_VALUE(PSTR("licznik (overflow*256)+TCNT0="),licznik);
// licznik na UART

UART_putstr_P(PSTR("Wyślij dowolny znak..."));


UART_putstr_P(NEWLINE);
UART_putstr_P(NEWLINE);
UART_getchar(); // czekaj na znak z UART
}
}

Tryb czasomierza

W tym trybie licznik jest taktowany wewnętrznym sygnałem zegarowym. Po każdym


takcie wartość TCNT0 jest zwiększana o jeden. Sygnał taktujący jest wynikiem podziału
przez x w stosunku do zegara procesora (CLK). Dzielnik x może przyjąć jedną z wartości:
1, 8, 64, 256, 1024. Np. x=1024 - TCNT0 jest zwiększany o 1 po 1024 taktach zegara
procesora. Współczynnik wstępnego podziału jest ustalany przez ustawianie następujących
bitów rejestru TCCR0.

Programy przykładowe
// Testowanie timera 0 (polling)

#include <avr/io.h> // dostęp do rejestrów

uint8_t led;
uint8_t state;

int main( void )


{
DDRC = 0xFF; // wszystkie linie PORTC jako wyjścia
TCNT0 = 0; // wartość początkowa T/C0
TCCR0 = _BV(CS00)|_BV(CS02); // preskaler ck/1024
while(1)
{
do
// ta pętla sprawdza bit przepełnienia rejestrze TIFR
state = inb(TIFR) & _BV(TOV0);
while (state != _BV(TOV0));

PORTC = ~led++; // wyprowadź wartość licznika


// przepełnień na PORTB

TIFR = _BV(TOV0);
// jeśli 1 wpiszemy do bitu TOV0 to
// ten bit powinien zostac skasowany
// przy następnym przepełnieniu licznika 0
}
}

W programie zostały zadeklarowane dwie zmienne globalne: licznik - zawiera liczbę


impulsów na wejściu T0 oraz overflow - programowy licznik przerwań od przepełnień
licznika TCNT0. Zostały one zdefiniowane ze słowem kluczowym volatile, które
powoduje wyłączenie optymalizacji w funkcjach używających tych zmiennych - w ten
sposób można modyfikować je w funkcjach obsługi przerwań i sprawdzać ich wartości
poza nimi. Dalej widać prostą definicję funkcji obsługi przerwania od przepełnienia
licznika 0. Znajduje się tam tylko inkrementacja zmiennej overflow. Na początku
programu głównego main() są ustalane kierunki danych w porcie B. Tutaj ustawiliśmy

43
linię PB0 jako wejście z uwagi na jej alternatywną funkcję: wejście licznika T0. Następnie
jest ustawiana wartość początkowa licznika TCNT0 na 0. W kolejnej instrukcji poprzez
ustawienie bitu TOIE0 w rejestrze TIMSK zostało włączone generowanie przerwań od
przepełnienia licznika TCNT0. Podobnie poprzez ustawienie bitów CS01 i CS02 w
rejestrze TCCR0 zostało włączone taktowanie licznika z wejścia T0. Przed wejściem do
głównej pętli programu instrukcją sei() została włączona obsługa przerwań. Pierwszą
instrukcją w głównej pętli jest UART_getchar() czekająca na dowolny znak z portu
szeregowego. Po odebraniu dowolnego znaku z tego portu obliczana jest wartość licznika
impulsów na wejściu T0 mikrokontrolera wg wzoru:

licznik = 256*overflow + TCNT0

jednak mnożenie przez 256 łatwiej i szybciej mikrokontroler wykona stosując przesuwanie
w lewo o 8 bitów a dodawanie poprzez sumę logiczną. W związku z powyższym wartość
licznika jest obliczana w następujący sposób:

licznik=(overflow<<8)|(inb(TCNT0));

Dalej występuje prezentacja wartości poszczególnych zmiennych i wyniku obliczeń.


Poniższy przykład prezentuje działanie licznika/czasomierza 0 w trybie czasomierza (ang.
Timer). Obsługa tego licznika jest realizowania poprzez przerwanie generowane podczas
przepełnienia licznika SIGNAL(SIG_OVERFLOW0). Stan licznika przepełnień licznika 0
jest prezentowany przez diody LED podłączone do portu B mikrokontrolera. Efekt
działania programu jest identyczny z tym z wcześniejszego listingu, lecz program główny
nie jest angażowany w obsługę licznika.

// Testowanie timera 0 (przerwania)

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT

#define T0_INIT 256-250

#define tbi(PORT,BIT) PORT^=_BV(BIT)


// przełącza stan BITu w PORTcie na przeciwny 1->0 ; 0->1

//unsigned char overflow;


volatile uint8_t overflow;
// |
// -- volatile jest konieczne ze względu na sprawdzanie
// zmiennej overflow poza procedurą obsługi przerwania

SIGNAL (SIG_OVERFLOW0)
{
TCNT0 = T0_INIT; // przeładuj timer 0
if (overflow>0)
overflow--; // dekrementuj
tbi(PORTD,PD5); // przełącz stan LED na PB1
}

int main(void)
{
DDRD = 0xFF; // wszystkie linie PORTD jako wyjścia
PORTD = 0xFF; // wygaś diody LED
TIMSK = _BV(TOIE0); // włącz obsługę przerwań T/C0

44
TCNT0 = T0_INIT; // wartość początkowa T/C0
TCCR0 = _BV(CS00)|_BV(CS02); // preskaler 1024

sei(); // włącz obsługę przerwań

while(1) // pętla nieskończona


{
cbi(PORTD,PD4); // zapal LED na PD4
overflow=4; // inicjuj zmienną overflow
while(overflow); // zmienna overflow jest
// dekrementowana w przerwaniu
sbi(PORTD,PD4); // zgaś LED na PD4
overflow=16; // inicjuj zmienną overflow
while(overflow); // zmienna overflow jest
// dekrementowana w przerwaniu
}
}

AVR-GCC - licznik/czasomierz TIMER 1


Licznik/czasomierz 1 posiada 16 bitową organizację. Z tego względu można go użyć do
zliczania większej ilości impulsów lub odmierzania dłuższych (lub dokładniejszych)
okresów czasu. Zliczane wartości mieszczą się w zakresie od 0x0000 do 0xFFFF. Dostęp
do nich jest możliwy przez dwa 8-bitowe rejestry. Oprócz swej funkcji podstawowej
licznik 1 może być użyty w trybie porównywania / przechwytywania (ang. compare /
capture) oraz sterowania wyjściami z modulowaną szerokością impulsu (PWM).

Rejestry licznika/czasomierza 1

Nazwa | Funkcja | Uwagi


-------------------------------------------------------------------------
---------------------
TCCR1A | Rejestr kontrolny A |
TCCR1B | Rejestr kontrolny B |
TCCR1L | Stan licznika L | możliwy dostęp do całej zawartości
licznika przez TCCR1
TCCR1H | Stan licznika H | j.w.
OCR1AL | Porównywanie - rejestr A L | możliwy dostęp do całej zawartości
rejestru przez OCR1A
OCR1AH | Porównywanie - rejestr A H | j.w.
OCR1BL | Porównywanie - rejestr B L | możliwy dostęp do całej zawartości
rejestru przez OCR1B
OCR1BH | Porównywanie - rejestr B H | j.w.
ICR1L | Przechwytywanie L | możliwy dostęp do całej zawartości
rejestru przez ICR1
ICR1H | Przechwytywanie H | j.w.

Tryb licznika

W tym trybie są zliczane zmiany stanu na końcówce T1. Zmiany stanu na końcówce T1 są
synchronizowane z częstotliwością zegarową CPU. Aby te zmiany były zauważone,
minimalny odstęp czasu pomiędzy tymi zmianami musi być większy od okresu zegara
CPU. Stan na wejściu T1 jest próbkowany w czasie narastającego zbocza wewnętrznego
sygnału zegarowego CPU. Aby włączyć zliczanie impulsów należy ustawić odpowiednią
kombinację bitów w rejestrze TCCR1B.

45
Bity rejestru TCCR1B określające zliczanie impulsów zewnętrznych przez licznik 1

CS12 | CS11 | CS10 | Opis


------------------------------------------------------
1 | 1 | 0 | Opadające zbocze na końcówce T1
1 | 1 | 1 | Narastające zbocze na końcówce T1

Tryb czasomierza

W tym trybie licznik jest taktowany wewnętrznym sygnałem zegarowym. Po każdym


takcie wartość licznika jest zwiększana o jeden. Sygnał taktujący jest wynikiem podziału
przez x w stosunku do zegara procesora. Dzielnik x może przyjąć jedną z wartości: 1, 8,
64, 256, 1024. Np. x=1024 - licznik jest zwiększany o 1 po 1024 taktach zegara procesora.
Współczynnik wstępnego podziału jest ustalany przez ustawianie odpowiednich bitów
rejestru TCCR1B.

Bity rejestru TCCR1B określające częstotliwość wejściową czasomierza 1

CS12 | CS11 | CS10 | Częstotliwość


----------------------------------
0 | 0 | 0 | 0
0 | 0 | 1 | CLK
0 | 1 | 0 | CLK/8
0 | 1 | 1 | CLK/64
1 | 0 | 0 | CLK/256
1 | 0 | 1 | CLK/1024

Programy przykładowe

Naszym celem będzie napisanie programów, które pokażą w możliwie najprostszy sposób
wykorzystanie licznika/czasomierza 1 do zliczania impulsów pochodzących z
wewnętrznego preskalera. W obu przypadkach do komunikacji z użytkownikiem będą
wykorzystane diody LED podłączone do portu B. Pierwszy przykład przedstawia
wykorzystanie cyklicznego sprawdzania jego stanu (polling) natomiast drugi realizuje
dokładnie to samo ale poprzez obsługę przerwania.

// Testowanie timera 1 (polling)

#include <avr/io.h> // dostęp do rejestrów

uint8_t led;
uint8_t state;

int main( void )


{
DDRC = 0xFF; // wszystkie linie PORTB jako wyjścia
TCNT1 = 0xFF00; // wartość początkowa T/C1
TCCR1A = 0x00; // T/C1 w trybie czasomierza
TCCR1B = _BV(CS10)|_BV(CS12); // preskaler ck/1024
while(1)
{
do
// ta pętla sprawdza bit przepełnienia rejestrze TIFR
state = inb(TIFR) & _BV(TOV1);
while (state != _BV(TOV1));

PORTC = ~led++; // wyslij licznik przepełnień na PORTB

46
TCNT1 = 0xFF00; // wartość początkowa T/C1

TIFR = _BV(TOV1);
// jeśli ustawimy bit TOV1 to
// ten bit zostanie skasowany
// przy następnym przepełnieniu licznika 1
}
}

// Testowanie timera 1 (przerwania)

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT

uint8_t led;

SIGNAL (SIG_OVERFLOW1)
{
PORTC = ~led++; // wyświetl na LED-ach
TCNT1 = 0xFF00; // przeładuj timer 1
}

int main(void)
{
DDRC = 0xFF; // wszystkie linie PORTC jako wyjścia
TIMSK = _BV(TOIE1); // włącz obsługę przerwań T/C1
TCNT1 = 0xFF00; // wartość początkowa T/C1
TCCR1A = 0x00; // włącz tryb czasomierza T/C1
TCCR1B = _BV(CS10)|_BV(CS12);
// preskaler ck/1024
sei(); // włącz obsługę przerwań
while(1); // pętla nieskończona
}

Tryb porównywania

Licznik 1 wyposażony jest w funkcję porównywania jego zawartości z wartością zadaną.


Do tego celu używa dwóch rejestrów OCR1A oraz OCR1B. Ich zawartość jest stale
porównywana z zawartością licznika TCNT1. Pozytywny wynik porównania może
wywołać wyzerowanie licznika TCNT1 lub zmianę stanu na końcówkach OC1A lub
OC1B. Bity CS10, CS11 i CS12 rejestru TCCR1B definiują przeskalowanie i źródło
taktowania.

Wybór trybu pracy wyjścia OC1A (bity rejestru TCCR1A

COM1A1 | COM1A0 | Opis


---------------------------------------------------------------
0 | 0 | Licznik 1 odłączony od końcówki OC1A
0 | 1 | Przełączanie stanu końcówki OC1A na przeciwny
1 | 0 | Zerowanie stanu końcówki OC1A
1 | 1 | Ustawianie stanu końcówki OC1A

Wybór trybu pracy wyjścia OC1B (bity rejestru TCCR1A)

COM1B1 | COM1B0 | Opis


---------------------------------------------------------------

47
0 | 0 | Licznik 1 odłączony od końcówki OC1B
0 | 1 | Przełączanie stanu końcówki OC1B na przeciwny
1 | 0 | Zerowanie stanu końcówki OC1B
1 | 1 | Ustawianie stanu końcówki OC1B

Program przykładowy

Naszym celem będzie napisanie programu, który pokaże nam zastosowanie trybu
porównywania.
Będzie to generator przebiegu prostokątnego o regulowanej częstotliwości i wypełnieniu
przebiegu.
Wyjściem generatora jest PB0. Częstotliwość reguluje się zwierając do masy linie PD3 lub
PD4.
Wypełnienie regulujemy zwierając do masy linie PD5 lub PD6.

// Testowanie timera 1 w trybie porównywania

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT

volatile uint16_t delay; // zmienna określająca częstotliwość


volatile uint16_t compare;// zmienna określająca wypełnienie

SIGNAL (SIG_OUTPUT_COMPARE1A) // przerwanie od porównania


{
cbi(PORTC,PB2); // zapal diodę na PC2
}

SIGNAL (SIG_OVERFLOW1) // przerwanie od przepełnienia


{
TCNT1 = delay; // przeładuj TIMER1
sbi(PORTC,PC2); // zgaś diodę na PC2
}

int main(void) // program główny


{
DDRC = 0xFF; // PORTC jako wyjścia dla LED
PORTC = 0xFF; // zgaś diody LED na PORTC
DDRD = 0x00; // PORTD jako wejścia dla
przycisków
PORTD = 0xFF; // podciągaj wejścia PORTD
delay = 0x0000; // domyślna wartość dla TIMERA1
compare = 0x7FFF; // domyślna wartość dla porównania
TIMSK = _BV(TOIE1)|_BV(OCIE1A);
// włącz przerwania
// od przepełnienia i porównania
TIMERA1

TCNT1 = delay; // zainicjuj TIMER1


TCCR1A = 0x00; // czasomierz 1 bez dodatków
TCCR1B = _BV(CS00); // taktowany F_CPU
sei(); // włącz obsługę przerwań
while(1)
{
if (bit_is_clear(PIND,PD2)) // jeżeli zwarto PD4 z masą
delay+=0x80; // zwiększ delay o 128
loop_until_bit_is_set(PIND,PD2);
// czekaj na zwolnienie PD4

48
if (bit_is_clear(PIND,PD3)) // jeżeli zwarto PD3 z masą
delay-=0x80; // zmniejsz delay o 128
loop_until_bit_is_set(PIND,PD3);
// czekaj na zwolnienie PD3

if (bit_is_clear(PIND,PD6)) // jeżeli zwarto PD6 z masą


compare+=0x80; // zwiększ compare o 128
loop_until_bit_is_set(PIND,PD6);
// czekaj na zwolnienie PD6

if (bit_is_clear(PINB,PB0)) // jeżeli zwarto PB0 z masą


compare-=0x80; // zmniejsz compare o 128
loop_until_bit_is_set(PINB,PB0);
// czekaj na zwolnienie PB0

#ifdef __AVR_AT90S2313__ // jeśli kompilujemy dla 2313


OCR1 = compare; // wpisz compare do OCR1
#else // w przeciwnym wypadku:
OCR1A = compare; // wpisz compare do OCR1A
#endif

}
}

Tryb przechwytywania

Licznik 1 wyposażony jest w funkcję przechwytywania (ang. Catpure) jego chwilowej


zawartości w specjalnym rejestrze jako reakcję na zdarzenie zewnętrzne. Kiedy zostanie
wykryte narastające lub opadające zbocze sygnału (w zależności od stanu bitu ICES1 w
rejestrze TCCR1B) na wejściu ICP (input capture), bieżąca wartość licznika zostaje
przepisana do 16 bitowego rejestru ICR1 (ICR1L, ICR1H) oraz ustawia znacznik ICF1.
Dodatkowo można uaktywnić funkcję eliminacji zakłóceń na wejściu ICP poprzez
ustawienie bitu ICNC1 w rejestrze TCCR1B. Przechwytywanie wartości licznika /
czasomierza 1 można również wywołać zmianą stanu na wyjściu wbudowanego w układ
komparatora analogowego (ustawiony bit ACIC rejestru ACSR). Jest też możliwe
wygenerowanie przerwania (SIG_INPUT_CAPTURE), jeżeli ustawiono bit TICIE1 w
rejestrze TIMSK. Bardzo ważna jest kolejność odczytu ośmiobitowych części rejestru
ICR1 - najpierw należy odczytać mniej znaczący bajt (ICR1L) a następnie bardziej
znaczący (ICR1H). Można również odczytać całą 16 bitową zawartość licznika poprzez
przypisanie np. var=ICR1.

Program przykładowy

Naszym celem będzie napisanie programu, który przedstawi nam przechwytywanie


zawartości licznika do specjalnego rejestru. W tym przykładzie urządzeniem wyjściowym
będzie terminal podłączony do portu szeregowego mikrokontrolera. Wejście ICP
mikrokontrolera należy podłączyć do zasilania przez rezystor 10k. W przypadku gdy
używamy mikrokontrolera AT90S2313 można ww. rezystora nie stosować wymuszając
programowo "podciągnięcie" wejścia ICP (końcówka PD6) do zasilania np. za pomocą
wstawienia w odpowiedniej instrukcji na początku funkcji main() np. sbi(PORTD,PD6).
Wejście ICP należy zwierać co jakiś czas z masą, co pozwoli zaobserwować
przechwytywanie zawartości rejestru TCNT1 w rejestrze ICR1.

// Testowanie licznika 1 (przechwytywanie)

49
#include <avr/io.h> // dostęp do rejestrów
#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT
#include "uart.h" // obsługa portu szeregowego

prog_char NEWLINE[] = {'\n','\r',0};


// tablica zawiarająca znaki nowej linii
prog_char CLEAR[] = {27,'[','H',27,'[','2','J',0};
// j.w. ale czyszczącza ekran terminala
prog_char HOME[] = {27,'[','H',0};
// j.w. ale przestawiająca kursor na początek
prog_char SPACE[] = {' ',' ',' ',0};

// funkcja używana do wypisywania zmiennych


void PRINT_VALUE(const char *s, int v)
{
UART_putstr_P(s); // wyświetl tekst s z pamięci programu
UART_putint(v,16); // wyświetl wartość v
UART_putstr_P(SPACE); // dopisz spacje na końcu linii
UART_putstr_P(NEWLINE); // dopisz znaki końca wiersza
}

int main(void)
{
UART_init(); // inicjalizacja portu szeregowego
TCCR1B = _BV(ICNC1)|_BV(CS10)|_BV(CS12);
// opadające zbocze i filtracja zakłóceń na ICP
// taktowanie T1 CK/1024

UART_putstr_P(CLEAR);

while(1) // pętla nieskończona


{
UART_putstr_P(HOME);

PRINT_VALUE(PSTR("TCNT1 = 0x"),TCNT1);
// TCNT1 na UART
PRINT_VALUE(PSTR("ICR1 = 0x"),ICR1);
// ICR1 na UART
}
}

Tryb PWM - modulowana szerokość impulsu

Kiedy zostanie wybrany tryb pracy licznika jako PWM (ang. Pulse Width Modulation)
czyli modulacja szerokości impulsów. Czasomierz 1 może być używany jako 8, 9 lub 10
bitowy samobieżny modulator PWM. Tryb PWM ustawiany jest przez ustawianie bitów
PWM10 i PWM11 znajdujących się w rejestrze TCCR1A.

Tryby pracy PWM (bity rejestru TCCR1A)

PWM11 | PWM10 | Opis


0 | 0 | Wyłączony PWM
0 | 1 | 8 bitowy PWM
1 | 0 | 9 bitowy PWM
1 | 1 | 10 bitowy PWM

50
Licznik może zliczać od 0x0000 do wybranej granicy (8 bitowy - 0x00FF, 9 bitowy -
0x01FF, 10 bitowy - 0x03FF), kiedy ją przekroczy liczy z powrotem od zera i powtarza ten
cykl w nieskończoność. Kiedy wartość licznika zrówna się z wartością rejestru
porównującego (OCR1A, OCR1B) daje następujący efekt na wyjściach OC1A i OC1B w
zależności od ustawień jak w tabeli:

Praca wyjścia PWM (bity rejestru TCCR1A)

COM1x1 | COM1x0 | Efekt na OC1x


----------------------------------------------------------
0 | 0 | brak
0 | 1 | brak
1 | 0 | Wyzerowany przy zrównaniu, liczy w górę,
| | ustawiony przy zrównaniu, liczy w dół
1 | 1 | Wyzerowany przy zrównaniu, liczy w dół,
| | ustawiony przy zrównaniu, liczy w górę

Gdzie: x to A lub B

Program przykładowy

Naszym celem będzie napisanie programu, który pokaże działanie wyjścia PWM.
Urządzeniem wyjściowym będzie dioda LED wraz z rezystorem 470 om podłączona
między końcówkę OC1A a zasilanie.
Sterowanie programem umożliwiają dwa przyciski podłączone między końcówki PD2 i
PD3 a masę.

// Testowanie timera 1 w trybie samobieżnego PWM

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT

#if defined(__AVR_AT90S4414__) || defined(__AVR_AT90S8515__) || \


defined(__AVR_AT90S4434__) || defined(__AVR_AT90S8535__) || \
defined(__AVR_ATmega163__) || defined(__AVR_ATmega16__)
#define PWM_out(value) OCR1A=value
#endif

#ifdef __AVR_AT90S2313__
#define PWM_out(value) OCR1=value
#endif

uint16_t pwm=512; // zmienna zawiarająca wartość


wypełnienia

void delay(void) // prosta pętla opóźniająca


{
unsigned int i;
for(i=0;i<50000;i++);
}

int main(void) // program główny


{
DDRD = 0x00; // PORTD jako wejścia dla przycisków
PORTD = 0xFF; // podciągaj wejścia PORTD

51
#if defined(__AVR_AT90S4414__) || defined(__AVR_AT90S8515__) || \
defined(__AVR_AT90S4434__) || defined(__AVR_AT90S8535__) || \
defined(__AVR_ATmega163__) || defined(__AVR_ATmega16__)
sbi(DDRD,PD5); // ustawienie kierunku wyjscia PWM
#endif
#ifdef __AVR_AT90S2313__
sbi(DDRB,PB3); // ustawienie kierunku wyjscia PWM
#endif

TCCR1A = _BV(COM1A1)|_BV(COM1A0)|_BV(PWM10)|_BV(PWM11);
// czasomierz 1 w trybie 10 bitowego
PWM
TCCR1B = _BV(CS00); // czasomierz 1 taktowany F_CPU

pwm=512; // przypisz wartość początkową PWM

while(1) // pętla nieskończona


{
PWM_out(pwm); // wpisz pwm do OCR1

if (bit_is_clear(PIND,PD3))// jeżeli zwarto PD3 z masą


{
delay(); // czekaj chwilę
if (++pwm==1024) pwm=1023;// zwiększ i ogranicz pwm
}

if (bit_is_clear(PIND,PD2))// jeżeli zwarto PD2 z masą


{
delay(); // czekaj chwilę
if (--pwm==0xFFFF) pwm=0; // zmniejsz i ogranicz pwm
}
}
}

AVR-GCC - licznik/czasomierz TIMER 2


UWAGA! Znajduje się tylko w niektórych typach mikrokontrolerów AVR.
Jest 8-bitowym licznikiem mogącym zliczać od 0 do 255. Ponadto może generować
przerwania które następnie należy obsłużyć. Również program może odczytywać jego
wartość w dowolnym momencie (tzw. polling) i na tej podstawie podejmować
odpowiednie działania. Źródło sygnału zegarowego dla jego preskalera zostało nazwane
PCK2. Sygnał PCK2 jest domyślnie podłączony do głównego sygnału zegarowego
procesora CLK. Przez ustawienie bitu AS2 w rejestrze ASSR, licznik/czasomierz 2 jest
taktowany asynchronicznie z końcówki TOSC1. Ten fakt pozwala użyć
licznika/czasomierza 2 jako zegara czasu rzeczywistego (RTC). Kiedy bit AS2 jest
ustawiony, końcówki TOSC1 i TOSC2 są odłączone od portu mikrokontrolera. Można
podłączyć kwarc pomiędzy końcówki TOSC1 oraz TOSC2 i stworzyć w ten sposób
niezależne źródło sygnału taktującego licznik/czasomierz 2. Generator ten jest
zoptymalizowany dla kwarcu o częstotliwości 32768 Hz. Alternatywnie można także
podłączyć zewnętrzny sygnał zegarowy do końcówki TOSC1. Częstotliwość tego sygnału
musi być mniejsza niż jedna czwarta zegara CPU ale nie wyższa niż 256 kHz.

Rejestry licznika/czasomierza 2

Nazwa | Funkcja
--------------------------------------

52
TCCR2 | Rejestr kontrolny
TCNT2 | Wartość
OCR2 | Rejestr porównujący
ASSR | Asynchroniczny rejestr statusu

Tryb czasomierza

W tym trybie licznik jest taktowany wewnętrznym sygnałem zegarowym. Po każdym


takcie wartość TCNT2 jest zwiększana o jeden. Sygnał taktujący jest wynikiem podziału
przez x w stosunku do zegara zewnętrznego. Dzielnik x może przyjąć jedną z wartości: 1,
8, 64, 256, 1024. Np. x=1024 - TCNT2 jest zwiększany o 1 po 1024 taktach zegara
procesora. Współczynnik wstępnego podziału jest ustalany przez wpis odpowiednich
wartości do rejestru TCCR2 (patrz tabela niżej).

Częstotliwość wejściowa czasomierza 2 (bity rejestru TCCR2)

CS22 | CS21 | CS20 | Częstotliwość


----------------------------------
0 | 0 | 0 | 0
0 | 0 | 1 | PCK2
0 | 1 | 0 | PCK2/8
0 | 1 | 1 | PCK2/32
1 | 0 | 0 | PCK2/64
1 | 0 | 1 | PCK2/128
1 | 1 | 0 | PCK2/256
1 | 1 | 1 | PCK2/1024

Program przykładowy

Poniższy przykład będzie miał szczególne znaczenie praktyczne. Wykorzystamy tutaj


czasomierz 2 jako zegar czasu rzeczywistego taktowany asynchronicznie przez generator
stabilizowany kwarcem 32768Hz podłączonym do końcówek TOSC1 i TOSC2 bez
dodatkowych kondensatorów. Czasomierz 2 jest taktowany powyższą częstotliwością
podzieloną przez 128, co powoduje, że przepełnia się on i generuje przerwania z
częstotliwością 1Hz. Zliczanie czasu jest realizowanie w procedurze obsługi przerwania z
czasomierza 2, zaczerpniętej z noty aplikacyjnej AVR134 firymy ATMEL. Wskazówka:
układ może zliczać czas również w trybie obniżonego poboru mocy (power save).
Ponieważ każde przerwanie (w tym przypadku z czasomierza 2) powoduje przełączenie
mikrokontrolera w tryb normalnego poboru mocy, należy zapewnić przejście w tryb
obniżonego poboru mocy po obsłużeniu przerwania (oczywiście w przypadku, kiedy
mamy takie wymaganie).

// Testowanie licznika 2 (zegar czasu rzeczywistego)

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT
#include "uart.h" // obsługa portu szeregowego

prog_char NEWLINE[] = {'\n','\r',0};


// tablica zawiarająca znaki nowej linii
prog_char CLEAR[] = {27,'[','H',27,'[','2','J',0};
// j.w. ale czyszczącza ekran terminala
prog_char HOME[] = {27,'[','H',0};
// j.w. ale przestawiająca kursor na początek
prog_char SPACE[] = {' ',' ',0};

53
// j.w. spacje

typedef struct // definicja struktury


{
uint8_t second; // sekunda
uint8_t minute; // minuta
uint8_t hour; // godzina
uint8_t date; // dzień
uint8_t month; // miesiąc
uint16_t year; // rok
} time; // typ przechowujący czas

time t; // zmienna przechowująca czas

//sprawdza rok przestępny


char not_leap(void)
{
if (!(t.year%100)) // jeśli rok jest podzielny przez 100
return (char)(t.year%400); // sprawdź i zwróć rok podzielny
przez 400
else // w przeciwnym wypadku
return (char)(t.year%4); // zwróć rok podzielny przez 4
}

// zliczanie czasu
// w oparciu o ATMEL Application Note AVR134
SIGNAL(SIG_OVERFLOW2) // obsługa przerwania od licznika 2
{
if (++t.second==60) // inkrementuj sekundy i sprawdź czy
jest ich 60
{ // jeśli tak to:
t.second=0; // wyzeruj licznik sekund oraz
if (++t.minute==60) // inkrementuj licznik minut i sprawdź czy
jest ich 60
{ // jeśli tak to:
t.minute=0; // wyzeruj licznik minut oraz
if (++t.hour==24) // inkrementuj licznik godzin i sprawdź
czy jest ich 24
{ // jeśli tak to:
t.hour=0; // wyzeruj licznik godzin oraz
if (++t.date==32) // inkrementuj licznik dni i sprawdź czy
jest ich 32
{ // jeśli tak to:
t.month++; // inkrementuj licznik miesięcy
t.date=1; // ustaw dzień na 1
}
else if (t.date==31) // jeżeli dzień równa się 31
{ // to sprawdź czy są to miesiące: 4, 6,
9, 11
if ((t.month==4) || (t.month==6) ||
(t.month==9) || (t.month==11))
{ // jeśli tak to:
t.month++; // inkrementuj licznik miesięcy
t.date=1; // ustaw dzień na 1
}
}
else if (t.date==30) // jeżeli dzień równa się 30
{ // to sprawdź czy
if (t.month==2) // jest to miesiąc 2 (luty)
{ // jeśli tak to:
t.month++; // inkrementuj licznik miesięcy

54
t.date=1; // ustaw dzień na 1
}
}
else if (t.date==29) // jeżeli dzień równa się 29
{ // to sprawdź czy:
if ((t.month==2) && (not_leap())) // miesiąc 2 i rok
nieprzestępny
{ // jeśli tak to:
t.month++; // inkrementuj licznik miesięcy
t.date=1; // ustaw dzień na 1
}
}
if (t.month==13) // jeśli miesiąc wynosi 13
{ // to:
t.month=1; // ustaw miesiąc na 1 (styczeń)
t.year++; // inkrementuj licznik lat
}
}
}
}
}

// funkcja używana do wypisywania czasu


void PRINT_VALUE(const char *s, int t)
{
UART_putstr_P(s); // wyświetl tekst s z pamięci programu
UART_putint(t,10); // wyświetl wartość t
UART_putstr_P(SPACE); // dopisz spacje na końcu linii
UART_putstr_P(NEWLINE); // dopisz znaki końca wiersza
}

volatile uint8_t last_second; // przechowuje ostatnio wyświtloną


sekundę

int main(void) // program główny


{
t.year=2004; // zainicjuj rok
t.month=3; // zainicjuj miesiąc
t.date=8; // zainicjuj dzień
t.hour=12; // zainicjuj godzinę

UART_init(); // inicjalizacja portu szeregowego

UART_putstr_P(CLEAR); // wyczyść ekran terminala

TIMSK &=~_BV(TOIE2); // Wyłącz przerwania TC2


ASSR |= _BV(AS2); // przełącz TC2 z taktowania zegarem CPU
// na generator asynchoniczny 32768 Hz
TCCR2 = _BV(CS22)|_BV(CS20); // ustaw preskaler na podział
przez 128
// aby generować przerwania dokładnie co 1
sekundę
while(ASSR&0x07); // czekaj na uaktualnienie TC2
TIMSK |= _BV(TOIE2); // włącz przerwania z TC2

sei(); // włącz obsługę przerwań

while(1) // pętla nieskończona


{
if ((last_second)!=(t.second)) // jeśli zmieniła się sekunda
{

55
last_second=t.second; // zapamiętaj obecną sekundę
UART_putstr_P(HOME); // ustaw kursor na początku
PRINT_VALUE(PSTR("Rok = "),t.year); // wyświetl rok
PRINT_VALUE(PSTR("Miesiac = "),t.month); // wyświetl miesiąc
PRINT_VALUE(PSTR("Dzien = "),t.date); // wyświetl dzien
PRINT_VALUE(PSTR("Godzina = "),t.hour); // wyświetl godzinę
PRINT_VALUE(PSTR("Minuta = "),t.minute); // wyświetl minutę
PRINT_VALUE(PSTR("Sekunda = "),t.second); // sekundę
}
}
}

Tryb porównywania

Licznik 2 wyposażony jest w funkcję porównywania jego zawartości z wartością zadaną.


Do tego celu używa rejestru OCR2. Jego zawartość jest stale porównywana z zawartością
TCNT2. Pozytywny wynik porównania może wywołać wyzerowanie licznika TCNT2 lub
zmianę stanu na pinie OC2.

Wybór trybu porównywania (rejestr TCCR2)

COM21 | COM20 | Opis


---------------------------------------------------
0 | 0 | Licznik 2 odłączony od końcówki OC2
0 | 1 | Przełączanie stanu końcówki OC2 na przeciwny
1 | 0 | Zerowanie stanu końcówki OC2
1 | 1 | Ustawianie stanu końcówki OC2

Bity rejestru TCCR1B określające częstotliwość wejściową czasomierza 1

CS22 | CS21 | CS20 | Częstotliwość


-------------------------------------
0 | 0 | 0 | 0
0 | 0 | 1 | CLK
0 | 1 | 0 | CLK/8
0 | 1 | 1 | CLK/64
1 | 0 | 0 | CLK/256
1 | 0 | 1 | CLK/1024

Jeśli chcemy wyzerować licznik gdy zachodzi równość z rejestrem porównującym należy
ustawić bit CTC2 w rejestrze TCCR2.

Program przykładowy

Naszym celem będzie napisanie programu, który pokaże nam zastosowanie trybu
porównywania.Będzie to generator przebiegu prostokątnego o regulowanej częstotliwości i
wypełnieniu przebiegu. Wyjściem generatora jest PB0. Częstotliwość reguluje się
zwierając do masy linie PD3 lub PD4. Wypełnienie regulujemy zwierając do masy linie
PD5 lub PD6.

// Testowanie timera 2 w trybie porównywania

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()

56
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT
#include <avr/delay.h> // funkcje opóźniające

volatile uint8_t delay; // zmienna określająca częstotliwość


volatile uint8_t compare; // zmienna określająca wypełnienie

SIGNAL (SIG_OUTPUT_COMPARE2) // przerwanie od porównania


{
cbi(PORTC,PB2); // zapal diodę na PC2
}

SIGNAL (SIG_OVERFLOW2) // przerwanie od przepełnienia


{
TCNT2 = delay; // przeładuj TIMER2
sbi(PORTC,PC2); // zgaś diodę na PC2
}

int main(void) // program główny


{
DDRC = 0xFF; // PORTB jako wyjścia dla LED
PORTC = 0xFF; // zgaś diody LED na PORTB
DDRD = 0x00; // PORTD jako wejścia dla
przycisków
PORTD = 0xFF; // podciągaj wejścia PORTD
delay = 0x10; // domyślna wartość dla TIMERA2
compare = 0x7F; // domyślna wartość dla porównania
TIMSK=_BV(TOIE2)|_BV(OCIE2); // włącz przerwania
// od przepełnienia i porównania
TIMERA1

TCNT2=delay; // zainicjuj TIMER1


TCCR2=_BV(CS20)|_BV(CS21)|_BV(CS22); // czasomierz 2 taktowany
F_CPU/1024

sei(); // włącz obsługę przerwań


while(1) // pętla nieskończona
{
if (bit_is_clear(PIND,PD2)) // jeżeli zwarto PD2 z masą
delay++; // zwiększ delay

if (bit_is_clear(PIND,PD3)) // jeżeli zwarto PD3 z masą


delay--; // zmniejsz delay

if (bit_is_clear(PIND,PD6)) // jeżeli zwarto PD6 z masą


compare++; // zwiększ compare

if (bit_is_clear(PINB,PB0)) // jeżeli zwarto PB0 z masą


compare--; // zmniejsz compare

OCR2 = compare; // wpisz compare do OCR2

_delay_loop_2(50000); // pętla opóźniająca


}
}

\section {Tryb PWM - modulowana szerokość impulsu}


Kiedy zostanie wybrany tryb pracy licznika jako PWM (Pulse Width Modulation) czyli
modulacja szerokości impulsów może być używany jako 8 bitowy samobieżny modulator
PWM. Aby wybrać ten tryb należy ustawić bit PWM2 w rejestrze TCCR2. Licznik może
zliczać od 0x00 do 0xFF, kiedy ją przekroczy liczy z powrotem od zera i powtarza ten cykl

57
w nieskończoność. Kiedy wartość licznika zrówna się z wartością rejestru porównującego
OCR2 daje następujący efekt na wyjściu OC2 w zależności od ustawień bitów jak w tabeli
\ref{pwm2wyj}.
Praca wyjścia PWM (bity rejestru TCCR2)

COM21 | COM20 | Efekt na OC2


-------------------------------------------
0 | 0 | brak
0 | 1 | brak

1 | 0 | Wyzerowany przy zrównaniu,


| liczy w górę, ustawiony
| przy zrównaniu, liczy w dół

1 | 1 | Wyzerowany przy zrównaniu,


| liczy w dół, ustawiony
| przy zrównaniu, liczy w górę

Program przykładowy
// Testowanie timera 2 w trybie samobieżnego PWM

#include <avr/io.h> // dostęp do rejestrów


#include <avr/delay.h> // zawiera definicję _delay_loop2

int main(void) // program główny


{
uint8_t pwm=128; // zmienna zawiarająca wartość
wypełnienia

DDRD = 0x80; // PORTD jako wejścia dla


przycisków
// oraz PD7 jako wyjście PWM
PORTD = 0x7F; // podciągaj wejścia PORTD

TCCR2 = _BV(COM21)|_BV(COM20)|_BV(PWM2)|_BV(CS20);
// czasomierz 2 w trybie PWM
// taktowany F_CPU

while(1) // pętla nieskończona


{
if (bit_is_clear(PIND,PD3)) // jeżeli zwarto PD3 z masą
if (++pwm==0) pwm=255; // zwiększ i ogranicz pwm

if (bit_is_clear(PIND,PD2)) // jeżeli zwarto PD2 z masą


if (--pwm==255) pwm=0; // zmniejsz i ogranicz pwm

OCR2 = pwm; // wpisz pwm do OCR2

_delay_loop_2(50000); // pętla opóźniająca


}
}

AVR-GCC - komparator analogowy


Komparator analogowy porównuje napięcia na wejściach AIN0 i AIN1. W przypadku gdy
napięcie na AIN0 jest wyższe od AIN1 ustawiany jest bit ACO w rejestrze ACSR. Wyjście
komparatora może zostać wykorzystane do wyzwalania przechwytywania wartości

58
licznika/czasomierza 1. Oprócz tego, komparator analogowy może wygenerować
przerwanie.
Można wybrać czy przerwanie ma być generowane przez wyjście komparatora zboczem
narastającym,
opadającym lub obydwoma. Ta funkcja jest kontrolowana przez bity ACIS0 i ACIS1
rejestru ACSR

Programy przykładowe

Aby przetestować działanie komparatora należy podłączyć do jego wejść prosty układ,
który wymusi różne napięcia na jego wejściach np. do wejścia AIN0 podłączmy
symetryczny dzielnik napięcia dający na wyjściu połowę napięcia zasilania. Do wejścia
AIN1 podłączmy końcówkę suwaka potencjometru, którego krańcowe wyprowadzenia są
podłączone pomiędzy zasilanie a masę. Ze względu na dużą rezystancję wejściową
komparatora, wartości rezystancji rezystorów i potencjometru nie są krytyczne i mogą
zawierać się w granicach od 1k do 1M.
Przykład z poniższego listingu ilustruje działanie komparatora analogowego. Stan wyjścia
komparatora jest cyklicznie sprawdzany i na jego podstawie sterowana jest dioda LED
podłączona do linii PB7.

// Testowanie komparatora analogowego bez użycia przerwania

#include <avr/io.h> // dostęp do rejestrów

int main(void)
{
sbi(DDRB, PB7); // linia PB7 jako wyjście (LED)
while(1) // pętla nieskończona
{
if (bit_is_set(ACSR, ACO)) // jeżeli na wyjściu komparatora
jest 1
cbi(PORTB, PB7); // zapal diodę na PB7
else // w przeciwnym wypadku
sbi(PORTB, PB7); // zgaś ją
}
}

Przykład z poniższego listingu pokazuje użycie komparatora analogowego do generowania


przerwań. Szczegóły w komentarzach listingu.

// Testowanie komparatora analogowego z użyciem przerwania

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT

#define tbi(PORT,BIT) PORT^=_BV(BIT)


// przełącza stan BITu w PORTcie na przeciwny 1->0 ; 0->1

SIGNAL(SIG_COMPARATOR)
{
tbi(PORTB, PB7); // zmień stan diody na PB7
}

int main(void)
{

59
sbi(DDRB, PB7); // linia PB7 jako wyjście (LED)
ACSR = _BV(ACIE); // komparator analogowy generuje przerwanie
// przy każdej zmianie stanu na jego wyjściu
sei(); // włącz obsługę przerwań
while(1); // pętla nieskończona
}

Przykład z poniższego listingu pokazuje użycie komparatora analogowego do wyzwalania


wejścia przechwytującego timera 1 i zbudowania w ten sposób prostego przetwornika
analogowo/cyfrowego. Układ połączeń zewnętrznych jest bardzo prosty i składa się tylko z
dwóch elementów: rezystora i kondensatora. Przetwornik ten nie ma wybitnych
parametrów - szczególnie liniowość i szybkość przetwarzania budzą wielkie zastrzeżenia
lecz należy pamiętać, że jest to tylko przykład mogący służyć jako inspiracja do
skonstruowania i oprogramowania lepszego przetwornika.

// Testowanie komparatora analogowego:


// wyzwalanie wejścia przechwytującego timera 1
// z wyjścia komparatora analogowego
// prosta implementacja przetwornika analogowo/cyfrowego

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT
#include "uart.h" // obsługa portu szeregowego
#include "delay.h" // funkcje opóźniające

#define R_PIN PB4 // końcówka rezystora

#ifdef __AVR_AT90S2313__
# define C_PIN PB1 // końcówka kondensatora i rezystora
#else
# define C_PIN PB3 // końcówka kondensatora i rezystora
#endif

// w związu z faktem, że w różnych układach te same bity


// mogą inaczej się nazywać wprowadzono poniższe makro:
#ifndef TICIE1 // jeśli nie zdefiniowano TICIE1
#define TICIE1 TICIE // zdefiniuj TICIE1 jako TICIE
#endif

prog_char NEWLINE[] = {'\n','\r',0};


// tablica zawiarająca znaki nowej linii
prog_char CLEAR[] = {27,'[','H',27,'[','2','J',0};
// j.w. ale czyszczącza ekran terminala
prog_char HOME[] = {27,'[','H',0};
// j.w. ale przestawiająca kursor na początek
prog_char SPACE[] = {' ',' ',' ',' ',0};
// j.w. spacje

volatile uint16_t value; // przechowuje przetworzoną wartość


volatile uint8_t busy; // do sprawdzania zajętości przetwornika

void ADC_start(void) // start przetwarzania


{
busy=1; // wpisz zajętość przetwornika
TCCR1B = 0; // licznik 1 zatrzymany
TCNT1 = 0; // wyzeruj timer 1
cbi(DDRB, R_PIN); // odłącz linię rezystora

60
sbi(DDRB, C_PIN); // ustaw linię kondenstaora (- komparatora)
jako wyjście
delay10us(); // i czekaj na rozładowanie kondensatora
cbi(DDRB, C_PIN); // ustaw linię kondenstaora (- komparatora)
jako wejście
sbi(DDRB, R_PIN); // podłącz linię rezystora (rozpocznij
ładowanie kondensatora)
TCCR1B = _BV(CS10)|_BV(ICNC1);
// licznik 1 taktowany F_CPU
// wraz z filtracją zakłóceń z wejścia
przechwytywania
// i przechwytywaniem za pomocą opadającego
zbocza
while(busy); // czekaj na przerwanie od przechwytywania
lub przepełnienia
}

SIGNAL (SIG_INPUT_CAPTURE1) // przerwanie od przechwytywania


licznika 1
{
value=ICR1; // odczytaj wartość z rejestru
przechwytującego
busy=0; // można zakończyć przetwarzanie
}

SIGNAL (SIG_OVERFLOW1) // przerwanie od przepełnienia licznika 1


{
value=0; // wpisz wartość 0
busy=0; // można zakończyć przetwarzanie
}

int main(void) // program główny


{
UART_init(); // inicjalizacja portu szeregowego

sbi(PORTB, R_PIN); // linia "zasilająca" rezystor


ACSR = _BV(ACIC); // wyjście komparatora analogowego połączone
// z wejściem przechytującym licznika 1

TIMSK = _BV(TOIE1)|_BV(TICIE1);
// włącz przerwania licznika 1
// od przepełnienia i przechwytywania

sei(); // włącz obsługę przerwań

UART_putstr_P(CLEAR); // wyczyść ekran terminala

while(1) // pętla nieskończona


{
ADC_start(); // zainicjuj przetwarzanie
UART_putstr_P(HOME); // ustaw kursor na początku ekranu
UART_putint(value,10); // wypisz przetworzoną wartość
UART_putstr_P(SPACE); // dopisz spacje na końcu wiersza
}
}

AVR-GCC - przetwornik analogowo/cyfrowy

61
Przetwornik analogowo/cyfrowy w mikrokontrolerze integruje przetwarzanie sygnałów
analogowych i cyfrowych w jednym układzie. Posiada on rozdzielczość 10 bitów (1024
poziomy). Pracuje na zasadzie sukcesywnej aproksymacji. Aby można było mierzyć
wartości sygnałów analogowych z wielu źródeł, na wejściu przetwornika umieszczono
multiplekser analogowy. W danej chwili tylko jeden z kilku sygnałów analogowych może
być zamieniony na postać cyfrową. Rejestr ADMUX z załadowaną odpowiednią wartością
wskazuje, który z tych kanałów jest połączony z przetwornikiem.

Wartość Używany
ADMUX kanał
--------------------
0 kanał 0
1 kanał 1
2 kanał 2
3 kanał 3
4 kanał 4
5 kanał 5
6 kanał 6
7 kanał 7

Do prawidłowej pracy przetwornik potrzebuje sygnału taktującego o częstotliwości z


przedziału od 50kHz do 200kHz. Sygnał ten jest generowany z sygnału zegarowego
procesora. Aby uzyskać właściwą częstotliwość należy skorzystać z wbudowanego
preskalera. Stopień podziału ustala się ustawiając trzy bity ADPS0, ADPS1 i ADPS2 w
rejestrze ADCSR.

ADPS2 ADPS1 ADPS0 podział


----------------------------
0 0 0 1
0 0 1 2
0 1 0 4
0 1 1 8
1 0 0 16
1 0 1 32
1 1 0 64
1 1 1 128

Przykład:

fosz = 4MHz
50kHz < fad < 200kHz
-> prescale = 32
fad = fosz/32 = 4000000/32 = 125000 = 125 kHz
50kHz < 125kHz < 200kHz
-> ADPS0 = 1, ADPS1 = 0, ADPS2 = 1

Przetwornik może pracować w dwóch trybach: na żądanie oraz samobieżnie. 10 bitowy


wynik konwersji jest dostępny w rejestrach ADCL (mniej znaczący bajt) i ADCH (bardziej
znaczący bajt). Podczas odczytu należy uważać aby najpierw odczytywać ADCL a
następnie ADCH, lub po prostu odczytywać 16 bitowy rejestr ADCW.

Rejestr ADCSR

Bit | Nazwa | Opis


0 | ADPS0 | preskaler
1 | ADPS1 | preskaler

62
2 | ADPS2 | preskaler
3 | ADIE | zezwolenie na generowanie przerwania przez przetwornik na
zakończenie przetwarzania
4 | ADIF | znacznik przerwania z przetwornika - jeśli jest ustawiony
to konwersja A/D została zakończona
5 | ADFR | przetwarzanie samodzielne (Free Run)
6 | ADSC | start konwersji - jeśli jest ustawiony ten bit oraz ADEN
7 | ADEN | włączenie przetwornika (AD Enable)

Poniższe programy pozwolą na bliższe poznanie przetwornika analogowo/cyfrowego


wbudowanego w mikrokontroler.
Do zaprezentowania działania przetwornika niezbędny jest odpowiednio wyposażony
układ. Może to być np. AT90S8535, ATmega16, ATmega8 itp. Poniżej przedstawiono
schemat niezbędnych połączeń potrzebnych do prawidłowego działania programów
przykładowych. Bardzo ważna jest obecność rezystora na wejściu ADC0 - bez niego może
dojść do uszkodzenia wejścia przetwornika w przypadku podania bezpośrednio na nie
napięcia zasilania mikrokontrolera. Końcówka AREF służy do podania napięcia
odniesienia dla przetwornika, co przekłada się na maksymalne napięcia jakie może zostać
przetworzone (minus wartość najmniej znaczącego bitu). Napięcie na wejściu AREF nie
może być wyższe od AVCC i niższe od 2V.

Programy przykładowe

Program pokazuje wykorzystanie przetwornika ADC w trybie pracy tzw. konwersji na


żądanie. Żądanie konwersji wywołuje się przez wysłanie dowolnego znaku na port

63
szeregowy mikrokontrolera. Wynik przetwarzania jest wysyłany również na port
szeregowy oraz 8 bardziej znaczących bitów wyniku jest prezentowanych na diodach LED
podłączonych do PORTB.

// Testowanie przetwornika analogowo/cyfrowego


// w trybie pojedynczej konwersji

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT
#include "uart.h" // obsługa portu szeregowego

prog_char NEWLINE[] = {'\n','\r',0};


// tablica zawiarająca znaki nowej linii
prog_char CLEAR[] = {27,'[','H',27,'[','2','J',0};
// j.w. ale czyszczącza ekran terminala

volatile uint16_t value; // przetworzona wartość z ADC

SIGNAL(SIG_ADC) // przerwanie z przetwornika ADC


{
value = ADCW; // czytaj wartość z przetwornika
ADC
PORTC,~(value>>2); // wyślij przetworzoną wartość na
LED
UART_putint(value,10); // wypisz przetworzoną wartość
UART_putstr_P(NEWLINE); // dopisz znaki nowej linii
}

int main(void) // program główny


{
UART_init(); // inicjalizacja portu szeregowego

DDRC = 0xFF; // wszystkie linie PORTB jako wyjścia


ADMUX = 0; // wybierz kanał 0 przetwornika ADC
ADCSR = _BV(ADEN)|_BV(ADIE)|_BV(ADPS0)|_BV(ADPS1);
// włącz przetwornik i uruchom generowanie
przerwań
// częstotilwość taktowania F_ADC=F_CPU/64
// przy F_CPU=8MHz : F_ADC=125 kHz

sei(); // włącz obsługę przerwań

UART_putstr_P(CLEAR); // wyczyść ekran terminala


while(1) // pętla nieskończona
{
UART_getchar(); // czekaj na znak z portu szeregowego
sbi(ADCSR,ADSC); // rozpocznij pomiar przetwornikiem ADC
}
}

Program pokazuje wykorzystanie przetwornika ADC w trybie tzw. konwersji samobieżnej.


Wynik przetwarzania jest wysyłany na port szeregowy. 8 bardziej znaczących bitów
wyniku jest prezentowanych na diodach led podłączonych do PORTB. Należy zwrócić
uwagę na fakt, że aby uruchomić ten tryb pracy należy oprócz bitu ADFR ustawić również
bit ADSC w rejestrze ADCSR.

// Testowanie przetwornika analogowo/cyfrowego


// konwersja samobieżna

64
#include <avr/io.h> // dostęp do rejestrów
#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT
#include "uart.h" // obsługa portu szeregowego

prog_char NEWLINE[] = {'\n','\r',0};


// tablica zawiarająca znaki nowej linii
prog_char CLEAR[] = {27,'[','H',27,'[','2','J',0};
// j.w. ale czyszczącza ekran terminala
prog_char HOME[] = {27,'[','H',0};
// j.w. ale przestawiająca kursor na początek
prog_char SPACE[] = {' ',' ',' ',' ',0};
// j.w. spacje

volatile uint16_t value;

SIGNAL(SIG_ADC) // przerwanie z przetwornika ADC


{
value = ADCW; // czytaj wartość z przetwornika
ADC
PORTC = ~(value>>2); // wyślij przetworzoną wartość na
LED
}

int main(void) // program główny


{
UART_init(); // inicjalizacja portu szeregowego

DDRC = 0xFF; // wszystkie linie PORTB jako


wyjścia
ADMUX = 0; // wybierz kanał 0 przetwornika ADC
ADCSR = _BV(ADEN)|_BV(ADIE)|_BV(ADFR)|_BV(ADSC)|_BV(ADPS0)|_BV(ADPS1);
// włącz przetwornik ADC w trybie
samobieżnym
// uruchom generowanie przerwań
// częstotilwość taktowania
F_ADC=F_CPU/64
// przy F_CPU=8MHz : F_ADC=125 kHz

sei(); // włącz obsługę przerwań

UART_putstr_P(CLEAR); // wyczyść ekran terminala


while(1) // pętla nieskończona
{
UART_putstr_P(HOME); // dopisz spacje na końcu wiersza
UART_putint(value,10); // wypisz przetworzoną wartość
UART_putstr_P(SPACE); // dopisz spacje na końcu wiersza
}
}

AVR-GCC - układ Watchdog


Mikrokontrolery AVR są wyposażone w układ nadzoru tzw. Watchdog czyli po polsku
"czuwający pies". Nazwa ta jest adekwatna do roli jaką pełni w systemie. Układ ten składa
się z czasomierza, taktowanego z wewnętrznego generatora o częstotliwości 1MHz (przy
napięciu zasilania Vcc=5V) niezależnego od zegara systemowego. Częstotliwość pracy
tego generatora jest silnie zależna od napięcia zasilającego mikrokontroler. Pomiędzy

65
czasomierzem a generatorem znajduje się preskaler, którym można ustalić jak często ma
być generowany sygnał restartujący mikrokontroler. Aby jednak nie dopuścić do
restartowania mikrokontrolera należy co pewien czas zerować czasomierz układu nadzoru.
Biblioteka avr-libc dołączona do kompilatora zawiera funkcje ułatwiające używanie
układu Watchdog. Do programu należy dołączyć nagłówek avr/wdt.h.

Najważniejsze funkcje zawarte w pliku avr/wdt.h

wdt_enable(timeout)
Załącza układ Watchdog z czasem zdefiniowanym jako timeout. Jako timeout można użyć
predefiniowanych wartości zamieszczonych w tabeli:

Stała Czas
-------------------
WDTO_15MS 15 ms
WDTO_30MS 30 ms
WDTO_60MS 60 ms
WDTO_250MS 250 ms
WDTO_500MS 500 ms
WDTO_1S 1000 ms
WDTO_2S 2000 ms

Przykład:

wdt_enable(WDTO_500MS);

Należy pamiętać, że czasy te podane są tylko orientacyjnie i mogą się różnić od podanych
nawet kilkukrotnie! Niższe napięcie zasilania mikrokontrolera oznacza wydłużenie czasu
zadziałania układu Watchdog.

wdt_reset()
Powoduje kasowanie czasomierza układu Watchdog. Kiedy układ Watchdog jest aktywny,
powyższą funkcję należy wywoływać odstępach czasu mniejszych od czasu zadziałania
układu Watchdog.

wdt_disable()
Powoduje wyłączenie układu Watchdog.

Program przykładowy

Poniżej zamieszczono listing programu mogącego służyć do różnych eksperymentów z


układem Watchdog. Komunikacja z użytkownikiem za pomocą terminala podłączonego do
portu szeregowego. Aby najlepiej poznać działanie układu proponuję wstawiać i usuwać z
różnych miejsc programu funkcję a właściwie makro WDR().

// Testowanie układu WATCHDOG

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT
#include <avr/wdt.h> // obsługa układu Watchdog
#include "uart.h" // obsługa portu szeregowego

66
#define WDT_ENABLE // określa czy używamy Watchdoga

#ifdef WDT_ENABLE // jeśli go używamy


#define WDR() wdt_reset() // to przypisz wdt_reset do WDR
#else // jeśli nie
#define WDR() // to WDR jest pustym ciągiem
#endif

SIGNAL (SIG_OVERFLOW0)
{
// WDR(); // reset licznika Watchdog
// nie powinno się resetować Wdoga
// w fukcjach obsługi przerwań !!!
}

void long_loop(void)
{
uint16_t i,j,k; // zmienne lokalne dla pętli
UART_putstr_P(PSTR("long_loop - start\n\r"));
for(i=0;i<0xFFFF;i++) // pierwsza pętla
{
for(j=0;j<0xFFFF;j++) // druga pętla
{
for(k=0;k<0xFFFF;k++) // trzecia pętla
{
// WDR(); // reset licznika Watchdog
}
// WDR(); // reset licznika Watchdog
}
WDR(); // reset licznika Watchdog
}
UART_putstr_P(PSTR("long_loop - stop\n\r"));
}

int main(void)
{
UART_init(); // inicjalizacja portu szeregowego
UART_putstr_P(PSTR("Program wystartował !!!\n\r"));

#ifdef WDT_ENABLE
// wdt_enable(WDTO_2S); // watchdog na czas ok 2s
// wdt_enable(WDTO_1S); // watchdog na czas ok 1s
wdt_enable(WDTO_500MS); // watchdog na czas ok 0,5s
// wdt_enable(WDTO_250MS); // watchdog na czas ok 0,25s
// wdt_enable(WDTO_120MS); // watchdog na czas ok 0,12s
// wdt_enable(WDTO_60MS); // watchdog na czas ok 0,06s
// wdt_enable(WDTO_30MS); // watchdog na czas ok 0,03s
// wdt_enable(WDTO_15MS); // watchdog na czas ok 0,015s
#endif

TIMSK = _BV(TOIE0); // włącz obsługę przerwań T/C0


TCCR0 = _BV(CS02); // taktowanie T/C0

sei(); // włącz obsługę przerwań

while(1) // pętla nieskończona


{
long_loop(); // długa pętla symulująca jakieś działania
// WDR(); // restart licznika Watchdoga
}
}

67
AVR-GCC - tryby zmniejszonego poboru
mocy
Mikrokontrolery AVR są wyposażone w układy pozwalające na wejście w tryby pracy ze
zmniejszonym poborem mocy.
Oczywiście nie ma nic zupełnie za darmo i odbywa się to różnymi kosztami jak np.
zmniejszona szybkość pracy, wyłączenie niektórych urządzeń peryferyjnych (komparator
analogowy, przetwornik analogowo/cyfrowy itp.).

Najważniejsze funkcje zawarte w pliku avr/sleep.h

set_sleep_mode(mode)
Przygotowuje mikrokontroler do wprowadzenia w tryb uśpienia przez ustawienie
odpowiednich bitów w rejestrze SMCR lub MCUCR (sprawdź w dokumentacji
mikrokontrolera): SM0, SM1, SM2.
Poniżej przedstawiono możliwe tryby obniżonego poboru mocy:

• SLEEP_MODE_IDLE
• SLEEP_MODE_PWR_DOWN
• SLEEP_MODE_PWR_SAVE
• SLEEP_MODE_ADC
• SLEEP_MODE_STANDBY
• SLEEP_MODE_EXT_STANDBY

Przykład:

set_sleep_mode(SLEEP_MODE_PWR_SAVE);

Należy sprawdzić w dokumentacji mikrokontrolera, które z nich są możliwe do


wykorzystania w użytym mikrokontrolerze.

sleep_mode()
Wprowadza mikrokontroler w tryb uśpienia wybrany wcześniej za pomocą funkcji
set_sleep_mode(mode).

Program przykładowy

Poniższy przykład będzie rozszerzeniem wcześniej zaprezentowanego zegara czasu


rzeczywistego zbudowanego z czasomierza 2 taktowanego z oddzielnego generatora
kwarcowego. W tryb obniżonego poboru mocy układ wchodzi przez zwarcie linii PB0 z
masą - wyjście przez podłączenie ww. linii do zasilania.

// Testowanie przejścia w tryb obniżonego poboru mocy


// (zegar czasu rzeczywistego zbudowany z licznika 2)

#include <avr/io.h> // dostęp do rejestrów


#include <avr/interrupt.h> // funkcje sei(), cli()
#include <avr/signal.h> // definicje SIGNAL, INTERRUPT
#include <avr/sleep.h> // funkcje obniżonego poboru mocy
#include "uart.h" // obsługa portu szeregowego

68
#define PWR_save bit_is_clear(PINB,PINB0)

prog_char NEWLINE[] = {'\n','\r',0};


// tablica zawiarająca znaki nowej linii
prog_char CLEAR[] = {27,'[','H',27,'[','2','J',0};
// j.w. ale czyszczącza ekran terminala
prog_char HOME[] = {27,'[','H',0};
// j.w. ale przestawiająca kursor na początek
prog_char SPACE[] = {' ',' ',0};
// j.w. spacje

typedef struct // definicja struktury


{
uint8_t second; // sekunda
uint8_t minute; // minuta
uint8_t hour; // godzina
uint8_t date; // dzień
uint8_t month; // miesiąc
uint16_t year; // rok
} time; // typ przechowujący czas

time t; // zmienna przechowująca czas

//sprawdza rok przestępny


char not_leap(void)
{
if (!(t.year%100)) // jeśli rok jest podzielny przez 100
return (char)(t.year%400); // sprawdź i zwróć rok podzielny
przez 400
else // w przeciwnym wypadku
return (char)(t.year%4); // zwróć rok podzielny przez 4
}

// zliczanie czasu
// w oparciu o ATMEL Application Note AVR134
SIGNAL(SIG_OVERFLOW2) // obsługa przerwania od licznika 2
{
if (++t.second==60) // inkrementuj sekundy i sprawdź czy
jest ich 60
{ // jeśli tak to:
t.second=0; // wyzeruj licznik sekund oraz
if (++t.minute==60) // inkrementuj licznik minut i
sprawdź czy jest ich 60
{ // jeśli tak to:
t.minute=0; // wyzeruj licznik minut oraz
if (++t.hour==24) // inkrementuj licznik godzin i
sprawdź czy jest ich 24
{ // jeśli tak to:
t.hour=0; // wyzeruj licznik godzin oraz
if (++t.date==32) // inkrementuj licznik dni i sprawdź czy
jest ich 32
{ // jeśli tak to:
t.month++; // inkrementuj licznik miesięcy
t.date=1; // ustaw dzień na 1
}
else if (t.date==31) // jeżeli dzień równa się 31
{ // to sprawdź czy są to miesiące: 4, 6,
9, 11
if ((t.month==4) || (t.month==6) ||
(t.month==9) || (t.month==11))

69
{ // jeśli tak to:
t.month++; // inkrementuj licznik miesięcy
t.date=1; // ustaw dzień na 1
}
}
else if (t.date==30) // jeżeli dzień równa się 30
{ // to sprawdź czy
if (t.month==2) // jest to miesiąc 2 (luty)
{ // jeśli tak to:
t.month++; // inkrementuj licznik miesięcy
t.date=1; // ustaw dzień na 1
}
}
else if (t.date==29) // jeżeli dzień równa się 29
{ // to sprawdź czy:
if ((t.month==2) && (not_leap())) // miesiąc 2 i rok
nieprzestępny
{ // jeśli tak to:
t.month++; // inkrementuj licznik miesięcy
t.date=1; // ustaw dzień na 1
}
}
if (t.month==13) // jeśli miesiąc wynosi 13
{ // to:
t.month=1; // ustaw miesiąc na 1 (styczeń)
t.year++; // inkrementuj licznik lat
}
}
}
}
if (PWR_save) // jeśli układ ma być w trybie
obniżonego poboru
sleep_mode(); // to wprować go w ten tryb
}

// funkcja używana do wypisywania czasu


void PRINT_VALUE(const char *s, int t)
{
UART_putstr_P(s); // wyświetl tekst s z pamięci programu
UART_putint(t,10); // wyświetl wartość t
UART_putstr_P(SPACE); // dopisz spacje na końcu linii
UART_putstr_P(NEWLINE); // dopisz znaki końca wiersza
}

volatile uint8_t last_second; // przechowuje ostatnio wyświtloną


sekundę

int main(void) // program główny


{
u16 temp0, temp1; // zmienne tymczasowe

t.year=2003; // zainicjuj rok


t.month=3; // zainicjuj miesiąc
t.date=8; // zainicjuj dzień

set_sleep_mode(SLEEP_MODE_PWR_SAVE);
// przygotuj układ do obniżonego poboru mocy

UART_init(); // inicjalizacja portu szeregowego

UART_putstr_P(CLEAR); // wyczyść ekran terminala

70
for(temp0=0;temp0<0x0040;temp0++)
for(temp1=0;temp1<0xFFFF;temp1++);
// czekaj na ustabilizowanie się generatora
32768 Hz

TIMSK &=~_BV(TOIE2); // Wyłącz przerwania TC2


ASSR |= _BV(AS2); // przełącz TC2 z taktowania zegarem CPU
// na generator asynchoniczny 32768 Hz
TCCR2 = _BV(CS22)|_BV(CS20); // ustaw preskaler na podział
przez 128
// aby generować przerwania dokładnie co 1
sekundę
while(ASSR&0x07); // czekaj na uaktualnienie TC2
TIMSK |= _BV(TOIE2); // włącz przerwania z TC2

sei(); // włącz obsługę przerwań

while(1) // pętla nieskończona


{
if (!PWR_save) // jeśli nie jest to tryb oszczędny
{
if ((last_second!=t.second))// jeśli zmieniła się sekunda
{
last_second=t.second; // zapamiętaj obecną sekundę
UART_putstr_P(HOME); // ustaw kursor na początku
PRINT_VALUE(PSTR("Rok = "),t.year); // wyświetl rok
PRINT_VALUE(PSTR("Miesiac = "),t.month); // wyświetl miesiąc
PRINT_VALUE(PSTR("Dzien = "),t.date); // wyświetl
dzien
PRINT_VALUE(PSTR("Godzina = "),t.hour); // wyświetl
godzinę
PRINT_VALUE(PSTR("Minuta = "),t.minute); // wyświetl minutę
PRINT_VALUE(PSTR("Sekunda = "),t.second); // sekundę
}
}
}
}

AVR-GCC - opcje wywoływania narzędzi


Avr-gcc posiada wiele wbudowanych przełączników wywoływanych bezpośrednio z linii
komend zawierających opcje do kontroli optymalizacji, ostrzeżeń i generacji kodu. Łącząc
opcje lub przełączniki, należy je podawać oddzielnie. Dlatego na przykład: złączenie `-dr'
zostanie zinterpretowane inaczej niż `-d -r'. Jeśli dodajemy do opcji nazwę pliku, możemy
to zrobić łącznie, np: `-onazwa' i `-o nazwa' da ten sam efekt.
Większość opcji zaczynających się od `-f' i `-W' posiada dwie formy: -fname i -fno-name
(podobnie: -Wname, -Wno-name). Pierwsza forma włącza przełącznik o podanej nazwie,
druga go wyłącza.

-mmcu=architektura
Kompiluje kod dla podanej architektury. Przez avr-gcc rozpoznawane są następujące
architektury:
• avr1 - "Prosty" rdzeń CPU, tylko programy w asemblerze
• avr2 - "Klasyczny" rdzeń CPU, do 8 KB ROM
• avr3 - "Klasyczny" rdzeń CPU, więcej niż 8 KB ROM

71
• avr4 - "Rozszerzony" rdzeń CPU, do 8 KB ROM
• avr5 - "Rozszerzony" rdzeń CPU, więcej niż 8 KB ROM
Domyślnie kod jest generowany dla architektury avr2. Zauważmy, że kiedy używamy -
mmcu=architecture a nie -mmcu=typ_MCU, plik nagłówkowy avr/io.h nie będzie
realizował swej funkcji dopóki nie zdecydujemy się, na konkretny typ MCU. Typ MCU
wybiera się podobnie.

-mmcu=typ_MCU
W tabeli przedstawiono zestawienie wszystkich typów MCU rozpoznawanych przez
kompilator avr-gcc (a dokładniej bibliotekę avr-libc).
arch typ_MCU makrodefinicja
----------------------------------
avr1 at90s1200 __AVR_AT90S1200__
avr1 attiny11 __AVR_ATtiny11__
avr1 attiny12 __AVR_ATtiny12__
avr1 attiny15 __AVR_ATtiny15__
avr1 attiny28 __AVR_ATtiny28__
avr2 at90s2313 __AVR_AT90S2313__
avr2 at90s2323 __AVR_AT90S2323__
avr2 at90s2333 __AVR_AT90S2333__
avr2 at90s2343 __AVR_AT90S2343__
avr2 attiny22 __AVR_ATtiny22__
avr2 attiny24 __AVR_ATtiny24__
avr2 attiny25 __AVR_ATtiny25__
avr2 attiny26 __AVR_ATtiny26__
avr2 attiny261 __AVR_ATtiny261__
avr2 attiny44 __AVR_ATtiny44__
avr2 attiny45 __AVR_ATtiny45__
avr2 attiny461 __AVR_ATtiny461__
avr2 attiny84 __AVR_ATtiny84__
avr2 attiny85 __AVR_ATtiny85__
avr2 attiny861 __AVR_ATtiny861__
avr2 at90s4414 __AVR_AT90S4414__
avr2 at90s4433 __AVR_AT90S4433__
avr2 at90s4434 __AVR_AT90S4434__
avr2 at90s8515 __AVR_AT90S8515__
avr2 at90c8534 __AVR_AT90C8534__
avr2 at90s8535 __AVR_AT90S8535__
avr2 at86rf401 __AVR_AT86RF401__
avr2 attiny13 __AVR_ATtiny13__
avr2 attiny2313 __AVR_ATtiny2313__
avr3 atmega103 __AVR_ATmega103__
avr3 atmega603 __AVR_ATmega603__
avr3 at43usb320 __AVR_AT43USB320__
avr3 at43usb355 __AVR_AT43USB355__
avr3 at76c711 __AVR_AT76C711__
avr4 atmega48 __AVR_ATmega48__
avr4 atmega8 __AVR_ATmega8__
avr4 atmega8515 __AVR_ATmega8515__
avr4 atmega8535 __AVR_ATmega8535__
avr4 atmega88 __AVR_ATmega88__
avr4 at90pwm2 __AVR_AT90PWM2__
avr4 at90pwm3 __AVR_AT90PWM3__
avr5 at90can32 __AVR_AT90CAN32__
avr5 at90can64 __AVR_AT90CAN64__
avr5 at90can128 __AVR_AT90CAN128__
avr5 at90usb646 __AVR_AT90USB646__
avr5 at90usb647 __AVR_AT90USB647__
avr5 at90usb1286 __AVR_AT90USB1286__

72
avr5 at90usb1287 __AVR_AT90USB1287__
avr5 atmega128 __AVR_ATmega128__
avr5 atmega1280 __AVR_ATmega1280__
avr5 atmega1281 __AVR_ATmega1281__
avr5 atmega16 __AVR_ATmega16__
avr5 atmega161 __AVR_ATmega161__
avr5 atmega162 __AVR_ATmega162__
avr5 atmega163 __AVR_ATmega163__
avr5 atmega164p __AVR_ATmega164P__
avr5 atmega165 __AVR_ATmega165__
avr5 atmega168 __AVR_ATmega168__
avr5 atmega169 __AVR_ATmega169__
avr5 atmega32 __AVR_ATmega32__
avr5 atmega323 __AVR_ATmega323__
avr5 atmega324p __AVR_ATmega324P__
avr5 atmega325 __AVR_ATmega325__
avr5 atmega3250 __AVR_ATmega3250__
avr5 atmega329 __AVR_ATmega329__
avr5 atmega3290 __AVR_ATmega3290__
avr5 atmega406 __AVR_ATmega406__
avr5 atmega64 __AVR_ATmega64__
avr5 atmega640 __AVR_ATmega640__
avr5 atmega644 __AVR_ATmega644__
avr5 atmega644p __AVR_ATmega644P__
avr5 atmega645 __AVR_ATmega645__
avr5 atmega6450 __AVR_ATmega6450__
avr5 atmega649 __AVR_ATmega649__
avr5 atmega6490 __AVR_ATmega6490__
avr5 at94k __AVR_AT94K__

-mint8
Domyślnie typ int jest 16 bitowy. Opcja ta przestawia typ int na typ 8 bitowy. Z uwagi, że
nie jest on normalnie obsługiwany przez bibliotekę avr-libc, nie powinno się używać tej
opcji bez wyraźnej potrzeby.

-mno-interrupts}
Generuje kod, który zmienia wskaźnik stosu bez wyłączania przerwań. Normalnie, stan
rejestru SREG jest przechowywany w rejestrze tymczasowym, obsługa przerwań jest
zablokowana podczas zmiany wskaźnika stosu i jest odtwarzany stan rejestru SREG.

-mcall-prologues
Używa wywoływania podprogramów dla prologów i epilogów funkcji. Powoduje
oszczędzanie miejsca w pamięci programu, nieznacznie tylko zwiększając czas wykonania
funkcji.

-minit-stack=nnnn
Ustala wskaźnik stosu na nnnn. Domyślnie wskaźnik stosu jest symbolem __stack, który
jest utalony na RAMEND poprzez kod inicjujący program.

-mtiny-stack
Powoduje zmianę tylko mniej znaczących 8 bitów wskaźnika stosu.

-mno-tablejump
Nie generuje tablicy skoków. Domyślnie, tablica skoków jest używana do optymalizacji
instrukcji switch. Kiedy jest wyłączona, w to miejsce są wstawiane sekwencje

73
porównujące. Tablice skoków zwykle powodują szybsze wykonywanie programu, lecz w
pewnych przypadkach konstrukcja switch zawierająca wiele skoków do jednej np.
domyślnej etykiety może spowodować większe użycie pamięci programu.

-mshort-calls
Wymusza używanie instrukcji rjmp/rcall (ograniczony zakres adresowania) w
mikrokontrolerach z pamięcią flash o pojemności większej niż 8kB. W architekturach avr2
i avr4 (mniej niż 8 kB pamięci flash), jest zawsze w użyciu. Natomias architektury avr3 i
avr5, wywołania funcji i skoki to miejsc poza bieżącą funkcją powinny używać instrukcji
jmp/call które umożliwiają wykonywanie skoków w całym zakresie pamięci, ale zajmują
więcej miejsca w pamięci flash i dłużej się wykonują.

-mrtl
Wypisuje wewnętrzne wyniki kompilacji zwane "RTL" jako komentarze w generowany
kod asemblera. Przydatne podczas debugowania avr-gcc.

-msize
Wypisuje adres, rozmiar i inne dotyczące wyrażenia jako komentarze w generowany kod
asemblera. Przydatne podczas debugowania avr-gcc.

-mdeb
Wypisuje wiele informacji przydatnych do debugowania na wyjście strumienia błędu
(stderr).

-On
Poziom optymalizacji n. Zwiększanie n zwiększa poziom optymalizacji. Poziom
optymalizacji równy 0 jest równoznaczny z jej brakiem.

-Wa,assembler-options, -Wl,linker-options
Przenosi opcje do asemblera lub linkera.

-g
Generuje informacje dla debugera avr-gdb.

-c
Powoduje zatrzymanie po etapie asemblacji, wyniki są umieszczane w pliku z
rozszerzeniem .o.

-E
Powoduje zatrzymanie po etapie prekompilacji, wyniki są wypisywane na ekran.

-o nazwa
Powoduje zmianę nazwy programu wynikowego na podaną przez użytkownika; np.: avr-
gcc -o prog main.c, powoduje nadanie nazwy prog zamiast standardowej a.out.

-S
Powoduje zatrzymanie po etapie generowania kodu asemblera, wyniki są umieszczane w
pliku z rozszerzeniem .s. Opcją -o możemy podać inną nazwę (rozszerzenie).

-H

74
Wypisz nazwę każdego używanego pliku nagłówkowego.

-ansi
Tekst źródłowy musi być w pełni zgodny z normą ANSI języka C.

-traditional
Toleruje starsze konstrukcje języka C, z tzw. wersji języka K&R (opis znajdujesię w
książce autorów języka B.W.Kernighan'a i D.M.Ritchie'go)

-v
Wypisywanie (na standardowym wyjściu dla błędów), komend wywoływanych podczas
kolejnych etapów kompilacji. Wypisuje również numer wersji programu sterującego
kompilatorem, preprocesora oraz właściwego kompilatora.

-fsyntax-only
Sprawdź kod pod kątem błędów składniowych i nie rób nic poza tym.

-include file
Najpierw przetwarza plik file (np. kompiluj najpierw file).

-imacros file
Najpierw przetwarzaj plik file, wypisz wyniki na wyjście, zanim zaczniesz przetwarzać
resztę plików.

-idirafter dir
Dodaj katalog dir jako drugi katalog do przeszukiwania dla plików nagłówkowych. Jeśli
plik nagłówkowy nie został znaleziony w żadnym ze wskazanych wcześniej katalogów,
kompilator przeszukuje ten katalog.

-nostdinc
Nie szukaj w katalogu ze standardowymi plikami nagłówkowymi, szukaj tylko w
katalogach wskazanych przez `-I' i w katalogu bieżącym.

-I dir
Dodaje katalog dir do listy katalogów przeszukiwanych ze wzgledu na pliki nagłówkowe.

-L dir
Dodaje katalog dir do listy katalogów przeszukiwanych przy użyciu przełącznika `-l'.

-Dmacro
Użycie opcji jest równoznaczne z umieszczeniem linii #define makro na początku pliku
zawierającego tekst źródłowy.

-Dmacro=defn
Zdefiniuj makro macro jako defn.

-Umacro
Użycie opcji jest równoznaczne z umieszczeniem linii #undef makro na początku pliku
zawierającego tekst źródłowy.

75
-Wall
Wypisuje ostrzeżenia dla wszystkich sytuacji, które pretendują do konstrukcji, których
używania się nie poleca i których użycie jest proste do uniknięcia, nawet w połączeniu z
makrami.

-funsigned-char
Ustala domyślny typ zmiennych typu char na unsigned.

-funsigned-bitfields
Ustala domyślny typ pól bitowych, gdy brak deklaracji signed albo unsigned. Użycie opcji
-traditional włącza także -funsigned-bitfields, w przeciwnym razie domyślnie jest -fsigned-
bitfields tak jak typ int.

-Wstrict-prototypes
Ostrzega, jeśli jakaś funkcja jest zadeklarowana lub zdefiniowana bez określenia typów
argumentów.

-fpack-struct
"Pakuje" struktury powodując utworzenie mniejszego kodu.

AVR-GCC - opis funkcji biblioteki avr-libc


W tym artykule zostały zebrane opisy większości funkcji dostępnych z biblioteki avr-libgc.
Niestety może brakować opisów części funkcji, a część z nich może zniknąć w nowych
wersjach biblioteki ponieważ kompilator AVR-GCC i jego biblioteki podlegają ciągłym
zmianom.

Lista plików nagłówkowych


avr/crc16.h Obliczanie 16 bitowego CRC
avr/delay.h Funkcje opóźniające (w rozwoju)
avr/eeprom.h Funkcje dostępu do wewnętrznej pamięci EEPROM
avr/ina90.h Nagłówek dla kompatybilności z IAR C
avr/interrupt.h Funkcje obsługi przerwań
avr/io.h Włącza pozostałe nagłówki I/O
avr/io[MCU].h Definicje I/O dla różnych mikrokontrolerów AVR
avr/parity.h Obliczanie bitu parzystości
avr/pgmspace.h Funkcje dostępu do pamięci programu
avr/sfr_defs.h Makra dla peryferii
avr/signal.h Obsługa przerwań i sygnałów AVR
avr/sleep.h Zarządzanie poborem energii
avr/timer.h Funkcje dla licznika/czasomierza 0
avr/twi.h Obsługa TWI (i2c) w ATMega
avr/wdt.h Funkcje kontrolujące układ watchdoga

76
ctype.h Funkcje testujące wartości typów znakowych
errno.h Obsługa błędów
inttypes.h Definicje różnych typów całkowitych
math.h Różne funkcje matematyczne
setjmp.h Zawiera funkcje długich skoków (long jumps)
stdio.h Standardowa biblioteka wejścia/wyjscia
stdlib.h Rozmaite funkcje standardowe
string.h Funkcje operujące na łańcuchach

avr/crc16.h

Zawiera funkcje obliczające 16 bitowe CRC.

unsigned int _crc16_update(unsigned int __crc, unsigned char __data)


Oblicza 16 bitowe CRC według standardu CRC16 (x^16 + x^15 + x^2 + 1).

avr/delay.h

Zawiera proste funkcje wstrzymujące działanie programu na pewien czas.

void _delay_loop_1(unsigned char __count)


8 bitowy licznik, 3 cykle procesora na __count.

void _delay_loop_2(unsigned int __count)


16 bitowy licznik, 4 cykle procesora na __count.

void _delay_ms (double __ms)


Wstrzymuje działanie programu na __ms millisekund, używając _delay_loop_2(). Makro
F_CPU powinno zawierać częstotliwość zegara w hercach. Maksymalne możliwe
wstrzymanie to 262.14 ms / (F_CPU w MHz).

void _delay_us (double __us)


Wstrzymuje działanie programu na __us mikrosekund, używając _delay_loop_1(). Makro
F_CPU powinno zawierać częstotliwość zegara w hercach. Maksymalne możliwe
wstrzymanie to 768 us / (F_CPU w MHz).

avr/eeprom.h

Zawiera funkcje dostępu do wewnętrznej pamięci EEPROM.

int eeprom_is_ready()

77
Zwraca wartość różną od 0 jeżeli EEPROM jest gotowy na następną operację (bit EEWE
w rejestrze EECR jest równy 0).

unsigned char eeprom_read_byte(unsigned int *addr)


Czyta jeden bajt z EEPROMu spod adresu addr.

unsigned int eeprom_read_word(unsigned int *addr)


Czyta 16-bitowe słowo z EEPROMu spod adresu addr.

void eeprom_write_byte(unsigned int *addr, unsigned char val);


Zapisuje bajt val do EEPROMu pod adres addr.

void eeprom_read_block(void *buf, unsigned int *addr, size_t n);


Czyta blok o wielkości n bajtów z EEPROMu spod adresu addr do buf.
Makra dla kompatybilności z IAR C.

#define _EEPUT(addr, val) eeprom_wb(addr, val)


#define _EEGET(var, addr) (var) = eeprom_rb(addr)

avr/ina90.h

Ten plik nagłówkowy zawiera kilka funkcji i makr do łatwiejszego przenoszenia aplikacji
z kompilatora IAR C do avr-gcc. Jednak nie powinno się go używać do pisania aplikacji
„od początku”.

avr/interrupt.h

Zawiera funkcje obsługi przerwań.

sei()
Włącza przerwania. Makro.

cli()
Wyłącza przerwania. Makro.

void enable_external_int(unsigned char ints)


Wpisuje ints do rejestrów EIMSK lub GIMSK, w zależności, który rejestr zdefiniowany w
mikrokontrolerze: EIMSK lub GIMSK.

void timer_enable_int( unsigned char ints );


Wpisuje ints do rejestru TIMSK, jeżeli TIMSK jest zdefiniowany

78
avr/io.h

Włącza pliki nagłówkowe avr/sfr_defs.h oraz odpowiedni avr/io[MCU].h. Służy do


definiowania stałych specyficznych dla danego mikrokontrolera na podstawie parametru -
mmcu=typ_MCU przekazanego do kompilatora. Ten plik powinien być włączany w
każdym programie na mikrokontroler AVR.

avr/io[MCU].h

Definicje rejestrów I/O dla odpowiedniego typu mikrokontrolera, gdzie [MCU] jest
tekstem określającym typ w rodzaju 2313, 8515 itp. Zobacz do dokumentacji
mikrokontrolera. Tych plików nie należy włączać do pisanych programów – robi to za nas
avr/io.h na podstawie parametru -mmcu=typ_MCU przekazanego do kompilatora np. w
pliku makefile.

avr/parity.h

Zawiera definicje funkcji pomocnej w obliczaniu bitu parzystości lub nieparzystości.

parity_even_bit(val)

avr/pgmspace.h

Zawiera funkcje dostępu do danych znajdujących się w pamięci programu.

#define PGM_P const prog_char *


Służy do deklaracji zmiennej, która jest wskaźnikiem do łańcucha znaków w pamięci
programu.

#define PGM_VOID_P const prog_void *


Służy do deklaracji wskaźnika do dowolnego obiektu w pamięci programu.

#define PSTR(s) ({static char __c[] PROGMEM = (s); __c;})


Służy do deklaracji wskaźnika do łańcuha znaków w pamięci programu.

unsigned char __elpm_inline(unsigned long __addr) [static]


Służy do odczytania zawartości pamięci programu o adresie powyżej 64kB (ATmega103,
ATmega128). Jeżeli jest to możliwe, należy umieścić tablice ze stałymi poniżej granicy
64kB (jest to bardziej efektywne rozwiązanie).

void *memcpy_P(void* dest, PGM_VOID_P src, size_t n)

79
Kopiuje n znaków z jednego ciągu do drugiego. Jako wynik zwraca wskaźnik do dest. Jest
odpowiednikiem funkcji memcpy() z tą różnicą, że łańcuch src znajduje się w pamięci
programu.

int strcasecmp_P(const char* s1, PGM_P s2)


Porównuje s1 z s2, ignorując wielkość liter. Parametr s1 jest wskaźnikiem do łańcucha
znajdującego się w pamięci SRAM. Parametr s2 jest wskaźnikiem do łańcucha
znajdującego się w pamięci programu. Zwraca wartość mniejszą od 0 jeżeli s1 jest
mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2. Jest
odpowiednikiem funkcji strcasecmp() z tą różnicą, że łańcuch s2 znajduje się w pamięci
programu.

char *strcat_P(char* dest, PGM_P src)


Dołącza znaki jednego ciągu do drugiego. Jako wynik zwraca wskaźnik do dest. Jest
odpowiednikiem funkcji strcat() z tą różnicą, że łańcuch src znajduje się w pamięci
programu.

int strcmp_P(const char* s1, PGM_P s2)


Porównuje s1 z s2, uwzględniając wielkość liter. Parametr s1 jest wskaźnikiem do
łańcucha znajdującego się w pamięci SRAM. Parametr s2 jest wskaźnikiem do łańcucha
znajdującego się w pamięci programu. Zwraca wartość mniejszą od 0 jeżeli s1 jest
mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2. Jest
odpowiednikiem funkcji strcmp() z tą różnicą, że łańcuch s2 znajduje się w pamięci
programu.

char* strcpy_P(char* dest, PGM_P src)


Kopiuje src do dest. Jako wynik zwraca wskaźnik do dest. Jest odpowiednikiem funkcji
strcpy() z tą różnicą, że łańcuch src znajduje się w pamięci programu.

size_t strlen_P(PGM_P src)


Zwraca ilość znaków w src. Jest odpowiednikiem funkcji strlen() z tą różnicą, że łańcuch
src znajduje się w pamięci programu.

int strncasecmp_P(const char *s1, PGM_P s2, size_t n)


Porównuje pierwszych n znaków s1 z s2, ignorując wielkość liter. Parametr s1 jest
wskaźnikiem do łańcucha znajdującego się w pamięci SRAM. Parametr s2 jest
wskaźnikiem do łańcucha znajdującego się w pamięci programu. Parametr n określa ile
znaków ma być porównywanych. Zwraca wartość mniejszą od 0 jeżeli pierwsze n znaków
s1 jest mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2. Jest
odpowiednikiem funkcji strncasecmp() z tą różnicą, że łańcuch s2 znajduje się w pamięci
programu.

int strncmp_P(const char* s1, PGM_P s2, size_t n)


Porównuje pierwszych n znaków s1 z s2, uwzględniając wielkość liter. Parametr s1 jest
wskaźnikiem do łańcucha znajdującego się w pamięci SRAM. Parametr s2 jest
wskaźnikiem do łańcucha znajdującego się w pamięci programu. Parametr n określa ile
znaków ma być porównywanych. Zwraca wartość mniejszą od 0 jeżeli pierwsze n znaków
s1 jest mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2. Jest
odpowiednikiem funkcji strncmp() z tą różnicą, że łańcuch s2 znajduje się w pamięci
programu.

80
char* strncpy_P(char* dest, PGM_P src, size_t n)
Kopiuje nie więcej niż n bajtów z src do dest. Jako wynik zwraca wskaźnik do dest. Jest
odpowiednikiem funkcji strncpy() z tą różnicą, że łańcuch src znajduje się w pamięci
programu.

avr/sfr_defs.h

Zawiera wiele bardzo przydatnych makr dla dostępu do portów wejścia/wyjścia.

_BV(x)
Zwraca wartość bitu (bit value) x. Zdefiniowany jako (1 << x). Makro.

inb(sfr)
Czyta bajt z sfr. Makro.

outb(sfr, val)
Wpisuje val do sfr. Makro. Odwrotnie jak inb(sfr).

cbi(sfr, bit)
Kasuje bit w sfr. Makro.

sbi(sfr, bit)
Ustawia bit w sfr. Makro.

bit_is_set(sfr, bit)
Zwraca wartość różną od 0, jeżeli bit w sfr jest ustawiony, w przeciwnym wypadku 0.
Makro.

bit_is_clear(sfr, bit)
Zwraca wartość różną od 0, jeżeli bit w sfr jest skasowany, w przeciwnym wypadku 0.
Makro.

loop_until_bit_ist_set(sfr, bit)
Wstrzymuje działanie programu (wykonuje pętlę) dopóki bit w sfr jest ustawiony. Makro.

loop_until_bit_is_clear(sfr, bit)
Wstrzymuje działanie programu (wykonuje pętlę) dopóki bit w sfr jest skasowany. Makro.

avr/signal.h

Definiuje nazwy uchwytów dla przerwań, które znajdują się na początku pamięci FLASH.
Oto one:

SIG_INTERRUPT0 do SIG_INTERRUPT7
81
Uchwyty funkcji obsługi przerwań zewnętrznych od 0 do 7. Przerwania o numerach
większych od 1 są dostępne tylko w niektórych układach ATmega.

SIG_OUTPUT_COMPARE2
Uchwyt funkcji obsługi przerwania od porównania licznika 2.

SIG_OVERFLOW2
Uchwyt funkcji obsługi przerwania do przepełnienia licznika 2.

SIG_INPUT_CAPTURE1
Uchwyt funkcji obsługi przerwania od przechwytywania licznika 1.

SIG_OUTPUT_COMPARE1A
Uchwyt funkcji obsługi przerwania od porównania licznika 1 (A).

SIG_OUTPUT_COMPARE1B
Uchwyt funkcji obsługi przerwania od porównania licznika 1 (B).

SIG_OVERFLOW1
Uchwyt funkcji obsługi przerwania do przepełnienia licznika 1.

SIG_OUTPUT_COMPARE0
Uchwyt funkcji obsługi przerwania od porównania licznika 0.

SIG_OVERFLOW0
Uchwyt funkcji obsługi przerwania do przepełnienia licznika 0.

SIG_SPI
Uchwyt funkcji obsługi przerwania SPI.

SIG_UART_RECV
Uchwyt funkcji obsługi przerwania UART(0) – odbiór znaku.

SIG_UART1_RECV
Uchwyt funkcji obsługi przerwania UART1 – odbiór znaku. UART1 jest dostępny w
niektórych układach ATmega.

SIG_UART_DATA
Uchwyt funkcji obsługi przerwania UART(0) – pusty rejestr danych.

SIG_UART1_DATA
Uchwyt funkcji obsługi przerwania UART1 – pusty rejestr danych. UART1 jest dostępny
tylko w niektórych układach ATmega.

SIG_UART_TRANS
Uchwyt funkcji obsługi przerwania UART(0) – zakończenie transmisji.

SIG_UART1_TRANS
Uchwyt funkcji obsługi przerwania UART1 – zakończenie transmisji. UART1 jest
dostępny tylko w niektórych układach ATmega.

82
SIG_ADC
Uchwyt funkcji obsługi przerwania ADC – zakończenie przetwarzania.

SIG_EEPROM
Uchwyt funkcji obsługi przerwania EEPROM – gotowość.

SIG_COMPARATOR
Uchwyt funkcji obsługi przerwania z komparatora analogowego.

SIGNAL(signame)
Używany do definicji uchwytu sygnału dla signame.

INTERRUPT(signame)
Używany do definicji uchwytu przerwania dla signame.
Dla uchwytu zdefiniowanego w SIGNAL(), dodatkowe przerwania są bezwarunkowo
zabronione, natomiast w uchwycie INTERRUPT(), pierwszą (bezwarunkowo) instrukcją
jest sei, i występujące w tym czasie przerwania mogą być obsługiwane.

avr/sleep.h

Zawiera definicje i funkcje pomocne w zarządzaniu poborem energii.

#define SLEEP_MODE_ADC
Redukcja zakłóceń z przetwornika analogowo/cyfrowego.

#define SLEEP_MODE_EXT_STANDBY
Rozszerzony tryb gotowości (Extended Standby).

#define SLEEP_MODE_IDLE
Tryb bezczynny (Idle).

#define SLEEP_MODE_PWR_DOWN
Wyłączenie zasilania (Power Down).

#define SLEEP_MODE_PWR_SAVE
Oszczędzanie zasilania (Power Save).

#define SLEEP_MODE_STANDBY
Tryb gotowości (Standby).

void set_sleep_mode(uint8_t mode)


Ustawia bity w rejestrze MCUCR aby wybrać odpowiedni tryb uśpienia.

void sleep_mode(void)
Wprowadza kontroler w tryb uśpienia na podstawie wcześniej wybranego trybu za pomocą
funkcji set_sleep_mode().

83
Aby uzyskać więcej informacji, zobacz do dokumentacji mikrokontrolera.

avr/timer.h

Zawiera definicje funkcji kontrolujących działanie licznika/czasomierza 0.

void timer0_source(unsigned int src)


Wpisuje src w rejestr TCCR0. Wartość src może przyjmować następujące wartości
symboliczne:
enum {
STOP = 0,
CK = 1,
CK8 = 2,
CK64 = 3,
CK256 = 4,
CK1024 = 5,
T0_FALLING_EDGE = 6,
T0_RISING_EDGE = 7
};

void timer0_stop()
Zatrzymuje Timer 0 poprzez wyzerowanie rejestru TCNT0.

void timer0_start()
Startuje Timer 0 poprzez wpisanie 1 w rejestr TCNT0.

avr/twi.h

Definiuje kilka stałych dla obsługa magistrali TWI (i2c) w ATMega.

avr/wdt.h

Zawiera definicje i funkcje pomocne w używaniu układu watchdoga.

wdt_reset()
Powoduje kasowanie czasomierza układu Watchdog.

wdt_enable(timeout)
Ustawia odpowiedni timeout i uruchamia układ watchdoga. Zobacz do dokumentacji
Atmel AVR. Wartość timeout może przyjmować jedną z predefiniowanych wartości:
WDTO_15MS
WDTO_30MS
WDTO_60MS

84
WDTO_250MS
WDTO_500MS
WDTO_1S
WDTO_2S

wdt_disable()
Wyłącza układ watchdoga.

ctype.h

Zawiera definicje funkcji testujących i zamieniających typy znakowe.

int isalnum(int __c);


Zwraca 1 jeżeli __c jest cyfrą lub literą, w przeciwnym wypadku 0.

int isalpha(int __c);


Zwraca 1 jeżeli __c jest literą, w przeciwnym wypadku 0.

int isascii(int __c);


Zwraca 1 jeżeli __c zawiera się w 7 bitowym ASCII, w przeciwnym wypadku 0.

int iscntrl(int __c);


Zwraca 1 jeżeli __c jest znakiem kontrolnym, w przeciwnym wypadku 0.

int isdigit(int __c);


Zwraca 1 jeżeli __c jest cyfrą, w przeciwnym wypadku 0.

int isgraph(int __c);


Zwraca 1 jeżeli __c jest „drukowalne” (z wyjątkiem spacji), w przeciwnym wypadku 0.

int islower(int __c);


Zwraca 1 jeżeli __c jest małą literą alfabetu, w przeciwnym wypadku 0.

int isprint(int __c);


Zwraca 1 jeżeli __c jest „drukowalne” (ze spacją), w przeciwnym wypadku 0.

int ispunct(int __c);


Zwraca 1 jeżeli __c jest znakiem interpunkcyjnym, w przeciwnym wypadku 0.

int isspace(int __c);


Zwraca 1 jeżeli __c jest spacją lub '\n', '\f', '\r', '\t', '\v', w przeciwnym wypadku 0.

int isupper(int __c);


Zwraca 1 jeżeli __c jest dużym znakiem alfanumerycznym, w przeciwnym wypadku 0.

int isxdigit(int __c);


Zwraca 1 jeżeli __c jest cyfrą szesnastkową (0-9 lub A-F), w przeciwnym wypadku 0.

85
int toascii(int __c);
Zamienia __c na 7 bitowy znak ASCII.

int tolower(int __c);


Zamienia __c na małą literę.

int toupper(int __c);


Zamienia __c na dużą literę.

errno.h

Obsługa błędów.

int errno;
Przechowuje systemowy kod błędu

inttypes.h

Definiuje typy danych całkowitych.

typedef signed char int8_t;


typedef unsigned char uint8_t;
Typy 8-bitowe

typedef int int16_t;


typedef unsigned int uint16_t;
Typy 16-bitowe

typedef long int32_t;


typedef unsigned long uint32_t;
Typy 32-bitowe

typedef long long int64_t;


typedef unsigned long long uint64_t;
Typy 64-bitowe

typedef int16_t intptr_t;


typedef uint16_t uintptr_t;
Typy wskaźnikowe
Należy świadomie używać opcji kompilatora -mint8 – nie będą wtedy dostępne typy 32 i
64 bitowe.

86
math.h

M_PI = 3.141592653589793238462643
Liczba PI.

M_SQRT2 = 1.4142135623730950488016887
Pierw. kwadr. z 2

double cos(double x)
Zwraca cosinus z x.

double fabs(double x)
Zwraca absolutną wartość z x.

double fmod(double x, double y)


Zwraca zmiennoprzecinkową resztę z dzielenia x/y.

double modf(double x, double *iptr)


Zwraca część ułamkową z x i zapamiętuje część całkowitą w *iptr.

double sin(double x)
Zwraca sinus z x.

double sqrt(double x)
Zwraca pierwiastek kwadratowy z x.

double tan(double x)
Zwraca tangens z x.

double floor(double x)
Zwraca większą wartość całkowitą mniejszą niż x.

double ceil(double x)
Zwraca mniejszą wartość całkowitą większą niż x.

double frexp(double x, int *exp)


Rozdziela x na znormalizowany ułamek, który jest zwracany, i na wykładnik, który jest
zapamiętany w *exp.

double ldexp(double x, int exp);


Zwraca x^exp.

double exp(double x)
Zwraca e^x.

double cosh(double x)
Zwraca cosinus hiperboliczny z x.

double sinh(double x)
Zwraca sinus hiperboliczny z x.

87
double tanh(double x)
Zwraca tangens hiperboliczny z x.

double acos(double x)
Zwraca arcus cosinus z x.

double asin(double x)
Zwraca arcus sinus z x.

double atan(double x)
Zwraca arcus tangens z x. Wyjście między -PI/2 i PI/2 (włącznie).

double atan2(double x, double y)


Zwraca arcus tangens z x/y. Zwraca uwagę na znak argumentów. Wyjście pomiędzy -PI a
PI (włącznie).

double log(double x)
Zwraca logarytm naturalny z x.

double log10(double x)
Zwraca logarytm dziesiętny z x.

double pow(double x, double y)


Zwraca x^y.

double strtod(const char *s, char **endptr)


Zamienia łańcuch ASCII na liczbę typu double.

double square(double x)
Zwraca x2.

double inverse(double x)
Zwraca 1/x.

UWAGA. Aby skorzystać z tych funkcji należy włączyć do projektu bibliotekę libm.a.

setjmp.h

int setjmp(jmp_buf env)


Deklaruje długi skok do miejsca przeznaczenia wykonanego przez longjmp().

void longjmp(jmp_buf env, int val)


Wykonuje długi skok do pozycji wcześniej zdefiniowanej przez setjmp(env), która
powinna zwrócić val.

88
stdlib.h

Definiuje następujące typy:

typedef struct {
int quot;
int rem;
} div_t;
typedef struct {
long quot;
long rem;
} ldiv_t;
typedef int (*__compar_fn_t)(const void *, const void *);
Używane w funkcjach porównujących np. qsort().

void abort();
Skutecznie przerywa wykonywanie programu przez wprowadzenie MCU w nieskończoną
pętlę.

long labs( long x );


Zwraca absolutną wartość x typu long.

div_t div( int x, int y );


Dzieli x przez y i zwraca rezultat (iloraz i resztę) w strukturze div_t.

ldiv_t ldiv( long x, long y );


Dzieli x przez y i zwraca rezultat (iloraz i resztę) w strukturze ldiv_t.

void qsort(void *base, size_t nmemb, size_t size, __compar_fn_t compar);


Sortuje tablicę base z nmemb elementami rozmiaru size, używając funkcji porównującej
compar.

long strtol(const char *nptr, char **endptr, int base);


Zamienia łańcuch nptr według podstawy base na liczbę typu long.

unsigned long strtoul(const char *nptr, char **endptr, int base);


Zamienia łańcuch nptr według podstawy base na liczbę typu unsigned long.

long atol( char *p );


Zamienia łańcuch p na liczbę typu long.

int atoi( char *p );


Zamienia łańcuch p na liczbę typu int.

void *malloc( size_t size );


Alokuje size bajtów pamięci i zwraca wskaźnik do niego.

void free( void *ptr );

89
Zwalnia pamięć wskazywaną przez ptr, która była wcześniej zaalokowana funkcją
malloc().

char *itoa( int value, char *string, int radix );


Zamienia liczbę całkowitą na łańcuch. Nie jest kompatybilna z ANSI C, lecz może być
użyteczna.

string.h

void *memcpy( void *to, void *from, size_t n );


Kopiuje n bajtów z from do to.

void *memmove( void *to, void *from, size_t n );


Kopiuje n bajtów z from do to, gwarantując poprawność zachowania dla nakładających się
łańcuchów.

void *memset( void *s, int c, size_t n );


Ustawia n bajtów z s na wartość c.

int memcmp( const void *s1, const void *s2, size_t n );


Porównuje n bajtów między s1 a s2.

void *memchr( void *s, char c, size_t n );


Zwraca wskaźnik do pierwszego wystąpienia c w pierwszych n bajtach s.

size_t strlen( char *s );


Zwraca długość łańcucha s.

char *strcpy( char *dest, char *src );


Kopiuje src do dest. Jako wynik zwraca wskaźnik do dest.

char *strncpy( char *dest, char *src, size_t n );


Kopiuje nie więcej niż n bajtów z src do dest. Jako wynik zwraca wskaźnik do dest.

char *strcat( char *dest, char *src );


Dołącza src do dest. Jako wynik zwraca wskaźnik do dest.

char *strncat( char *dest, char *src, size_t n );


Dołącza nie więcej niż n bajtów z src do dest. Jako wynik zwraca wskaźnik do dest.

int strcmp( const char *s1, const char *s2 );


Porównuje s1 z s2, uwzględniając wielkość liter. Zwraca wartość mniejszą od 0 jeżeli s1
jest mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2.

int strncmp( const char *s1, const char* s2, size_t n );


Porównuje pierwszych n znaków s1 z s2, uwzględniając wielkość liter. Parametr n określa
ile znaków ma być porównywanych. Zwraca wartość mniejszą od 0 jeżeli pierwsze n

90
znaków s1 jest mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe
od s2.

strdupa( s );
Duplikuje s, zwracając identyczny łańcuch. Makro.

strndupa( s, n );
Zwraca zaalokowaną kopię n batów z s. Makro.

char *strchr( const char *s, int c );


Zwraca wskaźnik do pierwszego wystąpienia c w s.

char *strrchr( const char *s, int c );


Zwraca wskaźnik do ostatniego wystąpienia c w s.

size_t strnlen( const char *s, size_t maxlen );


Zwraca długość łańcucha s, ale nie więcej niż maxlen.

void *memccpy(void *dest, const void *src, int c, size_t n);


Kopiuje nie więcej niż n bajtów z src do dest dopóki zostanie znaleziony c.

int strcasecmp(const char *s1, const char *s2);


Porównuje s1 z s2, ignorując wielkość liter. Zwraca wartość mniejszą od 0 jeżeli s1 jest
mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2.

char *strlwr(char *s);


Zamienia wszystkie duże litery w łańcuchu s na małe.

int strncasecmp(const char *s1, const char *s2, size_t n);


Porównuje n bajtów z s1 i s2, ignorując wielkość znaków.

char *strrev(char *s1);


Odwraca kolejność znaków w s1.

char *strstr(const char *haystack, const char *needle);


Znajduje needle w haystack, i zwraca wskaźnik do niego.

char *strupr(char *s);


Zamienia wszystkie małe litery w łańcuchu s na duże.

AVR-GCC - kompilacja środowiska ze źródeł


Opis ten będzie bardzo ogólny gdyż dotyczy bardzo wielu systemów operacyjnych i nie
sposób tu zawrzeć wszystkich możliwych opcji. Jednak myślę, że będzie pomocny
osobom, które podejmą trud samodzielnego skompilowania środowiska.
Na początku należy zgromadzić wersje źródłowe pakietów: binutils, gcc-core, avr-libc.
Wszystkie można pobrać z bardzo z internetu. Aby dokonać kompilacji na wybraną przez
siebie platformę należy dysponować kompilatorem GCC - większość dystrybucji systemu
Linux jest w niego domyślnie wyposażona. Pracując w systemie MS Windows mamy do

91
wyboru dwa środowiska: "Cygwin" i "MinGW". "Cygwin" jest środowiskiem
kompatybilnym z systemami unixowymi (standard Posix) natomiast "MinGW" jest tzw.
"minimalistyczną" wersją programów GNU dostosowaną specjalnie do systemu MS
Windows (nazwa MinGW pochodzi od: Minimalist GNU for Windows).
Opis instalacji i używania powyższych i innych środowisk zostanie pominięty - polecam
skorzystanie z bogatej bazy wiedzy znajdującej się w internecie.

Pakiet binutils

W pierwszej kolejności należy skompilować pakiet binutils. Ponieważ pliki źródłowe są


zarchiwizowane za pomocą programu tar i spakowane programem bzip2 (rozszerzenie
.bz2) lub gzip (rozszerzenie .gz) należy je najpierw rozpakować za pomocą polecenia:

bzip2 -dc binutils*.bz2 | tar xvf -

Jeżeli archiwum ma rozszerzenie .gz to zmieniamy powyższe polecenie bzip2 na gzip


(reszta pozostaje bez zmian). Następnie przechodzimy do utworzonego w wyniku
wykonania powyższego polecenia katalogu binutils*:

cd binutils*

i konfigurujemy programy do pracy z kontrolerami AVR:

configure --target=avr --prefix=/avrgcc --disable-nls

opcja --prefix oznacza ścieżkę, w której będą instalowane programy jeśli jej nie podamy
zostanie użyta domyślna (zazwyczaj będzie to /usr/local), opcja --disable-nls oznacza
wyłączenie wsparcia dla innych wersji językowych. Po wykonaniu powyższej instrukcji
zostanie utworzony plik make file i będzie można przeprowadzić właściwą kompilację
poprzez wydanie polecenia:

make

Gdy kompilacja zostanie zakończona należy pakiet zainstalować czyli umieścić pliki
wynikowe w miejscu podanym przez opcję -prefix skryptu configure. Wydajemy
polecenie:

> make install

po przejściu do katalogu, wybranym opcją -prefix (tu: /avrgcc) i wylistowaniu jego


zawartości zobaczymy tam podkatalog bin.

Pakiet gcc-core

Po udanej kompilacji i instalacji pakietu binutils możemy skompilować właściwy


kompilator odbywa się to analogicznie. Rozpakowujemy archiwum ze źródłami:

bzip2 -dc gcc-core*.bz2 | tar xvf -

konfigurujemy źródła do pracy z kontrolerami AVR:

92
configure --target=avr --prefix=/avrgcc --disable-nls --enable-
languages=c

opcja --enable-languages=c oznacza, że kompilator będzie w stanie kompilować jedynie


programy w języku C. Pozostałe opcje zostały omówione w opisie kompilacji pakietu
binutils. Po wykonaniu polecenia zostanie utworzony plik makefile i będzie można
przeprowadzić właściwą kompilację poprzez wywołanie polecenia:

make

Gdy kompilacja zostanie zakończona należy pakiet zainstalować czyli umieścić pliki
wynikowe
w miejscu podanym przez opcję -prefix skryptu configure. Wydajemy polecenie:

make install

Pakiet avr-libc

Jest to zestaw bibliotek standardowych do kompilatora avr-gcc. Do ich kompilacji jest


niezbędny skompilowany i działający kompilator avr-gcc w związku z czym muszą być
zainstalowane pakiety -avr-binutils i avr-gcc oraz dodana do systemu ścieżka poszukiwań
odpowiednia do opcji --prefix w programie configure uzupełniona o katalog bin np.
/avrgcc/bin. Rozpakowujemy archiwum ze źródłami:

gzip -dc avr-libc*.gz | tar xvf -

konfigurujemy ścieżkę do programów wynikowych za pomocą polecenia:

doconf --prefix=/avrgcc

zostanie utworzony plik makefile i będzie można przeprowadzić właściwą kompilację


poprzez wywołanie:

domake

Gdy kompilacja zostanie zakończona należy pakiet zainstalować czyli umieścić pliki
wynikowe
w miejscu podanym przez opcję -prefix skryptu doconf. Wydajemy polecenie:

domake install

Od tego momentu mamy do dyspozycji swoje własne środowisko kompilatora avr-gcc :)

93

You might also like