While looking for new embedded real-time operating systems, I came across the Zephyr Project. As it offers a wide range of possibilities for IoT, I thought that it would be very interesting to investigate it deeper. Let’s try to enable a BLE Environmental Sensing Service with Zephyr RTOS.
Software and hardware presentation
The Zephyr Project is an open-source monolithic real-time operating system (RTOS) licensed under Apache 2.0. Zephyr is optimized for resource-constrained devices and functions on multiple 32-bit architectures and dozens of boards, including the ARM Cortex-M family. In addition, it is also developed with IoT in mind as it supports current wireless standards such as Bluetooth Low Energy, Wi-Fi and IEEE 802.15.4; the standards are ready to use and need only to be selected for inclusion during compilation. Furthermore, Zephyr offers a large number of features, such as multi-threading with inter-thread synchronisation and data passing, memory allocation, interrupts and power management (including a tickless option). With that comes, among others, multiple scheduling algorithms, different file system supports and memory protection. Finally, the project provides native development tools for Linux, macOS and Windows.
A Bluetooth Low Energy Environmental Sensing Service is a service that contains environmental data, for example, temperature and humidity. This data can then be read with a BLE-enabled device, such as a smartphone. As a help, this article presents how Bluetooth Low Energy functions while this tutorial (read paragraph 2: basic theory) goes a bit deeper in the basics of the standard.
The device used for this article is a Nordic Semiconductor nRF52840 Development Kit (see also the Zephyr doc). Even though the device does not have extra sensors, the SoC (System on Chip) contains a temperature sensor that allows measuring the die temperature. Therefore, the result is not a totally correct temperature value, but it’s good enough for our test. Extra sensors can still be added on the device later if needed.
Application development
In the first place, the Development Environment must be set up. Once done, the development of the application can start. As a help, the Zephyr Project’s documentation contains many examples. For instance, this demo was inspired by the Bluetooth Peripheral HR demo and contains most of the general Bluetooth management functions found in the HR demo; hence, some pieces of code will be missing in the examples given later in this article. The demo is developed externally to Zephyr which means that it is an application that uses Zephyr and not an internal demo as found in Samples and Demos. Note that the code excerpts do not reflect the full code for compactness sake.
Some non-default kernel options are used for this demo which is necessary to define at compilation. It is possible to edit these options using the menuconfig tool, but this is not a lasting solution. The preferred way is thus to place these options in the prj.conf file.
CONFIG_BT=y # Enable Bluetooth Low Energy CONFIG_BT_DEBUG_LOG=y CONFIG_BT_DEVICE_NAME="Zephyr-BLE" CONFIG_BT_DEVICE_APPEARANCE=0 # Appearance of the device, 0 means Unknown CONFIG_BT_SMP=y # Enable the Security Manager Protocol CONFIG_BT_PERIPHERAL=y # The device is seen as a peripheral, it means that it is connectable CONFIG_BT_GATT_DIS=y # Enable the GATT Device Information Service (e.g. device model number) CONFIG_BT_GATT_DIS_PNP=n # Disable the PnP ID characteristic, we don’t need it CONFIG_SENSOR=y # Enable the Sensor Drivers in general CONFIG_TEMP_NRF5=y # Enable the Sensor Driver for the nRF5 Temperature Sensor CONFIG_PRINTK=y
We need now to manage the Bluetooth and the temperature sensor configurations and then glue both together. First, let’s start with the temperature sensor which is handled via the two following functions:
static struct device *sensor; void temp_init(void) { sensor = device_get_binding(CONFIG_TEMP_NRF5_NAME); printk("TEMP init, %p, %s\n", sensor, sensor->config->name); } int16_t temp_read(void) { struct sensor_value value; int16_t retv; sensor_sample_fetch(sensor); sensor_channel_get(sensor, SENSOR_CHAN_DIE_TEMP, &value); // Unit is degree Celsius. // Convert the Zephyr sensor value which is composed of an integer part // (int32) and of a fractional part (int32) elevated to 10^6, to BLE // Temperature characteristic value which is a small integer (int16) // with a resolution of 0.01 degrees retv = (value.val1 * 100) + (value.val2 / 10000); return retv; }
Second, the code below manages the BLE Environmental Sensing Service:
typedef int16_t (*ess_temperature_read_fct)(void); static ess_temperature_read_fct ess_temperature_get; static struct bt_gatt_ccc_cfg ess_ccc_cfg[BT_GATT_CCC_MAX] = {}; static bool ess_do_notify = false; static void ess_ccc_cfg_changed(const struct bt_gatt_attr *attr, u16_t value) { ess_do_notify = (value == BT_GATT_CCC_NOTIFY) ? true : false; } static ssize_t ess_read_temperature( struct bt_conn *conn, const struct bt_gatt_attr *attr, void *buf, u16_t len, u16_t offset) { int16_t value = ess_temperature_get(); return bt_gatt_attr_read(conn, attr, buf, len, offset, &value, sizeof(value)); } static struct bt_gatt_attr ess_attrs[] = { BT_GATT_PRIMARY_SERVICE(BT_UUID_ESS), BT_GATT_CHARACTERISTIC(BT_UUID_TEMPERATURE, BT_GATT_CHRC_NOTIFY | BT_GATT_CHRC_READ, BT_GATT_PERM_READ, ess_read_temperature, NULL, NULL), BT_GATT_CCC(ess_ccc_cfg, ess_ccc_cfg_changed), }; static struct bt_gatt_service ess_svc = BT_GATT_SERVICE(ess_attrs); void ess_init(ess_temperature_read_fct temp_read) { ess_temperature_get = temp_read; bt_gatt_service_register(&ess_svc); } void ess_notify(void) { u8_t temp[2]; if (!ess_do_notify) { return; } int16_t value = ess_temperature_get(); temp[0] = (uint8_t)((value ) & 0xFF); temp[1] = (uint8_t)((value >> 8) & 0xFF); bt_gatt_notify(NULL, &ess_attrs[1], &temp, sizeof(temp)); }
Third, the initialization and the notification functions of the ESS are called by the main part of the application.
void main(void) { int err = bt_enable(NULL); // Here we use synchronous configuration, bt_enable() has no callback. if (err) { printk("Bluetooth init failed (err %d)\n", err); } ... ess_init(temp_read); ... while (1) { k_sleep(MSEC_PER_SEC); ess_notify(); } }
Finally, compile and program the device.
Results
To use what we have just done, another BLE device capable of connecting to our BLE peripheral is required. Although there are many existing smartphone or desktop apps capable of doing that, the tools used for this article are the Nordic Semiconductor nRF Connect for Desktop coupled with the Nordic Semiconductor nRF52840 Dongle, or the Nordic Semiconductor nRF Connect for Mobile.
The image below depicts the value (0xC409) of the Temperature characteristic received via notification in nRF Connect for Desktop.
Unfortunately, the value is raw and is not interpreted by the software. The data is sent least significant octet first for multi-octet fields within the GATT profile [Core Specification (v5.1), Vol. 3, Part. G, §2.5], thus the hexadecimal value 0xC409 is equivalent to 2500 in decimal. As the value of the Temperature characteristic has a resolution of 0.01 °C, the die temperature is 25.00 °C. Note that nRF Connect for Mobile seems to interpret values for official GATT services, at least for the Temperature characteristic.
Conclusion
The quality of the Zephyr project and of its documentation allow setting up an IoT application within a few days. Its continuous evolution and its capabilities make of Zephyr a very useful tool for IoT developments. For these reasons, I would be very interested in using it in later projects.