Zephyr IO Part 1

Zephyr Sensor IO - Part 1

Background

Being relatively new to the Internet-of-Things on the truely compact, battery powered, and consumer delivered device, I had quite the task ahead of me.

I needed bluetooth, a decent chunk of memory (for a micro), reasonably fast IO to support a wide berth of sensors, and battery life that lasts for days.

A quick search and discovery led to the most excellent Nordic Semiconductor NRF52840 SoC, which has all of the above in spades.

What I didn't expect coming from the world of Atmegas with its relatively straightfoward avr-libc and bare metal programming was just how big the mental burden would be to develop on such a device. Bluetooth alone is very much like a network stack. While the NRF SDK is excellent, it would take quite some time to develop the application I needed. Then to debug my one off superloop and device drivers seemed like a poor route to go.

It was time to look at the array of RTOSes available to me for this chip. Zephyr was a clear winner to me after a quick search and discovery. The banner image showcasing my hometown had nothing to do with Zephyr winning me over, I swear. A quick git clone and I was running the sample apps on a dev board in less than an hour. Record breaking time as these things tend to go.

Trouble in Paradise

While most of the Zephyr API's are quite good the sensor API left me scratching my head a bit as I tried to create my own drivers for the TDK ICM20649, a 6 axis accelerometer/gyro combo with great ranges and sampling rates for my use case. This very fast Arm was failing to do the simplest of things, take in a stream of 6 16 bit samples at 1KHz over SPI and save it to memory. It turns out that the way the driver API is written leads to a wide variance in latency from the time the GPIO pin is triggered to the time the SPI transfer even begins. At first the fix was to use the built in FIFO to simply avoid the latency issue altogether by allowing a wider variance in interrupt servicing times, all was wonderful again. I worked around the API issue.

While this was fine at first there was yet another problem. Fusing the data of the TDK ICM20649 with an ST 3 axis magnetometer also communicating over SPI. Also at 1KHz. This chip does not happen to have a hardware FIFO on it. On the surface it seems relatively simple to align these samples in time over time. In reality the sample rates of the two chips varied enough over the 250ms of FIFO buffer the TDK chip would accumulate to add up to 10s of milliseconds over a 1 or 2 second recording. This was problematic for my use case.

I went back to pulling samples one by one every trigger, and the samples were once again all time aligned. But the problem remained, the time from the trigger interrupt to the time my k_work or k_thread got around to reading from the device varied.

I tried adjusting the priorities, which failed to change much. I tried to read directly in the interrupt handler, which had other poor consequences and caused other parts of my program to fail in surprising ways. There wasn't a good solution unfortunately, I just accepted I would be missing some data over time.

It turns out none of these problems were not new and an issue from 2017 by Anas Nashif, the chair of the TSC of Zephyr, described almost exactly the problems I had ran into. https://github.com/zephyrproject-rtos/zephyr/issues/1387

Short term I found ways of working around the general problem, longer term I really wanted a better way of using sensors in Zephyr. For this project and future ideas I have for products.

Zephyr IO Part 2

Zephyr Sensor IO - Part 2

Breakdown of Specifics

My specific issues with the current Zephyr API mostly stemmed around three things. The way triggers were dealt with, the format of the data, and the lack of hardware FIFO support.

Triggers in the sensor api were dealt with directly as callbacks, either in queued work or their own zephyr thread. Either way the data wasn't actually moved from the device to the application in some way until this callback first occured and the application explicitly requested data be moved. Which didn't occur until any other interrupts or higher priority tasks were dealt with. This is where the variable latency problem stemmed from.

The format of the data was not very useful for me to store any significant amounts of on a microcontroller, as each value was two 32bit integers representing the whole and fractional parts of the value. The sensor puts out 16bit values, this micro has SIMD instructions for 16 bit values, it makes sense to keep them as 16bit values as long as possible. It also makes it impossible to implement direct DMA transfers from the device to the application without an intermediary buffer and some sort of translation. Which wasn't that helpful for my use case of essentially gathering data, doing a few processing tasks on it, then passing it along over bluetooth.

Lastly the API had no way of directly supporting hardware FIFOs which can greatly reduce the interrupt and IO burden on the micro leaving it alone longer to do more useful things, like some math on the samples.

The Inspiration

To solve most of my problems I need the ability to abstract away a hardware fifo or simulate one in software when one didn't exist. There began my journey of creating a buffer api I could be driver impelmented. It turns out I wasn't the first one to come across this sort of problem, nor was the Zephyr project, and I'm sure its been around even longer than Linux.

Linux however has a pretty nice solution if you have the whole sysfs abstraction to work with in its IIO module. As a microcontroller RTOS sysfs is sort of beyond the scale of what I think could happen in a few kilobytes of ram.