Written by Jared Wolff, Hardware and firmware enthusiast and proud father of the nRF9160 Feather
One of the coolest thing about Zephyr is the ability to write an application and then use it across many platforms and architectures. For companies and individuals who prefer to stay vendor agnostic as much as possible Zephyr, as an RTOS, is a great tool.
In this post i’ll be going over some of the library code that I wrote for the new Air Quality Wing.
For the uninitiated, the Air Quality Wing is a board that I developed a few years ago (boy does time fly) to help keep tabs on the air quality inside in my apartment. It’s since been adopted by many all over the world for air quality monitoring projects in airports, apartments and industrial sites.
Let’s jump in and talk about some of the highlights.
For the uninitiated, Zephyr allows you to write platform/silicon agnostic code which you can use without having to do major re-writes every 2 years. In my case, I was able to develop the bulk of my code on one platform and then add other boards into the mix with minimal edits/changes.
For instance, the bulk of the library lives in this repository. It includes all the abstractions from the lower level driver access and device driver DTS bindings. If you’re new to the concept of developing drivers on Zephyr, make sure you check out my post on the Interrupt blog here.
If you look carefully, the drivers repo does not refer to any specific board. (no boards directory or
All of that fun happens within the demo code itself:
You can see that the boards directory has some board specific configurations and overlays.
You may be asking, why the need for any of these configurations/overlays?
To define each board’s console output and pin mappings.
For instance, the
.overlay files are used to map which pins go to the I2C bus and UART:
Most of the boards use the UART console for debug output. I found it easier though with the Xenon to use RTT instead:
The above turns on RTT an turns of the UART console.
The cool thing about this is that you can adapt your own board to run the Air Quality Wing demo code. Yes, you can BYOB (bring your own board). If your board is defined in Zephyr’s
boards directory, it’s as simple as creating board specific
.overlay files to match your configuration.
In this section i’ll break down some of the differences between some of the examples and how you can use them to kick start your own endeavors.
The Basic sample is the foundation for all the other samples. It’s the minimal amount of code needed to boot and start reading data from the Air Quality Wing. Let’s see how it’s set up:
Device name definitions
One of the first things you’ll see in
main.c are the definitions of each of the sensors. This is required to map your
.overlay to the functionality of the library.
The first line in each pair obtains the device that matches the specified compatible. The second line then finds the device by the provided label. Take a look at the
.overlay earlier in the post. Can you match each
.overlay entry to the macros above?
I tried to make the Air Quality Wing library as customizable as possible. In the next section you’ll see that the sensors are now declared as
struct aqw_sensor along with some other options:
This allows you to set different sensor types and map them to the devices in your device tree. You can also see i’m mapping the measurement type for the specific reading that is define in
enum sensor_channel. Since there’s only one measurement type per channel, you can see, for instance, the SHTC3 is defined twice: once for temperature and once for humidity.
You can also configure the measurement interval per-measurement type. This may be useful if you’re interested in a particular measurement periodically. This entry is set in seconds.
Finally, toward the bottom of those declarations, you can see i’ve wrapped them up in an array as pointers:
This sensor array can be initialized with the library which makes it extremely simple to iterate and get new sensor values. Don’t want to use/initialize a particular sensor? Remove it from the array. ( Side note: I also recommend disabling the sensor in your project’s
.conf file or else your driver code may still be loaded into your project! )
Getting the values back
As you’ll see in a second, initialization also requires a callback. This callback is used to get the values back from the AQW library when the measurements complete:
Due to the nature of sensors being placed in array, you’ll notice I skip any sensors that are invalid. This also goes for sensors which don’t have a value yet. This is applies particularly to the SGP40 since it takes ~1 minute to “warm-up”.
Finally, initialization is only a few function calls:
aqw_init pulls in the sensors defined at the top of the file. It also takes the callback we talked about earlier as an argument. Make sure both are define or else the library will not work!
The library will not work though until you run
aqw_sensor_start_fetch. This starts the reoccurring measurement process. The library uses
k_work_delayable to periodically obtain new sensor values thus the
main thread is dropped and freed for use by the system work thread and others.
Adding on top of what’s done in the Basic sample, the BLE (aka Bluetooth Low Energy) sample adds the capability to receive updates via Bluetooth. Most of the fun happens within
ble/services/aqw.c. Here’s what the overall file structure looks:
When developing new Bluetooth applications, I typically like to separate the services into separate files. It keeps things clean. Plus, there’s no confusion about what lives where! In the case of this example, there’s only one service, the Air Quality Wing service. It’s composed of the temperature, humidity, VOC and PM2.5 characteristics. The characteristics are defined in
aqw.h like so:
You can see, it starts with declaring a full 128-bit UUID for the service and characteristics. Since i’m creating a service from scratch I decided to use an arbitrary value. It’s typical to increment the characteristic UUIDs from the service one. You can see it goes from
0002 and so on.
Defining the service and characteristics happen within
src/ble/services/aqw.c. Zephyr uses the
BT_GATT_SERVICE_DEFINE macro to define new Bluetooth services/characteristics. Here’s what the final one looks like:
BT_GATT_SERVICE_DEFINE starts with defining the servcie name. Then, it follows on with setting the service UUID (as defined earlier in the header). Then, followed by the characteristic definitions and CCC definitions if you’re using notifications. Since all of this happens pre-processor, if you run into issues the compiler will let you know. No more programming, guessing and checking like with other SDKS.
Publishing data uses the
app_ble_aqw_pubish function. It’s particularly special since you can write any piece of supported sensor data and it will notify to the correct characteristic.
You can see, it sorts, sets the data an then notifies to connected clients. If you’re curious about the contents of
struct app_ble_payload you can check out
app_ble.c brings it all together with initialization in
app_ble_publish_sensor_data which forwards along published events to the
app_ble_aqw_publish function call. While in other SDKs Bluetooth is a big to-do, Zephyr has made it quite simple to get an application Bluetooth enabled.
Now, if we run over to
main.c you can see I also added the
app_ble_publish_sensor_data call when new sensor data is available. I hope you can visualize how the data gets propagated all the way to the underlying BLE syscalls.
Otherwise, everything else in
main.c should look very familiar!
This is just the tip of the iceberg about how I designed the Air Quality Wing firmware. I’m hoping it’s generic enough where you can import it into your pre-existing code base and start taking air quality measurements with ease.
There are also future examples coming out including one featuring Golioth (an up and coming IoT platform i’m a big fan of). A dedicated blog post for the Golioth code is forthcoming so stay tuned! In the meantime, you can check out the work in progress code at the Air Quality Wing Demo repository.
And one more thing..
Despite all the supply chain issues we’re going through these days, assembly on a small batch of the Air Quality Wing has begun! Parts have been at the factory for about a week now and should be completed by Mid-October.
If you’re interested in grabbing one, you can pre-order at my store here.
Thanks for reading. Feel free to drop me a line (email@example.com) if you think something is missing or if you have questions. Additionally, please feel free to reach out to Zephyr technical members, ambassadors and more on the Zephyr Discord Channel.