YaMenü

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

von Ralf Werner

Hier will ich mein Menüsystem vorstellen. Es ist in C geschrieben. Das Menü ist für Textdisplays, die zB den HD44780 verwenden oder das Nokia 3310 gedacht. In der jetzigen Ausführung wird es über 5 Tasten (Auf, Ab, Links, Rechts, Ok) gesteuert. Das Display muss mindestens über zwei Zeilen verfügen. Ein Beispielprojekt das dieses Menü verwendet habe ich in diesem Thread vorgestellt.

Einleitung

Um das Menü vorzustellen will ich Schritt für Schritt an Hand eines Beispiels zeigen wie es angepasst werden kann. Da nicht alle die selbe Hardware verwenden zeige ich das für eine Consolenanwendung auf dem PC. Es soll erstmal gezeigt werden wie eine einfache Uhr die das einstellen der Uhrzeit ermöglicht programmiert werden kann.

Vorbereitungen

Ich verwende Codeblocks für dieses Beispiel. Das könnt ihr hier herunterladen. Ihr braucht die Version mit dem mingw Compiler. Als nächstes benötigen wir PDCurses. Nehmt hier die aktuellste Version. Ich habe die Datei pdc34dll.zip heruntergeladen. Die enthält schon die kompilierte dll und die benötigten Header. PDCurses wird gebraucht um in der Konsole das Look&Feel eines LCD Displays nachzuempfinden.

Jetzt erstellt ihr ein neues Projekt für eine Consolenanwendung in der Sprache C mit dem Titel menu in Codeblocks (dieses solltet ihr mittlerweile installiert haben). Als nächstest müßt ihr PDCurses im Projektverzeichnis in einen Unterordner pdcurses entpacken. Jetzt braucht ihr die Datei:Menu.zip. Diese muß ebenfalls im Projektordner entpackt werden.

Es müssen zwei bzw eine Datei für die Zielhardware angepasst werden. Das ist einmal displayroutines.h und displayroutines.c.

displayroutines.h für PDCurses:

#ifndef DISPLAYROUTINES_H_INCLUDED
#define DISPLAYROUTINES_H_INCLUDED

#include <stdint.h>

#define DisplayRows 6
#define DisplayColumns 14

void InitDisplay(); //nur für PDCurses
void DeInitDisplay(); //nur für PDCurses
int GetChar(); //nur für PDCurses
void PrintStringPos(uint8_t x, uint8_t y, char *s);
void PrintString(char *s);
void PrintCharPos(uint8_t x, uint8_t y, char c);
void PrintChar( char c);
void HighlightInvers(uint8_t y, char *s);
void Highlight(uint8_t y);
void DeHighlight(uint8_t y);
void Clear();

#endif // DISPLAYROUTINES_H_INCLUDED

displayroutines.c für PDCurses:

#include "displayroutines.h"
#include "../pdcurses/curses.h"
#include <ctype.h>

static WINDOW *mywin;

void InitDisplay(){
    initscr();
    noecho();
    cbreak();
    keypad(stdscr, TRUE);

    int begin_x = 20 ;
    int begin_y = 7;
    int height = DisplayRows + 2;
    int width = DisplayColumns +2;
    mywin = newwin(height, width, begin_y, begin_x);
    //wborder(/*WINDOW *win*/ mywin, /*chtype ls*/ 0,/* chtype rs*/ 0,/* chtype ts*/ 0,/* chtype bs*/ 0,/*  chtype tl*/ 0,
    //       /* chtype tr*/ 0,/* chtype bl*/ 0, /* chtype br*/ 0);
    box(/*WINDOW *win*/ mywin, /*chtype verch*/ ACS_VLINE,  /*chtype horch*/ ACS_HLINE);
    curs_set(0);
    wtimeout(mywin,0);
    wrefresh(mywin);
}

void DeInitDisplay(){
    nocbreak();
    keypad(stdscr, FALSE);
    echo();
    endwin();
}

int GetChar(){
    int result;
    result = wgetch(mywin);
    return result;
}

void SetCursor(uint8_t x, uint8_t y){
    wmove(mywin, y + 1, x + 1);
    wrefresh(mywin);
}

void PrintString(char* s){
    waddstr(mywin, s);
    wrefresh(mywin);
}

void PrintChar(char c){
    waddch(mywin, c);
    wrefresh(mywin);
}

void PrintCharPos(uint8_t x, uint8_t y, char c){
    wmove(mywin, y + 1, x + 1);
    waddch(mywin, c);
    wrefresh(mywin);
}

void PrintStringPos(uint8_t x, uint8_t y, char *s){
    wmove(mywin, y + 1, x + 1);
    waddstr(mywin, s);
    wrefresh(mywin);
}

void Highlight(uint8_t y){
    wmove(mywin, y + 1, 1);
    waddstr(mywin, ">");
    wrefresh(mywin);
}

void HighlightInvers(uint8_t y, char *s){
    wmove(mywin, y + 1, 1);
    int c = tolower(s[1]);
    waddch(mywin, c);
    waddstr(mywin, s+1);
    wrefresh(mywin);
}

void DeHighlight(uint8_t y){
    waddstr(mywin, " ");
    wrefresh(mywin);
}

void Clear(){
    wclear(mywin);
    box(/*WINDOW *win*/ mywin, /*chtype verch*/ ACS_VLINE,  /*chtype horch*/ ACS_HLINE);
    wrefresh(mywin);
}

Mit diesen beiden Dateien wird dem Menüsystem ermöglicht Ausgaben auf dem Display zu machen. Ich denke die Funktionsnamen sollten selbsterklärend sein. Deshalb gehe ich auf die einzelnen Funktionen nicht weiter ein. Hier mal ein Beispiel für displayroutines.h. So wie ich es im oben genannten Projekt für einen ATMega88 mit dem Nokia 3310 LCD Display gelöst habe:

#ifndef DISPLAYROUTINES_H_INCLUDED
#define DISPLAYROUTINES_H_INCLUDED

#include <stdint.h>

#include "../driver/nokia3310library\lcd.h"

#define DisplayRows 6
#define DisplayColumns 14

#define PrintStringPos( x, y, s) {lcd_goto_xy((x) + 1 ,(y) + 1); lcd_str(s,0);}
#define PrintString(s) {lcd_str(s,0);}
#define PrintCharPos( x, y, c) {    lcd_goto_xy((x) + 1 ,(y) + 1); lcd_chr(c);}
#define PrintChar(c) {lcd_chr(c);}
#define Highlight(y) {lcd_goto_xy( 1 ,y + 1); lcd_chr(ARROW_RIGHT);}
#define HighlightInvers(y, s) {lcd_goto_xy( 1 ,(y) + 1);lcd_str(s,1);}
#define DeHighlight(y) {PrintCharPos(1, (y), ' ');}
#define Clear() {lcd_clear();}

#endif // DISPLAYROUTINES_H_INCLUDED

Wie ihr seht habe ich für die Funktionen nur defines verwendet und brauchte displayroutines.c gar nicht anfassen. So das war nur ein Beispiel für die Anpassung an einen Controller. Weiter geht es mit unserer Uhr für die Konsole. Wir brauchen einen Timer der jede Sekunde ein Signal liefert. Auf dem Controller liefert das der Timer (siehe dazu auch den Artikel AVR_-_Die_genaue_Sekunde_/_RTC). In der Konsole gibt es meines Wissens nach so einen Interrupt nicht. Da müssen wir uns behelfen: main.c mit Sekundentick:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>
#include <stdbool.h>

#include "menu/menu.h"

bool onesecondover = false;
clock_t start, diff;

char* getItemText(uint8_t ItemType, uint8_t index){
    return NULL;
}

uint8_t MenuCount = 0;
t_Menu *Menus[];

int main(){
    start = clock();
    while(1){
        if(onesecondover){
            printf("TimerTick\n");
            onesecondover = false;
        }
        diff = (clock() - start) * 1000 / CLOCKS_PER_SEC;
        if(diff >= 1000){
            onesecondover = true;
            start = clock();
        }
    }
}

Wenn ihr das jetzt ausführt erscheint jede Sekunde TimerTick in der Konsole. Damit hätten wir einen einfachen Timer.

Das kompilieren dürfte aber nicht klappen. Es fehlt noch eine Einstellung in Codeblocks. Ihr müßt auf Project/Build Options... gehen. Im folgenden Dialog den Reiter Linker settings öffnen und dann auf den Button Add drücken. Im folgenden Dialog müsst ihr den Pfad zu pdcurses.lib in eurem pdcurses Ordner angeben. Dann werdet ihr gefragt ob der Pfad relativ bleiben soll. Bestätigt dies mit ja. Jetzt müßte das Projekt kompilieren. Es sollte nur noch eine Warnung wegen dem leeren Menüarray übrigbleiben.

Die Uhr ohne Menü

Jetzt werden wir eine Uhr programmieren, die die Uhrzeit in der Konsole ausgibt, also noch ohne Menü. Dazu brauchen wir einen Datentyp für die Uhrzeit. Eine Funktion die die Zeit zählt und eine die die Zeit nach einem String wandelt, damit dieser ausgegeben werden kann.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#include "menu/menu.h"

typedef struct s_time{
    uint8_t hour;
    uint8_t minute;
    uint8_t second;
}t_stime;

t_stime mytime = {
    .hour = 20,
    .minute = 30,
    .second = 0
};

void TimerTick(){
    mytime.second++;
    if(mytime.second == 60){
        mytime.second = 0;
        mytime.minute++;
    }
    if(mytime.minute == 60){
        mytime.minute = 0;
        mytime.hour++;
    }
    if(mytime.hour == 24){
        mytime.hour = 0;
    }
}

void TimeToASCII(char *destination, t_stime *atime){
    char result[9] = "\0";
    char buffer[3] = "\0";
    destination[0] = '\0';
    itoa(atime->hour, buffer, 10);
    if(atime->hour < 10){
        result[0] = '0';
    }
    strcat(result, buffer);
    result[2] = ':';
    itoa(atime->minute, buffer, 10);
    if(atime->minute < 10){
        result[3] = '0';
    }
    strcat(result, buffer);
    result[5] = ':';
    itoa(atime->second, buffer, 10);
    if(atime->second <10){
        result[6] = '0';
    }
    strcat(result, buffer);
    strcat(destination, result);
}

bool onesecondover = false;
clock_t start, diff;

char* getItemText(uint8_t ItemType, uint8_t index){
    return NULL;
}

uint8_t MenuCount = 0;
t_Menu *Menus[];

int main(){
    char asciitime[9];
    start = clock();
    while(1){
        if(onesecondover){
            TimeToASCII(asciitime, &mytime);
            printf(asciitime);
            printf("\n");
            onesecondover = false;
        }
        diff = (clock() - start) * 1000 / CLOCKS_PER_SEC;
        if(diff >= 1000){
            TimerTick();
            onesecondover = true;
            start = clock();
        }
    }
}

Die Uhr mit Menü

So jetzt geht es darum Schritt für Schritt die Ausgabe des Programms in das Display, das durch PDCurses simuliert wird zu verlagern. Dazu müssen wir erstmal die Menüs deklarieren.

Menüs deklararieren

Für dieses Beispiel werde ich ein Hauptmenü erstellen, welches ein Untermenü Settings enthätlt. Das Untermenü Settings hat einen Menüpunt Set Time. Damit man die Menüs nicht umständlich geschachtelt aufschreiben muß, habe ich eine Automatik eingebaut. Diese findet die Untermenüs an Hand des Namens. Dazu muß man im Menü in dem das Untermenü erscheinen soll, dem Namen ein @ voransetzten. Das Menüsystem prüft auf dieses Zeichen und findet dann das passende Untermenü. Das ließt sich sicher kompliziert, ist es aber in der Praxis nicht. Deshalb hier gleich das Beispiel.

Für das erstellen der Menüs bietet sich der Header menuitems.h an.

#ifndef MENUITEMS_H_INCLUDED
#define MENUITEMS_H_INCLUDED

#include "menu.h"

t_Menu SettingsMenu = {
    .Text = "Settings", //der Name des Menüs
    .menu_id = 1, //eine eindeutige ID
    .Previous = NULL, //muß immer mit NULL initialisiert werden
    .Count = 1, //Anzahl Menüpunkte
    .ItemType = MENU_ITEMTYPE_BUILDIN, //Unterscheidung ob dynamisches Menü
    .MenuItems = {
        {
        .Text = "Set Time", //der Name des Menüpunkts
        .item_id = 10, //eine eindeutige ID für diesen Menüpunkt
        },
    },
};

t_Menu MainMenu ={
    .Text = "Hauptmenue",
    .menu_id = 2,
    .Previous = NULL,
    .ItemType = MENU_ITEMTYPE_BUILDIN,
    .Count = 1,
    .MenuItems = {
        {
        .Text = "@Settings", //dies ist das Untermenü Settings
                             //mit dem @ im Namen findet das Menüsystem die Untermenüs
                             //so braucht man die Menüs nicht geschachtelt aufschreiben
        .item_id = 20,
        },
    },
};

char* getItemText(uint8_t ItemType, uint8_t index){
    return NULL;
}

uint8_t MenuCount = 2;

t_Menu *Menus[] = {
    &MainMenu,
    &SettingsMenu,
};
#endif //MENUITEMS_H_INCLUDED

Wie man sieht hat das erste MenuItem im MainMenu das @ Zeichen vor dem Namen. Dieser Name entspricht genau dem Namen des SettingsMenu. Wenn der Benutzer eine Menüpunkt auswählt prüft das Menüsystem ob das erste Zeichen in Text ein @ ist. Findet es ein @ durchsucht es das Array Menus nach dem passenden Untermenü und ruft dieses auf. Dies geschiet automtisch ihr braucht euch nicht darum zu kümmern.

Ich habe die Funktion getItemText, MenuCount und das Pointerarray Menus aus der main.c in diesen Header verschoben. So bleibt main.c aufgeräumter.

Das Menü initialisieren

Noch haben wir keine Ausgabe des Menüs. Das wollen wir ändern. Dazu muß dem Menüsystem mitgeteilt werden, welches Menü das Startmenü ist und welche Funktion aufgerufen werden soll, wenn der Benutzer einen Menüpunkt selektiert. Dies geschieht durch ein Aufruf von EnterMenu am Anfang der main Routine bevor die EndlosSchleife betreten wird.

EnterMenu(&MainMenu, &MySelect);

&MainMenu ist wie gesagt das Startmenü und &MySelect erkäre ich jetzt. MySelect wird vom Menüsystem aufgerufen, wenn der Benutzer ein Menü wechselt oder einen Menüpunkt selektiert.

void MySelect(uint8_t MenuId, uint8_t action, uint8_t ItemType)

ItemType dient zur Unterscheidung für dynamische Menüs. Es ist der Wert des Members ItemType der Struktur t_Menu. Der Parameter action kann die Werte MENU_ACTION_CHANGE_MENU, MENU_ACTION_SELECT_BUILDINITEM, MENU_ACTION_SELECT_SPECIALITEM annehmen. MySelect wird also aufgerufen wenn ein Menü gewechselt wird und wenn ein Menüpunkt selektiert wird. So kann man flexibel auf die Wechsel reagieren. Der Parameter MenuId ist die eindeutige ID des Menüs. Diese wurde ja beim deklarieren angegeben.

Das Menü steuern

Wir fragen unsere Tastatur welche Buttons gedrückt wurden und rufen die Routinen des Menüsystems auf. Das machen wir in der Endlosschleife in main so:

        int a;
        a = GetChar() -48;
        switch (a){
            case 2: //Down
                NextItem();
                break;
            case 4: //Left
                ExitMenu();
                break;
            case 5: //Ok
            case 6: //Right
                SelectItem();
                break;
            case 8: //Up
                PreviousItem();
                break;
            case -49:
                break;
            default: 
                DeInitDisplay(); //nur auf dem PC um das Programm sauber zu verlassen
                return 0; //nur auf dem PC um das Programm sauber zu verlassen
                break;
        }

Jetzt fehlt noch der Aufruf für InitDisplay am Anfang der main Routine. Und Voila unser Menü erscheint. Nur die Zeitausgabe ist jetzt kaputt. Sie erscheint nach einem Tastendruck mehr oder weniger irgendwo in der Konsole. Das liegt daran, das wir nicht das Displaysystem zur Ausgabe nutzen. Ausserdem geschiet beim auswählen von Set Time überhaupt gar nichts. Dies wollen wir ändern.

Aufteilung in Apps

Das Problem ist, das sich die einzelnen Funktionen die Tastendrücke und das Display sowie die andere Hardware teilen müssen. Ich versuche das mal näher zu erklären. Stellt euch vor wir haben jetzt eine Funktion SetTime(button). Diese soll je nach Tastendruck die Stunden, Minuten und Sekunden unserer Zeit ändern und die Änderung speichern. Doch wie und vor allem wo soll SetTime aufgerufen werden. Im switch(a) in der main? Ungefähr so?

           case 2: //Down
                NextItem();
                SetTime(a);
                break;

Ihr seht das geht nicht irgendwie bekommt dann das Menüsystem und SetTime die Eingaben fast gleichzeitig. Das führt zu nichts.

Da habe ich mir folgendes ausgedacht. Eine App hat die Kontrolle über den Kontroller. Sie erhält alle Ereignisse und kann uneingeschränkt auf das Display zugreifen. Das hat nichts mit den Interrupts zu tun. Die laufen wie gehabt. Ereignisse sind von uns definierte Events, die für eine App interessant sein können. Eine Unterstützung für Apps ist im Menüsystem eingebaut. So braucht ihr das Rad nicht neu erfinden.

Als erstes legen wir einen neuen Ordner apps im Projektverzeichnis an. In diesem erstellen wir eine Datei apps.h. Dazu klicken wir im Menü von Codeblocks auf File/New/File... . Dann folgt bitte dem Dialog um apps.h im Unterverzeichnis apps zu erstellen. In diesem Header definieren wir uns jetzt erstmal ein paar Events.

#ifndef APPS_H_INCLUDED
#define APPS_H_INCLUDED

#define UP 0
#define DOWN 1
#define LEFT 2
#define RIGHT 3
#define OK 4
#define ONE_SECOND 5
#define ONE_MINUTE 6
#define NO_KEY 255

typedef void (*t_ActiveApplication) (uint8_t);
t_ActiveApplication ActiveApplication;

void MenuApplication(uint8_t aEvent);
void IdleApplication(uint8_t aEvent);

#endif // APPS_H_INCLUDED

Jetzt legen wir eine neue Datei menuapp.c an. Der Ablauf ist genau wie bei apps.h.

#include <stdint.h>

#include "apps.h"
#include "../menu/menu.h"


void MenuApplication(uint8_t aEvent){
    switch( aEvent){
     case UP:
        PreviousItem();
        break;
     case DOWN:
        NextItem();
        break;
     case LEFT:
        ExitMenu();
        break;
     case RIGHT:
     case OK:
        SelectItem();
     case ONE_SECOND:
        break;
    }
}

main.c ändern wir wie folgt:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#include "menu/menu.h"
#include "menu/menuitems.h"
#include "apps/apps.h"

bool onesecondover = false;
bool oneminuteover = false;
bool idle = true;

clock_t start, diff;

t_ActiveApplication ActiveApplication = &MenuApplication;

typedef struct s_time{
    uint8_t hour;
    uint8_t minute;
    uint8_t second;
}t_stime;

t_stime mytime = {
    .hour = 20,
    .minute = 30,
    .second = 0
};

void TimerTick(){
    mytime.second++;
    if(mytime.second == 60){
        mytime.second = 0;
        mytime.minute++;
    }
    if(mytime.minute == 60){
        oneminuteover = true;
        mytime.minute = 0;
        mytime.hour++;
    }
    if(mytime.hour == 24){
        mytime.hour = 0;
    }
}

void TimeToASCII(char *destination, t_stime *atime){
    char result[9] = "\0";
    char buffer[3] = "\0";
    destination[0] = '\0';
    itoa(atime->hour, buffer, 10);
    if(atime->hour < 10){
        result[0] = '0';
    }
    strcat(result, buffer);
    result[2] = ':';
    itoa(atime->minute, buffer, 10);
    if(atime->minute < 10){
        result[3] = '0';
    }
    strcat(result, buffer);
    result[5] = ':';
    itoa(atime->second, buffer, 10);
    if(atime->second <10){
        result[6] = '0';
    }
    strcat(result, buffer);
    strcat(destination, result);
}

void MySelect(uint8_t MenuId, uint8_t action, uint8_t ItemType){
    switch(ItemType){
        case MENU_ITEMTYPE_BUILDIN:{
            switch( MenuId){
                case 10:{
                    Clear();
                    //ActiveApplication = &SetTimeApp;
                    //ActiveApplication(NO_KEY);
                }
                break;
                default:
                break;
            }
        }
        break;
        case MENU_ITEMTYPE_SPECIAL:{//my special menu type for eeprom array
        }
        break;
    }
}


int main(){
    char asciitime[9];
    start = clock(); //nur für die PC Variante
    InitDisplay();
    EnterMenu(&MainMenu, &MySelect);
    while(1){
        if(onesecondover){
            //TimeToASCII(asciitime, &mytime);
            //printf(asciitime);
            //printf("\n");
            ActiveApplication(ONE_SECOND);
            onesecondover = false;
        }
        if(oneminuteover){
            idle = 1;
            //IdleApplication(ONE_MINUTE);
            oneminuteover = false;
        }
        if(idle){
            idle = false;
            //IdleApplication(NO_KEY);
        }
        int a;
        a = GetChar() -48;
        switch (a){
            case 2: //Down
                idle = false;
                ActiveApplication(DOWN);
                break;
            case 4: //Left
                idle = false;
                ActiveApplication(LEFT);
                break;
            case 5: //Ok
                idle = false;
                ActiveApplication(OK);
                break;
            case 6: //Right
                idle = false;
                ActiveApplication(RIGHT);
                break;
            case 8: //Up
                idle = false;
                ActiveApplication(UP);
                break;
            case -49:
                break;
            default:
                DeInitDisplay(); //nur auf dem PC um das Programm sauber zu verlassen
                return 0; //nur auf dem PC um das Programm sauber zu verlassen
                break;
        }

        /* nur für die PC Variante  Start*/
        diff = (clock() - start) * 1000 / CLOCKS_PER_SEC;
        if(diff >= 1000){
            TimerTick();
            onesecondover = true;
            start = clock();
        /* nur für die PC Variante  Ende*/
        }
    }
}

Wir setzen die Variable ActiveApplication auf MenuApplication. MySelect ist auch schon soweit ausgefüllt. Es fehlt halt noch die SetTimeApp. Ausserdem fehlt noch die IdleApplication. Mit der IdleApplication machen wir weiter.

Wir legen eine neue Datei idle.app im Unterordner apps an.

#include "apps.h"
#include "../menu/displayroutines.h"

void IdleApplication(uint8_t aEvent){
    static t_ActiveApplication LastActiveApplication;
    switch( aEvent){
        case UP:
        case DOWN:
        case LEFT:
        case RIGHT:
        case OK:{
            LastActiveApplication(UP);
            LastActiveApplication(DOWN);
            ActiveApplication = LastActiveApplication;
            return;
        }
        break;
        case ONE_SECOND:{
            char asciitime[9];
            TimeToASCII(asciitime, &mytime);
            PrintStringPos(2,2, asciitime);
        }
        break;
        case NO_KEY:{
            Clear();
            LastActiveApplication = ActiveApplication;
            ActiveApplication = IdleApplication;
        }
        break;
        case ONE_MINUTE:{
        return;
        }
        break;
    }
}

Und es müssen einige Teile aus main.c die mit der Zeit zu tun haben in die Dateien atime.h

#ifndef ATIME_H_INCLUDED
#define ATIME_H_INCLUDED

#include <stdbool.h>
#include <stdint.h>

bool onesecondover;
bool oneminuteover;

typedef struct s_time{
    uint8_t hour;
    uint8_t minute;
    uint8_t second;
}t_stime;

t_stime mytime;

void TimerTick();
void TimeToASCII(char *destination, t_stime *atime);

#endif // ATIME_H_INCLUDED

und atime.c

#include <string.h>
#include <stdlib.h>

#include "atime.h"

t_stime mytime = {
    .hour = 20,
    .minute = 30,
    .second = 0
};

void TimerTick(){
    mytime.second++;
    if(mytime.second == 60){
        oneminuteover = true;
        mytime.second = 0;
        mytime.minute++;
    }
    if(mytime.minute == 60){
        mytime.minute = 0;
        mytime.hour++;
    }
    if(mytime.hour == 24){
        mytime.hour = 0;
    }
}

void TimeToASCII(char *destination, t_stime *atime){
    char result[9] = "\0";
    char buffer[3] = "\0";
    destination[0] = '\0';
    itoa(atime->hour, buffer, 10);
    if(atime->hour < 10){
        result[0] = '0';
    }
    strcat(result, buffer);
    result[2] = ':';
    itoa(atime->minute, buffer, 10);
    if(atime->minute < 10){
        result[3] = '0';
    }
    strcat(result, buffer);
    result[5] = ':';
    itoa(atime->second, buffer, 10);
    if(atime->second <10){
        result[6] = '0';
    }
    strcat(result, buffer);
    strcat(destination, result);
}

verschoben werden. Jetzt zeige ich noch mal die geänderte main.c. Ja, ich weiß, ein Haufen Quelltext. Aber nur so könnt ihr dem Beispiel folgen.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>

#include "menu/menu.h"
#include "menu/menuitems.h"
#include "apps/apps.h"
#include "atime.h"

bool idle = true;
uint8_t menutimeout = 5;


clock_t start, diff;

t_ActiveApplication ActiveApplication = &MenuApplication;

void MySelect(uint8_t MenuId, uint8_t action, uint8_t ItemType){
    switch(ItemType){
        case MENU_ITEMTYPE_BUILDIN:{
            switch( MenuId){
                case 10:{
                    Clear();
                    //ActiveApplication = &SetTimeApp;
                    //ActiveApplication(NO_KEY);
                }
                break;
                default:
                break;
            }
        }
        break;
        case MENU_ITEMTYPE_SPECIAL:{//my special menu type for eeprom array
        }
        break;
    }
}


int main(){
    start = clock(); //nur für die PC Variante
    InitDisplay();
    EnterMenu(&MainMenu, &MySelect);
    while(1){
        if(onesecondover){
            menutimeout--;
            ActiveApplication(ONE_SECOND);
            onesecondover = false;
        }
        if(menutimeout == 0){
            menutimeout = 5;
            idle = 1;
        }
        if(oneminuteover){
            ActiveApplication(ONE_MINUTE);
            oneminuteover = false;
        }
        if(idle){
            idle = false;
            if(ActiveApplication == &MenuApplication){
                IdleApplication(NO_KEY);
            }
        }
        int a;
        a = GetChar() -48;
        switch (a){
            case 2: //Down
                idle = false;
                ActiveApplication(DOWN);
                break;
            case 4: //Left
                idle = false;
                ActiveApplication(LEFT);
                break;
            case 5: //Ok
                idle = false;
                ActiveApplication(OK);
                break;
            case 6: //Right
                idle = false;
                ActiveApplication(RIGHT);
                break;
            case 8: //Up
                idle = false;
                ActiveApplication(UP);
                break;
            case -49:
                break;
            default:
                DeInitDisplay(); //nur auf dem PC um das Programm sauber zu verlassen
                return 0; //nur auf dem PC um das Programm sauber zu verlassen
                break;
        }

        /* nur für die PC Variante  Start*/
        diff = (clock() - start) * 1000 / CLOCKS_PER_SEC;
        if(diff >= 1000){
            TimerTick();
            onesecondover = true;
            start = clock();
        /* nur für die PC Variante  Ende*/
        }
    }
}

Wie ihr seht, ist main.c jetzt wieder schön übersichtlich. Wenn ihr dem Beispiel bis hierher folgen konntet, haben wir einen kleinen Erfolg.

Die Uhr wird jetzt in unserem simulierten LCD Display angezeigt. Wenn wir eine der Steuertasten drücken, gelangen wir ins Menü. Nach einem Timeout von 5 Sekunden nach dem letzten Tastendruck wird wieder die Uhr angezeigt. Nur die SetTimeApp fehlt noch. Das ändern wir jetzt. Im Header apps.h ergänzen wir folgende Zeile:

void SetTimeApp(uint8_t aEvent);

Im Unterorder apps legen wir mit Hilfe von Codeblocks eine Datei settimeapp.c an. Diese bekommt folgenden Inhalt:

#include "apps.h"
#include "../atime.h"
#include "../types/ranged.h"
#include "../menu/menu.h"
#include "../menu/displayroutines.h"

t_stime timetochange;

void SetTimeApp(uint8_t aEvent){
    static t_ranged_uint8_t SetPosition = {
            .value = 0,
            .top = 2,
            .bottom = 0,
    };

    switch( aEvent){
     case OK:
        mytime = timetochange;
        SetPosition.value = 0;
        ActiveApplication = &MenuApplication;
        ExitItem();
        return;
     case UP:
        switch(SetPosition.value){
            case 0:{
                timetochange.hour++;
                if(timetochange.hour >= 24) timetochange.hour = 0;
            break;
            }
            case 1:{
                timetochange.minute++;
                if(timetochange.minute >= 60) timetochange.minute = 0;
            break;
            }
            case 2:{
                timetochange.second++;
                if(timetochange.second >= 60) timetochange.second = 0;
            break;
            }
        }
        break;
     case DOWN:
        switch(SetPosition.value){
            case 0:{
                if(timetochange.hour == 0) timetochange.hour = 24;
                timetochange.hour--;
            break;
            }
            case 1:{
                if(timetochange.minute == 0) timetochange.minute = 60;
                timetochange.minute--;
            break;
            }
            case 2:{
                if(timetochange.second == 0) timetochange.second = 60;
                timetochange.second--;
            break;
            }
        }
        break;
     case LEFT:
        rangedDec_uint8_t(&SetPosition);
        break;
     case RIGHT:
        rangedInc_uint8_t(&SetPosition);
        break;
     case NO_KEY:
        timetochange = mytime;
        break;
    }
    //Zeit ausgeben
    char asciitime[9];
    TimeToASCII(asciitime, &timetochange);
    PrintStringPos(2,2, asciitime);
    //HighlightChar löschen
    PrintStringPos(2, 3, "        ");

    char *HighlightChar = "^^";
    uint8_t Highlightx = 0;
    uint8_t Highlighty = 3;
    switch(SetPosition.value){
        case 0:{
            /*                    19:52:46*/
            Highlightx = 2;
        break;
        }
        case 1:{
            /*                    19:52:46*/
            Highlightx = 5;
        break;
        }
        case 2:{
            /*                    19:52:46*/
            Highlightx = 8;
        break;
        }
    }
    //HighlightChar ausgeben
    PrintStringPos  (Highlightx, Highlighty, HighlightChar);
}

Und in main.c kommentieren wir folgende zwei Zeilen aus.

                case 10:{
                    Clear();
                    ActiveApplication = &SetTimeApp;
                    ActiveApplication(NO_KEY);

Das wars. Jetzt kompilieren und starten und wir können unsere Uhr sehen und auch stellen.

Zum Schluß

Ich hoffe ihr konntet trotz des vielen Quellcodes und dem wenigen Text dem Beispiel folgen. Ich denke das die Verwendung von yaMenü einfach ist wenn man das Prinzip verstanden hat. Sicher kann man hier und da noch was verbessern Menütexte im Flash zB.

CodeBlocks habe ich verwendet um zu zeigen wie man auf dem PC sein Programm für einen Kontroller entwickeln kann. Dabei hilft einem der kompfortablere Debugger usw.. Man muß halt nur aufpassen, das man keine Befehle verwendet die der AVRGCC nicht kennt. Dies ist im vorliegenden Beispiel der Fall. Alle Ausgaben landen über eine definierte auf dem Kontroller leicht zu ersetzende Schnittstelle ( displayroutines.h und displayroutines.c) auf dem LCD. Andere Programmteile sind gekennzeichnet und können gelöscht werden (der einfache Timer). Es müssen nur noch Kontrollerspezifische Initialisierungen und Timerinterupts und Tastenabfragen programmiert werden. Die Grundlogik steht aber schon. Man kann sich jetzt auf diesen Teil der Aufgaben konzentrieren.

Downloads

Siehe auch