DigiKey - Introduction to Zephyr Part 6: How to Write a Device Driver | DigiKey
The video provides a comprehensive guide on writing a custom device driver for the Zephyr operating system, specifically focusing on creating a button driver. It begins by explaining the importance of understanding tools like CMake, Kconfig, and device trees, which are essential for connecting applications to drivers. The process involves creating an out-of-tree module, setting up CMakeLists, defining Kconfig options, and writing device tree bindings. The tutorial walks through creating a simple button driver that interfaces with GPIO pins, using macros for code generation, and setting up logging for debugging. The video emphasizes the importance of matching compatible strings across device tree nodes, bindings, and driver code to ensure proper integration.
The tutorial also includes a practical challenge to create a driver for the MCP9808 temperature sensor, encouraging viewers to use existing Zephyr drivers as a reference. It highlights the use of Zephyr's logging system and the importance of understanding macro magic for driver instantiation. The video concludes by demonstrating how to test the driver with application code and provides insights into sharing the driver with the community or integrating it into the Zephyr repository.
Key Points:
- Understand the importance of CMake, Kconfig, and device trees in Zephyr projects.
- Create an out-of-tree module for custom drivers to avoid modifying Zephyr's source code.
- Use macros for code generation and ensure compatible strings match across device tree, bindings, and driver code.
- Implement logging for debugging and use Zephyr's logging system effectively.
- Challenge: Create a driver for the MCP9808 temperature sensor using existing Zephyr drivers as a reference.
Details:
1. 🔧 Building Foundations for Zephyr Drivers
- Understanding cmake is essential for managing build processes in Zephyr projects. It helps in defining project structure and build configuration.
- Kenig is crucial for configuring kernel options specific to the hardware and application requirements, allowing fine-tuning of performance and features.
- Device tree is a data structure for describing hardware components, which is vital for enabling the operating system to manage hardware resources effectively.
- Bindings in Zephyr provide a way to define how software components interact with hardware, ensuring proper communication between drivers and devices.
- Even for application development with built-in drivers, mastering these tools is essential for seamless integration and customization.
- Embedded developers often write custom device drivers to interface with unique hardware components like sensors, demanding a deep understanding of these foundational tools.
2. 📚 Crafting Custom Bindings Files
- Bindings files act as an interface between the device tree and driver code, crucial for device management.
- Start by reviewing existing bindings files to understand their structure and functionality, which is essential for creating effective custom files.
- Creating custom bindings files involves tailoring them to meet specific driver requirements, thereby enhancing device management.
- Detailed steps include: analyzing the device's specific needs, structuring the file according to those needs, and testing to ensure compatibility and functionality.
- Examples of successful custom bindings include adjustments that allow for unique device operations or optimizations.
- Using these strategies can significantly improve device management efficiency and compatibility.
3. 🛠 Developing an Out-of-Tree Module
- Connect two buttons to pins four and five on the ES32 board to start developing a custom device driver, providing a practical example of hardware interaction.
- Create an out-of-tree module to avoid modifying the Zephyr RTOS source code, maintaining a separate modules folder. This approach allows for cleaner project management and easier updates.
- Name the newly created folder 'button' and convert existing button code from episode 4 into a driver, ensuring modular development and reusability.
- Establish a 'drivers' folder to adhere to the conventional structure found in Zephyr, and use CMake to link to the driver code, promoting consistency and ease of integration.
- Create a header file with header guards to prevent multiple inclusions, wrapping existing Zephyr drivers into a new driver for efficient code management.
- Develop a simple application to utilize the newly created driver, using the existing code from the button demo to test functionality and integration.
- Design a custom API comprising a set of functions or function signatures, instead of using a predefined API from Zephyr, to tailor the driver to specific project needs.
4. 📂 Structs and APIs in Zephyr: An In-Depth Look
- Publicly expose the API and pass it into the device tree using public functions for driver integration with application code. This allows seamless communication and control between the hardware and software components.
- Implement a 'get' function that uses the device structure and state as an output parameter to read button states. It should also return standard Zephyr error codes to ensure proper execution and error handling.
- Utilize function pointers stored in structs to enable integration with device tree macros. This feature allows multiple functions to be passed and enhances flexibility in driver design.
- Develop a driver for the mcp9808 temperature sensor on the board, using existing Zephyr drivers as templates, especially those supporting the common I2C interface. This facilitates a structured approach to driver development.
- Focus initially on creating a simple version of the driver that reads temperature data without relying on triggers or interrupts. This approach helps grasp the fundamentals of driver creation in Zephyr.
- Examine header files and source code to comprehend function definitions and the sensor driver API. This API offers a standardized method for sensor integration in Zephyr, promoting code reuse and consistency.
5. 🔍 Unraveling Macro Magic for Device Initialization
- Zephyr drivers require specific API or interface usage, particularly for sensors requiring a driver API.
- The JC c42 is a subclass of the sensor class, which is a subclass of driver class, implemented in C.
- Creating a configuration struct involves mapping device tree specifications to instance numbers for macro expansion.
- Driver code requires a compatible field symbol that matches entries in Binding zml and the device tree.
- The macro dtdv compact must be correctly spelled and match the compatible fields in device tree bindings.
- Symbols with commas are replaced with underscores within macros for consistency.
- Creating a driver involves matching compatible fields across device tree, bindings file, and C code.
- The NNO file includes necessary error codes, and logging headers assist in driver logging.
6. 🧩 Establishing Device Instances and DT Integration
6.1. Logging Configuration in Zephyr
6.2. Device Initialization and Configuration
7. 🔗 Comprehensive Kconfig and CMake Setup
- Ensure GPIO is ready: Check that the GPIO is ready before proceeding. If not, log an error and return an error code indicating device initialization failure.
- Configure Button Pin: Use 'gpio_pin_configure_DT' to set the button pin as input, logging an error and returning an error code if unsuccessful.
- Return Codes: Successfully initialized functions return zero; errors return specific error codes.
- Public Function Definition: Define public functions to interact with the API struct, such as 'button_state_get', which retrieves and returns the button's state.
- Button State Retrieval: Use 'gpio_pin_get_DT' to obtain the state of the GPIO pin, returning an error if the state retrieval fails.
- Error Handling: Log errors and return error codes for troubleshooting, ensuring users can handle exceptions accordingly.
8. 🔄 Advanced Macro Usage for Code Generation
- Advanced macro usage in Zephyr involves creating multiple code instances based on device tree information, facilitating efficient driver code generation.
- Macros enable extracting structured data from the device tree, which is vital for instantiating device-specific code efficiently.
- Developers must define a public API with properly assigned functions to API struct members, ensuring correct function signature matching.
- Creating macros for code generation requires clean macro definitions that handle multiple instances, using proper syntax to avoid interference.
- Macros like `gpiod DT spec` are crucial for filling struct fields with device tree data, such as GPIO properties.
- The instantiation process uses macros like `DT inst` to access specific device instances during the build process, aligning with device-specific data like pin numbers.
- The `device DT inst defin` macro ensures unique device instances are created from device tree identifiers, tying each instance to specific data.
- Driver initialization includes setting a priority level, aligning with predefined GPIO priorities for consistent sequences.
- Passing the API struct ensures driver functions are initialized and ready for use with accurate configuration data.
9. 🔍 Deep Dive into Macro Definitions
- The macro calls a 'Define' with arguments for device initialization, including node ID and power management, simplifying the process by setting some to null.
- It enables the creation of multiple driver instances using variable arguments, analogous to object-oriented programming techniques, allowing flexibility and reusability in C.
- DT_INST_FOREACH_STATUS_OKAY macro in Zephyr is essential for creating instances, supporting devices with different configurations efficiently.
- Macros allow handling various initialization levels and priorities, adjustable via Kconfig, enhancing adaptability.
- The API struct pointer facilitates access to device functions like button state retrieval across instances, demonstrating practical application.
10. 🗂 Structuring Your Zephyr Module
10.1. Code Completion and Macro Usage
10.2. Module Creation: Bindings, Kconfig, and Device Tree
10.3. CMake Lists and Zephyr Library Declaration
10.4. Subdirectory Management and Conditional Compilation
10.5. Top-Level Module Configuration and Compilation Process
11. 📜 Configuring Device Tree Bindings Effectively
11.1. Setting Up Kconfig for Custom Driver
11.2. Defining Custom Device Tree Information
11.3. Declaring the Driver as a Zephyr Module
12. 🗃 Declaring Your Module in Zephyr
- Zephyr requires a specific naming convention, DTS, within the directory structure for module declaration.
- The system searches recursively in the bindings folder for YAML files that correspond with driver code.
- To share custom drivers, developers can use platforms like GitHub for out-of-tree modules.
- Contributing to the official Zephyr repository involves forking the repository, making necessary changes, and submitting a pull request for integration.
13. 📜 Application Code: Integrating the Custom Driver
13.1. Setting Up the Folder Structure
13.2. Defining Button Nodes in Overlay
13.3. Configuring Button Nodes
13.4. Setting Up GPIO Keys and Debounce
13.5. Configuring GPIO Properties
13.6. Finalizing Device Tree Setup
13.7. Writing the Main Application Code
13.8. Device Tree Information and Pre-Made Functions
13.9. Using API Structs and Error Handling
13.10. Mapping Public and Private Functions
14. 🔧 Building, Flashing, and Testing Your Application
14.1. Writing Application Code
14.2. Building Configuration
14.3. Project Setup and Logging
14.4. Building and Flashing
14.5. Flashing and Debugging
14.6. Driver Code and Challenge
14.7. Output and Debugging
15. 🎯 Driver Development Challenge: Create an I2C Driver
- The challenge involves creating an I2C driver, utilizing the core tools of Zephyr.
- Participants are encouraged to revisit previous episodes to refresh their knowledge on device drivers.
- Step-through debugging in Zephyr can be performed using GDB connected to OpenOCD, facilitated by VS Code.
- The task is a culmination of the skills and knowledge developed throughout the series.