Wie man 4 LEDs auf CortexM mit C ++ 17, Tupel und ein bisschen Fantasie blinkt

Gute Gesundheit an alle!

Wenn ich Studenten beibringe, wie man an der Universität eingebettete Software für Mikrocontroller entwickelt, verwende ich C ++ und manchmal gebe ich Studenten, die sich besonders für alle möglichen Aufgaben interessieren, begabte Studenten, die besonders krank sind .

Wieder einmal hatten solche Schüler die Aufgabe, 4 LEDs in der C ++ 17-Sprache und der Standard-C ++ - Bibliothek zu blinken, ohne zusätzliche Bibliotheken wie CMSIS und ihre Header-Dateien mit einer Beschreibung der Registerstrukturen usw. zu verbinden. Die mit dem Code gewinnt im ROM ist die kleinste Größe und der am wenigsten verbrauchte RAM. Die Compileroptimierung sollte nicht höher als Mittel sein. IAR-Compiler 8.40.1.
Der Gewinner geht nach Canary und erhält 5 für die Prüfung.

Vorher habe ich dieses Problem auch selbst nicht gelöst, daher werde ich Ihnen erzählen, wie die Schüler es gelöst haben und was mit mir passiert ist. Ich warne Sie sofort, dass es unwahrscheinlich ist, dass ein solcher Code in realen Anwendungen verwendet werden kann. Deshalb habe ich die Veröffentlichung im Abschnitt "Abnormale Programmierung" veröffentlicht, obwohl wer weiß.

Aufgabenbedingungen


An den Ports GPIOA.5, GPIOC.5, GPIOC.8, GPIOC.9 befinden sich 4 LEDs. Sie müssen blinken. Um etwas zu vergleichen, haben wir den in C geschriebenen Code genommen:

void delay() { for (int i = 0; i < 1000000; ++i){ } } int main() { for(;;) { GPIOA->ODR ^= (1 << 5); GPIOC->ODR ^= (1 << 5); GPIOC->ODR ^= (1 << 8); GPIOC->ODR ^= (1 << 9); delay(); } return 0 ; } 

Die delay() Funktion ist hier rein formal, ein regulärer Zyklus, sie kann nicht optimiert werden.
Es wird davon ausgegangen, dass die Ports bereits für die Ausgabe konfiguriert sind und auf sie eine Taktung angewendet wird.
Ich werde auch gleich sagen, dass Bitbanging nicht verwendet wurde, um den Code portabel zu machen.

Dieser Code benötigt 8 Bytes auf dem Stapel und 256 Bytes im ROM bei mittlerer Optimierung
255 Bytes schreibgeschützter Codespeicher
1 Byte schreibgeschützter Datenspeicher
8 Bytes Readwrite-Datenspeicher

255 Bytes aufgrund der Tatsache, dass ein Teil des Speichers unter die Tabelle der Interruptvektoren ging, Aufrufe von IAR-Funktionen zum Initialisieren eines Gleitkommablocks, aller Arten von Debugging-Funktionen und der Funktion __low_level_init, bei der die Ports selbst konfiguriert wurden.

Die vollständigen Anforderungen sind also:

  • Die main () - Funktion sollte so wenig Code wie möglich enthalten
  • Sie können keine Makros verwenden
  • IAR 8.40.1 Compiler, der C ++ 17 unterstützt
  • CMSIS-Header-Dateien wie "#include" stm32f411xe.h "können nicht verwendet werden
  • Sie können die __forceinline-Direktive für Inline-Funktionen verwenden
  • Mittlere Compiler-Optimierung

Studentenentscheidung


Im Allgemeinen gab es mehrere Lösungen, ich werde nur eine zeigen ... es ist nicht optimal, aber es hat mir gefallen.

Da Header nicht verwendet werden können, haben die Schüler als erstes die Gpio Klasse verwendet, die an ihren Adressen einen Link zu den Gpio speichern sollte. Zu diesem Zweck verwenden sie eine Strukturüberlagerung. Höchstwahrscheinlich haben sie die Idee von hier übernommen: Strukturüberlagerung :

 class Gpio { public: __forceinline inline void Toggle(const std::uint8_t bitNum) volatile { Odr ^= bitNum ; } private: volatile std::uint32_t Moder; volatile std::uint32_t Otyper; volatile std::uint32_t Ospeedr; volatile std::uint32_t Pupdr; volatile std::uint32_t Idr; volatile std::uint32_t Odr; //    static_assert(sizeof(Gpio) == sizeof(std::uint32_t) * 6); } ; 

Wie Sie sehen können, identifizierten sie die Gpio Klasse sofort mit Attributen, die sich an den Adressen der entsprechenden Register befinden sollten, und einer Methode zum Umschalten des Status anhand der Anzahl der Beine:
Dann haben wir die Struktur für GpioPin die den Zeiger auf Gpio und die Nummer des Beins enthält:

 struct GpioPin { volatile Gpio* port ; std::uint32_t pinNum ; } ; 

Dann erstellten sie eine Reihe von LEDs, die auf den spezifischen Beinen des Ports sitzen, und gingen darüber hinweg, indem sie die Toggle() -Methode jeder LED aufriefen:

 const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9} } ; struct LedsDriver { __forceinline static inline void ToggelAll() { for (auto& it: leds) { it.port->Toggle(it.pinNum); } } } ; 

Nun, eigentlich der ganze Code:
 constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; class Gpio { public: __forceinline inline void Toggle(const std::uint8_t bitNum) volatile { Odr ^= bitNum ; } private: volatile std::uint32_t Moder; volatile std::uint32_t Otyper; volatile std::uint32_t Ospeedr; volatile std::uint32_t Pupdr; volatile std::uint32_t Idr; volatile std::uint32_t Odr; } ; //    static_assert(sizeof(Gpio) == sizeof(std::uint32_t) * 6); struct GpioPin { volatile Gpio* port ; std::uint32_t pinNum ; } ; const GpioPin leds[] = {{reinterpret_cast<volatile Gpio*>(GpioaBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 5}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9}, {reinterpret_cast<volatile Gpio*>(GpiocBaseAddr), 9} } ; struct LedsDriver { __forceinline static inline void ToggelAll() { for (auto& it: leds) { it.port->Toggle(it.pinNum); } } } ; int main() { for(;;) { LedsContainer::ToggleAll() ; delay(); } return 0 ; } 


Statistik ihres Codes zur Medienoptimierung:
275 Bytes schreibgeschützter Codespeicher
1 Byte schreibgeschützter Datenspeicher
8 Bytes Readwrite-Datenspeicher

Eine gute Lösung, die aber viel Speicherplatz beansprucht :)

Meine Entscheidung


Natürlich habe ich mich entschieden, nicht nach einfachen Wegen zu suchen und mich ernsthaft zu verhalten :).
LEDs befinden sich an verschiedenen Anschlüssen und verschiedenen Beinen. Als erstes müssen Sie die Port Klasse erstellen. Um jedoch die Zeiger und Variablen zu entfernen, die RAM belegen, müssen Sie statische Methoden verwenden. Die Portklasse könnte folgendermaßen aussehen:

 template <std::uint32_t addr> struct Port { //  -  }; 

Als Vorlagenparameter hat es eine Portadresse. Im "#include "stm32f411xe.h" ist er beispielsweise für Port A als GPIOA_BASE definiert. Wir dürfen die Header jedoch nicht verwenden, daher müssen wir nur unsere eigene Konstante erstellen. Daher kann die Klasse folgendermaßen verwendet werden:

 constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; 

Zum Blinken benötigen Sie die Toggle-Methode (const std :: uint8_t-Bit), mit der das erforderliche Bit mithilfe einer exklusiven ODER-Verknüpfung umgeschaltet wird. Die Methode muss statisch sein. Fügen Sie sie der Klasse hinzu:

 template <std::uint32_t addr> struct Port { //   __forceinline,        __forceinline inline static void Toggle(const std::uint8_t bitNum) { *reinterpret_cast<std::uint32_t*>(addr+20) ^= (1 << bitNum) ; //addr + 20  ODR  } }; 

Ausgezeichneter Port<> ist, er kann den Zustand der Beine ändern. Die LED befindet sich auf einem bestimmten Bein, daher ist es logisch, einen Klassen- Pin , der Port<> und die Beinnummer als Vorlagenparameter enthält. Da der Port<> -Typ eine Vorlage ist, d.h. Unterschiedlich für verschiedene Ports können wir nur den Universaltyp T übertragen.

 template <typename T, std::uint8_t pinNum> struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ; 

Es ist schlecht, dass wir jeden Unsinn vom Typ T , der eine Toggle() -Methode hat, und dies wird funktionieren, obwohl davon ausgegangen wird, dass wir nur den Typ Port<> . Um PortBase zu schützen, werden wir Port<> von der PortBase Basisklasse erben lassen und in der Vorlage überprüfen, ob unser übergebener Typ tatsächlich auf PortBase basiert. Wir bekommen folgendes:

 constexpr std::uint32_t OdrAddrShift = 20U; struct PortBase { }; template <std::uint32_t addr> struct Port: PortBase { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr ) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum, class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>> //   struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ; 

Jetzt wird die Vorlage nur instanziiert, wenn unsere Klasse die Basisklasse PortBase .
Theoretisch können Sie diese Klassen bereits verwenden. Mal sehen, was ohne Optimierung passiert:

 using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; using Led1 = Pin<PortA, 5> ; using Led2 = Pin<PortC, 5> ; using Led3 = Pin<PortC, 8> ; using Led4 = Pin<PortC, 9> ; int main() { for(;;) { Led1::Toggle(); Led2::Toggle(); Led3::Toggle(); Led4::Toggle(); delay(); } return 0 ; } 

271 Bytes schreibgeschützter Codespeicher
1 Byte schreibgeschützter Datenspeicher
24 Bytes Readwrite-Datenspeicher

Woher kommen diese zusätzlichen 16 Bytes im RAM und 16 Bytes im ROM? Sie sind darauf zurückzuführen, dass wir den Bit-Parameter an die Toggle-Funktion (const std :: uint8_t-Bit) der Port-Klasse übergeben und der Compiler beim Aufrufen der Hauptfunktion 4 zusätzliche Register auf dem Stapel speichert, durch den dieser Parameter geleitet wird, und diese dann verwendet Register, in denen die Werte der Beinnummer für jeden Pin gespeichert sind, und beim Verlassen von main werden diese Register vom Stapel wiederhergestellt. Und obwohl dies im Wesentlichen eine Art völlig nutzlose Arbeit ist, da die Funktionen integriert sind, handelt der Compiler in voller Übereinstimmung mit dem Standard.
Sie können dies beseitigen, indem Sie die Portklasse im Allgemeinen entfernen, die Portadresse als Vorlagenparameter für die Pin Klasse übergeben und innerhalb der Toggle() -Methode die Adresse des ODR-Registers berechnen:

 constexpr std::uint32_t OdrAddrShift = 20U; template <std::uint32_t addr, std::uint8_t pinNum, struct Pin { __forceinline inline static void Toggle() { *reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift ) ^= (1 << bit) ; } } ; using Led1 = Pin<GpioaBaseAddr, 5> ; 

Das sieht aber nicht sehr gut und benutzerfreundlich aus. Wir hoffen daher, dass der Compiler diese unnötige Registererhaltung mit ein wenig Optimierung entfernt.

Wir setzen die Optimierung auf Medium und sehen das Ergebnis:
251 Bytes schreibgeschützter Codespeicher
1 Byte schreibgeschützter Datenspeicher
8 Bytes Readwrite-Datenspeicher

Wow wow wow ... wir haben 4 Bytes weniger
Code
255 Bytes schreibgeschützter Codespeicher
1 Byte schreibgeschützter Datenspeicher
8 Bytes Readwrite-Datenspeicher


Wie kann das sein? Schauen wir uns den Assembler im Debugger für C ++ - Code (links) und C-Code (rechts) an:

Bild

Es ist ersichtlich, dass der Compiler zum einen alle Funktionen integriert hat, jetzt überhaupt keine Aufrufe mehr erfolgt und zum anderen die Verwendung von Registern optimiert hat. Es ist ersichtlich, dass im Fall von C-Code der Compiler entweder das Register R1 oder R2 verwendet, um die Portadressen zu speichern, und jedes Mal, wenn das Bit geschaltet wird, zusätzliche Operationen ausführt (speichern Sie die Adresse im Register entweder in R1 oder in R2). Im zweiten Fall wird nur das R1-Register verwendet, und da die letzten 3 Anrufe zum Umschalten immer von Port C kommen, muss nicht mehr dieselbe Port C-Adresse im Register gespeichert werden. Dadurch werden 2 Teams und 4 Bytes gespeichert.

Hier ist es ein Wunder der modernen Compiler :) Na gut. Im Prinzip könnte man dort aufhören, aber lasst uns weitermachen. Ich denke nicht, dass es möglich sein wird, etwas anderes zu optimieren, obwohl es wahrscheinlich nicht richtig ist, wenn Sie Ideen haben, schreiben Sie in die Kommentare. Aber mit der Menge an Code in main () können Sie arbeiten.

Jetzt möchte ich, dass sich alle LEDs irgendwo im Container befinden, und Sie können die Methode aufrufen, alles umschalten ... So etwas:

 int main() { for(;;) { LedsContainer::ToggleAll() ; delay(); } return 0 ; } 

Wir werden das Schalten von 4 LEDs nicht dumm in die Funktion LedsContainer :: ToggleAll einfügen, weil es nicht interessant ist :). Wir wollen die LEDs in einen Container legen und sie dann durchgehen und jeweils die Toggle () -Methode aufrufen.

Die Schüler verwendeten ein Array, um Zeiger auf LEDs zu speichern. Ich habe jedoch verschiedene Typen, zum Beispiel: Pin<PortA, 5> , Pin<PortC, 5> , und ich kann keine Zeiger auf verschiedene Typen in einem Array speichern. Sie können eine virtuelle Basisklasse für alle Pins erstellen, aber dann wird eine Tabelle mit virtuellen Funktionen angezeigt, und es gelingt mir nicht, Schüler zu gewinnen.

Daher werden wir das Tupel verwenden. Hier können Sie Objekte unterschiedlichen Typs speichern. Dieser Fall sieht folgendermaßen aus:

 class LedsContainer { private: constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } 

Es gibt einen tollen Behälter, in dem alle LEDs gespeichert sind. ToggleAll() nun die ToggleAll() -Methode hinzu:

 class LedsContainer { public: __forceinline static inline void ToggleAll() { //        } private: constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } 

Sie können nicht einfach durch die Elemente eines Tupels gehen, da das Tupelelement nur in der Kompilierungsphase empfangen werden sollte. Um auf die Elemente des Tupels zuzugreifen, gibt es eine Template-Get-Methode. Nun, d.h. Wenn wir std::get<0>(records).Toggle() schreiben, wird die Toggle() -Methode für das Objekt der Klasse Pin<PortA, 5> , wenn std::get<1>(records).Toggle() , dann wird die Toggle() -Methode für das Objekt der Klasse Pin<Port, 5> usw. Pin<Port, 5> ...

Sie könnten Ihren Schülern die Nase abwischen und einfach schreiben:

  __forceinline static inline void ToggleAll() { std::get<0>(records).Toggle(); std::get<1>(records).Toggle(); std::get<2>(records).Toggle(); std::get<3>(records).Toggle(); } 

Wir möchten den Programmierer, der diesen Code unterstützt, nicht belasten und ihm zusätzliche Arbeit ermöglichen, indem er beispielsweise die Ressourcen seines Unternehmens ausgibt, falls eine andere LED erscheint. Sie müssen den Code an zwei Stellen hinzufügen, im Tupel und bei dieser Methode - und das ist nicht gut und der Eigentümer des Unternehmens wird nicht sehr zufrieden sein. Daher umgehen wir das Tupel mithilfe von Hilfsmethoden:

 class class LedsContainer { friend int main() ; public: __forceinline static inline void ToggleAll() { //    3,2,1,0    ,     visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>()); } private: __forceinline template<std::size_t... index> static inline void visit(std::index_sequence<index...>) { Pass((std::get<index>(records).Toggle(), true)...); //    get<3>(records).Toggle(), get<2>(records).Toggle(), get<1>(records).Toggle(), get<0>(records).Toggle() } __forceinline template<typename... Args> static void inline Pass(Args... ) {//      } constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } 

Es sieht beängstigend aus, aber ich habe am Anfang des Artikels gewarnt, dass die Shizany- Methode nicht sehr gewöhnlich ist ...

All diese Magie von oben in der Kompilierungsphase bewirkt buchstäblich Folgendes:

 //  LedsContainer::ToggleAll() ; //   4 : Pin<Port, 9>().Toggle() ; Pin<Port, 8>().Toggle() ; Pin<PortC, 5>().Toggle() ; Pin<PortA, 5>().Toggle() ; //     Toggle() inline,   : *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 9) ; *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 8) ; *reinterpret_cast<std::uint32_t*>(0x40020814 ) ^= (1 << 5) ; *reinterpret_cast<std::uint32_t*>(0x40020014 ) ^= (1 << 5) ; 

Kompilieren und überprüfen Sie die Codegröße ohne Optimierung:

Der Code, der kompiliert wird
 #include <cstddef> #include <tuple> #include <utility> #include <cstdint> #include <type_traits> //#include "stm32f411xe.h" #define __forceinline _Pragma("inline=forced") constexpr std::uint32_t GpioaBaseAddr = 0x4002'0000 ; constexpr std::uint32_t GpiocBaseAddr = 0x4002'0800 ; constexpr std::uint32_t OdrAddrShift = 20U; struct PortBase { }; template <std::uint32_t addr> struct Port: PortBase { __forceinline inline static void Toggle(const std::uint8_t bit) { *reinterpret_cast<std::uint32_t*>(addr + OdrAddrShift) ^= (1 << bit) ; } }; template <typename T, std::uint8_t pinNum, class = typename std::enable_if_t<std::is_base_of<PortBase, T>::value>> struct Pin { __forceinline inline static void Toggle() { T::Toggle(pinNum) ; } } ; using PortA = Port<GpioaBaseAddr> ; using PortC = Port<GpiocBaseAddr> ; //using Led1 = Pin<PortA, 5> ; //using Led2 = Pin<PortC, 5> ; //using Led3 = Pin<PortC, 8> ; //using Led4 = Pin<PortC, 9> ; class LedsContainer { friend int main() ; public: __forceinline static inline void ToggleAll() { //    3,2,1,0    ,     visit(std::make_index_sequence<std::tuple_size<tRecordsTuple>::value>()); } private: __forceinline template<std::size_t... index> static inline void visit(std::index_sequence<index...>) { Pass((std::get<index>(records).Toggle(), true)...); } __forceinline template<typename... Args> static void inline Pass(Args... ) { } constexpr static auto records = std::make_tuple ( Pin<PortA, 5>{}, Pin<PortC, 5>{}, Pin<PortC, 8>{}, Pin<PortC, 9>{} ) ; using tRecordsTuple = decltype(records) ; } ; void delay() { for (int i = 0; i < 1000000; ++i){ } } int main() { for(;;) { LedsContainer::ToggleAll() ; //GPIOA->ODR ^= 1 << 5; //GPIOC->ODR ^= 1 << 5; //GPIOC->ODR ^= 1 << 8; //GPIOC->ODR ^= 1 << 9; delay(); } return 0 ; } 


Assembler Proof, wie geplant ausgepackt:
Bild

Wir sehen, dass der Speicher übertrieben ist, 18 Bytes mehr. Die Probleme sind die gleichen, plus weitere 12 Bytes. Ich habe nicht verstanden, woher sie kommen ... vielleicht wird es jemand erklären.
283 Bytes schreibgeschützter Codespeicher
1 Byte schreibgeschützter Datenspeicher
24 Bytes Readwrite-Datenspeicher

Nun das Gleiche bei der mittleren Optimierung und siehe da ... wir haben Code, der mit C ++ - Implementierungen in der Stirn identisch ist, und optimaleren C-Code.
251 Bytes schreibgeschützter Codespeicher
1 Byte schreibgeschützter Datenspeicher
8 Bytes Readwrite-Datenspeicher

Assembler
Bild

Wie Sie sehen, habe ich gewonnen und bin auf die Kanarischen Inseln gegangen und freue mich, in Tscheljabinsk zu ruhen :), aber die Studenten waren auch großartig, sie haben die Prüfung erfolgreich bestanden!

Wen kümmert es, der Code ist hier

Wo kann ich das verwenden? Ich habe zum Beispiel Parameter im EEPROM-Speicher und eine Klasse, die diese Parameter beschreibt (Lesen, Schreiben, Initialisieren auf den Anfangswert). Die Klasse ist eine Vorlage wie Param<float<>> , Param<int<>> und Sie müssen beispielsweise alle Parameter auf die Standardwerte zurücksetzen. Hier können Sie alle in ein Tupel SetToDefault() , da der Typ unterschiedlich ist, und die SetToDefault() -Methode für jeden Parameter aufrufen. Wenn es 100 solcher Parameter gibt, frisst der ROM zwar viel, aber der RAM leidet nicht.

PS Ich muss zugeben, dass dieser Code bei maximaler Optimierung dieselbe Größe hat wie in C und in meiner Lösung. Alle Bemühungen des Programmierers, den Code zu verbessern, beruhen auf demselben Assembler-Code.

P.S1 Vielen Dank 0xd34df00d für den guten Rat. Sie können das Entpacken eines Tupels mit std::apply() vereinfachen. Der Funktionscode von ToggleAll() vereinfacht sich dann folgendermaßen:

  __forceinline static inline void ToggleAll() { std::apply([](auto... args) { (args.Toggle(), ...); }, records); } 

Leider ist std :: apply in der IAR in der aktuellen Version noch nicht implementiert, aber es funktioniert auch, siehe Implementierung mit std :: apply

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


All Articles