DeviceTree Overlays on Zephyr RTOS: Adding I2C or SPI

After 18 months developing with the Zephyr RTOS, I’m starting to become a strong proponent. In my opinion, one of the key advantages of the Zephyr RTOS is the hardware abstraction. It allows applications to be written for Zephyr that are platform independent and can be moved between different boards including different manufacturers of microcontrollers. In a world still suffering from chip shortages, it has been a breath of fresh air.

The Zephyr application (project) contains the application source code while the board files detail the hardware and GPIO assignment, or essentially, how the printed circuit board is wired. This allows the same application – say a Bluetooth app, to run on different microcontrollers, for example one from Nordic, STM or Espressif. General purpose LEDs and switches are specified by name (function), not port numbers.

Zephyr is a small real-time operating system (RTOS) designed for embedded microcontrollers such as the ARM Cortex-M series devices. As a project of the Linux Foundation, it shares many similarities with Linux, including DeviceTree and more recently, Pin Control. This is at the heart of how Zephyr gets its platform independence.

Having developed on embedded Linux systems for over a decade and a half, I remember well the introduction of DeviceTree to the Linux kernel. Prior to DeviceTrees, the kernel would have to be compiled to support your specific hardware set-up. With device tree support, a binary blob (The DeviceTree) was loaded along with the one common Linux kernel.

The architecture on Zephyr is slightly different, catering to smaller resource constrained micro-controllers, but the concept is the same. The magic is done at compile time, not boot time like Linux.

Board Files

Board files are the primary descriptor of the hardware in Zephyr. The board files contain the primary Devicetree and other default Kconfig project settings, among other things. We won’t delve into board files, as we assume the board you are targeting is already supported by the Zephyr RTOS.

But the problem we want to address is if you want to add or reconfigure a common communications port such as I2C, SPI or UART on an existing board, or change the pin number assignment. You don’t really want to have to go in and hack the board files everytime.

This is where DeviceTree Overlays come to the rescue.

DeviceTree Overlays

DeviceTree Overlays can be used by the Zephyr application to specify new hardware or re-assign existing hardware described elsewhere in the board files. The reassignment could be as simple as a different UART baud rate or IO pins.

The DeviceTree overlay should belong in your application folder – within the folder called board. They are given the same name than your target board and with an extension called overlay. For example, if you are targeting the Nordic nRF52840 dongle that has the board name called nrf52840dongle_nrf52840, the filename should be nrf52840dongle_nrf52840.overlay. Multiple targets can exist in the one folder/application.

A complete overlay file to enable an I2C sensor on a Nordic part is shown below:

&pinctrl {

	i2c2_default: i2c2_default {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 0, 30)>,
				<NRF_PSEL(TWIM_SCL, 0, 31)>;
				bias-pull-up;
		};
	};

	i2c2_sleep: i2c2_sleep {
		group1 {
			psels = <NRF_PSEL(TWIM_SDA, 0, 30)>,
				<NRF_PSEL(TWIM_SCL, 0, 31)>;
			        low-power-enable;
		};
	};
};

&i2c2 {
	compatible = "nordic,nrf-twim";
	status = "okay";
	pinctrl-0 = <&i2c2_default>;
	pinctrl-1 = <&i2c2_sleep>;
	pinctrl-names = "default", "sleep";
	clock-frequency = <100000>;
	shtcx@70 {
		compatible = "sensirion,shtcx";
		label = "SHTC3";
		reg = <0x70>;
		chip = "shtc3";
		measure-mode = "normal";
	};
};

Pin Control

The @pinctrl node is used to specify I/O pin assignment and properties, i.e. input, output, pull-down, pull-up resistor etc.

Many of the properties will be specific to your vendor of MCU. This example is specific to a Nordic Semiconductor MCU and information for Nordic pinctrl can be found at

In the example above, we have two groups of pins – one used by default when the device is in an active state, and another when the device is asleep. By default, when the device is operating, SDA and SCL, pins 30 and 31, respectively, have internal pull-up resistors enabled. When in sleep, the pins are configured as an input with the input buffer disconnected (low-power-enable).

Standard pin properties for Nordic devices are:

  • bias-disable: Disable pull-up/down resistors.
  • bias-pull-up: Enable pull-up resistor.
  • bias-pull-down: Enable pull-down resistor.
  • low-power-enable: Configure pin as an input with input buffer disconnected.

I2C

The @i2c node is used to set up the I2C peripheral, also known as the Two Wire Interface (TWI) in Nordic Semiconductor documentation.

Once again many of the node properties are vendor specific. Documentation on the Nordic Two Wire Interface (Master) can be found here:

The last digit following I2C specifies the instance of the peripheral. In our example, the nRF9160 has four instantiated serial communications peripherals. Each instance can be configured as either I2C, SPI or UART. This means you can have four I2C ports, but it prevents you from having an UART or SPI ports.

The compatible node is generic and used to bind to the right peripheral (i.e. nordic TWI master). It generally takes the format of vendor,device.

The status node is also generic and in zephyr can be set to either "okay" or "disabled".

pinctrl-0, pinctrl-1 and pinctrl-names are used to link to the pin control parameters in the pinctrl node.

Clock-frequency is vendor specific and sets the I2C clock frequency to 100kHz – slow speed for I2C.

SPI

A SPI peripheral node takes on a very similar format:

&spi2 {
	compatible = "nordic,nrf-spim";
	status = "okay";
	cs-gpios = <&gpio0 10 GPIO_ACTIVE_LOW>;
	pinctrl-0 = <&spi2_default>;
	pinctrl-1 = <&spi2_sleep>;
	pinctrl-names = "default", "sleep";
	adxl345: adxl345@0 {
		compatible = "adi,adxl345";
		reg = <0>; 
		spi-max-frequency = <1000000>;
	};
};

But rather than having an address like I2C, SPI uses a chip select (CS) line. This is set up in the cs-gpios property and can contain one or more chip select lines.

Documentation on the Nordic SPI (Master) can be found here:

Device Subnodes

Zephyr includes a sensor subsystem with common API (sensor_sample_fetch(), sensor_channel_get()) used to interrogate a wide range of sensors.

In addition to describing the communications bus, you can specify what is connected to that bus in terms of supported sensors or devices. This subnode takes on the format as below:

	shtcx@70 {
		compatible = "sensirion,shtcx";
		label = "SHTC3";
		reg = <0x70>;
		chip = "shtc3";
		measure-mode = "normal";
	};

Here we have specified that a Sensirion SHTC3 digital temperature and humidity sensor is connected to this I2C bus and it has an I2C address of 0x70 (reg property). The I2C address can also be found after the node name, concatenated by an @ symbol. This is what is called unit addressing, but is not mandatory.

The subnode is made up of base properties and device specific properties. You will want to consult Zephyr documentation for the properties specific to your sensor:

Documentation for the sensirion,shtcx node can be found at:

For the example above, chip and measure-mode are required. Chip specifies exactly what integrated circuit is used – 'sthc1' or 'sthc3', and measure-mode specifies which measurement mode is used – 'normal' or 'low-lower'.

adxl345: adxl345@0 {
    compatible = "adi,adxl345";
    reg = <0>; 
    spi-max-frequency = <1000000>;
};

An example SPI device subnode is shown above. The reg property selects the index of the peripheral CS line.

Details on the sensor subsystem API is out of scope of this guide, but can be found at:

Examples

Complete and functioning examples based on the Lemon-IoT nRF9160 can be found below:

Reference Documentation




Be the first to comment

Leave a Reply

Your email address will not be published.


*