Sensors

The sensor subsystem exposes an API to uniformly access sensor devices. Common operations are: reading data and executing code when specific conditions are met.

Basic Operation

Channels

Fundamentally, a channel is a quantity that a sensor device can measure.

Sensors can have multiple channels, either to represent different axes of the same physical property (e.g. acceleration); or because they can measure different properties altogether (ambient temperature, pressure and humidity). Complex sensors cover both cases, so a single device can expose three acceleration channels and a temperature one.

It is imperative that all sensors that support a given channel express results in the same unit of measurement. Consult the API Reference for all supported channels, along with their description and units of measurement:

Values

Sensor stable APIs return results as sensor_value. This representation avoids use of floating point values as they may not be supported on certain setups.

A newer experimental (may change) API that can interpret raw sensor data is available in parallel. This new API exposes raw encoded sensor data to the application and provides a separate decoder to convert the data to a Q31 format which is compatible with the Zephyr Digital Signal Processing (DSP). The values represented are in the range of (-1.0, 1.0) and require a shift operation in order to scale them to their SI unit values. See Async Read for more information.

Fetching Values

Getting a reading from a sensor requires two operations. First, an application instructs the driver to fetch a sample of all its channels. Then, individual channels may be read. In the case of channels with multiple axes, they can be read in a single operation by supplying the corresponding _XYZ channel type and a buffer of 3 sensor_value objects. This approach ensures consistency of channels between reads and efficiency of communication by issuing a single transaction on the underlying bus.

Below is an example illustrating the usage of the BME280 sensor, which measures ambient temperature and atmospheric pressure. Note that sensor_sample_fetch() is only called once, as it reads and compensates data for both channels.

 1
 2/*
 3 * Get a device structure from a devicetree node with compatible
 4 * "bosch,bme280". (If there are multiple, just pick one.)
 5 */
 6static const struct device *get_bme280_device(void)
 7{
 8	const struct device *const dev = DEVICE_DT_GET_ANY(bosch_bme280);
 9
10	if (dev == NULL) {
11		/* No such node, or the node does not have status "okay". */
12		printk("\nError: no device found.\n");
13		return NULL;
14	}
15
16	if (!device_is_ready(dev)) {
17		printk("\nError: Device \"%s\" is not ready; "
18		       "check the driver initialization logs for errors.\n",
19		       dev->name);
20		return NULL;
21	}
22
23	printk("Found device \"%s\", getting sensor data\n", dev->name);
24	return dev;
25}
26
27int main(void)
28{
29	const struct device *dev = get_bme280_device();
30
31	if (dev == NULL) {
32		return 0;
33	}
34
35	while (1) {
36		struct sensor_value temp, press, humidity;
37
38		sensor_sample_fetch(dev);
39		sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &temp);
40		sensor_channel_get(dev, SENSOR_CHAN_PRESS, &press);
41		sensor_channel_get(dev, SENSOR_CHAN_HUMIDITY, &humidity);
42
43		printk("temp: %d.%06d; press: %d.%06d; humidity: %d.%06d\n",
44		      temp.val1, temp.val2, press.val1, press.val2,
45		      humidity.val1, humidity.val2);
46
47		k_sleep(K_MSEC(1000));
48	}
49	return 0;
50}

Async Read

To enable the async APIs, use CONFIG_SENSOR_ASYNC_API.

Reading the sensors leverages the Real Time I/O (RTIO) subsystem. Applications gain control of the data processing thread and even memory management. In order to get started with reading the sensors, an IODev must be created via the SENSOR_DT_READ_IODEV. Next, an RTIO context must be created. It is strongly suggested that this context is created with a memory pool via RTIO_DEFINE_WITH_MEMPOOL.

#include <zephyr/device.h>
#include <zephyr/drivers/sensor.h>
#include <zephyr/rtio/rtio.h>

static const struct device *lid_accel = DEVICE_DT_GET(DT_ALIAS(lid_accel));
SENSOR_DT_READ_IODEV(lid_accel_iodev, DT_ALIAS(lid_accel), SENSOR_CHAN_ACCEL_XYZ);

RTIO_DEFINE_WITH_MEMPOOL(sensors_rtio,
                         4,  /* submission queue size */
                         4,  /* completion queue size */
                         16, /* number of memory blocks */
                         32, /* size of each memory block */
                         4   /* memory alignment */
                         );

To trigger a read, the application simply needs to call sensor_read() and pass the relevant IODev and RTIO context. Getting the result is done like any other RTIO operation, by waiting on a completion queue event (CQE). In order to help reduce some boilerplate code, the helper function sensor_processing_with_callback() is provided. When called, the function will block until a CQE becomes available from the provided RTIO context. The appropriate buffers are extracted and the callback is called. Once the callback is done, the memory is reclaimed by the memorypool. This looks like:

static void sensor_processing_callback(int result, uint8_t *buf,
                                       uint32_t buf_len, void *userdata) {
  // Process the data...
}

static void sensor_processing_thread(void *, void *, void *) {
  while (true) {
    sensor_processing_with_callback(&sensors_rtio, sensor_processing_callback);
  }
}
K_THREAD_DEFINE(sensor_processing_tid, 1024, sensor_processing_thread,
                NULL, NULL, NULL, 0, 0, 0);

Note

Helper functions to create custom length IODev nodes and ones that don’t have static bindings will be added soon.

Processing the Data

Once data collection completes and the processing callback was called, processing the data is done via the sensor_decoder_api. The API provides a means for applications to control when to process the data and how many resources to dedicate to the processing. The API is entirely self contained and requires no system calls (even when CONFIG_USERSPACE is enabled).

static struct sensor_decoder_api *lid_accel_decoder = SENSOR_DECODER_DT_GET(DT_ALIAS(lid_accel));

static void sensor_processing_callback(int result, uint8_t *buf,
                                       uint32_t buf_len, void *userdata) {
  uint64_t timestamp;
  sensor_frame_iterator_t fit = {0};
  sensor_channel_iterator_t cit = {0};
  enum sensor_channel channels[3];
  q31_t values[3];
  int8_t shift[3];

  lid_accel_decoder->get_timestamp(buf, &timestamp);
  lid_accel_decoder->decode(buf, &fit, &cit, channels, values, 3);

  /* Values are now in q31_t format, we're going to convert them to micro-units */

  /* First, we need to know by how much to shift the values */
  lid_accel_decoder->get_shift(buf, channels[0], &shift[0]);
  lid_accel_decoder->get_shift(buf, channels[1], &shift[1]);
  lid_accel_decoder->get_shift(buf, channels[2], &shift[2]);

  /* Shift the values to get the SI units */
  int64_t scaled_values[] = {
    (int64_t)values[0] << shift[0],
    (int64_t)values[1] << shift[1],
    (int64_t)values[2] << shift[2],
  };

  /*
   * FIELD_GET(GENMASK64(63, 31), scaled_values[]) - will give the integer value
   * FIELD_GET(GENMASK64(30, 0), scaled_values[]) / INT32_MAX - is the decimal value
   */
}

Configuration and Attributes

Setting the communication bus and address is considered the most basic configuration for sensor devices. This setting is done at compile time, via the configuration menu. If the sensor supports interrupts, the interrupt lines and triggering parameters described below are also configured at compile time.

Alongside these communication parameters, sensor chips typically expose multiple parameters that control the accuracy and frequency of measurement. In compliance with Zephyr’s design goals, most of these values are statically configured at compile time.

However, certain parameters could require runtime configuration, for example, threshold values for interrupts. These values are configured via attributes. The example in the following section showcases a sensor with an interrupt line that is triggered when the temperature crosses a threshold. The threshold is configured at runtime using an attribute.

Triggers

Triggers in Zephyr refer to the interrupt lines of the sensor chips. Many sensor chips support one or more triggers. Some examples of triggers include: new data is ready for reading, a channel value has crossed a threshold, or the device has sensed motion.

To configure a trigger, an application needs to supply a sensor_trigger and a handler function. The structure contains the trigger type and the channel on which the trigger must be configured.

Because most sensors are connected via SPI or I2C buses, it is not possible to communicate with them from the interrupt execution context. The execution of the trigger handler is deferred to a thread, so that data fetching operations are possible. A driver can spawn its own thread to fetch data, thus ensuring minimum latency. Alternatively, multiple sensor drivers can share a system-wide thread. The shared thread approach increases the latency of handling interrupts but uses less memory. You can configure which approach to follow for each driver. Most drivers can entirely disable triggers resulting in a smaller footprint.

The following example contains a trigger fired whenever temperature crosses the 26 degree Celsius threshold. It also samples the temperature every second. A real application would ideally disable periodic sampling in the interest of saving power. Since the application has direct access to the kernel config symbols, no trigger is registered when triggering was disabled by the driver’s configuration.

  1
  2#define UCEL_PER_CEL 1000000
  3#define UCEL_PER_MCEL 1000
  4#define TEMP_INITIAL_CEL 25
  5#define TEMP_WINDOW_HALF_UCEL 500000
  6
  7static const char *now_str(void)
  8{
  9	static char buf[16]; /* ...HH:MM:SS.MMM */
 10	uint32_t now = k_uptime_get_32();
 11	unsigned int ms = now % MSEC_PER_SEC;
 12	unsigned int s;
 13	unsigned int min;
 14	unsigned int h;
 15
 16	now /= MSEC_PER_SEC;
 17	s = now % 60U;
 18	now /= 60U;
 19	min = now % 60U;
 20	now /= 60U;
 21	h = now;
 22
 23	snprintf(buf, sizeof(buf), "%u:%02u:%02u.%03u",
 24		 h, min, s, ms);
 25	return buf;
 26}
 27
 28#ifdef CONFIG_MCP9808_TRIGGER
 29
 30static struct sensor_trigger sensor_trig;
 31
 32static int set_window(const struct device *dev,
 33		      const struct sensor_value *temp)
 34{
 35	const int temp_ucel = temp->val1 * UCEL_PER_CEL + temp->val2;
 36	const int low_ucel = temp_ucel - TEMP_WINDOW_HALF_UCEL;
 37	const int high_ucel = temp_ucel + TEMP_WINDOW_HALF_UCEL;
 38	struct sensor_value val = {
 39		.val1 = low_ucel / UCEL_PER_CEL,
 40		.val2 = low_ucel % UCEL_PER_CEL,
 41	};
 42	int rc = sensor_attr_set(dev, SENSOR_CHAN_AMBIENT_TEMP,
 43				 SENSOR_ATTR_LOWER_THRESH, &val);
 44	if (rc == 0) {
 45		val.val1 = high_ucel / UCEL_PER_CEL,
 46		val.val2 = high_ucel % UCEL_PER_CEL,
 47		rc = sensor_attr_set(dev, SENSOR_CHAN_AMBIENT_TEMP,
 48				     SENSOR_ATTR_UPPER_THRESH, &val);
 49	}
 50
 51	if (rc == 0) {
 52		printf("Alert on temp outside [%d, %d] milli-Celsius\n",
 53		       low_ucel / UCEL_PER_MCEL,
 54		       high_ucel / UCEL_PER_MCEL);
 55	}
 56
 57	return rc;
 58}
 59
 60static inline int set_window_ucel(const struct device *dev,
 61				  int temp_ucel)
 62{
 63	struct sensor_value val = {
 64		.val1 = temp_ucel / UCEL_PER_CEL,
 65		.val2 = temp_ucel % UCEL_PER_CEL,
 66	};
 67
 68	return set_window(dev, &val);
 69}
 70
 71static void trigger_handler(const struct device *dev,
 72			    const struct sensor_trigger *trig)
 73{
 74	struct sensor_value temp;
 75	static size_t cnt;
 76	int rc;
 77
 78	++cnt;
 79	rc = sensor_sample_fetch(dev);
 80	if (rc != 0) {
 81		printf("sensor_sample_fetch error: %d\n", rc);
 82		return;
 83	}
 84	rc = sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &temp);
 85	if (rc != 0) {
 86		printf("sensor_channel_get error: %d\n", rc);
 87		return;
 88	}
 89
 90	printf("trigger fired %u, temp %g deg C\n", cnt,
 91	       sensor_value_to_double(&temp));
 92	set_window(dev, &temp);
 93}
 94#endif
 95
 96int main(void)
 97{
 98	const struct device *const dev = DEVICE_DT_GET_ANY(microchip_mcp9808);
 99	int rc;
100
101	if (dev == NULL) {
102		printf("Device not found.\n");
103		return 0;
104	}
105	if (!device_is_ready(dev)) {
106		printf("Device %s is not ready.\n", dev->name);
107		return 0;
108	}
109
110#ifdef CONFIG_MCP9808_TRIGGER
111	rc = set_window_ucel(dev, TEMP_INITIAL_CEL * UCEL_PER_CEL);
112	if (rc == 0) {
113		sensor_trig.type = SENSOR_TRIG_THRESHOLD;
114		sensor_trig.chan = SENSOR_CHAN_AMBIENT_TEMP;
115		rc = sensor_trigger_set(dev, &sensor_trig, trigger_handler);
116	}
117
118	if (rc != 0) {
119		printf("Trigger set failed: %d\n", rc);
120		return 0;
121	}
122	printk("Trigger set got %d\n", rc);
123#endif
124
125	while (1) {
126		struct sensor_value temp;
127
128		rc = sensor_sample_fetch(dev);
129		if (rc != 0) {
130			printf("sensor_sample_fetch error: %d\n", rc);
131			break;
132		}
133
134		rc = sensor_channel_get(dev, SENSOR_CHAN_AMBIENT_TEMP, &temp);
135		if (rc != 0) {
136			printf("sensor_channel_get error: %d\n", rc);
137			break;
138		}
139
140		printf("%s: %g C\n", now_str(),
141		       sensor_value_to_double(&temp));
142
143		k_sleep(K_SECONDS(2));
144	}
145	return 0;
146}

API Reference

Sensor Interface
Sensor emulator backend API