Feedback
Feedback

If you are having issues with the exercises, please create a ticket on DevZone: devzone.nordicsemi.com
Click or drag files to this area to upload. You can upload up to 2 files.

Device driver implementation

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

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;
      ...
};
C
  • name – 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.

Device definition

Devices are defined using

Devices are automatically initialized at boot time, before main is reached, in a specific order determined by a level, and manually defined priorities.

Key components of a device definition

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.

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)
C

DT_DRV_COMPAT

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.

MYSENSOR_DEFINE(inst)

Let`s look MYSENSOR_DEFINE(inst) macro. It requires inst parameter, which is the index number of the node in a group of nodes compatible with the binding defined by DT_DRV_COMPAT. Thanks to instance (inst) we can iterate through all the nodes compatible with driver binding and use them without creating the list with these nodes in the driver.

First operation on MYSENSOR_DEFINE macro is defining driver data structure for a given instance. Next we define configuration structure with SPI for this instance, we use instance number to get SPI bus from devicetree using SPI_DT_SPEC_INST_GET macro helper.

At the end of MYSENSOR_DEFINE macro device object is created using DEVICE_DT_INST_DEFINE.

DEVICE_DT_INST_DEFINE

DEVICE_DT_INST_DEFINE(inst, ...) uses inst to create a device object. It is done by getting node id DT_DRV_INST(inst) inside DEVICE_DT_INST_DEFINE macro and passing it with rest of the parameters into DEVICE_DT_DEFINE macro which creates object directly. We can look at DEVICE_DT_INST_DEFINE definition.

Lets try to look again at device object definition:

            
	DEVICE_DT_INST_DEFINE(inst,				  \
				mysensor_init,								\
				NULL,													\
				&data_##inst,				          \
				&config_##inst,			          \
				POST_KERNEL, 									\
				CONFIG_SENSOR_INIT_PRIORITY, 	\
				&mysensor_api);


C
  • inst as inst - we need to have an instance number to create a device object. We will get all instance numbers using DT_INST_FOREACH_STATUS_OKAY.
  • mysensor_init as init_fn – driver initialization function. It sets up necessary resources (like power, security, and initial states of pins and memory). Implementation of this function is in the device driver.
  • NULL as pm – some drivers support power management features to optimize energy consumption when dealing with battery-powered devices. Then driver needs to implement callback responsible for power management actions. See device power management for details.In Exercise 2 of this lesson we will also learn how to create driver using power management. If the driver does not use power management NULL can be used as parameter.
  • &data_##inst as data – instance of device`s data structure (structure definition: mysensor_data ). Using inst in the template ( created by MYSENSOR_DEFINE macro ) makes sure that every device object has its own instance of this structure.
  • &config_##inst as 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. Using inst in the template ( created by MYSENSOR_DEFINE macro ) makes sure that every device object has its own instance of this config structure ( structure definition: mysensor_config )
  • POST_KERNEL as level –  allow the driver to specify at what time during the boot sequence the init function will be executed.

PRE_KERNEL_1: Used for devices that have no dependencies, such as those that rely solely on hardware present in the processor/SOC. These devices cannot use any kernel services during configuration, since the kernel services are not yet available. The interrupt subsystem will be configured however so it’s OK to set up interrupts. Init functions at this level run on the interrupt stack.

PRE_KERNEL_2: Used for devices that rely on the initialization of devices initialized as part of the PRE_KERNEL_1 level. These devices cannot use any kernel services during configuration since the kernel services are not yet available. Init functions at this level run on the interrupt stack.

POST_KERNEL: Used for devices that require kernel services during configuration. Init functions at this level run in context of the kernel main task.

  • CONFIG_SENSOR_INIT_PRIORITY as prio -Within each initialization level the driver may specify a priority level, relative to other devices in the same initialization level. The priority level is specified as an integer value in the range 0 to 999; lower values indicate earlier initialization. CONFIG_SENSOR_INIT_PRIORITY is default priority for the sensor subsystem.
  • &mysensor_api as api – Driver API structure. The driver exposes an API 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 driver defines the structure and contains all necessary implementations of the functions in the API.

DT_INST_FOREACH_STATUS_OKAY

Then in the last line, DT_INST_FOREACH_STATUS_OKAY gets instance from each devicetree node compatible with DT_DRV_COMPAT and status “okay“. Next it uses MYSENSOR_DEFINE macro to create device object for every instance.

Register an account
Already have an account? Log in
(All fields are required unless specified optional)

  • 8 or more characters
  • Upper and lower case letters
  • At least one number or special character

Forgot your password?
Enter the email associated with your account, and we will send you a link to reset your password.