Vaja 2: GPIO

Namen tokratne vaje je, da spoznate še preostale podrobnosti naprave za splošno-namenski vhod/izhod (GPIO) na mikrokmilnikih STM32H7. Končni cilj bo zopet branje stanje gumba in prižiganje LED diode, ko je gumb pritisnjen. Gradivo v določenih delih ponovi informacije, ki smo jih spoznali zadnjič, da boste imeli vse zbrano na enem mestu.

GPIO v STM32H7

Gradivo

Povezavi do dokumentacije mikrokrmilnika in razvojne plošče, ki vsebujeta informacije opisane v nadaljevanju:

Upravljanje perifernih ur GPIO naprav

Nastavitve za posamezne pine znotraj GPIO naprave so shranjene v registrih. Kot smo spoznali na zadnji vaji, registri za svoje delovanje potrebujejo uro. Ure GPIO naprav so po resetu izključene, zato bo običajno prva stvar, ki jo bomo naredili, vklop ur naprav, ki jih bomo potrebujemo.

Upravljanje perifernih ur je del ločene naprave 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 enajst bitov tega registra je namenjenih enajstim 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.

Register se nahaja na naslovu 0x580244E0.

RCC AHB4 clock register - RCC_AHB4ENR

GPIO - naslovni prostor

Mikrokrmilnik STM32H750 ima 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.

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 1.

Slika 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
0x1C LCKR Configuration lock register
0x20 AFRL Alternate function low register
0x24 AFRH Alternate function high register

Zadnjih treh registrov zaenkrat ne bomo potrebovali. LCKR omogoča zaklepanje konfiguracije GPIO naprave, da le-te ni mogoče spreminjati do naslednjega reseta. Alternate function registra pa se uporabljata, kadar s pinom uporablja tretja naprava znotraj mikrokrmilnika. Z vrednostmi v teh dveh registrih določimo napravo, ki prevzame nadzor nad pinom.

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, 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 tednih). 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: 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 kombinaciji s stikali na vhodu in open-drain načinu izhoda (več o tem v naslednjem odstavku). Pull-up upor povezuje pin z virom pozitivne napetosti (3,3V ali 5V), pull-down pa z ozemljitvijvo (0V).

Imamo tri možne nastavitve za vsak pin: pull-up upor, pull-down upor ali brez uporov. Posledično za vsak pin porabimo dva bita v registru. Vrednost 00 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. Po resetu so biti registra na vrednosti 0.

Slika 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 je, da omogoča hitrejše prehode med visokim in nizkim logičnim nivojem, slabost pa da porabi 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 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 (vodilo).

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

Slika 4: OTYPER - Output type register

Za uporabi LED lahko uporabimo oba tipa izhoda. Pozorni moramo biti da pri open-drain tipu izhoda uporabimo tudi pull-up upor.

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 2MHz, zelo visoka pa 100MHz ali več. Višje hitrosti uporabimo le kadar to zahteva priklopljena naprava na izhodu, sicer 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. Po resetu so biti registra na vrednosti 0.

Slika 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 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.

Slika 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).

Slika 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 (glej Slika 9). V BSRR je možno le pisati, branje ni dovoljeno.

Slika 8: BSRR - Bit set/reset register

Slika 9: Shema delovanja BSRR v navezavi z ODR

Kadar 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); // pobrisi 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

Shema GPIO

Slika 10 prikazuje shemo vhodno/izhodne stopnje z vsemi zgoraj omenjenimi registri.

Slika 10: Poenostavljena shema vhodno/izhodne stopnje z registri

LED in gumb na STM32H750 Discovery razvojni plošči

Še enkrat ponovimo pine za LED in gumb na razvojni plošči:

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 - uporaba vseh registrov za inicializacijo LED

Podobno kot na zadnji vaji, bomo inicializirali LED na pinu PD3. Le da bomo tokrat nastavili vse registre. Pin bomo inicializirali kot izhod tipa push-pull brez dodatnih uporov. Hitrost osveževanja bo najnižja.

Začnemo kot vedno z vklopom periferne ure, enako kot zadnjič.

#define RCC_AHB4ENR ((volatile uint32_t *)0x580244E0)

*RCC_AHB4ENR = *RCC_AHB4ENR | (1 << 3); // postavimo GPIODEN

Ker bomo uporabili vse registre, najprej zapišemo vse naslove naprave GPIOD:

#define GPIOD_MODER ((volatile uint32_t *)0x58020C00)
#define GPIOD_OTYPER ((volatile uint32_t *)0x58020C04)
#define GPIOD_OSPEEDR ((volatile uint32_t *)0x58020C08)
#define GPIOD_PUPDR ((volatile uint32_t *)0x58020C0C)
#define GPIOD_IDR ((volatile uint32_t *)0x58020C10)
#define GPIOD_ODR ((volatile uint32_t *)0x58020C14)
#define GPIOD_BSRR ((volatile uint32_t *)0x58020C18)

Sedaj inicializirajmo PD3:

// pin 3 inicializiramo kot izhod
*GPIOD_MODER = *GPIOD_MODER & ~(3 << (2 * 3));
*GPIOD_MODER = *GPIOD_MODER | (1 << (2 * 3));

// pobrisemo bit 3 v OTYPER -> pin 3 deluje kot push-pull izhod 
*GPIOD_OTYPER = *GPIOD_OTYPER & ~(1 << 3);

// pobrisemo bit 3 v OSPEEDR -> pin 3 deluje z najpocanejso hitrostjo
// osvezevanja
*GPIOD_OSPEEDR = *GPIOD_OSPEEDR & ~(1 << 3);

// pobrisemo bita 6 in 7 v PUPDR -> pin 3 deluje brez dodatnih uporov
*GPIOD_PUPDR = *GPIOD_PUPDR & ~(3 << (2 * 3));

Kot rečeno imamo sedaj za prižiganje/ugašanje LED dve možnosti. Prva je neposredno delo z ODR, kot zadnjič:

// prižgemo LED
*GPIOD_ODR = *GPIOD_ODR | (1 << 3);

// ugasnimo LED
*GPIOD_ODR = *GPIOD_ODR & ~(1 << 3);

Druga je uporaba BSRR registra:

// prižgemo LED
*GPIOD_BSRR = 1 << 3;

// ugasnimo LED
*GPIOD_BSRR = 1 << (3 + 16);

Uporaba struktur za registre naprave

Če bi želeli sedaj inicializirati še ostale pine, bi morali zopet dodati sedem konstant za naslove za vsakega izmed pinov, saj vsak pripada drugi napravi. Že pri štirih pinih bi to hitro postalo nepregledno, možnost za napake pa bi se bistveno povečala. Zato bomo ubrali pristop s strukturami. Najprej definiramo strukturo za GPIO napravo:

typedef struct {
  volatile uint32_t MODER;
  volatile uint32_t OTYPER;
  volatile uint32_t OSPEEDR;
  volatile uint32_t PUPDR;
  volatile uint32_t IDR;
  volatile uint32_t ODR;
  volatile uint32_t BSRR;
} GPIO_device;

Nato zapišemo zgolj naslove za začetke GPIO naprav:

#define GPIOC ((GPIO_device *)0x58020800)
#define GPIOD ((GPIO_device *)0x58020C00)
#define GPIOI ((GPIO_device *)0x58022000)
#define GPIOJ ((GPIO_device *)0x58022400)

Inicializacijo in uporabo pina PD3 bi sedaj zapisali kot:

/****   inicializacija   ****/

// pin 3 inicializiramo kot izhod
GPIOD->MODER = GPIOD->MODER & ~(3 << (2 * 3));
GPIOD->MODER = GPIOD->MODER | (1 << (2 * 3));

// pobrisemo bit 3 v OTYPER -> pin 3 deluje kot push-pull izhod 
GPIOD->OTYPER = GPIOD->OTYPER & ~(1 << 3);

// pobrisemo bit 3 v OSPEEDR -> pin 3 deluje z najpocanejso hitrostjo
// osvezevanja
GPIOD->OSPEEDR = GPIOD->OSPEEDR & ~(1 << 3);

// pobrisemo bita 6 in 7 v PUPDR -> pin 3 deluje brez dodatnih uporov
GPIOD->PUPDR = GPIOD->PUPDR & ~(3 << (2 * 3));


/****   uporaba   ****/

// prizgemo LED
GPIOD->BSRR = 1 << 3;

// ugasnimo LED
GPIOD->BSRR = 1 << (3 + 16);

Izdelava knjižnice za uporabo GPIO naprav

Namesto, da bi se vsakič ukvarjali neposredno z registri, si bomo sedaj pripravili makroje in funkcije, ki nam bojo omogočale lažjo uporabo.

Začeli bomo z makroji za vklop ur GPIO naprav:

#define RCC_AHB4ENR ((volatile uint32_t *)0x580244E0)

#define GPIOC_CLK_ENABLE() (*RCC_AHB4ENR |= (1 << 2))
#define GPIOD_CLK_ENABLE() (*RCC_AHB4ENR |= (1 << 3))

Za inicializacijo bomo za začetek izdelali poenostavljeno funkcijo, ki bo sprejela argumente o tem katero napravo, kateri pin in ali želimo da je pin inicializiran kot izhod ali kot vhod. Prototip funkcije bo torej:

void GPIO_Init(GPIO_device *gpio, uint32_t pin, uint32_t mode);

Pin bomo podali kot številko, za način delovanja pa si bomo izdelali konstante:

#define GPIO_MODE_INPUT 0
#define GPIO_MODE_OUTPUT 1

Primeri klicov funkcij za gumb (PC13) in LED (PD3) bodo torej:

GPIO_Init(GPIOC, 13, GPIO_MODE_INPUT);
GPIO_Init(GPIOD, 3, GPIO_MODE_OUTPUT);

V funkciji zgolj posplošimo vrstice, ki smo jih do sedaj že zapisali večkrat. Za način delovanja vedno nastavljamo dva bita, ki sta pred klicem funkcije lahko poljubnih vrednosti. Zato bomo vedno najprej pobrisali vrednosti bitov, ki jih bomo potem prepisali.

void GPIO_Init(GPIO_device *gpio, uint32_t pin, uint32_t mode) {
  // pobrisemo oba bita
  gpio->MODER = gpio->MODER & ~(3 << (2 * pin));
  // nastavimo na zeljeno vrednost
  gpio->MODER = gpio->MODER | (mode << (2 * pin));
}

V primeru da ima mode vrednost GPIO_MODE_INPUT, torej 00, druga vrstica ne naredi ničesar. Alternativni pristop bi bil, da preverjamo vrednost mode, kot je prikazano spodaj. Vendar, ker je koda v zgornjem primeru enostavnejša in bolj pregledna, dodatna vrstica pa ni pretirano časovno potratna, se bomo raje izognili pisanju vejitev.

gpio->MODER = gpio->MODER & ~(3 << (2 * pin));
if (mode == GPIO_MODE_OUTPUT) {
  gpio->MODER = gpio->MODER | (mode << (2 * pin));
}

Pripravimo si še funkcijo za nastavljanje vrednosti izhoda. Uporabili bomo register BSRR. V primeru, da bo željena vrednost enica, bomo zapisali enico na spodnjih 16 bitov, sicer pa na zgornjih 16.

void GPIO_WritePin(GPIO_device *gpio, uint32_t pin, uint32_t value) {
  if (value == 1) {
    gpio->BSRR = 1 << pin;
  } else {
    gpio->BSRR = 1 << (pin + 16);
  }
}

V funkciji za branje stanje pina ravno tako le posplošimo to, kar smo počeli na zadnji vaji. Vračali bomo enico, če je iskani pin 1, in ničlo sicer.

uint32_t GPIO_ReadPin(GPIO_device *gpio, uint32_t pin) {
  if (gpio->IDR & (1 << pin)) {
    return 1;
  } else {
    return 0;
  }
}

Spodnje vrstice bodo tako sedaj inicializirale gumb in LED ter prižgale LED, ko bo gumb pritisnjen.

GPIO_Init(GPIOC, 13, GPIO_MODE_INPUT);
GPIO_Init(GPIOD, 3, GPIO_MODE_OUTPUT);

while(1) {
  if (GPIO_ReadPin(GPIOC, 13)) {
    GPIO_WritePin(GPIOD, 3, 1);
  } else {
    GPIO_WritePin(GPIOD, 3, 0);
  }
}

Naloga 2

Dokončajte izdelavo funkcij za delo z GPIO:

  • dodajte manjkajoče makroje za vklop ur GPIO naprav,
  • dopolnite funkcijo GPIO_Init tako, da bo podpirala inicializacijo preostalih nastavitev GPIO pinov (tip izhoda, pull-up/pull-down upor, hitrost osveževanja). Uporabite konstante zapisane spodaj.
  • z dokončanimi funkcijami inicializirajte pine za LED in gumb. Pina PD3 in PI13 inicializirajte kot izhod tipa push-pull brez dodatnih uporov. Pin PJ2 inicializirajte kot izhod tipa open-drain s pull-up uporom. Gumb inicializirajte kot vhod,
  • napišite program, ki bo po pritisku gumba prižigal in ugašal LED kot pri nalogi iz vaje 1.
#define GPIO_OUTPUT_TYPE_PUSH_PULL 0
#define GPIO_OUTPUT_TYPE_OPEN_DRAIN 1

#define GPIO_NO_PULL 0
#define GPIO_PULL_UP 1
#define GPIO_PULL_DOWN 1

#define GPIO_SPEED_LOW 0
#define GPIO_SPEED_MEDIUM 1
#define GPIO_SPEED_HIGH 2
#define GPIO_SPEED_VERY_HIGH 3