Using protothreads on MCUs

A project log for 8051 tuner

A 3-octave tone generator using an 8051 MCU

Ken YapKen Yap 01/03/2019 at 00:020 Comments

In my previous projects there are places where periods of a fraction of a second are required between actions, for example, debouncing a switch, or waiting for an autorepeat threshold. These periods are much longer than the tick period. The MCU must do work every tick, such as refreshing the display or scanning the switches, so a blocking wait is not acceptable.

In larger systems this is often handled using threads. But threads are relatively heavyweight constructs, requiring stack space and thread switching code. These are hard or impossible to provide on simple MCUs.

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. You can see this in my 8042 switch handling code where counters store the state.

But state machine code is 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.

Let's look at an example. This is a simulated version of the switch handling code in the tuner. The specifications are these:

  1. When a button is pressed the thread waits until the debounce period has passed. If the button is released before this, 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.

To make things concrete, this design uses a debounce period of 100ms, an autorepeat threshold of 400ms more, and a repeat period of 250ms (4 times a second).

Now look at the thread handler and main program:

    33  static
    34  PT_THREAD(switchhandler(struct pt *pt))
    35  {
    36          PT_BEGIN(pt);
    37          PT_WAIT_UNTIL(pt, swstate != swtent);
    38          swtent = swstate;
    39          PT_WAIT_UNTIL(pt, --swmin <= 0 || swstate != swtent);
    40          if (swstate != swtent) {                // changed, restart
    41                  reinitstate();
    42                  PT_RESTART(pt);
    43          }
    44          switchaction("single");
    45          PT_WAIT_UNTIL(pt, --swrepeat <= 0 || swstate != swtent);
    46          if (swstate != swtent) {                // changed, restart
    47                  reinitstate();
    48                  PT_RESTART(pt);
    49          }
    50          switchaction("repeat start");
    51          for (;;) {
    52                  swrepeat = RPTPERIOD;
    53                  PT_WAIT_UNTIL(pt, --swrepeat <= 0 || swstate == SWMASK);
    54                  if (swstate == SWMASK) {        // released, restart
    55                          reinitstate();
    56                          PT_RESTART(pt);
    57                  }
    58                  switchaction("repeat");
    59          }
    60          PT_END(pt);
    61  }
    63  void main()
    64  {
    65          for (int i = 0; i < 64; i++) {
    66                  port = SWMASK;
    67                  if (i == 1)
    68                          port &= ~INCBUTTON;     // transient, should ignore
    69                  else if (10 <= i && i < 12)
    70                          port &= ~INCBUTTON;     // single action
    71                  else if (16 <= i && i < 24)
    72                          port &= ~INCBUTTON;     // single, repeat doesn't kick in
    73                  else if (30 <= i && i < 50)
    74                          port &= ~INCBUTTON;     // single, repeat twice
    75                  if (port != SWMASK)
    76                          printf("%d down\n", i);
    77                  swstate = port;
    78                  PT_SCHEDULE(switchhandler(&pt));
    79          }
    80  }

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. But you are not supposed to know this unless you have read the under the hood page.

You can hear an audio clip of the note increment button working demonstrating single depressions and autorepeat. The decrement button works similarly.

There is no automatic scheduling. You have to arrange to periodically call the thread handler. In the tuner firmware this happens every 4ms.

There are some restrictions. You cannot use a switch statement in the thread handler, as the implementation uses a switch statement. You should avoid local variables as they won't behave like in normal functions across the PT_WAIT_UNTIL calls.

I was fortunate that the states were simple, in case of button release, just go back to waiting. What do you do if you find your program state transitions are complicated? You could reorganise the logic so that it can be handled with conditionals and loops, not unlike structuring a spaghetti go-to program, or you could implement a state machine with one state variable, using the principles underlying Protothreads.

Wrapping your head around Protothreads is not easy, but their use makes the code easier to understand.