Introducing JavaScript* Runtime for Zephyr™ OS

By Geoff Gustafson, Intel Corporation

JavaScript* is one of the most widely used programming languages today, and in recent years has jumped from its origins on desktop web browsers to servers with Node.js*. But it can also be useful in the smallest devices for the Internet of Things (IoT) with the JavaScript Runtime for Zephyr™ OS (ZJS) environment. ZJS is based on JerryScript (a lightweight JavaScript engine) and the Zephyr real-time operating system (RTOS). The ZJS project provides a subset of Node.js APIs and JavaScript APIs that employ sensors, actuators, and communications, as well as the tools to build applications with them. The ZJS environment is easy to learn and great for rapid prototyping, especially for developers who have existing JavaScript skills.

The initial goal for the ZJS project was to work well on the Arduino 101* board. The Arduino 101 is an Intel® x86-based, entry level Arduino platform. The features are similar to the Arduino Uno*, but it adds Bluetooth® Low Energy and an accelerometer and gyroscope.

Preview

Before we get into the details, let’s take a sneak peek at what it can look like to develop a JavaScript application for a small device. This script controls a pair of peril sensitive sunglasses.

1280px-RayBanAviator.jpg
Figure 1: Sunglasses prior to ZJS enhancements
By Rich Niewiroski Jr. - http://www.projectrich.com/gallery, CC BY 2.5, commons.wikimedia.org

var aio = require('aio'); var gpio = require('gpio'); var pins = require('arduino101_pins'); var sensitivityUp = 1500; var sensitivityDown = 1200; // hysteresis to avoid flicker var perilDetector = aio.open({device: 0, pin: pins.A0}); var sunglasses = gpio.open({pin: pins.IO4, direction: 'out'}); var dark = false; setInterval(function () { var perilLevel = perilDetector.read(); if (perilLevel > sensitivityUp && !dark) { print('Peril detected, enabling sunglasses'); sunglasses.write(true); dark = true; } else if (perilLevel < sensitivityDown && dark) { print('Peril averted'); sunglasses.write(false); dark = false; } }, 10);

The peril sensor reports a voltage proportional to the amount of danger detected. The Arduino 101 converts the voltage to a number with 12 bits of precision (a range of 0 to 4095). When the danger level is too high the glasses turn totally black, and the glasses’ wearer is kept free from alarm.

Background

The Zephyr Project is an open source, real-time operating system released earlier this year by the Linux Foundation. It supports x86, ARM*, and ARC* processors on a growing number of boards, such as the Arduino 101, Minnowboard Turbot, NXP* FRDM-K64F, and Arduino Due.

JerryScript is an open source, lightweight JavaScript engine fully implementing ECMAScript 5 for constrained devices, supporting even devices with less than 64 KB of RAM or 200 KB of ROM. It already supports x86 and ARM processors on various boards and operating systems.

Intel started working to combine these two technologies in early 2016, with the idea of providing an alternative development environment for the Arduino 101 and future IoT boards using the Zephyr OS. Currently, Zephyr Project applications are written in C, and the programming model requires a fair amount of expertise. A JavaScript interface hides a lot of this complexity. We also wanted to explore the benefits of having JavaScript available at every level, from small embedded devices to the largest servers, and from prototyping to seamless end-to-end applications.

The resulting project is called Javascript Runtime for Zephyr™ OS (ZJS) and provides a JerryScript environment, adds APIs to expose IoT hardware features, and provides tools to simplify the developer experience.

Architecture

When you build a Zephyr OS application, you create a monolithic image dedicated to running your single application with only the OS components that your application actually uses, which keeps the image small. The Zephyr OS supports execution thread services, timer services, memory management, synchronization, and data passing services, each of which can be individually included or excluded depending on whether the service is needed. It also provides drivers for the various hardware features of the SoCs and boards it supports, such as general purpose I/O (GPIO), analog-to-digital conversion (ADC), pulse width modulation (PWM), I2C bus, SPI bus, UART, and Bluetooth Low Energy. Again, these can be individually included or excluded to minimize the image’s ROM and RAM profile.

ZJS builds on top of the Zephyr OS’s configurability by simple static analysis of the incoming JavaScript code. If the GPIO interface is used, the needed driver and corresponding ZJS API code will be built and included; otherwise, it won’t. If you use every possible driver, the resulting image will be too large for small devices. For example, adding Bluetooth Low Energy support (including both the Zephyr OS drivers and the ZJS API layer) currently uses an additional 7.2 KB of RAM and 56 KB of ROM. But if you keep an eye on the image size as you develop and limit including additional dependencies as necessary, you can balance these constraints.

Currently, ZJS provides APIs for GPIO, ADC, PWM, I2C, BLE, and a simplified interface for the Grove LCD panel with RGB backlight (an I2C device). It also supports setInterval/setTimeout, a simplified implementation of Promises, and a subset of the Node.js Buffer and Events APIs. Additionally, ZJS includes support for the Open Connectivity Foundation (OCF) specification for IoT interoperability, with the same JavaScript API that the iotivity-node module provides in Node.js. We are working on supporting the Serial Peripheral Interface (SPI) bus, the W3C Generic Sensor API, and other Node.js APIs.

diagram1.png
Figure 2: JavaScript Runtime for Zephyr™ OS (ZJS) Architecture on Arduino 101

The Intel® Quark™ SE processor inside the Arduino 101 has two MCU cores: one with x86 architecture and one with ARC architecture. Taking full advantage of the hardware requires using both cores, which the C programmer has to deal with directly. But fortunately ZJS makes all the hardware features available from the same JavaScript context. The ZJS runtime communicates with the ZJS support image running on the ARC core seamlessly when necessary. (See Figure 2.)

The Arduino 101 includes 80 KB of RAM and 384 KB of flash memory. By default the flash memory is partitioned to allocate 144 KB to the x86 application and 152 KB to the ARC application. ZJS needs minimal code on the ARC side, so it is beneficial to repartition the device with more room for the x86 application. We have developed a patch for the released Arduino firmware that lets you migrate from the 144 KB x86 partition to a 256 KB partition, and back again. This provides enough room to run much larger ZJS applications. Additionally, we will provide binary flashpacks that make changing or restoring your partitions simple.

Advantages and Tradeoffs

There are several advantages to bringing JavaScript to small, embedded devices.

  • Many people know JavaScript already. This should make development within the ZJS environment more accessible to more people.

  • Javascript enables the ability to reuse code between different environments—desktop, cloud, mobile, and IoT devices.

  • We gain the potential to use a common data exchange format, JSON, between these systems, which reduces the overhead of marshaling boilerplate. JavaScript also opens the possibility of more easily simulating device behavior within a browser.

  • As is typical to any interpreted language, including JavaScript, the development cycle from making a change to seeing the change in action is fast. Admittedly, this advantage is initially lost with our system because you need to compile the Zephyr OS with your application and flash it to the device. But we can reclaim some of this benefit with the ashell feature, which is an insecure developer mode that lets you interact with the device and upload new JavaScript to run without flashing. We plan to build on top of this to provide a browser-based IDE, which might give us the ability to develop entirely in the browser with a simulator.

Of course, there are tradeoffs to using JavaScript in small, embedded devices. Native code is generally going to be faster and smaller. This problem is much more acute on a constrained device because the overhead of the JavaScript engine consumes a significant portion of the system RAM and ROM. This limits the resources left for your application. At the time of this writing, the minimal HelloWorld.js sample requires about 133 KB of ROM. A larger WebBluetoothDemo.js sample requires about 199 KB of ROM and 42 KB of RAM. For comparison, a C implementation of the same WebBluetooth sample requires about 65 KB of ROM and 18 KB of RAM. (The RAM difference includes a large heap allocated for JavaScript which is not all being used.)

The performance differences will also be quite noticeable in cases where a C application might have handled something simple in an interrupt handler. To perform this logic in JavaScript, we have to first queue up an event to be handled in the task context (the Zephyr OS equivalent of process context), make the context switch, then make a callback into the JavaScript engine, and finally have that call into the native API again to have an effect.

There is also the problem of JavaScript being less deterministic in terms of real-time performance. For example, garbage collection can introduce a surprise hiccup in a regular pulse of activity. So JavaScript might not be suitable for apps that have strict real-time requirements, but it is very suitable for general purpose apps. However, as the project matures we might be able to mitigate these types of issues.

Detailed Example

Now let’s take a closer look at some sample code using ZJS that we can use to implement part of an altitude controller for a miniature hot-air balloon. We’ll measure the altitude with an analog sensor and use a PWM pin to control the flow of gas to the burner. Then we’ll blink an LED light for the safety of other air travellers. Finally, we’ll add an abort switch and a vent control for a quick landing.

10013-a-hot-air-balloon-in-a-blue-sky-pv.jpg
Figure 3: Hot air balloon
“Hot air balloon" by Rona Proudfoot is licensed under Creative Commons Attribution Share-Alike 2.0.

First, we use the require() function to make optional API modules available. This is a simplified version of what Node.js offers, which currently just lets you pick from existing components built into the ZJS system. This code is based on the early versions of the APIs, and we expect these APIs to undergo significant changes soon.

var aio = require('aio'); var gpio = require('gpio'); var pwm = require('pwm'); var pins = require('arduino101_pins');

Now that we have the API modules we need and the named pin shortcuts for the Arduino 101, we will set up the pins we’re using.

var led = pwm.open({channel: pins.IO3}); var altimeter = aio.open({device: 0, pin: pins.A0}); var gasValve = pwm.open({channel: pins.IO5}); var abort = gpio.open({pin: pins.IO2, direction: 'in', edge: 'rising'}); var vent = gpio.open({pin: pins.IO4, direction: 'out'});

The Arduino 101 supports PWM on pins 3, 5, 6, and 9. Note that vent is a GPIO output, while abort is a GPIO input. We enable an interrupt in the Zephyr OS by configuring the abort pin with a rising edge input.

Now let’s configure the LED on the balloon to blink on for one half second every four seconds:

led.setPeriod(4000); led.setPulseWidth(500); // values in milliseconds

Next, let’s write the main feedback loop to aim toward the desired altitude, assuming an ideal altimeter sensor that reports the altitude with 12 bits of precision and covers a range from 1000 m below sea level to 9,000 m above sea level. We’ll take a reading every 50 ms and adjust up or down as necessary.

var targetAltitude = 42 * 20; // meters, enough to clear Burj Khalifa gasValve.setPeriod(50); // ms var totalError = 0; var lastError = 0; timerID = setInterval(function () { var raw = altimeter.read(); // convert raw reading to meters var altitude = raw / 4095.0 * 10000 - 1000; // implement simple PID controller var error = targetAltitude - altitude; totalError += error; // carefully tuned PID constants for our device var proportional = 0.042 * error; var integral = 0.00042 * totalError; var derivative = 0.0042 * (error - lastError); lastError = error; // limit pulse to range of valve var pulse = proportional + integral + derivative; if (pulse < 10000) pulse = 10000; else if (pulse > 20000) pulse = 20000; gasValve.setPulseWidth(pulse); }, 50); // every 50ms

Finally, let’s land fast if we detect that we’re out of gas or receive an abort command from a radio controller:

abort.onchange = function(event) { // open the vent event.write(true); // stop the loop (smooth landing left as an exercise for the reader) clearInterval(timerId); }

When we set the onchange function, internally we set up a callback to handle the rising edge interrupt from the abort switch pin. When that interrupt comes, we store the updated value from the pin and signal a callback to be handled from task context in the main loop. When the main loop runs and processes that callback, it is safe to call into the JavaScript engine and update this onchange function with the new value (although we don’t bother to look at the event.value in this particular JavaScript callback).

This code should be pretty easy to understand if you have experience with JavaScript, and you can see the power of being able to work with real world sensors and actuators in such a straightforward way!

Getting Involved

We are just now opening up our repository on GitHub and it’s still quite early. We invite you to take our code for a spin, and welcome you to join us in defining the project’s future. There are plenty of areas waiting for contributions and ownership, such as new APIs, new platform support, improved debugging, image size optimization, software update, or whatever you can think of.

The project is hosted at http://github.com/01org/zephyr.js and development discussions happen on a #zjs channel on Freenode.

Intel and Quark are trademarks of Intel Corporation or its subsidiaries in the U.S. and/or other countries. Zephyr is a trademark of the Linux Foundation. The Bluetooth® word mark and logos are registered trademarks owned by Bluetooth SIG, Inc., and any use of such marks by Intel Corporation is under license. *Other names and brands may be claimed as the property of others.