Pulse width measurement driver

The pulse width measurement custom driver (pulse_meas) measures the width of pulses on GPIO inputs. It combines the GPIOTE, TIMER, and (D)PPI hardware peripherals so that the time between the pulse edges is captured entirely in hardware. Because the CPU is not part of the timing path, the reported pulse widths are not affected by interrupt latency or CPU load.

Results are delivered in series of pulse widths, with each value expressed in microseconds.

Overview

The driver operates as follows:

  1. The GPIOTE peripheral detects the edge that starts the pulse on the assert-gpios pin and the edge that ends the pulse on the deassert-gpios pin.

  2. (D)PPI connects the GPIOTE event of the starting edge to the TIMER CLEAR task, and the GPIOTE event of the ending edge to the TIMER CAPTURE[0] task.

  3. The TIMER counts at 1 MHz, so the value latched in its CC[0] register is the pulse width in microseconds.

  4. The driver reads the latched value in the GPIOTE interrupt handler and appends it to the current measurement buffer.

  5. When the configured number of pulse widths has been collected, the buffer is queued for the application and the optional callback is called.

The edge detection and TIMER operations are performed entirely in hardware, so the accuracy of a single measurement does not depend on interrupt latency. The CPU still wakes up once for each measured pulse to copy the latched value, which limits the pulse repetition rate that the driver can handle. See Limitations for details.

Reference code

The following sample and test use the pulse_meas driver and can serve as a usage reference:

Application

Comment

Pulse width measurement

Sample that measures pulses from an external signal source in continuous mode.

pulse_meas driver Twister test

Test that measures pulses generated by an onboard PWM peripheral, covering both pulse polarities and both operation modes.

Configuration

Configure the driver by describing the hardware resources in devicetree, enabling the driver through Kconfig, and setting the measurement parameters at runtime.

Static configuration

Configure the hardware resources statically in devicetree. A typical driver node in an application overlay looks as follows:

&timer20 {
    status = "reserved";
};

&gpio1 {
    status = "okay";
};

/ {
    pulse_meas: pulse-meas {
        compatible = "nordic,pulse-meas";
        status = "okay";
        timer-instance = <&timer20>;
        assert-gpios = <&gpio1 8 GPIO_ACTIVE_HIGH>;
        deassert-gpios = <&gpio1 9 GPIO_ACTIVE_HIGH>;
    };
};

The node accepts the following properties, all of which are required:

Property

Description

timer-instance

Phandle to the TIMER instance used to measure the time between the pulse edges.

assert-gpios

Pin on which the edge starting the pulse is detected.

deassert-gpios

Pin on which the edge ending the pulse is detected.

The devicetree configuration is subject to the following constraints:

  • The measured signal must be connected to both the assert-gpios and the deassert-gpios pins. A single GPIOTE channel detects only one edge polarity, so the driver uses one pin for the starting edge and another one for the ending edge.

  • Both pins must belong to GPIO ports served by the same GPIOTE instance. Otherwise, the build fails with an assertion, because a single (D)PPI connection cannot be shared between two GPIOTE instances.

  • The TIMER instance is driven directly by the pulse_meas driver, so its node must be set to status = "reserved". This makes the TIMER peripheral unavailable to other Zephyr drivers while keeping its devicetree node available for the timer-instance reference.

  • The GPIO port nodes referenced by both pins must be enabled, because the driver derives the GPIOTE instance and the absolute pin numbers from their properties.

Kconfig configuration

To enable the driver, set the CONFIG_PULSE_MEAS Kconfig option to y. It is enabled by default when at least one nordic,pulse-meas devicetree node is enabled. It also selects the required nrfx drivers automatically.

You can configure the following additional options:

Option

Description

CONFIG_PULSE_MEAS_USE_HFCLK

Requests the high-frequency clock for the duration of the measurement. Enable this option when the TIMER must be clocked from an accurate source. This increases power consumption while the measurement is running.

CONFIG_PULSE_MEAS_INIT_PRIORITY

Device driver initialization priority within the POST_KERNEL level. The default value is 50.

CONFIG_PULSE_MEAS_LOG_LEVEL

Log level of the pulse_meas logging module.

Note

The driver shares the GPIOTE peripheral with Zephyr’s General-Purpose Input/Output (GPIO) driver. When CONFIG_GPIO is disabled, the pulse_meas driver connects the GPIOTE interrupt itself. When CONFIG_GPIO is enabled, Zephyr’s GPIO driver connects the GPIOTE interrupt, and the pulse_meas driver uses the channels that remain unallocated.

Runtime configuration

At runtime, configure the measurement using the pulse_meas_config structure. Pass this structure to the pulse_meas_configure() function before starting the measurement.

Configure the following fields:

Field

Description

num_of_meas

Number of pulse widths that form one measurement series. The callback is called and the buffer becomes available to the application only after this many pulses have been measured.

pulse_type

Pulse polarity. Set it to PULSE_MEAS_PULSE_POSITIVE to measure the time from a rising edge to a falling edge, or to PULSE_MEAS_PULSE_NEGATIVE to measure the time from a falling edge to a rising edge.

mode

Operation mode. Set it to PULSE_MEAS_MODE_ONE_SHOT to capture a single series and stop automatically, or to PULSE_MEAS_MODE_CONTINUOUS to capture series until the measurement is stopped explicitly.

pull_config

GPIO pull configuration applied to both input pins.

user_handler

Optional callback invoked when a measurement series completes. Set it to NULL if the application polls for results instead.

user_context

Opaque pointer passed to user_handler.

Measurement results are stored in buffers taken from a memory slab supplied by the application. Define the slab with the K_MEM_SLAB_DEFINE macro, using the PULSE_MEAS_BLOCK_SIZE macro to calculate the required block size:

#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <drivers/pulse_meas.h>

#define NUMBER_OF_MEASUREMENTS 16
#define NUMBER_OF_BLOCKS       4

K_MEM_SLAB_DEFINE(pulse_meas_slab, PULSE_MEAS_BLOCK_SIZE(NUMBER_OF_MEASUREMENTS),
                  NUMBER_OF_BLOCKS, 4);

static const struct device *const pulse_meas_dev = DEVICE_DT_GET(DT_NODELABEL(pulse_meas));

static void pulse_meas_handler(void *context)
{
    /* Called from the GPIOTE interrupt context. */
}

static const struct pulse_meas_config pulse_meas_cfg = {
    .num_of_meas = NUMBER_OF_MEASUREMENTS,
    .pulse_type = PULSE_MEAS_PULSE_POSITIVE,
    .mode = PULSE_MEAS_MODE_CONTINUOUS,
    .pull_config = NRF_GPIO_PIN_NOPULL,
    .user_handler = pulse_meas_handler,
    .user_context = NULL,
};

int main(void)
{
    int err;

    if (!device_is_ready(pulse_meas_dev)) {
        return -ENODEV;
    }

    err = pulse_meas_configure(pulse_meas_dev, &pulse_meas_cfg);
    if (err < 0) {
        ...
    }
}

The pulse_meas_configure() function reconfigures the GPIOTE channels, the TIMER instance, and the (D)PPI connections, so it must not be called while a measurement is running.

See Memory slab requirements for the memory slab constraints.

Measurement

After configuring the device, control the measurement with the pulse_meas_start() and pulse_meas_stop() functions. Collect the results with pulse_meas_get() and pulse_meas_put().

Starting a measurement

The pulse_meas_start() function allocates the first buffer from the supplied memory slab and enables the hardware:

err = pulse_meas_start(pulse_meas_dev, &pulse_meas_slab);
if (err < 0) {
    ...
}

The function returns -ENOMEM when no free block is available in the slab.

The hardware can be enabled in the middle of a pulse, in which case the first value of the first series does not represent a complete pulse. To avoid this, either start the measurement while the input is in its idle state, or discard the first value of the first series.

Reading the results

The pulse_meas_get() function returns a pointer to the pulse widths of the oldest completed series. The buffer holds num_of_meas values of type uint32_t, each representing a pulse width in microseconds. After processing the values, return the buffer to the memory slab using the pulse_meas_put() function:

uint32_t *widths;

err = pulse_meas_get(pulse_meas_dev, &widths);
if (err == 0) {
    for (uint32_t i = 0; i < NUMBER_OF_MEASUREMENTS; i++) {
        printk("pulse %u: %u us\n", i, widths[i]);
    }

    pulse_meas_put(pulse_meas_dev, widths);
}

pulse_meas_get() reports the state of the measurement using the following return values:

Return value

Description

0

A completed series has been returned in widths.

-EAGAIN

No series is ready yet, but a series is still being captured. Retry later.

-EIO

No completed series is available and none is being captured.

Use the pulse_meas_pending() function to check how many completed series are waiting to be read, for example to drain the remaining series after the measurement has been stopped.

Note

The user_handler callback is invoked from the GPIOTE interrupt context. Only ISR-safe Zephyr kernel APIs may be used inside the callback. Do not call the pulse_meas_get() function from the callback. Instead, signal a waiting thread with the k_sem_give() function and read the results from that thread:

K_SEM_DEFINE(series_ready, 0, 1);

static void pulse_meas_handler(void *context)
{
    k_sem_give(&series_ready);
}

int main(void)
{
    uint32_t *widths;

    ...

    k_sem_take(&series_ready, K_FOREVER);

    if (pulse_meas_get(pulse_meas_dev, &widths) == 0) {
        /* Process the series, then release the buffer. */
        pulse_meas_put(pulse_meas_dev, widths);
    }
}

Stopping a measurement

In the PULSE_MEAS_MODE_ONE_SHOT mode, the driver stops the hardware automatically once the first series is complete.

In the PULSE_MEAS_MODE_CONTINUOUS mode, the measurement runs until it is stopped with the pulse_meas_stop() function. The function is non-blocking, and its immediate parameter selects one of the following behaviors:

  • Set immediate to false to let the driver complete the series that is currently being captured before stopping the hardware. While that series is in progress, pulse_meas_get() returns -EAGAIN.

  • Set immediate to true to stop the hardware immediately and discard the incomplete series. Series that have already been completed remain available to pulse_meas_get().

Memory slab requirements

The driver stores each measurement series in a single memory slab block. Each block starts with a small header that the driver uses to queue completed series. The PULSE_MEAS_BLOCK_SIZE macro accounts for this header, so the memory slab must be defined with the following parameters:

  • Block size of at least PULSE_MEAS_BLOCK_SIZE(num_of_meas) bytes, where num_of_meas matches the value in the pulse_meas_config structure.

  • Block alignment of at least 4 bytes, because the pulse widths are stored as 32-bit words.

The number of blocks determines how much data the application can leave unprocessed:

  • One block is held by the driver for the series that is currently being captured.

  • Each completed series occupies one block until the application releases it with the pulse_meas_put() function.

In the PULSE_MEAS_MODE_CONTINUOUS mode, the driver allocates the block for the next series from the interrupt context, immediately after the previous series completes. If no block is free at that moment, the driver stops the measurement and no further series are captured. For this reason, provide at least two blocks in continuous mode. Add more blocks to give the application enough time to read the results before the next series completes.

Limitations

The driver has the following limitations:

  • Fixed resolution and range - The TIMER instance is always configured to count at 1 MHz in 32-bit mode, so the resolution is fixed at 1 µs and cannot be changed. This limits the longest measurable pulse to approximately 4295 seconds.

  • Pulse repetition rate - The driver takes one GPIOTE interrupt for each measured pulse to copy the latched value out of the CC[0] register. If the next pulse ends before that interrupt has been serviced, the previously latched value is overwritten and the measurement is silently lost. The shortest usable pulse period depends on the interrupt latency and interrupt activity of the application.

  • Single driver instance - The queue of completed series is defined once for the whole driver instead of once for each device. For this reason, only one nordic,pulse-meas devicetree node can be enabled at a time.

  • No runtime reconfiguration - The pulse_meas_configure() function reinitializes the underlying hardware, so the measurement must be stopped before the configuration is changed.

Dependencies

The driver uses the following nrfx drivers:

  • nrfx_gpiote

  • nrfx_timer

  • nrfx_gppi

API documentation

Header file: include/drivers/pulse_meas.h
Source files: drivers/pulse_meas/
Pulse width measurement