My button presses vs bounce vs EMI notes

Started 19Nov2020. Updated 18Dec2020. In work. This note is in group Technology (plus My XMOS pages).

For the TIOBE index 016:[10]: «xC programming».

Fold handling with Collapse-O-Matic plugin

I am using Collape-O-Matic (here) on many pages.

Expand All (for searching)
Collapse All

Typical fold

This text 123456789 will only be found with browser search when the fold is expanded

Intro

The confusing title «My button presses vs bounce vs EMI notes» – reread as «how I handled button’s electrical bounce noise with a fairly ok (and «proven in use») algorithm, but was surprised when I saw my display coming to life, caused by the wires (=antennas) picking up electromagnetic interference (EMI) from the refrigerator compressor’s electrical motor starting and stopping.»

In this note I have moved (or rather ripped) some chapters from note 208.
The buttons discussed here are the three buttons in front of a box with an audio bass and treble unit (AudioMux from MikroElektronica 208:[5]). There also is a small display.

My box of timed out XMOS boards contained an xCORE-XA Code Module board, but I was not able to flash it. (If you are, please tell me how to. My full story is still covered in 208.) (Flashing means leaving the code in flash memory so that the board may start again after a power down, ie. it contains a non-volatile program.) I put the board back to where it came from – up with an XMOS startKIT instead; it certainly is flash-able. And besides, I love it. Except for the built-in A/D converter, where one needs to average lots of values: here.

Technically, the three buttons are handled by three event-driven xC tasks:

The XMOS Programming Guide (2015/9/18 XM004440A, chapter 8 «Handling button presses») (141:[1]) shows an example of how to handle debouncing. This works as described there, and I have used it in several projects. Until now. This is the first project where its limitation turned against me.

None of my other boxes with buttons have been placed on the fridge. Even this one also is close to an induction cooktop, I’m pointing at the fridge’s compressor. But it takes two to dance. It turned out that I had designed an effective EMI noise meter!

A historical example

I found, in the MOSTEK LINE OCTOBER 1973 data manual, on page 99-100 a «MOS 10-Digit Calculator MK 5021 C» that it contained «Internal debouncing of keyboard inputs».

I scanned the cover and pp 99-100. The PDF is here.

Aside 1: 1973 is the year I married and was 23, and had two years left at the NTH university and it was three years before I started working for Autronica. It was the year we played Peter, Paul and Mary. I listened to that album on my record player.. (alive again, after 25 years of idleness, but it this time needed an RIAA preamp (which has an USB digital output!)) ..the other day – and Album 1700 is just a great record, still. Even then it was 7 years old. The manual I saved from being thrown away at some time. It had been received 1. JUL. 1974 by the person at Autronica responsible for this. This was when all printed stuff received was stamped!

Aside 2. Hold your breath: I see in Wikipedia (here) that MOSTEK was bought by UTC in 1979. UTC bought Autronica in 2005, while I was 29 years into working there. However, in 1985, UTC sold MOSTEK to Thomson SA. It was SGS-Thomson (related to Thomson SA in another chain of ownerships, and now STMicroelectronics) that bought the British (Bristol based) Inmos Ltd. company, in 1989. Inmos invented the transputer that climbed too steeply in the mid nineties. Then, in 2005 a new Bristol based company named XMOS took over some of the ideas and experience with the transputer, and made their xCORE architecture. The code in all my notes, including this note, are in the xC language for the xCORE processors. Evolution takes its toll, and some times the steps are hard to climb. Or fall off from. By the way, at the time of writing UTC is now masqueraded as Raytheon while Otis and Carrier were spun off. Autronica (which in the meantime was Autronica Fire and Security, including when I left them in 2017) is now a Carrier company. Puh!

I just assume that the debouncing logic is digital in that chip, and that the switches of that time probably had a lot of bounce noise – and the that making the debounce circuitry analogue would be difficult.

I don’t know about the keyboard inputs of the very early computers, how did they debounce the switches?

When is the first example shown? Who first discovered that debouncing was necessary?

startKIT’s external connections

This circuit diagram is also seen in note 208. (I use iCircuit to make such simple diagrams).

fig17_208_external_connections_startkit_oyvind_teig

Fig.16 – External connections with startKIT and AudioMUX, Adafruit #931 display and three buttons (PDF, JPG)

Button press and release bounce noise

Fig.2 – Button press and release bounce noise (rather hard to get!)

When a button is depressed the line goes nicely down, when it’s released it goes nicely up again. The button’s dome and contact set quite behave. Like the top picture screen clip here. (I use a Siglent scope, standard disclaimer.)

But every so often, I could manage to get the button to actually behave badly and bounce. The center picture shows a bad press with bounce noise. Almost every time I actually saw it bounce it seemed to appear at around 75 µs after the initial press. Then there often is some «first» noise, then a rather wide pulse. I guess this in some way reflects the design of the buttons. They are product #1490 from Adafruit (here, data sheet of Omron B3F-40XX here).

I discovered that it’s mostly during the release that it’s worst. I have counted 99 edges, done by the software (below)). This is unbelievable many!

Of course, the software has to tackle this. A push is one push, not two er more. A release is one release, not 99!

EMI as sitting on the top of a fridge

Originally at 208:[EMI as sitting on the top of a fridge] chapter.

I discovered that the display went on by itself. My button tasks picked up noise from starting or stopping of the frigde compressor motor. My unit was powered from an 5V, 1A Apple USB charger, obviously not grounded, as is the fridge. When my cables (top, left in picture) were that close to the fridge, they picked up more that I could believe. In the software client that gets events from the three button tasks I counted how often the display went on by itself. Ten times a day was not unusual.

Fig.1 Non-shielded cables from buttons inside a plastic box was no good idea on top of the fridge!

I then realised that I should instead rewrite the whole button task (as mentioned, it’s started three times) and then be able to count how many positive and how many negative edges I had. Update: it now also delivers how many microseconds the noise persists. The code is at Handling button presses (below). Depressed button should ideally have caused one negative edge, releasing it one positive edge, since a button pulls a pull-up resistor down. (Exactly so, if I had bothered to add an SR latch with two NAND gates and two resistors, plus a switch with contact sets both when pressed and released [1] or Wiki-refs[Contact bounce]. I used that circuitry several times at work, in the eighties.) I saw that I did get quite many bouncing edges on both pressing and releasing on each SW event. An event was produced after 50 ms of steady signal. However, with the EMI (Electromagnetic interference) noise I could see that only released event were present. This is logical. After all, the EMI would settle, and in my testing time frame I never saw it settle as depressed.

The gross majority of EMI pickup was on the left button. I could not explain it.

But I absolutely needed to do something with the non-shielded HW. Like adding three 220 nF capacitors across R4-R6 to ground, for an RC debouncer [1]. But I didn’t have any defined hysteresis, so this just messed up my SW (the original SW without the EMI handling) since it would detect several depressed and released events per keypress. I dropped the idea even before I scoped and could have rewritten the code. However, I thought it better not to pick up the noise in the first place!

So I made a new buttons HW row, with 2.7k – 270R instead of 10k – 1k. (Photo above, diagram even further up.) (I had to make a new row, because I had been stupid enough to use Epoxy to keep the buttons mechanically stable when depressed. But now I unmounted a metal frame from three other buttons instead, some with too short pins.) Lower impedance (245 Ohm vs. 909 Ohm) usually picks up less EMI. And I shielded and grounded the cable, plus an aluminium shield below the electronics. This made EMI pickup disappear (update: two weeks after there have been no, zero, EMI pickup), but the buttons certainly bounced much more! I could get 17 edges on a single press! I even got depressed and released event numbers out of phase after I had pressed a lot! This could be because I had a different batch of buttons, or had increased the current. I simply don’t know. Stay tuned.

Aside: The display and AudioMUX I2C buses never seemed to cause any hiccups. AudioMUX I2C lines have 4.7k pull-ups. The display [2] have 10k pull-ups, but also have a logic level shifter MOSFET. Besides, I2C has a high EMC (Electromagnetic compatibility), since any noise would start a state machine, but it should be strange if the noise pulses would make any I2C-sense. Plus, these lines could be as lucky as the two rightmost buttons that seemed to be in some kind of shadow.

The results so far (after two weeks):

  1. No edges from induced EMI
  2. Much more bouncing when buttons pressed and release (discussed above)

Handling button presses

Originally at 208:[Handling button presses] chapter.

In the XMOS code (below), referred to in the intro (141:[1] has, in my opinion a major flaw, even if I have used it in several projects with success. One could say that it is good enough. And besides, my code (further down) is more complex.

The problem is that any first change of the port pin, be it a button press or release, or bouncing noise or EMI noise, will start a delay and the code does not follow the port and sees how it behaves during the wait. One could say that this is the whole purpose: forget about the port pin, we want to filter it! Come on!

But then, after the delay, the present state of the pin isn’t checked. What is sent off is the new value of the port after the change which started the delay. It basically sends over the value that was, not the one that is.

A good thing about this, and the code, is that a up-up or down-down is not possible. Only down-up or up-down. Any state machine in the other end (in the client that listens to the buttons), that may be dependent on any sequence of up-down (pressed) or down-up (released) would then automatically be pleased – no filter required.

Go to the source for more explanation of this code, if you are unfamiliar with xC.

Handling button presses code, original by XMOS

#include <platform.h>
#include <stdio.h>
#include <timer.h>  // XS1_TIMER_HZ etc

[[combinable]]
void task1a(port p_button) {
    int current_val = 0;
    int is_stable = 1;
    timer tmr;
    const unsigned debounce_delay_ms = 50;
    unsigned debounce_timeout;
    while (1) {
        select {
            // If the button is "stable", react when the I/O pin changes value
            case is_stable => p_button when pinsneq(current_val) :> current_val: {
                if (current_val == 1) {
                    printf("Button up\n");
                } else {
                     printf("Button down\n");
                }
                is_stable = 0;
                int current_time;
                tmr :> current_time;
                // Calculate time to event after debounce period
                // note that XS1_TIMER_HZ is defined in timer.h
                debounce_timeout = current_time + (debounce_delay_ms * XS1_TIMER_HZ);
            } break;
            // If the button is not stable (i.e. bouncing around) then select
            // when we the timer reaches the timeout to renter a stable period
            case !is_stable => tmr when timerafter(debounce_timeout) :> void: {
                is_stable = 1;
            } break;
        }
    }
}
Since the above code effectively picked up so much EMI noise in my unshielded hardware, I needed to go further. I did this before I made the shielded version, because I wanted to make a button line signal analyser software.every

This code has the long_button_enabled functionality, so it’s not entirely true that I used the above code per se. However, the compiled code uses long_button_disabled. I use another mechanism to handle repeat. On the client side there are combinations of button activations and a repeat interval that starts after longer time (like after 3.0 sec), but then goes faster (like 0.5 sec, with increasing strength of action (adds or subtracts with more and more). That code is also available in the download. This could have been handled here, but I was afraid I needed to feed state back into Button_Task_4_. Now, only data is sent from it, which of course is simpler. This implementation discovers and records all noise, and will then add a delay after the last edge.

It also allows to do something during the wait: it counts the number of edges plus how long time the noise/EMI was active.

There is nothing tricky about the code (below). The select takes three cases:

  1. (not max_time_reached) => p_button when pinsneq(button_on_event) :> button_on_event:
    This triggers on any change of the port. There is no boolean guard (like is_stable in XMOS code) to stop it being a signal analyser
  2. do_timeout_debounce_now => tmr_debounce when timerafter(timeout_debounce) :> void:
    DEBOUNCE_TIMEOUT_MS after last pin change the present value is sent. This cannot be anything else than the present value. Theoretically any EMI noise that does not have a DEBOUNCE_TIMEOUT_MS pause might cause this code never stop. I could have added a max count or max time. I will. Stay tuned. Update: as you see, I did add the boolean guard (not max_time_reached) to fix this
  3. do_timeout_long_now => tmr_long when timerafter(timeout_long) :> void:
    This handles long presses.

Press for menu

The below code returns button_edge_cnt and button_noisy_time_us via the interface call button, defined in the button_if_3 interface. For the full explanation, press the picture to go to blog note 208. But there are three value lines, one for each button. The INN/IN column shows the number of presses/pushes, UT/OUT shows the number of releases. Nå/Now shows button_edge_cnt the count of the present IN or OUT. To see the IN value, the button must be held depressed. Max is the max button_edge_cnt of any IN or OUT. usMax/µsMax shows the how long time in micro-seconds that the noise/bounce/EMI lasted.

Typical values might be those shown. Observe the usMax=0 in the center button. It has been pressed once, there was one edge only and per def the noise then lasted zero us. This happens quite often.

It may look strange that INN/IN and UT/OUT may not always have the same count. I have only seen values with INN/IN less than UT/OUT. This may happen if I press a button «wildly». I once saw usMax/µsMax up to 40000 µs (40 ms) in that case. Since the value that’s sent off is not the opposite of the previous value (as the above task1a did), this task sends off a stable value, and after DEBOUNCE_TIMEOUT_MS (100 ms). When I then press very short and then again very short it might of course happen that it would be stable as released UT/OUT more than once in a row. My hypothesis to the fact that I have only observed more UT/OUT than INN/IN is that the pull-up resistors R1-R3 would naturally «end» any relaxed state as voltage high = UT/OUT.

In the code below I have implemented a button bouncing / noise / EMI analyser. There is no filtering. Therefore one should take care to design the connected driver such that it filters what might be necessary. My client buttons_client_task in file _AudioMux_controller_startKIT.xc did not need any filtering. See code download:

Download code from 208:[Startkit code].

Handling button presses code

[[combinable]]
void Button_Task_4_ (
        const unsigned              button_n,
        const unsigned              max_button_noisy_time_us, // added
        const long_button_enabled_e long_button_enabled,
        in buffered port:1          p_button,
        client button_if_3          i_button_out)
{
    int      button_on_event = BUTTON_PRESSED;
    bool     do_timeout_debounce_now = false;
    bool     do_timeout_long_now = false;
    bool     filter_next_button_released = true; // This would come initially
    timer    tmr_debounce; // Only one combined hardware timer..
    timer    tmr_long;     // ..is used for these two software timers
    time32_t timeout_debounce;
    time32_t timeout_long;
    time32_t current_time;
    unsigned button_edge_cnt = 0;
    time32_t noisy_start_time;
    unsigned button_noisy_time_us = 0;
    bool     noisy_start_measure = true; // AMUX=011 new
    bool     max_time_reached = false;   // AMUX=011 new

    while(1) {
        #pragma xta endpoint "start" // in "my_script.xta" script
        select {
            case (not max_time_reached) => p_button when pinsneq(button_on_event) :> button_on_event: {
                #pragma xta endpoint "stop" // in "my_script.xta" script
                do_timeout_debounce_now = true;
                do_timeout_long_now     = false;
                button_edge_cnt++;

                tmr_debounce :> current_time;

                if (noisy_start_measure) {
                    noisy_start_measure = false;
                    noisy_start_time = current_time;
                } else {}

                button_noisy_time_us = (current_time - noisy_start_time) / XS1_TIMER_MHZ; // DEBOUNCE_TIMEOUT_MS may start several times after this
                max_time_reached = (button_noisy_time_us > max_button_noisy_time_us);

                if (max_time_reached) {
                    // Avoid forever picking up noise in the DEBOUNCE_TIMEOUT_MS "shadow"
                    timeout_debounce = current_time; // immediately
                } else {
                    timeout_debounce = current_time + (DEBOUNCE_TIMEOUT_MS * XS1_TIMER_KHZ);
                }
            } break;

            case do_timeout_debounce_now => tmr_debounce when timerafter(timeout_debounce) :> void: {

                if (button_on_event == BUTTON_PRESSED) {
                    i_button_out.button (BUTTON_ACTION_PRESSED, button_edge_cnt, button_noisy_time_us); // Button down
                    if (long_button_enabled == long_enabled) {
                        do_timeout_long_now = true;
                        tmr_long :> current_time;
                        timeout_long = current_time + (BUTTON_ACTION_PRESSED_FOR_LONG_TIMEOUT_MS * XS1_TIMER_KHZ);
                    } else {
                        // long_disabled, no code
                    }
                } else if (filter_next_button_released) {
                    // BUTTON_RELEASED, but we don't want it after BUTTON_ACTION_PRESSED_FOR_LONG, no code
                } else { // BUTTON_RELEASED
                    i_button_out.button (BUTTON_ACTION_RELEASED, button_edge_cnt, button_noisy_time_us);
                }

                do_timeout_debounce_now     = false;
                filter_next_button_released = false;
                button_edge_cnt             = 0;
                noisy_start_measure         = true;
                max_time_reached            = false;
            } break;

            case do_timeout_long_now => tmr_long when timerafter(timeout_long) :> void: {
                // It is important that during this waiting max_time_reached is false, so that a BUTTON_ACTION_PRESSED may cancel this waiting

                if (button_on_event == BUTTON_PRESSED) {
                    i_button_out.button (BUTTON_ACTION_PRESSED_FOR_LONG, button_edge_cnt, button_noisy_time_us);
                    filter_next_button_released = true;
                } else { // BUTTON_RELEASED
                    // cannot happen here since do_timeout_long_now was set when BUTTON_PRESSED, no code
                }

                do_timeout_long_now = false;
                button_edge_cnt     = 0;
                noisy_start_measure = true;
            } break;
        }
    }
}

How fast is it?

I have added two #pragma xta .. in the code fold above. This is to let the XTA timing analysis tool (introduction 208:[Timing analysis]) analyse how fast the code is at fastest. I also added the my_script-xta file:

analyze endpoints start stop 
set required - 1.0 us

Here’s the result after the compilation and XTA analysis:

...
xta: .././my_script.xta:10: warning: route(0)     Pass with 1 unknown, Num Paths: 1, Slack: 872.0 ns, Required: 1.0 us, Worst: 128.0 ns, Min Core Frequency: 64 MHz
...

In other words, the code above is able to detect edges 128 ns apart → 7.8 MHz/2 = 3.6MHz. Divide by two since a period would be one raising and one falling edge (128 ns) plus 128 ns to the start of the next raising edge again. This is quite ok, considering what I need to pick up in this application. It’s far faster than any specification I could have dreamt up. It also shows how suitable this processor and the xC language as a pair is. Observe that 128 ns is a guarantee! All the other tasks could do what they want, but here, with this instance of that task, it’s a guarantee!

The code (download)

Download at My xC code downloads page, note #214.sub. It’s part of the My processor-to-analogue audio equaliser notes code base.

References

Wiki-refs

Numbered refs

  1. A Guide to Debouncing (2004-2008) by Jack G. Ganssle, see https://my.eng.utah.edu/~cs5780/debouncing.pdf. Also see Wiki-refs Switch[Contact bounce] above
  2. Monochrome 128×32 I2C OLED graphic display, I2C, see Adafruit https://www.adafruit.com/product/931. It contains an SSD1306 from SOLOMON SYSTECH

Leave a Reply

Dette nettstedet bruker Akismet for å redusere spam. Lær om hvordan dine kommentar-data prosesseres.