Vaja 1: Uvod v GPIO

Namen tokratne vaje je, da spoznate delo z osnovami splošno-namenskega vhoda/izhoda (GPIO) v STM32. Končni cilj je, da prižgemo LED diodo in beremo stanje gumba.

Delo z naslovi v C-ju in rezervirana beseda volatile

Za delo z GPIO bomo morali znati brati iz specifičnih naslovov ter na njih tudi pisati nove vrednosti. Na RISC-V ste v preteklem letu to naredili z:

li t0, 0x10012008   # naložimo naslov v register

li t2, 0x01         # naložimo enico v register
slli t2, t2, 19     # (1 << 19)

lw t3, 0(t0)        # preberemo trenutno stanje na naslovu

or t3, t3, t2       # postavimo 19-ti bit in ohranimo stanje ostalih
sw t3, 0(t0)        # shranimo rezultat na isti naslov

Enako bi v C-ju storili z naslednjimi vrsticami:

uint32_t *p = ((uint32_t *)0x10012008);
*p = *p | (1 << 19);

oziroma z uporabo konstant

#define GPIO_OUTPUT_EN ((uint32_t *)0x10012008)
uint32_t *p = GPIO_OUTPUT_EN;
*p = *p | (1 << 19);

ali še krajše

*GPIO_OUTPUT_EN = *GPIO_OUTPUT_EN | (1 << 19);

Rezervirana beseda volatile

Spodaj imamo primer kode, ki v zanki bere stanje bita 7 registra REG_A, ki je register pomnilniško-preslikane vhodno/izhodne naprave (angl. memory mapped I/O). To pomeni, da njegovo vrednost lahko spreminja tako vhod kot CPE. V primeru, da je vrednost iskanega bita enica, postavimo 19-ti bit v REG_B, ki je ravno tako register pomnilniško-preslikane V/I naprave:

#define REG_A ((uint32_t *)0x10012000)
#define REG_B ((uint32_t *)0x1001200C)

int main() {
    uint32_t *p = (uint32_t *) REG_A;
    while(1) {
        if (*p & (1 << 7)) {
            *REG_B = *REG_B | (1 << 19);
        }
    }
}

V primeru, da ima C prevajalnik vklopljene optimizacije, se to prevede v:

main:
            li     a5,REG_A      # nalozimo naslov REG_A
            lw     a4,0(a5)      # branje vrednosti REG_A
            li     a5,0x80       # nalozimo 1 << 7
            lui     a3,0x80      # nalozimo 1 << 19 (lui namesto li)
            and     a5,a5,a4     # *p & (1 << 7)
            li     a4,REG_B      # nalozimo naslov REG_B
oznaka_1:   bnez    a5,oznaka_2  # ce je b7 postavljen preskocimo naslednjo vrstico
loop:       j       loop         # neskoncna zanka
oznaka_2:   sw      a3,0(a4)    # ce b7 ni postavljen, shranimo
            j       oznaka_1

Če pozorno pregledamo kodo v zbirniku, opazimo, da se vsebina iz naslova REG_A prebere zgolj enkrat. Prevajalnik namreč nima informacije, da gre za registro pomnilniško-preslikane V/I naprave. Vrednost registra se lahko vsebino na naslovu, na katerega kaže kazalec, spreminja tudi izven normalnega toka programa. Vsebino na naslovu namreč v tem primeru spreminja zunanjost, recimo gumb. Prevajalniku to informacijo damo tako, da ob deklaraciji spremenljivke dodamo rezervirano besedo volatile:

volatile uint32_t *p = (volatile uint32_t *) REG_A;

Rezervirana beseda volatile se v C-ju uporablja predvsem kadar delamo z:

  • pomnilniško preslikanimi registri vhodno/izhodnih naprav,

  • globalnimi spremenljivkami, ki jih spreminjajo prekinitveno-servisne rutine/funkcije - več o tem čez nekaj tednov,

  • globalnimi spremenljivkami, do katerih dostopa več niti.

GPIO v STM32H7

Na mikrokrmilnikih STM32H750, ki jih uporabljamo, je na voljo 176 splošno-namenskih pinov (angl. general-purpose). Označujemo jih s kratico GPIO. Pini so razdeljeni v skupine po 16. Vsaka skupina šestnajstih pinov pripada ločeni GPIO napravi, ki so označene s črko: GPIOA, GPIOB, GPIOC, GPIOD, GPIOE, GPIOF, GPIOG, GPIOH, GPIOI, GPIOJ, GPIOK. Pini znotraj naprave so označeni s številkami od 0 do 15.

Kadar bomo govorili o pinih, bomo uporabljali 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, PA0 pa da gre za pin 0 na napravi GPIOA.

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 veste, 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

Naslovni prostor GPIO naprav

Vsaka GPIO naprava ima svoj ločen, 1kB velik del naslovnega prostora, v katerem se nahajajo registri za upravljanje njenih pinov. Razdelitev naslovnega prostora je vidna na Slika 3.1. Registri naprave GPIOE se tako nahajajo med naslovi 0x58021000 in 0x580213FF, registri naprave GPIOC pa med naslovom 0x58020BFF.

Slika 3.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. Danes bomo spoznali tri registre, preostale pa naslednjič.

Registri GPIO naprav
Odmik Kratko ime Polno ime
0x00 MODER Mode register
0x10 IDR Input data register
0x14 ODR Output data register

Odmik predstavlja število byteov od začetnega naslova naprave. Register MODER naprave GPIOE se torej nahaja na naslovu 0x58021000, register IDR na naslovu 0x58021010, ODR pa na 0x58021014. Register MODER naprave GPIOC se nahaja na naslovu 0x58020800, register IDR na naslovu 0x58020810, ODR pa na 0x58020814.

Inicializacija GPIO naprave - vhod/izhod

Za osnovno inicializacijo GPIO pinov, to je nastavljanje ali bomo pin uporabljali kot vhod ali kot izhod, služi register MODER. Ostale registre, s katerimi lahko določamo še preostale nastavitve, bomo spoznali naslednjič. Slika 3.2 prikazuje poenostavljeno shemo vhodno/izhodne stopnje, na kateri vidimo vse zgoraj omenjene registre in kako se uporabljajo, da dosežejo njihove funkcije.

Slika 3.2: Poenostavljena shema vhodno/izhodne stopnje z registri

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 3.3, 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. Za začetek nas bosta zanimala predvsem prva dva načina delovanja. Ob resetu je večina pinov, razen redkih izjem, v analognem načinu delovanja.

Slika 3.3: MODER - Mode 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 3.4, 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 3.4: 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 3.5: ODR - Output data register

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: inicializacija in uporaba zelene LED (PD3)

Preden lahko naredimo karkoli na napravi GPIO, moramo prižgati uro GPIO naprave. Kot rečeno, se RCC_AHB4ENR nahaja na naslovu 0x580244E0. Postaviti moramo pin 3, ker vklapljamo uro za napravo GPIOD:

#define RCC_AHB4ENR ((volatile uint32_t *)0x580244E0)
volatile uint32_t *p = RCC_AHB4ENR;
*p = *p | (1 << 3);  // bit 3 = GPIODEN

oziroma skrajšano

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

*RCC_AHB4ENR = *RCC_AHB4ENR | (1 << 3);

Po vklopu registrov naprave GPIOD nastavimo način delovanja pina 3 na izhod:

#define GPIOD_MODER ((volatile uint32_t *)0x58020C00)

*GPIOD_MODER = *GPIOD_MODER & ~(3 << (2 * 3)); // pobrišemo dva bita na bitih 6 in 7
*GPIOD_MODER = *GPIOD_MODER | (1 << (2 * 3));  // postavimo enico na bitu 6

Zdaj lahko LED končno tudi upravljamo preko registra ODR:

#define GPIOD_ODR ((volatile uint32_t *)0x58020C14)

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

// ugasnimo LED
*GPIOD_ODR = *GPIOD_ODR & ~(1 << 3); // pobrišemo bit 3

Še enkrat zapišimo vse skupaj:

#define RCC_AHB4ENR ((volatile uint32_t *)0x580244E0)
#define GPIOD_MODER ((volatile uint32_t *)0x58020C00)
#define GPIOD_ODR ((volatile uint32_t *)0x58020C14)

// vklopimo uro GPIOD
*RCC_AHB4ENR = *RCC_AHB4ENR | (1 << 3); // bit 3 = GPIODEN

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

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

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

Primer: inicializacija in uporaba gumba

Zopet začnemo s prižiganjem ure GPIO naprave, tokrat naprave GPIOC (bit 2).

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

*RCC_AHB4ENR = *RCC_AHB4ENR | (1 << 2); // bit 2 = GPIOCEN

Nastavimo pin 13, da bo deloval kot vhod.

#define GPIOC_MODER ((volatile uint32_t *)0x58020800)

*GPIOC_MODER = *GPIOC_MODER & ~(3 << (2 * 13)); // pobrišemo dva bita na bitih 26 in 27

nato stanje gumba beremo preprosto z branjem registra IDR:

#define GPIOC_IDR ((volatile uint32_t *)0x58020810)

uint32_t stanje_gumba = *GPIOC_IDR & (1 << 13);

Primer: uporaba gumba in LED

Napišimo sedaj primer, kjer inicializiramo LED in gumb. LED prižgemo kadar je gumb pritisnjen, sicer pa LED ugasnemo. Na vrhu main.c definiramo vse konstante:

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

#define GPIOD_MODER ((volatile uint32_t *)0x58020C00)
#define GPIOD_ODR ((volatile uint32_t *)0x58020C14)

#define GPIOC_MODER ((volatile uint32_t *)0x58020800)
#define GPIOC_IDR ((volatile uint32_t *)0x58020810)

Dodamo še kodo za inicializacijo ter zanko kjer preverjamo stanje guma in prižigamo LED.

// vklopimo uro GPIOD
*RCC_AHB4ENR = *RCC_AHB4ENR | (1 << 3) | (1 << 2);

// init LED
*GPIOD_MODER = *GPIOD_MODER & ~(3 << (2 * 3));
*GPIOD_MODER = *GPIOD_MODER | (1 << (2 * 3));

// init gumb
*GPIOC_MODER = *GPIOC_MODER & ~(3 << (2 * 13));


while(1) {
    // ce zelite spremljate stanje gumba v "Live Expressions"
    // deklarirajte stanje_gumba kot globalno spremenljivko
    uint32_t stanje_gumba = *GPIOC_IDR & (1 << 13);
    if (stanje_gumba) {
        // prižgemo LED
        *GPIOD_ODR = *GPIOD_ODR | (1 << 3);
    } else {
        // ugasnimo LED
        *GPIOD_ODR = *GPIOD_ODR & ~(1 << 3);
    }
}

Naloga 1

Dodajte inicializacijo preostalih dveh LED diod. Pozorni bodite, da sta LED prižgani, ko je na izhodu logična ničla. Nato napišite program, ki bo po pritisku gumba prižigal in ugašal LED kot kaže spodnji GIF. Za časovne zamike tokrat uporabite zanke z veliko ponovitvami.