RIOT: The power of variability

After we covered the quality and technical debt of RIOT’s code in our previous post, we will now take a deeper look into RIOT’s variability.
Since RIOT is an OS that is supposed to work with quite a lot of different hardware, and applications using RIOT with all kinds of different modules, there are a lot of options and different features to choose from.
What it comes down to is that RIOT offers variability, and a lot of it. But how it offers this and how it implements this is this post’s topic.
In this post we will look at RIOT’s main features, its variability management and its implementation.

Variability modeling

The variability of RIOT can be categorised in mandatory choices and optional choices. First the mandatory choice for RIOT is the target board on which the software will be flashed. At the moment of writing, developers can choose up to 144 different boards. Furthermore it is optional to use different drivers and network libraries to implement the desired functionality. RIOT categorises these drivers into Actuator Device Drivers, Display Device Drivers, Miscellaneous Drivers, Network Device Drivers, Peripheral Driver Interface, Power Supply Drivers, Sensor Device Drivers, Soft Peripheral Driver Interface and Storage Device Drivers. Furthermore it supports different network libraries, such as IPv4 and IPv6 protocols, MQTT, LoRa, 6LoWPAN, Bluetooth, CAN and many more.

Naturally there are also incompatibilities and requirements between the different features. For example, a standard Arduino nano does not support ethernet communication by default. To accomodate this functionality an ENC28J60 can be used as an extension board. Furthermore some libraries are only implemented for certain boards, for example the ws281x library is at the moment of writing only implemented for Atmega and ESP32 boards as well as the native environment.1

Feature model of the mandatory CPU choice. For readability only a selection of the possible cpu's and boards is shown

Feature model of the optional drivers. For readability only a small selection of the possible drivers is shown.

Variability management

In our first post we identified 5 major stakeholders: the end users, the business, the customers, the domain experts and the developers and a minor stakeholder: academic institutes. In our post we determined that the business and customer stakeholders weren’t really applicable, so they will not be discussed here. We also saw that the end users, the domain experts, and the developer stakeholder have a lot of overlap. Furthermore we wanted to mention academic institutes, but in this section they would not add anything that the end users, the domain experts, and the developer already do so they also will not be discussed here.

Variability is managed the same for end users, the domain experts, and the developers. They might use it toward different ends, but it is managed the same nonetheless. The main way variability is managed is through Makefiles. Every application using RIOT has a Makefile, in the Makefile one would specify which board they would like to use, which modules and dependencies they would like to be included, as well as which flags they would like to be set or unset. The modules included in the Makefile can then be used by the C or C++ file containing the main function positioned in the same directory. The Makefiles only specify what is to be used, to actually provide the right environment is still the responsibility of the end users, the domain experts, or the developers using it.
Luckily, the maintainers at RIOT provided some general documentation and some documentation to help out with running apps.
As for documentation for updating RIOT and applications, there isn’t a one size fits all solution. To update RIOT itself, the easiest way is to pull the latest version of the repository. The same is valid for applications running RIOT.

In RIOT’s coding workflow they already recommend developing on their own emulator first, and if it is bug-free, develop further on the target hardware. Furthermore, their Makefiles are set up with the macros BOARD_BLACKLIST and BOARD_WHITELIST. When a board is not on the whitelist, it essentially means that errors are expected when an application is built for that board. The same behaviour results from any board on the blacklist. These macros are especially useful for the CI pipeline. If a board is on the blacklist or not on the whitelist then the application for that board is not built at all. But this also means that a developer can test an application for multiple boards at once.
This variability does have an impact on testing though. The time required to create and test a module becomes longer with every board it is to be used on. Not just because there is more to develop, but also because debugging becomes longer and harder. Bug fixes for one board need to make sure they don’t break the code for other boards.

Variability implementation

The use of Makefile already hinted a bit at the variability implementation mechanism and what binding time the developers at RIOT use. It is clear they use compile-time variability binding as they use Makefiles to configure the settings they wish to use for their application before it is compiled. Furthermore many modules in RIOT offer configuration options that are considered during compile-time. This also makes it clear RIOT makes use of conditional compiling, as only the environments and modules the developers wish to use are compiled and built. They also make a little use of run-time binding time. For example some aspects of their own RIOT native instances can be configured at runtime. The implementation mechanism they use for this is a design pattern, in particular the decorator pattern.

As mentioned in section Variability modeling the first mandatory variable feature is choosing a target board. This feature is implemented using Makefiles; in the Makefile the developer chooses which boards to use and during compile-time the application will only be built against the boards specified in the Makefile. Since an application is not compiled for all boards, but only those which satisfy the condition of being specified in the Makefile, one could conclude that the implementation mechanism used here is conditional compiling.

Hardware variability implementation

IoT applications are naturally involved with different types of devices. It is reasonable for the design of an IoT operating system to configure for the variability of running on various platforms. From common x86 architecture to popular STM32 family, RIOT supports a wide range of IoT platforms.2 If we take a closer look into RIOT’s implementation, we can find that all the platform related code is organized in separate folders named cpu and boards. As mentioned before, different Makefiles are contained in these files and are used to configure RIOT to run on the specific platform.

Under the folder cpu, files related to certain cpu/mpu specific functions and the mapping of the peripherals of the cpu/mpu are included. Most files are collected into the subfolder named as the platform that those files are related to, for instance, lpc1768. One thing to notice is that certain common files shared among different types of cpu/mpu of the same base architecture are put into separate folders (like arm7_common).

In boards, locate the boards related initialization files. Each subfolder contains the drivers (I2C, SPI, UART), pinouts information and other board specific files. Like what the maintenance team did for the cpu part, they also extracted common files of a family of boards and organize them into a single subfolder boards/common.

Another crucial feature for an IoT operating system is to ensure the connection between all kinds of devices wirely or wirelessly. Also different types of connection protocol is used under different scenarios for IoT applications. RIOT handles this variability by including different driver files to support different communication protocols.

Apart from communication other optional features are also handled by RIOT through the driver files. A wide range of sensors , ADC/DACs and other components (servos for example) used for IoT development are included in this folder as well. This way of organization allows the maintainer to keep adding new functionality and features of RIOT, which increases the flexibility and variability in features of RIOT.

Variability, done right?!

This post has discussed the different forms of variability available in RIOT. Since the way of implementing the variability is a design choice, it is interesting to see if this choice is a good one. The variability for cpus and boards is well implemented, and offers the required flexibility of adding new boards. However with the increasing number of cpus and boards, it would be nice to organize the cpu and boards into folders for similar devices.

Regarding the variability implementation of the drivers, there might arise a scalability issue in the drivers/Makefile.dep file. This file contains information of supported architectures and required features for the use of a specific driver. This file already has 822 lines, and is not easily readable.

Concluding, the compile-time variability implementation with Makefiles seems like a useful option for RIOT, however there is room for improving the implementation for scalability.