Befehlsstring unterteilen (tokenize)


  • Im vorangegangenen Teil ging es lediglich um die Aufteilung des ankommenden Befehlsstrings und wenn man sich den Code angesehen hat, dann war vermutlich nicht auf den ersten Blick ersichtlich, weshalb man das Ergebnis des Vorganges in einer Struktur zwischenspeichert, die dann auch noch etwas umständlich daher kommt.
    Nach dem Tokenisieren muss nun Fleisch an den Knochen kommen und man könnte versucht sein, jetzt die String in einem if() else Block abzufragen oder, wenn es viele unterschiedliche Befehle sind, dies in einem grossen switch() nach Funktionen aufzuteilen.
    Mit einem etwas grösseren Code wäre man aber auch in der Lage, viele Befehle zu implementieren ohne dabei jedes mal eine If() oder eine case zu implementieren.
    Mit diesem Ziel versuchte ich den Vorgang, tokenisieren, Befehl evaluieren und den Befehl ausführen mittels einer Tabelle zu „normalisieren“. Dabei lege ich die Tabelle im Flash der CPU ab, da jegliche Daten die sich im Code Segment befinden, bei den kleinen Atmelprozessoren zuerst vom Flash ins Ram gebracht werden müssen, was unnötig RAM belegt, aber etwas mehr CPU Zeit beansprucht.


    Bei meinem kleinen Beipielprogramm sind fürs Erste lediglich zwei Befehle leer implementiert, um die Lösung zu demonstrieren. Anstelle des Aufrufs der cmd_tokenizeCmdString() Funktion im main(), wird nun die Funktion cmd_Exec(strCommand) aufgerufen. Als einzigen Parameter bekommt die Funktion das Befehlsstring. Diese Funktion kopiert dann das strCommand string in einen temporären Buffer, der dynamisch vom Stackmemory zur Verfügung gestellt wird. Die Lösung mit __builtin_alloca() habe ich auch erst kürzlich entdeckt und wollte sie hier mal ausprobieren. Anschliessend wird der String wie bereits gezeigt mit der Funktion cmd_tokenizeCmdString() zerlegt. Im Parameter-Array der Struktur StructCmdPara steht nun auf dem ersten Eintrag das zu behandelnde Befehlswort (z.B: get). Mittels einer Helperfunktion tool_lookUpName_P () sucht das Programm in der Tabelle CmdTable, die sich ausschliesslich im Flash befindet, nach dem Befehl abgesucht. Die Funktion liefert in der Para Struktur im Feld CmdIdx dann den entsprechenden Index ab, an welcher Stelle der Befehl gefunden wurde. Wurde der Befehl nicht gefunden, dann liefert die Funktion den Indexwert 255 zurück.
    Wurde ein Eintrag gefunden, wird ein Strukturzeiger in die Tabelle erzeugt

    const __flash  StructCmdTable *pCmdEntry = &CmdTable[Para.CmdIdx];

    der Zeiger nochmals geprüft und dann die zugehörende Funktion via eines Funktionszeiger in der Tabelle aufgerufen. Im Falle des Befehls «get» setzt das Programm die Fortführung direkt auf


    int fp_get(StructCmdPara * pPara )

    {

       return 0;

    }

    fort. Die Parameter Struktur vom Tokenizer wird als Parameter an die Funktion übergeben. Damit kann die Funktion nun erledigen was sie will und die Parameter entnehmen, gegebenenfalls konvertieren und ausführen was sie auszuführen hat.

    Um eine neue Funktion (help) zu implementieren muss man:


    Den Stringnamen im Flash hinterlegen:
    const __flash char cmdName_help[] = "help";


    Eine Funktion implementieren:

    int fp_help(StructCmdPara * pPara )

    {

       return 0; // hier tut man das was der Befehl help tun soll

    }


    Und die Beiden in der Tabelle CmdTable eintragen. Damit ist der Overhead für einen neuen Befehl sizeof (cmdName_help) + sizeof(StructCmdTable) + der Code der Funktion.


    Das wars von mir, für dieses Jahr und ich wünsche Allen Lesern einen Guten Rutsch ins 2022.:)



    Pius

    Dateien

    • CmdParse.c

      (8,09 kB, 1 Mal heruntergeladen, zuletzt: )
    • CmdParse.h

      (3,63 kB, 2 Mal heruntergeladen, zuletzt: )
    • main.c

      (431 Byte, 1 Mal heruntergeladen, zuletzt: )
  • Manchmal möchte man von einem Gerät aus (z.B: Terminal) einen Befehl an seine HW Entwicklung schicken. Dieser Befehlsstring muss dann, bevor er darauf in irgendeiner Weise interpretiert werden kann, in einzelne Teilstrings (Token) zerlegt werden. Ein Beispiel, wie man sowas realisieren kann, habe ich in der folgend beschriebenen Funktion erstellt.


    const char strCommand[] = "command Param0 Param1 \n Param2";
    const char strCommand1[] = "set Name \" Muster Peter\" ";
    (ein Parameter darf auch Leerzeichen, wie
    strCommand1 verdeutlicht)


    Mein Testprogramm (in main()) sieht folgendermassen aus:


    1 char workBuffer[ sizeof(strCommand) ];

    2 StructCmdPara  ParaList;

    3 int Error;

    4 strncpy(workBuffer, strCommand, sizeof(strCommand)-1);


    5 Error = cmd_tokenizeCmdString(&ParaList, workBuffer);


    Zeile 1 reserviert einen temporären Buffer auf dem Stack, da meine Funktion den Inhalt des Strings an den entsprechenden Stellen mit einem \0 versieht.

    Zeile 2 legt eine StructCmdPara Struktur temporär auf den Stack an. Diese Struktur beinhaltet das Resultat des Vorganges.

    In Zeile 4 wird der aufzuteilende Befehlsstring in den tmp. Buffer kopiert. Daher darf strCommand const deklariert sein. Der Grund des Kopierens ist einfach. Wird z.B: der Befehlsstring von der UART empfangen und die Implementierung dieses Codes ist per RX Interrupt implementiert, dann muss das Programm davon ausgehen, dass während der Behandlung des Befehlsstrings weitere Zeichen von der UART empfangen werden. Das kopieren des Inhaltes verhindert ein unbeabsichtigtes Überschreiben des RX Buffers, bevor dieser abgearbeitet werden konnte.


    Ich habe die Zeilen 1-4 absichtlich nicht in eine Funktion gepackt, da ein logischer Aufbau einer Befehlsstringinterpretation aus mehreren Teilaufgaben besteht:

    • Eingabe aufteilen
    • Befehl suchen und prüfen auf Gültigkeit
    • möglicherweise die Gültigkeit der Parameter prüfen
    • und den Befehl ausführen

    Wird obiger Befehlsstring tokenisiert dann resultiert der Inhalt von StructCmdPara wie folgt:

    StructCmdPara.png


    Da CMD_PARA_MAX in der CmdParse.h Datei auf 5 gesetzt ist, werden höchstens 5 Token erlaubt. Werden mehr Tokens erwartet, dann muss CMD_PARA_MAX angepasst werden.


    ParaSet[0] = "command"  Token 0

    ParaSet[1] = "Param0"     Token 1

    ParaSet[2] = "Param1" Token 2

    ParaSet[3] = "Param2" Token 3

    ParaSet[2] = NULL; Token 4 Der letzte Eintrag wird mit NULL gefüllt.


    TokCnt zeigt die Anzahl gefundener Token (0-4)

    CmdIdx ist reserviert für den späteren Gebrauch (Index für den detektierten Befehl)

    Error ist 0 da der Vorgang erfolgreich war.


    CmdParse.h     CmdParse.c