|
|
ARM Bitbanding
[Bearbeiten] HintergrundEine 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
oder in C formuliert
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. [Bearbeiten] ArbeitsweiseIn 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. Dadurch wird die obige Sequenz nun zu:
oder in C
Das skizzierte Problem mit Interrupt-Handlern kann hier nicht mehr auftreten, da die Bitmodifikation als ununterbrechbarer Teil des Store-Befehls ausgeführt wird. [Bearbeiten] Zu beachten[Bearbeiten] Interner AblaufEs handelt sich bei einer Bitmodifikation weiterhin um eine Abfolge von drei getrennten Operationen:
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 [Bearbeiten] WortbreiteDer Core verwendet bei den Bitbanding-Operationen intern die Zugriffsbreite genau so, wie sie im Befehl angegeben wird. Ports die nur nur wortweise angesprochen werden dürfen, müssen also auch beim Bitbanding wortweise angesprochen werden. Halbwort- oder Bytebefehle sind dann nicht zulässig. [Bearbeiten] AliasingDer 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. [Bearbeiten] MakrosUm die Adressrechnung vom Bitbanding zu vereinfachen wird man sinnvollerweise Makros einsetzen. Wer C++ verwendet kann natürlich auch Templates nutzen. [Bearbeiten] GCCBeim 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: [Bearbeiten] Direkte Adressierung
Damit lassen sich direkt adressierte Peripherieregister ansprechen:
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. [Bearbeiten] Indirekte Adressierung
Beispiel:
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. [Bearbeiten] Portierung auf andere CompilerEs 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. |