When implementing a device driver, one needs to consider several key components. Most of the implementation details of the device driver are surrounded around the driver data structure.
Zephyr devices are represented by struct device
, defined in <zephyr/device.h>
, which holds a series of references to resources defined by drivers or defined internally by the device definition macros.
struct device
is shown below in a simplified code snippet.
struct device {
const char *name;
const void *config;
void * const data;
const void *api;
...
};
Cname
– Device name (unique), which corresponds to the label
property for devicetree devices.config
– Reference to the read-only configuration set at compile time, typically used to store devicetree properties.data
– Reference to device data that needs to be modified at runtime, e.g. counter, state, etc.api
– Reference to the device API operations.Devices are defined using
DEVICE_DEFINE()
for non-devicetree devices.DEVICE_DT_DEFINE()
or DEVICE_DT_INST_DEFINE()
for devicetree-based devices.Devices are automatically initialized at boot time, before main
is reached, in a specific order determined by a level, and manually defined priorities.
Let’s take a closer look at the definition of a devicetree-based device, using the instance-based DEVICE_DT_INST_DEFINE()
, which is more commonly used for device drivers since we typically want to write drivers that are multi-instance capable.
First it is important to note that all instance-based macros use an instance of a DT_DRV_COMPAT
compatible, so this will need to be defined before calling the instance-based macros. This is done by setting DT_DRV_COMPAT
to the lowercase-and-underscores version of the compatible that the device driver supports. If our compatible is "vendor,mysensor"
, we will define DT_DRV_COMPAT
to vendor,mysensor
in our driver C file. This tells DT_INST
based macros which compatible to use.
In the device driver, the instance-based device definition will typically look something like this
#define DT_DRV_COMPAT vendor_mysensor
#define MYSENSOR_DEFINE(inst) \
static struct mysensor_data data_##inst; \
static const struct mysensor_config config_##inst \
= { .spi = SPI_DT_SPEC_INST_GET(inst, SPIOP, 0),}; \
\
DEVICE_DT_INST_DEFINE(inst, \
mysensor_init, \
NULL, \
&data_##inst, \
&config_##inst, \
POST_KERNEL, \
CONFIG_SENSOR_INIT_PRIORITY, \
&mysensor_api);
DT_INST_FOREACH_STATUS_OKAY(MYSENSOR_DEFINE)
CSo first we define the macro MYSENSOR_DEFINE
that will create an instance of mysensor_data data
, mysensor_config config
, and call DEVICE_DT_INST_DEFINE
with the init()
, data
, config
and api
defined in the driver.
Then in the last line, DT_INST_FOREACH_STATUS_OKAY
expands MYSENSOR_DEFINE
for each device in the devicetree with the compatible defined by DT_DRV_COMPAT
and status “okay
“.
A typical device driver in Zephyr consists of several key components.
Configuration: One of the members of the driver data structure, const void * config
, stores the configuration data of the device and is read-only. This configuration data can be different for different devices and is filled with information by the device driver implementation at the time of driver initialization.
Initialization: During initialization, the driver sets up necessary resources (like power, security, and initial states of pins and memory). The device driver initialization has to run before the application’s main thread would run, and Zephyr offers SYS_INIT()
to plug in such initialization calls at the time of the boot.
Interrupt Handlers: For devices that generate interrupts (e.g., GPIO pins, peripherals, timers), interrupt handlers are implemented inside device drivers to handle these events asynchronously and inform the application about the changes that happen through the driver API.
Power Management: Some drivers support power management features to optimize energy consumption when dealing with battery-powered devices. If the implemented device driver needs or wants to support a plug-in to the Zephyr system power management, then the device driver needs to implement and expose the implementation of struct
pm_device
and pass it to the DEVICE_DT_DEFINE()
macro at the time of device definition. NULL
needs to be passed to DEVICE_DEFINE
if the driver does not wish to implement the power management to the device.
APIs: The driver exposes a set of APIs that allow application code to interact with the hardware device. This API inside the device driver is made as a structure of function pointers. The address of this structure is passed to the api
parameter of the driver data structure in the DEVICE_DT_DEFINE()
macro at the time of the definition of the device in the device driver implementation. These APIs include functions for reading sensor data, controlling actuators, configuring registers, etc.