Forum Rule: Always post complete source code & details to reproduce any issue!
Results 1 to 16 of 16

Thread: Help neede: Debouncing a button matrix

  1. #1
    Junior Member
    Join Date
    Nov 2016
    Posts
    8

    Help needed: debouncing a button matrix

    Dear forum users,

    I want to build a button based MIDI/Ableton Live controller and need some project guidance, especially regarding debouncing. I am in an early planning and breadboarding/prototyping phase in order to check the technical possibilities.

    My MIDI/Ableton Live controller should have the following outline:

    - first stage: round about 32 buttons/keys, maybe up to 64 (not sure about it yet). The whole controller should be polyphonic (play multiple notes simultaneously) and have a low latency.
    - later stage: maybe add in some more features (e.g. RGB LEDs via WS2812B in order to represent Ableton Live status or add in some potentiometers)

    My main focus for now are the buttons/keys: based on the OpenMusicLabs article

    http://www.openmusiclabs.com/learnin...mux/index.html

    , I have breadboarded basically the circuit described in the article in order to scan the button matrix (see attached: my schema and the picture of my breadboard; hopefully the schema is ok, it’s my first KiCad project).

    One difference to circuit of the article exists: instead of driving the columns LOW to read the rows (and use PullUps), I’m using the 74HC595 shift register to drive the columns HIGH to read the rows (and use PullDowns). As I learned, it’s easier to close circuits on the breadboard than to have them normally closed as the buttons tend to slip out of the breadboard holes...

    My question is: what is the best way to debounce the big amount of switches in order to reach polyphony and low latency? In a nutshell: I want to build a polyphonic MIDI keyboard.

    In another project, I have used an RC filter and a Schmitt trigger to debounce a few switches in hardware. But in the button matrix, I do not see how this would work as the rows are not permanently connected to the RC filter/Schmitt trigger.

    I looked into the Teensy USB MIDI buttons example (Examples > Teensy > USB_MIDI > Buttons), but I am not sure if the code can be adapted to the input matrix as the single buttons are not permanently connected to the Teensy input pins.

    I have read the Ganssle article regarding software debouncing ( http://www.ganssle.com/debouncing-pt2.htm ) and I like the referenced approach by “tcleg” ( https://github.com/tcleg/Button_Debouncer ) as the state based algorithm is quite short and effective. But same here: can it be applied to a button matrix?

    Any help would be greatly appreciated!

    Best wishes,

    afterwards
    Attached Thumbnails Attached Thumbnails Click image for larger version. 

Name:	keyboard_scanner_breadboard.jpg 
Views:	143 
Size:	98.7 KB 
ID:	11460   Click image for larger version. 

Name:	keyboard_scanner_schematics.jpg 
Views:	209 
Size:	96.2 KB 
ID:	11461  

    Last edited by afterwards; 09-04-2017 at 05:04 PM. Reason: corrected typos, added attachment

  2. #2
    Junior Member
    Join Date
    Nov 2016
    Posts
    8
    Here is my button matrix scan code so far, no debouncing yet:

    Code:
    //****************************************************************
    // button matrix scanning code with 74hc595
    // see: http://www.openmusiclabs.com/learning/digital/input-scanning-matrix/shift-out-mux/index.html
    // code see also: https://github.com/evankale/ArduinoMidiPiano/blob/master/MidiPiano_PullDown/MidiPiano_PullDown.ino
    //****************************************************************
    
    // define serial speed
    const uint32_t serialSpeed = 115200;
    
    ////Pin connected to DS of 74HC595
    const uint8_t dataPin = 2;
    //Pin connected to ST_CP of 74HC595
    const uint8_t latchPin = 3;
    //Pin connected to SH_CP of 74HC595
    const uint8_t clockPin = 4;
    
    // pins for reading button matrix rows
    const uint8_t rowPin1 = 5;
    const uint8_t rowPin2 = 6;
    const uint8_t rowPin3 = 7;
    
    // defining the size of the button matrix
    const uint8_t numRows = 3;
    const uint8_t numCols = 4;
    
    // bit mask for shift register ("activates" columns)
    uint8_t columnBitMask[numCols];
    
    // state arrays
    boolean keyPressed[numCols][numRows];
    uint8_t keyToMidiMap[numCols][numRows];
    
    // holds row values at a certain column (defined by the column scanning)
    boolean rowValue[numRows];
    
    elapsedMillis myElapsedMillis = 0;
    
    void setup() {
    
      // begin serial
      Serial.begin(serialSpeed);
    
      // short delay
      delay(500);
    
      // print to serial
      Serial.println("## Keyboard Scanner with 74HC595 ##");
    
      //set shift register pins as outputs
      pinMode(latchPin, OUTPUT);
      pinMode(clockPin, OUTPUT);
      pinMode(dataPin, OUTPUT);
    
      // set input pins for reading matrix rows
      pinMode(rowPin1, INPUT);
      pinMode(rowPin2, INPUT);
      pinMode(rowPin3, INPUT);
    
      // bit masks for scanning columns
      columnBitMask[0] = B00000001;
      columnBitMask[1] = B00000010;
      columnBitMask[2] = B00000100;
      columnBitMask[3] = B00001000;
    
      // local helper for presetting values
      uint8_t note = 0; //31
    
      Serial.println("C R Index");
    
      for (uint8_t columnCtr = 0; columnCtr < numCols; columnCtr++) {
        for (uint8_t rowCtr = 0; rowCtr < numRows; rowCtr++) {
    
          // init keypress states
          keyPressed[columnCtr][rowCtr] = false;
          // init midi map
          keyToMidiMap[columnCtr][rowCtr] = note++;
    
    
          Serial.print(columnCtr);
          Serial.print(" ");
          Serial.print(rowCtr);
          Serial.print(" ");
          Serial.println(keyToMidiMap[columnCtr][rowCtr]);
        }
      }
    
    
    }
    
    void loop() {
    
      // iterate columns
      for (uint8_t columnCtr = 0; columnCtr < numCols; columnCtr++) {
    
        // pull latchPin LOW to keep current state
        digitalWrite(latchPin, LOW);
    
        // shift out the bits for the current column; replace by SPI transmit
        shiftOut(dataPin, clockPin, MSBFIRST, columnBitMask[columnCtr]);
    
        // pull latch pin high so that the bitmask is represented on the 74HC595 output pins
        digitalWrite(latchPin, HIGH);
    
        // read all input rows
        rowValue[0] = digitalRead(rowPin1);
        rowValue[1] = digitalRead(rowPin2);
        rowValue[2] = digitalRead(rowPin3);
    
        // process keys pressed
        for (int rowCtr = 0; rowCtr < numRows; rowCtr++)
        {
          if (rowValue[rowCtr] != 0 && !keyPressed[columnCtr][rowCtr])
          {
            keyPressed[columnCtr][rowCtr] = true;
            Serial.print("Key Press: R");
            Serial.print(rowCtr);
            Serial.print(" C");
            Serial.println(columnCtr);
    
            // send noteOn command here...
          }
        }
    
        // process keys released
        for (int rowCtr = 0; rowCtr < numRows; rowCtr++)
        {
          if (rowValue[rowCtr] == 0 && keyPressed[columnCtr][rowCtr])
          {
            keyPressed[columnCtr][rowCtr] = false;
            Serial.print("Key Release: R");
            Serial.print(rowCtr);
            Serial.print(" C");
            Serial.println(columnCtr);
    
            // send noteOff command here...
          }
        }
      }
    
      //if (myElapsedMillis >= 1000) {
      //  Serial.println(myElapsedMillis);
      //  myElapsedMillis = 0;
      //}
    }
    p.s. is there a way to correct the typo in the thread title? sorry for that.
    Last edited by afterwards; 09-04-2017 at 04:24 PM.

  3. #3
    Senior Member oddson's Avatar
    Join Date
    Feb 2013
    Location
    Isle in the Salish Sea
    Posts
    1,297
    Did you write or do you at least understand your code?

    If so you can use a technique very similar to the bit-masking you're doing to scan the matrix to tell you when you have a rising or falling edge detected.

    As you scan each switch you record the putative on/off state as 1/0 in a shift-register variable. Then you scan for patterns B1000 for falling edge and B0111 for rising (which is decimal 8 and 7 when converted to bytes).

    There's a bit more detail and a link to the post where I learned about the technique in the following:
    https://forum.pjrc.com/threads/40492...l=1#post125947

  4. #4
    What about Keypad library ?

    4x4 matrix without additional IC:

    Code:
    /* @file MultiKey.ino
    || @version 1.0
    || @author Mark Stanley
    || @contact mstanley@technologist.com
    ||
    || @description
    || | The latest version, 3.0, of the keypad library supports up to 10
    || | active keys all being pressed at the same time. This sketch is an
    || | example of how you can get multiple key presses from a keypad or
    || | keyboard.
    || #
    */
    
    #include <Keypad.h>
    
    const byte ROWS = 2; //four rows
    const byte COLS = 2; //three columns
    char keys[ROWS][COLS] = {
      {'1','2'},
      {'3','4'}
    };
    byte rowPins[ROWS] = {2, 3}; //connect to the row pinouts of the kpd
    byte colPins[COLS] = {11, 12}; //connect to the column pinouts of the kpd
    
    Keypad kpd = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS );
    
    
    
    void setup() {
      Serial.begin(9600);
      kpd.setDebounceTime(1);
    }
    
    unsigned long loopCount = 0;
    unsigned long startTime = millis();
    String msg = "";
    
    
    
    void loop() {
    
      loopCount++;
      if ( (millis()-startTime)>1000 ) {
          //Serial.println(loopCount);
          startTime = millis();
          loopCount = 0;
      }
    
      // Fills kpd.key[ ] array with up-to 10 active keys.
      // Returns true if there are ANY active keys.
      if (kpd.getKeys())
      {
        for (int i=0; i<LIST_MAX; i++)   // Scan the whole key list.
        {
          if ( kpd.key[i].stateChanged )   // Only find keys that have changed state.
          {
            switch (kpd.key[i].kstate) {  // Report active key state : IDLE, PRESSED, HOLD, or RELEASED
                case PRESSED:
                    msg = " PRESSED.";
                    break;
                case HOLD:
                    msg = " HOLD.";
                    break;
                case RELEASED:
                    msg = " RELEASED.";
                    break;
                case IDLE:
                    msg = " IDLE.";
            }
            Serial.print("Key ");
            Serial.print(kpd.key[i].kchar);
            Serial.println(msg);
          }
        }
      }
    }  // End loop

  5. #5
    Junior Member
    Join Date
    Nov 2016
    Posts
    8
    Thank you for your replies, oddson and DanieleFromItaly!

    Yes, I usually do write (and understand) my code. I am just not so fond of reinventing the wheel, so for learning purposes I often adapt existing code and rewrite it if necessary.

    Regarding the keypad library: thank you for showing up this method! I took this already in consideration, but I wanted to save some of the Teensy pins as I am planning up to 64 keys and might add some more I/O stuff on the other pins. I also enjoy tinkering with some hardware, so I think I will stick with the shift register.

    Thanks for pointing me to the rising/falling edge detection method, it looks very good and promising to me! I have tried the described macros (see attached sketch, teensy_110_debounce_bit.ino) and they work fine so far.

    If the edge detection macros are executed in "full speed" (each cycle of the loop), I sometimes still get multiple rising/falling edges per button press due to the high reading speeds. It gets better if the detection macros are executed after a short delay using elapsedMicros (in the sketch I tried every 250s). That would be in the ideal case (4 readings) a latency of a little bit more than 1ms for an edge detection. Maybe thats a little bit to ambitious. What timings would you use in this case?

    Just out of curiosity, one further question: is there a special reason, why for example the DRE method is a macro and not a usual function? I guess the DRE macro could be rewritten as

    Code:
    boolean detectRisingEdge(uint8_t inputSignal, uint8_t *state) {
    	return ((*state = ((*state << 1) | (inputSignal & 1)) & DMASK ) == DR);
    }
    and then called with

    Code:
    ...
    detectRisingEdge(mySignal, &myButtonState)
    ...
    , which would make a class integration easier.

    Thank you!

  6. #6
    Senior Member oddson's Avatar
    Join Date
    Feb 2013
    Location
    Isle in the Salish Sea
    Posts
    1,297
    Quote Originally Posted by afterwards View Post
    ...Yes, I usually do write (and understand) my code. I am just not so fond of reinventing the wheel, so for learning purposes I often adapt existing code and rewrite it if necessary.
    Sorry if I offended; most people posting similar questions don't really understand the code they've posted as it's hacked loosely together from multiple (non-cited) sources.

    If that were you I'd say 'good luck' and be done with it. But the breadboard and schematic suggested it was likely you had at least a clue about what your code is doing.


    If the edge detection macros are executed in "full speed" (each cycle of the loop), I sometimes still get multiple rising/falling edges per button press due to the high reading speeds. It gets better if the detection macros are executed after a short delay using elapsedMicros (in the sketch I tried every 250s). That would be in the ideal case (4 readings) a latency of a little bit more than 1ms for an edge detection. Maybe that’s a little bit to ambitious. What timings would you use in this case?
    I was about to say "that shouldn't be" but I think it does make sense.

    If you're reading super fast you could well see several values of one state in a row before the rebound force breaks/makes contact again.

    So for rock solid operation you need to sample enough points so that they represent a period longer than the settle time for your switch. You can sample more points (use longer masks) or you can slow down the sampling.


    Just out of curiosity, one further question: is there a special reason, why for example the DRE method is a macro and not a usual function?
    Showmanship? I was impressed he could do it; would never occur to me to try that.

    I don't have the chops to read your code and comment intelligently but I'd always intended to write my own code when I finally went to try this.


    I'd be happy to help if I can but I suspect you're at least as able to pull this off as I am... so

    good luck
    (but not in the sense of 'you'll need it' as used above)
    Last edited by oddson; 09-27-2017 at 11:45 PM.

  7. #7
    Senior Member oddson's Avatar
    Join Date
    Feb 2013
    Location
    Isle in the Salish Sea
    Posts
    1,297
    It occurs to me the miss-triggers might be from residual charge as the mux (edit - shift register!) is switched between columns and the improvement doesn't have to do with switching noise but with running the mux too fast.

    I'm more familiar with the problem with analog readings but I think it would be consistent with rapid on/off toggling that does not settle (if it's not from the switch it would not stop happening when you hold the switch closed).

    If there were miss-fires on adjacent columns then I'd say that would clinch it.

    If this is the issue then you need to have the elapsedMicros reset after the mux is switched and tested before reading values.

    Once there is a more modest sample rate the time between columns is cumulative so the de-bounce shouldn't need more than two or three readings in a row to change state properly.
    Last edited by oddson; 09-28-2017 at 09:51 PM.

  8. #8
    Senior Member
    Join Date
    Feb 2015
    Location
    Finland
    Posts
    164
    If the only false signals occur at real transitions, and you want minimal latency at key press and release, you can also use a counter per key to disable state changes for a specific duration after the key is pressed or released. A simplified example:
    Code:
    /* Keys form a matrix, where each row is read at once. At most 32 columns, but see below. */
    #define  KEY_ROWS  4
    #define  KEY_COLUMNS  3
    
    /* Digital pins matching each column */
    static const int  key_column_pin[KEY_COLUMNS] = { 5, 6, 7 };
    
    /* Number of keyboard scan cycles the key is unresponsive
        after a key press and a key release event. */
    #define  KEY_PRESS_SCANS  8
    #define  KEY_RELEASE_SCANS  8
    
    /* Key state map.
        If KEY_COLUMNS > 16, use 'uint32_t' type.
        If KEY_COLUMNS > 8, use 'uint16_t' type.
        Here, KEY_COLUMNS <= 3, so we use 'uint8_t' type.
    */
    uint8_t  key_state[KEY_ROWS];
    
    /* Key state change counters. One per key.
        If KEY_PRESS_SCANS or KEY_RELEASE_SCANS > 255,
        use 'uint16_t' or 'uint32_t'.
    */
    uint8_t  key_counter[KEY_ROWS][KEY_COLUMNS];
    
    void begin(void)
    {
        int c, r;
    
        /* None of the keys are currently pressed. */
        for (r = 0; r < KEY_ROWS; r++)
            key_state[r] = 0;
    
        /* All counters are zero, so we are responsive to events. */
        for (r = 0; r < KEY_ROWS; r++)
            for (c = 0; c < KEY_COLUMNS; c++)
                key_counter[r][c] = 0;
    }
    It seems to me that it makes most sense to interleave the scan and the event checks, especially if a short delay between switching rows is useful, as we do meaningful work during that delay:
    Code:
    void loop(void)
    {
        int  r, c;
    
        /*
         * Code omitted: prepare the multiplexer for row 0.
         * May need a stabilizing delay afterwards.
        */
    
        for (r = 0; r < KEY_ROWS; r++) {
            uint32_t  state = 0;
    
            /* Loop to read key row states. */
            for (c = 0; c < KEY_COLUMNS; c++)
                state |= (uint32_t)(!!digitalRead(key_column_pin[c])) << c;
    
            if (r < KEY_ROWS - 1) {
                /*
                 * Code omitted: prepare multiplexer for row (r+1).
                */
            }
    
            /* Loop to generate events for keys on this row. */
            for (c = 0; c < KEY_COLUMNS; c++) {
                const uint32_t  mask = 1u << c;
                if (key_counter[r][c] > 0) {
                    /* Recent key event; ignore state changes */
                    key_counter[r][c]--;
                } else
                if ((state & mask) && !(key_state[r] & mask)) {
                    /* Key press event */
                    key_counter[r][c] = KEY_PRESS_SCANS;
                    key_state[r] |= mask;
                    /*
                     * Code omitted: Queue key press event for key on row r, column c.
                    */
                } else
                if (!(state & mask) && (key_state[r] & mask)) {
                    /* Key release event */
                    key_counter[r][c] = KEY_RELEASE_SCANS;
                    key_state[r] &= ~mask;
                    /*
                     * Code omitted: Queue key release event for key on row r, column c.
                    */
                }
    
                /* Might need an optional delay here, if there are only a couple of columns
                 * and the multiplexer needs more time than spent above to stabilize.
                */
            }
        }
    
        /*
         * Code omitted: reset multiplexer.
        */
    
        /*
         * Code omitted: Send queued key press and release events.
         * (At most KEY_ROWS * KEY_COLUMNS events per loop iteration.)
        */
    }
    The code is completely untested, although the idea is quite sound, so there may be typos, thinkos, or bugs in it.

    The difference to hardware debouncing and typical software debounce methods is that this one registers the event immediately as it occurs, rather than after a delay, after it is determined that it is not a spurious bounce effect. Furthermore, this allows very fast scan rates (long debounce durations, counted in number of full key matrix scan cycles), and asymmetric debounce durations for key press and release.

    The downside of this method is that if there are any spurious signals unrelated to bouncing, the spurious signals are registered as key presses (or key release followed by the specified delay, and then a key press event if still pressed). Both hardware debouncing and typical software debounce methods are quite resilient against spurious signals, as long as they are shorter than the debounce interval.

  9. #9
    Junior Member
    Join Date
    Nov 2016
    Posts
    8
    Thank you for your replies, oddson and Nominal Animal!

    @oddson: No problem, I did not feel offended! Instead, I am very glad to get the good input and new ways to get stuff running from the forum! But you are absolutely right about people copying/pasting things together, not knowing what they do (I have seen them too). But through the forum, there is no way of finding out (without analysing the code), who is posting the topics with what knowledge. In short: no problem - I am glad you are here

    Regarding the macro, I was also astonished to see something like that. But nice to know!

    Regarding your both further posts: I will take some time to digest the info, gather more details and get back to you.

    Thank you very much so far!

  10. #10
    Senior Member oddson's Avatar
    Join Date
    Feb 2013
    Location
    Isle in the Salish Sea
    Posts
    1,297
    Or you could try a BOUNCE array with nine objects on three pins.

  11. #11
    Junior Member
    Join Date
    Nov 2016
    Posts
    8
    Now I had the time to dig a little deeper into the matter: the false triggers happened at real transitions. There were no mis-triggers in adjacent columns.

    After a while of searching, I replaced a few of the small buttons on the breadboard by proper buttons (some good quality arcade buttons I had lying around). The result was astonishing: no false triggers at all!
    I guess I was fooled by cheap bouncy breadboard buttons and/or not properly seated buttons slipping out of the breadboard holes...

    I also tested a short delay of a few microseconds (with delayMicroseconds()) after setting the shift register and before reading the row values in order to let it settle a little. With the proper (and properly seated) buttons, the delay does not seem to be necessary.

    Regarding the debounce method:

    The edge detection macros work perfectly fine, even at high matrix scan rates (even up to 4kHz / triggering the scan routine every 250s).

    I also tested an array of the Button_Debouncer by "tcleg" (eight instances) mentioned in the first post, as a single instance is capable of debouncing a whole port/8 inputs at once. This also works fine at scan rates up to 4kHz.

    Regarding the Bounce array: I have read somewhere in the forum here to better not to use 64 instances of the Bounce library (cannot find the post right now, sorry). Thats what I have not tried so far.

    The counter method mentioned by Nominal Animal looks indeed very promising as it allows asymmetric debounce durations that would allow the button press to be detected on the first rising edge. I guess that I will still have to dig a little deeper into bit twiddling in order to get the lowest latency (will have to watch out for spurious signals, though).

    But so far I am happy and I can dig deeper

    Thank you all very much for your help!

    Best wishes,

    afterwards

  12. #12
    Senior Member
    Join Date
    Dec 2016
    Posts
    106
    Quote Originally Posted by afterwards View Post
    Now I had the time to dig a little deeper into the matter: the false triggers happened at real transitions. There were no mis-triggers in adjacent columns.

    After a while of searching, I replaced a few of the small buttons on the breadboard by proper buttons (some good quality arcade buttons I had lying around). The result was astonishing: no false triggers at all!
    I guess I was fooled by cheap bouncy breadboard buttons and/or not properly seated buttons slipping out of the breadboard holes...

    I also tested a short delay of a few microseconds (with delayMicroseconds()) after setting the shift register and before reading the row values in order to let it settle a little. With the proper (and properly seated) buttons, the delay does not seem to be necessary.

    Regarding the debounce method:

    The edge detection macros work perfectly fine, even at high matrix scan rates (even up to 4kHz / triggering the scan routine every 250s).

    I also tested an array of the Button_Debouncer by "tcleg" (eight instances) mentioned in the first post, as a single instance is capable of debouncing a whole port/8 inputs at once. This also works fine at scan rates up to 4kHz.

    Regarding the Bounce array: I have read somewhere in the forum here to better not to use 64 instances of the Bounce library (cannot find the post right now, sorry). Thats what I have not tried so far.

    The counter method mentioned by Nominal Animal looks indeed very promising as it allows asymmetric debounce durations that would allow the button press to be detected on the first rising edge. I guess that I will still have to dig a little deeper into bit twiddling in order to get the lowest latency (will have to watch out for spurious signals, though).

    But so far I am happy and I can dig deeper

    Thank you all very much for your help!

    Best wishes,

    afterwards
    Hi

    The macro method can also be adapted (at compile time) to get different debouncing delays for push and release, as long as the variable has enough bits (a byte has 8 bits).

    I've also read around the web that buttons can bounce for more than 5ms. So reading "real" mechanical buttons at more than 1Khz is not very useful.
    I've also read that humans won't see delays below 13-50ms.

    The counting method has the advantage that it's very intuitive and that it will react even if there's still a bit of bouncing going on.

    Another approach is also using a lock out period where there can't be another state switch, but that needs a 32bit variable (per button) for the time of the last switch.

  13. #13
    Senior Member
    Join Date
    Dec 2016
    Posts
    106
    Quote Originally Posted by AlainD View Post
    Another approach is also using a lock out period where there can't be another state switch, but that needs a 32bit variable (per button) for the time of the last switch.
    just a small addition:

    If the buttons are checked more or less regularly (for example every msec) it can be done just by counting the nr of checks until a max value. For example code with each check (8 bit uint) : if (timeLastSwitch < 255) {++timeLastSwitch;}
    (after setting it to zero at the state switch.)
    Of course the lock out period's should be smaller than the max value.

  14. #14
    Junior Member
    Join Date
    Nov 2016
    Posts
    8
    Hello everyone,

    yes, you are right about adapting the macro method for asymmetric debounce times, AlainD. I did not event think about this option yet.

    Latency in (electronic) musical instruments is quite an issue. I learned to play the piano on a an acoustic piano with real keys, real hammers and real strings. You hit the key and in the same moment the sound is there. A decade ago, I played the first time on a virtual piano over a PC sound card, with an overall latency of I guess round about 30ms and I was absolutely disappointed, because it did not 'feel' right. The delay - even if it seems not much - between pressing the key and hearing sound does not match. But things got immensely better nowadays.

    Here is an article about the latency of virtual pianos which says, as a rule of thumb, a latency of 5 to 6 ms feels ok.

    I do not want to go crazy about key scan rates (roughly 1kHz should be enough), I just want to keep the latency in the MIDI controller down. The mentioned counting approach seems very promising for that kind of debouncing, because the first rising edge will already trigger a MIDI note. The lockout approach seems also suitable for detecting the first rising edge. Both methods do have the disadvantage, that spurious signals are not filtered out. Speed vs accuracy... choose one

    Maybe it's the best way to implement multiple debounce methods, make them selectable via compiler directives and then choose the best option for the use case.

  15. #15
    Senior Member
    Join Date
    Dec 2016
    Posts
    106
    Quote Originally Posted by afterwards View Post
    ...

    Latency in (electronic) musical instruments is quite an issue. I learned to play the piano on a an acoustic piano with real keys, real hammers and real strings. You hit the key and in the same moment the sound is there. A decade ago, I played the first time on a virtual piano over a PC sound card, with an overall latency of I guess round about 30ms and I was absolutely disappointed, because it did not 'feel' right. The delay - even if it seems not much - between pressing the key and hearing sound does not match. But things got immensely better nowadays.

    Here is an article about the latency of virtual pianos which says, as a rule of thumb, a latency of 5 to 6 ms feels ok.

    I do not want to go crazy about key scan rates (roughly 1kHz should be enough), I just want to keep the latency in the MIDI controller down. The mentioned counting approach seems very promising for that kind of debouncing, because the first rising edge will already trigger a MIDI note. The lockout approach seems also suitable for detecting the first rising edge. Both methods do have the disadvantage, that spurious signals are not filtered out. Speed vs accuracy... choose one

    Maybe it's the best way to implement multiple debounce methods, make them selectable via compiler directives and then choose the best option for the use case.
    I'm looking for buttons where I even want to do action depending on short-long push and conditional even actions on release. So I really don't mind 10-20msec delay's

    I would say that the counting approach needs at least two measurements, but it avoids one time spurious signals. The same can be done with the macro when only looking at 00 or 11.
    The lockout can be done with one edge, but then I would test the setup for noise first. Very short pulses can be filtered out with two readings only a microsecond apart or so.

  16. #16
    Junior Member
    Join Date
    Nov 2016
    Posts
    8
    Hello everyone,

    for those of you interested in the counting approach scribbled by @Nominal Animal in Post #8:

    I made the original sketch working, changed it a little and especially added comments to it in order to make the bitwise operations clearer (for learning purposes). There are a lot of things going on in a few lines (at least for me), so I added the comments. Maybe it is helpful

    Two notes to the sketch:
    1) Regarding the original sketch, I exchanged rows and columns in my sketch as it matches my hardware setup
    2) the interleaved calls are not implemented (yet), but it works really fine nevertheless.

    Thank you again, Nominal Animal!

    Here is the sketch:

    Code:
    // debouncing a button matrix
    //
    // based on code: 
    // https://forum.pjrc.com/threads/46188-Help-neede-Debouncing-a-button-matrix?&p=154816&viewfull=1#post154816
    //
    // NOTE: we are pulling up columns and read the rows as this matches the present hardware. In the 
    // mentioned post it is vice versa: there, the rows are pulled up and the columns are read)
    
    
    
    // define serial speed for serial debugging
    const uint32_t serialSpeed = 115200;
    
    
    // teensy pin connected to ST_CP of 74HC595
    const uint8_t slaveSelectPin = 10;
    // teensy pin connected to DS of 74HC595
    const uint8_t dataPin = 11;
    // teensy pin connected to SH_CP of 74HC595
    const uint8_t clockPin = 13;
    
    // bit mask for shift register ("activates" columns as the bit masks are cycled and shifted out to the shift register)
    uint8_t columnBitMask[] = {
        B00000001,
        B00000010,
        B00000100,
        B00001000,
        B00010000,
        B00100000,
        B01000000,
        B10000000
    };
    
    const uint8_t pinTable[] = { 6, 7, 8, 20, 21, 22, 23, 24 }; // choosing arbitrary pins here
    
    const uint8_t numCols = sizeof(columnBitMask);
    const uint8_t numRows = sizeof(pinTable);
    
    // debounce stuff
    const uint8_t keyPressScans = 8;
    const uint8_t keyReleaseScans = 8;
    
    // holds the current reading of whole row (for each column)
    uint8_t  keyRowState[numCols];
    
    // one counter per key
    uint8_t  keyCounter[numCols][numRows];
    
    // set up timer for checking button states 
    elapsedMicros buttonStateTimer = 0;
    
    
    void setup()
    {   
        
        // wait for serial and begin serial communication
        while (!Serial && (millis() < 1000));
        Serial.begin(serialSpeed);
        
        Serial.println("## Counter Debounce Test ##");
    
        // set input mode for the row pins
        for (int i = 0; i < numRows; i++) {
            pinMode(pinTable[i], INPUT);
        }
    
        //set shift register slave select pin as output in order to control the columns
        pinMode(slaveSelectPin, OUTPUT);
        pinMode(clockPin, OUTPUT);
        pinMode(dataPin, OUTPUT);
    
    
        uint8_t colCounter = 0;
        uint8_t rowCounter = 0;
    
        // set the row state to 0 (B00000000) for each column 
        for (colCounter = 0; colCounter < numCols; colCounter++) {
            keyRowState[colCounter] = 0;
        }
    
        // set the counter to 0 for all keys
        for (colCounter = 0; colCounter < numCols; colCounter++) {
            for (rowCounter = 0; rowCounter < numRows; rowCounter++) {
                keyCounter[colCounter][rowCounter] = 0;
            }
        }
    }
    
    void loop()
    {
        uint8_t colCounter  = 0;
        uint8_t rowCounter  = 0;
        uint8_t state   = 0;
    
    
        // query the port every 250 microseconds i.e. scan rate = 4kHz
        if (buttonStateTimer > 250) {
    
            // reset the polling timer
            buttonStateTimer = 0;
    
            // iterate columns
            for (colCounter = 0; colCounter < numCols; colCounter++) {
    
                state = 0;
                
                // shift out the bit mask for the current columns
                //
                //      if we shift out the a pattern like "B00000001" to the serial input of the shift register, the parallel ouputs will be represent the serial input
                //      serial in LSB is mapped to ouptut Q(A)
                //      serial in MSB is mapped to ouptut Q(H)      
                //                      
                //                                  Parallel Outs Q(n)
                //                                  Q(A)    Q(B)    Q(C)    Q(D)    Q(E)    Q(F)    Q(G)    Q(H):
                //              Serial In
                //              B00000001           1       0       0       0       0       0       0       0
                //              B00000010           0       1       0       0       0       0       0       0
                //              B00000100           0       0       1       0       0       0       0       0
                //              B00001000           0       0       0       1       0       0       0       0
                //              B00010000           0       0       0       0       1       0       0       0
                //              B00100000           0       0       0       0       0       1       0       0
                //              B01000000           0       0       0       0       0       0       1       0
                //              B10000000           0       0       0       0       0       0       0       1
                //                          
                // -> with each column counter loop we pull up one column
                //
                shiftOutBitMask(columnBitMask[colCounter]);
    
                // after pulling up one column in the current counter loop, we will read all row inputs to detect if there was a key press
                // example:     the first and third row pin is HIGH, the rest is LOW
                //              -> state is 00000101
                //
                state = readInputPins();
    
                // loop through the rows to generate events for the keys on this column
                for (rowCounter = 0; rowCounter < numRows; rowCounter++) {
                    
                    // create a mask for the current row
                    // the mask looks like this: 
                    //      loop 0 (rowCounter = 0): 00000001 
                    //      loop 1 (rowCounter = 1): 00000010
                    //      loop 2 (rowCounter = 2): 00000100
                    //      ...
                    const uint8_t mask = 1u << rowCounter;
    
                    // key state "ignore":
                    // keyCounter is set -> so we are ignoring the current reading and just decrease the keyCounter so that we know, when we have to check the kex again
                    if (keyCounter[colCounter][rowCounter] > 0) {
    
                        // recent key event -> ignore state changes 
                        keyCounter[colCounter][rowCounter]--;
    
                        printKeyEventIgnored(colCounter, rowCounter, keyCounter[colCounter][rowCounter]);
    
                    }
    
    
    
                    // key state: "press":
                    //      loop 0 (rowCounter = 0):
                    //          state:                              00000101
                    //          mask:                               00000001
                    //          state & mask:                       00000001
                    //          (state & mask):                     00000001 -> true
                    //      
                    //      loop 0  (colCounter = 0):   
                    //          keyRowState[colCounter]:            00000000
                    //          mask:                               00000001
                    //          (keyRowState[colCounter] & mask):   00000000 -> false
                    //          !(keyRowState[colCounter] & mask):  00000000 -> true
                    // 
                    // both expression are true -> jump into block
    
                    else if ((state & mask) && !(keyRowState[colCounter] & mask)) {
                        
                        // we have a key press event ->
    
                        // set the keyCounter in order to ignore the next "keyPressScans" values
                        keyCounter[colCounter][rowCounter] = keyPressScans;
    
                        //  loop 0  (colCounter = 0):   
                        //      keyRowState[colCounter]:            00000000
                        //      mask:                               00000001
                        //      ORed with mask:                     00000001
                        // 
                        // -> keyRowState[colCounter] represents the actual key presses / releases of the row keys of the current column
                        keyRowState[colCounter] |= mask;
    
                        printKeyEvent(colCounter, rowCounter, true);
    
                        
                    }
    
    
                    // key state: "release":
                    //      loop 0 (rowCounter = 0):
                    //          state:                              00000101
                    //          mask:                               00000001
                    //          state & mask:                       00000001
                    //          !(state & mask):                    00000000 -> false
                    //      
                    //      loop 0  (colCounter = 0):   
                    //          keyRowState[colCounter]:            00000000
                    //          mask:                               00000001
                    //          (keyRowState[colCounter] & mask):   00000000 -> false
                    // 
                    // both expression are false -> do not jump into block, we do not have a key release yet
                    //
                    //
                    // Another example (example 2) (a little later): 
                    //  assumptions: 
                    //      state = 00000100 (the third row pin is HIGH, the rest is LOW; so, in comparison to the example above, the first row pin was released)
                    //      keyRowState[0] =  00000001 (keyRowState[0] expresses the state in the loop before)
                    //
                    //      loop 0 (rowCounter = 0):
                    //          state:                              00000100
                    //          mask:                               00000001
                    //          state & mask:                       00000000
                    //          !(state & mask):                    00000001 -> true
                    //      
                    //      loop 0  (colCounter = 0):   
                    //          keyRowState[colCounter]:            00000001
                    //          mask:                               00000001
                    //          (keyRowState[colCounter] & mask):   00000001 -> true
                    // 
                    // both expression are true -> do jump into block, we have a key release now
                    else if (!(state & mask) && (keyRowState[colCounter] & mask)) {
    
                        // we have a key release event ->
    
                        // set the keyCounter in order to ignore the next "keyReleaseScans" values                  
                        keyCounter[colCounter][rowCounter] = keyReleaseScans;
    
                        // example 2:   
                        //  loop 0  (colCounter = 0):   
                        //      keyRowState[colCounter]:            00000001
                        //      mask:                               00000001
                        //      ~mask (NOT mask):                   11111110
                        //      ANDed with ~mask:                   00000000
                        // 
                        // -> keyRowState[colCounter] represents the actual key presses / releases of the row keys of the current column
                        keyRowState[colCounter] &= ~mask;
    
                        printKeyEvent(colCounter, rowCounter, false);
                    }
                }
            }
    
            // finally, process the events
        }
    }
    
    
    void shiftOutBitMask(uint8_t bitMask) {
        
        // pull slaveSelectPin LOW to keep current state
        digitalWriteFast(slaveSelectPin, LOW);
        // shift out the bits for the current column; replace by SPI transmit
        shiftOut(dataPin, clockPin, MSBFIRST, bitMask);
        // pull latch pin high so that the bitmask is represented on the 74HC595 output pins
        // 74HC595 outputs the reveived update on this rising edge
        digitalWriteFast(slaveSelectPin, HIGH);
    }
    
    
    
    
    
    // reads the specified input pins into a single 8 bit variable (single reads, with bit shifting) and returns it
    uint8_t readInputPins() {
    
        uint8_t ret = 0;
    
        if (digitalReadFast(pinTable[0])) ret |= (1 << 0);
        if (digitalReadFast(pinTable[1])) ret |= (1 << 1);
        if (digitalReadFast(pinTable[2])) ret |= (1 << 2);
        if (digitalReadFast(pinTable[3])) ret |= (1 << 3);
        if (digitalReadFast(pinTable[4])) ret |= (1 << 4);
        if (digitalReadFast(pinTable[5])) ret |= (1 << 5);
        if (digitalReadFast(pinTable[6])) ret |= (1 << 6);
        if (digitalReadFast(pinTable[7])) ret |= (1 << 7);
    
        return ret;
    }
    
    void printKeyEvent(uint8_t col, uint8_t row, boolean isKeyPress) {
    
        Serial.print(col);
        Serial.print(" ");
        Serial.print(row);
        Serial.print(" was ");
        if (isKeyPress) {
            Serial.print("pressed");
        }
        else {
            Serial.print("released");
        }
        Serial.println("");
    }
    
    void printKeyEventIgnored(uint8_t col, uint8_t row, uint8_t counter) {
    
        Serial.print(col);
        Serial.print(" ");
        Serial.print(row);
        Serial.print(" ignored. Will be ignored further ");
        Serial.print(counter);
        Serial.println(" times.");
    }

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •