Protothreads
Learning outcomes
- Understand why we need protothreads
- Understand how they work
- Be able to diagnose basic protothread errors
Motivation
- Consider any task that involves blocking:
- opening a file
- reading a file
- interacting with the i2c bus
- sending a message over the radio
- Without multi-tasking, we need to loop around and wait for completion
- Modern operating systems have some form of `wait’ instruction that `blocks’ this task until a certain thing has happened.
- In the meantime, other tasks can be run by `context switching’
Wait, what?
- To multitask, we need to be able to do several things:
- save the context (program counter, registers)
- load a different context (same but for a different process)
- restart executing
- To do it really well, we also want to protect process A from process B
- we need memory protection / virtual memory addressing
- we need supervisor mode to do special things like switch context
- not all CPUs have support for all this!
State machines
- Consider a simple control problem: toggling a light
- When we press the button and the light is on, we turn it off
- When we press the button and the light is off, we turn it on
How do we express this
- First we enumerate all possible states as
$q_1, q_2, \ldots$ - In this case, we have 2 states (light on or off)
- Then we define the conditions for transitions between states
- The result can be drawn as a graph
Alternatively to labelling the circles (states) with whether the light is on or off, we can label the arcs (transitions) with the action of turning on or off the light.
Representing a state machine as a table
The previous state machine can also be represented as a table with columns for
- what state we are in
- what additional condition causes a transition
- what new state to transition to
- actions to take when transitioning
state | condition | new state | actions |
---|---|---|---|
push | turn_on_light |
||
push | turn_off_light |
||
Converting to code
void toggle() {
while (1) {
while (! get_button()) {}
turn_on_light();
while (! get_button()) {}
turn_off_light();
}
}
- but this means we can’t do anything else while waiting for the button press
Just one state transition
- Key idea: perform one state transition per function call
int fsm(int state) {
button = get_button();
if (state == 1 && button) {
turn_on_light();
state = 2;
}
else if (state == 2 && button) {
turn_off_light();
state = 1;
}
return state;
}
So what do protothreads do?
http://dunkels.com/adam/pt/expansion.html
Note: in Contiki, the macros start PROCESS
, not PT
Exercise - Check out the macro expansion
$ cd contiki/examples/hello-world
$ make TARGET=native hello-world.e
Examine the result
- can you see what each macro got substituted for?
static char process_thread_hello_world_process(struct pt *process_pt, process_event_t ev, process_data_t data)
{
static struct etimer timer;
{ char PT_YIELD_FLAG = 1; if (PT_YIELD_FLAG) {;} switch((process_pt)->lc) { case 0:;
etimer_set(&timer, 1000 * 3);
while(1) {
printf("Hello, James\n");
do { PT_YIELD_FLAG = 0; (process_pt)->lc = 60; case 60:; if((PT_YIELD_FLAG == 0) || !(etimer_expired(&timer))) { return 1; } } while(0);
etimer_reset(&timer);
}
}; PT_YIELD_FLAG = 0; (process_pt)->lc = 0;; return 3; };
}
Communicating between two processes
- It seems like we can call one process from another
- However, you should never do this!
- Think about what
process_pt
struct you are passing in
- Think about what
- Instead use
PROCESS_POST
to queue an event that is then received by the other process
Understanding PAUSE versus YIELD
- As discussed in the wiki,
PROCESS_PAUSE
is not the same asPROCESS_YIELD
- PAUSE expects to be called again as soon as possible
- YIELD says - wait for the next event (and the processor can sleep)
- This is why we typically want to use event timers and event waits, so that the processor can sleep while waiting
- A nice exercise to try here is to compare an ordinary
timer
with anetimer
- What sort of wait do we need for
timer
? - Do both operate in the same way?
- Which one allows the processor to sleep?
- What sort of wait do we need for
Using event timer
The normal approach to sleeping (or delaying) for some duration is to use an event timer.
static struct etimer timer;
PROCESS_BEGIN ();
/* Setup a periodic timer that expires after 10 seconds. */
etimer_set (&timer, 10 * CLOCK_SECOND);
while (1)
{
printf ("etimer reading is %lu\n", clock_time());
/* Wait for the periodic timer to expire and then restart the timer. */
PROCESS_WAIT_EVENT_UNTIL (etimer_expired (&timer));
etimer_reset (&timer);
}
PROCESS_END ();
Using a normal timer
It is possible to use a normal timer but notice that we need to use PAUSE to ensure that the process is still considered active. This will have the negative side effect of not allowing the processor to sleep.
static struct timer timer;
PROCESS_BEGIN ();
/* Setup a periodic timer that expires after 10 seconds. */
timer_set (&timer, 10 * CLOCK_SECOND);
while (1)
{
printf ("timer reading is %lu\n", clock_time());
/* Wait for the periodic timer to expire and then restart the
timer. */
do {
PROCESS_PAUSE();
} while (!timer_expired(&timer));
timer_reset (&timer);
}
PROCESS_END ();
Things to watch for
process_pt
is a structure withlc
being the line counter- rather than loop and wait, set
lc
to the current line and return immediately - the
switch
andcase
causes a jump into the inside of the loop whenlc
is 60!
Key ideas
- Protothreads are a super-lightweight way to get multiple processes to run concurrently.
- PT use (tricky) macros to turn ordinary looking code into a state machine
- Understanding how they work helps when diagnosing compilation problems
Summary
- We’ve uncovered the heart of Contiki, which is concurrency through protothreads
- Understanding PTs will help when trying to understand compiler errors
Additional reading
https://github.com/contiki-ng/contiki-ng/wiki/Documentation:-Multitasking-and-scheduling