0

I have a project that involves using a Raspberry Pi to sense the state of a mechanical switch (actually, a relay, part of this light beam sensor). When the switch is closed, I want to sound a doorbell.

Using the Raspberry Pi is convenient because it means I can turn off the doorbell from my computer and it allows me to record the times when visitors came. It also simplifies the implementation of certain requirements, for example the doorbell solenoid will melt if it receives current for too long; with the Raspberry Pi, I can control exactly how long the solenoid should receive power.

I used this code example from Alex on RasPi.TV, which shows how to use Ben Croston's RPi.GPIO Python module to detect changes on GPIO inputs, implemented with interrupts rather than busy-waiting. The example was easy to follow and the hardware requirements are minimal. For my current project, I set the pin to 21 and configure an internal pull-up resistor. The last two pins of the GPIO header are GPIO 21 and GND. So I just connect a normally-open switch between the last two pins - the ones closest to the Ethernet jack - here is a picture of my test setup:

picture of pi connected to switch

I discovered that correct "debouncing" of the switch signal is important. First of all, with my doorbell I sometimes get false alarms in the middle of the night - perhaps a leaf falling in the path of the light beam, or a squirrel. Secondly, sometimes just the act of sending power to the doorbell causes a glitch on the GPIO ports, so that it rings several times in succession even though the switch only closed once.

I modified the code example to use the debouncing feature of the RPi.GPIO module, but after playing around with it and reading the module code, I can see that the debouncing feature doesn't do what most users would want or expect. In RPI.GPIO, "debouncing" seems to mean that if the event you're waiting for ("falling edge") happened less than bouncetime milliseconds ago, we ignore it. Consider the following signal, representing a noisy switch that has been closed and then opened. Imagine that we have a bouncetime of 300 ms, and each character represents the switch's state for 100ms. The original code will detect a "debounced" falling edge at the locations indicated:

111111111111101010110010100000000000010101101111011111111
             |                        |         |

What I really want is something more like this:

111111111111101010110010100000000000010101101111011111111
                            |

in other words, the program waits for the signal to go to zero and stay there for bouncetime milliseconds, before performing the action.

I would think that other Pi users would have solved this problem already, so maybe there is good code out there that I should refer to. I have solved it, at least to my satisfaction, and will post the code as an answer to this question.

5 Answers 5

4

The stated question is regarding contact bounce. However, the data presented in the question does not reflect contact bounce - contact bounce does not repeat over the 100msec & 300msec durations presented in the text of the question. Let me try to be clear: The data provided does not rule out contact bounce, but the issue described by the data is not due to contact bounce alone.

This answer proposes a solution for contact bounce. It also offers some thoughts on potential causes of the problem described by the data. However, an answer to that problem from the information provided is not possible.

A Hardware Solution for Debouncing

I'll first point out that there are several hardware-based solutions for switch bounce. In the interests of brevity I will propose only one solution. It should work in most cases, but the reader should know there are alternatives. There are some excellent technical articles available online that discuss and describe switch bounce and alternative solutions in rich detail. Two of those are: 1. Max Maxfield's 9-part saga on debouncing, which includes both hardware and software solutions and 2. Jack Ganssle's 2-part article, which also offers a hardware & software solutions.

The following circuit will debounce both low-to-high and high-to-low transitions. Fundamentally it's a simple low-pass filter, and since it depends on the R-C time constant, the length of the debouncing period is easily changed with a resistor or capacitor value.

schematic

simulate this circuit – Schematic created using CircuitLab

The function of this debounce circuit is straightforward:

  1. When SW1 is OPEN, current flows through R1 and R2 charging C1. After some time, the voltage across C1 will be (nearly) equal to Vcc.

  2. When SW1 is CLOSED, C1 will discharge, and current will flow through R2 to GROUND. After some time, the voltage across C1 will be zero/ground potential.

  3. The time required in both cases is determined by the product of R and C. This is the R-C time constant (aka τ); τ = R x C

    • when SW1 is OPEN, τ = (R1 + R2) x C1
    • when SW1 is CLOSED, τ = R2 x C1
  4. If it is important/desirable that the time constant be approximately the same for OPEN and CLOSED switch SW1, the diode D1 can accomplish that by making the voltage across R2 equal to the forward-biased drop across D1 (0.7V for a p-n junction diode, 0.2V for a Schottky diode).

  5. Debouncers similar to this one may benefit from some hysteresis on the output. RPi's GPIO pins are said to include a Schmitt Trigger, and that may be sufficient. If more hysteresis is needed (perhaps due to a noisy input), a comparator can provide adjustable hysteresis levels.

An Example:

Assume that contact bounce persists for 10 msec after switch closure. According to Jack Ganssle's data, this would be a long bounce interval. Therefore, we want our low-pass filter to remove/attenuate everything shorter than 10 msec.

Referring to the graph below, if we set our R-C time constant to 25 msec, we should knock out all the transitions that occur during the first 10 msec with some margin for error:

τ = 25 x 10-3 = R2 x C1 ;

setting C1 = 1.0uF (an easy-to-find value) determines R2:

R2 = τ / 10-6 = 25K𝛀 ; we'll round up to the standard value 27K𝛀

R-C time constant

Even Longer "Bounce" Intervals?

Again, the OP's question may be misleading in the sense that the events his data describes are not contact bounce at all; e.g. "falling leaves and squirrels". They are most certainly problematic, but can't be considered as contact bounce. A low-pass filter may be of some use, but from the data presented, an impractically large R-C time constant may be required. The connection between the doorbell/push-button switch and the light beam sensor is unclear in the question as currently stated, but one thing does seem clear: the data supplied by the OP is NOT contact bounce. The OP may have two issues; the answer here will solve only the contact bounce problem. It is also possible that the equipment is defective, or the wiring is picking up noise. There are ways to deal with this, but a better description of the system will be needed.

"Plug & Play" Hardware Solutions:

If you've got a larger budget for parts, you can buy an integrated circuit designed specifically to debounce switches:

Software Switch Debouncing Redux:

For those that are committed to resolving contact bounce in software, here's a link to Hackaday's compendium of software-based solutions: "Debounce Code – One Post To Rule Them All". Yes... rather dated, but so is the debounce problem.

2

I have written several GPIO Python modules.

They each implement a "proper" debounce.

pigpio set_glitch_filter

lgpio gpio_set_debounce_micros

rgpio gpio_set_debounce_micros

6
  • I've never used this feature, but I seem to recall reading that there was a Schmitt Trigger option for GPIO inputs... is my recollection accurate?
    – Seamus
    Nov 13, 2020 at 16:34
  • @seamus There is, and it is meant to be enabled by default. I have never known it make any practical difference for the stuff I do. I think it operates in the nanosecond region. raspberrypi.org/documentation/hardware/raspberrypi/gpio/…
    – joan
    Nov 13, 2020 at 16:46
  • Thank you @joan. It looks like set_glitch_filter is a hardware feature that can "filter pulses up to 25ns in width" - too short for me. As far as gpio_set_debounce_micros, I couldn't tell from the code what that is doing. Is that configuring another feature of the MCU, or is it implemented in software? If software, does it use interrupts or a busy loop? Nov 17, 2020 at 23:57
  • @Metamorphic They are all software functions. They each implement a "proper" debounce. A level is only reported if it has been stable for a debounce time. I put "proper" as some people use a different definition of debounce. I agree with your definition. The code looks messy because of all the edge conditions and I have tried to make it time efficient because it is called so often. The glitch filter code uses DMA time-stamped data (bypassing the Linux kernel). The other code uses the new kernel /dev/gpiochip time-stamped data.
    – joan
    Nov 18, 2020 at 9:00
  • Thanks again @joan. I asked my question about interrupts because I wanted to know if your code would be efficient and consume zero CPU in the absence of a changing signal. Since you mentioned timestamps, it looks like this must be the case. I didn't find the code to be messy, rather I simply couldn't locate the place where the filtering is done, due to the indirection created by the client-server model and RPC. It looks like I can play with a tool called gpiomon to see these timestamps. Nov 19, 2020 at 10:17
2

Your Question is misleading.
Debounce settings work for their intended purpose (short term contact bounce), but this is not really your problem.

Incidentally if you wanted to test bounce you couldn't have found a better "switch" to generate it - proper switches are designed to minimise bounce, although relay contacts are notorious for bounce.

What you seem to want is prevent repeated activations. Human generated interactions take several hundred mS. For a doorbell you are unlikely to want activations more frequently than 5 seconds - I would expect any person to wait 30 seconds for an answer.

The normal solution to this is to record activation time, and subsequent activation within a designated period is ignored. You will find hundreds of examples.

Using internal pullups is OK for some purposes, but if you are going to connect long wires (aka antennas) you need to implement some form of interference suppression. The simplest is to use low impedance circuitry i.e. a low value pullup ~470Ω.

Incidentally using a Pi4 for a doorbell seems overkill and a doorbell that needs ~1A to run seems impracticable.

0

Here is a script I call waitswitch, which runs the command specified in its argument every time a mechanical switch is pressed. It is based on the example from RasPi.TV. Ideally it should be rewritten so that the pin number to listen on is specified via a command-line option, as well as the "on" value and the pull-up direction.

There are also some race-conditions, for example where GPIO.input is called. These calls are used so that we don't end up waiting for a rising edge after it has already risen. However, it is possible for the signal to change in between the call to GPIO.input and GPIO.wait_for_edge. Looking at the library code for GPIO.wait_for_edge, it appears that the interface provided by the kernel in /sys/ would allow that function to be written in a more useful way, which would eliminate these race conditions. What the function appears to be doing (source/event_gpio.c:508) is polling the file descriptor for /sys/class/gpio/gpio21/value; currently, it ignores the first read, which the comment says is reading the "current state". What we would like is a similar function to GPIO.wait_for_edge(), maybe called GPIO.wait_for_value(). Rather than ignoring the first read, the new function would use that read to check if the input is already at the desired value. If it is, then edge detection is skipped.

That being said, this is as good as I could do with just the existing (broken) GPIO interface. It uses wait_for_edge to detect a falling edge, and then it uses wait_for_edge once more to detect a rising edge. The second call is given a 300 ms timeout; that's my "bounce time" parameter. If the signal falls and takes more than this amount of time to rise again, then we consider the switch has been pressed.

#!/usr/bin/env python3
# https://raspi.tv/2013/how-to-use-interrupts-with-python-on-the-raspberry-pi-and-rpi-gpio

import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)

import subprocess

import sys, time
import atexit

from functools import partial
warn = partial(print, file=sys.stderr)

args=sys.argv
args.pop(0)
warn("args=",args)
if len(args) == 0:
    warn("Please specify a command")
    sys.exit(1)

# note that RPi.GPIO blocks SIGINT, which is annoying. this function
# is only called during normal exit, if you have to kill this process
# with SIGTERM then it doesn't get run
def cleanup():
    warn("Cleaning up")
    GPIO.cleanup()           # clean up GPIO on normal exit
atexit.register(cleanup);

pin=21
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_UP)

if 0:
    # original version
    while 1:
        res = GPIO.wait_for_edge(pin, GPIO.FALLING, bouncetime=300)
        warn("Got falling edge, waiting for rise (res=%s,time=%.2f)"%(str(res),time.monotonic()))
        subprocess.call(args)
else:
    # superior version, eliminate most bounces
    # after falling edge is detected, wait for rising edge with a timeout

    # use 'changed' to prevent calling command in loop; without it
    # we have infinite timeouts on GPIO.RISING
    changed=0

    while 1:
        if changed and not GPIO.input(pin):
            warn("Strange: already down again (time=%.2f)"%(time.monotonic()));
        else:
            res = GPIO.wait_for_edge(pin, GPIO.FALLING)
            warn("Got falling edge, waiting for rise (res=%s,time=%.2f)"%(str(res),time.monotonic()))
        timeout=300
        changed=1
        if GPIO.input(pin):
            warn("Strange: falling but now up (time=%.2f)"%(time.monotonic()));
        else:
            res = GPIO.wait_for_edge(pin, GPIO.RISING, timeout=timeout)
            if res == None: # timeout occurred
                warn("Timeout occurred, running command (timeout=%d, time=%.2f)"%(timeout, time.monotonic()));
                subprocess.call(args)
                changed=0
            else:
                warn("Rose too quickly, ignoring (time=%.2f)"%(time.monotonic()));

Here is an example output, with normal opening and closing of the switch. It shows that the second GPIO.input conditional is often activated ("Strange: ..."):

$ ./waitswitch echo hi
args= ['echo', 'hi']
Got falling edge, waiting for rise (res=21,time=9367360.11)
Timeout occurred, running command (timeout=300, time=9367360.41)
hi
Got falling edge, waiting for rise (res=21,time=9367362.88)
Strange: falling but now up (time=9367362.88)
Got falling edge, waiting for rise (res=21,time=9367362.88)
Strange: falling but now up (time=9367362.88)
Got falling edge, waiting for rise (res=21,time=9367364.96)
Timeout occurred, running command (timeout=300, time=9367365.27)
hi
Got falling edge, waiting for rise (res=21,time=9367367.83)
Strange: falling but now up (time=9367367.83)
Got falling edge, waiting for rise (res=21,time=9367370.85)
Timeout occurred, running command (timeout=300, time=9367371.15)
hi
Got falling edge, waiting for rise (res=21,time=9367374.59)
Strange: falling but now up (time=9367374.59)

The first such conditional is more commonly seen when opening and closing the switch rapidly, for example:

Got falling edge, waiting for rise (res=21,time=9367471.52)
Rose too quickly, ignoring (time=9367471.70)
Got falling edge, waiting for rise (res=21,time=9367471.82)
Strange: falling but now up (time=9367471.82)
Strange: already down again (time=9367471.82)
Rose too quickly, ignoring (time=9367471.83)
Strange: already down again (time=9367471.83)

Because of the race conditions, it is still possible for the command argument to be run even when the state change is shorter than 300 ms. This would happen if the value falls and stays low through the second GPIO.input(pin) call, but rises before GPIO.wait_for_edge(pin, GPIO.RISING, timeout=timeout) and then stays constant. The chance for this to happen might be reduced by inserting a sleep statement before the second call to GPIO.input(pin); this would allow some time for the signal to "settle" before reading its value again after edge detection. It could also be fixed permanently by extending the GPIO library as described above.

0

Older pos, but still relevant. Here is my solution with the LGPIO lib. Where a normal debounce would accept an input after it is active for x milliseconds, this triggers from the first moment and ignores input change for x miliseconds. Works well and fast, disadvantage is that if the input is faster then the set times it ignores the change. So make the on/offDebounceMilis value small, it only needs the ignore the initial button bounce.

import lgpio

handle = lgpio.gpiochip_open(0)
pinnr  = 23
onDebounceMilis = 60 
offDebounceMilis = 100

value = False
old_onTimestamp, old_offTimestamp, testCounter = 0

# Debounce, value is the output  
def my_callback(chip, gpio, level, timestamp):
    global old_onTimestamp
    global old_offTimestamp
    global value
    global testCounter    
    
    if level != value: # not required but prevents unneeded calculations
        if level == True and not value:    
            if timestamp - old_offTimestamp > offDebounceMilis * 1000000:
                print(f"{testCounter} \t on: \t {timestamp - old_offTimestamp / 1000000}")
                old_onTimestamp = timestamp      
                value = True
                testCounter +=1       
                        
        elif value:
            if timestamp - old_onTimestamp > onDebounceMilis * 1000000 :    
                print(f"{testCounter} \t off: \t {timestamp - old_onTimestamp / 1000000}")
                old_offTimestamp = timestamp   
                value = False
                testCounter +=1
                
lgpio.gpio_claim_alert(handle, pinnr, lgpio.BOTH_EDGES)        
lgpio.callback(handle, pinnr, lgpio.BOTH_EDGES, my_callback)            

while True: 
    pass

I chose this approach as it can debounce even with an unpredictable amount of calls/bounces. Another tested solution is to use 2 on/off timers that reset after a bounce and get only active after x time. The end result is a real debounce. Disadvantage is that the normal Python timer can only be reset by creating a new instance, causing new threads after every push :) Works, but not very subtle.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.