Tactile buttons are being flaky

I made an LED heart (similar to https://www.ourglowinghearts.ca/) using a Neopixel RGB neon-like strip, an Arduino Pro Mini 168, and two push buttons -- one to cycle between pre-programmed colours, and the other to cycle through pre-programmed modes.

Everything works great, except for the push/tactile buttons. When I press them normally, their effectiveness at actually changing the mode or colour of the strip is 50/50 at best. Sometimes it will make the appropriate change, other times it won't. It seems however, that smacking the button decently hard works pretty consistently, and holding the buttons down and rocking them around results in in constantly flipping between modes/colours (expected behaviour).

I'm struggling to determine whether or not they're not working properly because they're crappy and broken, or if I'm screwing something up with the hardware or programmatically. It's worth noting that I think a few months ago when I was working on this and using different tactile buttons, I think I had the same issue.

The schematic is as such -- the max power output available is 15W (5V 3A) and I measured a max consumption at one point around 8W. All "5V" bus sources are the same source.

The sketch is also below -- it is a bit long, however the portions pertaining to the button setup and monitoring are short/repetitive. I would have shortened it down but sometimes people here don't like that because the bug could have been in a deleted portion (that's fine, I understand). If I should upload a shortened version, I will do so.

But essentially, all I do is check if the button is pressed & that it hasn't been pressed in the last 250 number of milliseconds using an "if" statement rather than interrupts as a super crude "debouncing" method. When the buttons are working properly (e.g. when I smack them) this debouncing works quite well. And to clarify, it is NOT that I'm pressing the buttons too fast within the 250ms timeout, even if I wait a good 10 seconds the button's success at changing colour or mode is still 50/50.

Could someone potentially provide a sanity check? Or let me know if I'm screwing up?

#include <Adafruit_NeoPixel.h>
#define LED_PIN 6 // LED driver pin
#define LED_COUNT 96
#define COLBUT 10 // Color Button Pin
#define MODEBUT 3 // Mode Button Pin
Adafruit_NeoPixel strip(LED_COUNT, LED_PIN, NEO_RGB + NEO_KHZ800);

////////////////////////// TIME VARIABLES /////////////////////////////
unsigned long lastPressTimeColor = 0; // Tracks last time the colour button was pressed
unsigned long lastPressTimeMode = 0; // Tracks last time the mode button was pressed

///////////////////////// GLOBAL VARIABLES ///////////////////////////
int top_left = 49;
int top_right = 48;
bool colorFlag = 0; // Checks if colour button was pressed whilst in a function call
bool modeFlag = 0; // Checks if the mode button was pressed whilst in a function call
bool firstLoop = true; // Used for solid color mode, 
int debounce = 250; // Amount of time before listening to button presses again (ms)

/////////////////////////////// MODES & COLORS ////////////////////////////////////       
enum {solid, pulse, breathe, rainbowWave, theaterChase, brightnessChange};
int currentMode = solid;
// Red, orange, yellow, blue, green, purple, pink, teal
int colors[9][3] = {{145,145,125}, {255, 0, 0}, {255, 30, 0}, {240, 100, 0}, {0, 255, 0}, {0, 55, 255}, {105, 0, 90}, {255, 0, 75}, {0, 170, 60}};
int currColor = 0;

void setup() {
  pinMode(COLBUT, INPUT);
  pinMode(MODEBUT, INPUT);
  strip.begin();           
  strip.show();
  strip.setBrightness(210); // Set BRIGHTNESS to max
}

void loop() {
  switch(currentMode) {
    // Solid mode
    case solid:
      {
        if (firstLoop == true) {
          // Set strip colour
          setStrip(colors[currColor][0], colors[currColor][1], colors[currColor][2], 255);
          firstLoop = false;
        }
        if (digitalRead(MODEBUT) == HIGH && ((millis() - lastPressTimeMode) >= debounce)) {
          // Change mode
          lastPressTimeMode = millis();
          modeFlag = false;
          firstLoop = true;
          currentMode = pulse;
        } else if (digitalRead(COLBUT) == HIGH && ((millis() - lastPressTimeColor) >= debounce)) {
          // Change colour
          lastPressTimeColor = millis();
          if (currColor >= 8) {
            firstLoop = true;
            currColor = 0;
          } else {
            firstLoop = true;
            currColor++;
          }
          colorFlag = false;
        }
        break;
      }
    // Pulse modde
    case pulse:
      {
        pulseEffect(colors[currColor][0], colors[currColor][1], colors[currColor][2]);
        if (modeFlag == true) { // check if mode was pressed during function call
          modeFlag = false;
          currentMode = breathe;
        } else if (colorFlag == true) { // check if colour was pressed during function call
          if (currColor >= 8) {
            currColor = 0;
          } else {
            currColor++;
          }
          colorFlag = false;
        }
        break;
      }
    // Breathe effect mode
    case breathe:
      {
        breatheEffect(colors[currColor][0], colors[currColor][1], colors[currColor][2]);
        if (modeFlag == true) { // check if mode was pressed during function call
          modeFlag = false;
          currentMode = rainbowWave;
          break;
        } else if (colorFlag == true) { // check if colour was pressed during function call
          if (currColor >= 8) {
            currColor = 0;
          } else {
            currColor++;
          }
          colorFlag = false;
        }
        break;
      }
    // Continuouse rainbow fade mode
    case rainbowWave:
      {
        rainbow(10);
        if (modeFlag == true) { // check if mode was pressed during function call
          modeFlag = false;
          currentMode = theaterChase;
        }
        break;
      }
    // Theater chase effect mode
    case theaterChase:
      {
        rainbowTheaterChase(150);
        if (modeFlag == true) { // check if mode was pressed during function call
          modeFlag = false;
          currentMode = solid;
        }
        break;
      }    
  }
}

void brighten(int pixel, int R, int G, int B, int factor, int basebr = 255, int fadedur = 60) {
  int j;
  for (j = basebr; j < factor; j+=fadedur) {
    setPixel(pixel, R, G, B, j);
    strip.show();
  }  
}

void dim(int pixel, int R, int G, int B, int factor, int basebr = 255, int fadedur = 70) {
  int j;
  for (j = factor; j > basebr; j-=fadedur) {
    setPixel(pixel, R, G, B, j);
    strip.show();
  }
  if ((digitalRead(COLBUT) == HIGH) && ((millis() - lastPressTimeColor) >= debounce)) {
    lastPressTimeColor = millis();
    colorFlag = true;
  } 
  if ((digitalRead(MODEBUT) == HIGH) && ((millis() - lastPressTimeMode) >= debounce)) {
    lastPressTimeMode = millis();
    modeFlag = true;
  } 
}

void setPixel(int pixel, int r, int g, int b, int brightness) {
  strip.setPixelColor(pixel, (brightness*r/255) , (brightness*g/255), (brightness*b/255));
}

void setStrip(int r, int g, int b, int brightness) {
  for(int i=0; i<LED_COUNT; i++) {
    setPixel(i, r, g, b, brightness); 
  }
  strip.show();
}

void pulseEffect(int R, int G,int B) {
  int baselineBrightness = 12;
  setStrip(R, G, B, baselineBrightness);
  int br8 = 255;
  int br7 = 245;
  int br6 = 235;
  int br5 = 225;
  int br4 = 215;
  int br3 = 205;
  int br2 = 195;
  int br1 = 185;
  int offset = 0;
  while (true) {
    int curr_right = top_right-offset;
    int curr_left = top_left+offset;
    if ((digitalRead(COLBUT) == HIGH) && ((millis() - lastPressTimeColor) >= 100)) {
      lastPressTimeColor = millis();
      colorFlag = true;
      break;
    }
    if ((digitalRead(MODEBUT) == HIGH) && ((millis() - lastPressTimeMode) >= 100)) {
      lastPressTimeMode = millis();
      modeFlag = true;
      break;
    }
    // In this particular instance, I actually found nested if statements to be a clearer implementation for the LED dimming
    // of trailing LEDs rather than a switch case.
    if (offset >= 1) {
      dim(curr_right+1, R, G, B, br8, br7);
      if (modeFlag || colorFlag) {break;}
      dim(curr_left-1, R, G, B, br8, br7);
      if (offset >= 2) {
        dim(curr_right+2, R, G, B, br7, br6);
        if (modeFlag || colorFlag) {break;}
        dim(curr_left-2, R, G, B, br7, br6);
        if (offset >= 3) {
          dim(curr_right+3, R, G, B, br6, br5);
          if (modeFlag || colorFlag) {break;}
          dim(curr_left-3, R, G, B, br6, br5);
          if (offset >= 4) {
            dim(curr_right+4, R, G, B, br5, br4);
            if (modeFlag || colorFlag) {break;}
            dim(curr_left-4, R, G, B, br5, br4);
            if (offset >= 5) {
              dim(curr_right+5, R, G, B, br4, br3);
              if (modeFlag || colorFlag) {break;}
              dim(curr_left-5, R, G, B, br4, br3);
              if (offset >= 6) {
                dim(curr_right+6, R, G, B, br3, br2);
                if (modeFlag || colorFlag) {break;}
                dim(curr_left-6, R, G, B, br3, br2);
                if (offset >= 7) {
                  dim(curr_right+7, R, G, B, br2, br1);
                  if (modeFlag || colorFlag) {break;}
                  dim(curr_left-7, R, G, B, br2, br1);
                  if (offset >= 8) {
                    dim(curr_right+8, R, G, B, br1, baselineBrightness);
                    if (modeFlag || colorFlag) {break;}
                    dim(curr_left-8, R, G, B, br1, baselineBrightness);
                  }
                }
              }
            }
          }
        }
      }
    }
    brighten(curr_right, R, G, B, br8, baselineBrightness, 110);
    brighten(curr_left, R, G, B, br8, baselineBrightness, 110);
    if ((digitalRead(COLBUT) == HIGH) && ((millis() - lastPressTimeColor) >= debounce)) {
      lastPressTimeColor = millis();
      colorFlag = true;
      break;
    }
    if ((digitalRead(MODEBUT) == HIGH) && ((millis() - lastPressTimeMode) >= debounce)) {
      lastPressTimeMode = millis();
      modeFlag = true;
      break;
    }
    offset += 1;
    if (offset >= 55) { 
      offset = 0;
    }
  }
}

void breatheEffect(int R, int G,int B) {
  int brightnessVal = 255;
  bool countingDown = true;
  while(true) {
    if (brightnessVal <= 255) {
      setStrip(R,G,B,brightnessVal);
    }
    if (countingDown && (brightnessVal >= 5)) {
      brightnessVal--;
    } else if (!countingDown && (brightnessVal <= 255)) {
      brightnessVal += 2;
      if (brightnessVal >= 255) {
        countingDown = true;
      }
    } else {
      countingDown = false;
    }
    if ((digitalRead(COLBUT) == HIGH) && ((millis() - lastPressTimeColor) >= debounce)) {
      lastPressTimeColor = millis();
      colorFlag = true;
      break;
    }
    if ((digitalRead(MODEBUT) == HIGH) && ((millis() - lastPressTimeMode) >= debounce)) {
      lastPressTimeMode = millis();
      modeFlag = true;
      break;
    } 
  }
}

void rainbow(int wait) {
  for(long firstPixelHue = 0; ;firstPixelHue += 256) {
    strip.rainbow(firstPixelHue);
    strip.show();
    delay(wait);
    if (firstPixelHue >= 65536) {
      firstPixelHue = 0;
    }
    if ((digitalRead(MODEBUT) == HIGH) && ((millis() - lastPressTimeMode) >= debounce)) {
      lastPressTimeMode = millis();
      modeFlag = true;
      break;
    }
  }
}

void rainbowTheaterChase(int wait) {
  modeFlag = false;
  int firstPixelHue = 0;    
  for(int a=0; a<30; a++) { 
    for(int b=0; b<3; b++) { 
      strip.clear();         
      for(int c=b; c<strip.numPixels(); c += 3) {
        int hue   = firstPixelHue + c * 65536L / strip.numPixels();
        uint32_t color = strip.gamma32(strip.ColorHSV(hue)); // hue -> RGB
        strip.setPixelColor(c, color); // Set pixel 'c' to value 'color'
      }
      strip.show();
      unsigned long startT = millis();
      unsigned long endT = startT;
      while ((endT-startT) <= 125) { //Delay while reading button
        if ((digitalRead(MODEBUT) == HIGH) && ((millis() - lastPressTimeMode) >= debounce)) {
          lastPressTimeMode = millis();
          modeFlag = true;
          break;
        }
        endT = millis();
      }
      if (modeFlag == true) {
        break;
      }
      firstPixelHue += 65536 / 90; // One cycle of color wheel over 90 frames
    }
    if (modeFlag == true) {
        break;
    }
    if ((digitalRead(MODEBUT) == HIGH) && ((millis() - lastPressTimeMode) >= debounce)) {
      lastPressTimeMode = millis();
      modeFlag = true;
      break;
    }
    if (a >= 29) {
      a = 0;
    }
  }
}

Pure luck the buttons work at all.
read about pinMode in more detail

Am I not copying this button setup, to a T? Minus the resistor size? I used a 100K at some point, same thing. I don't have 10Ks but if that's crucial, I can get them.

https://docs.arduino.cc/built-in-examples/digital/Button/

Maybe this:

Are you saying it can be wired diagonally as in, 5V across to the digital pin, with no ground? If so, I can try that tomorrow. It's not that they aren't socketed properly -- both are soldered.

If you think your buttons are not functioning, find the simplest code that uses buttons that you can.

Maybe something from the examples that all it does is

  • debounce
  • turn a LED on and off each press

This will show you the buttons work, then you can energize the search in your code for the flaws it has.

Or that they are crap or worn out, then you can buy some new ones.

It might also help you discover a wiring or other electrical issue. If you can't get the examl,e to work as it should do with a jumper used instead of a pushbutton.

Divide and conquer.

a7

1 Like

Then I'd say flaky switches. Guessing, I'd venture heat damage from soldering or flux intrusion into the switch body. Also consider cold solder joints.

No, you've got to have a ground somewhere. You also have to be aware how the switches are made. As shown in the image two pins are shorted through the body, the contact is made from one side to the other.

See post #6 in the linked thread for switch wiring options.

1 Like

Only thing is they're kind of adhered into place LOL. But yea, I guess I can sacrifice those two to see if they're worn out, I have more. If anyone has more context on "look at PinMode closer" please let me know because I genuinely don't get it. The summary page for that function is like, a 20 second read, and I swear I'm just copying Arduino's own guide.

Heat damage might be possible, but I tried to keep persistent contact time to a minimum. Though, maybe it just didn't get hot enough then -- however, I tinned both the switch legs and the wires that I connected them to, before joining them with more solder so it (should?) be pretty solid.

I see in #6 that I have the worst configuration. I'll try the internal pull-up method tomorrow and update -- I genuinely have no clue if the Pro Mini 168 has them or not but I can look. I'm super unfamiliar with this board -- used to Micros and Unos.

Aren't they attached to an Arduino somehow? Load up some test sketch.

Most things I've built have sketches that test one sensor or other divisible element of the project, and when things go wrong, they are at hand to test from the bottom up.

a7

1 Like

Yep, they are you're right -- it's late, Friday night brain. I'll test this and if its still crap, I'll look at changing the config to internal pull-up. If that's still crap, I'll replace them. I'll update some time tomorrow.

Do you think there could be any chance that since I'm using if statements to check the buttons rather than interrupts, that its just straight up missing button presses? I guess the logical answer is no -- even in the solid colour mode, where basically nothing is happening, the colour switch button behaves flakily. I could try interrupts, however, every time that I've tried to use interrupts for something and and trouble with it and posted on here, its been met with extreme scrutiny & is highly discouraged. But if I do use those, then I get to deal with a whole bunch of potential EMI problems.

To just test your buttons, you can use something like below. Wire your buttons between pin and GND.

The expected behaviour is that you do not see anything in serial monitor till you press the button; the reading will then be 0 (LOW) and it should show immediately. If it does not show immediately or you have to wiggle the button a bit to get the 0, you have bad contacts.

When releasing the button, it should immediately change to 1 (HIGH). If not, your button is sticky.

const uint8_t b1 = 2;

void setup()
{
  Serial.begin(115200);
  pinMode(b1, INPUT_PULLUP);
}

void loop()
{
  static uint8_t oldB1 = HIGH;

  uint8_t bStatus = digitalRead(b1);

  if (bStatus != oldB1)
  {
    Serial.print(millis());
    Serial.print(F("\tb1 : "));
    Serial.println(bStatus);

    oldB1 = bStatus;

  }
}

The button that I used is reasonable good. The output below shows that it reacts immediately but it might bounce on release; it does not bounce on press.

349205	b1 : 0 // press
351375	b1 : 1 // release
351376	b1 : 0 // bounce
351376	b1 : 1 // stable

606526	b1 : 0 // press
609215	b1 : 1 // release, stable

827359	b1 : 0 // press
827616	b1 : 1 // release
827617	b1 : 0 // bounce
827618	b1 : 1 // bounce
827618	b1 : 0 // bounce
827619	b1 : 1 // stable
1 Like

Thank you for that sketch! It seems to be a combination of both potential issues.

One button, is 100% crap. The other is okay-ish. However, changing to input pull-up helped with the consistency of both, but its still not outstanding.

Does anyone have a link to any good quality 12x12mm tactile buttons LOL? These apparently wore out after very little use or were never great to begin with. I'm using these: https://www.ebay.com/itm/224909627372

Aside from that, it works great now! Thanks for the suggestions & the test sketch.

I use Omron tact switches. I buy from "the usual suspects". I just googled

  mouser omron tact

You could start there.

I wish just paying more on ebay would mean you got a better product.

a7

I didn’t buy on eBay, I bought at a local electronics store a while back, they were super inexpensive — I just found a random link to the same model. But I will look at those omron ones!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.