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.
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 herenative_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 codeKconfig
– application specific configurations if neededCMakeLists.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.
Leave a Reply