ARM Bitbanding

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Hintergrund

Eine RISC Architektur wie die der ARM Prozessoren besitzt oft keine Befehle, um einzelne Bits im Speicher zu manipulieren. Um ein einzelnes Bit in einem Peripherieregister zu setzen ist daher eine Folge mehrerer Befehle erforderlich, wie beispielsweise

 
ldr     r0, =0x40012C0C
ldrh    r1, [r0]
orr     r1, #1<<9
strh    r1, [r0]

oder in C formuliert

uint16_t temp = TIM1.DIER;
temp |= 1<<9;
TIM1.DIER = temp;

Wenn eine solche Befehlssequenz im Hauptprogramm steht und in einem Interrupt Handler ein anderes Bit des gleichen Peripherieregisters verändert wird, dann kann es vorkommen, dass der Handler zwischen dem Load- und dem Store-Befehl dieser Sequenz ausgeführt wird. In diesem Fall geht die Änderung im Handler mit dem Store-Befehl verloren, der seinen Wert aus dem Zustand vor dem Aufruf des Handlers ableitet.

Nun kann man natürlich die Befehlssequenz dagegen absichern, indem man sie mit abgeschalteten Interrupts ausführt. Das ist aber recht umständlich und verlängert zudem die Reaktionszeit von Interrupts.

ARM Controller mit den Cores Cortex-M3 und -M4 sowie auch manche Cortex M0+ besitzen jedoch die Fähigkeit, einzelne Bits im RAM und im Peripheriebereich direkt adressieren zu können. Dazu existiert für den Peripheriebereich 0x40000000-0x400FFFFF ein weiterer Adressbereich 0x42000000-0x43FFFFFF und für den RAM-Bereich 0x20000000-0x200FFFFF der Bereich 0x22000000-0x23FFFFFF. Das sogenannte Bitbanding.

Arbeitsweise

In der Bitbanding-Region wird aus den Adressbits 5..24 die Byteadresse abgeleitet, indem diese Bits um 5 Bits nach rechts verschoben zur Basisadresse der normalen Region addiert werden. Die Adressbits 2..4 geben die Bitnummer im Byte an. Die Adressbits 0..1 besitzen keine Funktion und sollten 0 enthalten.

Ladebefehle in einer Bitbanding-Region laden den Wert 1, wenn das adressierte Bit gesetzt ist, sonst 0. Speicherbefehle speichern Bit 0 des Wertes ins adressierte Bit.

Adressierung

Dadurch wird die obige Sequenz nun zu:

 
ldr     r0, =0x422581A4
mov     r1, #1
strh    r1, [r0]

oder in C

*(volatile uint16_t *)0x422581A4 = 1;

Das skizzierte Problem mit Interrupt-Handlern kann hier nicht mehr auftreten, da die Bitmodifikation als ununterbrechbarer Teil des Store-Befehls ausgeführt wird.

Zu beachten

Interner Ablauf

Es handelt sich bei einer Bitmodifikation weiterhin um eine Abfolge von drei getrennten Operationen:

  • Wert aus dem RAM/Peripherieregister laden,
  • Bit setzen,
  • Wert wieder speichern.

Nur wird diese Sequenz vom Core im Store-Befehl ausgeführt. Die Bitbanding-Operation ist also nicht exakt äquivalent zu speziellen Peripherieregistern mit Setzen/Löschen Funktion, wie sie insbesondere bei GPIO Ports nicht selten zu finden sind. Diese speziellen Register führen wirklich nur eine Bitoperation durch, während es sich beim Bitbanding technisch um Operationen auf das gesamte Register handelt.

Siehe: Wo Bitbanding nicht funktioniert

Wortbreite

Der Core verwendet bei den Bitbanding-Operationen intern die Zugriffsbreite genau so, wie sie im Befehl angegeben wird. Ports die nur wortweise angesprochen werden dürfen, müssen also auch beim Bitbanding wortweise angesprochen werden. Halbwort- oder Bytebefehle sind dann nicht zulässig.

Aliasing

Der C Compiler weiss nicht, dass es sich bei den verschiedenen Adressen für die normalen Daten und deren Bitbanding-Adressen in Wirklichkeit um die gleiche Speicherstelle handelt. Es kann also sogenanntes Aliasing auftreten.

Damit der Optimizer des Compilers keinen Strich durch die Rechnung macht, sollten alle Daten, die per Bitbanding angesprochen werden, als "volatile" deklariert werden. Das gilt sowohl für die Daten selbst, als auch für deren Spiegelbereich in der Bitbanding-Region.

Dies betrifft in der Praxis hauptsächlich Bitbanding im RAM-Bereich, da die Peripherieregister ohnehin als "volatile" deklariert sind.

Makros

Um die Adressrechnung vom Bitbanding zu vereinfachen, wird man sinnvollerweise Makros einsetzen. Wer C++ verwendet, kann natürlich auch Templates nutzen.

GCC

Beim GNU Compiler kann man für den Peripheriebereich beispielsweise diese Makros verwenden, die auf den Definitionen und der Konvention von ARM CMSIS aufsetzen und Besonderheiten des GNU Compilers nutzen:

Direkte Adressierung

// Access bitbanded peripheral register by direct address.
#define BBPeriphBit(perReg, bit)   (*(__typeof__(perReg)*) ((PERIPH_BB_BASE + (((unsigned)&(perReg) - PERIPH_BASE) << 5) + ((bit) << 2))))
#define BBPeriphMask(perReg, mask) BBPeriphBit(perReg, BBitOfMask(mask))
#define BBitOfMask(mask)           (31 - __builtin_clz(mask))

Damit lassen sich direkt adressierte Peripherieregister ansprechen:

BBPeriphBit(TIM1->DIER, 9) = 1;

Das Makro BBPeriphMask erwartet keine Bitnummer, sondern eine Bitmaske, da die Definitionen in den Include-Files der Hersteller die Bits u.U. nur als Maske zur Verfügung stellen. Die Umrechnung einer konstanten Maske in eine Bitnummer erfolgt durch den Compiler.

Die Adressrechnung erfolgt bei konstanten Adressen durch den Compiler. Über einen Pointer adressierte Register sollten so nicht verwendet werden, jedenfalls nicht wenn man über diesen Pointer mehrere Zugriffe in der Funktion durchführt, da dann die Adressrechnung zur Laufzeit erfolgt und man sehr von der Intelligenz des Optimierers abhängt.

Indirekte Adressierung

// Convert regular base address of a peripheral to bitbanded base address.
// perPtr: base address of peripheral, typed as pointer to peripheral_TypeDef 
#define BBaseOfPeriph(perPtr)		((__typeof__(*(perPtr))*) (PERIPH_BB_BASE + (((unsigned)(perPtr) - PERIPH_BASE) << 5)))

// Access bitbanded peripheral register by bitbanded base address and register offset.
// perBB: bitbanded base address of peripheral, typed as pointer to peripheral_TypeDef 
// member: member name of peripheral_TypeDef
#define BBOffset(perBB, member)         (__builtin_offsetof(__typeof__(*(perBB)), member) << 5)
#define BBasedBit(perBB, member, bit)   (*(__typeof__((perBB)->member)*) (((unsigned)(perBB) + BBOffset(perBB, member) + ((bit) << 2))))
#define BBasedMask(perBB, member, mask) BBasedBit(perBB, member, BBitOfMask(mask))

Beispiel:

extern TIM_TypeDef *timer;
TIM_TypeDef *timer_bbptr = BBaseOfPeriph(timer);
BBasedMask(timer_bbptr, DIER, TIM_DIER_CC1DE) = 1;

Die Adressrechnung erfolgt hier zur Laufzeit in BBaseOfPeriph(). Bei konstanter Bitnummer erfolgt die Berechnung des Offsets relativ zum Pointer durch den Compiler, beim Zugriff entsteht dann also kein Zusatzaufwand. Der Bitbanding-Pointer timer_bbptr lässt sich für alle Register des Timers verwenden.

Portierung auf andere Compiler

Es wird der Maschinenbefehl CLZ (Count Leading Zeroes) verwendet, um eine Bitmaske in eine Bitnummer zu übersetzen. Alternativ kann dies auch per ?: Kaskade realisiert werden, nur übersetzt sich dies bei nicht-konstanter Maske in hässlich viel Code. GCC berechnet bei konstanter Maske die CLZ Operation bereits selbst.

Mit __typeof__ wird erreicht, dass die Zugriffe automatisch mit der richtigen Breite durchgeführt werden. Ein in Halbwortbreite deklariertes Register wird auch mit Halbwortbefehlen angesprochen.

Das Konstrukt __builtin_offsetof existiert auch als offsetof.