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;
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; } ;
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 {
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>>
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
Code255 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:

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() {
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() {
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:
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: 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
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 hierWo 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