Abb. I. KiykoGute Gesundheit an alle!
Sie erinnern sich wahrscheinlich an eine bärtige Anekdote und vielleicht an eine wahre Geschichte darüber, wie ein Schüler gefragt wurde, wie er die Höhe eines Gebäudes mit einem Barometer messen kann. Der Schüler zitierte meiner Meinung nach ungefähr 20 oder 30 Möglichkeiten, ohne den direkten (durch den Druckunterschied) zu erwähnen, den der Lehrer erwartet hatte.
In etwa der gleichen Weise möchte ich die Verwendung von C ++ für Mikrocontroller weiter diskutieren und Möglichkeiten für die Arbeit mit Registern unter Verwendung von C ++ in Betracht ziehen. Und ich möchte darauf hinweisen, dass es keinen einfachen Weg gibt, um einen sicheren Zugang zu den Registern zu erreichen. Ich werde versuchen, alle Vor- und Nachteile der Methoden aufzuzeigen. Wenn Sie mehr Möglichkeiten kennen, werfen Sie sie in die Kommentare. Also fangen wir an:
Methode 1. Offensichtlich und offensichtlich nicht die beste
Die gebräuchlichste Methode, die auch in C ++ verwendet wird, ist die Beschreibung der Registerstrukturen aus der Header-Datei des Herstellers. Zur Demonstration nehme ich zwei Port-A-Register (ODR - Ausgangsdatenregister und IDR - Eingangsdatenregister) des STM32F411-Mikrocontrollers, damit ich die blinkende LED „Hello World“ zum Einbetten ausführen kann.
int main() { GPIOA->ODR ^= (1 << 5) ; GPIOA->IDR ^= (1 << 5) ;
Mal sehen, was hier passiert und wie dieses Design funktioniert. Der Mikroprozessor-Header enthält die
GPIO_TypeDef Struktur und eine
GPIOA auf diese
GPIOA Struktur. Es sieht so aus:
typedef struct { __IO uint32_t MODER;
Um es in einfachen menschlichen Worten
GPIO_TypeDef Die gesamte Struktur des
GPIO_TypeDef Typs "legt" sich an der Adresse
GPIOA_BASE . Wenn Sie sich auf ein bestimmtes Feld der Struktur beziehen, beziehen Sie sich im Wesentlichen auf die Adresse dieser Struktur + Offset zu einem Element dieser Struktur. Wenn Sie
#define GPIOA entfernen,
#define GPIOA der Code folgendermaßen aus:
((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ; ((GPIO_TypeDef *) GPIOA_BASE)->IDR ^= (1 << 5) ;
In Bezug auf die Programmiersprache C ++ wird eine Ganzzahladresse in einen
GPIO_TypeDef auf die
GPIO_TypeDef Struktur
GPIO_TypeDef . In C ++ versucht der Compiler bei Verwendung der C-Konvertierung, die Konvertierung in der folgenden Reihenfolge durchzuführen:
- const_cast
- static_cast
- static_cast neben const_cast,
- reinterpret_cast
- reinterpret_cast neben const_cast
d.h. Wenn der Compiler den Typ nicht mit const_cast konvertieren konnte, versucht er, static_cast usw. anzuwenden. Als Ergebnis wird der Anruf:
((GPIO_TypeDef *) GPIOA_BASE)->ODR ^= (1 << 5) ;
es gibt nichts wie:
reinterpret_cast<GPIO_TypeDef *> (GPIOA_BASE)->ODR ^= (1 << 5) ;
In der Tat wäre es für C ++ - Anwendungen richtig, die Struktur wie folgt auf die Adresse zu ziehen:
GPIO_TypeDef * GPIOA{reinterpret_cast<GPIO_TypeDef *>(GPIOA_BASE)} ;
In jedem Fall gibt es aufgrund der Typkonvertierung ein großes Minus für diesen Ansatz für C ++. Es besteht in der Tatsache, dass
reinterpret_cast weder in
constexpr Konstruktoren und -Funktionen noch in Vorlagenparametern verwendet werden kann, was die Verwendung von C ++ - Funktionen für Mikrocontroller erheblich reduziert.
Ich werde dies anhand von Beispielen erklären. Dies ist möglich:
struct Test { const int a; const int b; } ; template<Test* mystruct> constexpr const int Geta() { return mystruct->a; } Test test{1,2}; int main() { Geta<&test>() ; }
Dies können Sie jedoch noch nicht tun:
template<GPIO_TypeDef * mystruct> constexpr volatile uint32_t GetIdr() { return mystruct->IDR; } int main() {
Die direkte Verwendung dieses Ansatzes führt daher zu erheblichen Einschränkungen bei der Verwendung von C ++. Wir werden das Objekt, das den
GPIOA im ROM verwenden möchte, mit den
GPIOA nicht finden können, und wir werden die Metaprogrammierung für ein solches Objekt nicht nutzen können.
Darüber hinaus ist diese Methode im Allgemeinen keine Sicherheit (wie unsere westlichen Partner sagen). Immerhin ist es durchaus möglich, NON-FUN zu machen
Im Zusammenhang mit dem oben Gesagten fassen wir zusammen:
Vorteile
- Die Überschrift des Herstellers wird verwendet (es wird geprüft, es gibt keine Fehler)
- Es gibt keine zusätzlichen Gesten und Kosten, die Sie nehmen und verwenden
- Benutzerfreundlichkeit
- Jeder kennt und versteht diese Methode.
- Kein Overhead
Nachteile
- Eingeschränkte Verwendung von Metaprogrammierung
- Unfähigkeit zur Verwendung in constexpr-Konstruktoren
- Bei Verwendung von Wrappern in Klassen ist der zusätzliche RAM-Verbrauch ein Zeiger auf ein Objekt dieser Struktur
- Du kannst dumm machen
Schauen wir uns nun Methode 2 an
Methode 2. Brutal
Es ist offensichtlich, dass jeder Embed-Programmierer die Adressen aller Register für alle Mikrocontroller berücksichtigt, sodass Sie einfach immer die folgende Methode verwenden können, die von Anfang an folgt:
*reinterpret_cast<volatile uint32_t *>(GpioaOdrAddr) ^= (1 <<5) ; *reinterpret_cast<volatile uint32_t *>(GpioaIdrAddr) ^= (1 <<5) ;
Überall im Programm können Sie jederzeit die Konvertierung in die
volatile uint32_t Registeradresse
volatile uint32_t aufrufen und dort zumindest etwas installieren.
Hier gibt es insbesondere keine Pluspunkte, aber zu den Minuspunkten, die zu zusätzlichen Unannehmlichkeiten und der Notwendigkeit führen, die Adresse jedes Registers selbst in eine separate Datei zu schreiben. Daher wenden wir uns der Methode Nummer 3 zu.
Methode 3. Offensichtlich und offensichtlich korrekter
Wenn der Zugriff auf die Register über das Strukturfeld erfolgt, können Sie anstelle eines Zeigers auf das Strukturobjekt die ganzzahlige Strukturadresse verwenden. Die Adresse der Strukturen befindet sich in der Header-Datei des Herstellers (z. B. GPIOA_BASE für GPIOA). Sie müssen sich diese also nicht merken, können sie jedoch in Vorlagen und in constexpr-Ausdrücken verwenden und die Struktur dann mit dieser Adresse "überlagern".
template<uint32_t addr, uint32_t pinNum> struct Pin { using Registers = GPIO_TypeDef ; __forceinline static void Toggle() {
Aus meiner Sicht gibt es keine besonderen Minuspunkte. Im Prinzip eine funktionierende Option. Aber schauen wir uns doch andere Möglichkeiten an.
Methode 4. Exoteric Wrap
Für Kenner von verständlichem Code können Sie einen Wrapper über das Register erstellen, damit Sie bequem darauf zugreifen können und „schön“ aussehen. Erstellen Sie einen Konstruktor und definieren Sie die Operatoren neu:
class Register { public: explicit Register(uint32_t addr) : ptr{ reinterpret_cast<volatile uint32_t *>(addr) } { } __forceinline inline Register& operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile uint32_t *ptr;
Wie Sie sehen können, müssen Sie sich entweder die Ganzzahladressen aller Register merken oder sie irgendwo setzen, und Sie müssen auch einen Zeiger auf die Registeradresse speichern. Aber was wieder nicht sehr gut ist,
reinterpret_cast passiert wieder im Konstruktor
Einige Nachteile und die Tatsache, dass in der ersten und zweiten Version die Notwendigkeit hinzugefügt wurde, dass jedes verwendete Register einen Zeiger auf 4 Bytes im RAM speichert. Im Allgemeinen keine Option. Wir schauen uns Folgendes an.
Methode 4,5. Exoteric Wrap mit Muster
Wir fügen ein Körnchen Metaprogrammierung hinzu, aber es gibt nicht viel Nutzen daraus. Diese Methode unterscheidet sich von der vorherigen nur dadurch, dass die Adresse nicht an den Konstruktor übertragen wird. Im Vorlagenparameter sparen wir jedoch ein wenig an Registern, wenn wir die Adresse an den Konstruktor übergeben. Es ist bereits gut:
template<uint32_t addr> class Register { public: Register() : ptr{reinterpret_cast<volatile uint32_t *>(addr)} { } __forceinline inline Register &operator^=(const uint32_t right) { *ptr ^= right; return *this; } private: volatile std::uint32_t *ptr; }; int main() { using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5);
Und so der gleiche Rechen, Seitenansicht.
Methode 5. Vernünftig
Natürlich müssen Sie den Zeiger entfernen, also machen wir dasselbe, aber entfernen Sie den unnötigen Zeiger aus der Klasse.
template<uint32_t addr> class Register { public: __forceinline Register &operator^=(const uint32_t right) { *reinterpret_cast<volatile uint32_t *>(addr) ^= right; return *this; } }; using GpioaOdr = Register<GpioaOdrAddr>; GpioaOdr Odr; Odr ^= (1 << 5); using GpioaIdr = Register<GpioaIdrAddr>; GpioaIdr Idr; Idr ^= (1 << 5);
Sie können hier bleiben und ein wenig nachdenken. Diese Methode löst sofort zwei Probleme, die zuvor von der ersten Methode geerbt wurden. Erstens kann ich jetzt den Zeiger auf das
Register Objekt in der Vorlage verwenden, und zweitens kann ich ihn an den
constexrp Konstruktor übergeben.
template<Register * register> void Xor(uint32_t mask) { *register ^= mask ; } Register<GpioaOdrAddr> GpioaOdr; int main() { Xor<&GpioaOdr>(1 << 5) ;
Natürlich ist es wieder notwendig, entweder einen eidetischen Speicher für die Adressen der Register zu haben oder alle Adressen der Register irgendwo in einer separaten Datei manuell zu bestimmen ...
Vorteile
- Benutzerfreundlichkeit
- Fähigkeit zur Verwendung von Metaprogrammierung
- Fähigkeit zur Verwendung in constexpr-Konstruktoren
Nachteile
- Die überprüfte Header-Datei des Herstellers wird nicht verwendet
- Sie müssen alle Adressen der Register selbst einstellen
- Sie müssen ein Objekt der Klasse Register erstellen
- Du kannst dumm machen
Großartig, aber es gibt immer noch viele Minuspunkte ...
Methode 6. Klüger als vernünftig
Bei der vorherigen Methode war es für den Zugriff auf das Register erforderlich, ein Objekt dieses Registers zu erstellen. Dies ist eine unnötige Verschwendung von RAM und ROM. Daher erstellen wir einen Wrapper mit statischen Methoden.
template<uint32_t addr> class Register { public: __forceinline inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile uint32_t *>(addr) ^= mask; } }; int main() { using namespace Case6 ; using Odr = Register<GpioaOdrAddr>; Odr::Xor(1 << 5); using Idr = Register<GpioaIdrAddr>; Idr::Xor(1 << 5);
Ein Plus hinzugefügt
- Kein Overhead. Schneller kompakter Code, wie in Option 1 (Bei Verwendung von Wrappern in Klassen fallen keine zusätzlichen RAM-Kosten an, da das Objekt nicht erstellt wird, sondern statische Methoden verwendet werden, ohne Objekte zu erstellen.)
Mach weiter ...
Methode 7. Dummheit entfernen
Offensichtlich mache ich ständig NON-FUNNY im Code und schreibe etwas in das Register, das eigentlich nicht zum Schreiben gedacht ist. Es ist natürlich okay, aber DUMM muss verboten werden. Verbieten wir Unsinn. Dazu führen wir Hilfsstrukturen ein:
struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {};
Jetzt können wir die Register zum Schreiben einstellen und die Register sind schreibgeschützt:
template<uint32_t addr, typename RegisterType> class Register { public:
Versuchen wir nun, unseren Test zu kompilieren und
Idr dass der Test nicht kompiliert wird, da der Operator
^= für das
Idr Register nicht vorhanden ist:
int main() { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr Odr ; Odr ^= (1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr Idr ; Idr ^= (1 << 5) ;
Also, jetzt gibt es mehr Pluspunkte ...
Vorteile
- Benutzerfreundlichkeit
- Fähigkeit zur Verwendung von Metaprogrammierung
- Fähigkeit zur Verwendung in constexpr-Konstruktoren
- Schneller kompakter Code, wie in Option 1
- Bei der Verwendung von Wrappern in Klassen fallen keine zusätzlichen RAM-Kosten an, da das Objekt nicht erstellt wird, sondern statische Methoden verwendet werden, ohne Objekte zu erstellen
- Dummheit kann man nicht machen
Nachteile
- Die überprüfte Header-Datei des Herstellers wird nicht verwendet
- Sie müssen alle Adressen der Register selbst einstellen
- Sie müssen ein Objekt der Klasse Register erstellen
Entfernen wir also die Möglichkeit, eine Klasse zu erstellen, um mehr zu sparen
Methode 8. Ohne Unsinn und ohne Klassenobjekt
Sofort Code:
struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; int main { using GpioaOdr = Register<GpioaOdrAddr, WriteReg> ; GpioaOdr::Xor(1 << 5) ; using GpioaIdr = Register<GpioaIdrAddr, ReadReg> ; GpioaIdr::Xor(1 << 5) ;
Wir fügen ein weiteres Plus hinzu, wir erstellen kein Objekt. Aber mach weiter, wir haben immer noch Nachteile
Methode 9. Methode 8 mit Strukturintegration
In der vorherigen Methode wurde nur der Fall definiert. Bei Methode 1 werden jedoch alle Register zu Strukturen zusammengefasst, sodass Sie bequem über Module darauf zugreifen können. Lass es uns tun ...
namespace Case9 { struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T> class Register { public: __forceinline template <typename T1 = T, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { *reinterpret_cast<volatile int*>(addr) ^= mask; } }; template<uint32_t addr> struct Gpio { using Moder = Register<addr, ReadWriteReg>;
Hier ist das Minus, dass die Strukturen neu registriert werden müssen und die Offsets aller Register irgendwo gespeichert und bestimmt werden sollten. Es wäre schön, wenn die Offsets vom Compiler und nicht von der Person festgelegt würden, aber dies ist später, aber im Moment werden wir eine andere interessante Methode betrachten, die mein Kollege vorgeschlagen hat.
Methode 10. Wrap über das Register durch einen Zeiger auf ein Mitglied der Struktur
Hier verwenden wir ein solches Konzept als Zeiger auf ein Mitglied der Struktur und
Zugriff darauf .
template<uint32_t addr, typename T> class RegisterStructWrapper { public: __forceinline template<typename P> inline static void Xor(PT::*member, int mask) { reinterpret_cast<T*>(addr)->*member ^= mask ;
Vorteile
- Benutzerfreundlichkeit
- Fähigkeit zur Verwendung von Metaprogrammierung
- Fähigkeit zur Verwendung in constexpr-Konstruktoren
- Schneller kompakter Code, wie in Option 1
- Bei der Verwendung von Wrappern in Klassen fallen keine zusätzlichen RAM-Kosten an, da das Objekt nicht erstellt wird, sondern statische Methoden verwendet werden, ohne Objekte zu erstellen
- Die vom Hersteller überprüfte Header-Datei wird verwendet.
- Sie müssen nicht alle Registeradressen selbst festlegen
- Es ist nicht erforderlich, ein Objekt der Klasse Register zu erstellen
Nachteile
- Sie können Dummheit machen und sogar über die Verständlichkeit des Codes spekulieren.
Methode 10.5. Kombinieren Sie Methode 9 und 10
Um die Verschiebung des Registers relativ zum Anfang der Struktur herauszufinden, können Sie den Zeiger auf das Mitglied der Struktur verwenden:
volatile uint32_t T::*member gibt den Offset des Mitglieds der Struktur relativ zu seinem Anfang in Bytes zurück. Zum Beispiel haben wir die
GPIO_TypeDef Struktur, dann ist die Adresse
&GPIO_TypeDef::ODR 0x14.
Wir nutzen diese Gelegenheit und berechnen die Adressen der Register aus Methode 9 mit dem Compiler:
struct WriteReg {}; struct ReadReg {}; struct ReadWriteReg: public WriteReg, public ReadReg {}; template<uint32_t addr, typename T, volatile uint32_t T::*member, typename RegType> class Register { public: __forceinline template <typename T1 = RegType, class = typename std::enable_if_t<std::is_base_of<WriteReg, T1>::value>> inline static void Xor(const uint32_t mask) { reinterpret_cast<T*>(addr)->*member ^= mask ; } }; template<uint32_t addr, typename T> struct Gpio { using Moder = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, ReadWriteReg>; using Otyper = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OTYPER, ReadWriteReg>; using Ospeedr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::OSPEEDR, ReadWriteReg>; using Pupdr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::PUPDR, ReadWriteReg>; using Idr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::IDR, ReadReg>; using Odr = Register<addr, GPIO_TypeDef, &GPIO_TypeDef::ODR, WriteReg>; } ;
Sie können exoterischer mit Registern arbeiten:
using namespace Case11 ; using Gpioa = Gpio<GPIOA_BASE, GPIO_TypeDef> ; Gpioa::Odr::Xor(1 << 5) ;
Natürlich müssen hier alle Strukturen neu geschrieben werden. Dies kann automatisch durch ein Skript in Phyton an der Eingabe wie stm32f411xe.h an der Ausgabe Ihrer Datei mit Strukturen zur Verwendung in C ++ erfolgen.
In jedem Fall gibt es verschiedene Möglichkeiten, die in einem bestimmten Projekt funktionieren können.
Bonus Wir führen die Spracherweiterung und den Parsim-Code mit Phyton ein
Das Problem der Arbeit mit Registern in C ++ besteht schon seit geraumer Zeit. Die Leute lösen es auf unterschiedliche Weise. Natürlich wäre es großartig, wenn die Sprache beim Kompilieren so etwas wie das Umbenennen von Klassen unterstützen würde. Sagen wir mal, was wäre, wenn es so wäre:
template<classname = [PortName]> class Gpio[Portname] { __forceinline inline static void Xor(const uint32_t mask) { GPIO[PortName]->ODR ^= mask ; } }; int main() { using GpioA = Gpio<"A"> ; GpioA::Xor(5) ; }
Leider unterstützt diese Sprache nicht. Daher besteht die Lösung darin, Code mit Python zu analysieren. Das heißt, Einige Spracherweiterungen werden eingeführt. Der Code, der diese Erweiterung verwendet, wird dem Python-Parser zugeführt, der ihn in C ++ - Code übersetzt. Ein solcher Code sieht ungefähr so aus: (Ein Beispiel stammt aus der Modm-Bibliothek;
hier sind die vollständigen Quellen ):
%% set port = gpio["port"] | upper %% set reg = "GPIO" ~ port %% set pin = gpio["pin"] class Gpio{{ port ~ pin }} : public Gpio { __forceinline inline static void Xor() { GPIO{{port}}->ODR ^= 1 << {{pin}} ; } }
Update: Bonus. SVD-Dateien und Parser auf Phyton
Ich habe vergessen, eine weitere Option hinzuzufügen. ARM veröffentlicht für jeden SVD-Hersteller eine Registerbeschreibungsdatei. Daraus können Sie dann eine C ++ - Datei mit einer Beschreibung der Register generieren. Paul Osborne hat alle diese Dateien auf
GitHub zusammengestellt . Er schrieb auch ein Python-Skript, um sie zu analysieren.
Das ist alles ... meine Fantasie ist erschöpft. Wenn Sie noch Ideen haben, wenden Sie sich bitte an. Hier liegt ein Beispiel mit allen Methoden
.Referenzen
Typesicherer Registerzugriff in C ++Dinge erledigen lassen - Zugriff auf Hardware aus C ++Dinge dazu bringen, Dinge zu tun - Teil 3Dinge dazu bringen, Dinge zu tun - Strukturüberlagerung