IntroductionEmbed from Getty Images
Most IoT (Internet of Things) based products are programmed using C and have tight power and cost budgets, and consequently need to be kept small and efficient. To achieve these goals, I wonder many people have considered their choice in operating systems as being one of the biggest influencing factors. If so, you may have come to realise that there is a huge selection, each having its own strengths and weaknesses, each more suited to a specific environment and meeting a different set of requirements.
Choosing an OSEmbed from Getty Images
One can easily be tempted into using a preemptive multitasking RTOS (real-time operating system). The main catchlines for these are multithreading, resource management, low latency and a familiar programming paradigm. Some may even have really good toolchain integration. The fact that they offer threads is also really nice – you can isolate your code from other processes and thereby reduce complexity and reduce certain types of risks.
However, one thing seldom advertised is how expensive they are in terms of RAM usage and how prone they are to certain types of difficult to find resource management bugs (mainly deadlocks, priority inversion and race conditions). Each thread has its own stack, each stack having to be big enough to store all local variables used all the way down to the deepest nested procedure (which can be quite sizable).
Some modest systems I am aware of have up to one hundred threads. This all adds up and as I have mentioned in my previous post, this can easily be ten times what an equivalent single-threaded event-driven system would use. This constantly reminds me of why many years ago, I chose to build my own OS using a single round robin worker thread with interrupt-driven RTC (run to completion) processes to implement something similar to threads.
For resource constrained devices (which includes most microcontroller based systems), I would suggest that the genre of OS you choose has the single biggest impact on RAM usage. The three main categories of operating systems are :
- Event-Driven Task Schedulers – the smallest and simplest, non-real-time, using a single stack and RTC.
- Cooperative multitasking – medium and flexible, non-real time, using multiple stacks,
- Preemptive multitasking – large and RAM hungry, and often real-time.
If forced to use a conventional multitasking OS, the factor that would influence the RAM usage the most is which type of process management you favour. The more you make use of an event scheduler instead of conventional threads, the more memory you will save, and the more you can avoid issues relating to preemption mentioned above. In many applications, a preemptive RTOS is also an overkill, and one can achieve the same goals at a lower cost by simply using an Event-Driven Task Scheduler.
Focussing on Event-Driven Task Schedulers
When choosing an event-driven kernel, your two main choices are TinyOS and Contiki OS. Unfortunately, Tiny OS isn’t normally programmed using C. Rather it uses nesC, a dialect of C. Although this has advantages (eg offering clean event-centric programming semantics and memory protection), it requires a fair amount of learning, it isn’t widely used (making reusability difficult), and I don’t expect it will work well with the many tools available for C (eg static analysis tools). So, as nice as it is for certain projects, I will strike it from my list of serious choices.
For C programmers, this leaves us with Contiki. What makes Contiki particularly interesting is its use of coroutines – the exact flavour being Protothreads. Normally coroutines are associated with one’s ability to launch thousands or even millions of instances of a specific process with a relatively small impact on your machine. Certain languages have even been designed with this in mind and have made it a first-class citizen. However, what makes Protothreads so exciting is not the hope that many instances can be launched concurrently (which is not the case or the intention), but the fact that many of their semantics and much of their behaviour are very similar to normal threads coupled with the fact that they do not have the memory or context switching overheads of conventional threads. For lower priority (or non-real-time) tasks, this is perfectly fine.
Behind the scenes, Protothreads “run to completion”. This means that any local variables need to be qualified with static if they are to be preserved between continuations. However, from a programmers perspective, they can be made to look as if they are merely delayed and made to continue when a timer event (or other selected event) occurs.
Although Contiki supports Protothreads natively, one isn’t limited to using it when you need Protothreads. To use Protothreads, all one needs to do is to include the pt.h header file (which includes a few macros and a definition which saves the Protothread context). This means you do not need to sacrifice the real-time behaviour of your RTOS and should be able to use many Protothreads inside any conventional low priority single thread you have available and thereby significantly reduce the number of real threads you end up using.
Each Protothread is simply encapsulated in a single C function. One topic not clearly covered by documentation is how one should invoke Protothreads. The simplest way is to simply place them in a while(1) loop and let them manage themselves using a polling mechanism. A more sophisticated way to call them would be to launch them from interrupts triggered by a timer or by other interrupt-driven events you have associated them with. If used within the context of your favourite OS, can also simply invoke the thread in which they reside using a standard signalling mechanism. I am sure you can also think of other ways they can be invoked.
Another potentially very useful use of Protothreads is to drive state machines. One of my pet peeves with state machines implemented in C (and similar languages) is that their semantics do not read or flow very well and they can be quite difficult to follow, especially when states are changed outside the function they are primarily implemented in. Well, Protothreads introduce a much simpler, more intuitive and easier to read semantic which flows a lot more easily, and the state does not get explicitly changed by the user (rather implicitly by the flow of the code). An added benefit is that it significantly reduces your line count.
CaveatsEmbed from Getty Images
While programming in C using Protothreads produces nice clean readable code and at little cost to your system, I would like to point out that it does have a small cost in terms of how you need to program. Some of these limitations are:
- You need to implement all the control mechanism for the Protothread in the same top-level function. However, you can spawn a child Protothread if you need to, but this will feel like a new thread.
- Unless you are using GCC (which implements non-standard commands making continuations more flexible), you shouldn’t use switch statements in your top-level protothread function (normally it won’t work).
- All local variables need to be declared static to be preserved. This means that you usually cannot create more than one instance of a Protothread. However, if you are prepared to modify the context storage structure of the Protothreads to hold your local variables (ie thread local storage), you will be able to get around this limitation.
- The macros used in Protothreads use some rough mechanisms normally frowned on, such as using goto statements or using switch statements to jump into the middle of a while loop. This, ironically, is to make the code easier to read, so if you end up using Protothreads or similar implementations, I recommend that you ignore how the macros actually work – just focus on what they do. Also, hide them from any code reviews or static analysis software.
ConclusionEmbed from Getty Images
C programmers often feel left out with many of the innovations introduced in other languages. Unfortunately, the designers of the C language have not been able to keep up with many of these features in the same way that C++ has. However, C does offer some levels of flexibility and you can implement many of the missing features with common language semantics although usually not always as elegantly.
Although there have been generalised attempts at introducing coroutines into C, success has been fairly mixed in terms of robustness, flexibility and portability, and many of the most successful attempts are limited pretty much to larger systems.
However, the Protothread implementation appears to be a fairly usable attempt, specifically useful for filling our niche low-cost thread semantic needs (required by many small embedded products such as wireless IoT sensors) and for improving state machine implementations. I will be definitely be planning to investigate these further and experiment with them whenever the next opportunity arrives.
My goal hasn’t been to show you the details of how to use coroutines or Protothreads, but to try to make you aware of them (if you are not already) and to add value to what has already been covered in the many other pages on the topic by looking at them from a different angle. I have done this by showing you possible ways they can be used to save cost and reduce complexity in small products – typical of IoT-based systems. For those interested in learning more, you may like to follow some of the links below – many of which I used as references. Also, feel free to add any extra insights you may have or any other points of interest.
- Operating Systems for Wireless Sensor Networks: A Survey
- Coroutines in C
- Contiki OS website
- Adam Dunkels (the inventor of Protothreads) website,
- Using Protothreads for Sensor Node Programming – an interesting academic paper,
- Comparison of Operating Systems TinyOS and Contiki – an interesting academic paper,
- Protothreads: Simplifying Event-Driven Programming of Memory-Constrained Embedded Systems – an interesting academic paper,
- TinyOS – another small and popular OS also using event scheduling – using nesC as a programming language