18.2 GPIO közvetlen vezérlése: bitbanging

A memory-mapped I/O alapelve szerint a perifériaregisztereket ugyanúgy érhetjük el, mint a memóriát: load/store utasításokkal. Most ezt konkrét portára alkalmazzuk. Megnézzük, hogyan lesz egy GPIO-pin kimenet, hogyan állítunk be vagy törlünk egyetlen bitet a porta többi bitjének érintetlenül hagyásával, hogyan olvasunk bemenetet és ágazunk el az értékére, és hogyan lesz mindebből bitbanging – szoftveresen, ciklusokkal előállított jelsorozat.

18.2.1 Motiváció

A memory-mapped I/O önmagában csak annyit mond, hogy van egy cím, amit írni lehet. A gyakorlati hardvervezérlés ennél több: egy 32 bites portán egyszerre 32 pin lakik, és jellemzően egyetlen bitet akarunk megváltoztatni, a többit békén hagyni. Ha a STR-rel a teljes szót felülírjuk, minden más kimenet is áldozatul esik. Ez az alfejezet megmutatja a helyes read-modify-write (RMW) mintát, összekapcsolva a logikai alapműveletek és bitmaszkolás szakaszban tanult ORR/BIC maszkolással – azzal a fontos különbséggel, hogy itt Thumb-2 / Armv7-M kontextusban dolgozunk, nem A64-ben.

18.2.2 Definíció és működés

GPIO (General-Purpose Input/Output): olyan digitális periféria, amelynek minden csatlakoztatott lábát programozottan bemenetnek vagy kimenetnek lehet kijelölni, és kimenetként magas (1) vagy alacsony (0) szintre lehet hajtani.

Egy GPIO-porta regiszterkészlete tipikusan kétféle információt tárol:

Bitbanging: olyan technika, amikor egy protokoll vagy jelsorozat időzítését nem dedikált hardverperiféria (UART, SPI, I²C vezérlő), hanem maga a processzor állítja elő, közvetlen GPIO-írásokkal és szoftveres késleltetéssel. Következménye, hogy az időzítés pontossága a processzor ciklusidejétől és a megszakításoktól függ.

18.2.3 A CMSDK GPIO regiszterei az MPS2 AN385-ön

A fejezet célplatformja a qemu-system-arm -machine mps2-an385, amely ARM MPS2+ AN385 FPGA-képet modellez, CMSDK perifériákkal. A GPIO-portok báziscíme és regiszteroffszetjei az MPS2 dokumentált példatartományából származnak:

Port Báziscím
GPIO0 0x40010000
GPIO1 0x40011000

A regisztertérkép az Arm Cortex-M System Design Kit műszaki leírásának 3-8. táblázatát (3.5.2) követi. Ugyanez a térkép a II. rész egyetlen forrása a GPIO-offsetekhez: a periféria-struktúra lab-cmsdk-gpio.h fejléce és az alábbi assembly-konstansok is ebből készültek, és a záróprojekt is ezt a fejlécet használja.

Offszet Név Típus Szerep
0x000 DATA RW olvasás: a lábak tényleges szintje; írás: kimeneti adat
0x004 DATAOUT RW kimeneti adatregiszter (utolsó kiírt érték)
0x008–0x00C — — fenntartott (két szó)
0x010 OUTENSET RW 1 a kívánt bitre → az a láb kimenet lesz
0x014 OUTENCLR RW 1 a kívánt bitre → az a láb bemenet lesz
0x018 ALTFUNCSET RW alternatív funkció engedélyezése
0x01C ALTFUNCCLR RW alternatív funkció letiltása
0x020 INTENSET RW megszakítás engedélyezése bitenként
0x024 INTENCLR RW megszakítás tiltása bitenként (nem nyugtázás)
0x028 INTTYPESET RW 1 → élvezérelt
0x02C INTTYPECLR RW 1 → szintvezérelt
0x030 INTPOLSET RW 1 → felfutó él / magas szint
0x034 INTPOLCLR RW 1 → lefutó él / alacsony szint
0x038 INTSTATUS / INTCLEAR R / W1C olvasás: függő megszakítások; írás: 1 a bitre → az eseményjelző törlése

Platformtámogatás. A memory-mapped I/O alfejezet támogatási mátrixa szerint a QEMU 8.2.2 mps2-an385 gépen a GPIO nincs implementálva. A regiszterek írása és visszaolvasása ott semmit nem igazol: egy nem modellezett tartomány 0-t ad vagy épp a beírt értéket tükrözi, és egyik sem jelenti, hogy egy láb kapcsolt. Ezért a futó GPIO-próba QEMU-n SKIP. Érdemben csak támogatott hardveren (fizikai MPS2+ kártya AN385 FPGA-képpel) futtatható. Egy más gyártói mikrokontroller (például STM32) GPIO-ja más regisztertérképet használ, a fenti offsetek ott nem érvényesek.

18.2.4 Egy bit beállítása és törlése: ORR/BIC

A DATAOUT regiszter egy teljes 32 bites szó. Egy pin megváltoztatásához három lépés kell: olvasás, módosítás, visszaírás:

@ Thumb-2 / Armv7-M
LDR  r2, [r0, #GPIO_DATAOUT]   @ 1) olvas: aktuális kimeneti szó
ORR  r2, r2, r1                @ 2) a kijelölt bit(ek) beállítása 1-re
STR  r2, [r0, #GPIO_DATAOUT]   @ 3) visszaír

A törlés ugyanez BIC-kel, ahol BIC r2, r2, r1 a maszkkal kijelölt biteket nullázza, a többit megtartja:

LDR  r2, [r0, #GPIO_DATAOUT]
BIC  r2, r2, r1
STR  r2, [r0, #GPIO_DATAOUT]

Ez pontosan a a032 mintázata: ORR beállít, BIC töröl, a maszk pedig bitpozíciót jelöl. Két gyakorlati korlát azonban M-felé fokozottan érvényes:

  1. Nem atomi. Két megszakítás vagy két feladat között ugyanerre a portára írva elveszhet a másik módosítása (lost update). Ahol a hardver kínál külön SET/CLR regisztert, azt kell használni.
  2. Az immediate korlátozott. Thumb-2-ben az ORR/BIC immediate a modified immediate (ThumbExpandImm) családból kódolható; például 0x80000001 nem írható le, ilyenkor regiszterbe töltött maszk kell.

18.2.5 Kidolgozott példa

Munkakönyvtár: ~/arm-labor/cm/a073/ — az alfejezet minden laborfájlja ide kerül, és minden parancs innen futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le). A példa a Linker script alapjai lab-mps2.ld fájljára és a Startup kód lab-startup.s fájljára épít; ezeket átmásoljuk, nem a korábbi könyvtárakban dolgozunk tovább.

mkdir -p ~/arm-labor/cm/a073
cd ~/arm-labor/cm/a073
cp ~/arm-labor/cm/a065/lab-mps2.ld ~/arm-labor/cm/a066/lab-startup.s .

A GPIO-műveletek implementált CMSDK GPIO perifériát és valódi bemenetet feltételeznek. A QEMU 8.2.2 mps2-an385 modelljében a GPIO nincs implementálva; ott a fizikai be-/kimeneti próba SKIP, és a regiszterek írása nem igazolja a lábak működését. A program ezért egy LAB_HAVE_GPIO build-kapcsolót kap (a lab-cmsdk-gpio.h alapértéke 0). A fejlécet a periféria-struktúra alfejezet készíti; ha az még nincs meg, itt is elég a fájl tartalmát átvenni:

cp ~/arm-labor/cm/a074/lab-cmsdk-gpio.h .

A lab-gpio-bitbang.s GPIO0 pin0-t kimenetként villogtatja, és megvárja, amíg a pin1 (bemenet) magas szintre nem billen.

@ lab-gpio-bitbang.s — Cortex-M / Thumb-2 (Armv7-M)
.syntax unified
.thumb

@ CMSDK GPIO offsetek — ugyanaz a térkép, mint a lab-cmsdk-gpio.h-ban (a074)
.equ GPIO_DATA,       0x000
.equ GPIO_DATAOUT,    0x004
                                  @ 0x008-0x00C: fenntartott
.equ GPIO_OUTENSET,   0x010
.equ GPIO_OUTENCLR,   0x014
.equ GPIO_ALTFUNCSET, 0x018
.equ GPIO_ALTFUNCCLR, 0x01C
.equ GPIO_INTENSET,   0x020
.equ GPIO_INTENCLR,   0x024
.equ GPIO_INTTYPESET, 0x028
.equ GPIO_INTTYPECLR, 0x02C
.equ GPIO_INTPOLSET,  0x030
.equ GPIO_INTPOLCLR,  0x034
.equ GPIO_INTSTATUS,  0x038       @ olvasás: INTSTATUS, írás: INTCLEAR (W1C)
.equ GPIO0_BASE,      0x40010000

.text

@ void gpio_out_enable(uint32_t base, uint32_t pin);   r0=base, r1=pin
.global gpio_out_enable
.thumb_func
gpio_out_enable:
    MOVS r2, #1
    LSL  r2, r2, r1            @ pinindex -> bitmaszk
    STR  r2, [r0, #GPIO_OUTENSET]
    BX   lr

@ void gpio_set(uint32_t base, uint32_t mask);
.global gpio_set
.thumb_func
gpio_set:
    LDR  r2, [r0, #GPIO_DATAOUT]
    ORR  r2, r2, r1
    STR  r2, [r0, #GPIO_DATAOUT]
    BX   lr

@ void gpio_clear(uint32_t base, uint32_t mask);
.global gpio_clear
.thumb_func
gpio_clear:
    LDR  r2, [r0, #GPIO_DATAOUT]
    BIC  r2, r2, r1
    STR  r2, [r0, #GPIO_DATAOUT]
    BX   lr

@ uint32_t gpio_read_input(uint32_t base, uint32_t mask);
.global gpio_read_input
.thumb_func
gpio_read_input:
    LDR  r2, [r0, #GPIO_DATA]  @ a pad tényleges szintje
    AND  r0, r2, r1            @ 0 vagy mask
    BX   lr

@ void delay(void);  durva, szoftveres késleltetés (illusztratív)
.global delay
.thumb_func
delay:
    LDR  r1, =0x00080000
delay_loop:
    SUBS r1, r1, #1
    BNE  delay_loop
    BX   lr

@ void wait_button(uint32_t base, uint32_t mask);  vár magas szintre
.global wait_button
.thumb_func
wait_button:
    PUSH {r4, r5, r6, lr}
    MOV  r4, r0
    MOV  r5, r1
wait_loop:
    MOV  r0, r4
    MOV  r1, r5
    BL   gpio_read_input
    CMP  r0, #0
    BEQ  wait_loop             @ 0 -> még alacsony, újraolvasunk
    POP  {r4, r5, r6, pc}

@ void gpio_demo(void);
.global gpio_demo
.thumb_func
gpio_demo:
    PUSH {r4, lr}
    LDR  r4, =GPIO0_BASE

    MOV  r0, r4                @ pin0 = kimenet
    MOVS r1, #0
    BL   gpio_out_enable

    MOV  r0, r4                @ pin1 bemenet marad (default)
    MOVS r1, #2                @ 0b10 -> pin1 maszkja
    BL   wait_button

    MOV  r0, r4                @ rövid magas impulzus pin0-n
    MOVS r1, #1
    BL   gpio_set
    BL   delay
    MOV  r0, r4
    MOVS r1, #1
    BL   gpio_clear

    POP  {r4, pc}

A futtatást egy rövid C main vezérli, amely a gpio_demo-t csak LAB_HAVE_GPIO = 1 mellett hívja, egyébként SKIP-et ír ki semihostingon:

/* lab-gpio-main.c — a gpio_demo futtatása csak támogatott platformon */
#include "lab-cmsdk-gpio.h"

void gpio_demo(void);

static void sh_call(uint32_t op, const void *arg)
{
    register uint32_t r0 asm("r0") = op;
    register const void *r1 asm("r1") = arg;
    asm volatile ("bkpt 0xab" : "+r"(r0) : "r"(r1) : "memory");
}

int main(void)
{
#if LAB_HAVE_GPIO
    gpio_demo();                                  /* pin1-re vár, majd impulzus pin0-n */
    sh_call(0x04u, "gpio_demo: kesz\n");
#else
    sh_call(0x04u, "SKIP: GPIO nincs implementalva ezen a platformon (LAB_HAVE_GPIO=0)\n");
#endif
    sh_call(0x18u, (const void *)0x20026u);       /* SYS_EXIT */
    for (;;) { }
}

Build és futtatás. A firmware build és QEMU-betöltés munkafolyamat szerint, a startup kód mellé linkelve:

# QEMU-alapút: a GPIO-próba SKIP
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -ffreestanding -nostdlib -O1 \
  -T lab-mps2.ld -o lab-gpio.elf lab-startup.s lab-gpio-bitbang.s lab-gpio-main.c
qemu-system-arm -M mps2-an385 -cpu cortex-m3 -nographic -semihosting \
  -kernel lab-gpio.elf

# Támogatott hardveren (MPS2+ AN385): ugyanez -DLAB_HAVE_GPIO=1 kapcsolóval,
# a debuggeren keresztül, semihostinggal

A QEMU-alapút kimenete a helyettesítő eszközlánccal (Clang + ld.lld) és a tools/cm-run.py emulátorral mérve, amely a GPIO-tartományt a QEMU-hoz hasonlóan nem modellezi (0-t olvas, az írást eldobja):

SKIP: GPIO nincs implementalva ezen a platformon (LAB_HAVE_GPIO=0)

Ellenpróba: miért nem elég „megpróbálni”. Ugyanezt a programot -DLAB_HAVE_GPIO=1-gyel a nem modellezett GPIO-n futtatva a wait_button örökké vár, mert a DATA mindig 0-t ad; az emulátor az utasításlimitnél állt le. Sem az írás, sem a visszaolvasás nem árulja el, hogy a periféria hiányzik; ezt csak a támogatási mátrix és a build-kapcsoló kezeli megbízhatóan.

Hardveren, GDB-vel (a II. rész egységes -S -gdb tcp::3333 / target remote :3333 mintája helyett ott a debugprobe GDB-szervere a cél):

(gdb) break gpio_set
(gdb) continue
(gdb) info registers r0 r1 r2
(gdb) x/2xw $r0

Várt állapot támogatott hardveren. A gpio_set belépésénél r0 = 0x40010000, r1 = 0x00000001. A STR után a 0x40010004 (DATAOUT) címen 0x00000001 olvasható, és a 0x40010010 (OUTENSET) visszaolvasva a pin0 bitjét mutatja. A gpio_read_input a DATA regiszterből (0x40010000) olvas, és AND-del izolálja a kért bitet; a CMP r0, #0 utáni BEQ wait_loop addig ismétel, amíg a pin1 magas nem lesz. Ezeket az értékeket QEMU-n nem lehet ellenőrizni; ott a próba SKIP.

Illusztratív: kimeneti regiszter visszaolvasása írás után - Hipotetikus, csak a regiszter-RMW szemléltetésére; nem tényleges hardveres mérés, és QEMU-n nem is mérhető (a GPIO ott nincs implementálva).

18.2.6 Tipikus hibák és ellenpróbák

Tünet Hipotézis Adatgyűjtés Egy változtatás
rossz pin vált, más nem pinindexet adtunk át maszk helyett GDB: p/x $r1 a gpio_set-ben LSL-lel maszkká alakítani, vagy maszkot átadni
semmi nem történik a kimeneten elmaradt OUTENSET (a láb bemenet maradt), vagy a régi, hibás 0x008 offsetre írtunk x/1xw 0x40010010 (hardveren) gpio_out_enable a kiírás előtt, GPIO_OUTENSET = 0x010
a QEMU-n a demó „lefagy” a GPIO nincs implementálva, a DATA mindig 0 támogatási mátrix (a072) LAB_HAVE_GPIO = 0, a próba SKIP
az egyik másik kimenet is átbillen teljes szó felülírása STR-rel RMW helyett olvassuk ki a DATAOUT-ot írás előtt/után ORR/BIC RMW bevezetése
a beolvasott érték mindig 0 DATAOUT-ot olvastunk DATA helyett nézzük a load offszetjét [r0, #GPIO_DATA]
ORR nem fordul le kódolhatatlan immediate hibaüzenet: „invalid immediate” maszk betöltése regiszterbe

18.2.7 Feladatok

1. (magyarázó, alapszint — cél: megértés-ellenőrzés) Mutasd be lépésenként, miért nem atomi a LDR/ORR/STR minta, és magyarázd el, miért oldja meg a OUTENSET/OUTENCLR az irányváltás RMW-problémáját, míg az adatírásnál miért nem áll rendelkezésre ilyen.

2. (implementációs, alapszint — cél: rutinépítés) Írj void gpio_write_pin(uint32_t base, uint32_t pin, uint32_t value) függvényt, amely value != 0 esetén beállítja, különben törli a pinnel jelölt bitet, a többi bitet érintetlenül hagyva.

3. (hibakeresési/mérési, középszint — cél: rutinépítés) A megadott gpio_demo verzióban a wait_button helyett gpio_read_input hívás r1 = 1 (pin0 maszkja) értékkel szerepel. Add meg, hogy milyen viselkedést várunk, és javítsd a kódot.

4. (integráló, elmélyült — cél: fejezetek összekapcsolása: ISR írása: C és assembly vegyesen + a jelen fejezet nem-atomi RMW-korlátja) Tegyük fel, hogy a gpio_set/gpio_clear rutint a main ciklus és egy Timer-ISR (a070 TIMER0_IRQHandler mintájára) is hívja, ugyanarra a GPIO-portra. Írd le pontosan, milyen utasítássorrend vezet elveszett frissítéshez (lost update), ha a hardveres kivétel-belépés éppen a main() LDR és STR utasítása közé “ékelődik be”. Mivel a Thumb-2 LDR/ORR/STR több különálló utasításból áll, bármelyik köztes ponton megszakítható — nincs itt egyetlen atomi RMW-utasítás. Milyen védekezési stratégiát javasolnál?

18.2.8 Megoldások

1. megoldás — Segítség. Gondold át, mi történik, ha két végrehajtási szál ugyanazt a pillanatképet olvassa be LDR-rel, mielőtt bármelyikük visszaírna — majd nézd meg, hogy az OUTENSET/OUTENCLR miért nem igényel előzetes olvasást.

1. megoldás — Teljes megoldás. Az LDR-rel beolvasott érték egy pillanatkép. Két konkurens író ugyanazt a pillanatképet kapja, mindkettő módosít, és a későbbi STR felülírja az előzőt: elveszett frissítés. Az OUTENSET/OUTENCLR éppen azért külön regiszter, hogy a hardver belül végezze a bitekérték szerinti módosítást, és a sw csak 1-eseket írjon a kívánt pozíciókra; nem kell visszaolvasni. Az adatregiszternél ilyen SET/CLR pár a CMSDK GPIO-n nem áll rendelkezésre, ezért ott RMW-t kell végezni, és ha a portát több kontextus is írja, szoftveres kölcsönös kizárás vagy kritikus szakasz szükséges.

2. megoldás — Segítség. A meglévő gpio_set/gpio_clear rutinok már elvégzik a maszkolt RMW-t; a feladat csak egy elágazás a value alapján, a megfelelő meglévő rutin hívásával.

2. megoldás — Teljes megoldás.

@ void gpio_write_pin(uint32_t base, uint32_t pin, uint32_t value)
.global gpio_write_pin
.thumb_func
gpio_write_pin:
    PUSH {r4, lr}
    MOV  r4, r0
    MOVS r3, #1
    LSL  r1, r3, r1            @ 1 << pin
    CMP  r2, #0
    BEQ  1f
    MOV  r0, r4
    BL   gpio_set
    POP  {r4, pc}
1:  MOV  r0, r4
    BL   gpio_clear
    POP  {r4, pc}

A LSL r1, r1, r1 tömör, de olvashatóság kedvéért külön regiszterbe érdemes másolni. A lényeg, hogy az ág mindkét esetben a meglévő ORR/BIC maszkoló rutinra épül, így a többi bit érintetlen marad.

3. megoldás — Segítség. Nézd meg, melyik pinhez tartozik az átadott maszk, és hogy az a pin éppen bemenet-e vagy kimenet — a wait_button csak bemeneti pin változására adhat értelmes választ.

3. megoldás — Teljes megoldás. A wait_button gpio_read_input-ot hívja mask = 1-gyel (pin0). Mivel pin0 épp a kimenet, a DATA olvasása a saját kimeneti szintet adja vissza; a ciklus azonnal vagy soha nem teljesül a gomb állapotától függetlenül. A javítás: a gomb maszkja 0b10, azaz MOVS r1, #2. Ez a klasszikus „rossz maszk” hiba, amelyet a pinindex-maszk összekeverése okoz.

4. megoldás — Segítség. Kövesd nyomon lépésről lépésre: main() már elvégezte az LDR-t (pillanatkép r2-ben), amikor megszakítás érkezik — mit lát és mit ír az ISR a DATAOUT-ban, és mit ír végül vissza main() a saját, már elavult pillanatképéből?

4. megoldás — Teljes megoldás. A main() ciklus LDR r2, [r0, #GPIO_DATAOUT]-pal beolvassa a DATAOUT aktuális értékét egy pillanatképbe (r2). Mielőtt az ORR/BIC + STR lefutna, a Timer-ISR megszakítja a folyamot — a070 szerint a hardver automatikusan a verembe menti R0–R3, R12, LR, PC, xPSR-t, majd a kezelő fut. Ha az ISR maga is hívja a gpio_set/gpio_clear rutint ugyanarra a portra, a saját LDR/ORR/STR szekvenciája lefut, és sikeresen frissíti a DATAOUT-ot. A megszakításból visszatérve a main() ciklus folytatja a saját, már elavult r2 pillanatképével: erre alkalmazza a saját ORR/BIC-jét, majd STR-rel visszaírja — ezzel felülírja (eltörli) az ISR által közben elvégzett módosítást. Ez pontosan az elveszett frissítés (lost update), amelyet a fejezet 1. korlátja általánosan már megemlít, itt konkrét ISR-kontextusban. Védekezés: vagy a megszakítások rövid letiltása (CPSID i/CPSIE i) a main() RMW-szekvenciája köré kritikus szakaszként — ugyanaz az elv, mint a070 megosztott g_tick/g_pending változóinak védelme —, vagy ha a hardver kínálna dedikált SET/CLR regisztert DATAOUT-ra (ahogy OUTENSET/OUTENCLR már van az iránybitekhez), azt kellene használni RMW helyett, mert az a hardver szintjén atomi.

18.2.9 Rövid összefoglaló

A GPIO-pin vezérlése három regiszter köré szerveződik: irány (OUTENSET/OUTENCLR), adat (DATA/DATAOUT). Egy bit megváltoztatása M-felé RMW-minta: LDR + ORR (beállítás) vagy BIC (törlés) + STR, ami nem atomi. A bemenetet a DATA adja vissza, és CMP után feltételes elágazással dolgozunk fel. A bitbanging szoftveresen, késleltetéssel állítja elő a jelsorozatot, ezért időzítése nem cikluspontos.

Továbbhasznált definíciók: - irányregiszter (direction): bitenként kijelöli, hogy a láb bemenet- vagy kimenetirányú. - bitbanging: szoftveres, programvezérelt jelforma-előállítás GPIO-pinen, dedikált periféria nélkül. - read-modify-write (RMW): olvasás–módosítás–visszaírás minta egyetlen bit vagy bitcsoport megváltoztatására.

18.2.10 Források és ellenőrzési hiányok

A linkek és a dokumentumverziók közös állapota az Olvasói útmutató forrásállapot-táblájában van; itt csak az alfejezet saját megállapításai szerepelnek.

Ellenőrzési hiányok, amelyek a fenti ellenőrzés után is fennmaradnak: A regisztertérkép a CMSDK műszaki leírás 3-8. táblázatát követi (a lektor ellenőrzése alapján javítva: a 0x008–0x00C fenntartott, az OUTENSET a 0x010-en van). A lab-gpio-bitbang.s-t és a lab-gpio-main.c-t a helyettesítő eszközlánccal lefordítottuk; a SKIP-ág és a nem modellezett GPIO-n végtelen várakozó ág az emulátorban ténylegesen lefutott. A hardveres próba (a lábak tényleges kapcsolása, a DATAOUT/OUTENSET visszaolvasása) támogatott hardveren végzendő, ebben a munkafolyamatban nem történt meg. Az Ubuntu 24.04 helyi csomagolt toolchain- és QEMU-verzió, valamint a fizikai STM32 Nucleo viselkedése ettől eltérhet, és külön jelölést kíván.