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:

  1. wybrać gdzie chcemy zapisać ustawienia - zaznajomić się z podziałem na strony
  2. utworzyć sekcję w linkerze
  3. utworzyć strukturę i przypisać ją do sekcji
  4. przetestować działanie
  5. 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:

fasdf

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ę:

fasdf

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:

fasdf

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:

fasdf

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:

fasdf

zmienna pod właściwym adresem

  • Konsolowe narzędzie z toolchaina - “nm” - wyświetla on symbole i adresy.

fasdf

  • Czy faktycznie po zaprogramowaniu mamy tam taką wartość - CubeProgrammer:

fasdf

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:

fasdf

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:

fasdf

jeszcze dla potwierdzenia działania - dla:

SETTINGS const uint8_t variable[64] = { 0 };

Program w mikrokontrolerze to:

fasdf