diff --git a/tests/periph_ptp_clock/Makefile b/tests/periph_ptp_clock/Makefile new file mode 100644 index 0000000000..69aaf1a546 --- /dev/null +++ b/tests/periph_ptp_clock/Makefile @@ -0,0 +1,16 @@ +BOARD ?= nucleo-f767zi +include ../Makefile.tests_common + +FEATURES_REQUIRED += periph_ptp +FEATURES_OPTIONAL += periph_ptp_speed_adjustment +FEATURES_REQUIRED += periph_timer_periodic + +USEMODULE += fmt + +TIMER_FREQ ?= + +include $(RIOTBASE)/Makefile.include + +ifneq (,$(TIMER_FREQ)) + CFLAGS += -DTIM_FREQ=$(TIMER_FREQ) +endif diff --git a/tests/periph_ptp_clock/README.md b/tests/periph_ptp_clock/README.md new file mode 100644 index 0000000000..6ef63dd55a --- /dev/null +++ b/tests/periph_ptp_clock/README.md @@ -0,0 +1,67 @@ +Peripheral PTP Clock Test Application +===================================== + +Hardware Requirements +--------------------- + +In addition to the `periph_ptp` feature, this tests requires the peripheral +timer to be run on a clock synchronous to the PTP clock. Otherwise clock +drift between the two clocks might result in the test failing even though the +driver works correctly. + +Verifying Clock Speed Adjustment +-------------------------------- + +In the first part of the test the speed of the PTP clock is adjusted to +different values. The test sleeps for 0.5 seconds and verifies that the clock +drift between the high level timer API and PTP clock matches the speed +adjustment (± 0.01%). + +Verifying Clock Adjustment +-------------------------- + +The second part of the test verifies that adjusting the clock does not +introduce noticeable errors. The clock speed is set back to nominal speed +prior to this test part (so that it synchronous to the high level timer). For +each of a set of predefined clock adjustment values the following is done: + +- `xtimer_period_wakeup()` is used to get 50 wake ups after 10 ms each + - right before sleeping, the PTP clock is adjusted + - right after the wake up, the PTP clock is read +- The difference between the two PTP timestamps should be the sum of the + 10 ms period length and the adjustment + - The difference of the actual result and the expected result + (10 ms + adjustment) is stored for each round +- The average, minimum, maximum, and variance (σ²) is calculated from these + differences between actual period length and expected period length + +A test run is considered a pass, if the average error is less than ± 250 ns +and minimum/maximum error are less than ± 500 ns. + +Expected Output on Success +-------------------------- + + main(): This is RIOT! (Version: ) + Testing clock at speed 0 (~100.000 % of nominal speed): SUCCEEDED + Testing clock at speed 32767 (~149.999 % of nominal speed): SUCCEEDED + Testing clock at speed -32768 (~50.000 % of nominal speed): SUCCEEDED + Testing clock at speed 1337 (~102.040 % of nominal speed): SUCCEEDED + Testing clock at speed -1337 (~97.961 % of nominal speed): SUCCEEDED + Testing clock at speed 42 (~100.064 % of nominal speed): SUCCEEDED + Testing clock at speed -42 (~99.937 % of nominal speed): SUCCEEDED + Testing clock at speed 665 (~101.015 % of nominal speed): SUCCEEDED + Testing clock at speed -665 (~98.986 % of nominal speed): SUCCEEDED + Testing clock adjustments for offset 0: SUCCEEDED + Statistics: avg = 0, min = 0, max = 0, σ² = 0 + Testing clock adjustments for offset -1337: SUCCEEDED + Statistics: avg = 0, min = 0, max = 0, σ² = 0 + Testing clock adjustments for offset 1337: SUCCEEDED + Statistics: avg = 0, min = 0, max = 0, σ² = 0 + Testing clock adjustments for offset 2147483647: SUCCEEDED + Statistics: avg = 0, min = 0, max = 0, σ² = 0 + TEST SUCCEEDED! + +Note: If the values in the statistics of the clock adjustments differ to some + degree, this might be due ISR overhead and jitter when reading the + PTP timer from the periodic peripheral timer callback. The test will only + fail if either the worst case offset or the variance are too big. diff --git a/tests/periph_ptp_clock/main.c b/tests/periph_ptp_clock/main.c new file mode 100644 index 0000000000..f3afc03b73 --- /dev/null +++ b/tests/periph_ptp_clock/main.c @@ -0,0 +1,261 @@ +/* + * Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @ingroup tests + * @{ + * + * @file + * @brief Peripheral PTP clock test application + * + * @author Marian Buschsieweke + * + * @} + */ + +#include +#include +#include + +#include "fmt.h" +#include "kernel_defines.h" +#include "macros/units.h" +#include "mutex.h" +#include "periph/ptp.h" +#include "periph/timer.h" + +#define TEST_TIME_US (US_PER_SEC / 2) +#define TEST_ROUNDS (50) +#define PERIOD_US (TEST_TIME_US / TEST_ROUNDS) + +#ifndef TIM +#define TIM TIMER_DEV(0) +#endif +#ifndef TIM_FREQ +#define TIM_FREQ MHZ(1) +#endif + +#define TIM_PERIOD (1ULL * PERIOD_US * TIM_FREQ / US_PER_SEC - 1) +#define TIM_TIME (1ULL * TEST_TIME_US * TIM_FREQ / US_PER_SEC) + +static mutex_t sync_mutex = MUTEX_INIT_LOCKED; +static atomic_uint_least64_t timestamp; + +static inline void print_s64_dec(int64_t _num) +{ + uint64_t num = _num; + if (_num < 0) { + print_str("-"); + num = -_num; + } + print_u64_dec(num); +} + +static void speed_adj_cb(void *arg, int chan) +{ + (void)arg; + (void)chan; + mutex_unlock(&sync_mutex); +} + +static int test_speed_adjustment(const int16_t speed) +{ + + uint32_t expected_ns = TEST_TIME_US * 1000; + expected_ns += ((int64_t)expected_ns * speed) >> 16; + const uint32_t expected_ns_lower = expected_ns * 9999ULL / 10000ULL; + const uint32_t expected_ns_upper = expected_ns * 10001ULL / 10000ULL; + + ptp_clock_adjust_speed(speed); + print_str("Testing clock at speed "); + print_s32_dec(speed); + print_str(" (~"); + { + int64_t tmp = speed * 100000ULL; + tmp = (tmp + UINT16_MAX / 2) / UINT16_MAX; + tmp += 100000ULL; + char output[16]; + print(output, fmt_s32_dfp(output, (int32_t)tmp, -3)); + } + print_str(" % of nominal speed): "); + + if (timer_init(TIM, TIM_FREQ, speed_adj_cb, NULL)) { + print_str("FAILED.\nCouldn't set up periph_timer for comparison\n"); + return 1; + } + + /* be double sure mutex is indeed locked */ + mutex_trylock(&sync_mutex); + + timer_stop(TIM); + + if (timer_set(TIM, 0, TIM_TIME)) { + print_str("FAILED.\nCouldn't set periph_timer for comparison\n"); + return 1; + } + + uint64_t start_ns = ptp_clock_read_u64(); + timer_start(TIM); + + mutex_lock(&sync_mutex); + uint64_t got_ns = ptp_clock_read_u64() - start_ns; + timer_stop(TIM); + uint64_t diff_ns = 0; + + int failed = 0; + if (got_ns > expected_ns_upper) { + failed = 1; + diff_ns = got_ns - expected_ns; + } + else if (got_ns < expected_ns_lower) { + failed = 1; + diff_ns = expected_ns - got_ns; + } + + if (failed) { + int64_t tmp = (int64_t)got_ns - (int64_t)expected_ns; + tmp = (tmp * 100000LL + expected_ns / 2) / expected_ns; + tmp += 100000LL; + char percentage[16]; + print_str("FAILED\n"); + print_str("expected: "); + print_u64_dec(expected_ns); + print_str(", got: "); + print_u64_dec(got_ns); + print_str(", diff: "); + print_u64_dec(diff_ns); + print_str(" ("); + print(percentage, fmt_s32_dfp(percentage, (int32_t)tmp, -3)); + print_str("% offset)\n"); + } + else { + print_str("SUCCEEDED\n"); + } + + return failed; +} + +static void clock_adj_cb(void *arg, int chan) +{ + int32_t offset = (uintptr_t)arg; + (void)chan; + uint64_t now = ptp_clock_read_u64(); + ptp_clock_adjust(offset); + atomic_store(×tamp, now); + mutex_unlock(&sync_mutex); +} + + +static int test_clock_adjustment(int32_t offset) +{ + /* Record one extra sample, to throw away the first measurement */ + static int64_t diffs[TEST_ROUNDS + 1]; + int64_t period_ns = PERIOD_US * 1000ULL + offset; + uint64_t last_ns; + + print_str("Testing clock adjustments for offset "); + print_s32_dec(offset); + print_str(": "); + + /* be double sure mutex is indeed locked */ + mutex_trylock(&sync_mutex); + + if (timer_init(TIM, TIM_FREQ, clock_adj_cb, (void *)offset) || + timer_set_periodic(TIM, 0, TIM_PERIOD, TIM_FLAG_RESET_ON_MATCH)) { + print_str("FAILED.\nCouldn't set up periph_timer for comparison\n"); + return 1; + } + + /* wait for periodic timer IRQ */ + mutex_lock(&sync_mutex); + last_ns = atomic_load(×tamp); + + for (unsigned i = 0; i < TEST_ROUNDS + 1; i++) { + /* wait for periodic timer IRQ */ + mutex_lock(&sync_mutex); + uint64_t now_ns = atomic_load(×tamp); + diffs[i] = (int64_t)(now_ns - last_ns) - period_ns; + last_ns = now_ns; + } + + timer_stop(TIM); + + int64_t avg = 0, var = 0, min = INT64_MAX, max = INT64_MIN; + for (unsigned i = 1; i < TEST_ROUNDS + 1; i++) { + avg += diffs[i]; + } + avg = (avg + TEST_ROUNDS / 2) / TEST_ROUNDS; + for (unsigned i = 1; i < TEST_ROUNDS + 1; i++) { + int64_t tmp = diffs[i] - avg; + var += tmp * tmp; + if (diffs[i] < min) { + min = diffs[i]; + } + if (diffs[i] > max) { + max = diffs[i]; + } + } + + var = (var + TEST_ROUNDS / 2) / TEST_ROUNDS; + + int failed = 0; + if ((max < 500) && (min > -500) && (avg < 250) && (avg > -250)) { + print_str("SUCCEEDED\n"); + } + else { + failed = 1; + print_str("FAILED\nDiffs:\n"); + for (unsigned i = 1; i < TEST_ROUNDS + 1; i++) { + print_s64_dec(diffs[i]); + print_str("\n"); + } + } + print_str("Statistics: avg = "); + print_s64_dec(avg); + print_str(", min = "); + print_s64_dec(min); + print_str(", max = "); + print_s64_dec(max); + print_str(", σ² = "); + print_s64_dec(var); + print_str("\n"); + + return failed; +} + +int main(void) +{ + static const int16_t speeds[] = { + 0, INT16_MAX, INT16_MIN, 1337, -1337, 42, -42, 665, -665 + }; + static const int32_t offsets[] = { + 0, -1337, +1337, INT32_MAX + }; + int failed = 0; + + if (IS_USED(MODULE_PERIPH_PTP_SPEED_ADJUSTMENT)) { + for (size_t i = 0; i < ARRAY_SIZE(speeds); i++) { + failed |= test_speed_adjustment(speeds[i]); + } + + /* Restore nominal clock speed */ + ptp_clock_adjust_speed(0); + } + + for (size_t i = 0; i < ARRAY_SIZE(offsets); i++) { + failed |= test_clock_adjustment(offsets[i]); + } + + if (failed) { + print_str("TEST FAILED!\n"); + } + else { + print_str("TEST SUCCEEDED!\n"); + } + return 0; +} diff --git a/tests/periph_ptp_clock/tests/01-run.py b/tests/periph_ptp_clock/tests/01-run.py new file mode 100755 index 0000000000..d69fd91a34 --- /dev/null +++ b/tests/periph_ptp_clock/tests/01-run.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2020 Otto-von-Guericke-Universität Magdeburg +# +# This file is subject to the terms and conditions of the GNU Lesser +# General Public License v2.1. See the file LICENSE in the top level +# directory for more details. + +import sys +from testrunner import run + + +def testfunc(child): + child.expect_exact("TEST SUCCEEDED!\r\n") + + +if __name__ == "__main__": + sys.exit(run(testfunc))