Gestione 4 tasti combinazione multiple e multitrigger

La gestione di 4 tasti può risultare banale ma non è questo il caso. Il codice gestisce correttamente i 4 tasti pressioni singole e multiple, tutti i trigger necessari sono implementati e lavorano correttamente. Si può interagire con l'app tramite tastiera o telecomando.

Quindi visto che il codice funziona, perché posti? Il motivo è che non sono contentissimo della implementazione. Vincoli hardware mi impongono di ricavare la pressione dei tasti usando un solo pin e 4 già usati per il display 4x7 seg, questa parte ve la risparmio.

La funzione di basso livello get_key_code() ritorna uno dei seguenti codici.

KNull = 0 KSet = 16 KExit = 2 KUp = 4 KDown = 8 KSet + KUp + KDown + KExit = 30 KExit + KDown = 10 KExit + KUp = 6 KExit + KSet = 18 KDown + KUp = 12 KDown + KSet = 24 KUp + KSet = 20

Inevitabilmente la pressione multipla genera un codice che poi muta, ad esempio: KExit + KSet genera 2 che muta in 18, ovviamente KSet + KExit genera 16 che muta in 18. PS: get_key_code() richiede circa ~10ms per collezionare un codice tasto valido. Essa ritorna sempre codice validi. Il tempo di 10ms è legato alla gestione del display in multiplex.

I trigger: Il trigger è il grilletto che scatta al fine di attivare codice. L'applicazione richiede i seguenti trigger:

  • 1) Trigger alla pressione: Si tratta di sentire il passaggio di stato da 0 -> 1, cioè da tasto released a tasto pressed. Viene usato per navigare nei menu

  • 2) Trigger alla pressione trasformabile Questo trigger si può trasformare in eventi di tempo e ciò avviene mantenendo premuto il tasto per un tempo > timeClicked che ipotizziamo essere di 400ms. Superato timeClicked il tasto è in repeat mode con tempo di ripetizione variabile. Questo tipo di trigger viene usato per modificare valori numerici a display (parametri), per alcuni parametri si incremento o decrementa in centesimi e questo richiede un tempo non accettabile per passare ad esempio da -50.0 a

  • 50.0, a tal fine il tempo di ripetizione tasto viene ridotto tanto più a lungo viene mantenuto premuto il tasto. Al rilascio il tempo di ripetizione si resetta al valore standard.

  • 3) Trigger a tempo Questo trigger serve per accedere a diversi menu, es KSet premuto per almeno 3 secondi da l'accesso al menu "Parametri comuni". Questo trigger permette pressioni multiple, es KSet + KUp per almeno 3 secondi permette lo spostamento di un item da un menu ad un altro, ad esempio un item presente nel 'User Menu' viene spostato nel 'Service Menu'.

Il codice attuale: void uiScanKey() { scanCode(&buttonSET); scanCode(&buttonEXIT); scanCode(&buttonUP); scanCode(&buttonDOWN); }

La funzione uiScanKey chiama scanCode per ogni pulsante, la chiamata a uiScanKey si trova nel loop.

Come triggere uso nel main questo codice:

if (buttonHasPressed(&buttonSET)) {

            currentComand = Enter;

        } else if (buttonOldPressed(&buttonSET, seconds_to_ms(3))) {

            currentComand = EnterLong;

        } else if (buttonOldPressed(&buttonUP, 1)) {

            currentComand = Increases;

       ....

Questo mi da la possibilità di interagire con l'applicazione tramite i 4 tasti e con telecomando allo stesso tempo. Ciò che fa questo codice è di intercettare il trigger e trasformarlo in codice comando. Il menu e l'applicazione ovviamente hanno interfaccia a comandi.

Ora viene la domanda: Cerco di stravolgere il tutto al fine di migliorare l'implementazione, cioè la semplicità di uso e la manutenzione. Cerco di individuare dei pattern da usare, ad esempio potrei usare un ring buffer di key code per i trigger (1) e (2), ma per il trigger (3) come lo gestisco?

Si accettano consigli, link, codice, critiche, ecc.

Ciao.

Dimenticavo: Non uso IDE e librerie Arduino, solo C std=gnu99.

Non è una domanda semplice, bisognerebbe analizzare tutto il codice per vedere come strutturarlo. Si potrebbe creare un vettore di funzioni che mappino le chiamate dei menu e delle varie funzionalità dei pulsanti, il tutto magari normalizzando il risultato ottenuto dei pulsanti se non proprio modificare la funzione get_key_code() per farle ritornare un valore più esplicativo.

Intanto ti ringrazio per l'intervento, io non mi aspettavo la risposta risolutiva, ma dei suggerimenti sui pattern che normalmente si usano in questi casi e il consiglio di seguire questa strada e scartare l'altra, però mi rendo conto che non è facile intervenire a ragion veduta perché manca l'attuale implementazione. Ma per ipotesi se il menu non fosse ancora implementato (cioè non mi spaventa riscriverlo e/o adattarlo) sarebbe un pelo più semplice?

se non proprio modificare la funzione get_key_code() per farle ritornare un valore più esplicativo.

mmmm... cosa intendi per "valore più esplicativo", no perché ho un margine minimo ma c'è. La get_key_code non fa altro che ritornare un codice che viene decodificato nella ISR di gestione del display 4x7 seg. Nella ISR viene chiamato solo questo codice:

static inline void
display_update()
{
    /* Prima di collezionare un key_code definitivo deve ciclare da 0 a 3 */
    if(idx_digit == N_DIGIT) {
        idx_digit = 0;
        key_code = key_status;
    }

    // read idx digit value from buffer and write this value to segment out of portb
    PORTB = buffer_display[ idx_digit ];

    RESET_OUT_CATHODE();
    SWITCH_ON_CATHODE(idx_digit);
    _asm_nop();     // don't remove me

    //if (PINC & _BV(PC2)) {  // sostituito da key_event
    if (key_event()) {
        key_status |= _BV(N_DIGIT - idx_digit);
    } else {
        key_status &= ~_BV(N_DIGIT - idx_digit);
    }
    // Quando (PINC & _BV(PC2)) = 1 e N_DIGIT - idx_digit = 1
    // la macro set_bit_to non opera correttamente

    //set_bit_to(key_status, (N_DIGIT - idx_digit), (PINC & _BV(PC2)));

    idx_digit++;

}

get_key_code() ritorna la variabile key_code, che risulta decodificata in 4 chiamate. Sto tentando di implementare un buffer di codici, i codici li inserisco in base ad intervallo di tempo, una cosa simile a: KeyBuffer { 16, // è entrato subito perché il timer è scarico e quindi carico il timer 16 // è entrato dopo es 100ms ricarico il timer 16 // n key code dopo siamo in repeat mode ricarico il timer // è entrato 90ms dopo ricarico il timer 16 // è entrato 30ms dopo ricarico il timer }

Che ne pensi, un tentativo conviene farlo?

Il codice al momento lavora e tutte le funzioni sono implementate, tranne il modulo statistic che comunque è sperimentale. La dimensione da avr-size: avr-size --mcu=atmega644a apptest1.hex text data bss dec hex filename 0 10574 0 10574 294e apptest1.hex

Il programma lavora anche su 168/328, ovviamente ci sono meno uscite e meno ingressi e prende un poco di flash in meno. Se non esce niente di nuovo tra un po, lascio tutto così e mi aiuto con la documentazione.

Ciao.

Mauro, perché non usi un semplice byte i cui bit sono associati a ciascun tasto?

In pratica hai fatto la stessa cosa, ma così è più semplice da gestire con operatori sui bit.

Esempio:

7654 3210 0000 0000 0x00 nessun tasto premuto 0000 0001 0x01 premuto tasto SET 0000 0010 0x02 premuto tasto EXIT 0000 0100 0x04 premuto tasto UP 0000 1000 0x08 premuto tasto DOWN

0000 0101 0x05 premuto tasto SET + UP 0000 1001 0x09 premuto tasto SET + DOWN ... 0000 1111 0x0F premuto tasto SET + EXIT + UP + DOWN

@cyberhs ha chiarito cosa intendessi per codice più significativo. Il discorso del menu non l'ho proprio ben capito.

Io creerei un albero statico e lo userei come base del menu, si potrebbe addirittura assegnare un pulsante a ramo ma non avendo ulteriori informazioni di quanti e come siano legati tra di loro ne tantopiu il significato dei pulsanti rimane difficile consigliarti.

Mauro, perché non usi un semplice byte i cui bit sono associati a ciascun tasto?

In pratica hai fatto la stessa cosa, ma così è più semplice da gestire con operatori sui bit.

    if (key_event()) {
        key_status |= _BV(N_DIGIT - idx_digit);
    } else {
        key_status &= ~_BV(N_DIGIT - idx_digit);
    }

N_DIGIT = 4
idx_digit va da 0 a 3 e viene incrementata ad ogni chiamata della ISR, circa 2ms
Quindi i dati sono collezionati a partire da N_DIGIT - idx_digit:
[ 4-0 = 4 ]
[ 4-1 = 3 ]
[ 4-2 = 2 ]
[ 4-3 = 1 ]
In pratica il bit 0 non è contemplato, il perché è fatto così non lo ricordo ma so che c’è.
Comunque di questa parte sono contentissimo, perché è facile da capire e modificare ed è efficiente e non ci sono side effect. Qui nella ISR posso ancora aggiungere qualcosa che richieda al massimo 1.8ms ad ogni chiamata, non l’ho fatto perché è stato sufficiente gestire tutto in loop.

La parte di codice che non mi piace è questa:

typedef struct aButton {
    uint16_t timeOldPressed;    //  tempo di pressione in secondi
    uint32_t saveTimer;         //  il valore del timer alla pressione
    uint8_t state;              //  stato del pulsante
    uint8_t speed;              //  velocità di ripetizione
    key_button_e kc;            //  il codice interno del tasto
} Button;

Button *lastButtonPtr = 0;

#define DEFSPEED    108

Button buttonSET    = {0, 0, 0, DEFSPEED, KSet};
Button buttonEXIT   = {0, 0, 0, DEFSPEED, KExit};
Button buttonUP     = {0, 0, 0, DEFSPEED, KUp};
Button buttonDOWN   = {0, 0, 0, DEFSPEED, KDown};

static key_button_e keyButton;

void scanCode(Button *const b);


void uiScanKey()
{
    scanCode(&buttonSET);
    scanCode(&buttonEXIT);
    scanCode(&buttonUP);
    scanCode(&buttonDOWN);
}

void scanCode(Button *const b)
{

    keyButton = get_key_code();
    if ((b->state == 0 ) && (keyButton == b->kc)) {
        b->state = 1;
        b->saveTimer = current_systimer_value;
    } else if ((keyButton == b->kc) && (current_systimer_value - b->saveTimer) > b->speed) {

        if (b->speed < 48)
            b->speed = 0;
        else
            b->speed -= 6;
        b->state = 3;
        b->timeOldPressed = current_systimer_value - b->saveTimer;

    } else if ((b->state == 3) && (keyButton == KNull)) {
        b->saveTimer = 0;
        b->speed = DEFSPEED;
        b->state = 0;
    } else if ((b->state == 1) && (keyButton == KNull)) {
        b->state = 2;
    }

}

Non mi piace perché:

  1. Ho 4 variabili di tipo Button (struct)
  2. Ad ogni ciclo di loop devo chiamare uiScanKey() che chiama 4 volte scanCode()
  3. scanCode() è difficile da seguire, ci vogliono 3 cervelli in parallelo, questo punto potrei anche farmelo piacere.
  4. Le funzioni di trigger hanno side effect determinato dall’ordine di chiamata, quindi dopo mesi e mesi appeno aggiungo qualcosa possono sorgere side effect che mi costringono ad andare a studiare l’implementazione di basso livello.

Le funzioni di trigger:

Bool buttonHasPressed(Button *const b)
{
    if (b->state == 2) {
        b->state = 0;
        return TRUE;
    }
    return FALSE;
}

Bool buttonOldPressed(Button *const b, uint16_t triggerTime)
{
    if (b->state == 3)  {
        if (b->timeOldPressed > triggerTime) {
            b->timeOldPressed = 0;
            b->saveTimer = current_systimer_value;
            return TRUE;
        }
    }
    return FALSE;
}

Non mi lamento neanche della efficienza, rapidità, reattività, insomma non digerisco quelle 4 chiamate a scanCode e i side effect delle funzioni di trigger. Intuisco che si possono evitare ma non ho ancora il pattern chiaro.

@vbextreme

Il discorso del menu non l’ho proprio ben capito.

Il discorso è che non ho problemi a stravolgere tutto, l’ho fatto decine di volte, ma se l’ho fatto c’era un motivo e il motivo
principale è quello di rendere facilmente modificabile o sostituibile un modulo senza subire pesanti side effect.

Quando l’applicazione ha molti moduli e librerie e si tenta una modifica importante l’applicazione si può rompere a tal punto che conviene riscriverla perché non la prendi più, con questa implementazione non accade, ci sono solo quei punti che ho elencato che possono rompere l’applicazione.

PS: L’applicazione principale non deve rispondere realtime, cioè ci può stare anche 1 secondo per prendere una decisione.

Io creerei un albero statico e lo userei come base del menu, si potrebbe addirittura assegnare un pulsante a ramo ma non avendo ulteriori informazioni di quanti e come siano legati tra di loro ne tantopiu il significato dei pulsanti rimane difficile consigliarti.

Fatto, c’è l’albero statico in FLASH con puntatori in eeprom e in ram. Ci sono Data Wrapper, Generic Number, ecc
Il menu usa solo quattro funzioni:
static void showStd();
static void showPar();
static void showValue();
static void showMenuName();

Anche il menu è ok come codice, grazie alla interfaccia a comandi posso portare il codice verso altre applicazioni che richiedono un menu. Quindi anche se il menu è ok, qualora avessi un pattern chiaro non ci penserei due volte a stravolgere tutto, tanto mi basta clonare il repo.

mmmm…sto per caso cercando il pelo nell’uvo? :smiley:
Ciao.