Bei der Entwicklung von Software für Mikrocontroller in C ++ kann häufig festgestellt werden, dass die Verwendung der Standardbibliothek zu unerwünschten zusätzlichen Kosten für Ressourcen, sowohl RAM als auch ROM, führen kann. Daher sind die Klassen und Methoden aus der
std
häufig nicht für die Implementierung im Mikrocontroller geeignet. Es gibt auch einige Einschränkungen bei der Verwendung von dynamisch zugewiesenem Speicher, RTTI, Ausnahmen usw. Um kompakten und schnellen Code zu schreiben, können Sie im Allgemeinen nicht einfach die
typeid
verwenden und beispielsweise
typeid
Operatoren verwenden, da Sie RTTI-Unterstützung benötigen. Dies ist ein Overhead, wenn auch nicht sehr groß.
Daher muss man manchmal das Rad neu erfinden, um all diese Bedingungen zu erfüllen. Es gibt nur wenige solche Aufgaben, aber sie sind es. In diesem Beitrag möchte ich über eine scheinbar einfache Aufgabe sprechen - die Rückkehrcodes vorhandener Subsysteme in Mikrocontroller-Software zu erweitern.
Herausforderung
Angenommen, Sie haben ein CPU-Diagnose-Subsystem und es gibt unzählige Rückkehrcodes. Sagen Sie Folgendes:
enum class Cpu_Error { Ok, Alu, Rom, Ram } ;
Wenn das CPU-Diagnosesubsystem einen Fehler eines der CPU-Module (z. B. ALU oder RAM) feststellt, muss es den entsprechenden Code zurückgeben.
Dasselbe gilt für ein anderes Subsystem: Es handelt sich um eine Messdiagnose, bei der überprüft wird, ob der gemessene Wert im Bereich liegt und allgemein gültig ist (nicht gleich NAN oder Infinity):
enum class Measure_Error { OutOfLimits, Ok, BadCode } ;
GetLastError()
für jedes Subsystem eine
GetLastError()
-Methode an, die den aufgezählten Fehlertyp dieses Subsystems zurückgibt. Für
CpuDiagnostic
Code vom Typ
CpuDiagnostic
zurückgegeben, für
MeasureDiagnostic
Code vom Typ
Measure_Error
.
Und es gibt ein bestimmtes Protokoll, das im Fehlerfall den Fehlercode protokollieren sollte.
Zum Verständnis werde ich dies in einer sehr vereinfachten Form schreiben:
void Logger::Update() { Log(static_cast<uint32_t>(cpuDiagnostic.GetLastError()) ; Log(static_cast<uint32_t>(measureDiagstic.GetLastError()) ; }
Es ist klar, dass wir beim Konvertieren der Aufzählungstypen in eine Ganzzahl denselben Wert für verschiedene Typen erhalten können. Wie kann man unterscheiden, dass der erste Fehlercode der Fehlercode des CPU-Diagnose-Subsystems und des zweiten Mess-Subsystems ist?
Suche nach Lösungen
Es wäre logisch,
GetLastError()
die
GetLastError()
-Methode unterschiedlichen Code für unterschiedliche Subsysteme
GetLastError()
. Eine der direktesten Entscheidungen auf der Stirn wäre die Verwendung unterschiedlicher Codebereiche für jeden aufgezählten Typ. So etwas in der Art
constexpr tU32 CPU_ERROR_ALU = 0x10000001 ; constexpr tU32 CPU_ERROR_ROM = 0x10000002 ; ... constexpr tU32 MEAS_ERROR_OUTOF = 0x01000001 ; constexpr tU32 MEAS_ERROR_BAD = 0x01000002 ; ... enum class Cpu_Error { Ok, Alu = CPU_ERROR_ALU, Rom = CPU_ERROR_ROM, Ram = CPU_ERROR_RAM } ; ...
Ich denke, die Nachteile dieses Ansatzes liegen auf der Hand. Erstens müssen Sie bei viel manueller Arbeit die Bereiche und Rückkehrcodes manuell bestimmen, was sicherlich zu einem menschlichen Fehler führen wird. Zweitens kann es viele Subsysteme geben, und das Hinzufügen von Aufzählungen für jedes Subsystem ist überhaupt keine Option.
Eigentlich wäre es großartig, wenn es möglich wäre, die Übertragungen überhaupt nicht zu berühren, ihre Codes auf eine etwas andere Weise zu erweitern, zum Beispiel, um dies tun zu können:
ResultCode result = Cpu_Error::Ok ;
Oder so:
ReturnCode result ; for(auto it: diagnostics) {
Oder so:
void CpuDiagnostic::SomeFunction(ReturnCode errocode) { Cpu_Error status = errorcode ; switch (status) { case CpuError::Alu:
Wie Sie dem Code
ReturnCode
wird hier eine Klasse
ReturnCode
verwendet, die sowohl den Fehlercode als auch dessen Kategorie enthalten sollte. In der Standardbibliothek gibt es eine solche Klasse
std::error_code
, die eigentlich fast alles macht. Sehr gut wird sein Zweck hier beschrieben:
Dein eigener std :: code_errorUnterstützung für Systemfehler in C ++Deterministische Ausnahmen und Fehlerbehandlung in „C ++ der Zukunft“Die Hauptbeschwerde ist, dass wir zur Verwendung dieser Klasse
std::error_category
erben
std::error_category
, das für die Verwendung in Firmware auf kleinen Mikrocontrollern eindeutig stark überlastet ist. Sogar zumindest mit std :: string.
class CpuErrorCategory: public std::error_category { public: virtual const char * name() const; virtual std::string message(int ev) const; };
Darüber hinaus müssen Sie die Kategorie (Name und Nachricht) für jeden der aufgezählten Typen manuell beschreiben. Und auch der Code, der das Fehlen eines Fehlers in
std::error_code
ist 0. Und es gibt mögliche Fälle, in denen der Fehlercode für verschiedene Typen unterschiedlich ist.
Ich möchte keinen Overhead haben, außer eine Kategorienummer hinzuzufügen.
Daher wäre es logisch, etwas zu „erfinden“, das es dem Entwickler ermöglicht, ein Minimum an Bewegungen auszuführen, um eine Kategorie für seinen aufgezählten Typ hinzuzufügen.
Zuerst müssen Sie eine Klasse
std::error_code
, die
std::error_code
und in der Lage ist, jeden Aufzählungstyp in eine Ganzzahl umzuwandeln und umgekehrt von einer Ganzzahl in einen Aufzählungstyp. Zusätzlich zu diesen Funktionen, um die Kategorie, den tatsächlichen Wert des Codes zurückgeben zu können und um Folgendes überprüfen zu können:
Lösung
Die Klasse muss den Fehlercode, den Kategoriecode und den Code, der der Abwesenheit von Fehlern entspricht, den Umwandlungsoperator und den Zuweisungsoperator speichern. Die entsprechende Klasse lautet wie folgt:

Klassencode class ReturnCode { public: ReturnCode() { } template<class T> explicit ReturnCode(const T initReturnCode): errorValue(static_cast<tU32>(initReturnCode)), errorCategory(GetCategory(initReturnCode)), goodCode(GetOk(initReturnCode)) { static_assert(std::is_enum<T>::value, " ") ; } template<class T> operator T() const {
Es ist notwendig, ein wenig zu erklären, was hier passiert. Beginnen Sie mit einem Vorlagenkonstruktor
template<class T> explicit ReturnCode(const T initReturnCode): errorValue(static_cast<tU32>(initReturnCode)), errorCategory(GetCategory(initReturnCode)), goodCode(GetOk(initReturnCode)) { static_assert(std::is_enum<T>::value, " ") ; }
Sie können eine Objektklasse aus einem beliebigen Aufzählungstyp erstellen:
ReturnCode result(Cpu_Error::Ok) ; ReturnCode result1(My_Error::Error1); ReturnCode result2(cpuDiagnostic.GetLatestError()) ;
Um sicherzustellen, dass der Konstruktor nur einen Aufzählungstyp akzeptieren kann, wird
static_assert
zu seinem Hauptteil hinzugefügt, der beim Kompilieren den an den Konstruktor übergebenen Typ mit
std::is_enum
und
std::is_enum
Fehler mit Klartext
std::is_enum
. Hier wird kein echter Code generiert, dies ist alles für den Compiler. Tatsächlich ist dies also ein leerer Konstruktor.
Der Konstruktor initialisiert auch private Attribute, darauf werde ich später zurückkommen ...
Als nächstes der Darsteller:
template<class T> operator T() const {
Es kann auch nur zu einem Aufzählungstyp führen und ermöglicht uns Folgendes:
ReturnCode returnCode(Cpu_Error::Rom) ; Cpu_Error status = errorCode ; returnCode = My_Errror::Error2; My_Errror status1 = returnCode ; returnCode = myDiagnostic.GetLastError() ; MyDiagsonticError status2 = returnCode ;
Gut und getrennt der Operator bool ():
operator bool() const { return (GetValue() != goodCode); }
Auf diese Weise können wir direkt prüfen, ob der Rückkehrcode einen Fehler enthält:
Das ist im Wesentlichen alles. Die Frage bleibt in den Funktionen
GetCategory()
und
GetOkCode()
. Wie Sie vielleicht erraten haben, ist der erste für den Aufzählungstyp vorgesehen, um seine Kategorie irgendwie an die
ReturnCode
Klasse zu
ReturnCode
, und der zweite für den Aufzählungstyp, um anzuzeigen, dass es sich um einen guten Rückkehrcode handelt, da wir ihn mit dem Operator
bool()
werden.
Es ist klar, dass diese Funktionen vom Benutzer bereitgestellt werden können, und wir können sie in unserem Konstruktor über den argumentabhängigen Suchmechanismus ehrlich aufrufen.
Zum Beispiel:
enum class CategoryError { Nv = 100, Cpu = 200 }; enum class Cpu_Error { Ok, Alu, Rom } ; inline tU32 GetCategory(Cpu_Error errorNum) { return static_cast<tU32>(CategoryError::Cpu); } inline tU32 GetOkCode(Cpu_Error) { return static_cast<tU32>(Cpu_Error::Ok); }
Dies erfordert zusätzlichen Aufwand vom Entwickler. Wir benötigen für jeden Aufzählungstyp, den wir kategorisieren möchten, diese beiden Methoden hinzuzufügen und die
CategoryError
Aufzählung zu aktualisieren.
Wir möchten jedoch, dass der Entwickler dem Code fast nichts hinzufügt und sich nicht darum kümmert, wie er seinen Aufzählungstyp erweitern kann.
Was kann getan werden?
- Erstens war es großartig, dass die Kategorie automatisch berechnet wurde und der Entwickler nicht für jede Aufzählung eine Implementierung der
GetCategory()
-Methode GetCategory()
. - Zweitens wird in 90% der Fälle in unserem Code Ok verwendet, um guten Code zurückzugeben. Daher können Sie für diese 90% eine allgemeine Implementierung schreiben, und für 10% müssen Sie sich spezialisieren.
Konzentrieren wir uns also auf die erste Aufgabe - die automatische Kategorieberechnung. Die Idee meines Kollegen ist, dass der Entwickler seinen Aufzählungstyp registrieren kann. Dies kann mithilfe einer Vorlage mit einer variablen Anzahl von Argumenten erfolgen. Deklarieren Sie eine solche Struktur
template <typename... Types> struct EnumTypeRegister{};
Um nun eine neue Aufzählung zu registrieren, die um eine Kategorie erweitert werden soll, definieren wir einfach einen neuen Typ
using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error>;
Wenn wir plötzlich eine weitere Aufzählung hinzufügen müssen, fügen Sie sie einfach der Liste der Vorlagenparameter hinzu:
using CategoryErrorsList = EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>;
Offensichtlich kann die Kategorie für unsere Auflistungen eine Position in der Liste der Vorlagenparameter sein, d.h. für
Cpu_Error
es
0 , für
Measure_Error
es
1 , für
My_Error
es
2 . Es bleibt, den Compiler zu zwingen, dies automatisch zu berechnen. Für C ++ 14 machen wir das:
template <typename QueriedType, typename Type> constexpr tU32 GetEnumPosition(EnumTypeRegister<Type>) { static_assert(std::is_same<Type, QueriedType>::value, " EnumTypeRegister"); return tU32(0U) ; } template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(EnumTypeRegister<Type, Types...>) { return 0U ; } template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<!std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(EnumTypeRegister<Type, Types...>) { return 1U + GetEnumPosition<QueriedType>(EnumTypeRegister<Types...>()) ; }
Was ist hier los? Kurz gesagt, die Funktion
GetEnumPosition<T<>>
, wobei der Eingabeparameter eine Liste der Aufzählungstypen von
EnumTypeRegister
, in unserem Fall
EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>
, und der Vorlagenparameter
T ist ein Aufzählungstyp, dessen Index wir in dieser Liste finden sollten. läuft durch die Liste und wenn T mit einem der Typen in der Liste übereinstimmt, gibt es seinen Index zurück, andernfalls wird die Meldung "Typ ist nicht in der EnumTypeRegister-Liste registriert" angezeigt
Lassen Sie uns genauer analysieren. Niedrigste Funktion
template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<!std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(TypeRegister<Type, Types...>) { return 1U + GetEnumPosition<QueriedType>(TypeRegister<Types...>()) ; }
Hier prüft der Zweig
std::enable_if_t< !std::is_same ..
, ob der angeforderte Typ mit dem ersten Typ in der Vorlagenliste
std::enable_if_t< !std::is_same ..
Wenn nicht,
GetEnumPosition
der zurückgegebene Typ der
GetEnumPosition
Funktion
tU32
und dann wird der Funktionskörper ausgeführt,
tU32
ein rekursiver Aufruf derselben Funktion , während die Anzahl der Vorlagenargumente um
1 abnimmt und der Rückgabewert um
1 zunimmt. Das heißt, bei jeder Iteration wird es etwas Ähnliches geben:
Sobald alle Typen in der Liste beendet sind, kann
std::enable_if_t
nicht mehr auf den Typ des Rückgabewerts der Funktion
GetEnumPosition()
, und an dieser Iteration endet:
Was passiert, wenn der Typ in der Liste enthalten ist? In diesem Fall funktioniert ein anderer Zweig, Zweig c
std::enable_if_t< std::is_same ..
:
template <typename QueriedType, typename Type, typename... Types> constexpr std::enable_if_t<std::is_same<Type, QueriedType>::value, tU32> GetEnumPosition(TypeRegister<Type, Types...>) { return 0U ; }
Hier wird
std::enable_if_t< std::is_same ...
ob die Typen
std::enable_if_t< std::is_same ...
Und wenn beispielsweise am Eingang ein Typ
Measure_Error
, wird die folgende Sequenz erhalten:
Bei der zweiten Iteration endet der rekursive Funktionsaufruf und wir erhalten 1 (von der ersten Iteration) + 0 (von der zweiten) =
1 am Ausgang - dies ist ein Index vom Typ Measure_Error in der Liste
EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>
Da dies eine
constexpr,
Funktion ist
constexpr,
alle Berechnungen in der Kompilierungsphase durchgeführt und es wird tatsächlich kein Code generiert.
All dies konnte nicht geschrieben werden, steht C ++ 17 zur Verfügung. Leider unterstützt mein IAR-Compiler C ++ 17 nicht vollständig, sodass das gesamte Fußtuch durch den folgenden Code ersetzt werden konnte:
Es bleiben nun die Vorlagenmethoden
GetCategory()
und
GetOk()
, die
GetEnumPosition
.
template<typename T> constexpr tU32 GetCategory(const T) { return static_cast<tU32>(GetEnumPosition<T>(categoryDictionary)); } template<typename T> constexpr tU32 GetOk(const T) { return static_cast<tU32>(T::Ok); }
Das ist alles. Mal sehen, was mit dieser Objektkonstruktion passiert:
ReturnCode result(Measure_Error::Ok) ;
ReturnCode
wir zum Konstruktor der
ReturnCode
Klasse zurück
template<class T> explicit ReturnCode(const T initReturnCode): errorValue(static_cast<tU32>(initReturnCode)), errorCategory(GetCategory(initReturnCode)), goodCode(GetOk(initReturnCode)) { static_assert(std::is_enum<T>::value, "The type have to be enum") ; }
Es handelt sich um eine Vorlage, und wenn
T
ein
Measure_Error
bedeutet dies, dass die Instanziierung der
GetCategory(Measure_Error)
-Methodenvorlage für den Typ
Measure_Error
, der wiederum
GetEnumPosition
mit dem Typ
Measure_Error
,
GetEnumPosition<Measure_Error>(EnumTypeRegister<Cpu_Error, Measure_Error, My_Error>)
Measure_Error
gibt die Position von
Measure_Error
in der Liste zurück. Position ist
1 . Und tatsächlich wird der gesamte Konstruktorcode bei der Instanziierung des
Measure_Error
Typs durch den Compiler ersetzt durch:
explicit ReturnCode(const Measure_Error initReturnCode): errorValue(1), errorCategory(1), goodCode(1) { }
Zusammenfassung
Für einen Entwickler, der
ReturnCode
verwenden
ReturnCode
gibt es nur eines:
Registrieren Sie Ihren Aufzählungstyp in der Liste.
Und keine unnötigen Bewegungen, der vorhandene Code bewegt sich nicht und für die Erweiterung müssen Sie nur den Typ in der Liste registrieren. Darüber hinaus wird dies alles in der Kompilierungsphase durchgeführt, und der Compiler berechnet nicht nur alle Kategorien, sondern warnt Sie auch, wenn Sie vergessen haben, den Typ zu registrieren, oder versucht haben, einen nicht nicht aufzählbaren Typ zu übergeben.
Fairerweise ist anzumerken, dass Sie in den 10% des Codes, in denen die Aufzählungen anstelle des Ok-Codes einen anderen Namen haben, Ihre eigene Spezialisierung für diesen Typ vornehmen müssen.
template<> constexpr tU32 GetOk<MyError>(const MyError) { return static_cast<tU32>(MyError::Good) ; } ;
Ich habe hier ein kleines Beispiel gepostet:
CodebeispielIm Allgemeinen ist hier eine Anwendung:
enum class Cpu_Error { Ok, Alu, Rom, Ram } ; enum class Measure_Error { OutOfLimits, Ok, BadCode } ; enum class My_Error { Error1, Error2, Error3, Error4, Ok } ;
Drucken Sie die folgenden Zeilen:
Rückkehrcode: 3 Rückkehrkategorie: 0
Rückkehrcode: 3 Rückkehrkategorie: 2
Rückkehrcode: 2 Rückkehrkategorie: 1
mError: 1