[2] Kurs: pierwsze kroki z STM32F0DISCOVERY. Zabawy z LED-ami
Uwaga! Opublikowane odcinki kurs są dostępne pod adresem.
Samouczek jest dedykowany szczególnie tym projektantom, którzy stają przed perspektywą zmiany mikrokontrolera z 8-bitowego na nowszy i tańszy 32-bitowy. Wszystkie programy są napisane w języku C. W przykładach nie użyto bibliotek służących do obsługi peryferiali, dzięki czemu kod programów jest krótki i czytelny, a zajętość pamięci znacznie mniejsza, niż w typowych programach demonstracyjnych, udostępnianych przez producentów mikrokontrolerów. Zwrócono również szczególną uwagę na poprawność prezentowanych rozwiązań i sposób zapisu programu ułatwiający optymalizację kodu i wychwytywanie błędów przez kompilator. Przedstawione programy zostały napisane w taki sposób, że nie generują one żadnych ostrzeżeń kompilatora.
Odmierzanie czasu przy użyciu timera systemowego SysTick
Kolejny program będzie, tak jak poprzedni, migał diodami, ale tym razem użyjemy prawidłowej techniki odmierzania czasu przy użyciu timera zgłaszającego przerwania ze stałą, zadaną przez programistę częstotliwością.
Gotowy program znajduje się w projekcie blink1.
Zaprogramowanie timera SysTick
Timer SysTick jest częścią samego procesora Cortex-M. Zwykle służy on do zgłaszania przerwań ze stałą częstotliwością. W naszym przykładzie zaprogramujemy go tak, aby zgłaszał przerwania z częstotliwością 100 Hz. Zadanie obliczenia wartości, którą należy w tym celu załadować do rejestr okresu timera, pozostawimy kompilatorowi, zapisując stosowne wyrażenie w instrukcji ładowania rejestru sterującego timera. Zdefiniujemy jako symbol preprocesora częstotliwość procesora SYSCLK_FREQ. Jest ona równa częstotliwości wewnętrznego oscylatora HSI_VALUE zdefiniowanej w pliku nagłówkowym definicji zasobów mikrokontrolera. Zdefiniujemy również częstotliwość zgłaszania przerwań przez timer SYSTICK_FREQ, podając jej wartość w Hz oraz okres zmiany stanu diod, wyrażając go w okresach timera SysTick, czyli w dziesiątkach milisekund.
Programowanie portów wygląda tak samo, jak w poprzednim przykładzie. Następną czynnością jest zaprogramowanie timera SysTick. W tym celu kolejno:
- ustawiamy w rejestrze LOAD maksymalną wartość licznika timera (o 1 mniejszą od długości okresu);
- zerujemy rejestr licznika czasu VAL;
- wybieramy jako źródło zegara timera zegar procesora, włączamy przerwanie timera i uruchamiamy timer, zapisując odpowiednią stałą do rejestru CTRL.
Struktura programu
Zmianą stanu diod zajmie się procedura obsług przerwania timera. Rola programu głównego kończy się na zainicjowaniu portu i timera, dlatego po wykonaniu tych czynności włączymy w procesorze automatyczne usypianie po powrocie z obsługi przerwania (zapis rejestru SCR procesora), a następnie uśpimy procesor w oczekiwaniu na przerwanie (pseudofunkcja __WFI()). Na tym zakończy się wykonanie programu głównego. Procesor będzie budził się na czas obsługi przerwania, a po zakończeniu obsługi będzie usypiał. Nie jest potrzebna żadna „pętla główna”, nawet pusta. Taki sposób programowania jest typowy dla większości prostych aplikacji, które nie wymagają użycia systemu operacyjnego i zapewnia oszczędność energii.
Przerwanie timera SysTick nie wymaga oddzielnego włączenia w sterowniku przerwań NVIC, gdyż timer ten nie jest traktowany jako moduł peryferyjny.
Musimy napisać procedurę obsługi przerwania timera. Jej nazwa jest zdefiniowana w module startowym. Procedura deklaruje zmienną statyczną blink_timer, służącą do odliczania czasu pomiędzy zmianami stanu diod. Jest ona dekrementowana przy każdym przerwaniu, a po jej wyzerowaniu następuje załadowanie długości okresu i zmiana stanu diod.
Dlaczego nie zaprogramowaliśmy timera na docelową częstotliwość migotania diod? Oczywiście można byłoby tak zrobić, ale zazwyczaj w programach, służących do czegoś więcej niż demonstracja działania mikrokontrolera, potrzebujemy odmierzania dużo krótszych odcinków czasu – typowo od 1 do 10 ms. Do sygnalizacji stanu urządzenia użyjemy więc tego samego timera, który byłby użyty do generowania bazy czasu urządzenia, zmieniając stan diod co pewną liczbę przerwań. Zademonstrowane w programie rozwiązanie jest często spotykane w realnym oprogramowaniu.
/* STM32F0DISCOVERY SysTick-based blinker gbm, 12'2012 */ #include "stm32f0xx.h" //======================================================================== // defs for STM32F05x chips #define GPIO_MODER_OUT 1 //======================================================================== // defs for STM32F0DISCOVERY board #define LED_PORT GPIOC #define BLUE_LED_BIT 8 #define GREEN_LED_BIT 9 //======================================================================== // Timings #define SYSCLK_FREQ HSI_VALUE #define SYSTICK_FREQ 100 #define BLINK_PERIOD 50 // * 10 ms //======================================================================== void SystemInit(void) { } //======================================================================== int main(void) { // port setup RCC->AHBENR = RCC_AHBENR_GPIOCEN; // GPIOC LED_PORT->MODER = GPIO_MODER_OUT <<(GREEN_LED_BIT <<1) | GPIO_MODER_OUT <<(BLUE_LED_BIT <<1); // LED outputs LED_PORT->ODR = 1 <LOAD = SYSCLK_FREQ / SYSTICK_FREQ - 1; SysTick->VAL = 0; SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk; SCB->SCR = SCB_SCR_SLEEPONEXIT_Msk; // sleep while not in handler __WFI(); // go to sleep } //======================================================================== void SysTick_Handler(void) { static uint8_t blink_timer = BLINK_PERIOD; if (-- blink_timer == 0) { blink_timer = BLINK_PERIOD; // toggle both LEDs LED_PORT->ODR ^= 1 <
Ten sam program z drobnymi usprawnieniami
Kolejny projekt, blink2, nie różni się funkcjonalnie od poprzedniego. Wprowadzono tu dwie modyfikacje:
- Procedura SystemInit włącza bufor pobierania instrukcji, co powoduje wzrost wydajności procesora.
- Sekwencję instrukcji podstawienia służącą do inicjowania peryferiali zastępujemy prostą funkcją kopiującą dane pod wskazane adresy oraz tablicą zawierającą adresy rejestrów sterujących i wartości, które mają być do nich zapisane.
Takie rozwiązanie sprawdza się zwłaszcza w bardziej złożonych programach, gdzie należy zainicjować kilkadziesiąt rejestrów sterujących – podnosi ono czytelność kodu i redukuje błędy.
W tak zmodyfikowanym programie warto zwrócić uwagę na następujące szczegóły:
- W programie konsekwentnie ograniczono widoczność danych i procedury writeregs, deklarując je z atrybutem static, który w tym przypadku oznacza, że będą to obiekty prywatne dla modułu, w którym zostały zdefiniowane. Umożliwia to kompilatorowi skuteczniejszą optymalizację i zwiększa prawdopodobieństwo wykrycia przez kompilator błędów w kodzie.
- Tablica init_table została zadeklarowana z atrybutem const, dzięki czemu nie zajmuje ona miejsca w pamięci RAM.
- Procedura writeregs została zadeklarowana z atrybutem inline (użyto tu makrodefinicji __INLINE, gdyż kompilator środowiska Keil nie reaguje na standardowe słowo kluczowe). Jest to sugestia dla kompilatora, że może on zastąpić wywołanie procedury wstawieniem jej ciała w miejsce, z którego jest wywoływana.
- W tablicy inicjowania użyto rzutowania typu wskaźnikowego (__IO uint32_t *); wynika to z nietypowej definicji wielu rejestrów sterujących w pliku nagłówkowym dla STM32F0 – są one zdefiniowane jako 16-bitowe.
/* STM32F0DISCOVERY tutorial SysTick-based blinker with table-driven init gbm, 12'2012 */ #include "stm32f0xx.h" //======================================================================== // defs for STM32F05x chips #define GPIO_MODER_OUT 1 //======================================================================== // defs for STM32F0DISCOVERY board #define LED_PORT GPIOC #define BLUE_LED_BIT 8 #define GREEN_LED_BIT 9 //======================================================================== // Timings #define SYSCLK_FREQ HSI_VALUE #define SYSTICK_FREQ 100 // 100 Hz ->10 ms #define BLINK_PERIOD 50 // * 10 ms //======================================================================== struct init_entry_ { volatile uint32_t *loc; uint32_t value; }; static __INLINE void writeregs(const struct init_entry_ *p) { for (; p->loc; p ++) *p->loc = p->value; } //======================================================================== void SystemInit(void) { FLASH->ACR = FLASH_ACR_PRFTBE; // enable prefetch } //======================================================================== static const struct init_entry_ init_table[] = { // port setup {&RCC->AHBENR, RCC_AHBENR_GPIOCEN}, // GPIOC { &LED_PORT->MODER, GPIO_MODER_OUT << (GREEN_LED_BIT <<1) | GPIO_MODER_OUT <<(BLUE_LED_BIT <<1) }, // set LED pins as outputs {(__IO uint32_t *)&LED_PORT->ODR, 1 <LOAD, SYSCLK_FREQ / SYSTICK_FREQ - 1}, {&SysTick->VAL, 0}, { &SysTick->CTRL, SysTick_CTRL_CLKSOURCE_Msk | SysTick_CTRL_TICKINT_Msk | SysTick_CTRL_ENABLE_Msk }, {&SCB->SCR, SCB_SCR_SLEEPONEXIT_Msk}, // sleep while not in handler {0, 0} }; //======================================================================== int main(void) { writeregs(init_table); __WFI(); // go to sleep } //======================================================================== void SysTick_Handler(void) { static uint8_t blink_timer = BLINK_PERIOD; if (-- blink_timer == 0) { blink_timer = BLINK_PERIOD; // toggle both LEDs LED_PORT->ODR ^= 1 <