tests: Added test for PTP clock

This commit is contained in:
Marian Buschsieweke 2020-07-15 17:18:59 +02:00
parent 0da5d1607c
commit 2e529e92a4
No known key found for this signature in database
GPG Key ID: 61F64C6599B1539F
4 changed files with 362 additions and 0 deletions

View File

@ -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

View File

@ -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: <INSERT VERSION HERE>)
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.

View File

@ -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 <marian.buschsieweke@ovgu.de>
*
* @}
*/
#include <stdint.h>
#include <stdatomic.h>
#include <stdlib.h>
#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(&timestamp, 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(&timestamp);
for (unsigned i = 0; i < TEST_ROUNDS + 1; i++) {
/* wait for periodic timer IRQ */
mutex_lock(&sync_mutex);
uint64_t now_ns = atomic_load(&timestamp);
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;
}

View File

@ -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))