How to run the same C code on chips from multiple vendors

I’ve been building my skills with Zephyr RTOS over the last two years. One of the most interesting parts to me is the ability to maintain one codebase and compile it for chips from multiple vendors. I recently built an IoT sensor fleet demo that has different nodes running Nordic, NXP, or Espressif chips and when Josh and Kelsie from DigiKey saw it they invited us to show it off at the DigiKey booth for the Sensors Converge Conference.

Golioth makes it easy to connect micrcontroller-based IoT devices to the cloud, making the data available on the cloud-side, and facilitating remote device management. The research part of this project centered around how to approach the Zephyr Real-Time Operating System application code in a way that makes it hardware agnostic. I’ll go into detail about how we did that, but if you want to see the code right now, check out the open source IoT Weather Fleet repository we published.

Three very different versions of the same hardware

Because this is just a demo, the sensor data is pretty simple: report temperature readings at an interval that can be configured remotely. But of course once you have it up and running, it’s trivial to swap that sensor data out for literally any other type of data.

Having been bit by the chip shortage in recent years, I wanted to demonstrate the ability to move not just within a chip family, but to completely different vendors. I landed on the Nordic nRF9160 (here that’s a Sparkfun Thing Plus nRF9160), the NXP i.MX RT1062 (here that’s an RT1060-EVKB board), and an Espressif ESP32 (here that’s an Adafruit Huzzah32 which uses the Feather form factor).

How do you run the same code on all of this different hardware? Zephyr uses Kconfig and Devicetree to abstract the hardware in a way that doesn’t bloat the build. This includes pin muxing and a clever sensor model, so all we needed to do was to make two files for each board variant. Here’s what the sensor and pin assignment looks like for the NXP board:

/ {
    aliases {
        weather = &bme280;
    };
};

&lpi2c1 {
	status = "okay";

	bme280: bme280@76 {
		status = "okay";
		compatible = "bosch,bme280";
		reg = <0x76>;
	};
};

You can see that I’ve chosen the i2c1 bus with a Bosche BME280 sensor. Don’t worry if the syntax isn’t totally clear right now, just look at the top where an alias is assigned for the sensor. This makes it possible for the C code to reference a sensor called weather. It doesn’t know what type of sensor that is, and it doesn’t need to know… Zephyr takes care of all of that and makes a generic “channel” available for the sensor reading:

// Create a pointer to our sensor
const struct device *weather_dev = DEVICE_DT_GET(DT_ALIAS(weather));

// Perform a sensor reading and access the temperature data
sensor_sample_fetch(weather_dev);
sensor_channel_get(weather_dev, SENSOR_CHAN_AMBIENT_TEMP, &tem);

So here’s the kicker, yes we used different microcontrollers, but we also used different sensors!

Different sensors, same C code

In the age of chip shortages, the ability to swap out a completely different sensor without rewriting your firmware is incredibly liberating. An Infineon DPS310 temperature sensor (shown here is a DPS310 breakout from Adafruit) stands in for the Bosch BME280 (shown in the image is a MikroE Weather Click).

The only thing that changed is the Devicetree overlay file that tells the build which sensor we’re using (and which pins to use for the i2c bus):

/ {
    aliases {
        weather = &dps310;
    };
};

&i2c1 {
	status = "okay";
	clock-frequency = <I2C_BITRATE_STANDARD>;
	pinctrl-0 = <&i2c1_default>;

	dps310: dps310@77 {
		status = "okay";
		compatible = "infineon,dps310";
		reg = <0x77>;
	};
};

&pinctrl {
	i2c1_default: i2c1_default {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 0, 26)>,
				<NRF_PSEL(TWIM_SCL, 0, 27)>;
		};
	};
};

Notice that the same alias is being created (weather) but a different sensor is assigned to that name. Again, Zephyr will take care of the abstraction, enabling and building in the proper driver library for the DPS310 instead of the BME280.

See the data come rolling in

Once built and flashed, the data looks the same. The only real difference is that the Infineon sensor has six digits of precision while the Bosche sensor only has two. Otherwise, the data from your fleet is being recorded on the Golioth servers and is ready to query, visualize, and use on any cloud platform you desire.

The demo also includes the ability to change fleetwide settings, like how frequently readings are being taken. And of course, if you need to tweak how the firmware is working, all of these devices can receive Over-the-Air (OTA) firmware updates. Give it a try, Golioth’s Dev Tier is free for your first 50 devices.

As the data rolls into the servers, we can immediately visualize it. Above we are using Grafana to graph temperature readings received from the three different boards.

See the demo at Sensors Converge

The Sensors Converge conference is happening right now in Santa Clara. The entire Golioth team is excited that our hardware demo is part of the DigiKey booth at the event. Head over and see the IoT weather sensor fleet sending readings live!

Resources

3 Likes