pin change interrupt + debouncing

All ESP8266 boards running MicroPython.
Official boards are the Adafruit Huzzah and Feather boards.
Target audience: MicroPython users with an ESP8266 board.
grafalex
Posts: 6
Joined: Sun Mar 04, 2018 4:53 pm

pin change interrupt + debouncing

Post by grafalex » Sun Apr 08, 2018 1:31 pm

Can someone help me understanding what is wrong with my pin change interrupt handler?

I have a button, that is connected between GPIO14 (D5 pin on Wemos D1 mini) and ground. I need to count number of button presses.

First I tried to trigger pin change interrupt only on falling edge, but this code often was triggered on button release. With the code below I an handling both rising and falling edge, and applying 50ms filter to debounce button pins.

Code: Select all

import machine
import utime

pin = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)
value = 0
last_value_change_ms = 0

def callback(p):
    global value
    global last_value_change_ms

    pin_state = p.value()
    cur_time = utime.ticks_ms()
    diff = cur_time - last_value_change_ms

    if diff > 500:
        print("")
    
    #print("CurTime={}, LastChange={}, Diff={}, State={}".format(cur_time, last_value_change_ms, diff, p.value()))
    
    if diff > 50:
        if pin_state == 0:
            value += 1
            print('pin change', value)
        else:
            print("Button up")        
    else:  
        print("Debouncing...", diff)
 
    last_value_change_ms = cur_time
    

pin.irq(trigger=machine.Pin.IRQ_FALLING | machine.Pin.IRQ_RISING, handler=callback)

prev_value = value
while True:
    new_value = value
    if new_value != prev_value:
        print("Value Changed:", value)
        prev_value = value
Unfortunately this did not help. On button release I hit the callback, but the value I read from the pin is 0, instead of 1. Here is the output:

Code: Select all

pin change 8
Value Changed: 8
pin change 9
Debouncing... 1
Debouncing... 0
Debouncing... 1
Debouncing... 0
Value Changed: 9
The code (sometimes) does not hit 'button up' condition. Instead I am hitting another button press.

Does micropython firmware provide an information about why pin change interrupt has been triggered? How can I make it more reliable?

kevinkk525
Posts: 180
Joined: Sat Feb 03, 2018 7:02 pm

Re: pin change interrupt + debouncing

Post by kevinkk525 » Sun Apr 08, 2018 1:58 pm

I can tell you why:

You have a callback() that is only called by an interrupt. In your callback you run this:

Code: Select all

if diff > 50:
        if pin_state == 0:
            value += 1
            print('pin change', value)
        else:
            print("Button up")        
    else:  
        print("Debouncing...", diff)
Now what happens if the button gets into a stable state within 30ms? The callback is called by an interrupt after 30ms. At that time your code would just print("Debouncing...") as diff is <50ms. But as the state is stable now, your callback won't be called anymore (as there is no interrupt) and therefore "Button up" is not recognized.

What you want to do is use a lock and a timer, that checks the pin_state after the debounce time if it has the same state as at the interrupt. The lock is to prevent from creating a timer on every interrupt.
I use it that way (but with asyncio) in my application for publishing the bell to mqtt.

grafalex
Posts: 6
Joined: Sun Mar 04, 2018 4:53 pm

Re: pin change interrupt + debouncing

Post by grafalex » Sun Apr 08, 2018 2:23 pm

Now what happens if the button gets into a stable state within 30ms?
I am capturing value of the first trigger, not the last.
I use it that way (but with asyncio) in my application for publishing the bell to mqtt.
Could you please show me the code so that I am not reinventing the wheel? (I use uasyncio as well)

kevinkk525
Posts: 180
Joined: Sat Feb 03, 2018 7:02 pm

Re: pin change interrupt + debouncing

Post by kevinkk525 » Sun Apr 08, 2018 3:26 pm

Yes you capture the value of the first trigger but you won't get past 50ms if the pin state is already stable at 30ms as there won't be an interrupt after that.

As you are using uasyncio anyways, you could either try the aswitch library of Peter Hinch: https://github.com/peterhinch/micropyth ... aswitch.py

Or base it on my code:

Code: Select all

import gc
from pysmartnode import config
from pysmartnode import logging
log = logging.getLogger("bell")
mqtt = config.getMQTT()
from pysmartnode.utils.Event import Event
import machine
import uasyncio as asyncio
gc.collect()


class Bell:
    def __init__(self, pin, debounce_time, on_time=None, irq_direction=None, mqtt_topic=None):
        self.mqtt_topic = mqtt_topic or "home/bell"
        self.PIN_BELL_IRQ_DIRECTION = irq_direction or machine.Pin.IRQ_FALLING
        self.debounce_time = debounce_time
        self.on_time = on_time or 500
        self.pin_bell = pin if type(pin) != str else config.pins[pin]
        self.loop = asyncio.get_event_loop()
        self.loop.create_task(self.__initializeBell())

    async def __initializeBell(self):
        self.pin_bell = machine.Pin(self.pin_bell, machine.Pin.IN, machine.Pin.PULL_UP)
        self.eventBell = Event()
        self.timerLock = Event(onTrue=False) #this is actually just an asyncio Lock()
        irq = self.pin_bell.irq(trigger=self.PIN_BELL_IRQ_DIRECTION, handler=self.__irqBell)
        self.eventBell.clear()
        self.loop.create_task(self.__bell())
        self.timer_bell = machine.Timer(1)
        log.info("Bell initialized")

    async def __bell(self):
        while True:
            await self.eventBell
            await mqtt.publish("home/bell", "ON", qos=1)
            await asyncio.sleep_ms(self.on_time)
            await mqtt.publish("home/bell", "OFF", True, 1)
            self.eventBell.clear()

    def __irqBell(self, p):
        # print("BELL",p)
        # print("BELL",time.ticks_ms())
        if self.timerLock.is_set() == True or self.eventBell.is_set() == True:
            return
        else:
            self.timerLock.set()
            self.timer_bell.init(period=self.debounce_time,
                                 mode=machine.Timer.ONE_SHOT, callback=self.__irqTime)

    def __irqTime(self, t):
        # print("timer",time.ticks_ms())
        if self.PIN_BELL_IRQ_DIRECTION == machine.Pin.IRQ_FALLING and self.pin_bell.value() == 0:
            self.eventBell.set()
        elif self.PIN_BELL_IRQ_DIRECTION == machine.Pin.IRQ_RISING and self.pin_bell.value() == 1:
            self.eventBell.set()
        self.timer_bell.deinit()
        self.timerLock.clear()
Note: pysmartnode is my project, so you won't find these files (yet) but you actually only need uasyncio, uasyncio Lock & Event (but can be implemented differently)

This code only supports one irq direction as it does not save the initial state as it is obvious that a falling irq always has 0 as it initial state.

grafalex
Posts: 6
Joined: Sun Mar 04, 2018 4:53 pm

Re: pin change interrupt + debouncing

Post by grafalex » Sun Apr 08, 2018 7:28 pm

Thank you very much for your code.

I am basically using a polling approach, that was inspired by Peter's Switch class. But I got number of comments from the community that polling is not the best way, so now I am investigating pin change interrupt way.

OutoftheBOTS_
Posts: 416
Joined: Mon Nov 20, 2017 10:18 am

Re: pin change interrupt + debouncing

Post by OutoftheBOTS_ » Sun Apr 08, 2018 9:41 pm

Not sure if somehting like this will do the job.

Code: Select all

import machine
import time

pin = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)
value = 0
last_value_change_ms = 0

def callback(p):
    
	#not sure if there is a way to pause the call back so it doen't call multi events
	
	global value
    global last_value_change_ms
	
	time.sleep_ms(50) #debounce time
	value = pin.value()
	
	#if call back was paused then unpause now :)

    

pin.irq(trigger=machine.Pin.IRQ_FALLING | machine.Pin.IRQ_RISING, handler=callback)

prev_value = value
while True:
    new_value = value
    if new_value != prev_value:
        print("Value Changed:", value)
        prev_value = value

		
		

User avatar
pythoncoder
Posts: 3053
Joined: Fri Jul 18, 2014 8:01 am
Location: UK
Contact:

Re: pin change interrupt + debouncing

Post by pythoncoder » Mon Apr 09, 2018 6:56 am

grafalex wrote:
Sun Apr 08, 2018 7:28 pm
...I got number of comments from the community that polling is not the best way, so now I am investigating pin change interrupt way.
It depends what the purpose of the switch actually is, and what latency is acceptable. In most cases switches are used for user interfaces where a lag of (say) 50ms will not be noticeable. There are cases where millisecond response is required such as limit switches in NC machines; in such cases an interrupt is justified.

In user interface applications running uasyncio polling works fine. For example, my GUI code for the official LCD160CR display uses uasyncio polling to check for touch events. Visually the response to touches appears instantaneous. The wetware inside our cranium is dog-slow...

There are significant benefits to polling. It is easy to extend to large numbers of switches. Detecting events like double clicks is easy. And there are no nasty edge cases to worry about, such as bounces which are so fast that an edge occurs before the ISR has finished execution.

Are you experiencing actual problems with my switch class? If you are experiencing perceptible latency it may be in the design of your coroutines.

If "the community" has given reasons for disliking polling I'd be interested to hear (and engage with) them.
Peter Hinch

OutoftheBOTS_
Posts: 416
Joined: Mon Nov 20, 2017 10:18 am

Re: pin change interrupt + debouncing

Post by OutoftheBOTS_ » Mon Apr 09, 2018 10:07 am

@pythoncoder I just looked at your LCD160CR llibiary and this demo of it https://www.youtube.com/watch?v=OOz9U_YdstM

I am impressed.

grafalex
Posts: 6
Joined: Sun Mar 04, 2018 4:53 pm

Re: pin change interrupt + debouncing

Post by grafalex » Mon Apr 09, 2018 1:29 pm

Hi All,

Thank you very much for your comments. I ended up with this code. It feels pretty robust

Code: Select all

import machine
import utime

pin = machine.Pin(14, machine.Pin.IN, machine.Pin.PULL_UP)
new_click_detected = False

def callback(p):
    global new_click_detected
    new_click_detected = True
    

#pin.irq(trigger=machine.Pin.IRQ_FALLING | machine.Pin.IRQ_RISING, handler=callback, hard=True)
pin.irq(trigger=machine.Pin.IRQ_FALLING, handler=callback, hard=True)

# The code below will be replaced with uasyncio coro
value = 0
while True:
    while not new_click_detected:
        pass

    utime.sleep_ms(20)  
    if pin.value() == 0:
        value += 1
        print("New value: ", value)

    new_click_detected = False
@pythoncoder, It is hard to say right now whether I really have issues with your Switch class (I actually did a simplified copy due to memory concerns). I just built a water counter that counts impluses every litre passed through the pipe. I think it I have not consumed enough water to state I did not miss any impulses. Since impulses comes quite slow (once in 5 seconds, 1 sec duration) I am pretty sure polling should work fine, latency is not an issue at all.

Here is my code: https://github.com/grafalex82/WaterMeter

I wrote an article for a Russian speaking community about my device, but got number of comments that TRUE microcontroller applications do not use polling. The primary concern is a waste of CPU cycles in a polling loop instead of doing something useful. That is why I started investigating pin change interrupt way.

User avatar
dhylands
Posts: 2880
Joined: Mon Jan 06, 2014 6:08 pm
Location: Peachland, BC, Canada
Contact:

Re: pin change interrupt + debouncing

Post by dhylands » Mon Apr 09, 2018 5:06 pm

If you were to go the interrupt method, then the way I typically do it is that the interrupt handler disables itself and starts a debounce timer. The debounce timer would typically be set for about 10 msec (most switches will be fully debounced in 10 msec, but switches can vary considerably).

When the timer goes off, then you capture the state of the gpio line, and re-enable the interrupt handler. This way you don't have a bunch of spurious interrupts going off during the debounce period.

The above is what I've done for writing linux kernel debouncers where doing any kind of polling is frowned upon. For most of my micro controller work (which is often related to robotics), I typically go with the polling approach. If you were trying to write a super low-power device, then using interrupts will generally be lower power than using polling. For robotics, the amount of power drawn by the motors makes the micro controller use insignificant regardless of the approach.

Post Reply