Maker.io main logo

Introduction to Zephyr Part 6: Device Driver Development

133

2025-04-10 | By ShawnHymel

Microcontrollers ESP32

Previously, we looked at how bindings files are used as interfaces for Devicetree nodes and how they connect the Devicetree to the appropriate driver code. In this post, we will create our device driver that wraps a button.

A device driver is a specialized piece of software that acts as a bridge between the operating system and a hardware component. It translates the operating system’s generic commands into the hardware-specific instructions needed to operate the device. Without a device driver, the OS wouldn’t know how to interact with or control the hardware, making it inaccessible to applications. This abstraction allows developers and users to work with hardware at a higher level without needing to know its internal implementation details.

Zephyr uses a device driver model similar to Linux–it relies on the Devicetree to act as a glue between driver layers to allow for truly abstracted code, which means that application code can rely on the device drivers to handle the lower-level functionality.

All code for this Introduction to Zephyr series can be found here: https://github.com/ShawnHymel/introduction-to-zephyr

Hardware Connections

For this demonstration, we will be using two pushbuttons connected to pins 4 and 5 on the ESP32-S3-DevKitC. Here is a Fritzing diagram showing all of the connections we will use throughout this series:

Introduction to Zephyr Part 6: Device Driver Development

Create the Device Driver Module

To save on space in this blog post, the driver module code can be found here: https://github.com/ShawnHymel/introduction-to-zephyr/tree/main/workspace/modules/button

Directory Structure

Note that the module is “out-of-tree” (meaning it is not in the main Zephyr source directory), and it is separate from our application directory. The directory structure of the driver module is important, and we will provide a summary of the files.

Copy Code
workspace/modules/button/
├── drivers
│   ├── button
│   │   ├── CMakeLists.txt
│   │   ├── Kconfig
│   │   ├── button.c
│   │   └── button.h
│   ├── CMakeLists.txt
│   └── Kconfig
├── dts
│   └── bindings
│       └── input
│           └── custom,button.yaml
├── zephyr
│   └── module.yaml
├── CMakeLists.txt
└── Kconfig

Files

Rather than paste the file contents here, we will link to the code and give a brief description of what each file does. Feel free to copy the contents to your own development environment to build the driver.

workspace/modules/button/drivers/button/CMakeLists.txt

Specifies how the button driver source files are built and linked. It uses Zephyr-specific CMake functions to declare the library and add source/header files to the target.

workspace/modules/button/drivers/button/Kconfig

Defines Kconfig options for the button driver, allowing users to enable or disable the driver and configure its features at build time.

workspace/modules/button/drivers/button/button.c

Contains the primary implementation of the button driver logic. It handles button initialization, reading button states, and returning state information to the rest of the system.

workspace/modules/button/drivers/button/button.h

Provides the public interface for the button driver, including function prototypes and data structures. Other parts of the code include this header to interact with the driver’s functionality.

workspace/modules/button/drivers/CMakeLists.txt

Uses a Zephyr-specific function to call the button/CMakeLists.txt file only if the CONFIG_CUSTOM_BUTTON Kconfig symbol is defined.

workspace/modules/button/drivers/Kconfig

Sources the button/Kconfig file.

workspace/modules/button/dts/bindings/input/custom,button.yaml

Describes the hardware properties for the custom button in the Device Tree. This file tells the Zephyr build system how to interpret button-related Devicetree nodes and map them to the driver. It also acts as an interface that defines how the user/developer should work with the Devicetree nodes and properties.

workspace/modules/button/zephyr/module.yaml

Provides metadata and configuration that tells Zephyr how to treat this directory as a module. It defines module-level information like build settings, dependencies, and how this code integrates into Zephyr.

workspace/modules/button/CMakeLists.txt

Serves as the top-level CMake build file for the button module. It includes or references subdirectories (such as drivers) and sets up module-wide build rules.

workspace/modules/button/Kconfig

Sources drivers/Kconfig.

Discussion

Let’s take a moment to talk about the macro magic in the button.c driver source file. At the top of the file, you’ll find this line:

Copy Code
#define DT_DRV_COMPAT custom_button

The Zephyr build system looks for this line at the top of the driver source files when it tries to match the appropriate “compatible” string in the Devicetree and bindings file. Because macro symbols cannot have dashes or commas, we change the string to “custom_button,” and Zephyr knows it should match to “custom,button” in our bindings file (and Devicetree node). This is how Zephyr knows which driver code to use.

The bulk of the file provides functions for working with the button, but at the end, you will find the following:

Copy Code
// Define the public API functions for the driver
static const struct button_api button_api_funcs = {
    .get = button_state_get,
};

// Expansion macro to define driver instances
#define BUTTON_DEFINE(inst)                                                 \
                                                                            \
    /* Create an instance of the config struct, populate with DT values */  \
    static const struct button_config button_config_##inst = {              \
        .btn = GPIO_DT_SPEC_GET(                                            \
            DT_PHANDLE(DT_INST(inst, custom_button), pin), gpios),          \
        .id = inst                                                          \
    };                                                                      \
                                                                            \
    /* Create a "device" instance from a Devicetree node identifier and */  \
    /* registers the init function to run during boot. */                   \
    DEVICE_DT_INST_DEFINE(inst,                                             \
                          button_init,                                      \
                          NULL,                                             \
                          NULL,                                             \
                          &button_config_##inst,                            \
                          POST_KERNEL,                                      \
                          CONFIG_GPIO_INIT_PRIORITY,                        \
                          &button_api_funcs);                               \

// The Devicetree build process calls this to create an instance of structs for

// each device (button) defined in the Devicetree

DT_INST_FOREACH_STATUS_OKAY(BUTTON_DEFINE)

This section uses a set of Zephyr macros (DT_INST_FOREACH_STATUS_OKAY and BUTTON_DEFINE) to automatically generate a separate driver instance for each matching node in the Device Tree. In other words, if you define multiple custom,button nodes in your Devicetree, these macros will expand once per node, creating a unique config struct (with its GPIO pin configuration and ID) and registering a device instance (via DEVICE_DT_INST_DEFINE) for each button in the system.

When Zephyr’s build process runs, it looks through all Devicetree nodes that match DT_DRV_COMPAT = custom_button and have status = "okay". For each one, it calls the BUTTON_DEFINE(inst) macro, which compiles into code that sets up a static button_config struct and then declares a new device object with the provided initialization function. This ensures you can have multiple buttons, each recognized and initialized by the same driver logic, but with their own configuration data (pin assignments, IDs, etc.).

Create the Example Application

To save on space in this blog post, the application code can be found here: https://github.com/ShawnHymel/introduction-to-zephyr/tree/main/workspace/apps/06_demo_device_driver.

Directory Structure

We will create the usual boilerplate file/folder structure that we have been using for previous projects:

Copy Code
workspace/modules/button/‎
├── boards
│   └── esp32s3_devkitc.overlay‎
├── src
│   └── main.c‎
├── CMakeLists.txt
└── prj.conf‎

Files

Here are summaries for each of the application files:

workspace/apps/06_demo_device_driver/boards/esp32s3_devkitc.overlay

Defines board-specific hardware configuration overrides for the ESP32-S3 DevKitC, ensuring the correct pins and properties are associated with the custom button driver in Zephyr’s Device Tree.

workspace/apps/06_demo_device_driver/src/main.c

Implements the main application that initializes and exercises the button driver, reading button state and providing feedback to confirm correct operation.

workspace/apps/06_demo_device_driver/CMakeLists.txt

Serves as the build script for the test application, linking the source code and the button driver module, and controlling how the final binary is generated.

workspace/apps/06_demo_device_driver/prj.conf

Holds configuration options for the Zephyr project, enabling the button driver and other required settings to properly test button functionality.

Build and Flash

Build the demo application with:

Copy Code
cd apps/06_demo_device_driver
west build -p always -b esp32s3_devkitc/esp32s3/procpu -- -DDTC_OVERLAY_FILE=boards/esp32s3_devkitc.overlay

Once done, activate the virtual environment on your host computer:

Linux/macOS:

Copy Code
source venv/bin/activate

Windows:

Copy Code
venv\Scripts\activate

Flash the binary to your board. Change <PORT>  to the serial port for your ESP32 board (e.g. /dev/ttyS0 for Linux, /dev/tty.usbserial-1420 for macOS, COM7 for Windows).

Copy Code
python -m esptool --port "<PORT>" --chip auto --baud 921600 --before default_reset --after hard_reset write_flash -u --flash_mode keep --flash_freq 40m --flash_size detect 0x0 workspace/apps/01_blink/build/zephyr/zephyr.bin

Open a serial port for debugging. Change <PORT>  to the serial port for your ESP32 board.

Copy Code
python -m serial.tools.miniterm "<PORT>" 115200‎

Press the buttons on your breadboard, and you should see the 0s change to 1s for each of the buttons.

Introduction to Zephyr Part 6: Device Driver Development

Challenge

Your challenge is to work through the workshop given here: https://github.com/ShawnHymel/workshop-zephyr-device-driver

The workshop walks you through the process of creating an I2C driver that talks to the MCP9808 temperature sensor on our breadboard.

Going Further

Device drivers in Zephyr provide an efficient, modular way to interface with hardware in a consistent and maintainable manner. By leveraging Devicetree and the driver model, you can create flexible software components that scale easily across multiple platforms. This approach ensures that hardware interactions remain reliable, standardized, and straightforward to configure.

To learn more about device driver development in Zephyr, I recommend the following content:

制造商零件编号 ESP32-S3-DEVKITC-1-N32R8V
ESP32-S3-WROOM-2-N32R8V DEV BRD
Espressif Systems
制造商零件编号 3533
GRAPHIC DISPLAY TFT RGB 0.96"
Adafruit Industries LLC
制造商零件编号 1782
MCP9808 TEMP I2C BREAKOUT BRD
Adafruit Industries LLC
制造商零件编号 3386P-1-103TLF
TRIMMER 10K OHM 0.5W PC PIN TOP
Bourns Inc.
制造商零件编号 1825910-6
SWITCH TACTILE SPST-NO 0.05A 24V
TE Connectivity ALCOSWITCH Switches
制造商零件编号 CF14JT220R
RES 220 OHM 5% 1/4W AXIAL
Stackpole Electronics Inc
制造商零件编号 LTL-4224
LED RED CLEAR T-1 3/4 T/H
Lite-On Inc.
制造商零件编号 PRT-14284
JUMPER M/M 4" 26AWG 30PCS
SparkFun Electronics
制造商零件编号 FIT0096
BREADBRD TERM STRIP 3.20X2.00"
DFRobot
制造商零件编号 DH-20M50055
USB AM TO USB MICRO, USB 2.0 - 1
Cvilux USA
Add all DigiKey Parts to Cart
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.