Adding an Out-of-Tree sensor driver to Zephyr

Embedded firmware development almost always involves an interaction between an MCU and sensors used for detecting environmental factors. The Zephyr OS has a very particular way of interacting with sensors that can be challenging to learn and duplicate.

I needed to incorporate a sensor driver without it being part of the Zephyr tree. I won’t cover all of the detail necessary to build a driver from scratch, but I certainly learned a lot about how the driver model works in Zephyr. I’ll be sharing that with you today.

In the tree, or out of the tree?

There are two methods of adding a driver to Zephyr. The first is to add the relevant files to the OS directory internally, such as in the sensors folder you see on the main Zephyr repo. The second is to add the driver into a directory structure outside of Zephyr, known as Out-of-Tree.

Working “In-Tree” (IT) is the most straightforward: the sensor driver lives in the official Zephyr repository as if it was native to Zephyr the day the project started. Any hardware vendor hoping to get their device driver In-Tree would need to submit a Pull Request (PR) to the Zephyr project to be included on every computer compiling Zephyr in the future. This is also a benefit for my learning: there are many examples of how to do this in the main repository from all the PRs. I can go to GitHub and track down the change that incorporated any particular sensor. This serves as a guide, with relevant file additions and existing file modifications.

Working in an “Out-of-Tree” (OOT) context means we will develop driver code independent of the central Zephyr repository, so no upstream changes are required. I think this helps to clarify the driver binding process and hierarchy. There are use cases for retaining the driver code alongside application code and not incorporating the driver within the OS, especially for projects that have customization or need to keep some aspect of driver code out of the public repositories.

We’re going to move an IT driver to an OOT context. The benefit of this exercise is that we will see how Zephyr locates and binds the driver to the application target device. All of the relevant files and file changes are contained to a single driver folder rather than spread out over the OS in various include, src, and specialized header files.

Understanding how Zephyr drivers are structured

There are three categorical topics to understand when adding and interacting with a driver. These are:

  1. Get the build system to find the driver
  2. Have the target-specific overlay file in place that aligns with the sensor yaml file
  3. Understand the Zephyr generalized sensor interaction functions

Getting the build system to find your driver and incorporate it involves one manifest file and a series of CMakeLists.txt and Kconfig files. Configuration of the driver could be accomplished with a single CMakeLists.txt file and a single Kconfig file, but using many files allows for the content of the files to be brief. It also establishes a directory build hierarchy that becomes intuitive after some study. This hierarchy will be explained in more detail later.

Some of the most cryptic errors will be generated when the target-specific overlay file that matches the sensor yaml file is either missing or contains errors. This file pair should be studied carefully within existing sensor examples.

Overlay and yaml files

To start, let’s find a pair of overlay and yaml files that we can study. Navigate to zephyr/samples/sensor and open any of the sensor folders. There will be a boards folder containing overlay files. Open one of them. Next navigate to zephyr/dts/bindings/sensor and open the sensor yaml file corresponding the the sensor example.

The board overlay file will typically declare and describe the communication method (i2c,SPI, etc). It will declare relevant pins used and communication bus speeds. The sensor yaml file will declare properties which may or may not need to be initialized by the target-specific overlay file for correct driver operation. The properties declared within the yaml file will have a key called required which may be true or false. If property:required: is false then it does not need to be specified in the overlay file. If it is true then it must be specified in the overlay file or the project will not build.

The sensor_driver_api struct

The most generalized functions for interacting with a sensor in Zephyr are defined by the sensor_driver_api struct. This can be found by searching for it within the sensor.h file found in /zephyr/include/drivers/ folder. These generalized functions include a getter and setter for sensor ‘attributes’, a sample fetching function, a ‘channel’ getter, and a ‘trigger’ setter. These generalized functions allow for ease of interaction with the sensor from the perspective of application code.

The most important methods are the sample_fetch and channel_get functions. The sample_fetch function can be executed from the application simply by passing the sensor device object. Sensor data for all defined channels will be obtained and stored in the ‘data’ portion of the ‘device’ struct. This struct can be found in /zephyr/include/zephyr.h. Sensor data specific to a channel can then be obtained with the channel_get function by passing this getter the device object, the relevant channel #define, and the address of a sensor_value struct into which we will store the sensor data value integer and decimal components.

Adding the out-of-tree sensor driver

Adding a sensor driver to project in the OOT context is most easily achieved by modifying the Example Application provided by the Zephyr Project . Lets add the hx711 load sensor driver to this project.

I mentioned before that it’s helpful to review the pull-request that added the driver to Zephyr in the first place. Here’s the source PR for the hx711. This driver adds source files, configuration files, and also modifies internal zephyr files. Fortunately, there is a method to avoid modifying the internal zephyr files.

  1. Add a folder called hx711 inside of example-application/drivers/sensor/
  2. Add the hx711.c and hx711.h files from the PR inside the hx711 folder
  3. Add the drivers/sensor/hx711/CMakeLists.txt and drivers/sensor/hx711/Kconfig files from the PR to this folder
  4. Add the avia,hx711.yaml file from the PR to the dts/bindings/sensor directory in the example-application project
  5. Add the nrf52dk_nrf52832.overlay file to example-application/app/boards directory
  6. Add samples/sensor/hx711/prj.conf from the PR to example-application/app/
  7. Overwrite the main.c source code from the PR to the main.c file in the example-application/app/src folder.

That’s it for file addition. The remaining steps are to modify the configuration files to locate and build the driver, and finally modify the driver header file to incorporate the changes made to internal zephyr files into the driver directly instead.

West manifest, CMakeList, and Kconfig (oh my!)

Look at the config file in the .west folder in the project root directory. It specifies the path to, and name of the manifest file (west.yml) used by this project. For the Example Application the west manifest is found in the example-application directory. It is also in this directory that the outer-most CMakeLists.txt and Kconfig files are found.

Multiple CMakeLists.txt and Kconfig files exist as pairs beginning at the example-application/ directory level. In this example, only the innermost CMakelists.txt and Kconfig file pairs contain actual build or configuration instructions. Working from the innermost directories back to the example-application/ directory, these file pairs act as sign posts, directing the build tools to find the innermost build and configuration instructions.

Open the Kconfig file in example-application. It has only one line: rsource "drivers/Kconfig". This directs the build system to look in the ‘drivers’ folder for more Kconfig information. The Kconfig file in the ‘drivers’ folder will further direct to the sensor folder. To the Kconfig file in the sensor folder which adds rsource "hx711/Kconfig". This will complete the Kconfig navigation to the hx711 folder where you earlier placed the Kconfig file containing the actual Kconfig build instructions.

The CMakeLists.txt files work in a similar manner directing the build system to inner directories. In the Example Application project the first CMakeLists.txt file that will need modification is in the ‘sensor’ folder. Here you will add add_subdirectory_ifdef(CONFIG_HX711 hx711) following the existing example for the example sensor. This is the configuration file that defines the incorporation of the driver when CONFIG_HX711=y is defined in the proj.conf file. The innermost CMakeLists.txt file that was already added will define the source files for the driver.

Adding attributes and channels

The last item to discuss is the addition of attributes and channels to the driver header file. In the PR, notice that changes were added to two enumerated lists in include/drivers/sensor.h. The second-to-last item in each of these lists is a ‘private start’ item. We can use this to create custom extensions of the enumerated lists for attributes and channels.

Add the following code to the top of the hx711.h file in your project to accommodate the attribute and channel additions in the driver file instead of the internal sensor.h file:

/** @brief Sensor specific attributes of hx711. */
enum hx711_attribute {

	/**
	 * The sensor value returned will be altered by the amount indicated by
	 * slope: final_value = sensor_value * slope.
	 */
	SENSOR_ATTR_SLOPE = SENSOR_ATTR_PRIV_START,

	/**
	 * The sensor gain.
	 */
	SENSOR_ATTR_GAIN,
};

/** @brief Sensor specific channels of hx711. */
enum hx711_channel{

	/** Weight in grams */
	SENSOR_CHAN_WEIGHT = SENSOR_CHAN_PRIV_START,


};

The project can now be built with the following line:

west build -p -b nrf52dk_nrf52832 app/.

I hope this article helped sort out some of the confusion surrounding the Zephyr methodology of adding and interacting with a sensor driver. All drivers must satisfy the three components of build direction/inclusion files, the sensor yaml and target overlay file pair, and the incorporation of the generalized sensor access functions. Seeing all of these components come together in the Out-of-Tree driver context should provide valuable insight into how they function within the drivers that are internal to Zephyr.