Code to detect single press, long press and double click of a push switch

mrm

Well-known member
hello

Can anybody point me to some simple code to detect a momentary press, a long press and a double click of a push switch?

Preferably it would not use interrupts so that it is applicable to all pins.

Thank you for your assistance.
 

Buzby

Senior Member
Hi mrm,

This is some code to get you started. It does not use interrupts, and should run on any chip.

The two time values are counts which are updated each time the main loop executes. You will need to select suitable values by trial and error.

The sertxd lines starting with ++ are where you could put calls to subroutines

It runs in the simulator, so I suggest you try it there and get a feel for how it works, and if it does what you need.

Cheers,

Buzby

Rich (BB code):
#picaxe 08M2

symbol sw1pin        = pinC.2       ' Switch input

symbol doubletime    = 10     ' Time value for doubleclick
symbol longtime      = 25     ' Time value for longpress

symbol sw1           = bit8   ' sw1 copy
symbol sw1mem        = bit9   ' sw1 memory
symbol sw1Press1     = bit10  ' Set on first press of doubleclick

symbol sw1timerShort = w3     ' Timer for doubleclick
symbol sw1timerLong  = w4     ' Timer for longpress


' Code starts here
' ----------------

sertxd("-- Start --",cr,lf)

do ' Main loop

      ' Copy pinstate to bit
      sw1 = sw1pin

      ' Detect button press
      if sw1 = 1 and sw1mem = 0 then
            ' Button pressed
            sertxd("++Pressed ",#sw1timerShort,cr,lf)

            if sw1Press1 = 0 then ' This could be first press of doubleclick
                  sw1timerShort = doubletime ' Start doubleclick timer  
                  sw1Press1 = 1              ' Set click memory   
                  sertxd("First press ",#sw1timerShort,cr,lf)

            else ' This is second press of doubleclick

                  sertxd("Second press ",#sw1timerShort,cr,lf)

                  sw1Press1 = 0     
                  if sw1timerShort > 0 then ' If timeout not expired
                        ' Switch doubleclicked
                        sertxd("++Doubleclicked ",#sw1timerShort,cr,lf) 
                        sw1timerShort = 0 ' Clear doubleclick timer
                  endif
            endif
            
      endif

      ' Detect button release
      if sw1 = 0 and sw1mem = 1 then
            ' Switch released
            sertxd("++Released ",#sw1timerShort,cr,lf)
            sw1timerLong = 0 ' Reset longtimer
      endif

      sw1mem = sw1

      ' Doubleclick timer
      if sw1timerShort > 0 then 
            dec sw1timerShort 
            if sw1timerShort = 0 then
                  sw1Press1 = 0
            endif 
      endif

      ' Longpress detect
      if sw1 = 1 then
            inc sw1timerLong
            if sw1timerLong > longtime then
                  sw1timerLong = 0 ' Reset longtimer
                  sw1Press1 = 0    ' Reset doubleclick flag
                  sertxd("++LongPressed ",#sw1timerShort,cr,lf)
            endif
      endif

'
'
'  
'
'

loop
 
  • Like
Reactions: mrm

AllyCat

Senior Member
Hi,

It depends when you need to (or can) receive a "notification" of the button state(s). For example the leading edge cannot tell you anything about the type of press, but you might want to be notified to sound a tone/click or light a LED, etc.. Similarly, the first (short) release cannot tell you if there is a double-click to come.

Which got me thinking if a single timing period is all that might be needed. So here's my "minimalist" version (but maybe hippy will do better). ;)
Code:
#picaxe 08M2
#no_data
symbol key = pinc.2
symbol timeout = 10                        ; For simulator, longer for real time
symbol elapsed = b1
symbol pressed = 1
symbol released = 0

main:
elapsed = 0
do : loop until key = released        ; Ensure we start with button released    
do : loop until key = pressed            ; Wait for button to be pressed
do
    inc elapsed
    if elapsed > timeout then long
loop until key = released                ; Fall through if button released (short press)
do
    inc elapsed
    if elapsed > timeout then short    ; Not pressed again before the timeout
loop until key = pressed                ; Fall though if double
double:    
    sertxd("Double Click",cr,lf)
    goto main
long:
    sertxd("Long Press",cr,lf)
    goto main
short:
    sertxd("Single Press",cr,lf)
    goto main
Could have written it as a subroutine to get rid of those pesky GOTOs, but you might want to do more than just send a SERTXD. ;)

Cheers, Alan.
 
  • Like
Reactions: mrm

inglewoodpete

Senior Member
A few years ago I wrote a program to handle a range of press lengths of a couple of switches using a 100mS Timer interrupt (28X2). As you can imagine, the code was considerably more complicated than the solutions already offered!

I used interrupts to handle the switch timing because my foreground task/s were quite time sensitive and I didn't want the foreground tasks to be slowed significantly when a key was pressed.
 
  • Like
Reactions: mrm

mrm

Well-known member
A few years ago I wrote a program to handle a range of press lengths of a couple of switches using a 100mS Timer interrupt (28X2). As you can imagine, the code was considerably more complicated than the solutions already offered!

I used interrupts to handle the switch timing because my foreground task/s were quite time sensitive and I didn't want the foreground tasks to be slowed significantly when a key was pressed.
Hi,

It depends when you need to (or can) receive a "notification" of the button state(s). For example the leading edge cannot tell you anything about the type of press, but you might want to be notified to sound a tone/click or light a LED, etc.. Similarly, the first (short) release cannot tell you if there is a double-click to come.

Which got me thinking if a single timing period is all that might be needed. So here's my "minimalist" version (but maybe hippy will do better). ;)
Code:
#picaxe 08M2
#no_data
symbol key = pinc.2
symbol timeout = 10                        ; For simulator, longer for real time
symbol elapsed = b1
symbol pressed = 1
symbol released = 0

main:
elapsed = 0
do : loop until key = released        ; Ensure we start with button released   
do : loop until key = pressed            ; Wait for button to be pressed
do
    inc elapsed
    if elapsed > timeout then long
loop until key = released                ; Fall through if button released (short press)
do
    inc elapsed
    if elapsed > timeout then short    ; Not pressed again before the timeout
loop until key = pressed                ; Fall though if double
double:   
    sertxd("Double Click",cr,lf)
    goto main
long:
    sertxd("Long Press",cr,lf)
    goto main
short:
    sertxd("Single Press",cr,lf)
    goto main
Could have written it as a subroutine to get rid of those pesky GOTOs, but you might want to do more than just send a SERTXD. ;)

Cheers, Alan.
Thank you for this, I will try it out and report back in due course.
 

mrm

Well-known member
Hi mrm,

This is some code to get you started. It does not use interrupts, and should run on any chip.

The two time values are counts which are updated each time the main loop executes. You will need to select suitable values by trial and error.

The sertxd lines starting with ++ are where you could put calls to subroutines

It runs in the simulator, so I suggest you try it there and get a feel for how it works, and if it does what you need.

Cheers,

Buzby

Rich (BB code):
#picaxe 08M2

symbol sw1pin        = pinC.2       ' Switch input

symbol doubletime    = 10     ' Time value for doubleclick
symbol longtime      = 25     ' Time value for longpress

symbol sw1           = bit8   ' sw1 copy
symbol sw1mem        = bit9   ' sw1 memory
symbol sw1Press1     = bit10  ' Set on first press of doubleclick

symbol sw1timerShort = w3     ' Timer for doubleclick
symbol sw1timerLong  = w4     ' Timer for longpress


' Code starts here
' ----------------

sertxd("-- Start --",cr,lf)

do ' Main loop

      ' Copy pinstate to bit
      sw1 = sw1pin

      ' Detect button press
      if sw1 = 1 and sw1mem = 0 then
            ' Button pressed
            sertxd("++Pressed ",#sw1timerShort,cr,lf)

            if sw1Press1 = 0 then ' This could be first press of doubleclick
                  sw1timerShort = doubletime ' Start doubleclick timer  
                  sw1Press1 = 1              ' Set click memory   
                  sertxd("First press ",#sw1timerShort,cr,lf)

            else ' This is second press of doubleclick

                  sertxd("Second press ",#sw1timerShort,cr,lf)

                  sw1Press1 = 0     
                  if sw1timerShort > 0 then ' If timeout not expired
                        ' Switch doubleclicked
                        sertxd("++Doubleclicked ",#sw1timerShort,cr,lf) 
                        sw1timerShort = 0 ' Clear doubleclick timer
                  endif
            endif
            
      endif

      ' Detect button release
      if sw1 = 0 and sw1mem = 1 then
            ' Switch released
            sertxd("++Released ",#sw1timerShort,cr,lf)
            sw1timerLong = 0 ' Reset longtimer
      endif

      sw1mem = sw1

      ' Doubleclick timer
      if sw1timerShort > 0 then 
            dec sw1timerShort 
            if sw1timerShort = 0 then
                  sw1Press1 = 0
            endif 
      endif

      ' Longpress detect
      if sw1 = 1 then
            inc sw1timerLong
            if sw1timerLong > longtime then
                  sw1timerLong = 0 ' Reset longtimer
                  sw1Press1 = 0    ' Reset doubleclick flag
                  sertxd("++LongPressed ",#sw1timerShort,cr,lf)
            endif
      endif

'
'
'  
'
'

loop
Thank you for this code. I will try it out and report back to the forum thread in case others have the same problem.
 

Buzby

Senior Member
... the first (short) release cannot tell you if there is a double-click to come. ...
Hi Alan,

This is the tricky part.

I'm thinking there maybe a way to separate single and double clicks by delaying the result until we are sure what it is.

Code:
                     _______                                                 _______            _______
Switch : ___________|       |_______________________________________________|       |__________|       | _____________________________

                            ^----------------------^                                 ^----------------------^
                            |                      |                                 |                      |

                     Start Dclick timer          Timer done                   Start Dclick timer          Timer done
                                              ( Single click )                                          ( Double click )

I've got a wall to paint, but I'll get on this later.


Cheers,


Buzby
 
  • Like
Reactions: mrm

hippy

Senior Member
I'm thinking there maybe a way to separate single and double clicks by delaying the result until we are sure what it is
Yes; that's the only way to do it.
Code:
#Picaxe 08M2
#Terminal 4800
#No_Data

Symbol BTN          = pinC.3
Symbol PUSHED       = 1

Symbol LONG_TIMEOUT = 500
Symbol GAP_TIMEOUT  = 500

Symbol NO_PUSH      = 0
SYmbol SHORT_PUSH   = 1
Symbol LONG_PUSH    = 2
Symbol DOUBLE_PUSH  = 3

Symbol timeout      = w1 ; b3:b2
Symbol action       = b4

MainLoop:
  Do
    Gosub CheckButtonPush
    Select case action
      Case SHORT_PUSH  : SerTxd( "Short Push",  CR, LF )
      Case LONG_PUSH   : SerTxd( "Long Push",   CR, LF )
      Case DOUBLE_PUSH : SertXd( "Double Push", CR, LF )
    End Select
  Loop

CheckButtonPush:
  If action = DOUBLE_PUSH Then
    Do : Loop Until BTN <> PUSHED
  End If
  action = NO_PUSH
  If BTN = PUSHED Then
    timeout = LONG_TIMEOUT
    Do 
      timeout = timeout Min 10 - 10
      Pause 10
    Loop Until BTN <> PUSHED
    If timeout = 0 Then
      action = LONG_PUSH
    Else
      action = SHORT_PUSH
    End If
    timeout = GAP_TIMEOUT
    Do 
      timeout = timeout - 10
      Pause 10
    Loop Until timeout = 0 Or BTN = PUSHED
    If timeout <> 0 Then
      action = DOUBLE_PUSH
    End If
  End If
  Return
 

mrm

Well-known member
Yes; that's the only way to do it.
Code:
#Picaxe 08M2
#Terminal 4800
#No_Data

Symbol BTN          = pinC.3
Symbol PUSHED       = 1

Symbol LONG_TIMEOUT = 500
Symbol GAP_TIMEOUT  = 500

Symbol NO_PUSH      = 0
SYmbol SHORT_PUSH   = 1
Symbol LONG_PUSH    = 2
Symbol DOUBLE_PUSH  = 3

Symbol timeout      = w1 ; b3:b2
Symbol action       = b4

MainLoop:
  Do
    Gosub CheckButtonPush
    Select case action
      Case SHORT_PUSH  : SerTxd( "Short Push",  CR, LF )
      Case LONG_PUSH   : SerTxd( "Long Push",   CR, LF )
      Case DOUBLE_PUSH : SertXd( "Double Push", CR, LF )
    End Select
  Loop

CheckButtonPush:
  If action = DOUBLE_PUSH Then
    Do : Loop Until BTN <> PUSHED
  End If
  action = NO_PUSH
  If BTN = PUSHED Then
    timeout = LONG_TIMEOUT
    Do
      timeout = timeout Min 10 - 10
      Pause 10
    Loop Until BTN <> PUSHED
    If timeout = 0 Then
      action = LONG_PUSH
    Else
      action = SHORT_PUSH
    End If
    timeout = GAP_TIMEOUT
    Do
      timeout = timeout - 10
      Pause 10
    Loop Until timeout = 0 Or BTN = PUSHED
    If timeout <> 0 Then
      action = DOUBLE_PUSH
    End If
  End If
  Return
Thank you for this. Will try it out and report back for the record. Thank you also to Allycat, inglewoodpete and Buzby for the helpful input.
The picaxe forum really is an excellent community.
 

mrm

Well-known member
Hi,

It depends when you need to (or can) receive a "notification" of the button state(s). For example the leading edge cannot tell you anything about the type of press, but you might want to be notified to sound a tone/click or light a LED, etc.. Similarly, the first (short) release cannot tell you if there is a double-click to come.

Which got me thinking if a single timing period is all that might be needed. So here's my "minimalist" version (but maybe hippy will do better). ;)
Code:
#picaxe 08M2
#no_data
symbol key = pinc.2
symbol timeout = 10                        ; For simulator, longer for real time
symbol elapsed = b1
symbol pressed = 1
symbol released = 0

main:
elapsed = 0
do : loop until key = released        ; Ensure we start with button released   
do : loop until key = pressed            ; Wait for button to be pressed
do
    inc elapsed
    if elapsed > timeout then long
loop until key = released                ; Fall through if button released (short press)
do
    inc elapsed
    if elapsed > timeout then short    ; Not pressed again before the timeout
loop until key = pressed                ; Fall though if double
double:   
    sertxd("Double Click",cr,lf)
    goto main
long:
    sertxd("Long Press",cr,lf)
    goto main
short:
    sertxd("Single Press",cr,lf)
    goto main
Could have written it as a subroutine to get rid of those pesky GOTOs, but you might want to do more than just send a SERTXD. ;)

Cheers, Alan.
GOTOs are generally regarded as an arch crime against logic these days but sometimes they are the simplest way particularly when the program is only 20 lines. The main complaint I have with picaxe basic is that you cannot pass arguments to subroutines (unless someone knows better) but controlled use of goto's is fine.
 

hippy

Senior Member
The main complaint I have with picaxe basic is that you cannot pass arguments to subroutines (unless someone knows better)
That can be faked if one is prepared to get creative ...
Code:
#Define PrintSquared(n) DummyRoutine : b1 = n : Gosub Do_PrintSquared

For b0 = 1 To 10
  Gosub PrintSquared(b0)
Next
End

Do_PrintSquared:
  w1 = b1 * b1
  SerTxd( #b1, " squared is ", #w1, CR, LF )
  Return

DummyRoutine:
  Return
The main issue is the PICAXE isn't a 'stack machine', everything is global. Though PUSH/POP and PUSHRAM/POPRAM can be used on the X2's and there are also tricks with @ptr and @bptr to be had.

Local variables can sort of be faked, though not entirely elegantly ...
Code:
Symbol number           = b0
Symbol PrintSquared.arg = b1
Symbol PrintSquared.sqr = w1

For number = 1 To 10
  PrintSquared.arg = number
  Gosub PrintSquared
Next
End

PrintSquared:
  PrintSquared.sqr = PrintSquared.arg * PrintSquared.arg
  SerTxd( #PrintSquared.arg, " squared is ", #PrintSquared.sqr, CR, LF )
  Return
With clever use of #INCLUDE, #DEFINE and #MACRO commands one can hide implementation details away from the main program, have the main program looking like parameter passing is allowed.

In most cases though it's more effort than its worth.
 

mrm

Well-known member
That can be faked if one is prepared to get creative ...
Code:
#Define PrintSquared(n) DummyRoutine : b1 = n : Gosub Do_PrintSquared

For b0 = 1 To 10
  Gosub PrintSquared(b0)
Next
End

Do_PrintSquared:
  w1 = b1 * b1
  SerTxd( #b1, " squared is ", #w1, CR, LF )
  Return

DummyRoutine:
  Return
The main issue is the PICAXE isn't a 'stack machine', everything is global. Though PUSH/POP and PUSHRAM/POPRAM can be used on the X2's and there are also tricks with @ptr and @bptr to be had.

Local variables can sort of be faked, though not entirely elegantly ...
Code:
Symbol number           = b0
Symbol PrintSquared.arg = b1
Symbol PrintSquared.sqr = w1

For number = 1 To 10
  PrintSquared.arg = number
  Gosub PrintSquared
Next
End

PrintSquared:
  PrintSquared.sqr = PrintSquared.arg * PrintSquared.arg
  SerTxd( #PrintSquared.arg, " squared is ", #PrintSquared.sqr, CR, LF )
  Return
With clever use of #INCLUDE, #DEFINE and #MACRO commands one can hide implementation details away from the main program, have the main program looking like parameter passing is allowed.

In most cases though it's more effort than its worth.
Unfortunately #MACRO does not seem to exist on MacAXEpad?
 

mrm

Well-known member
Yes; that's the only way to do it.
Code:
#Picaxe 08M2
#Terminal 4800
#No_Data

Symbol BTN          = pinC.3
Symbol PUSHED       = 1

Symbol LONG_TIMEOUT = 500
Symbol GAP_TIMEOUT  = 500

Symbol NO_PUSH      = 0
SYmbol SHORT_PUSH   = 1
Symbol LONG_PUSH    = 2
Symbol DOUBLE_PUSH  = 3

Symbol timeout      = w1 ; b3:b2
Symbol action       = b4

MainLoop:
  Do
    Gosub CheckButtonPush
    Select case action
      Case SHORT_PUSH  : SerTxd( "Short Push",  CR, LF )
      Case LONG_PUSH   : SerTxd( "Long Push",   CR, LF )
      Case DOUBLE_PUSH : SertXd( "Double Push", CR, LF )
    End Select
  Loop

CheckButtonPush:
  If action = DOUBLE_PUSH Then
    Do : Loop Until BTN <> PUSHED
  End If
  action = NO_PUSH
  If BTN = PUSHED Then
    timeout = LONG_TIMEOUT
    Do
      timeout = timeout Min 10 - 10
      Pause 10
    Loop Until BTN <> PUSHED
    If timeout = 0 Then
      action = LONG_PUSH
    Else
      action = SHORT_PUSH
    End If
    timeout = GAP_TIMEOUT
    Do
      timeout = timeout - 10
      Pause 10
    Loop Until timeout = 0 Or BTN = PUSHED
    If timeout <> 0 Then
      action = DOUBLE_PUSH
    End If
  End If
  Return
This works brilliantly and very simple to implement.
 

AllyCat

Senior Member
Hi,
In most cases though it's more effort than its worth.
Yes, for quite some time now, I've just reserved b1 and w1 as "tempb" and "tempw" to pass parameters (and for any other "local" purposes) which seems much the same in practice. But this also works on (the faster) PE5 and Axepad. :)

I did consider returning a "status" parameter in my code above, but then I probably would have used an ON ... GOTO in the main program, because it can execute around five times faster than the SELECT ... CASE structure, and in about half the number of instruction bytes. ;)

Cheers, Alan.
 
Top