Co znajduje się w różnych typach pamięci mikrokontrolera?

25

Istnieją różne segmenty pamięci, do których różne typy danych są wprowadzane z kodu C po kompilacji. Czyli: .text, .data, .bss, stosu i sterty. Chcę tylko wiedzieć, gdzie każdy z tych segmentów znajdowałby się w pamięci mikrokontrolera. Oznacza to, które dane trafiają do jakiego rodzaju pamięci, biorąc pod uwagę typy pamięci RAM, NVRAM, ROM, EEPROM, FLASH itp.

Znalazłem tutaj odpowiedzi na podobne pytania, ale nie udało im się wyjaśnić, jaka byłaby zawartość każdego z różnych typów pamięci.

Każda pomoc jest bardzo ceniona. Z góry dziękuję!

Soju T Varghese
źródło
1
NVRAM, ROM, EEPROM i Flash to właściwie tylko różne nazwy dla tej samej rzeczy: pamięć nieulotna.
Lundin
Nieco styczne do pytania, ale kod może (wyjątkowo) istnieć w większości dowolnych z nich, szczególnie jeśli weźmie się pod uwagę użycie łaty lub kalibracji. Czasami zostanie przeniesiony przed wykonaniem, czasem wykonany w miejscu.
Sean Houlihane
@SeanHoulihane OP pyta o mikrokontrolery, które prawie zawsze uruchamiają się z Flasha (twój komentarz został zakwalifikowany wyjątkowo). Na przykład mikroprocesory z MB zewnętrznej pamięci RAM z systemem Linux kopiowałyby swoje programy do pamięci RAM, aby je wykonać, być może z karty SD działającej jako wolumin montowany.
tcrosley
@tcrosley Istnieją teraz mikrokontrolery z TCM, a czasami mikrokontrolery są częścią większego SoC. Podejrzewam również, że zdarzają się przypadki takie jak urządzenia eMMC, w których mcu ładuje się, by uruchomić się z pamięci RAM z własnej pamięci (na podstawie pamięci twardego telefonu sprzed kilku lat). Zgadzam się, nie jest to bezpośrednia odpowiedź - ale uważam, że bardzo istotne jest to, że typowe mapowania nie są w żaden sposób surowymi regułami.
Sean Houlihane
1
nie ma żadnych zasad łączenia jednej rzeczy z drugą, na pewno najlepiej byłoby czytać we flashu tylko tekst i rodata, ale idą tam również dane, przesunięcie i rozmiar pliku .bss (a następnie zostaną skopiowane przez kod ładujący). te terminy (.tekst itp.) nie mają nic wspólnego z mikrokontrolerami. Jest to kompilator / zestaw narzędzi, który dotyczy wszystkich celów kompilatorów / zestawów narzędzi. na koniec dnia programista decyduje, dokąd idzie, i zwykle informuje łańcuch narzędzi za pomocą skryptu linkera.
old_timer

Odpowiedzi:

38

.tekst

Segment .text zawiera rzeczywisty kod i jest zaprogramowany w pamięci Flash dla mikrokontrolerów. Może istnieć więcej niż jeden segment tekstowy, gdy istnieje wiele nieciągłych bloków pamięci Flash; np. wektor startowy i wektory przerwań umieszczone na górze pamięci i kod rozpoczynający się od 0; lub oddzielne sekcje dla programu ładującego i programu głównego.

.bss i .data

Istnieją trzy rodzaje danych, które można przypisać zewnętrznie do funkcji lub procedury; pierwszy to niezainicjowane dane (historycznie nazywane .bss, które obejmuje również 0 zainicjowanych danych), a drugi to inicjalizacja (nie bss) lub .data. Nazwa „bss” historycznie pochodzi od „Block Started by Symbol”, używanego w asemblerze około 60 lat temu. Oba te obszary znajdują się w pamięci RAM.

Podczas kompilacji programu zmienne zostaną przypisane do jednego z tych dwóch ogólnych obszarów. Podczas etapu łączenia wszystkie elementy danych będą zbierane razem. Wszystkie zmienne, które należy zainicjować, będą miały część pamięci programu przeznaczoną do przechowywania wartości początkowych, a tuż przed wywołaniem funkcji main () zmienne zostaną zainicjowane, zwykle przez moduł o nazwie crt0. Sekcja bss jest inicjalizowana do wszystkich zer przez ten sam kod startowy.

Z kilkoma mikrokontrolerami dostępne są krótsze instrukcje, które umożliwiają dostęp do pierwszej strony (pierwsze 256 lokalizacji, czasami nazywanej stroną 0) pamięci RAM. Kompilator dla tych procesorów może zarezerwować słowo kluczowe, takie jak nearoznaczenie zmiennych, które mają zostać tam umieszczone. Podobnie istnieją również mikrokontrolery, które mogą odwoływać się tylko do niektórych obszarów za pośrednictwem rejestru wskaźników (wymagające dodatkowych instrukcji) i takie zmienne są oznaczone far. Wreszcie, niektóre procesory mogą adresować sekcję pamięci bit po bicie, a kompilator będzie w stanie to określić (na przykład słowo kluczowe bit).

Mogą więc istnieć dodatkowe segmenty, takie jak .nearbss i .neardata itp., W których gromadzone są te zmienne.

.rodata

Trzeci typ danych zewnętrznych względem funkcji lub procedury przypomina zmienne zainicjowane, z tym że są one tylko do odczytu i nie mogą być modyfikowane przez program. W języku C zmienne te oznaczone są za pomocą constsłowa kluczowego. Zazwyczaj są one przechowywane jako część pamięci flash programu. Czasami są one identyfikowane jako część segmentu .rodata (dane tylko do odczytu). Na mikrokontrolerach korzystających z architektury Harvarda kompilator musi użyć specjalnych instrukcji, aby uzyskać dostęp do tych zmiennych.

stos i stos

Stos i stos są umieszczone w pamięci RAM. W zależności od architektury procesora stos może rosnąć lub rosnąć. Jeśli dorośnie, zostanie umieszczony na dole pamięci RAM. Jeśli wzrośnie, zostanie umieszczony na końcu pamięci RAM. Sterta wykorzysta pozostałą pamięć RAM nieprzydzieloną do zmiennych i będzie rosła w przeciwnym kierunku do stosu. Maksymalny rozmiar stosu i sterty można zwykle określić jako parametry linkera.

Zmienne umieszczone na stosie to dowolne zmienne zdefiniowane w funkcji lub procedurze bez słowa kluczowego static. Kiedyś nazywano je zmiennymi automatycznymi ( autosłowo kluczowe), ale to słowo kluczowe nie jest potrzebne. Historycznie autoistnieje, ponieważ był częścią języka B poprzedzającego C i tam był potrzebny. Parametry funkcji są również umieszczane na stosie.

Oto typowy układ pamięci RAM (przy założeniu braku specjalnej sekcji strony 0):

wprowadź opis zdjęcia tutaj

EEPROM, ROM i NVRAM

Zanim pojawiła się pamięć Flash, EEPROM (elektronicznie kasowalna programowalna pamięć tylko do odczytu) był używany do przechowywania programu i stałych danych (segmenty .text i .rodata). Teraz dostępna jest tylko niewielka ilość (np. 2 KB do 8 KB) pamięci EEPROM, o ile w ogóle jest dostępna, i jest zwykle używana do przechowywania danych konfiguracyjnych lub innych niewielkich ilości danych, które muszą zostać zachowane po wyłączeniu zasilania cykl. Nie są one deklarowane jako zmienne w programie, ale są zapisywane przy użyciu specjalnych rejestrów w mikrokontrolerze. EEPROM może być również zaimplementowany w osobnym układzie scalonym i dostępny za pośrednictwem magistrali SPI lub I²C.

Pamięć ROM jest zasadniczo taka sama jak Flash, tyle że jest programowana fabrycznie (nie programowalna przez użytkownika). Jest używany tylko w przypadku urządzeń o bardzo dużej głośności.

NVRAM (nieulotna pamięć RAM) jest alternatywą dla EEPROM i zwykle jest implementowana jako zewnętrzny układ scalony. Zwykła pamięć RAM może być uważana za nieulotną, jeśli jest zasilana z baterii; w takim przypadku nie są potrzebne żadne specjalne metody dostępu.

Chociaż dane można zapisywać we Flashu, pamięć Flash ma ograniczoną liczbę cykli kasowania / programów (od 1000 do 10 000), więc nie jest do tego specjalnie zaprojektowana. Wymaga również usunięcia bloków pamięci jednocześnie, więc aktualizowanie zaledwie kilku bajtów jest niewygodne. Jest przeznaczony do kodu i zmiennych tylko do odczytu.

EEPROM ma znacznie wyższe limity cykli kasowania / programów (od 100 000 do 1 000 000), więc jest znacznie lepszy w tym celu. Jeśli w mikrokontrolerze jest dostępna pamięć EEPROM i jest ona wystarczająco duża, tam właśnie chcesz zapisać nieulotne dane. Będziesz jednak musiał najpierw skasować bloki (zwykle 4KB) przed napisaniem.

Jeśli nie ma EEPROM lub jest za mały, potrzebny jest zewnętrzny układ. Pamięć EEPROM o pojemności 32 KB ma tylko 66 centów i można ją usunąć / zapisać do 1 000 000 razy. NVRAM z taką samą liczbą operacji kasowania / programu jest znacznie droższy (x10). NVRAM są zwykle szybsze do odczytu niż EEPROM, ale wolniejsze do zapisu. Mogą być zapisywane do jednego bajtu na raz lub blokami.

Lepszą alternatywą dla obu z nich jest FRAM (ferroelektryczna pamięć RAM), która ma zasadniczo nieskończone cykle zapisu (100 bilionów) i nie ma opóźnień zapisu. Ma mniej więcej tę samą cenę co NVRAM, około 5 USD za 32 KB.

tcrosley
źródło
To była naprawdę przydatna informacja. Czy możesz podać odniesienie do swojego wyjaśnienia? Podobnie jak podręczniki lub czasopisma, na wypadek, gdybym chciał dowiedzieć się więcej na ten temat ...?
Soju T Varghese
jeszcze jedno pytanie, czy mógłbyś podać wyobrażenie o zaletach lub wadach jednej pamięci nad drugą (EEPROM, NVRAM i FLASH)?
Soju T Varghese
Niezła odpowiedź. Zamieściłem komplementarny, koncentrujący się bardziej szczegółowo na tym, co się dzieje w języku C.
Lundin
1
@SojuTVarghese Zaktualizowałem swoją odpowiedź i zamieściłem również informacje o FRAM.
tcrosley,
@Lundin użyliśmy tych samych nazw segmentów (np .rodata), więc odpowiedzi ładnie się uzupełniają.
tcrosley,
21

Normalny system osadzony:

Segment     Memory   Contents

.data       RAM      Explicitly initialized variables with static storage duration
.bss        RAM      Zero-initialized variables with static storage duration
.stack      RAM      Local variables and function call parameters
.heap       RAM      Dynamically allocated variables (usually not used in embedded systems)
.rodata     ROM      const variables with static storage duration. String literals.
.text       ROM      The program. Integer constants. Initializer lists.

Ponadto zwykle istnieją oddzielne segmenty flash dla kodu startowego i wektorów przerwań.


Wyjaśnienie:

Zmienna ma statyczny czas przechowywania, jeśli jest zadeklarowana jako staticlub znajduje się w zakresie pliku (czasami niedbale nazywana „globalną”). C ma regułę stwierdzającą, że wszystkie zmienne czasu trwania przechowywania statycznego, których programista nie zainicjował jawnie, muszą być inicjowane na zero.

Każda zmienna czasu przechowywania statycznego, która jest inicjowana na zero, domyślnie lub jawnie, kończy się na .bss. Podczas gdy te, które są wyraźnie zainicjowane na niezerową wartość, kończą się w .data.

Przykłady:

static int a;                // .bss
static int b = 0;            // .bss      
int c;                       // .bss
static int d = 1;            // .data
int e = 1;                   // .data

void func (void)
{
  static int x;              // .bss
  static int y = 0;          // .bss
  static int z = 1;          // .data
  static int* ptr = NULL;    // .bss
}

Należy pamiętać, że bardzo powszechną niestandardową konfiguracją systemów osadzonych jest „minimalne uruchomienie”, co oznacza, że ​​program pominie wszystkie inicjalizacje obiektów o statycznym czasie przechowywania. Dlatego może być mądre, aby nigdy nie pisać programów, które opierają się na wartościach inicjujących takich zmiennych, ale zamiast tego ustawiają je w „czasie wykonywania” przed ich pierwszym użyciem.

Przykłady innych segmentów:

const int a = 0;           // .rodata
const int b;               // .rodata (nonsense code but C allows it, unlike C++)
static const int c = 0;    // .rodata
static const int d = 1;    // .rodata

void func (int param)      // .stack
{
  int e;                   // .stack
  int f=0;                 // .stack
  int g=1;                 // .stack
  const int h=param;       // .stack
  static const int i=1;    // .rodata, static storage duration

  char* ptr;               // ptr goes to .stack
  ptr = malloc(1);         // pointed-at memory goes to .heap
}

Zmienne, które mogą wejść na stos, często kończą się w rejestrach procesora podczas optymalizacji. Zasadniczo każdą zmienną, która nie ma pobranego adresu, można umieścić w rejestrze procesora.

Zauważ, że wskaźniki są nieco bardziej skomplikowane niż inne zmienne, ponieważ pozwalają na dwa różne rodzaje const, w zależności od tego, czy wskazane dane powinny być tylko do odczytu, czy też sam wskaźnik powinien być. Bardzo ważne jest, aby znać różnicę, aby wskaźniki nie trafiły przypadkowo do pamięci RAM, gdy chcesz, aby były we flashu.

int* j=0;                  // .bss
const int* k=0;            // .bss, non-const pointer to const data
int* const l=0;            // .rodata, const pointer to non-const data
const int* const m=0;      // .rodata, const pointer to const data

void (*fptr1)(void);       // .bss
void (*const fptr2)(void); // .rodata
void (const* fptr3)(void); // invalid, doesn't make sense since functions can't be modified

W przypadku stałych całkowitych, list inicjalizujących, literałów łańcuchowych itp., Mogą one kończyć się w .text lub .rodata, w zależności od kompilatora. Prawdopodobnie kończą jako:

#define n 0                // .text
int o = 5;                 // 5 goes to .text (part of the instruction)
int p[] = {1,2,3};         // {1,2,3} goes to .text
char q[] = "hello";        // "hello" goes to .rodata
Lundin
źródło
Nie rozumiem, w twoim pierwszym przykładzie kodu, dlaczego „static int b = 0;” przechodzi do .bss i dlaczego „static int d = 1;” przechodzi do .data ..? W moim rozumieniu obie są zmiennymi statycznymi, które zostały zainicjowane przez programistę. Co zatem robi różnicę? @Lundin
Soju T Varghese
2
@SojuTVarghese Ponieważ dane .bss są inicjowane na 0 jako blok; określone wartości, takie jak d = 1, muszą być zapisane we flashu.
tcrosley,
@SojuTVarghese Dodano pewne wyjaśnienia.
Lundin
@Lundin Ponadto, z twojego ostatniego przykładowego kodu, czy to oznacza, że ​​wszystkie zainicjowane wartości przechodzą w .text lub .rodata, a ich odpowiednie zmienne same w .bss lub .data? Jeśli tak, w jaki sposób zmienne i odpowiadające im wartości są zamapowane względem siebie (tj. Między segmentami .bss / .data i .text / .rodata)?
Soju T Varghese
@SojuTVarghese Nie, .datazwykle ma tak zwany adres ładowania we flashu, w którym przechowywane są wartości początkowe, i tak zwany adres wirtualny (nie tak naprawdę wirtualny w mikrokontrolerze) w pamięci RAM, gdzie zmienna jest przechowywana podczas wykonywania. Przed mainrozpoczęciem wartości początkowe są kopiowane z adresu ładowania na adres wirtualny. Nie musisz przechowywać zer, więc .bssnie musisz przechowywać ich początkowych wartości. sourceware.org/binutils/docs/ld/…
starblue
1

Chociaż dowolne dane mogą trafić do dowolnej pamięci wybranej przez programistę, ogólnie system działa najlepiej (i jest przeznaczony do użycia), gdy profil użytkowania danych jest dopasowany do profili odczytu / zapisu pamięci.

Na przykład kod programu to WFRM (napisz kilka, czytaj wiele), a jest ich dużo. Ładnie pasuje do FLASH. ROM OTOH jest W raz RM.

Stos i stos są małe, z dużą ilością odczytów i zapisów. To najlepiej pasowałoby do pamięci RAM.

Pamięć EEPROM nie pasuje dobrze do żadnego z tych zastosowań, ale pasuje do profilu niewielkich ilości danych trwających między uruchomieniami, więc dane inicjalizacji specyficzne dla użytkownika i być może rejestrowanie wyników.

Neil_UK
źródło