Vaja 2: GPIO

Namen tokratne vaje je, da spoznate delo s splošno-namenskim vhodom/izhodom (GPIO) na mikrokmilnikih STM32H7, ki bazirajo na mikrokmilnikih Arm Cortex M7. Spoznali bomo tudi STM-ovo knjižnico HAL. Končni cilj bo, da poizkušamo brati stanje gumba in prižigati LED diode, ko je gumb pritisnjen.

Priprava okolja in projekta v STM32Cube IDE

Pred uporabo orodja STM32CubeIDE si morate ustvariti račun na st.com. Aktivni račun potrebujete za uporabo orodja.

  1. Zaženite STM32CubeIDE in izberite vaš direktorij za delovno okolje (workspace) ter izberite Launch.
  2. Izberite MyST -> Login ter vpišite e-mail in geslo. V primeru, da ste že vpisani, se vam ni potrebno ponovno prijavljati.
  3. Ustvarite nov projekt z File -> New -> STM32 Project
  4. Izberite zavihek MCU/MPU Selector ter v Commercial Part Number vpišite STM32H750XBH6 (glej sliko spodaj). V seznamu desno spodaj izberite iskani MCU ter klinikte Next.

  1. Vpišite poljubno ime projekta (izogibajte se šumnikom in imenom s presledki) ter kliknite Finish. Ostale nastavitve pustite na izbranih vrednostih.
  2. Če se vam pojavi še kakšno pojavno okno, izberite Yes.
  3. Za debuggiranje izberite Run -> Debug. Če se pojavi dodatno pojavno okno, izberite OK.

Okolje za debuggiranje je zelo podobno temu, ki smo ga uporabljali na prvih dveh vajah.

V primeru, da se pri koraku 7 pojavi pojavno okno ST-LINK firmware verification, izberite Yes. Pojavilo se bo odprlo novo pojavno okno, ki je prikazano spodaj. Kliknite Open in update mode ter nato Upgrade. Nato zaprite odprto okno in ponovno izvedite 7. korak.

Gradivo

Nekaj povezav do dokumentacije mikrokrmilnika in razvojne plošče, ki vsebuje informacije opisane v nadaljevanju:

GPIO v STM32H7

Naprava za splošno namenski vhod/izhod (GPIO) v STM32H7 ima malo večji nabor nastavitev in je posledično malo bolj kompleksna kot tista, ki smo jo spoznali pri FE310 RISC-V procesorju. Na mikrokrmilnikih STM32H750, ki jih bomo uporabljali na vajah, je na voljo 176 GPIO pinov, ki so razdeljeni v skupine po 16 pinov. Vsaka skupina 16-ih pinov pripada ločeni GPIO napravi, ki so označene s črko: GPIOA, GPIOB, GPIOC, in tako dalje do GPIOK. Pini znotraj naprave so označeni s številkami od 0 do 15.

Kadar bomo govorili o pinih, nam torej ne bo zadostovala zgolj številska oznaka (19, 21, …) kot pri FE310. Uporabljali bomo oznake kot so PA0, PC13 in PI2. Vse oznake se začnejo s črko P, ki nam pove, da govorimo o pinu. Temu sledita črkovna oznaka GPIO naprave ter številska oznaka pina znotraj naprave. Oznaka PC13 torej pomeni, da gre za pin 13 na napravi GPIOC.

Upravljanje perifernih ur GPIO naprav

Za razliko od FE310 RISC-v, registri GPIO naprav nimajo vklopljene ure. Kar pomeni, da pisanje ali branje teh registrov ni mogoče brez da najprej vklopimo njihove ure. Upravljanje perifernih ur je del ločene naprave, ki se imenuje Reset and Clock Control oz krajše RCC. Za GPIO naprave skrbi 32-bitni register RCC_AHB4ENR (RCC AHB4 clock register - register za upravljanje perifernih ur naprav na vodilu AHB4). Spodnjih 11 bitov registra RCC_AHB4ENR je namenjenih 11 GPIO napravam. Bit 0 skrbi za uro naprave GPIOA, bit 1 za uro naprave GPIOB, in tako dalje do bita 10, ki skrbi za uro naprave GPIOK. Enica na posameznem bitu pomeni, da je periferna ura vklopljena.

RCC AHB4 clock register - RCC_AHB4ENR

Naslovni prostor GPIO naprav

Vsaka GPIO naprava ima svoj ločen del naslovnega prostora v velikosti 1kB, v katerem se nahajajo registri za upravljanje 16-ih pinov, ki posamezni napravi pripadajo. Razdelitev naslovnega prostora je vidna na Slika 2.1. Registri naprave GPIOE se tako na primer nahajajo med naslovi 0x58021000 in 0x580213FF.

Slika 2.1: Razdelitev naslovnega prostora za GPIO naprave

V naslovnem prostoru vsake GPIO naprave se nahaja devet 32-bitnih registrov, s pomočjo katerih lahko konfiguriramo vse kar se tiče splošno-namenskih pinov:

Registri GPIO naprav
Odmik Kratko ime Polno ime
0x00 MODER Mode register
0x04 OTYPER Output type register
0x08 OSPEEDR Output speed register
0x0C PUPDR Pull-up/pull-down register
0x10 IDR Input data register
0x14 ODR Output data register
0x18 BSRR Bit set/reset register
0x20 AFRL Alternate function low register
0x24 AFRH Alternate function high register

V nadaljevanju si bomo pogledali kako pine inicializiramo (jih damo željene nastavitve) ter kako jih po inicializaciji uporabimo.

Inicializacija GPIO naprave - vhod/izhod

Za inicializacijo pinov GPIO naprave kot vhod ali izhod uporabimo prve štiri zgoraj navedene registre: MODER, OTYPER, OSPEEDR in PUPDR.

MODER - mode register

Register MODER je namenjen nastavljanju načina delovanja pina - posamezen pin lahko deluje kot vhod, izhod, načinu alternativne funkcije ali analognem načinu. Ker imamo štiri možne nastavitve, za vsak pin potrebujemo dva bita. Kot vidite na Slika 2.2, sta bita 0 in 1 namenjena določanju načina delovanja za pin 0, bita 2 in 3 določanju načina delovanja pina 1, in tako dalje do bitov 30 in 31, ki določata način delovanja pina 15. Vrednost 00 na dveh bitih pomeni, da pin deluje kot vhod, vrednost 01 pa pomeni da pin deluje kot izhod. Vpis vrednosti 10 na dva bita vključi način alternativne funkcije. V tem načinu vrednost na pinu določa ena izmed preostalih naprav v mikrokmilniku (več o tem v naslednjih tednig). Vrednost 11 pin vklopi v analognem načinu. Na tokratni vaji nas bosta zanimala predvsem prva dva načina delovanja. Ob resetu je večina pinov, razen redkih izjem, v analognem načinu delovanja.

Slika 2.2: MODER - Mode register

PUPDR - pull-up/pull-down register

Za vsak pin imamo na razpolago t.i. pull-up in pull-down upora. Tovrstni upori so uporabni predvsem za zagotavljanje logičnega nivoja, ko ni drugih zunanjih virov, ali le-ti niso aktivni. Največkrat jih uporabljamo v povezavi z vhodom. Pull-up upor povezuje pin z virom pozitivne napetosti (3,3V ali 5V), pull-down pa z ozemljitvijvo (0V). Imamo torej tri možne nastavitve za vsak pin (pull-up, pull-down, brez dodatnih uporov), sta za vsak pin zopet uporabljena dva bita. Vrednost 00 na dveh bitih pomeni, da ne želimo uporabiti nobenega izmed obeh uporov, vrednost 01, da želimo uporabiti pull-up upor, vrednost 10 pa izbere uporabo pull-down upora. Vrednost 11 je rezervirana in ni v uporabi.

Slika 2.3: PUPDR - Pull-up/pull-down register

OTYPER - output type register

V primeru, da pin uporabimo kot izhod, imamo na voljo dva različna tipa zagotavljanja logičnih nivojev na izhdou: push-pull in open-drain. Pri tipu push-pull pin aktivno vleče izhodni logični nivo k visoki ali nizki vrednosti. Prednost tega tipa izhoda je, da omogoča hitrejše prehode med visoko in nizko vrednostjo, slabost pa da porabi bistveno več energije v visokem logičnem nivoju.

Pri open-drain pin aktivno vleče le proti nizki logični vrednosti, za zagotavljanje visoke vrednosti pa uporabimo pull-up upor. To ima za posledico bistveno večjo porabo energije kadar je pin v nizkem logičnem stanju. Open-drain način izhoda se uporablja predvsem kadar je na pin priklopljenih več naprav (preko vodila).

Ker imamo zgolj dve nastavitvi, za nastavitev posameznega pina zadošča 1 bit. Zato v registru OTYPER uporabljamo zgolj spodnjih 16 bitov. Ničla pomeni, da pin deluje v načinu push-pull, enica pa open-drain.

Slika 2.4: OTYPER - Output type register

OSPEEDR - output speed register

V primeru, da pin uporabimo kot izhod, lahko nastavimo tudi hitrost osveževanja vrednosti na pinu. Na voljo so štiri hitrosti: nizka (low), srednja (medium), visoka (high) in zelo visoka (very high). Natančna frekvenca za omenjene štiri nastavitve je odvisna od frekvenc ur na vodilu GPIO naprav. Nizka hitrost je običajno okol 2MHz, zelo visoka pa 100MHz ali več. Višje hitrosti uporabimo le kadar to zahteva priklopljena naprava na izhodu, sicer vedno uporabimo najnižjo hitrost. Pri višjih hitrostih se namreč pojavi več šuma, prav tako se bistveno poviša poraba energije.

Hitrost osveževanj določamo v registru OSPEEDR, za vsak pin uporabimo dva bita. Če na bita zapišemo ničli, s tem izberemo nizko hitrost osveževanje, enica srednjo hitrost osveževanja, dvojka visoko ter trojka zelo visoko hitrost osveževanja.

Slika 2.5: OSPEEDR - Output speed register

Uporaba vhoda in izhoda

Inicializaciji naprav sledi uporaba. Če pin uporabljamo kot vhod, nas zanima logična vrednost na vhodu (branje stanje pina). V primeru uporabe kot izhod, pa želimo nastavljati logično vrednost na izhodu.

IDR - Input data register

Register IDR je sicer 32-biten, vendar uporabljamo zgolj spodnjih 16 bitov (po enega za vsak pin). Kot vidite v Slika 2.6, so biti označeni s črko r, kar pomeni, da jih lahko samo beremo (read-only). Branje bita n vrne logično vrednosti na pinu n. Ima torej isto funkcijo, kot jo je imel register INPUT_VAL pri FE310 procesorju.

Slika 2.6: IDR - Input data register

ODR - Output data register

Tudi pri registru ODR uporabljamo zgolj spodnjih 16 bitov. Ničla na bitu n pomeni, da bo na pinu n GPIO naprave nizka logična vrednost (običajno 0V). Enica na bitu n pomeni, da bo na pinu n GPIO naprave visoka logična vrednost (običajno 3.3V ali 5V). Register ODR je torej ekvivalent registra OUTPUT_VAL pri FE310 RISC-V procesorju.

Slika 2.7: ODR - Output data register

BSRR - Bit set/reset register

Register BSRR omogoča atomarno postavljanje ali brisanje bitov v registru ODR. Vpis enice v spodnjih 16 bitov postavi enico na istoležnem bitu v ODR, vpis enice v zgornjih 16 bitov pa pobriše bit v registru ODR. Če uporabimo register ODR neposredno, postavimo ali brišemo bit n z naslednjima ukazoma:

GPIO->ODR = GPIO->ODR | (1 << n); // postavi bit
GPIO->ODR = GPIO->ODR & ~(1 << n); // postavi bit

Ta ukaz zahteva branje stare vrednosti ter uporabo logične operacije ali. Z registrom BSRR isto dosežemo z:

GPIO->BSRR = 1 << n; // postavi bit
GPIO->BSRR = 1 << (n + 16); // briši bit - +16 da uporabimo zgornjih 16 bitov

Slika 2.8: BSRR - Bit set/reset register

Shema GPIO

Slika 2.9 prikazuje poenostavljeno shemo vhodno/izhodne stopnje, na kateri vidimo vse zgoraj omenjene registre in kako se uporabljajo, da dosežejo njihove funkcije.

Slika 2.9: Poenostavljena shema vhodno/izhodne stopnje z registri

STM HAL in GPIO

Za uporabo GPIO in vseh naprav, ki jih bomo spoznali, bomo uporabljali STM HAL knjižnico. Organizirana je na način, ki smo ga sponzali na zadnji vaji, ko smo si sami izdelali knjižnico za delo z GPIO.

Vklop ure

Za vklop ure posamezne GPIO naprave so na voljo enostavni makroji, ki so zapisani v obliki __HAL_RCC_GPIOx_CLK_ENABLE():

__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOC_CLK_ENABLE();
__HAL_RCC_GPIOD_CLK_ENABLE();
__HAL_RCC_GPIOE_CLK_ENABLE();
__HAL_RCC_GPIOF_CLK_ENABLE();
__HAL_RCC_GPIOH_CLK_ENABLE();
__HAL_RCC_GPIOI_CLK_ENABLE();
__HAL_RCC_GPIOJ_CLK_ENABLE();
__HAL_RCC_GPIOK_CLK_ENABLE();

Na voljo so tudi makroji za izklop ure GPIO naprav, ki so zapisani v obliki __HAL_RCC_GPIOx_CLK_DISABLE().

Struktura GPIO registrov in naslovi naprav

Knjižnica ima definirano strukturo naprave, ki jo vidite spodaj. Ta vsebuje vse registre, podobno kot smo to naredili na zadnji vaji za FE310. Strukturo najdemo jo v stm32h750xx.h.

typedef struct
{
  __IO uint32_t MODER;    /*!< GPIO port mode register,               Address offset: 0x00      */
  __IO uint32_t OTYPER;   /*!< GPIO port output type register,        Address offset: 0x04      */
  __IO uint32_t OSPEEDR;  /*!< GPIO port output speed register,       Address offset: 0x08      */
  __IO uint32_t PUPDR;    /*!< GPIO port pull-up/pull-down register,  Address offset: 0x0C      */
  __IO uint32_t IDR;      /*!< GPIO port input data register,         Address offset: 0x10      */
  __IO uint32_t ODR;      /*!< GPIO port output data register,        Address offset: 0x14      */
  __IO uint32_t BSRR;     /*!< GPIO port bit set/reset,               Address offset: 0x18      */
  __IO uint32_t LCKR;     /*!< GPIO port configuration lock register, Address offset: 0x1C      */
  __IO uint32_t AFR[2];   /*!< GPIO alternate function registers,     Address offset: 0x20-0x24 */
} GPIO_TypeDef;
__IO

__IO je zgolj redefinicija rezervirane besede volatile. V eni izmed .h datotek knjižnice bomo našli vrstico typedef volatile __IO;.

V isti datoteki so definirani tudi vsi kazalci na naprave:

#define GPIOA               ((GPIO_TypeDef *) GPIOA_BASE)
#define GPIOB               ((GPIO_TypeDef *) GPIOB_BASE)
#define GPIOC               ((GPIO_TypeDef *) GPIOC_BASE)
#define GPIOD               ((GPIO_TypeDef *) GPIOD_BASE)
#define GPIOE               ((GPIO_TypeDef *) GPIOE_BASE)
#define GPIOF               ((GPIO_TypeDef *) GPIOF_BASE)
#define GPIOG               ((GPIO_TypeDef *) GPIOG_BASE)
#define GPIOH               ((GPIO_TypeDef *) GPIOH_BASE)
#define GPIOI               ((GPIO_TypeDef *) GPIOI_BASE)
#define GPIOJ               ((GPIO_TypeDef *) GPIOJ_BASE)
#define GPIOK               ((GPIO_TypeDef *) GPIOK_BASE)

Inicializacija GPIO

Za inicializacijo pinov uporabimo funkcijo HAL_GPIO_Init(GPIO_TypeDef *GPIOx, GPIO_InitTypeDef *GPIO_Init). Prvi argument je tako kot pri zadnji vaji kazalec na napravo (strukturo z registri). Uporabili bomo torej eno izmed konstant, ki so zapisane zgoraj. Drugi argument je kazalec na inicializacijsko strukturo. Ta je definirana v stm32h7xx_hal_gpio.h:

typedef struct
{
  uint32_t Pin;
  uint32_t Mode;
  uint32_t Pull;
  uint32_t Speed;
  uint32_t Alternate;
} GPIO_InitTypeDef;

Za element Pin uporabimo eno izmed pripravljenih konstant za oznako pinov (ali njihovo kombinacijo), kot smo to počeli že na zadnji vaji:

#define GPIO_PIN_0                 ((uint16_t)0x0001)  /* Pin 0 selected    */
#define GPIO_PIN_1                 ((uint16_t)0x0002)  /* Pin 1 selected    */
#define GPIO_PIN_2                 ((uint16_t)0x0004)  /* Pin 2 selected    */
#define GPIO_PIN_3                 ((uint16_t)0x0008)  /* Pin 3 selected    */
#define GPIO_PIN_4                 ((uint16_t)0x0010)  /* Pin 4 selected    */
#define GPIO_PIN_5                 ((uint16_t)0x0020)  /* Pin 5 selected    */
#define GPIO_PIN_6                 ((uint16_t)0x0040)  /* Pin 6 selected    */
#define GPIO_PIN_7                 ((uint16_t)0x0080)  /* Pin 7 selected    */
#define GPIO_PIN_8                 ((uint16_t)0x0100)  /* Pin 8 selected    */
#define GPIO_PIN_9                 ((uint16_t)0x0200)  /* Pin 9 selected    */
#define GPIO_PIN_10                ((uint16_t)0x0400)  /* Pin 10 selected   */
#define GPIO_PIN_11                ((uint16_t)0x0800)  /* Pin 11 selected   */
#define GPIO_PIN_12                ((uint16_t)0x1000)  /* Pin 12 selected   */
#define GPIO_PIN_13                ((uint16_t)0x2000)  /* Pin 13 selected   */
#define GPIO_PIN_14                ((uint16_t)0x4000)  /* Pin 14 selected   */
#define GPIO_PIN_15                ((uint16_t)0x8000)  /* Pin 15 selected   */
#define GPIO_PIN_All               ((uint16_t)0xFFFF)  /* All pins selected */

Za element Mode so na voljo spodnje nastavitve. Kot vidimo s tem elementom nastavimo ali bo pin deloval kot vhod, izhod, alternativna funkciaj ali v analognem načinu. Hkrati pa, v primeru, da uporabljamo pin kot izhod, nastavimo tudi kakšen tip izhoda bomo uporabljali: push-pull (PP) ali open-drain (OD). Obstaja še nekaj dodatnih možnosti, ki niso navedena spodaj. Te bomo spoznali v nadaljevanju semestra.

GPIO_MODE_INPUT     // vhod
GPIO_MODE_OUTPUT_PP // izhod push-pull
GPIO_MODE_OUTPUT_OD // izhod open-drain
GPIO_MODE_AF_PP     // alternativna funkcija push-pull
GPIO_MODE_AF_OD     // alternativna funkcija open-drain
GPIO_MODE_ANALOG    // analogni način

Za element Pull so na voljo spodnje nastavitve, ki sovpadajo neposredno z vrednostmi v registru PUPDR:

GPIO_NOPULL    // brez pull-up ali pull-down upora
GPIO_PULLUP    // vklopi pull-up upor
GPIO_PULLDOWN  // vklopi pull-down upor

Nastavitve, ki jih lahko uporabimo za Speed podobno sovpadajo neposredno z vrednostmi v registru OSPEEDR:

GPIO_SPEED_FREQ_LOW       // nizka hitrost osveževanja
GPIO_SPEED_FREQ_MEDIUM    // srednja hitrost osveževanja
GPIO_SPEED_FREQ_HIGH      // visoka hitrost osveževanja
GPIO_SPEED_FREQ_VERY_HIGH // zelo visoka hitrost osveževanja

O Alternate elementu init strukture bomo več povedali v nadaljevanju semestra, zaenkrat ga namreč ne potrebujemo.

Inicializacija pina PB12 kot izhod tipa push-pull brez pull-up ali pull-down uporov bi zapisali kot:

__HAL_RCC_GPIOB_CLK_ENABLE();     // vklop ure naprave GPIOB

GPIO_InitTypeDef init = {0};
init.Pin = GPIO_PIN_12;           // pin 12
init.Mode = GPIO_MODE_OUTPUT_PP;  // izhod push-pull
init.Pull = GPIO_NOPULL;          // brez pull-up/down uporov
init.Speed = GPIO_SPEED_FREQ_LOW; // nizka hitrost osveževanja
HAL_GPIO_Init(GPIOB, &init);      // naprava GPIOB

Nastavljanje izhoda

Za nastavljanje izhoda imamo na voljo dve funkciji:

void HAL_GPIO_WritePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin, GPIO_PinState PinState);
void HAL_GPIO_TogglePin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

Funkcija HAL_GPIO_WritePin omogoča nastavljanje logične vrednosti pina na poljubno vrednost, funkcija HAL_GPIO_TogglePin pa negira trenutno logično vrednost pina. Za stanje pina uporabimo eno izmed možnosti definiranih v enumu GPIO_PinState:

typedef enum
{
  GPIO_PIN_RESET = 0U,
  GPIO_PIN_SET
} GPIO_PinState;

Nekaj primerov uporabe obeh funckij:

HAL_GPIO_WritePin(GPIOA, GPIO_PIN_0, GPIO_PIN_SET); // nastavimo pin PA0 na 1
HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1 | GPIO_PIN_10, GPIO_PIN_RESET); // nastavimo pin PB1 in PB10 na 0
HAL_GPIO_TogglePin(GPIOK, GPIO_PIN_11); // obrnemo logično vrednost pina PK11

Branje vhoda

Za branje stanja na vhodu imamo na voljo funkcijo:

GPIO_PinState HAL_GPIO_ReadPin(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin);

Funkcija vrne GPIO_PIN_SET v primeru, da je vrednost na vhodu podanega pina enica. V nasprotnem primeru vrne GPIO_PIN_RESET.

LED in gumb na STM32H750 Discovery razvojni plošči

STM32H750 Discovery plošča nam ponuja nekaj pinov, ki jih lahko uporabimo kot enostaven digitalen vhod ali izhod:

Oznaka pina Funkcija
PC13 Modri gumb
PD3 Zelena LED - prižgana ob enici na izhodu
PJ2 Zelena LED - prižgana ob ničli na izhodu
PI13 Rdeča LED - prižgana ob ničli na izhodu

Primer

Koda za prižig LED diode na pinu PD3:

__HAL_RCC_GPIOD_CLK_ENABLE();

GPIO_InitTypeDef init = {0};
init.Pin = GPIO_PIN_3;
init.Mode = GPIO_MODE_OUTPUT_PP;
init.Pull = GPIO_NOPULL;
init.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOD, &init);

HAL_GPIO_WritePin(GPIOD, GPIO_PIN_3, GPIO_PIN_SET);

Lahko bi izbrali tudi open-drain tip izhoda. V tem primeru je potrebno uporabiti pull-up upor:

__HAL_RCC_GPIOD_CLK_ENABLE();

GPIO_InitTypeDef init = {0};
init.Pin = GPIO_PIN_3;
init.Mode = GPIO_MODE_OUTPUT_OD;
init.Pull = GPIO_PULLUP;
init.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOD, &init);

HAL_GPIO_WritePin(GPIOD, GPIO_PIN_3, GPIO_PIN_SET);
Naloga 2.1

Napišite program, ki ob pritisku gumba vklopi vse 3 LED diode. Ko gumb spustimo naj se vse LED ugasnejo.

Naloga 2.2

Ob pritisku gumba naj začnejo 10 sekund utripati vse 3 LED diode (pol sekunde prižgane, pol sekunde ugasnjene). Za zakasnitev uporabite funckijo HAL_Delay().