samples: net: latmon: latency monitor tool

Supports the EVL/Xenomai4 benchmarking tool as described in the
documentation. The benchmarking tool is accessed via the latmon service.

This code uses the J2 socket on FRDMk64-F:
             PIN_20: tx pulse to SUT
             PIN_18: rx ack from SUT

The client code running on the SUT shall monitor the falling edge of
PIN_20.

Example usage from the latmus client running Xenomai4:
 $ latmus -I gpiochip2,23,falling-edge  \
          -O gpiochip2,21 -g"histogram" \
          -z broadcast

Signed-off-by: Jorge Ramirez-Ortiz <jorge.ramirez@oss.qualcomm.com>
This commit is contained in:
Jorge Ramirez-Ortiz 2025-07-13 21:14:25 +02:00 committed by Chris Friedt
parent 8c0c4bdace
commit f9eeefa6e0
7 changed files with 529 additions and 0 deletions

View File

@ -0,0 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(latmon)
target_sources(app PRIVATE src/main.c)

View File

@ -0,0 +1,13 @@
# Private config options for Latmon Sample app
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 Jorge Ramirez-Ortiz <jorge.ramirez@oss.qualcomm.com>
mainmenu "Latmon Sample application"
config LATMON_LOOPBACK_CALIBRATION
bool "Run the sample code in calibration mode"
default n
help
Run Latmon in calibration mode.
source "Kconfig.zephyr"

View File

@ -0,0 +1,190 @@
.. zephyr:code-sample:: latmon-client
:name: Latmon Client
:relevant-api: latmon
Measures delta time between GPIO events and reports the latency metrics via latmon to the latmus
service executing on the SUT.
Overview
********
This project provides tools to measure the worst-case response time of a system under test (SUT) to
GPIO events using:
- **Latmon**:
Runs on a Zephyr-based board to generate and monitor GPIO events while collecting metrics
- **Latmus**:
Runs on the SUT to respond to the falling edge of input GPIO event, displaying the latency metrics
and generate histogram data.
This project is part of the open-source initiative
`EVL Project - Latmus GPIO Response Time <https://evlproject.org/core/benchmarks/#latmus-gpio-response-time>`_.
The main program is designed to monitor latency using GPIO pins on a Zephyr-based system. It generates a
pulse signal on a GPIO pin and measures the time it takes for the SUT (executing Latmus) to respond to
it.
The SUT must be running Latmus to capture the latency metrics and histogram information reported over the
network. The program uses LEDs to indicate the different states, such as DHCP binding(red), waiting for the
Latmus connection (blue) and sampling (green).
Why Not Just Use a Timer?
=========================
Timer tests miss external factors like GPIO signals, hardware, and interrupt handling.
Latmon and Latmus simulate real-world scenarios, capturing end-to-end latency metrics.
This ensures accurate assessment of real-time responsiveness across the entire system.
- **Real-Time Thread Testing**:
Evaluates how a user-space thread processes external interrupts.
- **End-to-End Latency Measurement**:
Captures delays from hardware, drivers, and user-space threads.
- **Versatile Platform Support**:
Works on EVL, PREEMPT_RT, and other platforms.
Code Structure
==============
The Latmon sample application is divided into two main components:
- **Application Logic** (:zephyr_file:`samples/net/latmon/src/main.c`):
This file contains the application logic for Latmon.
It initializes networking, provides the instrumentation mechanism and handles LED indicators for the
different states.
- **Library** (:zephyr_file:`subsys/net/lib/latmon/latmon.c`):
This file provides reusable functions and abstractions for latency monitoring via Latmus.
It includes the core logic for reporting latency metrics and histogram data.
Requirements
************
- **Zephyr-Compatible Board**:
A board with external GPIO support and an IPv4 network interface (e.g., FRDM-K64F).
- **System under Test**:
A system with external GPIO pins running the Latmus service and an IPv4 network interface.
- **Network Connection**:
A DHCP server for IP assignment.
- **Physical Connection**:
GPIO wires connecting the Zephyr board to the SUT and both systems connected to the network.
Setup and Usage
***************
- **Flash Latmon onto the Zephyr board**:
The application will connect to the network and wait for a connection from the SUT. The application
will use DHCP to obtain an IPv4 address.
- **Connect GPIO pins for transmit (Zephyr to SUT) and receive (SUT to Zephyr)**
On **FRDM-K64F**, the sample code uses the **Arduino header J2**, ``pin 20`` for transmit the pulse to
the SUT and ``pin 18`` to receive the acknowledgment from the SUT.
- **Run Latmus on the SUT**
Request the appropriate options with `Latmus <https://evlproject.org/core/testing/#latmus-program>`_. Users
can for example modify the sampling period with the ``-p`` option or generate historgram data for
postprocessing with the ``-g`` option,
- **Monitor results from the SUT**
Latmus will report latency figures and, if requested, generate the histogram data file.
- **Calibrating the Latmus latencies: CONFIG_LATMON_LOOPBACK_CALIBRATION**:
Users can connect the GPIO pins in loopback mode (transmit to ack) and build the Latmon sample application with
CONFIG_LATMON_LOOPBACK_CALIBRATION enabled. When connecting to Latmus in this configuration, Latmus is providing
a calibration value that can be used to adjust the final latencies.
Example
=======
On the host and to build and flash the Zephyr FRDM-K64F board with the Latmon sample:
.. code-block:: console
user@host:~$ west build -b frdm_k64f samples/net/latmon
user@host:~$ west flash
On the SUT running on Linux, latmus **MUST** track the falling edge of the signal:
.. code-block:: console
root@target:~$ latmus -I gpiochip2,23,falling-edge -O gpiochip2,21 -z -g"histogram" "broadcast"
Monitoring both consoles, you should see the following:
.. code-block:: console
[00:00:03.311,000] <inf> phy_mc_ksz8081: PHY 0 is up
[00:00:03.311,000] <inf> phy_mc_ksz8081: PHY (0) Link speed 100 Mb, full duplex
[00:00:03.312,000] <inf> eth_nxp_enet_mac: Link is up
*** Booting Zephyr OS build v4.1.0-3337-g886443a190b1 ***
[00:00:03.313,000] <inf> sample_latmon: DHCPv4: binding...
[00:00:03.313,000] <inf> latmon: Latmon server thread priority: 14
[00:00:10.964,000] <inf> net_dhcpv4: Received: 192.168.1.58
[00:00:10.964,000] <inf> sample_latmon: Listening on 192.168.1.58
[00:00:30.966,000] <inf> latmon: Waiting for Latmus ...
[00:00:31.356,000] <inf> latmon: Monitor thread priority: -16
[00:00:31.356,000] <inf> latmon: monitoring started:
[00:00:31.356,000] <inf> latmon: - samples per period: 1000
[00:00:31.356,000] <inf> latmon: - period: 1000 usecs
[00:00:31.356,000] <inf> latmon: - histogram cells: 200
[00:00:31.393,000] <inf> latmon: Transfer thread priority: 14
.. code-block:: console
root@target:~$ latmus -I gpiochip2,23,falling-edge -O gpiochip2,21 -Z -g"histogram" broadcast
Received broadcast message: 192.168.1.58
warming up on CPU0 (not isolated)...
connecting to latmon at 192.168.1.58:2306...
RTT| 00:00:16 (oob-gpio, 1000 us period, priority 98, CPU0-noisol)
RTH|----lat min|----lat avg|----lat max|-overrun|---msw|---lat best|--lat worst
RTD| 26.375| 30.839| 33.508| 0| 0| 26.375| 33.508
RTD| 26.333| 30.801| 37.633| 0| 0| 26.333| 37.633
RTD| 26.375| 30.801| 31.966| 0| 0| 26.333| 37.633
RTD| 26.375| 30.911| 49.675| 0| 0| 26.333| 49.675
RTD| 26.333| 30.830| 41.658| 0| 0| 26.333| 49.675
RTD| 26.375| 31.107| 59.216| 0| 0| 26.333| 59.216
RTD| 26.333| 30.767| 30.925| 0| 0| 26.333| 59.216
RTD| 26.333| 30.781| 41.616| 0| 0| 26.333| 59.216
RTD| 26.375| 30.768| 32.925| 0| 0| 26.333| 59.216
RTD| 26.375| 30.768| 37.633| 0| 0| 26.333| 59.216
On completion and from your host, retrieve the histogram file from the SUT, and generate a plot (a PNG file) using
gnuplot:
.. code-block:: console
user@host:~$ gnuplot plot_data.gp
The ``plot_data.gp`` script should look like this for a file named ``histogram``:
.. code-block:: gnuplot
set terminal pngcairo size 800,600
set output 'plot.png'
set title "Data Plot"
set xlabel "Latency (usec)"
set ylabel "Sample Count"
set grid
set style data linespoints
plot 'histogram' using 1:2 with linespoints title "Data Points"

View File

@ -0,0 +1,12 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright (c) 2025 Jorge Ramirez-Ortiz <jorge.ramirez@oss.qualcomm.com>
*/
/ {
zephyr,user {
pulse_gpios = <&gpioe 24 GPIO_ACTIVE_HIGH>;
ack_gpios = <&gpioe 25 GPIO_ACTIVE_HIGH>;
};
};

View File

@ -0,0 +1,22 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2025 Jorge Ramirez-Ortiz <jorge.ramirez@oss.qualcomm.com>
CONFIG_POSIX_API=y
# General config
CONFIG_LOG=y
CONFIG_GPIO=y
# Networking config
CONFIG_NETWORKING=y
CONFIG_NET_DHCPV4=y
CONFIG_NET_IPV4=y
CONFIG_NET_LOG=y
CONFIG_NET_TCP=y
# Latmon config
CONFIG_NET_LATMON=y
CONFIG_LATMON_LOG_LEVEL_DBG=y
# Heap for Latmon
CONFIG_HEAP_MEM_POOL_SIZE=8192

View File

@ -0,0 +1,14 @@
sample:
description: Latency Benchmarking Tool
name: Latmon sample app
common:
harness: net
tags:
- net
- latmon
tests:
sample.net.latmon:
build_only: true
platform_allow:
- frdm_k64f
depends_on: eth

View File

@ -0,0 +1,271 @@
/*
*
* SPDX-License-Identifier: Apache-2.0
*
* Copyright (c) 2025 Jorge Ramirez-Ortiz <jorge.ramirez@oss.qualcomm.com>
*/
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(sample_latmon, LOG_LEVEL_DBG);
#include <zephyr/drivers/gpio.h>
#include <zephyr/net/latmon.h>
#include <zephyr/net/socket.h>
#include <zephyr/spinlock.h>
#include <zephyr/sys/atomic.h>
/*
* Blink Control
* DHCP: red
* waiting for connection: blue
* sampling: green
*/
#define LED_WAIT_PERIOD 1000000
#define LED_DHCP_PERIOD 500000
#define LED_RUN_PERIOD 200000
#define BLINK_THREAD_PRIORITY K_IDLE_PRIO
#define BLINK_STACK_SIZE 4096
static K_THREAD_STACK_DEFINE(blink_stack, BLINK_STACK_SIZE);
static const struct gpio_dt_spec pulse =
GPIO_DT_SPEC_GET_OR(DT_PATH(zephyr_user), pulse_gpios, {0});
static const struct gpio_dt_spec ack =
GPIO_DT_SPEC_GET_OR(DT_PATH(zephyr_user), ack_gpios, {0});
static K_SEM_DEFINE(ack_event, 0, 1);
#define DHCP_DONE (atomic_test_bit(&dhcp_done, 0) == true)
#define SET_DHCP_DONE atomic_set_bit(&dhcp_done, 0)
static atomic_val_t dhcp_done;
static struct k_spinlock lock;
static void gpio_ack_handler(const struct device *port,
struct gpio_callback *cb,
gpio_port_pins_t pins)
{
k_sem_give(&ack_event);
}
static int configure_measurement_hardware(void)
{
static struct gpio_callback gpio_cb = { };
int ret = 0;
if (!gpio_is_ready_dt(&pulse) || !gpio_is_ready_dt(&ack)) {
LOG_ERR("GPIO device not ready");
return -ENODEV;
}
ret = gpio_pin_configure_dt(&pulse, GPIO_OUTPUT_HIGH);
if (ret < 0) {
LOG_ERR("failed configuring pulse pin");
return ret;
}
ret = gpio_pin_configure_dt(&ack, GPIO_INPUT);
if (ret < 0) {
LOG_ERR("failed configuring ack pin");
return ret;
}
#if defined(CONFIG_LATMON_LOOPBACK_CALIBRATION)
/*
* Connect GPIO pins in loopback mode for validation (tx to ack)
* On FRDM_K64F, Latmus will show around 3.2 usec of latency.
*
* You can then use these values to adjust the reported latencies (ie,
* subtract the loopback latency from the measured latencies).
*/
ret = gpio_pin_interrupt_configure_dt(&ack, GPIO_INT_EDGE_FALLING);
#else
ret = gpio_pin_interrupt_configure_dt(&ack, GPIO_INT_EDGE_RISING);
#endif
if (ret < 0) {
LOG_ERR("failed configuring ack pin interrupt");
return ret;
}
gpio_init_callback(&gpio_cb, gpio_ack_handler, BIT(ack.pin));
ret = gpio_add_callback_dt(&ack, &gpio_cb);
if (ret < 0) {
LOG_ERR("failed adding ack pin callback");
return ret;
}
return ret;
}
static void blink(void*, void*, void*)
{
const struct gpio_dt_spec led_run =
GPIO_DT_SPEC_GET_OR(DT_ALIAS(led0), gpios, {0});
const struct gpio_dt_spec led_wait =
GPIO_DT_SPEC_GET_OR(DT_ALIAS(led1), gpios, {0});
const struct gpio_dt_spec led_dhcp =
GPIO_DT_SPEC_GET_OR(DT_ALIAS(led2), gpios, {0});
const struct gpio_dt_spec *led = &led_dhcp, *tmp = NULL;
uint32_t period = LED_DHCP_PERIOD;
if (gpio_is_ready_dt(&led_run)) {
gpio_pin_configure_dt(&led_run, GPIO_OUTPUT_INACTIVE);
}
if (gpio_is_ready_dt(&led_wait)) {
gpio_pin_configure_dt(&led_wait, GPIO_OUTPUT_INACTIVE);
}
if (gpio_is_ready_dt(&led_dhcp)) {
gpio_pin_configure_dt(&led_dhcp, GPIO_OUTPUT_INACTIVE);
}
for (;;) {
k_usleep(period);
if (DHCP_DONE) {
led = net_latmon_running() ? &led_run : &led_wait;
}
if (tmp && led != tmp) {
gpio_pin_set_dt(tmp, 0);
}
if (!gpio_is_ready_dt(led)) {
continue;
}
if (led == &led_wait) {
period = LED_WAIT_PERIOD;
}
if (led == &led_run) {
period = LED_RUN_PERIOD;
}
gpio_pin_toggle_dt(led);
tmp = led;
}
gpio_pin_set_dt(led, 0);
}
static k_tid_t start_led_blinking_thread(struct k_thread *blink_thread,
k_thread_entry_t blink_thread_func)
{
return k_thread_create(blink_thread, blink_stack, BLINK_STACK_SIZE,
(k_thread_entry_t)blink_thread_func,
NULL, NULL, NULL,
BLINK_THREAD_PRIORITY, 0, K_NO_WAIT);
}
/* Raw ticks */
#define CALCULATE_DELTA(ack, pulse) \
((ack) < (pulse) ? \
(~(pulse) + 1 + (ack)) : ((ack) - (pulse)))
static int measure_latency_cycles(uint32_t *delta)
{
k_spinlock_key_t key;
uint32_t tx = 0;
uint32_t rx = 0;
int ret = 0;
/* Remove spurious events */
k_sem_reset(&ack_event);
/* Generate a falling edge pulse to the DUT */
key = k_spin_lock(&lock);
if (gpio_pin_set_dt(&pulse, 0)) {
k_spin_unlock(&lock, key);
LOG_ERR("Failed to set pulse pin");
ret = -1;
goto out;
}
tx = k_cycle_get_32();
k_spin_unlock(&lock, key);
/* Wait for a rising edge from the Latmus controlled DUT */
if (k_sem_take(&ack_event, K_MSEC(1)) == 0) {
rx = k_cycle_get_32();
/* Measure the cycles */
*delta = CALCULATE_DELTA(rx, tx);
} else {
ret = -1;
}
out:
if (gpio_pin_set_dt(&pulse, 1)) {
LOG_ERR("Failed to clear pulse pin");
ret = -1;
}
return ret;
}
int main(void)
{
struct net_if *iface = net_if_get_default();
struct k_thread blink_thread;
static k_tid_t blink_tid;
int client, socket = 0;
int ret = 0;
/* Prepare the instrumentation */
if (configure_measurement_hardware() < 0) {
LOG_ERR("Failed to configure the measurement hardware");
return -1;
}
/* Start visual indicators - dhcp/blue, waiting/red, running/green */
blink_tid = start_led_blinking_thread(&blink_thread, blink);
if (!blink_tid) {
LOG_WRN("Failed to start led blinking thread");
}
/* Get a valid ip */
LOG_INF("DHCPv4: binding...");
net_dhcpv4_start(iface);
for (;;) {
ret = net_mgmt_event_wait(NET_EVENT_IPV4_DHCP_BOUND, NULL,
NULL, NULL, NULL, K_SECONDS(10));
if (ret == -ETIMEDOUT) {
LOG_WRN("DHCPv4: binding timed out, retrying...");
continue;
}
if (ret < 0) {
LOG_ERR("DHCPv4: binding failed, aborting...");
goto out;
}
break;
}
SET_DHCP_DONE;
/* Get a socket to the Latmus port */
socket = net_latmon_get_socket(NULL);
if (socket < 0) {
LOG_ERR("Failed to get a socket to latmon (errno %d)", socket);
ret = -1;
goto out;
}
for (;;) {
/* Wait for Latmus to connect */
client = net_latmon_connect(socket,
&iface->config.dhcpv4.requested_ip);
if (client < 0) {
if (client == -EAGAIN) {
continue;
}
LOG_ERR("Failed to connect to latmon");
ret = -1;
goto out;
}
/* Provide latency data until Latmus closes the connection */
net_latmon_start(client, measure_latency_cycles);
}
out:
k_thread_abort(blink_tid);
close(socket);
return ret;
}