Zephyr Device Drivers and Instances Explained

The article on Zephyr device drivers and their implementation explains that device drivers are general and reusable code modules. For the same type of device, regardless of how many instances there are, the driver code is the same. For example, on the ESP32, there are multiple ADC devices, and these ADCs are identical in hardware, so the control method is also the same. Using the same driver logic to operate on different ADC register groups can achieve control over different ADC hardware under the ESP32. This process is referred to as the multi-instance capability of Zephyr device drivers—”one driver, multiple instances.” To make a somewhat inappropriate analogy, the driver implementation is like a class, while the driver instances are like objects.

Basic Concepts

The implementation of device drivers can be understood as a static concept, which is a piece of code that can drive and control hardware devices. The device driver instance can be understood as a dynamic concept, which refers to which devices will operate and be controlled by the device driver at runtime in Zephyr. Based on this, there are two points to note in Zephyr:

  1. The number of driver instances depends on the Device Tree configuration.
  2. The type of driver compiled depends on the Device Tree configuration.
  3. Whether the driver will be compiled into the Zephyr image depends on the Kconfig configuration.

In other words, regardless of how many hardware devices are on a SoC, the driver instances at runtime in Zephyr are determined by the Device Tree. Regardless of how many devices are in the Device Tree, the construction of device drivers is controlled by Kconfig.

The previous description might still be somewhat vague; the following diagram can fully illustrate the relationship between drivers and instances:

Zephyr Device Drivers and Instances Explained

The RT1062 has 4 I2C devices in hardware, but there is only one device driver file<span>i2c_mcux_lpi2c.c</span> for its driver implementation. When all configurations are enabled using the Device Tree, there will be 4 I2C driver instances at runtime.

The ESP32-C3 has 2 I2C devices in hardware, which also corresponds to one device driver file<span>i2c_esp32.c</span> for its driver implementation. When only one I2C is enabled using the Device Tree configuration, there will only be 1 I2C driver instance at runtime.

Based on which SoC the Device Tree corresponds to, either<span>i2c_mcux_lpi2c.c</span> or<span>i2c_esp32.c</span> will be chosen for compilation and linking.

Finally, the Kconfig configuration will determine whether the system actually uses I2C, which decides whether to compile the I2C driver.

Driver Instances

In Zephyr, the concepts of driver implementation and driver instances are classified. The driver implementation abstracts the logic flow of controlling the driver, while the driver instance abstracts the actual data management of a driver device. Therefore, in Zephyr, a piece of management data for a driver can be understood as a driver instance.

Taking the ADC of ESP32 as an example, this section illustrates how Zephyr creates management data for driver instances.

In<span>adc_esp32.c</span>, you can see the macro for initializing the ADC instance.

#define ESP32_ADC_INIT(inst)       
          
 static const struct adc_esp32_conf adc_esp32_conf_##inst = {  
  .unit = DT_PROP(DT_DRV_INST(inst), unit) - 1,   
  .channel_count = DT_PROP(DT_DRV_INST(inst), channel_count), 
  ADC_ESP32_CONF_GPIO_PORT_INIT                                   
  ADC_ESP32_CONF_DMA_INIT(inst)                                   
 };         
          
 static struct adc_esp32_data adc_esp32_data_##inst = {   
 };         
          
DEVICE_DT_INST_DEFINE(inst, &amp;adc_esp32_init, NULL,    
  &amp;adc_esp32_data_##inst,      
  &amp;adc_esp32_conf_##inst,      
  POST_KERNEL,       
  CONFIG_ADC_INIT_PRIORITY,     
  &amp;api_esp32_driver_api);

Driver Management Data as Global Variables

A driver instance consists of two parts of management data:

  • Configuration Data<span>struct adc_esp32_conf</span>
    • Read hardware configuration from the Device Tree
    • Will not change at runtime
  • Runtime Data<span>struct adc_esp32_data</span>
    • The driver code relies on this structure to save process and control data at runtime
    • Will be modified at runtime

It is important to note that while different hardware drivers have these two parts of data, their configuration data structures will vary based on hardware configuration, while the runtime data structures will vary based on driver implementation and do not have a fixed form, entirely defined by the driver implementer.

A Driver Instance

Remember the mention of the Zephyr device model and driver API that states: “Device drivers are accessed through<span>struct device *</span>? Here, a<span>struct device *</span> is a pointer to a driver instance, which is how the device driver API performs operations on the device driver instance<span>struct device*</span>. Therefore, it can be understood that<span>struct device</span> represents an instance in the Zephyr device driver model.

In the device driver model, the following two macros can be used to create instances, establishing global variables of<span>struct device</span>.

#define DEVICE_DT_DEFINE(node_id, init_fn, pm, data, config, level, prio, api, ...)  
#define DEVICE_DT_INST_DEFINE(inst, ...) 

From an implementation perspective,<span>DEVICE_DT_DEFINE</span> is just a wrapper around<span>DEVICE_DT_DEFINE</span>, converting the instance number read from the device count<span>inst</span> into<span>node_id</span>, making it convenient to create driver instances by instance number.

<span>DEVICE_DT_DEFINE</span> primarily creates global variables of<span>struct device</span> and initializes them. Analyzing the code snippet from<span>ESP32_ADC_INIT</span>:

DEVICE_DT_INST_DEFINE(inst, &amp;adc_esp32_init, NULL,    
  &amp;adc_esp32_data_##inst,      
  &amp;adc_esp32_conf_##inst,      
  POST_KERNEL,       
  CONFIG_ADC_INIT_PRIORITY,     
  &amp;api_esp32_driver_api);
  • The init_fn initialization function uses<span>adc_esp32_init</span>
  • pm is the power management function NULL; if the driver needs to implement power management, it needs to prepare<span>struct pm_device</span>
  • data runtime management data<span>adc_esp32_data_##inst</span>,<span>ESP32_ADC_INIT</span> is already prepared as<span>struct adc_esp32_data</span> type
  • config configuration management data<span>adc_esp32_conf_##inst</span>,<span>ESP32_ADC_INIT</span> is already prepared as<span>struct adc_esp32_conf</span> type
  • level the initialization level of this driver<span>POST_KERNEL</span>
  • prio the priority of the initialization of this driver instance<span>CONFIG_ADC_INIT_PRIORITY</span>
  • api the driver API,<span>api_esp32_driver_api</span>, is defined in the driver model; for ADC, it is<span>struct adc_driver_api</span> type

<span>DEVICE_DT_INST_DEFINE</span> will also prepare a<span>struct device_state</span> for managing device status, which stores the initialization results of the device instance and marks whether it has been initialized.

These prepared global variables are used to create and initialize a global variable of<span>struct device</span>.

struct device {
 constchar *name;
 constvoid *config;
 constvoid *api;
 struct device_state *state;
 void *data;

 #if defined(CONFIG_PM_DEVICE) || defined(__DOXYGEN__)
 union {
 struct pm_device_base *pm_base;
 struct pm_device *pm;
 struct pm_device_isr *pm_isr;
 };
 #endif
};

<span>struct device</span> initializes each member with the previously prepared global data, with name read from the device tree. From<span>struct device</span>, it can be seen that its managed data are:

  • name comes from the device tree and is unrelated to the driver implementation
  • config driver instance configuration data, abstracted as void*, without binding data structure relationships; every driver instance has this member
  • api driver implementation API, abstracted as void*, without binding data structure relationships; every driver implementation has this member
  • data driver instance management data, abstracted as void*, without binding data structure relationships; every driver instance has this member
  • state initialization status of the driver instance, the same for all instances

In summary, the fundamental elements of a driver instance are:

  • Driver instance configuration data, global variable, content determined at build time, unchangeable
  • Driver instance management data, global variable, content modified at runtime
  • Driver implementation API pointer, global variable, content determined at build time, unchangeable
  • Device instance status, global variable, content determined at runtime initialization
  • Device instance management structure<span>struct device</span>, content determined at build time, pointing to the previous four parts

Taking the ESP32 ADC as an example, its instance is ultimately reflected in the code as:

//Driver implementation API pointer
staticconststruct adc_driver_api api_esp32_driver_api = {
 .channel_setup = adc_esp32_channel_setup,
 .read = adc_esp32_read,

 .ref_internal = (1100),
};

//Driver instance configuration data
staticconststruct adc_esp32_conf adc_esp32_conf_0 = {
 .unit = 1 - 1,
 .channel_count = 5,
}; 

//Driver instance management data
staticstruct adc_esp32_data adc_esp32_data_0 = { }; 

//Driver instance status
static _attribute__((__aligned__(__alignof(struct device_state)))) struct device_state __devstate_dts_ord_54 __attribute__((__section__(".z_devstate")));

//Driver instance management structure
struct device __device_dts_ord_54 __attribute__((section("." "_device" ".

Leave a Comment