Definiujesz rozmiar sterty i stosu dla mikrokontrolera ARM Cortex-M4?

11

Pracowałem z przerwami nad projektem małych systemów wbudowanych. Niektóre z tych projektów wykorzystywały podstawowy procesor ARM Cortex-M4. W folderze projektu znajduje się plik startup.s . Wewnątrz tego pliku zanotowałem następujące dwa wiersze poleceń.

;******************************************************************************
;
; <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Stack   EQU     0x00000400

;******************************************************************************
;
; <o> Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
;
;******************************************************************************
Heap    EQU     0x00000000

Jak definiuje się wielkość stosu i stosu mikrokontrolera? Czy w arkuszu danych znajdują się jakieś konkretne informacje, które pomogą ustalić prawidłową wartość? Jeśli tak, czego należy szukać w arkuszu danych?


Bibliografia:

Mahendra Gunawardena
źródło

Odpowiedzi:

12

Stos i stos to pojęcia oprogramowania, a nie pojęcia sprzętu. Sprzęt zapewnia pamięć. Definiowanie stref pamięci, z których jedna nazywana jest „stosem”, a druga „stertą”, jest wyborem twojego programu.

Sprzęt pomaga w stosach. Większość architektur ma dedykowany rejestr zwany wskaźnikiem stosu. Jego zamierzonym zastosowaniem jest to, że gdy program wykonuje wywołanie funkcji, parametry funkcji i adres zwrotny są wypychane na stos i są wyskakujące, gdy funkcja kończy się i wraca do swojego wywołującego. Naciskanie na stos oznacza pisanie pod adresem podanym przez wskaźnik stosu i odpowiednio zmniejszanie wskaźnika stosu (lub zwiększanie, w zależności od kierunku, w którym rośnie stos). Popping oznacza zwiększanie (lub zmniejszanie) wskaźnika stosu; adres zwrotny jest odczytywany z adresu podanego przez wskaźnik stosu.

Niektóre architektury (choć nie ARM) mają instrukcję wywołania podprogramu, która łączy skok z zapisem na adres podany przez wskaźnik stosu oraz instrukcję powrotu podprogramu, która łączy odczyt z adresu podanego przez wskaźnik stosu i przejście do tego adresu. W ARM zapisywanie i przywracanie adresu odbywa się w rejestrze LR, instrukcje wywoływania i zwracania nie używają wskaźnika stosu. Istnieją jednak instrukcje ułatwiające zapisywanie lub odczytywanie wielu rejestrów na adres podany przez wskaźnik stosu, w celu argumentowania funkcji push i pop.

Aby wybrać rozmiar sterty i stosu, jedyną istotną informacją ze sprzętu jest ilość posiadanej pamięci. Następnie dokonujesz wyboru w zależności od tego, co chcesz przechowywać w pamięci (uwzględniając kod, dane statyczne i inne programy).

Program zwykle używa tych stałych do inicjowania niektórych danych w pamięci, które zostaną wykorzystane przez resztę kodu, takich jak adres górnej części stosu, być może wartość gdzieś w celu sprawdzenia przepełnienia stosu, granice dla alokatora stosu itp.

W kodzie, który oglądasz , Stack_Sizestała służy do rezerwowania bloku pamięci w obszarze kodu (poprzez SPACEdyrektywę w zespole ARM). Górny adres tego bloku ma etykietę __initial_spi jest przechowywany w tabeli wektorów (procesor używa tego wpisu do ustawienia SP po resecie oprogramowania), a także eksportowany do użycia w innych plikach źródłowych. Heap_SizeStała jest podobnie stosowane zarezerwować blok pamięci i etykiet do jej granic ( __heap_basei __heap_limit) są eksportowane do wykorzystania w innych plików źródłowych.

; Amount of memory (in bytes) allocated for Stack
; Tailor this value to your application needs
; <h> Stack Configuration
;   <o> Stack Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Stack_Size      EQU     0x00000400

                AREA    STACK, NOINIT, READWRITE, ALIGN=3
Stack_Mem       SPACE   Stack_Size
__initial_sp


; <h> Heap Configuration
;   <o>  Heap Size (in Bytes) <0x0-0xFFFFFFFF:8>
; </h>

Heap_Size       EQU     0x00000200

                AREA    HEAP, NOINIT, READWRITE, ALIGN=3
__heap_base
Heap_Mem        SPACE   Heap_Size
__heap_limit

…
__Vectors       DCD     __initial_sp               ; Top of Stack
                DCD     Reset_Handler              ; Reset Handler
                DCD     NMI_Handler                ; NMI Handler

…

                 EXPORT  __initial_sp
                 EXPORT  __heap_base
                 EXPORT  __heap_limit
Gilles „SO- przestań być zły”
źródło
Czy wiesz, w jaki sposób określa się te wartości 0x00200 i 0x000400
Mahendra Gunawardena
@MahendraGunawardena To zależy od Ciebie, na podstawie tego, czego potrzebuje Twój program. Odpowiedź Nialla daje kilka wskazówek.
Gilles „SO- przestań być zły”
7

Rozmiary stosu i sterty są definiowane przez aplikację, a nie w dowolnym miejscu w arkuszu danych mikrokontrolera.

Stos

Stos służy do przechowywania wartości zmiennych lokalnych wewnątrz funkcji, poprzednich wartości rejestrów procesora używanych dla zmiennych lokalnych (aby można je było przywrócić po wyjściu z funkcji), adresu programu, do którego należy wrócić przy opuszczaniu tych funkcji, plus trochę narzutów na zarządzanie samym stosem.

Podczas opracowywania systemu osadzonego szacujesz maksymalną oczekiwaną głębokość wywołania, dodajesz rozmiary wszystkich zmiennych lokalnych w funkcjach w tej hierarchii, następnie dodajesz dopełnienie, aby uwzględnić wspomniany wyżej narzut, a następnie dodajesz więcej dla wszelkie przerwy, które mogą wystąpić podczas wykonywania programu.

Alternatywną metodą szacunkową (gdzie pamięć RAM nie jest ograniczona) jest przydzielenie o wiele więcej miejsca na stosie, niż kiedykolwiek będziesz potrzebować, wypełnienie stosu wartością wartownika, a następnie monitorowanie, ile faktycznie używasz podczas wykonywania. Widziałem debugowane wersje środowisk uruchomieniowych języka C, które zrobią to automatycznie. Następnie, gdy zakończysz programowanie, możesz zmniejszyć rozmiar stosu, jeśli chcesz.

Kupa

Obliczenie wielkości stosu, którego potrzebujesz, może być trudniejsze. Sterty służy do dynamicznie alokowanych zmiennych, więc jeśli używasz malloc()i free()w programie języka C, czy newi deletew C ++, to gdzie te zmienne żyć.

Jednak szczególnie w C ++ może istnieć pewna dynamiczna alokacja pamięci. Na przykład, jeśli masz statycznie przydzielone obiekty, język wymaga, aby ich destruktory były wywoływane przy zamykaniu programu. Mam świadomość co najmniej jednego środowiska uruchomieniowego, w którym adresy niszczycieli są przechowywane na dynamicznie przydzielanej liście połączonej.

Aby oszacować rozmiar stosu, którego potrzebujesz, spójrz na cały dynamiczny przydział pamięci w każdej ścieżce w drzewie połączeń, oblicz maksimum i dodaj dopełnienie. Środowisko wykonawcze języka może zapewniać diagnostykę, której można użyć do monitorowania całkowitego zużycia sterty, fragmentacji itp.

Niall C.
źródło
Dziękuję za odpowiedź, lubię określać konkretną liczbę, taką jak 0x00400 i tak dalej
Mahendra Gunawardena
5

Oprócz innych odpowiedzi chciałbym dodać, że podczas tworzenia pamięci RAM między przestrzenią stosu i sterty, należy również wziąć pod uwagę przestrzeń dla statycznych niestałych danych (np. Globale plików, statyka funkcji i cały program globals z perspektywy C i prawdopodobnie inne dla C ++).

Jak działa alokacja stosu / sterty

Warto zauważyć, że plik zestawu startowego jest jednym ze sposobów definiowania regionu; łańcuch narzędzi (zarówno środowisko kompilacji, jak i środowisko wykonawcze) w głównej mierze dbają o symbole, które definiują początek obszaru stosu (używany do przechowywania początkowego wskaźnika stosu w tabeli wektorów) oraz początek i koniec obszaru sterty (używany przez dynamikę alokator pamięci, zazwyczaj dostarczany przez bibliotekę libc)

W przykładzie OP zdefiniowano tylko 2 symbole, rozmiar stosu na 1 kB i rozmiar stosu na 0 kB. Wartości te są używane gdzie indziej, aby faktycznie utworzyć stos i przestrzenie sterty

W przykładzie @Gilles rozmiary są zdefiniowane i używane w pliku zestawu do ustawiania przestrzeni stosu, zaczynając od miejsca, w którym trwa i trwa rozmiar, identyfikowany przez symbol Stack_Mem i ustawiający na końcu etykietę __initial_sp. Podobnie dla sterty, gdzie spacja jest symbolem Heap_Mem (rozmiar 0,5kiB), ale z etykietami na początku i na końcu (__heap_base i __heap_limit).

Są one przetwarzane przez linker, który nie przydzieli niczego w przestrzeni stosu i przestrzeni stosu, ponieważ ta pamięć jest zajęta (przez symbole Stack_Mem i Heap_Mem), ale może umieścić te wspomnienia i wszystkie globale tam, gdzie jest to potrzebne. Etykiety kończą się symbolami bez długości pod podanymi adresami. __Initial_sp jest używany bezpośrednio do tabeli wektorów w czasie łączenia, a __heap_base i __heap_limit przez kod środowiska wykonawczego. Rzeczywiste adresy symboli są przypisywane przez linker na podstawie tego, gdzie je umieścił.

Jak już wspomniałem powyżej, te symbole nie muszą pochodzić z pliku startup.s. Mogą pochodzić z twojej konfiguracji linkera (plik ładowania rozproszonego w Keil, linkerscript w GNU), aw tych możesz mieć dokładniejszą kontrolę nad umieszczaniem. Na przykład możesz zmusić stos do umieszczenia na początku lub na końcu pamięci RAM lub trzymać globale z daleka od stosu lub cokolwiek chcesz. Możesz nawet określić, że HEAP lub STACK zajmują tylko tyle pamięci RAM, ile pozostało po umieszczeniu globali. Pamiętaj jednak, że musisz uważać, aby dodać więcej zmiennych statycznych, aby zmniejszyć inne pamięci.

Jednak każdy zestaw narzędzi jest inny, a sposób zapisania pliku konfiguracyjnego i symboli używanych przez dynamiczny alokator pamięci będzie musiał pochodzić z dokumentacji konkretnego środowiska.

Rozmiar stosu

Jeśli chodzi o sposób określania wielkości stosu, wiele łańcuchów narzędzi może zapewnić maksymalną głębokość stosu, analizując drzewa wywołań funkcji w twoim programie, JEŻELI nie używasz wskaźników rekurencyjnych lub funkcyjnych. Jeśli ich użyjesz, oszacuj rozmiar stosu i wstępnie wypełnij go wartościami kardynalnymi (być może za pomocą funkcji wprowadzania przed głównym), a następnie sprawdź po uruchomieniu programu przez chwilę, gdzie była maksymalna głębokość (czyli tam, gdzie wartości kardynalne koniec). Jeśli w pełni wykorzystałeś swój program do granic swoich możliwości, będziesz dość dokładnie wiedział, czy możesz zmniejszyć stos lub, jeśli program się zawiesi lub nie pozostaną żadne wartości kardynalne, że musisz zwiększyć stos i spróbować ponownie.

Rozmiary sterty

Określenie wielkości sterty jest nieco bardziej zależne od aplikacji. Jeśli dokonujesz alokacji dynamicznej tylko podczas uruchamiania, możesz po prostu dodać miejsce wymagane w kodzie startowym (plus trochę narzuty na zarządzanie pamięcią). Jeśli masz dostęp do źródła swojego menedżera pamięci, możesz dokładnie wiedzieć, co to jest narzut, a może nawet napisać kod, aby przejść pamięć i podać informacje o użytkowaniu. W przypadku aplikacji, które potrzebują dynamicznej pamięci środowiska wykonawczego (np. Przydzielanie buforów dla przychodzących ramek ethernetowych), najlepsze, co mogę zasugerować, to dokładnie wyostrzyć swój rozmiar stosu i dać stosowi wszystko, co pozostało po stosie i statyce.

Uwaga końcowa (RTOS)

Pytanie OP zostało oznaczone jako goły metal, ale chcę dodać notatkę dla RTOS. Często (zawsze?) Każdemu zadaniu / procesowi / wątkowi (po prostu napiszę tutaj zadanie dla uproszczenia) zostanie przypisany rozmiar stosu podczas tworzenia zadania, a oprócz stosów zadań prawdopodobnie będzie mały system operacyjny stos (używany do przerwań i tym podobnych)

Struktury rozliczania zadań i stosy muszą być skądś przydzielone, a często będzie to związane z ogólną przestrzenią sterty aplikacji. W takich przypadkach początkowy rozmiar stosu często nie ma znaczenia, ponieważ system operacyjny użyje go tylko podczas inicjalizacji. Widziałem na przykład, że WSZYSTKIE pozostałe miejsce podczas łączenia ma zostać przydzielone do HEAP i umieszczenie początkowego wskaźnika stosu na końcu stosu, aby wyrósł na stertę, wiedząc, że system operacyjny przydzieli od początku stosu i przydzieli stos systemu operacyjnego tuż przed opuszczeniem stosu initial_sp. Następnie cała przestrzeń jest wykorzystywana do przydzielania stosów zadań i innej dynamicznie alokowanej pamięci.

John O'M.
źródło