Pointer auf Funktionen: Neue Software für das Graphik-Display

Das Graphik-Display “Smart I2C-GLCD” hat inzwischen eine ganze Reihe Anwendungsgebiete gefunden. Dabei sind einige neue Funktionen hinzugekommen, z.B. das Zeichnen von gepunkteten Linien oder die Ausgabe von negativen Zahlen. Durch den größeren Funktionsumfang war der Instruktions-Interpreter aber nicht mehr effizient und musste verbessert werden.

Zur Erinnerung: Das Graphik-Display empfängt Instruktionen über das I2C-Interface. Das erste Byte jeder Datensendung ist das Command-Token und bestimmt, welche Funktion ausgeführt werden soll. Z.B steht 17 für das Zeichnen einer Linie oder 32 für die Ausgabe eines Zeichens. Im bisherigen Programm gab es eine Switch/Case-Konstruktion, die sich durch die Liste der möglichen Instruktion-Tokens hindurch hangelte, bis der passende Token gefunden wurde. Dort wurde dann in die entsprechende Graphik-Funktion verzweigt. Das funktionierte gut mit 5 oder 10 möglichen Tokens, macht aber keinen Sinn mehr für die derzeitige Liste von über 30.

Wie geht es besser? Die Idee ist ganz einfach: Eine Tabelle, die für jeden Token einen Zeiger auf die zugehörige Graphik-Funktion. Mit Hilfe dieser Tabelle kann der Instruktions-Token als Index genutzt werden, um ohne Schleifen direkt den jeweiligen Tabellen-Eintrag zu finden.

Allerdings benötigt der Instruktionsinterpreter nicht nur den Zeiger auf die zugehörige Funktion, sondern auch die Anzahl der Parameter, die zum Ausführen der Funktion benötigt werden. Die Graphik-Funktion draw_char() braucht nur einen Parameter, eben das zu zeichnende Zeichen, während die Funktion draw_line() 5 Parameter benötigt, die Koordinaten des Start- und Endpunktes und der Modus. So ergibt sich eine Tabelle mit zwei Spalten: dem Zeiger auf die zugehörige Graphik-Funktion und der Anzahl der Parameter.

Tabelle mit Zeigern auf Funktionen
Tabelle mit Zeigern auf Funktionen

Die Sprache C macht den Umgang mit Zeigern nicht gerade einfach. Auch nach vielen Jahren Programmierpraxis ist die zugehörige Syntax immer wieder verwirrend. Zum Glück konnte ich noch meinen alten “Kernighan & Ritchie” im Regal finden (meine Ausgabe ist von 1986). Dieses immer wieder erstaunliche Buch zeigt die zugehörige Syntax.

Kernighan & Ritchie, Ausgabe 1986
Kernighan & Ritchie, Ausgabe 1986

Und so ist der neue Instruktions-Interpreter entstanden. Das Kernstück, die zweidimensionale Tabelle, wird mit Hilfe einer Struktur definiert.

typedef struct {
   void (*cmd_func)(void);
   int8_t parm_count;
} cmd_entry_t;

Die Formulierung void (*cmd_func)(void) ist ein Platzhalter für die Zeiger auf die jeweiligen Graphik-Funktion. Die Instruktions-Tabelle ist ein Array von der oben definierten Struktur. Sie wird gleich mit den entsprechenden Einträgen initialisiert:

 cmd_entry_t cmd_table[] = {
   {cmd_ignore, 0, },              // 0 
   {cmd_glcd_on, 0, },             // 1
   {cmd_glcd_off, 0, },            // 2
   {cmd_set_display_light, 1, },   // 3
   {cmd_set_dim_on, 0, },          // 4
   {cmd_set_dim_off, 0, },         // 5
   {cmd_set_i2c, 1, },             // 6
   ...
};

Natürlich müssen die Funktionen, die in der Tabelle auftauchen, entsprechend definiert sein. Hier sind die Funktions-Prototypen gezeigt, die üblicherweise in einer #include-Datei stehen:

// command function prototypes
void cmd_ignore(void);
void cmd_glcd_on(void);
void cmd_glcd_off(void);
void cmd_set_display_light(void);
void cmd_set_dim_off(void);
void cmd_set_dim_on(void);
void cmd_set_i2c(void); 
...

Im Instruktions-Interpreter wird das Command-Token über das I2C-Interface eingelesen, der Wertebereich überprüft, und wenn der passt die entsprechende Funktion aufgerufen.

cmd_token = I2C_fetch_byte();        // get instruction token 
if (cmd_token < sizeof(cmd_table)/sizeof(cmd_entry_t)) { // range check
   parm_buffer[0] = cmd_token;
   for (i = 1; i <= cmd_table[cmd_token].parm_count; ++i) 
      parm_buffer[i] = I2C_fetch_byte(); // if yes, get parameters 
   (*cmd_table[cmd_token].cmd_func)(); // and call graphic function
} else {
   cmd_show_error();                  // if not, show error
};

parm_buffer[] ist ein globales Array, in dem der aktuelle Command-Token und die zugehörigen Parameter abgelegt werden. Dieses Array wird von den Graphik-Funktionen genutzt, um die jeweilige Aktion zu steuern. Als Beispiel wird hier die Funktion cmd_draw_line() gezeigt, die die zugehörige Graphik-Funktion zum Zeichnen einer Linie aufruft:

/* cmd_draw_line - plots a straight line -----------------------*/
void cmd_draw_line(void) {
   draw_line(parm_buffer[1], parm_buffer[2], parm_buffer[3], parm_buffer[4], parm_buffer[5]);
};

Nach einigen Versuchen funktioniert der neue Instruktions-Interpreter ganz hervorragend. Das Programm ist damit deutlich kürzer und effizienter geworden.

Die neue Software ist bei den Ressourcen zu finden unter Smart I2C Display, Firmware

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.