Forum: Compiler & IDEs Befehlsschnittstelle erstellen


von Gast (Gast)


Lesenswert?

Hallo zusammen.

Ich glaube, heute steht bei mir was riesiges auf der "Leitung". 
Eventuell könnt ihr mir ja mit einem Lösungsansatz helfen.

Ich habe folgendes Problem:
Ich habe eine Sammlung von Funktionen, welche per Interface (z.B. UART) 
vom Benutzer ausgeführt werden sollen. Die Funktionen haben dabei 
unterschiedliche Anzahl an Parametern.

Mein erster Ansatz war nun die Funktionen via 2 Kommando-Bytes zu 
"selektieren" und nach diesen die Parameter mitzugeben.
Problem ist nun, aufgrund der variablen Parameterinhalte kann ich die 
Kommandosequenz nicht mit einem fixen Byteinhalt "terminieren" da dieses 
in jeder der Parameter ungewollt enthalten sein könnte.
Also war der näxhste Gedanke, ein Byte zu senden welches die 
Kommandosequenz einleitet und dann das Kommando+Parameter zu senden. 
Hier stellt sich nun das Problem, dass die Schnittstelle Wissen über die 
Anzahl der Parameter für die Kommandos haben müsste um die komplette 
Kommandosequenz entgegenzunehmen.
Ich komme nach längerem Überlegen nicht wirklich auf eine sinnvolle 
Variante.

Ich hoffe Ihr konntet wenigstens meinen Ausführungen soweit folgen und 
seit jetzt nicht gerade so verpeilt wie ich.

Wie würdet ihr diese Aufgabe angehen ?

von Rufus Τ. F. (rufus) Benutzerseite


Lesenswert?

Komplettes Kommando mitsamt aller Parameter als komplette Zeile 
übertragen (also mit CR abschließen).

Erst nach vollständigem Empfang der Zeile wird Dein Parser darauf 
angesetzt und zerpflückt sich die Zeile nach Gutdünken.

Setzt natürlich voraus, daß ausreichend Speicher vorhanden ist, um eine 
"Zeile" einzulesen und zu speichern.

von Gast (Gast)


Lesenswert?

Ja, das würde für die UART sicher funktionieren. Da das System aber auch 
andere Schnittstellen bedienen können soll bräuchte ich leider etwas 
unabhängigeres.

von Stefan (Gast)


Lesenswert?

Du kannst ja der "Schnittstelle" mitteilen, wieviele Parameter (Bytes) 
demnächst im Telegramm kommen z.B. als Vorspann vor den 
Funktionsauswahlbytes oder nach diesen und vor den Parameterbytes.

Oder die Schnittstelle kann das anhand der Funktionsauswahlbytes und 
einer entsprechenden Information im Quellcode selbstständig feststellen, 
wieviele Parameter kommen müssen.

von Gast (Gast)


Lesenswert?

Den Gedanke die Sequenzlänge als zusäzliches Byte mitzugeben hatte ich 
auch schon. Ist ja ansich auch nichts gegen einzuwenden, nur für den 
Benutzer der Schnittstelle zusätzlicher Aufwand und potentielle 
Fehlerquelle.

Im Moment bin ich gerade beim Letzten Vorschlag. Die Schnittstelle holt 
sich die noch benötigte Anzahl an Paramatern, entsprechend den 
empfagenen Kommandobytes, via Lookup vom Parser. Aber das bedeutet 
zusätzlichen Code- oder Datenoverhead für die Bereitstellung der 
Informationen.

..aber im Moment seh ich auch nicht wirklich einen andere Lösung.
Also werd ich mal versuchen den Loopup Vorgang so effektiv wie möglich 
zu gestallten

danke für Initiative und Eure Vorschläge :)))

von Zip (Gast)


Lesenswert?

Eine Statusmaschine macht das alles. Im header steht ein code fuer den 
befehl, und oder die Laenge. Von da an wird runtergzaehlt und dann das 
Command auseinandergenommen.

Z

von Mister mit Kanister (Gast)


Lesenswert?

Ich hab das ähnlich gemacht:

Die Kommandos werden mit CR, LF oder CR+LF abgeschlossen.
Eine ISR holt die Zeichen einzeln ab und speichert sie in ein Array.
In der ISR prüfe ich ab, ob CR oder LF gekommen ist. Wenn ja, also 
Kommando vollständig übertragen, wird ein Flag gesetzt und der komplette 
Befehl in ein anderes Array kopiert, das dann ausgewertet werden kann.

In der main() gibt es dann eine Abfrage auf das "Kommando Empfangen 
Flag". Erst dann zerpflückt die main das Kommando und man kann über 
switch... if usw. die Befehle auswerten.

Das funktioniert soweit ganz gut mit wenig Fehlern.

von Andreas S. (andreas) (Admin) Benutzerseite


Lesenswert?

Gast wrote:
> Problem ist nun, aufgrund der variablen Parameterinhalte kann ich die
> Kommandosequenz nicht mit einem fixen Byteinhalt "terminieren" da dieses
> in jeder der Parameter ungewollt enthalten sein könnte.

Die Lösung nach ASCII-Standard: den Anfang des Pakets markiert man mit 
DLE STX, das Ende mit DLE ETX; wenn irgendwo im Datenteil DLE auftaucht 
wird es durch DLE DLE ersetzt. Der Empfänger muss nichts anderes machen 
als die Start- und Endemarkierungen zu erkennen und DLE DLE im Datenteil 
durch DLE zu ersetzen. DLE, STX, ETX sind ASCII-Steuerzeichen; du kannst 
stattdessen natürlich auch beliebige Bytes verwenden. Siehe z.B. 
http://www.tecchannel.de/netzwerk/grundlagen/402421/index5.html.

von Gast (Gast)


Lesenswert?

erstmal danke an ALLE für die vielen, tollen Vorschläge.

@Andreas:
toll, an diese Variante hatte ich noch gar nicht gedacht.
Sie hat leider einen grossen Nachteil. Der (end)Benutzer der 
Schnittstelle(n) müsste Mechanismen zur Duplizierung der 
Markierungscodes innerhalb der Daten bereitstellen.
D.h. würde mein Markierungscode z.B. $AA sein und ein Parameter(byte) 
zufällig den gleichen Wert haben, müsste der Benutzer diese in $AA $AA 
umwandeln. Nennt mich nörgelig, aber das verkompliziert die 
Schnittstelle auf Benutzerseite ganz erheblich :-)

Falls es jemanden interessiert, möchte ich Euch auch meine derzeitige 
Lösung nicht vorenthalten.
Da ich ohnehin einen Parser implementieren musste, ist der Schritt zum 
Abfragen der nötigen Parameterlänge für einen Befehl nicht mehr soo 
gross.

PSEUDOCODE
1
/* Struktur für einen Tabelleneintag in der Lookup-Tabelle ==> Befehl */
2
struct CommandEntry
3
{
4
  char    CB1;   /* Kommandobyte 1 */
5
  char    CB2;   /* Kommandobyte 1 */
6
  char    NumParam;   /* Anzahl der benötigten Parameter */
7
  char    (*function)( const char* ParamDat);  /* Funktionszeiger auf die   
8
                                                  zugehörige Funktion */
9
}
10
11
/* 
12
 die Tabelle (im FLASH) welche Befehlscodes, Parameterlänge und zugehörige 
13
 Funktion definiert
14
*/
15
struct CommandEntry CommandLookup[] = {
16
  { 'S', 'A', 0, SystemFunktion_A },    /* Beispiel (S)ystembefehl A */
17
  { 'I', 'B', 5, IOFunktion_B },        /* Beispiel (I)O-Befehl B  */
18
  /* 
19
    hier weitere Befehlsdefinitionen
20
  */
21
  { 0x00, 0x00, 0x00, NULL }            /* terminator Entry - do not remove */
22
};
23
24
/* 
25
  Die eigentlichen Funktionen übernehmen lediglich einen Zeiger auf den
26
  Begin der Parameterdaten und pop'en ( Öhhmm, jaja ;-) )sich diese vom
27
  übergebenen Eingabepuffer 
28
*/
29
char SystemFunktion_A( const char* ParamDat )
30
{
31
  /* Parameter pop'en */
32
  /* Funktionscode ausführen ..*/
33
  /* ..und Ergebniss zurückliefern */
34
}

dazu gibt es noch eine simple Parser-Funktion welche anhand übergebener 
Kommandobytes einen Eintrag vom Typ "CommandEntry" aus der Tabelle 
zurückliefert.
Sowohl die Eingabeschnittstelle als auch die eigentliche Parserfunktion 
können diese aufrufen um die benötigten Informationen des jeweiligen 
Befehl zu erhalten.

für Verbesserungen, Anmerkungen und Kritik bin ich jederzeit offen ..

PS: super Forum, man bekommt Anregungen und Hilfe - find ich toll

von Mister mit Kanister (Gast)


Lesenswert?

Hi,
kannst Du mal den REst vom Code posten? Würde mich interessieren wie Du 
die Abfrage, um welchen Befehl es sich handelt, den Uart Empfang usw. 
realisiert hast.

von Gast (Gast)


Lesenswert?

Hallo

..hatte bisher wenig zeit :(

Einen ReadyToRun-Code kann ich leider nicht liefern.
Es würde Verständniss und Übersicht nicht dienen, selbst wenn ich meinen 
kompletten Code posten würde.
Daher lediglich vom Errorchecking bereinigte und weitestgehend vom 
Kontext gelösete (Pseudo)Funktionen ..sorry

die "Parser"-Funktionen sind eigentlich sehr einfach..
1
/*
2
  die Lookup-Funktion des "Parsers", welche entsprechend den Kommandocodes, 
3
  den passenden Eintrag aus der Tabelle liefert 
4
  vermutlich häufig aufgerufenen Befehle stehen in der Tabelle am besten
5
  weiter oben 
6
*/
7
struct CommandEntry* Parser_Lookup( const sl_union16* CBytes )
8
{
9
  struct CommandEntry* Entry = (struct CommandEntry*)CommandLookup;
10
11
/* durch alle Tabelleneinträge gehen bis das Ende (Dummy) erreicht ist..*/
12
  while ( Entry->CB1 != 0x00 )
13
  {
14
    /* und nach den übergebenen Kommandobytes suchen */
15
    /* wenn gefunden, Funktion verlassen und einen Zeiger auf den
16
       entsprechenen Eintrag zurückliefern */
17
    if ((Entry->CB1 == CBytes->int8.hi) && (Entry->CB2 == CBytes->int8.lo))
18
      return Entry;         
19
20
    /* ab zum nächsten Tabelleneintrag */
21
    Entry++;
22
  }
23
24
  return NULL; /* nix gefunden, NULL-Pointer zurückgeben */
25
}
26
27
/* 
28
  Die eigentliche Parserfunktion (eigentlich ist es ja gar kein Parser
29
  mehr), welche auch die entsprechende Funktion ausführt
30
*/
31
SL_UINT8 Parser_Run( const sl_union16* CBytes )
32
{
33
  struct CommandEntry* Entry;
34
   
35
  /* finde den Eintrag für die übergebenen Kommando-Bytes */
36
  Entry = Parser_Lookup( CBytes );
37
38
  /* wenn gefunden.. */
39
  if ( NULL != Entry )
40
    /* rufe die zugehörige Funktion auf */
41
    return Entry->function( Command.named.CA ); 
42
   /* Command.named.CA ist ein Zeiger auf den Begin der empfangenen
43
      Parameterdaten */  
44
45
  /* der Befehl konnte via Lookup nicht gefunden werden, also FEHLER */
46
  return RESULT_INVALIDCOMMAND; 
47
}

Die Schnittstellenfunktionen sind so ausgelegt, dass sie einfach ein 
empfangenes Byte einer beliebigen Schnittstelle entgegen nehmen und im 
Input-Puffer ablegen (schnelle kurze Funktion, da der typische call aus 
einer ISR erfolgt).
Eine zweite, zyklisch aufgerufene, Schnittstellenfunktion verarbeitet 
dann die neu angekommenen Daten und reicht kommplett empfangene 
Befehlssequenzen einfach an eine Ausführungskette weiter. Man könnte 
auch direkt Parser_Run() aufrufen.
Der Empfangspuffer besteht dabei aus einem Ringpuffer auf welchen zwei 
Zeiger verweisen. Ein "Empfangszeiger" zeigt auf die aktuelle Position 
des letzen empfangenen Bytes und ein "Verarbeitungszeiger" auf die 
aktuelle
Position der Verarbeitung durch die zyklische Funktion.

Also ganz simpel :-)

Ist sicher auch nicht für jeden Anwendungsfall das System der Wahl, aber 
es erfüllt ganz gut meine konkreten Anforderungen mit möglichst wenig 
Aufwand.

LG

von Mister mit Kanister (Gast)


Lesenswert?

Hi Gast, danke!

Was mich interessieren würde, wie sieht Deine ISR aus? Ich hab nämlich 
folgendes Problem:
Mein Code ist ähnlich wie der von Dir aufgebaut. Meine ISR holt ein 
einzelnes Zeichen ab, speichert es im Puffer. Kommt ein "CR" 
interpretiert die ISR das als Befehlsende, der Puffer wird kopiert und 
anschließend geleert damit er für den weiteren Empfang wieder bereit 
ist. Jetzt ist das Problem, dass manchmal Zeichen nicht übertragen 
werden, also verlorengehen. Kann es sein, dass meine ISR zu lange ist? 
Somit ein paar Zeichen verschluckt werden?
Wie hast Du das gemacht?

was ist übrigens SL_UINT8? und sl_union16?

von Gast (Gast)


Lesenswert?

Hallo

@Mister: Ohne deinen Code zu kennen, vermute ich jetzt mal das Dein ISR 
Code zu lang braucht. Das führt bei schnellen Transferraten zu den von 
Dir geschilderten Problemen.

Das ist auch der Grund, warum ich die eigentliche Auswertung der Daten 
in einer zweiten, zyklisch aufgerufenen, Funktion realisiere. Das setzt 
allerdings einen gewissen Puffer zum Halten der Daten voraus.
Meine Funktion welche die Daten von der Schnittstelle entgegennimmt ist 
daher sehr kurz.
Um genau zu sein, ist es sogar nur ein Makro um den Call-Overhead zu 
sparen..
1
/* ..legt das empfangene Byte auf der aktuellen Position im Puffer ab und
2
   erhöht den Pufferzeiger. */
3
#define IReciever_PushByte( data )(*IR_RecievePtr++ = data;)

was die Definition bzw. union betrifft..
1
#define SL_UINT8  unsigned char     /* unsigned 8 Bit */
2
3
typedef union         /* 8/16 bit IO conversion union */
4
{
5
  SL_UINT16 uint16;   /* unsigned 16-bit int */
6
  SL_SINT16 sint16;   /* signed 16-bit int */
7
  struct
8
  {                       
9
    SL_UINT8 lo;      /* LSB byte */
10
    SL_UINT8 hi;      /* MSB byte */
11
  }int8;
12
} sl_union16;
13
14
typedef union         /* 8/16/32 bit IO conversion union */
15
{
16
  SL_UINT32 uint32;      
17
  SL_SINT32 sint32;      
18
  struct
19
  {                       
20
    SL_UINT16 lo;     /* LSB word */
21
    SL_UINT16 hi;     /* MSB word */
22
  }int16;
23
  struct
24
  {
25
    SL_UINT8 b0;   /* LSB byte */
26
    SL_UINT8 b1;
27
    SL_UINT8 b2;
28
    SL_UINT8 b3;   /* MSB byte (exp) */
29
  }int8;
30
} sl_union32;

Die Redefinitionen nutzt das System um auf verschiedenen Zielplattformen 
den gleichen Code, unter gleichen Voraussetzungen, kompilieren zu 
können.
Dazu werden einfach nur die Datentypen auf die des Zielcompilers 
redefiniert.
Die Union's sparen Bitschieberei beim Zugriff auf z.B. Lo- und Hibytes.

hoffe geholfen zu haben :-)

LG

von Patrick D. (oldbug) Benutzerseite


Lesenswert?

Gast wrote:

[snip]

> Die Redefinitionen nutzt das System um auf verschiedenen Zielplattformen
> den gleichen Code, unter gleichen Voraussetzungen, kompilieren zu
> können.
> Dazu werden einfach nur die Datentypen auf die des Zielcompilers
> redefiniert.

Da fragt man sich doch, warum es immer wieder Compilerbauer und/oder 
Bibliothekenersteller gibt, die diese dämlichen Headerfiles wie 
'stdint.h' und 'inttypes.h' Portieren. Völlig überflüssig, denn man 
kann's ja auch selber machen.

SCNR

von Gast (Gast)


Lesenswert?

Hi Patrick :)

Generell hast Du da durchaus Recht. Ich wollte damit auch nur auf Nummer 
sicher gehen.
Mal eben 6 Datentypen zu portieren ist mir lieber als mich auf 
Richtigkeit und Vorhandensein von 'stdint.h' zu verlassen.
Ich kenne Compiler, bei welchen durchaus keine 'stdint.h' oder 
entsprechende Definitionen zu finden sind.
Zugegebenermaßen ist dies hoffentlich eher die Ausmahme.

LG

von Peter D. (peda)


Lesenswert?

Ich mache das auch so wie Andreas, daß ich eine bestimmte Kombination 
als Kommandoende benutze.

Das hat den Grund, daß ja Daten grundsätzlich gestört sein können 
(fehlende Bytes, falsche Bytes).

Wenn ich dann schon zwischendurch parse, kann ja sonstwas passieren.


Außerdem werden die einzelnen Routinen schön übersichtlich:

Die Empfangsroutine sammelt nur die Bytes ein, prüft die maximale Länge 
und das Endekennzeichen.

Eventuell prüft jetzt noch ne CRC-Routine, ob das Kommando fehlerfrei 
ist.

Dann erst kann die Parse-Routine loslegen, während die Empfangsroutine 
schon das nächste Kommando einlesen kann.


Wichtig ist für mich, daß bei einer Störung nicht alles austillt, 
sondern schnell wieder die Synchronität hergestellt wird.
D.h. das fehlerhafte Kommando wird verworfen und das nächste aber wieder 
richtig ausgeführt.
Wenn es aber kein eindeutiges Endekennzeichen gibt, weiß man ja nicht, 
wo nun das nächste Kommando beginnt.


Es gibt leider viele nicht durchdachte Protokolle, die, einmal 
ausgerastet, nur durch den Netzhauptschalter wieder zum Leben erweckt 
werden können.


Peter

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.