Statisches Abonnement unter Verwendung der Observer-Vorlage unter Verwendung von C ++ und des Cortex M4-Mikrocontrollers


Gute Gesundheit an alle!


Am Vorabend des neuen Jahres möchte ich weiterhin über die Verwendung von C ++ auf Mikrocontrollern sprechen. Dieses Mal werde ich versuchen, über die Verwendung der Observer-Vorlage zu sprechen (im Folgenden nenne ich sie Publisher-Subscriber oder nur Subscriber, so ein Wortspiel) sowie über die Implementierung eines statischen Abonnements für C ++ 17 und die Vorteile dieses Ansatzes in einigen Anwendungen.


Einleitung


Template Subscriber ist eine der häufigsten Vorlagen, die in der Softwareentwicklung verwendet werden. Damit führen sie beispielsweise die Verarbeitung von Tastenklicks in Windows Form durch. Wie auch immer, an jedem Ort, an dem Sie auf Änderungen der Systemparameter reagieren müssen, sei es auf Änderungen in Dateien oder auf die Aktualisierung des Messwerts vom Sensor, ist es Zeit ohne nachzudenken Verwenden Sie die Abonnentenvorlage.


Der Vorteil der Vorlage besteht darin, dass wir das Wissen des Herausgebers und Abonnenten freisetzen, ohne an bestimmte Objekte gebunden zu sein. Wir können jeden für jeden signieren, ohne die Implementierung der Publisher- und Subscriber-Objekte zu beeinträchtigen.


Anfangsbedingungen


Bevor wir uns mit der Vorlage vertraut machen, stimmen wir zunächst zu, dass wir zuverlässige Software entwickeln möchten, in der:


  • Verwenden Sie keine dynamische Speicherzuordnung
  • Minimieren Sie die Arbeit mit Zeigern
  • Wir verwenden so viele Konstanten wie möglich, damit niemand so viel wie möglich ändern kann
  • Gleichzeitig verwenden wir jedoch so wenig Konstanten wie möglich, die sich im RAM befinden

Betrachten wir nun die Standardimplementierung der Subscriber-Vorlage.


Standardimplementierung


Angenommen, wir haben eine Schaltfläche, und wenn Sie auf die Schaltfläche klicken, müssen wir die LEDs blinken lassen, aber wie viele davon sind bisher unbekannt, und in der Tat müssen Sie möglicherweise nicht mit LEDs blinken, sondern mit einem Scheinwerfer auf dem Schiff, um Nachrichten im Morsecode zu übertragen. Es ist wichtig, dass wir nicht wissen, wer abonniert wird. Leider habe ich keinen Scheinwerfer zur Hand, so dass alle Beispiele im Artikel der Einfachheit halber und zum besseren Verständnis mit LEDs gemacht werden.


Wenn Sie also die Taste drücken, müssen Sie die LED über diese Betätigung benachrichtigen. Nachdem Sie das Drücken gelernt haben, sollte die LED in den entgegengesetzten Zustand wechseln.
Die Standardimplementierung in UML lautet wie folgt ...



Hier ist die ButtonController Klasse dafür verantwortlich, die Schaltfläche ButtonController und die Abonnenten über den Klick zu benachrichtigen. In diesem Fall ist Led der Abonnent. Diese beiden Klassen werden über die IPublisher und ISubsriber und keine der Klassen kennt die andere. Somit kann jedes Objekt, das von der ISubscriber Schnittstelle erbt, ein Ereignis von ButtonController .


Da die dynamische Speicherzuweisung verboten ist, habe ich ein Array von 3 Elementen für die Subskription deklariert. Das heißt Maximal können 3 Abonnenten sein. In erster Näherung könnte die Methode zur Benachrichtigung von Abonnenten der ButttonsController Klasse daher aussehen


 struct ButtonController : IPublisher { void Run() { for(;;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const override { //          HandleEvent() for(auto it: pSubscribers) { if (it != nullptr) { it->HandleEvent() ; } } } } ; 

Alles Salz befindet sich in der Notify() -Methode der Publisher Klasse. Bei dieser Methode gehen wir die Liste der Abonnenten durch und rufen HandleEvent() für jeden von ihnen auf. Dies ist cool, da jeder Abonnent diese Methode auf seine eigene Weise implementiert und dies dort tun kann alle Was auch immer Ihr Herz begehrt (in der Tat müssen Sie vorsichtig sein, sonst weiß der Teufel, was der Abonnent dort tut, Sie können seine Methode zum Beispiel durch eine Unterbrechung aufrufen und Sie müssen wachsam sein, um zu verhindern, dass Abonnenten lange und schlechte Dinge tun)


In unserem Fall darf die LED alles tun, also schaltet sie ihren Zustand um:


 template <typename Port, std::uint32_t pinNum> struct Led: ISubscriber { static void Toggle() { Port::ODR::Toggle(1 << pinNum); } void HandleEvent() override { //  ,    ,  Toggle() ; } }; 

Vollständige Implementierung aller Klassen
 template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0) //   { while ((Port::IDR::Read() & (1 << pinNum)) == 0) //     { }; result = true; } return result; } } ; //     GPIOC.13 using UserButton = Button<GPIOC, 13> ; struct ISubscriber { virtual void HandleEvent() = 0; } ; struct IPublisher { virtual void Notify() const = 0; virtual void Subscribe(ISubscriber* subscriber) = 0; } ; template <typename Port, std::uint32_t pinNum> struct Led: ISubscriber { static void Toggle() { Port::ODR::Toggle(1 << pinNum); } void HandleEvent() override { Toggle() ; } }; struct ButtonController : IPublisher { void Run() { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const override { for(auto it: pSubscribers) { if (it != nullptr) { it->HandleEvent() ; } } } void Subscribe(ISubscriber* subscriber) override { if (index < pSubscribers.size()) { pSubscribers[index] = subscriber ; index ++ ; } //   3   ...   } private: std::array<ISubscriber*, 3> pSubscribers ; std::size_t index = 0U ; } ; 

Wie kann ein Abonnement im Code aussehen? Und so:


 int main() { //  Led1    5  GPIOC static Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static Led<GPIOC,9> Led3 ; ButtonController buttonController ; //  3  buttonController.Subscribe(&Led1) ; buttonController.Subscribe(&Led2) ; buttonController.Subscribe(&Led3) ; //       buttonController.Run() ; } 

Die gute Nachricht ist, dass wir jedes Objekt signieren können und der Zeitpunkt seiner Erstellung für uns keine Rolle spielt. Es kann ein globales Objekt sein, statisch oder lokal. Einerseits ist das gut, andererseits, warum müssen wir die Laufzeit in diesem Code abonnieren. Tatsächlich ist hier die Adresse der Objekte Led1 , Led2 , Led3 in der Led3 bekannt. Warum können Sie sich also bei der Kompilierung nicht anmelden und eine Reihe von Zeigern auf Abonnenten im ROM behalten?


Darüber hinaus besteht die Gefahr potenzieller Fehler. Wie viele haben sich beispielsweise gefragt, was beim Aufrufen der Subsribe() Methode passieren würde, wenn sie von mehreren Threads aus aufgerufen würde? Wir sind auf 3 Abonnenten beschränkt. Was passiert, wenn wir 4 LEDs signieren?


In den meisten Fällen benötigen wir dieses Abonnement einmal im Leben während der Initialisierung. Wir speichern lediglich Zeiger auf Abonnenten und das wars. Der Zeiger behält die Adresse dieser Abonnenten ein Leben lang. Und der Tag ist unvermeidlich, an dem es ruiniert werden kann aufgrund von Supernova-Ausbruch (natürlich, wenn wir einen längeren Zeitraum in Betracht ziehen). In jedem Fall ist die Wahrscheinlichkeit eines RAM-Ausfalls jedoch viel höher als die des ROM, und es wird nicht empfohlen, permanente Daten im RAM zu speichern.


Nun, die schlechte Nachricht ist, dass eine solche Architekturlösung sowohl im ROM als auch im RAM oooooochen viel Platz beansprucht. Nur für den Fall, wir schreiben, wie viel ROM und RAM diese Lösung benötigt:


ModulRo-Codero DatenRW-Daten
main.o4886421

Das heißt Insgesamt 552 Bytes im ROM und 21 Bytes im RAM - sagen wir nicht so viel, um eine Taste zu drücken und drei LEDs zu blinken.


Um sich vor solchen Problemen zu schützen und den Verbrauch von Controller-Ressourcen zu reduzieren, sollten Sie die Option mit einem statischen Abonnement in Betracht ziehen.


Statisches Abonnement


Um das Abonnement statisch zu machen, können Sie verschiedene Ansätze verwenden. Ich werde sie so benennen:


  • Der traditionelle Ansatz ist derselbe, verwendet jedoch den Konstruktor constexpr und legt die Liste der Abonnenten fest.
  • Unkonventionell Vorlagen verwenden - Übertragen Sie die Liste der Abonnenten über die Vorlagenparameter. (hier ist eine Vorlage eine Definition aus dem Bereich der Metaprogrammierung, keine Entwurfsmuster)

Der traditionelle Ansatz für statische Abonnements


Lassen Sie uns versuchen, in der Kompilierungsphase zu abonnieren. Dazu optimieren wir unsere Architektur ein wenig:



Das Bild unterscheidet sich nicht wesentlich vom Original, es gibt jedoch einige Unterschiede: Die Methode Subscribe() wurde entfernt, und das Abonnement wird jetzt direkt im Konstruktor ausgeführt. Der Konstruktor muss eine variable Anzahl von Argumenten akzeptieren, und um in der Kompilierungsphase statisch signieren zu können, ist er constexpr . Darin wird ein Array von Abonnenten initialisiert, und diese Initialisierung kann zur Kompilierungszeit durchgeführt werden:


 struct ButtonController : IPublisher { template<typename... Args> constexpr ButtonController(Args const*... args): pSubscribers() { std::initializer_list<ISubscriber const*> result = {args...} ; std::size_t index = 0U; for(auto it: result) { if (index < size) { pSubscribers[index] = const_cast<ISubscriber*>(it); } index ++ ; } } private: static constexpr std::size_t size = 3U; ISubscriber* pSubscribers[size] ; } ; 

Vollständiger Code für eine solche Implementierung
 struct ISubscriber { virtual void HandleEvent() const = 0; } ; struct IPublisher { virtual void Notify() const = 0; } ; template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0) //   { while ((Port::IDR::Read() & (1 << pinNum)) == 0) //     { }; result = true; } return result; } } ; template <typename Port, std::uint32_t pinNum> struct Led: ISubscriber { constexpr Led() { } static void Toggle() { Port::ODR::Toggle(1<<pinNum); } void HandleEvent() const override { Toggle() ; } }; //     GPIOC.13 using UserButton = Button<GPIOC, 13> ; struct ButtonController : IPublisher { template<typename... Args> constexpr ButtonController(Args const*... args): pSubscribers() { std::initializer_list<ISubscriber const*> result = {args...} ; std::size_t index = 0U; for(auto it: result) { if (index < size) { pSubscribers[index] = const_cast<ISubscriber*>(it); } index ++ ; } } void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const override { for(auto it: pSubscribers) { if (it != nullptr) { it->HandleEvent() ; } } } private: static constexpr std::size_t size = 3U; ISubscriber* pSubscribers[size] ; } ; 

Jetzt kann das Abonnement zur Kompilierungszeit erfolgen:


 int main() { //  Led1    5  GPIOC static constexpr Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static constexpr Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static constexpr Led<GPIOC,9> Led3 ; static constexpr ButtonController buttonController(&Led1, &Led2, &Led3) ; buttonController.Run() ; return 0 ; } ; 

Hier befindet sich das buttonController Objekt zusammen mit einer Reihe von Zeigern auf Abonnenten vollständig im ROM:


main :: buttonController 0x800'1f04 0x10 Daten main.o [1]

Alles scheint nichts zu sein, außer dass wir wieder auf nur 3 Abonnenten beschränkt sind. Und die Publisher-Klasse muss über einen Constexpr-Konstruktor verfügen und im Allgemeinen vollständig konstant sein, um einen Zeiger auf Abonnenten im ROM zu gewährleisten. Andernfalls wird unser Objekt mit allen Inhalten auch bei bekannten Abonnentenadressen wieder in den RAM verschoben.


Von den anderen Nachteilen - da virtuelle Funktionen immer noch verwendet werden, werden die virtuellen Funktionstabellen Stück für Stück in unserem ROM gespeichert. Und die Ressource ist zwar erschwinglich, aber nicht unendlich. In den meisten Anwendungen ist es möglich, darauf zu hämmern und einen größeren Mikrocontroller zu verwenden. Es kommt jedoch häufig vor, dass jedes Byte zählt, insbesondere bei Produkten, die von Hunderttausenden hergestellt werden, z. B. physischen Sensoren.


Mal sehen, wie es mit dem Speicher in dieser Lösung aussieht:


ModulRo-Codero DatenRW-Daten
main.o172760

Und obwohl das Ergebnis „umwerfend“ ist: Der gesamte RAM-Verbrauch beträgt 0 Byte und der ROM-Speicher 248 Byte, was der Hälfte der ersten Lösung entspricht, besteht nach Ansicht des Herstellers noch Verbesserungspotenzial. Von diesen 248 Bytes belegen ungefähr 50 nur die virtuellen Methodentabellen.


Ein kleiner Exkurs:
Ein Schritt in der ROM-Größe von 256 kByte für moderne Mikrocontroller ist die Norm (beispielsweise hat der TI Cortex M4-Mikrocontroller 256 kByte ROM und die nächste Version ist bereits 512 kByte). Und es wird nicht sehr gut sein, wenn wir aufgrund von 50 zusätzlichen Bytes einen Controller mit einem 256-kByte-ROM nehmen müssen, der größer und teurer ist. Daher können durch den Verzicht auf virtuelle Funktionen bis zu 50 Cent eingespart werden (der Unterschied zwischen dem Mikrocontroller in einem 256- und 512-kByte-ROM beträgt ungefähr 50-60 Cent).


Das klingt für einen Mikrocontroller lächerlich, aber bei einer Charge von 400.000 Geräten pro Jahr können Sie 200.000 US-Dollar sparen. Schon gar nicht so lustig, aber was für eine Ratte. Sie können das Angebot mit einem Diplom und einer Geschenkkarte für 3.000 Rubel belohnen. Es besteht absolut kein Zweifel an der Richtigkeit, virtuelle Funktionen abzulehnen und zusätzliche 50 Bytes im ROM zu sparen.


Unkonventioneller Ansatz


Lassen Sie uns sehen, wie Sie dasselbe ohne virtuelle Funktionen tun und etwas mehr ROM sparen können.


Lassen Sie uns zuerst herausfinden, wie es sein könnte:


 int main() { //  Led1    5  GPIOC static Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static Led<GPIOC,9> Led3 ; //   ButtonController<Led1, Led2, Led3> buttonController ; buttonController.Run() ; return 0 ; } 

Unsere Aufgabe ist es, die beiden Objekte Publisher ( ButtonController ) und Subscriber ( Led ) voneinander zu entkoppeln, damit sie sich nicht kennen, ButtonController aber gleichzeitig Led benachrichtigen kann.


Sie können die ButtonController Klasse ButtonController irgendeine Weise deklarieren.


 template <Led<GPIOC,5>& subscriber1, Led<GPIOC,8>& subscriber2, Led<GPIOC,9>& subscriber3> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { subscriber1.HandleEvent() ; subscriber2.HandleEvent() ; subscriber3.HandleEvent() ; } ... } ; 

Sie verstehen jedoch, dass wir hier an bestimmte Typen gebunden sind und die Definition der BbuttonController Klasse jedes Mal in einem neuen Projekt neu BbuttonController . Und ich möchte ButtonController einfach ohne ButtonController in das neue Projekt aufnehmen und verwenden.


C ++ 17 rettet Sie, indem Sie den Typ nicht angeben können, sondern den Compiler bitten, den Typ für Sie abzuleiten - genau das brauchen Sie. Wir können, genau wie beim traditionellen Ansatz, das Wissen des Herausgebers und des Abonnenten freisetzen, während die Anzahl der Abonnenten praktisch unbegrenzt ist.


 template <auto& ... subscribers> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { pass((subscribers.HandleEvent() , true)...) ; } ... } ; 

So funktioniert die Funktion pass (..)

Die Notify() -Methode ruft die pass() -Funktion auf und erweitert die Vorlagenparameter mit einer variablen Anzahl von Argumenten


  void Notify() const { pass((subscribers.HandleEvent() , true)...) ; } 

Die Implementierung der pass() -Funktion ist einfach nicht vorstellbar, es ist nur eine Funktion, die eine variable Anzahl von Argumenten akzeptiert:


 template<typename... Args> void pass(Args...) const { } } ; 

Wie erweitert sich die Funktion HandleEvent() in mehrere Aufrufe für jeden der Teilnehmer?


Da die Funktion pass() mehrere Argumente eines beliebigen Typs bool , können Sie mehrere Argumente des Typs bool Sie können beispielsweise die Funktion pass(true, true, true) aufrufen. In diesem Fall wird natürlich nichts passieren, aber wir brauchen nicht.


In der Zeile (subscribers.HandleEvent() , true) wird der Operator "," (Komma) verwendet, der beide Operanden (von links nach rechts) ausführt und den Wert des zweiten Operators zurückgibt, d. H. subscribers.HandleEvent() wird zuerst ausgeführt und dann true die Funktion true pass() wird auf true .


Nun, "..." ist ein Standardeintrag zum Erweitern einer variablen Anzahl von Argumenten. Für unseren Fall können die Aktionen des Compilers sehr schematisch wie folgt beschrieben werden:


 pass((subscribers.HandleEvent() , true)...) ; -> pass((Led1.HandleEvent() , true), (Led2.HandleEvent() , true), (Led3.HandleEvent() , true)) ; -> Led1.HandleEvent() ; -> pass(true, (Led2.HandleEvent() , true), (Led3.HandleEvent() , true)) ; -> Led2.HandleEvent() ; -> pass(true, true, (Led3.HandleEvent() , true)) ; -> Led3.HandleEvent() ; -> pass(true, true, true) ; 

Anstelle von Links können Sie Zeiger verwenden:


 template <auto* ... subscribers> struct ButtonController { ... } ; 

Ergänzung: Eigentlich danke an Vamireh, der darauf hingewiesen hat, dass all diese Tänze mit sind Tamburin pass Funktion in C ++ 17 ist nicht erforderlich. Da der Operator "," das Komma im Fold-Ausdruck (der im C ++ 17-Standard eingeführt wurde) unterstützt, wird der Code weiter vereinfacht:


 template <auto& ... subscribers> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { ((subscribers.HandleEvent()), ...) ; } } ; 

Generell sieht es architektonisch sehr einfach aus:



Ich habe hier eine weitere LCD-Klasse hinzugefügt, aber nur zum Beispiel, um zu zeigen, dass es jetzt nicht mehr auf den Typ und die Anzahl der Abonnenten ankommt. Hauptsache, es würde die HandleEvent() -Methode implementieren.


Und der gesamte Code im Allgemeinen ist jetzt auch einfacher:


 template<typename Port, std::size_t pinNum> struct Button { static bool IsPressed() { bool result = false; if ((Port::IDR::Read() & (1 << pinNum)) == 0) //   { while ((Port::IDR::Read() & (1 << pinNum)) == 0) //     { }; result = true; } return result; } } ; //     GPIOC.13 using UserButton = Button<GPIOC, 13> ; template <typename Port, std::uint32_t pinNum> struct Led { static void Toggle() { Port::ODR::Toggle(1<<pinNum); } void HandleEvent() const { Toggle() ; } }; template <auto& ... subscribers> struct ButtonController { void Run() const { for(; ;) { if (UserButton::IsPressed()) { Notify() ; } } } void Notify() const { ((subscribers.HandleEvent()), ...) ; } } ; int main() { //  Led1    5  GPIOC static constexpr Led<GPIOC,5> Led1 ; //  Led2    8  GPIOC static constexpr Led<GPIOC,8> Led2 ; //  Led3    9  GPIOC static constexpr Led<GPIOC,9> Led3 ; static constexpr ButtonController<Led1, Led2, Led3> buttonController ; buttonController.Run() ; return 0 ; } 

Der Aufruf Notify() in der Methode Run() wird zu einem einfachen sequenziellen Aufruf


 Led1.HandleEvent() ; Led2.HandleEvent() ; Led3.HandleEvent() ; 

Was ist mit der Erinnerung hier?


ModulRo-Codero DatenRW-Daten
main.o18640

ROM insgesamt 190 Bytes und 0 Bytes RAM. Jetzt ist die Bestellung fast 3-mal kleiner als die Standardversion, während es genau das Gleiche leistet.


Wenn Sie also die Adressen der Abonnenten bereits in der Anmeldung bekannt haben und die am Anfang des Artikels definierten Bedingungen einhalten


Bedingungen am Anfang des Artikels
  • Verwenden Sie keine dynamische Speicherzuordnung
  • Minimieren Sie die Arbeit mit Zeigern
  • Wir verwenden so viele Konstanten wie möglich, damit niemand so viel wie möglich ändern kann
  • Gleichzeitig verwenden wir jedoch so wenig Konstanten wie möglich, die sich im RAM befinden

Mit Vertrauen können Sie eine solche Implementierung der Publisher-Subscriber-Vorlage verwenden, um Codezeilen zu reduzieren und Ressourcen zu sparen. Dort können Sie nicht nur eine Geschenkkarte, sondern auch einen Bonus für das Jahr anfordern.


Das Testbeispiel unter IAR 8.40.2 liegt hier


Alles mit dem Kommen! Und viel Glück im neuen Jahr!

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


All Articles