
#022 - Zabawy z linkerem #01
Krótki opis jak rozwiązać dość często spotykany problem w urządzeniach komercyjnych - umieszczenie struktury z np. danymi kalibracyjnymi w określonej lokalizacji w pamięci.
Załóżmy, że nasze urządzenie dokonuje jakiś precyzyjnych pomiarów i musi być skalibrowane w labie/na osobnym setupie i dodatkowo zajmie się tym ktoś z elektroników. Odchyły w pomiarach jakie wynikają z różnic w komponentach itp powodują, że praktycznie każde urządzenie ma inny zestaw wartości tych parametrów. Takie dane chcemy przechować w pamięci FLASH, ale ponieważ urządzenia są w trakcie developmentu - będziemy w “międzyczasie” programować je nowym wsadem i nie chcemy nadpisać przypadkiem tych ustawień.
Na początku trochę offtop odnośnie plików linkera - wydaje mi się, że w hobbystycznych projektach plik linkera jest dla nas totalnie przezroczysty - jest sobie na boku, wygenerowany przez IDE i praktycznie nie ma potrzeby go zmieniać. Natomiast w komercyjnych projektach czasami zachodzi taka potrzeba i wtedy warto przygotowywać sobie notatki/uwagi na ten temat, które przydadzą się na przyszłość. Zapewne mało kto “regularnie” modyfikuje skrypty linkera, więc nie pamięta jego składni itp, a większość rzeczy dodaje się/modyfikuje przez analogię do podstawowego skrypu z IDE. Warto jednak wiedzieć co można w ten sposób uzyskać i kiedy takie modyfikacje są wymagane. Dlatego stwierdziłem, że opiszę kilka przypadków z jakimi miałem do czynienia plus jakie dodatkowo były z nimi problemy i jak je rozwiązać.
Zadanie na dziś - umieścić strukturę z jakimiś parametrami w konkretnym miejscu w pamięci. W rezultacie pojawi się jeden problem, który trzeba dodatkowo rozwiązać - domyślnie CubeIDE będzie nam taką strukturę nadpisywał, a tego nie chcemy.
Zademonstruję to na przykładzie małego mikrokontrolerka STM32F030F4P6 - bardziej rozbudowane mikrokontrolery mają bardziej rozbudowane linkery co może zaciemnić obraz, a zasada dla nich będzie dokładnie taka sama (chociaż plik linkera do tego procka to ~150linii).
Procedurę, aby to wykonać ogólnie określiłbym tak:
- wybrać gdzie chcemy zapisać ustawienia - zaznajomić się z podziałem na strony
- utworzyć sekcję w linkerze
- utworzyć strukturę i przypisać ją do sekcji
- przetestować działanie
- rozwiązać problem z nadpisywaniem struktury domyślnymi danymi
Gdzie zapisać dane
W praktyce spotykałem się zawsze z umieszczeniem takich danych na samym końcu pamięci FLASH mikrokontrolera, a ich dokładny adres zależy od tego jak zorganizowana jest taka pamięć w konkretnym modelu. Interesuje nas ile zajmują poszczególne strony w pamięci FLASH - strona jest “najmniejszą jednostką” jaka podlega czyszczeniu jednorazowo - czyli jeśli strona ma 1kB to aby wpisać tam nowe dane należy wyczyścić ją całą - czyli domyślnie nie chcemy, aby znalazł się tam kod aplikacji (chociaż można też tak zrobić jeśli nie ma już na nic miejsca w pamięci itp - ale to bardziej zaawansowany problem do rozwiązania - taką kwestię pomijam).
Do sprawdzenia jest to w datasheecie i tu muszę przyznać, że o ile dla serii L4 itp jest to ładnie podane wprost - tutaj ta informacja jest trochę schowana:
Czyli w naszym przypadku mamy 16 stron po 1kB każda. Dlatego możemy śmiało użyć “klasycznego” podejścia i umieścić te dane w ostatnim sektorze.
Kiedy będzie inaczej? Niektóre mikrokontrolery zwłaszcze te z dużą ilością pamięci FLASH mogą mieć pamieć zorganizowaną w taki sposób, że początkowe sektory mają po 1/2kB pamięci, ale końcowe np. 64kB - często nie możemy wtedy pozwolić na “zmarnowanie” takiej ilości. Jednym z rozwiązań jest wtedy przesunięcie głównej aplikacji i wrzucenie struktury z konfiguracją w początkowy sektor - w sumie sprowadza się to tylko do zamiany kolejności w linkerze + zmiany adresu wektorów przerwań w programie.
Jeszcze podpowiedź jak można dodatkowo zweryfikować podział na strony - w CubeProgrammerze po podłączeniu się do mikrokontrolera można zobaczyć taką tabelkę:
W tym przypadku potwierdziło się to co znaleźliśmy w datasheecie - 32 sektory (w tabelce 15 bo liczone od 0) i adres ostatniego to 0x08003C00 - ta informacja się przyda na później. Jeśli mikrokontroler miałby inny układ stron - tutaj byłby pokazany.
Podsumowując - chcemy dane umieścić w ostatniej stronie pamięci FLASH i jest to adres 0x08003C00.
Utworzenie sekcji w linkerze
Zobaczmy najpierw jakie sekcje już są utworzone w “gołym” projekcie - tu ST zrobiło fajną rzecz - plugin “Build Analyzer” wbudowany w CubeIDE - więc screen z niego:
Są to totalnie podstawowe sekcje - nie będę dziś wnikać w ich opisy.
Zobaczmy jak wygląda opis pamięci i takich sekcji w pliku “STM32F030F4PX_FLASH.ld”.
Najpierw pamięć:
/* Memories definition */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 4K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 16K
}
I sekcje:
/* Sections */
SECTIONS
{
/* The startup code into "FLASH" Rom type memory */
.isr_vector :
{
. = ALIGN(4);
KEEP(*(.isr_vector)) /* Startup code */
. = ALIGN(4);
} >FLASH
/* Wycięta zawartość pozostałych sekcji */
}
Jest to definicja sekcji .isr_vector z dodatkowymi informacjami - że musi być wyrównana do 4, “KEEP” czyli zawsze ma być wpisana, że ma trafić do pamięci FLASH + skoro jest na pierwszej pozycji w liście to będzie umieszczona na początku.
W “SECTIONS” umieszczamy:
/* Sekcja na ustawienia */
.settings 0x08003C00 :
{
KEEP (*(.settings))
} >FLASH
Opis chyba zbędny.
Wpisanie danych do sekcji
Umieszczenie zmiennej w sekcji wygląda następująco:
__attribute__((section(".settings"))) const int variable = 10;
Wtedy w “Build Analyzer” zobaczymy:
Osobiście polecam ukrywać takie atrybuty pod definem - tak jak jest zrobiony np. PROGMEM w AVRkach, więc:
#define SETTINGS __attribute__((section(".settings")))
SETTINGS const uint8_t variable = 10;
a może nawet o krok dalej i w defina wciągnąć też “const” - jak kto woli.
Weryfikacja działania
Tak jak w poprzednim akapicie - bazujemy wstępnie na tym:
#define SETTINGS __attribute__((section(".settings")))
SETTINGS const uint8_t variable = 10;
Zweryfikujmy to na kilka sposobów.
- Dla CubeIDE - używając “Build Analyzera” - już to wcześniej zrobiliśmy, ale przeklejam dla kompletu:
zmienna pod właściwym adresem
- Konsolowe narzędzie z toolchaina - “nm” - wyświetla on symbole i adresy.
- Czy faktycznie po zaprogramowaniu mamy tam taką wartość - CubeProgrammer:
Super - mamy wpisaną wartość 10. Czyli podstawowe “zadanie” wykonane.
Natomiast jak poradzić sobie z nadpisywaniem - w kolejnym wpisie (tu będzie link gdy się pojawi).
Ale - pobawmy się jeszcze - co jeśli przekroczymy rozmiar sekcji:
Otrzymamy ładny error o przekroczeniu pamięci.
Generalnie tę metodę można rozbudować o jeszcze jeden krok - może się to okazać lepsze w przypadku danych na początku FLASHa - można dodać nową pamięć w sekcji MEMORY (oczywiście w obrębie FLASHa mikrokontrolera), a potem zmodyfikować start i rozmiar sekcji FLASH. Wtedy będzie łatwiej zapanować nad umieszczeniem kolejnych danych za naszą nową sekcją - tutaj nie musieliśmy o to dbać ponieważ zapisywaliśmy na końcu FLASHa.
Zmiana podejścia
Chociaż - jak teraz przemyślałem ten pomysł - wydaje się mieć przewagę - głównie pod kątem “BuildAnalyzera” - umożliwia on poprawne przeliczanie rozmiaru/pozostałego miejsca w pamięci FLASH - w zaprezentowanej wcześniej metodzie jeśli wykryje on, że dane w sekcji settings zajmują np. 4B to tyle wpisze do zajętości - a tak naprawdę z naszego punktu widzenia zawsze tracimy 1024B. Czyli zmodyfikowana metoda jest bardziej “userFriendly” - więc czemu sobie nie ułatwiać pracy.
Wyszło na to, że zmieniam podejście podczas pisania wpisu - na szczęście jest to mega prosta modyfikacja.
Dodajemy nową pamięć i modyfikujemy wcześniejszy FLASH, czyli zamiast:
/* Memories definition */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 4K
FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 16K
}
będzie:
/* Memories definition */
MEMORY
{
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 4K
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 15K
FLASH_SETTINGS (rx) : ORIGIN = 0x08003C00, LENGTH = 1K
}
I z naszej sekcji kasujemy adres oraz zmieniamy do jakiej pamięci ma trafić, czyli:
/* Sekcja na ustawienia */
.settings :
{
KEEP (*(.settings))
} >FLASH_SETTINGS
I to wszystko.
Efekt jest taki, że teraz “Build Analyzer” poprawnie pokazuje rozmiary FLASHa gdzie ręcznie odjęliśmy 1kB od jego rozmiaru:
jeszcze dla potwierdzenia działania - dla:
SETTINGS const uint8_t variable[64] = { 0 };
Program w mikrokontrolerze to: