diff --git a/samples/net/latmon/CMakeLists.txt b/samples/net/latmon/CMakeLists.txt new file mode 100644 index 00000000000..85ded7c5c74 --- /dev/null +++ b/samples/net/latmon/CMakeLists.txt @@ -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) diff --git a/samples/net/latmon/Kconfig b/samples/net/latmon/Kconfig new file mode 100644 index 00000000000..1486a998e36 --- /dev/null +++ b/samples/net/latmon/Kconfig @@ -0,0 +1,13 @@ +# Private config options for Latmon Sample app +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2025 Jorge Ramirez-Ortiz + +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" diff --git a/samples/net/latmon/README.rst b/samples/net/latmon/README.rst new file mode 100644 index 00000000000..b945bdac6b0 --- /dev/null +++ b/samples/net/latmon/README.rst @@ -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 `_. + +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 `_. 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] phy_mc_ksz8081: PHY 0 is up + [00:00:03.311,000] phy_mc_ksz8081: PHY (0) Link speed 100 Mb, full duplex + [00:00:03.312,000] eth_nxp_enet_mac: Link is up + *** Booting Zephyr OS build v4.1.0-3337-g886443a190b1 *** + [00:00:03.313,000] sample_latmon: DHCPv4: binding... + [00:00:03.313,000] latmon: Latmon server thread priority: 14 + [00:00:10.964,000] net_dhcpv4: Received: 192.168.1.58 + [00:00:10.964,000] sample_latmon: Listening on 192.168.1.58 + [00:00:30.966,000] latmon: Waiting for Latmus ... + [00:00:31.356,000] latmon: Monitor thread priority: -16 + [00:00:31.356,000] latmon: monitoring started: + [00:00:31.356,000] latmon: - samples per period: 1000 + [00:00:31.356,000] latmon: - period: 1000 usecs + [00:00:31.356,000] latmon: - histogram cells: 200 + [00:00:31.393,000] 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" diff --git a/samples/net/latmon/boards/frdm_k64f.overlay b/samples/net/latmon/boards/frdm_k64f.overlay new file mode 100644 index 00000000000..97a32616629 --- /dev/null +++ b/samples/net/latmon/boards/frdm_k64f.overlay @@ -0,0 +1,12 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright (c) 2025 Jorge Ramirez-Ortiz + */ + +/ { + zephyr,user { + pulse_gpios = <&gpioe 24 GPIO_ACTIVE_HIGH>; + ack_gpios = <&gpioe 25 GPIO_ACTIVE_HIGH>; + }; +}; diff --git a/samples/net/latmon/prj.conf b/samples/net/latmon/prj.conf new file mode 100644 index 00000000000..6d1ef64b8ba --- /dev/null +++ b/samples/net/latmon/prj.conf @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) 2025 Jorge Ramirez-Ortiz + +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 diff --git a/samples/net/latmon/sample.yaml b/samples/net/latmon/sample.yaml new file mode 100644 index 00000000000..a1a405f9203 --- /dev/null +++ b/samples/net/latmon/sample.yaml @@ -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 diff --git a/samples/net/latmon/src/main.c b/samples/net/latmon/src/main.c new file mode 100644 index 00000000000..92eb1fc82aa --- /dev/null +++ b/samples/net/latmon/src/main.c @@ -0,0 +1,271 @@ +/* + * + * SPDX-License-Identifier: Apache-2.0 + * + * Copyright (c) 2025 Jorge Ramirez-Ortiz + */ + +#include +LOG_MODULE_REGISTER(sample_latmon, LOG_LEVEL_DBG); + +#include +#include +#include +#include +#include +/* + * 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; +}