|
| 1 | +--- |
| 2 | +title: "Bouncing Buttons" |
| 3 | +excerpt: Mechanical elements of buttons can create noise when pressed and released. I was looking at options to debounce buttons in the most memory efficient way. |
| 4 | +image: &image "/assets/images/switches.jpg" |
| 5 | +categories: Programming |
| 6 | +tags: Arduino |
| 7 | +header: |
| 8 | + teaser: *image |
| 9 | + overlay_image: *image |
| 10 | + overlay_filter: 0.5 |
| 11 | +toc: true |
| 12 | +toc_sticky: true |
| 13 | +--- |
| 14 | +I am writing an [Arduino Button Library Extension (ABLE)](https://www.jsware.io/able-buttons/) to provide a simple, efficient, low-memory usage button library. It will support push buttons, clickable buttons and long press buttons, but the library won't include all these features unless you need them. |
| 15 | + |
| 16 | +If you use buttons, microswitches and similar mechanical devices in your projects, you may find when pressed (or released) the transition between open and closed is not completely clean. As the metal contacts (dis)connect, the state can "bounce around" between open and closed before stabalising. |
| 17 | + |
| 18 | +To a program running on a micro-controller, this [contact bouncing](https://en.wikipedia.org/wiki/Switch#Contact_bounce) creates "noise", producing a stream of signals interpreted as multiple presses until the signal becomes stable in its intended state. A simple loop-counting program running on the Arduino Nano can `loop()` approximately 289,000 times per second. |
| 19 | + |
| 20 | +{% include figure image_path="https://upload.wikimedia.org/wikipedia/commons/a/ac/Bouncy_Switch.png" alt="Contact Bounce" class="align-center" width="300" caption="Contact Bouncing" %} |
| 21 | + |
| 22 | +Additional circuitry could smooth these transitions, but a software-only solution is useful. This involves sampling the button signal in quick succession to ensure the signal has stabalised. |
| 23 | + |
| 24 | +There is a [Debounce](https://docs.arduino.cc/built-in-examples/digital/Debounce) LED toggle example found in the Arduino IDE under File > Examples > 02. Digital > Debounce. This uses significant program and variable memory, especially for microcontrollers with limited memory such as Arduino Nanos: |
| 25 | + |
| 26 | +``` |
| 27 | +Sketch uses 1116 bytes (3%) of program storage space. Maximum is 30720 bytes. |
| 28 | +Global variables use 19 bytes (0%) of dynamic memory, leaving 2029 bytes for local variables. Maximum is 2048 bytes. |
| 29 | +``` |
| 30 | + |
| 31 | +I looked at various approaches based on the Arduino sample and some internet searches. Using a simple setup of an Arduino Nano and push button connected from pin 2 to ground (using the internal pull-up resistor in the Nano), I optimised the debouncing algorithm. |
| 32 | + |
| 33 | +{% include figure image_path="/assets/images/able-buttons.jpg" alt="Simple Button Setup" class="align-center" width="600" caption="Simple Button" %} |
| 34 | + |
| 35 | +In this configuration, the internal pull-up resistor of pin 2 keeps the signal HIGH when not pressed and closing the push button grounds the signal, making it LOW when pressed. Thus a `digitalRead(BUTTON)` returns 1 when not pressed and 0 when pressed. |
| 36 | + |
| 37 | +# Non-Debounced Button |
| 38 | + |
| 39 | +The simplest program involves not debouncing the button. In some scenarios this might be acceptable (for example direct control of an LED instead of toggling): |
| 40 | + |
| 41 | +```c++ |
| 42 | +#define BUTTON 2 |
| 43 | + |
| 44 | +void setup() { |
| 45 | + pinMode(BUTTON, INPUT_PULLUP); |
| 46 | + pinMode(LED_BUILTIN, OUTPUT); |
| 47 | +} |
| 48 | + |
| 49 | +void loop() { |
| 50 | + digitalWrite(LED_BUILTIN, digitalRead(BUTTON)); |
| 51 | +} |
| 52 | +``` |
| 53 | + |
| 54 | +``` |
| 55 | +Sketch uses 890 bytes (2%) of program storage space. Maximum is 30720 bytes. |
| 56 | +Global variables use 9 bytes (0%) of dynamic memory, leaving 2039 bytes for local variables. Maximum is 2048 bytes. |
| 57 | +``` |
| 58 | + |
| 59 | +When a button grounding pin 2 is pressed, the LED stops lighting up. This consumes a small amount of program storage and dynamic memory. I use it as a benchmark. |
| 60 | + |
| 61 | +## Setup `pinMode` order |
| 62 | + |
| 63 | +Interestingly switching the order of `pinMode` calls in the `setup()` function, it costs 2 bytes of program storage: |
| 64 | + |
| 65 | +```c++ |
| 66 | +void setup() { |
| 67 | + pinMode(LED_BUILTIN, OUTPUT); |
| 68 | + pinMode(BUTTON, INPUT_PULLUP); |
| 69 | +} |
| 70 | +``` |
| 71 | + |
| 72 | +``` |
| 73 | +Sketch uses 892 bytes (2%) of program storage space. Maximum is 30720 bytes. |
| 74 | +Global variables use 9 bytes (0%) of dynamic memory, leaving 2039 bytes for local variables. Maximum is 2048 bytes. |
| 75 | +``` |
| 76 | + |
| 77 | +There must be a compiler optimisation available. I am not sure what at this point. It would be worth dissassembing the generated code to see. Something to investigate later (with `byte` vs `bool` types discussed below). |
| 78 | + |
| 79 | +## Non-Debounced Toggle |
| 80 | + |
| 81 | +To illustrate the contact-bounce problem, the toggle example below fails intermittently. When working correctly, releasing the button (i.e. completing a "click") should toggle the in-built LED. However, sometimes the LED changes during both button-press and release, or it flickers for a moment. A non-debounced button generates multiple signals toggling the button on and off with a single press: |
| 82 | + |
| 83 | +```c++ |
| 84 | +#define BUTTON 2 |
| 85 | + |
| 86 | +bool led = false; |
| 87 | +bool state = false; |
| 88 | + |
| 89 | +void setup() { |
| 90 | + pinMode(BUTTON, INPUT_PULLUP); |
| 91 | + pinMode(LED_BUILTIN, OUTPUT); |
| 92 | +} |
| 93 | + |
| 94 | +void loop() { |
| 95 | + bool reading = digitalRead(BUTTON); |
| 96 | + |
| 97 | + // If button not pressed but was pressed. |
| 98 | + if(reading && !state) { |
| 99 | + led = !led; |
| 100 | + digitalWrite(LED_BUILTIN, led); |
| 101 | + } |
| 102 | + |
| 103 | + state = reading; |
| 104 | +} |
| 105 | +``` |
| 106 | + |
| 107 | +``` |
| 108 | +Sketch uses 910 bytes (2%) of program storage space. Maximum is 30720 bytes. |
| 109 | +Global variables use 11 bytes (0%) of dynamic memory, leaving 2037 bytes for local variables. Maximum is 2048 bytes. |
| 110 | +``` |
| 111 | + |
| 112 | +# Cleaning up Debounce |
| 113 | + |
| 114 | +I went through the Arduino [Debounce](https://docs.arduino.cc/built-in-examples/digital/Debounce) example and cleaned it up whilst keeping toggle functionality: |
| 115 | + |
| 116 | +* I avoided the unnecessary unsigned long delay value as we're only interested in the delta between two `millis()` calls. |
| 117 | +* Since the digital pins can only be on or off, I also changed the `led`, `state`, `last` and `reading` variables to `bool` types. |
| 118 | +* Finally I combined the toggle checks into a single check. |
| 119 | + |
| 120 | +The result is: |
| 121 | + |
| 122 | +```c++ |
| 123 | +#define BUTTON 2 |
| 124 | +#define DELAY 50 |
| 125 | + |
| 126 | +bool led = false; |
| 127 | +bool state = false; |
| 128 | +bool last = false; |
| 129 | +unsigned long debounce = 0; |
| 130 | + |
| 131 | +void setup() { |
| 132 | + pinMode(BUTTON, INPUT_PULLUP); |
| 133 | + pinMode(LED_BUILTIN, OUTPUT); |
| 134 | +} |
| 135 | + |
| 136 | +void loop() { |
| 137 | + bool reading = digitalRead(BUTTON); |
| 138 | + |
| 139 | + // New reading, so start the debounce timer. |
| 140 | + if (reading != last) { |
| 141 | + debounce = millis(); |
| 142 | + } else if ((millis() - debounce) > DELAY) { |
| 143 | + // Use reading if we have the same reading for > DELAY ms. |
| 144 | + |
| 145 | + // If button not pressed but was pressed. |
| 146 | + if(reading && !state) { |
| 147 | + led = !led; |
| 148 | + digitalWrite(LED_BUILTIN, led); |
| 149 | + } |
| 150 | + |
| 151 | + state = reading; |
| 152 | + } |
| 153 | + |
| 154 | + last = reading; |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +``` |
| 159 | +Sketch uses 1030 bytes (3%) of program storage space. Maximum is 30720 bytes. |
| 160 | +Global variables use 16 bytes (0%) of dynamic memory, leaving 2032 bytes for local variables. Maximum is 2048 bytes. |
| 161 | +``` |
| 162 | + |
| 163 | +## Bytes vs Bools |
| 164 | + |
| 165 | +Another currently unclear compiler optimisation worth investigation was achieved by using `byte` variables for `state`, `last` and `reading`. Using `byte` instead of `bool` types saved some program storage space: |
| 166 | + |
| 167 | +``` |
| 168 | +Sketch uses 1020 bytes (3%) of program storage space. Maximum is 30720 bytes. |
| 169 | +Global variables use 16 bytes (0%) of dynamic memory, leaving 2032 bytes for local variables. Maximum is 2048 bytes. |
| 170 | +``` |
| 171 | + |
| 172 | +Conversely, changing `led` to a `byte` variable increased program storage by 2 bytes. |
| 173 | + |
| 174 | +# Bitshift Readings |
| 175 | + |
| 176 | +One elegant way to take multiple measurements follows an algorithm described in [A Guide to Deboucing, Part 2](http://www.ganssle.com/debouncing-pt2.htm) by The Ganssle Group. |
| 177 | + |
| 178 | +This involves shifting pin reads into a `readings` variable on a regular basis. The resulting `readings` value is `0x00` when the last 8 readings returned the same closed state and the value `0xff` when the last 8 readings were the same open state. Any other value indicates an intermediate state. Shifting a new reading in every 5ms means 8 readings every 40ms. |
| 179 | + |
| 180 | +|Binary Readings|Meaning| |
| 181 | +|--------|---------| |
| 182 | +|`11111111`|Button has been open for >=40ms.| |
| 183 | +|`11111110`|Last reading shows button closed...| |
| 184 | +|`11110100`|Button bouncing around...| |
| 185 | +|`10100000`|Button has been closed for 25ms, still might bounce around...| |
| 186 | +|`10000000`|Button reads closed for 7 x 5ms cylces (35ms). Almost there...| |
| 187 | +|`00000000`|Button closed for >=40ms, so can be considered stable.| |
| 188 | + |
| 189 | +The challenge is `loop()` speed (remember the 289,000 loops/sec). We can't read each time round the loop as this passes too quickly. Adding a timer guard to only read every 5ms ensures enough time passes independent of CPU speed and work done by the `loop()`: |
| 190 | + |
| 191 | +```c++ |
| 192 | +#define BUTTON 2 |
| 193 | + |
| 194 | +bool led = false; |
| 195 | +byte readings = 0; |
| 196 | +bool state = 0; |
| 197 | +unsigned long debounce = 0; |
| 198 | + |
| 199 | +void setup() { |
| 200 | + pinMode(BUTTON, INPUT_PULLUP); |
| 201 | + pinMode(LED_BUILTIN, OUTPUT); |
| 202 | +} |
| 203 | + |
| 204 | +void loop() { |
| 205 | + if(millis() - debounce >= 5) { |
| 206 | + readings = (readings << 1) | digitalRead(BUTTON); |
| 207 | + debounce = millis(); |
| 208 | + } |
| 209 | + |
| 210 | + // If button not pressed but was pressed. |
| 211 | + if(readings == 0xff && !state) { |
| 212 | + state = true; |
| 213 | + led = !led; |
| 214 | + digitalWrite(LED_BUILTIN, led); |
| 215 | + } else if(readings == 0x00) { |
| 216 | + state = false; |
| 217 | + } |
| 218 | +} |
| 219 | +``` |
| 220 | + |
| 221 | +``` |
| 222 | +Sketch uses 1040 bytes (3%) of program storage space. Maximum is 30720 bytes. |
| 223 | +Global variables use 16 bytes (0%) of dynamic memory, leaving 2032 bytes for local variables. Maximum is 2048 bytes. |
| 224 | +``` |
| 225 | + |
| 226 | +Timing with a 4-byte unsigned long `debounce` value results in program storage higher than other options. Any gains with shifting readings into a `readings` variable is lost. |
| 227 | + |
| 228 | +If you know the `loop()` speed you could count the loop calls and read at regular intervals: |
| 229 | + |
| 230 | +```c++ |
| 231 | +#define DELAY 0xff |
| 232 | +... |
| 233 | +byte debounce = 0; |
| 234 | +... |
| 235 | +void loop() { |
| 236 | + if(!(++debounce & DELAY)) { |
| 237 | + readings = (readings << 1) | digitalRead(BUTTON); |
| 238 | + } |
| 239 | + ... |
| 240 | +``` |
| 241 | +
|
| 242 | +``` |
| 243 | +Sketch uses 960 bytes (3%) of program storage space. Maximum is 30720 bytes. |
| 244 | +Global variables use 13 bytes (0%) of dynamic memory, leaving 2035 bytes for local variables. Maximum is 2048 bytes. |
| 245 | +``` |
| 246 | +
|
| 247 | +The `debounce` variable becomes a `byte` or `unsigned int` large enough to count the `loop()` calls. To use part of a larger variable, you need to mask off the low-order bits using `0x1`, `0x3`, `0x7`, `0xf`, `0x1f`, `0x3f`, `0x7f`, `0xff`, `0x1ff` ... `0xffff` etc. to test for zero. When the value becomes zero (through masking) or because it wraps round, a reading can be taken. |
| 248 | +
|
| 249 | +Another option would be to capture the readings using a [TimerInterrupt](https://www.arduino.cc/reference/en/libraries/timerinterrupt/) callback function and process them in the `loop()` function. This approach is beyond the scope of this blog post. The program storage required would likely exceed guarding readings in the `loop()` function. |
| 250 | +
|
| 251 | +# Conclusions |
| 252 | +
|
| 253 | +Debouncing is important in many situations (though not all). Even if not critical, the debounce code can be optimised to minimise additional memory overhead making it worthwhile for many situations. If memory is paramount, perhaps hardware solutions would be better. |
| 254 | +
|
| 255 | +Whilst the bit-shifting option is elegant, the main challenge is reading at regular intervals. Care should be taken not to incur additional memory requirements making the result less memory efficient than an optimised original solution. |
| 256 | +
|
| 257 | +If you can count the `loops()` and take readings only when the count wraps/masks to zero, this can be the most memory efficient solution. This becomes dependent on CPU speed **and** the amount of work performed in the `loop()`. |
| 258 | +
|
| 259 | +The sample programs shown in this post can be found on [GitHub](https://github.com/jsware/DebounceTests). |
0 commit comments