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_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.
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.
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.
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.
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.
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.
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
.
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).
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.
Kadar uporabimo register ODR
neposredno, postavimo ali brišemo bit n
z naslednjima ukazoma:
= GPIO_ODR | (1 << n); // postavi bit
GPIO_ODR = GPIO_ODR & ~(1 << n); // pobrisi bit GPIO_ODR
Ta ukaz zahteva branje stare vrednosti ter uporabo logične operacije ali. Z registrom BSRR isto dosežemo z:
= 1 << n; // postavi bit
GPIO_BSRR = 1 << (n + 16); // briši bit - +16 da uporabimo zgornjih 16 bitov GPIO_BSRR
Shema GPIO
Slika 10 prikazuje shemo vhodno/izhodne stopnje z vsemi zgoraj omenjenimi 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
->MODER = GPIOD->MODER & ~(3 << (2 * 3));
GPIOD->MODER = GPIOD->MODER | (1 << (2 * 3));
GPIOD
// pobrisemo bit 3 v OTYPER -> pin 3 deluje kot push-pull izhod
->OTYPER = GPIOD->OTYPER & ~(1 << 3);
GPIOD
// pobrisemo bit 3 v OSPEEDR -> pin 3 deluje z najpocanejso hitrostjo
// osvezevanja
->OSPEEDR = GPIOD->OSPEEDR & ~(1 << 3);
GPIOD
// pobrisemo bita 6 in 7 v PUPDR -> pin 3 deluje brez dodatnih uporov
->PUPDR = GPIOD->PUPDR & ~(3 << (2 * 3));
GPIOD
/**** uporaba ****/
// prizgemo LED
->BSRR = 1 << 3;
GPIOD
// ugasnimo LED
->BSRR = 1 << (3 + 16); GPIOD
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:
(GPIOC, 13, GPIO_MODE_INPUT);
GPIO_Init(GPIOD, 3, GPIO_MODE_OUTPUT); GPIO_Init
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
->MODER = gpio->MODER & ~(3 << (2 * pin));
gpio// nastavimo na zeljeno vrednost
->MODER = gpio->MODER | (mode << (2 * pin));
gpio}
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.
->MODER = gpio->MODER & ~(3 << (2 * pin));
gpioif (mode == GPIO_MODE_OUTPUT) {
->MODER = gpio->MODER | (mode << (2 * pin));
gpio}
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) {
->BSRR = 1 << pin;
gpio} else {
->BSRR = 1 << (pin + 16);
gpio}
}
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.
(GPIOC, 13, GPIO_MODE_INPUT);
GPIO_Init(GPIOD, 3, GPIO_MODE_OUTPUT);
GPIO_Init
while(1) {
if (GPIO_ReadPin(GPIOC, 13)) {
(GPIOD, 3, 1);
GPIO_WritePin} else {
(GPIOD, 3, 0);
GPIO_WritePin}
}
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