Forum: Mikrocontroller und Digitale Elektronik C Präprozessor: Zeichenketten vergleichen oder anderes enum-ähnliches Konstrukt?


von Walter T. (nicolas)


Lesenswert?

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:
1
enum options_e
2
{
3
    opt_trutenprugel_ =    opt_trutenprugel,
4
    opt_noelpenproettel_ = opt_noelpenproettel,
5
    opt_hoelterhofer_ =    opt_hoelterhofer,
6
    opt_boelterkamp_ =     opt_boelterkamp,
7
}
oder geht es irgendwie besser?

von MaWin (Gast)


Lesenswert?

Walter T. schrieb:
> oder geht es irgendwie besser?

Nicht wirklich.

von oerks (Gast)


Lesenswert?

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.

von ich nehme den Preis nicht an (Gast)


Lesenswert?

Walter T. schrieb:
> ch koennte jetzt zusätzlich ein enum definieren:enum options_e
> {
>     opt_trutenprugel_ =    opt_trutenprugel,

geht das überhaut?

von Rolf M. (rmagnus)


Lesenswert?

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?

von A. S. (Gast)


Lesenswert?

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.

von ich nehme den Preis nicht an (Gast)


Lesenswert?

Rolf M. schrieb:
> Warum sollte es nicht?

weil alles vom Preprocessor ersetzt wird.

von ich nehme den Preis nicht an (Gast)


Lesenswert?

Walter T. schrieb:
> opt_trutenprugel_

Ach, da ist ein Untersrich am Ende!

Meine Güte, das sieht man doch nicht ;-)

von Walter T. (nicolas)


Lesenswert?

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.

von Klaus H. (klummel69)


Lesenswert?

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?

von Walter T. (nicolas)


Lesenswert?

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
    enum options_e a = 1;
2
    switch( a )
3
    {
4
        case opt_trutenprugel_: /* Fallthru */
5
        case opt_noelpenproettel_: /* Fallthru */
6
        case opt_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.)

von Klaus H. (klummel69)


Lesenswert?

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.

von Jörg W. (dl8dtl) (Moderator) Benutzerseite


Lesenswert?

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.

von BeBe (Gast)


Lesenswert?

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....

von Walter T. (nicolas)


Lesenswert?

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:
1
/* config_nucleo.h */
2
#define FAST_LOOP_ISR_TIMx    timer_isr_STM32F4XX_TIM6
3
#define FAST_LOOP_IRQ_HANDLER TIM6_DAC_IRQ_Handler
4
5
#define SLOW_LOOP_ISR_TIMx    timer_isr_STM32F4XX_TIM7
6
#define SLOW_LOOP_IRQ_HANDLER TIM7_IRQHandler
7
8
#define BUZZER_PWM            buzzer_STM32F4XX_TIM4_PB7
9
10
#define ENCODER_HARD_AUX0     encoder_STM32F4XX_TIM5_PA0_PA1
11
#define ENCODER_HARD_AUX1     encoder_STM32F4XX_TIM2_PB8_PB9
12
#define ENCODER_HARD_AUX2     encoder_STM32F4XX_TIM3_PB4_PB5
13
14
/* Grafik-LCD: SPI-Port (Hard-SPI mit DMA) */
15
//#define SPI_HARD_DMA spi_hard_none
16
#define SPI_HARD_DMA spi_hard_STM32F4XX_SPI1_PA5_PA6_PA7
17
//#define SPI_HARD_DMA spi_hard_STM32F4XX_SPI2_PB13_PB14_PB15
18
//#define SPI_HARD_DMA spi_hard_STM32F4XX_SPI3_PC10_PC11_PC12
19
20
/* ... */
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:
1
/* anderer_header.h */
2
enum timer_isr_e
3
{
4
    timer_isr_none,
5
    timer_isr_STM32F4XX_TIM6,
6
    timer_isr_STM32F4XX_TIM7,
7
    timer_isr_systick,
8
};
9
10
/** Hilfsfunktion: Timer auswaehlen */
11
static_inline TIM_TypeDef* timer_isr_which(enum timer_isr_e timer)
12
{
13
    switch( timer )
14
    {
15
        case timer_isr_none:
16
            return NULL;
17
        case timer_isr_STM32F4XX_TIM6:
18
            return TIM6;
19
        case timer_isr_STM32F4XX_TIM7:
20
            return TIM7;
21
        case timer_isr_systick:
22
            return NULL;
23
        default:
24
            return NULL;
25
    }
26
}
27
28
/** Timer-ISR-Startup; funktioniert auch fuer SysTick
29
 * @param[in] timer: enum Timer */
30
static_inline void isr_function_startup(enum timer_isr_e timer)
31
{
32
    switch( timer )
33
    {
34
        case timer_isr_none:
35
            assert( false );
36
            break;
37
38
        case timer_isr_STM32F4XX_TIM6: /* fallthru */
39
        case timer_isr_STM32F4XX_TIM7:
40
            {
41
                TIM_TypeDef* TIMx = timer_isr_which(timer);
42
                TIM_ClearITPendingBit(TIMx, TIM_IT_Update);
43
            }
44
            break;
45
46
        case timer_isr_systick:
47
            break;
48
    }
49
}
50
51
/** Timer-ISR-Ende
52
 * @param[in] timer: enum Timer */
53
static_inline void isr_function_closing(enum timer_isr_e timer)
54
{
55
    /* 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
void FAST_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
.align 4
3
.text
4
.type TIM6_DAC_IRQHandler, %function
5
.global TIM6_DAC_IRQHandler
6
TIM6_DAC_IRQHandler:
7
    @ IT pending Bit loeschen TIMx->SR = (uint16_t)~TIM_IT;
8
    ldr r0, =TIM6_BASE
9
    ldr r1, =~TIM_IT_Update
10
    str r1, [r0, #TIMx_SR]
11
12
    @ Nutzlast
13
    do s0, m3
14
    stu #FF
15
    bx lr
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.

von A. S. (Gast)


Lesenswert?

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.

von Klaus H. (klummel69)


Lesenswert?

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.

von Klaus H. (klummel69)


Lesenswert?

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?

von Rolf M. (rmagnus)


Lesenswert?

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.

von Klaus H. (klummel69)


Lesenswert?

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?

von Walter T. (nicolas)


Lesenswert?

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.

von Klaus H. (klummel69)


Lesenswert?

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.

von Rolf M. (rmagnus)


Lesenswert?

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.

von Walter T. (nicolas)


Lesenswert?

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.

von Klaus H. (klummel69)


Lesenswert?

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:
1
extern void function_does_not_exist(void);
2
#define CODE_SHOULD_NOT_BE_USED_ASSERT() function_does_not_exist()
3
4
static_inline TIM_TypeDef* timer_isr_which(enum timer_isr_e timer)
5
{
6
    switch( timer )
7
    {
8
        case timer_isr_none:
9
            return NULL;
10
        case timer_isr_STM32F4XX_TIM6:
11
            return TIM6;
12
        case timer_isr_STM32F4XX_TIM7:
13
            return TIM7;
14
        case timer_isr_systick:
15
            return NULL;
16
        default:
17
            CODE_SHOULD_NOT_BE_USED_ASSERT();
18
            return NULL;
19
    }
20
}

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.

von Vincent H. (vinci)


Lesenswert?

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.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.