19 marca 2017

Porty wejścia-wyjścia

Wiele osób zaczynających przygodę z STM32 uważa, że konfiguracja portów jest bardzo skomplikowana, a już na pewno dużo bardziej niż była w układach 8-bitowych. Okazuje się, że to tylko niekorzystne pierwsze wrażenie. Jak zobaczymy w przypadku 32-bitowego STM32F103 wcale nie jest dużo trudniej.

Na początek ważna informacja - zanim użyjemy jakiegokolwiek układu peryferyjnego, musimy go najpierw włączyć. Dawniej nie przywiązywano zbytnio wagi do poboru energii przez mikrokontrolery, a same układy miały relatywnie niewiele peryferiów, więc wszystkie były domyślnie włączone. Obecnie podejście diametralnie się zmieniło i po resecie nasz układ ma wszystko wyłączone. Nie jest to wielki problem, ale trzeba o tym pamiętać.
Dotychczas używaliśmy nieco magicznego kodu:

/* Enable the GPIO Clock */
RCC->APB2ENR |= 0x00000004;

Teraz czas na wyjaśnienie do czego to służyło. Nasz STM32F103 posiada trzy wbudowane magistrale: AHB, APB1 i APB2, do których podłączone są układy peryferyjne. Czym jest magistrala? Ja najbardziej lubię wyobrażać sobie wnętrze starego PC-ta z widocznymi gniazdami ISA:


Świat był wtedy prostszy, wystarczyło zdjąć obudowę i zarówno magistralę, jak i układy do niej podłączone było widać, nawet można było ostrożnie dotknąć. Obecnie wszystko jest zminiaturyzowane i wbudowane w jeden kawałek krzemu. Jednak sama architektura jest podobna. Nasz mikrokonotroler ma w swoim wnętrzu trzy magistrale, a do nich podłączone całkiem sporo modułów.
Teraz chcemy używać portu A, musimy więc go uruchomić. W dokumentacji znajdziemy informację, że jest podpięty do magistrali APB2. Kod który napisaliśmy właśnie to robił - jednak magiczna liczba 0x00000004 nie świadczy dobrze o naszym stylu programowania. Lepiej będzie więc zastąpić ją odpowiednią stałą.
W pliku stm32f10x.h znajdziemy definicje stałych odpowiadających układom peryferyjnym. Poniżej fragment pliku:

#define RCC_APB2ENR_AFIOEN ((uint32_t)0x00000001) /*!< Alternate Function I/O clock enable */
#define RCC_APB2ENR_IOPAEN ((uint32_t)0x00000004) /*!< I/O port A clock enable */
#define RCC_APB2ENR_IOPBEN ((uint32_t)0x00000008) /*!< I/O port B clock enable */
#define RCC_APB2ENR_IOPCEN ((uint32_t)0x00000010) /*!< I/O port C clock enable */
#define RCC_APB2ENR_IOPDEN ((uint32_t)0x00000020) /*!< I/O port D clock enable */

#define RCC_APB2ENR_ADC1EN ((uint32_t)0x00000200) /*!< ADC 1 interface clock enable */

Jak łatwo się domyślić potrzebujemy użyć RCC_APB2ENR_IOPAEN zamiast magicznej wartości. Nasz kod wygląda więc następująco:

/* Enable the GPIO Clock */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

Później będziemy w podobny sposób uruchamiać kolejne peryferia. Skoro port A jest już uruchomiony, czas skonfigurować wyjście dla diody LED. Dla przypomnienia, nasz poprzedni kod wyglądał następująco:

/* Configure the GPIO pin */
GPIOA->CRL = 0x44344444;

Porty STM32F103 są wyposażone 16 wyprowadzeń i oznaczone kolejnymi literami alfabetu A,B,C,D itd. Na każdy pin przypadają 4 bity konfiguracji. W zapisie szesnastkowym wypada więc jedna cyfra na pin, stad zmiana którą wprowadziliśmy polegała na zamianę 4 na 3 w szóstej cyfrze (piny są numerowane od zera). 
Oczywiście była to nieco sztuczka, więc teraz wykonamy konfigurację inaczej.
Najpierw musimy doczytać jakie są opcje konfiguracji wyprowadzeń. W dokumentacji znajdziemy odpowiednią tabelkę:

Wydaje mi się, że właśnie ten nieco zawiły zapis jest przyczyną opinii o skomplikowaniu portów STM32. Spróbujmy więc krok po kroku przeanalizować konfigurację pinu.
Najpierw musimy wybrać, czy chcemy żeby pin pracował jako wejście, czy wyjście. Okazuje się, że zamiast jednego bitu do konfiguracji kierunku musimy ustawić dwa. Są to bity MODE[1:0] i ich wartości mają następujące znaczenie:


Czyli wybierając wejście ustawiamy 00b w bitach MODE[1:0], a w przypadku wyjść mamy do wyboru maksymalną prędkość działania portu. W rzeczywistości jest do prędkość narastania zbocza, im wyższa tym większy będzie pobór prądu oraz emisja zakłóceń. 
W zależności czy port jest wyjściem, czy wejściem pozostałe bity konfiguracji, czyli CNF[1:0] mają inne znaczenia.
Dla portu będącego wejściem mamy do wyboru:


Ustawienie analog przyda nam się, gdy wykorzystamy przetwornik analogowo-cyfrowy. Pozostałe to po prostu ustawienia rezystorów podciągających. Input floating to wejście bez podciągów, czyli tzw. wysoka impedancja, pull-up to tradycyjne podciągnięcie do zasilania, a pull-do do masy.

Dla pinu pracującego jako wyjście opcje konfiguracyjne są następujące:


Bit CNF1 ustala, czy jest to pin ogólnego przeznaczenia (GPIO). Jeśli CNF1=0 wyjście jest sterowane przez nasz program. Dla CNF=1 za sterowanie portu odpowiada podłączony do niego układ peryferyny, np. UART, czy PWM (timer).
Wartość bitu CNF0 określa, czy jest to zwykłe wyjście push-pull, czy open-drain.
Jak widać możliwości wyboru jest trochę więcej niż w starszych układach, ale nie jest to nic skomplikowanego.
Spróbujmy odszyfrować co znaczyły użyte przez nas wcześniej magiczne wartości 4 oraz 3. Napierw zapiszemy je binarnie: 4 = 0100b, 3 = 0011b. Teraz musimy popatrzeć co zawiera rejestr CRL:

Jak widzimy są to konfiguracje pinów od 0 do 7. Nasuwa się pytanie, gdzie podziały się wyższe piny (od 8 do 15)? Są ustwiane w rejestrze CRH.
Spróbujmy odszyfrować wartość 4, czyli 0100b. CNF[1:0] to wyższe bity, czyli CNF1=0, CNF0=1. Natomiast niższe bity opisują tryb MODE[1:0] = 00. Zaglądamy do tabelek i widzimy, że tryb to wejście pływające (input floating). Jest to domyślny tryb pracy większości wyprowadzeń STM32F103. 
Nasza konfiguracja polegała na zmianę bitów odpowiadających pinowi PA5 na 3, czyli CNF[1:0] = 00b, MODE[1:0] = 11b. Z tabelki odczytujemy ustawienie - wyjście typu push-pull o prędkości maksymalnej 50MHz.

W typowym programie nie chcemy konfigurować wszystkich pinów danego portu na raz, spróbujmy więc przepisać nasz program, tak żeby zmieniał ustawienia tylko PA5 pozostawiając resztę pinów w domyślnej konfiguracji. Program może nie jest zbyt czytelny, ale od czego są komentarze:

/* Enable the GPIO Clock */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

/* Configure the GPIO pin */
SET_BIT(GPIOA->CRL, GPIO_CRL_MODE5_0 | GPIO_CRL_MODE5_1); // MODE=11b, output max 50MHz
CLEAR_BIT(GPIOA->CRL, GPIO_CRL_CNF5_0 | GPIO_CRL_CNF5_1); // CNF=00b, output push-pull

Mamy więc nasz program w nieco poprawniejszej wersji:

#include "stm32f10x.h"

int main(void)
{
volatile uint32_t dly;

/* Enable the GPIO Clock */
RCC->APB2ENR |= RCC_APB2ENR_IOPAEN;

/* Configure the GPIO pin */
SET_BIT(GPIOA->CRL, GPIO_CRL_MODE5_0 | GPIO_CRL_MODE5_1); // MODE=11b, output max 50MHz
CLEAR_BIT(GPIOA->CRL, GPIO_CRL_CNF5_0 | GPIO_CRL_CNF5_1); // CNF=00b, output push-pull

while (1) {
GPIOA->BSRR = GPIO_BSRR_BS5; // set PA5
for (dly = 0; dly < 500000; dly++)
;
GPIOA->BRR = GPIO_BRR_BR5; // reset PA5
for (dly = 0; dly < 1000000; dly++)
;
}
}

Jego działanie nie uległo zmianie, dioda nadal pięknie miga. W kolejnej części dodamy obsługę przycisku i zastanowimy się jak uczynić program czytelniejszym.

Brak komentarzy:

Prześlij komentarz