Webinar: Zephyr Workshop - Writing Device Drivers

Zephyr

Webinar Date: April 24, 2025

Key Takeaway

In this workshop, Shawn Hymel guides you through the process of writing an I2C temperature sensor device driver for Zephyr. Device drivers are one of the fundamental building blocks of Zephyr, and by creating your own, you get experience working with C code, CMake, Kconfig, and the Devicetree. Understanding these languages and systems is crucial for developing applications in Zephyr.

If you plan to follow along, we recommend the following hardware:

Frequently Asked Questions

Can you suggest some projects on NRF5340, with Zephyr and BLE?

  • Just about anything you can think of with a BLE-enabled microcontroller. Here are some ideas: smart watch, smart home sensors, wearable step counter, e-ink todo list

Can I think of Zephyr as a framework?

  • The term “framework” is a bit nebulous in the embedded and programming worlds. But in essence, yes, Zephyr is a framework that contains an RTOS, abstraction layers to interact with vendor HALs, a build system, middleware libraries, and various testing and debugging tools.

Why should I use Zephyr over FreeRTOS?

  • The core FreeRTOS is just a scheduler. Amazon has added some networking stacks over the years to enable easiler IoT development. However, FreeRTOS does not have the device-independent abstraction layers that Zephyr offers. As a result, you will still need to use the silicon vendor HAL to work with any particular hardware when using FreeRTOS. When you write applications in Zephyr, you can make them truly device-indpenedent, which means you can port your code to other hardware more easily.

Is there a script available to set up the driver stub?

  • I am not aware of any scripts to set up stubs, but this Nordic page might be a good starting point:

When is the next Zephyr webinar session?

  • We do not have another Zephyr webinar planned right now, but it’s good to know there is interest! I recommend checking out our Intro to Zephyr video series that covers a lot of these topics in more depth:

How much overhead will Zephyr bring?

  • You can expext around 16 kB RAM overhead and 32-64 kB flash overhead for a simple blinky example. You should expect a few hundred CPU cycles for context switching. Obviously, all this increases as you enable more features/drivers (e.g. WiFi, Bluetooth, LVGL, etc.). This person has a good writeup with a benchmark of Zephyr vs. RIOT-OS:

In what scenario will the sample_fetch() and channal_get() callbacks be called? Called by who?

  • Apologies if this was not clear in the webinar. sample_fetch() and channel_get() are not callbacks necessarily. They are mapped to an “API” scruct as function pointers. The main application directly calls them (through this API struct) after the driver is initialized in the kernel during boot.

Why does the Zephyr Project use C copying C++ mechanisms?

  • I don’t know the exact reason, but I assume the Zephyr team wanted to guarantee that code could be compiled for nearly all microcontrollers, even those with legacy C-only compilers.

I have a container for developing with Zephyr running on WSL2. How do I pass USB/ACM devices to the container for debugging or looking at the serial console, without having to explicitly pass each device and create a new container?

  • I haven’t not gotten this to work consistently across different host operating systems. I recommend looking into USB-IP to see if that works. Here is a good discussion on the subject:

I use the remote container extension to run the container. How do I pass my ACM device to it in order to debug it or view its serial console. One way I know of making this work is by passing the specific device as an argument to create a new container. Is there a better way?

  • Not that I am familiar with. USB-IP (if supported by your host OS) might be your best bet. For debugging, you can connect gdb to OpenOCD across the container boundary, as it uses a networking protocol. One of the Intro to Zephyr videos covers this:

Can we add a non-existing sensor channel?

  • You would need to modify the Zephyr source code to do so. Your best bet is to use an existing channel or create your own sensor-like interface that adheres to the “device” type in Zephyr.

I’m Using HiFive1 Rev B, the guide you show is possible to do the same for my board?

  • I do not have experience with that board. Zephyr’s official documentation is a good starting place:

https://docs.zephyrproject.org/latest/boards/sifive/hifive1/doc/index.html

Would this conversion handle negative temperature properly? (I guess with val1 negative and val2 positive)

  • In theory, it should handle negative temperatures, as it looks at the sign bit. I have not tested it. If you find that it does not, please file an issue or pull request in the GitHub repository:

What is the best approach to work with Zephyr versions? Using GitHub tags for example. How do you set the version normally?

  • Personally, I like pinning versions in my Dockerfile, which makes it easier for creating educational content. Zephyr recommends using the “west” metatool for handling versions and dependencies (including Zephyr itself) in a project. Please see the docs for more information on west:

What kinds of GUIs can be integrated with Zephyr?

Would I be able to find the .dtsi file for the Node MCU here?

  • I do not believe that the Node MCU board is officially supported by Zephyr, so you won’t find the .dtsi files for it there. The ESP8266 is supported (ESP-8266 Modules — Zephyr Project Documentation), so you would need to create a custom board definition module if you want to support the Node MCU board. You can read about board porting here:

Does this driver lookup happen at runtime or build time?

  • I am assuming you are referring to the Zephyr build system matching the “compatible” strings to match driver source code to the Devicetree. In that case, it is happening at build time.

Is modules part of an SDK?

  • I’m not sure I quite follow the question. An SDK is a collection of tools, libraries, docs, and/or examples. With this definition, Zephyr itself could be considered an SDK. A module by itself is just a software library formatted in a way that Zephyr understands. If you created other tools, docs, examples, etc. to go along with that library, then yes, you could consider a module (or collection of modules) to be part of an SDK.

Can you write device drivers that rely on and make calls to other device drivers?

  • Yes. In the example, we wrote a driver that used (depended on) the I2C driver. Hope that helps!

So, is it a correct understanding that i2c_write/read_dt would be linked to the ESP bare metal drivers?

  • Yes. If you look in the drivers/i2c folder (zephyr/drivers/i2c at main · zephyrproject-rtos/zephyr · GitHub), you can see how the abstracted I2C helper functions (like i2c_write/read) are connected to hardware. Vendors (or Zephyr representatives for those vendors) maintain the various hardware drivers. The Devicetree (when we connect it to a particular board/chip and I2C bus) says that the high-level i2c_write function should call a particular low-level driver function(s) for that chip, such as i2c_esp32_transmit(). Hope that helps!

Do you ever think of releasing tutorial series on modern embedded C++?

  • I do not have plans for it, but it is something I would like to pursue at some point. C++ and Rust are both gaining in popularity in the embedded world, and it’s something I would eventually like to cover.

This driver file mcp9808.h, when we write driver code for it, do we need to go through its datasheet to write the code up to register level? Or is it writing a driver for Zephyr in a certain way provided by Zephyr where we will call the APIs provided by espressif?

  • You still need to look at the datasheet to determine the device address, register addresses, what bits to read from or write to registers, and what features you want to implement.

How large did that Zephyr build end up being?

  • 138 kB, which is quite large for a simple I2C read demo! Zephyr requires a lot of overhead.

Does Zephyr have/support JESD204C driver?

  • To my knowledge, Zephyr does not implement JESD204C.

Is this similar to Linux FileOps structure?

  • Yes. You will likely notice many similarities between Zephyr and Linux.

Is it possible to template this file structure?

  • I do not think Zephyr has a built-in way to create such templates, but I’m sure a simple Bash or Python script would do the trick.

How to call a dynamic C library in Zephyr? For example, I have some proprietary AI C module for sensor data analysis, which is programmed in C/C++. Thanks.

  • You need to enable CONFIG_CPP in Kconfig so that Zephyr knows how to build and link to your library. See this page for more information:

Is it a common pattern to check for each type of device inside the main function?

  • Yes, it is common practice to check that each device is present in the device tree (null pointer check) and then to ensure that it has been initialized in the kernel during boot (i.e. using the device_is_ready() function).

Can you go back and click the problems tab?

  • Apologies for not seeing this during the live session. The problems listed were with IntelliSense not finding some symbols (e.g. CONFIG_SYS_CLOCK_TICKS_PER_SEC), even though they were definitely available during the build process. IntelliSense is not working 100%, but it works well enough for the Zephyr development and teaching that I do.

This simple code used 50KB of RAM…it adds so much of things at the background.

  • Yes, it does. Zephyr has a lot of abstraction and runs one (or more) default threads in the background for managing some tasks (such as the workqueue and logging).

I have been given a finished Zephyr project that I need to be able to extend. I’m totally lost because there’s both the Zephyr learning curve and the project on top. How would you recommend getting started with being able to extend it?

  • I recommend carefully going through all of our Intro to Zephyr series on YouTube, including trying your hand at the challenges to get a deeper understanding of how to work with Zephyr:

  • From there, start to break down the project you are examining into its various components: where is main? What functions is it calling? Drill down into those functions to see what drivers are being used. How is the Devicetree and Kconfig configured for a particular board? It might take days or weeks, as Zephyr projects can be incredibly complex, but spend some time understanding what’s going on. Don’t be afraid to copy and paste sections into ChatGPT or Claude and have it describe what’s going on. Hope that helps!

For debug, would you recommend using VS or another IDE like Embedded Studio from JLink?

  • I would use whatever IDE you are most comfortable with. I like VS Code because I know it pretty well (and it seems to be quite popular). Zephyr uses gdb for debugging, which plugs in nicely into OpenOCD. You can set up most IDEs to use gdb for things like step-through debugging and memory peeking. See this video for more information:

And do you recommend we just use T3: Forest topology?

  • I’m using something similar to T3 for teaching, but I did not use west for managing the repo in order to simplify the process (for students). T3 is great if you have a number of projects and want to use Zephyr mostly as-is (which is what we did in the workshop and the YouTube series). T1 seems to be the suggested topology for large, complex projects.

I have been able to use usbipd to pass through my JLink to a Docker container - can take some work to get it running but allows me to use West Flash from my containers. West is awesome!

  • That’s great to hear! I love using West to flash if available. Unfortunately, I have been unable to set up USB passthrough for all the major operating systems (macOS continues to elude me), and I have need to have something for these lessons that works across all major operating systems.

Is the compiler able to inline these function pointer calls or is there a lot of indirection that happens at runtime?

  • When you are assigning API function pointers to the API struct in a driver, these cannot be inlined, so there is still a decent amount of indirection that happens at runtime. It’s one of the unfortunate tradeoffs that Zephyr imposes to give us better abstraction.

I’m new to Zephyr. With all these abstractions, what can be said about real time behavior and determinism?

  • There will be some processor overhead as function indirection is required (especially when assigning function pointers to API structs). However, Zephyr is designed first and foremost as an RTOS with guaranteed determinism for most things. Just be aware that some features are not deterministic (e.g. networking stacks, logging, dynamic memory allocation, and work queues).

Why is ‘my-mcp9808’ defined with dash in one file, but with an underscore in another?

  • This was to demonstrate how Zephyr provides character replacement to match strings in the Devicetree (something I likely forgot to mention in the live webinar), similar to how it handles the “compatible” strings/tokens. By convention, alias labels in the Devicetree use dashes instead of underscores. However, dashes are not allowed in C macro tokens, so the Zephyr build system knows to do a character replacement for dashes to underscores when looking for a match. Hope this helps!

What’s the significance of having the MCP9808 folder under drivers, when drivers is already a subfolder of MCP9808?

  • This is a convention in the Zephyr source code, but not strictly required. If you dig into the official Zephyr drivers, you’ll often see <device_name>/drivers/<device_name> to hold the source code for that driver. However, Zephyr is not checking this. So long as you have the bindings YAML file in the right place, the top-level folder has a zephyr/module.yaml, and your CMakeLists.txt correctly builds the driver target source, your driver should build and link correctly.

Why do we not use _ in the driver binding file name instead of a comma?

  • This is Zephyr convention borrowed from the Linux world: the string for the compatible should generally be “,”. It was made like this with little regard to how such strings should work in C (especially as macro tokens). You can definitely use an underscore, and it should still work. But, know that Zephyr convention uses a comma.

What is a macro in this context?

  • In C, a macro is a piece of code that gets expanded by the preprocessor (before the compiler). This is usually things like #if, #define, and so on.

Does that mean we can custom name config like CONFIG_NAME_MCP9809=y?

  • Almost. You can define custom Kconfig symbols in Kconfig files that get picked up by the Zephyr build system. In the example in the webinar, we defined the custom Kconfig symbol “MCP9808” in modules/mcp9808/drivers/mcp9808/Kconfig. That means you can refer to that symbol in C code as CONFIG_MCP9808 to get its status. I recommend checking out the Kconfig episode in the Intro to Zephyr series to learn more:

Can I use I2C with interrupt?

  • Yes. This is usually configured in the Devicetree, which connects the I2C (or other peripheral) to the interrupt controller. Most board and microcontroller vendors have it set to use interrupts by default (rather than i.e. polling). You can see how the I2C0 node on the ESP32S3 is set to use the interrupt controller (intc) by default here:

How do you organize the application? One workspace for all apps or multiple workspaces?

  • There are multiple ways to do this. Zephyr generally recommends one workspace per application with the assumption that you are working on something large and complex. This is the “T1” topology according to the Zephyr docs. I use something closer to the “T3” topology for teaching (lots of smaller projects in one workspace). You can read more about these recommended workspace topologies here:

Why not do “select I2C” if it depends on it? Is there a reason why you would do it this way versus the other?

  • You’re right: “select I2C” is probably the better way to do it so that it automatically enables I2C. I think I just wanted to show how a depency worked (especially in menuconfig, if I had more time).

Thoughts on the USB stack in Zephyr? I don’t think there are many docs on implementing multiple classes on the same USB line. Would you recommend trying to integrate tinyusb?

  • I honestly have not worked with the USB stack in Zephyr, so I can’t really comment on it. It seems that most of the USB classes are supported, though:

It may not be today’s topic, but could you please talk briefly about ztest framework, what is it for, how to use etc.? Can we further our learning on Zephyr by testing the firmware we create?

  • I honestly do not have experience with ztest. It’s something that I hope to cover in the future, but I did not have time to dive into Zephyr’s testing frameworks for the workshop or series. In general, I recommend doing test-driven development for code you hope to have in production. While it’s a lot more overhead (in terms of code writing), it can help find bugs long before they happen in the field. The extent of my knowledge is that you would write your unit tests in ztest and then manage the test runs using the twister tool.

Why does the directory tree need MCP9808 twice? Isn’t it implied by the parent?

  • This is a convention in the Zephyr source code but not strictly required. If you dig into the official Zephyr drivers, you’ll often see <device_name>/drivers/<device_name> to hold the source code for that driver. However, Zephyr is not checking this. So long as you have the bindings YAML file in the right place, the top-level folder has a zephyr/module.yaml, and your CMakeLists.txt correctly builds the driver target source, your driver should build and link correctly.

What is the advantage of splitting the fetch and get for reading sensors? Why should that be exposed in the application vs hidden?

  • I honestly do not know the exact reason the Zephyr team decided to split fetch and get. My guess is so that you can put the calls in separate threads/tasks so that fetching can happen outside your main application code, which then only needs to get the latest reading (without waiting for a long I2C write/read cycle).

Would you recommend using the “sensor” template even if you’re using an unusual sensor which isn’t listed in the channel enum?

  • You don’t have to–a generic “device” type/interface is probably good enough. The “sensor” interface gives you some advantages, such as splitting get/fetch and a pre-configured place to store your data. You could also use a placeholder channel from the sensor_channel type enum, and it should work.

Will the super loop call each of the devices for each iteration?

  • Not quite–the Zephyr build process unrolls the giant macro at the end of the driver code to create copies of the driver structs for each instance in the Devicetree. You (as the developer) have the option of calling those get/fetch function calls for each of the devices (if you wish).

This super loop will only call that one address. Other devices would have different address and would require additions to the superloop?

  • Yes, that is correct.

If I’m looking for ways to interact with device-specific features beyond what the sensor interface provides, is there a tutorial you’d recommend I look at to bypass it?

  • The “Device Driver” episode of the YouTube series demonstrates creating a simple driver for a generic “device” type (without the “sensor” type):

Is there a good reference for why that file hierarchy is the way it is? It’s worth distinguishing the node_id and inst entries in that space. Getting back and forth matters a lot.

  • I have not seen anything that explicitly talks about the “why” behind the folder structure, but these resources should help you dive more into creating drivers from scratch:

Is this the right workshop to ask how Zephyr writes cross-platform code? Is it achieved using pre-processor directives?

  • Creating cross-platform applications is achieved through a variety of mechanisms in Zephyr. One is through the use of lots of pre-processor directives (as we saw in the driver code). The other is by using the Devicetree. When you pass in a particular board as an argument to the build system, Zephyr generates a compiled Devicetree for your application (.overlay), board (.dts and .dtsi files), and controller (usually .dtsi files). That is a list of all the hardware periphals you want to use for that board and controller. Zephyr then looks for that matching source code requried to work with those peripherals, such as the ESP32 I2C driver. Through the use of common interfaces (like we saw with fetch/get) and function pointers, Zephyr calls the correct underlying functions for that driver code. If you modified the Devicetree and told Zephyr to use some other board (e.g. STM32 Nucleo), it would instead search for (during build time) the STM32 I2C driver, and your I2C functions would now point to those ST-provided driver functions. In most cases, the hardware vendor provides this underlying driver code, so you (as the developer) do not need to worry about it. The hope is that you can just focus on application code and any necessary drivers for non-supported hardware (e.g. sensors). That lets you migrate to another microcontroller vendor with little effort (just a new .overlay file with new Devicetree settings). I hope that helps!

Shouldn’t “zephyr/drivers/sensor.h” be enclosed in “” rather than <> because it is a user defined header file?

  • Anything that started with “zephyr” for those include directives were part of the official Zephyr source code. As such, I assumed that they are part of the “system” search path (i.e. pulling from the official Zephyr includes rather than from within the project or module), so I put them in < > rather than quotes " ". You can see this official Zephyr app doing the same thing:

Referral Webinar Link

If you would like more information from this webinar, please review the details below.

Martin Lampacher’s Memfault Series on the Zephyr Devicetree

Zephyr Official Documentation: Build System (CMake):

Zephyr Official Documentation: Devicetree Bindings:

Mastering Zephyr Driver Development by Gerard Marull Paretas:

DigiKey Webinar Center

DigiKey TechForum Webinar Posts