Linux Audio

Check our new training course

Embedded Linux Audio

Check our new training course
with Creative Commons CC-BY-SA
lecture materials

Bootlin logo

Elixir Cross Referencer

Loading...
/*
 * Copyright (c) 2022 Intel Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <limits.h>
#include <math.h>
#include <stdlib.h>

#include <zephyr/kernel.h>

#include <zephyr/tc_util.h>
#include <zephyr/ztest.h>

static uint32_t periodic_idx;
static uint32_t periodic_rollovers;
static uint64_t periodic_data[CONFIG_TIMER_TEST_SAMPLES + 1];
static uint64_t periodic_start, periodic_end;
static struct k_timer periodic_timer;
K_SEM_DEFINE(periodic_sem, 0, 1);

void periodic_fn(struct k_timer *t)
{
	if (periodic_idx < CONFIG_TIMER_TEST_SAMPLES) {

#ifdef CONFIG_TIMER_HAS_64BIT_CYCLE_COUNTER
		periodic_end = k_cycle_get_64();
#else
		periodic_end = k_cycle_get_32();
#endif
		periodic_data[periodic_idx + 1] = periodic_end;
		periodic_idx++;
	} else {
		k_timer_stop(t);
		k_sem_give(&periodic_sem);
	}
}

/* Get a difference in cycles between one timer count and an earlier one
 * accounting for potentially wrapped values.
 *
 * @retval 0 an unhandled wrap of the timer occurred and the value should be ignored
 */
uint64_t periodic_diff(uint64_t later, uint64_t earlier)
{
	/* Timer wrap around, will be ignored in statistics */
	if (later < earlier) {
		TC_ERROR("Caught a timer wrap around which isn't handled!\n");
		return 0;
	}

	return later - earlier;
}

double cycles_to_us(double cycles)
{
	return 1000000.0 * (cycles / (double)CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC);
}

/**
 * @brief Test a timers jitter and drift over time
 *
 * Named alpha_jitter_drift as this test requires ideally no
 * clock counter roll overs while running on Arm and should be
 * the first test in the suite to run
 */
ZTEST(timer_jitter_drift, test_jitter_drift)
{
	TC_PRINT("periodic timer behavior test\n");

	k_timer_init(&periodic_timer, periodic_fn, NULL);

	/* Tick align */
	k_usleep(1);

#ifdef CONFIG_TIMER_HAS_64BIT_CYCLE_COUNTER
	periodic_start = k_cycle_get_64();
#else
	periodic_start = k_cycle_get_32();
#endif

	periodic_data[0] = periodic_start;
	k_timer_start(&periodic_timer, K_USEC(CONFIG_TIMER_TEST_PERIOD),
		      K_USEC(CONFIG_TIMER_TEST_PERIOD));
	k_sem_take(&periodic_sem, K_FOREVER);

	TC_PRINT("periodic timer samples gathered, calculating statistics\n");

	/* calculate variance, and precision */
	uint64_t total_cycles = 0;

	uint64_t max_cyc = 0;
	uint64_t min_cyc = UINT64_MAX;

	for (int i = 0; i < CONFIG_TIMER_TEST_SAMPLES; i++) {
		uint64_t diff = periodic_diff(periodic_data[i + 1], periodic_data[i]);

		if (diff == 0) {
			periodic_rollovers++;
		} else {
			total_cycles += diff;
			min_cyc = MIN(diff, min_cyc);
			max_cyc = MAX(diff, max_cyc);
		}
	}

	double min_us = cycles_to_us((double)min_cyc);
	double max_us = cycles_to_us((double)max_cyc);

	double mean_cyc =
		(double)total_cycles / (double)(CONFIG_TIMER_TEST_SAMPLES - periodic_rollovers);
	double mean_us = cycles_to_us((double)total_cycles) /
			 (double)(CONFIG_TIMER_TEST_SAMPLES - periodic_rollovers);
	double variance_us = 0;
	double variance_cyc = 0;

	for (int i = 0; i < CONFIG_TIMER_TEST_SAMPLES; i++) {
		uint64_t diff = periodic_diff(periodic_data[i + 1], periodic_data[i]);

		if (diff != 0) {
			double mean_cyc_diff = (double)diff - mean_cyc;
			double mean_us_diff = cycles_to_us((double)diff) - mean_us;
			double mean_cyc_diff_sq = mean_cyc_diff * mean_cyc_diff;
			double mean_us_diff_sq = mean_us_diff * mean_us_diff;

			variance_us += mean_us_diff_sq;
			variance_cyc += mean_cyc_diff_sq;
		}
	}

	/* A measure of how wide the distribution is, ideal is 0 */
	variance_us = variance_us / (double)(CONFIG_TIMER_TEST_SAMPLES - periodic_rollovers);
	variance_cyc = variance_cyc / (double)(CONFIG_TIMER_TEST_SAMPLES - periodic_rollovers);

	/* A measure of timer precision, ideal is 0 */
	double stddev_us = sqrtf(variance_us);
	double stddev_cyc = sqrtf(variance_cyc);

	/* Use double precision math here as integer overflows are possible in doing all the
	 * conversions otherwise
	 */
	double expected_time_us =
		(double)CONFIG_TIMER_TEST_PERIOD * (double)CONFIG_TIMER_TEST_SAMPLES;
	double actual_time_us = cycles_to_us((double)periodic_end - (double)periodic_start);

	/* While this could be non-integer, the mean should be very close to it over time */
	double expected_period =
		((double)CONFIG_TIMER_TEST_PERIOD * (double)CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC) /
		1000000.0;
	/*
	 * Expected period drift(us) due to round up/down errors during the
	 * conversion between ticks, cycles and delay.
	 */
	k_timeout_t actual_timeout = K_USEC(CONFIG_TIMER_TEST_PERIOD);
	uint64_t cyc_per_tick = CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC
		/ CONFIG_SYS_CLOCK_TICKS_PER_SEC;
	double expected_period_drift = ((double)actual_timeout.ticks * cyc_per_tick
		- expected_period) / CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC * 1000000;
	double expected_time_drift_us = expected_period_drift
		* CONFIG_TIMER_TEST_SAMPLES;
	double time_diff_us = actual_time_us - expected_time_us
		- expected_time_drift_us;

	TC_PRINT("timer clock rate %d, kernel tick rate %d, expected period %d us, samples %d, "
		"period %f cycles, expected period drift %f us\n",
		CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC, CONFIG_SYS_CLOCK_TICKS_PER_SEC,
		CONFIG_TIMER_TEST_PERIOD, CONFIG_TIMER_TEST_SAMPLES, expected_period,
		expected_period_drift);
	TC_PRINT("statistics samples %d, rollovers %u, mean %f us, variance %f us, stddev %f us, "
		 "min %f us, max %f us\n",
		 CONFIG_TIMER_TEST_SAMPLES - periodic_rollovers, periodic_rollovers, mean_us,
		 variance_us, stddev_us, min_us, max_us);
	TC_PRINT("statistics samples %d, rollovers %u, mean %f cycles, variance %f cycles, stddev "
		 "%f cycles, min %llu cycles, max %llu cycles\n",
		 CONFIG_TIMER_TEST_SAMPLES - periodic_rollovers, periodic_rollovers, mean_cyc,
		 variance_cyc, stddev_cyc, min_cyc, max_cyc);
	TC_PRINT("timer start cycle %llu, end cycle %llu, total time %f, "
		"expected time %f, expected time drift %f, difference %f\n",
		periodic_start, periodic_end, actual_time_us, expected_time_us,
		expected_time_drift_us, time_diff_us);

	/* Validate the maximum/minimum timer period is off by no more than 10% */
	double test_period = (double)CONFIG_TIMER_TEST_PERIOD;
	double period_max_drift_percentage =
		(double)CONFIG_TIMER_TEST_PERIOD_MAX_DRIFT_PERCENT / 100;
	double min_us_bound = test_period - period_max_drift_percentage * test_period
		+ expected_period_drift;
	double max_us_bound = test_period + period_max_drift_percentage * test_period
		+ expected_period_drift;

	zassert_true(min_us >= min_us_bound,
		"Shortest timer period too short (off by more than expected %d%)",
		CONFIG_TIMER_TEST_PERIOD_MAX_DRIFT_PERCENT);
	zassert_true(max_us <= max_us_bound,
		"Longest timer period too long (off by more than expected %d%)",
		CONFIG_TIMER_TEST_PERIOD_MAX_DRIFT_PERCENT);

	/* Validate the timer deviation (precision/jitter of the timer) is within a configurable
	 * bound
	 */
	zassert_true(stddev_us < (double)CONFIG_TIMER_TEST_MAX_STDDEV,
		     "Standard deviation (in microseconds) outside expected bound");

	/* Validate the timer drift (accuracy over time) is within a configurable bound */
	zassert_true(abs(time_diff_us) < CONFIG_TIMER_TEST_MAX_DRIFT,
		     "Drift (in microseconds) outside expected bound");
}

ZTEST_SUITE(timer_jitter_drift, NULL, NULL, NULL, NULL, NULL);