As the title says... Chat on...

User avatar
By marcelstoer
#33300 I'm looking for a way to properly debounce GPIO readings. So far I failed.

I have a reed switch (magnetic switch, http://j.mp/1Hy4nGf) connected to the ESP.

The first attempt was to debounce based on timer and delay. This works fine for a few dozen minutes until it simply stalls. I don't get any change events anymore at all, the rest of the application runs fine. I suspect that the cause is 'tmr.now()' which behaves not as you'd expect (https://github.com/nodemcu/nodemcu-firmware/issues/690).

Code: Select all-- source: https://github.com/hackhitchin/esp8266-co-uk/blob/master/tutorials/introduction-to-gpio-api.md
local pin = 4    --> GPIO2

function debounce (func)
    local last = 0
    local delay = 200000

    return function (...)
        local now = tmr.now()
        if now - last < delay then return end

        last = now
        return func(...)
    end
end

function onChange ()
    print('The pin value has changed to '..gpio.read(pin))
end

gpio.mode(pin, gpio.INT, gpio.PULLUP)
gpio.trig(pin, 'both', debounce(onChange))


The next attempt was to do it simple and straight forward by registering two separate functions for GPIO up/down events. This runs stable for days. However, a few times per day I get 'down' events even though the switch wasn't triggered physically at all.

Code: Select all-- inspired by: http://www.esp8266-projects.com/2015/03/buttons-pushbuttons-and-debouncing-story.html
local GPIO14 = 5
gpio.mode(GPIO14, gpio.INT, gpio.PULLUP)
gpio.trig(GPIO14, "down", doorLocked)

function doorLocked()
    print("Door locked")
    tmr.delay(50)                          -- time delay for switch debounce
    gpio.trig(GPIO14, "up", doorUnlocked)  -- change trigger on falling edge
end

function doorUnlocked()
    print("Door unlocked")
    tmr.delay(50)
    gpio.trig(GPIO14, "down", doorLocked)  -- trigger on rising edge
end


Could it be that the false 'down' event has nothing to do with the debounce at all but is simply a characteristic of the reed switch?
User avatar
By TerryE
#33433 Hi Marcel. My understanding of the characteristic of read relays is that when you pulldown you normally get a single break tirgger, but when you pullup to close the relay then the contanct can literally bounce a couple of time (a bit reminiscent of Philae!!) so that the you will get short sequence of make + break triggers ending in a final make when the contact finally lands.

Now to the code sample. This is definitely not the way to do it. It's a flawed piece of coding, because it ignores any edge triggers within the first 200mSec. Occasionally the relay will land and settle within this 200mSec window and in this scenario the algo will suppress the trigger entirely. The problem is that the number of bounces and the time to the last bounce can vary from relay type to relay type, so you need to time your "settle" parameter to your own relays.

The bounces occur too fast to log immediately but something like this will help you to set the correct constants for your relay optimally
Code: Select alllocal pin = 4    --> GPIO2

function debounced_trigger(pin, onchange_function, dwell)
  local start, last_event = tmr.now()
  local times = {}  -- collect trigger time offsets +/- = high/low
  local function trigger_cb(event)
    last_event = event
    if (last_even == nil) then tmr.alarm(6, dwell, 0, function()
        onchange_function(pin, last_event, times)
      end)
    end
    local delta = tmr.now() - start
    times[#times+1] = (event == gpio.HIGH) and delta or -delta
  end
  gpio.trig(pin, 'both', trigger_cb)
end

function onChange (pin, last_event, times)
    ('The pin value has changed to %d with last event %d after these contacts: %s'):format(
       gpio.read(pin), last_event, table.concat(times, ', '))
end

debounced_trigger(pin, onChange, 1000)
gpio.mode(pin, gpio.INT, gpio.PULLUP)

Happy to discuss here or by 1-1 email :D

Edit
: Example updated to fix logic flaw that Marcel found.
Last edited by TerryE on Wed Nov 18, 2015 6:20 am, edited 3 times in total.
User avatar
By TerryE
#33561 For Marcel and others who might come across this example. let me expand on a few of the points underpinning this.
  • Delays of more than 10mSec are a no-no, and I have a personal guideline of avoiding making any Lua callback executed under the SDK more than 100 executed instructions or so. There needs to be a really good reasons for doing so.
  • UART output is slow and just doing this will impact any timing collection, so any real time delays need to collected into an array and only dumped on completion of the timing.
  • So what we want to do is to have (a) some gpio.trig callback collecting the timings and then a second (this is tunable) later collect the timings.
  • Those with the latest dev build can do ./luac.cross -l -o /dev/null thisExample.lua to see what code it generates. (I use an Ubuntu VM to do my builds as described in my Toolchain wiki article).
  • The main file defines two routines: debounced_trigger and onChange, and the first of these in turn defines trigger_cb and an anonymous callback in the alarm API call.
  • The main routine calls debounced_trigger() passing onChange as a function parameter., and this then registers its own trigger_cb and books an alarm delay mSec later to call the supplied loggin function parameter
It's useful to understand how Lua implements closures by upvalues. Upvalue analysis is done as part of the compilation. They are sort of like parameters but different. A parameter is bound to a function when the call is executed. An upvalue is bound to the routine at the point in the code when the statement declaring the function is declared. If you understand why the following prints "1=4 1=5 1=6 2=4 2=5 2=6 3=4 3=5 3=6" then you've got what I mean.
Code: Select alls = ''
for i = 1,3 do
  function a(n) s = string.format('%s %s=%s', s, i, n) end
  for j = 4,6 do a(j) end
 end
print(s)

The function a() is only compiled once, but the statement where it is declared is executed 3 times and the one where is is called is executed 3×3 times.

BTW, Upvalues which are still in scope are kept by the Lua runtime even if the routine that declared then has gone and been garbage collected. Don't worry about how this happens. I understand the details and it's really complicated, but its standard LUa stuff and the general advice is to treat this as magic. There there is a subtle "bug" in this code in that the timer firing does not clear down the trigger callback, so trigger_cb() and its upvalues (last_event, start, times) will persist and in effect will act as memory leak. To clear these down the alarm trigger would either need to set the pins mode to other than gpio.INT or declare a replacement dummy callback: for example
Code: Select allgpio.trig(pin, 'both', function(e) end)
or even the "trick" which evaluates type(event) returning "number" to the library trigger function which then ignored.
Code: Select allgpio.trig(pin, 'both', type)
User avatar
By marcelstoer
#33910 Thanks Terry, very nice explanation. I have ways to go to feel comfortable with Lua...Your description of the characteristics of a reed switch is spot on AFAIU. Also, based on my very limited experience I would claim that reed switches are lot more stable as far as bouncing goes than regular push-button switches - which makes sense IMHO.

A couple of remarks & questions:
  • The 'local delay = 200000' in the first code snippet is bogus but it's my fault. I pasted an early experiment of mine (when I knew even less than now what I'm actually doing). The original code I linked to from my code uses 'delay = 5000' which makes a lot more sense.
  • Isn't any debounce code that relies on 'tmr.now()' doomed to fail at some point because of https://github.com/nodemcu/nodemcu-firmware/issues/690 (time leaping backwards)?
  • "My" second piece of code basically works fine and doesn't violate your personal guideline because the delay is only 50us?
  • Thanks to a colleague at work and https://learn.sparkfun.com/tutorials/pull-up-resistors I learned a bit about pull-up resistors today. The lack of such a resistor could very well explain the behavior I'm seeing. Even though in both my examples I uses the ESPs internal pull-up resistor on the GPIO pin ('gpio.mode(GPIO14, gpio.INT, gpio.PULLUP)') I now also added an extra 10kΩ pull-up resistor to my setup. I'll keep it running for a couple of days and see what the behavior is.