Device Driver Model - Zephyr Project Documentation
Device Driver Model - Zephyr Project Documentation
Device Driver Model - Zephyr Project Documentation
This is the documentation for the latest (main) development branch of Zephyr. If you are
looking for the documentation of previous releases, use the drop-down menu on the left
and select the desired version.
Introduction
The Zephyr kernel supports a variety of device drivers. Whether a driver is available depends
on the board and the driver.
The Zephyr device model provides a consistent device model for configuring the drivers that
are part of a system. The device model is responsible for initializing all the drivers configured
into the system.
Each type of driver (e.g. UART, SPI, I2C) is supported by a generic type API.
In this model the driver fills in the pointer to the structure containing the function pointers
to its API functions during driver initialization. These structures are placed into the RAM
section in initialization level order.
Subsystem 1
Instance 1 of Device Driver 1
struct device {
const struct config *config;
const void *api;
void * const data;
};
Generic Device
struct config {...};
Type API Driver 1
API Impl 1
API 1
API Impl 3
API 3 Instance 2 of Device Driver 1
struct device {
const struct config *config;
const void *api;
void * const data;
};
Subsystem 2
Device
Instance 1 of Device Driver 2 Driver 2
struct device {
const struct config *config; API Impl 1
const void *api;
void * const data;
Generic }; API Impl 2
Type API struct config {...};
API Impl 3
API 1
API 2
API 3
API Impl 1
Instance 1 of Device Driver 3
struct device { API Impl 2
const struct config *config;
const void *api;
void * const data; API Impl 3
};
Standard Drivers
Device drivers which are present on all supported board configurations are listed below.
Interrupt controller: This device driver is used by the kernel’s interrupt management
subsystem.
Timer: This device driver is used by the kernel’s system clock and hardware clock
subsystem.
Serial communication: This device driver is used by the kernel’s system console
subsystem.
Entropy: This device driver provides a source of entropy numbers for the random
number generator subsystem.
Important
Use the random API functions for random values. Entropy functions should not be
directly used as a random number generator source as some hardware
implementations are designed to be an entropy seed source for random number
generators and will not provide cryptographically secure random number streams.
Synchronous Calls
Zephyr provides a set of device drivers for multiple boards. Each driver should support an
interrupt-based implementation, rather than polling, unless the specific hardware does not
provide any interrupt.
High-level calls accessed through device-specific APIs, such as i2c.h or spi.h , are usually
intended as synchronous. Thus, these calls should be blocking.
Driver APIs
The following APIs for device drivers are provided by device.h . The APIs are intended for
use in device drivers only and should not be used in applications.
DEVICE_DEFINE()
Create device object and related data structures including setting it up for boot-time
initialization.
DEVICE_NAME_GET()
DEVICE_GET()
DEVICE_DECLARE()
Declare a device object. Use this when you need a forward reference to a device that has
not yet been defined.
The device initialization macros populate some data structures at build time which are split
into read-only and runtime-mutable parts. At a high level we have:
struct device {
const char *name;
const void *config;
const void *api;
void * const data;
};
The config member is for read-only configuration data set at build time. For example, base
memory mapped IO addresses, IRQ line numbers, or other fixed physical characteristics of
the device. This is the config pointer passed to DEVICE_DEFINE() and related macros.
The data struct is kept in RAM, and is used by the driver for per-instance runtime
housekeeping. For example, it may contain reference counts, semaphores, scratch buffers,
etc.
The api struct maps generic subsystem APIs to the device-specific implementations in the
driver. It is typically read-only and populated at build time. The next section describes this in
more detail.
typedef int (*subsystem_do_this_t)(const struct device *dev, int foo, int bar);
typedef void (*subsystem_do_that_t)(const struct device *dev, void *baz);
struct subsystem_api {
subsystem_do_this_t do_this;
subsystem_do_that_t do_that;
};
static inline int subsystem_do_this(const struct device *dev, int foo, int bar)
{
struct subsystem_api *api;
static int my_driver_do_this(const struct device *dev, int foo, int bar)
{
...
}
Note
Since pointers to the API functions are referenced in the api struct, they will always be
included in the binary even if unused; gc-sections linker option will always see at least
one reference to them. Providing for link-time size optimizations with driver APIs in most
cases requires that the optional feature be controlled by a Kconfig option.
Some devices can be cast as an instance of a driver subsystem such as GPIO, but provide
additional functionality that cannot be exposed through the standard API. These devices
combine subsystem operations with device-specific APIs, described in a device-specific
header.
#include <zephyr/drivers/subsystem.h>
#ifdef CONFIG_USERSPACE
#include <zephyr/internal/syscall_handler.h>
#include <syscalls/specific_from_user_mrsh.c>
#endif /* CONFIG_USERSPACE */
Applications use the device through both the subsystem and specific APIs.
Note
Public API for device-specific extensions should be prefixed with the compatible for the
device to which it applies. For example, if adding special functions to support the Maxim
DS3231 the identifier fragment specific in the examples above would be
maxim_ds3231 .
Single Driver, Multiple Instances
Some drivers may be instantiated multiple times in a given system. For example there can
be multiple GPIO banks, or multiple UARTS. Each instance of the driver will have a different
config struct and data struct.
Configuring interrupts for multiple drivers instances is a special case. If each instance needs
to configure a different interrupt line, this can be accomplished through the use of per-
instance configuration functions, since the parameters to IRQ_CONNECT() need to be
resolvable at build time.
For example, let’s say we need to configure two instances of my_driver , each with a
different interrupt line. In drivers/subsystem/subsystem_my_driver.h :
struct my_driver_config {
DEVICE_MMIO_ROM;
my_driver_config_irq_t config_func;
};
DEVICE_MMIO_MAP(dev, K_MEM_CACHE_NONE);
config->config_func(dev);
return 0;
}
DEVICE_DECLARE(my_driver_0);
#endif /* CONFIG_MY_DRIVER_0 */
Note the use of DEVICE_DECLARE() to avoid a circular dependency on providing the IRQ
handler argument and the definition of the device itself.
Initialization Levels
Drivers may depend on other drivers being initialized first, or require the use of kernel
services. DEVICE_DEFINE() and related APIs allow the user to specify at what time during
the boot sequence the init function will be executed. Any driver will specify one of four
initialization levels:
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.
Within each initialization level you 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 99;
lower values indicate earlier initialization. The priority level must be a decimal integer literal
without leading zeroes or sign (e.g. 32), or an equivalent symbolic name (e.g. \#define
MY_INIT_PRIO 32 ); symbolic expressions are not permitted (e.g.
CONFIG_KERNEL_INIT_PRIORITY_DEFAULT + 5 ).
Drivers and other system utilities can determine whether startup is still in pre-kernel states
by using the k_is_pre_kernel() function.
System Drivers
In some cases you may just need to run a function at boot. For such cases, the SYS_INIT
can be used. This macro does not take any config or runtime data structures and there isn’t
a way to later get a device pointer by name. The same device policies for initialization level
and priority apply.
Device drivers declared with DEVICE_DEFINE (or any variations of it) and SYS_INIT are
processed at boot time and the corresponding initialization functions are called sequentially
according to their specified level and priority.
Sometimes it’s useful to inspect the final sequence of initialization function call as produced
by the linker. To do that, use the initlevels CMake target, for example west build -t
initlevels .
Error handling
In general, it’s best to use __ASSERT() macros instead of propagating return values unless
the failure is expected to occur during the normal course of operation (such as a storage
device full). Bad parameters, programming errors, consistency checks,
pathological/unrecoverable failures, etc., should be handled by assertions.
When it is appropriate to return error conditions for the caller to check, 0 should be returned
on success and a POSIX errno.h code returned on failure. See
https://github.com/zephyrproject-rtos/zephyr/wiki/Naming-Conventions#return-codes for
details about this.
Memory Mapping
On some systems, the linear address of peripheral memory-mapped I/O (MMIO) regions
cannot be known at build time:
The I/O ranges must be probed at runtime from the bus, such as with PCI express
A memory management unit (MMU) is active, and the physical address of the MMIO
range must be mapped into the page tables at some virtual memory location determined
by the kernel.
These systems must maintain storage for the MMIO range within RAM and establish the
mapping within the driver’s init function. Other systems do not care about this and can use
MMIO physical addresses directly from DTS and do not need any RAM-based storage for it.
For drivers that may need to deal with this situation, a set of APIs under the DEVICE_MMIO
scope are defined, along with a mapping function device_map() .
The simplest case is for drivers which need to maintain one MMIO region. These drivers will
need to use the DEVICE_MMIO_ROM and DEVICE_MMIO_RAM macros in the definitions for their
config_info and driver_data structures, with initialization of the config_info from DTS
using DEVICE_MMIO_ROM_INIT . A call to DEVICE_MMIO_MAP() is made within the init function:
struct my_driver_config {
DEVICE_MMIO_ROM; /* Must be first */
...
}
struct my_driver_dev_data {
DEVICE_MMIO_RAM; /* Must be first */
...
}
Some drivers may have multiple MMIO regions. In addition, some drivers may already be
implementing a form of inheritance which requires some other data to be placed first in the
config_info and driver_data structures.
This can be managed with the DEVICE_MMIO_NAMED variant macros. These require that
DEV_CFG() and DEV_DATA() macros be defined to obtain a properly typed pointer to the
driver’s config_info or dev_data structs. For example:
struct my_driver_config {
...
DEVICE_MMIO_NAMED_ROM(corge);
DEVICE_MMIO_NAMED_ROM(grault);
...
}
struct my_driver_dev_data {
...
DEVICE_MMIO_NAMED_RAM(corge);
DEVICE_MMIO_NAMED_RAM(grault);
...
}
#define DEV_CFG(_dev) \
((const struct my_driver_config *)((_dev)->config))
#define DEV_DATA(_dev) \
((struct my_driver_dev_data *)((_dev)->data))
Some drivers may have multiple MMIO regions defined into the same DT device node using
the reg-names property to differentiate them, for example:
/dts-v1/;
/ {
a-driver@40000000 {
reg = <0x40000000 0x1000>,
<0x40001000 0x1000>;
reg-names = "corge", "grault";
};
};
This can be managed as seen in the previous section but this time using the
DEVICE_MMIO_NAMED_ROM_INIT_BY_NAME macro instead. So the only difference would be in
the driver config struct:
Some drivers or driver-like code may not user Zephyr’s device model, and alternative storage
must be arranged for the MMIO data. An example of this are timer drivers, or interrupt
controller code.
This can be managed with the DEVICE_MMIO_TOPLEVEL set of macros, for example:
DEVICE_MMIO_TOPLEVEL_STATIC(my_regs, DT_DRV_INST(..));
void some_init_code(...)
{
...
DEVICE_MMIO_TOPLEVEL_MAP(my_regs, K_MEM_CACHE_NONE);
...
}
void some_function(...)
...
sys_write32(DEVICE_MMIO_TOPLEVEL_GET(my_regs), 0xDEADBEEF);
...
}
Some drivers may not obtain the MMIO physical address from DTS, such as is the case with
PCI-E. In this case the device_map() function may be used directly:
void some_init_code(...)
{
...
struct pcie_bar mbar;
bool bar_found = pcie_get_mbar(bdf, index, &mbar);
API Reference
group device_model
Device Model.
Since
1.0
Version
1.0.0
Defines
DEVICE_HANDLE_NULL
Return the full name of a device object symbol created by DEVICE_DEFINE(), using the
dev_id provided to DEVICE_DEFINE(). This is the name of the global variable storing
the device structure, not a pointer to the string in the device::name field.
It is meant to be used for declaring extern symbols pointing to device objects before
using the DEVICE_GET macro to get the device object.
This macro is normally only useful within device driver source code. In other
situations, you are probably looking for device_get_binding().
This macro defines a device that is automatically configured by the kernel during
system initialization. This macro should only be used when the device is not being
allocated from a devicetree node. If you are allocating a device from a devicetree
node, use DEVICE_DT_DEFINE() or DEVICE_DT_INST_DEFINE() instead.
Parameters: dev_id – A unique token which is used in the name of the global
device structure as a C identifier.
name – A string name for the device, which will be stored in
device::name. This name can be used to look up the device with
device_get_binding(). This must be less than
Z_DEVICE_MAX_NAME_LEN characters (including terminating
NULL ) in order to be looked up from user mode.
init_fn – Pointer to the device’s initialization function, which will
be run by the kernel during system initialization. Can be NULL .
pm – Pointer to the device’s power management resources, a
pm_device, which will be stored in device::pm field. Use NULL if
the device does not use PM.
data – Pointer to the device’s private mutable data, which will be
stored in device::data.
config – Pointer to the device’s private constant data, which will
be stored in device::config.
level – The device’s initialization level (PRE_KERNEL_1,
PRE_KERNEL_2 or POST_KERNEL).
prio – The device’s priority within its initialization level. See
SYS_INIT() for details.
api – Pointer to the device’s API structure. Can be NULL .
DEVICE_DT_NAME(node_id)
Create a device object from a devicetree node identifier and set it up for boot time
initialization.
This macro defines a device that is automatically configured by the kernel during
system initialization. The global device object’s name as a C identifier is derived from
the node’s dependency ordinal. device::name is set to DEVICE_DT_NAME(node_id) .
The device is declared with extern visibility, so a pointer to a global device object can
be obtained with DEVICE_DT_GET(node_id) from any source file that includes
<zephyr/device.h> . Before using the pointer, the referenced object should be
checked using device_is_ready().
DEVICE_DT_INST_DEFINE(inst, ...)
DEVICE_DT_NAME_GET(node_id)
This macro is normally only useful within device driver source code. In other
situations, you are probably looking for DEVICE_DT_GET().
DEVICE_DT_GET(node_id)
Returns a pointer to a device object created from a devicetree node, if any device was
allocated by a driver.
If no such device was allocated, this will fail at linker time. If you get an error that
looks like undefined reference to __device_dts_ord_<N> , that is what happened.
Check to make sure your device driver is being compiled, usually by enabling the
Kconfig options it requires.
DEVICE_DT_INST_GET(inst)
DEVICE_DT_GET_ANY(compat)
If an enabled devicetree node has the given compatible and a device object was
created from it, this returns a pointer to that device.
If this returns non-NULL, the device must be checked for readiness before use, e.g.
with device_is_ready().
DEVICE_DT_GET_ONE(compat)
If an enabled devicetree node has the given compatible and a device object was
created from it, this returns a pointer to that device.
If there are no such devices, this will fail at compile time.
If this returns non-NULL, the device must be checked for readiness before use, e.g.
with device_is_ready().
DEVICE_DT_GET_OR_NULL(node_id)
If the node identifier refers to a node with status okay , this returns
DEVICE_DT_GET(node_id) . Otherwise, it returns NULL .
DEVICE_GET(dev_id)
Return the address of a device object created by DEVICE_DEFINE(), using the dev_id
provided to DEVICE_DEFINE().
DEVICE_DECLARE(dev_id)
This macro can be used at the top-level to declare a device, such that DEVICE_GET()
may be used before the full declaration in DEVICE_DEFINE().
This is often useful when configuring interrupts statically in a device’s init or per-
instance config function, as the init function itself is required by DEVICE_DEFINE() and
use of DEVICE_GET() inside it creates a circular dependency.
DEVICE_INIT_DT_GET(node_id)
DEVICE_INIT_GET(dev_id)
Every device has an associated handle. You can get a pointer to a device from its
handle and vice versa, but the handle uses less space than a pointer. The device.h API
mainly uses handles to store lists of multiple devices in a compact way.
The extreme values and zero have special significance. Negative values identify
functionality that does not correspond to a Zephyr device, such as the system clock or
a SYS_INIT() function.
See also
device_handle_get()
See also
device_from_handle()
Such a function may be used in API that identifies a set of devices and provides a
visitor API supporting caller-specific interaction with each device in the set.
See also
device_required_foreach()
See also
device_supported_foreach()
Functions
This function returns a pointer to an array of device handles. The length of the array is
stored in the count parameter.
The array contains a handle for each device that dev requires directly, as determined
from the devicetree. This does not include transitive dependencies; you must
recursively determine those.
This function returns a pointer to an array of device handles. The length of the array is
stored in the count parameter.
The array contains a handle for each device that dev manually injected as a
dependency, via providing extra arguments to Z_DEVICE_DEFINE. This does not
include transitive dependencies; you must recursively determine those.
Parameters: dev – the device for which injected dependencies are desired.
count – pointer to where this function should store the length of
the returned array. No value is stored if the call returns a null
pointer. The value may be set to zero if the device has no
devicetree dependencies.
Returns: a pointer to a sequence of *count device handles, or a null pointer
if dev does not have any dependency data.
The array contains a handle for each device that dev “supports” — that is,
devices that require dev directly — as determined from the devicetree. This
does not include transitive dependencies; you must recursively determine those.
Zephyr maintains information about which devices are directly required by another
device; for example an I2C-based sensor driver will require an I2C controller for
communication. Required devices can derive from statically-defined devicetree
relationships or dependencies registered at runtime.
This API supports operating on the set of required devices. Example uses include
making sure required devices are ready before the requiring device is used, and
releasing them when the requiring device is no longer needed.
If the visitor_cb function returns a negative value iteration is halted, and the
returned value from the visitor is returned from this function.
Note
Parameters: dev – a device of interest. The devices that this device depends
on will be used as the set of devices to visit. This parameter must
not be null.
visitor_cb – the function that should be invoked on each device
in the dependency set. This parameter must not be null.
context – state that is passed through to the visitor function.
This parameter may be null if visitor_cb tolerates a null
context .
Returns: The number of devices that were visited if all visits succeed, or the
negative value returned from the first visit that did not succeed.
Zephyr maintains information about which devices are directly supported by another
device; for example an I2C controller will support an I2C-based sensor driver.
Supported devices can derive from statically-defined devicetree relationships.
This API supports operating on the set of supported devices. Example uses include
iterating over the devices connected to a regulator when it is powered on.
If the visitor_cb function returns a negative value iteration is halted, and the
returned value from the visitor is returned from this function.
Note
Parameters: dev – a device of interest. The devices that this device supports
will be used as the set of devices to visit. This parameter must
not be null.
visitor_cb – the function that should be invoked on each device
in the support set. This parameter must not be null.
context – state that is passed through to the visitor function.
This parameter may be null if visitor_cb tolerates a null
context .
Returns: The number of devices that were visited if all visits succeed, or the
negative value returned from the first visit that did not succeed.
This function iterates through the devices on the system. If a device with the given
name field is found, and that device initialized successfully at boot time, this function
returns a pointer to the device.
This function also returns NULL when a device is found, but it failed to initialize
successfully at boot time. (To troubleshoot this case, set a breakpoint on your device
driver’s initialization function.)
This can be used with device pointers captured from DEVICE_DT_GET(), which does not
include the readiness checks of device_get_binding(). At minimum this means that the
device has been successfully initialized.
#include <device.h>
Runtime device dynamic structure (in RAM) per driver instance.
Public Members
uint8_t init_res
Device initialization functions return a negative errno code if they fail. In Zephyr,
errno values do not exceed 255, so we can store the positive result value in a
uint8_t type.
bool initialized
struct device
#include <device.h>
Runtime device structure (in ROM) per driver instance.
Public Members
This encodes a sequence of sets of device handles that have some relationship to
this node. The individual sets are extracted with dedicated API, such as
device_required_handles_get(). Only available if CONFIG_DEVICE_DEPS is enabled.
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: