Tinykon

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche


Eine Benutzerschnittstelle für Mikrocontroller

Die Rufnummer des Gesprächspartners, das Waschprogramm für die Unterhosen, die Kontonummer - elektronische Geräte wollen Daten und/oder Anweisungen. Über den Kontostand gibt ein mehrzeiliges LC-Display Auskunft, die Wassertemperatur erscheint auf einer großformatigen LED-Anzeige und das Handy informiert auf einem bunten VGA-Display über den Verbindungsstatus.
Kurzum: zwischen Gerät und Benutzer findet ein intensiver Informationsaustausch statt.

Auf der Geräteseite wird die Kommunikation über zwei Module durchgeführt, Tastatur und Anzeige, für die sich als zusammenfassender, übergeordneter Begriff die Bezeichnung "Konsole" eingebürgert hat.

In diesem Beitrag soll eine Methode zur programmtechnischen Behandlung der Konsole beschrieben werden, die auf die Gegebenheiten eines mikrocontrollergesteuerten Gerätes zugeschnitten ist.

*.png
Multifunktionskonsole mit Display und Tastatur:
Auswahlmenü und LED-Anzeige
Menu.png
Konsole mit Kathodenstrahlröhre:
Auswahlmenü und Softkeys

Vom Desktopmenü zu TinyKon

Geräte mit Mikrocontrollern verfügen teilweise über einen beachtlichen Funktionsumfang, der den Vergleich mit den Auswahlmöglichkeiten in der Bedienoberfläche eines PC-Programms nicht zu scheuen braucht. Allerdings existiert ein gravierender Unterschied zwischen beiden Anwendungen, die PC-Konsole ist mit reichhaltiger Hardware ausgestattet, ein elektronisches Gerät muss sich mit vergleichsweise bescheidenen Mitteln begnügen.
Trotz der andersartigen Ausgangsbasis gestattet TinyKon dem Programmentwickler, die mehr oder weniger zweckmäßige Gliederung der Programbedienung eines PC-Programms nachzuahmen, da sich einige Bedienabfolgen zu einem Quasistandard entwickelt haben.

Die untenstehende Skizze deutet die Schritte an, die Tinykon aus dem gewöhnlichen Desktopmenü ableiten.

Menuentw.png

Ausgangspunkt ist die Menüdarstellung auf dem Bildschirm.
Die Hauptmenüzeile ist sichtbar, ein Menü ist aktiv, die restlichen Menüpunkte sind verdeckt. Die Navigation in diesem "planaren" Menüsystem geschieht mit Maus und/oder Tastatur über eine nicht näher bekannte Programmschnittstelle.

Das Verbergen der nicht aktiven Menü in der Fläche ist mit den oben angedeuteten Darstellungseinschränkungen nicht möglich, daher werden die inaktiven Menü "unter" dem aktiven Menü bereitgehalten. Das planare Menüsystem wird in ein gestapeltes System überführt, bei dem, wie bei einem Stapel Spielkarten, immer nur das oberste Menü/die oberste Karte sichtbar ist. Die zweckmäßige Wahl des Datentyps, der "Lageninhalt" und "Lagenposition" des Stapels repräsentiert, ist für eine einfache Navigation in diesem Menüsystem entscheidend.
Zu einer wirklich winzigen Konsole wird TinyKon erst durch die Verknüpfung jedes einzelnen Menüpunktes mit den ihm zugedachten Eingabefunktionen. Jeder Menüpunkt wird dadurch zu einem unabhängigen Objekt, dessen Eigenschaften über die bloße Darstellung des Bedienzustandes eines Gerätes hinausgehen.

Codierung

Die unten angegebene (Minimal-) Implementierung von TinyKon geht davon aus, dass die Geräteanzeige in waagerechte logische Einheiten (Zeilen) gegliedert ist. Die Teilung ist entweder durch durch Aufbau der Anzeige vorgegeben(zB.LCD) oder wird an anderer Stelle des Programms durchgeführt.
Für die Kodierung von TinyKon ist die Struktur der doppelt verketteten, ringförmig geschlossenen Liste gut geeignet, da in sie die geforderte Navigationsmöglichkeit bereits "eingebaut" ist.

/*einige definitionen*/
#define  BYTE            unsigned char      /*wir basteln uns ein byte*/
#define  ANZAHL_TASTEN   einige_tasten
#define  TASTE_BEREIT    irgendetwas
#define  FENSTERHOEHE    fensterhoehe       /*fensterhöhe in zeilen*/
/*grundlegender datentyp */
typedef struct t{ struct t *prev,           /*der vorige menüpunkt*/
                           *next,           /*der nächste menüpunkt*/
                           *submen;         /*das nachgeordnete submenü*/
                  BYTE     *mtxt,           /*menütext*/
                           *mdat;           /*menüdaten*/
                  BYTE     (*tastfun[ANZAHL_TASTEN])(struct t**, struct t**);
                                            /*die diesem menüpunkt zugeordneten tastenfunktionen*/
                }TINYKON;

Im Datentyp von Tinykon erkennt man die doppelt verkettete Liste, mit der man sich im Menü bewegen kann. Das diesem Menüpunkt zugewiesene Submenü hat den gleichen Typ wie das aufrufenden Menü. Dadurch ergibt sich die interessante Möglichkeit, quasi durch die Hintertür eine ebenfalls doppelt verkettete Liste für die Navigation von Submenü zu Submenü aufzubauen.
Die Tasten werden durch eine Liste von Funktionszeigern repräsentiert, so dass die vom Tastaturtreiber gelieferten Tastennummern direkt an dieses Array weitergeleitet und die entsprechende Aktion ausgeführt werden.

Ein Menü ist tabellenartig aufgebaut und hätte etwa folgenden Inhalt:

/*vorläufiges menü mit 4 menüpunkten*/
/*die anzahl der tasten wird mit 5 angenommen, unbekannte (funktions)zeiger werden mit "0" vorbesetzt*/
TINYKON tk[]=
 /* vor menu, nä menu, submen, mtext  , mdaten,  tf0 , tf1 , tf2, tf3, tf4 */
{{  &tk[3]  ,  &tk[1],   0   , "Menü1",   0   ,{  0  ,  0  ,  0 ,  0 ,  0}},
 {  &tk[0]  ,  &tk[2],   0   , "Menü2",   0   ,{  0  ,  0  ,  0 ,  0 ,  0}},
 {  &tk[1]  ,  &tk[3],   0   , "Menü3",   0   ,{  0  ,  0  ,  0 ,  0 ,  0}},
 {  &tk[2]  ,  &tk[0],   0   , "Menü4",   0   ,{  0  ,  0  ,  0 ,  0 ,  0}}};

Um mit dem Menüsystem zu arbeiten, sind vergleichsweise wenige Funktionen erforderlich.
Mit der Startfunktion gehts los:

void menu(TINYKON *tk)               
{   TINYKON *nullzeile,              /*oberste menüzeile im display*/      
            *curzeile;               /*cursorzeile*/
    BYTE taste, ende=FALSE;
    
    nullzeile=curzeile=tk;           /*der cursorzeile bzw nullzeile den ersten menüeintrag zuweisen*/
    while(!ende)
    {    display(nullzeile,          /*die anzeigefunktion muss an die vorhandene*/
                 curzeile);          /*hardware angepasst werden*/
        if(taste=get_taste()==TASTE_BEREIT)/*taste holen und entsprechende aktion auslösen*/
           ende=(*curzeile->tastfun[taste])(&nullzeile,&curzeile);
    }
}

Der Auruf eines Submenüs an der aktuellen Cursorposition gestaltet sich völlig unproblematisch, denn die Startfunktion wird mit geänderten Parametern erneut aufgerufen:

BYTE submenu(TINYKON **nullzeile,TINYKON **curzeile)
{   
    menu((*curzeile)->submen);           
    return(FALSE);
}

Eine Submenüebene wird mit dem Aufruf der Endefunktion verlassen. Da in das Untermenü, in dem diese Funktion aktiviert wird, rekursiv eingetreten wurde, kehrt die Endefunktion automatisch zum aufrufende Menü zurück.

BYTE endsubmenu(TINYKON **nullzeile,TINYKON **curzeile)
{   
    return(TRUE);
}

Mit den folgenden zwei Funktionen kann zwischen den Menüpunkten des aktuellen Menüs navigiert werden.

BYTE curup(TINYKON **nullzeile,TINYKON **curzeile)
{   TINYKON *z;
    BYTE i;
    *curzeile=(*curzeile)->prev;     /*cursor !immer! in die vorige zeile setzen*/
    z=(*nullzeile);
    for(i=0; (*curzeile)!=z; i++)    /*der abstand zwischen oberster displayzeile*/
        z=z->next;                   /*und cursorzeile wird durch abzählen bestimmt*/
    
    if(i>=FENSTERHOEHE-1)            /*falls der abstand größer als die fensterhöhe (in zeilen)ist,*/
                                     /*fensterinhalt nach unten scrollen*/
    {   *nullzeile=(*nullzeile)->prev;
        *curzeile=*nullzeile;}
    
    return(FALSE);
}
BYTE curdwn(TINYKON **nullzeile,TINYKON **curzeile)
{   TINYKON *z;
    BYTE i;
    *curzeile=(*curzeile)->next;      /*cursor !immer! in die nächste zeile setzen*/
    z=*nullzeile;
    for(i=0; (*curzeile)!=z; i++)     /*der abstand zwischen oberster displayzeile*/
        z=z->next;                    /*und cursorzeile wird durch abzählen bestimmt*/

     if(i>FENSTERHOEHE-1)             /*falls der abstand größer ist als die fensterhöhe (in Zeilen),*/
                                      /*fensterinhalt nach oben scrollen*/
         *nullzeile=(*nullzeile)->next;
     return(FALSE);
}

Zuletzt noch eine Hilfsfunktion, die die Implementierung der Startfunktion enorm vereinfacht.

BYTE dumfun(TINYKON **nullzeile,TINYKON **curzeile)
{
     return(FALSE);
}

An diesem Punkt sind alle Größen für die Definition einer vollständigen, funktionsfähigen Benutzerschnittstelle bekannt.

/*vollständige Benutzerschnittstelle vom Typ TINYKON*/
/*die anzahl der tasten wird mit 5 angenommen, unbekannte zeiger sind mit "0" vorbesetzt*/
/*an Stelle des Menü 1 ist ein (numerisches)eingabefeld getreten. auf die editierfunktionen*/
/*wird an dieser stelle nicht näher eingegangen. sie sind in den beispielprogrammen beschrieben*/
BYTE ep[PUFFERLAENGE];                         /*eingabepuffer*/ 
TINYKON ed=
 /* vor menu, nä menu, submen, mtext  , mdaten,  tf0 , tf1 , tf2, tf3, tf4 */
{    0      ,   0    ,   0  ,"Eingabe:", ep   ,{zplu ,zmin ,curli,curre,endsubmenu}};       
TINYKON tk[]=
 /* vor menu, nä menu, submen, mtext  , mdaten,  tf0 , tf1 , tf2, tf3, tf4 */
{{  &tk[3]  ,  &tk[1],  &ed  ,"Eingabefeld",ep,{curup,curdwn,dumfun,dumfun,submenu}},
 {  &tk[0]  ,  &tk[2],   0   , "Menü2",      0,{curup,curdwn,dumfun,dumfun,dumfun}},
 {  &tk[1]  ,  &tk[3],   0   , "Menü3",      0,{curup,curdwn,dumfun,dumfun,dumfun}},
 {  &tk[2]  ,  &tk[0],   0   , "Menü4",      0,{curup,curdwn,dumfun,dumfun,dumfun}}};
void main(void)
{   
    menu(tk);
}

Die Anzeige- und Tastenfunktion, deren Codierung naturgemäß stark von der vorhandenen Hardware abhängt, sollen im Pseudocode angegeben werden.
Aufgabe der Tastenfunktion ist es, die Tastenkodierung, die als bereits vorliegend angenommen wird, in den von Tinykon benötigten Arrayindex umzuwandeln und zurückzugeben.

BYTE get_taste()
{  
   konvertiere den von der Hardware gelieferten Tastencode in geeigneten index;
   liefere index zurück;
}

In der Anzeigefunktion wird das aktuelle Menü ganz oder teilweise dargestellt. Die Darstellung beginnt in der ersten (obersten) Zeile und gibt nacheinander alle Menüpunkte aus, bis entweder das Displayfenster vollständig beschrieben oder kein weiterer Menüpunkt mehr vorhanden ist.

void display(TINYKON *nullzeile, TINYKON *cursorzeile)
{  BYTE display_zeile=0;
   TINYKON *aktuelle_zeile;
   aktuelle_zeile=nullzeile;
   wiederhole solange die display_zeile ungleich der FENSTERHOEHE
   {   wenn aktuelle_zeile==cursorzeile,
          dann zeilenmarkierung in displayzeile darstellen;
       den übrigen zeileninhalt in displayzeile darstellen;
       aktuelle_zeile=aktuelle_zeile->next;
       wenn aktuelle_zeile==nullzeile,
          dann schleifenabbruch;
       display_zeile+=1;}
}

Speicherbelastung

Tinykon verbindet geringen Programmieraufwand mit einem kompaktem Datentyp, so dass es sich leicht in eine Mikrocontrolleranwendung integrieren läßt. Es ist allerdings immer zweckmäßig, den Grundbedarf an Datenspeicher für ein lauffähiges Minimalsystem abzuschätzen, zumal RAM bei Mikrocontrollern in der Regel ein knappes Gut ist.
Tinykon legt seine Variablen auf dem Stapel ab. Die Vielfalt der Mikrocontrollerarchitekturen und damit einhergehender Speichermodelle bringt es mit sich, dass eine quantitative Aussage zum lokalen Speicherverbrauch schwierig ist und deshalb hier nicht gemacht werden soll. Qualitativ lasst die vorgestellte Implementierung auf folgende Stapelbelastung schliessen:

ständige Stapelbelegung nach Start von Tinykon:

  • Rückkehradresse der Menüstartfunktion
  • Zeiger auf das aktuelle Menü
  • lokale Variable: Cursorzeile
  • lokale Variable: Nullzeile(=oberste Displayzeile)

jedes aufgerufene Submenü belegt weitere Stapelplätze:

  • Rückkehradresse der Aufruffuntion
  • Zeiger auf das aufgerufene Submenü
    • Rückkehradresse zum aufrufenden Menü
    • Zeiger auf das aktuelle Menü
    • lokale Variable: Cursorzeile
    • lokale Variable: Nullzeile(=oberste Displayzeile)

Die Aufstellung zeigt eine zwar vorhandene, aber moderate Belastung des Stapels, deren Umfang sich innerhalb einer Menüebene nicht verändert. Allerdings, je mehr Menüebenen eingeführt werden, desto höher ist der Platzbedarf auf dem Stapel.


Beispiele

Mit zwei Beispielen soll TinyKon demonstriert werden.
Die bereits erwähnte Architekturvielfalt unter den Prozessoren und die Hardwareabhängigkeit der Tastatur- und Anzeigefunktíon lässt ein Eingehen auf eine spezielle Konstellation nicht zu. Daher wurde als kleinster gemeinsamer Nenner das DOS-Fenster, das sicherlich auf jedem PC verfügbar ist, gewählt, in dem Tinykon als Simulation ausgeführt wird.
Bedient wird die Simulation mit den Tasten des Cursorblocks, der Enter- und der ESC-Taste.

1. Beispiel

Das erste Beispiel zeigt TinyKon in einem 4 Zeilen hohen Fenster. TinyKon ist als Eingabefeld konfiguriert, der Textcursor ist unter dem ersten Zeichen plaziert. Das Eingabefeld befindet sich in einem Submenü, der Weg dorthin ist nicht erkennbar.

Dosmenu1.png

Der Quellcode zu diesem Beispiel eignet sich gut als Ausgangspunkt für eigene Versuche, da er schnörkellos geschrieben ist und die DOS-spezifischen Programmteile besonders gekennzeichnet sind.<br´/> Quelltext und ausführbare Datei zu Beispiel1

2.Beispiel

TinyKon kann sehr einfach in übergeordnete Datenstrukturen eingebunden werden. Das Beispiel führt Tinykon mit wechselnden Menüstrukturen in verschiedenen Fenstern aus. Gezeigt ist das Systemdatum, das ständig, unabhängig von seiner Position, aktualisiert wird.

Dosmenu2.png

Quelltext und ausführbare Datei zu Beispiel 2

Veränderungen und Erweiterungen

Die aktuelle Version von TinyKon schränkt die Nutzung des Datenspeichers auf das absolut notwendige Maß ein. Die Daten für das Display werden direkt aus den Menüdaten, die sich im Programmbereich befinden sollten, abgeleitet. Daher beansprucht zB. das Eingabefeld, wenn es aktiv ist, die gesamte Displayfläche für sich. Abhilfe würde ein 'Videospeicher', dessen Umfang der Zeilenzahl des Displays entspräche, schaffen. Im Videospeicher befänden sich Kopien der aktuellen Anzeigedaten, in die bei Bedarf ein Eingabefeld oä. eingefügt werden könnte, ohne dass der oben beschriebene Effekt eintritt. Der Videospeicher eignet sich gut für die Speicherung von Daten, die permanent dargestellt werden sollen, zB. Menütitel, unter denen die Menüpunkte hindurchwandern.
Wenn genügend RAM bereitsteht, könnten alle Menüdaten im Datenspeicher angelegt werden. Dadurch wäre es möglich, den aktuellen Status eines jeden Menüs zu konservieren und in diesen Zustand beim erneuten Aufruf des Menüs unmittelbar einzutreten. Zeiger auf diese Zustände könnten nichtflüchtig gespeichert und die letzte Menüzustand mit ihrer Hilfe, zB. beim Einschalten des Gerätes, wieder hergestellt werden.

Zusammenfassung

Mit TinyKon wurde ein vielseitig einsetzbares Menüsystem für Mikrocontroller vorgestellt. Seine Stärke ist die klare Strukturierung der Daten und die Modularisierung des Programmcodes. Die Codierung in C sichert seine Portierbarkeit.
TinyKon ist ein offenes System, das auf Grund seiner Flexibilität den weitgestreuten Einsatzbereichen mikrocontrollerbasierter Geräte optimal angepasst werden kann.