|
Użytkownik |
Dołączył(a): 07 cze 2016 Posty: 563
Pomógł: 143
|
• Dane w segmentach powyżej 64KiB
Tym razem nie będę pokazywał aż tylu przykładów z różnymi typami danych. Skoncentruję się raczej na omówieniu różnic pomiędzy obsługą danych poniżej i powyżej granicy 64KiB. Różnice te są niezależne od typu danych, więc nadal można korzystać z przykładów z poprzedniego rozdziału, zmieniając tylko to, co konieczne.
◦ Umieszczanie danych w pamięci FLASH
Wiemy już, że zarówno makro PROGMEM, jak i kwalifikator __flash oznaczają dane do zapisu w sekcji .progmem.data, którą później linker lokuje na początku pamięci FLASH. Nie każdy chyba jednak wie, że istnieją również kwalifikatory __flash1 __flash2 __flash3 __flash4 oraz __flash5 , które oznaczają dane do zapisu w sekcjach (odpowiednio) .progmem1.data .progmem2.data .progmem3.data .progmem4.data oraz .progmem5.data Przeznaczeniem poszczególnych sekcji powinny być kolejne 64KiB segmenty pamięci FLASH, czyli tak jak sekcja .progmem.data (__flash) jest zapisywana w pierwszym segmencie 64KiB (czyli o indeksie 0), tak sekcja .progmem1.data (__flash1) powinna być zapisana w drugim segmencie 64KiB (czyli o indeksie 1), sekcja .progmem2.data (__flash2) powinna być zapisana w trzecim segmencie 64KiB (czyli o indeksie 2) itd.
Napisałem, że powinny być, dlatego że tak naprawdę domyślne skrypty linkera (te dołączone do toolchain’u) obecnie nie wspierają tej techniki. Nie wiem tak do końca jaka jest tego przyczyna, prawdopodobnie nowe skrypty są nadal w fazie testów przed dopuszczeniem do powszechnego użycia. Dokładniejsze omówienie tematu skryptów linkera wykracza jednak poza zakres tak skromnego artykułu, więc nie będę się tutaj rozpisywał (choć oczywiście można o tym poczytać gdzie indziej i poeksperymetować). Póki co powinniśmy przyjąć, że niezależnie od numeru sekcji czy też kwalifikatora, linker i tak umieszcza wszystkie dane w sekcji .progmem.data Nie należy więc używać tych numerowanych kwalifikatorów (__flashN) do deklarowania danych we FLASH, dlatego że odczyt danych może być nieprawidłowy – dane mogą zostać zapisane w innym segmencie, niż kompilator będzie się ich spodziewał. Właściwie wspomniałem o nich, ponieważ mogą się one przydać w sytuacjach, kiedy dokładnie wiemy, gdzie dane zostaną umieszczone, ale o tym później.
Kiedy deklarujemy dużo danych do zapisu we FLASH, prędzej czy później może dojść do sytuacji, że ich łączny rozmiar plus to, co linker umieszcza przed tymi danymi (czyli np. wektory przerwań, ale czasami też inne sekcje) przekroczy limit 64KiB. Ważne jest, żebyśmy o tym wiedzieli. O ile możliwe jest (choć może być kłopotliwe) policzenie, ile zajmą nasze dane, o tyle trudno będzie przewidzieć rozmiar sekcji umieszczonych przed nimi.
Jak się dowiedzieć, czy przekroczyliśmy już limit 64KiB i którego bloku danych to ewentualnie dotyczy?
Możemy oczywiście przeanalizować plik *.map wygenerowany podczas budowania programu, jednak jest to mało wygodne i dość czasochłonne. Spróbujmy więc wydobyć te informacje bezpośrednio z pliku *.elf w inny sposób, używając do tego celu aplikacji avr-objdump.exe Najpierw utworzymy plik o nazwie np. flashsymbols.bat zawierający tekst:
Kod: avr-objdump %1 -t | find "O .text" | sort i zapisujemy w dogodnym dla siebie miejscu.
Atmel Studio 7
Wybieramy w menu:
Tools→External tools…
Pojawi się okno do konfiguracji narzędzi zewnętrznych. Klikamy na przycisk Add i wprowadzamy:
- w polu Title: Flash objects lub jaką nazwę kto woli,
- w polu Command: podajemy pełną ścieżkę do utworzonego wcześniej pliku *.bat
- w polu Arguments: $(TargetDir)$(TargetName).elf
- w polu Initial directory: podajemy pełną ścieżkę do folderu, w którym znajduje się plik avr-objdump.exe – w przypadku Atmel Studio 7 standardowo znajduje się w folderze:
C:\Program Files (x86)\Atmel\Studio\7.0\toolchain\avr8\avr8-gnu-toolchain\bin\ (nie musimy nic wpisywać w Working directory, jeżeli mamy już ten folder dodany do zmiennej środowiskowej PATH w systemie Windows), - zaznaczamy tylko opcję: Use Output window, pozostałe pozostawiamy niezaznaczone.
Zatwierdzamy wszystko klikając na przycisk Apply, a później na OK. W menu Tools powinna się teraz pojawić nowa pozycja o nazwie, jaką wprowadziliśmy w polu Title.
Eclipse MARS 2
Wybieramy w menu:
Run→External Tools…→External Tools Configurations…
Pojawi się okno do konfiguracji narzędzi zewnętrznych. Po lewej stronie znajduje się pole z listą. Zwykle bezpośrednio po instalacji lista zawiera jeden wpis – Program. Zaznaczamy ten wpis. Nad listą powinien znajdować się wiersz pięciu ikonek. Klikamy na pierwszą od lewej (New launch configuration), po czym wpisujemy:
- w polu Name: Flash objects lub jaką nazwę kto woli,
- w polu Location: podajemy pełną ścieżkę do utworzonego wcześniej pliku *.bat
- w polu Working Directory: podajemy pełną ścieżkę do folderu, w którym znajduje się plik avr-objdump.exe – należy go szukać w podfolderze:
[toolchain_path]\avr8\avr8-gnu-toolchain\bin\ (nie musimy nic wpisywać w Working directory, jeżeli mamy już ten folder dodany do zmiennej środowiskowej PATH w systemie Windows), - w polu Arguments: ${project_loc}/${config_name:${project_name}}/${project_name}.elf
- wybieramy zakładkę Build i usuwamy lub pozostawiamy zaznaczenie przy Build before launch w zależności od własnych preferencji – kiedy opcja ta jest zaznaczona, w momencie uruchomienia naszego narzędzia zostanie najpierw wykonane budowanie programu; w przypadku pozostawienia tej opcji zaznaczonej zalecałbym wybranie opcji The project containing selected resource,
- wybieramy zakładkę Common i na liście Display in favorites menu robimy zaznaczenie przy External tools.
Zatwierdzamy klikając kolejno przyciski Apply i Close. Teraz gdy wybierzemy menu Run→External tools... powinniśmy zauważyć nową pozycję o nazwie wprowadzonej w polu Name podczas konfiguracji. Dzięki zastosowaniu zmiennych środowiskowych nasze narzędzie będzie uniwersalne – nie trzeba będzie go konfigurować inaczej przy każdym kolejnym projekcie. Z drugiej jednak strony należy pamiętać (dotyczy Eclipse), że w momencie uruchomienia narzędzia musimy mieć wybrany właściwy projekt (i konfigurację Debug/Release). Jeśli spróbujemy uruchomić nasze narzędzie, kiedy np. będzie akurat aktywne okno konsoli, operacja zakończy się błędem.
Efekt działania naszego narzędzia będzie widoczny w Atmel Studio 7 w oknie Output, natomiast w Eclispe w oknie Console i będzie wyglądał mniej więcej tak (oczywiście rekordów będzie zapewne więcej):
Kod: 000000e4 g O .text 00000014 pgm_string 000000f8 g O .text 00000007 pgm_array
Znaczenie poszczególnych kolumn jest następujące:- adres w pamięci FLASH w postaci liczby szesnastkowej,
- literka g oznacza symbol globalny,
- literka O oznacza, że jest to obiekt (a nie na przykład jedna z funkcji, które też przecież są umieszczane w pamięci FLASH),
- .text oznacza sekcję w której znajduje się obiekt,
- liczba w postaci szesnastkowej oznaczająca rozmiar,
- nazwa symbolu, czyli nazwa stałej zadeklarowana przez nas w kodzie.
WAŻNE:
Na podstawie uzyskanych w ten sposób informacji możemy odpowiednio zmieniać definicje i sposób odczytu naszych danych. Niestety istnieje tutaj pewna niedogodność niezależna od użytej metody obsługi naszych danych (PROGMEM czy __flash). W miarę rozbudowy naszego programu, kiedy będziemy dodawali nowe bloki danych do pamięci FLASH, może zmieniać się kolejność ich rozmieszczenia, przez co może być konieczne ponowne modyfikowanie programu adekwatnie do nowych adresów początkowych i końcowych. Zwykle dane są umieszczane we FLASH w odwrotnej kolejności, niż są zadeklarowane, więc dodawanie nowych danych zawsze przed wcześniej zdefiniowanymi powinno nas uchronić od problemów, ponieważ nowe dane zostaną dołączone na końcu i adresy poprzednich się nie zmienią. Nadal jednak pozostaje problem, gdy dane będziemy deklarować w innych plikach *.c – decyduje wtedy kolejność kompilacji, której nie mamy pod kontrolą korzystając z automatycznie generowanego pliku Makefile.
Na dodatek trzeba też uważać na opcje kompilatora. Przykładowo wyłączenie optymalizacji (czyli ustawienie poziomu optymalizacji na -O0 np. na potrzeby debugowania) może spowodować zmianę kolejności rozmieszczenia danych we FLASH i co za tym idzie nieprawidłowy ich odczyt.
Zapewne skrypt linkera, który umieszczałby dane w odpowiednich sekcjach __flashN w dużej mierze wyeliminowałby powyższe problemy. Póki co trzeba sobie jednak radzić w inny sposób. Dobrym sposobem na uzyskanie kontroli nad rozmieszczenie danych może być tworzenie własnych sekcji w obszarze pamięci FLASH, ale o tym później. Najpierw spróbuję pokazać, jak to zrobić przy zastosowaniu standardowych, lepiej wszystkim znanych metod.
- Definicja przy pomocy atrybutu PROGMEM
Stosując PROGMEM, dane definiujemy tak samo, niezależnie od tego w jakim segmencie 64KiB się znajdą. Tutaj różnica będzie polegać tylko na sposobie odczytu tych danych. Jeśli choćby jeden bajt zdefiniowanego przez nas obszaru danych we FLASH znajdzie się poza pierwszym segmentem 64KiB, należy przyjąć, że obszar taki będzie wymagał specjalnego traktowania, czyli odczytu poprzez tzw. „dalekie wskaźniki” (ang. „far pointers”). „Dalekie” ponieważ ich rozmiar jest 32-bitowy, więc mogą wskazywać na adresy położone „daleko”, czyli poza pierwszym segmentem 64KiB.
Stwórzmy wstępnie taki oto przykładowy kod, nie uwzględniający jeszcze rozmieszczenia danych we FLASH:
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
W celu sprawdzenia rozmieszczenia danych w naszym programie, najpierw wydajemy polecenie budowania (Build→Build Solution [F7] w Atmel Studio, lub Project→Build project w Eclipse), a następnie uruchamiamy nasze narzędzie (znajdujące się w menu Tools w Atmel Studio lub w menu Run→External tools w Eclipse). W wyniku tej operacji w oknie Output w Atmel Studio lub w oknie Console w Eclipse powinniśmy otrzymać następujący tekst:
Kod: 000000cc g O .text 00007fff pgm_array2 000080cb g O .text 00007ffe pgm_array1
Oznacza to, że nasza tablica pgm_array2 została umieszczona pod adresem 0xCC i ma rozmiar 0x7FFF, natomiast tablica pgm_array1 została umieszczona pod adresem 0x80CB i ma rozmiar 0x7FFE. Wszystkie wartości podane są w systemie szesnastkowym. Można oczywiście przekształcić je na system dziesiętny, jednak moim zdaniem lepiej dokonać obliczeń w systemie szesnastkowym, gdyż łatwiej wtedy ocenić przekroczenie limitu. Obecnie chyba każdy kalkulator posiada opcję obliczeń w systemie szesnastkowym, więc nie powinno to sprawić większego problemu.
„Far pointer” jest wymagany zawsze wtedy, kiedy adres końcowy zdefiniowanego przez nas bloku danych jest większy od maksymalnej wartości, jaką może przyjąć liczba całkowita szesnastobitowa bez znaku, czyli od 0xFFFF (65535). W systemie szesnastkowym jest to największa liczba, jaką można zapisać za pomocą czterech cyfr, więc każdy adres składający się z większej ilości cyfr (nie licząc poprzedzających ją zer nieznaczących) oznacza przekroczenie limitu (w systemie dziesiętnym ta granica nie jest tak oczywista).
Ze względu na to, że nasze narzędzie nie podaje adresu końcowego musimy go obliczyć. Robimy to za pomocą wzoru:
adres_końcowy = adres_początkowy + rozmiar – 1
więc dla pgm_array2 adres końcowy będzie równy (obliczenia oczywiście w systemie szesnastkowym):
CC + 7FFF - 1 = 80CA
Widzimy, że adres ten jest czterocyfrowy, co oznacza, iż mieści się w zakresie liczby szesnastobitowej, możemy w takim przypadku do odczytu takiego bloku danych używać makr z grupy pgm_read_xxx(), które jako argument przyjmują adres szesnastobitowy.
Liczymy teraz adres końcowy dla tablicy pgm_array1:
80CB + 7FFE - 1 = 1 00C8
Jak widać adres (w systemie szesnastkowym) ostatniego elementu tablicy jest pięciocyfrowy. Oznacza to, że nie da się go zaadresować za pomocą 16-bitowego wskaźnika. W związku z tym do odczytu elementów tej tablicy nie można użyć makra pgm_read_word(), ponieważ obsługuje ono tylko adres szesnastobitowy. Zamiast tego należy zastosować makro pgm_read_word_far(). Makro to jednak wymaga wskaźnika 32-bitowego, więc nie można użyć zwyczajnie operatora & do pobrania adresu odczytywanych danych, ponieważ wynikiem takiej operacji będzie wskaźnik 16-bitowy. Do pobrania adresu 32-bitowego trzeba użyć makra pgm_get_far_address().
Biorąc pod uwagę powyższe wymagania okazuje się, że napisany przez nas wcześniej kod odczytujący elementy tablicy pgm_array1 nie będzie działał prawidłowo. Powinien on wyglądać następująco:
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Dopiero teraz tablica pgm_array1 będzie odczytywana prawidłowo.
Należy tutaj zwrócić tutaj uwagę na pewien istotny fakt. Wskaźniki w avr-gcc mają rozmiar 16 bitów, co nie jest wystarczające do zaadresowania danych znajdujących się poza granicą 64KiB. Dlatego też należałoby użyć typu o większym zasięgu, czyli np. wskaźników 32-bitowych. Jednakże uint_farptr_t (zwracany przez makro pgm_get_far_address()) tak naprawdę nie jest wskaźnikem tylko typem całkowitym 32-bitowym. Jest to istotne ze względu na arytmetykę wskaźników. W przypadku prawdziwego wskaźnika, operacja array2_ptr++ spowodowałaby zwiększenie wartości wskaźnika o 2, ponieważ elementy tablicy są dwubajtowe, więc adres kolejnego elementu jest zawsze większy o 2 od adresu poprzedniego. Jednak ze względu na to, że array2_ptr jest typu uint_farptr_t, operacja taka nie zwróci prawidłowego rezultatu – po prostu wartość zmiennej zostanie powiększona o 1, niezależnie od tego, jaki jest rozmiar elementów tablicy. Programista musi więc osobiście zadbać o prawidłowe obliczanie adresu, dlatego właśnie należy zastosować inny sposób obliczenia adresu (podczas inkrementacji wskaźnika), na przykład:
array2_ptr += sizeof( pgm_array2[0] );
Znając powyższe zasady można sobie poradzić z zapisem i odczytem danych za pomocą makr z pliku nagłówkowego pgmspace.h, choć każdy chyba przyzna, że jest to dość kłopotliwe.
- Definicja przy użyciu kwalifikatorów __flash i __memx
Podobnie jak poprzednio stwórzmy wstępnie kod, nie biorący pod uwagę granicy 64KiB:
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
W celu sprawdzenia rozmieszczenia danych uruchamiamy nasze narzędzie (tak jak poprzednio – patrz wyżej), w wyniku czego w oknie Output w Atmel Studio lub w oknie Console w Eclipse powinno pojawić co następuje:
Kod: 000000cc g O .text 00007fff pgm_array2 000080cb g O .text 00007ffe pgm_array1
W tym przypadku to tablica pgm_array1 wykracza poza limit 64KiB, więc trzeba zmienić kod tak, by odczyt danych był prawidłowy. Tym razem jednak będzie to o wiele prostsze. Wystarczy bowiem w deklaracji tablicy pgm_array1 zamienić kwalifikator __flash na kwalifikator __memx:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
i to wszystko, co musimy zrobić. Reszta kodu może pozostać bez zmian. Zastosowanie kwalifikatora __memx zamiast __flash spowoduje następujące zmiany w kodzie wynikowym kompilatora:- wskaźnik na elementy tablicy jest 24-bitowy, dzięki czemu jego zasięg się zwiększa,
- rejestr RAMPZ jest ustawiany adekwatnie do potrzeb,
- instrukcja LPM zostaje zamieniona na instrukcję ELPM, która pozwala prawidłowo odczytywać dane z całego zakresu pamięci FLASH.
Można byłoby w tej chwili zadać pytanie: Dlaczego w ogóle stosować kwalifikator __flash, a nie tylko __memx, skoro ten drugi jest wygodniejszy w użyciu? Można przecież za jego pomocą odczytać dowolną komórkę pamięci FLASH.
Odpowiedź brzmi: Ze względu na wydajność kodu (szybkość działania).
Należy pamiętać, że czas wykonania operacji arytmetycznych i logicznych szczególnie w mikrokontrolerach 8‑bitowych jest zależny od rozmiaru operandów (w bajtach). Operacje odczytu i zapisu wskaźnika 24‑bitowego w pamięci RAM również zajmują więcej taktów. Poza tym użycie __memx wymaga aktualizowania na bieżąco rejestru RAMPZ, co również wymaga dodatkowych taktów. Dlatego odczyt danych za pomocą wskaźników 16‑bitowych jest szybszy. Oczywiście nikt nikomu nie zabroni stosowania __memx do odczytu danych w dowolnym miejscu wedle uznania, np. we fragmentach kodu, które nie są krytyczne czasowo.
- Użycie kwalifikatorów __flashN
Wspomniałem wcześniej o tym, że standardowe skrypty linkera w toolchain’ie nie wspierają przestrzeni adresowych __flashN, mimo tego ich obsługa jest zaimplementowana w kompilatorze. Oznacza to, że po znalezieniu danych oznaczonych kwalifikatorem __flashN, będzie on generował kod odczytujący te dane z N-tego segmentu 64KiB (ustawiając odpowiednio rejestr RAMPZ i używając instrukcji ELPM). Piszę o tym, ponieważ mogą zdarzyć się sytuacje, w których warto z tych kwalifikatorów skorzystać.
Przypuśćmy, że do poprzedniego przykładowego kodu dopisaliśmy jeszcze jedną dużą tablicę i tablica pgm_array1 trafiła (w całości) do drugiego segmentu 64KiB (czyli o indeksie 1). Zależy nam jednak na maksymalnie wydajnym odczycie, więc użycie __memx nie będzie optymalne, ze względów, o których pisałem nieco powyżej.
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Sprawdzamy rozmieszczenie danych:
Kod: 000000cc g O .text 00007fff pgm_array3 000080cb g O .text 00007ffe pgm_array2 000100c9 g O .text 00007ffe pgm_array1
Po obliczeniu adresu końcowego:
1 00C9 + 7FFE = 1 80C7
okazuje się, że interesująca nas tablica pgm_array1 znalazła się w całości w drugim segmencie 64KiB (adres początkowy i końcowy należą do tego samego segmentu 64KiB), czyli tam, gdzie teoretycznie powinna się znaleźć po zadeklarowaniu z użyciem kwalifikatora __flash1. Jest to istotne, bo tylko w takiej sytuacji możemy użyć kwalifikatora __flash1. Gdyby adresy początkowy i końcowy należały do różnych segmentów, konieczne byłoby użycie kwalifikatora __memx.
W tym konkretnym przypadku można bez obaw użyć kwalifikatora __flash1 do zadeklarowania tablicy pgm_array1. W efekcie można napisać taki przykładowy kod (tym razem dla odmiany przy użyciu wskaźników):
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Podsumowując, taki sposób definiowania danych we FLASH może być nieco kłopotliwy, szczególnie w projektach, w których definiujemy ich dużo, bo po każdym dodaniu danych wskazane jest sprawdzenie rozmieszczenia danych i ewentualnie wykonanie korekt w kodzie ze względu na możliwość zmiany adresów. Niemniej pokonanie tych niedogodności jest niezbędne w przypadku korzystania ze standardowych skryptów linkera. Myślę jednak, że kiedy zna się z grubsza zasady, którymi kieruje się kompilator i linker, łatwo wypracować sobie skuteczne metody i wtedy nie jest to jakoś szczególnie kłopotliwe.
- Dane w zdefiniowanej sekcji
Zdarzają się sytuacje, kiedy z różnych powodów chcielibyśmy umieścić nasze dane w pamięci FLASH pod konkretnym adresem (na przykład po to, by mieć możliwość maksymalnego zoptymalizowania pętli odczytu tych danych). GCC oferuje do osiągnięcia tego celu atrybut ‘section’ dodawany przy definiowaniu zmiennej, dzięki któremu można umieścić dane w zdefiniowanej wcześniej przez siebie sekcji pod wskazanym adresem.
Najpierw musimy więc zdefiniować sekcję, w której później będziemy umieszczać nasze dane.
Atmel Studio 7
Możliwość zdefiniowania własnej sekcji znaleźć można w menu:
Project‑>Properties‑>Toolchain‑>AVR/GNU Linker‑>Memory Settings
W linii opisanej jako „FLASH segment” należy kliknąć ikonkę „Add Item” i w okienku, które się pojawi wpisać oczekiwane parametry sekcji w formacie:
.nazwa=adres_hex
gdzie:
- nazwa to oczywiście nazwa, jaką chcemy nadać naszej sekcji; należy oczywiście unikać nazw zdefiniowanych przez GCC – nie sposób wymienić tu wszystkie, ale głównie chodzi o sekcje: .text .data .bss .eeprom .noinit .initN .finiN,
- adres_hex to adres początku sekcji w postaci heksadecymalnej (poprzedzony 0x); UWAGA: wpisać należy adres słowa, czyli adres bajtu podzielony przez 2.
Przykładowo, jeśli chcemy utworzyć sekcję o nazwie .waveforms, która zaczyna się od adresu bajtu 0x10100 (czyli 256 bajtów za poczętkiem drugiego segmentu 64KiB), dzielimy adres bajtu przez 2, po czym wynik wpisujemy w pole tekstowe w formie:
.waveforms=0x8080
Eclipse MARS 2
Tego środowiska rzadko używam do programowania AVR, więc nie znam zbyt dobrze pluginu. Niestety nie udało mi się znaleźć tutaj wygodniejszego sposobu zdefiniowania sekcji (np. w opcjach projektu), więc uznałem, że trzeba to zrobić poprzez dodanie opcji linkera.
Należy uruchomić z menu:
Project‑>Properties‑>C/C++ Build‑>Settings‑> (zakładka Tool Settings) AVR C Linker‑>General
i w polu tekstowym Other Arguments wpisać:
-Wl,-section-start=.nazwa=adres_hex
gdzie:- nazwa to oczywiście nazwa, jaką chcemy nadać naszej sekcji; należy oczywiście unikać nazw zdefiniowanych przez GCC – nie sposób wymienić tu wszystkie, ale głównie chodzi o sekcje: .text .data .bss .eeprom .noinit .initN .finiN,
- adres_hex to adres początku sekcji w postaci heksadecymalnej (poprzedzony 0x); w przeciwieństwie do Atmel Studio wpisujemy adres bajtu, a nie słowa, przy czym należy pamiętać, że adres musi być parzysty.
Przykładowo dla sekcji o nazwie .waveforms rozpoczynającej się od adresu bajtu 0x10100 należy wpisać:
-Wl,-section-start=.waveforms=0x10100 Jeśli teraz będziemy chcieli, aby nasze wcześniej utworzone narzędzie wyświetliło nam informacje o rozmieszczeniu danych w naszej sekcji, powinniśmy w pliku flashsymbols.bat dodać na końcu linijkę:
avr-objdump %1 -t | find "O .waveforms" | sort
W celu umieszczenia danych w nowo utworzonej sekcji musimy w momencie definiowania zmiennej nadać jej odpowiedni atrybut, przykładowo:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Ze względu na to, że taki zapis jest dość długi i niewygodny w użyciu, warto stworzyć makro, które uprości nam życie:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Nadany przez nas atrybut to informacja dla linkera, gdzie ma umieścić dane. Kompilator natomiast nie wie, gdzie znajduje się sekcja (nawet, że znajduje się w pamięci read‑only), więc istotne jest, aby zmienną poprzedzić kwalifikatorem const, ponieważ pozwoli mu to np. wygenerować błąd w przypadku omyłkowej próby modyfikacji takiej zmiennej.
Skoro już zapisaliśmy jakieś dane w naszej sekcji, na pewno będziemy chcieli je odczytywać w trakcie działania programu. Dane jednak są umieszczone w pamięci FLASH, więc bezpośredni odczyt w stylu:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
nie zadziała. Mamy dwie możliwości: użyć makr z pliku pgmspace.h lub zrealizować odczyt przy pomocy wskaźników z kwalifikatorami __flash, __flashN lub __memx. Odczyt danych z wykorzystaniem pgmspace.h z naszej przykładowej sekcji, która znajduje się w drugim segmencie 64KiB, musimy zrealizować go przy pomocy makr pgm_read_xxx_far(), adresy pobierając za pomocą makra pgm_get_far_address(). Dodatkowo należy pamiętać o tym, że adres zwracany przez makro nie jest typowym wskaźnikiem, tylko liczbą 32‑bitową, więc programista musi zadbać osobiście np. o prawidłowe obliczanie adresu kolejnych elementów tablicy w zależności od typu tych elementów, o czym pisałem już wcześniej, lub pobierać adres każdej zmiennej wewnątrz struktury osobno (nie da się za pomocą tego wskaźnika odwołać do elementu struktury za pomocą operatora ‑>).
Wybierając drugą opcję, właściwie wystarczy utworzyć wskaźnik z kwalifikatorem __flash1 i za jego pomocą odczytywać dane. Oczywiście dotyczy to sytuacji (takiej jak w podanym powyżej przykładzie), kiedy blok odczytywanych danych znajduje się w całości w drugim segmencie 64KiB. Gdyby zaistniała sytuacja, w której początek i koniec danych znajdowały się w różnych sekcjach, musimy użyć kwalifikatora __memx, który operuje wskaźnikami 24‑bitowymi.
Dla porównania podam może 2 możliwie proste przykłady:
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
użycie kwalifikatora __flash1
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Obydwa kody realizują to samo zadanie. Ocenę czytelności kodu oraz wygody użycia obydwu metod pozostawiam czytelnikowi.
Niestety istnieją również pewne niedogodności związane z definiowaniem danych we własnej sekcji. Jedną z nich jest pobieranie 24-bitowego wskaźnika do danych w naszej sekcji. Gdyby było konieczne zastąpienie wskaźnika z kwalifikatorem __flash1 na taki z kwalifikatorem __memx (np. ze względu na położenie tablicy po obu stronach granicy segmentów 64KiB, zwykła zamiana kwalifikatorów nie wystarczy:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Po wprowadzeniu takiej modyfikacji program wprawdzie się skompiluje, jednak nie będzie prawidłowo odczytywał danych. Dlaczego? Kompilator po prostu zmienne zdefiniowane z atrybutem section traktuje jako ulokowane w pamięci RAM) i stamtąd będzie próbował odczytywać dane (nie dotyczy to atrybutu PROGMEM – pobranie wskaźnika na dane oznaczone tym atrybutem będzie prawidłowe). Trzeba więc użyć sposobu, dzięki któremu możliwe będzie pobranie prawidłowego („pełnego”) adresu danych. Osobiście rozwiązałem ten problem za pomocą następującego makra:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Aby nie psuć sobie czytelności kodu wstawkami asemblerowymi i/lub udostępnić makro dla innych modułów programu, można makro umieścić w osobnym pliku nagłówkowym, który później można dołączyć dyrektywą #include.
W celu zmiany typu wskaźników w poprzednim przykładzie kodu powinno teraz wyglądać w ten sposób:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Innym problemem, aczkolwiek nieco podobnej natury, jest definiowanie wskaźników do naszych danych jako zmiennych globalnych. Chodzi mi tutaj o sytuację podobną do tej z przykładu pokazującego umieszczanie ciągów znaków we FLASH, coś w stylu (skrótowo):
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
W tym przykładzie zarówno ciągi znaków, jak i tablica zawierająca wskaźniki do tych ciągów, zostaną zapisane w pamięci FLASH.
Bazując na tym przykładzie można by się spodziewać, że pisząc analogiczny kod, ale już z użyciem własnej sekcji, otrzymamy taki sam efekt:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
jednak byłoby tak tylko wtedy, gdybyśmy naszą sekcję .waveforms zdefiniowali w pierwszym segmencie 64KiB (ta sama zasada zresztą dotyczy również PROGMEM – jeżeli umieścimy dużo danych w pamięci programu i nasze ciągi znaków wylądują w drugiej sekcji, powyższy kod nie będzie działał prawidłowo). W związku z tym, że sekcję umieściliśmy w drugim segmencie, powyższa metoda nie zadziała.
Myślę, że podstawowa przyczyna takiego stanu rzeczy jest dość oczywista – uzyskane w ten sposób wskaźniki mają rozmiar 16-bitowy, czyli zbyt mały, by zaadresować dane powyżej 64KiB. Ktoś mógłby powiedzieć: „No ale przecież mamy do dyspozycji ‘wskaźniki’ 32‑bitowe (far pointers).” Owszem, jednak ich uzyskanie wymaga użycia makra pgm_get_far_address(), którego można użyć tylko wewnątrz funkcji, czyli nie da się z jego pomocą zdefiniować zmiennej globalnej. Z kolei wewnątrz funkcji nie da się zdefiniować danych w sekcji .waveforms (w ogóle nie można używać atrybutu section do definiowania zmiennych lokalnych, czyli wewnątrz funkcji). Użycie kwalifikatora __flash (a tym bardziej __memx) do zdefiniowania wskaźników na nasze dane poza funkcją też nie zda egzaminu. Próba zdefiniowania tablicy wskaźników do tablic i zapisanie jej w naszej sekcji .waveforms w ten sposób:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
zakończy się błędem: „initializer element is not computable at load time”. Wytłumaczenie przyczyny jest dosyć zawiłe, więc nie będę tutaj opisywał tego szczegółowo, ale generalnie chodzi o to (co wynika ze standardu C), że nie można dokonać konwersji pomiędzy wskaźnikami do różnych przestrzeni adresowych w trakcie ładowania programu (czyli przed jego wejściem do funkcji main() ).
Może się wydawać, że to nie jest poważny problem. Można przecież zdefiniować wskaźniki w pamięci RAM wewnątrz funkcji main() i później przekazywać je do różnych funkcji, ale nie zawsze jest to tak wygodne rozwiązanie, jak globalna tablica wskaźników dostępna w każdej funkcji w obrębie danego pliku. Można oczywiście zadeklarować tablicę wskaźników w RAM jako zmienną globalną, a później zainicjować ją wewnątrz funkcji main(), ale wtedy nie można zadeklarować wskaźników jako const, przez co mogą być podatne na niezamierzone modyfikacje.
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Należy też wziąć pod uwagę, że możemy potrzebować tablicy zawierającej dużo więcej wskaźników (niż w przedstawionym powyżej przykładzie), po 2 bajty każdy (lub 3 w przypadku __memx) i wtedy umieszczenie ich w pamięci RAM może okazać się problemem.
Rozwiązaniem tego problemu może być zdefiniowanie wskaźników za pomocą wartości liczbowych równych adresom danych. Oczywiście jest ono nieco kłopotliwe, pracochłonne i mało eleganckie, ale ma też duże zalety. Otrzymujemy bowiem tablicę wskaźników, która nie zajmuje pamięci RAM, jest dostępna globalnie i jest przeznaczona tylko do odczytu, dzięki czemu nie ma ryzyka nieintencjonalnej zmiany ich wartości.
W celu utworzenia takiej tablicy wskaźników, definiujemy najpierw swoje zmienne:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Następnie należy dowiedzieć się, pod jakimi adresami zastały one umieszczone. Niestety zanim zmienne nie będą w jakiś sposób użyte w kodzie, linker ich nie dołączy do pliku wynikowego. Można oczywiście napisać jakiś fragment (później do niczego niepotrzebny), w którym te dane będą użyte, aby wymusić dołączenie ich przez linker do pliku wynikowego *.hex, ale wygodniejszym i bardziej eleganckim rozwiązaniem będzie dodanie opcji -u symbol do linii poleceń linkera (opcja ta to skrót od opcji --undefined=symbol). W Atmel Studio 7 służy do tego celu:
menu Project‑> Properties…‑> Toolchain‑> AVR/GNU Linker‑> Miscellaneous‑> pole tekstowe Other Linker Flags
lub w Eclipse MARS 2 to samo miejsce, w którym wcześniej definiowaliśmy naszą sekcję, czyli:
menu Project‑>Properties‑> C/C++ Build‑> Settings‑> (zakładka Tool Settings) AVR C Linker‑> General‑> pole tekstowe Other Arguments
W zasadzie wpisanie jednej zmiennej znajdującej się w danej sekcji powinno spowodować, że linker dołączy całą sekcję, czyli wszystkie zmienne, które w tej sekcji zostały zdefiniowane. W naszym przypadku oznacza to, że dodanie linkerowi opcji:
-u sine_wave
powinno spowodować dołączenie przez linker nie tylko tablicy sine_wave, ale także tablicy sawtooth_wave. W niektórych przypadkach (np. kiedy mamy nasze tablice umieszczone w innych sekcjach) może być konieczne dodanie pozostałych zmiennych, czyli w naszym przypadku:
-u sine_wave -u sawtooth_wave
Po tym zabiegu i skompilowaniu projektu, zmienne powinny już być dołączone do pliku wynikowego, nawet jeśli nie są nigdzie używane w kodzie. Uruchomienie teraz naszego narzędzia wydobywającego informacje o rozmieszczeniu danych w pamięci FLASH (pod warunkiem dodania do pliku *.bat nazwy naszej sekcji .waveforms, jak to opisałem wcześniej) powinno wygenerować co następuje:
Kod: 00010100 g O .waveforms 00000800 sawtooth_wave 00010900 g O .waveforms 00000800 sine_wave
Należy tu zwrócić uwagę na to, że kolejność umieszczenia danych w pamięci FLASH jest odwrotna do kolejności definicji w kodzie. Oczywiście możemy sobie przyjąć dowolną kolejność naszych wskaźników w tablicy, chodzi tylko o to, by mieć świadomość, jakie wartości adresów są przyporządkowane poszczególnym nazwom zmiennych. Gdybyśmy chcieli zachować taką samą kolejność wskaźników jak definicji, deklaracja naszej tablicy powinna wyglądać następująco:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Wprawdzie tablica wskaźników nie zostanie zapisana w naszej sekcji .waveforms, tylko (standardowo) w pierwszym segmencie 64KiB, jednak nie powinno to stanowić problemu, ponieważ zwykle tablice wskaźników nie mają jakiegoś znaczącego rozmiaru, za to obsługa zapisanych w ten sposób wskaźników będzie wygodniejsza.
Przedstawię teraz kod demonstrujący opisany powyżej sposób. Nie jest to wprawdzie jakiś użyteczny projekt, choć powinien działać prawidłowo na mikrokontrolerze ATmega2560. Kod generuje za pomocą sygnału PWM na pinie OC1A cztery przebiegi (rozdzielczość 10-bitowa) – sinus, piła, trójkąt i użytkownika – o częstotliwości około 15Hz oraz o czasie trwania około 1 sekundy z przerwami trwającymi około 0,5 sekundy. Jeśli ktoś dysponuje mikrokontrolerem ATmega2560 i chciałby poeksperymentować, to oprócz przedstawionych poniżej plików main.c oraz wavedata.h należy do projektu dołączyć plik wavedata.c do ściągnięcia w postaci spakowanej (ze względu na obszerność).
Plik main.c:
język c
Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Plik wavedata.h:
język c Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.
Plik wavedata.c (start sekcji .waveforms: adres słowa 0x8000 lub adres bajtu 0x10000):
Załączniki: |
Aby zobaczyć załączniki musisz się zalogować. Tylko zalogowani użytkownicy mogą oglądać i pobierać załączniki.
|
Ostatnio edytowano 9 wrz 2017, o 14:32 przez andrews, łącznie edytowano 2 razy
|
|