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.

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

So 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“.

Key components of a device driver

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.

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.