Close

Tweaking a protothreads button handler

ken-yapKen Yap wrote 02/11/2025 at 05:49 • 6 min read • Like

I have written on protothreads for switch handling in the past, as a log in a 8051 project of mine, but a recent modification that I wanted to do put protothreads in my mind again so I decided to rewrite it as a page.

Interface handling usually involves keeping track of the state. The changes in state are at human pace so waiting is not feasible as the MCU has other tasks to do. An example is switch debouncing and autorepeat.

You could do it with a full fledged thread facility. Or the programming language may offer coroutines, like Lua. In other languages usually the programmer resorts to writing a state machine, where variables remember where the subtask is up to. Sometimes the state machine uses an explicit state variable, sometimes the state is encoded in the values of variables.

But state machine code can be hard to comprehend. So I thought there should be a solution with very lightweight threads. A search found Protothreads by Adam Dunkels which has been available since 2005. This is designed for low resource MCUs, only requiring one integer location to store the thread state. It is implemented in standard C, not requiring any assembler assist, so is portable to any MCU with a suitable C compiler. In fact the C code consists of preprocessor macros.

Here's an example of the code I reuse unchanged in many of my projects. The requirements are:

  1. When a button is pressed the thread waits until the debounce period has passed. If the button is released before this, i.e. it was just a temporary spike, the thread restarts.
  2. On expiry of the debounce period, the action is taken. The thread then waits until the autorepeat threshold is reached. If the button is released before this, the thread restarts.
  3. On expiry of the autorepeat threshold, the action is taken. The thread then repeatedly waits for the repeat period to elapse, taking the action every time this happens. If the button is released at any time, the thread restarts.

Note that swstate and swtent (tentative state) are actually bit vectors representing an array of switches, so this code handles more than one switch. The code that corresponds to each switch is called from switchaction(). To make things concrete, this design uses a debounce period of 100 ms, an autorepeat threshold of 400 ms more, and a repeat period of 250 ms (4 times a second).

static inline void reinitstate()
{
        swtent = swstate;
        swmin = DEPMIN;
        swrepeat = RPTTHRESH;
}

static
PT_THREAD(switchhandler(struct pt *pt, uchar oneshot))
{
        PT_BEGIN(pt);
        PT_WAIT_UNTIL(pt, swstate != swtent);
        swmin = (swstate == SWMASK) ? DEPMIN : RELMIN;
        swtent = swstate;
        PT_WAIT_UNTIL(pt, --swmin <= 0 || swstate != swtent);
        if (swstate != swtent) {                // changed, restart
                reinitstate();
                PT_RESTART(pt);
        }
        switchaction();
        if (oneshot) {
                reinitstate();
                PT_RESTART(pt);
        }
        PT_WAIT_UNTIL(pt, --swrepeat <= 0 || swstate != swtent);
        if (swstate != swtent) {                // changed, restart
                reinitstate();
                PT_RESTART(pt);
        }
        switchaction();
        for (;;) {
                swrepeat = RPTPERIOD;
                PT_WAIT_UNTIL(pt, --swrepeat <= 0 || swstate == SWMASK);
                if (swstate == SWMASK) {        // released, restart
                        reinitstate();
                        PT_RESTART(pt);
                }
                switchaction();
        }
        PT_END(pt);
}

You can see that it is programmed as a sequential routine. The structure of the handler mirrors the specification above. You have to imagine that the thread has a life of its own and waits at the PT_WAIT_UNTIL macro until the condition is satisfied. In reality, the MCU exits the routine and uses a local continuation to know where to restart when the thread is called again. It's really thinly disguised state machine code, but written like sequential code.

Unlike a real thread library, there is no scheduling. You have to arrange to periodically call the thread handler.

Now the modification that I wanted was to have a shorter debounce time for depress than release. The change turned out to be trivial:

diff --git a/clock.h b/clock.h
index 9701dfb..db74d57 100644
--- a/clock.h
+++ b/clock.h
@@ -3,7 +3,8 @@ typedef unsigned int    uint;
 
 #define        TICKSINHALFSEC  (TICKSINSEC/2)
 #define        TICK            4                       // ms, roughly
-#define        DEPMIN          (100 / TICK)            // debounce period
+#define        DEPMIN          (50 / TICK)             // depress debounce period
+#define        RELMIN          (100 / TICK)            // release debounce period
 #define        RPTTHRESH       ((400 / TICK) + 1)      // repeat threshold after debounce
 #define        RPTPERIOD       (250 / TICK)            // repeat period
 #define        BUTTON_TIMEOUT  (64000u / TICK)         // revert to Time mode after 64 seconds
diff --git a/clock.c b/clock.c
index 8043fd8..c8a4569 100644
--- a/clock.c
+++ b/clock.c
@@ -319,6 +319,7 @@ PT_THREAD(switchhandler(struct pt *pt, uchar oneshot))
 {
        PT_BEGIN(pt);
        PT_WAIT_UNTIL(pt, swstate != swtent);
+       swmin = (swstate == SWMASK) ? DEPMIN : RELMIN;
        swtent = swstate;
        PT_WAIT_UNTIL(pt, --swmin <= 0 || swstate != swtent);
        if (swstate != swtent) {                // changed, restart

If the state of the switch is the idle state, then we set the timer appropriately for depress, otherwise release.

Like

Discussions