button bounce profiler

I'm working on a USB macro keyboard project and wanted to get button bounce handling just right. I think that knowing what to expect from a button would be useful for optimally debouncing it.

Bounce Profiler accumulates bounce recordings from repeated button presses and outputs to serial a csv formatted report that can easily be graphed in a spreadsheet program.

I'm just starting with programming so please don't hold back the critique.

// configuration values block
const byte test_pin = 5;
const byte pressed_value = 0;     //set appropriately to account for INPUT_PULLUP or NC switches
const byte test_count = 42;       //gather this many button presses at a time
const word sample_size = 512;     //sample resolution .. requires twice this amount of memory (1000 is too much)
const word sample_millis = 20;    //preferred monitoring window in milliseconds (+- 1ms)
const byte clock_mhz = 16;        //MCU clock speed and sample code run time ...
const byte sample_clocks = 100;   //... are used by setDelay() to calculate the required delay between samples

// these arrays accumulate bounce data as the button is repeatedly pressed
byte press_data[sample_size] = {};
byte release_data[sample_size] = {};

word set_delay = 0;
word setDelay(){
  unsigned long sm = sample_millis;
  unsigned long mh = clock_mhz;
  unsigned long ss = sample_size;
  unsigned long sc = sample_clocks;
  unsigned long sd;
  sd = (sm * 1000 * mh - sc * ss) / (ss * mh);
  return sd;
}

void setup() {
  pinMode(test_pin, INPUT_PULLUP);
  Serial.begin(9600);
  set_delay = setDelay();
  while(!Serial.available()); // waits here until something, anything, is sent via serial
}

void loop() {
  static boolean data_full = false;
  static word loop_counter = 1;
  unsigned long time_profile;
  if (loop_counter == 1){
    Serial.print("Bounce Profiler :- Press and release the button ");
    Serial.print(test_count);
    Serial.print(" times to gather data.");
  }
  byte state_previous = !pressed_value;
  for (byte i=0; i < test_count * 2 && !data_full; i++) {
    byte state_initial;
    while (state_previous == (state_initial = digitalRead(test_pin)));
    state_previous = state_initial;
    unsigned long time_start = micros();
    for (word i=0; i < sample_size && !data_full; i++) {
      delayMicroseconds(set_delay);
      if (digitalRead(test_pin) != state_initial) {
        if (state_initial ^ pressed_value) {
          press_data[i]++;
          if (press_data[i] > 254) {
            data_full = true;
          }
        }
        else {
          release_data[i]++;
          if (release_data[i] > 254) {
            data_full = true;
          }
        }
      }
    }
    if (i==2) {
      time_profile = micros() - time_start;
    }
    Serial.print('.');
  }
  Serial.println();
  Serial.print("press bounce over ");
  Serial.print(sample_millis);
  Serial.print("ms,");
  for (word i=0; i < sample_size; i++) {
    Serial.print(press_data[i]);
    Serial.print(',');
  }
  Serial.println();
  Serial.print("release bounce over ");
  Serial.print(sample_millis);
  Serial.print("ms,");
  for (word i=0; i < sample_size; i++) {
    Serial.print(release_data[i]);
    Serial.print(',');
  }
  Serial.println();
  Serial.print("button cycle count,");
  Serial.println(loop_counter * test_count);  
  Serial.print("profile duration,");  
  Serial.println(sample_millis);
  //Serial.println(time_profile);       //useful for debugging setDelay or other timing stuff
  Serial.print("Moment please...  ");  
  delay(3000);
  
  if (data_full == false) {
    Serial.println("This process can optionally be repeated to accumulate more sample data.");  
    loop_counter++;
  }
  else { 
    while (Serial.read() >= 0);
    Serial.println("Maximum data accumulated. Send any key to reset data and start over.");
    while(!Serial.available());
    for (int i=0; i < sample_size; i++) {
      press_data[i] = 0;
      release_data[i] = 0;
    }
    loop_counter = 1;
    data_full = false;
  }
}

*edit: reduced default sample size to resolve out of memory bug on the uno
*edit: added a configuration constant to simplify changes to input or switch polarity
*edit: found and fixed an error in the overflow handler (last_run). Also moved some declarations to better spots.

buttonProfileExample.png

The results attached show a very short 'make' bounce followed 2ms later by a larger 'break' bounce. The poles look close to symmetrical in behaviour. There was a fair bit of measured noise due to a hurried connection by twisted wire but even so the bounce signal is easily seen.

// configuration values block
const byte test_pin = 5;
const boolean two_pole = true;    // this will double the size of the data set ... reduce sample size
const byte test_pin_2 = 11;       // second pin for double pole profiling
const byte pressed_value = 0;     // set appropriately to account for INPUT_PULLUP or NC switches
const byte test_count = 42;       // gather this many button presses at a time
const word sample_size = 256;     // sample resolution .. requires twice this amount of memory (1000 is too much)
const word sample_millis = 20;    // preferred monitoring window in milliseconds (+- 1ms)
const byte clock_mhz = 16;        // MCU clock speed and sample code run time ...
const byte sample_clocks = 100;   // ... are used by setDelay() to calculate the required delay between samples
      word set_delay;

      // this array accumulates bounce data as the button is repeatedly pressed
      byte sample_data[4][sample_size] = {};

void setup() {
  pinMode(test_pin, INPUT_PULLUP);
  pinMode(test_pin_2, INPUT_PULLUP);
  Serial.begin(9600);
  set_delay = setDelay();
  while(!Serial.available());     // wait here until something, anything, is sent via serial
}

word loop_counter = 1;
void loop() {
  static boolean data_full = false;
  unsigned long time_profile;
  if (loop_counter == 1){
    Serial.print("Bounce Profiler :- Press and release the button ");
    Serial.print(test_count);
    Serial.print(" times to gather data.");
  }
  byte button_state = !pressed_value;
  for (byte i=0; i < test_count * 2 && !data_full; i++) {
    while (button_state == digitalRead(test_pin) && (!two_pole || button_state != digitalRead(test_pin_2)));
    button_state = !button_state;
    unsigned long time_start = micros();
    for (word i=0; i < sample_size && !data_full; i++) {
      delayMicroseconds(set_delay);
      if (digitalRead(test_pin) != button_state) {
        if (sample_data[button_state ^ pressed_value][i]++ > 253)
          data_full = true;
      }
      if (digitalRead(test_pin_2) == button_state) {
        if (sample_data[(button_state ^ pressed_value) + 2][i]++ > 253)
          data_full = true;
      }
    }
    if (i==2) {
      time_profile = micros() - time_start;
    }
    //Serial.print('.');   //visual feedback for each button press
  }
  //Serial.println(time_profile);       //useful for debugging setDelay or other timing stuff
  printReport();
  Serial.println("Moment please...  ");  
  delay(3000);
  
  if (data_full == false) {
    Serial.println("This process can optionally be repeated now to accumulate more sample data.");  
    loop_counter++;
  }
  else { 
    while (Serial.read() >= 0);
    Serial.println("Maximum data accumulated. Send any key to reset data and start over.");
    while(!Serial.available());
    for (int i=0; i < sample_size; i++) {
      sample_data[0][i] = 0;
      sample_data[1][i] = 0;
    }
    loop_counter = 1;
    data_full = false;
  }
}

word setDelay(){
  unsigned long sm = sample_millis;
  unsigned long mh = clock_mhz;
  unsigned long ss = sample_size;
  unsigned long sc = sample_clocks;
  unsigned long sd;
  sd = (sm * 1000 * mh - sc * ss) / (ss * mh);
  return sd;
}

void printReport(){  
  Serial.println();
  Serial.print("press bounce over ");
  Serial.print(sample_millis);
  Serial.print("ms,");
  for (word i=0; i < sample_size; i++) {
    Serial.print(sample_data[0][i]);
    Serial.print(',');
  }
  Serial.println();
  Serial.print("release bounce over ");
  Serial.print(sample_millis);
  Serial.print("ms,");
  for (word i=0; i < sample_size; i++) {
    Serial.print(sample_data[1][i]);
    Serial.print(',');
  }
  Serial.println();
  Serial.print("pole 2 press bounce over ");
  Serial.print(sample_millis);
  Serial.print("ms,");
  for (word i=0; i < sample_size; i++) {
    Serial.print(sample_data[2][i]);
    Serial.print(',');
  }
  Serial.println();
  Serial.print("pole 2 release bounce over ");
  Serial.print(sample_millis);
  Serial.print("ms,");
  for (word i=0; i < sample_size; i++) {
    Serial.print(sample_data[3][i]);
    Serial.print(',');
  }
  Serial.println();
  Serial.print("button cycle count,");
  Serial.println(loop_counter * test_count);  
  Serial.print("profile duration,");  
  Serial.println(sample_millis);
}

ButtonProfileDoublePole.png

What is the y-axis on the charts? I assume the x-axis is time, some tick marks would be helpful.

I'm working on a USB macro keyboard project and wanted to get button bounce handling just right. I think that knowing what to expect from a button would be useful for optimally debouncing it.

Great idea - I agree that knowing what to expect is very useful. You may find this interesting, especially "Anatomy of a Bounce" starting on page 7.

A while back, I wrote code to try and debounce most any type of signal on 10 inputs (worked OK), but to repeat - knowing what to expect is very useful!

X = linear time in milliseconds, the max value of which is printed on the legend labels.
Y = "probability of bouncing", a count of excursions from expected value during the time slice defined by X.

The default labels that excel assigns to the axes aren't terribly useful. I removed them to tidy up which I now see was a mistake.

With a little bit of spreadsheet formula work it's possible to display actual time values for the x axis and a percentage fault rate for the y axis. The numbers required to do so are sent in the csv report.

I found that once the axes are understood the default labelling is useless but in hindsight I should have included axis descriptions.

FYI I originally tested that switch with a sample_millis of 50 ms. I used the values from the first result to do a second run that zoomed in on the interesting part that lasted for 5ms.

Based on the results of my double throw testing the total bulk of my bounce handling code is now this:

if (pin1_reading ^ digitalRead(pin2)) //the button state is stable