Device driver model

v2.x.x

In order to interact with a hardware peripheral or a system block, we need to use a device driver (or driver for short), which is software that deals with the low-level details of configuring the hardware the way we want. In the nRF Connect SDK, the driver implementation is highly decoupled from its API. This basically means that we are able to switch out the low-level driver implementation without modifying the application because we can use the same generic API.

This decoupling has many benefits, including a high level of portability, as it makes it possible to use the same code on different boards without needing to manually modify the underlying driver implementation.

The application interacts with the hardware through the generic API by obtaining a device pointer for the hardware in question, using the macro DEVICE_DT_GET() or related macros.

Note

It is also possible to get the device pointer through device_get_binding(), though this is no longer recommended.

As opposed to the previous practice of using device_get_binding() to retrieve the device pointer, using DEVICE_DT_GET() has the benefit of failing at build time if the device was not allocated by the driver, for instance, if it does not exist in the devicetree or has the status disabled.

In addition, unlike device_get_binding(), it does not perform a run-time string comparison, which could impact performance in some situations.

The Zephyr device model is responsible for the association between generic APIs and device driver implementations.

Zephyr device model

The macro DEVICE_DT_GET() has the signature shown below: 

To get the device pointer, you need to pass the devicetree node identifier. As mentioned in the Devicetree section, there are many ways to get the node identifier. Two common ways are by the node label through the macro DT_NODELABEL() and by an alias through the macro DT_ALIAS().

Before using the device pointer, it should be checked using device_is_ready(), which has the following signature:

device_is_ready() will check if the device is ready for use, for instance, if it is properly initialized.

The following code snippet will take the devicetree node identifier returned by DT_NODELABEL() and return a pointer to the device object. Then device_is_ready() verifies that the device is ready for use, i.e. in a state so that it can be used with its standard API.

const struct device *dev;
dev = DEVICE_DT_GET(DT_NODELABEL(uart0));

if (!device_is_ready(dev)) {
    return;
}

To use a device driver generic API, you must have a pointer of type const struct device to point to its implementation. You need to do this per peripheral instance. For example, if you have two UART peripherals (&uart0 and &uart1) and you want to use them both, you must have two separate pointers of type const struct device.

In other words, you need to have two different calls to DEVICE_DT_GET().

v1.6.0 – v1.9.1

In order to interact with a hardware peripheral or a system block, we need to use a device driver (or driver for short), which is software that deals with the low-level details of configuring the hardware the way we want. In nRF Connect SDK, the driver implementation is highly decoupled from its API. This basically means we are able to switch out the low-level driver implementation without modifying the application, because we can use the same generic API.

This decoupling has many benefits including a high level of portability as it makes it possible to use the same code on different boards without having to worry about manually modifying the underlying driver implementation.

Most of the time, the application code interacts with the hardware through a generic API, which is defined by the Zephyr RTOS. A binding between the generic API and the actual device driver implementation must be set through a device data structure as shown in the figure below. In the application, this device data structure is then obtained using the function device_get_binding(). The Zephyr device model is responsible for the association between generic APIs and device driver implementations.

Zephyr device model

The function device_get_binding() has the signature shown below: 

device_get_binding() signature

The name of the peripheral to look up its driver implementation must be passed as a string (const char *). The function will look up all the configured drivers and returns a pointer to the device driver implementation if there is a match. Otherwise, the function will return NULL.

The device name (left) maps to the label property of the node in the device tree (right).

device_get_binding("device_name")

For instance, the following device_get_binding() call will look up if there is a registered device driver implementation with the name GPIO_0. If yes, the dev pointer will provide us access to it. Otherwise, dev is set to NULL.

    dev = device_get_binding(“GPIO_0”);
    if (dev == NULL) {
        return;
    }

To use a device driver generic API, you must have a pointer of type const struct device to point to its implementation. You need to do this per peripheral instance. For example, if you have two UART peripherals (UART_0 and UART_1) and you want to use them both, you must have two separate pointers of type const struct device. In other words, you need to have two different calls to device_get_binding().

Note

Remember that any hardware (peripheral, or system block) is eventually accessed as a pointer of type const struct device.

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

Forgot your password?
Enter your email address, and we will send a link to reset your password.