Can millis be used after a button?

For all the state machines to run independently, they each need to get a chance to do their stuff each time loop() runs. But you seem to have commented out two of them here.

      if (val_s1 == LOW) {

With buttons, you probably want to check that the button itself has changed from HIGH to LOW (or vice versa) since it was last checked, not that it is simply LOW at the time it was checked. See the "state change" example sketch. HIGH and LOW are states of the button. Changes between HIGH and LOW are events, and it's those events you need to drive your state machines.

You can often simplify things somewhat by using a simple delay(20) for debouncing buttons. A 20ms delay won't be noticed by the user so won't cause blocking as such. Only use the delay(20) when you detect that a button has changed state since it was last checked. Don't use delay(20) just because the button is LOW at the moment you check it, because the button might be LOW for an unknown length of time resulting in many delay(20), which is not wanted.

Beginners often get confused between 'states' and 'events' when making their first state machines. A 'state' is something the state machine spends some time in, waiting for a time to pass or an event to happen. An 'event' is a change or external action that happens instantaneously and may cause the machine to change state. In the above, 'WAIT' (state 1) is indeed a state. The machine remains in that state until the button is pressed. But 'ARMING' (state 2) is not really a state because the machine moves immediately and unconditionally on to another state, it doesn't wait for any event before doing that. So state 2 can be removed:

      case 1: //WAIT
      //wait for the switch to go low
      if (val_s1 == LOW && prev_val_s1 == HIGH) {
        delay(20); // ignore any button bounce
        //Record the time and proceed to ARMED
        t_0_s1 = millis();
        state_s1 = 3;
      }
      break;
      
      case 3: //ARMED
      //Check to see if the proper delay has passed. If a bounce occurs then RESET
      t_s1 = millis();
      if (t_s1 - t_0_s1 > bounce_delay_s1) {state_s1 = 4;}
      if (val_s1 == HIGH) {state_s1 = 0;}
      break;

I would suggest simplifying your state machines by using a simple delay(20) for debouncing and removing any 'states' that can be removed because they immediately and unconditionally change to some other state.

I turned them off because I wanted them to be triggered by the button and when they were active they were triggering by themselves. I put a line in StateMachine_s1, case 5 to start the buzzer and LED state machines. For some reason the LED state machine works (other than the fact that I have to push the button each time to advance the case) but the buzzer does not.

case 5: //TRIGGERED!
      //reset the state machine, start the LED state machine
      if (DEBUG) {Serial.println("TRIGGERED!!");}
      StateMachine_bz1();
      StateMachine_led1();
      state_s1 = 0;
      break;

I also found this section did not do what I wanted. It would set the LED to case 2 but would never actually turn on the LED or advance the LED state machine.

//Provide events that can force the state machines to change state
  //I like to program interactions between state machines here in the top level loop
  if (state_s1 == 5) {state_led1 = 2;} //short press
  //if (state_s1 == 5) {state_bz1 = 0;}  //long press

I wonder if perhaps I am misunderstanding how the state machine actually runs? Do you know of any good resources which explain in detail how it works? I don't think I am understanding the serial monitor output for that matter either. I understand the part about what case each machine is on, but not the other numbers from the Loop Counter from @GoForSmoke .
thanks

I will try to simplify this now and re-read your part about the debouncing delay (I think I'll need to read that a few times to wrap my head around it)
Thanks

also - question for the admins as I don't want to get in trouble....

  1. am i replying correctly to these posts by using multiple replies?
  2. this topic has kind of migrated from my original questions which was regarding using millis after a button and is now close to a topic I posted a few months ago. I'm not sure if they should be combined or just left separate? I don't want to break any rules.
    Thanks

@GoForSmoke @PaulRB
I wanted to take a quick second to thank you both for your assistance. After many many hours problem solving and your suggestions, I have been able to make a simple-ish State Machine which seems to do what I want it to do. Next I will be increasing the complexity to fulfill my end goals.
Thank you again,
EQL

Yes, I don't see a problem with that.

After 6 months with no further posts, a topic is automatically closed, so no-one can really blame you for starting a new one.

If it's less than 6 months, but no-one has responded to the old topic in many weeks, then although it could be considered "cross-posting" (which is against forum rules, and quite right too) it's unlikely you will be in danger of wasting forum member's time by having 2 open topics on the same subject, and that is what the "cross-posting" rule is really meant to prevent.

Is that because you don't understand contact bounce at the pin and insanely fast code speed level? Fear Not, button bounce is like an Arduino rite of passage!

Bookmark this as a Reference to use later. It gives wiring and code.
A clear and simple complete tutorial on switches and using them.
He shows what bounce is on an oscilloscope, that for a short time the value of the pin may not tell if the button is up or down.

A clean solution once the delays and other time-hog code is gone is to write a function that runs every time in loop to watch that pin for change, see the bounce and wait for it to for sure end and only then ---- change the value of a global buttonState variable.

Then in your old code, read that variable instead of reading the pin, it will instantly tell you the clean answer. If you want more info like not just button position but did it just change then the status has to be more than 0 or 1, but yes I do something like that.

We're all here to help. I want to help you see more than one code run "at the same time" by showing that messy parts don't have to be wedged or weaved into a big all-in-one code block. You can put that in its own function.... do ya wanna make version 2?

Those are how many times void loop() ran in the last second.
When your code doesn't block the get into 5 digit values.
Add delay(1) and loop counter gives less than 1000, but close.

When you can check 1 pin 50 times a millisecond while the lights are blinking and other things happening, that's in the non-blocking ballpark.

1 milli delays 16000 cpu cycles. The rest of the sketch might use a few hundred.

Thats unnecessarily complicated. You don't need to see the bounce and wait for its end. It is sufficient to read the button state into a variable at fixed time intervalls that are greater than the maximum bounce time. This can easily be done with millis() without blocking the sketch ( or with a library :wink: ).
Of course in the code you must check that variable instead of directly reading the button as @GoForSmoke already suggested.

In non-blocked code you can read every button press as a whole bunch of button presses and if the button is dirty you can pick up false events for many millis.

It's bad practice to ASSUME what will happen in the real world.
If you're going to check millis to non-block an interval, you might as well check the pin while you're it.

" It is sufficient to read the button state into a variable at fixed time intervalls that are greater than the maximum bounce time"

Which assumes that the pin will not be read during bounce. That's why I read twice a milli and keep a read history status, perhaps you didn't see the posts.

It doesn't matter if you read during a bounce. The read intervall must only be great enough to ensure that you read at max once during a bounce.


At the top the direct signal from the button, in the middle the sampling intervals. Below, the state of the variable with the button state in loop(). This is a perfectly debounced signal. Even if the sampling time falls in the middle of the bounce time.
Whether you read HIGH or LOW during bouncing does not matter. It only results in detecting the state change one interval earlier or later.
With this method, you can debounce dozens of buttons without any problems. You only need one interval timer for all buttons. The buttons can be arranged in a matrix, connected by an IO extender or directly to pins. I have been doing it this way for decades :wink:

Bookmarked and will read, thanks :slight_smile:

Here's a link to my second post with the more-complex code I was/am working on (I post the code on July 26).

https://forum.arduino.cc/t/attaching-a-wire-to-board-with-shield/1016095/20

Thanks.

thanks this was helpful :slight_smile:

That's really just your opinion. :roll_eyes:

While not critical, it also has the consequence that you have delayed the response to the button by the necessarily long time you have chosen; you have made it longer than the entire duration of the bounce process.

If you instead read the button on every cycle of the loop - which you might as well do as you otherwise have to check millis() on every cycle of the loop anyway to determine your delay - you only have to wait longer than the bounce frequency determines - the duration (period) of each of those small oscillations - in order to be positive about the result.

Mind you, even that is not optimal if an immediate response is desired. You can simply conclude that the very first transition represents the important event and then wait for the bounce to be over before responding to the next transition to the opposite state. This is arguably a more valid approach.

// Multiple toggles!
const int led1Pin =  3;    // LED pin number
const int button1 =  2;
const int led2Pin =  5; 
const int button2 =  4;
const int led3Pin =  6;
const int button3 =  7;
const int led4Pin =  9;
const int button4 =  8;
char bstate1 = 0;
char bstate2 = 0;
char bstate3 = 0;
char bstate4 = 0;
unsigned long bcount1 = 0; // button debounce timer.  Replicate as necessary.
unsigned long bcount2 = 0;
unsigned long bcount3 = 0;
unsigned long bcount4 = 0;

char led1State = LOW;        // initialise the LED
char led2State = LOW;
char led3State = LOW;
char led4State = LOW;

// Have we completed the specified interval since last confirmed event?
// "marker" chooses which counter to check
// Routines by Paul__B of Arduino Forum
boolean timeout(unsigned long *marker, unsigned long interval) {
  if (millis() - *marker >= interval) { 
    *marker += interval;    // move on ready for next interval
    return true;       
  } 
  else return false;
}

// Deal with a button read; true if button pressed and debounced is a new event
// Uses reading of button input, debounce store, state store and debounce interval.
// Routines by Paul__B of Arduino Forum
boolean butndown(char button, unsigned long *marker, char *butnstate, unsigned long interval) {
  switch (*butnstate) {               // Odd states if was pressed, >= 2 if debounce in progress
  case 0: // Button up so far, 
    if (button == HIGH) return false; // Nothing happening!
    else { 
      *butnstate = 2;                 // record that is now pressed
      *marker = millis();             // note when was pressed
      return false;                   // and move on
    }

  case 1: // Button down so far, 
    if (button == LOW) return false; // Nothing happening!
    else { 
      *butnstate = 3;                 // record that is now released
      *marker = millis();             // note when was released
      return false;                   // and move on
    }

  case 2: // Button was up, now down.
    if (button == HIGH) {
      *butnstate = 0;                 // no, not debounced; revert the state
      return false;                   // False alarm!
    }
    else { 
      if (millis() - *marker >= interval) {
        *butnstate = 1;               // jackpot!  update the state
        return true;                  // because we have the desired event!
      }
      else 
        return false;                 // not done yet; just move on
    }

  case 3: // Button was down, now up.
    if (button == LOW) {
      *butnstate = 1;                 // no, not debounced; revert the state
      return false;                   // False alarm!
    }
    else { 
      if (millis() - *marker >= interval) {
        *butnstate = 0;               // Debounced; update the state
        return false;                 // but it is not the event we want
      }
      else 
        return false;                 // not done yet; just move on
    }
  default:                            // Error; recover anyway
    {  
      *butnstate = 0;
      return false;                   // Definitely false!
    }
  }
}

// ----------------------------- toggle ------------------------------------------
char toggle(char *flip) {   // Yes, it toggles the variable pointed to
  if (*flip == LOW) {
    *flip = HIGH;
  }
  else {
    *flip = LOW; 
  } 
  return *flip;
}

void setup() {
  pinMode(led1Pin, OUTPUT);      
  pinMode(button1, INPUT_PULLUP); 
  pinMode(led2Pin, OUTPUT);      
  pinMode(button2, INPUT_PULLUP);      
  pinMode(led3Pin, OUTPUT);      
  pinMode(button3, INPUT_PULLUP);      
  pinMode(led4Pin, OUTPUT);      
  pinMode(button4, INPUT_PULLUP);        
  digitalWrite (led1Pin, LOW);
  digitalWrite (led2Pin, LOW);
  digitalWrite (led3Pin, LOW);
  digitalWrite (led4Pin, LOW);
}

void loop() {
  // Toggle LED if button debounced
  if (butndown(digitalRead(button1), &bcount1, &bstate1, 10UL )) {
    toggle(&led1State);
    digitalWrite(led1Pin, led1State);
  } 

  if (butndown(digitalRead(button2), &bcount2, &bstate2, 10UL )) {
    toggle(&led2State);
    digitalWrite(led2Pin, led2State);
  } 

  if (butndown(digitalRead(button3), &bcount3, &bstate3, 10UL )) {
    toggle(&led3State);
    digitalWrite(led3Pin, led3State);
  } 

  if (butndown(digitalRead(button4), &bcount4, &bstate4, 10UL )) {
    toggle(&led4State);
    digitalWrite(led4Pin, led4State);
  } 
}

If you read during a bounce, you need a clean read after the bouncing that as has been demonstrated on the web that all buttons even from the same bag do not have short debounces and in the less than ideal world, dirty contact bounce has been shown to last long enough to justify 20ms waits.

Maxim on bounce. They show 5+ ms bounces and their solutions.

And this is much the same with 6ms max in a test.

If you only every how long?

When the bouncing is over for about 3.5ms I call it clean since bounce transitions may be over 1ms apart and I don't want any false positives. But I read more often because I do want speed, that switch could be part of a machine and not humanly slow.

In fewer words than I used...

You don't have to do millis math to make regular events like reads as long as the interval is a single bit value... and you know already the bits all have a rollover of their own. Check the bit for transition is the trigger, no subtraction involved. Mask and compare, that's it!

But there's more as was pointed out. Millis low byte counts 250ms, not 256. Bit 8 of millis() is 1/4 seconds and bit 10 is seconds.

I use bit 9 of micros() to read the pin at an interval to catch bounce. I used to use a micros() time check, it lets me show pin state history in 1 byte. Instead of start and interval variables I have that history byte, saves RAM/button.

When I was trying the idea out, I worked out that longer intervals will work but as you describe. There's another tradeoff for speed like with RAM and speed.

Let's say that the I/O pin is pulled up and the button switch pulls it down. Now when the pin is polled and found to be high then there is no button press. The first poll that returns a low indicates that the button is pressed (or has just recently been pressed) so, the sketch can branch. A problem may exist in that the sketch returns to polling the pin before the switch has completed its bouncing. I think we should not slow the loop because the switch may be bouncing because the poll shows the button has been pressed. If anything maybe the branch should delay the return to polling, if required.

Take it as fact that plain contact switches will bounce for 2 to 6 or more ms,

I used to check every loop to find the end of bounce as a period of stable reads longer than any gap between bounces. I wouldn't go less than 2ms to call it stable.

I read twice a milli because I want to catch the bounce.

byte pinHistory; // buts hold pin states from past -> now

0b10000000 = 128 = switch closed ~3ms ago, call it a press.
0b01111111 = 127 = switch open etc, release.

0b10110000 = bouncing/unstable

But certainly reading the button every 10, even 20ms won't be humanly noticeable and it will work and what do we say about what works?

Yes, what has been shown to work, does work, and I cannot argue against success! If the button press branches to code that just sets a flag, and because of bounce, the flag is set, say, six times, it just causes a delay the same as debounce. If the button press branches to code that moves a servo or does much math, then that will probably take longer than the bounce time, so debounce is not needed there either. I think we should do debounce only when needed, such as toggling a boolean or incrementing a variable.

If you wrote non-blocking code the inputs get checked frequently. No long math has to be done in one single step, code does not have to wait for a servo or a serial char to arrive, we don't process an entire set of anything in a single loop() unless we have to and it's quick.

On AVR scale, 16000 cycles/ms, bounce takes a looong time.
The faster you run void loop(), the smoother your automation.