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