Embedded

Na początku był chaos. Czyli o tablicy wektorów przerwań w ARM Cortex M3 i M4.

Lipiec 5, 2019 1
Podziel się:

Początki zwykle bywają trudne. Nie inaczej jest ze zrozumieniem zasad wykonywania programu na mikrokontrolerze przez osobę bez wykształcenia i doświadczenia w tym konkretnym kierunku. Dawno temu kiedy jako student automatyki stawiałem pierwsze kroki w świecie systemów wbudowanych wiele aspektów stanowiło na poły techniczną, na poły mitologiczną zagadkę.

W czasie studiów wytłumaczono mi bowiem bardzo ogólne podstawy: funkcja main, pętla nieskończona, przerwania. Jednak nie wspomniano ni słowem o tym jak i dlaczego wykonywanie programu opiera się o funkcję main, ani też w jaki sposób platforma mapuje konkretne zdarzenie (przerwanie) na konkretną funkcję obsługi.

Na początku był chaos

Niczym w Mitologii Jana Parandowskiego, wszystko bierze swój początek w chaosie. Aby jednak wspomniany chaos stał się trochę bardziej przystępny dla zwykłego śmiertelnika to przyjmijmy, że zaczyna się on zawsze pod pewnym (roboczo przyjmijmy, że tym samym) adresem w pamięci programu.

Wektor reset’u

Tym sposobem dochodzimy do tzw. wektora reset’u. Jest to adres w pamięci programu, od którego zaczyna się jego wykonywanie. Adres ten moglibyśmy uczynić stałym i niezmiennym (np. przyjmującym wartość zero), jednak takie podejście ograniczy mocno swobodę budowania pliku wykonywalnego. Istnieje jeszcze szereg innych adresów, które należałoby znać i uczynić niezmiennymi.  Na potrzeby dalszych rozważań przyjmijmy, że adres ten jest nam znany po zbudowaniu pliku wykonywalnego.

Przerwania

Obsługa przerwania również opiera się o wykonywanie konkretnego fragmentu kodu w odpowiedzi na zaistniałe zdarzenie. W systemie takich zdarzeń możemy mieć kilkanaście/kilkadziesiąt. W momencie zaistnienia zdarzenia należy sprawnie przełączyć kontekst z wykonywania głównego programu, na obsługę przerwania.

Wyjątki procesora

Do mitologicznego Tartaru – uosabianego w tym przypadku przez Hard Fault/Bus/Mem Manage Handler, możemy zostać zepchnięci na skutek szeregu niekorzystnych zdarzeń: odwołania do adresu zerowego, wykonania nieznanej instrukcji przez CPU, próby dostępu do chronionego/niedostępnego adresu. W każdym z tych przypadków nieszczęsny program jest spychany w jedną z czarnych otchłani. Kolejny już raz należy wiedzieć, gdzie owa czarna dziura (pod jakim adresem) się znajduje.

Adres początku stosu

Ostatnim elementem naszej mityczno-mitologicznej układanki jest stos. W pierwszym kroku wykonywania programu adres (początku) stosu powinien zostać załadowany do sprzętowego rejestru, pełniącego funkcję wskaźnika stosu. Stąd wartość ta powinna być znana jeszcze przed uruchomieniem programu.

Tablica wektorów przerwań

Czysto hipotetycznie wszystkie w/w wartości moglibyśmy uznać za stałe w ramach jednej platformy/mikrokontrolera/rodziny mikrokontrolerów.

Dla przykładu:

  • początek programu mógłby być zawsze umieszczony pod adresem 0x00000000,
  • funkcje obsługi przerwań moglibyśmy rozmieścić odpowiednio od adresu 0x00001000 co 100 bajtów,
  • podobnie z rozmieszczeniem funkcji obsługi wyjątków
  • początek stosu mógłby zaś znaleźć się zawsze na końcu dostępnego nam obszaru pamięci RAM

Odnosząc się do bardziej życiowego przykładu moglibyśmy powiedzieć, że tablica wektorów przerwań jest swoistym spisem treści w książce (w naszym przypadku książką jest kompletny plik wykonywalny).

Taka mocno usztywniona wizja pliku wykonywalnego ogranicza swobodę przy jego tworzeniu. Co jeżeli funkcje obsługi przerwań nie zmieszczą się we wspomnianych 100 bajtach? Albo z jakiegoś powodu chcielibyśmy wykorzystać koniec pamięci RAM do przechowywania innych danych? Wracając do przykładu z książką – to tak jakby każdy rozdział w książce miał się rozpocząć na wcześniej ustalonej stronie. Ustalonej przed napisaniem treści książki.

Potrzebujemy więc rozwiązania, które zapewni nam z jednej strony elastyczność w rozmieszczaniu poszczególnych funkcji w pliku wykonywalnym, a z drugiej pozwoli na łatwe ich znalezienie.

Tablica wektorów przerwań w ARM Cortex M3/M4

W przypadku mikrokontrolerów z rdzeniem ARM Cortex M3/M4 tablicę wektorów przerwań zorganizowano w sposób następujący:

  • umieszczono ją w pamięci programu pod adresem (domyślnym) 0x08000000,
  • zafiksowano w niej kolejność występowania poszczególnych wartości (adresów): początku stosu, wektora resetu, funkcji obsługi poszczególnych przerwań
  • same wartości (adresy) obliczane są na etapie budowania pliku wynikowego i umieszczane w tablicy pod konkretnym offsetem
Tablica wektorów przerwań

Przykładowy wygląd tablicy wektorów przerwań wraz z wartościami offsetów.

Tak opracowane rozwiązanie jest proste, a jednocześnie wystarczająco elastyczne. Nasz spis treści umieszczony jest na początku książki (adres 0x08000000), a poszczególne rozdziały zaczynają się pod zmiennymi (per książka) numerami stron. Oczywiście kolejność rozdziałów jest zawsze taka sama, ale to już nie blokuje nas przed przygotowaniem optymalnego pliku wykonywalnego.

Uruchamianie programu

Z chwilą uruchomienia mikrokontrolera wspomniany wcześniej spis treści, jest wykorzystywany do pobrania i zainicjowania dwóch kluczowych wartości:

  • adresu procedury resetu (reset handler’a)
  • wskaźnika stosu, znajdującego się pod offsetem 0x0000 w tablicy wektorów przerwań

Adres reset handler’a ładowany jest sprzętowo do rejestru licznika programu (PC). Z kolei adres początku stosu ładowany jest już wewnątrz procedury resetu. W ostatnim kroku procedury resetu wywoływana jest funkcja main().

Start programu

Wartości wyciągane z tablicy wektorów przy starcie programu wraz przykładowym Reset Handler’em

Obsługa przykładowego przerwania

W momencie wystąpienia przerwania na platformie wykorzystującej rdzeń Cortex M3/M4 następuje automatyczny, sprzętowy zapis aktualnego kontekstu na stosie. W ramach kontekstu zapisywany jest rejestr statusu (xPSR), licznik programu (PC), rejestr powrotu (LR) wraz z czterema pierwszymi rejestrami ogólnego przeznaczenia (R0-R3) oraz wskaźnikiem stosu (R12/SP).

Następnie wykonywany jest skok do konkretnej procedury obsługi przerwania. A więc z tablicy wektorów, spod konkretnego offsetu, wyciągany jest adres procedury obsługi danego przerwania i ładowany do licznika programu (PC). Po obsłużeniu przerwania, kontekst jest odtwarzany ze stosu, co powoduje załadowanie licznika programu adresem miejsca, z którego nastąpił skok do przerwania.

Kontekst 1 - Na początku był chaos. Czyli o tablicy wektorów przerwań w ARM Cortex M3 i M4.


                                       Procedura skoku do obsługi przerwania z funkcji main() z zapisaniem kontekstu na stosie

Relokowanie tablicy wektorów przerwań

Wartą wzmianki funkcjonalnością procesorów rodziny Cortex M3/M4, jest możliwość relokowania tablicy wektorów przerwań podczas pracy programu. Służy do tego rejestr VTOR (Vector Table Offset Register).

Należy przy tym bezwzględnie pamiętać, iż 7 najmłodszych bitów wspomnianego rejestru nie może być użyte. Stąd adres tablicy wektorów musi być odpowiednio wyrównany.

VTOR ARM - Na początku był chaos. Czyli o tablicy wektorów przerwań w ARM Cortex M3 i M4.

Struktura rejestru VTOR dla Cortex M3/M4

Domyślnie tablica wektorów przerwań umieszczona jest pod adresem 0x08000000 i wartość rejestru VTOR określa offset względem początku pamięci (adres 0x00000000) pod jakim w danym momencie znajduje się wspomniana tablica. A skoro mapa pamięci jest ciągła to możemy umieścić nasze wektory zarówno w pamięci programu, jak i w RAMie. W tym celu po zapisaniu nowej tablicy wektorów w pamięci ustawiamy rejestr VTOR na adres, odpowiadający początkowi tablicy.

Użycie rejestru VTOR, aby wskazywała na nową tablicę wektorów

Przykładowe użycie rejestru VTOR celem wskazania innej tablicy wektorów

Przykład programistyczny

W załączonym, przykładowym projekcie przyjrzymy się w pierwszej kolejności skryptowi linkera (STM32L432KCUx_FLASH.ld). Tam znajdziemy informację o rozmieszczeniu poszczególnych modułów w pamięci programu. W okolicach linii 76 znajdziemy wylistowane sekcje pamięci. Pierwszą z umieszczonych w pamięci programu sekcji jest właśnie tablica wektorów (isr_vector).

Tablica wektorów w pamięci programu

Lokalizacja tablicy wektorów w pamięci programu

Sama tablica jest zdefiniowana w pliku startup_stm32l432xx.s. Idąc od szczytu tablicy wektorów (g_pfnVectors) widzimy kolejno wartości reprezentowane przez adresy:

  • początku stosu,
  • wektora resetu,
  • funkcji obsługi wyjątków
  • funkcji obsługi przerwań.

Jednej z nich przyjrzymy się w szczegółach. Jest nią umieszczony w linii 188 adres funkcji obsługi przerwań od Timer’a 1 (TIM1_UP_TIM16_Original).

Struktura tablicy wektorów

Struktura tablicy wektorów przerwań

Na marginesie. Nie musimy definiować od razu wszystkich funkcji obsługi przerwań. Zamiast tego możemy posłużyć się sprytną konstrukcją wykorzystującą adnotację .weak oraz alias między tak utworzonym symbolem (nazwą funkcji), a domyślnym, wspólnym dla wszystkich niewykorzystywanych przerwań,  handlerem (Default_Handler).

Tak przygotowana “słaba” funkcja może być w dowolnym momencie zdefiniowana w innym pliku źródłowym i już jako “silny” symbol, użyta zamiast domyślenia handlera. Takie rozwiązanie ma tę zaletę, że nie wymaga modyfikowania kodu źródłowego tabeli wektorów oraz usuwania domyślnego handlera. Po prostu definiujemy funkcję obsługi o nazwie takiej samej jak uprzednio używana “słaba” funkcja i wszystkim zajmuję się już linker.

Słaby handler

Przykład weak handlera dla przerwania od modułu RTC

Na potrzeby przykładu utworzymy jeszcze dwie, prawie identyczne tablice wektorów. Pierwszą z nich umieścimy na końcu użytego obszaru w pamięci FLASH (g_pfnVectorsRelocatedFlash, linia 254 w pliku stm32l432xx.s). Drugą zaś umieścimy na początku pamięci RAM (g_pfnVectorsRelocatedRAM, linia 361 w pliku stm32l432xx.s).

Wszystkie trzy tablice wektorów są praktycznie identyczne. Różnią się tylko jednym, subtelnym elementem – adresem funkcji obsługi przerwań od Timer’a 1. Są to kolejno funkcje:

  • TIM1_UP_TIM16_Original dla tablicy wektorów na początku FLASH’a
  • TIM1_UP_TIM16_RelocatedFlash dla tablicy wektorów na końcu FLASH’a
  • TIM1_UP_TIM16_RelocatedRAM dla tablicy wektorów umieszczonej w pamięci RAM

Wspomniane funkcje obsługi przerwań umieszczone są jedna za drugą w pliku stm32l4xx_it.c.

Z kolei wewnątrz pętli nieskończonej w funkcji main.c następuje manipulacja zawartością rejestru VTOR. Z pomocą zmiennej globalnej funkcja main dokonuje podmiany tablicy wektorów – co skutkuje cyklicznym wywołaniem kolejnych funkcji obsługi przerwań Timer’a 1, zdefiniowanych w pliku stm32f0xx_it.c.

main - Na początku był chaos. Czyli o tablicy wektorów przerwań w ARM Cortex M3 i M4.

Aktualizacja rejestru VTOR z poziomu kodu źródłowego (main.c)

Tak więc w trakcie działania programu możemy szybko i sprawnie podmienić wszystkie procedury obsługi przerwań/wyjątków.

Widocznym efektem jest wywoływanie kolejno wspomnianych trzech funkcji obsługi przerwania przy przepełnieniu Timer’a – dwóch z pamięci programu i jednej z RAMu. Mechanizm, pozwalający na przemieszczanie tablicy wektorów jest to szczególnie przydatny przy wykonywaniu programu z pamięci RAM.

Ale to już temat na zupełnie inną opowieść…

Pobierz przykładowy projekt

 

.223rem

5 / 5
Kategorie: Embedded
Mateusz Januszkiewicz
Autor: Mateusz Januszkiewicz
Ze światem urządzeń wbudowanych związany jestem od 10 lat. Na co dzień pracuję na stanowisku Architekta Rozwiązań w Centrum Kompetencji Embedded gdzie mam do czynienia z różnymi ARMowymi i nie-ARMowymi platformami oraz dotykam tematów związanych z niskopoziomowym bezpieczeństwem. W wolnym czasie zajmuję się strzelectwem i majsterkowaniem przez duże O.

Imię i nazwisko (wymagane)

Adres email (wymagane)

Temat

Treść wiadomości

komentarze(1)

avatar'
Jakub Standarski
14 lipca 2019 Odpowiedz

Fantastyczny artykuł, lekki i przyjemny w czytaniu, z ciekawą i chwytliwą analogią. Liczę na więcej tego typu materiałów tłumaczących zagadnienia "światów wbudowanych".

Zostaw komentarz