Embedded Open Source Summit 2023

Embedded Open Source Summit 2023

This year the Embedded Linux Conference is colocated with Automotive Linux Summit, Embedded IOT summit, Safety-critical software summit, LFEnergy and Zephyr Summit. The event was held in Prague, Czech Republic this time.

It's the second time I'm at a Linux conference in Czech Republic, and it clearly is my favorite place for such a event. Not only for the cheap beer but also for the architecture and the culture.

I've collected notes from some of the talks. Mostly for my own good, but here they're:

9 Years in the making, the story of Zephyr [1]

Much has happened since the project started by an announcement at an internal event at Intel in 2014. Two years later it went public and was quickly picked up by the Linux Foundation, and now it's listed as one of the top critical open source projects by Google.

Now, in June 2023, it has mad 40 releases and has over a milion lines of code. What a trip, hue?

The project has made a huge progress, but the road hasn't been straight forward. Many design decisions has been made and changed over time. Not only technical decisions but in all areas. For example, Zephyr was originally BSD licenced. The current license, Apache2, was not the first choice. The license was changed upon requests from other vendors. I think it's good that not only one company has full dominance on the project.

Even the name has been up for discussion before it landed in Zephyr. One fun thing is that Zephyr has completely taken over all search results, it's hard to find anything that are not related to the Zephyr project as it masks out all other hits... oopsie.

Some major transitions and transformations made by the project:

  • The build system which was initially a bunch of custom made Makefiles, which then became Kbuild and finally CMake.
  • The kernel itself moved from a nano/micro kernel model to a unified kernel.
  • Even the review system has changed from Garrit to Github.

The change from the dual kernel model to a unified kernel was made in 2016. The motivation was that the older model suffers from a few drawbacks:

  • Non-intutive nature of the nano/micro kernel split
  • Double context switch affecting the performance
  • Duplication of object types for nano and micro
  • System initialixation in the idle task

Instead, we ended up with something that:

  • Made the nanokernel 'pre-emptible thread' aware
  • Unified fibers and tasks as one type of threads by dropping the Microkernel server
  • Allowed cooperative threads to operate on all types of objects
  • Clarified duplicated object types
  • Created a new, more streamlined API, without any loss of functionality

Many things points to that Zephyr has healthy eco system. If we look at the contributions we can se that the member/ community contributions are strictly increasing every year and the commits by Intel is decreasing.

It shows us that the project itself is an evolving and become more and more of a self- sustaining open eco-system.

System device trees [2]

As the current usage of device tree does not scale well, especially when working with Multi-core AMP SoCs. we have to come up with some alternatives.

One such alternative is the System Device Tree. It's an extenstion of the DT specification that are devleoped in the open. To me it sounded uncomfortible at the first glance, but the talker made it clear that the work is heavily in cooperate with the DT specifications and the Linux device tree maintainters.

The main problem is that there are one instance of everything that is available for all CPUs and that is not suitable for AMP architectures where each core could be of a completely different types. The CPU cores are normally instantiated by one CPU node. One thing that the system device trees contribute to is to change that to independent CPU clusters instead.

Also, in a normal setup, many peripherals are attached to the global simple bus, and are shared across cores. The new indirect-bus on the other hand, which are introduced in System Device Tree, addresses this problem by map the bus to a particular CPU cluster which makes the peripheral visable for a specific set of cores.

System Device Tree will also introduce independent execution domains, of course also mapped to a specific set of CPU cluster. By this we can encapsulate which peripherals that should be accessable from which application.

But how does it work? The suggestion is to let a tool, sysbuild to postprocess the standard DT structure into several standard devicetrees, one for each execution domain.

Manifests: Project sanity in the ever-changing Zephyr world [3]

Mike Szczys talked about manifests files and why you should use those in your project.

But first, what is a manifest file?

It's a file that manages the project hiearchy by specify all repositories by URL, which branch/tag/hash to use and the local path for checkout. The manifest file also support some more advanced features such as:

  • Inheritance
  • Allow/block lists
  • Grouping
  • West support for validation

The Zephyr tree already uses Manifest files to manage versions of modules and libraries, and there is no reason to do not use the same method in your application. It let you keep control of which versions of all modules that your application requires in a clear way. Besides, as the manifest file is part of your application repository, it does also has a commit history and all changes to the manifest is trackable and hopefully explained in the commit message.

The inheritance feature in the manifest file is a powerful tool. It let you to import other manifest files and explicitely allow or exclude parts of it. This let you reduce the size of of your project significally.

West will handle everything for you. It will parse the manifest file, recursively clone all repositories and update those to a certain commit/tag/branch. It's preferred to not use branches (or even tags) in the manifest files as those may change. Use the hash if possible. Generally speaking, this is the preferred way in any such system (Yocto, Buildroot, ...).

The biggest benifit that I see is that you treat all dependencies aside from your application and that those dependencies are locked to known versions. Zephyr itself will be treated as a dependency to your application, not the other way around.

It's easy to draw parallells to the Yocto project. My first impression of Yocto was that it's REALLY hard to maintain, pretty much for the same reason that we are talking about here - how do I keep track of every layer in a controllable way? The solution for me wasto use KAS which pretty much do exactly the same thing - it creates a manifest files with all layers (read dependencies) that you can version control.

Zbus [4]

Rodrigo Peixoto, the maintainer and author of the Zbus subsystem had a talk where he gave us an introduction on what it's.

(Rodrigo is a nice guy. If you see him, throw a snowball at him and say hi from me - he will understand).

Zephyr has support for many IPC mechanisms such as LIFO, FIFO, Stack, Message Queue, Mailbox and pipes. All of those works great for one-to-one communication, but that is not allways what we need. Even one-to-many could be tricky with the existing mechanism that Zephyr provides.

ZBus is an internal bus used in Zephyr for Many-to-Many communication, besides, such a infrastructure cover all cases (1:1, 1:N, N:M) as a bonus.

I like these kind of infrastructure. It reminds me of dbus (and kbus..) but in a more simplier manner (and that is a good thing). It allows you to have a event-driven architecture in your application and a unified way to make threads talk and share data. Testability is also a bulletpoint for ZBus. You may easily swap a real sensor for stubbed code and the rest of the system would not notice.

The conference

/media/myself-embedded-open-source-summit.jpg

(I got stuck on a picture. Don't know which talk, but it seems like I enjoyed it)

Write a device driver for Zephyr - Part 1

Write a device driver for Zephyr - Part 1

This is the first post in this series. See also part part2, part3 and part4.

Overview

The first time I came across Zephyr [1] was on Embedded Linux Conference in 2016. Once back from the conference I tried to install it on a Cortex-M EVK board I had on my desk. It did not go smoothly at all. The documentation was not very good back then and I don't think I ever got system up and running. That's where I left it.

Now, seven years Later, I'm going to give it another try. A friend of mine, Benjamin Börjesson, who is an active contributor to the project has inspired me to test it out once again.

So I took whatever I could find at home that could be used for an evaluation. What I found was :

  • A Raspberry Pi Pico [2] to run Zephyr on
  • A Segger J-Link [3] for programming and debugging
  • A Digital-To-Analogue-Converter IC (ltc1665 [4]) that the Zephyr project did not support

Great! Our goal will be to write a driver for the DAC, test it out and contribute to the Zephyr project.

/media/zephyr-logo.png

Zephyr

First a few words about Zephyr itself. Zephyr is a small Real-Time Operating System (RTOS) which became a hosted collaborative project for the Linux Foundation in 2016.

Zephyr targets small and cheap MCU:s with constrained resources rather than those bigger SoCs that usually runs Linux. It supports a wide range of architectures and has a extensive suite of kernel services that you can use in the application.

It offers a kernel with a small footprint and a flexible configuration build system. Every Linux kernel hacker will recognize itself in the filesystem structure, Kconfig and device trees - which felt good to me.

To me, it feels like a more modern and fresh alternative to FreeRTOS [5] which I'm quite familiar with already.

Besides, FreeRTOS uses the Hungarian notation [6], and just avoiding that is actually reason enough for me to choose Zephyr over FreeRTOS. I fully agree with the Linux kernel documentation [7]:

Encoding the type of a function into the name (so-called Hungarian` notation) is asinine - the compiler knows the types anyway and can check those, and it only confuses the programmer.

Even if I personally prefer the older version (before our Code-of-Conduct) [8] :

Encoding the type of a function into the name (so-called Hungarian notation) is brain damaged - the compiler knows the types anyway and can check those, and it only confuses the programmer. No wonder MicroSoft makes buggy programs.

Hardware setup

No fancy hardware setup. I did solder the LTC1665 chip on a break-out board and connected everything with jumper cables. The electrical interface for the LTC1665 is SPI.

/media/rpi-ltc1665.jpg

The connection between the Raspberry Pi Pico and the J-Link:

Pin RP Pico Pin J-Link Signal
"DEBUG SWCLKW 9 SWCLK
"DEBUG GND" 4 GND
"3V3" Pad 36 1 VTref

The connection between Raspberry Pi Pico and LTC1665:

Pin RP Pico LTC1665 Signal
"SPI0_RX" Pad 16 DIN Pin 9 SPI_RX
"SPI0_CSN" Pad 17 CS Pin 7 SPI_CS
"SPI0_SCK" Pad 18 SCK pin 8 SPI_SCK
"SPI0_TX" Pad 19 DOUT Pin 10 SPI_TX

Software setup

Install Zephyr

Zephyr does use west [10] for pretty much everything. West is a meta tool used for repository management, building, debugging, deploying.. you name it. It has many similarities with bitbake that you will find in Yocto. I'm more of a "do one thing and do it well"-guy, so these tools (nor west or bitbake) makes a huge impression on me.

West is written in Python, as the nature of Python is as it's, you have to make a virtual environment to make sure that your setup will work for more than a week. Otherwise you will end up in incompatibilities as soon you upgrading some of the python dependencies.

The documentation [9] is actually really good nowadays. Most of these commands are just copy&paste from there.

Create a new virtual environment:

python -m venv ~/zephyrproject/.venv

Activate the virtual environment:

source ~/zephyrproject/.venv/bin/activate

Install west:

pip install west

Get the Zephyr source code:

west init ~/zephyrproject
cd ~/zephyrproject
west update

Export a Zephyr CMake package to allow CMake to automatically load boilerplate code required for building Zephyr applications:

west zephyr-export

The Zephyr project does contain a file with additional Python dependencies, install them:

pip install -r ~/zephyrproject/zephyr/scripts/requirements.txt

Install Zephyr SDK

The Zephyr Software Development Kit (SDK) contain toolchains for all architectures that is supported by Zephyr.

Download the latest SDK bundle:

cd ~
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.0/zephyr-sdk-0.16.0_linux-x86_64.tar.xz
wget -O - https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.16.0/sha256.sum | shasum --check --ignore-missing

Extract the archive:

tar xvf zephyr-sdk-0.16.0_linux-x86_64.tar.xz

Run the setup script:

cd zephyr-sdk-0.16.0
./setup.sh

Build OpenOCD

The Raspberry Pi Pico has an SWD interface that can be used to program and debug the on board RP2040 MCU.

This interface can be utilized by OpenOCD. Support for RP2040 is not mainlined though, so we have to go for a rpi fork [11].

Clone repository:

git clone https://github.com/raspberrypi/openocd.git
cd openocd

Build:

./bootstrap
./configure
make

And install:

make install

Build sample application

The Raspberry Pi Pico does have a LED on board. So blinky, an application that will flash the LED with 1Hz, is a good test to prove that at least something is alive. Build it:

cd ~/zephyrproject/zephyr
west build -b rpi_pico samples/basic/blinky -- -DOPENOCD=/usr/local/bin/openocd -DOPENOCD_DEFAULT_PATH=/usr/local/share/openocd/scripts -DRPI_PICO_DEBUG_ADAPTER=jlink

Note that we specify the board (-b) to rpi_pico.

OPENOCD and OPENOCD_DEFAULT_PATH should point to where OpenOCD is installed in the previous step.

Flash the application

To flash our Raspberry Pi Pico, we just run:

west flash

As we have set the RPI_PICO_DEBUG_ADAPTER during the build stage, it's cached so it can be omitted from the west flash and west debug commands. Otherwise we had to provide the --runner option. E.g. :

west flash --runner jlink

You don't have to use a J-link to flash the Raspberry Pi Pico, you could also copy the UF2 file to target. If you power up the Pico with the BOOTSEL button pressed, it will appear on the host as a mass storage device where you could simply copy the UF2 file to. You loose the possibility to debug with GDB though.

Debug the application

The most straight forward way is to use west to start a GDB session (--runner is still cached from the build stage):

west debug

I prefer to use the Text User Interface (TUI) as it's easier to follow the code, both in C and assembler. Enter TUI mode by press CTRL+X+A or enter "tui enable" on the command line.

If you do not want to use west, you could start openocd by yourself:

openocd -f interface/jlink.cfg -c 'transport select swd' -f target/rp2040.cfg -c "adapter speed 2000" -c 'targets rp2040.core0'

And manually connect with GDB:

gdb-multiarch -tui
(gdb) target external :3333
(gdb) file ./build/zephyr/zephyr.elf

The result is the same.

/media/zephyr-gdb.png

Summary

Both the hardware and software environment is now ready to do some real work. In the part2 we will focus on how to integrate the driver into the Zephyr project.

Write a device driver for Zephyr - Part 2

Write a device driver for Zephyr - Part 2

This is the second post in this series. See also part part1, part3 and part4.

Overview

In the first part1 of this series, we did setup the hardware and prepared the software environment. In this part we will focus on pretty much everything but writing the actual driver implementation. We will touch multiple areas in order to fully integrate the driver into the Zephyr project, this includes:

  • Devicetrees
  • The driver
  • KConfig
  • Unit tests

Lets introduce each one of those before we start.

Devicetrees

A Devicetree [2] is a data structure that describe the static hardware configuration in a standard manner. One of the motivations behind devicetree is that it should not be specific for any kernel. In the best of the worlds, you should be able to boot a Linux kernel, BSD kernel or Zephyr (well..) with the same devicetree. I've never heard about a working example IRL though, but the idea is good.

In the same way, you should be able to boot the same kernel on different board by only swap the devicetree. In Zephyr, the devicetree is integrated to the binary blob, so this idea does not fully apply to Zephyr though.

There are two types of files related to device trees in Zephyr:

  • Devicetree sources - the devicetree itself (including dts, interface files and overlays).
  • Devicetree bindings - description of its content. E.g. data types and which properties that is required or optional.

Zephyr does make use of both of these type of files during the build process. It allows the build process to make a build-time validation of the devicetree sources against the bindings, generate KConfig macros and a whole bunch of other macros that is to be used by the application and by Zephyr itself. We will see example of these macros later on.

Here is a simplified picture of the build process with respect to devicetrees:

/media/zephyr-devicetree.png

Driver

All drivers is located in the ./driver directory. It's C-files that contains the actual implementation of the driver.

KConfig

Like the Linux kernel (and U-boot, busybox, Barebox, Buildroot...), Zephyr uses the KConfig system to select what subsystem, libraries and drivers to be included in the build.

Remember when we did build the blinky application in the part1? We did provide -b rpi_pico to the build command to specify board:

west build -b rpi_pico ....

This will load ./boards/arm/rpi_pico/rpi_pico_defconfig as the default configuration and store it into ./build/zephyr/.config, which is the actual configuration the build system will use.

The .config file contains all configuration options selected by e.g. menuconfig AND the generated configuration options from the devicetree.

Unit tests

Zephyr makes use of Twister [1] for unit tests. By default it will build the majority of all tests on a defined set of boards. All these tests is part of the automatic test procedure for every pull request.

Lets start!

First we have to create a few files and integrate them into the build system. The directory hiearchy is similiar to the Linux kernel, lucky for me, it was quite obvious where to put things.

Driver

Create an empty file for now:

touch drivers/dac/dac_ltc166x.c

The driver will support both ltc1660 (10-bit, 8 channels) and ltc1665 (8-bit, 8 channels) DAC. I do not prefer to name drivers with an x as there actually are chips out there with an x in their name, so it could be a little fraudulent. That is at least something we try to avoid it in the Linux kernel.

A better name would be just dac_ltc1660.c and support all ICs that are compatible with dac_ltc1660. However, the Zephyr project has choosen to make use of the x in names to indicate that multiple chips are supported. When in Rome, do as the Romans do.

Add the file to the CMake build system:

diff --git a/drivers/dac/CMakeLists.txt b/drivers/dac/CMakeLists.txt
index b0e86e3bd4..800bc895fd 100644
--- a/drivers/dac/CMakeLists.txt
+++ b/drivers/dac/CMakeLists.txt
@@ -9,6 +9,7 @@ zephyr_library_sources_ifdef(CONFIG_DAC_SAM             dac_sam.c)
 zephyr_library_sources_ifdef(CONFIG_DAC_SAM0           dac_sam0.c)
 zephyr_library_sources_ifdef(CONFIG_DAC_DACX0508       dac_dacx0508.c)
 zephyr_library_sources_ifdef(CONFIG_DAC_DACX3608       dac_dacx3608.c)
+zephyr_library_sources_ifdef(CONFIG_DAC_LTC166X     dac_ltc166x.c)
 zephyr_library_sources_ifdef(CONFIG_DAC_SHELL          dac_shell.c)
 zephyr_library_sources_ifdef(CONFIG_DAC_MCP4725                dac_mcp4725.c)
 zephyr_library_sources_ifdef(CONFIG_DAC_MCP4728                dac_mcp4728.c)

CONFIG_DAC_LTC166X comes from the Kconfig system and could be either 'y' or 'n' dependig on if it's selected or not.

Kconfig

Create two new Kconfig configuration options. One for the driver itself and one for its init priority:

diff --git a/drivers/dac/Kconfig.ltc166x b/drivers/dac/Kconfig.ltc166x
new file mode 100644
index 0000000000..6053bc39bf
--- /dev/null
+++ b/drivers/dac/Kconfig.ltc166x
@@ -0,0 +1,22 @@
+# DAC configuration options
+
+# Copyright (C) 2023 Marcus Folkesson <marcus.folkesson@gmail.com>
+#
+# SPDX-License-Identifier: Apache-2.0
+
+config DAC_LTC166X
+       bool "Linear Technology LTC166X DAC"
+       default y
+       select SPI
    +       depends on DT_HAS_LLTC_LTC1660_ENABLED  || \
+               DT_HAS_LLTC_LTC1665_ENABLED
+       help
+         Enable the driver for the Linear Technology LTC166X DAC
+
+if DAC_LTC166X
+
+config DAC_LTC166X_INIT_PRIORITY
+       int "Init priority"
+       default 80
+       help
+         Linear Technology LTC166X DAC device driver initialization priority.
+
+endif # DAC_LTC166X

DT_HAS_LLTC_LTC1660_ENABLED and DT_HAS_LLTC_LTC1660_ENABLED is configuration options that is generated from the seleted devicetree. By depend on it, the DAC_LTC166X option will only show up if there are such a node specified. I really like this feature.

Also add it into the build stucture:

diff --git a/drivers/dac/Kconfig b/drivers/dac/Kconfig
index 7b54572146..77b0db902b 100644
--- a/drivers/dac/Kconfig
+++ b/drivers/dac/Kconfig
@@ -42,6 +42,8 @@ source "drivers/dac/Kconfig.dacx0508"

 source "drivers/dac/Kconfig.dacx3608"

+source "drivers/dac/Kconfig.ltc166x"
+
 source "drivers/dac/Kconfig.mcp4725"

 source "drivers/dac/Kconfig.mcp4728"

Device tree

The bindings for all devices has to be described in the YAML format. These bindings is verified during compile time in order to make sure that the device tree node fulfills all required properties and not tries to invent some new ones. This protects us against typos, which also is a really good feature. The Linux kernel does not have this...

We have to create such a binding, one for each chip:

diff --git a/dts/bindings/dac/lltc,ltc1660.yaml b/dts/bindings/dac/lltc,ltc1660.yaml
new file mode 100644
index 0000000000..196204236a
--- /dev/null
+++ b/dts/bindings/dac/lltc,ltc1660.yaml
@@ -0,0 +1,8 @@
+# Copyright (C) 2023 Marcus Folkesson <marcus.folkesson@gmail.com>
+# SPDX-License-Identifier: Apache-2.0
+
+include: [dac-controller.yaml, spi-device.yaml]
+
+description: Linear Technology Micropower octal 10-Bit DAC
+
+compatible: "lltc,ltc1660"
diff --git a/dts/bindings/dac/lltc,ltc1665.yaml b/dts/bindings/dac/lltc,ltc1665.yaml
new file mode 100644
index 0000000000..2c789ecc56
--- /dev/null
+++ b/dts/bindings/dac/lltc,ltc1665.yaml
@@ -0,0 +1,8 @@
+# Copyright (C) 2023 Marcus Folkesson <marcus.folkesson@gmail.com>
+# SPDX-License-Identifier: Apache-2.0
+
+include: [dac-controller.yaml, spi-device.yaml]
+
+description: Linear Technology Micropower octal 8-Bit DAC
+
+compatible: "lltc,ltc1665"

dac-controller.yaml and spi-device.yaml is included to inherit some of the required properties (such as spi-max-speed) for this of device.

Unit tests

Add the driver to the test framework and allow the test to be executed on the native_posix platform:

diff --git a/tests/drivers/build_all/dac/testcase.yaml b/tests/drivers/build_all/dac/testcase.yaml
index fa2eb5ac7a..1c7fa521d0 100644
--- a/tests/drivers/build_all/dac/testcase.yaml
+++ b/tests/drivers/build_all/dac/testcase.yaml
@@ -5,7 +5,7 @@ tests:
   drivers.dac.build:
     # will cover I2C, SPI based drivers
     platform_allow: native_posix
-    tags: dac_dacx0508 dac_dacx3608 dac_mcp4725 dac_mcp4728
+    tags: dac_dacx0508 dac_dacx3608 dac_mcp4725 dac_mcp4728 dac_ltc1660 dac_ltc1665
     extra_args: "CONFIG_GPIO=y"
   drivers.dac.mcux.build:
     platform_allow: frdm_k22f

Also add nodes in app.overlay to make it possible for the unit tests to instantiate the DAC:

diff --git a/tests/drivers/build_all/dac/app.overlay b/tests/drivers/build_all/dac/app.overlay
index 471bfae6e8..c1e9146974 100644
--- a/tests/drivers/build_all/dac/app.overlay
+++ b/tests/drivers/build_all/dac/app.overlay
@@ -68,6 +68,8 @@

                        /* one entry for every devices at spi.dtsi */
                        cs-gpios = <&test_gpio 0 0>,
+                                  <&test_gpio 0 0>,
+                                  <&test_gpio 0 0>,
                                   <&test_gpio 0 0>,
                                   <&test_gpio 0 0>;

@@ -118,6 +120,20 @@
                                channel6-gain = <0>;
                                channel7-gain = <0>;
                        };
+
+                       test_spi_ltc1660: ltc1660@3 {
+                               compatible = "lltc,ltc1660";
+                               reg = <0x3>;
+                               spi-max-frequency = <0>;
+                               #io-channel-cells = <1>;
+                       };
+
+                       test_spi_ltc1665: ltc1665@4 {
+                               compatible = "lltc,ltc1665";
+                               reg = <0x4>;
+                               spi-max-frequency = <0>;
+                               #io-channel-cells = <1>;
+                       };
                };
        };
 };

Summary

It are some work that needs to be done to integrate the driver into the Zephyr project. This has to be done for every driver.

In part3 we will start writing the driver code.

Write a device driver for Zephyr - Part 3

Write a device driver for Zephyr - Part 3

This is the third post in this series. See also part part1, part2 and part4.

Overview

In the previous part we prepared Zephyr for our soon to be born driver.

Now we have finally come to the fun point - write the actual driver code!

Driver API

I used to write code for the Linux kernel which is a little bit more complex kernel than Zephyr. The Zephyr driver API for DAC must be one of the most simpliest API:s I have ever seen.

You have to populate just only two functions in the struct dac_driver_api found in inlcude/zephyr/drivers/dac.h:

 * DAC driver API
 *
 * This is the mandatory API any DAC driver needs to expose.
 */
__subsystem struct dac_driver_api {
    dac_api_channel_setup channel_setup;
    dac_api_write_value   write_value;
};

Where channel_setup is used to configure the channel:

/**
 * @brief Configure a DAC channel.
 *
 * It's required to call this function and configure each channel before it's
 * selected for a write request.
 *
 * @param dev          Pointer to the device structure for the driver instance.
 * @param channel_cfg  Channel configuration.
 *
 * @retval 0         On success.
 * @retval -EINVAL   If a parameter with an invalid value has been provided.
 * @retval -ENOTSUP  If the requested resolution is not supported.
 */
typedef int (*dac_api_channel_setup)(const struct device *dev,
             const struct dac_channel_cfg *channel_cfg);

dac_channel_cfg specifies the channel and desired resolution:

/**
 * @struct dac_channel_cfg
 * @brief Structure for specifying the configuration of a DAC channel.
 *
 * @param channel_id Channel identifier of the DAC that should be configured.
 * @param resolution Desired resolution of the DAC (depends on device
 *                   capabilities).
 */
struct dac_channel_cfg {
    uint8_t channel_id;
    uint8_t resolution;
};

Our DAC supports 8 channels and 8bit or 10bit resolution.

write_value is rather self-explained:

/**
 * @brief Write a single value to a DAC channel
 *
 * @param dev         Pointer to the device structure for the driver instance.
 * @param channel     Number of the channel to be used.
 * @param value       Data to be written to DAC output registers.
 *
 * @retval 0        On success.
 * @retval -EINVAL  If a parameter with an invalid value has been provided.
 */
typedef int (*dac_api_write_value)(const struct device *dev,
                                uint8_t channel, uint32_t value);

It writes value to channel on dev.

Device tree

We have to create a device node that represent the DAC in order to make it available in Kconfig. During the build, we specified rpi_pico as board, remember?

west build -b rpi_pico ....

which uses the boards/arm/rpi_pico/rpi_pico.dts device tree. It's possible to add the DAC node directly to rpi_pico.dts, but it's strongly preferred to use overlays.

Device tree overlays

A Device tree overlay is a fragment of a device tree that extends or modifies the existing device tree. As we do not want to add the DAC to all rpi_pico boards, but only to those that actually have it connected, overlays is the way to go.

Device tree overlays can be specified in two ways:

  • DTC_OVERLAY_FILE or
  • .overlay files

The CMake variable DTC_OVERLAY_FILE contains a space- or semicolon-separated list of overlay files that will be used to overlay the device tree.

.overlay files on the other hand, is overlays that the build system automatically will pickup in the following order:

  1. If the file boards/<BOARD>.overlay exists, it will be used.
  2. If the current board has multiple revisions and boards/<BOARD>_<revision>.overlay exists, it will be used. This file will be used in addition to boards/<BOARD>.overlay if both exist.
  3. If one or more files have been found in the previous steps, the build system stops looking and just uses those files.
  4. Otherwise, if <BOARD>.overlay exists, it will be used, and the build system will stop looking for more files.
  5. Otherwise, if app.overlay exists, it will be used.

Our device tree overlay looks as follow:

 &spi0 {
    dac0: dac0@0 {
        compatible = "lltc,ltc1665";
        reg = <0>;
        spi-max-frequency = <1000000>;
        duplex = <0>;
        #io-channel-cells = <8>;
        status = "okay";
    };
};
  • compatible is matching against our driver
  • reg specify chip select 0
  • spi-max-frequency is set to 1MHz
  • duplex specifies duplex mode, 0 equals full duplex
  • status is set to "okay"

Configuration

Once the DAC is added to the device tree, it's time enable the driver in the configuration as well.

Start menuconfig:

west build -t menuconfig

Navigate to:

Device Drivers -> Digital-to-Analog Converters (DAC) drivers -> Linear Technology LTC166X DAC and add support for the driver.

/media/zephyr-menuconfig.png

(What the heck have they done to menuconfig by the way?! It does not behave nor looks like it used to.)

The driver

The chip itself is quite simple and that is reflected in the driver.

Here is the complete driver code:

/*
 * Driver for Linear Technology LTC1660/LTC1665  DAC
 *
 * Copyright (C) 2023 Marcus Folkesson <marcus.folkesson@gmail.com>
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/kernel.h>
#include <zephyr/drivers/spi.h>
#include <zephyr/drivers/dac.h>
#include <zephyr/logging/log.h>

LOG_MODULE_REGISTER(dac_ltc166x, CONFIG_DAC_LOG_LEVEL);

#define LTC166X_REG_MASK               GENMASK(15, 12)
#define LTC166X_DATA8_MASK             GENMASK(11, 4)
#define LTC166X_DATA10_MASK            GENMASK(12, 2)

struct ltc166x_config {
    struct spi_dt_spec bus;
    uint8_t resolution;
    uint8_t nchannels;
};

static int ltc166x_reg_write(const struct device *dev, uint8_t addr,
            uint32_t data)
{
    const struct ltc166x_config *config = dev->config;
    uint16_t regval;

    regval = FIELD_PREP(LTC166X_REG_MASK, addr);

    if (config->resolution == 10) {
        regval |= FIELD_PREP(LTC166X_DATA10_MASK, data);
    } else {
        regval |= FIELD_PREP(LTC166X_DATA8_MASK, data);
    }

    const struct spi_buf buf = {
            .buf = &regval,
            .len = sizeof(regval),
    };

    struct spi_buf_set tx = {
        .buffers = &buf,
        .count = 1,
    };

    return spi_write_dt(&config->bus, &tx);
}


static int ltc166x_channel_setup(const struct device *dev,
                   const struct dac_channel_cfg *channel_cfg)
{
    const struct ltc166x_config *config = dev->config;

    if (channel_cfg->channel_id > config->nchannels - 1) {
        LOG_ERR("Unsupported channel %d", channel_cfg->channel_id);
        return -ENOTSUP;
    }

    if (channel_cfg->resolution != config->resolution) {
        LOG_ERR("Unsupported resolution %d", channel_cfg->resolution);
        return -ENOTSUP;
    }

    return 0;
}

static int ltc166x_write_value(const struct device *dev, uint8_t channel,
                uint32_t value)
{
    const struct ltc166x_config *config = dev->config;

    if (channel > config->nchannels - 1) {
        LOG_ERR("unsupported channel %d", channel);
        return -ENOTSUP;
    }

    if (value >= (1 << config->resolution)) {
        LOG_ERR("Value %d out of range", value);
        return -EINVAL;
    }

    return ltc166x_reg_write(dev, channel + 1, value);
}

static int ltc166x_init(const struct device *dev)
{
    const struct ltc166x_config *config = dev->config;

    if (!spi_is_ready_dt(&config->bus)) {
        LOG_ERR("SPI bus %s not ready", config->bus.bus->name);
        return -ENODEV;
    }
    return 0;
}

static const struct dac_driver_api ltc166x_driver_api = {
    .channel_setup = ltc166x_channel_setup,
    .write_value = ltc166x_write_value,
};


#define INST_DT_LTC166X(inst, t) DT_INST(inst, lltc_ltc##t)

#define LTC166X_DEVICE(t, n, res, nchan) \
    static const struct ltc166x_config ltc##t##_config_##n = { \
        .bus = SPI_DT_SPEC_GET(INST_DT_LTC166X(n, t), \
            SPI_OP_MODE_MASTER | \
            SPI_WORD_SET(8), 0), \
        .resolution = res, \
        .nchannels = nchan, \
    }; \
    DEVICE_DT_DEFINE(INST_DT_LTC166X(n, t), \
                &ltc166x_init, NULL, \
                NULL, \
                &ltc##t##_config_##n, POST_KERNEL, \
                CONFIG_DAC_LTC166X_INIT_PRIORITY, \
                &ltc166x_driver_api)

/*
 * LTC1660: 10-bit
 */
#define LTC1660_DEVICE(n) LTC166X_DEVICE(1660, n, 10, 8)

/*
 * LTC1665: 8-bit
 */
#define LTC1665_DEVICE(n) LTC166X_DEVICE(1665, n, 8, 8)

#define CALL_WITH_ARG(arg, expr) expr(arg)

#define INST_DT_LTC166X_FOREACH(t, inst_expr) \
    LISTIFY(DT_NUM_INST_STATUS_OKAY(lltc_ltc##t), \
             CALL_WITH_ARG, (), inst_expr)

INST_DT_LTC166X_FOREACH(1660, LTC1660_DEVICE);
INST_DT_LTC166X_FOREACH(1665, LTC1665_DEVICE);

Most of the driver part should be rather self-explained. The driver consists of only four functions:

  • ltc166x_reg_write: write data to actual register.
  • ltc166x_channel_setup: validate channel configuration provided by application.
  • ltc166x_write_vale: validate data from application and then call ltc166x_reg_write.
  • ltc66x_init: make sure that the SPI bus is ready. Used by DEVICE_DT_DEFINE.

The only tricky part is the macro-magic that is used for device registration:

#define INST_DT_LTC166X(inst, t) DT_INST(inst, lltc_ltc##t)

#define LTC166X_DEVICE(t, n, res, nchan) \
    static const struct ltc166x_config ltc##t##_config_##n = { \
        .bus = SPI_DT_SPEC_GET(INST_DT_LTC166X(n, t), \
            SPI_OP_MODE_MASTER | \
            SPI_WORD_SET(8), 0), \
        .resolution = res, \
        .nchannels = nchan, \
    }; \
    DEVICE_DT_DEFINE(INST_DT_LTC166X(n, t), \
                &ltc166x_init, NULL, \
                NULL, \
                &ltc##t##_config_##n, POST_KERNEL, \
                CONFIG_DAC_LTC166X_INIT_PRIORITY, \
                &ltc166x_driver_api)

/*
 * LTC1660: 10-bit
 */
#define LTC1660_DEVICE(n) LTC166X_DEVICE(1660, n, 10, 8)

/*
 * LTC1665: 8-bit
 */
#define LTC1665_DEVICE(n) LTC166X_DEVICE(1665, n, 8, 8)

#define CALL_WITH_ARG(arg, expr) expr(arg)

#define INST_DT_LTC166X_FOREACH(t, inst_expr) \
    LISTIFY(DT_NUM_INST_STATUS_OKAY(lltc_ltc##t), \
             CALL_WITH_ARG, (), inst_expr)

INST_DT_LTC166X_FOREACH(1660, LTC1660_DEVICE);
INST_DT_LTC166X_FOREACH(1665, LTC1665_DEVICE);

Which became even more trickier as I wanted the driver to support both LTC1660 and LTC1665. To give some clarity, this is what happens:

  • INST_DT_LTC166X_FOREACH expands for each node compatible with "lltc,ltc1660" or "lltc,ltc1665" in the devicetree.
  • A struct ltc166x_config will be created for each instance and populated by the arguments provided by LTC1665_DEVICE or LTC1660_DEVICE.
  • The ltc166x_driver_api struct is common for all instances.
  • DEVICE_DT_DEFINE creates a device object and set it up for boot time initialization.

The documentation [1] describe these macros more in depth.

Test of the driver

Zephyr has a lot of sample applicatons. I used samples/drivers/dac/src/main.c to test my driver

/*
 * Copyright (c) 2020 Libre Solar Technologies GmbH
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <zephyr/kernel.h>
#include <zephyr/sys/printk.h>
#include <zephyr/drivers/dac.h>

#define ZEPHYR_USER_NODE DT_PATH(zephyr_user)

#if (DT_NODE_HAS_PROP(ZEPHYR_USER_NODE, dac) && \
        DT_NODE_HAS_PROP(ZEPHYR_USER_NODE, dac_channel_id) && \
        DT_NODE_HAS_PROP(ZEPHYR_USER_NODE, dac_resolution))
#define DAC_NODE DT_PHANDLE(ZEPHYR_USER_NODE, dac)
#define DAC_CHANNEL_ID DT_PROP(ZEPHYR_USER_NODE, dac_channel_id)
#define DAC_RESOLUTION DT_PROP(ZEPHYR_USER_NODE, dac_resolution)
#else
#error "Unsupported board: see README and check /zephyr,user node"
#define DAC_NODE DT_INVALID_NODE
#define DAC_CHANNEL_ID 0
#define DAC_RESOLUTION 0
#endif

static const struct device *const dac_dev = DEVICE_DT_GET(DAC_NODE);

static const struct dac_channel_cfg dac_ch_cfg = {
    .channel_id  = DAC_CHANNEL_ID,
    .resolution  = DAC_RESOLUTION
};

void main(void)
{
    if (!device_is_ready(dac_dev)) {
        printk("DAC device %s is not ready\n", dac_dev->name);
        return;
    }

    int ret = dac_channel_setup(dac_dev, &dac_ch_cfg);

    if (ret != 0) {
        printk("Setting up of DAC channel failed with code %d\n", ret);
        return;
    }

    printk("Generating sawtooth signal at DAC channel %d.\n",
        DAC_CHANNEL_ID);
    while (1) {
        /* Number of valid DAC values, e.g. 4096 for 12-bit DAC */
        const int dac_values = 1U << DAC_RESOLUTION;

        /*
         * 1 msec sleep leads to about 4 sec signal period for 12-bit
         * DACs. For DACs with lower resolution, sleep time needs to
         * be increased.
         * Make sure to sleep at least 1 msec even for future 16-bit
         * DACs (lowering signal frequency).
         */
        const int sleep_time = 4096 / dac_values > 0 ?
            4096 / dac_values : 1;

        for (int i = 0; i < dac_values; i++) {
            ret = dac_write_value(dac_dev, DAC_CHANNEL_ID, i);
            if (ret != 0) {
                printk("dac_write_value() failed with code %d\n", ret);
                return;
            }
            k_sleep(K_MSEC(sleep_time));
        }
    }
}

The application generates a saw-tooth signal on DAC_CHANNEL_ID. Here is the result:

/media/zephyr-sawtooth.jpg

Looks great!

Summary

The implementation of the driver was quite straigt forward. The only part I was actually struggle with was the macros. But in fact, most of the problems I had was due to local build caches. The wierd errors I had disappeard when I did rebuild the whole project. Hrmf.

In part4 of this series we will look on howto contribute this driver back to the Zephyr project.

Write a device driver for Zephyr - Part 4

Write a device driver for Zephyr - Part 4

This is the forth post in this series. See also part part1, part2 and part3.

Overview

This is the forth and last part of this series where we will focus on contribute the driver back to the Zephyr project.

Zephyr use Github for hosting the project and all contribution is by Pull Requests. The process is all well documented [1], both on how to contribute but also what the project expect from you as a contributor.

I'm not really a fan of Github. I prefer to send patches by mail and handle all communication that way, but I probably have to realize soon that I'm just getting old and grumpy (needless to say that I prefer IRC over all other chat systems for instant messaging?).

Split up the changes

As we touch multiple areas of the project, we have to break up the changes into multiple commits. This pull request will contain three commits:

Author: Marcus Folkesson <marcus.folkesson@gmail.com>
Date:   Wed Apr 5 14:21:47 2023 +0200

    dts: bindings: dac: add bindings for ltc1660/ltc1665

    Add bindings for LTC1665/LTC1660, which is a 8/10-bit
    Digital-to-Analog Converter with eight individual channels.

    Signed-off-by: Marcus Folkesson <marcus.folkesson@gmail.com>

commit 6dec8308528a6a5fdf123a8bc24e75ba3e0e8cbd
Author: Marcus Folkesson <marcus.folkesson@gmail.com>
Date:   Wed Apr 5 14:18:00 2023 +0200

    tests: build_all: add entries for ltc1660/ltc1665

    Add the new DAC-drivers to the test suite.

    Signed-off-by: Marcus Folkesson <marcus.folkesson@gmail.com>

commit b66b7aade39b79fb3d6194be1b6414491f57a828
Author: Marcus Folkesson <marcus.folkesson@gmail.com>
Date:   Wed Apr 5 14:16:13 2023 +0200

    drivers: dac: add support for ltc1660/ltc1665

    LTC1665/LTC1660 is a 8/10-bit Digital-to-Analog Converter
    (DAC) with eight individual channels.

    Signed-off-by: Marcus Folkesson <marcus.folkesson@gmail.com>

One miss I see right now as I'm writing this blog post is the commit order. The device tree bindings and the test suite should swap order as the test depends on the bindings.

However, the PR is already merged.

Requirements on the PR

The Zephyr project has several requirements on each pull request, these are:

  • Each commit in the PR must provide a commit message following the Commit Message Guidelines.
  • All files in the PR must comply with Licensing Requirements.
  • Follow the Zephyr Coding Style and Coding Guidelines.
  • PRs must pass all CI checks. This is a requirement to merge the PR. Contributors may mark a PR as draft and explicitly request reviewers to provide early feedback, even with failing CI checks.
  • When breaking a PR into multiple commits, each commit must build cleanly. The CI system does not enforce this policy, so it's the PR author’s responsibility to verify.
  • When major new functionality is added, tests for the new functionality shall be added to the automated test suite. All API functions should have test cases and there should be tests for the behavior contracts of the API. Maintainers and reviewers have the discretion to determine if the provided tests are sufficient. The examples below demonstrate best practices on how to test APIs effectively.
  • Kernel timer tests provide around 85% test coverage for the kernel timer , measured by lines of code.
  • Emulators for off-chip peripherals are an effective way to test driver APIs. The fuel gauge tests use the smart battery emulator , providing test coverage for the fuel gauge API and the smart battery driver .
  • Code coverage reports for the Zephyr project are available on Codecov.
  • Incompatible changes to APIs must also update the release notes for the next release detailing the change. APIs marked as experimental are excluded from this requirement.
  • Changes to APIs must increment the API version number according to the API version rules.
  • PRs must also satisfy all Merge Criteria before a member of the release engineering team merges the PR into the zephyr tree.

This may look overwelming for some, but lets break down some of the requirements.

Commit message Guidelines

All commits should have the following format:

[area]: [summary of change]

[Commit message body (must be non-empty)]

Signed-off-by: [Your Full Name] <[your.email@address]>

This is more of a common sense rather than something specific for the Zephyr project.

The Signed-off-by: tag should be used for open source licensing reasons. By adding the tag you agree to the Developer Certificate of Origin (DCO) [3]:

Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

  1. The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or
  2. The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I'm permitted to submit under a different license), as Indicated in the file; or
  3. The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it.
  4. I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved.

License requirements

Zephyr uses the Apache 2.0 license [4] which is a permissive open source license that allows you to freely use, modify, distribute and sell your own produduct that include Apache 2.0 licensed software.

The license is specified by a SPDX tag in the header of each source file. E.g.:

/*
 * Copyright (c) 2020 Libre Solar Technologies GmbH
 *
 * SPDX-License-Identifier: Apache-2.0
 */

Coding style

All projects has its own coding styles guidelines [5]. Read those carefully. The comment I got on my pull request [2] was just regarding the coding style:

/media/zephyr-remarks.jpg

Final words

My initial thought with this blog series was to give Zephyr another chance since my evaluation didn't go well the first time.

Many people and organizations do use open source for several (good) reasons, but too few actually contribute back to the projects they make use of. Sometimes it's the company culture that doesn't encourage or see the value in it, but mostly it's just a matter of insecurity on the part of the individual developer.

Therefore, this series changed the focus from purely evaluating Zephyr to instead focusing on all the steps I took to get my code into a project I'm quite unfamiliar with. I even changed the blog subject from "First look into Zephyr" to "Write a device driver for Zephyr".

Hopefully it helps someone see that it's not impossible to actually join in and contribute.

/media/zephyr-merged.jpg