Forum: Mikrocontroller und Digitale Elektronik C: Alternative zu switch case "mit Lücken"


von Case-Switcher (Gast)


Lesenswert?

Hallo,

ich schreibe gerade einen "Übersetzer" zwischen zwei Netzen, der mir die 
aus dem einen Netz empfangene (binäre) Daten in etwas übersetzt, was ich 
in das andere weiter schicken kann (ASCII).
Das erste Byte eines jeden Datenblocks ist eine Zahl als "Kommando".
Dadurch ist automatisch die Länge des danach gesendeten Wertes, die Art 
des Wertes, die Funktion zur Umrechnung und die Funktion zum 
Zusammenbasteln des übersetzten Strings festgelegt.

Ich könnte also einfach eine Struktur definieren:
1
struct comm {
2
       uint8_t len;
3
       uint8_t *val;
4
       f_ptr calculate_val;
5
       f_ptr output_string;
6
};
7
struct comm command[10];
8
...
Dann könnte ich bei jedem empfangenen cmd_byte einfach prüfen, ob die 
Payload danach = command[cmd_byte].len lang ist, mit der Funktion 
command[cmd_byte].calculate_val() den Wert berechnen, in *val speichern 
und mit der Funktion output_string weitersenden.
Das wäre doch elegant.

Leider sind die cmd_bytes nicht aufeinander folgende Zahlen, sondern 08, 
21, 27, ...

Nun kann ich dem natürlich abhelfen mit
1
switch (cmd_byte) {
2
     case 07:
3
         cmd_idx=0;
4
         break;
5
     case 21:
6
         cmd_idx=1;
7
         break;
8
....
9
}
aber das ist irgendwie weit weniger elegant. Dann kann ich auch gleich 
die Funktionsaufrufe und Berechnungen noch mit in die case Blöcke 
packen, und es wird hässlich zu lesen.
Gibt es eine Alternative?

von Vincent H. (vinci)


Lesenswert?

Look-up table?

von M.K. B. (mkbit)


Lesenswert?

Eine Lookuptabelle in der man das Kommando sucht und der Index ist dann 
der cmd_idx.

von abc.def (Gast)


Lesenswert?

Case-Switcher schrieb:


void foo(void) {
> switch (cmd_byte) {
>      case 07:
>          fkt_07();
>          break;
>      case 21:
>          fkt_21();
>          break;
> }
}//foo

Der Compiler macht daraus in Assembler eine Leiter von Vergleichen und 
Sprüngen JMP (nicht CALL), gleich zur fkt_07 (undsoweiter). fkt_07 endet 
mit einem RET, was gleich ans Ende der Funktion }//foo führt.
Warum sollte das nicht übersichtlich sein? (wenn man aussagefähige Namen 
vergibt)
Erst in einer Tabelle nach dem idx suchen halte ich für 
unübersichtlicher, zumal die Tabelle von 08 bis 20 "nichts" enthält.

von A. S. (Gast)


Lesenswert?

M.K. B. schrieb:
> Eine Lookuptabelle in der man das Kommando sucht und der Index ist dann
> der cmd_idx.

Aber nur, wenn man an jedem Byte spart. Sonst macht man halt die 
komplette Tabelle und sucht nicht.

von Wolfgang (Gast)


Lesenswert?

abc.def schrieb:
> Erst in einer Tabelle nach dem idx suchen halte ich für
> unübersichtlicher, zumal die Tabelle von 08 bis 20 "nichts" enthält.

Da sucht man auch nicht, sondern adressiert mit dem Kommandobyte.

Es kommt drauf an, ob Effizienz, Codegröße, Speicherbedarf, 
Schreibaufwand beim Programmieren, Lesbarkeit des Quellcodes oder 
sonstwas das Kriterium für Eleganz ist.

von Case-Switcher (Gast)


Lesenswert?

M.K. B. schrieb:
> Eine Lookuptabelle in der man das Kommando sucht und der Index ist dann
> der cmd_idx.
Das heißt, die struct wird zu:
1
struct comm {
2
       uint8_t cmd;
3
       uint8_t len;
4
       uint8_t *val;
5
       f_ptr calculate_val;
6
       f_ptr output_string;
7
};
8
struct comm command[10];
9
10
// und dann das empfangene cmd_byte zuordnen:
11
12
for (i=0; i<max; i++) {
13
       if (command[i].cmd == cmd_byte)
14
           cmd_idx = i;
15
}
OK. Das sieht etwas hübscher aus.

Macht das einen Unterschied in der Ausführungsgeschwindigkeit oder im 
RAM-Verbrauch?
Wo liegen eigentlich die Konstanten der switch... case Anweisung? Im 
Flash oder im RAM? Ich nehme an, die kommen nur für die Ausführung des 
Codeblocks der switch... case Anweisung ins RAM, oder?
Die struct wäre im RAM. Dann würde 10*sizeof(cmd) zusätzlich verbraucht.

Wolfgang schrieb:
> Es kommt drauf an, ob Effizienz, Codegröße, Speicherbedarf,
> Schreibaufwand beim Programmieren, Lesbarkeit des Quellcodes oder
> sonstwas das Kriterium für Eleganz ist.
In Netz 1 kursieren etwa 100 verschiedene Befehle, von denen mich aber 
nur 10 bis 15 interessieren. Bei 15 ist eine komplette Tabelle mit 255 
Werten, so dass ich cmd_byte als Index nehmen kann, Verschwendung auf 
einem kleinen µC mit 1k RAM und 16k Flash, finde ich.

Ausführungsgeschwindigkeit ist sicher auch ein Kriterium, aber nicht 
sooo kritisch. Ich glaube nicht, dass ich eingangsseitig so schnell 
einen Überlauf bekomme, weil ich die Daten nicht übersetzt bekomme.

Aber Schreibaufwand/Lesbarkeit wäre mir wichtig. Wenn's unübersichtlich 
wird oder wenn zusammengehörende Daten an verschiedenen Stellen 
definiert werden, bin ich selbst derjenige, der das pflegen muss.

von Fipo (Gast)


Lesenswert?

Mach doch ein bindump und kuck nach?

von (prx) A. K. (prx)


Lesenswert?

Welches Schema ein Compiler bei der der Umsetzung von switch Statements 
abhängig von den konkreten case Werte implementiert, ist nicht trivial 
vorherzusagen, und kann sich bei einer Änderung der Werte oder der 
Compilerversion jederzeit ändern.

von S. R. (svenska)


Lesenswert?

Warum nicht einfach eine Funktion:
1
int8_t get_payload_len(uint8_t cmd) {
2
  switch(cmd) {
3
  case 0x07: return(4);
4
  case 0x28: return(28);
5
  /* usw */
6
  default: return(-1);
7
  }
8
}

Sowas finde ich zumindest recht gut lesbar und es lässt sich einfach 
erweitern. Setzt allerdings voraus, dass die Payload-Länge nicht allzu 
umständlich zu berechnen ist.

Der Compiler wird das schon halbwegs effizient umsetzen, abhängig von 
den Parametern (wenn du auf Größe optimierst, eher nicht mit einer 
vollen 256 Byte-Tabelle).

Oder du implementierst die gesamte Verarbeitung direkt so:
1
void handle_cmd(uint8_t *buf, uint8_t len) {
2
  switch(buf[0]) {
3
  case 0x07: handle_first_cmd(buf, len); break;
4
  case 0x28: handle_second_cmd(buf, len); break;
5
  default: panic("unknown cmd %d", buf[0]); break;
6
  }
7
}

Das lässt sich ebenfalls wunderbar erweitern und ergibt eine 
übersichtliche Struktur im Code. Ob das sinnvoll ist, hängt vom 
Befehlssatz ab, im schlimmsten Fall kriegst du viel Boilerplate und 
Redundanz rein.

Wenn du den Code aber aus einer anderen Beschreibung (z.B. Excel, XML 
oder sowas) generieren lässt, ist das aus meiner Sicht die Struktur der 
Wahl.

von Axel S. (a-za-z0-9)


Lesenswert?

Case-Switcher schrieb:

> ich schreibe gerade einen "Übersetzer" zwischen zwei Netzen, der mir die
> aus dem einen Netz empfangene (binäre) Daten in etwas übersetzt, was ich
> in das andere weiter schicken kann (ASCII).
> Das erste Byte eines jeden Datenblocks ist eine Zahl als "Kommando".
> Dadurch ist automatisch die Länge des danach gesendeten Wertes, die Art
> des Wertes, die Funktion zur Umrechnung und die Funktion zum
> Zusammenbasteln des übersetzten Strings festgelegt.
>
> Ich könnte also einfach eine Struktur definieren:
>
1
struct comm {
2
>        uint8_t len;
3
>        uint8_t *val;
4
>        f_ptr calculate_val;
5
>        f_ptr output_string;
6
> };
7
>
...
> Leider sind die cmd_bytes nicht aufeinander folgende Zahlen, sondern 08,
> 21, 27, ...
>
> Nun kann ich dem natürlich abhelfen mit
>
1
switch (cmd_byte) {
2
>      case 07:
3
>          cmd_idx=0;
4
>          break;
5
>      case 21:
6
>          cmd_idx=1;
7
>          break;
8
> ....
9
> }
10
>

> aber das ist irgendwie weit weniger elegant.

Finde ich nicht. Im Prinzip hast du die zwei Möglichkeiten der Lösung 
erkannt. Du kannst die Logik entweder als Programmcode oder als 
Datenstruktur aufschreiben.

Programmcode läuft auf ein switch() Statement hinaus. Und ja, dann würde 
man die notwendigen Aktionen direkt in den case: Blöcken unterbringen. 
Wobei man identischen Code natürlich trotzdem in Funktionen auslagern 
und von dort aus aufrufen kann.

Die Alternative besteht aus einem Array (oder einer Liste oder einer C++ 
std::map) von structs, die notwendige Daten und Funktionszeiger 
enthalten. In dem Fall ist der Code selber eher trivial - anhand des 
ersten Bytes deiner Nachricht die richtige struct finden und dann die 
Funktion per Zeiger aufrufen. Wenn deine Indizes nichtkontinuierlich 
sind, ist ein assoziatives Array (z.B. std::map) der kanonische 
Container. Eine Liste müßtest du durchsuchen. Ein Array mit dem 
Kommandobyte als Index würde Platz verschwenden.

Von der Nachvollziehbarkeit halte ich die switch() Variante für schöner. 
Denn im zweiten Fall müßte man immer erstmal die Datenstruktur 
konsultieren, um den Namen der referenzierten Action-Funktion zu 
erfahren. Das kann man zwar vereinfachen, z.B. indem man sprechende 
Namen für die Funktionen verwendet. Aber irgendwann wird sich irgendwer 
nicht mehr daran halten and dann ist Essig.

Bei der Variante mit switch() kann man diesbezüglich nichts falsch 
machen. Man kann da sogar noch eine Optimierung anbringen, indem man die 
häufigsten Fälle an den Anfang zieht.

von Case-Switcher (Gast)


Lesenswert?

Erst einmal vielen Dank an alle!

Die "eierlegende Wollmilchsau" gibt's also mal wieder nicht.
Ich kann entweder mit dem array of struct arbeiten und muss dann das 
empfangene cmd_byte einem Index cmd_idx zuordnen, entweder durch
1
for (i=0; i<max; i++) {
2
       if (command[i].cmd == cmd_byte)
3
           cmd_idx = i;
4
}
oder durch
1
switch (cmd_byte) {
2
     case 07:
3
         cmd_idx=0;
4
         break;
5
     case 21:
6
         cmd_idx=1;
7
         break;
8
....
9
}
Oder ich packe wie S. R. (svenska) gleich (inline) Wrapper-Funktionen in 
das switch() statement.

Axel S. schrieb:
> Eine Liste müßtest du durchsuchen. Ein Array mit dem
> Kommandobyte als Index würde Platz verschwenden.
Ein array of struct wie oben würde hier sogar das RAM sprengen (1kb).
Also bleibt mit einer for loop zu suchen oder eben switch().

Ich finde die struct eigentlich ganz praktisch, da man da alle 
Informationen an einem Ort im Überblick hat.
Schade, dass man in den Array eben nicht direkt hinein springen kann.
Für ein assoziatives Array müsste ich plain C verlassen...

von Ralf G. (ralg)


Lesenswert?

Falls es aufeinanderfolgende Codenummern gibt und dann der 'cmd_idx' 
nur hochgezählt wird, könnte man noch etwas vereinfachen:
1
switch (cmd_byte)
2
{
3
     case 07:
4
         cmd_idx = 0;
5
         break;
6
     case 21: case 22: case 23:
7
         cmd_idx = cmd_byte - 20;
8
         break;
9
....
10
}

von A. S. (Gast)


Lesenswert?

Case-Switcher schrieb:
> oder durch
>
1
> switch (cmd_byte) {
2
>      case 07:
3
>          cmd_idx=0;
4
>          break;
5
>      case 21:
6
>          cmd_idx=1;
7
>          break;
8
> ....
9
> }
10
>
> Oder ich packe wie S. R. (svenska) gleich (inline) Wrapper-Funktionen in
> das switch() statement

Die Darstellung ist aber einfach Müll, egal wer da irgendwelche 
Kodierungsrichtlienen erstellt. Sowas gehört so, und nicht anders:
1
switch (cmd_byte) {
2
     case 0x07: cmd_idx=0;  break;
3
     case 0x21: cmd_idx=1;  break;
4
....
5
}

Dann ist es so klar les- und änderbar wie jede Tabelle

von Axel S. (a-za-z0-9)


Lesenswert?

Case-Switcher schrieb:
> Ich finde die struct eigentlich ganz praktisch, da man da alle
> Informationen an einem Ort im Überblick hat.

Außer den Code selber. Denn von den Funktionen sieht man in der struct 
nur den Namen. Eben deswegen finde ich die Variante mit switch() 
besser. Der Code steht in unmittelbarer Nähe des Tokens (der Wert nach 
dem case: Schlüsselwort).

Wenn man die Funktionsweise verstehen will, ist es trivial im Editor 
nach dem Token zu suchen und dann an praktisch der gleichen Stelle den 
zugehörigen Code zu finden.

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.