W artykule przedstawiamy krok-po-kroku kroki niezbędne do szybkiego rozpoczęcia pracy z zestawem STM32F0Discovery, a także prosty projekt przygotowany w środowisku programistycznym ?Vision firmy Keil/ARM. Przykładowa aplikacja obsługuje przycisk i dwie diody LED zastosowane na płytce zestawu.
Przed przystąpieniem do pracy ze środowiskiem IDE ważnym jest aby mieć zainstalowany w systemie sterownik programatora ST-LINK/V2. Jeżeli nie był on wcześniej instalowany to należy pobrać ze strony producenta w zakładce Design support plik ST-LINK/V2 USB driver for Windows 7, Vista and XP i zainstalować go.
Środowisko programistyczne ?Vision można pobrać ze strony producenta. W tym celu na stronie głównej należy przejść do sekcji Download, wybrać opcję Product Downloads i spośród czterech realizacji oprogramowania wybrać wersję MDK-ARM przeznaczoną dla mikrokontrolerów z rdzeniem ARM Cortex-M. Następnie należy wypełnić formularz i kliknąć przycisk Submit po czym pojawi się link do pobrania oprogramowania. Po pobraniu i zainstalowaniu środowiska przystąpimy do utworzenia projektu prostej aplikacji obsługującej dwie diody i przycisk jakie są dostępne na płytce STM32F0Discovery.
Film ilustrujący działanie zestawu STM32F0Discovery z programem przykładowym, opisanym w artykule
Projekt programistyczny
Nowy projekt tworzymy wybierając z menu Project->New uVision Project… i po pojawieniu się okienka Create New Project wybieramy katalog w którym będzie zapisany plik projektu. Przykładowo, plik projektu można zapisać w katalogu STM32F0_SimpleApp\Project, natomiast pliki źródłowe można umieszczać w katalogu STM32F0_SimpleApp\Application. Po zapisaniu pojawi się okienko wyboru urządzenia docelowego Select Device for Target i należy tutaj wybrać mikrokontroler ST->STM32F051R8 jaki znajduje się na płytce STM32F0Discovery. Klikamy OK i po pojawieniu się pytania czy dodać plik startowy startup_stm32f0xx.s do projektu wybieramy Tak. Standardowo plik będzie się znajdował w katalogu gdzie jest zapisany plik projektu.
Na płytce STM32F0Discovery znajduje się wbudowany programator ST-LINK/V2 dlatego informujemy środowisko ?Keil, że dany projekt będzie z niego korzystał. Z głównego okna programu wywołujemy okienko Options for Target (rysunek 1) w którym znajdują się dwie zakładki: Debug oraz Utilities. W pierwszej zaznaczamy radio button Use i z rozwijanej listy wybieramy ST-Link (Deprecated Version) (rysunek 2). Jeżeli przy próbie wejścia w tryb Debug pojawi się komunikat Unknown target connected to należy sprawdzić czy jest wybrany protokół SWD w okienku STLink Setup wywoływany przyciskiem Settings w tejże zakładce Debug obok wybranego wcześniej interfejsu ST-Link.
Rys. 1. Ikona do wywołania okienka Options for Target
Rys. 2. Przykładowa konfiguracja w zakładce Debug
W drugiej zakładce zaznaczamy Use Target Driver for Flash Programming i z listy rozwijanej wybieramy ST-Link (Deprecated Version) (rysunek 3).
Rys. 3. Przykładowa konfiguracja w zakładce Utilities
Kolejnym ważnym krokiem jest pobranie kodów źródłowych obsługi rdzenia i peryferii, które znacząco ułatwiają pisanie programów końcowych. Na stronie producenta zestawu w zakładce Design support należy odszukać plik archiwalny, który nazywa się STM32F0 Discovery kit firmware package, including 21 examples and preconfigured projects for 4 different IDEs. Po pobraniu i rozpakowaniu archiwum należy skopiować katalog STM32F0xx_StdPeriph_Driver z katalogu Libraries oraz katalog STM32F0-Discoveryz katalogu Utilities i umieścić je w katalogu plików źródłowych tworzonego projektu SimpleApp/Application. Tworzymy również tutaj nowe katalogi o nazwach CMSIS oraz User. Do katalogu CMSIS kopiujemy pliki core_cm0.h, core_cmInstr.c oraz core_cmFunc.c z katalogu Libraries\CMSIS\Include, pliki stm32f0xx.h i system_stm32f0xx.h z katalogu Libraries\CMSIS\ST\STM32F0xx\Include, oraz plik system_stm32f0xx.c z katalogu system_stm32f0xx.c.
W pobranym pliku archiwalnym znajdują się również przykładowe projekty przedstawiające możliwości zestawu. Kody źródłowe jednego z nich zostaną wykorzystane do budowy prostej aplikacji. Z katalogu Project\Demonstration do naszego katalogu User kopiujemy pliki: main.c, main.h, stm32f0xx_conf.h, stm32f0xx_it.c oraz stm32f0xx_it.h. Pierwsze dwa pliki zawierają odpowiednio algorytm końcowy działania aplikacji oraz definicje. Kolejny plik zawiera informację jakie elementy biblioteki StdPeriph włączyć do kompilacji projektu. Ostatnie dwa pliki opisują jak mają być obsługiwane przerwania.
W oknie plików projektu Project programu głównego klikamy prawym przyciskiem myszy na najwyższym elemencie struktury plików ze standardową nazwą Target 1 i z wywołanego menu kontekstowego wybieramy Add group… po czym podajemy nazwę grupy User. W podobny sposób tworzymy grupy: STM32F0_Discovery, STM32F0xx_StdPeriph_Driver oraz CMSIS. Warto również zmienić nazwę grupy do której został dodany plik startowy z rozszerzeniem .s na MDK-ARM. Następnie klikając po kolei na utworzonych grupach i wywołując menu kontekstowe wybieramy opcję Add Files to Group… i dodajemy pliki źródłowe w taki sposób jak to przedstawiono na rysunku 4.
Rys. 4. Struktura plików projektu
W trakcie kompilacji projektu potrzebne są ścieżki dostępu do poszczególnych plików źródłowych dlatego w okienku Options for Target w zakładce C/C++ klikamy na przycisk obok Include Paths (rysunek 5) i dodajemy je (rysunek 6). Oprócz tego w polu Define należy wpisać USE_STDPERIPH_DRIVER.
Rys. 5. Zakładka konfiguracyjna C/C++
Rys. 6. Okno Include Paths
Teraz można skompilować projekt przechodząc do menu Project->Build Target lub wcisnąć na klawiaturze klawisz F7. Jeżeli wszystko jest w porządku to w okienku Build Output powinien się pojawić następujący komunikat:
1 2 |
Program Size: Code=4532 RO-data=252 RW-data=56 ZI-data=1632 "SimpleApp.axf" - 0 Error(s), 0 Warning(s). |
Aby zobaczyć efekt działania aplikacji należy podłączyć płytkę STM32F0Discovery do portu USB oraz wejść w tryb debugowania (rysunek 7) i uruchomić wykonywanie programu (rysunek 8). Aby zatrzymać wykonywanie się programu należy wcisnąć przycisk po prawej stronie obok przycisku Start, a przycisk po lewej stronie zeruje układ.
Rys. 7. Ikona wejścia w tryb Debug
Rys. 8. Ikona inicjująca uruchomienie programu
Przykładowa aplikacja
Elementy jakie znajdują się na płytce zestawu i jakie zostaną wykorzystane w prostej aplikacji to (fotografia 9):
- zielona dioda LD3 podłączona do wyprowadzenia mikrokontrolera PC9,
- niebieska dioda LD4 podłączona do wyprowadzenia mikrokontrolera PC8,
- przycisk USER podłączony do wyprowadzenia mikrokontrolera PA0.
Fot. 9. Rozmieszczenie na płytce STM32F0Discovery elementów wykorzystanych w przykładowym projekcie
Według schematu elektrycznego (rysunek 10) aby włączyć LED należy podać logiczną jedynkę na wyprowadzenie GPIO (skonfigurowane jako wyjście), do którego jest ona podłączona, natomiast aby ją zgasić należy podać na tę linię logiczne zero. Przycisk USER jest podłączony do linii PA0, a pomiędzy nim, a ujemnym biegunem źródła zasilania znajduje się rezystor. Jest to więc układ typu pull-down, dlatego stan niski odczytany na wejściu linii PA0 oznacza, że przycisk nie jest wciśnięty.
Rys. 10. Schemat podłączenia LED i przycisku do linii GPIO mikrokontrolera
Funkcje pomocnicze zdefiniowane w pliku stm32f0_discovery.c
Plik stm32f0_discovery.h zawiera kilka definicji stałych wykorzystywanych w programie:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
typedef enum { LED3 = 0, LED4 = 1 } Led_TypeDef; typedef enum { BUTTON_USER = 0 } Button_TypeDef; typedef enum { BUTTON_MODE_GPIO = 0, BUTTON_MODE_EXTI = 1 } ButtonMode_TypeDef; #define LEDn 2 #define LED3_PIN GPIO_Pin_9 #define LED3_GPIO_PORT GPIOC #define LED3_GPIO_CLK RCC_AHBPeriph_GPIOC #define LED4_PIN GPIO_Pin_8 #define LED4_GPIO_PORT GPIOC #define LED4_GPIO_CLK RCC_AHBPeriph_GPIOC #define BUTTONn 1 #define USER_BUTTON_PIN GPIO_Pin_0 #define USER_BUTTON_GPIO_PORT GPIOA #define USER_BUTTON_GPIO_CLK RCC_AHBPeriph_GPIOA #define USER_BUTTON_EXTI_LINE EXTI_Line0 #define USER_BUTTON_EXTI_PORT_SOURCE EXTI_PortSourceGPIOA #define USER_BUTTON_EXTI_PIN_SOURCE EXTI_PinSource0 #define USER_BUTTON_EXTI_IRQn EXTI0_1_IRQn |
W pliku stm32f0_discovery.c znajdują się definicje sześciu pomocnych funkcji do pracy z diodami i przyciskiem:
1 2 3 4 5 6 7 8 9 10 11 12 |
--- Konfiguracja wyprowadzeń do których podłączone są diody void STM_EVAL_LEDInit(Led_TypeDef Led); --- Włączenie diody void STM_EVAL_LEDOn(Led_TypeDef Led); --- Wyłączenie diody void STM_EVAL_LEDOff(Led_TypeDef Led); --- Przełączenie stanu diody na przeciwny void STM_EVAL_LEDToggle(Led_TypeDef Led); --- Konfiguracja wyprowadzenia do którego podłączony jest przycisk void STM_EVAL_PBInit(Button_TypeDef Button, ButtonMode_TypeDef Button_Mode); --- Odczytanie stanu linii do której jest podłączony przycisk uint32_t STM_EVAL_PBGetState(Button_TypeDef Button); |
Pierwsza funkcja przyjmuje jako argument wartość LED3 (liczba 0) albo LED4 (liczba 1), włącza sygnał zegarowy w obu przypadkach taktujący układ PORTC, który jest podłączony do szyny AHB, konfiguruje wyprowadzenie (PC8 lub PC9) na wyjście typu push-pull bez wewnętrznych rezystorów podciągających i do pracy z maksymalną częstotliwością 50 MHz:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void STM_EVAL_LEDInit(Led_TypeDef Led){ GPIO_InitTypeDef GPIO_InitStructure; /* Włączenie sygnału zegarowego dla danego portu */ RCC_AHBPeriphClockCmd(GPIO_CLK[Led], ENABLE); /* Konfiguracja wyprowadzenia GPIO_LED */ GPIO_InitStructure.GPIO_Pin = GPIO_PIN[Led]; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_OUT; GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIO_PORT[Led], &GPIO_InitStructure); } |
Druga funkcja wykorzystuje rejestr BSRR do ustawiania w stan wysoki określonych linii portu. Tutaj pod nazwą GPIO_PIN[Led] kryje się wartość GPIO_Pin_8 lub GPIO_Pin_9 (które są zdefiniowane jawnie w pliku stm32f0xx_gpio.h):
1 2 3 |
void STM_EVAL_LEDOn(Led_TypeDef Led){ GPIO_PORT[Led]->BSRR = GPIO_PIN[Led]; } |
Funkcja wyłączająca diodę jest podobna do poprzedniej, ale wykorzystuje rejestr BRR do zerowania określonej linii portu:
1 2 3 |
void STM_EVAL_LEDOff(Led_TypeDef Led){ GPIO_PORT[Led]->BRR = GPIO_PIN[Led]; } |
Funkcja zmieniająca stan diody na przeciwny operuje na rejestrze danych wyjściowych ODR. Pobiera ona zawartość tego rejestru, wykonuje na niej operację XOR z wartością GPIO_Pin_8 lub GPIO_Pin_9 i zapisuje z powrotem do rejestru ODR:
1 2 3 |
void STM_EVAL_LEDToggle(Led_TypeDef Led){ GPIO_PORT[Led]->ODR ^= GPIO_PIN[Led]; } |
Linia do której podłączony jest przycisk konfigurowana jest w funkcji STM_EVAL_PBInit() do której należy przekazać 2 argumenty. Pierwszy określa jaki przycisk będzie konfigurowany, a że na płytce jest dostępny tylko jeden przycisk do zastosowania w aplikacji to wystarczy przekazać stałą BUTTON_USER, albo liczbę 0. Drugi argument mówi czy linia będzie tylko skonfigurowana (BUTTON_MODE_GPIO) czy również będzie skonfigurowane przerwanie od zmiany jej stanu (BUTTON_USER_EXTI). W programie głównym wykorzystano pierwszą opcję bez wykorzystania przerwań, a linia PA0 jest konfigurowana jako wejście bez rezystorów podciągających, gdyż taki znajduje się już na płytce. Listing funkcji jest następujący:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
void STM_EVAL_PBInit(Button_TypeDef Button, ButtonMode_TypeDef Button_Mode){ GPIO_InitTypeDef GPIO_InitStructure; EXTI_InitTypeDef EXTI_InitStructure; NVIC_InitTypeDef NVIC_InitStructure; /* Włącz sygnały zegarowe */ RCC_AHBPeriphClockCmd(BUTTON_CLK[Button], ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_SYSCFG, ENABLE); /* Konfiguracja linii jako wejście */ GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN; GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; GPIO_InitStructure.GPIO_Pin = BUTTON_PIN[Button]; GPIO_Init(BUTTON_PORT[Button], &GPIO_InitStructure); if (Button_Mode == BUTTON_MODE_EXTI){ /* Connect Button EXTI Line to Button GPIO Pin */ SYSCFG_EXTILineConfig(BUTTON_PORT_SOURCE[Button], BUTTON_PIN_SOURCE[Button]); /* Configure Button EXTI line */ EXTI_InitStructure.EXTI_Line = BUTTON_EXTI_LINE[Button]; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; if (Button != BUTTON_USER){ EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising; } else{ EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; } EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_Init(&EXTI_InitStructure); /* Enable and set Button EXTI Interrupt to the lowest priority */ NVIC_InitStructure.NVIC_IRQChannel = BUTTON_IRQn[Button]; NVIC_InitStructure.NVIC_IRQChannelPriority = 0x03; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure); } } |
Ostatnia funkcja z dostępnych w omawianym pliku pobiera stan linii PA0 i zwraca wartość SET (przycisk włączony) lub RESET (przycisk wyłączony):
1 2 3 4 |
uint32_t STM_EVAL_PBGetState(Button_TypeDef Button){ /* There is no Wakeup button on STM32f0-Discovery Kit */ return GPIO_ReadInputDataBit(BUTTON_PORT[Button], BUTTON_PIN[Button]); } |
Program główny
Algorytm działania programu głównego jest następujący:
- po podłączeniu układu do portu USB komputera zaczyna migać zielona dioda LED z częstotliwością 2,5 Hz,
- jeżeli zostanie wciśnięty przycisk USER i przytrzymany aż do zapalenia się niebieskiej diody LED to zielona dioda zacznie migać szybciej, z częstotliwością 5 Hz,
- ponowne wciśnięcie i przytrzymanie przycisku USER do ponownego zapalenia się niebieskiej diody powoduje, że zielona dioda zostanie wyłączona,
- kolejne wciśnięcia przycisku powodują powtórzenie powyższych działań.
Listing funkcji main() z pliku main.c przedstawiono poniżej:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
int main(void){ RCC_ClocksTypeDef RCC_Clocks; /* Konfiguracja wyprowadzeń z diodami LED3 i LED4 */ STM_EVAL_LEDInit(LED3); STM_EVAL_LEDInit(LED4); /* Konfiguracja wyprowadzenia z przyciskiem USER */ STM_EVAL_PBInit(BUTTON_USER, BUTTON_MODE_GPIO); /* Konfiguracja SysTick do odliczania 1 ms */ RCC_GetClocksFreq(&RCC_Clocks); SysTick_Config(RCC_Clocks.HCLK_Frequency / 1000); /* Wybór trybu migania diody */ BlinkSpeed = 1; while(1){ /* Czy przycisk USER jest wciśnięty? */ if(STM_EVAL_PBGetState(BUTTON_USER)== SET){ /* Jeżeli wciśnięty, to wykonaj poniższy fragment program */ /* BlinkSpeed: 1 -> 2 -> 0, then re-cycle */ /* Włącz niebieską diodę LED4 na czas 1s */ STM_EVAL_LEDOn(LED4); /* Czekaj 1s */ Delay(1000); /* Wyłącz diodę LED4 */ STM_EVAL_LEDOff(LED4); /* Wybierz kolejny tryb migania */ BlinkSpeed++; /* Jeżeli BlinkSpeed == 3 */ if(BlinkSpeed == 3){ /* Wybierz tryb 0 – dioda nie świeci */ BlinkSpeed = 0; } } /* Sprawdź jaki tryb wybrano */ if(BlinkSpeed == 2){ /* LED3 zmienia stan co 100 ms */ STM_EVAL_LEDToggle(LED3); /* Czekaj 100ms */ Delay(100); } else if(BlinkSpeed == 1){ /* LED3 zmienia stan co 200 ms */ STM_EVAL_LEDToggle(LED3); /* Czekaj 200ms */ Delay(200); } else{ /* Wyłącz diodę LED3 */ STM_EVAL_LEDOff(LED3); } } } |
Odliczanie czasu 1 ms jest realizowane przez systemowy timer SysTick, który domyślnie zlicza kolejne takty sygnału zegarowego HCLK do zadanej przez użytkownika wartości, a po przekroczeniu jej zeruje licznik i generuje zdarzenie, które skutkuje pojawieniem się przerwania i wywołaniem funkcji obsługi przerwania SysTick_Handler(), której zawartość może być modyfikowana w pliku SysTick_Handler(). Częstotliwość sygnału HCLK wynosi 48 MHz – szczegóły znajdują się w pliku system_stm32f0xx.c.
Na początku funkcji main() jest deklarowana zmienna RCC_Clocks, która jest przekazywana jako argument do funkcji RCC_GetClocksFreq(). Jest to potrzebne do pobrania informacji o częstotliwościach poszczególnych zegarów w systemie, a w danym przypadku interesuje nas częstotliwość zegara HCLK, która zostanie podzielona przez 1000 i przekazana do funkcji SysTick_Config(). Oznacza to w danym konkretnym przypadku, że jeżeli wartość pola HCLK_Frequency podzielimy przez 1000 to otrzymamy 48,000 i po przekazaniu jej do powyższej funkcji SysTick będzie zliczał 48,000 impulsów sygnału zegarowego HCLK po czym wygeneruje przerwanie. Podczas działania programu przerwanie to będzie generowane przez cały czas co 1 ms.
Wywołanie funkcji Delay() z argumentem w jednostkach 1 ms powoduje zapisanie do zmiennej TimingDelay wartości tego argumentu oraz oczekiwanie w pętli while() dopóki zmienna ta jest różna od 0:
1 2 3 4 |
void Delay(__IO uint32_t nTime){ TimingDelay = nTime; while(TimingDelay != 0); } |
Zmienna TimingDelay jest dekrementowana (jeżeli jest różna od zera) w funkcji TimingDelay_Decrement() wywoływanej co 1 ms z funkcji obsługi przerwania od SysTick:
1 2 3 4 5 |
void TimingDelay_Decrement(void){ if (TimingDelay != 0x00){ TimingDelay--; } } |
Jan Szemiet