Podstawy AVR-GCC - mysinski.wieik.pk.edu.plmysinski.wieik.pk.edu.pl/UPCiMCU/Podstawy_AVRGCC.pdf ·...

93
1 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 ukladu........................................................................................ 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ń ukladu ...................................................... 20 Podciąganie wejścia do logicznej jedynki ........................................................................ 20 Programy przykladowe .................................................................................................... 21 AVR-GCC - port szeregowy .................................................................................................... 23 Inicjalizacja ...................................................................................................................... 24 Wysylanie znaku .............................................................................................................. 25 Odbiór znaku .................................................................................................................... 25 Program przykladowy ...................................................................................................... 26 AVR-GCC - pamięć SRAM ..................................................................................................... 28 Operacje na zmiennych .................................................................................................... 29 Program przykladowy ...................................................................................................... 30 AVR-GCC - pamięć programu (FLASH) ................................................................................ 32 Najważniejsze funkcje zawarte w pliku avr/pgmspace.h................................................. 33 Tworzenie stalych w pamięci programu .......................................................................... 33 Czytanie stalych z pamięci programu .............................................................................. 33 Tworzenie tablic w pamięci programu ............................................................................. 33 Czytanie wartości z tablic w pamięci programu .............................................................. 33 Tworzenie lańcucha znaków ............................................................................................ 33 Czytanie lańcucha znaków ............................................................................................... 34 Program przykladowy ...................................................................................................... 34 AVR-GCC - pamięć EEPROM ................................................................................................ 36 Najważniejsze funkcje zawarte w pliku avr/eeprom.h..................................................... 36

Transcript of Podstawy AVR-GCC - mysinski.wieik.pk.edu.plmysinski.wieik.pk.edu.pl/UPCiMCU/Podstawy_AVRGCC.pdf ·...

1

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

2

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

3

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

4

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.

5

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;

6

• 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:

7

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

8

-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)

9

#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

10

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:

11

@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.

12

%.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.

13

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:

14

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:

15

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

16

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.

17

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);

18

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

19

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.

20

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.

21

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)

22

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

23

}

}

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 -

24

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)

25

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

26

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

27

#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

}

28

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

29

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

30

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

31

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

32

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:

33

• 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:

34

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);

}

35

}

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

36

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

37

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 ->

"));

38

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ń

39

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)

40

{

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,

41

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ą

42

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

43

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

44

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

45

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.

46

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

47

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

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

48

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

49

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)

50

#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

51

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

52

#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

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

53

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};

54

// 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

55

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

{

56

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()

57

#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

58

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

59

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)

{

60

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

61

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

62

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

63

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

64

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

65

#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

66

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

67

#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

}

}

68

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

69

#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))

70

{ // 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

71

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

72

• 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__

73

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

74

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

75

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.

76

-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

77

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()

78

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

79

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)

80

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.

81

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

82

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.

83

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().

84

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

85

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.

86

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.

87

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.

88

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.

89

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 );

90

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

91

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

92

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:

93

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 :)