Sicherer Zugriff auf Registerfelder in C ++ ohne Einbußen bei der Effizienz (am Beispiel von CortexM)

Bild
Abb. entnommen aus www.extremetech.com/wp-content/uploads/2016/07/MegaProcessor-Feature.jpg

Gute Gesundheit an alle!

In einem früheren Artikel habe ich das Problem des Zugriffs auf Register eines Mikrocontrollers mit einem CortexM-Kern in C ++ untersucht und einfache Lösungen für einige der Probleme gezeigt.

Heute möchte ich die Idee zeigen, wie der Zugriff auf das Register und seine Felder sicher sein kann, ohne die Effizienz zu beeinträchtigen, indem C ++ - Klassen verwendet werden, die aus SVD-Dateien generiert wurden.

Jeder, an dem Sie interessiert sind, willkommen bei cat. Es wird viel Code geben.

Einführung


In einem Artikel zu C ++ Hardware Register Access Redux zeigte Ken Smith, wie man sicher und effizient mit Registern arbeitet, und zeigte es sogar am Beispiel von github.com/kensmith/cppmmio .
Dann entwickelten mehrere Leute diese Idee, zum Beispiel machte Niklas Hauser eine wundervolle Bewertung und schlug mehrere weitere Möglichkeiten vor, um sicher auf Register zuzugreifen.

Einige dieser Ideen wurden bereits in verschiedenen Bibliotheken umgesetzt, insbesondere in modm . Für immer können Sie alle diese Sätze im wirklichen Leben verwenden. Zum Zeitpunkt der Entwicklung dieser Bibliotheken hatten die Beschreibungen von Peripheriegeräten und Registern gerade erst begonnen, standardisiert zu werden, und daher wurden einige Dinge mit dem Ziel getan, dass die Hauptarbeit zur Beschreibung von Registern beim Programmierer liegen würde. Einige Lösungen sind auch in Bezug auf Code- und Mikrocontroller-Ressourcen nicht effektiv.

Heute liefert jeder Hersteller eines ARM-Mikrocontrollers eine Beschreibung aller Register im SVD-Format. Aus diesen Beschreibungen können Header-Dateien generiert werden, sodass nicht nur eine einfache, sondern auch eine komplexere Beschreibung der Register erstellt werden kann, was gleichzeitig die Zuverlässigkeit Ihres Codes erhöht. Und es ist großartig, dass die Ausgabedatei in einer beliebigen Sprache, C, C ++ oder sogar D vorliegen kann

Aber nehmen wir es in der Reihenfolge, was im Allgemeinen sicherer Zugang zu Registern ist und warum es überhaupt notwendig ist. Die Erklärung kann an einfachen synthetischen, höchstwahrscheinlich unwahrscheinlichen, aber durchaus möglichen Beispielen gezeigt werden:

int main(void) { //      GPIOA //,    AHB1ENR RCC->APB1ENR |= RCC_AHB1ENR_GPIOAEN ; //    ,      RCC->AHB1ENR = RCC_AHB1ENR_GPIOAEN; //,  TIM1    APB2 RCC->APB1ENR |= RCC_APB1ENR_TIM2EN | RCC_APB2ENR_TIM1EN; // - ,        . auto result = GPIOA->BSRR ; if (result & GPIO_BSRR_BS1) { //do something } //-     .   ... GPIOA->IDR = GPIO_IDR_ID5 ; } 

All diese Fälle sind in der Praxis möglich, und ich habe so etwas definitiv von meinen Schülern gesehen. Es wäre großartig, wenn Sie solche Fehler verbieten könnten.

Es scheint mir auch viel angenehmer zu sein, wenn der Code ordentlich aussieht und keine Kommentare benötigt. Selbst wenn Sie beispielsweise den Mikrocontroller STM32F411 gut kennen, ist es nicht immer möglich zu verstehen, was in diesem Code geschieht:

 int main() { uint32 temp = GPIOA->OSPEEDR ; temp &=~ GPIO_OSPEEDR_OSPEED0_Msk ; temp = (GPIO_OSPEEDR_OSPEED0_0 | GPIO_OSPEEDR_OSPEED0_1) ; GPIOA->OSPEEDR = temp; } 

Kein Kommentar hier kann nicht tun. Der Code setzt die Betriebsfrequenz des GPIOA.0-Ports auf Maximum (Klarstellung von mctMaks : Tatsächlich beeinflusst dieser Parameter die Anstiegszeit der Front (d. H. Ihre Steilheit) und bedeutet, dass der Port ein digitales Signal normal bei einer bestimmten verarbeiten kann (VeryLow \ Low \ Medium) \ Hochfrequenz).

Versuchen wir, diese Mängel zu beseitigen.

Abstraktion registrieren


Zunächst müssen Sie herausfinden, wie das Register aus Sicht des Programmierers und des Programms aussieht.

Das Register hat eine Adresse, Länge oder Größe, Zugriffsmodus: Einige Register können geschrieben werden, einige können nur gelesen werden und die meisten können gelesen und geschrieben werden.

Zusätzlich kann das Register als eine Reihe von Feldern dargestellt werden. Ein Feld kann aus einem Bit oder mehreren Bits bestehen und befindet sich an einer beliebigen Stelle im Register.

Daher sind für uns folgende Feldmerkmale wichtig: Länge oder Größe ( Breite oder Größe ), Versatz vom Registeranfang ( Versatz ) und Wert.

Feldwerte sind der Raum aller möglichen Größen, die das Feld aufnehmen kann, und hängen von der Länge des Feldes ab. Das heißt, Wenn das Feld eine Länge von 2 hat, gibt es 4 mögliche Feldwerte (0,1,2,3). Felder und Feldwerte haben wie das Register einen Zugriffsmodus (Lesen, Schreiben, Lesen und Schreiben)

Nehmen wir zur Verdeutlichung das Register TIM1 CR1 vom Mikrocontroller STM32F411. Schematisch sieht es so aus:

Bild

  • Bit 0 CEN: Zähler aktivieren
    0: Zähler aktiviert: Deaktivieren
    1: Zähler aus: Aktivieren
  • UDIS-Bit 1: UEV-Ereignis aktivieren / deaktivieren
    0: UEV-Ereignis aktiviert: Aktivieren
    1: UEV-Ereignis aus: Deaktivieren
  • URS-Bit 2: Wählen Sie UEV-Ereignisgenerierungsquellen aus
    0: UEV wird beim Überlaufen oder beim Setzen des UG: Beliebiges Bit erzeugt
    1: UEV wird nur bei Überlauf generiert: Überlauf
  • Bit 3 OPM: Einmaliger Betrieb
    0: Der Timer zählt nach dem Ereignis UEV: ContinueAfterUEV weiter
    1: Der Timer stoppt nach dem Ereignis UEV: StopAfterUEV
  • Bit 4 DIR: Zählrichtung
    0: Direktes Konto: Upcounter
    1: Countdown : Downcounter
  • Bit 6: 5 CMS: Ausrichtungsmodus
    0: Ausrichtungsmodus 0: CenterAlignedMode0
    1: Ausrichtungsmodus 1: CenterAlignedMode1
    2: Ausrichtungsmodus 2: CenterAlignedMode2
    3: Ausrichtungsmodus 3: CenterAlignedMode3
  • Bit 7 APRE: Preload-Modus für ARR-Register
    0: TIMx_ARR-Register ist nicht gepuffert: ARRNotBuffered
    1: TIMx_ARR-Register ist nicht gepuffert: ARRBuffered
  • Bit 8: 9 CKD: Taktteiler
    0: tDTS = tCK_INT: ClockDevidedBy1
    1: tDTS = 2 * tCK_INT: ClockDevidedBy2
    2: tDTS = 4 * tCK_INT: ClockDevidedBy4
    3: Reserviert: Reserviert

Hier ist CEN beispielsweise ein 1-Bit-Feld mit einem Versatz von 0 relativ zum Beginn des Registers. Und Enable (1) und Disable (0) sind mögliche Werte.

Wir werden uns nicht darauf konzentrieren, wofür jedes Feld dieses Registers speziell verantwortlich ist. Für uns ist es wichtig, dass jedes Feld und jeder Feldwert einen Namen hat, der eine semantische Last trägt und von dem wir im Prinzip verstehen können, was es tut.

Wir müssen sowohl auf das Register als auch auf das Feld und seinen Wert zugreifen können. Daher kann die Registerabstraktion in einer sehr ungefähren Form durch die folgenden Klassen dargestellt werden:

Bild

Neben Klassen ist es für uns auch wichtig, dass Register und einzelne Felder bestimmte Eigenschaften haben, das Register eine Adresse, Größe, Zugriffsmodus (schreibgeschützt, schreibgeschützt oder beides).
Das Feld hat Größe, Versatz und auch Zugriffsmodus. Außerdem sollte das Feld einen Link zu dem Register enthalten, zu dem es gehört.

Der Feldwert muss einen Link zum Feld und ein zusätzliches Attribut enthalten - den Wert.

Daher wird unsere Abstraktion in einer detaillierteren Version folgendermaßen aussehen:



Zusätzlich zu den Attributen sollte unsere Abstraktion über Änderungs- und Zugriffsmethoden verfügen. Der Einfachheit halber beschränken wir uns auf Installations- / Schreib- und Lesemethoden.

Bild

Wenn wir uns für eine Fallabstraktion entschieden haben, müssen wir überprüfen, wie diese Abstraktion dem entspricht, was in der SVD-Datei beschrieben ist.

SVD-Datei (System View Description)


Das CMSIS-Systempräsentationsbeschreibungsformat (CMSIS-SVD) ist eine formale Beschreibung von Mikrocontroller-Registern, die auf dem ARM Cortex-M-Prozessor basieren. Die in den Beschreibungen der Systemdarstellung enthaltenen Informationen entsprechen praktisch den Daten in den Referenzhandbüchern für Geräte. Die Beschreibung der Register in einer solchen Datei kann sowohl Informationen auf hoher Ebene als auch den Zweck eines einzelnen Bits des Feldes im Register enthalten.

Schematisch kann der Detaillierungsgrad der Informationen in einer solchen Datei durch das folgende Schema beschrieben werden, das auf der Keil-Website abgerufen wird :

Bild

Beschreibung SVD-Dateien werden von Herstellern bereitgestellt und während des Debuggens verwendet, um Informationen über den Mikrocontroller und die Register anzuzeigen. Die IAR verwendet sie beispielsweise, um Informationen im Bereich Ansicht-> Register anzuzeigen. Die Dateien selbst befinden sich im Ordner Programme (x86) \ IAR Systems \ Embedded Workbench 8.3 \ arm \ config \ debugger.

Clion von JetBrains verwendet auch SVD-Dateien, um Registerinformationen während des Debuggens anzuzeigen.

Sie können jederzeit Beschreibungen von den Websites des Herstellers herunterladen. Hier können Sie die SVD-Datei für den STM32F411-Mikrocontroller aufnehmen

Im Allgemeinen ist das SVD-Format ein Standard, den Hersteller unterstützen. Mal sehen, wie die Beschreibungsebenen in SVD sind.

Insgesamt werden 5 Ebenen unterschieden: Geräteebene, Mikrocontrollerebene, Registerebene, Feldebene, Ebene der aufgezählten Werte.

  • Geräteebene : Die Beschreibung der obersten Ebene der Systemansicht ist das Gerät. Auf dieser Ebene werden die Eigenschaften des gesamten Geräts beschrieben. Zum Beispiel ein Gerätename, eine Beschreibung oder eine Version. Die minimal adressierbare Einheit sowie die Bittiefe des Datenbusses. Die Standardwerte für Registerattribute wie Registergröße, Rücksetzwert und Zugriffsberechtigungen können für das gesamte Gerät auf dieser Ebene festgelegt werden und werden implizit von niedrigeren Beschreibungsebenen übernommen.
  • Mikrocontroller-Ebene: Der Abschnitt CPU beschreibt den Kern des Mikrocontrollers und seine Funktionen. Dieser Abschnitt ist erforderlich, wenn die SVD-Datei zum Erstellen der Geräte-Header-Datei verwendet wird.
  • Peripherieschicht : Ein Peripheriegerät ist eine benannte Sammlung von Registern. Ein Peripheriegerät wird einer bestimmten Basisadresse im Adressraum des Geräts zugeordnet.
  • Registerebene : Ein Register ist eine benannte programmierbare Ressource, die zu einem Peripheriegerät gehört. Register werden einer bestimmten Adresse im Adressraum des Geräts zugeordnet. Die Adresse ist relativ zur Basis-Peripherieadresse. Für das Register wird auch der Zugriffsmodus (Lesen / Schreiben) angezeigt.
  • Feldebene : Wie oben erwähnt, können Register in Teile von Bits unterschiedlicher Funktionalität unterteilt werden - Felder. Diese Ebene enthält die Namen von Feldern, die innerhalb desselben Registers eindeutig sind, ihre Größe, Offsets relativ zum Registeranfang sowie den Zugriffsmodus.
  • Die Ebene der aufgezählten Feldwerte : Tatsächlich handelt es sich um benannte Feldwerte, die zur Vereinfachung in C, C ++, D usw. verwendet werden können.

Tatsächlich sind SVD-Dateien normale XML-Dateien mit einer vollständigen Beschreibung des Systems. Hier gibt es beispielsweise SVD-Dateikonverter für C-Code, die C-freundliche Header und Strukturen für jede Peripherie und jedes Register generieren.

Es gibt auch einen in Phyton geschriebenen SVD -Dateiparser cmsis-svd , der so etwas wie das Deserialisieren von Daten aus einer Datei in Phython-Klassenobjekte ausführt, die dann bequem in Ihrem Codegenerierungsprogramm verwendet werden.

Ein Beispiel für die Beschreibung des Registers des STM32F411-Mikrocontrollers kann unter dem Spoiler eingesehen werden:

Beispielregister CR1 Timer TIM1
 <peripheral> <name>TIM1</name> <description>Advanced-timers</description> <groupName>TIM</groupName> <baseAddress>0x40010000</baseAddress> <addressBlock> <offset>0x0</offset> <size>0x400</size> <usage>registers</usage> </addressBlock> <registers> <register> <name>CR1</name> <displayName>CR1</displayName> <description>control register 1</description> <addressOffset>0x0</addressOffset> <size>0x20</size> <access>read-write</access> <resetValue>0x0000</resetValue> <fields> <field> <name>CKD</name> <description>Clock division</description> <bitOffset>8</bitOffset> <bitWidth>2</bitWidth> </field> <field> <name>ARPE</name> <description>Auto-reload preload enable</description> <bitOffset>7</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>CMS</name> <description>Center-aligned mode selection</description> <bitOffset>5</bitOffset> <bitWidth>2</bitWidth> </field> <field> <name>DIR</name> <description>Direction</description> <bitOffset>4</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>OPM</name> <description>One-pulse mode</description> <bitOffset>3</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>URS</name> <description>Update request source</description> <bitOffset>2</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>UDIS</name> <description>Update disable</description> <bitOffset>1</bitOffset> <bitWidth>1</bitWidth> </field> <field> <name>CEN</name> <description>Counter enable</description> <bitOffset>0</bitOffset> <bitWidth>1</bitWidth> </field> </fields> </register> <register> 


Wie Sie sehen können, gibt es alle Informationen, die für unsere Abstraktion erforderlich sind, mit Ausnahme der Beschreibung der Werte bestimmter Bits der Felder.

Nicht alle Hersteller möchten Zeit mit einer vollständigen Beschreibung ihres Systems verbringen. Wie Sie sehen, wollte ST die Feldwerte nicht beschreiben und übertrug diese Belastung auf Kundenprogrammierer. TI kümmert sich jedoch um seine Kunden und beschreibt das System vollständig, einschließlich der Beschreibung der Feldwerte.

Das Obige zeigt, dass das Format der SVD-Beschreibung sehr gut mit unserer Fallabstraktion übereinstimmt. Die Datei enthält alle notwendigen Informationen, um das Register vollständig zu beschreiben.

Implementierung


Registrieren


Nachdem wir eine Abstraktion des Registers vorgenommen haben und eine Beschreibung der Register in Form einer DVD von den Herstellern haben, die für diese Abstraktion ideal geeignet ist, können wir direkt zur Implementierung übergehen.

Unsere Implementierung sollte so effektiv wie C-Code und benutzerfreundlich sein. Ich möchte, dass der Zugriff auf die Register so klar wie möglich aussieht, zum Beispiel wie folgt:

  if (TIM1::CR1::CKD::DividedBy2::IsSet()) { TIM1::ARR::Set(10_ms) ; TIM1::CR1::CEN::Enable::Set() ; } 

Denken Sie daran, dass Sie reinterpret_cast verwenden müssen, um auf die Ganzzahlregisteradresse zuzugreifen:

 *reinterpret_cast<volatile uint32_t *>(0x40010000) = (1U << 5U) ; 

Die Registerklasse wurde bereits oben beschrieben. Sie muss über eine Adresse, Größe und einen Zugriffsmodus sowie zwei Methoden Get() und Set() verfügen:

 //      template<uint32_t address, size_t size, typename AccessMode> struct RegisterBase { static constexpr auto Addr = address ; using Type = typename RegisterType<size>::Type ; // Set     , //     __forceinline template<typename T = AccessMode, class = typename std::enable_if_t<std::is_base_of<WriteMode, T>::value>> inline static void Set(Type value) { *reinterpret_cast<volatile Type *>(address) = value ; } // Get    , //    ,    __forceinline template<typename T = AccessMode, class = typename std::enable_if_t<std::is_base_of<ReadMode, T>::value>> inline static Type Get() { return *reinterpret_cast<volatile Type *>(address) ; } } ; 

Wir übergeben die Adresse, die Registerlänge und den Zugriffsmodus an die Vorlagenparameter (dies ist auch eine Klasse). Unter Verwendung des SFINAE- Mechanismus, nämlich der enable_if enable_if, werden die enable_if Set() oder Get() für Register "verworfen", die sie nicht unterstützen sollten. Wenn das Register beispielsweise schreibgeschützt ist, übergeben wir ReadMode Typ an den Template-Parameter. enable_if prüft, ob der Zugriff der Nachfolger von ReadMode ist. ReadMode wird ein kontrollierter Fehler erstellt (Typ T kann nicht angezeigt werden), und der Compiler enthält die Set() Methode nicht Set() für ein solches Register. Gleiches gilt für ein Register, das nur zum Schreiben bestimmt ist.

Für die Zugriffskontrolle verwenden wir die Klassen:

 //    struct WriteMode {}; struct ReadMode {}; struct ReadWriteMode: public WriteMode, public ReadMode {}; 

Register gibt es in verschiedenen Größen: 8, 16, 32, 64 Bit. Für jedes von ihnen legen wir unseren Typ fest:

Registertyp je nach Größe
 template <uint32_t size> struct RegisterType {} ; template<> struct RegisterType<8> { using Type = uint8_t ; } ; template<> struct RegisterType<16> { using Type = uint16_t ; } ; template<> struct RegisterType<32> { using Type = uint32_t ; } ; template<> struct RegisterType<64> { using Type = uint64_t ; } ; 


Danach können Sie für den TIM1-Timer das CR1-Register und beispielsweise das AGR-Register folgendermaßen definieren:

 struct TIM1 { struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { } struct EGR : public RegisterBase<0x40010014, 32, WriteMode> { } } int main() { TIM1::CR1::Set(10) ; auto reg = TIM1::CR1::Get() ; // ,     reg = TIM1::EGR::Get() } 

Da der Compiler die Get() -Methode nur für Register ReadMode , in denen der Zugriffsmodus von ReadMode geerbt ReadMode , und die Set() -Methoden für Register, in denen der Zugriffsmodus von WriteMode geerbt wird, erhalten Sie bei falscher Verwendung von Zugriffsmethoden einen Fehler in der Kompilierungsphase. Wenn Sie moderne Entwicklungstools wie Clion bereits in der Codierungsphase verwenden, wird vom Code-Analysator eine Warnung angezeigt:

Bild

Nun, der Zugriff auf Register ist jetzt sicherer geworden. Unser Code erlaubt es Ihnen nicht, Dinge zu tun, die für dieses Register nicht akzeptabel sind, aber wir möchten weiter gehen und nicht auf das gesamte Register, sondern auf seine Felder verweisen.

Felder


Das Feld anstelle der Adresse hat einen Verschiebungswert relativ zum Anfang des Registers. Um zu wissen, an welche Adresse oder welchen Typ der Wert des Feldes gebracht werden muss, muss außerdem ein Link zum Registrieren vorhanden sein:

 //        template<typename Reg, size_t offset, size_t size, typename AccessMode> struct RegisterField { using RegType = typename Reg::Type ; using Register = Reg ; static constexpr RegType Offset = offset ; static constexpr RegType Size = size ; using Access = AccessMode ; template<typename T = AccessMode, class = typename std::enable_if_t<std::is_base_of<WriteMode, T>::value>> static void Set(RegType value) { assert(value < (1U << size)) ; //CriticalSection cs ; //    RegType newRegValue = *reinterpret_cast<RegType *>(Register::Address) ; //       newRegValue &= ~ (((1U << size) - 1U) << offset); //    newRegValue |= (value << offset) ; //      *reinterpret_cast<RegType *>(Reg::Address) = newRegValue ; } __forceinline template<typename T = AccessMode, class = typename std::enable_if_t<std::is_base_of<ReadMode, T>::value>> inline static RegType Get() { return ((*reinterpret_cast<RegType *>(Reg::Address)) & (((1U << size) - 1U) << offset)) >> offset ; } }; 

Danach ist es bereits möglich, folgende Dinge zu tun:

 struct TIM1 { struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { using CKD = RegisterField<TIM1::CR1, 8, 2, ReadWriteMode> ; using ARPE = RegisterField<TIM1::CR1, 7, 1, ReadWriteMode> ; using CMS = RegisterField<TIM1::CR1, 5, 2, ReadWriteMode> ; using DIR = RegisterField<TIM1::CR1, 4, 1, ReadWriteMode> ; using OPM = RegisterField<TIM1::CR1, 3, 1, ReadWriteMode> ; using URS = RegisterField<TIM1::CR1, 2, 1, ReadWriteMode> ; using UDIS = RegisterField<TIM1::CR1, 1, 1, ReadWriteMode> ; using CEN = RegisterField<TIM1::CR1, 0, 1, ReadWriteMode> ; } } int main() { //   CR1  9   1,  8  0 TIM1::CR1::CKD::Set(2U) ; auto reg = TIM1::CR1::CEN::Get() ; } 

Obwohl im Großen und Ganzen alles ziemlich gut aussieht, ist immer noch nicht ganz klar, was TIM1::CR1::CKD::Set(2) bedeutet. Was bedeuten die an die Set() -Funktion übergebenen magischen zwei? Und was bedeutet die von der TIM1::CR1::CEN::Get() -Methode zurückgegebene Zahl?

Gehen Sie nahtlos zu den Feldwerten über.

Feldwert


Die Abstraktion eines Feldwertes ist im Wesentlichen auch ein Feld, kann jedoch nur einen Zustand akzeptieren. Der Feldabstraktion werden Attribute hinzugefügt - der tatsächliche Wert und eine Verknüpfung zum Feld. Die Set() -Methode zum Festlegen des Feldwerts ist identisch mit der Set() -Methode zum Festlegen des Felds, außer dass der Wert selbst nicht an die Methode übergeben werden muss, sondern im Voraus bekannt ist, sondern nur festgelegt werden muss. Die Get() -Methode macht jedoch keinen Sinn. Stattdessen sollten Sie überprüfen, ob dieser Wert festgelegt ist oder nicht. Ersetzen Sie diese Methode durch die IsSet() -Methode.

 //        template<typename Field, typename Field::Register::Type value> struct FieldValueBase { using RegType = typename Field::Register::Type ; template<typename T = typename Field::Access, class = typename std::enable_if_t<std::is_base_of<WriteMode, T>::value>> static void Set() { RegType newRegValue = *reinterpret_cast<RegType *>(Field::Register::Address) ; newRegValue &= ~ (((1U << Field::Size) - 1U) << Field::Offset); newRegValue |= (value << Field::Offset) ; *reinterpret_cast<RegType *>(Field::Register::Address) = newRegValue ; } __forceinline template<typename T = typename Field::Access, class = typename std::enable_if_t<std::is_base_of<ReadMode, T>::value>> inline static bool IsSet() { return ((*reinterpret_cast<RegType *>(Field::Register::Address)) & static_cast<RegType>(((1U << Field::Size) - 1U) << Field::Offset)) == (value << Field::Offset) ; } }; 

Das Registerfeld kann nun durch eine Reihe seiner Werte beschrieben werden:

Werte der CR1-Registerfelder des Timers TIM1
 template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_CKD_Values: public RegisterField<Reg, offset, size, AccessMode> { using DividedBy1 = FieldValue<TIM_CR_CKD_Values, 0U> ; using DividedBy2 = FieldValue<TIM_CR_CKD_Values, 1U> ; using DividedBy4 = FieldValue<TIM_CR_CKD_Values, 2U> ; using Reserved = FieldValue<TIM_CR_CKD_Values, 3U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_ARPE_Values: public RegisterField<Reg, offset, size, AccessMode> { using ARRNotBuffered = FieldValue<TIM_CR_ARPE_Values, 0U> ; using ARRBuffered = FieldValue<TIM_CR_ARPE_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_CMS_Values: public RegisterField<Reg, offset, size, AccessMode> { using CenterAlignedMode0 = FieldValue<TIM_CR_CMS_Values, 0U> ; using CenterAlignedMode1 = FieldValue<TIM_CR_CMS_Values, 1U> ; using CenterAlignedMode2 = FieldValue<TIM_CR_CMS_Values, 2U> ; using CenterAlignedMode3 = FieldValue<TIM_CR_CMS_Values, 3U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_DIR_Values: public RegisterField<Reg, offset, size, AccessMode> { using Upcounter = FieldValue<TIM_CR_DIR_Values, 0U> ; using Downcounter = FieldValue<TIM_CR_DIR_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_OPM_Values: public RegisterField<Reg, offset, size, AccessMode> { using ContinueAfterUEV = FieldValue<TIM_CR_OPM_Values, 0U> ; using StopAfterUEV = FieldValue<TIM_CR_OPM_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_URS_Values: public RegisterField<Reg, offset, size, AccessMode> { using Any = FieldValue<TIM_CR_URS_Values, 0U> ; using Overflow = FieldValue<TIM_CR_URS_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_UDIS_Values: public RegisterField<Reg, offset, size, AccessMode> { using Enable = FieldValue<TIM_CR_UDIS_Values, 0U> ; using Disable = FieldValue<TIM_CR_UDIS_Values, 1U> ; } ; template <typename Reg, size_t offset, size_t size, typename AccessMode> struct TIM_CR_CEN_Values: public RegisterField<Reg, offset, size, AccessMode> { using Disable = FieldValue<TIM_CR_CEN_Values, 0U> ; using Enable = FieldValue<TIM_CR_CEN_Values, 1U> ; } ; 


Dann wird das CR1-Register selbst bereits wie folgt beschrieben:

 struct TIM1 { struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { using CKD = TIM_CR1_CKD_Values<TIM1::CR1, 8, 2, ReadWriteMode> ; using ARPE = TIM_CR1_ARPE_Values<TIM1::CR1, 7, 1, ReadWriteMode> ; using CMS = TIM_CR1_CMS_Values<TIM1::CR1, 5, 2, ReadWriteMode> ; using DIR = TIM_CR1_DIR_Values<TIM1::CR1, 4, 1, ReadWriteMode> ; using OPM = TIM_CR1_OPM_Values<TIM1::CR1, 3, 1, ReadWriteMode> ; using URS = TIM_CR1_URS_Values<TIM1::CR1, 2, 1, ReadWriteMode> ; using UDIS = TIM_CR1_UDIS_Values<TIM1::CR1, 1, 1, ReadWriteMode> ; using CEN = TIM_CR1_CEN_Values<TIM1::CR1, 0, 1, ReadWriteMode> ; } ; } 

Jetzt können Sie den Wert des Registerfelds direkt einstellen und lesen: Wenn Sie beispielsweise den Timer für das Konto aktivieren möchten, rufen Sie einfach die Set() -Methode für den Enable des CEN-Felds des Registers CR1 des Timers TIM1: TIM1::CR1::CEN::Enable::Set() ; . Im Code sieht es folgendermaßen aus:

 int main() { if (TIM1::CR1::CKD::DividedBy2::IsSet()) { TIM1::ARR::Set(100U) ; TIM1::CR1::CEN::Enable::Set() ; } } 

Zum Vergleich dasselbe mit dem C-Header:
 int main() { if((TIM1->CR1 & TIM_CR1_CKD_Msk) == TIM_CR1_CKD_0) { TIM1->ARR = 100U ; regValue = TIM1->CR1 ; regValue &=~(TIM_CR1_CEN_Msk) ; regValue |= TIM_CR1_CEN ; TIM1->CR1 = regValue ; } } 


Wenn also die wichtigsten Verbesserungen vorgenommen werden, können wir einfach und verständlich auf das Register, seine Felder und Werte zugreifen.Der Zugriff wird auf Kompilierungsebene gesteuert. Wenn das Register, Feld oder der Wert kein Schreiben oder Lesen zulässt, wird dies bereits gelöscht, bevor der Code in den Mikrocontroller geflasht wird.

Es gibt jedoch immer noch einen Nachteil: Es ist unmöglich, mehrere Feldwerte gleichzeitig in das Register aufzunehmen. Stellen Sie sich vor, was Sie dazu brauchen:

 int main() { uint32_t regValue = TIM1->CR1 ; regValue &=~(TIM_CR1_CKD_Msk | TIM_CR1_DIR) ; regValue |= (TIM_CR1_CEN | TIM_CR1_CKD_0 | TIM_CR1_CKD_0) ; TIM1->CR1 = regValue ; } 

Dazu müssen wir entweder eine Methode im Register Set(...)mit einer variablen Anzahl von Argumenten erstellen oder versuchen, die Werte der Felder anzugeben, die in der Vorlage festgelegt werden müssen. Das heißt, Implementieren Sie eine der folgenden Optionen:

 int main() { // 1,      Set() TIM1::CR1::Set(TIM1::CR1::DIR::Upcounter, TIM1::CR1::CKD::DividedBy4, TIM1::CR1::CEN::Enable) ; // 2,     TIM1::CR1<TIM1::CR1::DIR::Upcounter, TIM1::CR1::CKD::DividedBy4, TIM1::CR1::CEN::Enable>::Set() ; } 

Da die Option mit einer variablen Anzahl von Argumenten für die Funktion nicht immer vom Compiler optimiert wird und tatsächlich alle Parameter über die Register und den Stapel übertragen werden, was sich auf die Geschwindigkeit und die Kosten des RAM auswirken kann, habe ich den zweiten Ansatz gewählt, bei dem die Berechnung der zu setzenden Bitmaske in der Phase erfolgt Zusammenstellung.

Wir werden eine Vorlage mit einer variablen Anzahl von Argumenten verwenden. Die Werteliste wird als Liste der Typen übergeben:

 //    ,          template<uint32_t address, size_t size, typename AccessMode, typename ...Args> class Register { private: ... 

Um den gewünschten Wert im Register einzustellen, benötigen wir:

  1. Aus dem gesamten Wertesatz eine Maske bilden, um die gewünschten Bits in den Registern zurückzusetzen.
  2. Generieren Sie aus dem gesamten Wertesatz einen Wert, um die gewünschten Bits zu setzen.

Dies sollten Constexpr-Methoden sein, die alle erforderlichen Aktionen in der Kompilierungsphase ausführen:

 //    ,          template<uint32_t address, size_t size, typename AccessMode, typename ...Args> class Register { private: // ,    //     . __forceinline template<typename T> static constexpr auto GetIndividualMask() { Type result = T::Mask << T::Offset ; return result ; } // ,    //       . static constexpr auto GetMask() { //       const auto values = {GetIndividualMask<Args>()...} ; Type result = 0UL; for (auto const v: values) { //       result |= v ; } return result ; } //    __forceinline template<typename T> static constexpr auto GetIndividualValue() { Type result = T::Value << T::Offset ; return result ; } static constexpr auto GetValue() { const auto values = {GetIndividualValue<Args>()...}; Type result = 0UL; for (const auto v: values) { result |= v ; } return result ; } }; 

Es bleiben nur öffentliche Methoden zu definieren Set()und IsSet():

 //    ,          template<uint32_t address, size_t size, typename AccessMode, typename ...Args> class Register { public: using Type = typename RegisterType<size>::Type; template<typename T = AccessMode, class = typename std::enable_if_t<std::is_base_of<WriteMode, T>::value>> static void Set() { Type newRegValue = *reinterpret_cast<Type *>(address) ; //GetMask()    ,     newRegValue &= ~GetMask() ; //GetValue()    ,     newRegValue |= GetValue() ; //     *reinterpret_cast<Type *>(address) = newRegValue ; } template<typename T = AccessMode, class = typename std::enable_if_t<std::is_base_of<ReadMode, T>::value>> static bool IsSet() { Type newRegValue = *reinterpret_cast<Type *>(address) ; return ((newRegValue & GetMask()) == GetValue()) ; } private: ... 

Fast alles, ein kleines Problem bleibt, wir können solche Dummheit tun:

 int main() { // ,     TIM1::CR1 TIM1::CR1<TIM2::CR1::Enabled, TIM1::CR2::OIS1::OC1OutputIs0>::Set() ; } 

Natürlich müssen Sie irgendwie überprüfen, ob bei unseren Werten zwischen Groß- und Kleinschreibung unterschieden wird. Dies ist ganz einfach. Fügen Sie dem Vorlagenparameter einfach einen zusätzlichen Typ hinzu, nennen wir ihn FieldValueBaseType. Nun sollten sowohl das Register als auch die Werte der Felder, die in diesem Register eingestellt werden können, vom gleichen FieldValueBaseTypeTyp sein:

Fügen Sie diesem Register eine Prüfung für den Besitz des Feldwerts hinzu
 template<uint32_t address, size_t size, typename AccessMode, typename FieldValueBaseType, typename ...Args> class Register { private: //     BaseType     FieldValueBaseType,    . __forceinline template<typename T, class = typename std::enable_if_t<std::is_same<FieldValueBaseType, typename T::BaseType>::value>> static constexpr auto GetIndividualMask() { Type result = T::Mask << T::Offset ; return result ; } static constexpr auto GetMask() { const auto values = {GetIndividualMask<Args>()...} ; Type result = 0UL; for (auto const v: values) { result |= v ; } return result ; } //     BaseType     FieldValueBaseType,    . __forceinline template<typename T, class = typename std::enable_if_t<std::is_same<FieldValueBaseType, typename T::BaseType>::value>> static constexpr auto GetIndividualValue() { Type result = T::Value << T::Offset ; return result ; } static constexpr auto GetValue() { const auto values = {GetIndividualValue<Args>()...}; Type result = 0UL; for (const auto v: values) { result |= v ; } return result ; } }; 


Beim Festlegen mehrerer Feldwerte prüft der SFINAE-Mechanismus erneut, ob dieser Wert vom Typ des zulässigen Typs für Registerfelder ist. Wenn dies der Fall ist, wird die Methode vom Compiler angezeigt. Andernfalls wird beim Kompilieren eine Fehlermeldung angezeigt.

Die vollständige Beschreibung des CR1-Registers von TIM1 sieht folgendermaßen aus:

 struct TIM1 { struct TIM1CR1Base {} ; struct CR1 : public RegisterBase<0x40010000, 32, ReadWriteMode> { using CKD = TIM_CR_CKD_Values<TIM1::CR1, 8, 2, ReadWriteMode, TIM1CR1Base> ; using ARPE = TIM_CR_ARPE_Values<TIM1::CR1, 7, 1, ReadWriteMode, TIM1CR1Base> ; using CMS = TIM_CR_CMS_Values<TIM1::CR1, 5, 2, ReadWriteMode, TIM1CR1Base> ; using DIR = TIM_CR_DIR_Values<TIM1::CR1, 4, 1, ReadWriteMode, TIM1CR1Base> ; using OPM = TIM_CR_OPM_Values<TIM1::CR1, 3, 1, ReadWriteMode, TIM1CR1Base> ; using URS = TIM_CR_URS_Values<TIM1::CR1, 2, 1, ReadWriteMode, TIM1CR1Base> ; using UDIS = TIM_CR_UDIS_Values<TIM1::CR1, 1, 1, ReadWriteMode, TIM1CR1Base> ; using CEN = TIM_CR_CEN_Values<TIM1::CR1, 0, 1, ReadWriteMode, TIM1CR1Base> ; } ; } 

Jetzt ist es möglich, jeden Wert des Registerfelds separat zu ändern und zu prüfen. Sie können mehrere Feldwerte gleichzeitig einstellen und prüfen. Sie können die Werte des Felds oder des Registers ändern und abrufen und gleichzeitig absolut sicher sein, dass Sie nichts verwirren und keine ungültige Operation ausführen oder das falsche Bit in das Registerfeld schreiben können.

Kommen wir nun zur Originalversion in C zurück, in der wir ein paar Scheiße gemacht haben:

Erste Option
 int main(void) { //     GPIOA //,    AHB1ENR RCC->APB1ENR |= RCC_AHB1ENR_GPIOAEN ; //    ,      RCC->AHB1ENR = RCC_AHB1ENR_GPIOAEN; //,  TIM1    APB2 RCC->APB1ENR |= RCC_APB1ENR_TIM2EN | RCC_APB2ENR_TIM1EN; // - ,        . auto result = GPIOA->BSRR ; if (result & GPIO_BSRR_BS1) { //do something } //-     .   ... GPIOA->IDR = GPIO_IDR_ID5 ; 

Und versuchen Sie dasselbe mit dem neuen Ansatz zu tun:

 int main(void) { //     GPIOA // ,   APB1ENR   GPIOAEN RCC::APB1ENR::GPIOAEN::Enable::Set() ; // ,     GPIOA RCC::AHB1ENR::GPIOAEN::Enable::Set() ; // , RCC::APB2ENR::TIM1EN::Enable  //   APB1ENR RCC::APB1ENRPack<RCC::APB1ENR::TIM2EN::Enable, RCC::APB2ENR::TIM1EN::Enable>::Set(); // ,  BSRR    auto result = GPIOA::BSRR::Get() ; // ,  Reset    if (GPIOA::BSRR::BS1::Reset::IsSet()) { //do something } // ,       GPIOA::IDR::IDR5::On::Set() } 

In jedem dieser Fälle erhalten wir beim Kompilieren einen Fehler, genau das haben wir erreicht.

Nun, wir haben einen schönen und sicheren Zugang zum Register und seinen Feldern bereitgestellt, aber was ist mit der Geschwindigkeit?

Leistung


Zum Vergleich, wie optimal unser Ansatz ist, verwenden wir den C- und C ++ - Code, der die Uhr an Port A speist, die drei Ports in den Ausgabemodus versetzt und die Ausgabeports in diesen drei Ports 1:

C-Code:
 int main() { uint32_t res = RCC->AHB2ENR; res &=~ RCC_AHB1ENR_GPIOAEN_Msk ; res |= RCC_AHB1ENR_GPIOAEN ; RCC->AHB2ENR = res ; res = GPIOA->MODER ; res &=~ (GPIO_MODER_MODER5 | GPIO_MODER_MODER4 | GPIO_MODER_MODER1) ; res |= (GPIO_MODER_MODER5_0 | GPIO_MODER_MODER4_0 | GPIO_MODER_MODER1_0) ; GPIOA->MODER = res ; GPIOA->BSRR = (GPIO_BSRR_BS5 | GPIO_BSRR_BS4 | GPIO_BSRR_BS1) ; return 0 ; } 


C ++ - Code:
 int main() { RCC::AHB1ENR::GPIOAEN::Enable::Set() ; GPIOA::MODERPack< GPIOA::MODER::MODER5::Output, GPIOA::MODER::MODER4::Output, GPIOA::MODER::MODER1::Output>::Set() ; GPIOA::BSRRPack< GPIOA::BSRR::BS5::Set, GPIOA::BSRR::BS4::Set, GPIOA::BSRR::BS1::Set>::Write() ; return 0 ; } 


Ich benutze den IAR-Compiler. Sehen wir uns zwei Optimierungsmodi an: Ohne Optimierung und Medienoptimierung:

C-Code- und Assembler-Darstellung ohne Optimierung:

Bild

C ++ - Code und Assembler-Darstellung ohne Optimierung:

Bild

18 Assembler-Zeilen in beiden Fällen und der Code ist nahezu identisch, was eigentlich nicht überraschend ist, da dies der Fall ist Das haben wir erreicht.

Wir überprüfen bei durchschnittlicher Optimierung den Code in C:

Bild

Wie erwartet schon 13 Assembler-Zeilen.

Und der C ++ - Code ist im Durchschnitt optimiert:

Bild

Auch hier ist die Situation identisch: kein Overhead, mit einem offensichtlichen Vorteil bei der Lesbarkeit des Codes.

Nun, alle Aufgaben sind gelöst, die letzte Frage stellt sich. Wie viel Zeit und Mühe ist erforderlich, um alle Register in dieser Form zu beschreiben?

Wie man alle Register beschreibt


Wir haben zuverlässigen, bequemen und schnellen Zugang zu Registern. Eine Frage bleibt offen. Wie man alle Register beschreibt, gibt es auch unter hundert für den Mikrocontroller. So lange dauert es, alle Register zu beschreiben, da Sie bei einer solchen Routinearbeit viele Fehler machen können. Ja, Sie müssen dies nicht manuell tun. Stattdessen verwenden wir den Codegenerator aus der SVD-Datei, der, wie oben am Anfang des Artikels angegeben, die von mir akzeptierte Registerabstraktion vollständig abdeckt.

Ich habe das Skript eines Kollegen fertiggestellt, das auf der Grundlage dieser Idee ungefähr dasselbe tat, aber etwas einfacher Enum anstelle von Klassen für Feldwerte verwendete. Das Skript dient nur zum Testen und Überprüfen von Ideen, ist daher nicht optimal, ermöglicht es Ihnen jedoch, so etwas zu generieren .
Bild
Wen interessiert das Drehbuch ?

Zusammenfassung


Infolgedessen besteht die Aufgabe des Programmierers nur darin, die generierte Datei ordnungsgemäß zu verbinden. Wenn Sie Register verwenden müssen, beispielsweise für das Modul gpioa oder rcc, müssen Sie nur die gewünschte Header-Datei angeben:

 #include "gpioaregisters.hpp" //for GPIOA #include "rccregisters.hpp" //for RCC int main() { RCC::AHB1ENR::GPIOAEN::Enable::Set() ; GPIOA::MODER::MODER15::Output::Set() ; GPIOA::MODERPack< GPIOA::MODER::MODER12::Output, GPIOA::MODER::MODER14::Analog >::Set() ; } 

Ich wiederhole, die SVD-Datei kann von der Website des Herstellers heruntergeladen werden. Sie können sie aus der Entwicklungsumgebung abrufen, an die Skripteingabe senden und das ist alles.

Aber wie ich oben sagte, kümmern sich nicht alle Hersteller um ihre Verbraucher, so dass nicht jeder eine Aufzählung in der SVD-Datei hat. Aus diesem Grund sehen alle Aufzählungen für ST-Mikrocontroller nach der Generierung so aus:

 template <typename Reg, size_t offset, size_t size, typename AccessMode, typename BaseType> struct GPIOA_MODER_MODER_Values: public RegisterField<Reg, offset, size, AccessMode> { using Value0 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 0U> ; using Value1 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 1U> ; using Value2 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 2U> ; using Value3 = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 3U> ; } ; 

In dem Moment, in dem Sie sie verwenden müssen, können Sie in der Dokumentation nachsehen und die Wörter Wert ändern, um etwas Verständlicheres zu erhalten:

 template <typename Reg, size_t offset, size_t size, typename AccessMode, typename BaseType> struct GPIOA_MODER_MODER_Values: public RegisterField<Reg, offset, size, AccessMode> { using Input = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 0U> ; using Output = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 1U> ; using Alternate = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 2U> ; using Analog = FieldValue<GPIOA_MODER_MODER_Values, BaseType, 3U> ; } ; 

Danach haben alle Ihre Feldwerte bereits einen eindeutigen Namen.

Ich hoffe, dass ST in Zukunft immer noch alle Feldwerte beschreibt, dann wird die manuelle Arbeit im Allgemeinen 0 sein.

Fairerweise ist es erwähnenswert, dass Programmierer in den meisten Fällen Aufzählungsaufzählungen zum besseren Verständnis von Hand erstellen.

Eigentlich alles, Anregungen und Kommentare sind willkommen.

Das Projekt unter IAR 8.40.1 liegt hier.
Der Quellcode selbst liegt hier. "Online GDB"
-Code PS: Danke putyavka für den gefundenen Fehler in der Methode und Ryppka für den gefundenen Fehler mit assert.

RegisterField::Get()


Im Artikel verwendete Links und Artikel


Typesafe Register Access in C++
One Approach to Using Hardware Registers in C++
SVD Description (*.svd) Format

Source: https://habr.com/ru/post/de459642/


All Articles