-
Tweaking a protothreads button handler
02/11/2025 at 05:49 • 0 commentsI 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:
- 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.
- 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.
- 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 +++...
Read more » -
Stitching boards with paper clips
01/11/2025 at 09:44 • 0 commentsPeople who follow my display board projects know that I usually design them to be cascaded for more digits. They are connected in series using either Dupont pin sockets and pin headers, or just soldered jumpers as in the photo above. Neither is satisfactory as there is still flexure. I have considered using small aluminium straps and screws to hold the boards together stiffly using the corner holes. But I would have to look for aluminium strip of the right width and hacksaw small lengths off.
Today I tried another idea, using reusable paper clips that were designed to hold multiple sheets of a document together. They are attached to a corner of the document stack with a special tool. It's a solution between standard paper clips and fold back paper clips.
It works and I would also need to put some hot glue on the clips to prevent them from sliding off. However in future I will also need to design enough clearance so that the clip doesn't short PCB connections as the one on top in the picture almost does.
-
A buglet initialising the CH552
01/05/2025 at 00:08 • 0 commentsWhen I finally put the two boards described in Flashing the CH552 dev board from the command line to use I noticed that button actions were faster. Counter advance worked twice as fast. The no-activity timeout for exiting set mode was 32 seconds instead of 64. At boot-up the MCU runs at 24 MHz, but this can be set to 12 MHz for compatibility with the classic 8051. The tick timer which governs the speed of button actions depends on the clock speed.
I thought I had taken care of this in the initialisation code, but to cut to the chase, here's the diff showing what I did wrong:
$ git diff 227da07..67b402a diff --git a/227da07 b/67b402a index 227da07..67b402a 100644 --- a/227da07 +++ b/67b402a @@ -200,8 +200,7 @@ void main(void) // change the clock divisor to generate 12 MHz SAFE_MOD = 0x55; SAFE_MOD = 0xAA; - CLOCK_CFG &= ~CLOCK_DIVISOR; - CLOCK_CFG |= CLOCK_DIVISOR; + CLOCK_CFG = (bOSC_EN_INT | CLOCK_DIVISOR); SAFE_MOD = 0x00; // any value will do #ifdef DEBUG #else
The clock configuration register is protected against accidental modification by a protocol. You have to write the values 0x55 and 0xAA to the SAFE_MOD register, make your change, then exit by writing anything to SAFE_MOD. The intention of the original code was to read the bits in the CLOCK_CFG register, except for the CLOCK_DIVISOR (bottom 3) bits, then write it back with those bits modified. You can see that the first line ought to be:
CLOCK_CFG &= (0xF8 | ~CLOCK_DIVISOR);
to preserve the top 5 bits.
But I also fell foul of another restriction and that is modification mode only lasts for a limited number of clock cycles. More instructions and the changes don't take. That was probably what was happening, it timed out before CLOCK_CFG was changed. So I just went for a simple one line replacement and that fixed the double speed problem.
BTW, I prefer to run the MCU at a lower clock rate rather than recalibrate the timer for a faster clock. It's more reliable at lower clock rates, and the MCU is idle most of the time anyway.
Hi Ken, thanks for liking my #Isetta TTL computer !