Con questo progetto mostro come si può realizzare un relè passo-passo con Arduino.
Un relè passo-passo è un relè che può essere comandato da un singolo pulsante e mantenere il suo stato fino al comando successivo.
In pratica quando si preme il pulsante viene eccitato il relè una successiva pressione del pulsante lo diseccita rimanendo nello stato eccitato/diseccitato fino al successivo comando .

Questo è il grafico che descrive il funzionamento del relè passo-passo
Grafico relè Passo-passoGrafico relè Passo-passo
Il desiderata è che sul fronte di salita, avvero quando lo stato passa da basso (false) ad altro (true) il relè cambia stato.

Lo schema elettrico del circuito è questo
Schema elettricoSchema elettrico

Soluzione 1

Si potrebbe pensare di risolverlo in questo modo:
Arduino
const String VERSION_STR = "Relè passo passo v. 1.2021-07-10 - Sgart.it";

/*
 * Definizione IO
 */
const byte RELE = 2; // relè salita - Arduino GPIO pin to use. (D2).

const byte BUTTON = 5; // pulsante salita - Arduino GPIO pin to use. (D5).

/*
 * variabili
 */
boolean  rele = false; 

/*
 * Setup
 */
void setup() {
  Serial.begin(115200);
 
  Serial.println("Set pin ...");

  // conigurazione output
  pinMode(RELE, OUTPUT);

  // configurazione input con resistenza di pullup
  pinMode(BUTTON, INPUT_PULLUP);

  Serial.println(VERSION_STR);
}

/*
 * Loop
 */
void loop() {
  // lettura ingresso pulsante (quando premuto = true)
  bool pinButton = digitalRead(BUTTON) == LOW;

  if (pinButton == true && rele == false) {
    // se era diseccitato lo attivo
    Serial.println("ON");
    rele = true;
  } else if (pinButton == true && rele == true) {
    // se era eccitato lo spengo
    Serial.println("OFF");
    rele = false;
  } 

  // comando l'uscita del relè (quando true = acceso)
  digitalWrite(RELE, rele == true ? LOW : HIGH);
}
ma si può facilmente notare, attivando il monitor seriale, che il relè cambia stato molte volte nel giro di pochi millisecondi rendendo imprevedibile lo stato finale del relè.

Questo perché il metodo loop viene invocato di continuo con una frequenza dell'ordine dei microsecondi, essendo il pulsante premuto durante questi cicli, lo stato del relè continua a cambiare
Monitor serialeMonitor seriale

Soluzione 2 (impulso)

Quello che servirebbe è comandare il relè solo durante il fronte di salita, ignorando i cambiamenti successivi nel breve periodo.

Per far questo si può generare un impulso, sul fronte di salita, che dura un solo ciclo della funzione loop.
Questo lo si ottiene memorizzando lo stato precedente del pulsante in una variabile (vedi dopo buttonPrev)
Grafico relè Passo-passo con impulsoGrafico relè Passo-passo con impulso

Questa è la nuova implementazione del loop
Arduino
// stato precedente pulsante
boolean buttonPrev = false;

void loop() {
  // lettura ingresso pulsante
  bool pinButton = digitalRead(BUTTON) == LOW;

  
  // impulso / fronte di salita pulsante
  bool pulse = pinButton == true && buttonPrev == false;

  if (pulse == true && rele == false) {
    Serial.println("ON");
    rele = true;
  } else if (pulse == true && rele == true) {
    Serial.println("OFF");
    rele = false;
  } 

  digitalWrite(RELE, rele ? LOW : HIGH);

  // memorizzo lo stato precedente del pulsante
  buttonPrev = pinButton;
}
Funziona molto meglio, ma ancora non ci siamo, in alcune situazioni il comportamento è incerto.
Monitor serialeMonitor seriale

Soluzione finale (antirimbalzo)

L'incertezza è dovuta al fatto che il pulsante, quando premuto, non sempre genera un contatto certo, a volte per alcuni millisecondi ci possono essere dei cambiamenti di stato come riportato nel grafico seguente (vedi grafico input)
Grafico con ingresso sporcoGrafico con ingresso sporco
Quello che serve è un circuito antirimbalzo che possa evitare gli ingressi sporchi finché il contatto non si stabilizza. Si può raggiungere lo scopo tramite un timer che inibisce gli ingressi per circa 200 millisecondi dopo aver ricevuto il primo impulso.

Per gestire il timer uso la libreria arduino-timer. I passi per usarla sono:
  • includere il file arduino-timer.h
  • inizializzare il timer
  • impostare la funzione di callback
  • chiamare il metodo tick() nel loop per aggiornare il timer
  • definire la la variabile che inibisce l'ingresso (timerButtonDisabled)
Arduino
#include <arduino-timer.h>

// timer antirimbalzo
boolean  timerButtonDisabled = false;

/*
 * Timer
 */
const int TIMER_BUNTTON_DISABLED_MILLISEC = 200; // 0.2 sec

// inizializzazione timer 
Timer<1, millis> timer; // inizializo x 1 timer

// funzione di callbak del timer
bool handleTimerButton(void *argument) {
  Serial.println("Timer timerButton fine");
  timerButtonDisabled = false;
  return false;  // ritornare sempre false
}

// variabile x timer da usare poi
auto timerButton = timer.in(1, [](void*) -> bool { return false; });

...

void loop() {
  // gestione timer (importante altrimenti il timer non funziona)
  timer.tick();
 ...
}
Il vantaggio di usare un timer anziché la funzione delay() è che questa mette in pausa l'esecuzione del programma finché non è passato il tempo impostato.
Il timer permette, nel caso servisse, di gestire più timer contemporaneamente senza bloccare l'esecuzione del programma.

A questo punto va modificato il loop in questo modo
Arduino
void loop() {
  // gestione timer (importante altrimenti il timer non funziona)
  timer.tick();
  
  // lettura ingresso pulsante
  bool pinButton = digitalRead(BUTTON) == LOW;
  
  // impulso / fronte di salita pulsante
  bool pulse = pinButton == true && buttonPrev == false && timerButtonDisabled == false;
  // Serial.print("Pulse: ");  Serial.print(pulse); Serial.print(", timerButtonDisabled: ");  Serial.println(timerButtonDisabled);
  
  if (pulse == true) { 
    if (timerButtonDisabled == false) {
      // antirimbalzo
      Serial.println("Start timerButtonDisabled");
      timerButtonDisabled = true;

      // avvio il timer che disabilita l'ingresso
      timer.cancel(timerButton);
      timerButton = timer.in(TIMER_BUNTTON_DISABLED_MILLISEC, handleTimerButton);
    }
    // comando relè
    Serial.print("Rele: ");
    if (rele == false) {
      Serial.println("ON");
      rele = true;
    } else {
      Serial.println("OFF");
      rele = false;
    } 
  }
  
  digitalWrite(RELE, rele ? LOW : HIGH);

  // memorizzo lo stato precedente del pulsante
  buttonPrev = pinButton;
}
Il timer viene avviato con l'istruzione timer.in(delay, function_to_call), quando viene raggiunto il tempo impostato (delay), richiama la funzione function_to_call.

Il codice completo è questo
Arduino
/*
   Sgart.it Relè passo-passo
   Version 1.2021-07-10
   Copyright 2021 Sgart.it
   https://www.sgart.it/IT/elettro/rele-passo-passo-con-arduino/post
   Board: 'Arduino UNO'
   Port: COM7
*/
#include <arduino-timer.h>

const String VERSION_STR = "Relè passo passo v. 1.2021-07-10 - Sgart.it";

/*
 * Definizione IO
 */
const byte RELE = 2; // relè salita - Arduino GPIO pin to use. (D2).

const byte BUTTON = 5; // pulsante salita - Arduino GPIO pin to use. (D5).

/*
 * variabili
 */
// stato precedente pulsante
boolean buttonPrev = false;

// timer antirimbalzo
boolean  timerButtonDisabled = false;

// comando relè
boolean  rele = false; 

/*
 * Timer
 */
const int TIMER_BUNTTON_DISABLED_MILLISEC = 200; // 0.2 sec
 
Timer<1, millis> timer; // inizializo x 1 timer

bool handleTimerButton(void *argument) {
  Serial.println("Timer timerButton fine");
  timerButtonDisabled = false;
  return false;
}

auto timerButton = timer.in(1, [](void*) -> bool { return false; });

/*
 * Setup
 */
void setup() {
  Serial.begin(115200);
 
  Serial.println("Set pin ...");

  // conigurazione output
  pinMode(RELE, OUTPUT);

  // configurazione input con resistenza di pullup
  pinMode(BUTTON, INPUT_PULLUP);

  Serial.println(VERSION_STR);
}

/*
 * Loop
 */
void loop() {
  // gestione timer (importante altrimenti il timer non funziona)
  timer.tick();
  
  // lettura ingresso pulsante
  bool pinButton = digitalRead(BUTTON) == LOW;
  
  // impulso / fronte di salita pulsante
  bool pulse = pinButton == true && buttonPrev == false && timerButtonDisabled == false;
  // Serial.print("Pulse: ");  Serial.print(pulse); Serial.print(", timerButtonDisabled: ");  Serial.println(timerButtonDisabled);
  
  if (pulse == true) { 
    if (timerButtonDisabled == false) {
      // antirimbalzo
      Serial.println("Start timerButtonDisabled");
      timerButtonDisabled = true;

      // avvio il timer che disabilita l'ingresso
      timer.cancel(timerButton);
      timerButton = timer.in(TIMER_BUNTTON_DISABLED_MILLISEC, handleTimerButton);
    }
    // comando relè
    Serial.print("Rele: ");
    if (rele == false) {
      Serial.println("ON");
      rele = true;
    } else {
      Serial.println("OFF");
      rele = false;
    } 
  }
  
  digitalWrite(RELE, rele ? LOW : HIGH);

  // memorizzo lo stato precedente del pulsante
  buttonPrev = pinButton;
}
Potrebbe interessarti anche: