Architektura ARM Marcin Peczarski Instytut Informatyki Uniwersytetu Warszawskiego 17 lutego 2012
ARM Międzynarodowa firma, mająca główną siedzibę w Cambrdge w Wielkiej Brytanii. Projektuje i sprzedaje licencje na rdzenie ARM. Nie produkuje chipów. Podobno ok. 3/4 telefonów komórkowych i systemów wbudowanych zawiera rdzenie ARM.
Przegląd aktualnych wersji architektury ARM Źródło: http://www.arm.com
Zapowiadana 64-bitowa wersja architektury ARM Źródło: http://www.arm.com
ARM = Advanced RISC Machines Zestaw instrukcji wzorowany na RISC, aby łatwo potokować: ldr r3, [r1, #36] adds r2, r3, #1 RISC-owe wywoływanie procedur: bl bx etykieta lr Prawie każda instrukcja może być wykonywana warunkowo: movge r7, r4 Argument można przesunąć przed wykonaniem operacji: orr r1, r1, r4, lsl #12 str r2, [r4, r0, lsl #2] Występują instrukcje typowo CISC-owe: push pop {r4, r5, lr}; prolog funkcji {r4, r5, pc}; epilog funkcji
Cortex-M3/M4 Rdzenie do zastosowań mikrokontrolerowych Cienkokońcówkowe Jednolita przestrzeń adresowa architekura typu Princeton Osobne szyny do pamięci danych i programu organizacja typu Hardward Na kolejnych slajdach omawiam architekturę Cortex-M3, posługując się przykładami przetestowanymi na mikrokontrolerach z rodziny STM32.
Przykład mikrokontrolera z rdzeniem Cortex-M3 Źródło: http://www.st.com
Przestrzeń adresowa Źródło: http://mcu.ee
Bezpośrednie adresowanie bitów bitaddr = baseaddr + 32 dwordoffset + 4 bitnumber Źródło: http://www.kimura-lab.net
Organizacja pamięci programu Flash sidata = etext.data.rodata RAM stack heap estack end ebss.bss.text edata = sbss 0x08000000.isr vector.data sdata = 0x20000000
Kod startowy deklaracje extern unsigned long _sidata; extern unsigned long _sdata; extern unsigned long _edata; extern unsigned long _sbss; extern unsigned long _ebss; extern unsigned long _estack; int main(void);
Kod startowy wykonywany po wyzerowaniu static void Reset_Handler(void) { unsigned long *src, *dst; for (dst = &_sdata, src = &_sidata; dst < &_edata; ++dst, ++src) *dst = *src; for (dst = &_sbss; dst < &_ebss; ++dst) *dst = 0; main(); for (;;); }
Tablica przerwań attribute ((section(".isr_vector"))) void (* const g_pfnvectors[])(void) = { (void*)&_estack, Reset_Handler, /* Przerwania rdzenia Cortex-M3 */ NMI_Handler, HardFault_Handler, MemManage_Handler, BusFault_Handler, UsageFault_Handler,... SysTick_Handler, /* Przerwania układów peryferyjnych */... };
Zaślepki dla nieużywanych przerwań static void Default_Handler(void) { for (;;); } #define WEAK attribute ((weak)) void WEAK NMI_Handler(void); #pragma weak NMI_Handler = Default_Handler
Połączenie tego w całość (1) MEMORY { FLASH (rx) : ORIGIN = 0x8000000, LENGTH = 128K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K } _minimum_stack_and_heap = 2560; SECTIONS {... }
Połączenie tego w całość (2).text : { KEEP(*(.isr_vector)) *(.text) *(.text.*) *(.rodata) *(.rodata.*). = ALIGN(4); _etext =.; _sidata = _etext; } >FLASH =0xff
Połączenie tego w całość (3).data : AT(_sidata) {. = ALIGN(4); _sdata =.; *(.data) *(.data.*). = ALIGN(4); _edata =.; } >RAM =0
Połączenie tego w całość (4).bss : {. = ALIGN(4); _sbss =.; *(.bss) *(.bss.*) *(COMMON). = ALIGN(4); _ebss =.; } >RAM
Połączenie tego w całość (5) _estack = (ORIGIN(RAM) + LENGTH(RAM)) & 0xFFFFFFFC; ASSERT(_ebss + _minimum_stack_and_heap <= _estack, "There is not enough space in RAM.") PROVIDE(end = _ebss);...
Zaświecenie diody hard-code Dioda jest podłączona do wyprowadzenia PB8. typedef volatile uint32_t reg_t; uint32_t tmp; *(reg_t*)(0x40021000 + 0x18) = 0x00000008; tmp = *(reg_t*)(0x40010c00 + 0x04); tmp &= 0xfffffff0; tmp = 0x00000001; *(reg_t*)(0x40010c00 + 0x04) = tmp; *(reg_t*)(0x40010c00 + 0x10) = 1 << 8;
Biblioteki CMSIS Cortex Microcontroller Software Interface Standard STM32... x StdPeriph Driver biblioteka obsługująca peryferie specyficzne dla danego modelu mikrokontrolera
Zaświecenie diody z użyciem CMSIS Trzeba włączyć właściwy plik nagłówkowy (jeden): #include <stm32f10x.h> #include <stm32f2xx.h> #include <stm32l1xx.h> Potem już jest łatwiej: uint32_t tmp; RCC->APB2ENR = RCC_APB2ENR_IOPBEN; tmp = GPIOB->CRH; tmp &= ~(GPIO_CRH_MODE8_1 GPIO_CRH_CNF8_0 GPIO_CRH_CNF8_1); tmp = GPIO_CRH_MODE8_0; GPIOB->CRH = tmp; GPIOB->BSRR = GPIO_BSRR_BS8;
Zaświecenie diody z użyciem StdPeriph Driver Trzeba włączyć właściwe pliki nagłówkowe, np.: #inlcude <stm32f10x_gpio.h> #inlcude <stm32f10x_rcc.h> Potem już jest łatwo, choć nie zawsze wydajnie: GPIO_InitTypeDef GPIO_InitStruct; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_StructInit(&GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin = GPIO_Pin_8; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_2MHz; GPIO_Init(GPIOB, &GPIO_InitStruct); GPIO_WriteBit(GPIOB, GPIO_Pin_8, Bit_SET);
CMSIS dostarcza funkcji dla operacji niedostępnych w języku C Włączamy odpowiedni plik nagłówkowy: #include <stm32...x.h> I możemy używać różnych ciekawych funkcji: #define htons(x) REV16(x) #define htonl(x) REV(x)
Używając GCC, można je zdefiniować nieco bardziej efektywnie #if defined GNUC #define htons(x) ({ \ uint16_t result; \ asm ("rev16 %0, %1" : "=r" (result) : "r" (x)); \ result; \ }) #define htonl(x) ({ \ uint32_t result; \ asm ("rev %0, %1" : "=r" (result) : "r" (x)); \ result; \ }) #endif
Rejestry R0 do R12 rejestry ogólnego przeznaczenia SP (R13, MSP, PSP) wskaźnik stosu LR (R14) adres powrotu PC (R15) licznik programu PSR (APSR, IPSR, EPSR) rejestr znaczników PRIMASK, FAULTMASK, BASEPRI rejestry maskujące przerwania CONTROL rejestr sterujący trybami pracy rdzenia
Przerwania Reset NMI Hard fault Memory management fault Bus fault Usage fault Supervisor call PendSV SysTick IRQ0... IRQ67 Zerowanie rdzenia Przerwanie niemaskowalne Błąd podczas obsługi przerwania Naruszenie ochrony pamięci Błąd szyny pamięci Niepoprawna instrukcja lub argument instrukcji Wywołanie usługi systemu operacyjnego za pomocą instrukcji SVC Wywołanie planisty w celu przełączenia kontekstu, w założeniu niski priorytet Licznik systemowy, cykliczne przerwanie systemu operacyjnego, w założeniu wysoki priorytet Przerwania zgłaszane przez peryferie
Eskalacja wyjątków Źródło: http://www.eetimes.com
ABI funkcji i przerwań Pierwsze 4 argumenty funkcji są przekazywane w rejestrach R0 do R3, pozostałe przez stos. Funkcja może dowolnie modyfikować rejestry R0 do R3 i R12. Funkcja musi przywrócić wartości rejestrów R4 do R11, LR i SP sprzed wywołania. Przed wywołaniem procedury przerwania procesor odkłada na stos rejestry R0 do R3, R12, adres powrotu, PSR, LR. Funkcja, która ma obsługiwać przerwanie, to najzwyklejsza funkcja języka C o sygnaturze void Handler(void); Nie są potrzebne żadne dodatkowe atrybuty! attribute ((interrupt))
Łańcuchowa obsługa przerwań Źródło: http://www.arm.com Może dochodzić do zagłodzenia!
Łańcuchowa obsługa przerwań, dalsze przykłady Źródło: http://www.arm.com
ABI przerwań, problem Skąd procesor wie, czy instrukcja bx lr jest powrotem ze zwykłej funkcji, czy z obsługi przerwania (trzeba coś zdjąć ze stosu)? Czy ktoś zauważył problem?
Tryby pracy rdzenia Źródło: http://sigalrm.blogspot.com Są dwa stosy: MSP dla trybów uprzywilejowanych (ang. privileged thread, privileged handler), PSP dla trybu użytkownika (ang. user thread).
ABI przerwań, rozwiązanie problemu Przed wywołaniem funkcji obsługującej przerwanie procesor zapisuje do rejestru LR wartość: 0xfffffff1 powrót do obsługi innego przerwania (ang. privileged handler), odtwórz stan z MSP, po powrocie używaj MSP; 0xfffffff9 powrót do wątku uprzywilejowanego (ang. privileged thread), odtwórz stan z MSP, po powrocie używaj MSP; 0xfffffffd powrót do wątku użytkownika (ang. user thread), odtwórz stan z PSP, po powrocie używaj PSP.
Priorytety przerwań Przerwanie może mieć priorytet od 3 do 15. Czym mniejsza wartość, tym większy priorytet. Trzy przerwania mają ustalony, niekonfigurowalny priorytet: Reset priorytet 3, NMI priorytet 2, HardFault priorytet 1. Pozostałym przerwaniom można nadać priorytet, który jest liczbą 4-bitową i składa się z dwóch pól: priorytet wywłaszczania, podpriorytet. Liczba bitów przeznaczonych na priorytet wywłaszczania jest konfigurowalna. Pozostałe bity są przeznaczone na podpriorytet.
Numery przerwań CMSIS definiuje typ wyliczeniowy: typedef enum IRQn {... }; Przerwania zgłaszane przez rdzeń mają ujemne numery: NonMaskableInt_IRQn = -14, MemoryManagement_IRQn = -12, BusFault_IRQn = -11, UsageFault_IRQn = -10,... SysTick_IRQn = -1, Przerwania zgłaszane przez peryferie mają nieujemne numery: WWDG_IRQn = 0,... TIM2_IRQn = 28,...
Blokowanie przerwań o konfigurowalnych priorytetach Użyjemy CMSIS: #include <stm32f10x.h> #include <stm32f2xx.h> #include <stm32l1xx.h> Makro deklarujące zmienną, przechowującą poprzedni stan: #define SYS_ARCH_DECL_PROTECT(x) \ uint32_t x Makro zapisujące aktualny stan i blokujące przerwania: #define SYS_ARCH_PROTECT(x) \ (x = get_primask(), disable_irq()) Makro przywracające poprzedni stan: #define SYS_ARCH_UNPROTECT(x) \ set_primask(x)
Użycie SYS_ARCH_DECL_PROTECT(x); /* Tutaj może zostać zgłoszone przerwanie w dowolnym momencie. */ SYS_ARCH_PROTECT(x); /* Tutaj kod wykonuje się przy zablokowanych przerwaniach. */ SYS_ARCH_UNPROTECT(x);
Wersja makr dla GCC #if defined GNUC #define SYS_ARCH_DECL_PROTECT(x) \ uint32_t x #define SYS_ARCH_PROTECT(x) ({ \ asm volatile ( \ "mrs %0, PRIMASK\n\t" \ "cpsid i" : \ "=r" (x) : \ ); \ }) #define SYS_ARCH_UNPROTECT(x) ({ \ asm volatile ("msr PRIMASK, %0" : : "r" (x)); \ }) #endif
Konfigurowanie priorytetu przerwania Wystarczą 4 priorytety wywłaszczania i 4 podpriorytety: #define PRE_PRIO_BITS 2 #define SET_IRQ_PREEMPTION_PRIORITY_BITS() \ NVIC_SetPriorityGrouping(7 - PRE_PRIO_BITS) Priorytety (wywłaszczania) wygodnie jest nazwać: #define HIGH_IRQ_PRIO 1 #define MIDDLE_IRQ_PRIO 2 #define LOW_IRQ_PRIO 3 CMSIS dostarcza kilka funkcji: NVIC_SetPriority(SysTick_IRQn, NVIC_EncodePriority(NVIC_GetPriorityGrouping(), HIGH_IRQ_PRIO, 0));
Konfigurowanie priorytetu przerwania, cd. Przerwania zgłaszane przez peryferie można też konfigurować za pomocą StdPeriph Driver: #include <misc.h> NVIC_InitTypeDef NVIC_InitStruct; NVIC_InitStruct.NVIC_IRQChannel = TIM2_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = LOW_IRQ_PRIO; NVIC_InitStruct.NVIC_IRQChannelSubPriority = 0; NVIC_InitStruct.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStruct);
Blokowanie przerwań wg priorytetu Makro deklarujące zmienną, która przechowuje informację o blokowanych przerwaniach: #define IRQ_DECL_PROTECT(prv) \ uint32_t prv Makro zapisujące stan aktualnie blokowanych przerwań i blokujące przerwania o priorytecie niższym lub równym lvl: #define IRQ_PROTECT(prv, lvl) \ (prv = get_basepri(), \ set_basepri((lvl) << (8 - PRE_PRIO_BITS))) Makro przywracające poprzedni stan blokowania przerwań: #define IRQ_UNPROTECT(prv) \ set_basepri(prv)
Wersja makr dla GCC #if defined GNUC #define IRQ_DECL_PROTECT(prv) \ uint32_t prv #define IRQ_PROTECT(prv, lvl) ({ \ uint32_t tmp; \ asm volatile ( \ "mrs %0, BASEPRI\n\t" \ "movs %1, %2\n\t" \ "msr BASEPRI, %1" : \ "=r" (prv), "=r" (tmp) : \ "i" ((lvl) << (8 - PRE_PRIO_BITS)) \ ); \ }) #define IRQ_UNPROTECT(prv) ({ \ asm volatile ("msr BASEPRI, %0" : : "r" (prv)); \ }) #endif
Inna wersja makr dla GCC #if defined GNUC #define IRQ_DECL_PROTECT(prv) \ uint32_t prv #define IRQ_PROTECT(prv, lvl) ({ \ uint32_t tmp; \ tmp = (lvl) << (8 - PRE_PRIO_BITS); \ asm volatile ("mrs %0, BASEPRI" : "=r" (prv)); \ asm volatile ("msr BASEPRI, %0" : : "r" (tmp)); \ }) #define IRQ_UNPROTECT(prv) ({ \ asm volatile ("msr BASEPRI, %0" : : "r" (prv)); \ }) #endif
O czym jeszcze można opowiedzieć? Tryby uśpienia Wsparcie dla DSP Przykłady użycia peryferii...