Co dzieje się po zakończeniu programu osadzonego?

29

Co dzieje się we wbudowanym procesorze, gdy wykonanie osiąga tę końcową returninstrukcję Czy wszystko po prostu się zawiesza; zużycie energii itp., z jednym długim wiecznym NOP na niebie? lub czy NOP są ciągle wykonywane, czy procesor całkowicie się wyłączy?

Jednym z powodów, dla których pytam, jest to, czy zastanawiam się, czy procesor musi się wyłączyć, zanim skończy wykonywanie, a jeśli tak, to w jaki sposób kiedykolwiek zakończy wykonywanie, jeśli wcześniej się wyłączył.

Toby
źródło
22
To zależy od twoich przekonań. Niektórzy twierdzą, że odrodzi się.
Telaclavo
9
Czy to pocisk?
Lee Kowalkowski
6
niektóre systemy obsługują instrukcję HCF (Halt and Catch Fire) . :)
Stefan Paul Noack,
1
przejdzie do rutyny samozniszczenia

Odpowiedzi:

41

To pytanie zawsze zadawał mi mój tata. „ Dlaczego po prostu nie wykonuje wszystkich instrukcji i nie zatrzymuje się na końcu?

Rzućmy okiem na patologiczny przykład. Poniższy kod został skompilowany w kompilatorze C18 Microchip dla PIC18:

void main(void)
{

}

Tworzy następujące dane wyjściowe asemblera:

addr    opco     instruction
----    ----     -----------
0000    EF63     GOTO 0xc6
0002    F000     NOP
0004    0012     RETURN 0
.
. some instructions removed for brevity
.
00C6    EE15     LFSR 0x1, 0x500
00C8    F000     NOP
00CA    EE25     LFSR 0x2, 0x500
00CC    F000     NOP
.
. some instructions removed for brevity
.
00D6    EC72     CALL 0xe4, 0            // Call the initialisation code
00D8    F000     NOP                     //  
00DA    EC71     CALL 0xe2, 0            // Here we call main()
00DC    F000     NOP                     // 
00DE    D7FB     BRA 0xd6                // Jump back to address 00D6
.
. some instructions removed for brevity
.

00E2    0012     RETURN 0                // This is main()

00E4    0012     RETURN 0                // This is the initialisation code

Jak widać, wywoływana jest funkcja main (), która na końcu zawiera instrukcję return, chociaż nie umieściliśmy jej tam osobiście. Kiedy main powraca, CPU wykonuje następną instrukcję, która jest po prostu GOTO, aby powrócić do początku kodu. main () jest po prostu wywoływany w kółko.

Powiedziawszy to, ludzie nie robią tego zwykle. Nigdy nie napisałem żadnego wbudowanego kodu, który pozwalałby main () na takie wyjście. Przeważnie mój kod wyglądałby mniej więcej tak:

void main(void)
{
    while(1)
    {
        wait_timer();
        do_some_task();
    }    
}

Więc normalnie nigdy nie pozwoliłbym main () wyjść.

„OK ok” mówisz. Wszystko to jest bardzo interesujące, ponieważ kompilator upewnia się, że nigdy nie ma ostatniego zwrotu. Ale co się stanie, jeśli wymuszimy ten problem? Co jeśli ręcznie zakoduję asembler i nie cofnę skoku na początek?

Oczywiście procesor wykonywałby kolejne instrukcje. Te wyglądałyby mniej więcej tak:

addr    opco     instruction
----    ----     -----------
00E6    FFFF     NOP
00E8    FFFF     NOP
00EA    FFFF     NOP
00EB    FFFF     NOP
.
. some instructions removed for brevity
.
7EE8    FFFF     NOP
7FFA    FFFF     NOP
7FFC    FFFF     NOP
7FFE    FFFF     NOP

Następny adres pamięci po ostatniej instrukcji w main () jest pusty. W mikrokontrolerze z pamięcią FLASH pusta instrukcja zawiera wartość 0xFFFF. Przynajmniej na PIC ten kod operacji jest interpretowany jako „nop” lub „brak operacji”. Po prostu nic nie robi. Procesor kontynuowałby wykonywanie tych zer do końca pamięci.

Co potem?

W ostatniej instrukcji wskaźnikiem instrukcji procesora jest 0x7FFe. Gdy CPU dodaje 2 do wskaźnika instrukcji, otrzymuje 0x8000, co jest uważane za przepełnienie PIC z jedynie 32k FLASH, więc zawija się z powrotem do 0x0000, a CPU szczęśliwie kontynuuje wykonywanie instrukcji z powrotem na początku kodu , tak jakby został zresetowany.


Pytałeś także o potrzebę wyłączenia zasilania. Zasadniczo możesz robić, co chcesz, i to zależy od twojej aplikacji.

Jeśli miałeś aplikację, która musiała zrobić tylko jedną rzecz po włączeniu zasilania, a następnie nie robić nic innego, możesz po prostu odłożyć trochę czasu (1); na końcu main (), aby procesor przestał robić cokolwiek zauważalnego.

Jeśli aplikacja wymagała wyłączenia procesora, wówczas w zależności od procesora prawdopodobnie będą dostępne różne tryby uśpienia. Procesory mają jednak zwyczaj budzić się ponownie, więc musisz się upewnić, że nie ma limitu czasu na sen i nie jest włączony Timer Watch Dog itp.

Możesz nawet zorganizować jakieś zewnętrzne obwody, które pozwolą procesorowi całkowicie odciąć własną moc po zakończeniu. Zobacz to pytanie: Używanie przycisku chwilowego jako przełącznika dwustabilnego włączania / wyłączania .

Rocketmagnet
źródło
20

W przypadku skompilowanego kodu zależy to od kompilatora. Kompilator gcc ARM Rowley CrossWorks, którego używam, przeskakuje do kodu w pliku crt0.s, który ma nieskończoną pętlę. Kompilator Microchip C30 dla 16-bitowych urządzeń dsPIC i PIC24 (również opartych na gcc) resetuje procesor.

Oczywiście większość wbudowanych programów nigdy się tak nie kończy i wykonuje kod w sposób ciągły w pętli.

Leon Heller
źródło
13

Należy tu zwrócić uwagę na dwa punkty:

  • Program osadzony, ściśle mówiąc, nie może „zakończyć”.
  • Bardzo rzadko trzeba uruchamiać program osadzony przez pewien czas, a następnie „zakończyć”.

Koncepcja zamknięcia programu zwykle nie istnieje w środowisku osadzonym. Na niskim poziomie procesor wykona instrukcje, dopóki może; nie ma czegoś takiego jak „końcowe oświadczenie zwrotne”. Procesor może przerwać wykonywanie, jeśli napotka nieodwracalny błąd lub zostanie wyraźnie zatrzymany (przełączony w tryb uśpienia, tryb niskiego poboru mocy itp.), Ale należy pamiętać, że nawet tryby uśpienia lub usterki nieodwracalne ogólnie nie gwarantują, że nie będzie już więcej kodu być straconym. Możesz obudzić się z trybów uśpienia (tak są zwykle używane), a nawet zablokowany procesor może nadal wykonywać procedurę obsługi NMI (tak jest w przypadku Cortex-M). Watchdog również będzie nadal działał i możesz nie być w stanie go wyłączyć na niektórych mikrokontrolerach, gdy jest włączony. Szczegóły różnią się znacznie w zależności od architektury.

W przypadku oprogramowania układowego napisanego w języku takim jak C lub C ++, co się stanie, jeśli wyjście main () zostanie określone przez kod startowy. Na przykład tutaj jest odpowiednia część kodu startowego ze Standardowej Biblioteki Peryferyjnej STM32 (w przypadku łańcucha narzędzi GNU komentarze są moje):

Reset_Handler:  
  /*  ...  */
  bl  main    ; call main(), lr points to next instruction
  bx  lr      ; infinite loop

Ten kod wejdzie w nieskończoną pętlę, gdy main () powróci, chociaż w sposób nieoczywisty ( bl mainładuje się lrz adresem następnej instrukcji, która faktycznie jest skokiem do siebie). Nie podejmowane są próby zatrzymania procesora lub przejścia w tryb niskiego poboru mocy itp. Jeśli masz uzasadnioną potrzebę zastosowania tego w swojej aplikacji, musisz to zrobić samodzielnie.

Należy zauważyć, że jak określono w ARMv7-M ARM A2.3.1, rejestr łącza jest ustawiany na 0xFFFFFFFF po zresetowaniu, a rozgałęzienie pod tym adresem wywoła błąd. Dlatego projektanci Cortex-M postanowili potraktować zwrot z procedury resetowania jako nienormalny i trudno się z nimi kłócić.

Mówiąc o uzasadnionej potrzebie zatrzymania procesora po zakończeniu oprogramowania układowego, trudno sobie wyobrazić takie, które nie byłyby lepiej obsługiwane przez wyłączenie urządzenia. (Jeśli wyłączysz procesor „na dobre”, jedyne, co można zrobić z urządzeniem, to cykl zasilania lub resetowanie sprzętu zewnętrznego.) Możesz usunąć sygnał ENABLE dla konwertera DC / DC lub wyłączyć zasilanie w w inny sposób, tak jak robi to komputer ATX.

Cierń
źródło
1
„Możesz obudzić się z trybów uśpienia (tak są zwykle używane), a nawet zablokowany procesor może nadal wykonywać procedurę obsługi NMI (tak jest w przypadku Cortex-M).” <- brzmi jak niesamowita część fabuła książki lub filmu. :)
Mark Allen
„Bl main” załaduje „lr” z adresem następującej instrukcji („bx lr”), prawda? Czy jest jakiś powód, aby oczekiwać, że „lr” zawiera cokolwiek innego, gdy wykonywany jest „bx lr”?
supercat
@ supercat: oczywiście masz rację. Zredagowałem swoją odpowiedź, aby usunąć błąd i nieco go rozwinąć. Myśląc o tym, sposób implementacji tej pętli jest dość dziwny; mogliby z łatwością zrobić loop: b loop. Zastanawiam się, czy rzeczywiście zamierzali dokonać zwrotu, ale zapomnieli zapisać lr.
Thorn
To jest ciekawe. Spodziewałbym się, że wiele kodu ARM wyjdzie z LR o tej samej wartości, co przy wejściu, ale nie wiem, czy jest to gwarantowane. Taka gwarancja często nie byłaby użyteczna, ale utrzymanie jej wymagałoby dodania instrukcji do procedur, które kopiują r14 do innego rejestru, a następnie wywołują inną procedurę. Jeśli po powrocie lr zostanie uznany za „nieznany”, można „sprawdzić” rejestr przechowujący zapisaną kopię. Spowodowałoby to jednak bardzo dziwne zachowanie ze wskazanym kodem.
supercat
Właściwie jestem całkiem pewien, że funkcje nie-liściowe mają ocalić lr. Zwykle wypychają lr na stos w prologu i zwracają, wstawiając zapisaną wartość do komputera. Tak właśnie zrobilibyśmy np. Main () C lub C ++, ale twórcy omawianej biblioteki oczywiście nie zrobili czegoś takiego w Reset_Handler.
Thorn
9

Pytając o return, myślisz o zbyt wysokim poziomie. Kod C jest tłumaczony na kod maszynowy. Jeśli więc pomyślisz o tym, że procesor ślepo wyciąga instrukcje z pamięci i wykonuje je, nie ma pojęcia, który z nich jest „ostateczny” return. Tak więc procesory nie mają wrodzonego końca, ale programista zajmuje się końcową sprawą. Jak Leon wskazuje w swojej odpowiedzi, kompilatory zaprogramowały zachowanie domyślne, ale często programista może chcieć własnej sekwencji wyłączania (robiłem różne rzeczy, takie jak wejście w tryb niskiego poboru mocy i zatrzymanie lub czekanie na podłączenie kabla USB w, a następnie restart).

Wiele mikroprocesorów posiada instrukcje zatrzymania, które zatrzymują procesor bez wpływu na części ciała. Inne procesory mogą polegać na „zatrzymaniu” poprzez zwykłe wielokrotne przeskakiwanie pod ten sam adres. Możliwe są opcje, ale to zależy od programisty, ponieważ procesor po prostu będzie dalej czytał instrukcje z pamięci, nawet jeśli ta pamięć nie była zamierzona jako instrukcje.

Klox
źródło
7

Problem nie jest osadzony (system osadzony może obsługiwać Linuxa, a nawet Windowsa), ale samodzielny lub bez systemu operacyjnego: (skompilowana) aplikacja jest jedyną rzeczą, która działa na komputerze (nie ma znaczenia, czy jest to mikrokontroler lub mikroprocesor).

W przypadku większości języków język nie określa, co się stanie, gdy zakończy się „główny” i nie ma systemu operacyjnego, do którego mógłby wrócić. W przypadku C zależy to od tego, co znajduje się w pliku startowym (często crt0.s). W większości przypadków użytkownik może (a nawet musi) dostarczyć kod startowy, więc ostateczna odpowiedź brzmi: cokolwiek napiszesz, to kod startowy lub co dzieje się w określonym przez Ciebie kodzie startowym.

W praktyce istnieją 3 podejścia:

  • nie podejmuj żadnych specjalnych środków. co dzieje się, gdy główne zwroty są niezdefiniowane.

  • przeskocz do 0 lub użyj dowolnego innego sposobu, aby zrestartować aplikację.

  • wejść w ciasną pętlę (lub wyłączyć przerwania i wykonać polecenie zatrzymania), blokując procesor na zawsze.

To, co jest właściwe, zależy od zastosowania. Fur-elise kartka z życzeniami i system kontroli hamulca (żeby wspomnieć tylko o dwóch systemach wbudowanych) prawdopodobnie powinien się zrestartować. Minusem ponownego uruchomienia jest to, że problem może pozostać niezauważony.

Wouter van Ooijen
źródło
5

Kiedyś patrzyłem na jakiś zdemontowany kod ATtiny45 (C ++ skompilowany przez avr-gcc), a na końcu kodu przeskakuje do 0x0000. Zasadniczo robi reset / restart.

Jeśli kompilator / asembler pomija ten ostatni skok do 0x0000, wszystkie bajty w pamięci programu są interpretowane jako „poprawny” kod maszynowy i działa przez cały czas, aż licznik programu przejdzie do 0x0000.

W AVR 00 bajtów (domyślna wartość, gdy komórka jest pusta) to NOP = brak operacji. Działa więc bardzo szybko, nie robiąc nic, tylko trochę czasu.

jippie
źródło
1

Zasadniczo skompilowany mainkod jest następnie łączony z kodem startowym (może być zintegrowany z zestawem narzędzi, dostarczanym przez dostawcę układu, napisany przez ciebie itp.).

Linker umieszcza następnie cały kod aplikacji i uruchamiania w segmentach pamięci, więc odpowiedź na pytania zależy od: 1. kodu od uruchomienia, ponieważ może na przykład:

  • zakończ pustą pętlą ( bl lrlub b .), która będzie podobna do „końca programu”, ale przerwania i urządzenia peryferyjne włączone wcześniej będą nadal działać,
  • kończy się skokiem na początek programu (albo całkowicie uruchom ponownie, albo jsut to main).
  • po prostu zignoruj ​​„co będzie dalej” po wezwaniu do mainzwrotu.

    1. W trzecim punkcie, gdy licznik programu po prostu przyrosty po powrocie z mainzachowania będą zależeć od ciebie linkera (i / lub skryptu linkera używanego podczas łączenia).
  • Jeśli po Twojej mainnazwie zostanie umieszczona inna funkcja / kod , zostanie wykonana z niepoprawnymi / niezdefiniowanymi wartościami argumentów,

  • Jeśli następująca pamięć zaczyna się od złej instrukcji, może zostać wygenerowany wyjątek i MCU ostatecznie zresetuje się (jeśli wyjątek generuje reset).

Jeśli watchdog jest włączony, ostatecznie zresetuje MCU pomimo wszystkich nieskończonych pętli, w których się znajdujesz (oczywiście jeśli nie zostaną przeładowane).

kwesolowski
źródło
-1

Najlepszym sposobem na zatrzymanie wbudowanego urządzenia jest wieczne czekanie z instrukcjami NOP.

Drugim sposobem jest zamknięcie urządzenia za pomocą samego urządzenia. Jeśli możesz sterować przekaźnikiem za pomocą instrukcji, możesz po prostu otworzyć przełącznik, który zasila urządzenie wbudowane i huh urządzenie wbudowane zniknęło bez zużycia energii.

Enes Unal
źródło
To naprawdę nie odpowiada na pytanie.
Matt Young,
-4

Zostało to jasno wyjaśnione w instrukcji. Zazwyczaj procesor generuje ogólny wyjątek, ponieważ uzyskuje dostęp do lokalizacji pamięci znajdującej się poza segmentem stosu. [wyjątek ochrony pamięci].

Co miałeś na myśli przez system wbudowany? Mikroprocesor lub mikrokontroler? Tak czy inaczej, jest to zdefiniowane w instrukcji.

W procesorze x86 wyłączamy komputer, wysyłając polecenie do kontrolera ACIP. Wejście w tryb zarządzania systemem. Tak więc kontroler jest układem we / wy i nie trzeba go ręcznie wyłączać.

Przeczytaj specyfikację ACPI, aby uzyskać więcej informacji.

Standardowy Sandun
źródło
3
-1: TS nie wspomniał o żadnym konkretnym CPU, więc nie zakładaj za dużo. Różne systemy obsługują tę sprawę na bardzo różne sposoby.
Wouter van Ooijen