mikrocontroller.net

Forum: Compiler & IDEs Befehlsschnittstelle erstellen


Autor: Gast (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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 ?

Autor: Rufus Τ. Firefly (rufus) (Moderator) Benutzerseite
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Gast (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Stefan (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Gast (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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 :)))

Autor: Zip (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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

Autor: Mister mit Kanister (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Andreas Schwarz (andreas) (Admin) Benutzerseite Flattr this
Datum:

Bewertung
0 lesenswert
nicht 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/40242....

Autor: Gast (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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
/* Struktur für einen Tabelleneintag in der Lookup-Tabelle ==> Befehl */
struct CommandEntry
{
  char    CB1;   /* Kommandobyte 1 */
  char    CB2;   /* Kommandobyte 1 */
  char    NumParam;   /* Anzahl der benötigten Parameter */
  char    (*function)( const char* ParamDat);  /* Funktionszeiger auf die   
                                                  zugehörige Funktion */
}

/* 
 die Tabelle (im FLASH) welche Befehlscodes, Parameterlänge und zugehörige 
 Funktion definiert
*/
struct CommandEntry CommandLookup[] = {
  { 'S', 'A', 0, SystemFunktion_A },    /* Beispiel (S)ystembefehl A */
  { 'I', 'B', 5, IOFunktion_B },        /* Beispiel (I)O-Befehl B  */
  /* 
    hier weitere Befehlsdefinitionen
  */
  { 0x00, 0x00, 0x00, NULL }            /* terminator Entry - do not remove */
};

/* 
  Die eigentlichen Funktionen übernehmen lediglich einen Zeiger auf den
  Begin der Parameterdaten und pop'en ( Öhhmm, jaja ;-) )sich diese vom
  übergebenen Eingabepuffer 
*/
char SystemFunktion_A( const char* ParamDat )
{
  /* Parameter pop'en */
  /* Funktionscode ausführen ..*/
  /* ..und Ergebniss zurückliefern */
}

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

Autor: Mister mit Kanister (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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.

Autor: Gast (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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..
/*
  die Lookup-Funktion des "Parsers", welche entsprechend den Kommandocodes, 
  den passenden Eintrag aus der Tabelle liefert 
  vermutlich häufig aufgerufenen Befehle stehen in der Tabelle am besten
  weiter oben 
*/
struct CommandEntry* Parser_Lookup( const sl_union16* CBytes )
{
  struct CommandEntry* Entry = (struct CommandEntry*)CommandLookup;

/* durch alle Tabelleneinträge gehen bis das Ende (Dummy) erreicht ist..*/
  while ( Entry->CB1 != 0x00 )
  {
    /* und nach den übergebenen Kommandobytes suchen */
    /* wenn gefunden, Funktion verlassen und einen Zeiger auf den
       entsprechenen Eintrag zurückliefern */
    if ((Entry->CB1 == CBytes->int8.hi) && (Entry->CB2 == CBytes->int8.lo))
      return Entry;         

    /* ab zum nächsten Tabelleneintrag */
    Entry++;
  }

  return NULL; /* nix gefunden, NULL-Pointer zurückgeben */
}

/* 
  Die eigentliche Parserfunktion (eigentlich ist es ja gar kein Parser
  mehr), welche auch die entsprechende Funktion ausführt
*/
SL_UINT8 Parser_Run( const sl_union16* CBytes )
{
  struct CommandEntry* Entry;
   
  /* finde den Eintrag für die übergebenen Kommando-Bytes */
  Entry = Parser_Lookup( CBytes );

  /* wenn gefunden.. */
  if ( NULL != Entry )
    /* rufe die zugehörige Funktion auf */
    return Entry->function( Command.named.CA ); 
   /* Command.named.CA ist ein Zeiger auf den Begin der empfangenen
      Parameterdaten */  

  /* der Befehl konnte via Lookup nicht gefunden werden, also FEHLER */
  return RESULT_INVALIDCOMMAND; 
}

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

Autor: Mister mit Kanister (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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?

Autor: Gast (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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..
/* ..legt das empfangene Byte auf der aktuellen Position im Puffer ab und
   erhöht den Pufferzeiger. */
#define IReciever_PushByte( data )(*IR_RecievePtr++ = data;)

was die Definition bzw. union betrifft..
#define SL_UINT8  unsigned char     /* unsigned 8 Bit */

typedef union         /* 8/16 bit IO conversion union */
{
  SL_UINT16 uint16;   /* unsigned 16-bit int */
  SL_SINT16 sint16;   /* signed 16-bit int */
  struct
  {                       
    SL_UINT8 lo;      /* LSB byte */
    SL_UINT8 hi;      /* MSB byte */
  }int8;
} sl_union16;

typedef union         /* 8/16/32 bit IO conversion union */
{
  SL_UINT32 uint32;      
  SL_SINT32 sint32;      
  struct
  {                       
    SL_UINT16 lo;     /* LSB word */
    SL_UINT16 hi;     /* MSB word */
  }int16;
  struct
  {
    SL_UINT8 b0;   /* LSB byte */
    SL_UINT8 b1;
    SL_UINT8 b2;
    SL_UINT8 b3;   /* MSB byte (exp) */
  }int8;
} 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

Autor: Patrick Dohmen (oldbug) (Moderator) Benutzerseite
Datum:

Bewertung
0 lesenswert
nicht 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

Autor: Gast (Gast)
Datum:

Bewertung
0 lesenswert
nicht 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

Autor: Peter Dannegger (peda)
Datum:

Bewertung
0 lesenswert
nicht 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

Antwort schreiben

Die Angabe einer E-Mail-Adresse ist freiwillig. Wenn Sie automatisch per E-Mail über Antworten auf Ihren Beitrag informiert werden möchten, melden Sie sich bitte an.

Wichtige Regeln - erst lesen, dann posten!

  • Groß- und Kleinschreibung verwenden
  • Längeren Sourcecode nicht im Text einfügen, sondern als Dateianhang

Formatierung (mehr Informationen...)

  • [c]C-Code[/c]
  • [avrasm]AVR-Assembler-Code[/avrasm]
  • [code]Code in anderen Sprachen, ASCII-Zeichnungen[/code]
  • [math]Formel in LaTeX-Syntax[/math]
  • [[Titel]] - Link zu Artikel
  • Verweis auf anderen Beitrag einfügen: Rechtsklick auf Beitragstitel,
    "Adresse kopieren", und in den Text einfügen




Bild automatisch verkleinern, falls nötig
Bitte das JPG-Format nur für Fotos und Scans verwenden!
Zeichnungen und Screenshots im PNG- oder
GIF-Format hochladen. Siehe Bildformate.
Hinweis: der ursprüngliche Beitrag ist mehr als 6 Monate alt.
Bitte hier nur auf die ursprüngliche Frage antworten,
für neue Fragen einen neuen Beitrag erstellen.

Mit dem Abschicken bestätigst du, die Nutzungsbedingungen anzuerkennen.