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:
The GPIOTE peripheral detects the edge that starts the pulse on the
assert-gpiospin and the edge that ends the pulse on thedeassert-gpiospin.(D)PPI connects the GPIOTE event of the starting edge to the TIMER
CLEARtask, and the GPIOTE event of the ending edge to the TIMERCAPTURE[0]task.The TIMER counts at 1 MHz, so the value latched in its
CC[0]register is the pulse width in microseconds.The driver reads the latched value in the GPIOTE interrupt handler and appends it to the current measurement buffer.
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 |
|---|---|
Sample that measures pulses from an external signal source in continuous mode. |
|
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 |
|---|---|
|
Phandle to the TIMER instance used to measure the time between the pulse edges. |
|
Pin on which the edge starting the pulse is detected. |
|
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-gpiosand thedeassert-gpiospins. 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_measdriver, so its node must be set tostatus = "reserved". This makes the TIMER peripheral unavailable to other Zephyr drivers while keeping its devicetree node available for thetimer-instancereference.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 |
|---|---|
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. |
|
Device driver initialization priority within the |
|
|
Log level of the |
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 |
|---|---|
|
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 polarity.
Set it to |
|
Operation mode.
Set it to |
|
GPIO pull configuration applied to both input pins. |
|
Optional callback invoked when a measurement series completes.
Set it to |
|
Opaque pointer passed to |
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 |
|---|---|
|
A completed series has been returned in |
|
No series is ready yet, but a series is still being captured. Retry later. |
|
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
immediatetofalseto 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
immediatetotrueto stop the hardware immediately and discard the incomplete series. Series that have already been completed remain available topulse_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, wherenum_of_measmatches the value in thepulse_meas_configstructure.Block alignment of at least
4bytes, 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-measdevicetree 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_gpiotenrfx_timernrfx_gppi
API documentation
include/drivers/pulse_meas.hdrivers/pulse_meas/