Podstawy AVR-GCC

Transkrypt

Podstawy AVR-GCC
Podstawy AVR-GCC
Zawartość
AVR-GCC - wstęp ..................................................................................................................... 4
AVR-GCC - dystrybucja WinAVR............................................................................................ 4
Praca z kompilatorem ......................................................................................................... 5
AVR-GCC - kompilator ............................................................................................................. 5
AVR-GCC - kompilacja prostych programów........................................................................... 6
AVR-GCC - program make........................................................................................................ 7
Plik sterujący (makefile) .................................................................................................... 8
AVR-GCC - programowanie układu ........................................................................................ 13
Programator AVRprog (AVR910) ................................................................................... 13
Programator STK200 ....................................................................................................... 14
Oprogramowanie .............................................................................................................. 15
AVR-GCC - dostęp do zasobów mikrokontrolera ................................................................... 16
Rejestry specjalne ............................................................................................................. 16
Najważniejsze funkcje zawarte w pliku "avr/io.h" .......................................................... 16
AVR-GCC - wejście i wyjście binarne .................................................................................... 18
Rejestry............................................................................................................................. 19
Praktyczny sposób dostępu do wyprowadzeń układu ...................................................... 20
Podciąganie wejścia do logicznej jedynki ........................................................................ 20
Programy przykładowe .................................................................................................... 21
AVR-GCC - port szeregowy .................................................................................................... 23
Inicjalizacja ...................................................................................................................... 24
Wysyłanie znaku .............................................................................................................. 25
Odbiór znaku .................................................................................................................... 25
Program przykładowy ...................................................................................................... 26
AVR-GCC - pamięć SRAM ..................................................................................................... 28
Operacje na zmiennych .................................................................................................... 29
Program przykładowy ...................................................................................................... 30
AVR-GCC - pamięć programu (FLASH) ................................................................................ 32
Najważniejsze funkcje zawarte w pliku avr/pgmspace.h ................................................. 33
Tworzenie stałych w pamięci programu .......................................................................... 33
Czytanie stałych z pamięci programu .............................................................................. 33
Tworzenie tablic w pamięci programu ............................................................................. 33
Czytanie wartości z tablic w pamięci programu .............................................................. 33
Tworzenie łańcucha znaków ............................................................................................ 33
Czytanie łańcucha znaków ............................................................................................... 34
Program przykładowy ...................................................................................................... 34
AVR-GCC - pamięć EEPROM ................................................................................................ 36
Najważniejsze funkcje zawarte w pliku avr/eeprom.h..................................................... 36
1
Program przykładowy ...................................................................................................... 36
AVR-GCC - obsługa przerwań ................................................................................................ 38
Program przykładowy ...................................................................................................... 39
AVR-GCC - licznik/czasomierz TIMER 0 .............................................................................. 40
Tryb licznika .................................................................................................................... 41
Tryb czasomierza ............................................................................................................. 43
AVR-GCC - licznik/czasomierz TIMER 1 .............................................................................. 45
Tryb licznika .................................................................................................................... 45
Tryb czasomierza ............................................................................................................. 46
Tryb porównywania ......................................................................................................... 47
Tryb przechwytywania ..................................................................................................... 49
Tryb PWM - modulowana szerokość impulsu ................................................................. 50
AVR-GCC - licznik/czasomierz TIMER 2 .............................................................................. 52
Tryb czasomierza ................................................................................................................. 53
Program przykładowy ...................................................................................................... 53
Tryb porównywania ............................................................................................................. 56
Program przykładowy ...................................................................................................... 56
Program przykładowy ...................................................................................................... 58
AVR-GCC - komparator analogowy........................................................................................ 58
Programy przykładowe .................................................................................................... 59
AVR-GCC - przetwornik analogowo/cyfrowy ........................................................................ 61
Programy przykładowe .................................................................................................... 63
AVR-GCC - układ Watchdog .................................................................................................. 65
Najważniejsze funkcje zawarte w pliku avr/wdt.h ........................................................... 66
Program przykładowy ...................................................................................................... 66
AVR-GCC - tryby zmniejszonego poboru mocy ..................................................................... 68
Najważniejsze funkcje zawarte w pliku avr/sleep.h ........................................................ 68
Program przykładowy ...................................................................................................... 68
AVR-GCC - opcje wywoływania narzędzi .............................................................................. 71
AVR-GCC - opis funkcji biblioteki avr-libc ............................................................................ 76
Lista plików nagłówkowych ................................................................................................ 76
avr/crc16.h ............................................................................................................................ 77
avr/delay.h ............................................................................................................................ 77
avr/eeprom.h ......................................................................................................................... 77
avr/ina90.h ............................................................................................................................ 78
avr/interrupt.h ....................................................................................................................... 78
avr/io.h.................................................................................................................................. 79
avr/io[MCU].h ...................................................................................................................... 79
avr/parity.h ........................................................................................................................... 79
avr/pgmspace.h ..................................................................................................................... 79
avr/sfr_defs.h ........................................................................................................................ 81
avr/signal.h ........................................................................................................................... 81
avr/sleep.h ............................................................................................................................ 83
2
avr/timer.h ............................................................................................................................ 84
avr/twi.h................................................................................................................................ 84
avr/wdt.h ............................................................................................................................... 84
ctype.h .................................................................................................................................. 85
errno.h .................................................................................................................................. 86
inttypes.h .............................................................................................................................. 86
math.h ................................................................................................................................... 87
setjmp.h ................................................................................................................................ 88
stdlib.h .................................................................................................................................. 89
string.h .................................................................................................................................. 90
AVR-GCC - kompilacja środowiska ze źródeł ........................................................................ 91
Pakiet binutils ................................................................................................................... 92
Pakiet gcc-core ................................................................................................................. 92
Pakiet avr-libc .................................................................................................................. 93
3
AVR-GCC - wstęp
Kilka lat temu firma Atmel wprowadziła nową rodzinę 8 bitowych mikrokontrolerów
zbudowanych w architekturze RISC (o zredukowanej liście rozkazów), z których
większość wykonuje się w pojedynczym takcie zegara, posiadające bardzo rozbudowane
peryferia i łatwo programowalne w systemie docelowym (pięć przewodów łącznie z
zasilaniem). Wydawać by się mogło, że nowa rodzina mikrokontrolerów z zupełnie nową
niekompatybilną z innymi rodzinami listą rozkazów nie zostanie dobrze przyjęta na rynku.
Jednak stało się inaczej - dzięki mądrej polityce firmy Atmel, która udostępnia za darmo
narzędzia uruchomieniowe (np. AVR Studio), wielu entuzjastów mikrokontrolerów z
łatwością może testować swoje własne konstrukcje z tymi układami. Jednak wśród tych
narzędzi brakowało czegoś w dzisiejszych czasach ważnego a mianowicie kompilatora
języka wysokiego poziomu - nie zawsze mamy tyle czasu i chęci, żeby pisać każdy
program w asemblerze (ucząc się nowej listy rozkazów i technik programowania) i później
długo go testować. W tym momencie przychodzi na myśl zastosowanie języka, który byłby
blisko związany ze sprzętem (dostęp do rejestrów, peryferii, pamięci itp.) i jednocześnie
można byłoby w nim pisać programy na dosyć wysokim poziomie abstrakcji (bez
ukierunkowywania się na konkretny sprzęt). Tutaj na myśl przychodzi język C. Jest to
najbardziej rozpowszechniony język programowania komputerów. Spotyka się jego
implementacje na najróżniejsze maszyny: od prostych mikroprocesorów po potężne
superkomputery. Jednak jeżeli posiadamy już darmowe oprogramowanie do uruchamiania
na pewno pomyślimy sobie: czy nie ma również jakiegoś darmowego kompilatora języka
C na mikrokontrolery AVR? Okazuje się, że jest i to bardzo dobry. Wywodzi się z rodziny
GCC (GNU Compiler Collection), nazywa się AVR-GCC. Pierwotnie pracował pod
systemami unixowymi jak FreeBSD czy Linux (zresztą na takich systemach ciągle są
rozwijane jego nowe wersje), ale posiadając wersje źródłowe (bez problemu dostępne w
internecie na zasadzie "otwartego źródła") każdy chętny może "przenieść" go pod swój
ulubiony system operacyjny. Jednak przeciętnego użytkownika najbardziej interesuje
szybki efekt i tu z pomocą przychodzą tzw. "dystrybucje" zawierające kompilator z całym
szeregiem narzędzi i bibliotek.
AVR-GCC - dystrybucja WinAVR
Od końca 2002 roku w internecie dostępna jest "dystrybucja" WinAVR, którą stworzył
Eric
Weddington.
Najnowszą
wersję
można
pobrać
ze
strony
http://sourceforge.net/project/showfiles.php?group_id=68108. Jest ona przeznaczona do
pracy pod systemami MS Windows. Instalacja kompilatora odbywa się poprzez
uruchomienie programu instalacyjnego. Program instalacyjny wprowadza niezbędne
modyfikacje do systemu (np. aktualizuje domyślne ścieżki poszukiwań programów).
Właśnie ta dystrybucja została użyta do przetestowania wszystkich programów tutaj
zawartych.
4
Praca z kompilatorem
Ponieważ programy wchodzące w skład pakietu są uruchamiane i przyjmują argumenty z
linii poleceń, dobrym pomysłem - przynajmniej na początek będzie więc praca z tzw.
"wiersza poleceń". Uruchamiamy wiersz poleceń systemu operacyjnego. W systemach
Windows można to zrobić z menu "Start -> Programy -> Wiersz poleceń".
Uwaga: w systemach bazujących na MS-DOS takich jak Windows 9x "wiersz poleceń"
nazywany jest "Tryb MS-DOS".
Wydajemy polecenie:
avr-gcc -v
W rezultacie powinien się nam wyświetlić tekst zawierający m.in wersję kompilatora:
Reading specs from C:/WinAVR/bin/../lib/gcc/avr/3.4.1/specs
Configured with: ../gcc-3.4.1/configure --prefix=e:/avrdev/install
build=mingw
32 --host=mingw32 --target=avr --enable-languages=c,c++
Thread model: single
gcc version 3.4.1
--
Jeśli otrzymaliśmy tekst identyczny lub podobny do powyższego to wyszystko przebiegło
zgodnie z oczekiwaniami i możemy rozpocząć normalną pracę.
AVR-GCC - kompilator
Na kompilator AVR-GCC składa się wiele programów, są to:
• avr-addr2line - tłumaczy adresy w plikach wynikowych na numery linii w plikach
źródłowych
• avr-ar - tworzy, modyfikuje i wyciąga dane z archiwów
• avr-as - asembler
• avr-cpp - preprocessor C
• avr-gcc - kompilator
• avr-ld - linker (konsolidator)
• avr-nm - wyświetla nazwy symboliczne z plików wynikowych
• avr-objcopy - kopiuje i tłumaczy pliki objektowe (może "pięknie" deasemblować)
• avr-objdump - wyświetla różne informacje z plików objektowych
• avr-ranlib - generuje indeks do archiwum
• avr-size - wyświetla listę rozmiarów sekcji
• avr-strings - wyświetla łańcuchy ("drukowalne" znaki) z pliku
• avr-strip - usuwa symbole z plików wynikowych
• avr-libc - standardowa biblioteka funkcji dla kompilatora.
Ogólne określenie kompilator obejmuje cały zestaw narzędzi, bibliotek, plików
nagłówkowych, które łącznie pozwalają przetworzyć kod programu stworzony w C i
assemblerze do wynikowej postaci binarnego kodu maszynowego ładowanego do pamięci
flash mikrokontrolera. Przetwarzanie takie obejmuje następujące etapy:
• wykonanie na tekście programu dyrektyw preprocesora (np. wstawienie plików
(dyrektywy #include), zastąpienie fragmentów odpowiednimi definicjami (dyrektywy
#define));
• kompilacja modułów C do postaci plików assemblerowych;
5
asemblacja plików assemblerowych do postaci relokowalnego kodu maszynowego / tj.
bez przydzielonych konkretnych adresów etykiet, skoków itp.;
• konsolidacja (linkowanie) czyli połączenie przygotowanych plików relokowalnych,
dołączenie kodu wywoływanych funkcji bibliotecznych i ustawienie wszystkiego
kolejno w przestrzeni adresowej programu z przydzieleniem konkretnych wartości
adresów; wynikiem jest plik w formacie elf zawierający wszelkie potrzebne
informacje o projekcie - wynikowy kod programu, zawartość obszaru eeprom, dane
dla debuggera itd.;
• utworzenie na podstawie pliku elf potrzebnych wyjściowych plików w odpowiednich
formatach (zawartość flash i eeprom w postaci bin lub hex, plik debuggera coff dla
potrzeb symulacji);
• pobranie z pliku elf dodatkowych użytecznych informacji o projekcie, jak np. zajętość
poszczególnych obszarów pamięci.
Właściwy program avr-gcc wykonuje dwa pierwsze etapy - czyli przetworzenie kodu C na
kod assemblerowy z uwzględnieniem ustawionych opcji, optymalizacji itp. Dalej sprawą
zajmują się specjalizowane narzędzia (binutils): assembler avr-as, linker avr-ld i szereg
innych: avr-objcopy, avr-objdump, avr-ar itd.).
Oczywiście wszystko należy uruchomić w odpowiedniej kolejności, z podaniem
potrzebnych argumentów i wymaganych opcji. Tradycyjnie służy do tego bardzo
uniwersalny i wszechstronny manager procesów make.
Istnieją dwie drogi prowadzące do przygotowania działającego środowiska kompilatora:
• pobranie źródeł z internetu, kompilacja i instalacja oprogramowania
• pobranie z internetu "dystrybucji" zawierającej skompilowane i (zazwyczaj)
przetestowane programy oraz jego instalacja
Każda z tych metod ma swoje "za" i "przeciw". Pierwsza gwarantuje, że będziemy mogli
używać najnowszych wersji narzędzi i bibliotek. Ponadto możemy pracować z tymi
narzędziami pod kontrolą swojego ulubionego systemu operacyjnego. Jest to jednak
okupione znacznie dłuższą i bardziej czasochłonną metodą instalacji nie gwarantującą
zawsze pełnego sukcesu.
Stosując drugą metodę efekt osiągamy niemal natychmiastowo. Otrzymujemy środowisko
jakie posiada zapewne wiele innych osób na świecie. Okupione jest to jednak mniejszą
elastycznością w pozyskiwaniu nowych wersji programów i bibliotek. Jednak dla
praktyków, którzy chcą wykorzystywać narzędzia a nie je tworzyć jest to najlepsze
rozwiązanie.
•
AVR-GCC - kompilacja prostych programów
Proste programy tzn. takie, które składają się tylko z jednego pliku źródłowego można
kompilować bez użycia programu make. Zakładając, że wybraliśmy mikrokontroler
AT90S2313 a program źródłowy znajduje się w pliku o nazwie program.c. Wydajemy
polecenie:
avr-gcc -mmcu=at90s2313 program.c
powoduje ono skompilowanie ww. programu na wybrany typ mikrokontrolera - tu:
AT90S2313 i utworzenie pliku wynikowego a.out. Należy zaznaczyć, że jest to najprostsze
z możliwych wywołanie kompilatora - program wynikowy nie jest optymalizowany itp.
Niestety plik a.out nie nadaje się do zaprogramowania pamięci flash mikrokontrolera.
Należy więc z niego "wydobyć" potrzebną treść - zrobimy to za pomocą programu avrobjcopy potrafiącym manipulować plikami wynikowymi. Wydajemy polecenie:
6
avr-objcopy -O ihex -R .eeprom a.out program.hex
opcja -O ihex powoduje, że plik wyjściowy będzie miał format Intel HEX,
opcja -R .eeprom wyłącza z przetwarzania sekcję z zawartością przeznaczoną dla pamięci
EEPROM mikrokontrolera, a.out - plik wejściowy, program.hex - plik wyjściowy. Po
wykonaniu powyższego polecenia otrzymujemy plik .hex przeznaczony dla programatora
układu.
Aby w przyszłości za każdym razem nie wypisywać z klawiatury tylu komend, można
stworzyć plik wsadowy np. o nazwie avr-build.bat z następującą treścią:
@echo off
avr-gcc -mmcu=%1 %2.c
avr-objcopy -O ihex -R .eeprom a.out %2.hex
Wtedy kompilacja programu sprowadzi się do wydania polecenia:
avr-build at90s2313 program
Uwaga! istotna jest tutaj kolejność parametrów (najpierw typ mikrokontrolera).
Nazwa pliku z programem źródłowym musi być podana bez rozszerzenia nazwy.
AVR-GCC - program make
Kompilacja programu w języku C składa się z kilku faz. Pierwszą z nich jest
wygenerowanie tzw. pliku pośredniego (object file), zazwyczaj z rozszerzeniem ".o".
Następnie pliki pośrednie modułów i głównego programu są łączone za pomocą
konsolidatora (linker) w plik wykonywalny (.elf). Dla prostych programów te dwie
operacje mogą być wykonane w jednym kroku. Jednak plik .elf nie nadaje się do
bezpośredniego zaprogramowania mikrokontrolera (na dzień dzisiejszy nie są znane
programatory mikrokontrolerów "rozumiejące" ten format plików) dlatego należy jeszcze z
niego "wydobyć" dane w formacie obsługiwanym przez popularne programatory np. Intel
HEX.
Make jest programem podejmującym decyzję, które części dużego programu muszą zostać
zrekompilowanen i wywołującym polecenia służące do tego. Aby korzystać z programu
make potrzebujemy pliku zawierającego informacje o tym jak należy postępować w
przypadku zmian w plikach źródłowych i zależnościach między nimi. Domyślnie ten plik
nosi nazwę makefile.
Gdy zmieni się zawartość któregokolwiek pliku źródłowego musi on zostać
zrekompilowany, eżeli zmieni się zawartość któregoś z plików nagłówkowych bezpiecznie
jest rekompilować wszystkie źródła zawierające ten plik.
Kiedy którykolwiek z plików wynikowych (ang. object files; np. .o) się zmieni wtedy
trzeba ponownie skonsolidować całość.
Korzystanie z make sprowadza się wiec do stworzenia pliku makefile, który pokieruje
procesem kompilacji naszego programu.
Najczęściej używane opcje programu make:
-d włącza tryb szczegółowego śledzenia
-f
plik_sterujacy umożliwia stosowanie innych niż standardowe nazw
plików sterujących
-i powoduje ignorowanie błędów kompilacji (stosować z ostrożnością!)
-n powoduje wypisanie poleceń na ekran zamiast ich wykonania
7
-p
-s
powoduje wypisanie makrodefinicji i reguł transformacji
wyłącza wypisywanie treści polecenia przed jego wykonaniem
Opcje można ze sobą łączyć. Np.: polecenie {make -np} powoduje wypisanie wszystkich
reguł i makrodefinicji oraz ciągu poleceń jakie powinne być wykonane, aby uzyskać
żądany cel.
Jest to pomocne w sytuacji, gdy chcemy sprawdzić poprawność definicji zawartych w
pliku sterującym bez uruchamiania długotrwałej kompilacji wielu plików.
Plik sterujący (makefile)
Plik sterujący zawiera definicje relacji zależności, które mówią w jaki sposób i z jakich
elementów należy stworzyć cel (program, bibliotekę, lub plik obiektowy) i wskazują pliki,
których zmiany implikują wykonanie powtórnej kompilacji poszczególnych celów. Plik
sterujący może również zawierać zdefiniowane przez programistę reguły transformacji. W
pliku makefile znakiem komentarza jest znak # (hash) umieszczony na początku linii.
Poniżej przedstawiłem skonstruowany przezemnie plik makefile, w którym w zasadzie
wystarczy zmienić tylko nazwę programu TARGET i typ mikrokontrolera MCU.
# Nazwa pliku z funkcją main() - BEZ ROZSZERZENIA!
TARGET = program
# typ mikrokontrolera
MCU = atmega163
# Katalog z bibliotekami użytkownika
USRLIB
= ../../lib
# Lista plików źródłowych bibliotek w języku C
SRCLIB =
#include $(USRLIB)/conv/sources
#include $(USRLIB)/lcd/sources
#include $(USRLIB)/i2c/sources
#include $(USRLIB)/led7seg/sources
#include $(USRLIB)/kbd/sources
#include $(USRLIB)/delay/sources
#include $(USRLIB)/pcf8583/sources
#include $(USRLIB)/uart/sources
# Lista plików źródłowych w języku C
SRC = $(TARGET).c
# Lista plików źródłowych w asemblerze (rozszerzenie S - DUŻE S !)
ASRC =
# Format pliku wyjściowego (srec, ihex)
FORMAT = ihex
# Poziom optymalizacji (0, 1, 2, 3, s)
# (Uwaga: 3 nie zawsze jest najlepszym wyborem)
OPT = s
# Dodatkowe biblioteki
#
# Minimalna wersja printf
#LDFLAGS += -Wl,-u,vfprintf -lprintf_min
#
# Zmiennoprzecinkowa wersja printf (wymaga biblioteki matematycznej)
8
#LDFLAGS += -Wl,-u,vfprintf -lprintf_flt
#
# Biblioteka matematyczna
#LDFLAGS += -lm
include $(USRLIB)/avr_make
Zauważmy, ze na końcu dyrektywą
include $(USRLIB)/avr_make
jest włączany kolejny plik będący zbiorem reguł i makrodefinicji wspólnych dla każdego
projektu.
Oto jego listing:
# Przykładowy wspólny plik włączany do makefile dla avr-gcc
#
# Wywołanie programu make z linii komend:
# make clean
<> czyści projekt
# make
<> kompiluje projekt
# make install <> programuje układ za pomocą avrdude
# --------------------------------------------------------# Programowanie układu w systemie (usunąć komentaż z odpowiedniej linii)
PROG = stk200
#PROG = stk500
#PROG = avr910
# --------------------------------------------------------# Konwersja ELF na COFF dla symulatora (usunąć komentaż z odpowiedniej
linii)
# AVR Studio 3.5x i VMLAB do v3.9:
# COFFOUT = coff-avr
# AVR Studio 4.x i VMLAB od v3.10:
COFFOUT = coff-ext-avr
# --------------------------------------------------------# Opcje kompilatora
CFLAGS += -g
CFLAGS += -funsigned-char
CFLAGS += -funsigned-bitfields
CFLAGS += -fpack-struct
CFLAGS += -fshort-enums
CFLAGS += -Wall
CFLAGS += -Wstrict-prototypes
CFLAGS += -Wa,-ahlms=$(<:.c=.lst)
CFLAGS += -I$(USRLIB)
CFLAGS += -O$(OPT)
# Opcje asemblera
ASFLAGS = -Wa,-ahlms=$(<:.asm=.lst),-gstabs
# Opcje linkera
LDFLAGS += $(TARGET).a
LDFLAGS += -Wl,-Map=$(TARGET).map,--cref
# Definicje programów i komend.
CC = avr-gcc
OBJCOPY = avr-objcopy
OBJDUMP = avr-objdump
9
AR = avr-ar
REMOVE = rm -f
COPY = cp
# --------------------------------------------------------HEXSIZE = avr-size --target=ihex $(TARGET).hex
ELFSIZE = avr-size $(TARGET).elf
FINISH = echo Errors: none
BEGIN = echo -------- begin -------END = echo -------- end -------# --------------------------------------------------------# Definicje plików obiektowych
OBJ = $(SRC:.c=.o) $(ASRC:.asm=.o)
# --------------------------------------------------------# Definicje plików z wygenerowanymi listingami
LST = $(SRC:.c=.lst) $(ASRC:.asm=.lst)
# --------------------------------------------------------# Definicje plików obiektowych bibliotek
OBJLIB = $(SRCLIB:.c=.o) $(ASRCLIB:.asm=.o)
# --------------------------------------------------------# Scala wszystkie opcje i przełączniki. Dodaje typ procesora.
ALL_CFLAGS = -mmcu=$(MCU) -I. $(CFLAGS)
ALL_ASFLAGS = -mmcu=$(MCU) -I. -x assembler-with-cpp $(ASFLAGS)
# --------------------------------------------------------# Domyślne wywołanie
all: begin sizebefore \
$(TARGET).a \
$(TARGET).elf \
$(TARGET).lss \
$(TARGET).hex \
$(TARGET).eep \
$(TARGET).cof \
sizeafter finished end
# --------------------------------------------------------# Wyświetlanie tekstów.
begin:
@$(BEGIN)
finished:
@$(FINISH)
end:
@$(END)
# --------------------------------------------------------# Wyświetla rozmiar kodu wynikowego
sizebefore:
10
@if [ -f $(TARGET).elf ]; then echo Size before:; $(ELFSIZE);fi
sizeafter:
@if [ -f $(TARGET).elf ]; then echo Size after:; $(ELFSIZE);fi
# --------------------------------------------------------# Wyświetla informację na temat wersji kompilatora
gccversion :
$(CC) --version
# --------------------------------------------------------# Konwersja ELF na COFF dla symulacji w AVR Studio
COFFCONVERT=$(OBJCOPY) --debugging \
--change-section-address .data-0x800000 \
--change-section-address .bss-0x800000 \
--change-section-address .noinit-0x800000 \
--change-section-address .eeprom-0x810000
%.cof: %.elf
$(COFFCONVERT) -O $(COFFOUT) $< $@
# --------------------------------------------------------# Tworzy pliki wynikowe (.hex, .eep) z pliku ELF.
%.hex: %.elf
$(OBJCOPY) -O ihex -R .eeprom $< $@
%.eep: %.elf
$(OBJCOPY) -j .eeprom --set-section-flags=.eeprom="alloc,load"
change-section-lma .eeprom=0 -O ihex $< $@
--
# --------------------------------------------------------# Deasemblacja: Tworzy rozszerzony listing z pliku ELF.
%.lss: %.elf
$(OBJDUMP) -h -S $< > $@
# --------------------------------------------------------# Konsolidacja: tworzy plik ELF z plików objektowych.
%.elf: $(OBJ)
$(CC) -mmcu=$(MCU) $(OBJ) $(LDFLAGS) --output $@
# --------------------------------------------------------# Kompilacja: tworzy pliki objektowe z plików źródłowych C.
%.o : %.c
$(CC) -c $(ALL_CFLAGS) $< -o $@
# --------------------------------------------------------# Kompilacja: tworzy pliki asemblera z plików źródłowych C.
%.s : %.c
$(CC) -S $(ALL_CFLAGS) $< -o $@
# --------------------------------------------------------# Asemblacja: tworzy pliki objektowe z plików źródłowych asemblera.
11
%.o : %.asm
$(CC) -c $(ALL_ASFLAGS) $< -o $@
# --------------------------------------------------------# Tworzenie pliku biblioteki użytkownika dla projektu
%.a : $(OBJLIB)
$(AR) rc $@ $?
# --------------------------------------------------------# Czyści projekt.
clean: begin clean_list finished end
clean_list :
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(REMOVE)
$(SRC:.c=.s)
$(SRCLIB:.c=.s)
$(SRCLIB:.c=.lst)
$(OBJLIB)
$(OBJ)
$(LST)
$(TARGET).a
$(TARGET).hex
$(TARGET).eep
$(TARGET).obj
$(TARGET).cof
$(TARGET).elf
$(TARGET).map
$(TARGET).a90
$(TARGET).sym
$(TARGET).lnk
$(TARGET).lss
# --------------------------------------------------------# Programowanie układu w systemie
program: begin install end
install:
avrdude
-p
$(MCU)
eeprom:w:$(TARGET).eep
-c
$(PROG)
-U
flash:w:$(TARGET).hex
-U
# --------------------------------------------------------# Zależności
$(TARGET).a : $(CONFIG)
$(TARGET).o : $(TARGET).c $(CONFIG)
Czasem występuje potrzeba "wyczyszczenia" projektu z plików będących efektem złych
kompilacji czy też podmiany niektórych plików wchodzących w skład projektu. Jeżeli
wydamy teraz komendę
> make clean
to zobaczymy w efekcie, że wszystkie pliki z wyjątkiem źródeł i makefile zostały usunięte
- to bardzo przydatna opcja i korzystajmy z niej jeśli wydaje się nam, że program powinien
działać a nie działa zgodnie z naszymi założeniami - oczywiście nie jest to metoda na
wszystkie bolączki programisty ale czasami właśnie tu tkwi problem.
12
AVR-GCC - programowanie układu
Jeśli kompilacja projektu przebiegnie poprawnie, można plik wynikowy (ten z
rozszerzeniem .hex) wpisać do pamięci programu mikrokontrolera. Tutaj nie powinno być
problemu, gdyż opublikowano bardzo dużo układów programatorów - od bardzo prostych
składających się tylko z wtyczki do portu równoległego aż do bardziej skomplikowanych,
niekiedy zawierających wbudowany mikrokontroler sterujący jego pracą, ale za to
pozwalające na programowanie różnych funkcji (np. przeprogramowywanie tzw. fuse bits
- blokowanie programowania szeregowego, uaktywnianie wewnętrznego generatora RC,
przyśpieszanie startu mikrokontrolera po włączeniu zasilania itp.), których (zwykle) nie
można zaprogramować prostymi programatorami (tzw. szeregowymi). Jednak do
szybkiego uruchamiania oprogramowania najlepsze są właśnie te proste programatory
szeregowe (ISP - ang. in system programming - programowanie w systemie docelowym
bez wyjmowania układu z podstawki).
Firma ATMEL ustandaryzowała złącza służące do programowania układów. W chwili
obecnej są stosowane złącza 6 i 10 stykowe. Ich schematy przedstawiono na rysunku:
Programator AVRprog (AVR910)
Jest to programator szeregowy komunikujący się z komputerem za pomocą łącza RS-232,
jego program obsługi na PC wchodzi w skład AVR Studio - można go uruchomić z menu
Tools|AVR prog. Schemat przedstawiono na rysunku:
13
Opis (PDF) znajduje się na stronie WWW producenta. Niestety główną przeszkodą w jego
skonstruowaniu może być konieczność zaprogramowania mikrokontrolera AT90S1200,
który jest jego głównym elementem. Programator jest zasilany napięciem pobieranym z
systemu, w związku z czym podczas korzystania z niego nie trzeba stosować dodatkowego
zasilacza.
W sieci można znaleźć wiele ciekawych "wariacji" na temat tego programatora jedną z
nich jest AVR910 na AT90S2313.
Programator STK200
Innym rozwiązaniem programatora ISP jest STK200 również firmowany przez firmę
Atmel lecz konstrukcję tego programatora jak również starterkitu STK200 opracowała
firma Kanda. Jest to programator szeregowy komunikujący się z komputerem za pomocą
pomocą portu równoległego (LPT), obsługiwany jest przez bardzo dużą liczbę programów.
Schemat elektryczny programatora STK200 pokazano na rysunku:
14
Układ IC1 spełnia rolę separatora linii wejść/wyjść interfejsu drukarkowego Centronics od
systemu, w którym znajduje się programowany mikrokontroler. Programator jest zasilany
napięciem pobieranym z systemu, w związku z czym podczas korzystania z niego nie
trzeba stosować dodatkowego zasilacza.
Oprogramowanie
Do obsługi ww. programatorów mogą służyć programy wchodzące w skład dystrybucji
WinAVR.
Są to: uisp oraz avrdude. Ich cechą szczególną jest praca w trybie wsadowym (z konsoli).
Pozwala to jednak na proste programowanie układu np. wykorzystując program make
poprzez wydanie komendy:
make program
oczywiście pod warunkiem, że wcześniej przygotowaliśmy sobie odpowiedni plik
makefile.
Wystarczy jego na końcu dopisać odpowiednią treść, która została podana niżej.
Uzupełnienie pliku makefile dla programu avrdude:
PROG = stk200
program:
avrdude -p $(MCU) -c $(PROG) -e -m flash -i $(TARGET).hex
avrdude -p $(MCU) -c $(PROG) -m eeprom -i $(TARGET).eep
Uzupełnienie pliku makefile dla programu uisp:
PROG = stk200
program:
uisp -v -dprog=$(PROG) --erase --upload if=$(TARGET).hex
uisp -v -dprog=$(PROG) --segment=eeprom --upload if=$(TARGET).eep
15
przy czym zmienna TARGET oznacza nazwę projektu a MCU - typ mikrokotrolera.
Po szczegóły dotyczące parametrów wywołania i konfiguracji programów uisp i avrdude
odsyłam do ich dokumentacji.
AVR-GCC - dostęp do zasobów
mikrokontrolera
Tutaj zostaną przedstawione podstawowe techniki dostępu do zasobów mikrokontrolera.
Oczywiście nie są to sposoby "jedynie słuszne" i każdy może uzyskać dostęp do
wybranych zasobów w inny wybrany przez siebie sposób.
W treści bardzo często będą występowały odwołania do różnych plików nagłówkowych dociekliwy czytelnik może je znaleźć w katalogu [avrgcc]/avr/include. Warto tam często
zaglądać - być może w ten sposób uda się łatwiej rozwiązać jakiś problem czy też znaleźć
bardziej odpowiednienią funkcję. Niestety dystrybucyjna kompilatora o nazwie WinAVR
nie zawiera w chwili obecnej plików źródłowych bibliotek. Nowe wersje bibliotek można
jednak znaleźć w internecie pod adresem: http://savannah.nongnu.org/projects/avr-libc/.
Jako platformę do testów używałem mikrokontrolera typu ATMega32. Zostało to
podyktowane jego bogatymi zasobami wewnętrznymi, łatwością montażu (obudowa
DIL40) oraz relatywnie niską ceną.
Oczywiście większość przykładów (tych mniej zaawansowanych) można skompilować
(czasem po drobnych modyfikacjach kodu źródłowego) i przetestować na mniejszych
układach np. AT90S2313.
Rodzina AVR jest bardzo duża i każdy może użyć takiego układu jaki ma "pod ręką" niestety z pewnymi wyjątkami: kompilator języka C zawarty w AVR-GCC nie obsługuje
prostszych układów AVR bez wewnętrznej pamięci SRAM i posługujących się
sprzętowym stosem. Nie będzie więc można wykorzystać bardzo popularnego układu
AT90S1200 i większości ATtiny. Pełną listę układów obsługiwanych przez kompilator i
jego biblioteki można zobaczyć przeglądając plik avr/io.h z biblioteki standardowej.
Rejestry specjalne
Dostęp do urządzeń peryferyjnych wbudowanych w mikrokontrolery AVR jest możliwy
poprzez rejestry specjalne. Do realizacji dostępu używa się makr zawartych w pliku
avr/sfr_defs.h. Dzięki nim jest możliwe (tak jak w komercyjnych kompilatorach)
uzyskanie dostępu do portów poprzez przypisanie wartości np. PORTA=0x55. Taką
konwencję dostępu do zasobów będziemy stosować w programach przykładowych. Jednak
w miejsce jednego z powyższych plików w naszym projekcie powinniśmy użyć avr/io.h,
który dodatkowo włącza odpowiednie definicje rejestrów w zależności od kontrolera, na
który jest kompilowany program (np. dla mikrokontrolera AT90S8515) włącza plik
avr/io8515.h. Typ mikrokontrolera, na który ma być skompilowany program jest określany
przez parametr przekazywany z linii poleceń do kompilatora np. -mmcu=at90s8515.
Łańcuch znaków po znaku równości zawiera wybrany typ mikrokontrolera.
Najważniejsze funkcje zawarte w pliku "avr/io.h"
Poniższe instrukcje ułatwiają podstawowe operacje związane z dostępem do rejestrów
specjalnych.
16
sbi (sfr, bit )
Instrukcja ta służy do ustawiania wskazanego bitu bit w rejestrze sfr. Pozostałe bity nie są
zmieniane. Przykład:
sbi (PORTB, 3);
lub
sbi (PORTB, PB3);
Obie funkcje robią dokładnie to samo czyli ustawiają 3 bit w PORTB. Zwróćmy uwagę na
fakt, że w drugim przykładzie jako numeru bitu użyto PB3 - w ten sposób możemy
zwiększyć czytelność programu.
cbi ( sfr, bit )
Instrukcja ta służy do kasowania (ustawiania na 0) wskazanego bitu bit w rejestrze sfr.
Pozostałe bity nie są zmieniane. Przykład:
cbi (PORTB, 3);
lub
cbi (PORTB, PB3);
Obie funkcje robią dokładnie to samo czyli kasują 3 bit w rejestrze PORTB.
bit_is_set ( sfr, bit )
Zwraca wartość większą od zera gdy bit jest ustawiony, w przeciwnym wypadku 0.
Przykład:
result = bit_is_set (PINB, PINB3);
loop_until_bit_is_set ( sfr, bit )
Wstrzymuje działanie programu (wykonuje pętlę) dopóki bit jest ustawiony. Przykład:
loop_until_bit_is_set (PINB, PINB3);
subsection {bit_is_clear ( sfr, bit )
Zwraca wartość większą od zera gdy bit jest skasowany, w przeciwnym wypadku 0.
Przykład:
result = bit_is_clear (PINB, PINB3);
loop_until_bit_is_clear ( sfr, bit)
Wstrzymuje działanie programu (wykonuje pętlę) dopóki bit jest skasowany. Przykład:
loop_until_bit_is_clear (PINB, PINB3);
subsection {inb ( sfr )
Instrukcja ta służy do czytania wartości w rejestrze sfr. Przykład:
res = inb (SREG);
17
lub poprzez przypisanie:
res = SREG;
Czyta rejestr SREG i wpisuje jego wartość do res.
inw ( sfr )
Instrukcja ta służy do czytania wartości 16 bitowej w rejestrze sfr. Tymi rejestrami są np.:
ADC, ICR1, OCR1A, OCR1B, TCNT1 itp. Zgodnie ze specyfikacją kontrolerów AVR ich
8 bitowe "połówki" muszą być odczytane w odpowiedniej kolejności aby poprawnie
została przeczytana 16 bitowa wartość. Właśnie inw wyręcza nas w pamiętaniu o tej
konieczności. Przykład:
res = inw (TCNT1);
lub poprzez przypisanie:
res = TCNT1;
Czyta rejestr TCNT1 i wpisuje jego wartość do res.
outb ( sfr , val )
Instrukcja ta służy do wpisania wartości val do portu sfr. Przykład:
outb(PORTB, 0xFF);
lub poprzez przypisanie:
PORTB = 0xFF;
Ustawiają wszystkie bity w rejestrze PORTB.
outw ( sfr , val )
Instrukcja ta służy do wpisania wartości 16 bitowej val do rejestru sfr. Tymi rejestrami są
np.: ADC, ICR1, OCR1A, OCR1B, TCNT1 itp. Zgodnie ze specyfikacją kontrolerów
AVR ich 8 bitowe "połówki" muszą być wpisane w odpowiedniej kolejności aby
poprawnie została wpisana 16 bitowa wartość. Właśnie outw wyręcza nas w pamiętaniu o
tej konieczności. Przykład:
outw (0xAAAA, OCR1A);
lub poprzez przypisanie:
OCR1A = 0xAAAA;
Wpisuje 0xAAAA do rejestru OCR1A.
AVR-GCC - wejście i wyjście binarne
18
Rejestry
Tutaj sytuacja się trochę (pozornie) komplikuje. Do każdego z fizycznych portów (tzn.
tych dostępnych z zewnątrz układu) przypisane są trzy rejestry. Ale dzięki temu możemy
niemal dowolnie konfigurować każdą końcówkę układu związaną z portami, tzn. ustalać
kierunek, podciągać wejścia do zasilania, odłączać od reszty układu elektronicznego itp.
Ustalanie kierunku - DDRx
Rejestr kierunku danych (ang. Data Direction Register X, gdzie X jest oznaczeniem
znakowym portu np. DDRA jest rejestrem kierunku dla portu A). Ustalenie kierunku
odbywa się wg zasady:
•
•
ustawienie odpowiedniego bitu - wyjście
skasowanie odpowiedniego bitu - wejście
Przykład:
DDRB = _BV(7)|_BV(6)|_BV(5)|_BV(4);
lub
DDRB = 0xF0;
Ustawia odpowiednie końcówki portu B: 0-3 na wejścia, 4-7 na wyjścia.
Rejestr zapisu danych - PORTx
Rejestr Danych Portu X (gdzie X jest oznaczeniem znakowym portu np. PORTA jest
rejestrem danych dla portu A). Na przykład aby załadować do rejestru PORTB wartość
0xAA należy użyć następującego kodu:
PORTB = 0xAA;
Rejestr odczytu danych - PINx
Odczyt z portu X (gdzie X jest oznaczeniem znakowym portu np. PINA jest rejestrem
odczytu portu A). Odczyt z tego portu daje nam fizyczny stan na końcówkach - oczywiście
pod warunkiem wcześniejszego ustalenia kierunku poprzez wpisanie zer w odpowiednie
bity rejestru kierunku.
Przykład:
DDRA = 0x00; // ustawienie kierunku na wejście
res = PINA;
Czyta fizyczną wartość na porcie A i umieszcza ją w res.
19
Praktyczny sposób dostępu do wyprowadzeń układu
Trzy rejestry dla jednego fizycznego portu mogą być dla wielu zbyt dużą uciążliwością
szczególnie w przypadku, gdy z jakiegoś powodu musimy zmienić port, do którego
podłączamy jakieś urządzenie - wtedy w całym programie musieli byśmy dokonywać
zmian dotyczących trzech portów - łatwo więc o pomyłkę. Można jednak wykorzystać
fakt, że wszystkie opisywane tu rejestry łączy pewna zależność: w przestrzeni adresowej
układu znajdują się one "koło siebie". Załóżmy że piszemy procedury obsługi np.
wyświetlacza LCD, będzie on podłączony do jednego fizycznego portu np. PORTB.
Wystarczy umieścić w programie odpowiednie makrodefinicje a cała operacja stanie się
banalnie prosta. Przykład:
#define
#define
#define
#define
#define
#define
DDR(x) _SFR_IO8(_SFR_IO_ADDR(x)-1)
PIN(x) _SFR_IO8(_SFR_IO_ADDR(x)-2)
LCD_PORT
PORTB
//
LCD_PORT_O
LCD_PORT
//
LCD_PORT_D
DDR(LCD_PORT)
//
LCD_PORT_I
PIN(LCD_PORT)
//
// adr.
// adr.
używany
rejestr
rejestr
rejestr
r. kier. PORTx
r. wej. PORTx
port
wyjściowy
kierunkowy
wejściowy
Po napisaniu takich makrodefinicji w dalszej części programu już nie posługujemy się
nazwami w rodzaju: PORTB, PINB, DDRB ale: LCD_PORT_O, LCD_PORT_I,
LCD_PORT_D. Ma to również jeszcze jedną zaletę - jeśli będziemy chcieli zmienić w
programie port do którego ma być przyłączone obsługiwane urządzenie wystarczy zmiana
tylko jednej linijki np.:
#define LCD_PORT
PORTB
na:
#define LCD_PORT
PORTD
a dalszą zamianą zajmie się kompilator (konkretnie jego preprocesor).
Podciąganie wejścia do logicznej jedynki
Bardzo interesującą cechą układów AVR (szczególnie dla używających w swej praktyce
układów MCS-51) jest możliwość "podciągania" wejść do logicznej jedynki bez użycia
zewnętrznych rezystorów. Robi się to w ten sposób, że przy wpisanym zerze do bitu
kierunku DDRx (ustawienie bitu portu jako wejście) należy wpisać jedynkę na ten sam bit ale do portu PORTx. Zostanie to zilustrowane na przykładzie:
cbi(DDRB,7); // użyj linii PB7 jako wejścia
sbi(PORTB,7);
// "podciągnij" do logicznej 1 linię PB7
Tutaj celowo użyto dwóch instrukcji, jednak praktycznie można użyć tylko ostatniej
"podciągającej" wejście gdy mamy pewność, że wcześniej nie był ustawiany dany bit
ustalający kierunek w pocie (DDRx) - po starcie mikrokontrolera wszystkie bity związanie
z portami fizycznymi są wyzerowane czyli porty są ustawione na odczyt danych.
20
Programy przykładowe
Po zapoznaniu z podstawowymi operacjami wejścia/wyjścia możemy napisać i
przetestować nasz pierwszy program na mikrokontroler AVR w języku C. Na początek
będzie to sterowanie diodą LED podłączoną przez rezystor 470 om między zasilanie a
wyjście PD4 przy pomocy przycisku monostabilnego podłączonego między linię PD3 a
masę.
// Sterowanie diodą LED podłączoną do linii PD4 mikrokontorlera
// za pomocą przycisku podłączonego do linii PD3 mikrokontrolera
#include <avr/io.h>
int main( void )
{
sbi(DDRD,4);
wyjścia
sbi(PORTD,3);
logicznej 1 linię PD3
while(1)
{
cbi(PORTD,4);
linii PD4
loop_until_bit_is_clear(PIND,3);
przycisku na PD3
sbi(PORTD,4);
linii PD4
loop_until_bit_is_clear(PIND,3);
przycisku na PD3
}
}
// dostęp do rejestrów
// program główny
// użyj linii PD4 jako
// "podciągnij" do
// pętla nieskończona
// zapal diodę LED podłączoną do
// czekaj na naciśnięcie
// zgaś diodę LED podłączoną do
// czekaj na naciśnięcie
Dyrektywa #include dołącza do programu pliki z definicjami portów, rejestrów i funkcji
dostępu do nich. Najlepiej, podczas pisania programu mieć zawsze otwarty gdzieś "na
boku" ten plik i inne, które on dodatkowo włącza - głównie plik z definicjami dla
mikrokontrolera, na który będzie skompilowany nasz program np. dla AT90S8515 będzie
to avr/io8515.h. Dalej znajduje się główna funkcja programu int main(void), od której
zawsze rozpoczyna się działanie programu napisanego w języku C. Instrukcja
sbi(DDRB,0) powoduje wpisanie jedynki do bitu 0 rejestru DDRB. Jest to rejestr ustalający
kierunek przepływu danych w porcie B. W efekcie możemy używać linię PB0
mikrokontrolera jako wyjścia. Kolejna instrukcja: sbi(PORTB,7) wpisuje jedynkę do bitu 7
rejestru PORTB, w tym samym czasie bit 7 portu DDRB jest wyzerowany, gdyż
mikrokontroler po starcie ma wpisane zera do wszystkich rejestrów związanych z
fizycznymi portami mikrokontrolera. W efekcie linia PB7 mikrokontrolera pracuje jako
wejście "podciągnięte" do logicznej jedynki - nie musimy używać rezystora zewnętrznego.
Kolejna instrukcja while(1) to pętla nieskończona, w której wykonywana jest reszta
programu mikrokontrolera. Każdy program na mikrokontrolery musi zawierać jakąś pętlę
nieskończoną - program nie może się bowiem zakończyć. W ostateczności może być
zakończony pustą pętlą nieskończoną. Tak będą realizowane niektóre proste programy
przykładowe. Dalej występuje instrukcja cbi(PORTB,0) powodująca wpisanie zera do bitu
0 rejestru PORTB co w efekcie powoduje pojawienie się stanu niskiego na wyprowadzeniu
PB0 mikrokontrolera i zapalenie diody LED podłączonej między linię PB0 a linię zasilania
(oczywiście przez rezystor). W następnej linii instrukcja loop_until_bit_is_clear(PINB,7)
21
powoduje zatrzymanie programu do momentu, aż na bicie 7 rejestru PINB pojawi się stan
niski. W efekcie program czeka na naciśnięcie przycisku podłączonego pomiędzy linię
PB7 mikrokontrolera a masę. Następnie instrukcja sbi(PORTB,0) wpisuje jedynkę do bitu
0 rejestru PORTB co w efekcie powoduje pojawienie się stanu wysokiego na
wyprowadzeniu PB0. W efekcie gasi diodę LED podłączoną do linii PB0. Dalej ponownie
pojawia się instrukcja loop_until_bit_is_clear(PINB,7) i pętla się zamyka - program
przechodzi ponownie do wykonywania instrukcji cbi(PORTB,0) itd.
Po zaprogramowaniu układu mikrokontrolera powinna zapalić się dioda LED podłączona
do linii PB0. Naciśnięcie przycisku podłączonego do linii PB7 powinno spowodować
lekkie zmniejszenie jasności świecenia diody (pojawia się tam fala prostokątna o
wypełnieniu 50\%). Gdy przestaniemy naciskać przycisk dioda zapali się lub zgaśnie w
zależności od tego, w którym miejscu był program w momencie zwolnienia przycisku.
Załóżmy jednak, że w przyszłości będziemy chcieli podłączyć diodę LED do innej
końcówki układu, bo np. w ten sposób uprościmy płytkę drukowaną dla układu. Podobnie
może być z przyciskiem. I co wtedy? Tutaj być może jeszcze nie będzie wielkiego
problemu, ot zmienimy wpisy w paru linijkach kodu i po kłopocie!
Jednak gdy kod źródłowy rozrośnie się powiedzmy do kilkuset a nawet kilku tysięcy linii
to tak napisany program może być bardzo trudny do późniejszej modyfikacji, a o pomyłkę
będzie bardzo łatwo. Warto więc zawczasu nabrać kilku dobrych nawyków. Po pierwsze korzystajmy z makrodefinicji (to nie pochłania pamięci programu mikrokontrolera!).
Program źródłowy jest większy i wygląda na bardziej skomplikowany lecz wielkość
programu wynikowego nie zmienia się w stosunku do tego z powyżsego listingu.
// Sterowanie diodą LED podłączoną do dowolnej linii mikrokontorlera
// za pomocą przycisku podłączonego do dowolnej linii mikrokontrolera
#include <avr/io.h>
// dostęp do rejestrów
#define DDR(x) _SFR_IO8(_SFR_IO_ADDR(x)-1) // adr. rej. kier. PORTx
#define PIN(x) _SFR_IO8(_SFR_IO_ADDR(x)-2) // adr. rej. wej. PORTx
#define
#define
#define
#define
#define
LED_PORT
LED_BIT
LED_PORT_O
LED_PORT_D
LED_PORT_I
PORTD
#define
#define
#define
#define
#define
KEY_PORT
KEY_BIT
KEY_PORT_O
KEY_PORT_D
KEY_PORT_I
PORTD
4
LED_PORT
DDR(LED_PORT)
PIN(LED_PORT)
int main( void )
{
sbi(LED_PORT_D,LED_BIT);
sbi(KEY_PORT_O,KEY_BIT);
3
KEY_PORT
DDR(KEY_PORT)
PIN(KEY_PORT)
// port diody LED
// bit diody LED
// rejestr wyjściowy
// rejestr kierunkowy
// rejestr wejściowy
// port przycisku
// bit przycisku
// rejestr wyjściowy
// rejestr kierunkowy
// rejestr wejściowy
// program główny
// użyj linii jako wyjścia
// "podciągnij" linię do logicznej 1
while(1)
// pętla nieskończona
{
cbi(LED_PORT_O,LED_BIT);
// zapal diodę LED
loop_until_bit_is_clear(KEY_PORT_I,KEY_BIT); // czekaj na naciśnięcie
przycisku
sbi(LED_PORT_O,LED_BIT);
// zgaś diodę LED
loop_until_bit_is_clear(KEY_PORT_I,KEY_BIT); // czekaj na naciśnięcie
przycisku
22
}
}
Makrodefinicjami:
#define DDR(x) _SFR_IO8(_SFR_IO_ADDR(x)-1)
#define PIN(x) _SFR_IO8(_SFR_IO_ADDR(x)-2)
wyliczają adresy rejestru kierunkowego i wejściowego dla podanego portu wyjściowego.
W kolejnych liniach mamy definicję:
#define LED_PORT
PORTB
gdzie symbolowi LED_PORT jest przypisywana wartość znajdująca się w linii po nim
czyli PORTB. W ten sposób zmieniając napis PORTB np. na PORTD spowodujemy, że
symbol LED_PORT przyjmie wartość PORTD. Teraz w dalszej części programu będziemy
się posługiwać symbolem LED_PORT. W kolejnym wierszu programu znajduje się
definicja:
#define LED_BIT
0
przyporządkowująca symbolowi LED_BIT wartość 0. Analogicznie jak w definicji portu
możemy zmieniać tę wartość w zakresie od 0 do 7 - oczywiście, jeżeli budowa portu
używanego mikrokontrolera na to pozwala. Linie programu zawierające makrodefinicje:
#define LED_PORT_O
LED_PORT
- definiuje rejestr wyjściowy - w dalszej części programu będziemy używać nazwy
LED_PORT_O
#define LED_PORT_D
DDR(LED_PORT)
- definiuje rejestr kierunkowy - w dalszej części programu będziemy używać nazwy
LED_PORT_D
#define LED_PORT_I
PIN(LED_PORT)
- definiuje rejestr wejściowy - w dalszej części programu będziemy używać nazwy
LED_PORT_I.
Analogiczne definicje dla klawisza (KEY_PORT, KEY_BIT) znajdziemy w kolejnych
liniach programu. Dalej znajduje się główna funkcja programu int main(void) której
"ciało" wygląda analogicznie do tej z listingu pierwszego z tą tylko różnicą, że nazwy
portów i numery bitów zastąpiono nazwami definiowanymi przez nas. Jak widać długość
listingu programu w stosunku do poprzedniego wzrosła dosyć znacznie - ale to się opłaca!
- teraz zmiana przyporządkowania urządzeń do końcówek układu wymaga zmian w
czterech miejscach. Już w tak krótkim programie zyskaliśmy 3 razy mniej zmian!
AVR-GCC - port szeregowy
Większość kontrolerów AVR posiada wbudowany układ pozwalający na przesyłanie
informacji w postaci szeregowej za pomocą linii: TXD - wyjście szeregowe i RXD 23
wejście szeregowe. Transmisja może się odbywać w trybie full-duplex, gdyż układ ten
posiada dwa niezależne rejestry transmisyjne. Układ posiada także własny układ taktujący,
co zwalnia liczniki-czasomierze z generowania tego sygnału. Jest to znaczne rozszerzenie
możliwości układu UART w stosunku do popularnej rodziny kontrolerów 8051.
Jego główne cechy to:
•
•
•
•
•
ustawianie praktycznie dowolnej prędkości transmisji z przedziału 2400 - 115000 kb/s
(dla częstotliwości zegara mikrokontrolera 1 - 8 MHz)
duże prędkości transmisji przy małej częstotliwości zegarowej
filtracja szumów i zakłóceń
rozmaite detekcje błędów
generuje trzy oddzielne przerwania:
o od zakończenia odbioru znaku,
o od zakończenia transmisji znaku,
o od opróźnienienia rejestru transmisji znaku.
Uwaga!
Ze względu na duże rozbieżności w nazewnictwie rejestrów i ich bitów kontrolujących
port szeregowy opis będzie dotyczył "klasycznych" układów AVR np. AT90S2313 i
AT90S8515 - w układach ATMega istnieją ich odpowiedniki o często rozszerzonej
funkcjonalności.
Interfejs pomiędzy AVR a PC
Inicjalizacja
Aby zrealizować transmisję danych należy zainicjować rejestry kontrolne odpowiednimi
wartościami.Po pierwsze należy ustalić prędkość transmisji. Robi się to przez wpisanie do
rejestru UBRR odpowiedniej wartości. Na przykład dla częstotliwości zegara 8MHz i
szybkości 9600 bodów wpisujemy do UBRR wartość 51 odczytaną z tabeli znajdującej się
w nocie katalogowej układu. Można też obliczyć tę wartość korzystając z zależności:
F_CPU
UBRR = ---------------- - 1
(UART_BAUD * 16)
24
gdzie:
F_CPU - częstotliwość zegara mikrokontrolera w Hz,
UART_BAUD - prędkość transmisji w b/s.
Powyższą formułę można umieścić w programie jako makrodefinicje np.
#define F_CPU
#define UART_BAUD
#define UART_CONST
8000000
//częstotliwość zegara w Hz
19200
//prędkość transmisji
(F_CPU/(16ul*UART_BAUD)-1)
następnie wyliczoną przez preprocesor wartość umieszczamy w rejestrze UBRR:
UBRR = (unsigned char)UART_CONST; // ustaw prędkość transmisji
Gdy mamy ustaloną prędkość transmisji należy zezwolić na transmisję i/lub odbiór
znaków (w zależności od potrzeb). Robi się to przez ustawianie odpowiednich bitów w
rejestrze UCR (RXEN - zezwolenie na odbiór, TXEN - zezwolenie na nadawanie) np.
poprzez wpisanie takiego kodu:
UCR = _BV(RXEN)|_BV(TXEN);
W tym momencie mamy zainicjowany interfejs szeregowy - pora więc z niego skorzystać.
Wysyłanie znaku
Główną częścią układu nadajnika transmisji jest rejestr przesuwający, połączony z
rejestrem bufora UDR, do którego należy wpisać transmitowany bajt. Po stwierdzeniu że
rejestr przesuwający jest pusty, zawartość rejestru UDR jest przepisywana do rejestru
przesuwającego co rozpoczyna transmisję tego bajtu. Napiszmy więc procedurę służącą do
wysłania znaku na port szeregowy. Oto ona:
void putchar (char c)
{
UDR=c;
loop_until_bit_is_set(USR,TXC);
sbi(USR,TXC);
}
Instrukcja UDR=c umieszcza w rejestrze UDR znak do wysłania. Instrukcja
loop_until_bit_is_set(USR,TXC) powoduje oczekiwanie programu na koniec transmisji
znaku. Instrukcja sbi(USR,TXC) ustawia znacznik TXC w rejestrze USR aby można było
sprawdzać kolejny wysyłany bajt. Dociekliwy czytelnik na pewno zauważy, że powyższa
instrukcja powoduje ustawienie już i tak ustawionego bitu TXC w rejesterze USR - więc po
co? Otóż niektóre bity znaczników w mikrokontrolerach AVR mają taką właściwość, że
skasować je można m.in. przez programowe ustawienie tego bitu. Do nich m.in. należy
TXC.
Odbiór znaku
Podobnie jak w przypadku nadajnika, główną częścią części odbiorczej jest rejestr
przesuwający oraz połączony z nim rejestr bufora UDR. Jak wspomniano na początku
25
część odbiorcza jest wyposażona w układ eliminujący zakłócenia jakie mogą wystąpić
podczas transmisji bitów. Mechanizm jego działania jest prosty. Polega on na
wielokrotnym próbkowaniu stanu linii RxD. Jeśli logika sterująca stwierdzi, że co najmniej
dwie ostatnie z trzech próbek – w środku “okna” dla każdego z bitów – są identyczne, stan
linii jest uznawany za ważny i bit trafia do rejestru przesuwającego.
Gdy mamy już procedurę służącą do wysyłania danych przydała by się do niej
komplementarna - czyli odbierająca znaki oto i ona:
char getchar (void)
{
loop_until_bit_is_set(USR,RXC);
cbi(USR,RXC);
return UDR;
}
Instrukcja loop_until_bit_is_set(USR,RXC) powoduje oczekiwanie programu na koniec
odbierania znaku. Instrukcja cbi(USR,RXC) kasuje znacznik RXC aby można było
sprawdzać kolejny odbierany bajt. Instrukcja {return UDR} zwraca wartość umieszczoną
w rejestrze UDR.
Program przykładowy
Poniższy przykład będzie rozwinięciem poprzednich dwóch prostych programów o
możliwość sterowania diodą LED z portu szeregowego i dodatkowo sygnalizowaniem jej
stanu znakiem wysyłanym na port szeregowy. Do sprawdzenia działania tego programu
potrzebny będzie program emulujący terminal np. tandardowy "windowsowy"
Hyperterminal lub darmowy program Br@y++ Terminal do pobrania ze strony autora
programu: http://bray.velenje.cx/avr/terminal/.
Dla wybranego łącza szeregowego (tego, do którego mamy podłączony mikrokontroler)
ustalamy następujące parametry: prędkość transmisji 19200 b/s, 8 bitów danych, brak
parzystości, 1 bit stopu, brak sterowania przepływem. Do podłączenia komputera portu
szeregowego w mikrokontrolerze można użyć konwertera poziomów MAX232 lub jego
odpowiednika.
// Sterownie diodą LED podłączoną do dowolnej linii mikrokontrolera
// za pomocą dowolnego znaku odebranego z portu szeregowego
// mikrokontrolera i wysyłanie jej stanu na port szeregowy
#include <avr/io.h>
// dostęp do rejestrów
// Zmieniając poniższe definicje można dostosować program do potrzeb
#define F_CPU
Hz
#define UART_BAUD
bit/s
#define LED_PORT
#define LED_BIT
8000000ul
19200ul
PORTD
4
// częstotliwość zegara w
// prędkość transmisji
// port diody LED
// 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
#define LED_PORT_D
LED_PORT
DDR(LED_PORT)
// rejestr wyjściowy
// rejestr kierunkowy
26
#define LED_PORT_I
PIN(LED_PORT)
// rejestr wejściowy
#define UART_CONST
(F_CPU/(16ul*UART_BAUD)-1)
// _UCR_
#ifdef
UCR
#define _UCR_
#endif
UCR
#ifdef
UCSRB
#define _UCR_
#endif
UCSRB
#ifdef
UCSR0B
#define _UCR_
UCSR0B
#endif
// _USR_
#ifdef
USR
#define _USR_
#endif
USR
#ifdef
UCSRA
#define _USR_
#endif
UCSRA
#ifdef
UCSR0A
#define _USR_
UCSR0A
#endif
// Definicje funkcji
// inicjalizacja portu szeregowego
void UART_init(void)
{
UBRR = (unsigned char)UART_CONST;
_UCR_ = _BV(RXEN)|_BV(TXEN);
}
// ustaw prędkość transmisji
// załącz tx, rx
// wysyła znak podany jako parametr na port szeregowy
void UART_putchar (char c)
{
UDR = c;
// wpisz c do rejestru UDR
loop_until_bit_is_set(_USR_,TXC);
// czekaj na zakończenie
transmisji
sbi(_USR_,TXC);
// ustaw bit TXC w rej.
USR
}
// odbiera znak z portu szeregowego i zwraca go jako wartość funkcji
char UART_getchar (void)
{
loop_until_bit_is_set(_USR_,RXC);
// czekaj na zakończenie
odbioru
cbi(_USR_,RXC);
// skasuj bit RXC w rej.
USR
return UDR;
// zwróć zawartość rejestru
UDR
}
27
int main(void)
{
UART_init();
// program główny
// inicjalizacja portu szeregowego
sbi(LED_PORT_D,LED_BIT);
// użyj linii jako wyjścia
while(1)
{
cbi(LED_PORT_O,LED_BIT);
UART_putchar('1');
UART_getchar();
sbi(LED_PORT_O,LED_BIT);
UART_putchar('0');
UART_getchar();
}
// pętla nieskończona
// zapal diodę LED
// wyślij '1' na
// czekaj na znak z
// zgaś diodę LED
// wyślij '0' na
// czekaj na znak z
port szeregowy
portu szeregowego
port szeregowy
portu szeregowego
}
Na początku programu znajdują się makrodefinicje, które globalnie ustalają parametry
pracy układu takie jak: częstotliwość zegara (kwarcu) mikrokontrolera, prędkość transmisji
szeregowej, port i jego bit, do którego jest podłączona dioda LED. Wartości te
(wyróżnione przez podkreślenie i wytłuszczenie) w razie konieczności można zmienić. W
dalszej części znajdują się poznane już wcześniej makrodefinicje służące do określania
adresów rejestrów portów mikrokontrolera. Później spotykamy makrodefinicję wyliczającą
stałą, którą mamy załadować do rejestru UBRR aby ustawić żądaną prędkość transmisji. Po
tej sporej dawce makr widzimy definicje funkcji, które wcześniej zostały szczegółowo
opisane: UART_init(), UART_putchar() i UART_getchar(). W końcu docieramy do
głównej funkcji naszego programu: main(). Tutaj opis zostanie pominięty - komentarze
powinny wystarczyć. Po starcie programu w układzie powinna się świecić dioda
podłączona do PB0 a na terminalu ostatnim wyświetlonym znakiem powinna być "1"
(jedynka), następnie z terminala wysyłamy dowolny znak np. naciskając spację. Dioda
powinna zgasnąć a na terminalu powinien pojawić się znak "0" (zero).
AVR-GCC - pamięć SRAM
Zmienne, które definiujemy w programie bez żadnych dodatkowych atrybutów są
umieszczane przez kompilator w pamięci SRAM mikrokontrolera. Jest ona podłączona
bezpośrednio do magistrali (bez rejestrów pośredniczących jak w opisywanej w dalszej
części książki pamięci EEPROM) co znacznie przyspiesza dostęp a także nie ma potrzeby
korzystania z dodatkowych funkcji aby z niej skorzystać.
Należy podkreślić, że w przestrzeni adresowej pamięci SRAM znajdują się także 32
rejestry ogólnego przeznaczenia R0-R31 oraz 64 (większość) lub 224 (np. ATmega128)
bajty zarezerwowane dla rejestrów zintegrowanych w mikrokontrolerze układów
peryferyjnych. W związku z tym adres rzeczywistej pamięci SRAM zaczyna się dla
większości układów od adresu 96 (0x60) lub 256 (0x100) jak np. w ATmega128. Podobnie
jak w większości układów rodziny MCS-51 istniała możliwość dołączenia zewnętrznej
pamięci danych tak i w rodzinie AVR mamy taką możliwość (ale tylko niektóre układy
AVR). Istnieje tu jednak znacząca różnica - jeżeli w MCS-51 ta dodatkowa pamięć
zajmowała oddzielną przestrzeń adresową, to w AVR ta pamięć stanowi "przedłużenie"
istniejącej przestrzeni adresowej pamięci SRAM. Co więcej pamięć tą można w dowolnej
chwili programowo podłączać i odłączać od systemu (poprzez zmianę stanu bitu SRE w
rejestrze MCUCR), przez co uzyskuje się jeszcze większą elastyczność w konstruowaniu
28
urządzeń, ponieważ możemy w zasadzie w dowolnej chwili skorzystać z linii używanych
do komunikacji z pamięcią np. w celu obsłużenia innych urządzeń. W pamięci SRAM
przechowywane są wszystkie zmienne. Deklaracje często używanych całkowitych typów
danych znajdują się w pliku avr/inttypes.h. Stałe są tworzone poprzez podanie słowa
kluczowego const. Taka "zmienna" posiada atrybut tylko do odczytu i nie może być
zmieniana - jest stałą przechowywaną w pamięci SRAM.
W tabeli przedstawiono predefiniowane typy zmiennych wg standardu ISO C99 stworzone
po to, aby ułatwić pisanie programów i było łatwiejsze szybkie zorientowanie się w ilości
bajtów (bitów) zajmowanych przez daną zmienną.
Predefiniowane typy zmiennych w pliku inttypes.h
Nazwa typu danych
int8_t
uint8_t
int16_t
uint16_t
int32_t
uint32_t
int64_t
uint64_t
| Długość w bajtach | Zakres wartości
|
1
|
-128 ... 127
|
1
|
0 ... 255
|
2
|
-32768 ... 32767
|
2
|
0 ... 65535
|
4
| -2147483648 ... 2147483647
|
4
|
0 ... 4294967295
|
8
| -9,22*10^18 ... 9,22*10^18
|
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
typedef
typedef
typedef
typedef
typedef
typedef
typedef
unsigned char
char
unsigned short
short
unsigned long
long
unsigned long long
long long
u08;
s08;
u16;
s16;
u32;
s32;
u64;
s64;
//
//
//
//
//
//
//
//
8 bitów bez znaku (int8_t)
8 bitów ze znakiem (uint8_t)
16 bitów bez znaku (int16_t)
16 bitów ze znakiem (uint16_t)
32 bity bez znaku (int32_t)
32 bity ze znakiem (uint32_t)
64 bity bez znaku (int64_t)
64 bity ze znakiem (uint64_t)
Operacje na zmiennych
Tworzenie zmiennej
Tworzenie zmiennej to prosta operacja, którą można wykonać na wiele sposobów np.:
unsigned char val = 8;
lub
uint_8 val = 8;
lub
u08 val = 8;
Po wywołaniu jednej z powyższych instrukcji zostanie utworzona nowa zmienna o nazwie
val i zainicjowana wartością 8. Ta zmienna zajmie 1 bajt pamięci SRAM. Widać wyraźnie
29
korzyść
z zastosowania krótszej nazwy typu - po prostu mniej pisania a efekt taki sam!
Tworzenie zmiennej w rejestrze
Poprzez użycie słowa kluczowego register informujemy kompilator o potrzebie
umieszczenia
zmiennej w rejestrze procesora. Jest to użyteczne rozwiązanie jeżeli dana zmienna jest
bardzo często wykorzystywana i dostęp do niej ma być jak najszybszy. Należy zaznaczyć,
że tak można deklarować tylko zmienne lokalne (w obrębie funkcji).
register unsigned char val = 8;
lub
register uint_8 val = 8;
lub
register u08 val = 8;
Po wywołaniu powyższej instrukcji zostanie utworzona nowa zmienna o nazwie Val i
zainicjowana wartością 8. Ta zmienna zajmie 1 rejestr kontrolera.
Zmiana wartości zmiennej
Zmiany wartości zmiennej możemy dokonać poprzez przypisanie np.:
val = 10;
czy też poprzez inne operacje jak np. inkrementacja:
val++;
Po wywołaniu obu powyższych instrukcji zmienna o nazwie val przyjmie wartość 11.
Tworzenie stałych w pamięci SRAM
Stałe deklaruje się bez podania typu za pomocą słowa kluczowego const.
const pazdziernik = 10;
Po wywołaniu powyższej instrukcji zostanie utworzona nowa "zmienna" o nazwie
pazdziernik z przypisaną wartością 10, której nie można zmienić.
Program przykładowy
Będzie to program bardziej złożony w stosunku do poprzednich, ale przybliży nas do
"prawdziwych" projektów. Naszym celem będzie napisanie programu, który pokaże
30
zachowanie zmiennych. Komunikacja z użytkownikiem będzie zrealizowana tak jak w
poprzednim przykładzie za pomocą łącza szeregowego.
Jednak same funkcje komunikacji szeregowej powinny zostać "ukryte" aby nie zaciemniać
głównego programu. W związku z tym funkcje związane z portem szeregowym
przeniesiono do osobnych plików i umieszczono w oddzielnym katalogu (../lib), tak aby i
inne programy mogły z nich korzystać. Funkcje biblioteczne umieszczone w tym katalogu
zostały omówione tutaj.
// Testowanie zmiennych i stałych w pamięci SRAM
#include
#include
itoa
#include
#include
<avr/io.h>
<stdlib.h>
// dostęp do rejestrów
// zawiera m.in. deklarację funkcji
"global.h"
"uart.h"
// zawiera definicje typów całkowitych
// obsługa portu szeregowego
// zamiana nazw funkcji (zobacz do uart.h)
#define
getchar
UART_getchar
#define
putstr
UART_putstr
void putint(int value)
przedtawiający value
{
char string[4];
itoa(value, string, 10);
dziesiętną
putstr(string);
}
// wysyła na port szeregowy tekst
// bufor na wynik funkcji itoa
// konwersja value na
wartość
// 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 )
{
u08 val = 8;
zmiennej
register u08 val2 = 12;
rejestrze
UART_init();
puttekst(8);
getchar();
putint(val);
val = 130;
puttekst(130);
getchar();
putint(val);
puttekst(12);
getchar();
// program główny
// deklaracja i inicjalizacja
// deklaracja i inicjalizacja zmiennej w
// inicjalizacja portu szeregowego
// spodziewamy się 8
// czekaj na znak z portu szeregowego
// wyświetl val
// zmień wartość zmiennej
// spodziewamy się 130
// czekaj na znak z portu szeregowego
// wyświetl val
// spodziewamy się 12
// czekaj na znak z portu szeregowego
31
putint(val2);
// wyświetl val2
puttekst(10);
getchar();
putint(pazdziernik);
// spodziewamy się 10
// czekaj na znak z portu szeregowego
// wyświetl pazdziernik
pazdziernik=3;
// próba zmiany wartości stałej
puttekst(3);
getchar();
putint(pazdziernik);
// spodziewamy się 3
// czekaj na znak z portu szeregowego
// wyświetl pazdziernik
while(1);
// pętla nieskończona
}
Zauważmy, że na początku programu została dokonana "podmiana" nazw kilku funkcji
poprzez zastosowanie dyrektyw #define. Zostało to zrobione celowo aby pokazać, że w ten
sposób możemy łatwo zmieniać np. urządzenie na którym będą prezentowane komunikaty.
Np. poprzez zmianę linii:
#define
putstr UART_putstr
na linię w rodzaju:
#define
putstr LCD_putstr
można w prosty zmienić wyświetlanie komunikatów na ekranie terminala na ekran
wyświetlacza LCD.
W tym przykładzie już sama kompilacja nie przebiegnie zupełnie "bezboleśnie" gdyż
kompilator zauważy, że chcemy zmienić coś co z definicji jest niezmienne. Pomimo tego
powinien zostać wygenerowany plik .hex, który wpisujemy do mikrokontrolera za pomocą
programatora.
Po uruchomieniu programu na ekranie terminala pojawi się tekst w rodzaju:
Spodziewamy sie wartosci 8 - wyslij dowolny znak...
po czym wysyłamy dowolny znak np. poprzez naciśnięcie klawisza ENTER i
obserwujemy wynik... wszędzie powinniśmy otrzymać taką wartość jakiej się
spodziewaliśmy za wyjątkiem ostatniej.
AVR-GCC - pamięć programu (FLASH)
Do przechowywania stałych np. tekstów dla wyświetlaczy LCD najlepiej nadaje się
pamięć programu. Uzasadnienie tego jest proste: stałe zadeklarowane w pamięci SRAM
zajmują cenne miejsce w tej (małej) pamięci (są tam kopiowane z pamięci flash podczas
startu programu i nie są z niej usuwane) mogą być więc powodem różnych dziwnych
błędów (na domiar złego kompilator nas zwykle o nich nie informuje).
To tworzenia tego typu danych przeznaczone jest wyrażenie: __attribute__ ((progmem)).
Aby w pełni z niego skorzystać należy włączyć do projektu plik nagłówkowy
avr/pgmspace.h. Ten plik zawiera funkcje służące do czytania danych z pamięci programu.
Są tam również zdefiniowane następujące typy danych:
32
•
•
•
•
8 bitowy: prog_char
16 bitowy: prog_int
32 bitowy: prog_long
64 bitowy: prog_long_long
Najważniejsze funkcje zawarte w pliku avr/pgmspace.h
Poniższe funkcje (są one w rzeczywistości jest makroinstrukcjami) ułatwiają prawidłowe
tworzenie i odczytywanie stałych z pamięci programu.
•
•
pgm_read_byte( addr ) - jako argument pobiera adres pamięci programu a zwraca
wartość znajdującą się pod tym adresem.
PSTR( s ) - tworzy łańcuch znaków w pamięci programu i zwraca adres do niego.
Tworzenie stałych w pamięci programu
Tutaj stałej LINE przypisano wartość 1:
prog_char LINE = {1};
Czytanie stałych z pamięci programu
Spod adresu LINE czytana jest wartość i zapamiętana w zmiennej res:
char res = pgm_read_byte(&LINE);
Tworzenie tablic w pamięci programu
Na przykład tak:
prog_char TAB[10] = {0,1,2,3,4,5,6,7,8,9};
jest również możliwe utworzenie otwartej tablicy (bez określenia wielkości - kompilator
robi to za nas) ...
prog_char TAB[] = {0,1,2,3,4,5,6,7,8,9};
Czytanie wartości z tablic w pamięci programu
Spod adresu TEN[5] czytana jest wartość i zapamiętana w zmiennej res:
char res = pgm_read_byte(&TEN[5]);
Tworzenie łańcucha znaków
Tworzy łańcuch w pamięci programu i zwraca wskaźnik do niego w LINE1:
char *LINE1 = PSTR("Pierwsza linia na LCD");
lub taki przykład:
tworzy łańcuch w pamięci programu i zwraca wskaźnik do niego w LINE2:
33
char LINE2[] __attribute__ ((progmem)) = "to jest drugi wiersz";
inny przykład:
tworzy łańcuch w pamięci programu i zwraca wskaźnik do niego w text1.
char text1[] PROGMEM = "Tak tez mozna definiowac teksty";
Czytanie łańcucha znaków
Zmienna firstchar zawiera teraz pierwszy znak z łańcucha LINE2.
firstchar = pgm_read_byte (&LINE2[0]);
Zmienna lastchar zawiera teraz 43 znak z łańcucha LINE1 ponieważ w języku C wszystkie
tablice są indeksowane od zera.
lastchar = pgm_read_byte (LINE1+42);
Program przykładowy
Naszym celem będzie napisanie programu, który pokaże zachowanie stałych i tablic
umieszczonych w pamięci programu. Komunikacja z użytkownikiem będzie zrealizowana
tak jak w poprzednim przykładzie za pomocą łącza szeregowego.
// Testowanie stałych w pamięci programu
#include <avr/io.h>
#include "uart.h"
prog_char STALA = {'1'};
ASCII
// dostęp do rejestrów
// obsługa portu szeregowego
// stała o wartości będącej znakiem 1
prog_char NEWLINE[] = {'\n','\r',0}; // tablica zawiarająca znaki nowej
linii
prog_char
CLEAR[]
=
{27,'[','H',27,'[','2','J',0};
czyszczącza ekran terminala
//
j.w.
ale
prog_char HOME[] = {27,'[','H',0}; // j.w. ale przestawiająca kursor na
początek
char text1[] __attribute__ ((progmem)) = "To jest pierwszy wiersz ....";
char text2[] PROGMEM = "Tak latwiej mozna definiowac teksty :-)";
char VERSION[] PROGMEM = __DATE__" "__TIME__; // w ten sposób można
pilnować wersji :-)
// wysyła na port szeregowy łańcuch umieszczony w PAMIECI PROGRAMU
// jako argument przyjmuje wskaźnik (adres pierwszego znaku) do niego
void _UART_putstr_P(const char *s)
{
register u08 c;
while ((c = pgm_read_byte(s++)))
{
UART_putchar(c);
}
34
}
int main( void )
{
char res;
UART_init();
_UART_putstr_P(CLEAR);
CLEAR
res = pgm_read_byte(&STALA);
UART_putchar(res);
// program główny
// deklaracja zmiennej
// inicjalizacja portu szeregowego
// wyślij na port szeregowy znaki
// pobierz wartość stałej STALA
// 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()
umieszczonymi w pamięci FLASH"));
_UART_putstr_P(NEWLINE);
znak nowej linii
nie
dziala
z
łańcuchami
// wyślij na port szeregowy
_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);
wszcześniej w tablicy text1
_UART_putstr_P(NEWLINE);
znak nowej linii
UART_getchar();
szeregowego
// wyślij tekst zdefiniowany
// wyślij na port szeregowy
// czekaj na znak z portu
_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);
wszcześniej w tablicy text2
_UART_putstr_P(NEWLINE);
znak nowej linii
UART_getchar();
szeregowego
// wyślij tekst zdefiniowany
// wyślij na port szeregowy
// czekaj na znak z portu
_UART_putstr_P(PSTR("Czyścimy ekran... "));
UART_getchar();
// czekaj na znak z portu
szeregowego
_UART_putstr_P(CLEAR);
// wyślij na port szeregowy znaki
CLEAR
_UART_putstr_P(PSTR("Data kompilacji: "));
_UART_putstr_P(VERSION);
// wyślij na port szeregowy
wersję programu
35
while(1);
// pętla nieskończona
}
Po skompilowaniu i uruchomieniu przykładu ekran terminala powinien zostać
wyczyszczony przez wysłanie odpowiedniej sekwencji znaków ANSI zdefiniowanej w
tablicy CLEAR. Następnie na ekranie terminala ukaże się "1" (jedynka) jako efekt
wyprowadzenia na port szeregowy stałej STALA. Kolejne instrukcje powodują odczyt
poszczególnych bajtów z tablicy NEWLINE i wysyłanie ich na port szeregowy co skutkuje
przejściem kursora na terminalu do nowej linii. W kolejnej linii użyto funkcji putstr() do
wyłania tekstu umieszczonego w pamięci programu. Na terminalu powinien się pojawić
jakiś przypadkowy znak. Takie zachowanie jest dowodem na to, że do wysyłania stałych
zdefiniowanych w pamięci programu należy używać specjalnie do tego stworzonych
funkcji. Uzasadnienie tego jest proste: mikrokontrolery AVR mają architekturę harwardzką
co oznacza, że posiadają oddzielne przestrzenie adresowe dla danych oraz programu i do
dostępu do nich używają zupełnie innych technik. W następnej linii została użyta
zdefiniowana wcześniej funkcja putstr_P() do wysłania tekstu z pamięci programu - tutaj
widać efekt. W dalszej części programu znajduje się demonstracja kilku sekwencji ANSI
do sterowania terminalem. Na samym końcu znajduje się przykład wykorzystania
predefiniowanych symboli __DATE__ i __TIME__ do wpisywania ich w pamięć programu
tak aby np. mieć w przyszłości kontrolę nad wersją programu (zawierają one datę i czas
kompilacji programu w postaci tekstowej).
AVR-GCC - pamięć EEPROM
W odróżnieniu do prostych definicji zmiennych w pamięci SRAM, użycie pamięci
EEPROM, do której dostęp odbywa się przez specjalne rejestry wymaga użycia
specjalnych funkcji dla uzyskania dostępu do niej. Deklaracje tych funkcji znajdują się w
pliku nagłówkowym avr/eeprom.h.
Jednak należy uważać by nie stosować zmiennych w EEPROM, do których często
zapisywane będą dane - np. zmienna sterująca pętli. Dzieje się tak dlatego, iż nominalnie
pamięć EEPROM ma ograniczona możliwość przeprogramowywania. Producent
gwarantuje tylko 100 tysięcy operacji zapisu. Łatwo więc w tym przypadku o
przekroczenie tej liczby w dość krótkim czasie. Dlatego nie należy pochopnie używać tej
pamięci, i w żadnym wypadku nie w instrukcjach pętli!
Najważniejsze funkcje zawarte w pliku avr/eeprom.h
eeprom_write_byte ( *adres, val) - zapisuje wartość val pod adres adres.
eeprom_read_byte ( *adres ) - czyta zawartość pamięci spod adresu adres.
eeprom_read_word ( *adres ) - czyta 16 bitową zawartość pamięci spod adresu adres.
eeprom_read_block ( *buf, *adres, n) - czyta n wartości od adresu adres i zapisuje do
pamięci SRAM w *buf.
eeprom_is_ready () - zwraca 1 jeśli pamięć jest wolna lub 0 jeśli na niej jest wykonywana
jakaś operacja (trwa zapis).
Program przykładowy
Naszym celem będzie napisanie programu, który pokaże zachowanie danych
umieszczonych w wewnętrznej pamięci EEPROM. Komunikacja z użytkownikiem będzie
36
zrealizowana tak jak w poprzednich przykładach za pomocą terminala podłączonego do
łącza szeregowego. Podczas jego testowania należy zwrócić uwagę na powstały w wyniku
kompilacji plik .eep zawierający dane przeznaczone do wpisania w pamięć EEPROM
mikrokontrolera.
// Testowanie pamięci danych EEPROM
#include <avr/io.h>
#include <avr/eeprom.h>
#include "uart.h"
#define RADIX
10
// dostęp do rejestrów
// dostęp do pamięci EEPROM
// obsługa portu szeregowego
// 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)
{
char buffer[32];
eeprom
uint8_t val1 = 123;
uint8_t val2;
UART_init();
// program główny
// bufor dla tablicy odczytywanej z
// inicjalizacja portu szeregowego
UART_putstr_P(PSTR("\n\r1. Odczyt z pamięci EEPROM\n\reeprom_val
"));
val2 = eeprom_read_byte(&eeprom_val); // odczyt z eeprom
UART_putint(val2,RADIX);
->
UART_putstr_P(PSTR("\n\r2. Zapis do pamięci EEPROM\n\reeprom_val
eeprom_val+1"));
eeprom_write_byte (&eeprom_val, ++val2); // zapis do eeprom
<-
UART_putstr_P(PSTR("\n\r3. Odczyt z pamięci EEPROM\n\reeprom_val
"));
val2 = eeprom_read_byte(&eeprom_val); // odczyt z eeprom
UART_putint(val2,RADIX);
->
UART_putstr_P(PSTR("\n\r4. Zapis do pamięci EEPROM\n\rKomorka E2END <"));
UART_putint(val1,RADIX);
eeprom_write_byte((uint8_t*)E2END, val1); // zapis do eeprom
UART_putstr_P(PSTR("\n\r5. Odczyt z pamięci EEPROM\n\rKomorka E2END ->
"));
37
val2 = eeprom_read_byte((uint8_t*)E2END); // odczyt eeprom spod adresu
E2END
UART_putint(val2,RADIX);
UART_putstr_P(PSTR("\n\r6. Odczyt wersji z pamięci EEPROM za pomocą
eeprom_read_block()\n\r"));
eeprom_read_block(&buffer,&ver,32);
UART_putstr(buffer);
UART_putstr_P(PSTR("\n\r7. Odczyt wersji z pamięci EEPROM za pomocą
UART_putline_E()\n\r"));
_UART_putstr_E(ver);
while(1);
// pętla nieskończona
}
jest zdefiniowana tablica o nazwie ver[] również w pamięci EEPROM zawierająca ciąg
znaków z datą i czasem kompilacji programu. Dostęp do tych zmiennych jest możliwy
przez specjalne funkcje dostępu do pamięci EEPROM. W kolejnych liniach mamy
zdefiniowaną funkcję UART_putstr_E(*s), która służy do wysyłania przez port zeregowy
łańcucha znajdującego się w pamięci EEPROM. Dalej znajduje się program główny funkcja main(), w której są zadeklarowane zmienne: buffer, val1 z zainicjowaną wartością
oraz val2. Po zainicjowaniu portu szeregowego przez funkcję UART_init() następuje
wypisanie tekstu:
1. Odczyt z pamięci EEPROM eeprom_val ->
przez wywołanie funkcji UART_putstr_P(). Następnie za pomocą instrukcji
val2 = eeprom_read_byte((u08*)&eeprom_val);
jest odczytywana wartość z komórki pamięci EEPROM o adresie eeprom_val,
przepisywana do zmiennej val2 i za pomocą funkcji UART_putint() wyprowadzana na port
szeregowy w postaci liczby o podstawie zdefiniowanej w RADIX. Później jest wypisywany
komunikat o zapisie do pamięci EEPROM zwiększonej o 1 wartości zmiennej eeprom_val.
Wartość ta jest zapisywana instrukcją:
eeprom_write_byte ((u08*)&eeprom_val, ++val2);
Zauważmy, że w powyższej instrukcji zostało napisane ++val2 (preinkrementacja) a nie
val2++ (postinkrementacja) ponieważ wtedy została by wpisania niezmieniona wartość a
zostałaby ona zmieniona po wykonaniu funkcji eeprom_write_byte(). Później (w trzecim
kroku) jest ponownie odczytywana wartość zmiennej eeprom_val - powinna być większa o
1 od tej w kroku 1. Najlepiej to widać restartując mikrokontroler (lub nawet lepiej poprzez
wyłączanie i ponowne włączanie układu) - nie tracimy zawartości pamięci - możemy
zliczać programowo np. ilość restartów mikrokontrolera. W kolejnych krokach mamy
przedstawione zastosowanie stałej E2END, która zawiera adres ostatniej komórki pamięci
EEPROM. Następnie mamy przykład zastosowania funkcji eeprom_read_block() oraz
zdefiniowanej na początku programu UART_putstr_E() do odczytu danych zawartych w
tablicy znajdującej się w pamięci EEPROM.
AVR-GCC - obsługa przerwań
38
Przerwania są stosowane w przypadku, gdy program ma szybko reagować na zdarzenia.
Powyższe zdarzenia mogą być generowane zarówno przez urządzenia wewnętrzne jak i
zewnętrzne. Każde z przerwań może być maskowane przez kasowanie bitów w
odpowiednich rejestrach i w rejestrze statusu.
Aby użyć funkcji obsługi przerwań należy włączyć plik avr/interrupt.h.
Funkcja, która ma służyć do obsługi przerwania musi mieć nazwę składającą się ze słowa
SIGNAL lub INTERRUPT.
SIGNAL (nazwa_uchwytu)
{
// Instrukcje tu zawarte będą wykonywane jako obsługa przerwania
}
Funkcja obsługi przerwania z nazwą SIGNAL będzie obsługiwana z wyłączoną obsługą
innych przerwań.
lub:
INTERRUPT (nazwa_uchwytu)
{
// Instrukcje tu zawarte będą wykonywane jako obsługa przerwania
}
Funkcja obsługi przerwania z nazwą INTERRUPT będzie obsługiwana z włączoną obsługą
innych przerwań - jej realizacja może być przerwana przez przerwanie o wyższym
priorytecie.
Priorytety przerwań należy sprawdzić w dokumentacji mikrokontrolera.
Rejestry systemu obsługi przerwań:
•
•
•
TIFR - Znaczniki przerwania z liczników/czasomierzy
TIMSK - Maska przerwań liczników/czasomierzy
GIMSK - Globalna maska przerwań
Funkcje zdefiniowane w interrupt.h:
•
•
•
•
sei() - Włącza obsługę przerwań. Makro.
cli() - Wyłącza obsługę przerwań. Makro.
enable_external_int(ints) - Wpisuje ints do rejestrów EIMSK lub GIMSK
timer_enable_int(ints) - Wpisuje ints do rejestru TIMSK
Program przykładowy
Naszym celem będzie napisanie programu, który pokaże w możliwie najprostszy sposób
wykorzystanie przerwań. Komunikacja z użytkownikiem będzie zrealizowana za pomocą
przycisków podłączonych z jednej strony do masy a z drugiej do linii INT0 i INT1
mikrokontrolera oraz diod LED podłączonych do portu B.
// Testowanie przerwań zewnętrznych
#include <avr/io.h>
#include <avr/interrupt.h>
// dostęp do rejestrów
SIGNAL (SIG_INTERRUPT0)
39
{
PORTD = 0x1d;
// zgaś diode LED
}
SIGNAL (SIG_INTERRUPT1)
{
PORTD = 0x0d;
// zapal diode LED
}
int main(void)
{
DDRD = 0x10;
jako wejścia (przyciski)
PORTD = 0x0d;
(przyciski)
// program główny
// bit 4 PortD jako wyjscie (LED) pozostale
// podciąganie bitów 3 i 4 PortD
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();
while(1);
// włącz obsługę przerwań
// pętla nieskończona
}
W tym przykładzie została zaimplementowana obsługa dwóch przerwań zewnętrznych
INT0 i INT1 za pomocą słów SIGNAL z odpowiednimi parametrami: SIG_INTERRUPT0
dla INT0 i SIG_INTERRUPT1 dla INT1. Na początku programu głównego są ustawiane
parametry portów.
W następnej kolejności za pomocą instrukcji: outb(GIMSK, _BV(INT0)|_BV(INT1)); są
ustawiane bity INT0 i INT1 w rejestrze GIMSK (globalnej maski przerwań) co w efekcie
przygotowuje
mikrokontroler na obsługę przerwań zewnętrznych INT0 i INT1. Aby program uczynić
łatwiej przenośnym na inne kontrolery AVR proponuję zastąpić powyższą instrukcję taką:
enable_external_int(_BV(INT0)|_BV(INT1));
Kolejna
instrukcja:
outb(MCUCR,
_BV(ISC01)|_BV(ISC11)); powoduje, że mikrokontroler będzie generował przerwania
podczas opadającego zbocza sygnału na wejściach INT0 lub INT1. Jednak aby przerwania
były rzeczywiście obsługiwane należy zezwolić mikrokontrolerowi na ich obsługę. Robi to
kolejna instrukcja: sei();.
Dalej mamy już tylko znaną z poprzednich przykładów instrukcję while(1); czyli pętlę
nieskończoną. Wydawało by się, że program tu zakończy działanie, tak jak to było w
poprzednich przypadkach, lecz tak nie jest. Wystarczy, że na dowolne z wejść INT1 lub
INT0 podamy na krótko stan niski (np. z przycisku) a diody LED podłączone do portu B
zapalą się lub zgasną. Widać tu więc wyraźnie na czym polegają przerwania - są one
obsługiwane niezależnie od programu głównego.
AVR-GCC - licznik/czasomierz TIMER 0
Timer 0 jest to 8-bitowy licznik/czasomierz mogący zliczać impulsy w zakresie od 0 do
255. Pracując w trybie czasomierza używa wewnętrznego sygnału zegarowego, w trybie
licznika - zewnętrznego sygnału pobranego z odpowiedniej końcówki układu AVR (z
której - należy sprawdzić w dokumentacji układu). Ponadto może generować przerwania,
40
które następnie należy obsłużyć. Również program może odczytywać jego wartość w
dowolnym momencie (tzw. polling) i na tej podstawie podejmować odpowiednie działania.
Tryb licznika
W tym trybie są zliczane zmiany stanu na końcówce T0. Zmiany stanu na końcówce T0 są
synchronizowane z częstotliwością zegarową CPU.Aby te zmiany były zauważone,
minimalny czas pomiędzy tymi zmianami musi być większy od okresu zegara CPU. Stan
na wejściu T0 jest próbkowany w czasie narastającego zbocza wewnętrznego sygnału
zegarowego CPU. Aby włączyć zliczanie impulsów należy ustawić odpowiednią
kombinację bitów w rejestrze TCCR0.
Programy przykładowe
Prosty przykład na wykorzystanie licznika 0 do zliczania impulsów na wejściu T0 bez
użycia przerwań. Obsługa tego programu polega na częstym podawaniu impulsów na
wejście T0 mikrokontrolera (np. poprzez zwieranie z masą). Diody LED podłączone do
linii portu C sygnalizują binarnie ilość przepełnień licznika.
// Testowanie licznika 0 (polling)
#include <avr/io.h>
// dostęp do rejestrów
uint8_t led;
int main( void )
{
DDRC = 0xFF;
// PortC jako wyjścia
TCNT0 = 0xFE;
// wartość początkowa T/C0
TCCR0 = _BV(CS01)|_BV(CS02);
// T/C0 zlicza opadające
// zbocza na wejściu T0
while(1)
{
loop_until_bit_is_set(TIFR,TOV0);
// ta pętla sprawdza bit przepełnienia
// w rejestrze TIFR
TCNT0 = 0xFE;
// przeładuj T/C0
PORTC = ~led++;
// wyślij ilość przepełnień na PortC
sbi(TIFR,TOV0);
// jeśli wpiszemy 1 do bitu TOV0
// to ten bit zostanie skasowany
// przy następnym przepełnieniu licznika 0
}
}
Na początku są ustalane kierunki danych w porcie C. Następnie jest ustawiana wartość
początkowa licznika TCNT0 na 0xFE co oznacza, że po dwóch impulsach na wejściu T0
pojawi się przepełnienie licznika (licznik 8 bitowy liczy w górę czyli 0xFE, 0xFF, 0x00) i
w konsekwencji ustawienie bitu TOV0 w rejestrze TIFR. Kolejna instrukcja konfiguruje
wejście licznika w taki sposób aby zliczanie następowało podczas opadającego zbocza na
wejściu T0. W pętli nieskończonej sprawdzany jest stan bitu TOV0. Następnie jest
przeładowywany jest licznik 0 wartością 0xFE, inkrementowana zmienna led określająca
liczbę przepełnień. Zmienna ta po zanegowaniu jest wystawiana na PORTC. Ostatnią
41
instrukcją w tej pętli jest ustawienie bitu TOV0, które w efekcie powoduje jego
skasowanie.
Bardziej złożony przykład na wykorzystanie licznika 0 do zliczania impulsów na wejściu
T0. Dzięki wykorzystaniu przerwań można całkowicie oddzielić zliczanie impulsów od
programu głównego. Obsługa tego programu polega na częstym podawaniu impulsów na
wejście T0 mikrokontrolera (np. poprzez zwieranie go z masą) i wysyłaniu dowolnego
znaku na port szeregowy z terminala. Na terminalu otrzymany wyniki działania programu.
// Testowanie licznika 0 (przerwania)
#include
#include
#include
#include
<avr/io.h>
<avr/interrupt.h>
<avr/signal.h>
"uart.h"
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
// 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();
DDRB = 0xFF;
PORTB
TIMSK
TCNT0
TCCR0
sei();
=
=
=
=
// inicjalizacja portu szeregowego
// wszystkie linie PORTB jako wyjścia
// nawet linia PB0 - wejście T0
0xFF;
// wszystkie linie PORTB w stan wysoki
_BV(TOIE0);
// włącz obsługę przerwań T/C0
0x00;
// wartość początkowa T/C0
_BV(CS01)|_BV(CS02);
// wyzwalanie z T0 opad. zboczem
// włącz obsługę przerwań
while(1)
// pętla nieskończona
{
licznik=(overflow<<8)|TCNT0;
// oblicz wartość
PRINT_VALUE(PSTR("overflow="),overflow);
// overvlow na UART
PRINT_VALUE(PSTR("TCNT0="),TCNT0);
// TCNT0 na UART
42
PRINT_VALUE(PSTR("licznik (overflow*256)+TCNT0="),licznik);
// licznik na UART
UART_putstr_P(PSTR("Wyślij dowolny znak..."));
UART_putstr_P(NEWLINE);
UART_putstr_P(NEWLINE);
UART_getchar();
// czekaj na znak z UART
}
}
Tryb czasomierza
W tym trybie licznik jest taktowany wewnętrznym sygnałem zegarowym. Po każdym
takcie wartość TCNT0 jest zwiększana o jeden. Sygnał taktujący jest wynikiem podziału
przez x w stosunku do zegara procesora (CLK). Dzielnik x może przyjąć jedną z wartości:
1, 8, 64, 256, 1024. Np. x=1024 - TCNT0 jest zwiększany o 1 po 1024 taktach zegara
procesora. Współczynnik wstępnego podziału jest ustalany przez ustawianie następujących
bitów rejestru TCCR0.
Programy przykładowe
// Testowanie timera 0 (polling)
#include <avr/io.h>
// dostęp do rejestrów
uint8_t led;
uint8_t state;
int main( void )
{
DDRC = 0xFF; // wszystkie linie PORTC jako wyjścia
TCNT0 = 0; // wartość początkowa T/C0
TCCR0 = _BV(CS00)|_BV(CS02); // preskaler ck/1024
while(1)
{
do
// ta pętla sprawdza bit przepełnienia rejestrze TIFR
state = inb(TIFR) & _BV(TOV0);
while (state != _BV(TOV0));
PORTC = ~led++;
// wyprowadź wartość licznika
// przepełnień na PORTB
TIFR = _BV(TOV0);
// jeśli 1 wpiszemy do bitu TOV0 to
// ten bit powinien zostac skasowany
// przy następnym przepełnieniu licznika 0
}
}
W programie zostały zadeklarowane dwie zmienne globalne: licznik - zawiera liczbę
impulsów na wejściu T0 oraz overflow - programowy licznik przerwań od przepełnień
licznika TCNT0. Zostały one zdefiniowane ze słowem kluczowym volatile, które
powoduje wyłączenie optymalizacji w funkcjach używających tych zmiennych - w ten
sposób można modyfikować je w funkcjach obsługi przerwań i sprawdzać ich wartości
poza nimi. Dalej widać prostą definicję funkcji obsługi przerwania od przepełnienia
licznika 0. Znajduje się tam tylko inkrementacja zmiennej overflow. Na początku
programu głównego main() są ustalane kierunki danych w porcie B. Tutaj ustawiliśmy
43
linię PB0 jako wejście z uwagi na jej alternatywną funkcję: wejście licznika T0. Następnie
jest ustawiana wartość początkowa licznika TCNT0 na 0. W kolejnej instrukcji poprzez
ustawienie bitu TOIE0 w rejestrze TIMSK zostało włączone generowanie przerwań od
przepełnienia licznika TCNT0. Podobnie poprzez ustawienie bitów CS01 i CS02 w
rejestrze TCCR0 zostało włączone taktowanie licznika z wejścia T0. Przed wejściem do
głównej pętli programu instrukcją sei() została włączona obsługa przerwań. Pierwszą
instrukcją w głównej pętli jest UART_getchar() czekająca na dowolny znak z portu
szeregowego. Po odebraniu dowolnego znaku z tego portu obliczana jest wartość licznika
impulsów na wejściu T0 mikrokontrolera wg wzoru:
licznik = 256*overflow + TCNT0
jednak mnożenie przez 256 łatwiej i szybciej mikrokontroler wykona stosując przesuwanie
w lewo o 8 bitów a dodawanie poprzez sumę logiczną. W związku z powyższym wartość
licznika jest obliczana w następujący sposób:
licznik=(overflow<<8)|(inb(TCNT0));
Dalej występuje prezentacja wartości poszczególnych zmiennych i wyniku obliczeń.
Poniższy przykład prezentuje działanie licznika/czasomierza 0 w trybie czasomierza (ang.
Timer). Obsługa tego licznika jest realizowania poprzez przerwanie generowane podczas
przepełnienia licznika SIGNAL(SIG_OVERFLOW0). Stan licznika przepełnień licznika 0
jest prezentowany przez diody LED podłączone do portu B mikrokontrolera. Efekt
działania programu jest identyczny z tym z wcześniejszego listingu, lecz program główny
nie jest angażowany w obsługę licznika.
// Testowanie timera 0 (przerwania)
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>
#define T0_INIT
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
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;
if (overflow>0)
overflow--;
tbi(PORTD,PD5);
}
int main(void)
{
DDRD = 0xFF;
PORTD = 0xFF;
TIMSK = _BV(TOIE0);
// przeładuj timer 0
// dekrementuj
// przełącz stan LED na PB1
// wszystkie linie PORTD jako wyjścia
// wygaś diody LED
// włącz obsługę przerwań T/C0
44
TCNT0 = T0_INIT;
// wartość początkowa T/C0
TCCR0 = _BV(CS00)|_BV(CS02); // preskaler 1024
sei();
while(1)
{
cbi(PORTD,PD4);
overflow=4;
while(overflow);
sbi(PORTD,PD4);
overflow=16;
while(overflow);
// włącz obsługę przerwań
// pętla nieskończona
// zapal LED na PD4
// inicjuj zmienną overflow
// zmienna overflow jest
// dekrementowana w przerwaniu
// zgaś LED na PD4
// inicjuj zmienną overflow
// zmienna overflow jest
// dekrementowana w przerwaniu
}
}
AVR-GCC - licznik/czasomierz TIMER 1
Licznik/czasomierz 1 posiada 16 bitową organizację. Z tego względu można go użyć do
zliczania większej ilości impulsów lub odmierzania dłuższych (lub dokładniejszych)
okresów czasu. Zliczane wartości mieszczą się w zakresie od 0x0000 do 0xFFFF. Dostęp
do nich jest możliwy przez dwa 8-bitowe rejestry. Oprócz swej funkcji podstawowej
licznik 1 może być użyty w trybie porównywania / przechwytywania (ang. compare /
capture) oraz sterowania wyjściami z modulowaną szerokością impulsu (PWM).
Rejestry licznika/czasomierza 1
Nazwa | Funkcja
| Uwagi
--------------------------------------------------------------------------------------------TCCR1A | Rejestr kontrolny A
|
TCCR1B | Rejestr kontrolny B
|
TCCR1L | Stan licznika L
| możliwy dostęp do całej zawartości
licznika przez TCCR1
TCCR1H | Stan licznika H
| j.w.
OCR1AL | Porównywanie - rejestr A L | możliwy dostęp do całej zawartości
rejestru przez OCR1A
OCR1AH | Porównywanie - rejestr A H | j.w.
OCR1BL | Porównywanie - rejestr B L | możliwy dostęp do całej zawartości
rejestru przez OCR1B
OCR1BH | Porównywanie - rejestr B H | j.w.
ICR1L | Przechwytywanie L
| możliwy dostęp do całej zawartości
rejestru przez ICR1
ICR1H | Przechwytywanie H
| j.w.
Tryb licznika
W tym trybie są zliczane zmiany stanu na końcówce T1. Zmiany stanu na końcówce T1 są
synchronizowane z częstotliwością zegarową CPU. Aby te zmiany były zauważone,
minimalny odstęp czasu pomiędzy tymi zmianami musi być większy od okresu zegara
CPU. Stan na wejściu T1 jest próbkowany w czasie narastającego zbocza wewnętrznego
sygnału zegarowego CPU. Aby włączyć zliczanie impulsów należy ustawić odpowiednią
kombinację bitów w rejestrze TCCR1B.
45
Bity rejestru TCCR1B określające zliczanie impulsów zewnętrznych przez licznik 1
CS12 | CS11 | CS10 | Opis
-----------------------------------------------------1 | 1
| 0
| Opadające zbocze na końcówce T1
1 | 1
| 1
| Narastające zbocze na końcówce T1
Tryb czasomierza
W tym trybie licznik jest taktowany wewnętrznym sygnałem zegarowym. Po każdym
takcie wartość licznika jest zwiększana o jeden. Sygnał taktujący jest wynikiem podziału
przez x w stosunku do zegara procesora. Dzielnik x może przyjąć jedną z wartości: 1, 8,
64, 256, 1024. Np. x=1024 - licznik jest zwiększany o 1 po 1024 taktach zegara procesora.
Współczynnik wstępnego podziału jest ustalany przez ustawianie odpowiednich bitów
rejestru TCCR1B.
Bity rejestru TCCR1B określające częstotliwość wejściową czasomierza 1
CS12 | CS11 | CS10 | Częstotliwość
---------------------------------0
| 0
| 0
| 0
0
| 0
| 1
| CLK
0
| 1
| 0
| CLK/8
0
| 1
| 1
| CLK/64
1
| 0
| 0
| CLK/256
1
| 0
| 1
| CLK/1024
Programy przykładowe
Naszym celem będzie napisanie programów, które pokażą w możliwie najprostszy sposób
wykorzystanie licznika/czasomierza 1 do zliczania impulsów pochodzących z
wewnętrznego preskalera. W obu przypadkach do komunikacji z użytkownikiem będą
wykorzystane diody LED podłączone do portu B. Pierwszy przykład przedstawia
wykorzystanie cyklicznego sprawdzania jego stanu (polling) natomiast drugi realizuje
dokładnie to samo ale poprzez obsługę przerwania.
// Testowanie timera 1 (polling)
#include <avr/io.h>
// dostęp do rejestrów
uint8_t led;
uint8_t state;
int main( void )
{
DDRC = 0xFF; // wszystkie linie PORTB jako wyjścia
TCNT1 = 0xFF00; // wartość początkowa T/C1
TCCR1A = 0x00; // T/C1 w trybie czasomierza
TCCR1B = _BV(CS10)|_BV(CS12); // preskaler ck/1024
while(1)
{
do
// ta pętla sprawdza bit przepełnienia rejestrze TIFR
state = inb(TIFR) & _BV(TOV1);
while (state != _BV(TOV1));
PORTC = ~led++; // wyslij licznik przepełnień na PORTB
46
TCNT1 = 0xFF00; // wartość początkowa T/C1
TIFR = _BV(TOV1);
// jeśli ustawimy bit TOV1 to
// ten bit zostanie skasowany
// przy następnym przepełnieniu licznika 1
}
}
// Testowanie timera 1 (przerwania)
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
uint8_t led;
SIGNAL (SIG_OVERFLOW1)
{
PORTC = ~led++;
TCNT1 = 0xFF00;
}
// wyświetl na LED-ach
// przeładuj timer 1
int main(void)
{
DDRC = 0xFF;
// wszystkie linie PORTC jako wyjścia
TIMSK = _BV(TOIE1);
// włącz obsługę przerwań T/C1
TCNT1 = 0xFF00;
// wartość początkowa T/C1
TCCR1A = 0x00;
// włącz tryb czasomierza T/C1
TCCR1B = _BV(CS10)|_BV(CS12);
// preskaler ck/1024
sei();
// włącz obsługę przerwań
while(1);
// pętla nieskończona
}
Tryb porównywania
Licznik 1 wyposażony jest w funkcję porównywania jego zawartości z wartością zadaną.
Do tego celu używa dwóch rejestrów OCR1A oraz OCR1B. Ich zawartość jest stale
porównywana z zawartością licznika TCNT1. Pozytywny wynik porównania może
wywołać wyzerowanie licznika TCNT1 lub zmianę stanu na końcówkach OC1A lub
OC1B. Bity CS10, CS11 i CS12 rejestru TCCR1B definiują przeskalowanie i źródło
taktowania.
Wybór trybu pracy wyjścia OC1A (bity rejestru TCCR1A
COM1A1 | COM1A0 | Opis
--------------------------------------------------------------0
|
0
| Licznik 1 odłączony od końcówki OC1A
0
|
1
| Przełączanie stanu końcówki OC1A na przeciwny
1
|
0
| Zerowanie stanu końcówki OC1A
1
|
1
| Ustawianie stanu końcówki OC1A
Wybór trybu pracy wyjścia OC1B (bity rejestru TCCR1A)
COM1B1 | COM1B0 | Opis
---------------------------------------------------------------
47
0
0
1
1
|
|
|
|
0
1
0
1
|
|
|
|
Licznik 1 odłączony od końcówki OC1B
Przełączanie stanu końcówki OC1B na przeciwny
Zerowanie stanu końcówki OC1B
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>
#include <avr/interrupt.h>
#include <avr/signal.h>
// dostęp do rejestrów
// funkcje sei(), cli()
// 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)
{
cbi(PORTC,PB2);
}
SIGNAL (SIG_OVERFLOW1)
{
TCNT1 = delay;
sbi(PORTC,PC2);
}
// przerwanie od porównania
// zapal diodę na PC2
// przerwanie od przepełnienia
// przeładuj TIMER1
// zgaś diodę na PC2
int main(void)
// program główny
{
DDRC = 0xFF;
// PORTC jako wyjścia dla LED
PORTC = 0xFF;
// zgaś diody LED na PORTC
DDRD = 0x00;
// PORTD jako wejścia dla
przycisków
PORTD = 0xFF;
// podciągaj wejścia PORTD
delay = 0x0000;
// domyślna wartość dla TIMERA1
compare = 0x7FFF;
// domyślna wartość dla porównania
TIMSK = _BV(TOIE1)|_BV(OCIE1A);
// włącz przerwania
//
od
przepełnienia
i
porównania
TIMERA1
TCNT1 = delay;
// zainicjuj TIMER1
TCCR1A = 0x00;
// czasomierz 1 bez dodatków
TCCR1B = _BV(CS00);
// taktowany F_CPU
sei();
// włącz obsługę przerwań
while(1)
{
if (bit_is_clear(PIND,PD2))
// jeżeli zwarto PD4 z masą
delay+=0x80;
// zwiększ delay o 128
loop_until_bit_is_set(PIND,PD2);
// czekaj na zwolnienie PD4
48
if (bit_is_clear(PIND,PD3))
// jeżeli zwarto PD3 z masą
delay-=0x80;
// zmniejsz delay o 128
loop_until_bit_is_set(PIND,PD3);
// czekaj na zwolnienie PD3
if (bit_is_clear(PIND,PD6))
// jeżeli zwarto PD6 z masą
compare+=0x80;
// zwiększ compare o 128
loop_until_bit_is_set(PIND,PD6);
// czekaj na zwolnienie PD6
if (bit_is_clear(PINB,PB0))
// jeżeli zwarto PB0 z masą
compare-=0x80;
// zmniejsz compare o 128
loop_until_bit_is_set(PINB,PB0);
// czekaj na zwolnienie PB0
#ifdef __AVR_AT90S2313__
OCR1 = compare;
#else
OCR1A = compare;
#endif
// jeśli kompilujemy dla 2313
// wpisz compare do OCR1
// w przeciwnym wypadku:
// wpisz compare do OCR1A
}
}
Tryb przechwytywania
Licznik 1 wyposażony jest w funkcję przechwytywania (ang. Catpure) jego chwilowej
zawartości w specjalnym rejestrze jako reakcję na zdarzenie zewnętrzne. Kiedy zostanie
wykryte narastające lub opadające zbocze sygnału (w zależności od stanu bitu ICES1 w
rejestrze TCCR1B) na wejściu ICP (input capture), bieżąca wartość licznika zostaje
przepisana do 16 bitowego rejestru ICR1 (ICR1L, ICR1H) oraz ustawia znacznik ICF1.
Dodatkowo można uaktywnić funkcję eliminacji zakłóceń na wejściu ICP poprzez
ustawienie bitu ICNC1 w rejestrze TCCR1B. Przechwytywanie wartości licznika /
czasomierza 1 można również wywołać zmianą stanu na wyjściu wbudowanego w układ
komparatora analogowego (ustawiony bit ACIC rejestru ACSR). Jest też możliwe
wygenerowanie przerwania (SIG_INPUT_CAPTURE), jeżeli ustawiono bit TICIE1 w
rejestrze TIMSK. Bardzo ważna jest kolejność odczytu ośmiobitowych części rejestru
ICR1 - najpierw należy odczytać mniej znaczący bajt (ICR1L) a następnie bardziej
znaczący (ICR1H). Można również odczytać całą 16 bitową zawartość licznika poprzez
przypisanie np. var=ICR1.
Program przykładowy
Naszym celem będzie napisanie programu, który przedstawi nam przechwytywanie
zawartości licznika do specjalnego rejestru. W tym przykładzie urządzeniem wyjściowym
będzie terminal podłączony do portu szeregowego mikrokontrolera. Wejście ICP
mikrokontrolera należy podłączyć do zasilania przez rezystor 10k. W przypadku gdy
używamy mikrokontrolera AT90S2313 można ww. rezystora nie stosować wymuszając
programowo "podciągnięcie" wejścia ICP (końcówka PD6) do zasilania np. za pomocą
wstawienia w odpowiedniej instrukcji na początku funkcji main() np. sbi(PORTD,PD6).
Wejście ICP należy zwierać co jakiś czas z masą, co pozwoli zaobserwować
przechwytywanie zawartości rejestru TCNT1 w rejestrze ICR1.
// Testowanie licznika 1 (przechwytywanie)
49
#include
#include
#include
#include
<avr/io.h>
<avr/interrupt.h>
<avr/signal.h>
"uart.h"
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
// 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)
{
UART_putstr_P(HOME);
// pętla nieskończona
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
0
0
1
1
| PWM10 | Opis
|
0
| Wyłączony PWM
|
1
| 8 bitowy PWM
|
0
| 9 bitowy PWM
|
1
| 10 bitowy PWM
50
Licznik może zliczać od 0x0000 do wybranej granicy (8 bitowy - 0x00FF, 9 bitowy 0x01FF, 10 bitowy - 0x03FF), kiedy ją przekroczy liczy z powrotem od zera i powtarza ten
cykl w nieskończoność. Kiedy wartość licznika zrówna się z wartością rejestru
porównującego (OCR1A, OCR1B) daje następujący efekt na wyjściach OC1A i OC1B w
zależności od ustawień jak w tabeli:
Praca wyjścia PWM (bity rejestru TCCR1A)
COM1x1 | COM1x0 | Efekt na OC1x
---------------------------------------------------------0
|
0
| brak
0
|
1
| brak
1
|
0
| Wyzerowany przy zrównaniu, liczy w górę,
|
| ustawiony przy zrównaniu, liczy w dół
1
|
1
| Wyzerowany przy zrównaniu, liczy w dół,
|
| ustawiony przy zrównaniu, liczy w górę
Gdzie: x to A lub B
Program przykładowy
Naszym celem będzie napisanie programu, który pokaże działanie wyjścia PWM.
Urządzeniem wyjściowym będzie dioda LED wraz z rezystorem 470 om podłączona
między końcówkę OC1A a zasilanie.
Sterowanie programem umożliwiają dwa przyciski podłączone między końcówki PD2 i
PD3 a masę.
// Testowanie timera 1 w trybie samobieżnego PWM
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>
// dostęp do rejestrów
// funkcje sei(), cli()
// 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)
#endif
uint16_t pwm=512;
wypełnienia
void delay(void)
{
unsigned int i;
for(i=0;i<50000;i++);
}
int main(void)
{
DDRD = 0x00;
PORTD = 0xFF;
OCR1=value
// zmienna zawiarająca wartość
// prosta pętla opóźniająca
// program główny
// PORTD jako wejścia dla przycisków
// podciągaj wejścia PORTD
51
#if defined(__AVR_AT90S4414__) || defined(__AVR_AT90S8515__) ||
defined(__AVR_AT90S4434__) || defined(__AVR_AT90S8535__) ||
defined(__AVR_ATmega163__) || defined(__AVR_ATmega16__)
sbi(DDRD,PD5);
// ustawienie kierunku wyjscia
#endif
#ifdef __AVR_AT90S2313__
sbi(DDRB,PB3);
// ustawienie kierunku wyjscia
#endif
\
\
PWM
PWM
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)
{
PWM_out(pwm);
// pętla nieskończona
// wpisz pwm do OCR1
if (bit_is_clear(PIND,PD3))// jeżeli zwarto PD3 z masą
{
delay();
// czekaj chwilę
if (++pwm==1024) pwm=1023;// zwiększ i ogranicz pwm
}
if (bit_is_clear(PIND,PD2))// jeżeli zwarto PD2 z masą
{
delay();
// czekaj chwilę
if (--pwm==0xFFFF) pwm=0;
// zmniejsz i ogranicz pwm
}
}
}
AVR-GCC - licznik/czasomierz TIMER 2
UWAGA! Znajduje się tylko w niektórych typach mikrokontrolerów AVR.
Jest 8-bitowym licznikiem mogącym zliczać od 0 do 255. Ponadto może generować
przerwania które następnie należy obsłużyć. Również program może odczytywać jego
wartość w dowolnym momencie (tzw. polling) i na tej podstawie podejmować
odpowiednie działania. Źródło sygnału zegarowego dla jego preskalera zostało nazwane
PCK2. Sygnał PCK2 jest domyślnie podłączony do głównego sygnału zegarowego
procesora CLK. Przez ustawienie bitu AS2 w rejestrze ASSR, licznik/czasomierz 2 jest
taktowany asynchronicznie z końcówki TOSC1. Ten fakt pozwala użyć
licznika/czasomierza 2 jako zegara czasu rzeczywistego (RTC). Kiedy bit AS2 jest
ustawiony, końcówki TOSC1 i TOSC2 są odłączone od portu mikrokontrolera. Można
podłączyć kwarc pomiędzy końcówki TOSC1 oraz TOSC2 i stworzyć w ten sposób
niezależne źródło sygnału taktującego licznik/czasomierz 2. Generator ten jest
zoptymalizowany dla kwarcu o częstotliwości 32768 Hz. Alternatywnie można także
podłączyć zewnętrzny sygnał zegarowy do końcówki TOSC1. Częstotliwość tego sygnału
musi być mniejsza niż jedna czwarta zegara CPU ale nie wyższa niż 256 kHz.
Rejestry licznika/czasomierza 2
Nazwa | Funkcja
--------------------------------------
52
TCCR2
TCNT2
OCR2
ASSR
|
|
|
|
Rejestr kontrolny
Wartość
Rejestr porównujący
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
#include
#include
#include
<avr/io.h>
<avr/interrupt.h>
<avr/signal.h>
"uart.h"
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
// obsługa portu szeregowego
prog_char NEWLINE[] = {'\n','\r',0};
// tablica zawiarająca znaki nowej linii
prog_char CLEAR[] = {27,'[','H',27,'[','2','J',0};
// j.w. ale czyszczącza ekran terminala
prog_char HOME[] = {27,'[','H',0};
// j.w. ale przestawiająca kursor na początek
prog_char SPACE[] = {' ',' ',0};
53
// j.w. spacje
typedef struct
{
uint8_t second;
uint8_t minute;
uint8_t hour;
uint8_t date;
uint8_t month;
uint16_t year;
} time;
time t;
//sprawdza rok przestępny
char not_leap(void)
{
if (!(t.year%100))
return (char)(t.year%400);
przez 400
else
return (char)(t.year%4);
}
// definicja struktury
// sekunda
// minuta
// godzina
// dzień
// miesiąc
// rok
// typ przechowujący czas
// zmienna przechowująca czas
// jeśli rok jest podzielny przez 100
// sprawdź i zwróć rok podzielny
// w przeciwnym wypadku
// zwróć rok podzielny przez 4
// zliczanie czasu
// w oparciu o ATMEL Application Note AVR134
SIGNAL(SIG_OVERFLOW2)
// obsługa przerwania od licznika 2
{
if (++t.second==60)
// inkrementuj sekundy i sprawdź czy
jest ich 60
{
// jeśli tak to:
t.second=0;
// wyzeruj licznik sekund oraz
if (++t.minute==60)
// inkrementuj licznik minut i sprawdź czy
jest ich 60
{
// jeśli tak to:
t.minute=0;
// wyzeruj licznik minut oraz
if (++t.hour==24)
// inkrementuj licznik godzin i sprawdź
czy jest ich 24
{
// jeśli tak to:
t.hour=0;
// wyzeruj licznik godzin oraz
if (++t.date==32)
// inkrementuj licznik dni i sprawdź czy
jest ich 32
{
// jeśli tak to:
t.month++;
// inkrementuj licznik miesięcy
t.date=1;
// ustaw dzień na 1
}
else if (t.date==31)
// jeżeli dzień równa się 31
{
// to sprawdź czy są to miesiące: 4, 6,
9, 11
if ((t.month==4) || (t.month==6) ||
(t.month==9) || (t.month==11))
{
// jeśli tak to:
t.month++;
// inkrementuj licznik miesięcy
t.date=1;
// ustaw dzień na 1
}
}
else if (t.date==30)
// jeżeli dzień równa się 30
{
// to sprawdź czy
if (t.month==2)
// jest to miesiąc 2 (luty)
{
// jeśli tak to:
t.month++;
// inkrementuj licznik miesięcy
54
t.date=1;
// ustaw dzień na 1
}
}
else if (t.date==29)
{
if ((t.month==2)
nieprzestępny
{
t.month++;
t.date=1;
}
}
if (t.month==13)
{
t.month=1;
t.year++;
}
}
}
}
}
// jeżeli dzień równa się 29
// to sprawdź czy:
&& (not_leap())) // miesiąc 2
i
rok
// jeśli tak to:
// inkrementuj licznik miesięcy
// ustaw dzień na 1
// jeśli miesiąc wynosi 13
// to:
// ustaw miesiąc na 1 (styczeń)
// 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;
sekundę
int main(void)
{
t.year=2004;
t.month=3;
t.date=8;
t.hour=12;
UART_init();
UART_putstr_P(CLEAR);
// przechowuje ostatnio wyświtloną
// program główny
// zainicjuj rok
// zainicjuj miesiąc
// zainicjuj dzień
// zainicjuj godzinę
// inicjalizacja portu szeregowego
// wyczyść ekran terminala
TIMSK &=~_BV(TOIE2);
ASSR |= _BV(AS2);
// Wyłącz przerwania TC2
// przełącz TC2 z taktowania zegarem CPU
// na generator asynchoniczny 32768 Hz
TCCR2 = _BV(CS22)|_BV(CS20);
// ustaw preskaler na podział
przez 128
// aby generować przerwania dokładnie co 1
sekundę
while(ASSR&0x07);
// czekaj na uaktualnienie TC2
TIMSK |= _BV(TOIE2);
// włącz przerwania z TC2
sei();
// włącz obsługę przerwań
while(1)
// pętla nieskończona
{
if ((last_second)!=(t.second))
// jeśli zmieniła się sekunda
{
55
last_second=t.second;
UART_putstr_P(HOME);
PRINT_VALUE(PSTR("Rok
PRINT_VALUE(PSTR("Miesiac
PRINT_VALUE(PSTR("Dzien
PRINT_VALUE(PSTR("Godzina
PRINT_VALUE(PSTR("Minuta
PRINT_VALUE(PSTR("Sekunda
=
=
=
=
=
=
// zapamiętaj obecną sekundę
// ustaw kursor na początku
"),t.year);
// wyświetl rok
"),t.month);
// wyświetl miesiąc
"),t.date);
// wyświetl dzien
"),t.hour);
// wyświetl godzinę
"),t.minute);
// wyświetl minutę
"),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>
#include <avr/interrupt.h>
// dostęp do rejestrów
// funkcje sei(), cli()
56
#include <avr/signal.h>
#include <avr/delay.h>
// definicje SIGNAL, INTERRUPT
// 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)
{
cbi(PORTC,PB2);
}
SIGNAL (SIG_OVERFLOW2)
{
TCNT2 = delay;
sbi(PORTC,PC2);
}
int main(void)
{
DDRC = 0xFF;
PORTC = 0xFF;
DDRD = 0x00;
przycisków
PORTD = 0xFF;
delay = 0x10;
compare = 0x7F;
TIMSK=_BV(TOIE2)|_BV(OCIE2);
// przerwanie od porównania
// zapal diodę na PC2
// przerwanie od przepełnienia
// przeładuj TIMER2
// zgaś diodę na PC2
// program główny
// PORTB jako wyjścia dla LED
// zgaś diody LED na PORTB
// PORTD jako wejścia dla
// podciągaj wejścia PORTD
// domyślna wartość dla TIMERA2
// domyślna wartość dla porównania
// włącz przerwania
//
od
przepełnienia
i
porównania
TIMERA1
TCNT2=delay;
// zainicjuj TIMER1
TCCR2=_BV(CS20)|_BV(CS21)|_BV(CS22);
//
czasomierz
F_CPU/1024
2
taktowany
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))
delay--;
// jeżeli zwarto PD3 z masą
// 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;
_delay_loop_2(50000);
// wpisz compare do OCR2
// pętla opóźniająca
}
}
\section {Tryb PWM - modulowana szerokość impulsu}
Kiedy zostanie wybrany tryb pracy licznika jako PWM (Pulse Width Modulation) czyli
modulacja szerokości impulsów może być używany jako 8 bitowy samobieżny modulator
PWM. Aby wybrać ten tryb należy ustawić bit PWM2 w rejestrze TCCR2. Licznik może
zliczać od 0x00 do 0xFF, kiedy ją przekroczy liczy z powrotem od zera i powtarza ten cykl
57
w nieskończoność. Kiedy wartość licznika zrówna się z wartością rejestru porównującego
OCR2 daje następujący efekt na wyjściu OC2 w zależności od ustawień bitów jak w tabeli
\ref{pwm2wyj}.
Praca wyjścia PWM (bity rejestru TCCR2)
COM21 | COM20 | Efekt na OC2
------------------------------------------0
|
0
| brak
0
|
1
| brak
1
|
0
| Wyzerowany przy zrównaniu,
| liczy w górę, ustawiony
| przy zrównaniu, liczy w dół
1
|
1
| Wyzerowany przy zrównaniu,
| liczy w dół, ustawiony
| przy zrównaniu, liczy w górę
Program przykładowy
// Testowanie timera 2 w trybie samobieżnego PWM
#include <avr/io.h>
#include <avr/delay.h>
int main(void)
{
uint8_t pwm=128;
wypełnienia
DDRD = 0x80;
przycisków
PORTD = 0x7F;
// dostęp do rejestrów
// zawiera definicję _delay_loop2
// program główny
// zmienna zawiarająca wartość
// PORTD jako wejścia dla
// oraz PD7 jako wyjście PWM
// podciągaj wejścia PORTD
TCCR2 = _BV(COM21)|_BV(COM20)|_BV(PWM2)|_BV(CS20);
// czasomierz 2 w trybie PWM
// taktowany F_CPU
while(1)
{
if (bit_is_clear(PIND,PD3))
if (++pwm==0) pwm=255;
if (bit_is_clear(PIND,PD2))
if (--pwm==255) pwm=0;
OCR2 = pwm;
_delay_loop_2(50000);
// pętla nieskończona
// jeżeli zwarto PD3 z masą
// zwiększ i ogranicz pwm
// jeżeli zwarto PD2 z masą
// zmniejsz i ogranicz pwm
// wpisz pwm do OCR2
// pętla opóźniająca
}
}
AVR-GCC - komparator analogowy
Komparator analogowy porównuje napięcia na wejściach AIN0 i AIN1. W przypadku gdy
napięcie na AIN0 jest wyższe od AIN1 ustawiany jest bit ACO w rejestrze ACSR. Wyjście
komparatora może zostać wykorzystane do wyzwalania przechwytywania wartości
58
licznika/czasomierza 1. Oprócz tego, komparator analogowy może wygenerować
przerwanie.
Można wybrać czy przerwanie ma być generowane przez wyjście komparatora zboczem
narastającym,
opadającym lub obydwoma. Ta funkcja jest kontrolowana przez bity ACIS0 i ACIS1
rejestru ACSR
Programy przykładowe
Aby przetestować działanie komparatora należy podłączyć do jego wejść prosty układ,
który wymusi różne napięcia na jego wejściach np. do wejścia AIN0 podłączmy
symetryczny dzielnik napięcia dający na wyjściu połowę napięcia zasilania. Do wejścia
AIN1 podłączmy końcówkę suwaka potencjometru, którego krańcowe wyprowadzenia są
podłączone pomiędzy zasilanie a masę. Ze względu na dużą rezystancję wejściową
komparatora, wartości rezystancji rezystorów i potencjometru nie są krytyczne i mogą
zawierać się w granicach od 1k do 1M.
Przykład z poniższego listingu ilustruje działanie komparatora analogowego. Stan wyjścia
komparatora jest cyklicznie sprawdzany i na jego podstawie sterowana jest dioda LED
podłączona do linii PB7.
// Testowanie komparatora analogowego bez użycia przerwania
#include <avr/io.h>
// dostęp do rejestrów
int main(void)
{
sbi(DDRB, PB7);
// linia PB7 jako wyjście (LED)
while(1)
// pętla nieskończona
{
if (bit_is_set(ACSR, ACO))
// jeżeli na wyjściu komparatora
jest 1
cbi(PORTB, PB7);
// zapal diodę na PB7
else
// w przeciwnym wypadku
sbi(PORTB, PB7);
// zgaś ją
}
}
Przykład z poniższego listingu pokazuje użycie komparatora analogowego do generowania
przerwań. Szczegóły w komentarzach listingu.
// Testowanie komparatora analogowego z użyciem przerwania
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/signal.h>
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
#define tbi(PORT,BIT)
PORT^=_BV(BIT)
// przełącza stan BITu w PORTcie na przeciwny 1->0 ; 0->1
SIGNAL(SIG_COMPARATOR)
{
tbi(PORTB, PB7);
}
// zmień stan diody na PB7
int main(void)
{
59
sbi(DDRB, PB7);
ACSR = _BV(ACIE);
// linia PB7 jako wyjście (LED)
// komparator analogowy generuje przerwanie
// przy każdej zmianie stanu na jego wyjściu
// włącz obsługę przerwań
// pętla nieskończona
sei();
while(1);
}
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
#include
#include
#include
#include
<avr/io.h>
<avr/interrupt.h>
<avr/signal.h>
"uart.h"
"delay.h"
#define R_PIN
PB4
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
// obsługa portu szeregowego
// funkcje opóźniające
// końcówka rezystora
#ifdef __AVR_AT90S2313__
# define C_PIN
PB1
#else
# define C_PIN
PB3
#endif
// końcówka kondensatora i rezystora
// końcówka kondensatora i rezystora
// 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;
volatile uint8_t busy;
void ADC_start(void)
{
busy=1;
TCCR1B = 0;
TCNT1 = 0;
cbi(DDRB, R_PIN);
// przechowuje przetworzoną wartość
// do sprawdzania zajętości przetwornika
// start przetwarzania
// wpisz zajętość przetwornika
// licznik 1 zatrzymany
// wyzeruj timer 1
// odłącz linię rezystora
60
sbi(DDRB, C_PIN);
// ustaw linię kondenstaora (- komparatora)
jako wyjście
delay10us();
// i czekaj na rozładowanie kondensatora
cbi(DDRB, C_PIN);
// ustaw linię kondenstaora (- komparatora)
jako wejście
sbi(DDRB, R_PIN);
// podłącz linię rezystora (rozpocznij
ładowanie kondensatora)
TCCR1B = _BV(CS10)|_BV(ICNC1);
// licznik 1 taktowany F_CPU
//
wraz
z
filtracją
zakłóceń
z
wejścia
przechwytywania
// i przechwytywaniem za pomocą opadającego
zbocza
while(busy);
// czekaj na przerwanie od przechwytywania
lub przepełnienia
}
SIGNAL (SIG_INPUT_CAPTURE1)
// przerwanie od przechwytywania
licznika 1
{
value=ICR1;
// odczytaj wartość z rejestru
przechwytującego
busy=0;
// można zakończyć przetwarzanie
}
SIGNAL (SIG_OVERFLOW1)
{
value=0;
busy=0;
}
int main(void)
{
UART_init();
sbi(PORTB, R_PIN);
ACSR = _BV(ACIC);
// przerwanie od przepełnienia licznika 1
// wpisz wartość 0
// można zakończyć przetwarzanie
// program główny
// inicjalizacja portu szeregowego
// linia "zasilająca" rezystor
// 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();
UART_putstr_P(CLEAR);
// włącz obsługę przerwań
// wyczyść ekran terminala
while(1)
// pętla nieskończona
{
ADC_start();
// zainicjuj przetwarzanie
UART_putstr_P(HOME);
// ustaw kursor na początku ekranu
UART_putint(value,10);
// wypisz przetworzoną wartość
UART_putstr_P(SPACE);
// dopisz spacje na końcu wiersza
}
}
AVR-GCC - przetwornik analogowo/cyfrowy
61
Przetwornik analogowo/cyfrowy w mikrokontrolerze integruje przetwarzanie sygnałów
analogowych i cyfrowych w jednym układzie. Posiada on rozdzielczość 10 bitów (1024
poziomy). Pracuje na zasadzie sukcesywnej aproksymacji. Aby można było mierzyć
wartości sygnałów analogowych z wielu źródeł, na wejściu przetwornika umieszczono
multiplekser analogowy. W danej chwili tylko jeden z kilku sygnałów analogowych może
być zamieniony na postać cyfrową. Rejestr ADMUX z załadowaną odpowiednią wartością
wskazuje, który z tych kanałów jest połączony z przetwornikiem.
Wartość Używany
ADMUX
kanał
-------------------0
kanał 0
1
kanał 1
2
kanał 2
3
kanał 3
4
kanał 4
5
kanał 5
6
kanał 6
7
kanał 7
Do prawidłowej pracy przetwornik potrzebuje sygnału taktującego o częstotliwości z
przedziału od 50kHz do 200kHz. Sygnał ten jest generowany z sygnału zegarowego
procesora. Aby uzyskać właściwą częstotliwość należy skorzystać z wbudowanego
preskalera. Stopień podziału ustala się ustawiając trzy bity ADPS0, ADPS1 i ADPS2 w
rejestrze ADCSR.
ADPS2 ADPS1 ADPS0 podział
---------------------------0
0
0
1
0
0
1
2
0
1
0
4
0
1
1
8
1
0
0
16
1
0
1
32
1
1
0
64
1
1
1
128
Przykład:
fosz = 4MHz
50kHz < fad < 200kHz
-> prescale = 32
fad = fosz/32 = 4000000/32 = 125000 = 125 kHz
50kHz < 125kHz < 200kHz
-> ADPS0 = 1, ADPS1 = 0, ADPS2 = 1
Przetwornik może pracować w dwóch trybach: na żądanie oraz samobieżnie. 10 bitowy
wynik konwersji jest dostępny w rejestrach ADCL (mniej znaczący bajt) i ADCH (bardziej
znaczący bajt). Podczas odczytu należy uważać aby najpierw odczytywać ADCL a
następnie ADCH, lub po prostu odczytywać 16 bitowy rejestr ADCW.
Rejestr ADCSR
Bit
0
1
| Nazwa
| ADPS0
| ADPS1
| Opis
| preskaler
| preskaler
62
2
| ADPS2
| preskaler
3
| ADIE
| zezwolenie na generowanie przerwania przez przetwornik na
zakończenie przetwarzania
4
| ADIF
| znacznik przerwania z przetwornika - jeśli jest ustawiony
to konwersja A/D została zakończona
5
| ADFR
| przetwarzanie samodzielne (Free Run)
6
| ADSC
| start konwersji - jeśli jest ustawiony ten bit oraz ADEN
7
| ADEN
| włączenie przetwornika (AD Enable)
Poniższe programy pozwolą na bliższe poznanie przetwornika analogowo/cyfrowego
wbudowanego w mikrokontroler.
Do zaprezentowania działania przetwornika niezbędny jest odpowiednio wyposażony
układ. Może to być np. AT90S8535, ATmega16, ATmega8 itp. Poniżej przedstawiono
schemat niezbędnych połączeń potrzebnych do prawidłowego działania programów
przykładowych. Bardzo ważna jest obecność rezystora na wejściu ADC0 - bez niego może
dojść do uszkodzenia wejścia przetwornika w przypadku podania bezpośrednio na nie
napięcia zasilania mikrokontrolera. Końcówka AREF służy do podania napięcia
odniesienia dla przetwornika, co przekłada się na maksymalne napięcia jakie może zostać
przetworzone (minus wartość najmniej znaczącego bitu). Napięcie na wejściu AREF nie
może być wyższe od AVCC i niższe od 2V.
Programy przykładowe
Program pokazuje wykorzystanie przetwornika ADC w trybie pracy tzw. konwersji na
żądanie. Żądanie konwersji wywołuje się przez wysłanie dowolnego znaku na port
63
szeregowy mikrokontrolera. Wynik przetwarzania jest wysyłany również na port
szeregowy oraz 8 bardziej znaczących bitów wyniku jest prezentowanych na diodach LED
podłączonych do PORTB.
// Testowanie przetwornika analogowo/cyfrowego
// w trybie pojedynczej konwersji
#include
#include
#include
#include
<avr/io.h>
<avr/interrupt.h>
<avr/signal.h>
"uart.h"
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
// 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)
{
value = ADCW;
ADC
PORTC,~(value>>2);
LED
UART_putint(value,10);
UART_putstr_P(NEWLINE);
}
int main(void)
{
UART_init();
// przerwanie z przetwornika ADC
// czytaj wartość z przetwornika
// wyślij przetworzoną wartość na
// wypisz przetworzoną wartość
// dopisz znaki nowej linii
// program główny
// 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();
UART_putstr_P(CLEAR);
while(1)
{
UART_getchar();
sbi(ADCSR,ADSC);
}
// włącz obsługę przerwań
// wyczyść ekran terminala
// pętla nieskończona
// czekaj na znak z portu szeregowego
// rozpocznij pomiar przetwornikiem ADC
}
Program pokazuje wykorzystanie przetwornika ADC w trybie tzw. konwersji samobieżnej.
Wynik przetwarzania jest wysyłany na port szeregowy. 8 bardziej znaczących bitów
wyniku jest prezentowanych na diodach led podłączonych do PORTB. Należy zwrócić
uwagę na fakt, że aby uruchomić ten tryb pracy należy oprócz bitu ADFR ustawić również
bit ADSC w rejestrze ADCSR.
// Testowanie przetwornika analogowo/cyfrowego
// konwersja samobieżna
64
#include
#include
#include
#include
<avr/io.h>
<avr/interrupt.h>
<avr/signal.h>
"uart.h"
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
// 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)
{
value = ADCW;
ADC
PORTC = ~(value>>2);
LED
}
int main(void)
{
UART_init();
// przerwanie z przetwornika ADC
// czytaj wartość z przetwornika
// wyślij przetworzoną wartość na
// program główny
// 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();
UART_putstr_P(CLEAR);
while(1)
{
UART_putstr_P(HOME);
UART_putint(value,10);
UART_putstr_P(SPACE);
}
// włącz obsługę przerwań
// wyczyść ekran terminala
// pętla nieskończona
// dopisz spacje na końcu wiersza
// wypisz przetworzoną wartość
// dopisz spacje na końcu wiersza
}
AVR-GCC - układ Watchdog
Mikrokontrolery AVR są wyposażone w układ nadzoru tzw. Watchdog czyli po polsku
"czuwający pies". Nazwa ta jest adekwatna do roli jaką pełni w systemie. Układ ten składa
się z czasomierza, taktowanego z wewnętrznego generatora o częstotliwości 1MHz (przy
napięciu zasilania Vcc=5V) niezależnego od zegara systemowego. Częstotliwość pracy
tego generatora jest silnie zależna od napięcia zasilającego mikrokontroler. Pomiędzy
65
czasomierzem a generatorem znajduje się preskaler, którym można ustalić jak często ma
być generowany sygnał restartujący mikrokontroler. Aby jednak nie dopuścić do
restartowania mikrokontrolera należy co pewien czas zerować czasomierz układu nadzoru.
Biblioteka avr-libc dołączona do kompilatora zawiera funkcje ułatwiające używanie
układu Watchdog. Do programu należy dołączyć nagłówek avr/wdt.h.
Najważniejsze funkcje zawarte w pliku avr/wdt.h
wdt_enable(timeout)
Załącza układ Watchdog z czasem zdefiniowanym jako timeout. Jako timeout można użyć
predefiniowanych wartości zamieszczonych w tabeli:
Stała
Czas
------------------WDTO_15MS
15 ms
WDTO_30MS
30 ms
WDTO_60MS
60 ms
WDTO_250MS
250 ms
WDTO_500MS
500 ms
WDTO_1S
1000 ms
WDTO_2S
2000 ms
Przykład:
wdt_enable(WDTO_500MS);
Należy pamiętać, że czasy te podane są tylko orientacyjnie i mogą się różnić od podanych
nawet kilkukrotnie! Niższe napięcie zasilania mikrokontrolera oznacza wydłużenie czasu
zadziałania układu Watchdog.
wdt_reset()
Powoduje kasowanie czasomierza układu Watchdog. Kiedy układ Watchdog jest aktywny,
powyższą funkcję należy wywoływać odstępach czasu mniejszych od czasu zadziałania
układu Watchdog.
wdt_disable()
Powoduje wyłączenie układu Watchdog.
Program przykładowy
Poniżej zamieszczono listing programu mogącego służyć do różnych eksperymentów z
układem Watchdog. Komunikacja z użytkownikiem za pomocą terminala podłączonego do
portu szeregowego. Aby najlepiej poznać działanie układu proponuję wstawiać i usuwać z
różnych miejsc programu funkcję a właściwie makro WDR().
// Testowanie układu WATCHDOG
#include
#include
#include
#include
#include
<avr/io.h>
<avr/interrupt.h>
<avr/signal.h>
<avr/wdt.h>
"uart.h"
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
// obsługa układu Watchdog
// obsługa portu szeregowego
66
#define WDT_ENABLE
#ifdef WDT_ENABLE
#define WDR()
#else
#define WDR()
#endif
// określa czy używamy Watchdoga
wdt_reset()
// jeśli go używamy
// to przypisz wdt_reset do WDR
// jeśli nie
// to WDR jest pustym ciągiem
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);
// wdt_enable(WDTO_1S);
wdt_enable(WDTO_500MS);
// wdt_enable(WDTO_250MS);
// wdt_enable(WDTO_120MS);
// wdt_enable(WDTO_60MS);
// wdt_enable(WDTO_30MS);
// wdt_enable(WDTO_15MS);
#endif
TIMSK = _BV(TOIE0);
TCCR0 = _BV(CS02);
sei();
while(1)
{
long_loop();
//
WDR();
}
}
// watchdog na czas ok 2s
// watchdog na czas ok 1s
// watchdog na czas ok 0,5s
// watchdog na czas ok 0,25s
// watchdog na czas ok 0,12s
// watchdog na czas ok 0,06s
// watchdog na czas ok 0,03s
// watchdog na czas ok 0,015s
// włącz obsługę przerwań T/C0
// taktowanie T/C0
// włącz obsługę przerwań
// pętla nieskończona
// długa pętla symulująca jakieś działania
// restart licznika Watchdoga
67
AVR-GCC - tryby zmniejszonego poboru
mocy
Mikrokontrolery AVR są wyposażone w układy pozwalające na wejście w tryby pracy ze
zmniejszonym poborem mocy.
Oczywiście nie ma nic zupełnie za darmo i odbywa się to różnymi kosztami jak np.
zmniejszona szybkość pracy, wyłączenie niektórych urządzeń peryferyjnych (komparator
analogowy, przetwornik analogowo/cyfrowy itp.).
Najważniejsze funkcje zawarte w pliku avr/sleep.h
set_sleep_mode(mode)
Przygotowuje mikrokontroler do wprowadzenia w tryb uśpienia przez ustawienie
odpowiednich bitów w rejestrze SMCR lub MCUCR (sprawdź w dokumentacji
mikrokontrolera): SM0, SM1, SM2.
Poniżej przedstawiono możliwe tryby obniżonego poboru mocy:
•
•
•
•
•
•
SLEEP_MODE_IDLE
SLEEP_MODE_PWR_DOWN
SLEEP_MODE_PWR_SAVE
SLEEP_MODE_ADC
SLEEP_MODE_STANDBY
SLEEP_MODE_EXT_STANDBY
Przykład:
set_sleep_mode(SLEEP_MODE_PWR_SAVE);
Należy sprawdzić w dokumentacji mikrokontrolera, które z nich są możliwe do
wykorzystania w użytym mikrokontrolerze.
sleep_mode()
Wprowadza mikrokontroler w tryb uśpienia wybrany wcześniej za pomocą funkcji
set_sleep_mode(mode).
Program przykładowy
Poniższy przykład będzie rozszerzeniem wcześniej zaprezentowanego zegara czasu
rzeczywistego zbudowanego z czasomierza 2 taktowanego z oddzielnego generatora
kwarcowego. W tryb obniżonego poboru mocy układ wchodzi przez zwarcie linii PB0 z
masą - wyjście przez podłączenie ww. linii do zasilania.
// Testowanie przejścia w tryb obniżonego poboru mocy
// (zegar czasu rzeczywistego zbudowany z licznika 2)
#include
#include
#include
#include
#include
<avr/io.h>
<avr/interrupt.h>
<avr/signal.h>
<avr/sleep.h>
"uart.h"
// dostęp do rejestrów
// funkcje sei(), cli()
// definicje SIGNAL, INTERRUPT
// funkcje obniżonego poboru mocy
// obsługa portu szeregowego
68
#define PWR_save
bit_is_clear(PINB,PINB0)
prog_char NEWLINE[] = {'\n','\r',0};
// tablica zawiarająca znaki nowej linii
prog_char CLEAR[] = {27,'[','H',27,'[','2','J',0};
// j.w. ale czyszczącza ekran terminala
prog_char HOME[] = {27,'[','H',0};
// j.w. ale przestawiająca kursor na początek
prog_char SPACE[] = {' ',' ',0};
// j.w. spacje
typedef struct
{
uint8_t second;
uint8_t minute;
uint8_t hour;
uint8_t date;
uint8_t month;
uint16_t year;
} time;
time t;
// definicja struktury
// sekunda
// minuta
// godzina
// dzień
// miesiąc
// rok
// typ przechowujący czas
// zmienna przechowująca czas
//sprawdza rok przestępny
char not_leap(void)
{
if (!(t.year%100))
return (char)(t.year%400);
przez 400
else
return (char)(t.year%4);
}
// jeśli rok jest podzielny przez 100
// sprawdź i zwróć rok podzielny
// w przeciwnym wypadku
// zwróć rok podzielny przez 4
// zliczanie czasu
// w oparciu o ATMEL Application Note AVR134
SIGNAL(SIG_OVERFLOW2)
// obsługa przerwania od licznika 2
{
if (++t.second==60)
// inkrementuj sekundy i sprawdź czy
jest ich 60
{
// jeśli tak to:
t.second=0;
// wyzeruj licznik sekund oraz
if (++t.minute==60)
// inkrementuj licznik minut i
sprawdź czy jest ich 60
{
// jeśli tak to:
t.minute=0;
// wyzeruj licznik minut oraz
if (++t.hour==24)
// inkrementuj licznik godzin i
sprawdź czy jest ich 24
{
// jeśli tak to:
t.hour=0;
// wyzeruj licznik godzin oraz
if (++t.date==32)
// inkrementuj licznik dni i sprawdź czy
jest ich 32
{
// jeśli tak to:
t.month++;
// inkrementuj licznik miesięcy
t.date=1;
// ustaw dzień na 1
}
else if (t.date==31)
// jeżeli dzień równa się 31
{
// to sprawdź czy są to miesiące: 4, 6,
9, 11
if ((t.month==4) || (t.month==6) ||
(t.month==9) || (t.month==11))
69
{
// jeśli tak to:
// inkrementuj licznik miesięcy
// ustaw dzień na 1
t.month++;
t.date=1;
}
}
else if (t.date==30)
{
if (t.month==2)
{
t.month++;
t.date=1;
}
}
else if (t.date==29)
{
if ((t.month==2)
nieprzestępny
{
t.month++;
t.date=1;
}
}
if (t.month==13)
{
t.month=1;
t.year++;
}
}
}
}
if (PWR_save)
obniżonego poboru
sleep_mode();
}
// jeżeli dzień równa się 30
// to sprawdź czy
// jest to miesiąc 2 (luty)
// jeśli tak to:
// inkrementuj licznik miesięcy
// ustaw dzień na 1
// jeżeli dzień równa się 29
// to sprawdź czy:
&& (not_leap())) // miesiąc 2
i
rok
// jeśli tak to:
// inkrementuj licznik miesięcy
// ustaw dzień na 1
// jeśli miesiąc wynosi 13
// to:
// ustaw miesiąc na 1 (styczeń)
// inkrementuj licznik lat
// jeśli układ ma być w trybie
// 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;
sekundę
int main(void)
{
u16 temp0, temp1;
t.year=2003;
t.month=3;
t.date=8;
// przechowuje ostatnio wyświtloną
// program główny
// zmienne tymczasowe
// zainicjuj rok
// zainicjuj miesiąc
// zainicjuj dzień
set_sleep_mode(SLEEP_MODE_PWR_SAVE);
// przygotuj układ do obniżonego poboru mocy
UART_init();
UART_putstr_P(CLEAR);
// inicjalizacja portu szeregowego
// wyczyść ekran terminala
70
for(temp0=0;temp0<0x0040;temp0++)
for(temp1=0;temp1<0xFFFF;temp1++);
// czekaj na ustabilizowanie się generatora
32768 Hz
TIMSK &=~_BV(TOIE2);
ASSR |= _BV(AS2);
// Wyłącz przerwania TC2
// przełącz TC2 z taktowania zegarem CPU
// na generator asynchoniczny 32768 Hz
TCCR2 = _BV(CS22)|_BV(CS20);
// ustaw preskaler na podział
przez 128
// aby generować przerwania dokładnie co 1
sekundę
while(ASSR&0x07);
// czekaj na uaktualnienie TC2
TIMSK |= _BV(TOIE2);
// włącz przerwania z TC2
sei();
// włącz obsługę przerwań
while(1)
// pętla nieskończona
{
if (!PWR_save)
// jeśli nie jest to tryb oszczędny
{
if ((last_second!=t.second))// jeśli zmieniła się sekunda
{
last_second=t.second;
// zapamiętaj obecną sekundę
UART_putstr_P(HOME);
// ustaw kursor na początku
PRINT_VALUE(PSTR("Rok
= "),t.year);
// wyświetl rok
PRINT_VALUE(PSTR("Miesiac = "),t.month); // wyświetl miesiąc
PRINT_VALUE(PSTR("Dzien
= "),t.date);
// wyświetl
dzien
PRINT_VALUE(PSTR("Godzina = "),t.hour);
// wyświetl
godzinę
PRINT_VALUE(PSTR("Minuta = "),t.minute); // wyświetl minutę
PRINT_VALUE(PSTR("Sekunda = "),t.second); // sekundę
}
}
}
}
AVR-GCC - opcje wywoływania narzędzi
Avr-gcc posiada wiele wbudowanych przełączników wywoływanych bezpośrednio z linii
komend zawierających opcje do kontroli optymalizacji, ostrzeżeń i generacji kodu. Łącząc
opcje lub przełączniki, należy je podawać oddzielnie. Dlatego na przykład: złączenie `-dr'
zostanie zinterpretowane inaczej niż `-d -r'. Jeśli dodajemy do opcji nazwę pliku, możemy
to zrobić łącznie, np: `-onazwa' i `-o nazwa' da ten sam efekt.
Większość opcji zaczynających się od `-f' i `-W' posiada dwie formy: -fname i -fno-name
(podobnie: -Wname, -Wno-name). Pierwsza forma włącza przełącznik o podanej nazwie,
druga go wyłącza.
-mmcu=architektura
Kompiluje kod dla podanej architektury. Przez avr-gcc rozpoznawane są następujące
architektury:
• avr1 - "Prosty" rdzeń CPU, tylko programy w asemblerze
• avr2 - "Klasyczny" rdzeń CPU, do 8 KB ROM
• avr3 - "Klasyczny" rdzeń CPU, więcej niż 8 KB ROM
71
avr4 - "Rozszerzony" rdzeń CPU, do 8 KB ROM
avr5 - "Rozszerzony" rdzeń CPU, więcej niż 8 KB ROM
Domyślnie kod jest generowany dla architektury avr2. Zauważmy, że kiedy używamy mmcu=architecture a nie -mmcu=typ_MCU, plik nagłówkowy avr/io.h nie będzie
realizował swej funkcji dopóki nie zdecydujemy się, na konkretny typ MCU. Typ MCU
wybiera się podobnie.
•
•
-mmcu=typ_MCU
W tabeli przedstawiono zestawienie wszystkich typów MCU rozpoznawanych przez
kompilator avr-gcc (a dokładniej bibliotekę avr-libc).
arch
typ_MCU
makrodefinicja
---------------------------------avr1 at90s1200
__AVR_AT90S1200__
avr1 attiny11
__AVR_ATtiny11__
avr1 attiny12
__AVR_ATtiny12__
avr1 attiny15
__AVR_ATtiny15__
avr1 attiny28
__AVR_ATtiny28__
avr2 at90s2313
__AVR_AT90S2313__
avr2 at90s2323
__AVR_AT90S2323__
avr2 at90s2333
__AVR_AT90S2333__
avr2 at90s2343
__AVR_AT90S2343__
avr2 attiny22
__AVR_ATtiny22__
avr2 attiny24
__AVR_ATtiny24__
avr2 attiny25
__AVR_ATtiny25__
avr2 attiny26
__AVR_ATtiny26__
avr2 attiny261
__AVR_ATtiny261__
avr2 attiny44
__AVR_ATtiny44__
avr2 attiny45
__AVR_ATtiny45__
avr2 attiny461
__AVR_ATtiny461__
avr2 attiny84
__AVR_ATtiny84__
avr2 attiny85
__AVR_ATtiny85__
avr2 attiny861
__AVR_ATtiny861__
avr2 at90s4414
__AVR_AT90S4414__
avr2 at90s4433
__AVR_AT90S4433__
avr2 at90s4434
__AVR_AT90S4434__
avr2 at90s8515
__AVR_AT90S8515__
avr2 at90c8534
__AVR_AT90C8534__
avr2 at90s8535
__AVR_AT90S8535__
avr2 at86rf401
__AVR_AT86RF401__
avr2 attiny13
__AVR_ATtiny13__
avr2 attiny2313 __AVR_ATtiny2313__
avr3 atmega103
__AVR_ATmega103__
avr3 atmega603
__AVR_ATmega603__
avr3 at43usb320 __AVR_AT43USB320__
avr3 at43usb355 __AVR_AT43USB355__
avr3 at76c711
__AVR_AT76C711__
avr4 atmega48
__AVR_ATmega48__
avr4 atmega8
__AVR_ATmega8__
avr4 atmega8515 __AVR_ATmega8515__
avr4 atmega8535 __AVR_ATmega8535__
avr4 atmega88
__AVR_ATmega88__
avr4 at90pwm2
__AVR_AT90PWM2__
avr4 at90pwm3
__AVR_AT90PWM3__
avr5 at90can32
__AVR_AT90CAN32__
avr5 at90can64
__AVR_AT90CAN64__
avr5 at90can128 __AVR_AT90CAN128__
avr5 at90usb646 __AVR_AT90USB646__
avr5 at90usb647 __AVR_AT90USB647__
avr5 at90usb1286 __AVR_AT90USB1286__
72
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
avr5
at90usb1287
atmega128
atmega1280
atmega1281
atmega16
atmega161
atmega162
atmega163
atmega164p
atmega165
atmega168
atmega169
atmega32
atmega323
atmega324p
atmega325
atmega3250
atmega329
atmega3290
atmega406
atmega64
atmega640
atmega644
atmega644p
atmega645
atmega6450
atmega649
atmega6490
at94k
__AVR_AT90USB1287__
__AVR_ATmega128__
__AVR_ATmega1280__
__AVR_ATmega1281__
__AVR_ATmega16__
__AVR_ATmega161__
__AVR_ATmega162__
__AVR_ATmega163__
__AVR_ATmega164P__
__AVR_ATmega165__
__AVR_ATmega168__
__AVR_ATmega169__
__AVR_ATmega32__
__AVR_ATmega323__
__AVR_ATmega324P__
__AVR_ATmega325__
__AVR_ATmega3250__
__AVR_ATmega329__
__AVR_ATmega3290__
__AVR_ATmega406__
__AVR_ATmega64__
__AVR_ATmega640__
__AVR_ATmega644__
__AVR_ATmega644P__
__AVR_ATmega645__
__AVR_ATmega6450__
__AVR_ATmega649__
__AVR_ATmega6490__
__AVR_AT94K__
-mint8
Domyślnie typ int jest 16 bitowy. Opcja ta przestawia typ int na typ 8 bitowy. Z uwagi, że
nie jest on normalnie obsługiwany przez bibliotekę avr-libc, nie powinno się używać tej
opcji bez wyraźnej potrzeby.
-mno-interrupts}
Generuje kod, który zmienia wskaźnik stosu bez wyłączania przerwań. Normalnie, stan
rejestru SREG jest przechowywany w rejestrze tymczasowym, obsługa przerwań jest
zablokowana podczas zmiany wskaźnika stosu i jest odtwarzany stan rejestru SREG.
-mcall-prologues
Używa wywoływania podprogramów dla prologów i epilogów funkcji. Powoduje
oszczędzanie miejsca w pamięci programu, nieznacznie tylko zwiększając czas wykonania
funkcji.
-minit-stack=nnnn
Ustala wskaźnik stosu na nnnn. Domyślnie wskaźnik stosu jest symbolem __stack, który
jest utalony na RAMEND poprzez kod inicjujący program.
-mtiny-stack
Powoduje zmianę tylko mniej znaczących 8 bitów wskaźnika stosu.
-mno-tablejump
Nie generuje tablicy skoków. Domyślnie, tablica skoków jest używana do optymalizacji
instrukcji switch. Kiedy jest wyłączona, w to miejsce są wstawiane sekwencje
73
porównujące. Tablice skoków zwykle powodują szybsze wykonywanie programu, lecz w
pewnych przypadkach konstrukcja switch zawierająca wiele skoków do jednej np.
domyślnej etykiety może spowodować większe użycie pamięci programu.
-mshort-calls
Wymusza używanie instrukcji rjmp/rcall (ograniczony zakres adresowania) w
mikrokontrolerach z pamięcią flash o pojemności większej niż 8kB. W architekturach avr2
i avr4 (mniej niż 8 kB pamięci flash), jest zawsze w użyciu. Natomias architektury avr3 i
avr5, wywołania funcji i skoki to miejsc poza bieżącą funkcją powinny używać instrukcji
jmp/call które umożliwiają wykonywanie skoków w całym zakresie pamięci, ale zajmują
więcej miejsca w pamięci flash i dłużej się wykonują.
-mrtl
Wypisuje wewnętrzne wyniki kompilacji zwane "RTL" jako komentarze w generowany
kod asemblera. Przydatne podczas debugowania avr-gcc.
-msize
Wypisuje adres, rozmiar i inne dotyczące wyrażenia jako komentarze w generowany kod
asemblera. Przydatne podczas debugowania avr-gcc.
-mdeb
Wypisuje wiele informacji przydatnych do debugowania na wyjście strumienia błędu
(stderr).
-On
Poziom optymalizacji n. Zwiększanie n zwiększa poziom optymalizacji. Poziom
optymalizacji równy 0 jest równoznaczny z jej brakiem.
-Wa,assembler-options, -Wl,linker-options
Przenosi opcje do asemblera lub linkera.
-g
Generuje informacje dla debugera avr-gdb.
-c
Powoduje zatrzymanie po etapie asemblacji, wyniki są umieszczane w pliku z
rozszerzeniem .o.
-E
Powoduje zatrzymanie po etapie prekompilacji, wyniki są wypisywane na ekran.
-o nazwa
Powoduje zmianę nazwy programu wynikowego na podaną przez użytkownika; np.: avrgcc -o prog main.c, powoduje nadanie nazwy prog zamiast standardowej a.out.
-S
Powoduje zatrzymanie po etapie generowania kodu asemblera, wyniki są umieszczane w
pliku z rozszerzeniem .s. Opcją -o możemy podać inną nazwę (rozszerzenie).
-H
74
Wypisz nazwę każdego używanego pliku nagłówkowego.
-ansi
Tekst źródłowy musi być w pełni zgodny z normą ANSI języka C.
-traditional
Toleruje starsze konstrukcje języka C, z tzw. wersji języka K&R (opis znajdujesię w
książce autorów języka B.W.Kernighan'a i D.M.Ritchie'go)
-v
Wypisywanie (na standardowym wyjściu dla błędów), komend wywoływanych podczas
kolejnych etapów kompilacji. Wypisuje również numer wersji programu sterującego
kompilatorem, preprocesora oraz właściwego kompilatora.
-fsyntax-only
Sprawdź kod pod kątem błędów składniowych i nie rób nic poza tym.
-include file
Najpierw przetwarza plik file (np. kompiluj najpierw file).
-imacros file
Najpierw przetwarzaj plik file, wypisz wyniki na wyjście, zanim zaczniesz przetwarzać
resztę plików.
-idirafter dir
Dodaj katalog dir jako drugi katalog do przeszukiwania dla plików nagłówkowych. Jeśli
plik nagłówkowy nie został znaleziony w żadnym ze wskazanych wcześniej katalogów,
kompilator przeszukuje ten katalog.
-nostdinc
Nie szukaj w katalogu ze standardowymi plikami nagłówkowymi, szukaj tylko w
katalogach wskazanych przez `-I' i w katalogu bieżącym.
-I dir
Dodaje katalog dir do listy katalogów przeszukiwanych ze wzgledu na pliki nagłówkowe.
-L dir
Dodaje katalog dir do listy katalogów przeszukiwanych przy użyciu przełącznika `-l'.
-Dmacro
Użycie opcji jest równoznaczne z umieszczeniem linii #define makro na początku pliku
zawierającego tekst źródłowy.
-Dmacro=defn
Zdefiniuj makro macro jako defn.
-Umacro
Użycie opcji jest równoznaczne z umieszczeniem linii #undef makro na początku pliku
zawierającego tekst źródłowy.
75
-Wall
Wypisuje ostrzeżenia dla wszystkich sytuacji, które pretendują do konstrukcji, których
używania się nie poleca i których użycie jest proste do uniknięcia, nawet w połączeniu z
makrami.
-funsigned-char
Ustala domyślny typ zmiennych typu char na unsigned.
-funsigned-bitfields
Ustala domyślny typ pól bitowych, gdy brak deklaracji signed albo unsigned. Użycie opcji
-traditional włącza także -funsigned-bitfields, w przeciwnym razie domyślnie jest -fsignedbitfields 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
avr/delay.h
avr/eeprom.h
avr/ina90.h
avr/interrupt.h
avr/io.h
avr/io[MCU].h
avr/parity.h
avr/pgmspace.h
avr/sfr_defs.h
avr/signal.h
avr/sleep.h
avr/timer.h
avr/twi.h
avr/wdt.h
Obliczanie 16 bitowego CRC
Funkcje opóźniające (w rozwoju)
Funkcje dostępu do wewnętrznej pamięci EEPROM
Nagłówek dla kompatybilności z IAR C
Funkcje obsługi przerwań
Włącza pozostałe nagłówki I/O
Definicje I/O dla różnych mikrokontrolerów AVR
Obliczanie bitu parzystości
Funkcje dostępu do pamięci programu
Makra dla peryferii
Obsługa przerwań i sygnałów AVR
Zarządzanie poborem energii
Funkcje dla licznika/czasomierza 0
Obsługa TWI (i2c) w ATMega
Funkcje kontrolujące układ watchdoga
76
ctype.h
errno.h
inttypes.h
math.h
setjmp.h
stdio.h
stdlib.h
string.h
Funkcje testujące wartości typów znakowych
Obsługa błędów
Definicje różnych typów całkowitych
Różne funkcje matematyczne
Zawiera funkcje długich skoków (long jumps)
Standardowa biblioteka wejścia/wyjscia
Rozmaite funkcje standardowe
Funkcje operujące na łańcuchach
avr/crc16.h
Zawiera funkcje obliczające 16 bitowe CRC.
unsigned int _crc16_update(unsigned int __crc, unsigned char __data)
Oblicza 16 bitowe CRC według standardu CRC16 (x^16 + x^15 + x^2 + 1).
avr/delay.h
Zawiera proste funkcje wstrzymujące działanie programu na pewien czas.
void _delay_loop_1(unsigned char __count)
8 bitowy licznik, 3 cykle procesora na __count.
void _delay_loop_2(unsigned int __count)
16 bitowy licznik, 4 cykle procesora na __count.
void _delay_ms (double __ms)
Wstrzymuje działanie programu na __ms millisekund, używając _delay_loop_2(). Makro
F_CPU powinno zawierać częstotliwość zegara w hercach. Maksymalne możliwe
wstrzymanie to 262.14 ms / (F_CPU w MHz).
void _delay_us (double __us)
Wstrzymuje działanie programu na __us mikrosekund, używając _delay_loop_1(). Makro
F_CPU powinno zawierać częstotliwość zegara w hercach. Maksymalne możliwe
wstrzymanie to 768 us / (F_CPU w MHz).
avr/eeprom.h
Zawiera funkcje dostępu do wewnętrznej pamięci EEPROM.
int eeprom_is_ready()
77
Zwraca wartość różną od 0 jeżeli EEPROM jest gotowy na następną operację (bit EEWE
w rejestrze EECR jest równy 0).
unsigned char eeprom_read_byte(unsigned int *addr)
Czyta jeden bajt z EEPROMu spod adresu addr.
unsigned int eeprom_read_word(unsigned int *addr)
Czyta 16-bitowe słowo z EEPROMu spod adresu addr.
void eeprom_write_byte(unsigned int *addr, unsigned char val);
Zapisuje bajt val do EEPROMu pod adres addr.
void eeprom_read_block(void *buf, unsigned int *addr, size_t n);
Czyta blok o wielkości n bajtów z EEPROMu spod adresu addr do buf.
Makra dla kompatybilności z IAR C.
#define _EEPUT(addr, val) eeprom_wb(addr, val)
#define _EEGET(var, addr) (var) = eeprom_rb(addr)
avr/ina90.h
Ten plik nagłówkowy zawiera kilka funkcji i makr do łatwiejszego przenoszenia aplikacji
z kompilatora IAR C do avr-gcc. Jednak nie powinno się go używać do pisania aplikacji
„od początku”.
avr/interrupt.h
Zawiera funkcje obsługi przerwań.
sei()
Włącza przerwania. Makro.
cli()
Wyłącza przerwania. Makro.
void enable_external_int(unsigned char ints)
Wpisuje ints do rejestrów EIMSK lub GIMSK, w zależności, który rejestr zdefiniowany w
mikrokontrolerze: EIMSK lub GIMSK.
void timer_enable_int( unsigned char ints );
Wpisuje ints do rejestru TIMSK, jeżeli TIMSK jest zdefiniowany
78
avr/io.h
Włącza pliki nagłówkowe avr/sfr_defs.h oraz odpowiedni avr/io[MCU].h. Służy do
definiowania stałych specyficznych dla danego mikrokontrolera na podstawie parametru mmcu=typ_MCU przekazanego do kompilatora. Ten plik powinien być włączany w
każdym programie na mikrokontroler AVR.
avr/io[MCU].h
Definicje rejestrów I/O dla odpowiedniego typu mikrokontrolera, gdzie [MCU] jest
tekstem określającym typ w rodzaju 2313, 8515 itp. Zobacz do dokumentacji
mikrokontrolera. Tych plików nie należy włączać do pisanych programów – robi to za nas
avr/io.h na podstawie parametru -mmcu=typ_MCU przekazanego do kompilatora np. w
pliku makefile.
avr/parity.h
Zawiera definicje funkcji pomocnej w obliczaniu bitu parzystości lub nieparzystości.
parity_even_bit(val)
avr/pgmspace.h
Zawiera funkcje dostępu do danych znajdujących się w pamięci programu.
#define PGM_P const prog_char *
Służy do deklaracji zmiennej, która jest wskaźnikiem do łańcucha znaków w pamięci
programu.
#define PGM_VOID_P const prog_void *
Służy do deklaracji wskaźnika do dowolnego obiektu w pamięci programu.
#define PSTR(s) ({static char __c[] PROGMEM = (s); __c;})
Służy do deklaracji wskaźnika do łańcuha znaków w pamięci programu.
unsigned char __elpm_inline(unsigned long __addr) [static]
Służy do odczytania zawartości pamięci programu o adresie powyżej 64kB (ATmega103,
ATmega128). Jeżeli jest to możliwe, należy umieścić tablice ze stałymi poniżej granicy
64kB (jest to bardziej efektywne rozwiązanie).
void *memcpy_P(void* dest, PGM_VOID_P src, size_t n)
79
Kopiuje n znaków z jednego ciągu do drugiego. Jako wynik zwraca wskaźnik do dest. Jest
odpowiednikiem funkcji memcpy() z tą różnicą, że łańcuch src znajduje się w pamięci
programu.
int strcasecmp_P(const char* s1, PGM_P s2)
Porównuje s1 z s2, ignorując wielkość liter. Parametr s1 jest wskaźnikiem do łańcucha
znajdującego się w pamięci SRAM. Parametr s2 jest wskaźnikiem do łańcucha
znajdującego się w pamięci programu. Zwraca wartość mniejszą od 0 jeżeli s1 jest
mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2. Jest
odpowiednikiem funkcji strcasecmp() z tą różnicą, że łańcuch s2 znajduje się w pamięci
programu.
char *strcat_P(char* dest, PGM_P src)
Dołącza znaki jednego ciągu do drugiego. Jako wynik zwraca wskaźnik do dest. Jest
odpowiednikiem funkcji strcat() z tą różnicą, że łańcuch src znajduje się w pamięci
programu.
int strcmp_P(const char* s1, PGM_P s2)
Porównuje s1 z s2, uwzględniając wielkość liter. Parametr s1 jest wskaźnikiem do
łańcucha znajdującego się w pamięci SRAM. Parametr s2 jest wskaźnikiem do łańcucha
znajdującego się w pamięci programu. Zwraca wartość mniejszą od 0 jeżeli s1 jest
mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2. Jest
odpowiednikiem funkcji strcmp() z tą różnicą, że łańcuch s2 znajduje się w pamięci
programu.
char* strcpy_P(char* dest, PGM_P src)
Kopiuje src do dest. Jako wynik zwraca wskaźnik do dest. Jest odpowiednikiem funkcji
strcpy() z tą różnicą, że łańcuch src znajduje się w pamięci programu.
size_t strlen_P(PGM_P src)
Zwraca ilość znaków w src. Jest odpowiednikiem funkcji strlen() z tą różnicą, że łańcuch
src znajduje się w pamięci programu.
int strncasecmp_P(const char *s1, PGM_P s2, size_t n)
Porównuje pierwszych n znaków s1 z s2, ignorując wielkość liter. Parametr s1 jest
wskaźnikiem do łańcucha znajdującego się w pamięci SRAM. Parametr s2 jest
wskaźnikiem do łańcucha znajdującego się w pamięci programu. Parametr n określa ile
znaków ma być porównywanych. Zwraca wartość mniejszą od 0 jeżeli pierwsze n znaków
s1 jest mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2. Jest
odpowiednikiem funkcji strncasecmp() z tą różnicą, że łańcuch s2 znajduje się w pamięci
programu.
int strncmp_P(const char* s1, PGM_P s2, size_t n)
Porównuje pierwszych n znaków s1 z s2, uwzględniając wielkość liter. Parametr s1 jest
wskaźnikiem do łańcucha znajdującego się w pamięci SRAM. Parametr s2 jest
wskaźnikiem do łańcucha znajdującego się w pamięci programu. Parametr n określa ile
znaków ma być porównywanych. Zwraca wartość mniejszą od 0 jeżeli pierwsze n znaków
s1 jest mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2. Jest
odpowiednikiem funkcji strncmp() z tą różnicą, że łańcuch s2 znajduje się w pamięci
programu.
80
char* strncpy_P(char* dest, PGM_P src, size_t n)
Kopiuje nie więcej niż n bajtów z src do dest. Jako wynik zwraca wskaźnik do dest. Jest
odpowiednikiem funkcji strncpy() z tą różnicą, że łańcuch src znajduje się w pamięci
programu.
avr/sfr_defs.h
Zawiera wiele bardzo przydatnych makr dla dostępu do portów wejścia/wyjścia.
_BV(x)
Zwraca wartość bitu (bit value) x. Zdefiniowany jako (1 << x). Makro.
inb(sfr)
Czyta bajt z sfr. Makro.
outb(sfr, val)
Wpisuje val do sfr. Makro. Odwrotnie jak inb(sfr).
cbi(sfr, bit)
Kasuje bit w sfr. Makro.
sbi(sfr, bit)
Ustawia bit w sfr. Makro.
bit_is_set(sfr, bit)
Zwraca wartość różną od 0, jeżeli bit w sfr jest ustawiony, w przeciwnym wypadku 0.
Makro.
bit_is_clear(sfr, bit)
Zwraca wartość różną od 0, jeżeli bit w sfr jest skasowany, w przeciwnym wypadku 0.
Makro.
loop_until_bit_ist_set(sfr, bit)
Wstrzymuje działanie programu (wykonuje pętlę) dopóki bit w sfr jest ustawiony. Makro.
loop_until_bit_is_clear(sfr, bit)
Wstrzymuje działanie programu (wykonuje pętlę) dopóki bit w sfr jest skasowany. Makro.
avr/signal.h
Definiuje nazwy uchwytów dla przerwań, które znajdują się na początku pamięci FLASH.
Oto one:
SIG_INTERRUPT0 do SIG_INTERRUPT7
81
Uchwyty funkcji obsługi przerwań zewnętrznych od 0 do 7. Przerwania o numerach
większych od 1 są dostępne tylko w niektórych układach ATmega.
SIG_OUTPUT_COMPARE2
Uchwyt funkcji obsługi przerwania od porównania licznika 2.
SIG_OVERFLOW2
Uchwyt funkcji obsługi przerwania do przepełnienia licznika 2.
SIG_INPUT_CAPTURE1
Uchwyt funkcji obsługi przerwania od przechwytywania licznika 1.
SIG_OUTPUT_COMPARE1A
Uchwyt funkcji obsługi przerwania od porównania licznika 1 (A).
SIG_OUTPUT_COMPARE1B
Uchwyt funkcji obsługi przerwania od porównania licznika 1 (B).
SIG_OVERFLOW1
Uchwyt funkcji obsługi przerwania do przepełnienia licznika 1.
SIG_OUTPUT_COMPARE0
Uchwyt funkcji obsługi przerwania od porównania licznika 0.
SIG_OVERFLOW0
Uchwyt funkcji obsługi przerwania do przepełnienia licznika 0.
SIG_SPI
Uchwyt funkcji obsługi przerwania SPI.
SIG_UART_RECV
Uchwyt funkcji obsługi przerwania UART(0) – odbiór znaku.
SIG_UART1_RECV
Uchwyt funkcji obsługi przerwania UART1 – odbiór znaku. UART1 jest dostępny w
niektórych układach ATmega.
SIG_UART_DATA
Uchwyt funkcji obsługi przerwania UART(0) – pusty rejestr danych.
SIG_UART1_DATA
Uchwyt funkcji obsługi przerwania UART1 – pusty rejestr danych. UART1 jest dostępny
tylko w niektórych układach ATmega.
SIG_UART_TRANS
Uchwyt funkcji obsługi przerwania UART(0) – zakończenie transmisji.
SIG_UART1_TRANS
Uchwyt funkcji obsługi przerwania UART1 – zakończenie transmisji. UART1 jest
dostępny tylko w niektórych układach ATmega.
82
SIG_ADC
Uchwyt funkcji obsługi przerwania ADC – zakończenie przetwarzania.
SIG_EEPROM
Uchwyt funkcji obsługi przerwania EEPROM – gotowość.
SIG_COMPARATOR
Uchwyt funkcji obsługi przerwania z komparatora analogowego.
SIGNAL(signame)
Używany do definicji uchwytu sygnału dla signame.
INTERRUPT(signame)
Używany do definicji uchwytu przerwania dla signame.
Dla uchwytu zdefiniowanego w SIGNAL(), dodatkowe przerwania są bezwarunkowo
zabronione, natomiast w uchwycie INTERRUPT(), pierwszą (bezwarunkowo) instrukcją
jest sei, i występujące w tym czasie przerwania mogą być obsługiwane.
avr/sleep.h
Zawiera definicje i funkcje pomocne w zarządzaniu poborem energii.
#define SLEEP_MODE_ADC
Redukcja zakłóceń z przetwornika analogowo/cyfrowego.
#define SLEEP_MODE_EXT_STANDBY
Rozszerzony tryb gotowości (Extended Standby).
#define SLEEP_MODE_IDLE
Tryb bezczynny (Idle).
#define SLEEP_MODE_PWR_DOWN
Wyłączenie zasilania (Power Down).
#define SLEEP_MODE_PWR_SAVE
Oszczędzanie zasilania (Power Save).
#define SLEEP_MODE_STANDBY
Tryb gotowości (Standby).
void set_sleep_mode(uint8_t mode)
Ustawia bity w rejestrze MCUCR aby wybrać odpowiedni tryb uśpienia.
void sleep_mode(void)
Wprowadza kontroler w tryb uśpienia na podstawie wcześniej wybranego trybu za pomocą
funkcji set_sleep_mode().
83
Aby uzyskać więcej informacji, zobacz do dokumentacji mikrokontrolera.
avr/timer.h
Zawiera definicje funkcji kontrolujących działanie licznika/czasomierza 0.
void timer0_source(unsigned int src)
Wpisuje src w rejestr TCCR0. Wartość src może przyjmować następujące wartości
symboliczne:
enum {
STOP = 0,
CK = 1,
CK8 = 2,
CK64 = 3,
CK256 = 4,
CK1024 = 5,
T0_FALLING_EDGE = 6,
T0_RISING_EDGE = 7
};
void timer0_stop()
Zatrzymuje Timer 0 poprzez wyzerowanie rejestru TCNT0.
void timer0_start()
Startuje Timer 0 poprzez wpisanie 1 w rejestr TCNT0.
avr/twi.h
Definiuje kilka stałych dla obsługa magistrali TWI (i2c) w ATMega.
avr/wdt.h
Zawiera definicje i funkcje pomocne w używaniu układu watchdoga.
wdt_reset()
Powoduje kasowanie czasomierza układu Watchdog.
wdt_enable(timeout)
Ustawia odpowiedni timeout i uruchamia układ watchdoga. Zobacz do dokumentacji
Atmel AVR. Wartość timeout może przyjmować jedną z predefiniowanych wartości:
WDTO_15MS
WDTO_30MS
WDTO_60MS
84
WDTO_250MS
WDTO_500MS
WDTO_1S
WDTO_2S
wdt_disable()
Wyłącza układ watchdoga.
ctype.h
Zawiera definicje funkcji testujących i zamieniających typy znakowe.
int isalnum(int __c);
Zwraca 1 jeżeli __c jest cyfrą lub literą, w przeciwnym wypadku 0.
int isalpha(int __c);
Zwraca 1 jeżeli __c jest literą, w przeciwnym wypadku 0.
int isascii(int __c);
Zwraca 1 jeżeli __c zawiera się w 7 bitowym ASCII, w przeciwnym wypadku 0.
int iscntrl(int __c);
Zwraca 1 jeżeli __c jest znakiem kontrolnym, w przeciwnym wypadku 0.
int isdigit(int __c);
Zwraca 1 jeżeli __c jest cyfrą, w przeciwnym wypadku 0.
int isgraph(int __c);
Zwraca 1 jeżeli __c jest „drukowalne” (z wyjątkiem spacji), w przeciwnym wypadku 0.
int islower(int __c);
Zwraca 1 jeżeli __c jest małą literą alfabetu, w przeciwnym wypadku 0.
int isprint(int __c);
Zwraca 1 jeżeli __c jest „drukowalne” (ze spacją), w przeciwnym wypadku 0.
int ispunct(int __c);
Zwraca 1 jeżeli __c jest znakiem interpunkcyjnym, w przeciwnym wypadku 0.
int isspace(int __c);
Zwraca 1 jeżeli __c jest spacją lub '\n', '\f', '\r', '\t', '\v', w przeciwnym wypadku 0.
int isupper(int __c);
Zwraca 1 jeżeli __c jest dużym znakiem alfanumerycznym, w przeciwnym wypadku 0.
int isxdigit(int __c);
Zwraca 1 jeżeli __c jest cyfrą szesnastkową (0-9 lub A-F), w przeciwnym wypadku 0.
85
int toascii(int __c);
Zamienia __c na 7 bitowy znak ASCII.
int tolower(int __c);
Zamienia __c na małą literę.
int toupper(int __c);
Zamienia __c na dużą literę.
errno.h
Obsługa błędów.
int errno;
Przechowuje systemowy kod błędu
inttypes.h
Definiuje typy danych całkowitych.
typedef signed char int8_t;
typedef unsigned char uint8_t;
Typy 8-bitowe
typedef int int16_t;
typedef unsigned int uint16_t;
Typy 16-bitowe
typedef long int32_t;
typedef unsigned long uint32_t;
Typy 32-bitowe
typedef long long int64_t;
typedef unsigned long long uint64_t;
Typy 64-bitowe
typedef int16_t intptr_t;
typedef uint16_t uintptr_t;
Typy wskaźnikowe
Należy świadomie używać opcji kompilatora -mint8 – nie będą wtedy dostępne typy 32 i
64 bitowe.
86
math.h
M_PI = 3.141592653589793238462643
Liczba PI.
M_SQRT2 = 1.4142135623730950488016887
Pierw. kwadr. z 2
double cos(double x)
Zwraca cosinus z x.
double fabs(double x)
Zwraca absolutną wartość z x.
double fmod(double x, double y)
Zwraca zmiennoprzecinkową resztę z dzielenia x/y.
double modf(double x, double *iptr)
Zwraca część ułamkową z x i zapamiętuje część całkowitą w *iptr.
double sin(double x)
Zwraca sinus z x.
double sqrt(double x)
Zwraca pierwiastek kwadratowy z x.
double tan(double x)
Zwraca tangens z x.
double floor(double x)
Zwraca większą wartość całkowitą mniejszą niż x.
double ceil(double x)
Zwraca mniejszą wartość całkowitą większą niż x.
double frexp(double x, int *exp)
Rozdziela x na znormalizowany ułamek, który jest zwracany, i na wykładnik, który jest
zapamiętany w *exp.
double ldexp(double x, int exp);
Zwraca x^exp.
double exp(double x)
Zwraca e^x.
double cosh(double x)
Zwraca cosinus hiperboliczny z x.
double sinh(double x)
Zwraca sinus hiperboliczny z x.
87
double tanh(double x)
Zwraca tangens hiperboliczny z x.
double acos(double x)
Zwraca arcus cosinus z x.
double asin(double x)
Zwraca arcus sinus z x.
double atan(double x)
Zwraca arcus tangens z x. Wyjście między -PI/2 i PI/2 (włącznie).
double atan2(double x, double y)
Zwraca arcus tangens z x/y. Zwraca uwagę na znak argumentów. Wyjście pomiędzy -PI a
PI (włącznie).
double log(double x)
Zwraca logarytm naturalny z x.
double log10(double x)
Zwraca logarytm dziesiętny z x.
double pow(double x, double y)
Zwraca x^y.
double strtod(const char *s, char **endptr)
Zamienia łańcuch ASCII na liczbę typu double.
double square(double x)
Zwraca x2.
double inverse(double x)
Zwraca 1/x.
UWAGA. Aby skorzystać z tych funkcji należy włączyć do projektu bibliotekę libm.a.
setjmp.h
int setjmp(jmp_buf env)
Deklaruje długi skok do miejsca przeznaczenia wykonanego przez longjmp().
void longjmp(jmp_buf env, int val)
Wykonuje długi skok do pozycji wcześniej zdefiniowanej przez setjmp(env), która
powinna zwrócić val.
88
stdlib.h
Definiuje następujące typy:
typedef struct {
int quot;
int rem;
} div_t;
typedef struct {
long quot;
long rem;
} ldiv_t;
typedef int (*__compar_fn_t)(const void *, const void *);
Używane w funkcjach porównujących np. qsort().
void abort();
Skutecznie przerywa wykonywanie programu przez wprowadzenie MCU w nieskończoną
pętlę.
long labs( long x );
Zwraca absolutną wartość x typu long.
div_t div( int x, int y );
Dzieli x przez y i zwraca rezultat (iloraz i resztę) w strukturze div_t.
ldiv_t ldiv( long x, long y );
Dzieli x przez y i zwraca rezultat (iloraz i resztę) w strukturze ldiv_t.
void qsort(void *base, size_t nmemb, size_t size, __compar_fn_t compar);
Sortuje tablicę base z nmemb elementami rozmiaru size, używając funkcji porównującej
compar.
long strtol(const char *nptr, char **endptr, int base);
Zamienia łańcuch nptr według podstawy base na liczbę typu long.
unsigned long strtoul(const char *nptr, char **endptr, int base);
Zamienia łańcuch nptr według podstawy base na liczbę typu unsigned long.
long atol( char *p );
Zamienia łańcuch p na liczbę typu long.
int atoi( char *p );
Zamienia łańcuch p na liczbę typu int.
void *malloc( size_t size );
Alokuje size bajtów pamięci i zwraca wskaźnik do niego.
void free( void *ptr );
89
Zwalnia pamięć wskazywaną przez ptr, która była wcześniej zaalokowana funkcją
malloc().
char *itoa( int value, char *string, int radix );
Zamienia liczbę całkowitą na łańcuch. Nie jest kompatybilna z ANSI C, lecz może być
użyteczna.
string.h
void *memcpy( void *to, void *from, size_t n );
Kopiuje n bajtów z from do to.
void *memmove( void *to, void *from, size_t n );
Kopiuje n bajtów z from do to, gwarantując poprawność zachowania dla nakładających się
łańcuchów.
void *memset( void *s, int c, size_t n );
Ustawia n bajtów z s na wartość c.
int memcmp( const void *s1, const void *s2, size_t n );
Porównuje n bajtów między s1 a s2.
void *memchr( void *s, char c, size_t n );
Zwraca wskaźnik do pierwszego wystąpienia c w pierwszych n bajtach s.
size_t strlen( char *s );
Zwraca długość łańcucha s.
char *strcpy( char *dest, char *src );
Kopiuje src do dest. Jako wynik zwraca wskaźnik do dest.
char *strncpy( char *dest, char *src, size_t n );
Kopiuje nie więcej niż n bajtów z src do dest. Jako wynik zwraca wskaźnik do dest.
char *strcat( char *dest, char *src );
Dołącza src do dest. Jako wynik zwraca wskaźnik do dest.
char *strncat( char *dest, char *src, size_t n );
Dołącza nie więcej niż n bajtów z src do dest. Jako wynik zwraca wskaźnik do dest.
int strcmp( const char *s1, const char *s2 );
Porównuje s1 z s2, uwzględniając wielkość liter. Zwraca wartość mniejszą od 0 jeżeli s1
jest mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2.
int strncmp( const char *s1, const char* s2, size_t n );
Porównuje pierwszych n znaków s1 z s2, uwzględniając wielkość liter. Parametr n określa
ile znaków ma być porównywanych. Zwraca wartość mniejszą od 0 jeżeli pierwsze n
90
znaków s1 jest mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe
od s2.
strdupa( s );
Duplikuje s, zwracając identyczny łańcuch. Makro.
strndupa( s, n );
Zwraca zaalokowaną kopię n batów z s. Makro.
char *strchr( const char *s, int c );
Zwraca wskaźnik do pierwszego wystąpienia c w s.
char *strrchr( const char *s, int c );
Zwraca wskaźnik do ostatniego wystąpienia c w s.
size_t strnlen( const char *s, size_t maxlen );
Zwraca długość łańcucha s, ale nie więcej niż maxlen.
void *memccpy(void *dest, const void *src, int c, size_t n);
Kopiuje nie więcej niż n bajtów z src do dest dopóki zostanie znaleziony c.
int strcasecmp(const char *s1, const char *s2);
Porównuje s1 z s2, ignorując wielkość liter. Zwraca wartość mniejszą od 0 jeżeli s1 jest
mniejsze od s2. Zero jeśli są równe. Większą od zera jeśli s1 jest większe od s2.
char *strlwr(char *s);
Zamienia wszystkie duże litery w łańcuchu s na małe.
int strncasecmp(const char *s1, const char *s2, size_t n);
Porównuje n bajtów z s1 i s2, ignorując wielkość znaków.
char *strrev(char *s1);
Odwraca kolejność znaków w s1.
char *strstr(const char *haystack, const char *needle);
Znajduje needle w haystack, i zwraca wskaźnik do niego.
char *strupr(char *s);
Zamienia wszystkie małe litery w łańcuchu s na duże.
AVR-GCC - kompilacja środowiska ze źródeł
Opis ten będzie bardzo ogólny gdyż dotyczy bardzo wielu systemów operacyjnych i nie
sposób tu zawrzeć wszystkich możliwych opcji. Jednak myślę, że będzie pomocny
osobom, które podejmą trud samodzielnego skompilowania środowiska.
Na początku należy zgromadzić wersje źródłowe pakietów: binutils, gcc-core, avr-libc.
Wszystkie można pobrać z bardzo z internetu. Aby dokonać kompilacji na wybraną przez
siebie platformę należy dysponować kompilatorem GCC - większość dystrybucji systemu
Linux jest w niego domyślnie wyposażona. Pracując w systemie MS Windows mamy do
91
wyboru dwa środowiska: "Cygwin" i "MinGW". "Cygwin" jest środowiskiem
kompatybilnym z systemami unixowymi (standard Posix) natomiast "MinGW" jest tzw.
"minimalistyczną" wersją programów GNU dostosowaną specjalnie do systemu MS
Windows (nazwa MinGW pochodzi od: Minimalist GNU for Windows).
Opis instalacji i używania powyższych i innych środowisk zostanie pominięty - polecam
skorzystanie z bogatej bazy wiedzy znajdującej się w internecie.
Pakiet binutils
W pierwszej kolejności należy skompilować pakiet binutils. Ponieważ pliki źródłowe są
zarchiwizowane za pomocą programu tar i spakowane programem bzip2 (rozszerzenie
.bz2) lub gzip (rozszerzenie .gz) należy je najpierw rozpakować za pomocą polecenia:
bzip2 -dc binutils*.bz2 | tar xvf -
Jeżeli archiwum ma rozszerzenie .gz to zmieniamy powyższe polecenie bzip2 na gzip
(reszta pozostaje bez zmian). Następnie przechodzimy do utworzonego w wyniku
wykonania powyższego polecenia katalogu binutils*:
cd binutils*
i konfigurujemy programy do pracy z kontrolerami AVR:
configure --target=avr --prefix=/avrgcc --disable-nls
opcja --prefix oznacza ścieżkę, w której będą instalowane programy jeśli jej nie podamy
zostanie użyta domyślna (zazwyczaj będzie to /usr/local), opcja --disable-nls oznacza
wyłączenie wsparcia dla innych wersji językowych. Po wykonaniu powyższej instrukcji
zostanie utworzony plik make file i będzie można przeprowadzić właściwą kompilację
poprzez wydanie polecenia:
make
Gdy kompilacja zostanie zakończona należy pakiet zainstalować czyli umieścić pliki
wynikowe w miejscu podanym przez opcję -prefix skryptu configure. Wydajemy
polecenie:
> make install
po przejściu do katalogu, wybranym opcją -prefix (tu: /avrgcc) i wylistowaniu jego
zawartości zobaczymy tam podkatalog bin.
Pakiet gcc-core
Po udanej kompilacji i instalacji pakietu binutils możemy skompilować właściwy
kompilator odbywa się to analogicznie. Rozpakowujemy archiwum ze źródłami:
bzip2 -dc gcc-core*.bz2 | tar xvf -
konfigurujemy źródła do pracy z kontrolerami AVR:
92
configure
--target=avr
languages=c
--prefix=/avrgcc
--disable-nls
--enable-
opcja --enable-languages=c oznacza, że kompilator będzie w stanie kompilować jedynie
programy w języku C. Pozostałe opcje zostały omówione w opisie kompilacji pakietu
binutils. Po wykonaniu polecenia zostanie utworzony plik makefile i będzie można
przeprowadzić właściwą kompilację poprzez wywołanie polecenia:
make
Gdy kompilacja zostanie zakończona należy pakiet zainstalować czyli umieścić pliki
wynikowe
w miejscu podanym przez opcję -prefix skryptu configure. Wydajemy polecenie:
make install
Pakiet avr-libc
Jest to zestaw bibliotek standardowych do kompilatora avr-gcc. Do ich kompilacji jest
niezbędny skompilowany i działający kompilator avr-gcc w związku z czym muszą być
zainstalowane pakiety -avr-binutils i avr-gcc oraz dodana do systemu ścieżka poszukiwań
odpowiednia do opcji --prefix w programie configure uzupełniona o katalog bin np.
/avrgcc/bin. Rozpakowujemy archiwum ze źródłami:
gzip -dc avr-libc*.gz | tar xvf -
konfigurujemy ścieżkę do programów wynikowych za pomocą polecenia:
doconf --prefix=/avrgcc
zostanie utworzony plik makefile i będzie można przeprowadzić właściwą kompilację
poprzez wywołanie:
domake
Gdy kompilacja zostanie zakończona należy pakiet zainstalować czyli umieścić pliki
wynikowe
w miejscu podanym przez opcję -prefix skryptu doconf. Wydajemy polecenie:
domake install
Od tego momentu mamy do dyspozycji swoje własne środowisko kompilatora avr-gcc :)
93

Podobne dokumenty