Biblioteki CMSIS dla STM32 ver. 3.1.0
Operowanie bezpośrednio na rejestrach 32-fbitowego mikrokontrolera nie należy do zadań łatwych. Mimo, że napisanie aplikacji z wykorzystaniem nazw rejestrów jest możliwe, to wprowadzanie zmian do istniejącego programu po upływie choćby krótkiego czasu, jest w zasadzie niemożliwe do wykonania w racjonalnym czasie. Z tego powodu programiści coraz częściej wykorzystują predefiniowane funkcje wykonujące operacje na zespołach wielu rejestrów. Do mikrokontrolerów STM32 (podobnie jak i do innych, wyposażonych w rdzeń Cortex-M3) producent dostarcza kompletne biblioteki zgodne z zaleceniami CMSIS (Cortex Microcontroller Software Interface Standard).
Więcej informacji o standardzie CMSIS (Cortex Microcontroller Software Interface Standard) znajduje się na stronie internetowej firmy ARM. |
Umożliwiają one pełną kontrolę nad mikrokontrolerami z rodziny STM32, bez konieczności szczegółowego poznawania tajników wbudowanych w nie peryferiów, co nie wyklucza oczywiście możliwości bezpośredniego operowania na rejestrach.
Standard CMSIS (Cortex Microcontroller Software Interface Standard) jest to uniwersalny interfejs programowy, stworzony przez firmę ARM, który umożliwia komunikację z peryferiami i rdzeniem Cortex za pomocą ustandaryzowanych funkcji i definicji. CMSIS dostarcza mechanizmów do obsługi układów peryferyjnych, systemów operacyjnych czasu rzeczywistego oraz aplikacji wykorzystujących interfejsy komunikacyjne: Ethernet, UART oraz SPI. Zestandaryzowane interfejsu dostępu do układów peryferyjnych oraz samego rdzenia ma na celu ułatwienie przenoszenia aplikacji pomiędzy mikrokontrolerami różnych producentów, jak również uproszczenie procesu tworzenia aplikacji. |
Strukturę interfejsu CMSIS i jego miejsce w aplikacji przedstawiono na rys.1.
Rys. 1. Budowa interfejsu CMSIS i jego miejsce w aplikacji
Standard CMSIS został podzielony na dwie podstawowe warstwy: Core Peripheral Access Layer oraz Middleware Access Layer. Pierwsza warstwa zawiera definicje nazw oraz umożliwia dostęp do rejestrów rdzenia oraz urządzeń peryferyjnych, natomiast druga udostępnia mechanizmy do współpracy z interfejsami komunikacyjnymi.
Dodatkowo, wymienione wyżej warstwy zostały rozszerzone przez producentów mikrokontrolerów, którzy współpracowali przy tworzeniu CMSIS, o dwie „warstwy”: Device Peripheral Access Layer oraz Access Functions for Peripherals.
CMSIS dla STM32
Biblioteka STM32F10x Standard Peripherals Library v3.1.0 (StdPeriph_Lib) została napisana zgodnie z formatem Doxygen, co znacznie upraszcza proces tworzenia dokumentacji oraz jej używanie. Cala dokumentacja omawianej biblioteki została umieszczona w pliku pomocy, a nie, jak było to dotychczas praktykowane przez STMicroelectronics w osobnym pliku PDF. Nowy sposób dostarczania dokumentacji ułatwił przeglądanie jej zawartości oraz wyszukiwanie informacji.
Wraz z archiwum biblioteki StdPeriph_Lib otrzymujemy szablony projektów do trzech najpopularniejszych kompilatorów (środowisk): IAR, Keil oraz ARM-GCC.
Strukturę plików projektu, dla przykładu wykorzystującego przetwornik A/C, zbudowanego na bazie szablonu projektu dla środowiska uVision przedstawiono na rys. 2.
Rys. 2. Struktura plików środowiska uVision dla projektu wykorzystującego przetwornik A/C
Jeśli szablon projektu zostanie skopiowany do innego katalogu, to należy poinformować kompilator, gdzie ma szukać stosownych plików oraz należy dodać wykorzystywane źródła do projektu. Dokonujemy tego (w środowisku uVision) poprzez prawe kliknięcie na nazwie projektu i wybór z menu kontekstowego opcji Manage Component (rys. 2). Następnie odszukujemy na dysku właściwe pliki i je dodajemy.
Jak wyżej wspomniano, aby projekt poprawnie się kompilował, należy zaktualizować miejsca (ścieżki), gdzie kompilator będzie szukał plików nagłówkowych *.h. W tym celu wybieramy menu Project/Options for Target…, po czym otworzy się okno, w którym na zakładce C/C++ edytujemy ścieżkę poszukiwania plików nagłówkowych.
Struktura biblioteki
Pliki w bibliotece podzielono na dwa bloki (moduły), ulokowane w katalogach \STM32F10x_StdPeriph_Driver oraz \CMSIS. Drzewo plików i katalogów modułu CMSIS biblioteki Standard Peripherals Library pokazano na rys. 3, natomiast drzewo modułu STM32F10x_StdPeriph_Driver na rys. 4.
Rys. 3. Struktura modułu CMSIS
Rys. 4. Struktura modułu STM32F10x_StdPefriph_Driver
Dla każdej z podrodzin mikrokontrolerów STM32 przygotowano oddzielne pliki startowe. W tab. 1 pokazano, który plik startowy należy wykorzystać z którą podrodziną STM32. Takich zestawów plików startowych otrzymujemy wraz z biblioteką API trzy – dla trzech najpopularniejszych kompilatorów: Keil (ARM), IAR oraz GCC.
Tab. 1. Pliki startowe dla podrodzin STM32
Plik | Podrodzina STM32 |
startup_stm32f10x_cl.s | Connectivity line |
startup_stm32f10x_hd.s | High Density |
startup_stm32f10x_ld.s | Low Density |
startup_stm32f10x_md.s | Medium Density |
Plik system_stm32f10x.c zawiera definicje i funkcje, które mogą być wykorzystane do konfiguracji sygnału zegarowego mikrokontrolera. Korzystając z funkcji zawartych w pliku należy w pierwszej kolejności wybrać, używając komentarzy, z jaką częstotliwością MCU ma pracować.
Fragment, który należy w tym celu poddać edycji przedstawiono na list. 1. W kodzie aplikacji wystarczy teraz wywołać funkcję SystemInit(), a jej wykonanie spowoduje skonfigurowanie wszystkich sygnałów zegarowych (zegar systemowy, HCLK, PCLK2, PCLK1).
List. 1. Fragment pliku system_stm32f10x.c -f definicje częstotliwości zegara systemowego, wykorzystywane przez funkcję SystemInit()
/* #define SYSCLK_FREQ_HSE HSE_Value */ /* #define SYSCLK_FREQ_24MHz 24000000 */ /* #define SYSCLK_FREQ_36MHz 36000000 */ /* #define SYSCLK_FREQ_48MHz 48000000 */ /* #define SYSCLK_FREQ_56MHz 56000000 */ #define SYSCLK_FREQ_72MHz 72000000
Do modułu STM32F10x_StdPeriph_Driver należą pliki, które zgodnie ze swoją nazwą zawierają funkcje API związane z poszczególnymi urządzeniami peryferyjnymi – patrz rys. 4. Tych plików bibliotecznych nie należy edytować.
Do każdego projektu są dołączone jeszcze dwa istotne pliki: stm32f10x_conf.h oraz stm32f10x_it.c. W pierwszym pliku nagłówkowym włączamy lub wyłączamy (za pomocą komentarzy) dołączanie do projektu plików nagłówkowych z modułu STM32F10x_StdPeriph_Driver -f patrz list. 2.
List. 2. Obszar pliku stm32f10x_conf.h, w którym za pomocą komentarzy włącza się lub wyłącza dołączanie plików nagłówkowych
/* #include "stm32f10x_adc.h" */ /* #include "stm32f10x_bkp.h" */ /* #include "stm32f10x_can.h" */ /* #include "stm32f10x_crc.h" */ /* #include "stm32f10x_dac.h" */ /* #include "stm32f10x_dbgmcu.h" */ #include "stm32f10x_dma.h" /* #include "stm32f10x_exti.h" */ /* #include "stm32f10x_flash.h" */ /* #include "stm32f10x_fsmc.h" */ #include "stm32f10x_gpio.h" /* #include "stm32f10x_i2c.h" */ /* #include "stm32f10x_iwdg.h" */ /* #include "stm32f10x_pwr.h" */ #include "stm32f10x_rcc.h" /* #include "stm32f10x_rtc.h" */ /* #include "stm32f10x_sdio.h" */ #include "stm32f10x_spi.h" /* #include "stm32f10x_tim.h" */ /* #include "stm32f10x_usart.h" */ /* #include "stm32f10x_wwdg.h" */ /* #include "misc.h" */
Z punktu widzenia programisty jednym z najważniejszych plików jest stm32f10x_it.c, w którym umieszcza się wszystkie funkcje obsługi przerwań. Jego nieco zmodyfikowany fragment wraz z pustymi funkcjami jest przestawiony na list. 3. Jak widać, jest to zestaw pustych funkcji, które jeżeli wystąpi odpowiednie przerwanie, są wywoływane. W stosunku do wersji tego pliku dostarczanej przez firmę STMicroelectronics zostały usunięte komentarze, które jak pokazała praktyka okazały się w tym miejscu zbędne. Nazwa funkcji obsługi danego przerwania jest już sama w sobie dość wymowna. W efekcie uzyskano bardziej zwięzły kod, nie tracąc tym samym na jego czytelności.
Zadaniem programisty jest umieszczenie w odpowiedniej funkcji obsługi przerwania kodu, jaki ma się wykonać po wystąpieniu określonego zdarzenia. Takie podejście sprawiło, że projekt jest przejrzysty, a obsługa wszystkich wyjątków znajduje się w jednym pliku i nie ma potrzeby samodzielnego definiowania funkcji wywoływanych w chwili, gdy system wykryje nadejście przerwania.
List. 3. Fragment pliku stm32f10x_it.c
void UsageFault_Handler(void) { while (1) {}; }; void SVC_Handler(void) { }; void DebugMon_Handler(void) { }; void PendSV_Handler(void) { }; void SysTick_Handler(void) { };
Konfiguracja urządzeń peryferyjnych
Dla każdego urządzenia, niezależnie od tego czy jest to GPIO, kontroler przerwań, czy jakikolwiek inny element systemu, są stworzone odrębne typy danych. W przypadku portów wejścia/wyjścia nazywają się one: GPIO_TypeDef oraz wykorzystywany do inicjalizacji typ GPIO_InitTypeDef. Dla programisty największe znaczenie ma typ inicjujący, ponieważ to właśnie zmienną tego typu jawnie tworzymy w pisanym kodzie.
Typ GPIO_TypeDef zapewnia dostęp do poszczególnych rejestrów mikrokontrolera i jest wykorzystywany przede wszystkim przez funkcje API, natomiast zmienna typu GPIO_InitTypeDef musi istnieć w każdej aplikacji wykorzystującej porty wejścia/wyjścia, ponieważ jest wykorzystywana do inicjalizowania i konfigurowania portów. Na list. 4 przedstawiono kluczowy fragment kodu odpowiedzialny za konfigurację portu GPIOB.
List. 4. Wykorzystanie API – konfiguracja portu GPIOB
//Wyprowadzenia PB8, PB9 jako wyjscia push - pull GPIO_InitStructure.GPIO_Pin = GPIO_Pin_8 | GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStructure);
Utworzona na początku zmienna GPIO_InitStructure jest strukturą. Inicjowanie pinów, lub w szczególnym przypadku całego portu odbywa się w ten sposób, że programista wypełnia pola struktury, a następnie przekazuje się adres tak przygotowanej zmiennej do funkcji inicjującej.
W przedstawianej sytuacji w naszym kręgu zainteresowań leżą trzy pola struktury GPIO_InitStructure. W pierwszej kolejności ustalamy, które z pinów będą konfigurowane, fnastępnie wybieramy żądany tryb pracy -f w tym przypadku będzie to wyjście typu push-pull. Następnie ustalamy maksymalna prędkość, z jaką będą mogły pracować piny. Tak przygotowaną zmienną należy przekazać przez podanie jej adresu w argumencie do funkcji inicjującej GPIO_Init(). Drugim argumentem, jaki należy funkcji przekazać jest nazwa portu, do jakiego mają być zastosowane wybrane ustawienia.