meta-readonly-rootfs-overlay

meta-readonly-rootfs-overlay

meta-readonly-rootfs-overlay [1] is a meta layer for the Yocto project [2] originally written by Claudius Heine. I took over the maintainership in May 2022 to keep it updated with recent Yocto releases and keep add functionality.

I've implemented it in a couple of industrial products so far and think it needs some extra attention as I find it so useful.

Why does this exists?

Having a read-only root file system is useful for many scenarios:

  • Separate user specific changes from system configuration, and being able to find differences
  • Allow factory reset, by deleting the user specific changes
  • Have a fallback image in case the user specific changes made the root file system no longer bootable.

Because some data on the root file system changes on first boot or while the system is running, just mounting the complete root file system as read-only breaks many applications. There are different solutions to this problem:

  • Symlinking/Bind mounting files and directories that could potentially change while the system is running to a writable partition
  • Instead of having a read-only root files system, mounting a writable overlay root file system, that uses a read-only file system as its base and writes changed data to another writable partition.

To implement the first solution, the developer needs to analyse which file needs to change and then create symlinks for them. When doing factory reset, the developer needs to overwrite every file that is linked with the factory configuration, to avoid dangling symlinks/binds. While this is more work on the developer side, it might increase the security, because only files that are symlinked/bind-mounted can be changed.

This meta-layer provides the second solution. Here no investigation of writable files are needed and factory reset can be done by just deleting all files or formatting the writable volume.

How does it work?

The implementation make use of OverlayFS [3], which is a union mount filesystem that combines multiple underlying mount points into one. The filesystem make use of the terms upper and lower filesystem where the upper is filesystem is applied as an overlay on the lower filesystem.

The resulting merge directory is a combination of these two where all files in the upper filesystem overrides all files in the lower.

/media/meta-readonly-rootfs-overlay.png

Dependencies

This layer only depends on:

URI: git://git.openembedded.org/bitbake
branch: kirkstone

and

URI: git://git.openembedded.org/openembedded-core
layers: meta
branch: kirkstone

Usage

Adding the readonly-rootfs-overlay layer to your build

In order to use this layer, you need to make the build system aware of it.

Assuming the readonly-rootfs-overlay layer exists at the top-level of your OpenEmbedded source tree, you can add it to the build system by adding the location of the readonly-rootfs-overlay layer to bblayers.conf, along with any other layers needed. e.g.:

BBLAYERS ?= " \
  /path/to/layers/meta \
  /path/to/layers/meta-poky \
  /path/to/layers/meta-yocto-bsp \
  /path/to/layers/meta-readonly-rootfs-overlay \
  "

To add the script to your image, just add:

IMAGE_INSTALL:append = " initscripts-readonly-rootfs-overlay"

to your local.conf or image recipe. Or use core-image-rorootfs-overlay-initramfs as initrd.

Read-only root filesystem

If you use this layer you do not need to set read-only-rootfs in the IMAGE_FEATURES or EXTRA_IMAGE_FEATURES variable.

Kernel command line parameters

These examples are not meant to be complete. They just contain parameters that are used by the initscript of this repository. Some additional paramters might be necessary.

Example using initrd

root=/dev/sda1 rootrw=/dev/sda2

This cmd line start /sbin/init with the /dev/sda1 partition as the read-only rootfs and the /dev/sda2 partition as the read-write persistent state.

root=/dev/sda1 rootrw=/dev/sda2 init=/bin/sh

The same as before but it now starts /bin/sh instead of /sbin/init.

Example without initrd

root=/dev/sda1 rootrw=/dev/sda2 init=/init

This cmd line starts /sbin/init with /dev/sda1 partition as the read-only rootfs and the /dev/sda2 partition as the read-write persistent state. When using this init script without an initrd, init=/init has to be set.

root=/dev/sda1 rootrw=/dev/sda2 init=/init rootinit=/bin/sh

The same as before but it now starts /bin/sh instead of /sbin/init

Details

All kernel parameters that is used to configure meta-readonly-rootfs-overlay:

  • root - specifies the read-only root file system device. If this is not specified, the current rootfs is used.
  • `rootfstype if support for the read-only file system is not build into the kernel, you can specify the required module name here. It will also be used in the mount command.
  • rootoptions specifies the mount options of the read-only file system. Defaults to noatime,nodiratime.
  • rootinit if the init parameter was used to specify this init script, rootinit can be used to overwrite the default (/sbin/init).
  • rootrw specifies the read-write file system device. If this is not specified, tmpfs is used.
  • rootrwfstype if support for the read-write file system is not build into the kernel, you can specify the required module name here. It will also be used in the mount command.
  • rootrwoptions specifies the mount options of the read-write file system. Defaults to rw,noatime,mode=755.
  • rootrwreset set to yes if you want to delete all the files in the read-write file system prior to building the overlay root files system.

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)

Route priorities - metric values

Route priorities - metric values

Brief

It's not an uncommon scenario that a Linux system has several network interfaces that are all up and routeable. For example, consider a laptop with both Ethernet and WiFi.

But how does the system determine which route to use when trying to reach another host?

I was up to setup a system with both a 4G modem and a WiFi connection. My use case was that when the WiFi is available, that interface should be prioritized over 4G. This achieved by adjusting the route metric values for those interfaces.

/media/route-metric.png

Metric values

The metric value is one of many fields in the routing table and indicates the cost of the route. This become useful if multiple routes exists to a given destination and the system has to make a decision on which route to use. With that said, the lower metric value (lower cost) a route have, the highter priority i gets.

It's up to you or your network manager to set proper metric values for your routes. The actual value could be determine based on several different factors depending on what is important for your setup. E.g:

  • Hop count - The number of routes (hops) in a path to reach a certein network. This is a common metric.
  • Delay - Some interfaces have higher delays than others. Compare a 4G modem with a fiber connecton.
  • Throughput - The expected throughput of the route.
  • Reliability - If some links are more prone på link failures than others, prefer to use other interfaces.

The ip route command will show you all the routes that your system currently have, the last number in the output is the metric value:

$ ip route
default via 192.168.20.1 dev enp0s13f0u1u4 proto dhcp src 192.168.20.173 metric 100
default via 192.168.20.1 dev wlp0s20f3 proto dhcp src 192.168.20.197 metric 600

I have two routes that both is routed via 192.168.20.1.

As you can see, my wlp0s20f3 (Wireless) interface has a higher metric value than my enp0s13f0u1u4 (Ethernet) interface, which will cause the system to choose the ethernet interface over WiFi. In my case, these values are chosen by NetworkManager.

Set metric value

If you want to set specific metric values for your routes, the way will differ depending on how your routes are created.

iproute2

The ip command could be handy to manually create or change the metric value for a certain route:

$ ip route replace default via {IP} dev {DEVICE} metric {METRIC}

ifmetric

ifmetric is a tool for setting the metric value for IPv4 routes attached to a given network interface. Compared to the raw ip command above, ifmetric works on interfaces rather than routes.

$ ifmetric INTERFACE [METRIC]

dhcpcd

Metric values could be set in /etc/dhcpcd.conf according to the manual [1]:

metric metric
Metrics are used to prefer an interface over another one, lowest wins.

e.g.:

interface wlan0
metric 200

If no metric value is given, the default metric is calculated by 200 + if_nametoindex(3). An extra 100 will be added for wireless interfaces.

NetworkManager

Add ipv4.route-metric METRIC to your /etc/NetworkManager/system-connections/<connection>.nmconnection file.

You could also use the command line tool:

    $ nmcli connection edit tuxnet

    ===| nmcli interactive connection editor | ===

    Editing existing '802-11-wireless' connection: 'tuxnet'

    Type 'help' or '?' for available commands.
    Type 'print' to show all the connection properties.
    Type 'describe [<setting>.<prop>]' for detailed property description.

    You may edit the following settings: connection, 802-11-wireless (wifi), 802-11-wireless-security (wifi-sec), 802-1x, ethtool, match, ipv4, ipv6, tc, proxy
    nmcli> set ipv4.route-metric 600
    nmcli> save
    nmcli> quit

PPPD

PPP is a protocol used for establishing internet links over dial-up modems. These links is usually not the preferred link when the device has other more reliable and/or cheaper connections.

The pppd daemon has a few options as specified in the manual [2] for creating a default route and set the metric value:

defaultroute
       Add a default route to the system routing tables, using
       the peer as the gateway, when IPCP negotiation is
       successfully completed.  This entry is removed when the
       PPP connection is broken.  This option is privileged if
       the nodefaultroute option has been specified.

defaultroute-metric
       Define the metric of the defaultroute and only add it if
       there is no other default route with the same metric.
       With the default value of -1, the route is only added if
       there is no default route at all.

replacedefaultroute
       This option is a flag to the defaultroute option. If
       defaultroute is set and this flag is also set, pppd
       replaces an existing default route with the new default
       route.  This option is privileged.

E.g.

replacedefaultroute
defaultroute-metric 900

Summary

It's not that often you actually have to set the metric value yourself. The network manager usually does a great job.

In my system, the NetworkManager did not manage the PPP interface so its metric-logic did not apply to that interface. Therefor I had to let pppd create a default route with a fixed metric.

Lund Linux Conference 2023

Lund Linux Conference 2023

The conference

Lund Linux Conference (LLC) [1] is a "half-open" conference located in Lund. It's a conference with with high quality and I appreciate that the athmosphere is more familiar than at the larger conferences. I've been at the conference a couple of times before and the quality on the talks this year was as good as usual. ( The talks are by the way availalble on Youtube [3].)

We are growing though. Axis generously assists with premisses, but it remains to be seen wether we will get place next year.

Anyway, I took some notes as usual, and this blog post is nothing more than the notes I took during the talk.

The RISC-V Linux port; past/current/next

Björn Töpel talked about the current status of RISC-V architecture in the Linux kernel.

For those who don't know - RISC-V is a open and royalty free Instruction Set Architecture. In practice, this means for example that whenever you want to implement your own CPU core in your FPGA, you are free to do so using the RISC-V ISA. Compare that to ARM that you are strictly not allowed to even think about it without pay royalties and other fees.

RISC-V is a rather new port, the first proposol was sent out to the mailing list in 2016. It makes it a pretty good target to get involved into if you want to get to know the kernel in-depth as the implementation is still quite small in lines of code, which makes it easier to overview.

Björn told us that kernel support for RISC-V has made huge progress in the embedded area, but still lack some important functionality to be useful on the server side. Parts that are missing is e.g. support for ACPI, EUFI, AP-TEE, hotplugs and an advanced interrupt controller.

The architecture gets more support for each kernel release though. Some of the news for RISC-V in linux v6.4 are:

  • Support for Kernel Adress Space Layout Randomization (KASLR)
  • Relocatable kernel
  • HWprobe syscall

Vector support is on its way, but it currently break the ABI, so there are a few things left that needs to be addressed before we can expect any merge.

One giant leap for security: leveraging capabilities in Linux

Kevin Brodsky talked about self aware pointers, which I found interresting. That we can use address bits for other purposes than addresses is nothing new. In a 64bit ARM kernel we do often use only 52bits anyway (4PiB of addressable memory is more than enough for all people(phun intended )).

What Kevin and his team has done is to extend the address to 129bits to even include meta data for boundaries, capabilities and validity tags. The 129bits reservaton has of course a huge impact on the system as it use more than double the size compared to a normal 64-bit system, but it also gives us much in return.

These 129 bits is by the way already a compressed version of the 256 bit variant they started with..

Unfortunately, the implementation is for userpace only, which is a little sad because we already have tons of tools to run application in a protected and constrained environment, but it proves that the concept works and maybe we will see something like this for kernel space in the future.

The implementation requires changes is several parts of the system. The memory allocator and unwind code is most affected, but even the kernel itself and glibc has to be modified. Most of the applications and libraries is not affected at all though.

There is a working meta-layer for Yocto called Morello that can be used to test it out. It contains a usage guide and even a little tutorial on howto build and run Doom :-)

Supporting zoned storage in Ublk

Andreas Hindborg has been working with support for zoned storage [2] in the ublk driver. Zoned storage is basically about to spit the address space into regions called zones that can only be written sequentially. This leads to higher throughput and increased capacity. It also eliminates the need for a Flash Translation Layer (FTL) for e.g. SSD devices.

ublk make use of io_uring internally, which by the way is a cool feature. The io_uring let you queue system calls into a ring buffer, which makes it possible to do more work every time you enter the kernel space. This has impact on the performance as you do not need to context switch back and forth to userspace between each system call.

It's quite easy to add support for io_uring operations to normal character devices, as the struct file_operation now has a uring_cmd callback function that could be populated. This makes it to a high performance alternative to the IOCTL we are used to.

ublk is used to create a block device driver in userspace. It works as all requests and results to/from the block device is redirected to a userspace daemon. The userspace daemon used for this is called ublk-rs, which is entirely written i Rust (of course..). Unfortunately, the source code is not yet available due to legal reasons, but is on its way.

His work was to add support for zoned storage (basically split the address space into regions called zones)

Rust

Then there was a couple of talks about the most hip programming language for now; Rust.

Linus Walleij gave us a history lecture in programming languages in his talk "Rust: Abstraction and Productivity" and his thoughts aout why Rust could be something good for kernel. Andreas Hindborg continued and showed how he implemented a null_blk driver completely in Rust.

But why should we even consider Rust for the kernel? In fact, the language is guaranteed to have a few properties C does not, and we basic Rust support was introduced in Linux v6.1.

We say that Rust is safe, and when we state that, we think of that Rust does have:

  • No buffer overflows
  • No use after free
  • No dereferencing null or invalid pointers
  • No double free
  • No pointer aliasing
  • No type errors
  • No data races
  • ... and more

What was new to me is that a Rust application does not even compile if you try something of the above.

This together makes Rust both memory safe, type safe and thread safe. Consider that 20-60% of the bug fixes in the kernel are for memory safety bugs. These memory bugs takes a lot of productivity away as it often takes long time to find and fix them. Maybe Rust is not that bad after all.

Many cool projects are going on in Rust, example on those are:

  • TLS handshake in the kernel
  • Ethernet-SPI drivers
  • M1&M3 GPU drivers.

The goal with Andreas null_blk driver is to first write a solid Rust API for the blk-mq implementation and then use it in the null_blk driver to provide a reference implementation for linux kernel developers to get started with.

Summary

This was far from all talks, but only those that I had some taken some meaningful notes from.

Hope to see you there next year!

/media/lund-linuxcon-2018.jpg

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

Encrypted storage on i.MX

Encrypted storage on i.MX

Brief

Many embedded Linux systems does have some kind of sensitive information on a file storage. It could be private keys, passwords or whatever. It's always a risk that this information could be revealed by an unauthorized person that got their physical hands on the device. The only protection against attackers that who simply bypass the system and access the data storage directly is encryption.

Let's say that we encrypt our sensitive data. Where should we then store the decryption key?

We need to store even that sensitive key on a secure place.

i.MX CAAM

Most of the i.MX SoCs has the Cryptographic Accelerator and Assurance Module (CAAM). This includes both the i.MX6 and i.MX8 SoCs series. The only i.MX SoC that I have worked with that does not have the CAAM module is i.MX6ULL, but there could be more.

The CAAM module does have many use cases and one of those is to generate and handle secure keys. Secure keys that we could use to encrypt/decrypt a file, partition or a whole disk.

Device mapper

Device mapper is a framework that adds an extra abstraction layer on block devices that lets you create virtual block devices to offer additional features. Such features could be snapshots, RAID, or as in our case, disc encryption.

As you can see in the picture below, the device mapper is a layer in between the Block layer and the Virtual File System (VFS) layer:

/media/device-mapper.png

The Linux kernel does support a bunch of different mappers. The current kernel (v6.2) does support the following mappers [1]:

  • dm-delay
  • dm-clone
  • dm-crypt
  • dm-dust
  • dm-ebs
  • dm-flakey
  • dm-ima
  • dm-integrity
  • dm-io
  • dm-queue-length
  • dm-raid
  • dm-service-time
  • dm-zoned
  • dm-era
  • dm-linear
  • dm-log-writes
  • dm-stripe
  • dm-switch
  • dm-verity
  • dm-zero

Where dm-crypt [2] is the one we will focus on. One cool feature of device mappers is that those are stackable. You could for example use dm-crypt on top of a dm-raid mapping. How cool isn't that?

DM-Crypt

DM-Crypt is a device mapper implementation that uses the Crypto API [3] to transparently encrypt/decrypt all access to the block device. Once the device is mounted, all users will not even notice that the data read/written to that mount point is encrypted.

Normally you will use cryptsetup [4] or cryptmount [5] as those are the preferred way to handle the dm-crypt layer. For this we will use dmsetup though, which is a very low level (and difficult) tool to use.

CAAM Secure Keys

Now it's time to answer the question in the introduction section;

Let's say that we encrypt our sensitive data. Where should we then store the decryption key?

The CAAM module has a way to handle these keys in a secure way by store the keys in a protected area that is only readable by the CAAM module itself. In other word, it's not even possible to read out the key. Together with dm-crypt, we can create a master key that will never leave this protected area. On each boot, we will generate a derived (session) key that is the key we could use from userspace. These session keys are called black keys.

How to use it?

Installation

We need to build and install keyctl_caam in order to generate black keys and encapsulate it into a black blob. Download the source code:

git clone https://github.com/nxp-imx/keyctl_caam.git
cd keyctl_caam

And build:

CC=aarch64-linux-gnu-gcc make

I build with a external toolchain prefixed with aarch64-linux-gnu-. If you have a Yocto environment, you could use the toolchain from that SDK instead by use the environment setup script, e.g.:

./environment-setup-aarch64-poky-linux
make

You also have to make sure that the following kernel configurations is enabled:

CONFIG_BLK_DEV_DM=y
CONFIG_BLK_DEV_MD=y
CONFIG_MD=y
CONFIG_DM_CRYPT=y
CONFIG_DM_MULTIPATH=y
CONFIG_CRYPTO_DEV_FSL_CAAM_TK_API=y

Usage

Create a black key from random data, use ECB encryption:

caam-keygen create randomkey ecb -s 16

The file is written to the /data/caam/ folder unless the application is built to use another location (specified with KEYBLOB_LOCATION). Two files should now been generated:

ls -l /data/caam/
total 8
-rw-r--r-- 1 root root 36 apr 3 21.09 randomkey
-rw-r--r-- 1 root root 96 apr 3 21.09 randomkey.bb

Add the generated black key to the kernel key retention service. To this we use the keyctl command:

cat /data/caam/randomkey | keyctl padd logon logkey: @s

Create a deivce-mapper device named $ENCRYPTED_LABEL and map it to the block device $DEVICE:

dmsetup -v create $ENCRYPTED_LABEL --table "0 $(blockdev --getsz $DEVICE) crypt capi:tk(cbc(aes))-plain :36:logon:logkey: 0 $DEVICE 0 1 sector_size:512"

Create a filesystem on our newly created mapper device:

mkfs.ext4 -L $VOLUME_LABEL /dev/mapper/$ENCRYPTED_LABEL

Mount it on $MOUNT_POINT:

mount /dev/mapper/$ENCRYPTED_LABEL ${MOUNT_POINT}

Congrats! Your encrypted device is now ready to use! All data written to $MOUNT_POINT will be encrypted on the fly and decrypted upon read.

To illustrate this, create a file on the encrypted volume:

echo "Encrypted data" > ${MOUNT_POINT}/encrypted-file

Clean up and reboot:

umount $MOUNT_POINT
dmsetup remove $ENCRYPTED_LABEL
keyctl clear @s
reboot

A new session key will be generated upon each cold boot. So we have to import the key from the blob and add it to the key retention service. We also have to create the device mapper. This has to be done at each boot:

caam-keygen import $KEYPATH/$KEYNAME.bb $IMPORTKEY
cat $IMPORTKEYPATH/$IMPORTKEY | keyctl padd logon logkey: @s
dmsetup -v create $ENCRYPTED_LABEL --table "0 $(blockdev --getsz $DEVICE) crypt capi:tk(cbc(aes))-plain :36:logon:logkey: 0 $DEVICE 0 1 sector_size:512"
mount /dev/mapper/$ENCRYPTED_LABEL ${MOUNT_POINT}

We will now be able read back the data from the encrypted device:

cat ${MOUNT_POINT}/encrypted-file
Encrypted data

That was it!

Conclusion

Encryption could be hard, but the CAAM module makes it pretty much straight forward. It protect your secrets from physical attacks, which could be hard to protect otherwise.

However, keep in mind that as soon as the encrypted device is mounted and available to the system, it's free to read for any intruder that have access to the system.

The device security chain is no stronger than its weakest link and you have to identify and handle all potential security risks. This is only one.

Bug in the iMX8MP ECSPI module?

Bug in the iMX8MP ECSPI module?

Background

I do have a system where I can swap between iMX8M Mini and iMX8M Plus CPU modules on the same carrier board.

I did write a a SPI driver for a device on the carrier board. The device is connected to the ECSPI1 (the CPU contains several ECSPI modules) and use the hardware chipselect 0 (SS0). The driver has been used with the iMX8MM CPU module for a while, but as soon I swapped to the iMX8MP it certainly stopped working.

Both iMX8MM and iMX8MP have the same ECSPI IP block that is managed by the spi-imx [1] Linux kernel driver, the application and root filesystem is the same as well.

Same driver, same application, different module. What is happening?

The driver layer also did not report anything suspicious, all SPI transactions contained the data I expected and was successfully sent out on the bus. After debugging the application, driver and devicetree for a while, I took a closer look on the actual SPI signals.

SPI signals

I'm not going to describe the SPI interface specifications, please see Wikipedia [2] or such for more details.

It turns out that the chip select goes inactive after each sent byte, which is a weird behavior. The chipselect should stay low during the whole data transaction.

Here is the signals of one transaction of two bytes:

/media/imx8mp-spi-ss0.jpg

The ECSPI modules supports dynamic burst size, so I was experimenting with that without any success.

Workaround

The best workaround I came up with was to MUX the chipselect pin to the GPIO function instead of SS0 and map that GPIO as chipselect to ECSPI1 by override the affected properties in the device tree file:

&ecspi1 {
          cs-gpios =
                      <&gpio5 9 GPIO_ACTIVE_LOW>,
                      <&gpio2 8 GPIO_ACTIVE_LOW>;
};

&pinctrl_ecspi1_cs0 {
        fsl,pins = <
                MX8MP_IOMUXC_ECSPI1_SS0__GPIO5_IO09         0x40000
                    >;
};

Then the signals looks better:

/media/imx8mp-spi-gpio.jpg

Conclusion

I do not know if all ECSPI modules with all HW chipselects is affected or only SS0 @ ECSPI1. I could not find anything about it in the iMX8MP Errata.

The fact that the workaround did work makes me suspect a hardware bug in the iMX8MP processor. I guess we will see if it shows up in the errata later on.