This post was originally published on mbed Developer Blog.
We do want to stress that we’re talking about writing the application layer in a higher level language. The core parts of your embedded OS, including the scheduler, peripheral drivers and network stack will remain native.
An event-driven model makes a lot of sense for IoT devices, especially on smaller sensor nodes. Many of them only run a networking stack and some peripherals, and respond either to events from the network, some interrupts or the real time clock. Using an event loop for scheduling, rather than manual scheduling, helps with a number of common problems when programming microcontrollers:
- Networking stacks that want to take over the main loop of the program, making it impossible to mix multiple networking stacks. Here’s an example (line 44).
- Delegation from interrupt context back to main thread to do actual work. Semaphores and mailboxes are complex beasts and taint your code very quickly. Also, the problem might not manifest itself straight away; doing things in ISRs is often fine with GCC but crashes when compiling with ARMCC.
Of course this model is not new, and we can run event loops in C++ code (we have a library for that as part of core mbed OS), but it’s a model that only works if everyone in the ecosystem plays along. When we tried to enforce the evented model as part of mbed OS 3 we ran into many problems, because it broke compatibility with a lot of libraries that were not written with the event loop in mind. This is the same problem that server-side languages faced before, where early attempts at event-driven development like Python’s Twisted or Ruby’s EventMachine never gained a large following, in part due to incompatibilities with the existing ecosystem. Node.js - without an existing ecosystem of synchronous libraries - however managed to popularize the event-driven programming model.
Event loops give us an additional benefit. When we’re aware of all scheduling, our OS is allowed to take over the sleep management schedule on your device. If we need to do sensor readings every 5 minutes, we can schedule this event and put the MCU in deep sleep in between. Combined with drivers implementing wake locks (for a reference of how this could work, see here), and furthering our work on tickless idle mode, we could get good sleep mode out of the box for many scenarios.
Cheaper firmware updates
One of the difficult problems in building IoT devices is encountered while patching devices in the field. For compiled code, it requires enough flash to hold two versions of the firmware as well as a bootloader which knows about firmware updates. The firmware is binary, which means it also generates relatively large diff files, even for small code updates. bsdiff generates 3-4 Kb diff files even for single line changes - while also being memory hungry. This makes firmware updates impossible on constraint networking protocols like LoRaWAN.
When we run an interpreted language, our application logic is just text, separated from the native libraries and the VM. This makes it easy to send new firmware to the device (it’s just text) and it compresses and diffs very well. This comes with the downside that you can only patch your application logic, but it can be combined with full device firmware update like we have in mbed Cloud to do cheap updates when possible, and full updates when necessary.
Developer tools innovations
A higher level language also allows us to think of new ways of writing code, by iterating faster on development tools. We have seen this with the BBC micro:bit project - in which a million children in the UK learned how to program as part of their primary school curriculum. The micro:bit comes with a visual block-based programming language in which students can create their program by dragging and dropping blocks together. On top of this the (in-browser) IDE also has a simulator present, which shows a simulated micro:bit running the program that was just created.
The simulator provides a fantastic way of shortening the develop test feedback loop from minutes to seconds. This is all enabled by the abstraction layer that the block editor provides. The same blocks can be compiled down to C++ to run on a device, or interpreted by the browser and be used to visualize the program in the simulator.
BBC:microbit Block Editor, with simulator running on the right side of the screen.
One of the other models that an interpreted language provides is the ability to run a Read-Eval-Print-Loop (REPL) on a device. With an REPL, we are given a shell in which we can interact with the device dynamically, without having to recompile code or flash new firmware. This allows for very flexible prototyping. Not sure if the datasheet is right for an external peripheral? Just use the REPL to quickly fire some I2C or UART messages to it. Again, this is about shortening the feedback loop and is also very useful if your actual application will be written in C++. Our REPL for JS on mbed is located here.
We might be able to take this further, offering pre-built images containing the core OS and a networking stack. When the device comes online, it can be programmed straight from the online compiler, storing the application code in flash. This could reduce the tools overhead for new developers even more.
Cost might be worth it (in the near future)
But, times are changing. With the advent of cheap modules, which combine MCUs and radios, the upfront cost of designing connected devices is being driven down by not having to pay an RF engineer or do certification for your device. Although this raises the price per unit, it will become feasible to do smaller runs of devices, creating a market where smaller companies can also produce (relatively) low-cost custom IoT solutions.
If you can save time developing your firmware by using a higher level language (at the price of a beefier MCU), it might be cost-effective at low volumes. With the price of modules constantly dropping (here’s a $2 WiFi module with a Cortex-M3 and 512K RAM), the cost effectiveness threshold will go up over time.