Create custom device drivers for Zephyr RTOS project

This article assumes that you have some experience working with the Zephyr RTOS. If not, it would be good to go through the documentation and try out a few samples or you may choose to continue reading to get an overview.

The Zephyr RTOS supports a wide range of boards and devices and the list is growing every day. But, like myself, you might face a situation where your application requires a peripheral or a sensor which is not supported by Zephyr yet. One way to tackle the situation is to write an application-level driver and call its functions directly, but the proper way would be if the driver can be put into Zephyr’s device driver model and use Zephyr’s Sensing Subsystem APIs to communicate with the driver. Luckily, Zephyr supports out-of-tree builds where you can add a device driver for a new peripheral to your project without modifying Zephyr’s source code. This device driver will follow Zephyr’s device driver model although not being part of the Zephyr RTOS’s source code.

Some reading

Before we dig deeper there are a few topics to get familiar with. If you are already familiar with these topics or have experience with developing Zephyr applications, you may ignore this and move on to the next section.

Zephyr RTOS getting started

Zephyr application development

Device tree

Device tree bindings

Sensing subsystem

Example

Let’s start with an example. The source code for this example is available at:

https://github.com/deyro/zephyr_custom_driver

This example is a Zephyr project which includes a custom device driver that uses the sensor driver interface so that an application can call the sensing subsystem APIs to interact with the device driver. The driver produces simulated data. It can be enhanced to become an actual driver for a temperature sensor, accelerometer or any other device/sensor.

Example project walkthrough

The project is structured as shown below.

zephyr_custom_driver
 |- source
     |- boards
	 |    |- native_posix.conf
	 |    |- native_posix.overlay
	 |
	 |- dts
	 |    |- bindings
	 |        |- sensor
	 |            |- rdt,sensor_sim.yaml
	 |
	 |- modules
	 |    |- sensor_sim
	 |    |   |- CMakeLists.txt
	 |    |   |- Kconfig
	 |    |   |- sensor_sim.c
	 |    |   |- sensor_sim.h
	 |    |
	 |    |- zephyr
	 |    |    |- module.yml
	 |    |
	 |    |- CMakeLists.txt
	 |    |- Kconfig
	 |
	 |- src
	 |    |- CMakeLists.txt
	 |    |- Kconfig
	 |    |- main.c
	 |
	 |- CMakeLists.txt
	 |- prj.conf
	 |- sample.yaml

The below points explain each directory and the files. The sequence is based on importance of the directories for this article.

modules directory

Contains our custom device driver; it can contain other device drivers or modules also. This directory will be provided to the CMake generator to include it in the build.

  • sensor_sim directory – contains the source files for our custom device driver. Explanation of the driver source code is provided below.
  • zephyr directory – contains a module.yml file which tells the build system to include the modules directory in the build. The zephyr/module.yml is a mandatory file
  • CMakeLists.txt – adds the list of sub directories to the upper level CMakeLists.txt
  • Kconfig – includes the Kconfig file available in the sub directories.

dts directory

Contains the device tree binding for our custom driver. Each device requires a device tree binding. The device tree bindings are described in a yaml file. The yaml file is basically a declaration of the device tree. It provides information on the contents of the nodes and provides specific semantic information. Please read Devicetree bindings for more information.

description: |
  Sensor simulation driver with dummy data

  Example usage:
  sensor_sim {
    compatible = "rdt,sensor_sim";
    sim-attr = <5>;
  };

compatible: "rdt,sensor_sim"

properties:
  sim-attr:
    type: int
    description: |
      A random integer.

boards directory

Our example uses the native_posix board, but can be built and run on other boards also.

  • native_posix.conf – board specific configurations are set here
  • native_posix.overlay – contains the device tree for our custom sensor driver. The dts includes two instances of the sensor_sim driver
/ {
	sensor_sim1: sensor_sim1 {
		status = "okay";
		compatible = "rdt,sensor_sim";
		sim-attr = <5>;
	};

	sensor_sim2: sensor_sim2 {
		status = "okay";
		compatible = "rdt,sensor_sim";
		sim-attr = <3>;
	};
};

src directory

Contains the application source code and configurations.

  • main.c – application source code
  • Kconfig – application specific configurations if needed
  • CMakeLists.txt – includes the application sources for the generator

CMakeLists.txt

This is the main CMakeLists file. It contains the relevant information to add our modules, dts and boards directory to the Zephyr’s build system. It also adds the application sources for the build.

cmake_minimum_required(VERSION 3.20.0)

# Add our modules directory to Zephyr Modules
list(APPEND ZEPHYR_EXTRA_MODULES ${CMAKE_CURRENT_SOURCE_DIR}/modules)

# Set project, dts and board directories
set(PROJ_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/)
set(DTS_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/dts)
set(BOARD_ROOT ${CMAKE_CURRENT_LIST_DIR})


find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(zephyr_custom_driver)

add_subdirectory(src)

prj.conf

application specific configurations are set here

sample.yaml

This is an information file related to the sample and what is being demonstrated. It can also contain test cases for Twister, Zephyr’s Test Runner. This file is not mandatory.

Code Walkthrough

Below are the important sections of the custom driver sensor_sim.c

1. Define the driver compatible

The driver’s compatible must be defined in the source file. This must match the compatible declared in the device tree binding, i.e. compatible: rdt,sensor_sim. The commas should be changed to underscores.

#define DT_DRV_COMPAT rdt_sensor_sim
2. Declare the config and data structures

Two structures should be declared (a) config, (b) data. These contain the variables needed for the device driver to work

struct sensor_sim_config {

};

struct sensor_sim_data {
	int sensor_attr;
	uint32_t sensor_val;
};
3. Define the init function

Each zephyr driver must have an init function. The init function is automatically called by the kernel during system boot. This function should ideally check the status of the hardware device and return a success or failure code accordingly. If this function returns a failure code, then the driver will not be registered with the Kernel and the application will not be able to call its APIs. Here we are returning 0 as a success code.

static int sensor_sim_init(const struct device *dev)
{
	ARG_UNUSED(dev);
	int ret = 0;

	LOG_INF("sensor_sim_init");

	return ret;
}
4. Define the driver API and its functions

Here we are using the sensor driver API interface, hence we are defining the sample fetch and sample get function pointers. When an application calls the sample fetch and sample get functions through the sensing API, these functions sensor_sim_sample_fetch and sensor_sim_sample_get will get called

static const struct sensor_driver_api sensor_sim_driver_api = {
	.sample_fetch = sensor_sim_sample_fetch,
	.channel_get = sensor_sim_channel_get,
};

If you don’t want to use the sensor driver API interface, you can also define a custom set of APIs to interact with the driver. This is beyond the scope of this article, so let’s proceed with the senor API.

5. Finally add the driver to the Zephyr kernel

Now we can add the above variables to the kernel though the DEVICE_DT_INST_DEFINE macro. This MACRO creates a device object from a devicetree node identifier and sets it up for boot time initialization. Here we need to create 2 variables for the config and data structures. Since there can be multiple instances for the same driver as in our case (see the native_posix.overlay file) we use the MACRO DT_INST_FOREACH_STATUS_OKAY which iterates for each of the nodes and calls DEVICE_DT_DEFINE through DEVICE_DT_INST_DEFINE to create the device object. See the documentation for DEVICE_DT_DEFINE for description on the parameters.

#define DEVICE_INSTANCE(inst) \
\
const static struct sensor_sim_config sensor_sim_##inst##_cfg;\
\
static struct sensor_sim_data sensor_sim_##inst##_drvdata = { \
		.sensor_attr = DT_PROP(DT_DRV_INST(inst),sim_attr), \
}; \
\
DEVICE_DT_INST_DEFINE(inst,						\
		sensor_sim_init,								\
		device_pm_control_nop,							\
		&sensor_sim_##inst##_drvdata,					\
		&sensor_sim_##inst##_cfg,						\
		POST_KERNEL, CONFIG_SENSOR_SIM_INIT_PRIORITY,	\
		&sensor_sim_driver_api);

DT_INST_FOREACH_STATUS_OKAY(DEVICE_INSTANCE);

That’s it. Now our application can call the custom driver.

6. Call the driver functions though Zephyr’s sensing APIs

 The below code is self-explanatory.

const struct device* sim_dev1 = DEVICE_DT_GET(DT_NODELABEL(sensor_sim1));
	if (sim_dev1 == NULL) {
		LOG_ERR("device sensor_sim1 not found");
		return -1;
	}

	const struct device* sim_dev2 = DEVICE_DT_GET(DT_NODELABEL(sensor_sim2));
	if (sim_dev2 == NULL) {
		LOG_ERR("device sensor_sim2 not found");
		return -1;
	}

	struct sensor_value val;

	while (1) {
		sensor_sample_fetch_chan(sim_dev1, SENSOR_CHAN_AMBIENT_TEMP);
		sensor_channel_get(sim_dev1, SENSOR_CHAN_AMBIENT_TEMP, &val);
		LOG_INF("sim1 val = %d\n", val.val1);

		sensor_sample_fetch_chan(sim_dev2, SENSOR_CHAN_AMBIENT_TEMP);
		sensor_channel_get(sim_dev2, SENSOR_CHAN_AMBIENT_TEMP, &val);
		LOG_INF("sim2 val = %d\n", val.val1);

		k_sleep(K_MSEC(1000));
	}

Thanks for reading! Please send your comments or questions.

  1. yqYVGrPiojFRvszt Avatar

    Your comment is awaiting moderation.

    LilkDKdhrNocfy

Leave a Reply

Your email address will not be published. Required fields are marked *