Momentary Pushbuttons to Toggle Dual Coil Latching Relays On/Off

JTEX

Well-known member
This code uses momentary, SPST, normally-open pushbuttons (or stompswitches) to toggle On/Off some dual coil latching relays. I'm not a seasoned Arduino programmer by any stretch, so it took me quite a while to come up with it. Especially since I had to figure out a way to cut down on power as much as possible, for battery operation, so I had to use sleep mode and hardware interrupts when buttons are pressed. First time using either...

Hope it helps someone. Do whatever you want with it.

a.png


C:
// ****** 3 momentary pushbuttons (tactile switches) toggle 3 dual coil DPDT latching relays (push on/push off) ******
// Coded by Jerry Catanescu (jtex.ca)
// VERSION 0.1, 2022-12-28
//
// Works with Digispark Pro (ATtiny167 Pro). Optimised for minimal power use
// (go to sleep mode whenever idle, use interrupts to wake up only when a button is pressed)
//
// I used Kemet EA2-5TNJ dual coil latching relays. A 5V-powered ATtiny167/87 can drive them directly. Not even discrete flyback diodes are needed.
// The momentary pushbuttons are NO (normally open).
// One pin of every pushbutton, as well as the negative pins of all relay coils, are connected to ground.



#include <avr/power.h>
#include <avr/sleep.h>
#include <avr/interrupt.h>

const byte rly_setpin[3] = { 6, 8, 10 };  // relay +SET pins
const byte rly_rstpin[3] = { 7, 9, 11 };  // relay +RESET pins
const byte btn_pin[3] = { 0, 1, 2 };      // pushbutton pins (note: there's an LED on pin 1 Digispark Pro. I removed it. You can leave it alone and use another pin if you prefer.)
bool rly_state[3] = { 0, 0, 0 };          // array containing the current relay states:  0 = RESET, 1 = SET



// setup() runs only once when program starts:
void setup() {
  for (byte i = 0; i < 12; i++) {
    pinMode(i, INPUT_PULLUP);  // initially set all pins to INPUT_PULLUP, which reduces current draw
  }

  // iterate through all relays, set up their pins and initialize
  for (byte i = 0; i < 3; i++) {
    pinMode(rly_setpin[i], OUTPUT);  // configure all SET coil pins as OUTPUT
    pinMode(rly_rstpin[i], OUTPUT);  // configure all RESET coil pins as OUTPUT
    digitalWrite(rly_setpin[i], 0);  // initialize SET pins to 0
    digitalWrite(rly_rstpin[i], 1);
    delay(10);
    digitalWrite(rly_rstpin[i], 0);  // send pulse to latch the relays in the RESET state
  }

  // Turn off unused ATtiny167 subsystems to save power
  power_adc_disable();  // Analog to Digital Converter
  power_lin_disable();  // LIN module
  power_spi_disable();  // Serial Peripheral Interface
  power_usi_disable();  // Universal Serial Interface
  power_timer1_disable();


  // Pin Change Mask Register 1. Applies to Port B pins (PB0, 1...). See ATtiny167 datasheet
  PCMSK1 = 0b00000111;  // Turn on interrupt triggering for the specified pin(s) (logic 1)

  // Pin Change Interrupt Control Register. See ATtiny167 datasheet.
  PCICR = 0b00000010;   // Second bit from the right is the Port B bank.

  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
}



// Interrupt handler fires whenever there is a logic change on the designated Port B interrupt pin(s) (where the pushbuttons are wired)
ISR(PCINT1_vect) {
  delayMicroseconds(5000);      // just wait a bit to avoid firing multiple times due to bouncy button
                                // then we simply exit and return control to the main loop()
}


// latch relays in the SET or RESET state
void latchRelay() {
  noInterrupts();  // disable interrupts, to ignore any button activity (glitches, double-presses etc) that may happen until we're done latching the relay (debounce)
  bool btnLastState[3] = { HIGH, HIGH, HIGH };    // for starters, initialize all button states as "up" (logic HIGH)

  do {
    for (byte i = 0; i < 3; i++) {
      if (digitalRead(btn_pin[i]) == LOW) {       // if this button # was pushed
        // delayMicroseconds() works even while the interrupts are turned off, unlike delay()
        delayMicroseconds(3000);                      // wait a bit to account for bouncy contacts.
        if (digitalRead(btn_pin[i]) == LOW) {         // read button again after a delay, to insure it's still down (no glitches)
          delayMicroseconds(2000);
          if (digitalRead(btn_pin[i]) == LOW) {       // read button state the 3rd time. If it's still low, we accept it as stable
            if (btnLastState[i] == HIGH) {
              rly_state[i] = !rly_state[i];           // toggle the relay state
              btnLastState[i] = LOW;                  // record the fact that the button was pressed

              if (rly_state[i]) {                     // if the relay should be latched to the SET state
                digitalWrite(rly_setpin[i], 1);
                delayMicroseconds(5000);              // Hold the logic "1" long enough to latch the relay. 10ms shoud do it.
                digitalWrite(rly_setpin[i], 0); 
              } else {                                // if the relay should be RESET
                digitalWrite(rly_rstpin[i], 1);
                delayMicroseconds(5000);
                digitalWrite(rly_rstpin[i], 0);
              }
            }
          }
        }
      } else {                                    // if this button # wasn't pushed
        btnLastState[i] = HIGH;
      }

      // optional: do something if all 3 buttons are pressed simulaneously
      if (!digitalRead(btn_pin[0]) && !digitalRead(btn_pin[1]) && !digitalRead(btn_pin[2])) {  // all three buttons pressed
        // do something awesome, maybe? Such as, true bypass? TBD.
      } else {
        // placeholder
      }
      
    }
  } while (!digitalRead(btn_pin[0]) || !digitalRead(btn_pin[1]) || !digitalRead(btn_pin[2]));  // stay in this loop as long as at least one button is pressed

  interrupts();     // re-enable interrupts before exiting function
}



/////////////////////////  MAIN PROGRAM LOOP ///////////////////////////////////
void loop() {
  sleep_cpu();   // Sleep all the time except when woken by an interrupt (meaning that a button was pressed)
  latchRelay();  // If we ever get here, we got kicked out of sleep by a button press, so we need to handle the relays
}
 
Last edited:
Geeks gotta geek.. playing around with this to use the Bounce2 library to handle debouncing, and saving the state to persistent EEPROM and it works pretty well.

Just for the LED but you can extend it easily to include relay pins. This is for ATtiny85, I know I have superfluous methods (pinOn, ledOn, etc.) but these helps my readability:

Code:
This doesn't work with sleep mode.
 
Last edited:
Geeks gotta geek.. playing around with this to use the Bounce2 library to handle debouncing, and saving the state to persistent EEPROM and it works pretty well.

Just for the LED but you can extend it easily to include relay pins. This is for ATtiny85, I know I have superfluous methods (pinOn, ledOn, etc.) but these helps my readability:
I hadn't thought about saving states in EEPROM. Never used that before. Could be useful. Hmmmm... food for thought.
How many write cycles can the EEPROM handle before it goes bad? I would want to be careful not to write to it too many times.
 
The EEPROM write cycle lifetime is 100,000. Read is unlimited.
I’m on the fence with it, but 100K is a lot of footswitch cycles..
I think it’s more useful to track a configuration state, like choosing between operational modes. Who really needs a relay to remember state when you power on?
 
The EEPROM write cycle lifetime is 100,000. Read is unlimited.
I’m on the fence with it, but 100K is a lot of footswitch cycles..
I think it’s more useful to track a configuration state, like choosing between operational modes. Who really needs a relay to remember state when you power on?
So you can reprogram a 24 series EEPROM up to around 100k times? Am I understanding you correctly?
 
Who really needs a relay to remember state when you power on?
If you have an ‘always on’ effect or one that you typically use, that’s a useful feature.

In terms of advanced control, I really like NOT functionality while held (i.e., if effect is on, off while held) in addition to standard state toggling.
 
If you have an ‘always on’ effect or one that you typically use, that’s a useful feature.

In terms of advanced control, I really like NOT functionality while held (i.e., if effect is on, off while held) in addition to standard state toggling.
It would be relatively simple to save a default startup state. The NOT functionality seems also fairly simple to do, and having it as standard is not such a bad idea.
 
I think I have the code for the NOT function. I need to test it when I get back to "the lab" tomorrow. Basically, it reverts back to the previous state if you hold the switch down more than 500ms.

Code:
void latchRelay() {
  cli(); // disable interrupts
  if(state) { // turn the relay on
    setRelay();
  }
  else { // turn it off
    unsetRelay();
  }
  while(true) {
    sw.update();
    if (sw.rose()) {
      if (sw.previousDuration() > 500) { // if the switch was held down >500ms, revert back to previous state
        state = !state;
        if(state) { // turn the relay on
          setRelay();
        }
        else { // turn it off
          unsetRelay();
        }
        break;
      }
      else {
        break;
      }
    }
  }
  sei(); // enable interrupts
}
 
Good news and bad news. Using the Bounce2 library to handle debouncing the switch works really well, and the NOT function is easily implemented. (Debouncing stops false positives from happening. Without it, a single button press can be interpreted as multiple presses).
Bad news is that it doesn't work with interrupts, so you can't put the controller to sleep. This is not such an issue for me, I'm not expecting to run it on battery and there's enough current to go around. I'll likely revisit the power side at some stage, but I prefer being able to have Bounce2 handle the switch state easily for me.

Code:
#include <Arduino.h>
#include <avr/power.h>
#include <Bounce2.h>
// Made for ATtiny85
const int pinButton = 3;    // PB3 == pin 2 --> SPST
const int pinRelaySet = 4;    // PB4 == pin 3 --> relay coil set (first pin)
const int pinRelayReset = 0;    // PB0 == pin 5 --> relay coil reset (last pin)
const int pinLed = 2;    // PB2 == pin 7 --> LED
Bounce pedal1 = Bounce();
const int debounceInterval = 3;
int state = 0;
void pinOn(int pin) {
  digitalWrite(pin, HIGH);
}
void pinOff(int pin) {
  digitalWrite(pin, LOW);
}
void ledOn(int pin) {
  pinOff(pin);
}
void ledOff(int pin) {
  pinOn(pin);
}
void relayLatch(int pin) {
  pinOn(pin); // tell relay to latch
  delayMicroseconds(5000); // wait for the latch
  pinOff(pin); // stop telling it to latch
}
void relayToggle() {
  if(state) { // turn the relay on
    ledOn(pinLed);
    relayLatch(pinRelaySet);
  }
  else { // turn it off
    ledOff(pinLed);
    relayLatch(pinRelayReset);
  }
}
void setup () {
  power_adc_disable(); // save some power
  for (byte i = 0; i < NUM_ANALOG_INPUTS; i++) {
    pinMode(i, INPUT_PULLUP);  // initially set all pins to INPUT_PULLUP, which reduces current draw
  }
  ledOff(pinLed); // Start with the LED off
  pinMode(pinLed, OUTPUT); // Set the LED pin as OUTPUT
  pinMode(pinRelaySet, OUTPUT); // Set relay set pin as output
  pinOff(pinRelaySet);  // turn it off
  pinMode(pinRelayReset, OUTPUT); // set relay reset ping as OUTPUT
  pedal1.attach(pinButton);
  pedal1.interval(debounceInterval);
  relayToggle(); // initialise in the required state
}
void loop() {
  pedal1.update();
  if(pedal1.changed()) {
    if(pedal1.read() == LOW) { // if button was pressed
      state = !state;
      relayToggle();
    }
    else { // if button was released
      if (pedal1.previousDuration() > 100) { // if you hold it down longer, revert back to the original state. NOT function.
        state = !state;
        relayToggle();
      }
    }
  }
}
 
This is fantastic. Do you happen to have your implementation schematic documented too?
Not yet, but I doubt if it'll look any different from the PPCB one, or the one Jerry has above.
It's only about supplying 5V to the controller, and then connecting the other things to the right pins and ground.
 
Back
Top