383 28 2MB
Polish Pages 39 Year 2005
KURS
AVR-GCC: kompilator C dla mikrokontrolerów AVR, część
1
Rozpoczynamy cykl artykułów, których zadaniem jest przedstawienie podstaw oraz praktycznych zasad programowania mikrokontrolerów AVR w języku C z użyciem kompilatora avr-gcc. Oczywiście wybór kompilatora AVR-GCC może się jednym podobać, a innym nie. Postaramy się jednak uzasadnić, że nie jest to zły wybór. Zanim przejdziemy do konkretów na początek kilka słów uzasadniających taki właśnie wybór narzędzi. Po pierwsze: po co w ogóle język wysokiego poziomu skoro mikrokontrolery świetnie programuje się w assemblerze, który daje pełną kontrolę nad kodem i zasobami procesora i pozwala na uzyskanie maksymalnej szybkości i zwięzłości? Otóż cały problem leży w skali. W przypadku małych układów z niewielkimi zasobami, wykonujących niezbyt złożone zadania (proste pomiary, sterowania czy transmisje) asembler rzeczywiście będzie całkowicie wystarczający (a czasem wręcz niezastąpiony). Gdy jednak program się nam rozrasta i komplikuje (obróbka większych ilości danych, bardziej złożone przeliczenia i konwersje, zaawansowane algorytmy sterowania itp.), prędzej czy później dochodzimy do progu, powyżej którego nasze dzieło staje się coraz mniej czytelne i coraz trudniejsze do opanowania. Nagle stwierdzamy, że dotychczasowe doświadczenia w asemblerze to za mało żeby szybko i skutecznie rozwiązać szerszy zakres problemów (taka zresztą była w ogóle geneza stworzenia języków wyższego poziomu). Tutaj przechodzimy do drugiego pytania. Po drugie: dlaczego właśnie C a nie np. BASIC czy Pascal? Otóż C był od początku projektowany jako język możliwie maksymalnie zbliżony do sprzętu i generujący kod niewiele odbiegający od samodzielnie pisanego w asemblerze. W połączeniu z optymalizatorem kodu (składnik każdego dobrego kompilatora) pozwala to na uzyskanie zaskakująco zwartego, krótkiego i szybkiego programu wynikowego. Oczywiście mamy również możliwość dopisania w czystym asemblerze fragmentów krytycznych czasowo (jak np. obsługa przerwań) jeśli nie zadowala nas kod generowany automatycznie. W ten sposób
Elektronika Praktyczna 3/2005
możemy bez problemu połączyć największe zalety obu sposobów programowania. Następna sprawa to przenośność. Znaczne fragmenty kodu (a szczególnie algorytmy, przeliczenia, konwersje itp. - czyli elementy nie korzystające bezpośrednio ze specyficznych zasobów i interfejsów danego mikrokontrolera) możemy łatwo zastosować w programie dla zupełnie innej kostki (praktycznie każda rodzina mikrokontrolerów posiada opracowany kompilator C - z innymi językami nie jest tak dobrze). Tu od razu przechodzimy do następnej zalety C: rozpowszechnienia. C jest od lat ogólnie przyjętym standardem programowania z czym wiąże się ogromna ilość dostępnych materiałów: bibliotek, przykładowych kodów, opisów, tutoriali, gotowych rozwiązań sprzętowo - programowych. W wielu przypadkach wystarczy dobrze poszukać w zasobach sieciowych żeby znaleźć prawie gotowe rozwiązania własnych zadań programowych. Po trzecie: dlaczego kompilator avr-gcc (o którym krążą opinie, że jest niewdzięczny i trudny w konfiguracji i obsłudze) a nie jakieś inne rozpowszechnione narzędzie - jak np. CodeVision czy ICC? Jednym z koronnych argumentów jest oczywiście fakt, że avr-gcc jest bezpłatny. Ale to nie wszystko: jest to narzędzie dostępne dla wielu platform (więc bez problemu możemy przenosić się z naszymi projektami pomiędzy np. Windows a Linuksem). Przy tym avr-gcc jest produktem open-source, z czym wiąże się cały szereg udogodnień: mamy cały czas dostęp do najnowszych uaktualnień, pełnej informacji o wykrytych błędach, a także do ogromnych zasobów bardziej lub mniej zaawansowanego kodu tworzonych i oferowanych do swobodnego wykorzystywania, możemy też cały czas liczyć na wsparcie i podpowiedzi na aktywnie działających
międzynarodowych forach, grupach i listach. A w dodatku - jak zaraz się przekonamy - przy użyciu dodatkowych narzędzi wspomagających avr-gcc staje się bardzo poręcznym i wygodnym w użyciu instrumentem. Można też dodać - już poza kontekstem stosowania w AVR - że gcc ma wersje (tzw. porty) dla wielu innych mikroprocesorów (np. MSP 430 czy ARM) więc raz opanowany znacznie ułatwi ewentualne „przesiadki”. Jednak żeby nie wyglądało to jak reklamowa laurka należy też powiedzieć o mankamentach. Główny z nich to nie do końca „rozpracowana” obsługa przez gcc nieciągłej przestrzeni adresowej AVR (dokładniej omówimy to w trakcie prezentacji przykładów). Zaleta jaką jest ciągły rozwój gcc może też stać się wadą w momencie wprowadzenia bardziej radykalnych zmian wymagających korekt we wcześniej działających projektach. Czasem pojawiają się drobniejsze błędy widoczne tylko w specyficznych sytuacjach (być może dlatego przeoczone w trakcie prac
Rys. 1. Przykładowy przebieg kompilacji kodu źródłowego
81
KURS
Rys. 2. Struktura folderów kompilatora avr-gcc
nad kompilatorem). Czy te wady są bardzo uciążliwe - ocenimy sami po pierwszych próbach programowania.
Nasze środowisko uruchomieniowe
Nasz „kursowy” zestaw do programowania AVR składa się z następujących pakietów narzędzi: – właściwego kompilatora avr-gcc, który jest programem typu CLI (command line interface) i bez dodatkowego wsparcia musiałby być obsługiwany z poziomu konsoli tekstowej; – graficznego środowiska eliminującego powyższą niedogodność - w tej roli występuje bezpłatne AvrSide pozwalające na szybkie i intuicyjne wykonanie podstawowych operacji (wersja PL); – najnowszej, mocno ostatnio rozwiniętej i unowocześnionej wersji firmowego pakietu Atmela AvrStudio, który służy jako symulator i debugger do testowania naszych przykładowych projektów. Całość jest zainstalowana na platformie Windows. Dopuszczalne są wersje 98, ME, XP, 2000. AvrSide nie da się uruchomić pod NT i 95 ze względu na brak obsługi magistrali USB (koniecznej dla wbudowanego
82
w środowisko programatora). Wymogi sprzętowe nie są krytyczne - jednak rzecz jasna szybkość i komfort pracy będą mocno zależeć od mocy komputera. Prototyp opisywanego zestawu używany przy opracowaniu kursu został uruchomiony na platformie Athlon XP 2600+512 MB+Windows XP Pro PL, która zapewniła rzeczywiście wygodną i bezproblemową pracę. Zauważmy, że nie ma na razie mowy o żadnym współpracującym układzie sprzętowym mikrokontrolera - wrócimy do tego tematu nieco później. Skąd to wszystko wziąć? Ponieważ artykuły przygotowywane są ze sporym wyprzedzeniem, a potrzebne programy są wciąż rozwijane - najlepiej sięgnąć do źródłowych witryn projektów po najnowsze wersje. AvrSide znajdziemy na http://www.avrside.fr.pl albo na mirrorze http://www. avrside.ep.com.pl. Instalujemy najpierw podstawową wersję D5 a następnie zastępujemy plik wykonawczy AvrSide.exe najnowszym dostępnym. Aktualne szczegóły znajdziemy w dołączonych opisach i bezpośrednio na w/w stronach. AvrStudio (w chwili pisania w wersji 4.10) jest udostępnione do bezpłatnego pobrania z firmowej witryny Atmela http://www.atmel.com. Trochę więcej zastanowienia wymaga sam kompilator avr-gcc. Sztandarową dystrybucją avr-gcc dla Windows jest pakiet WinAvr Erica Weddingtona. Jednak nie należy utożsamiać avr-gcc z WinAvr jak to jest często w uproszczeniu podawane. WinAvr jest ogromnym zestawem wszelkich narzędzi open-source przydatnych w programowaniu AVR. Oprócz aktualnej wersji avr-gcc znajdziemy tam notatnik programisty wspomagający tworzenie projektów, symulator simulavr, debugger avr-gdb z graficznym interfejsem Insight, programator avrdude, Avarice - interfejs komunikacji pomiędzy avr-gdb a sprzętowym adapterem JTAG, szeroki wachlarz małych pomocniczych programów narzędziowych ogólnego stosowania, generator plików makefile MFile oraz ogromny zbiór pomocy, manuali i dokumentacji. WinAvr jest aktualizowane co kilka miesięcy i dostępne na http://winavr.sourceforge.net. Wielką zaletą WinAvr jest wszechstronność dostarczonego materiału, który pozwala na dogłębne zapoznanie się z metodyką programowania, a także na indywidualny wybór naj-
bardziej „pasującego” zestawu narzędzi. Jednak dla początkującego ta wszechstronność może zmienić się w poważną wadę: po prostu trudno się w tym wszystkim połapać. Dodatkowym utrudnieniem jest konieczność zapoznania się (przynajmniej w ogólnym zarysie) z zasadami działania i używania managera procesów make, stosowanego standardowo do uruchamiania kompilatora. Ale w naszym środowisku ten nadmiar w niczym nie przeszkadza. Wykorzystamy po prostu tylko sam kompilator (AvrSide nie używa make), gdy zaś zechcemy poczytać dokumentację albo wypróbować inne techniki opracowania projektów wszystko będzie pod ręką. AvrSide było zresztą początkowo dedykowane do współpracy z WinAvr i domyślnie jest instalowane w jego folderze. Dopiero później pojawiła się własna dystrybucja avr-gcc (także na stronie http://www.avrside.fr.pl). Obecnie mamy więc do wyboru dwie możliwości: – instalacja kompletnego WinAvr a następnie AvrSide domyślnie w folderze głównym WinAvr; – uproszczona instalacja samego AvrSide we własnym niezależnym folderze np. c:\AvrSide i uzupełnienie kompilatorem (właśnie tak jest skonfigurowane środowisko używane na potrzeby artykułu). W obu przypadkach AvrStudio instalujemy całkiem niezależnie zgodnie z zaleceniami Atmela. Zanim zabierzemy się do pisania pierwszych programów zapoznajmy się ogólnie z działaniem elementów środowiska programistycznego. Znakomicie ułatwi to dalszą pracę i analizę pierwszych przykładów. Można oczywiście na razie te zagadnienia przejrzeć tylko pobieżnie - będziemy wielokrotnie do nich wracać.
Jak działa avr-gcc
Avr-gcc określamy ogólnym mianem kompilatora - jednak w rzeczywistości jest to cały szereg współpracujących ze sobą narzędzi i bibliotek używanych w odpowiedniej kolejności i z potrzebnymi opcjami („właściwy” kompilator avr-gcc, zestaw narzędziowy avr-binutils oraz biblioteki dla rodziny AVR avr-libc). Celem jest przetworzenie kodu źródłowego C zapisanego w jednym lub wielu plikach projektu na wynikowe pliki: kodu wykonawczego wpisywanego do pamięci Flash wybranego
Elektronika Praktyczna 3/2005
KURS modelu mikrokontrolera oraz (ewentualnie) danych wpisywanych do wewnętrznej pamięci EEPROM. W trakcie powstaje także szereg pomocniczych plików zawierających rozmaite informacje potrzebne do debugowania oraz pozwalające ocenić efekty pracy kompilatora. Przykładowy przebieg przetwarzania jest pokazany na rys. 1. Najpierw pliki źródłowe poddawane są działaniu preprocesora, który realizuje zmiany w kodzie nakazane wpisanymi przez nas dyrektywami: – dołącza dodatkowe pliki z dyrektyw #include, – wstawia definicje oraz rozwija makroinstrukcje z dyrektyw #define, – uwzględnia odpowiednie fragmenty kodu z dyrektyw kompilacji warunkowej #if #else #endif. Tak przygotowany kod poddawany jest właściwej kompilacji czyli przekodowaniu zapisu C na ciąg instrukcji asemblera (z przeprowadzeniem optymalizacji - automatycznego uproszczenia i skrócenia kodu), wynikiem tego są pośrednie pliki *.s. Pliki asemblerowe podlegają następnie przetworzeniu na kod maszynowy (asemblacji). Powstałe pliki *.o są na razie tzw. relokowalne (przemieszczalne) - adresy zmiennych i funkcji nie są jeszcze konkretnie ustalone (pozostają nadal określone jedynie nazwami symbolicznymi użytymi przez nas w programie). Pliki relokowalne są łączone (linkowane, konsolidowane) w następnej operacji - polega to właśnie na ulokowaniu wszystkich porcji kodu w obszarze pamięci programu oraz wyliczeniu i nadaniu symbolom określonych adresów. Jednocześnie zostają dołączone wszelkie niezbędne funkcje biblioteczne wykorzystywane przez nas (jawnie lub pośrednio) w programie, a także specyficzny dla danego typu mikrokontrolera plik kodu inicjalizującego. Wynikiem powyższych zabiegów jest tzw. plik obiektowy (zazwyczaj nadaje mu się rozszerzenie .elf), który zawiera wszelkie informacje o projekcie (kod wynikowy, informacje dla debuggera, wykaz symboli, rozmiary poszczególnych sekcji kodu). Z informacji tych możemy korzystać według potrzeb dekodując potrzebne fragmenty za pomocą zbioru narzędzi binutils. Rysunek pokazuje tylko podstawowe zastosowanie - utworzenie plików wykonywalnych wpisywanych przy pomocy programatora do pamięci Flash oraz EEPROM mikrokontrolera.
Elektronika Praktyczna 3/2005
Do tych - na razie bardzo skrótowych i ogólnych informacji - będziemy wielokrotnie wracać w trakcie omawiania przykładowych projektów. Na razie zobaczmy jeszcze jak są rozmieszczone poszczególne elementy kompilatora i gdzie szukać wymienionych wcześniej narzędzi. Rys. 2 przedstawia drzewko folderów kompilatora. Nie wszystkie subfoldery są dla nas jednakowo istotne ale do niektórych będziemy często zaglądać. Główny folder został nazwany na podstawie użytych wersji narzędzi (avr-gcc w wersji 3.4.2, binutils w wersji 2.15 i biblioteki avr-libc w wersji 1.0.4). W WinAvr powyższa nazwa głównego katalogu nie występuje - rolę tę pełni po prostu \WinAvr\, jednak zasadnicza struktura drzewa subfolderów pozostaje taka sama. Dla użytkownika - programisty AVR najbardziej istotne są podkatalogi: – \bin\ - zawierający zestaw bezpośrednio używanych programów narzędziowych avr-gcc i avr-binutils, – \avr\include\ (oraz \avr\include\ avr\) - grupujący pliki nagłówkowe (headers) biblioteki avr-libc, w szczególności pliki opisujące zasoby poszczególnych kostek, do których często zaglądamy dla sprawdzenia np. nazw rejestrów czy wektorów przerwań, – \avr\lib\ldscripts\ - zawiera skrypty sterujące pracą konsolidatora (linkera), w szczególności opisujące wielkości poszczególnych obszarów pamięci, jej podział na sekcje, adresy początkowe i końcowe, – \lib\gcc\avr\3.4.2\include - znajdziemy tu ogólne pliki nagłówkowe gcc, nie powiązane bezpośrednio z kostkami AVR ale często używane (np. przy zastosowaniu zmiennych logicznych bool), – \doc\avr-libc\avr-libc-user-manual\ - mieści aktualny opis funkcji bibliotecznych avr-libc oraz wiele istotnych szczegółowych informacji dotyczących różnych aspektów programowania (jest to pozycja obowiązkowa - tu zaglądamy prawie codziennie). Pozostałe subfoldery zawierają wewnętrzne podprogramy i biblioteki gcc, wywoływane podczas kompilacji w tle bez naszego bezpośredniego udziału. Zwróćmy uwagę na dość tajemnicze w pierwszej chwili oznaczenia: avr3 - avr4 - avr5. Wynikają one z przyjętego podziału szeregu
kostek AVR na różne typy architektury związane z wielkością zasobów danego mikrokontrolera. Każdy typ posiada oddzielny zestaw bibliotek. Jednak nie musimy się tym kłopotać - na podstawie podanej w wywołaniu nazwy kostki konsolidator samoczynnie wybiera odpowiedni zestaw z właściwego subfolderu (więcej na ten temat powiemy przy opisach działania avr-ld). Teraz pozostaje już tylko pytanie jak użyć tych wszystkich narzędzi dla uzyskania pożądanego efektu. Najbardziej podstawowym sposobem będzie kolejne wpisywanie potrzebnych komend w linii poleceń tekstowej konsoli. Taka metoda - chociaż dobra do przeprowadzenia eksperymentów edukacyjnych - jest zbyt uciążliwa i powolna przy praktycznym programowaniu - potrzebna jest nam automatyzacja całego procesu kompilacji. Tradycyjnym i bardzo rozpowszechnionym rozwiązaniem jest zastosowanie programu narzędziowego make. Mówiąc w wielkim skrócie jest to uniwersalny zarządca procesów, który wykonuje po kolei zadania określone podanymi mu przy wywołaniu zasadami (rules). Zasady te zapisujemy w określony sformalizowany sposób w tekstowych plikach makefile przygotowanych dla każdego projektu. Make jest bardzo wszechstronnym i potężnym narzędziem pozwalającym na praktycznie dowolne kształtowanie jego działania. Jednak ta zaleta potrafi stać się poważną wadą i przeszkodą przy rozpoczynaniu nauki programowania musimy na wstępie opanować dodatkowy spory zasób wiadomości. Poza tym make jest narzędziem typowo tekstowym - nie stanowi to żadnej przeszkody dla miłośników Linuksa, ale przyzwyczajeni do interfejsów graficznych użytkownicy Windows mają prawo mieć inne zdanie (absolutnie nie mam zamiaru powracać tu do odwiecznego sporu o wyższość jednego systemu nad drugim - stwierdzam po prostu fakt). W naszym kursie realizowanym na platformie Windows wykorzystamy więc inne narzędzie. Oczywiście w miarę nabierania doświadczenia opanowanie make będzie również bardzo wskazane - ale to oddzielny temat i w obecnym cyklu artykułów nie będziemy się tym zajmować. Jerzy Szczesiul, EP [email protected]
83
KURS
AVR-GCC: kompilator C mikrokontrolerów AVR, część
2
Kontynuujemy cykl artykułów, których zadaniem jest przedstawienie podstaw oraz praktycznych zasad programowania mikrokontrolerów AVR w języku C z użyciem kompilatora avr-gcc. Oczywiście wybór kompilatora AVR-GCC może się jednym podobać, a innym nie. Postaramy się jednak uzasadnić, że nie jest to zły wybór. Środowisko avrside
Uproszczone zintegrowane środowisko programistyczne (Avr S-imple I-ntegrated D-evelopment E-nvironment) powstało właśnie w celu łatwego rozpoczęcia nauki programowania mikrokontrolerów AVR Atmela z użyciem avr-gcc w systemie Windows. Zawiera edytor kodu źródłowego z kolorowaniem składni oraz systemem podpowiedzi i wyszukiwania. Pozwala na wygodne ustawianie w oknie dialogowym podstawowych opcji kompilacji. Uruchamia - podobnie jak make - kolejno potrzebne podprogramy avr-gcc i raportuje ewentualne błędy wykonania z możliwością ich lokalizacji w kodzie źródłowym. Jednocześnie zaś umożliwia dokładne zapoznanie się z wywołaniami avr-gcc i wynikiem pracy kompilatora, z czego będziemy na bieżąco korzystać. Szczegóły obsługi AvrSide najlepiej chyba będzie objaśniać w trak-
Rys. 3. Struktura folderów AvrSide
Elektronika Praktyczna 4/2005
cie realizacji kolejnych przykładów. Na początek warto jednak spojrzeć podobnie jak w przypadku avr-gcc na strukturę katalogów programu aby nie stanowił on tajemniczej „czarnej skrzynki”. Drzewo katalogów (dotyczące instalacji AvrSide we własnym niezależnym od WinAvr folderze) jest przedstawione na rys. 3. W podkatalogu [bin] umieszczony jest plik wykonawczy AvrSide.exe oraz pliki konfiguracyjne: – desktop.cfg - zapis ogólnej konfiguracji środowiska (np. położenie i rozmiar okien), – autocpl.cfg - plik tekstowy szablonów autokompletacji kodu, – libfunc.cfg - plik tekstowy autokompletacji funkcji avr-libc, – ftd2xx.dll - dodatkowo biblioteka FTDI niezbędna do uruchomienia AvrSide jeśli nie mamy w systemie zainstalowanego sterownika direct dla układów Ft8uxx. W podkatalogu [dev] przechowywane są pliki *.pdf firmowej dokumentacji mikrokontrolerów. Nazwane są zgodnie z typem mikrokontrolera ustalonym w AvrSide, co pozwala na ich otwieranie z poziomu menu pomocy programu. Dalej widzimy omówione wcześniej foldery kompilatora avr-gcc w różnych wersjach. Folder [isp] zawiera pliki programatora AvrProgrammer Adama Dybkowskiego. Foldery [Myinc] oraz [MyLib] są przeznaczone na własne - bardziej uniwersalne i stosowane w większej liczbie projektów - pliki nagłówkowe oraz biblioteki. Folder [Projects] grupuje podkatalogi (a także drzewa podkatalogów) poszczególnych projektów. Każdy nowy projekt powinien być zakładany w oddzielnym podkatalogu (np. \Projects\Nowy_projekt) lub drzewie (np. \Projects\Kurs\Przyklad_01). Takie rozwiązanie umożliwia zachowanie porządku i przejrzystości we własnych archiwach projektowych, a
także umożliwia korzystanie z wbudowanej przeglądarki projektów. W każdym subfolderze projektu zapisujemy pliki źródłowe (*.c, *.s oraz *.h) oraz plik konfiguracji projektu (nazwa_projektu.gcp) określający wszystkie opcje, ustawienia, ścieżki itp. użyte w projekcie. W tym samym subfolderze powstają wspomniane wcześniej pliki wynikowe działania kompilatora. W głównym folderze [Projects] znajdziemy też plik lastwork.cfg zapisujący dane projektu używanego w chwili zamykania AvrSide (co pozwala na natychmiastowe przywrócenie środowiska pracy podczas ponownego uruchomienia) oraz - tylko w przypadku używania AvrProgrammera - pliki eeprom.hex i flash.hex wykorzystywane przez ten programator. Foldery [Freaks] i [Notes] nie są bezpośrednio związane z AvrSide ani avr-gcc - przechowuję tam różne notatki, noty aplikacyjne, wypisy z forum i listy avr-gcc itp. - w ten sposób nie „rozbiega” mi się to po całym dysku.
Tworzymy pierwszy testowy projekt
Po zainstalowaniu wszystkich wcześniej wymienionych składników na dysku pora na wypróbowanie działania środowiska. Wszystkie przytaczane przykłady dotyczą AvrSide ulokowanego w folderze c:\AvrSide i korzystającego z wersji avr-gcc z sub-
Rys. 4. Opcje ustawień ekranowych AvrSide
89
KURS
Rys. 5. Opcje ustawień konfiguracji plików wynikowych i typu mikrokontrolera
folderu \AvrSide\gcc-3.4.2-2.15-1.0.4, bez użycia pakietu WinAvr. Należy je więc każdorazowo dopasować do ustawionych u siebie lokalizacji. Podczas pierwszego uruchomienia AvrSide otwiera w domyślnym położeniu okno pustego projektu NONAME z pojedynczą zakładką pustego pliku źródłowego NoName. Wywołajmy skrótem Ctrl+J wykaz szablonów kodu i wybierzmy „mainmod - szablon modułu głównego” - zostanie wpisany najprostszy, podstawowy program z pustą pętlą: // główny moduł projektu #define _MAIN_MOD_ 1 // pliki dołączone (include): // dane: // funkcje: /==================== // funkcja main() int main(void) { // inicjalizacja // pętla główna while (1) { } }
Teraz od razu wybierzmy lokalizację i nazwijmy plik oraz projekt: – z menu „Plik” wybieramy komendę „Zapisz” albo „Zapisz jako” (w przypadku nowego pliku są one równoważne); – w typowym windowsowym oknie dialogowym zapisywania pliku otwartym na folderze [Projects] tworzymy nowy podkatalog [Kurs] a w nim kol e j n y s u b f o l d e r [ Pr z y k l a d - 0 1 ] (używamy zwykłych narzędzi Windows czyli menu kontekstowego, możemy też jednak przygotować sobie te foldery wcześniej w
Rys. 6. Wynik kompilacji przykładowego projektu
90
jakimkolwiek managerze plików); – przejdźmy do [Przyklad-01] nazwijmy plik main.c (rozszerzenie jest dodawane domyślnie) i potwierdźmy; okno zapisu przełączy się samoczynnie na zachowywanie projektu - nazwijmy go np. Test01 i zapiszmy; – nasze nazwy zostały wprowadzone do projektu: plik na zakładce (tab) edytora, a projekt na belce tytułowej okna (w managerze plików możemy też od razy sprawdzić poprawność zapisu - subfolder \Projects\Kurs\Przyklad-01 powinien zawierać dwa pliki: main.c oraz Test01.gcp); – nazwy oczywiście możemy wybrać dowolnie, jedynym ograniczeniem jest aby plik projektu *.gcp nie nazywał się tak samo jak jakikolwiek plik źródłowy, gdyż spowoduje to nadpisywanie plików wynikowych listingu. Zanim ruszymy dalej z kompilacją zajmijmy się przez chwilę skonfigurowaniem środowiska oraz ustawieniem opcji projektu. Okno główne umieszczamy na ekranie według potrzeb - jego pozycja będzie odtąd na stałe zapamiętana we wspomnianym wcześniej pliku ogólnej konfiguracji desktop.cfg. Następnie wybieramy z menu komendę „Projekt>>Ustawienia” i przełączamy widok kolejno na potrzebne pozycje: 1. „Ekran” (rys. 4) - tutaj ustawimy sobie: – kolory i style dla podswietlanych elementów składni kodu źródłowego (wybieramy w prawym panelu posługując się typowymi windowsowymi kontrolkami); – rodzaj i wielkość czcionki oraz kolor tła dla głównych okienek AvrSide (edytor, komunikaty błędów oraz podgląd asemblera); używamy do tego kontekstowego menu (prawy-klik) trzech małych paneli po lewej stronie; – wygląd menu głównego (tradycyjny lub zgodny z XP); – rozmiar czcionki w generowanym kodzie HTML; – ustawienia dotyczą naszego pojedynczego projektu, jeśli chcemy ich używać w każdym nowym projekcie zapiszmy je jako domyślne (w desktop.cfg) przy pomocy przycisku DEF>> (podobnie przyciskiem DEF>Zapisz projekt”) ale nie jest to bezwzględnie konieczne gdyż zostaną samoczynnie zachowane przy przełączaniu projektu albo zamknięciu AvrSide. Po tych wszystkich wstępnych czynnościach możemy wreszcie wypróbować działanie kompilatora. Skrót klawiaturowy F9 spowoduje wykonanie niezbędnego szeregu operacji (jest to odpowiednik pozycji menu „Projekt>>Make” - jednak nazwa ta nie ma nic wspólnego z klasycznym programem make), pokazanych w oknie postępu (celowo wyłączyliśmy jego samoczynne zamykanie aby je sobie spokojnie obejrzeć) - rys. 6.
Rys. 7. Okno konfiguracyjne sesji debuggera w AvrStudio
Elektronika Praktyczna 4/2005
KURS Jeśli teraz przełączymy się do managera plików znajdziemy w folderze naszego projektu szereg nowych plików utworzonych w trakcie działania kompilatora (sięgnijmy do rys. 1): – main.o: plik relokowalny modułu main.c, – Test01.hex: plik wynikowy kodu programu (flash), – Test01.map: plik informacyjny konsolidatora, – Test01.lst: plik assemblerowego listingu całego projektu, – Test01.elf: plik obiektowy projektu, – Test01.smb: wykaz symboli użytych w projekcie, – Test01.txt: rejestracja komend wykonanych w trakcie kompilacji. Zauważmy, że nie ma tu pliku pośredniego main.s - dla przyśpieszenia działania nie jest on zapisywany na dysku, a powstaje w chwilowym buforze pamięci operacyjnej. Pliki relokowalne *.o zachowują nazwy odpowiadających modułów źródłowych, natomiast zbiorczym plikom wynikowym AvrSide nadaje nazwę projektu z odpowiednim rozszerzeniem. W ostatnim z plików - Test01.txt - znajdziemy zapis kolejnych komend kompilatora odpowiadających pozycjom okienka postępu. Nie będziemy się w nie teraz na początku zagłębiać, ale wykorzystamy ten plik wielokrotnie podczas dalszego poznawania avr-gcc. Z punktu widzenia elektronika - konstruktora najważniejszy jest oczywiście plik kodu *.hex, który za pomocą dowolnego programatora ładujemy do mikrokontrolera. Odłóżmy to jednak na nieco później i najpierw obejrzyjmy trochę dokładniej wygenerowany kod. Skorzystamy w tym celu z wbudowanych udogodnień AvrSide. Skrót klawiaturowy F7 otworzy nam okno podglądu kodu asm odtworzonego („zdeasemblowanego”) z pliku relokowalnego bieżącego modułu (u nas mamy tylko jeden main.o). Widzimy następującą treść (sekcja .text określa obszar pamięci programu Flash): main.o: file format elf32-avr Disassembly of section .text: 00000000 : /==================== // funkcja main() int main(void) { 0: c0 e0 ldi r28, 0x00 2: d0 e0 ldi r29, 0x00 4: de bf out 0x3e, r29 6: cd bf out 0x3d, r28 // inicjalizacja // pętla główna while (1) 8: ff cf rjmp .-2
; ; ; ;
0 0 62 61
; 0x8
Funkcja main() ustawia na samym początku wskaźnik stosu (re-
Elektronika Praktyczna 4/2005
jestry 0x3e,0x3d - na etapie pliku relokowalnego nie są jednak jeszcze wstawiane konkretne wartości) i od razu przechodzi do pustej nieskończonej pętli while (1). Jednak coś się tu nie zgadza: ten kod ma najwyżej kilka bajtów długości, a wynik kompilacji na pasku statusu pokazał nam 102 bajty. Aby znaleźć różnicę zajrzyjmy do pliku Test01.lst, który zawiera ostateczny kod po zakończeniu konsolidacji (do tego posłuży nam skrót Ctrl+F7): Test01.elf: file format elf32-avr Disassembly of section .text: 00000000 : 0: 12 c0 rjmp .+36 2: 2b c0 rjmp .+86 4: 2a c0 rjmp .+84 6: 29 c0 rjmp .+82 8: 28 c0 rjmp .+80 a: 27 c0 rjmp .+78 c: 26 c0 rjmp .+76 e: 25 c0 rjmp .+74 10: 24 c0 rjmp .+72 12: 23 c0 rjmp .+70 14: 22 c0 rjmp .+68 16: 21 c0 rjmp .+66 18: 20 c0 rjmp .+64 1a: 1f c0 rjmp .+62 1c: 1e c0 rjmp .+60 1e: 1d c0 rjmp .+58 20: 1c c0 rjmp .+56 22: 1b c0 rjmp .+54 24: 1a c0 rjmp .+52 00000026 26: 11 28: 1f 2a: cf 2c: d4 2e: de 30: cd
: 24 eor r1, r1 be out 0x3f, r1 e5 ldi r28, 0x5F e0 ldi r29, 0x04 bf out 0x3e, r29 bf out 0x3d, r28
00000032 32: 10 34: a0 36: b0 38: e6 3a: f0 3c: 02
: e0 ldi r17, 0x00 e6 ldi r26, 0x60 e0 ldi r27, 0x00 e6 ldi r30, 0x66 e0 ldi r31, 0x00 c0 rjmp .+4
; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ; ;
0x26 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a 0x5a
; ; ; ; ;
63 95 4 62 61
; ; ; ; ;
0 96 0 102 0 ; 0x42
0000003e : 3e: 05 90 lpm r0, Z+ 40: 0d 92 st X+, r0 00000042 42: a0 44: b1 46: d9
: 36 cpi r26, 0x60 ; 96 07 cpc r27, r17 f7 brne .-10 ; 0x3e
00000048 48: 10 4a: a0 4c: b0 4e: 01
: e0 ldi r17, 0x00 ; 0 e6 ldi r26, 0x60 ; 96 e0 ldi r27, 0x00 ; 0 c0 rjmp .+2 ; 0x52
00000050 : 50: 1d 92 st X+, r1 00000052 52: a0 54: b1 56: e1 58: 01
: 36 cpi r26, 0x60 ; 96 07 cpc r27, r17 f7 brne .-8 ; 0x50 c0 rjmp .+2 ; 0x5c
0000005a : 5a: d2 cf rjmp .-92
; 0x0
0000005c : /==================== // funkcja main() int main(void) { ldi r28, 0x5F 5c: cf e5 5e: d4 e0 ldi r29, 0x04 60: de bf out 0x3e, r29 62: cd bf out 0x3d, r28 // inicjalizacja // pętla główna while (1) 64: ff cf
rjmp
.-2
; ; ; ;
95 4 62 61
; 0x64
Teraz zgadza się wszystko: ostatni bajt kodu ma adres 0x65=101 - przy numeracji od zera długość wynosi 102 (przy okazji zauważmy, że gcc stosuje adresowanie bajtowe w przeciwieństwie do notacji Atme-
la używającej adresów dwubajtowych słów - dlatego adresy podawane w notach i dokumentacjach należy dla potrzeb avr-gcc mnożyć przez dwa). Dodatkowy kod spełnia kilka zadań związanych z inicjalizacją pracy programu (będziemy później wracać do tego tematu bardziej szczegółowo): 1. Sekcja ustawia obszar wektorów przerwań odpowiedni dla użytej kostki. Przerwanie pod adresem 0x0 (czyli reset) zawiera skok na koniec obszaru wektorów. Pozostałe wektory domyślnie (o ile nie została zdefiniowana obsługa danego przerwania) wskazują na błędne przerwanie. 2. Sekcja kończy obszar wektorów - w tym miejscu „ląduje” skok z przerwania spowodowanego resetem. Sekcja wykonuje kilka dodatkowych czynności: zeruje rejestr r1 (co jest wymagane przez standard wykorzystywania rejestrów w avr-gcc), zeruje rejestr stanu i ustawia rejestry stosu (zauważmy, że stos jest zatem ustawiany dwukrotnie - jest to jakaś „zaszłość rozwojowa” kompilatora, która będzie prawdopodobnie usunięta w dalszych wersjach). 3. Sekcje służą do załadowania wartościami zmiennych, które w programie zdefiniowaliśmy jako zainicjalizowane. 4. Sekcje służą z kolei do wyzerowania wszystkich zmiennych nie zainicjalizowanych (które w standardzie C muszą mieć domyślnie wartość zero). 5. Sekcja stanowi (jak wspomniano wyżej) domyślną obsługę błędnego przerwania, jest to po prostu skok pod adres zero czyli na początek programu. Cały ten blok kodu jest zawarty w relokowalnych plikach crtxx.o umieszczonych w podkatalogu (wróćmy do rys. 2) \avr\lib\ kompilatora. Komenda wywołania konsolidatora: avr-gcc.exe -mmcu=atmega8 -Wl,-Map=Test01.map,--cref -o Test01.elf main.o (znajdziemy ją we wspominanym już pliku Test01.txt) zawiera w sobie obowiązkowo opcję typu procesora. Na tej podstawie konsolidator dołącza odpowiedni plik crtxx.o oraz wybiera odpowiedni skrypt (ze skryptu pochodzi m.in. docelowa wartość rejestru stosu: 0x45F, która - jak łatwo sprawdzić w dokumentacji Atmega 8 - odpowiada końcowi obszaru pamięci SRAM).
91
KURS Obecność bezparametrowej funkcji main(void) wynika z zasad języka C - każdy program musi ją mieć jako podstawową „ramkę” obejmującą wszystkie wewnętrzne działania. Jednak aplikacja działająca w małym mikrokontrolerze będzie się dość istotnie różnić od „zwykłego” programu komputerowego. Taki program jest uruchamiany przez system operacyjny, zakończenie funkcji main() jest równoznaczne z zakończeniem programu i powrotem do systemu. Wstawienie takiej jak u nas zamkniętej pętli jest kardynalnym błędem, gdyż uniemożliwia wyjście z main() co jest równoznaczne z „zawieszeniem się” aplikacji. Zapis musi wyglądać nieco inaczej (zazwyczaj wstawimy jeszcze jakieś oczekiwanie na klawisz, żeby zamknięcie nie nastąpiło natychmiast): int main(void) { return 0; }
Natomiast w atmedze oczywiście nie ma żadnego nadrzędnego systemu operacyjnego, któremu można przekazać sterowanie. Wyjście z funkcji main() spowoduje po prostu opuszczenie obszaru kodu programu i utratę kontroli nad mikrokontrolerem - czyli znów kardynalny (chociaż zupełnie innej natury) błąd. Spróbujmy skompilować nasz test w takiej wersji. W kodzie wynikowym czeka nas miła niespodzianka: int main(void) { 5c: cf e5 5e: d4 e0 60: de bf 62: cd bf // inicjalizacja // pętla główna //while (1) //{ //} return 0; } 64: 80 e0 66: 90 e0 68: 00 c0
ldi r24, 0x00 ; 0 ldi r25, 0x00 ; 0 rjmp .+0 ; 0x6a
0000006a : 6a: ff cf
rjmp
ldi r28, 0x5F ldi r29, 0x04 out 0x3e, r29 out 0x3d, r28
.-2
; ; ; ;
95 4 62 61
; 0x6a
Jak widać avr-gcc przewidział taką sytuację i samoczynnie (po zwrocie w rejestrach r25,r24 deklarowanej wartości zero) dodał sekcję , która znów jest zamkniętą pętlą. Oczywiście mikrokontroler „zaniemówi” w oczekiwaniu na reset, niemniej jego zachowanie pozostaje całkowicie przewidywalne. Zwróćmy przy okazji uwagę, że w wersji „atmegowej” kompilator uwzględnia fakt nie opuszczania main() i nie generuje ostrzeżenia o braku zwracanej wartości (control reaches end of non-void function) nawet jeśli pomijamy końcową instrukcję return. Powyższy przykład wskazuje też
92
na konieczność zachowania dodatkowej ostrożności przy stosowaniu „pecetowych” umiejętności programowania w C dla potrzeb mikrokontrolerów - cały czas trzeba mieć świadomość, że przenosimy się do środowiska o radykalnie innych możliwościach i wymaganiach. Pojawi się niechybnie wątpliwość, czy tak długi kod dla „nic-nie-robiącego” programu nie jest marnowaniem cennej pamięci Flash mikrokontrolera? No cóż - zawsze trzeba jakoś zapłacić za wygodę i standaryzację. Poza tym - pisząc w C będziemy i tak sięgać raczej po kostki o większych możliwościach aby nie przejmowac się ograniczonymi zasobami. Dla przykładowej ATmega8 inicjalizacja zajmuje ok. 1% dostępnej pamięci programu, co jest wartością nieznaczną. Dla Atmega128 z 16-krotnie większym Flashem jest to już zupełnie pomijalne. Natomiast w ATtiny13 z 1 kB programu oraz 64 B SRAM rzeczywiście C może się zachowywać jak przysłowiowy słoń w składzie porcelany. W tym przypadku wybór narzędzia (C czy asm) będzie podyktowany rzeczywistymi wymaganiami naszej konkretnej aplikacji.
Uruchamiamy sesję debuggera
Na koniec tego wstępnego rozdziału sprawdźmy jeszcze jak działa AvrStudio 4.10 w roli symulatora i debuggera. Z poziomu AvrSide po prostu wciskamy F11 (albo używamy odpowiedniej komendy menu). Jednak za pierwszym razem zamiast startu AvrStudio zostaje wyświetlone okienko informacyjne ze wskazówkami dalszego postępowania. Zgodnie z nimi uruchamiamy ręcznie AvrStudio z „pustą” sesją a następnie: – otwieramy plik C:\AvrSide\Projects\ Kurs\Przyklad-01\Test01.elf, – w wyświetlonym oknie konfiguracji (rys. 7) wybieramy platformę debugera - AVR Simulator, – oraz wybieramy typ układu - Atmega 8 (może się zdarzyć, że wybór układu jest zablokowany świadczy to o braku w systemie odpowiedniej wersji parsera XML, należy przeprowadzić update z witryny Microsoftu - znajdziemy o tym wzmiankę w opisie usterek w pomocy AvrStudio). Po chwili projekt zostanie załadowany i w oknie edycji pojawi się nasz kod ze wskaźnikiem programu ustawionym na funkcji main(). Nasz skrajnie uproszczony projekt nie daje żadnych praktycznych możliwości debugowania, możemy jednak sprawdzić działanie: startu i wstrzymania wykonywania, resetu oraz pracy krokowej. Wybierzmy
też sobie pasujący układ okien i schemat kolorowania składni. Jeśli teraz zamkniemy AvrStudio parametry sesji zostaną zachowane w pliku Test01_elf.aps (ulokowanym w folderze naszego projektu), który posłuży do jej samoczynnego wznawiania. Ponowne użycie F11 z poziomu AvrSide spowoduje uruchomienie AvrStudio od razu z naszym projektem (w zależności od wydajności sprzętu może to chwilę potrwać). Natomiast przy działającym już Studiu efektem będzie po prostu przywołanie jego okna. Warto poświęcić chwilę czasu na przejrzenie możliwości, opcji i „klawiszologii” AvrStudio - to znakomicie ułatwi później analizę kolejnych przykładów. Należy jednak cały czas mieć na uwadze, że najnowsze wersje AvrStudio wyposażone w możliwość bezpośredniego odczytu plików *.elf (z pominięciem dodatkowych konwersji elf >> coff, które zawsze były piętą achillesową współpracy z avr-gcc) są cały czas rozwijane i dopracowywane - mogą się więc przytrafiać rozmaite błędy. Dlatego warto na bieżąco sprawdzać czy nie pojawiły się jakieś aktualizacje (chodzi głównie o poprawione wersje bibliotek, często przekazywane poprzez forum avrfreaks). Jeśli wszystko poszło bez problemów, to mamy po tej części artykułu: – przygotowane środowisko do tworzenia oraz symulacji i debugowania projektów, – wstępne rozeznanie jak są rozmieszczone i jak współpracują jego poszczególne komponenty, – orientację w typach plików używanych w trakcie budowy projektu, – podstawowe informacje o zasadach działania kompilatora i uzyskiwanym wynikowym kodzie. Jerzy Szczesiul, EP [email protected] Niektóre przydatne linki http://winavr.sourceforge.net - strona domowa WinAvr http://www.avrfreaks.net - największe forum mikrokontrolerów AVR http://www.atmel.com - firmowa strona producenta AVR http://www.nongnu.org/avr-libc/ - strona domowa projektu avr-libc http://gcc.gnu.org - strona główna kompilatora gcc http://sources.redhat.com/binutils/ - strona narzędzi binutils http://www.avrside.fr.pl - strona środowiska AvrSide http://www.avrside.ep.com.pl - strona środowiska AvrSide http://www.amwaw.edu.pl/~adybkows/elka/ ispprog.html - programator ISP LPT Adama Dybkowskiego
Elektronika Praktyczna 4/2005
KURS
AVR-GCC: kompilator C mikrokontrolerów AVR, część
3
Kontynuujemy cykl artykułów, których zadaniem jest przedstawienie podstaw oraz praktycznych zasad programowania mikrokontrolerów AVR w języku C z użyciem kompilatora avr-gcc. Oczywiście wybór kompilatora AVR-GCC może się jednym podobać, a innym nie. Postaramy się jednak uzasadnić, że nie jest to zły wybór. Zmienne liczbowe i organizacja pamięci wewnętrznej ATmega
Pisząc oprogramowanie dla mikrokontrolera cały czas operujemy na rozmaitych wielkościach: odczytujemy, uśredniamy i filtrujemy wyniki przetwarzania ADC, zliczamy impulsy na wejściach licznikowych, odmierzamy czas, wyświetlamy napisy i liczby na różnego rodzaju wyświetlaczach, wyliczamy wypełnienie cyklu PWM itd. Wszystkie te wielkości zmieniające swoją wartość w trakcie działania programu noszą ogólną nazwę zmiennych. Klasyfikacja zmiennych jest bardzo różnorodna, na przykład: – według pełnionej funkcji: zmienne liczbowe, znakowe, logiczne, tekstowe (łańcuchowe), wskaźniki; – według złożoności: zmienne proste (np. pojedyncza liczba) i złożone (tablice, struktury, unie); – według zakresu, znaku oraz typu liczby (dotyczy zmiennych liczbowych); – według sposobu obsługi przez kompilator (inicjalizowane lub nie). Tu t a j z a j m i e m y s i ę s p o s o b a mi używania różnych zmiennych w avr–gcc. Bardziej sformalizowane i szczegółowe opisy i klasyfikacje znajdziemy w każdym uniwersalnym podręczniku języka C. Ponieważ każda zmienna jest dla mikrokontrolera po prostu pewną liczbą bajtów ulo-
kowanych pod znanym adresem w pamięci danych, zobaczmy najpierw jak avr–gcc zarządza tą pamięcią (a konkretnie obszarem przeznaczonym dla użytkownika – powyżej rejestrów SFR). Na rys. 8 (zaczerpniętym z podręcznika avr–libc) widzimy domyślnie stosowany schemat wykorzystania wewnętrznego SRAMU. Stos (jak już stwierdziliśmy wcześniej) rozpoczyna się od końcowego adresu (określanego w avr–libc symbolem RAMEND) – jest wypełniany „w dół” czyli dekrementowany. Bezpośrednio za obszarem SFR rozpoczyna się sekcja .data, w której konsolidator umieszcza wszystkie zmienne inicjalizowane (z przypisaną wstępnie niezerową wartością). Dalej jest ulokowana sekcja .bss, zawierająca zmienne bez przypisanej wartości, które zgodnie ze standardem C zostają na początku programu wyzerowane; Następnie przewidziano dodatkową – specyficzną dla mikrokontrolerów – sekcję .noinit. Obejmuje ona zmienne, które chcemy pozostawić wyłącznie pod własną kontrolą – kompilator nie wykonuje na nich żadnych automatycznych operacji. Ma to na celu głównie zróżnicowanie sposobu inicjalizowania niektórych zmiennych w zależności od przyczyny resetu (np. możemy zechcieć aby licznik czasu pracy urządzenia był
Rys. 8. Sekcje pamięci w wewnętrznym SRAM ATmega
Elektronika Praktyczna 5/2005
zerowany po włączeniu zasilania, ale zachowywał swoją wartość podczas resetu spowodowanego zadziałaniem watchdoga). Oczywiście wtedy musimy sami zadbać w kodzie o wpisanie odpowiednich wartości (także zer) – w przeciwnym razie pozostaną one całkowicie przypadkowe. Pojawiło się tutaj pojęcie sekcji – jest to mechanizm wykorzystywany przez konsolidator do podziału dostępnych zasobów pamięci na poszczególne obszary i odpowiedniego przydzielenia do nich składników programu (oprócz wspomnianych powyżej sekcji danych mamy do czynienia z sekcją .text opisującą kod programu umieszczony w pamięci Flash oraz sekcją .eeprom przeznaczoną dla zawartości wewnętrznego EEPROMu kostki). Adresy startowe sekcji oraz ich maksymalne rozmiary dla danego mikrokontrolera znajdziemy we wspomnianych już wcześniej skryptach linkera. Przypisanie zmiennej do konkretnej sekcji jest realizowane albo domyślnie przez konsolidator (sekcje .data oraz .bss są obsługiwane samoczynnie na podstawie deklaracji zmiennej) albo poprzez dodatkowy atrybut (dla .noinit lub .eeprom). Zobaczmy teraz jak to działa w praktyce. Załóżmy sobie w AvrSide – zgodnie z poprzednimi opisami – nowy projekt Test02 w subfolderze [Projects\Kurs\Przyklad–02], z jednym plikiem źródłowym main.c. Zadeklarujmy kilka zmiennych typu int (mają one rozmiar 2 bajtów, o czym dokładniej za chwilę) oraz zdefiniujmy uproszczony zapis atrybutu sekcji .noinit (ta ostatnia operacja nic nie zmienia w działaniu kodu, służy wyłącznie wygodzie pisania): // główny moduł projektu #define _MAIN_MOD_ 1 #define NOINIT __attribute__ ((section („.noinit”))) // pliki dołączone (include): // dane: int data1 = 2; // zmienna inicjalizowa-
101
KURS na wartością int bss1; // zmienne zerowane int bss2; int noinit1 NOINIT; // zmienna nie inicjalizowana // funkcje: //================== // funkcja main() int main(void) { // inicjalizacja noinit1 = 0x55; // tutaj samodzielnie inicjalizujemy zmienną NOINIT // pętla główna while (1) { } }
Po kompilacji pasek statusu pokaże nam zużycie RAM równe 8 bajtów – jest to suma zmiennych we wszystkich sekcjach (4*2). Po bardziej szczegółowe informacje sięgnijmy do pliku rejestracyjnego Text02. txt. Znajdziemy tam m.in. tabelę dokładnej specyfikacji używanych sekcji (utworzoną w wyniku wywołania narzędzia avr–objdump z opcją –h): Sections: Idx Name 0 .text 1 .data 2 .bss 3 .noinit 4 .eeprom
Size 00000072 00000002 00000004 00000002 00000000
VMA 00000000 00800060 00800062 00800066 00810000
Widzimy, że zmienne powędrowały do odpowiednich sekcji (jedna inicjalizowana – 2 bajty w .data, dwie zerowane – 4 bajty w .bss, jedna „samodzielna” – 2 bajty w .noinit; w .eeprom nie deklarowaliśmy nic). W kolumnie VMA znajdziemy też adres startowy każdej sekcji (początek .data to 0x60 – zaraz po obszarze SFR w Atmega 8 ; rolę przesunięcia 0x800000 wyjaśnimy później). Spójrzmy jeszcze na sekcję kodu .text. Ma ona rozmiar 0x72=114 bajtów. Wynik kompilacji w AvrSide
pokazał nam 116 bajtów. Te dodatkowe dwa bajty to właśnie początkowa wartość zmiennej data1. Nie weźmie się ona przecież „z powietrza” i musi być gdzieś przechowywana – avr–gcc dopisuje ją na końcu pliku wynikowego kodu, skąd przy starcie programu jest przepisywana (spójrzmy jeszcze raz na omówiony wcześniej kod automatycznej inicjalizacji) pod odpowiedni adres SRAM. Zajęty obszar zasobów Flash jest więc w rzeczywistości równy sumie sekcji .text i .data – taki też rezultat wyświetla AvrSide. Zobaczmy teraz jak powyższe zmienne zachowają się w AvrStudio. Po uruchomieniu nowej sesji ustawmy sobie podgląd wszystkich zmiennych oraz uaktywnijmy okienko pamięci z obszarem danych (rys. 9). Widzimy, że data1 przybrała odpowiednią początkową wartość (2), zmienne bss1 i bss2 zostały wyzerowane, a noinit1 pozostała bez ingerencji (symulator AvrStudio jest nieco wyidealizowany i nadaje jej wartość 0xffff, w rzeczywistości komórki SRAM mogą po włączeniu zasilania zawierać całkiem przypadkowe wartości). Przejdźmy teraz pracą krokową (F11) do pętli while. Zmienna noinit1 przybierze wartość zgodną z wpisanym przez nas kodem (0x55 czyli 85 dziesiętnie). Jeśli teraz zresetujemy program (Shift + F5), to zobaczymy, że wartość noinit1 pozostanie nienaruszona. Zwróćmy uwagę, że debugger C z AvrStudio całkowicie pomija automatyczną inicjalizację – widzimy od razu efekty jej działania (możemy ją prześledzić w oknie disasemblera,
Rys. 9. Zmienne inicjalizowane w AvrStudio
102
ale niestety bez prawidłowego podglądu zawartości pamięci). Omówimy teraz dokładniej używane przed chwilą zmienne liczbowe. 1 – Najprostszą wersją zmiennej liczbowej jest liczba całkowita bez znaku, (czyli podzbiór liczb naturalnych oraz zero). Avr–gcc obsługuje następujące typy liczby bez znaku, różniące się tylko wielkością: unsigned char – zajmuje jeden bajt, może więc przyjąć wartość od zera do 0xff czyli 255; unsigned int – 2 bajty, a więc 0 – 0xffff (65535); unsigned long – 4 bajty – 0 – 0xffffffff (4294967295); unsigned long long – 8 bajtów – do rzeczywiście wielkich wartości (raczej rzadko będzie nam potrzebny w świecie małych mikrokontrolerów, nie jest też obsługiwany przez AvrStudio). Te typy są interpretowane najbardziej bezpośrednio – wartość jest po prostu równa zawartości odpowiedniej liczby jednobajtowych komórek pamięci. Jednak nawet w tym prostym przypadku konieczne jest przyjęcie pewnej konwencji – określanej mianem „endianess” – czyli sposobu uporządkowania kolejnych bajtów liczby w pamięci. W różnych kompilatorach możemy napotkać dwie przeciwstawne metody: big endian – bajty liczby są lokowane pod kolejnymi adresami pamięci od najbardziej do najmniej znaczącego (czyli np. liczba unsigned long 0x11223344 będzie zapamiętana w SRAM jako kolejno: 0x11, 0x22, 0x33, 0x44); little endian – bajty liczby są lokowane od najmniej znaczącego (czyli 0x44, 0x33, 0x22, 0x11). Jeśli spojrzymy na rys. 9 (oraz obejrzymy generowany kod asemblera) zauważymy od razu, że avr–gcc posługuje się modelem little–endian. Zazwyczaj ta informacja nie będzie nam specjalnie potrzebna, kompilator sam dba o odpowiedni porządek, jednak może być przydatna w momencie wykorzystywania zmiennej wielobajtowej z poziomu wstawki asemblerowej. Aby sprawy nie wyglądały tak prosto należy dodać, że niektóre opcje kompilacji potrafią zmieniać domyślny rozmiar powyższych typów. Jeśli więc mamy w planach ich stosowanie (konkretnie chodzi o opcję –mint8, która zmniejsza rozmiary typów liczb, a tym samym pozwala na zredukowanie w razie konieczności objętości kodu), to dla zmiennych o
Elektronika Praktyczna 5/2005
KURS
Rys. 10. Zapis pojedynczej precyzji liczby rzeczywistej
wymaganym znanym i stałym rozmiarze użyjmy raczej typów zdefiniowanych w pliku nagłówkowym stdint.h w subfolderze [avr\include] kompilatora. Jest to metoda bardzo zalecana przez autorów avr–libc jako zapewniająca całkowitą jednoznaczność określonego typu przy różnych warunkach kompilacji (np. int8_t ma zawsze 1 bajt, int16_t – 2 bajty itd.). Pozostaje oczywiście kwestia indywidualnych gustów i przyzwyczajeń, jednak C pozwala za pomocą operatora typedef określić zupełnie dowolne własne nazwy typów (np. często spotykane s08, s16, u08, u16). W prezentowanych przykładach również używam nazewnictwa tradycyjnego, które mi jakoś lepiej pasuje niż standard proponowany w avr–libc. 2 – Liczby całkowite ze znakiem są już nieco bardziej skomplikowane. Znak jest określony stanem najstarszego bitu – 0 oznacza plus, a 1 minus. Jednak wbrew oczekiwaniom pozostałe bity określają bezpośrednio wartość liczby tylko dla wartości dodatniej, wartości ujemne są zakodowane w tzw. dopełnieniu do dwóch (U2). Obejrzyjmy to zaraz w symulatorze wpisując do naszych zmiennych (np. data1) różne wartości i oglądając w okienku pamięci ich bajtową reprezentację. Na przykład wartość –1 zostanie zapisana jako 0xffff. Taki mało intuicyjny sposób kodowania wynika z prostoty zachowania wartości przy rzutowaniu typów oraz symetrycznej i niezależnej od liczby bajtów procedury odwracania znaku. Znak liczby w kodzie U2 zmieniamy negując wszystkie bity liczby i dodając do wyniku jeden. Możemy od razu sprawdzić jak to działa w praktyce poddając odpowiednim operacjom naszą zmienną – np. data1. Dopisujemy na początku main() sekwencję: // inicjalizacja data1 = ~data1; data1 +=1; data1 = ~data1; data1 +=1;
Elektronika Praktyczna 5/2005
Musimy też dodać do deklaracji data1 słowo kluczowe volatile (volatile int data1 = –1;) gdyż w przeciwnym razie optymalizator wytnie zbędne z jego punktu widzenia pośrednie operacje na data1 wstawiając od razu ostateczny wynik. Oczywiście, ponieważ zajęty został najbardziej znaczący bit – zakres wartości bezwzględnej typu zostanie zmniejszony o mniej więcej połowę. Ze sposobu kodowania wynika pewna asymetria: np. dla typu int wartością maksymalną będzie 0b0111 1111 1111 1111 (czyli 0x7fff)=32767 zaś minimalną 0b1000 0000 0000 0000 (0x8000)=–32768. Liczby ze znakiem możemy dla pełnej jasności deklarować ze słowem kluczowym signed, ale ponieważ jest to opcja domyślna zazwyczaj ją pomijamy. Należy jeszcze dodać, że typ int jest domyślny dla kompilatora. Wszędzie gdzie z kodu nie wynika jednoznacznie, jakiego „rozmiaru” liczby użyć w operacji stosowany jest int (tzw. promocja do int). Czasem jest to pożyteczne, ale w pewnych przypadkach powoduje zgoła nieprzewidziane rezultaty (np. „obcinanie” wielkości liczb na pośrednich etapach bardziej złożonych przeliczeń). Będziemy do tej sprawy, jak również powiązanego z nią rzutowania typów wielokrotnie przy różnych okazjach powracać. Jak widać z powyższego staranność i uwaga przy doborze odpowiedniego typu dla zmiennej może w wielu przypadkach wręcz decydować o poprawności działania programu. Przekroczenie zakresu, potraktowanie wartości dodatniej jako liczby ze znakiem (np. ten sam bajt 0xff zadeklarowany jako unsigned char jest traktowany przez kompilator jako wartość dodatnia +255, natomiast użyty jako signed char zostanie odczytany jako –1) itp. mogą powodować trudne do zlokalizowania (i w żaden sposób nie sygnalizowane na etapie kompilacji) błędy.
3 – Liczby rzeczywiste – możemy je obsługiwać w dwojaki sposób. Uproszczona forma to tzw. zapis stałoprzecinkowy (fixed point). Używamy tutaj z góry określonej i niezmiennej liczby cyfr po przecinku, niezależnie od wartości. Przykładem z codziennego świata liczb dziesiętnych mogą być ceny. Zauważmy, że stosowanie liczb stałoprzecinkowych w programie jest – przy odpowiednim doborze jednostek – równoznaczne z obliczeniami na liczbach całkowitych. Np. chcemy mierzyć napięcie w woltach z rozdzielczością 0,001 V. Wystarczy wtedy zaprojektować tor analogowo – cyfrowy tak, aby jednemu najmniej znaczącemu bitowi wyniku konwersji AC odpowiadał 1 mV sygnału wejściowego. Wszystkie wewnętrzne pomocnicze obliczenia (filtrowanie, alarmy itp.) wykonujemy wtedy na wartościach całkowitych wyrażonych w miliwoltach. Ostateczna prezentacja wyniku w woltach będzie polegać wyłącznie na wstawieniu kropki dziesiętnej w odpowiednim miejscu. Zapis stałoprzecinkowy sprawdzi się dobrze jeśli wartość zmiennej pozostaje w ustalonym, znanym zakresie. Jednak wartości zbyt małe lub zbyt duże nie będą przedstawiane skutecznie. Np. dla dużych wartości mierzonych z dokładnością 1000 istotne jest rozróżnienie pomiędzy 1200000 a 1201000 – zapis 1200000,00 nie wnosi żadnej informacji i może prowadzić tylko do marnowania czasu programu na zbędne przeliczenia. Z kolei małe liczby będą całkiem nierozróżnialne: zarówno 0,001 jak i 0,004 zostaną zapisane jako 0,00. Od razu widać, że istotna jest nie przyjęta liczba cyfr po przecinku, ale grupa cyfr znaczących niosąca rzeczywistą informację o wartości. W takich przypadkach stosujemy format zmiennoprzecinkowy (floating point). Jest on bardzo podobny do znanej notacji tzw. inżynierskiej: liczba jest zapisana jako mantysa (przedstawiająca grupę cyfr znaczących) oraz wykładnik potęgi (określający mnożnik decydujący o wielkości liczby). Zarówno wartości wielkie np. 2,78E6 (czyli 2,78*106=2780000), jak i małe np. 3,55E–3 (czyli 3,55*10–3=0,00355) są przedstawione w jednakowy sposób bez utraty dokładności. Zapis samej mantysy jest również znormalizowany: wartość X umieszczona przed kropką dziesiętną zawiera się w zakresie 1=Importuj z folderu (przechodzimy w oknie wyboru do nowego subfolderu ...\Przyklad–05\ i zaznaczamy wszystkie skopiowane tam przed chwilą pliki z kodem), ustawiamy potrzebne opcje (ścież-
Elektronika Praktyczna 1/2006
ka do własnych plików nagłówkowych) i zapisujemy projekt jako ...\ Przyklad–05\test05.gcp. W ten sposób utworzyliśmy kopię poprzedniego projektu, którą teraz możemy rozwijać pozostawiając wcześniejszy przykład w stanie nie naruszonym. Do pliku projdat.h dopisujemy kilka nowych definicji, zmiennych (klasyfikator volatile dla zmiennych używanych w przerwaniach !) oraz deklaracji funkcji: #define RXSIZE 4 // rozmiar bufora odbiornika
volatile Flags UsartFlags; // flagi stanu portu szeregowego volatile char RxBuffer[RXSIZE]; // definicja bufora odbiornika #define NEW_COMMAND UsartFlags.Bits.Flag1 #define TX_BUSY UsartFlags.Bits.Flag2 // wygodne nazwanie poszczególnych flag stanu portu extern void InitUsart(void); extern void SendAnswer(int AnswerId); extern void SendPrompt(void); // deklaracje funkcji obsługi USART Dodajemy nowy moduł usart.c o następującej treści: // obsługa USART #include „projdat.h” #include #include #define TX_ON (UCSRB |= _BV(UDRIE)) #define TX_OFF (UCSRB &= ~_BV(UDRIE)) // włączanie i wyłączanie przerwań nadajnika volatile static char *TxPtr; // wskaźnik na znak wysyłany static char Prompt[] = „Gotowość do odbioru komendy 1–3.\n”; // tekst zgłoszenia static char Odp0[] = komendy:–(!\n”; static char Odp1[] = numer jeden.\n”; static char Odp2[] = numer dwa.\n”; static char Odp3[] = numer trzy.\n”; // teksty odpowiedzi
„Nie mogę określić „Wykonuję komendę „Wykonuję komendę „Wykonuję komendę na komendy
static char *AnswerTable[] = {Odp0,Odp1,Odp2,Odp3}; // tablica wskaźników na komunikaty odpowiedzi void InitUsart(void) { // ==== single usart configuration ==== // 19200 baud with 8000 kHz osc./error=0,2% // data 8/stop 1/parity NONE // receiver ON/transmitter ON/recv interrupt enabled UBRRH = 0x00; UBRRL = 0x19; UCSRA = 0x0; UCSRB = _BV(RXEN) | _BV(TXEN) | _BV(RXCIE); UCSRC = _BV(URSEL) | _BV(UCSZ0) | _BV(UCSZ1); // ==== end usart ==== } void SendAnswer(int AnswerId) { if((AnswerId < 1) || (AnswerId > 3)) AnswerId = 0; TxPtr = AnswerTable[AnswerId]; TX_BUSY = true; TX_ON; } void SendPrompt(void) { TxPtr = Prompt; TX_BUSY = true; TX_ON; } SIGNAL (SIG_UART_DATA) { char Znak; Znak=*(TxPtr++); if(Znak) UDR = Znak; else {
}
TX_OFF; TX_BUSY = false; }
SIGNAL (SIG_UART_RECV) { RxBuffer[0] = UDR; if (! TX_BUSY) NEW_COMMAND = true; }
Szablony handlerów przerwań tworzymy korzystając z opisanego wcześniej okienka autokompletacji kodu. Natomiast dla ustawienia parametrów USART użyjemy wspomagającego konfiguratora. Polecenie Narzędzia –> Kreator kodu –> Atmega usart otwiera okienko pokazane na rys. 27. Wybieramy według potrzeb: – Długość słowa danych, liczbę bitów stopu i rodzaj parzystości. – Szybkość transmisji. Do dyspozycji mamy konwencjonalny szereg szybkości RS 232 oraz wartość dowolną (USER) przydatną przy mniej typowych rozwiązaniach. Pole Error pokazuje nam na bieżąco procentową odchyłkę szybkości rzeczywiście możliwej do uzyskania (przy stosowanej w projekcie częstotliwości oscylatora) od pożądanego ideału. Łatwo zauważymy, że wbudowany generator 8 MHz dopuszcza tylko kilka wartości z typowego szeregu. Doskonale natomiast nadaje się do nawiązania komunikacji USB z użyciem kostki FT8U232BM i szybkościami 125 kbaud lub 250 kbaud (do sprawy używania wewnętrznego generatora jeszcze za chwilę powrócimy). – Włączenie nadajnika, odbiornika oraz przerwań odbiornika (nie ma tu oczywiście, zgodnie z poprzednimi uwagami, możliwości włączenia przerwań nadajnika). – Numer portu (USART dla kostek z jednym portem, USART0 lub USART1 dla kostek dwuportowych). Zatwierdzenie dialogu (OK) powoduje wstawienie bloku odpowiedniego kodu w miejscu ustawienia kursora tekstowego (karetki). W naszym przykładzie kod ten lokujemy wewnątrz funkcji InitUsart (void) inicjalizującej port. Jerzy Szczesiul, EP [email protected] UWAGA! Środowisko IDE dla AVR–GCC opracowane przez autora artykułu można pobrać ze strony http://avrside.ep.com.pl.
101
KURS
AVR–GCC: kompilator C dla mikrokontrolerów AVR, część 12 Obsługa interfejsu USART
Jako uzupełnienie odcinków o przerwaniach przedstawiamy przykłady ich praktycznego zastosowania. Jednym z najpopularniejszych przykładów jest obsługa interfejsu komunikacji szeregowej USART. Po modyfikacjach przedstawionych w poprzedniej części artykułu, główny plik projektu main.c wygląda teraz następująco: // główny moduł projektu #define _MAIN_MOD_ 1 // pliki dołączone (include): #include „projdat.h” #include #include #include #include
–
#define MS100_DELAY 5 // dane: static char Ms100_counter; static volatile uchar LedState = 1; // funkcje:
–
//==================== // funkcja main() int main(void) { // inicjalizacja OSCCAL=eeprom_read_byte((uchar*)E2END); // zapis kalibracji w ostatniej komórce eeprom DDRB=0xff;
–
InitT2(); InitUsart(); sei(); SendPrompt(); // pętla główna while (1) { // obsługa systemowego „zegarka” 100ms if (MS100_FLAG) { MS100_FLAG = false; if (++Ms100_counter == MS100_DELAY) { Ms100_counter = 0; // nasza okresowa akcja (przełączenie wyjścia) uruchamiana // zegarem systemowym co 100ms * MS100_DELAY (0,5s) PORTB=LedState; if(LedState==128) LedState=1; else LedState = LedState