Egy beágyazott rendszerben a szoftver gyakran nem „számol”, hanem
eszközökkel beszél: LED-et kapcsol, UART-on küld, timert indít. A
Cortex-M-en ezt memory-mapped I/O (MMIO) néven tesszük: a periféria
regiszterei a 32 bites címtér részei. Az a063
alfejezetben láttuk, hogy a 0x40000000-tól kezdődő
Peripherals régió a perifériáké. Ha azonban ezeket a címeket ugyanúgy
kezeljük, mint egy SRAM-változót, hibás kódot kapunk. Egy
perifériaregiszter ugyanis nem passzív memóriacella: minden olvasása és
írása mellékhatással járhat. Ez az alfejezet megmutatja, miért
viselkedik másképp, és milyen programozási fegyelmet kényszerít ez
ránk.
Támogatási mátrix (a fejezet és a II. záróprojekt
laborjaihoz). Nem minden periféria fut minden platformon. A
könyv alapútja a QEMU mps2-an385; a GPIO-laborok ezen nem
futtathatók:
| Periféria | QEMU 8.2.2 mps2-an385 |
Fizikai MPS2+ kártya AN385 FPGA-képpel | A könyv útja |
|---|---|---|---|
CMSDK UART0 (0x40004000) |
implementálva | van | alapút (QEMU) |
CMSDK Timer0 (0x40000000) |
implementálva | van | alapút (QEMU) |
| NVIC, SysTick (PPB) | implementálva | van | alapút (QEMU) |
CMSDK GPIO (0x40010000–) |
nincs implementálva | van | csak támogatott hardveren; QEMU-n SKIP |
A QEMU-n nem implementált perifériánál a regiszter visszaolvasása
semmit nem bizonyít: egy nem modellezett tartomány tipikusan 0-t olvas
és eldobja az írást (vagy egy egyszerű RAM-modell épp a beírt értéket
adja vissza), és egyik sem jelenti, hogy egy láb ténylegesen kapcsolt. A
GPIO-t használó laborok (a073, a074) ezért egy LAB_HAVE_GPIO
build-kapcsolóval futnak. QEMU-n ennek értéke 0, és a futó próba
SKIP-et ír ki, nem feltételezett sikert. A fordítási és
disassembly-ellenőrzések (címek, offsetek, generált utasítások) ettől
függetlenül QEMU nélkül is elvégezhetők. A II. záróprojekt QEMU-alapútja
ugyanezért UART + timer (a078).
Definíció (memory-mapped I/O, MMIO): olyan I/O-modell, amelyben a perifériák vezérlő-, állapot- és adatregiszterei a processzor normál adatcímtérbe vannak leképezve; a processzor ugyanazokkal a load/store utasításokkal éri el őket, mint a memóriát, de a hozzáférés hatással lehet a periféria állapotára.
Definíció (perifériaregiszter): egy címmel azonosított, tipikusan 32 bites (vagy 8/16 bites nézetű) hardveres tároló, amelynek olvasása/írása nem csak adatcserét, hanem hardveres műveletet is kiválthat.
A kulcs: a cím mint memóriacím viselkedik, de a szemantika nem az. Az a073 alfejezetben ezt konkrét GPIO-vezérlésre fordítjuk le, az a074 pedig C struktúrával absztrahálja.
A processzor és a memóriarendszer optimalizál a közönséges memóriára: gyorsítótár (cache), írási puffer, soron kívüli végrehajtás, spekulatív betöltés, hozzáférés-összevonás. Ezek mind feltételezik, hogy egy cím olvasása nem változtat állapotot, és hogy két azonos olvasás ugyanazt adja, amíg senki nem ír. Egy perifériaregiszternél ez nem igaz.
Az ARMv7-M architektúra ezért a címtér régióit memóriatípusokba sorolja. A normál SRAM/Flash „Normal” memória, amely cache-elhető, újrarendezhető. A perifériaterület jellemzően „Device” vagy „Strongly-ordered” típusú. A Device memória:
A Cortex-M3 mag jellemzően nem is tartalmaz adatcache-t, de az írási puffer és a busz miatt a sorrend és a hozzáférések száma akkor is számít. Az architektúra-szintű garancia: a Device/Strongly-ordered minősítés biztosítja, hogy a hozzáférések ténylegesen megtörténjenek, és hogy a Device-hozzáférések egymáshoz képest a programsorrendben maradjanak. Ez nem jelenti, hogy egy Device-írás minden más (például Normal memóriába tett) hozzáféréshez képest is sorrendben marad; erre a gát utasítások valók (lásd lent).
Miért tilos a gyorsítótárazás? Ha a periféria állapotregiszterét cache-elnénk, a cache egy régi értéket adna vissza, miközben a hardver már változott. Ha egy írást cache-elnénk, az írás esetleg sosem jutna el a perifériáig, vagy késleltetve, más sorrendben. Ez hibás működés.
Miért tilos az átrendezés? Tegyük fel, hogy előbb beírjuk az adatregisztert, majd beállítjuk a „start” bitet. Ha a processzor a két írást felcseréli, a periféria a régi adatot küldi el. Device memóriánál az architektúra megőrzi a programozási sorrendet erre a területre.
A CMSDK-szerű perifériák regiszterei négy csoportba sorolhatók. A pontos nevek perifériánként változnak, de a szerepek visszatérnek.
| Szerep | Tipikus név | Olvasás | Írás | Példa |
|---|---|---|---|---|
| Adat | DATA, RX, TX | bejövő adat | kimenő adat | UART RX/TX, GPIO adat |
| Engedélyezés | CTRL, ENABLE | engedélyezettség | művelet indítása/engedélyezése | timer start, megszakítás engedélyezés |
| Állapot | STATUS, RIS | aktuális állapot/flagek | flag törlése (gyakran write-1-to-clear) | TX FIFO tele, RX adat elérhető |
| Konfiguráció | CFG, BAUDDIV, DIR | beállított érték | beállítás | baud rate, GPIO irány |
Ez a négyes fontos, mert a programozási minta más-más szerepnél más. Adatregiszternél az érték számít; állapotnál a flag szemantikája (olvasás töröl? írás töröl?); engedélyezésnél a bit beállítása műveletet indíthat; konfigurációnál az érték beállítása a periféria viselkedését változtatja.
A periféria-hozzáférés helyességét három külön réteg adja. Egyikből sem következik a másik kettő, és egyik sem ad univerzális sorrendet minden memóriaelérésre.
| Réteg | Ki garantálja | Mit garantál | Mit nem |
|---|---|---|---|
volatile (fordítói szerződés) |
a C fordító | minden volatile hozzáférés megtörténik, nem vonható
össze, nem hagyható el, és a volatile hozzáférések
egymáshoz képest programsorrendben maradnak a generált kódban |
a nem volatile hozzáférések átrendezhetők körülötte;
semmit nem mond a hardveres sorrendről, és nem ad atomitást |
| Device memóriaattribútum (architektúra) | a mag és a buszrendszer | a Device-hozzáférések nem spekulatívak, nem vonódnak össze, és egymáshoz képest sorrendben érik el a perifériát | a Normal memóriához (például egy RAM-pufferhez) képesti sorrendet nem garantálja; azt sem, hogy egy írás befejeződött, mielőtt a következő utasítás fut |
DMB / DSB / ISB (gát
utasítások) |
a mag, explicit utasításra | DMB: a gát előtti memóriaelérések megfigyelhetően a gát
utániak előtt történnek; DSB: a következő utasítás csak a
korábbi elérések befejeződése után fut;
ISB: a pipeline-t üríti, a későbbi utasítások az új
állapotot látják (például a rendszerregiszter-írás után) |
magától nem keletkezik; a fordító sem rak be ilyet a
volatile miatt |
A C fordító a C absztrakt gépet optimalizálja. Azt feltételezi, hogy
egy változó értéke csak akkor változik, ha a program írja, és hogy az
ismételt olvasás ugyanazt adja, ha közben nincs írás. Ezért egy
while (*reg & MASK) ; ciklusból könnyen
if (*reg & MASK) while (1) ; lesz, vagy a
*reg bekerül egy regiszterbe, és a ciklus örökké a régi
értéket vizsgálja. A volatile minősítő az első réteg:
minden hozzáférés valódi, nem elhagyható, és nem tartható regiszterben.
A volatile uint32_t * tehát nem stílus kérdése, hanem
funkcionális követelmény.
Assemblyben az első réteg nem kérdés, mert az assembler nem
optimalizál: amit leírtunk, az pontosan az utasítássorozat. Egy
LDR r1, [r0] egy olvasás; ha a ciklusban újra leírjuk, újra
megtörténik. A második és a harmadik réteg viszont assemblyben is
ugyanúgy érvényes: a Device-szabályok a mag szintjén hatnak, a gátakat
pedig nekünk kell kiírni, ahol kellenek. Jellemző eset: egy RAM-ban
előkészített puffer után egy periféria „start” regiszterébe írunk. Ekkor
a két írás közé DMB kell, mert a Normal és a Device
hozzáférés közti sorrendet sem a volatile, sem a
Device-attribútum nem garantálja. Egy NVIC- vagy VTOR-írás
után pedig DSB + ISB biztosítja, hogy a
következő utasítások már az új beállítással fussanak (NVIC-konfiguráció).
Munkakönyvtár: ~/arm-labor/cm/a072/ —
az alfejezet minden laborfájlja ide kerül, és minden parancs innen
futtatandó (a könyvtárrendet a Labor-alapkörnyezet alfejezet írja le).
mkdir -p ~/arm-labor/cm/a072
cd ~/arm-labor/cm/a072A példa a QEMU mps2-an385 tényleges
CMSDK APB UART0 perifériáját használja, nem egy elképzelt eszközt. A
CMSDK-nál az állapotregiszter (STATE, +0x004)
0. bitje a TXFULL: 1 jelenti, hogy az
adópuffer tele, tehát amíg 1, várunk, és 0-nál
írhatunk. (Egy elképzelt „TX_READY = 1 kész” bit pont fordított
polaritású lenne; a két minta keverése a klasszikus hiba, ezért itt csak
a valódi bitet használjuk.) Az adó használatához a BAUDDIV
és a CTRL.TXEN beállítása is kell (előfeltételek).
Assembly (Thumb-2, Armv7-M):
@ lab-a072-uart.s — Thumb-2 / Armv7-M, QEMU mps2-an385, CMSDK APB UART0
.syntax unified
.thumb
.equ UART0_BASE, 0x40004000 @ MPS2-AN385 UART0
.equ UART_DATA, 0x000 @ adatregiszter
.equ UART_STATE, 0x004 @ bit0: TXFULL, bit1: RXFULL
.equ UART_CTRL, 0x008 @ bit0: TXEN
.equ UART_BAUDDIV, 0x010 @ legalább 16
.equ UART_TXFULL, (1 << 0)
.text
.thumb_func
.global uart_init
uart_init:
ldr r1, =UART0_BASE
movs r2, #16
str r2, [r1, #UART_BAUDDIV]
movs r2, #1 @ TXEN
str r2, [r1, #UART_CTRL]
bx lr
.thumb_func
.global uart_putc
@ r0 = küldendő bájt
uart_putc:
ldr r1, =UART0_BASE
wait_tx:
ldr r2, [r1, #UART_STATE] @ állapotregiszter olvasása: minden körben valódi LDR
tst r2, #UART_TXFULL
bne wait_tx @ amíg TXFULL = 1, várunk
str r0, [r1, #UART_DATA] @ adatregiszter írása: elindítja a küldést
bx lrItt a wait_tx ciklus minden iterációban valódi
LDR-t hajt végre. A STR írás mellékhatása: a
periféria megkezdi a bájt kiküldését. Ha az LDR-t
kihagynánk, vagy a STR-t az állapotellenőrzés elé tennénk,
hibás működés lenne. Az adatregisztert szóhosszú STR-rel
írjuk; a regiszter alsó 8 bitje az adat.
C változat volatile-lal:
/* lab-a072-uart.c — ugyanaz C-ben, volatile mutatókkal */
#include <stdint.h>
#define UART0_BASE 0x40004000u
#define UART_DATA (*(volatile uint32_t *)(UART0_BASE + 0x000u))
#define UART_STATE (*(volatile uint32_t *)(UART0_BASE + 0x004u))
#define UART_TXFULL (1u << 0)
void uart_putc_c(uint8_t b)
{
while (UART_STATE & UART_TXFULL) {
/* amíg TXFULL = 1, várunk: minden iterációban valódi olvasás */
}
UART_DATA = b; /* írás: elindítja a küldést */
}A volatile nélkül a fordító a while ciklus
olvasását kiemelhetné, és a ciklus egyszer olvasná a
UART_STATE-et. -O2-vel fordítva a generált
kódban a cikluson belül ott a ldr r2, [r1] (a fordító a
ciklust kissé át is szervezi, de minden kör olvas).
A két változatot a kanonikus startup/linker párral (a065, a066) egy rövid
tesztprogramba linkelve, a helyettesítő eszközlánccal (Clang +
ld.lld) és a tools/cm-run.py Cortex-M3
emulátorral futtatva mindkét szöveg megjelent a UART0 kimenetén. Az
emulátor STATE regisztere mindig 0 (a puffer sosem telik
meg), ezért a várakozó ágat ott nem lehetett kipróbálni.
Elfelejtett volatile. Tünet: a
program a vártnál előbb kilép a ciklusból, vagy örökké vár. Hipotézis: a
fordító kiemelte az olvasást. Adatgyűjtés: objdump -d a C
objektumon, a ciklusban keresd a ldr utasítást. Ha nincs,
ez a hiba. Egy változtatás: volatile hozzáadása.
Ellenőrzés: a disassemblyben megjelenik a cikluson belüli
betöltés.
Írások összevonása. Tünet: két egymást követő írásból csak az utolsó érvényesül. Hipotézis: a periféria nem Device memóriaként van kezelve, vagy a fordító optimalizálta el az elsőt. C-ben a volatilis írások nem vonhatók össze; assemblyben a hardver Device-szabályai védenek. Ellenpróba: a két írás közé tegyünk egy olvasást, és nézzük meg, megmarad-e mindkét írás.
Nem megfelelő szélesség. Egy 32 bites
regiszterhez STRB-vel írni a CMSDK-nál nem garantáltan
támogatott, vagy más hatása lehet. A fenti példa ezért szóhosszú
STR-t használ. Ellenpróba: a CMSDK dokumentációban
ellenőrizni a regiszter szélességét és a támogatott
hozzáférést.
Fordított polaritású állapotbit. Tünet: a küldés
minden karaktert eldob, vagy örökké vár. Hipotézis: a kód „ready = 1”
bitet feltételez, a CMSDK STATE.TXFULL viszont 1-nél jelzi,
hogy nem lehet írni. Ellenpróba: a ciklus feltétele
bne wait_tx (amíg TXFULL = 1), nem
beq.
Hiányzó gát Normal és Device hozzáférés között.
Tünet: a periféria egy RAM-pufferből régi adatot dolgoz fel. Hipotézis:
a pufferírás és a „start” regiszter írása között nincs DMB.
Ellenpróba: DMB beszúrása a két írás közé; a
volatile önmagában ezt nem pótolja.
1. Magyarázó feladat. Magyarázd el, miért nem
cache-elhető és miért nem rendezhető át egy perifériaregiszter
hozzáférése. Adj példát arra, mi történne, ha a wait_tx
ciklus LDR-jét a processzor egyszer olvasná, és a ciklus a
regiszterben tartaná az értéket.
2. Implementációs feladat. Írj Thumb-2 assembly
rutint, amely beolvassa a GPIO0 adatregiszterét, majd írj C
függvényt volatile mutatóval, amely ugyanezt teszi. A
címeket a periféria-struktúra kanonikus
lab-cmsdk-gpio.h térképéből vedd. A támogatási mátrix
szerint a futtatás csak támogatott hardveren érdemi; QEMU-n a fordítás
és a disassembly ellenőrizhető.
3. Hibakeresési feladat. Adott egy C kód, amely
volatile nélkül polloz egy állapotbitet, és a program a
QEMU-ban nem lép ki a ciklusból. Írd le a diagnózis lépéseit: mit néznél
meg objdump -d-vel, mit várnál a volatile
hozzáadása után, és hogyan igazolnád az eredményt?
1. A cache egy gyors másolat, amelyet a hardver
állapotváltozásáról nem értesül. Ha a státuszregisztert cache-eljük, a
program a régi értéket olvassa, és nem veszi észre, hogy a flag beállt.
Az átrendezés akkor okoz gondot, ha az adatírást és a startírást
felcseréli a rendszer: a periféria a régi adatot küldi. A
wait_tx ciklusban, ha egyszer olvasnánk és regiszterben
tartanánk, a ciklus soha nem látná meg a flag változását, mert a
regiszter értéke nem frissül; ez végtelen ciklus. Ezért kell minden
iterációban valódi olvasás.
2. Assembly:
.syntax unified
.thumb
.equ GPIO0_BASE, 0x40010000 @ MPS2-AN385 GPIO0 (lab-cmsdk-gpio.h)
.equ GPIO_DATA, 0x000 @ DATA: a lábak bemeneti szintje
.text
.global gpio_read_data
gpio_read_data:
ldr r0, =GPIO0_BASE
ldr r0, [r0, #GPIO_DATA]
bx lrC:
#include <stdint.h>
#define GPIO0_BASE 0x40010000u /* MPS2-AN385 GPIO0 (lab-cmsdk-gpio.h) */
#define GPIO_DATA (*(volatile uint32_t *)(GPIO0_BASE + 0x000u))
uint32_t gpio_read_data(void)
{
return GPIO_DATA;
}A lényeg a volatile, és hogy az assemblyben az
LDR ténylegesen megtörténik.
3. Diagnózis: objdump -d a lefordított
objektumon; keresd a ciklus címét és a benne lévő ldr
utasítást. Ha a ciklusban nincs ldr a periféria-címre, a
fordító kiemelte. Add hozzá a volatile-t, fordítsd újra, és
a disassemblyben a cikluson belül megjelenik a betöltés. Ellenőrzés
QEMU-ban, a valódi UART0-n: a fenti uart_putc_c kiírja a
karaktert, tehát kilépett a várakozó ciklusból. Fontos: a QEMU modell
időzítése eltér a fizikai hardvertől (a puffer itt ritkán vagy soha nem
telik meg), ezért a várakozó ág viselkedése csak hardveren mérhető.
A memory-mapped I/O a perifériaregisztereket a normál címtérbe
képezi, de a hozzáférés mellékhatással járhat. Három réteg adja a
helyességet: a volatile a fordítót köti (a
volatile hozzáférések megtörténnek, sorrendben), a
Device-attribútum a hardvert (a Device-hozzáférések egymáshoz képest
sorrendben, spekuláció nélkül), a
DMB/DSB/ISB pedig ott ad
sorrendet vagy befejezést, ahol a másik kettő nem (Normal–Device határ,
rendszerregiszter-írás után). Egyik sem ad univerzális sorrendet minden
memóriaelérésre. A CMSDK UART STATE.TXFULL bitje 1-nél
jelzi, hogy várni kell. A GPIO a QEMU-alapúton nem érhető el; ott a
GPIO-próba SKIP. A regiszterek négy szerepe: adat, engedélyezés,
állapot, konfiguráció.
Továbbhasznált definíciók: - memory-mapped I/O (MMIO): perifériaregiszterek a normál adatcímtérben, load/store hozzáféréssel. - perifériaregiszter: címmel azonosított hardveres tároló, amelynek olvasása/írása mellékhatással járhat. - volatile: C minősítő, amely garantálja, hogy minden hozzáférés valódi és nem cache-elhető regiszterben.
Az alábbiak dokumentációs kiindulópontok (forrásállapot: nem megnyitott, lásd az Olvasói útmutató jelöléseit):
Ellenőrzési hiányok: - A UART-példa a CMSDK APB UART
dokumentált térképét használja (DATA 0x000,
STATE 0x004 – bit0 TXFULL, CTRL
0x008, BAUDDIV 0x010); a helyettesítő eszközlánccal
lefordítottuk, és a tools/cm-run.py emulátorban kiírt. A
támogatási mátrix GPIO-sora a lektor QEMU 8.2.2-es ellenőrzésén és a II. záróprojekt specifikációjának QEMU-korlát
megjegyzésén alapul; más QEMU-verziónál info mtree-vel és
ténylegesen bekötött eseményforrással kell újraellenőrizni. - Az Armv7-M
Device/Strongly-ordered szabályok részletes, oldalszintű idézése ebben a
folyamatban nem történt meg. - A QEMU viselkedése nem hardverhű; a mért
latenciák és versenyhelyzetek eltérhetnek a fizikai Cortex-M3-tól.