{"id":329,"date":"2024-01-12T13:12:16","date_gmt":"2024-01-12T12:12:16","guid":{"rendered":"https:\/\/redero.fr\/?p=329"},"modified":"2025-09-19T21:55:28","modified_gmt":"2025-09-19T19:55:28","slug":"my-first-zephyr-rtos-driver","status":"publish","type":"post","link":"https:\/\/redero.fr\/?p=329&lang=en","title":{"rendered":"My first Zephyr RTOS driver"},"content":{"rendered":"\n<p>I have been wanting to give it a try for a while: an RTOS with enough abstraction to make your embedded app multiplatform, backup up by the Linux foundation. So I tested for you: developing a Zephyr RTOS device driver. I will give my tips and thoughts about it.<\/p>\n\n\n\n<p>The project idea comes from my gizmo drawer: I have several standard eval kits such as Nucleo STM32, ESP32 and RF52DK. I also found several nRF24 2.4GHz transceivers I bought some time ago on AliExpress for a DIY project, and it does not have Zephyr support yet.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-thumbnail wp-duotone-unset-1\"><img loading=\"lazy\" decoding=\"async\" width=\"150\" height=\"150\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2024\/01\/nrf24l01-1-150x150.png\" alt=\"nRF24L01\" class=\"wp-image-354\"\/><\/figure>\n\n\n\n<p>I have watched carefully the sir Marull-Paretas in <a href=\"https:\/\/www.youtube.com\/watch?v=o-f2qCd2AXo\">Mastering Driver Development<\/a>, and let&rsquo;s have some fun with that!<\/p>\n\n\n\n<p>Also, you can refer to <a href=\"https:\/\/bootlin.com\/blog\/zephyr-implementing-a-device-driver-for-a-sensor\/\">this tutorial from Bootlin.<\/a><\/p>\n\n\n\n\n\n<h2 class=\"wp-block-heading\">1- Setting up the work folder<\/h2>\n\n\n\n<p>First, I forked the example app:<\/p>\n\n\n\n<p><a href=\"https:\/\/github.com\/zephyrproject-rtos\/example-application\">https:\/\/github.com\/zephyrproject-rtos\/example-application<\/a><\/p>\n\n\n\n<p>Oh but don\u2019t git clone, you have to use west!<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>west init -m https:\/\/github.com\/zephyrproject-rtos\/example-application --mr main my-workspace<\/code><\/pre>\n\n\n\n<p class=\"has-small-font-size\">Don\u2019t ask me how I know git cloning does not work. I really need to read the readmes sometimes&#8230;<\/p>\n\n\n\n<p>If you followed the <a href=\"https:\/\/docs.zephyrproject.org\/latest\/develop\/getting_started\/index.html\">base tutorial<\/a> to install Zephyr, you will have the complet sources in ~\/zephyrproject, but the example application has its own copy with a lighter modules folder.<\/p>\n\n\n\n<p>Here is my new Zephyr driver workspace, with its own Zephyr copy and its own module folder:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>zephyr_driver\n\u2514\u2500\u2500 my-workspace\n    \u251c\u2500\u2500 driver_nrf24l01\n    |   \u251c\u2500\u2500 west.yml\n    |   \u251c\u2500\u2500 CMakeList.txt\n    |   \u251c\u2500\u2500 ...\n    |\n    \u251c\u2500\u2500 modules\n    |   \u2514\u2500\u2500 hal\n    |       \u251c\u2500\u2500 cmsis\n    |       \u251c\u2500\u2500 espressif\n    |       \u251c\u2500\u2500 nordic\n    |       \u2514\u2500\u2500 stm32\n    \u2514\u2500\u2500 zephyr\n        \u251c\u2500\u2500 arch\n        \u251c\u2500\u2500 boards\n        \u251c\u2500\u2500 cmake\n        \u251c\u2500\u2500 CMakeLists.txt\n        \u251c\u2500\u2500 ...<\/code><\/pre>\n\n\n\n<p>Don\u2019t forget that your project will use its local Zephyr, the one with fewer modules &#8211; those contain third-party HAL code. If you need more, they are downloaded automatically by west, you just have to add them to west.yml project file and call:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>west update<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">2- Driver writing<\/h2>\n\n\n\n<p>nRF24 is a small 2.4 GHz transceiver, accessible via SPI bus, with a bunch of register described in <a href=\"https:\/\/infocenter.nordicsemi.com\/pdf\/nRF24L01P_PS_v1.0.pdf\">its datasheet<\/a>, the usual stuff.<\/p>\n\n\n\n<p>There is a pretty neat <a href=\"https:\/\/github.com\/nRF24\/RF24\">Raspberry Pi implementation<\/a>, I used it for reference. The header file describing registers was specially useful to prevent typos whilst copying datasheet.<\/p>\n\n\n\n<p>Then, Marull-Paretas sensei says we have to choose between:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Generic API implementation, or<\/li>\n\n\n\n<li>Custom API implementation<\/li>\n<\/ul>\n\n\n\n<p>The API is a list of function that can be called from the userspace application. It is far better to have a generic API, since it would allow to change the component for an equivalent one without any modification in the application code. But in my case, there is no proprietary radio API currently implemented, so I will have to start with a custom one, and hopefully change it when a generic API is released.<\/p>\n\n\n\n<p>Next, I modified the example-application to only keep out-of-tree driver skeleton files, rename them, and modify the configuration files so that it builds correctly. I also modified project files to call my work target, a Nucleo board.<\/p>\n\n\n\n<p>Once the application skeleton is ready, it is time to implement the real component logic, with datasheet in one hand and logic analyzer in the other.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Driver <strong>and example application<\/strong><\/h3>\n\n\n\n<p>The most important file of all is the driver source file, in my case:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.\/drivers\/nrf24l01\/nrf24l01.c<\/code><\/pre>\n\n\n\n<p>But it is not standalone and has to be called by a main.c somewhere:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.\/app\/src\/main.c<\/code><\/pre>\n\n\n\n<p>Zephyr calls its driver initialization (in my case nrf24l01_init) before starting the user application. If you like to write some kind of code blob and care about slicing it into functions later, it is possible to put everything in the init function and compile an empty main.c, before you create the rightful API functions (that is a very personal method, don&rsquo;t do this if you prefer to write clean code from the start).<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Device tree<\/h3>\n\n\n\n<p>In your driver, you will copy device tree data in two structures:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>struct nrf24l01_config<\/code><\/pre>\n\n\n\n<pre class=\"wp-block-code\"><code>struct nrf24l01_data<\/code><\/pre>\n\n\n\n<p>The config structure includes for instance GPIO and bus references. It is a const variable, because they are not supposed to change during runtime. The data structure can be modified, so I ended up adding in it one or two variables I would have liked to declare as globals.<\/p>\n\n\n\n<p>Every device tree option is listed and described in a Yaml file called the \u00ab\u00a0bindings\u00a0\u00bb. This file is both documentation and code, it is mandatory and you will have to be careful about filenames and typographic subtleties. I spend a full hour scratching my head because I called this file \u00ab\u00a0nrf24l01.yaml\u00a0\u00bb instead of \u00ab\u00a0nordic,nrf24l01.yaml\u00a0\u00bb.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.\/dts\/bindings\/nrf24\/nordic,nrf24l01.yaml<\/code><\/pre>\n\n\n\n<p>In my example, I did not have to create an entire device tree because I am using a standard evaluation board. I simply added nrf24l01 in a device tree overlay.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.\/app\/boards\/nucleo_f756zg.overlay<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>C<\/strong><strong>onfiguration options<\/strong><\/h3>\n\n\n\n<p>I also added a bunch of Kconfig options to my driver, the most important being the switch between polling behaviour and interrupt behaviour.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.\/drivers\/nrf24l01\/Kconfig<\/code><\/pre>\n\n\n\n<p>Those options are then selected in the app project file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.\/app\/prj.conf<\/code><\/pre>\n\n\n\n<p>Note that it would have been possible to choose this type of option in the device tree. Technically, it would have been the same, but philosophically, you are supposed to put hardware-related things in the device tree and software-related choices in the Kconfig. There is a grey area, since \u00ab\u00a0polling\u00a0\u00bb will be most useful when the hardware interrupt is not available, but it&rsquo;s how it&rsquo;s done in mainline drivers.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Custom API<\/h3>\n\n\n\n<p>Custom API is described in the include folder:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>.\/include\/app\/drivers\/propy_radio.h<\/code><\/pre>\n\n\n\n<p>It is not possible to have the same name for both API functions and their driver implementations, even if the API is only implemented by the one driver. In my case I decided to name my API \u00ab\u00a0propy radio\u00a0\u00bb so that other proprietary radio chips can fit it.<\/p>\n\n\n\n<p>On application side, \u00ab\u00a0propy_radio_read\u00a0\u00bb are \u00ab\u00a0propy_radio_write\u00a0\u00bb will be called, and they will \u00ab\u00a0nrf24l01_read\u00a0\u00bb and \u00ab\u00a0nrf24l01_write\u00a0\u00bb.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>CI testing<\/strong><\/h3>\n\n\n\n<p>In my case, it is hard to test this project with Github workflow and Qemu, since it is mostly hardware related. I thus removed all execution tests and set my CI to only verify that the project compiles correctly on all my platforms.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Results<\/h2>\n\n\n\n<p>You can find the resulting project here: <a href=\"https:\/\/github.com\/everedero\/driver_nrf24l01\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/github.com\/everedero\/driver_nrf24l01<\/a><\/p>\n\n\n\n<p>My example application uses the newly-created nRF24 driver to run it on 3 different eval kits from my drawer: a STM32 Nucleo, a nRF52DK, and an ESP32 WROOM. And it&rsquo;s super easy, I only had to create a device tree overlay for each platform, with the right GPIO and pinmuxes, and it just works! In order to demonstrate this magic, my example implements 3 different roles, one for each platform: Alice starts sending a message, Bob answers the message, and Eve just quietly listens everything. <\/p>\n\n\n\n<figure class=\"wp-block-image size-large wp-duotone-unset-2\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"768\" src=\"https:\/\/redero.fr\/wp-content\/uploads\/2024\/01\/multiplatform-1-1024x768.jpg\" alt=\"\" class=\"wp-image-313\" srcset=\"https:\/\/redero.fr\/wp-content\/uploads\/2024\/01\/multiplatform-1-1024x768.jpg 1024w, https:\/\/redero.fr\/wp-content\/uploads\/2024\/01\/multiplatform-1-300x225.jpg 300w, https:\/\/redero.fr\/wp-content\/uploads\/2024\/01\/multiplatform-1-768x576.jpg 768w, https:\/\/redero.fr\/wp-content\/uploads\/2024\/01\/multiplatform-1.jpg 1280w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>And it&rsquo;s working!<\/p>\n\n\n\n<figure class=\"wp-block-video\"><video height=\"1080\" style=\"aspect-ratio: 1920 \/ 1080;\" width=\"1920\" controls src=\"https:\/\/redero.fr\/wp-content\/uploads\/2024\/01\/term.webm\"><\/video><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p>It is often said about Zephyr that it has some steep learning curve, and I really think it does. However, I am very happy I spent some time getting this to work, because running the same code on 3 different embedded platforms, it is quite wonderful and promising.<\/p>\n\n\n\n<pre class=\"wp-block-verse has-text-align-center has-vivid-green-cyan-color has-secondary-background-color has-text-color has-background has-link-color has-small-font-size wp-elements-2e32ee8aa63f44753bc4cec154d043c9\">Article updated on 02\/02\/2025 with new installation instructions<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>I tested for you: developing a Zephyr RTOS device driver. I will give my tips and thoughts about it.<\/p>\n","protected":false},"author":1,"featured_media":276,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[49],"tags":[47],"class_list":["post-329","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog_en","tag-zephyr-en"],"_links":{"self":[{"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/posts\/329","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/redero.fr\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=329"}],"version-history":[{"count":23,"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/posts\/329\/revisions"}],"predecessor-version":[{"id":1302,"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/posts\/329\/revisions\/1302"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/redero.fr\/index.php?rest_route=\/wp\/v2\/media\/276"}],"wp:attachment":[{"href":"https:\/\/redero.fr\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=329"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/redero.fr\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=329"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/redero.fr\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=329"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}