Kanał - ATNEL tech-forum
Wszystkie działy
Najnowsze wątki



Teraz jest 28 mar 2024, o 19:53


Strefa czasowa: UTC + 1





Utwórz nowy wątek Odpowiedz w wątku  [ Posty: 10 ] 
Autor Wiadomość
PostNapisane: 9 wrz 2017, o 13:33 
Offline
Użytkownik

Dołączył(a): 07 cze 2016
Posty: 563
Pomógł: 143

Wstęp

    Język C w zasadzie został stworzony dla architektury Von Neumann, w której dane i kod wykonywalny współdzielą tę samą przestrzeń adresową. W związku z tym standard języka C nie przewiduje mechanizmów czy też słów kluczowych, które wspierałyby obsługę oddzielnych przestrzeni adresowych. Kompilatory przeznaczone dla architektury Harwardzkiej muszą stosować różnego rodzaju sztuczki, aby się z tą obsługą uporać.

    W AVR-GCC początkowo zostało to rozwiązane za pomocą nadawania atrybutu PROGMEM danym przeznaczonym do zapisu w pamięci programu (FLASH). Metoda ta wymaga jednak zastosowania kłopotliwych w użyciu makr. Kłopotliwych nie tylko ze względu na pogorszenie czytelności kodu, ale także ze względu na utratę przez kompilator kontroli na typami danych odczytywanymi w ten sposób.

    Później, od wersji 4.7, zostało dodane (oprócz PROGMEM) inne rozwiązanie. Polega ono na skierowaniu danych do umieszczenia we FLASH poprzez nadanie stałej w momencie definicji kwalifikatora __flash lub __memx. Kwalifikator taki zostaje przypisany do konkretnej stałej, dzięki czemu składnia odczytu danych z pamięci FLASH praktycznie jest taka sama, jak przy odczycie zmiennych z pamięci RAM (kompilator wie, skąd odczytywać dane na podstawie kwalifikatora) i dodatkowo zostaje zachowana w pełni kontrola typów.

    Obecnie mamy dwie opcje obsługi danych w pamięci FLASH. Pierwsza z nich jest nieco kłopotliwa w użyciu ze względu na konieczność stosowania makr zmniejszających czytelność kodu i utratę kontroli nad typami. Druga metoda nadal znajduje się w fazie testów, więc można mieć obawy, że nie wszystko działa zawsze zgodnie z założeniem. Osobiście korzystam z tej metody od dłuższego czasu i nie spotkałem się z sytuacją, żeby coś działało nieprawidłowo. Moim skromnym zdaniem warto znać tę drugą, nowszą metodę ze względu na lepszą czytelność kodu i zachowanie kontroli typów.

    Postaram się tutaj opisać i porównać obydwie metody oraz wskazać ewentualne problemy, jakie można napotkać przy ich stosowaniu.

Podstawy teoretyczne

    Na początek chciałbym opisać kilka istotnych dla przedmiotu sprawy zagadnień. Ich znajomość pozwoli lepiej zrozumieć, na czym polega problematyka odczytu danych z pamięci programu.

      • Adresowanie pamięci FLASH

      Pojemność pamięci FLASH w ośmiobitowych mikrokontrolerach AVR podawana jest wprawdzie w bajtach, jednak instrukcje mikrokontrolera mają rozmiar dwóch lub czterech bajtów. W związku z tym pamięć programu zorganizowana jest jako słowa 16-bitowe. Wynika z tego oczywiście, że w mikrokontrolerze o pojemności N[KiB] można umieścić maksymalnie N/2 instrukcji.

      Pomimo takiej organizacji pamięci FLASH, możliwe jest także odczytanie pojedynczego bajtu.

        ◦ Adres słowa
        Wszystkie instrukcje sterujące wykonywaniem programu, takie jak instrukcje skoku warunkowego i bezwarunkowego (względnego i bezwzględnego) czy też wywołania podprocedury, używają adresu słowa.
        Rejestr PC (Program Counter) również zawiera adres słowa – adres aktualnie wykonywanej instrukcji. Jego szerokość (w bitach) jest ściśle związana z pojemnością pamięci FLASH w słowach.
        Za pomocą 16-bitowego wskaźnika czy też rejestru PC można zaadresować maksymalnie 128KiB (64Ki słów) pamięci FLASH.

        ◦ Adres bajtu
        Instrukcja odczytu danych z pamięci FLASH (LPMLoad Program Memory) korzysta z adresu bajtu. Adres ten musi zostać umieszczony w rejestrze wskaźnikowym Z (R31:R30). Adres taki jest tworzony poprzez pomnożenie adresu słowa przez 2 (lub przesunięcie bitowe o jeden bit w lewo). Wartość najmniej znaczącego bitu decyduje o tym, który bajt słowa instrukcja ma odczytać:
      • Z[bit0]=0: odczytany zostanie mniej znaczący bajt słowa,
      • Z[bit0]=1: odczytany zostanie bardziej znaczący bit słowa

        Za pomocą 16-bitowego wskaźnika Z można zaadresować maksymalnie 64KiB pamięci FLASH.

      • Adresowanie rozszerzone

      Generalnie rejestry wskaźnikowe w ośmiobitowych mikrokontrolerach AVR mają szerokość 16 bitów. Pozwala to na zaadresowanie maksymalnie 64KiB danych w pamięci. W zupełności wystarcza to do obsługi pamięci RAM, jednak istnieją mikrokontrolery, w których pojemność pamięci FLASH kilkakrotnie przekracza tę wartość.

      W jaki sposób, w takim przypadku, zaadresować dane spoza limitu 64KiB?

      Otóż mikrokontrolery z tak dużymi pojemnościami FLASH mają dodatkowe rejestry rozszerzające rejestr Z o ilość bitów potrzebną do zaadresowania całego obszaru pamięci programu. Tymi rejestrami są:

      • RAMPZ używany przez instrukcję ELPM (Extended Load Program Memory) w połączeniu z rejestrem Z (RAMPZ:R31:R30) do zaadresowania bajtu danych w pamięci programu – rejestr ten występuje tylko w MCU o pojemności FLASH większej od 64KiB,

      • EIND używany przez instrukcje EIJMP (Extended Indirect JuMP) oraz EICALL (Extended Indirect CALL to Subroutine) w połączeniu z rejestrem Z (EIND:R31:R30) do zaadresowania słowa kodu wykonywalnego w pamięci programu – rejestr ten występuje tylko w MCU o pojemności FLASH większej od 128KiB (64K słów).

      • Troszkę podstaw z procesu budowania programu

      Przedstawię tutaj tylko wybrane wiadomości w dużym uproszczeniu. Chodzi tylko o ogólne zrozumienie zasad rozmieszczania danych i kodu w pamięci programu przez linker.

      Pisany przez nas program musi zawierać co najmniej jeden plik z kodem źródłowym (z rozszerzeniem *.c). Może też być ich więcej. Pliki te nie są bezpośrednio przetwarzane do pliku wykonywalnego. Każdy plik z kodem źródłowym jest najpierw poddany działaniu preprocesora, a następnie przetworzony przez kompilator do pliku obiektowego (plik z rozszerzeniem *.o). Następnie wszystkie pliki obiektowe powstałe w wyniku kompilacji naszych plików źródłowych (nawet, jeśli jest tylko jeden taki plik) zostają skierowane do linkera, który je łączy w jeden plik wynikowy (plik z rozszerzeniem *.elf). Pliki z rozszerzeniami *.hex oraz *.eep, którymi programujemy mikrokontroler powstają w wyniku wydobycia poprzez program objcopy (w naszym przypadku będzie to avr-objcopy) odpowiednich danych właśnie z pliku *.elf.

      Nasz plik źródłowy zawiera jednak różne dane: definicje zmiennych w RAM, dane do umieszczenia w pamięci FLASH lub EEPROM oraz kod funkcji. Kompilator musi więc te dane odpowiednio oznaczyć, aby linker mógł je prawidłowo zidentyfikować i rozmieścić. I tak na przykład zmienne w RAM są oznaczane przez kompilator w pliku obiektowym jako sekcja ”.data”, dane w pamięci programu – jako sekcja ”.progmem.data”, dane przeznaczone dla EEPROM – jako sekcja ”.eeprom” a kod funkcji – jako sekcja ”.text” (która docelowo też jest przeznaczona do zapisu w pamięci FLASH).

      Pliki obiektowe z kompilatora są plikami wejściowymi dla linkera. Plik wynikowy jest plikiem wyjściowym i również posiada różne sekcje. Nie jest jednak tak, że sekcje z plików obiektowych zawsze pokrywają się z sekcjami w pliku wynikowym. O tym, jakie będą relacje między sekcjami wejściowymi i sekcjami wyjściowymi (czyli w których sekcjach wyjściowych zostaną umieszczone poszczególne sekcje wejściowe i w jakiej kolejności) decydują opcje linkera w linii poleceń oraz specjalny plik konfiguracyjny, tak zwany skrypt linkera (zawierający kod w języku linker command language).

      Przykładowo w domyślnych skryptach linkera dla AVR zarówno sekcja wejściowa ”.text” jak i (między innymi) sekcje wejściowe ”.vectors” oraz ”.progmem.data” są przeznaczone do tej samej sekcji wyjściowej ”.text”. Skrypt linkera decyduje również o kolejności umieszczenia poszczególnych sekcji wejściowych w sekcji wyjściowej. Przykładowo standardowa kolejność w domyślnych skryptach linkera dla AVR to:
      • .vectors (czyli wektory przerwań)
      • .progmem.data (czyli dane w pamięci programu)
      • .text (czyli funkcje – kod wykonywalny)

      Oczywiście to tak w uproszczeniu. Pominąłem tutaj kilka (zapewne nie mniej istotnych) sekcji, aby zbytnio nie komplikować tematu.

      Opcje i skrypty linkera to temat na osobny, całkiem obszerny artykuł, więc nie będę tutaj opisywał tego szczegółowo. Chodzi tylko o zrozumienie pewnych ogólnych zasad.

Obsługa danych w pamięci FLASH

Postaram się teraz opisać zasady obsługi danych w pamięci programu w różnych sytuacjach przy zastosowaniu obydwu z metod oraz z możliwie dokładnym opisem. Przedstawione przykłady kodu mają za zadanie tylko pokazać zasady zapisu i odczytu danych i nie należy dopatrywać się w nich głębszego sensu, choć starałem się, aby były kompletne i działały prawidłowo w symulatorze Atmel Studio 7 (standard języka -std=gnu99). Niektóre proste przykłady do prawidłowego działania mogą wymagać wyłączenia optymalizacji, gdyż kompilator, zamiast zapisywać dane we FLASH, będzie je traktował jak makra zdefiniowane za pomocą dyrektywy #define.

    • Dane w pierwszym segmencie 64KiB

    W rzeczywistości należy pamiętać o tym, że w większości przypadków w pierwszej kolejności w pamięci FLASH (począwszy od adresu 0) muszą być zapisane wektory przerwań (chyba że zmieniliśmy to stosując odpowiednie ustawienia fusbitów i rejestrów). W związku z tym limit 64KiB będzie nieco mniejszy, gdyż musimy od niego odjąć rozmiar wektorów przerwań (zależny od typu mikrokontrolera). Oczywistym jest też fakt, że ograniczenie to dotyczy tylko mikrokontrolerów o pojemności większej niż 64KiB.

    Jeśli więc piszemy program na mikrokontroler o pojemności mniejszej lub równej 64KiB lub nasze dane do umieszczenia w pamięci FLASH nie przekraczają łącznie 64KiB (pomniejszonych ewentualnie o rozmiar wektorów przerwań), wystarczy nam znajomość atrybutu PROGMEM (oraz makr z grupy pgm_read_xxx(); ) lub kwalifikatora __flash.

    Właściwie w obydwu przypadkach definicji, tak za pomocą atrybutu PROGMEM jak i przy użyciu kwalifikatora __flash, kompilator umieści dane w sekcji ”.progmem.data”. Różnica polega tylko na sposobie dostępu do tych danych w kodzie źródłowym.

      ◦ Pojedyncza wartość liczbowa

      Wprawdzie w języku C zwykle pojedyncze stałe definiujemy raczej przy użyciu dyrektywy preprocesora #define, jednak mimo wszystko postanowiłem pokazać, jak zapisać je w pamięci FLASH, a następnie odczytać, bo być może to pozwoli lepiej zrozumieć następne przykłady.

        Definicja przy pomocy atrybutu PROGMEM

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


        Definicja przy pomocy kwalifikatora __flash

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Jeżeli teraz porównamy kilka linijek kodu, to nietrudno zauważyć różnice w czytelności:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Dodatkowym problemem w przypadku użycia PROGMEM jest nie tylko konieczność użycia makr w stylu pgm_read_xxx(), lecz również to, że muszą one być adekwatne do odczytywanego typu stałej zapisanej w pamięci FLASH. Jeżeli w powyższym przykładzie zamiast użyć makra pgm_read_word() napiszemy pgm_read_byte(), nie otrzymamy od kompilatora żadnego ostrzeżenia, a odczyt będzie nieprawidłowy (odczytany zostanie tylko jeden bajt).

      Odczyt danych zadeklarowanych do zapisu w pamięci programu przy użyciu kwalifikatora __flash jest pozbawiony tej wady. Kompilator zna typ odczytywanych danych i zawsze automatycznie odczyta prawidłową ilość bajtów.

      ◦ Tablica wartości stałych

        Definicja przy pomocy atrybutu PROGMEM

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


        Definicja przy pomocy kwalifikatora __flash

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      ◦ Ciągi znaków (string)

      Jest to jeden z typów danych najczęściej zapisywanych w pamięci FLASH, dlatego spróbuję przedstawić nieco bardziej rozbudowany przykład.

        Definicja przy pomocy atrybutu PROGMEM

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


        Definicja przy pomocy kwalifikatora __flash

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Myślę, że kolejne porównanie nie wymaga komentarza:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      ◦ Tablica struktur

      Struktury to również typ danych dosyć często zapisywany w pamięci FLASH (może być używany przykładowo do tworzenia elementów menu). Oczywiście poniższe przykłady to tylko pokazanie sposobu definiowania struktur w pamięci programu oraz ich późniejszego odczytu, a nie przykłady budowania i obsługi menu. Przedstawione tam funkcje nie robią niestety nic pożytecznego, za to mają pokazać, w jaki sposób przekazać dane ze struktury do funkcji oraz jak ich wewnątrz funkcji użyć.

        Definicja przy pomocy atrybutu PROGMEM

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


        Definicja przy pomocy kwalifikatora __flash

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      Jeszcze jedno porównanie:

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


      ◦ Dane w pierwszym segmencie 64KiB – podsumowanie

      Ze względów oczywistych niemożliwe jest pokazanie przykładów wszystkich możliwych kombinacji. Omówienie różnych typów danych oraz sposobów ich użycia wykracza poza ramy tego artykułu. Moim celem było jedynie pokazanie specyfiki umieszczania tych danych w pamięci programu przy użyciu dwóch możliwych metod i pokazanie różnic między tymi metodami. Myślę, że każdy kto potrafi już operować danymi w pamięci RAM (tzn. definiować, uzyskiwać do nich dostęp bezpośredni lub poprzez wskaźniki, przekazywać jako argumenty do funkcji) w łatwy sposób, na podstawie powyższych przykładów, powinien stworzyć własny kod spełniający jego oczekiwania.



Ostatnio edytowano 9 wrz 2017, o 14:36 przez andrews, łącznie edytowano 2 razy

Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 9 wrz 2017, o 14:27 
Offline
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:
        1. adres w pamięci FLASH w postaci liczby szesnastkowej,
        2. literka g oznacza symbol globalny,
        3. 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),
        4. .text oznacza sekcję w której znajduje się obiekt,
        5. liczba w postaci szesnastkowej oznaczająca rozmiar,
        6. 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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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.

        Składnia: [ Pobierz ] [ Ukryj ]
        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):

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

          użycie makr pgmspace.h

        Składnia: [ Pobierz ] [ Ukryj ]
        język c
        Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


          użycie kwalifikatora __flash1

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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):

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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.

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        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:

        Składnia: [ Pobierz ] [ Ukryj ]
        język c
        Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.


        Plik wavedata.h:

        Składnia: [ Pobierz ] [ Ukryj ]
        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

Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 9 wrz 2017, o 14:28 
Offline
Użytkownik

Dołączył(a): 07 cze 2016
Posty: 563
Pomógł: 143

      • Funkcje uniwersalne z wykorzystaniem kwalifikatora __memx

      Chyba powszechnie wiadomo, że do obsługi danych w pamięci FLASH, służą inne funkcje bibliotek standardowych, niż do obsługi danych w pamięci RAM (mają one przyrostek „_P” lub „_PF”). Wynika to z tego, że należy w inny sposób odczytywać takie dane, a sam wskaźnik (standardowy 16‑bitowy) nie informuje o tym, gdzie dane się znajdują. Dlatego szereg funkcji standardowych musi mieć trzy wersje. Jedna z wersji traktuje przekazywany do funkcji wskaźnik jako wskaźnik do RAM, druga (z przyrostkiem „_P”) – jako wskaźnik do FLASH poniżej limitu 64KiB, trzecia (z przyrostkiem „_PF”) – jako wskaźnik do FLASH powyżej 64KiB. Przykładami takich funkcji mogą być:
      • memcpy() – memcpy_P() – memcpy_PF()
      • strlen() – strlen_P() – strlen_PF()
      • strcmp() – strcmp_P() – strcmp_PF()


      Jeśli tworzymy własne funkcje, również musimy stosować tę samą zasadę. Każdy chyba przyzna, że – choć relatywnie szybkie (ze względu na 16-bitowe wskaźniki) – nie jest to wygodne rozwiązanie.

      Problem ten możemy jednak rozwiązać, stosując jako parametry funkcji wskaźniki z kwalifikatorem __memx. Wprawdzie funkcja taka będzie nieco wolniejsza ze względu na to, że takie wskaźniki są 24‑bitowe, ale nie wszystkie funkcje są krytyczne czasowo (np. obsługa menu) i nie zawsze musimy oszczędzać każdy takt zegara, a wygoda używania jednej funkcji do obsługi danych zapisanych zarówno w pamięci FLASH jak i RAM jest moim zdaniem nieoceniona.

      Jak to się dzieje, że jest to możliwe?

      Wskaźnik z kwalifikatorem __memx łączy w sobie wszystkie przestrzenie adresowe mikrokontrolera w ten sposób, że zawiera regiony:
      Kod:
       |   adres startowy   |   przeznaczenie  |
       -----------------------------------------
       | 0x000000           | FLASH            |
       | 0x80nnnn           | RAM              |
       | 0x810000           | EEPROM           |
       | 0x820000           | FUSE             |
       | 0x830000           | LOCK             |
       | 0x840000           | SIGNATURE        |
       | 0x850000           | USER_SIGNATURE   |


      Adres startowy regionu RAM (opisany jako 0x80nnnn) jest zależny od architektury mikrokontrolera, dla którego jest generowany kod. Może się on wahać od 0x800040 dla architektury avrtiny do 0x800200 dla architektury avr6 (dla architektur avrxmega wynosi nawet 0x802000).

      Rozmiary (czyli także adresy końcowe) poszczególnych regionów również zależą od architektury mikrokontrolera.

      Nas interesują tylko pierwsze dwa regiony. Jak łatwo zauważyć, podstawową cechą, która odróżnia te regiony jest siódmy bit najstarszego bajtu adresu. Kiedy więc przekazujemy do funkcji wskaźnik z kwalifikatorem __memx (bajt2:bajt1:bajt0), kompilator generuje kod, który na podstawie tego właśnie bitu odróżnia lokalizację danych.

      • Jeśli ten bit jest jedynką, ładuje do rejestru Z (R31:R30) dwa młodsze bajty przekazanego adresu (bajt1:bajt0) i używa instrukcji ST, STD / LD, LDD w celu zapisania/odczytania zmiennej do/z pamięci RAM.
      • Jeśli ten bit jest zerem, ładuje do rejestrów RAMPZ:R31:R30 cały przekazany adres (bajt2:bajt1:bajt0) i używa instrukcji ELPM w celu odczytu danych z pamięci FLASH.

      Niestety kompilator nie obsługuje w ten sposób danych zapisanych np. w regionie EEPROM (czy też w innych regionach). Gdybyśmy próbowali przekazać do funkcji wskaźnik do pamięci EEPROM, zostanie on potraktowany jako wskaźnik do pamięci RAM (pomimo tego, że adres jest z zakresu regionu EEPROM). Kompilator sprawdza tylko siódmy bit najstarszego bajtu, a nie sprawdza bitu 0 najstarszego bajtu, który w przypadku regionu EEPROM jest równy 1 (w odróżnieniu do regionu RAM, gdzie jest zerem), więc nie może odróżnić RAM i EEPROM. Inaczej mówiąc, wszystkie adresy rozpoczynające się od jedynki są traktowane jako wskaźniki do pamięci RAM, więc chcąc operować na danych w EEPROM musimy utworzyć osobną funkcję.

      Postaram się przedstawić w poniższym przykładzie kodu, jak taką funkcję stworzyć i jak jej używać. Będzie to kod dla ATmega2560 zawierający uniwersalną funkcję do wysyłania ciągów znaków poprzez UART (start sekcji .strings: adres słowa 0x10000 lub adres bajtu 0x20000):

      Składnia: [ Pobierz ] [ Ukryj ]
      język c
      Musisz się zalogować, aby zobaczyć kod źródłowy. Tylko zalogowani użytkownicy mogą widzieć kod.

Podsumowanie

    Przedstawiłem tutaj kilka przykładów kodu, jednak nie sposób rozpatrzyć wszystkich możliwych przypadków obsługi danych w pamięci FLASH. W pamięci FLASH można umieścić praktycznie każdy typ danych jak np. typy całkowite, zmiennoprzecinkowe, wskaźniki, ciągi znaków, struktury, tablice. Odczyt danych poszczególnych typów będzie inny i zależny od implementacji i potrzeb danego projektu. Dlatego starałem się przedstawić tutaj ogólne reguły i zasady obsługi, aby każdy mógł samodzielnie znaleźć własny, najbardziej optymalny dla swojego projektu sposób.

    Mam nadzieję, że było wystarczająco zrozumiale. Jeśli nie, można zadawać pytania. Postaram się odpowiedzieć na miarę mojej skromnej wiedzy. Mam też nadzieję, że poradnik okaże się dla kogoś przydatny.

    Starałem się z całej siły uniknąć błędów, jednak nie mogę w pełni zagwarantować ich braku. Proszę więc tych, którzy jakiekolwiek błędy zauważą o zwrócenie uwagi. Postaram się jak najszybciej je poprawić.





Ostatnio edytowano 17 wrz 2017, o 07:21 przez andrews, łącznie edytowano 2 razy

Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 10 wrz 2017, o 08:09 
Offline
Moderator
Avatar użytkownika

Dołączył(a): 03 paź 2011
Posty: 27212
Lokalizacja: Szczecin
Pomógł: 1041

BARDZO ŁADNY OPIS ;) ... piękna wisienka na torcie jeśli chodzi o AVR i wykorzystywanie pamięci FLASH - specyfikatory __flash czy __memx ;)

_________________
zapraszam na blog: http://www.mirekk36.blogspot.com (mój nick Skype: mirekk36 ) [ obejrzyj Kurs EAGLE ] [ mój kanał YT TV www.youtube.com/mirekk36 ]



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 10 wrz 2017, o 11:35 
Offline
Użytkownik

Dołączył(a): 07 cze 2016
Posty: 563
Pomógł: 143

Dziękuję bardzo za słowa uznania od samego Szefa.

Może jeszcze w kwestii formalnej:
SunRiver napisał(a):
pytania dopiero jak kol. andrews napisze że zakończył cykl

Jakoś mi to wcześniej umknęło, więc teraz chciałbym poinformować, że ja już "zakończyłem cykl" ;)
.



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 10 wrz 2017, o 11:37 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 04 paź 2011
Posty: 8583
Pomógł: 337

Ok zatem sprzątam :)

_________________
[b]San Escobar! Patria mia! Tu eres como la salud.[/b]



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 11 wrz 2017, o 13:41 
Offline
Użytkownik
Avatar użytkownika

Dołączył(a): 15 lut 2017
Posty: 369
Lokalizacja: Gliwice
Pomógł: 34

Dzięki za pouczający wpis. Czekam na następne :)



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 11 wrz 2017, o 14:37 
Offline
Użytkownik

Dołączył(a): 23 sty 2014
Posty: 1079
Pomógł: 73

O KURCZE! :) Kawał roboty, naprawdę WIELKI szacunek i podziękowanie za podzielenie się wiedzą :)



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 7 paź 2018, o 18:05 
Offline
Nowy

Dołączył(a): 01 lut 2015
Posty: 2
Pomógł: 0

Tego szukałem, właśnie się męczę z menu które chce "wcisnąć" do Flash-a ale poległem na wskaznikach :(
Ale dzięki temu opisowi mam nadzieję przełamać impas, jeszcze raz dzięki za opis i przykłady, kawał dobrej roboty :)



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
PostNapisane: 30 maja 2019, o 13:16 
Offline
Nowy

Dołączył(a): 02 sty 2015
Posty: 1
Pomógł: 0

A ja sie tak męczyłem ;)
Wielkie dzięki za poświęcony czas na wytłumaczenie tego !



Góra
 Zobacz profil  
cytowanie selektywne  Cytuj  
Wyświetl posty nie starsze niż:  Sortuj wg  
Utwórz nowy wątek Odpowiedz w wątku  [ Posty: 10 ] 

Strefa czasowa: UTC + 1


Kto przegląda forum

Użytkownicy przeglądający ten dział: Brak zidentyfikowanych użytkowników i 0 gości


Nie możesz rozpoczynać nowych wątków
Nie możesz odpowiadać w wątkach
Nie możesz edytować swoich postów
Nie możesz usuwać swoich postów
Nie możesz dodawać załączników

Szukaj:
Skocz do:  
Sitemap
Technologię dostarcza phpBB® Forum Software © phpBB Group phpBB3.PL
phpBB SEO