The early software for Photon's MSP430 was implemented without formal threads. As the features grew, the code became a mess of asynchronous logic and global state. A better solution was needed, and Scheduler.h
allowed the codebase to be organized as discrete threads with synchronous logic and local state, greatly improving its readability and writability.
Scheduler.h
is a C++ threads implementation for embedded devices, implemented as a single header. At the time of writing, it's ~650 lines, and supports MSP430 and ARM32. Scheduler.h
is intended for single-core embedded chips; it offers concurrency, not parallelism. Scheduler.h
is a cooperative (non-preemptive) scheduler. The cooperative nature can be both a blessing and a curse: knowing that other threads cannot run until the current thread yields makes reasoning about concurrency much easier and often lends itself to simplified logic. In a sense, the current thread holds a global mutex that prevents other threads from interrupting it until it decides relinquish the mutex (by sleeping or yielding).
On the other hand, if there's a bug that prevents a thread from yielding, other threads will not run. In practice this doesn't tend to be an issue because embedded systems tend to have limited complexity due to the limited codespace, unlike generic systems that run arbitrary code. (The beloved Mac OS 9 comes to mind – delightful, but not the most stable!)
Scheduler.h
threads don't have priorities — they are organized as a compile-time linked-list and executed as such. This means that there's a user-defined deterministic order that the threads are executed. Sometimes this can be useful in that it allows one thread to assume that another thread was given the oppurtunity to execute before itself.
Scheduler.h
calls a user-defined sleep function, which puts the processor to sleep to conserve power. Scheduler.h
is intended to be as small as possible to fit within the confines of our wee 16-bit friends. The two-thread code example given below consumes 886 bytes of codespace when compiled with gcc -Os
, which equates to about 6% of the chip's codespace.
Scheduler.h
implements simple stack overflow detection by writing canary values at the bottom of the stack. Whenever a thread yields, the stack is inspected, and if the canary values are corrupt, a user-defined function is called with an index representing which thread overflowed. Stack overflow detection can be disabled to eliminate both the codespace and runtime penalty, which might be chosen for production builds. (Photon leaves stack overflow detection enabled to catch problems in the field.)
Stack overflow detection is especially helpful when trying to determine the minimum size for a thread's stack to avoid wasting RAM.
Scheduler.h
comes from the expressiveness with which you can pause the current thread until a particular event occurs. Scheduler.h
implements the following ways to yield to the scheduler:
Wait(fn)
Pauses the current thread until fn
returns true
.
Wait(ticks, fn)
Pauses the current thread until fn
returns true
, or for ticks
to elapse.
Returns false
if the timeout elapsed, and true
otherwise.
WaitDeadline(deadline, fn)
Pauses the current thread until fn
returns true
, or for deadline
to arrive.
Returns false
if the deadline arrived, and true
otherwise.
Sleep(ticks)
Pauses the current thread for ticks
.
Delay(ticks)
Pauses the current thread for ticks
, but doesn't allow other threads to run.
While waiting, the chip is put to sleep by calling the user-supplied T_Sleep
template function.
Yield()
Yields to the scheduler to start executing the next thread.
A thread that continuously calls Yield()
does not allow the chip to sleep, because the scheduler must assume that the thread still has work to do.
The Wait()
functions should therefore be preferred to Yield()
.
Scheduler.h
is a single header that's intended to be easy to integrate into new or existing projects.
Scheduler.h
has two main requirements: For example, a MSP430 sleep function would modify the SR
register to enter one of its low-power modes, while a ARM32 sleep function would execute the WFI
instruction.
Scheduler::Tick()
from the interrupt context when the timer fires. The period of the timer is provided to the scheduler via a template argument (see Usage section, below).
Scheduler.h
requires defining your own scheduler type and configuring it via template arguments: using Scheduler = Toastbox::Scheduler< // T_TicksPeriod: time period between ticks std::ratio<1, SysTickFreqHz>, // T_Sleep: function to put processor to sleep Sleep, // T_StackGuardCount: number of stack canaries to use // 0 == disables stack overflow detection 4, // T_StackOverflow: function to handle stack overflow StackOverflow, // T_StackInterrupt: interrupt stack pointer // ARM32 can have a separate stack for interrupt handling. // If supplied here, it will be monitored for overflow. // Unused if T_StackGuardCount==0. StackInterrupt, // T_Tasks: list of threads TaskButton, TaskLED >;Once the scheduler type is defined, it needs to be invoked:
int main() { Scheduler::Run(); return 0; }The scheduler starts by executing the first thread that was supplied as a template argument. (
TaskButton
in the above example.) By default, all threads except the first are stopped, and must explicitly be started with Scheduler::Start()
for them to execute.
Scheduler.h
example targeting MSP430 is available on GitHub. This example implements two threads; TaskLED
toggles one LED every second:
void TaskLED::Run() { for (;;) { P1OUT ^= BIT0; Scheduler::Sleep(Scheduler::Ms<1000>); } }while
TaskButton
waits for a button to be pressed, and then rapidly flashes a second LED 100 times: void TaskButton::Run() { for (;;) { Scheduler::Wait([] { return _Pressed; }); for (int i=0; i<100; i++) { P1OUT ^= BIT1; Scheduler::Sleep(Scheduler::Ms<20>); } _Pressed = false; } }
Scheduler.h
can be ported to additional architectures by implementing three functions:
IntState::Get()
: returns whether interrupts are currently enabled IntState::Set()
: sets whether interrupts are enabled __TaskSwap()
: pushes callee-saved registers, saves/restores the stack pointer, and pops callee-saved registers Search Scheduler.h
for msp430 to see these three locations that require architecture-specific code.