Z rodziną STM32G0 jeszcze nie miałem zbyt wiele do czynienia, ale moi kursanci już tak. Ostatnio rozwiązywaliśmy wspólnie problem niedziałających przerwań na STM32G031J6M6 znanym z płytki STM32G0316-DISCO. Był to o tyle ciekawy problem, że postanowiłem się nim szerzej podzielić.
Obsługa przerwań przez NVIC w STM32
NVIC to skrót – Nested Vector Interrupt Controller. Jest to kontroler odpowiedzialny za przerwania w mikrokontrolerach STM32. Jest on dużo bardziej skomplikowany niż to, co kiedyś mieliśmy, chociażby w AVR. Jednak nie ma się co go bać 🙂
Omówię tylko to, co jest potrzebne do opisania problemu, z jakim się mierzyliśmy z kursantem. Interesuje nas literka V w tym skrócie, czyli Vector. Inaczej mówiąc jest to tablica wskaźników. Po co nam ona?
Do kontrolera NVIC idzie kilkadziesiąt linii przerwaniowych. Każda z tych linii wywołuje inne przerwanie. Każde przerwanie ma swoją obsługę. I tutaj właśnie przychodzi wektor przerwań. Jest to nic innego jak tablica wskaźników do funkcji, które realizują konkretne przerwania.
Wektor ten jest ułożony według tego jak producent układu podłączył bezpośrednio linie przerwań do NVIC. Bardzo nie chcemy zmieniać tej kolejności 🙂
Jak wygląda taki wektor zdefiniowany w programie? Jest napisany w asemblerze w pliku startupowym dla każdego z mikrokontrolera. Dla omawianego STM32G031J6M6 wygląda następująco:
/****************************************************************************** * * The minimal vector table for a Cortex M0. Note that the proper constructs * must be placed on this to ensure that it ends up at physical address * 0x0000.0000. * ******************************************************************************/ .section .isr_vector,"a",%progbits .type g_pfnVectors, %object .size g_pfnVectors, .-g_pfnVectors g_pfnVectors: .word _estack .word Reset_Handler .word NMI_Handler .word HardFault_Handler .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word 0 .word SVC_Handler .word 0 .word 0 .word PendSV_Handler .word SysTick_Handler .word WWDG_IRQHandler /* Window WatchDog */ .word PVD_IRQHandler /* PVD through EXTI Line detect */ .word RTC_TAMP_IRQHandler /* RTC through the EXTI line */ .word FLASH_IRQHandler /* FLASH */ .word RCC_IRQHandler /* RCC */ .word EXTI0_1_IRQHandler /* EXTI Line 0 and 1 */ .word EXTI2_3_IRQHandler /* EXTI Line 2 and 3 */ .word EXTI4_15_IRQHandler /* EXTI Line 4 to 15 */ .word 0 /* reserved */ .word DMA1_Channel1_IRQHandler /* DMA1 Channel 1 */ .word DMA1_Channel2_3_IRQHandler /* DMA1 Channel 2 and Channel 3 */ .word DMA1_Ch4_5_DMAMUX1_OVR_IRQHandler /* DMA1 Channel 4 to Channel 5, DMAMUX1 overrun */ .word ADC1_IRQHandler /* ADC1 */ .word TIM1_BRK_UP_TRG_COM_IRQHandler /* TIM1 Break, Update, Trigger and Commutation */ .word TIM1_CC_IRQHandler /* TIM1 Capture Compare */ .word TIM2_IRQHandler /* TIM2 */ .word TIM3_IRQHandler /* TIM3 */ .word LPTIM1_IRQHandler /* LPTIM1 */ .word LPTIM2_IRQHandler /* LPTIM2 */ .word TIM14_IRQHandler /* TIM14 */ .word 0 /* reserved */ .word TIM16_IRQHandler /* TIM16 */ .word TIM17_IRQHandler /* TIM17 */ .word I2C1_IRQHandler /* I2C1 */ .word I2C2_IRQHandler /* I2C2 */ .word SPI1_IRQHandler /* SPI1 */ .word SPI2_IRQHandler /* SPI2 */ .word USART1_IRQHandler /* USART1 */ .word USART2_IRQHandler /* USART2 */ .word LPUART1_IRQHandler /* LPUART1 */ .word 0 /* reserved */
To co tutaj widzimy, to nazwy funkcji, czyli wskaźniki do nich. Funkcje te to jest obsługa przerwań na odpowiednich pozycjach w wektorze. Możesz zauważyć, że część pozycji jest pusta. To dlatego, że ST nie podłączyło w tych offsetach żadnego przerwania. Nie ma więc możliwości na użycie tej linii.
Możemy ten wektor porównać z tym, co mamy w dokumentacji. Wektor przerwań razem z pozycjami i priorytetami przerwań znajdziesz w Reference Manualu.
Co się dzieje gdy NVIC wykryje przerwanie? Skacze pod adres podany w wektorze przerwań. Jeśli przerwanie było na linii nr 5, to skacze do funkcji w wektorze spod pozycji 5. Proste, co nie?
VTOR
Skąd NVIC ma wiedzieć gdzie w pamięci znajduje się ten wektor? Czy jest on na stałe odgórnie zaplanowany i przypisany? No nie.
Wektor ten może być w różnych miejscach. Wykorzystuje się to na przykład przy pisaniu bootloaderów. Bootloader to osobny program na mikrokontrolerze, który wywołuje aplikację docelową. Ona znajduje się w innym miejscu w pamięci i ma swój wektor przerwań.
Trzeba więc wskazać NVICowi gdzie ma skakać w celu obsługi przerwania. Teraz ważna rzecz. NVIC jest częścią rdzenia, który projektuje ARM. To nie jest moduł dodawany przez ST lub innego producenta mikrokontrolerów z Cortex-M.
Dokumentacja do rdzenia (i NVIC) jest oddzielnym plikiem niż Reference Manual. Dla Cortex-M0 i M0+ jest TUTAJ.
Przechodząc do sedna: Jest specjalny rejestr w rdzeniu mikrokontrolera, w którym przechowywany jest adres, pod którym NVIC ma szukać tablicy wektorów. I tyle 🙂
Trzeba ten rejestr wypełnić. Kiedy? Przed startem programu głównego.
BUG w STM32G0 HAL
Jeśli korzystasz z HALa i nie piszesz bootloadera, to raczej nie interesuje Cię ustawienie tego rejestru. Jest to dokonywane w funkcji void SystemInit(void), która wołana jest w pliku startupowym.
Nie interesuje do czasu… gdy VTOR nie jest ustawiany.
Otóż w HALu dla STM32G0 jest błąd, który domyślnie nie ustawia tablicy wektorów. Jest on konkretnie dla przykładu na STM32G031J6M6, ale znalazłem go również w samym rdzeniu HALa.
Kursanci dali mi znać, że inne G0 działają poprawnie. Całkiem możliwe, że dla konkretnych mikrokontrolerów jest coś inaczej w ich przykładach.
Błąd jest dziwny. Tak wygląda funkcja SystemInit:
void SystemInit(void) { /* Configure the Vector Table location -------------------------------------*/ #if defined(USER_VECT_TAB_ADDRESS) SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET; /* Vector Table Relocation */ #endif /* USER_VECT_TAB_ADDRESS */ }
Jest tutaj taki sprytny define: USER_VECT_TAB_ADDRESS
I niestety, ale on nie jest nigdzie zdefiniowany. Całe to ustawienie VTOR jest domyślnie nieaktywne.
W HALach dla starszych układów wygląda to nieco inaczej. Tutaj nawet ta stała jest nazwana jakby miała należeć na potrzeby użytkownika. Tylko gdzie jest ustawienie domyślne?
Mam pewną teorię.
Prawdopodobnie chciano dodać ujednoliconą relokację wektora przerwań. Jeśli piszemy konkretnie bootloader lub program całkowicie bez niego, to mamy ustawienie domyślne.
Jeśli chcemy napisać program główny, do którego skacze bootloader, to wtedy ustawiamy sobie własną tablicę użytkownika.
Tylko… zaimplementowane jest to w połowie. Jest część dla użytkownika, a zabrakło tej domyślnej 🙂 Coś poszło nie tak.
Co będzie się działo przy braku ustawionego VTOR?
Mamy 2 możliwości:
- Nie będzie przerwań (łagodna)
- Program będzie wpadał w HardFault przy próbie wejścia do obsługi przerwania (gorsza)
Rozwiązanie
Jak żyć? Zgłosiłem poprawkę na GitHubie: https://github.com/STMicroelectronics/STM32CubeG0/pull/33
Zaakceptowali, więc zobaczymy kiedy to poprawią. Co robić na dzień dzisiejszy(23 maja 2022)? Są 3 opcje:
- Dodać w funkcji SystemInit() przypisanie domyślnej bazy adresu: SCB->VTOR = VECT_TAB_BASE_ADDRESS | VECT_TAB_OFFSET;
Niestety to nie jest sekcja użytkownika w CubeMX, więc po przegenerowaniu rozwiązanie zniknie. - Dodać globalną definicję USER_VECT_TAB_ADDRESS. Całkiem dobry pomysł. Zrobisz to w ustawieniach kompilacji, lub poleceniu kompilacji flagą -D
- Zaciągnąć moją poprawkę do HALa. To byłoby najlepsze, ale może warto poczekać na to, aż ST zrobi poprawkę po swojej stronie 🙂
Podsumowanie
Jak widać HAL nadal potrafi mieć męczące bugi. Jednak wciąż uważam, że jest to bardzo dobre narzędzie i warto z niego korzystać.
Ustawienie VTOR przyda nam się, gdy będziemy pisali bootloader. Daj znać w komentarzu, czy chciałbyś, abym pokazał jak taki napisać.
Jeśli chciałbyś się ze mną uczyć programowana STM32 to zapraszam do mojego kursu: https://kursstm32.pl
Masz braki lub całkowicie nie znasz języka C? Już 1 czerwca opowiem Ci jak nauczyć się C pod kątem mikrokontrolerów. Zapisz się na darmowy webinar na https://cdlamikrokontrolerow.pl/webinar
4 komentarze
Alwaro · 30/03/2023 o 15:34
Problem występuje szerzej niż tylko we wspomnianym STM32G0 pojawia się on także na STM32WL5x
es2 · 02/07/2022 o 00:12
Bootloader fajna rzecz ale ja poszedłem inna drogą. W dużych projektach używam uC z 1 czy 2 MB FLASH a te są bankowane, banki można zamienic programowo. Sprawdziłem w praktyce, program “dostaje” niwy kod, umieszcza w “górnej” pamięci FLASH, jak CRC jest ok, zamienia banki, reset i startuje nowy soft.
naturalnie cały algorytm jest bardziej skomplikowany ale chodzi o idę.
Sprawdzone na H7 ale na innych STM32 z bankowaniem zadziała.
Mateusz Salamon · 04/07/2022 o 09:10
W H7, ale w jednordzenowym? Bo w dwóch Core’ach każdy core domyślnie ma swój bank Flasha 🙂 Pewnie da się to zmienić.
Nie mniej taki pomysł na podmianę FW jest spoko. Zawsze można się cofnąć w razie failu 🙂
JLU · 23/05/2022 o 20:23
przydatny artykuł. Jestem za tym abys pokazal jak pisac bootloader🙃