| /* |
| * 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); |