Verwenden von C ++ und Vorlagen mit einer variablen Anzahl von Argumenten beim Programmieren von Mikrocontrollern

ARM mit Cortex Mx-Kern (am Beispiel STM32F10x)


KDPV Der Mikrocontroller ARM Cortex M3 STM32F103c8t6 wird häufig als 32-Bit-Mikrocontroller für Amateurprojekte verwendet. Wie für fast jeden Mikrocontroller gibt es ein SDK, einschließlich C ++ - Header-Dateien zur Bestimmung der Peripherie des Controllers.

Dort wird beispielsweise die serielle Schnittstelle als Datenstruktur definiert, und eine Instanz dieser Struktur befindet sich in dem für Register reservierten Adressbereich, und wir haben über einen Zeiger auf eine bestimmte Adresse Zugriff auf diesen Bereich.

Für diejenigen, die dies noch nicht erlebt haben, werde ich ein wenig beschreiben, wie es definiert ist. Dieselben Leser, die damit vertraut sind, können diese Beschreibung überspringen.

Diese Struktur und ihre Instanz werden wie folgt beschrieben:

/* =========================================================================*/ typedef struct { __IO uint32_t CR1; /*!< USART Control register 1, Address offset: 0x00 */ . . . __IO uint32_t ISR; /*!< USART Interrupt and status register, ... */ } USART_TypeDef; // USART_Type   . /* =========================================================================*/ #define USART1_BASE (APBPERIPH_BASE + 0x00013800) . . . #define USART1 ((USART_TypeDef *) USART1_BASE) #define USART1_BASE 0x400xx000U 

Weitere Details finden Sie hier stm32f103xb.h ≈ 800 kB

Wenn Sie nur die Definitionen in dieser Datei verwenden, müssen Sie wie folgt schreiben (Beispiel für die Verwendung des Statusregisters für die serielle Schnittstelle):

 // ---------------------------------------------------------------------------- if (USART1->ISR & (ONE_ISR_FLAG & OTHER_ISR_FLAG)) { } 

Und Sie müssen es verwenden, weil die vorhandenen proprietären Lösungen CMSIS und HAL zu komplex sind, um in Amateurprojekten verwendet zu werden.

Wenn Sie jedoch in C ++ schreiben, können Sie folgendermaßen schreiben:

 // ---------------------------------------------------------------------------- USART_TypeDef & Usart1 = *USART1; // ---------------------------------------------------------------------------- if (Usart1.ISR & (ONE_ISR_FLAG & OTHER_ISR_FLAG)) { } 

Eine veränderbare Referenz wird mit einem Zeiger initialisiert. Das ist eine kleine Erleichterung, aber angenehm. Besser noch, natürlich eine kleine Wrapper-Klasse darüber zu schreiben, während diese Technik immer noch nützlich ist.

Natürlich möchte ich diese Wrapper-Klasse sofort über eine serielle Schnittstelle (EUSART - erweiterter universeller serieller asinhronischer Reseiver-Sender) schreiben, die so attraktiv ist, über erweiterte Funktionen verfügt, einen seriellen asynchronen Transceiver und die Möglichkeit bietet, unseren kleinen Mikrocontroller mit einem Desktop-System oder Laptop, aber mit Mikrocontrollern zu verbinden Cortex zeichnet sich durch ein entwickeltes Taktsystem aus, und Sie müssen davon ausgehen und dann die entsprechenden E / A-Pins für die Arbeit mit Peripheriegeräten konfigurieren, da in der STM32F1xx-Serie wie in legged andere Mikrocontrollern ARM Cortex können den Port-Pins zu Ein- oder Ausgang und arbeitet in der gleichen Zeit mit der Peripherie nicht nur konfigurieren.

Beginnen wir mit dem Einschalten des Timings. Das Taktsystem wird als RCC-Register zur Taktsteuerung bezeichnet und stellt auch eine Datenstruktur dar, deren deklariertem Zeiger ein bestimmter Adresswert zugewiesen ist.

 /* =========================================================================*/ typedef struct { . . . } RCC_TypeDef; 

Felder dieser Struktur werden wie folgt deklariert, wobei __IO flüchtig definiert:

 /* =========================================================================*/ __IO uint32_t CR; 

entsprechen den Registern von RCC, und die einzelnen Bits dieser Register sind eingeschaltet oder die Taktfunktionen der Peripherie des Mikrocontrollers. All dies ist in der Dokumentation (pdf) gut beschrieben.

Ein Zeiger auf eine Struktur ist definiert als

 /* =========================================================================*/ #define RCC ((RCC_TypeDef *)RCC_BASE) 

Das Arbeiten mit Registerbits ohne Verwendung des SDK sieht normalerweise folgendermaßen aus:

Hier ist die Aufnahme von Port A.

 // ---------------------------------------------------------------------------- RCC->APB2ENR |= RCC_APB2ENR_IOPAEN; 

Sie können zwei oder mehr Bits gleichzeitig aktivieren

 // ---------------------------------------------------------------------------- RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN; 

Es sieht für C ++ etwas ungewöhnlich aus oder etwas Ungewöhnliches. Es wäre besser, anders zu schreiben, beispielsweise mit OOP.

 // ---------------------------------------------------------------------------- Rcc.PortOn(Port::A); 

Es sieht besser aus, aber im 21. Jahrhundert werden wir etwas weiter gehen, C ++ 17 verwenden und mit Vorlagen mit einer variablen Anzahl von Parametern schreiben, die noch schöner sind:

 // ---------------------------------------------------------------------------- Rcc.PortOn<Port::A, Port::B>(); 

Wo Rcc wie folgt definiert ist:

 // ---------------------------------------------------------------------------- TRcc & Rcc = *static_cast<TRcc *>RCC; 

Daraus werden wir beginnen, einen Wrapper über die Taktregister zu bauen. Zuerst definieren wir eine Klasse und einen Zeiger (Link) darauf.

Zuerst wollte ich in den C ++ 11/14-Standard schreiben, indem ich die Parameter einer Funktionsvorlage rekursiv entpackte. Ein guter Artikel dazu finden Sie am Ende des Artikels im Link-Bereich.

 // ============================================================================ enum class GPort : uint32_t { A = RCC_APB2ENR_IOPAEN, B = RCC_APB2ENR_IOPBEN, C = RCC_APB2ENR_IOPCEN, }; // ---------------------------------------------------------------------------- class TRcc: public ::RCC_TypeDef { private: TRcc() = delete; ~TRcc() = delete; // ======================================================================== public: template<GPort... port> inline void PortOn(void) //    (inline) { //    -Og  -O0 APB2ENR |= SetBits<(uint32_t)port...>(); } // ------------------------------------------------------------------------ #define BITMASK 0x01 //    ,   #define MASKWIDTH 1 //      .   //          #undef. private: //   (fold)   . template<uint8_t bitmask> inline constexpr uint32_t SetBits(void) { //   ,  GPort  enum // (, , bitmask    ). // static_assert(bitmask < 16, " ."); return bitmask; } template<uint8_t bit1, uint8_t bit2, uint8_t... bit> inline constexpr uint32_t SetBits(void) { return SetBits<bit1>() | SetBits<bit2, bit...>(); } }; #undef BITMASK #undef MASKWIDTH // ------------------------------------------------------------------------ TRcc & Rcc = *static_cast<TRcc *>RCC; 

Betrachten Sie den Aufruf, um die Port-Taktung zu aktivieren:

  Rcc.PortOn<GPort::A>(); 

GCC wird es in einer Reihe von Befehlen bereitstellen:

  ldr r3, [pc, #376] ; (0x8000608 <main()+392>) ldr r0, [r3, #24] orr.w r0, r0, #4 str r0, [r3, #24] 

Hat es geklappt? Überprüfen Sie als nächstes

  Rcc.PortOn<GPort::A, GPort::B, GPort::C>(); 

Leider hat der nicht so naive GCC den nachfolgenden Rekursionsaufruf separat bereitgestellt:

  ldr r3, [pc, #380] ; (0x8000614 <main()+404>) ldr r0, [r3, #24] orr.w r0, r0, #4 ; APB2ENR |= GPort::A str r0, [r3, #24] ldr r0, [r3, #24] orr.w r0, r0, #28 ; APB2ENR |= Gport::B | GPort::C str r0, [r3, #24] #24] 

Zur Verteidigung von GCC muss ich sagen, dass dies nicht immer der Fall ist, sondern nur in komplexeren Fällen, die bei der Implementierung der E / A-Portklasse auftreten werden. Nun, C ++ 17 hat es eilig zu helfen. Schreiben Sie die TRCC-Klasse mithilfe der integrierten Bildlauffunktionen neu.

 // ---------------------------------------------------------------------------- class TRcc: public ::RCC_TypeDef { private: TRcc() = delete; //     ,  ~TRcc() = delete; //    . // ======================================================================== public: template<GPort... port> inline void PortOn(void) //    (inline) { //    -Og  -O0 APB2ENR |= SetBits17<(uint32_t)port...>(); } // ------------------------------------------------------------------------ #define BITMASK 0x01 //    ,   #define MASKWIDTH 1 //      .   //          #undef. private: //   (fold)   . ++ 17. template<uint8_t... bitmask> inline constexpr uint32_t SetBits17(void) { return (bitmask | ...); //     ... | bit } }; #undef BITMASK #undef MASKWIDTH 

Nun stellte sich heraus:

 ldr r2, [pc, #372] ; (0x800060c <main()+396>) ldr r0, [r2, #24] orr.w r0, r0, #28 ; APB2ENR |= Gport::A | Gport::B | GPort::C str r0, [r3, #24] 

Und der Klassencode ist einfacher geworden.

Fazit: Mit C ++ 17 können wir die Vorlagen mit einer variablen Anzahl von Parametern verwenden, um den gleichen Mindestbefehlssatz (auch wenn die Optimierung deaktiviert ist) zu erhalten, der bei Verwendung der klassischen Arbeit mit dem Mikrocontroller durch Registerdefinitionen erhalten wird. Gleichzeitig erhalten wir jedoch alle Vorteile einer starken C ++ - Typisierung und -Überprüfung während der Kompilierung durch die Struktur der Basisklassen des Codes wiederverwendet und so weiter.

Hier ist so etwas in C ++ geschrieben

 Rcc.PortOn<Port::A, Port::B, Port::C>(); 

Und der klassische Text auf Registern:

 RCC->APB2 |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_IOPBEN; 

entfalten Sie in einem optimalen Satz von Anweisungen. Hier ist der von GCC generierte Code (Optimierung aus -Og):

  ldr r2, [pc, #372] ; (0x800060c <main()+396>) [  RCC] ldr r0, [r2, #0] ; r0 = RCC->APB2 // [  APB2] orr.w r0, r0, #160 ; r0 |= 0x10100000 str r0, [r2, #0] ; RCC->APB2 = r0 

Jetzt sollten Sie weiterarbeiten und die Klasse des Eingabe-Ausgabe-Ports schreiben. Das Arbeiten mit E / A-Portbits wird durch die Tatsache erschwert, dass vier Bits für die Konfiguration eines Portabschnitts zugewiesen werden und daher 64 Bit Konfiguration für einen 16-Bit-Port erforderlich sind, die in zwei 32-Bit-CRL- und CRH-Register unterteilt sind. Außerdem wird die Breite der Bitmaske größer als 1. Das Scrollen durch C ++ 17 zeigt hier jedoch seine Funktionen.

Bild

Als nächstes werden die TGPIO-Klasse sowie Klassen für die Arbeit mit anderen Peripheriegeräten, eine serielle Schnittstelle, I2C, SPI, DAP, Timer und vieles mehr geschrieben, die normalerweise in ARM Cortex-Mikrocontrollern vorhanden sind, und dann können sie mit solchen LEDs blinken.

Aber mehr dazu in der nächsten Notiz. Quellen des Projekts auf Github .

Internetartikel zum Schreiben von Notizen


Vorlagen mit einer variablen Anzahl von Argumenten in C ++ 11 .
Innovationen in den Vorlagen .
C ++ Sprachinnovation 17. Teil 1. Faltung und Ableitung .
Liste der Links zur Dokumentation für STM-Mikrocontroller .
Makros für variable Parameter

Artikel über Khabr, die mich dazu veranlassten, diese Notiz zu schreiben


Ampel auf Attiny13 .

Julian Assange von der britischen Polizei festgenommen
Raum als vage Erinnerung

Geschrieben am 04/12/2019 - Happy Cosmonautics Day!

PS
STM32F103c8t6 in Stm CubeMx Bild STM32F103c8t6 von CubeMX.

Als Ausgangspunkt wird der von der Eclips-Erweiterung für die Arbeit mit den GNU MCU-Eclipse ARM Embedded- und STM CubeMX-Mikrocontrollern erstellte Text verwendet , d. H. Es gibt Dateien mit Standardfunktionen C ++, _start () und _init (), Definitionen von Interruptvektoren stammen aus Eclipse MCU ARM Embedded und das Cortex M3-Kernregister sowie die Arbeitsdateien stammen aus einem Projekt von CubeMX.


PPS
Beim KDPV-Debugging mit dem STM32F103c8t6-Controller ist dargestellt. Nicht jeder hat ein solches Board, aber es ist nicht schwierig, es zu kaufen. Dies würde jedoch den Rahmen dieses Artikels sprengen.

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


All Articles