Gestione pulsanti

Ciao Forum,

Ho qualche idea per poter controllare 4 pulsanti. Già ho scritto un altro post, ma il problema rimane se usato con OneButton il sistema mi pare troppo caricato.
Quindi ho pensato di usare la funzione Pin Change del microcontrollore (al momento un ATmega328).
Le impostazioni sono riuscite, ma mi rimane il dubbio di come filtrare i rimbalzi e se la pressione è di un certo periodo.
C'è anche la libreria che gestisce gli interrupts on change del micro, ma capire gli esempi è una cosa che non so fare.
Qui un esempio che mi dice quale è il pulsante rilevato.

#define DEBUG

volatile uint8_t shadowC;
volatile uint8_t shadowB;
volatile bool triggered = 0;

ISR (PCINT1_vect)
 {
    shadowC = PINC;
    triggered = 1;
 }

ISR (PCINT0_vect)
 {
    shadowB = PINB;
    triggered = 1;
 }

void setup ()
{
    #ifdef DEBUG
    Serial.begin(57600);
    while (!Serial) {;}
    Serial.println(F("System Ready"));
    delay(500);
    #endif
    pinMode (12,INPUT_PULLUP);
    pinMode (13,INPUT_PULLUP);
    pinMode (14,INPUT_PULLUP);
    pinMode (15,INPUT_PULLUP);

    // pin change interrupt
    PCMSK1 |= bit (PCINT8) | bit (PCINT9);
    PCMSK0 |= bit (PCINT4) | bit (PCINT5);
    PCIFR  |= bit (PCIF1) | bit (PCIF0);    // clear any outstanding interrupts
    PCICR  |= bit (PCIE1) | bit (PCIE0);

}  // end of setup

void loop ()
{
    uint8_t oldSREG = SREG;     // remember if interrupts are on or off
    if (triggered) {            // an interrupt has occured
        noInterrupts ();        // turn interrupts off
        btn_state = shadowC & 0b00000011;       // access the shared variable
        btn_state |= (shadowB & 0b00110000) >>2;
        SREG = oldSREG;    // turn interrupts back on, if they were on before
        triggered = 0;
        switch (btn_state) {
            // SELECT
            case 7: {
                #ifdef DEBUG
                Serial.println("Pressed 13");
                delay(500);
                #endif
                break;
            }
            // MENU
            case 11: {
                #ifdef DEBUG
                Serial.println("Pressed 12");
                #endif
                break;
            }
            // MINUS
            case 13: {
                #ifdef DEBUG
                Serial.println("Pressed 15");
                #endif
                break;
            }
            // PLUS
            case 14: {
                #ifdef DEBUG
                Serial.println("Pressed 14");
                #endif
                break;
            }
        }
    }
}

Ora vorrei rilevare quando uno dei pulsanti cambia di stato. Penso che dovrei memorizzare lo stato precedente, per il quale viene attivato triggered e confrontarlo con quello attuale. Dal risultato dovrei cronometrargli il tempo che rimane nello stato basso.
Fosse semplice da implementare con EnableInterrupt, potrei anche cominciare a scrivere il programma con quella. Comunque, nel mio o altro modo, qualcosa verrà scritto per svolgermi il compito di gestione dei pulsanti.
Stavo pensando di modificare la struttura di OneButton, ma son ancora lontano dal capire come si programma in OOP in C++.

Mi sembra tutto piuttosto complesso...

Se non ho capito male serve rilevare:

  • l'istante della pressione
  • se la pressione dura oltre un certo tempo
  • l'istante del rilascio

Queste informazioni potrebbero essere portate da tre variabili chiamate_

  • Press
  • LongPress
  • Release

Partiamo con un solo pulsante, lo leggiamo (ipotizzando che premuto =1):

byte in = digitalRead(p1pin);

Gli applichiamo un debounch in attacco e rilascio di 40ms:

if (in == p1)                   p1T1 = millis();
else if (millis() - p1T1 > 40)  p1 = in;

Determiniamo se c'è un evento press o release:

p1press = (p1 & !p1Prec) ? 1 : 0;      // determina se istante della pressione
p1release = (!p1 & p1Prec) ? 1 : 0;    // determina se istante del rilascio
p1Prec = p1;                           // e aggiorna stato precedente

Avviamo e fermiamo un timer per controllare il long press:

// gestisce timer LongPress, avviato da press, fermato da release e timeout
p1LongPress = 0;
if (p1Press)                                   { p1T2 = millis(); p1T2act = 1; }
else if (p1Release)                            p1T2act = 0;
else if (p1T2act  &&  millis() - p1T2 > 1000)  { p1LongPress = 1; p1T2act = 0; }

Se tutto questo è messo nel loop e continua a girare (si parla di alcune decine di migliaia di esecuzioni al secondo), allora in questo punto del programma le variabili 'p1Press' 'p1Release' e 'p1LongPress' risulteranno attive (=1) per un ciclo nel momento in cui si verifica l'evento.

Affinché tutto funzioni servono le variabili globali:

byte      p1;
byte      p1Prec;
byte      p1Press;
byte      p1release;
byte      p1LongPress;
uint32_t  p1T1;
uint32_t  p1T2;
byte      p1T2act;

Che nella setup vanno inizializzate:

p1 = 0;
p1Prec = 0;
p1T1 = millis();
p1T2 = millis();
p1T2act = 0;

Niente interrupt. Niente librerie. Niente OOP.

Se sconfinfera, ovvero se è adatto alla logica che si vuole implementare, allora si può vedere di "compattare" tramite struct e funzioni, in modo da rendere il tutto estendibile a N pulsanti a piacere senza duplicare il codice.

Ciao Claudio.

Il mio pensiero è quello di sgarvare il loop() da cicli probabilmente non necessari. Perché il programma in completo ha anche altri compiti da tenere a bada.

Se il micro offre l' hardware per tenere a bada l' evento di un input come quello dei quattro pulsanti. Le ISR non rubano poi tanto, anzi penso che sia più veloce vedere un flag, che la lettura di 4 ingressi. Poi meno probabile di avere delle pressioni concorrenti, sebbene possibili.
In fondo con Pin Change Interrupt ho il segnale di quando viene premuto un pulsante e quando viene rilasciato. Si potrebbe per quel caso registrare la durata dell' evento.

Comunque, per capire quale sia il modo ottimale, forse bisognerebbe misurare quanti cicli prende, per ogni metodo adottato :slight_smile:

Ho iniziato il programma con quattro moduli di OneButton (vedasi questo post). Sto lavorando solo con la simulazione, non avendo tutto l' occorente per implementare fisicamente il circuito.
In molti casi quando simulo la pressione di un tasto, la simulazione comincia a calare i cicli macchina, fino alla velocità di 10 cicli al secondo (essendo a 16 MHz, ho un rallentamento di oltre un milione di volte). Forse è anche il carico di simulazione che grava sull' insieme della prova.

Il tuo concetto è molto simile a quello della libreria OneButton. Forse allegerito dal non tenere conto degli stati di double-click e un altro.

Pensavo di disossare la OneButton per impacchettare la chiamata in una sola funzione che passa il parametro di quale pulsante si ha da vedere. Ovviamente non servono complicazioni come quelle che si devono rispettare per fare una libreria.

Comunque, grazie dei suggerimenti. Voglio provare anche questo metodo. In fondo non sono a programmare in un gruppo, se ho delle variabili globali so che ci devo stare attento a non crearmi dei conflitti.
Poi alla fine con la programmazione per MCU le variabili globali sono un metodo comune. Più la cosa è semplice e meno rischi che non funzioni :wink:

Ah, se hai l' idea di generalizzare per una serie di pulsanti, aspetto qualche buon consiglio.

Grazie

Allora misuriamo i cicli... che 10 cyc/s mi sembrano davvero anomali.
Questa è la versione per quattro pulsanti (per comodità li considero pulluppati e si chiudono verso massa, se funzionano al contrario basta togliere il ! davanti alle digitalRead). I dati di ogni pulsante sono contenuti in una struct, in modo da passare ad un'unica funzione di gestione di volta in volta i dati relativi ai diversi pulsanti:

#define p1pin  2
#define p2pin  3
#define p3pin  4
#define p4pin  5

typedef struct {
    byte      p;
    byte      prec;
    byte      press;
    byte      release;
    byte      longPress;
    uint32_t  t1;
    uint32_t  t2;
    byte      t2act;
} pulsData_t;

pulsData_t p1 = {0, 0, 0, 0, 0, millis(), millis(), 0};
pulsData_t p2 = {0, 0, 0, 0, 0, millis(), millis(), 0};
pulsData_t p3 = {0, 0, 0, 0, 0, millis(), millis(), 0};
pulsData_t p4 = {0, 0, 0, 0, 0, millis(), millis(), 0};

//-------------------------------------------------------------------------

void gest(::pulsData_t* self, byte in) {
    if (in == self->p)                  self->t1 = millis();
    else if (millis() - self->t1 > 40)  self->p = in;

    self->press = (self->p & !self->prec) ? 1 : 0;
    self->release = (!self->p & self->prec) ? 1 : 0;
    self->prec = self->p;

    self->longPress = 0;
    if (self->press)
        { self->t2 = millis(); self->t2act = 1; }
    else if (self->release)
        self->t2act = 0;
    else if (self->t2act  &&  millis() - self->t2 > 3000)  
        { self->longPress = 1; self->t2act = 0; } 
}

//-------------------------------------------------------------------------

void setup() {
    pinMode(p1pin, INPUT_PULLUP);
    pinMode(p2pin, INPUT_PULLUP);
    pinMode(p3pin, INPUT_PULLUP);
    pinMode(p4pin, INPUT_PULLUP);
}

//-------------------------------------------------------------------------

void loop() {
    gest(&p1, !digitalRead(p1pin));
    gest(&p2, !digitalRead(p2pin));
    gest(&p3, !digitalRead(p3pin));
    gest(&p4, !digitalRead(p4pin));

    // qui si possono testare p1.press
    //                        p1.longPress
    //                        p2.release
    //                        p3.press
    //                        ecc ecc
}

Siccome delle simulazioni non mi fido mai, misuro con il frequenzimetro: al netto delle altre operazioni necessarie per la misura, tutta l'elaborazione di cui sopra porta via da un massimo di 55.15µs a ciclo, a un minimo di 45.1µs a ciclo.

Questo vuol dire che nel caso peggiore abbiamo quasi diciottomila cicli al secondo... (vanno aggiunti anche i 0.74µs del loop vuoto).

Scrivere il tutto sotto forma di classe rende solo il codice leggermente più pulito, i tempi rimangono gli stessi (abbiate pietà è la seconda classe che provo a scrivere in C++, non so neppure se il costruttore è corretto):

#define p1pin  2
#define p2pin  3
#define p3pin  4
#define p4pin  5

//-------------------------------------------------------------------------

class Puls {
        byte      p;
        byte      prec;
        uint32_t  t1;
        uint32_t  t2;
        byte      t2act;
    public:
        byte      press;
        byte      release;
        byte      longPress;
        void      gest(byte in);
                  Puls();
};

void Puls::gest(byte in) {
    if (in == p)                  t1 = millis();
    else if (millis() - t1 > 40)  p = in;

    press = (p & !prec) ? 1 : 0;
    release = (!p & prec) ? 1 : 0;
    prec = p;

    longPress = 0;
    if (press)
        { t2 = millis(); t2act = 1; }
    else if (release)
        t2act = 0;
    else if (t2act  &&  millis() - t2 > 3000)  
        { longPress = 1; t2act = 0; } 
}

Puls::Puls() {
    p = 0;
    prec = 0;
    press = 0;
    release = 0;
    longPress = 0;
    t1 = millis();
    t2 = millis();
    t2act = 0;
}

//-------------------------------------------------------------------------

Puls p1;
Puls p2;
Puls p3;
Puls p4;

//-------------------------------------------------------------------------

void setup() {
    pinMode(p1pin, INPUT_PULLUP);
    pinMode(p2pin, INPUT_PULLUP);
    pinMode(p3pin, INPUT_PULLUP);
    pinMode(p4pin, INPUT_PULLUP);
}

//-------------------------------------------------------------------------

void loop() {
    p1.gest(!digitalRead(p1pin));
    p2.gest(!digitalRead(p2pin));
    p3.gest(!digitalRead(p3pin));
    p4.gest(!digitalRead(p4pin));

    // qui si possono testare p1.press
    //                        p1.longPress
    //                        p2.release
    //                        p3.press
    //                        ecc ecc
}

Adesso è da valutare se questa logica è compatibile con il resto, cioè se il resto la può chiamare almeno 50..100 volte al secondo.

Per gli interrupt dovrei pensarci... il problema maggiore mi sembrerebbe il debounch software, che credo sarebbe meglio spostarlo in hardware (in modo da avere sempre dei "change" puliti). La funzione per controllare il long time andrebbe comunque pollata a software.

Per quanto riguarda le variabili globali invece secondo me su una MCU con memoria risicata il problema non si pone: non andrebbe mai usata la memoria dinamica (per applicazioni critiche questa è una norma inderogabile), e lo spazio di stack può esaurirsi rapidamente, quindi ok per una bella "lavagna" di lavoro statica di dimensioni note.

Ancora grazie del prezioso lavoro.
Per me è ancora black magic, non sono così ferrato con C++. Ho più conoscenze con python. Però quel self-> mi è quasi familiare :slight_smile:

Non si potrebbe tenere t1 e t2 sull' ordine di uint16_t? (il periodo da considerare non supera i 3000 millisecondi. Il preprocessore del compilatore dovrebbe farmi il cast giusto)

Potrei fare una nota che i valori di longpress e di debounce da metterli in una #define, almeno si possono aggiustare alla bisogna all' inizio dello sketch. Almeno io li metto sempre all' inizio e non serve andare alla caccia in tutti i files.
Anche il calcolo di comparativo di millis() lo fare fare dopo l' if, così il calcolo è fatto solo una volta e non tutte le volte che ci passa. Si guadagna qualcosina.
Così:

#define DELAY 500;

if (millis() >= time_elapsed) {
   time_elapsed = millis() + DELAY;
   // il resto del caso
}

Scusa se approfitto un poco :blush:
Ho pensato di fare alcune modifiche, ma rintracciare l' errore a colpo d' occhio, ci vuole esperienza.

#define MENU  12
#define SELECT  13
#define PLUS  14
#define MINUS  15
#define TMPRESS 300                 // press button delay
#define LNGPRESS 1200               // long press button delay
volatile uint8_t shadowC;
volatile uint8_t shadowB;

uint8_t oldSREG = SREG;     // remember if interrupts are on or off
uint8_t btn_state;
uint8_t prev_btn_state = 0x0f;
volatile bool triggered = 0;

ISR (PCINT1_vect)
 {
    shadowC = PINC;
    triggered = 1;
 }

ISR (PCINT0_vect)
 {
    shadowB = PINB;
    triggered = 1;
 }

typedef struct {
    uint8_t        p;
    uint8_t        prec;
    uint8_t        press;
    uint8_t        release;
    uint8_t        longPress;
    uint32_t    t1;
    uint32_t    t2;
    uint8_t     t2act;
} pulsData_t;

pulsData_t menu = {0, 0, 0, 0, 0, millis(), millis(), 0};
pulsData_t select = {0, 0, 0, 0, 0, millis(), millis(), 0};
pulsData_t plus = {0, 0, 0, 0, 0, millis(), millis(), 0};
pulsData_t minus = {0, 0, 0, 0, 0, millis(), millis(), 0};

//-------------------------------------------------------------------------

void gest(::pulsData_t* self, uint8_t in)
{
    if (in == self->p) self->t1 = millis() + TMPRESS;
    else if (millis() - self->t1) self->p = in;
    self->press = (self->p & !self->prec) ? 1 : 0;
    self->release = (!self->p & self->prec) ? 1 : 0;
    self->prec = self->p;
    self->longPress = 0;
    if (self->press) {
        self->t2 = millis() + LNGPRESS;
        self->t2act = 1;
    }
    else if (self->release)
        self->t2act = 0;
    else if (self->t2act  &&  (millis() > self->t2)) {
        self->longPress = 1;
        self->t2act = 0;
    }
}

//-------------------------------------------------------------------------

void setup()
{
    #ifdef DEBUG
    Serial.begin(57600);
    while (!Serial) {;}
    Serial.println(F("System Ready"));
    delay(500);
    #endif
    pinMode(MENU, INPUT_PULLUP);
    pinMode(SELECT, INPUT_PULLUP);
    pinMode(PLUS, INPUT_PULLUP);
    pinMode(MINUS, INPUT_PULLUP);
    // pin change interrupt
    PCMSK1 |= bit (PCINT8) | bit (PCINT9);
    PCMSK0 |= bit (PCINT4) | bit (PCINT5);
    PCIFR  |= bit (PCIF1) | bit (PCIF0);    // clear any outstanding interrupts
    PCICR  |= bit (PCIE1) | bit (PCIE0);
}

void loop()
{
    // checking when a button has changed state and verify how long passed
    if (triggered) {
        btn_state = shadowC & 0b00000011;       // access the shared variable
        btn_state |= (shadowB & 0b00110000) >>2;
        SREG = oldSREG;    // turn interrupts back on, if they were on before
        if (prev_btn_state != btn_state) {
            prev_btn_state = btn_state;
            gest(&menu, !digitalRead(MENU));
        }
        triggered = 0;
    }
    if (menu.press) {
        #ifdef DEBUG
        Serial.println("menu pressed");
        delay(500);
        #endif
    }
    if (menu.longPress) {
        #ifdef DEBUG
        Serial.println("menu pressed for long");
        delay(500);
        #endif
    }
    if (menu.release) {
        #ifdef DEBUG
        Serial.println("menu released");
        delay(500);
        #endif
    }
}

Il tuo esempio lo compila ma questo mi da

pulscheck.ino:45:13: error: variable or field 'gest' declared void
 void gest(::pulsData_t* self, uint8_t in)
             ^
pulscheck.ino:45:11: error: '::pulsData_t' has not been declared
 void gest(::pulsData_t* self, uint8_t in)
           ^
pulscheck.ino:45:25: error: 'self' was not declared in this scope
 void gest(::pulsData_t* self, uint8_t in)
                         ^
pulscheck.ino:45:39: error: expected primary-expression before 'in'
 void gest(::pulsData_t* self, uint8_t in)
                                       ^
exit status 1

Poi in un certo senso, mi pare anche dispendioso dare un byte per quelle condizioni che hanno valore solo 0 oppure 1.
Forse tutti quei stati logici (boolean) li posso far stare in un bit e mi risparmio 5 bytes. Al momento tutto il progetto ho raggiunto il 43% di RAM.

Pensavo si potesse fare:

typedef struct {
    unsigned      p         :1;
    unsigned      prec      :1;
    unsigned      press     :1;
    unsigned      release   :1;
    unsigned      longPress :1;
    unsigned      t2act     :1;
    unsigned                :1;
    unsigned                :1;
    uint32_t  t1;
    uint32_t  t2;
} pulsData_t;

pulsData_t p1 = {0, millis(), millis()};
pulsData_t p2 = {0, millis(), millis()};
pulsData_t p3 = {0, millis(), millis()};
pulsData_t p4 = {0, millis(), millis()};

//-------------------------------------------------------------------------

void gest(::pulsData_t* self, byte in) {
    if (in & self->p)                  self->t1 = millis();
    else if (millis() - self->t1 > 40)  self->p = (in & 1);

    self->press = (self->p & !self->prec) ? 1 : 0;
    self->release = (!self->p & self->prec) ? 1 : 0;
    self->prec = self->p;

    self->longPress = 0;
    if (self->press)
        { self->t2 = millis(); self->t2act = 1; }
    else if (self->release)
        self->t2act = 0;
    else if (self->t2act  &&  millis() - self->t2 > 3000)
        { self->longPress = 1; self->t2act = 0; }
}
  1. Il tuo codice a me complila regolare, non ho idea di cosa sia quell’errore.

  2. Si, si possono usare variabili tempo a 16 bit con overflow ogni 65536ms.

  3. Per non avere problemi con l’overflow di millis vanno usate solo sottrazioni intere.

In sostanza con variabili a 16 bit va usata sempre la forma:

if( (uint16_t)millis() - t <condizione> <durata>)
  1. Vero, usare un byte per contenere un bit di stato spreca memoria, però semplifica le espressioni ed è più veloce che andare a testare / settare / resettare ogni volta un singolo bit con le funzioni bitRead / bitSet / bitClear.
  1. Io lo provato con l' Arduino IDE 1.8.2 o 1.8.1, ma da errore come esposto.

  2. Capito, meglio usare il cast in modo esplicito. Sebbene credo che il compilatore lo fa senza lamentarsi.

  3. Usato nella struct e non usando le varie funzioni/macro di verifica a bit, ma solo if(Vero/Falso) non penso si vada ad appesantire il programma.
    Nel particolare come if(pi.press){}, il compilatore dovrebbe risolvere in assembly nel modo più pulito di verifica a bit della locazione nella struct.

Ho notato, col simulatore, che sebbene tenuto premuto a lungo, viene comunque rilevato il press e quindi il longPress, insieme.

Claudio_FF:
4) Vero, usare un byte per contenere un bit di stato spreca memoria, però semplifica le espressioni ed è più veloce che andare a testare / settare / resettare ogni volta un singolo bit con le funzioni bitRead / bitSet / bitClear.

Mmm … se fossero veramente delle funzioni avresti ragione, ma …
… nel “core” sono definte come delle semplice #define:

#define bitRead(value, bit) (((value) >> (bit)) & 0x01)
#define bitSet(value, bit) ((value) |= (1UL << (bit)))
#define bitClear(value, bit) ((value) &= ~(1UL << (bit)))
#define bitWrite(value, bit, bitvalue) (bitvalue ? bitSet(value, bit) : bitClear(value, bit))

… che fanno un paio di operazioni bitwise, quindi nessuna chiamata a funzione con conseguente perdita di tempo, ma pochi cicli macchina per fare shift ed and/or :wink:

Guglielmo

P.S.: … a completare ne aggiungerei anche un’altra, così da avere tutte le possibilità :smiley: :

#define bitToggle(value, bit) ((value) ^=  (1UL << (bit)))

gpb01:
fanno un paio di operazioni bitwise, quindi nessuna chiamata a funzione con conseguente perdita di tempo

Ah, ottimo, così non serve scriversi le proprie espressioni :slight_smile:

ExperimentUno:
Ho notato, col simulatore, che sebbene tenuto premuto a lungo, viene comunque rilevato il press e quindi il longPress, insieme.

Certo, tutti gli eventi elementari vengono "flaggati" nel momento in cui avvengono. Se serve qualcos'altro, tipo rilevare uno short click, allora si tratta di rilevare un evento release che avviene prima dello scadere del tempo di un longpress. Ad esempio nella funzione gest basta aggiungere (appena dopo l'aggiornamento della variabile self->prec ) la riga:

self->click = (self->release && self->t2act) ? 1 : 0;

(naturalmente nella struct va aggiunta una variabile click).

Al momento della pressione c'è l'evento 'press'.
Se si tiene premuto a lungo, c'è il 'longpress' seguito dal 'release' quando si rilascia.
Se si tiene premuto per poco, ci sono il 'click' e il 'release' contemporanei quando si rilascia.

Bene!
Faccio qualche studio, sulle possibilità di implementare i consigli. Penso che potrei aggiungere anche il Pin Change Interrupt, per ridurre il polling al solo momento che viene premuto un pulsante.

Vorrei capire se usando un membro a bit di una struct, posso usare come condizione booleana (if o while), meglio delle macro bitwise ?