Hallo zusammen,
ich habe einen enum-ähnlichen Ausdruck, den ich schon im Präprozessor
auswerten können muss. Also so:
1
#define opt_trutenprugel 1
2
#define opt_noelpenproettel 2
3
#define opt_hoelterhofer 3
4
#define opt_boelterkamp 4
(Das startet bei 1, damit nicht bei einem fehlenden #define der
Vergleich 0 = 0 erfolgreich ist.)
Nun weiss ich allerdings durchaus den Komfort zu schätzen, wenn mich der
Compiler bei switch-case-Umgebungen davor warnt, wenn ich einen Fall
vergessen habe. Das geht bei den defines nicht.
Klar: Ich koennte jetzt zusätzlich ein enum definieren:
Es steht doch jedem selber frei, sich einen Praeprozessor zu
schreiben, der genau die Wunschfunktionen hat, die mann meint
unbedingt brauchen zu muessen.
Den haekelt man dann ins Makefile und alles wird gut.
Besitzer kommerzieller IDEs haben es da schwerer.
Die muessen einen Pre-Built-Step in ihre GUI klicken.
Und die Editoren und Codeparser werden bestimmt auch maulen.
ich nehme den Preis nicht an schrieb:> Walter T. schrieb:>> ch koennte jetzt zusätzlich ein enum definieren:enum options_e>> {>> opt_trutenprugel_ = opt_trutenprugel,>> geht das überhaut?
Warum sollte es nicht?
Wenn es nur 100 oder so sind, dann reicht es, die per Hand zu Pflegen.
Mit entsprechendem Compile-Time-Asserts und wenn möglich, mit üblicher
Schreibweise (#defines Groß)
Wenn es mehr sind, dann lass Dir die #defines aus dem enum mit einem
Textfresser erzeugen. Das geht relativ einfach, wenn man sich an ein
paar Regeln hält, z.B. alle enums im Klartext, nur einen pro Zeile, ...
.
Die ganzen erzeugten Dinge kommen dann in eine generated.h (oder so) und
werden im Make mit eingetragen.
A. S. schrieb:> Wenn es nur 100 oder so sind, dann reicht es, die per Hand zu Pflegen.
So viele sind es zum Glück nicht. Der Pflegeaufwand hält sich in
Grenzen. Was mich an der Lösung stört, ist dass die Schreibweise unter
Umständen Verwirrung stiften kann. (MAKROS sehen wie enums_aus, enums_
sehen so aus, als wären sie etwas Besonderes, obwohl sie das "normalere"
sind.)
Ein Tippfehler (vergessener Unterstrich am Ende) wird folgenlos bleiben.
Ich will nur sicherstellen, dass ich mich nicht an etwas Merkwürdiges
gewöhne.
Walter T. schrieb:> Nun weiss ich allerdings durchaus den Komfort zu schätzen, wenn mich der> Compiler bei switch-case-Umgebungen davor warnt, wenn ich einen Fall> vergessen habe. Das geht bei den defines nicht.
Ich verstehe deine Frage nicht. Sowas geht doch:
1
#define opt_trutenprugel 1
2
#define opt_noelpenproettel 2
3
#define opt_hoelterhofer 3
4
#define opt_boelterkamp 4
5
6
#define option 1
7
8
#if (option == opt_trutenprugel)
9
#pragma message ("Do case 1")
10
//...
11
#elif (option == opt_noelpenproettel)
12
#pragma message ("Do case 2")
13
//...
14
#elif (option == opt_hoelterhofer)
15
#pragma message ("Do case 3")
16
//...
17
#elif (option == opt_boelterkamp)
18
#pragma message ("Do case 4")
19
//...
20
#else
21
#error Invalid option!
22
#endif
Damit (else zweig) kann man prüfen, ob eine Option falsch gesetzt wurde.
Meinst Du etwas anderes?
Klaus H. schrieb:> Damit (else zweig) kann man prüfen, ob eine Option falsch gesetzt wurde.> Meinst Du etwas anderes?
Viele Sachen muss ich nicht im Präprozessor prüfen, sondern es reicht
ein normales switch-case
1
enumoptions_ea=1;
2
switch(a)
3
{
4
caseopt_trutenprugel_:/* Fallthru */
5
caseopt_noelpenproettel_:/* Fallthru */
6
caseopt_hoelterhofer_:
7
break;
8
}
Und der Compiler merkt sofort:
1
src_application\debugme.c|75|warning: enumeration value 'opt_boelterkamp_' not handled in switch [-Wswitch]|
Das ist schön. Es vereinfacht ungemein Implementierungen nach dem
"YAGNI"-Prinzip. (Wenn man eine neue Variante benötigt, neues
Enum-Element, und an den Stellen ergänzen, wo der Compiler dessen Fehlen
bemängelt.)
Ahhh, jetzt hab ich's kapiert. Merci!
Die -Wswitch ist beim direkten Tippen / Compilieren nicht schlecht.
Aber ich versuche meine Funktionalitäten per Tests zu prüfen.
Spätestens dann fällt einem eine nicht vorhandene Case auf.
Ich habe mal ein Tool in Python gemacht,
dass verschiedene andere Szenarien geprüft hat.
Aber hier lohnt sich vermutlich der Aufwand nicht,
bzw. man sollte die Zeit ins Testen investieren.
Walter T. schrieb:> ich habe einen enum-ähnlichen Ausdruck, den ich schon im Präprozessor> auswerten können muss.
Wobei sich mir die Frage stellt, warum.
Früher ging das nur so, aber mittlerweile sind die Compiler doch
problemlos in der Lage, zur Compilezeit konstante Ausdrücke auch selbst
zu berechnen. Wenn diese dann in Bedingungen benutzt werden, die
konstant "false" sind, wird der entsprechende Code nicht generiert –
nicht anders als bei einem #if 0 im Präprozessor.
Jörg W. schrieb:> Früher ging das nur so, aber mittlerweile sind die Compiler doch> problemlos in der Lage, zur Compilezeit konstante Ausdrücke auch selbst> zu berechnen.
Jepp. Aber solche Fälle werden selten per switch case realisiert.
Oft hab ich an verschiedenen Stellen if()...else if....
Da hilft die -Wswitch Warnung nicht....
Jörg W. schrieb:> Wobei sich mir die Frage stellt, warum.
Wenn ich die Frage beantworte, wird man mir wieder Obskurität vorwerfen.
Ich habe in meinem Projekt eine Konfigurationsdatei, in der alle
Hardware-Einstellungen stehen. Damit lässt sich halbwegs einfach
Übersicht halten, welche Hardware-Ressourcen wofür belegt sind. Für eine
Timer-ISR sieht das so aus:
Wie man sieht, geht es bei den Timern schon etwas eng zu, dass man
Übersicht gut gebrauchen kann. Und Ressourcen (z.B. Pins) mit Strg-F
suchen zu können finde ich auch nützlich.
Die Werte sind als enum in einem anderen Header hinterlegt:
/* kein implementierter Timer benoetigt aufraeumen am Ende */
56
(void)timerIsr;
57
}
Initialisierung usw. lässt sich in normalen C-Funktionen dann recht
komfortabel mit switch/case auswählen, und der Compiler warnt
netterweise, damit auch nichts vergessen wird.
Die eigentliche ISR sieht dann so aus:
1
/* andere .c */
2
/** Timer-Funktion */
3
voidFAST_LOOP_IRQ_HANDLER(void)
4
{
5
/* z.B. ISR-Status-Bits behandeln */
6
isr_function_startup(FAST_LOOP_ISR_TIMx);
7
do_some_stuff();
8
isr_function_closing(FAST_LOOP_ISR_TIMx);
9
}
Man beachte, dass das zweite #define in der Config-Datei nur deshalb
benötigt wird, weil ich mit Präprozessor-Direktiven nicht auch
enum-Werte vergleichen kann. Sei's drum. Ansonsten geht es gut und
meiner Meinung nach komfortabler, als im ISR Vector Table
herumzuschreiben.
Jetzt habe ich mir den Spaß gemacht, und eine der ISRs in Assembler
nachprogrammiert:
1
/* andere.S */
2
.align4
3
.text
4
.typeTIM6_DAC_IRQHandler,%function
5
.globalTIM6_DAC_IRQHandler
6
TIM6_DAC_IRQHandler:
7
@ITpendingBitloeschenTIMx->SR=(uint16_t)~TIM_IT;
8
ldrr0,=TIM6_BASE
9
ldrr1,=~TIM_IT_Update
10
strr1,[r0,#TIMx_SR]
11
12
@Nutzlast
13
dos0,m3
14
stu#FF
15
bxlr
16
.ltorg
Der globale Funktionsname lässt sich noch problemlos über den
C-Präprozessor austauschen. Mit der Präambel sieht es allerdings anders
aus. Hier fällt mir kein außer dem im Eröffnungsbeitrag genannten
Mechanismus ein, wie mich
Compiler/Assembler zur Compilezeit wenigstens warnen könnte, dass die
implementierte ISR nicht mehr funktionieren wird.
Walter T. schrieb:> Compiler/Assembler zur Compilezeit wenigstens warnen könnte, dass die> implementierte ISR nicht mehr funktionieren wird.
Du kannst einen "default" anlegen und dort ein Konstrukt unterbringen,
der bei Deinen Warnungen anschlägt, wenn der Code durchlaufen wird.
Ich kenne Deine Warnungen nicht. Möglich wäre sowas wie set twice before
used. Oder etwas mit einer uninitialisierten Variable. Wichtig ist nur,
dass der Compiler bzw. das Analysetool nur dann anspricht, wenn der Code
wirklich durchlaufen wird.
Sobald Du ein Konstrukt gefunden hast, wird es aufgehübscht, mit schönen
Makros und Namen versehen und ist dann so selbstverständlich wie ein
Compile-Time-Assert.
Deinen Aufbau von timer_isr_which() würde ich ändern.
Wenn ein ungültiger Wert übergeben wird, dann läuft dein Programm
weiter. Einen NULL Pointer zurückzugeben halte ich für fehleranfällig.
Mit static Assertions kommst du nicht weit, da die Funktion so definiert
ja auch mit Werten umgehen muss, die erst zur Laufzeit bekannt sind.
Ich würde die case Pfade die "eigentlich" nicht möglich sind, auch mit
Assertions abfangen, vor allem den default Zweig.
In isr_function_startup() hast du das ja zumindest einmal so gemacht.
Ich nutze BTW in meinen embedded Programmen eine Runtime Assertion, die
auch im Release drin bleibt. Man glaubt gar nicht wie oft ein Szenario
eintritt, das "sowieso niemals" auftreten kann.
Walter T. schrieb:> Ich habe in meinem Projekt eine Konfigurationsdatei, in der alle> Hardware-Einstellungen stehen.
Die Vorgehensweise mit der Konfigurationsdatei ist OK, mach ich auch
gerne so. Allerdings kann es Konflikte geben, wenn eine Ressouce
mehrfach eingesetzt wird. Nicht immer entsteht dadurch ein
Compilerfehler.
Wie fängst Du Kollisionen ab?
Klaus H. schrieb:> Ich nutze BTW in meinen embedded Programmen eine Runtime Assertion, die> auch im Release drin bleibt. Man glaubt gar nicht wie oft ein Szenario> eintritt, das "sowieso niemals" auftreten kann.
Aber was macht deine Assertion da? Auf einem µC kann man ja in der Regel
nicht einfach mal eine Fehlermeldung anzeigen und das Programm beenden.
Man braucht dann eher extra Code, der den Fall irgendwie behandelt und
das Programm wieder in einen sauberen Zustand oder zumindest eine
Rückfallebene überführt.
Rolf M. schrieb:> Aber was macht deine Assertion da? Auf einem µC kann man ja in der Regel> nicht einfach mal eine Fehlermeldung anzeigen und das Programm beenden.
Das kommt auf das Embedded System an.
Wenn das System „Stehen bleiben kann“ springe ich in eine Endlosschleife
und gebe den Fehler aus. Entweder über das Display oder über eine
serielle Schnittstelle - schlimmstenfalls über einen Standard Pin per
Software emulierter serieller Schnittstelle oder als LED Morsecode.
In Systemen, die weiterlaufen sollen, wird nach x Sekunden Fehleranzeige
ein Reset generiert.
Die Frage ist doch eher was machen embedded Programme, die keine fatalen
Fehler per Assertions abfangen?
Klaus H. schrieb:> Wie fängst Du Kollisionen ab?
Gar nicht. Es ist nur eine Merkhilfe.
Rolf M. schrieb:> Aber was macht deine Assertion da?
Bei mir springt sie in den Hardfault-Handler. Wenn das Display schon
initialisiert ist, steht dann eine Fehlermeldung da. Wenn nicht, wird
wenigstens "........" gemorst.
Wenn eine Assertion fehlschlägt, ist sowieso spätestens der Punkt
erreicht, mit dem Debugger nachzugucken.
Klaus H. schrieb:> Wenn ein ungültiger Wert übergeben wird, dann läuft dein Programm> weiter. Einen NULL Pointer zurückzugeben halte ich für fehleranfällig.
Alle nicht-Header-Funktionen prüfen grundsätzlich auf Nullzeiger -
insbesondere alle init()-Funktionen. Das sollte schnell auffallen. Ich
versuche Assertions in static-inline-Funktionen weitestgehend zu
vermeiden, weil die Zuordnung zum Quelltext sehr oft nicht einfach ist.
Walter T. schrieb:> Alle nicht-Header-Funktionen prüfen grundsätzlich auf Nullzeiger -> insbesondere alle init()-Funktionen. Das sollte schnell auffallen.
Defensive Programmierung 👍👍👍
Damit erschlägst Du doch einen Großteil deiner obigen Anfrage.
Präprozessorkonstrukte, die "zukünftige" neue Konfigurationen aufdecken
können würde ich so eher nicht umsetzen.
Klaus H. schrieb:> Die Frage ist doch eher was machen embedded Programme, die keine fatalen> Fehler per Assertions abfangen?
Ich sage ja nicht, dass man ihn nicht abfangen soll, sondern eher, dass
assertions gerade im Embedded-Bereich oft schwierig sind.
Deshalb ja auch:
Rolf M. schrieb:> Man braucht dann eher extra Code, der den Fall irgendwie behandelt und> das Programm wieder in einen sauberen Zustand oder zumindest eine> Rückfallebene überführt.
Rolf M. schrieb:> Ich sage ja nicht, dass man ihn nicht abfangen soll, sondern eher, dass> assertions gerade im Embedded-Bereich oft schwierig sind.> Deshalb ja auch:>> Rolf M. schrieb:>> Man braucht dann eher extra Code, der den Fall irgendwie behandelt und>> das Programm wieder in einen sauberen Zustand oder zumindest eine>> Rückfallebene überführt.
Nach meinem Kenntnisstand kommt nach einer fehlgeschlagenen Assertion
keine "graceful Degradation" mehr (im Gegensatz zu "error()" oder
try-catch). Nach einer fehlgeschlagenen Assertion ist das System tot.
Deswegen sind sie auch (im Gegensatz zu den anderen beiden) nur im
Debug-Code aktiv.
Aber das entfernt sich so langsam vom eigentlichen Thema. Es ging ja
eigentlich um präprozessortaugliche Enum-ähnliche Konstrukte. Wobei das
andere Thema sicherlich interessant genug ist, einen eigenen Thread zu
rechtfertigen.
Eine Idee zu deinem ursprünglichen Problem hätte ich noch:
Wenn die switch Strukturen immer mit Konstanten gefüttert werden,
so dass ungenutzter Code raus optimiert werden kann,
dann könntest Du "Linker Assertions" nutzen.
Beispiel:
Geht allerdings nur, wenn die Toolchain optimieren darf (z.B. gcc -O1).
Dann erkennst Du aber immerhin beim Linken, wenn neue case Pfade fehlen.
Das müsste dann auch bei if() else if() else ... funktionieren.
Mit Boost PP könnte man da vermutlich irgendwas unleserliches
zusammenhacken... davon würde ich aber abraten. Sinnvoller wäre es
vermutlich eine Programmiersprache zu verwenden die die gewünschten
Konstrukte unterstützt.