Jak krok po kroku uruchamia się i uruchamia mikrokontroler?


17

Kiedy kod C jest zapisywany, kompilowany i przesyłany do mikrokontrolera, mikrokontroler zaczyna działać. Ale jeśli wykonamy ten proces przesyłania i uruchamiania krok po kroku w zwolnionym tempie, mam pewne wątpliwości co do tego, co faktycznie dzieje się w MCU (pamięć, procesor, bootloader). Oto (najprawdopodobniej nie tak) to, na co odpowiedziałbym, gdyby ktoś mnie zapytał:

  1. Skompilowany kod binarny zapisywany jest na flash ROM (lub EEPROM) przez USB
  2. Bootloader kopiuje część tego kodu do pamięci RAM. Jeśli to prawda, to w jaki sposób moduł ładujący wie, co skopiować (którą część pamięci ROM skopiować do pamięci RAM)?
  3. Procesor rozpoczyna pobieranie instrukcji i danych kodu z pamięci ROM i RAM

Czy to źle?

Czy można podsumować ten proces uruchamiania i uruchamiania z pewnymi informacjami na temat interakcji pamięci, programu ładującego i procesora w tej fazie?

Znalazłem wiele podstawowych wyjaśnień dotyczących uruchamiania komputera za pomocą systemu BIOS. Ale utknąłem w procesie uruchamiania mikrokontrolera.

Odpowiedzi:


31

1) skompilowany plik binarny jest zapisywany na prom / flash tak. USB, szeregowy, i2c, jtag itp. Zależą od urządzenia, co jest obsługiwane przez to urządzenie, bez znaczenia dla zrozumienia procesu rozruchu.

2) Zazwyczaj nie jest to prawdą w przypadku mikrokontrolera, podstawowym przypadkiem użycia jest posiadanie instrukcji w rom / flash i danych w pamięci RAM. Bez względu na architekturę. w przypadku nie-mikrokontrolera, komputera, laptopa, serwera program jest kopiowany z nieulotnego (dysku) do pamięci RAM, a następnie uruchamiany z tego miejsca. Niektóre mikrokontrolery pozwalają również na użycie barana, nawet te, które twierdzą, że harvard jest niezgodny z definicją. Harvard nie ma nic, co uniemożliwiałoby odwzorowanie pamięci ram na stronę instrukcji, wystarczy mieć mechanizm, aby uzyskać instrukcje tam, gdy zasilanie jest już włączone (co narusza definicję, ale systemy Harvarda musiałyby to zrobić, aby były przydatne w innym przypadku niż jako mikrokontrolery).

3) w pewnym sensie.

Każda jednostka centralna „uruchamia się” w sposób deterministyczny, zgodnie z przeznaczeniem. Najczęstszym sposobem jest tablica wektorów, w której adres pierwszych instrukcji uruchamianych po włączeniu znajduje się w wektorze resetowania, adres odczytywany przez sprzęt, a następnie używa tego adresu do uruchomienia. Innym ogólnym sposobem jest uruchomienie procesora bez tablicy wektorów pod dobrze znanym adresem. Czasami układ ma „pasy”, niektóre piny, które można związać wysoko lub nisko przed zwolnieniem resetu, których logika używa do uruchamiania na różne sposoby. Musisz oddzielić procesor, rdzeń procesora od reszty systemu. Zrozum, jak działa procesor, a następnie zrozum, że projektanci układów / układów mają skonfigurowane dekodery adresów na zewnątrz procesora, dzięki czemu pewna część przestrzeni adresowej procesora komunikuje się z pamięcią flash, a niektóre z ram, a niektóre z urządzeniami peryferyjnymi (uart, i2c, spi, gpio itp.). Jeśli chcesz, możesz wziąć ten sam rdzeń procesora i owinąć go inaczej. To właśnie dostajesz, gdy kupujesz coś na bazie ręki lub mipsów. ramię i mips tworzą rdzenie procesora, które chipy kupują i owijają własne rzeczy, z różnych powodów nie robią tego kompatybilnym od marki do marki. Właśnie dlatego rzadko można zadać ogólne pytanie, jeśli chodzi o cokolwiek poza rdzeniem.

Mikrokontroler próbuje być układem na chipie, więc jego pamięć nieulotna (flash / rom), lotna (sram) i procesor są na tym samym chipie wraz z mieszaniną urządzeń peryferyjnych. Ale układ jest zaprojektowany wewnętrznie tak, że flash jest mapowany do przestrzeni adresowej procesora, która odpowiada charakterystyce rozruchowej tego procesora. Jeśli na przykład procesor ma wektor resetowania pod adresem 0xFFFC, to musi istnieć flash / rom, który odpowiada na adres, który możemy zaprogramować za pomocą 1), wraz z wystarczającą ilością flash / rom w przestrzeni adresowej dla przydatnych programów. Projektant układów może wybrać 0x1000 bajtów pamięci flash zaczynających się od 0xF000, aby spełnić te wymagania. I może umieścili pewną ilość pamięci RAM pod niższym adresem, a może 0x0000, a urządzenia peryferyjne gdzieś pośrodku.

Inna architektura procesora może zacząć działać pod adresem zero, więc musieliby zrobić coś przeciwnego, umieścić flash tak, aby odpowiadał zakresowi adresów wokół zera. powiedzmy na przykład 0x0000 do 0x0FFF. a następnie umieścić barana gdzie indziej.

Projektanci układów wiedzą, w jaki sposób procesor się uruchamia i umieścili tam nieulotną pamięć (flash / rom). Następnie do programistów należy napisanie kodu rozruchowego w celu dopasowania do dobrze znanego zachowania tego procesora. Musisz umieścić adres wektora resetowania w wektorze resetowania, a kod rozruchowy pod adresem zdefiniowanym w wektorze resetowania. Zestaw narzędzi może ci bardzo pomóc tutaj. czasami, szczególnie z idiomami wskazującymi i klikającymi lub innymi obszarami izolowanymi, mogą wykonać większość pracy dla ciebie, wystarczy zadzwonić do apis w języku wysokiego poziomu (C).

Jednak niezależnie od tego, czy zostało to zrobione, program załadowany do flash / rom musi dopasować się do zachowania procesora przez procesor. Przed częścią C programu main () i dalej, jeśli używasz main jako punktu wejścia, musisz coś zrobić. Programista AC zakłada, że ​​kiedy deklarują zmienną o wartości początkowej, oczekują, że to rzeczywiście zadziała. Cóż, zmienne inne niż stałe znajdują się w pamięci RAM, ale jeśli masz jedną z wartością początkową, ta wartość początkowa musi być w pamięci nieulotnej. Jest to więc segment .data, a bootstrap C musi skopiować pliki .data z pamięci flash do pamięci RAM (gdzie jest to zwykle określane przez łańcuch narzędzi). Zmienne globalne, które deklarujesz bez wartości początkowej, przyjmowane są jako zero przed uruchomieniem programu, chociaż tak naprawdę nie powinieneś tego zakładać i na szczęście niektóre kompilatory zaczynają ostrzegać przed niezainicjowanymi zmiennymi. Jest to segment .bss, a zera C bootstrap, które obecnie w pamięci RAM, treść, zera, nie muszą być przechowywane w nieulotnej pamięci, ale adres początkowy i ile. Znowu łańcuch narzędzi bardzo ci w tym pomaga. Wreszcie, absolutnym minimum jest konieczność ustawienia wskaźnika stosu, ponieważ programy C oczekują, że będą mogły mieć zmienne lokalne i wywoływać inne funkcje. Potem może zrobione są inne rzeczy specyficzne dla chipa, lub pozwolimy, aby reszta rzeczy specyficznych dla chipu wydarzyła się w C. nie musi być przechowywany w nieulotnej pamięci, ale adres początkowy i ile. Znowu łańcuch narzędzi bardzo ci w tym pomaga. Wreszcie, absolutnym minimum jest konieczność ustawienia wskaźnika stosu, ponieważ programy C oczekują, że będą mogły mieć zmienne lokalne i wywoływać inne funkcje. Potem może zrobione są inne rzeczy specyficzne dla chipa, lub pozwolimy, aby reszta rzeczy specyficznych dla chipu wydarzyła się w C. nie musi być przechowywany w nieulotnej pamięci, ale adres początkowy i ile. Znowu łańcuch narzędzi bardzo ci w tym pomaga. Wreszcie, absolutnym minimum jest konieczność ustawienia wskaźnika stosu, ponieważ programy C oczekują, że będą mogły mieć zmienne lokalne i wywoływać inne funkcje. Potem może zrobione są inne rzeczy specyficzne dla chipa, lub pozwolimy, aby reszta rzeczy specyficznych dla chipu wydarzyła się w C.

Rdzenie serii cortex-m z ramienia zrobią to za ciebie, wskaźnik stosu znajduje się w tabeli wektorów, istnieje wektor resetu wskazujący na kod, który ma być uruchomiony po resecie, tak aby cokolwiek innego, co musisz zrobić aby wygenerować tabelę wektorów (i tak zwykle używasz asm), możesz przejść do czystego C bez asm. teraz nie otrzymujesz kopii .data ani zerowania .bss, więc musisz to zrobić sam, jeśli chcesz spróbować przejść bez asm na coś opartego na korze-m. Większą cechą nie jest wektor resetu, ale wektory przerwania, w których sprzęt postępuje zgodnie z zalecaną przez uzbrojenie konwencją wywoływania C i zachowuje dla ciebie rejestry oraz używa poprawnego zwrotu dla tego wektora, dzięki czemu nie musisz owijać odpowiedniego asm wokół każdego modułu obsługi ( lub mieć konkretne wytyczne dla łańcucha narzędzi dla swojego celu, aby ten łańcuch narzędzi był dla Ciebie zawinięty).

Mogą to być na przykład układy specyficzne dla układów, mikrokontrolery są często używane w systemach opartych na bateriach, więc tak mało energii, więc niektóre wychodzą z resetowania przy wyłączonej większości urządzeń peryferyjnych, i trzeba włączyć każdy z tych podsystemów, aby można było z nich korzystać . Uarts, GPIO itp. Często używana jest niska prędkość zegara, prosto z kryształowego lub wewnętrznego oscylatora. Projekt systemu może wskazywać, że potrzebujesz szybszego zegara, więc zainicjuj go. Twój zegar może być za szybki na flash lub ram, więc być może trzeba było zmienić stan oczekiwania przed podniesieniem zegara. Może być konieczne skonfigurowanie interfejsu UART, USB lub innych interfejsów. wtedy Twoja aplikacja może to zrobić.

Komputer stacjonarny, laptop, serwer i mikrokontroler nie różnią się sposobem uruchamiania / działania. Tyle że nie są one w większości oparte na jednym układzie. Program bios jest często na osobnym chipie flash / rom od procesora. Chociaż ostatnio procesory x86 ściągają coraz więcej tego, co kiedyś służyło jako układy wspomagające do tego samego pakietu (kontrolery pcie itp.), Ale nadal masz większość swojego RAM-u i układu, ale wciąż jest to system i nadal działa dokładnie to samo na wysokim poziomie. Proces uruchamiania procesora jest dobrze znany, projektanci płyty umieszczają flash / rom w przestrzeni adresowej, w której uruchamia się procesor. ten program (część systemu BIOS na komputerze z procesorem x86) wykonuje wszystkie powyższe czynności, uruchamia różne urządzenia peryferyjne, inicjuje dram, wylicza magistrale pcie i tak dalej. Jest często dość konfigurowalny przez użytkownika na podstawie ustawień bios lub tego, co nazywaliśmy ustawieniami cmos, ponieważ w tym czasie była używana technologia. Nie ma znaczenia, istnieją ustawienia użytkownika, które można przejść i zmienić, aby poinformować kod rozruchowy systemu BIOS o tym, jak różnicować jego działanie.

różni ludzie będą używać innej terminologii. chip boot, czyli pierwszy uruchamiany kod. czasami nazywany bootstrap. bootloader ze słowem loader często oznacza, że ​​jeśli nie zrobisz nic, aby ingerować, jest to bootstrap, który zabiera cię od zwykłego uruchamiania do czegoś większego, aplikacji lub systemu operacyjnego. ale część programu ładującego oznacza, że ​​możesz przerwać proces rozruchu, a następnie załadować inne programy testowe. jeśli kiedykolwiek używałeś Uboota na przykład we wbudowanym systemie Linux, możesz nacisnąć klawisz i zatrzymać normalne uruchamianie, następnie możesz pobrać jądro testowe do pamięci RAM i uruchomić je zamiast tego, który jest flashowany, lub możesz pobrać swój własne programy lub możesz pobrać nowe jądro, a następnie poprosić bootloader o napisanie go do flashowania, aby przy następnym uruchomieniu uruchomił nowe rzeczy.

Jeśli chodzi o samą jednostkę centralną, procesor rdzeniowy, który nie zna pamięci RAM z pamięci flash z urządzeń peryferyjnych. Nie ma pojęcia o bootloaderze, systemie operacyjnym, aplikacji. Jest to tylko sekwencja instrukcji, które są podawane do procesora, który ma zostać wykonany. Są to terminy programowe służące do rozróżnienia różnych zadań programistycznych. Koncepcje oprogramowania od siebie nawzajem.

Niektóre mikrokontrolery mają oddzielny moduł ładujący dostarczony przez dostawcę układu w osobnej pamięci flash lub w oddzielnej strefie pamięci flash, której modyfikacja może być niemożliwa. W tym przypadku często występuje pin lub zestaw pinów (nazywam je paskami), które jeśli związasz je wysoko lub nisko przed zwolnieniem resetowania, mówisz logice i / lub temu programowi ładującemu, co ma robić, na przykład jedna kombinacja pasków może powiedz czipowi, aby uruchomił ten program ładujący i poczekaj na UART na zaprogramowanie danych we flashu. Ustaw pasy w inny sposób, a program nie uruchamia bootloadera dostawcy układu, umożliwiając programowanie układu w terenie lub odzyskanie po awarii programu. Czasami jest to czysta logika, która pozwala zaprogramować flash. Obecnie jest to dość powszechne,

Powodem, dla którego większość mikrokontrolerów ma znacznie więcej pamięci flash niż pamięci RAM, jest to, że podstawowym przypadkiem użycia jest uruchomienie programu bezpośrednio z pamięci flash i tylko tyle pamięci RAM, aby pokryć stos i zmienne. Chociaż w niektórych przypadkach możesz uruchamiać programy z pamięci RAM, które musisz poprawnie skompilować i zapisać we flashu, a następnie skopiować przed wywołaniem.

EDYTOWAĆ

flash.s

.cpu cortex-m0
.thumb

.thumb_func
.global _start
_start:
stacktop: .word 0x20001000
.word reset
.word hang
.word hang
.word hang

.thumb_func
reset:
    bl notmain
    b hang

.thumb_func
hang:   b .

notmain.c

int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;

    return(0);
}

flash.ld

MEMORY
{
    bob : ORIGIN = 0x00000000, LENGTH = 0x1000
    ted : ORIGIN = 0x20000000, LENGTH = 0x1000
}
SECTIONS
{
    .text : { *(.text*) } > bob
    .rodata : { *(.rodata*) } > bob
    .bss : { *(.bss*) } > ted
    .data : { *(.bss*) } > ted AT > bob
}

To jest przykład dla kory-m0, kora-ms działa tak samo, jak w tym przykładzie. Konkretny układ, w tym przykładzie, ma flash aplikacji pod adresem 0x00000000 w przestrzeni adresowej uzbrojenia i ram pod 0x20000000.

Sposób, w jaki uruchamia się kora-m, to 32-bitowe słowo pod adresem 0x0000 jest adresem inicjującym wskaźnik stosu. Nie potrzebuję dużo stosu dla tego przykładu, więc 0x20001000 wystarczy, oczywiście pod tym adresem musi być ram (sposób, w jaki ramię naciska, czy najpierw odejmuje, a następnie naciska, więc jeśli ustawisz 0x20001000, pierwszy element na stosie ma adres 0x2000FFFC nie musisz używać 0x2000FFFC). 32-bitowe słowo o adresie 0x0004 to adres procedury obsługi resetowania, w zasadzie pierwszy kod uruchamiany po resecie. Potem jest więcej programów obsługi przerwań i zdarzeń, które są specyficzne dla tego rdzenia i układu cortex m, być może aż 128 lub 256, jeśli ich nie użyjesz, to nie musisz ustawiać dla nich tabeli, wrzuciłem kilka dla demonstracji cele.

W tym przykładzie nie muszę zajmować się danymi .data ani .bss, ponieważ wiem, że w tych segmentach już nic nie ma, patrząc na kod. Gdyby tak było, poradziłbym sobie z tym i zrobię to za chwilę.

Tak więc stos jest skonfigurowany, sprawdzany, dbane o dane, sprawdzane, .bss, sprawdzanie, więc C bootstrap jest gotowy, może rozgałęzić się do funkcji wprowadzania dla C. Ponieważ niektóre kompilatory dodadzą dodatkowe śmieci, jeśli zobaczą funkcję main () i w drodze do main, nie używam tej dokładnej nazwy, użyłem notmain () jako mojego punktu wejścia C. Zatem funkcja obsługi resetowania wywołuje notmain (), a jeśli / kiedy notmain () zwraca, przechodzi w tryb zawieszenia, który jest tylko nieskończoną pętlą, być może źle nazwaną.

Mocno wierzę w opanowanie narzędzi, wielu ludzi tego nie robi, ale przekonasz się, że każdy deweloper bare metal robi swoje / swoje własne rzeczy, ze względu na prawie całkowitą swobodę, nie tak ograniczoną jak tworzenie aplikacji lub stron internetowych . Znowu robią swoje. Wolę mieć własny kod bootstrap i skrypt linkera. Inni polegają na łańcuchu narzędzi lub bawią się w piaskownicy dostawcy, w której większość pracy jest wykonywana przez kogoś innego (a jeśli coś się psuje, jesteś w świecie bólu, a nagie elementy metalowe łamią się często i dramatycznie).

A więc, kompilując, kompilując i łącząc się z narzędziami GNU, otrzymuję:

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

Skąd więc bootloader wie, gdzie są rzeczy. Ponieważ kompilator wykonał pracę. W pierwszym przypadku asembler wygenerował kod dla flash.s, robiąc to, wie gdzie są etykiety (etykiety to po prostu adresy takie jak nazwy funkcji lub nazwy zmiennych itp.), Więc nie musiałem liczyć bajtów i wypełniać wektora tabeli ręcznie, użyłem nazwy etykiety i asembler zrobił to za mnie. Teraz pytasz, czy reset to adres 0x14, dlaczego asembler umieścił 0x15 w tabeli wektorów. To jest kora-m, która uruchamia się i działa tylko w trybie kciuka. Z ARM, gdy rozgałęziasz się na adres, jeśli rozgałęziasz się do trybu kciuka, lsbit musi być ustawiony, jeśli tryb uzbrojenia następnie resetuje. Więc zawsze potrzebujesz tego zestawu bitów. Znam narzędzia i umieszczając .thumb_func przed etykietą, jeśli ta etykieta jest używana w takiej postaci, w jakiej znajduje się ona w tabeli wektorowej lub do rozgałęziania się na czymkolwiek. Toolchain wie, jak ustawić lsbit. Więc ma tutaj 0x14 | 1 = 0x15. Podobnie do zawieszenia. Teraz deasembler nie wyświetla 0x1D dla wywołania notmain (), ale nie martw się, narzędzia poprawnie zbudowały instrukcję.

Teraz, gdy ten kod jest najważniejszy, te lokalne zmienne nie są używane, są martwym kodem. Kompilator nawet komentuje ten fakt, mówiąc, że y jest ustawione, ale nie jest używane.

Zwróć uwagę na przestrzeń adresową, wszystkie te rzeczy zaczynają się od adresu 0x0000 i idą stamtąd, więc tablica wektorów jest poprawnie umieszczona, tekst .tekst lub przestrzeń programowa również są odpowiednio umieszczone, jak dostałem flash.s przed kodem notmain.c przez znając narzędzia, powszechnym błędem jest niedokładne zrobienie tego, awaria i mocne spalenie. IMO musisz zdemontować, aby upewnić się, że rzeczy są umieszczone tuż przed uruchomieniem za pierwszym razem, gdy już znajdziesz rzeczy we właściwym miejscu, niekoniecznie musisz to sprawdzać za każdym razem. Tylko dla nowych projektów lub jeśli się zawieszają.

Teraz czymś, co zaskakuje niektórych ludzi, jest to, że nie ma żadnego powodu, aby oczekiwać, że dwa kompilatory wygenerują tę samą moc wyjściową z tych samych danych wejściowych. Lub nawet ten sam kompilator z różnymi ustawieniami. Używając clanga, kompilatora llvm otrzymuję te dwa wyniki z optymalizacją i bez

zoptymalizowany dla llvm / clang

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   2000        movs    r0, #0
  1e:   4770        bx  lr

nie zoptymalizowany

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f802   bl  1c <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <notmain>:
  1c:   b082        sub sp, #8
  1e:   2001        movs    r0, #1
  20:   9001        str r0, [sp, #4]
  22:   2002        movs    r0, #2
  24:   9000        str r0, [sp, #0]
  26:   2000        movs    r0, #0
  28:   b002        add sp, #8
  2a:   4770        bx  lr

więc to kłamstwo, kompilator zoptymalizował dodanie, ale przydzielił zmienne dwa elementy na stosie, ponieważ są to zmienne lokalne, które znajdują się w pamięci RAM, ale na stosie nie mają ustalonych adresów, zobaczą za pomocą globałów, że zmiany. Ale kompilator zdał sobie sprawę, że może obliczyć y w czasie kompilacji i nie było powodu, aby go obliczać w czasie wykonywania, więc po prostu umieścił 1 w przestrzeni stosu przydzielonej dla x i 2 dla przestrzeni stosu przydzielonej dla y. kompilator „przydziela” to miejsce wewnętrznymi tabelami Deklaruję stos plus 0 dla zmiennej y i stos plus 4 dla zmiennej x. kompilator może robić, co chce, o ile implementowany kod jest zgodny ze standardem C lub oczekiwaniami programisty C. Nie ma powodu, dla którego kompilator musi pozostawić x na stosie + 4 na czas działania funkcji,

Jeśli dodam manekina funkcji w asemblerze

.thumb_func
.globl dummy
dummy:
    bx lr

i nazwij to

void dummy ( unsigned int );
int notmain ( void )
{
    unsigned int x=1;
    unsigned int y;
    y = x + 1;
    dummy(y);
    return(0);
}

wyjście zmienia się

00000000 <_start>:
   0:   20001000    andcs   r1, r0, r0
   4:   00000015    andeq   r0, r0, r5, lsl r0
   8:   0000001b    andeq   r0, r0, fp, lsl r0
   c:   0000001b    andeq   r0, r0, fp, lsl r0
  10:   0000001b    andeq   r0, r0, fp, lsl r0

00000014 <reset>:
  14:   f000 f804   bl  20 <notmain>
  18:   e7ff        b.n 1a <hang>

0000001a <hang>:
  1a:   e7fe        b.n 1a <hang>

0000001c <dummy>:
  1c:   4770        bx  lr
    ...

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

teraz, gdy mamy zagnieżdżone funkcje, funkcja notmain musi zachować swój adres zwrotny, aby mogła zablokować adres zwrotny dla zagnieżdżonego wywołania. dzieje się tak, ponieważ ramię używa rejestru do zwrotów, jeśli użyłby stosu, jak powiedzmy x86 lub kilku innych… nadal używałby stosu, ale inaczej. Teraz pytasz, dlaczego to popchnęło R4? No cóż, konwencja wywoływania niedawno się zmieniła, aby zachować wyrównanie stosu na granicach 64-bitowych (dwóch słów) zamiast 32-bitowych, granic jednego słowa. Muszą więc wcisnąć coś, aby utrzymać wyrównany stos, więc kompilator arbitralnie wybrał r4 z jakiegoś powodu, nieważne, dlaczego. Wpadnięcie do r4 byłoby błędem, jednak zgodnie z konwencją wywoływania dla tego celu, nie blokujemy r4 przy wywołaniu funkcji, możemy spychać r0 do r3. r0 jest wartością zwracaną. Wygląda na to, że optymalizuje ogon,

Ale widzimy, że matematyka xiy jest zoptymalizowana do zakodowanej na stałe wartości 2 przekazywanej do funkcji fikcyjnej (manekin został specjalnie zakodowany w osobnym pliku, w tym przypadku asm), dzięki czemu kompilator nie zoptymalizowałby całkowicie wywołania funkcji, gdybym miał funkcję fikcyjną, która po prostu wróciła do C w notmain.c, optymalizator usunąłby wywołanie funkcji x, y i fikcyjnej, ponieważ wszystkie są martwe / bezużyteczne.

Zauważ też, że ponieważ kod flash.s się powiększył, notmain jest już inny, a łańcuch narzędzi zajął się łataniem dla nas wszystkich adresów, więc nie musimy tego robić ręcznie.

niezoptymalizowany brzęk w celach informacyjnych

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   b082        sub sp, #8
  26:   2001        movs    r0, #1
  28:   9001        str r0, [sp, #4]
  2a:   2002        movs    r0, #2
  2c:   9000        str r0, [sp, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   b002        add sp, #8
  36:   bd80        pop {r7, pc}

zoptymalizowany klang

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   2002        movs    r0, #2
  26:   f7ff fff9   bl  1c <dummy>
  2a:   2000        movs    r0, #0
  2c:   bd80        pop {r7, pc}

autor kompilatora wybrał użycie r7 jako zmiennej zastępczej, aby wyrównać stos, również tworzy wskaźnik ramki za pomocą r7, chociaż nie ma nic w ramce stosu. w zasadzie instrukcja mogła zostać zoptymalizowana. ale użył pop do zwrócenia nie trzech instrukcji, to pewnie było na mnie, założę się, że mógłbym zmusić gcc do zrobienia tego z odpowiednimi opcjami wiersza poleceń (określając procesor).

powinno to przede wszystkim odpowiedzieć na pozostałe pytania

void dummy ( unsigned int );
unsigned int x=1;
unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

Mam teraz globale. więc wpisują dane .data lub .bss, jeśli nie zostaną zoptymalizowane.

zanim spojrzymy na końcowy wynik, spójrzmy na obiekt pośredni

00000000 <notmain>:
   0:   b510        push    {r4, lr}
   2:   4b05        ldr r3, [pc, #20]   ; (18 <notmain+0x18>)
   4:   6818        ldr r0, [r3, #0]
   6:   4b05        ldr r3, [pc, #20]   ; (1c <notmain+0x1c>)
   8:   3001        adds    r0, #1
   a:   6018        str r0, [r3, #0]
   c:   f7ff fffe   bl  0 <dummy>
  10:   2000        movs    r0, #0
  12:   bc10        pop {r4}
  14:   bc02        pop {r1}
  16:   4708        bx  r1
    ...

Disassembly of section .data:
00000000 <x>:
   0:   00000001    andeq   r0, r0, r1

teraz brakuje w tym informacji, ale daje wyobrażenie o tym, co się dzieje, linker to ten, który bierze obiekty i łączy je wraz z informacjami podanymi (w tym przypadku flash.ld), które mówią, gdzie .tekst i. dane i takie idzie. kompilator nie zna takich rzeczy, może skupić się tylko na kodzie, który jest prezentowany, każdy zewnętrzny musi pozostawić dziurę, aby linker mógł wypełnić połączenie. Wszelkie dane muszą pozostawić sposób na połączenie tych rzeczy, więc adresy dla wszystkiego są tutaj zerowe, ponieważ kompilator i ten deasembler nie wiedzą. istnieją inne informacje nie pokazane tutaj, których linker używa do umieszczania rzeczy. kod tutaj jest wystarczająco niezależny od pozycji, aby linker mógł wykonać swoją pracę.

wówczas widzimy co najmniej demontaż połączonego wyjścia

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   4b05        ldr r3, [pc, #20]   ; (38 <notmain+0x18>)
  24:   6818        ldr r0, [r3, #0]
  26:   4b05        ldr r3, [pc, #20]   ; (3c <notmain+0x1c>)
  28:   3001        adds    r0, #1
  2a:   6018        str r0, [r3, #0]
  2c:   f7ff fff6   bl  1c <dummy>
  30:   2000        movs    r0, #0
  32:   bc10        pop {r4}
  34:   bc02        pop {r1}
  36:   4708        bx  r1
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

Disassembly of section .bss:

20000000 <y>:
20000000:   00000000    andeq   r0, r0, r0

Disassembly of section .data:

20000004 <x>:
20000004:   00000001    andeq   r0, r0, r1

kompilator w zasadzie poprosił o dwie 32-bitowe zmienne w pamięci RAM. Jeden jest w .bss, ponieważ nie zainicjowałem go, więc przyjmuje się, że inicjuje jako zero. drugi to .data, ponieważ zainicjowałem go podczas deklaracji.

Ponieważ są to zmienne globalne, zakłada się, że inne funkcje mogą je modyfikować. kompilator nie przyjmuje żadnych założeń co do tego, kiedy można wywołać notmain, więc nie może zoptymalizować za pomocą tego, co widzi, matematyki y = x + 1, więc musi to zrobić. Musi odczytać z pamięci ram dwie zmienne dodać je i zapisać.

Teraz wyraźnie ten kod nie zadziała. Dlaczego? ponieważ mój bootstrap, jak pokazano tutaj, nie przygotowuje pamięci RAM przed wywołaniem notmain, więc cokolwiek śmieci znajdowało się w 0x20000000 i 0x20000004, kiedy chip się obudził, to będzie użyte dla y i x.

Nie zamierzam tego tutaj pokazywać. możesz przeczytać moją jeszcze dłuższą wędrówkę po .data i .bss i dlaczego nigdy nie potrzebuję ich w moim nagim metalowym kodzie, ale jeśli czujesz, że musisz i chcesz opanować narzędzia, zamiast mieć nadzieję, że ktoś zrobi to dobrze ... .

https://github.com/dwelch67/raspberrypi/tree/master/bssdata

skrypty linkera i bootstrapy są nieco specyficzne dla kompilatora, więc wszystko, czego dowiesz się o jednej wersji jednego kompilatora, może zostać rzucone na następną wersję lub na innym kompilatorze, co jest kolejnym powodem, dla którego nie wkładam ogromnego wysiłku w przygotowanie .data i .bss żeby być tak leniwym:

unsigned int x=1;

Wolałbym to zrobić

unsigned int x;
...
x = 1;

i pozwól kompilatorowi umieścić go w tekście. Czasami oszczędza w ten sposób flash, a czasem pali więcej. Zdecydowanie łatwiej jest programować i przenosić z wersji Toolchain lub jednego kompilatora do drugiego. Znacznie bardziej niezawodny, mniej podatny na błędy. Tak, nie jest zgodny ze standardem C.

a co jeśli zrobimy te statyczne globale?

void dummy ( unsigned int );
static unsigned int x=1;
static unsigned int y;
int notmain ( void )
{
    y = x + 1;
    dummy(y);
    return(0);
}

dobrze

00000020 <notmain>:
  20:   b510        push    {r4, lr}
  22:   2002        movs    r0, #2
  24:   f7ff fffa   bl  1c <dummy>
  28:   2000        movs    r0, #0
  2a:   bc10        pop {r4}
  2c:   bc02        pop {r1}
  2e:   4708        bx  r1

oczywiście te zmienne nie mogą być modyfikowane przez inny kod, więc kompilator może teraz w czasie kompilacji zoptymalizować martwy kod, tak jak wcześniej.

niezoptymalizowany

00000020 <notmain>:
  20:   b580        push    {r7, lr}
  22:   af00        add r7, sp, #0
  24:   4804        ldr r0, [pc, #16]   ; (38 <notmain+0x18>)
  26:   6800        ldr r0, [r0, #0]
  28:   1c40        adds    r0, r0, #1
  2a:   4904        ldr r1, [pc, #16]   ; (3c <notmain+0x1c>)
  2c:   6008        str r0, [r1, #0]
  2e:   f7ff fff5   bl  1c <dummy>
  32:   2000        movs    r0, #0
  34:   bd80        pop {r7, pc}
  36:   46c0        nop         ; (mov r8, r8)
  38:   20000004    andcs   r0, r0, r4
  3c:   20000000    andcs   r0, r0, r0

ten kompilator, który używał stosu dla lokalnych, teraz używa ram dla globałów i ten kod jak napisany jest zepsuty, ponieważ nie obsługiwałem poprawnie .data ani .bss.

i ostatnia rzecz, której nie możemy zobaczyć podczas demontażu.

:1000000000100020150000001B0000001B00000075
:100010001B00000000F004F8FFE7FEE77047000057
:1000200080B500AF04480068401C04490860FFF731
:10003000F5FF002080BDC046040000200000002025
:08004000E0FFFF7F010000005A
:0400480078563412A0
:00000001FF

Zmieniłem x, aby był wstępnie inicjowany z 0x12345678. Mój skrypt linkera (to jest dla GNU LD) ma to coś w stylu Boba. to mówi linkerowi, że chcę, aby ostatnie miejsce znajdowało się w przestrzeni adresowej Ted, ale przechowuj je w pliku binarnym w przestrzeni adresowej Ted, a ktoś przeniesie je dla ciebie. I widzimy, że tak się stało. to jest format hex hex. i możemy zobaczyć 0x12345678

:0400480078563412A0

znajduje się w przestrzeni adresowej flash pliku binarnego.

readelf również to pokazuje

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  EXIDX          0x010040 0x00000040 0x00000040 0x00008 0x00008 R   0x4
  LOAD           0x010000 0x00000000 0x00000000 0x00048 0x00048 R E 0x10000
  LOAD           0x020004 0x20000004 0x00000048 0x00004 0x00004 RW  0x10000
  LOAD           0x030000 0x20000000 0x20000000 0x00000 0x00004 RW  0x10000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RWE 0x10

linia LOAD, w której adres wirtualny to 0x20000004, a fizyczny to 0x48


na samym początku mam dwa rozmyte zdjęcia rzeczy:
user16307

1.) „podstawowym zastosowaniem jest posiadanie instrukcji w rom / flash i danych w ram.” kiedy mówisz „dane w pamięci RAM tutaj”, masz na myśli dane zapisane w toku programu. czy zawierasz również zainicjowane dane. mam na myśli, że kiedy przesyłamy kod do pamięci ROM, w naszym kodzie są już zainicjowane dane. na przykład w naszym oode, jeśli mamy: int x = 1; int y = x +1; powyższy kod zawiera instrukcje i dane początkowe, które wynoszą 1. (x = 1). czy dane te są również kopiowane do pamięci RAM lub pozostają tylko w pamięci ROM.
user16307

13
hah, teraz znam limit znaków dla odpowiedzi wymiany stosów!
old_timer

2
Powinieneś napisać książkę wyjaśniającą takie koncepcje początkującym. „Mam zillion przykładów w github” - Czy można podzielić się kilkoma przykładami
AkshayImmanuelD

1
Właśnie zrobiłem. Nie jest to nic przydatnego, ale wciąż jest to przykład kodu dla mikrokontrolera. I umieściłem link github, z którego można znaleźć wszystko, co udostępniłem, dobre, złe lub inne.
old_timer

8

Ta odpowiedź skupi się bardziej na procesie rozruchu. Po pierwsze, korekta - zapisuje do flashowania po zakończeniu MCU (lub przynajmniej jego części). Na niektórych MCU (zwykle bardziej zaawansowanych) sam procesor może obsługiwać porty szeregowe i zapisywać do rejestrów flash. Zatem pisanie i wykonywanie programu to różne procesy. Zakładam, że program został już napisany do flashowania.

Oto podstawowy proces rozruchu. Wymienię kilka typowych odmian, ale przede wszystkim utrzymuję to w prostocie.

  1. Reset: Istnieją dwa podstawowe typy. Pierwszym z nich jest reset po włączeniu zasilania, który jest generowany wewnętrznie, gdy napięcie zasilania rośnie. Drugi to zewnętrzny przełącznik pinów. Niezależnie od tego, reset wymusza wszystkie przerzutniki w MCU do z góry określonego stanu.

  2. Dodatkowa inicjalizacja sprzętowa: Zanim procesor zacznie działać, może być potrzebny dodatkowy czas i / lub cykle zegara. Na przykład w jednostkach MCU TI, nad którymi pracuję, ładowany jest wewnętrzny łańcuch skanowania konfiguracji.

  3. Uruchomienie procesora : CPU pobiera pierwszą instrukcję ze specjalnego adresu zwanego wektorem resetowania. Adres ten jest określany podczas projektowania procesora. Stamtąd jest to po prostu normalne wykonanie programu.

    Procesor powtarza trzy podstawowe kroki w kółko:

    • Pobieranie: Przeczytaj instrukcję (wartość 8, 16 lub 32-bitową) z adresu zapisanego w rejestrze licznika programu (PC), a następnie zwiększ komputer.
    • Dekodowanie: Konwertuje instrukcję binarną na zbiór wartości wewnętrznych sygnałów sterujących CPU.
    • Wykonaj: Wykonaj instrukcję - dodaj dwa rejestry, odczyt lub zapis do pamięci, rozgałęzienie (zmień komputer) lub cokolwiek innego.

    (Jest to w rzeczywistości bardziej skomplikowane. Procesory są zwykle przetwarzane potokowo , co oznacza, że ​​mogą wykonywać każdy z powyższych kroków na różnych instrukcjach jednocześnie. Każdy z powyższych kroków może mieć wiele etapów potoku. Następnie są równoległe potoki, przewidywanie rozgałęzień , i wszystkie wymyślne elementy architektury komputera, które sprawiają, że te procesory Intela wymagają miliarda tranzystorów.)

    Być może zastanawiasz się, jak działa pobieranie. Procesor ma magistralę składającą się z sygnałów adresowych (wyjściowych) i danych (wejściowych / wyjściowych). Aby wykonać pobieranie, CPU ustawia wiersze adresu na wartość w liczniku programu, a następnie wysyła zegar przez magistralę. Adres jest dekodowany w celu włączenia pamięci. Pamięć odbiera zegar i adres i umieszcza wartość pod tym adresem w liniach danych. CPU otrzymuje tę wartość. Odczyty i zapisy danych są podobne, z tym wyjątkiem, że adres pochodzi z instrukcji lub wartości w rejestrze ogólnego przeznaczenia, a nie na komputerze.

    Procesory z architekturą von Neumann mają pojedynczą magistralę, która służy zarówno do instrukcji, jak i do danych. Procesory z architekturą Harvarda mają jedną magistralę dla instrukcji i jedną dla danych. W prawdziwych MCU obie te magistrale mogą być podłączone do tych samych pamięci, więc często (ale nie zawsze) jest to coś, o co nie musisz się martwić.

    Powrót do procesu rozruchu. Po zresetowaniu komputer jest ładowany wartością początkową zwaną wektorem resetowania. Można to wbudować w sprzęt lub (w procesorach ARM Cortex-M) można automatycznie odczytać z pamięci. CPU pobiera instrukcję z wektora resetowania i rozpoczyna wykonywanie pętli przez powyższe kroki. W tym momencie procesor działa normalnie.

  4. Program ładujący: często trzeba wykonać konfigurację niskiego poziomu, aby reszta MCU mogła działać. Może to obejmować czyszczenie pamięci RAM i ładowanie fabrycznych ustawień wyposażenia dla komponentów analogowych. Może również istnieć opcja ładowania kodu z zewnętrznego źródła, takiego jak port szeregowy lub pamięć zewnętrzna. MCU może zawierać boot ROM, który zawiera mały program do robienia tych rzeczy. W takim przypadku wektor resetowania procesora wskazuje na przestrzeń adresową pamięci ROM rozruchu. Jest to w zasadzie normalny kod, który został po prostu dostarczony przez producenta, więc nie musisz go pisać sam. :-) W komputerze PC BIOS jest odpowiednikiem rozruchowej pamięci ROM.

  5. Konfiguracja środowiska C: C oczekuje na stos (obszar pamięci RAM do przechowywania stanu podczas wywołań funkcji) i zainicjowane lokalizacje pamięci dla zmiennych globalnych. Są to sekcje .stack, .data i .bss, o których mówi Dwelch. Na zainicjowanych zmiennych globalnych na tym etapie kopiowane są wartości inicjalizacji z pamięci flash do pamięci RAM. Niezainicjowane zmienne globalne mają adresy RAM, które są blisko siebie, więc cały blok pamięci można bardzo łatwo zainicjować na zero. Stosu nie trzeba inicjować (chociaż może być) - wszystko, co naprawdę musisz zrobić, to ustawić rejestr wskaźnika stosu procesora, aby wskazywał na przypisany region w pamięci RAM.

  6. Główna funkcja : Po skonfigurowaniu środowiska C moduł ładujący C wywołuje funkcję main (). Tam zwykle zaczyna się kod aplikacji. Jeśli chcesz, możesz pominąć standardową bibliotekę, pominąć konfigurację środowiska C i napisać własny kod, aby wywołać main (). Niektóre jednostki MCU mogą pozwolić Ci napisać własny moduł ładujący, a następnie możesz samodzielnie przeprowadzić całą konfigurację niskiego poziomu.

Różne rzeczy: wiele MCU pozwoli ci na wykonanie kodu z pamięci RAM dla lepszej wydajności. Jest to zwykle konfigurowane w konfiguracji linkera. Linker przypisuje dwa adresy do każdej funkcji - adres ładowania , w którym kod jest najpierw zapisywany (zwykle flash), oraz adres uruchomienia , który jest adresem ładowanym do komputera w celu wykonania funkcji (flash lub RAM). Aby wykonać kod z pamięci RAM, należy napisać kod, aby procesor skopiował kod funkcji z adresu ładowania we flashu na adres uruchomienia w pamięci RAM, a następnie wywołuje funkcję pod adresem uruchomienia. Linker może zdefiniować zmienne globalne, aby w tym pomóc. Ale wykonywanie kodu poza pamięcią RAM jest opcjonalne w MCU. Zwykle robisz to tylko wtedy, gdy naprawdę potrzebujesz wysokiej wydajności lub jeśli chcesz ponownie napisać flash.


1

Twoje podsumowanie jest w przybliżeniu prawidłowe dla architektury Von Neumann . Kod początkowy jest zwykle ładowany do pamięci RAM za pośrednictwem programu ładującego, ale nie (zwykle) program ładujący, do którego termin ten często odnosi się. Zwykle jest to zachowanie „upieczone w krzemie”. Wykonywanie kodu w tej architekturze często wymaga predykcyjnego buforowania instrukcji z pamięci ROM w taki sposób, że procesor maksymalizuje czas wykonywania kodu i nie czeka na załadowanie kodu do pamięci RAM. Czytałem gdzieś, że MSP430 jest przykładem tej architektury.

W Harvard Architecture instrukcje są wykonywane bezpośrednio z pamięci ROM, a dostęp do pamięci danych (RAM) jest uzyskiwany przez oddzielną magistralę. W tej architekturze kod zaczyna się po prostu od wektora resetowania. PIC24 i dsPIC33 są przykładami tej architektury.

Jeśli chodzi o rzeczywiste przerzucanie bitów, które uruchamiają te procesy, mogą one różnić się w zależności od urządzenia i mogą obejmować debuggery, JTAG, zastrzeżone metody itp.


Ale szybko pomijasz niektóre punkty. Weźmy to w zwolnionym tempie. Powiedzmy, że kod binarny „pierwszy” jest zapisywany na ROM. Ok .. Po tym piszesz „Dostęp do pamięci danych” .... Ale skąd dane „do pamięci RAM” pochodzą po uruchomieniu? Czy znowu pochodzi z ROM? A jeśli tak, to w jaki sposób moduł ładujący wie, która część pamięci ROM zostanie zapisana w pamięci RAM na początku?
user16307

Masz rację, dużo przeskoczyłem. Inni faceci mają lepsze odpowiedzi. Cieszę się, że masz to, czego szukałeś.
nieznacznie
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.