From d7cf39551f87e2619799451613635296851a8b44 Mon Sep 17 00:00:00 2001 From: Marian Buschsieweke Date: Tue, 27 Jun 2023 22:13:32 +0200 Subject: [PATCH] tests/periph/selftest_shield: add test application This test application makes use of the RIOT Peripheral Selftest Shield, which connects e.g. PWM to ADC or SPI MOSI to SPI MISO, UART TXD to RXD, etc. This provides quick and fully automated self testing capabilities. Please note that the simplicity and ease of use of the hardware comes with a prices: There are whole classes of issues that cannot be detected automatically. This test cannot replace other testing approaches (such as manual testing or PHiLIP on the HiL), but only complement them. --- tests/periph/selftest_shield/Makefile | 40 + .../periph/selftest_shield/Makefile.board.dep | 5 + tests/periph/selftest_shield/README.md | 134 +++ tests/periph/selftest_shield/main.c | 1046 +++++++++++++++++ .../tests-with-config/01-run.py | 18 + tests/periph/selftest_shield/tests/01-run.py | 21 + 6 files changed, 1264 insertions(+) create mode 100644 tests/periph/selftest_shield/Makefile create mode 100644 tests/periph/selftest_shield/Makefile.board.dep create mode 100644 tests/periph/selftest_shield/README.md create mode 100644 tests/periph/selftest_shield/main.c create mode 100755 tests/periph/selftest_shield/tests-with-config/01-run.py create mode 100755 tests/periph/selftest_shield/tests/01-run.py diff --git a/tests/periph/selftest_shield/Makefile b/tests/periph/selftest_shield/Makefile new file mode 100644 index 0000000000..d3166a9b86 --- /dev/null +++ b/tests/periph/selftest_shield/Makefile @@ -0,0 +1,40 @@ +BOARD ?= arduino-due + +include ../Makefile.periph_common + +FEATURES_REQUIRED += arduino_pins +FEATURES_REQUIRED += arduino_shield_uno + +FEATURES_OPTIONAL += arduino_analog +FEATURES_OPTIONAL += arduino_i2c +FEATURES_OPTIONAL += arduino_pwm +FEATURES_OPTIONAL += arduino_shield_isp +FEATURES_OPTIONAL += arduino_spi +FEATURES_OPTIONAL += arduino_uart +FEATURES_OPTIONAL += periph_adc +FEATURES_OPTIONAL += periph_gpio +FEATURES_OPTIONAL += periph_gpio_irq +FEATURES_OPTIONAL += periph_i2c +FEATURES_OPTIONAL += periph_pwm +FEATURES_OPTIONAL += periph_spi +FEATURES_OPTIONAL += periph_timer +FEATURES_OPTIONAL += periph_uart + +STOP_ON_FAILURE ?= 0 +DETAILED_OUTPUT ?= 0 + +include $(RIOTBASE)/Makefile.include + +ifneq ($(MCU),esp32) + # We only need 1 thread (+ the Idle thread on some platforms) and we really + # want this app working on as many boards as possible + CFLAGS += -DMAXTHREADS=2 +else + # ESP32x SoCs uses an extra thread for esp_timer + CFLAGS += -DMAXTHREADS=3 +endif + +CFLAGS += \ + '-DSTOP_ON_FAILURE=$(STOP_ON_FAILURE)' \ + '-DDETAILED_OUTPUT=$(DETAILED_OUTPUT)' \ + # diff --git a/tests/periph/selftest_shield/Makefile.board.dep b/tests/periph/selftest_shield/Makefile.board.dep new file mode 100644 index 0000000000..7d1378f079 --- /dev/null +++ b/tests/periph/selftest_shield/Makefile.board.dep @@ -0,0 +1,5 @@ +ifneq (,$(filter periph_i2c,$(FEATURES_USED))) + ifneq (,$(filter arduino_i2c,$(FEATURES_USED))) + USEMODULE += pcf8574 + endif +endif diff --git a/tests/periph/selftest_shield/README.md b/tests/periph/selftest_shield/README.md new file mode 100644 index 0000000000..8c1706ceff --- /dev/null +++ b/tests/periph/selftest_shield/README.md @@ -0,0 +1,134 @@ +# Peripheral Test Battery using the Peripheral Selftest Shield + +@warning Before you power your board, make sure the shield's VCC voltage + selector is matching the logic level of your board. E.g. having the + VCC selector at 5 V and testing on a 3.3 V logic board with pins that + are not 5 V tolerant, you might damage your board permanently. + +## Quick Start + +0. For the MCU family you want to test, find a compatible board + - the board needs to be mechanically and electrically compatible with + Arduino UNO shields + - The Arduino GPIO pin mapping and ideally mapping of SPI, I2C, ADC, and + PWM needs to be provided +1. Unplug your board (so that it is not powered) +2. Connect the shield +3. Move the VCC selector to match the logic level of the board. + - If unsure, go for 3.3 V + - The nRF52840DK works fine with 3.3 V, despite the logic level being only + 3.0 V +4. Close the loops for the peripherals you want to test be moving the DIP + switches into the `ON` position, leave the untested open by moving it in the + `OFF` position + - Start by enabling all but UART + - If the UART test is actually run and fails, also enable the UART switch + - (Background: If the UART at D0 and D1 is used for stdio, it cannot be + looped and tested) +5. Flash and run the test + - In this directory, run `make BOARD= flash test` + +## Details + +This test application does a full self test of any of the following peripheral +driver, if supported by the board and Arduino I/O mapping is available: + +- `periph_adc` (1) +- `periph_gpio` +- `periph_gpio_irq` +- `periph_i2c` (2) +- `periph_pwm` +- `periph_spi` (3) +- `periph_uart` (4) + +Notes: +1. The ADC input is generated via a 4-bit R-2R resistor ladder and the I2C + attached GPIO extender and/or PWM. If neither is available, no tests are + performed. If both are available, up to three ADC channels are tested: + Those muxed to pins A0, A1, and A2. +2. I2C is indirectly tested by operating the PCF8574 I2C GPIO extender and + observing the result using internal GPIOs connected to the external ones. + This test is only enabled if both `periph_i2c` and `periph_gpio` is + available +3. Correctness of the bit order and the clock phase is not checked due to the + limitations of the loopback mode testing approach. The clock polarity is + validated (at idle level), the correctness of the clock frequency is roughly + tested when `periph_timer` is implemented. +4. UART self testing is skipped when the D0/D1 UART interface is used for + stdio. Correctness of the symbol rate (often incorrectly referred to as + "baudrate") is only checked for plausibility when `periph_timer` is + available. Checking different bits per character, number of stop bits, + parity bit etc. is possible using `periph_timer`, but not yet implemented + here. + +## Debugging Failures + +### Increase Verbosity + +Compile with `make DETAILED_OUTPUT=1` to increase the verbosity of the output +at the cost of increased ROM. When you start seeing link failures due to the +ROM cost, you could disable some of the succeeding tests by blacklisting the +peripheral features unaffected. However, be aware some peripherals are tested +with the help of others; e.g. disabling the ADC will prevent testing the +duty cycle of PWM outputs for correctness. + +### Early Critical Failure + +If the application fails before performing any tests, e.g. the output looks +similar to this: + +``` +START +main(): This is RIOT! (Version: 2023.10-devel-348-gf1c68-peripheral-selftest) +self-testing peripheral drivers +=============================== +CRITICAL FAILURE in tests/periph/selftest_shield/main.c: +``` + +This will indicate a failure to initialize the GPIO extender. Possible reasons +are: + +1. Wrong I2C configuration + - Is the I2C bus (typically in the `periph_conf.h` or in a file included by + `periph_conf.h`) correctly configured? Is it connected to the Arduino UNO + pins D14 / D15? + - Is the `ARDUINO_I2C_UNO` macro correctly defined to the index of the + I2C bus connected Arduino UNO pins D14 / D15 +2. Missing pull-up resistor + - The shield has no pull up resistors on the I2C bus + - All RIOT I2C drivers will enable the internal MCU pull up resistors for + the I2C buses by default. But some MCUs do not have internal pull up + resistors (on some pins) or may not be able to enable them when used in + I2C mode. + - If possible, the I2C driver / I2C board configuration in `periph_conf.h` + should be configured so that robust operation works out of the box without + having to add external pull ups. + - If the board depends on external pulls ups (no suitable internal pull ups + can be used and the board has no external pull ups on D15 / D15), pull + ups need to be added to the board. The easiest is to just plug in an + I2C sensor/EEPROM breakout into the I2C pin socket at J2, as those + breakout boards typically have external pull up resistors integrated. +3. Bug in the I2C driver + - A bug in the I2C driver could cause this + - A logic analyzer connected to the I2C Header (J3), the I2C Socket (J2), + or the I2C Groove Connector (J1) will come in handy +4. Hardware issue + - This is the least likely cause :wink: + +### Repeated Instances of the Same Error + +Many tests are performed in a loop. E.g. some tests are repeated for +`flaky_test_repetitions` (by default `100`), as some tests may pass by chance. +Some tests also iterate over different configurations (such as clock speeds) +and repeat the same tests for each configuration. + +To reduce the noise, you can pass `STOP_ON_FAILURE=1` as environment +variable or as parameter to `make` on compilation to abort the test run on the +first failing test. + +## Configuration + +The test requires no configuration and will by default test all of the above +peripheral drivers for which Arduino I/O mappings exists. To disable a subset +of the tests (e.g. to safe RAM/ROM to trim down the test to a low end board), it +is possible to disable features (`FEATURES_BLACKLIST += periph_`). diff --git a/tests/periph/selftest_shield/main.c b/tests/periph/selftest_shield/main.c new file mode 100644 index 0000000000..9450968d4f --- /dev/null +++ b/tests/periph/selftest_shield/main.c @@ -0,0 +1,1046 @@ +/* + * Copyright (C) 2021 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 Test application for the Peripheral GPIO Low-Level API + * + * @author Marian Buschsieweke + * + * @} + */ + +#include +#include +#include +#include +#include +#include + +#include "arduino_iomap.h" +#include "macros/units.h" +#include "macros/utils.h" +#include "modules.h" +#include "pcf857x.h" +#include "periph/adc.h" +#include "periph/gpio.h" +#include "periph/pwm.h" +#include "periph/spi.h" +#include "periph/timer.h" +#include "periph/uart.h" +#include "stdio_uart.h" /* for STDIO_UART_DEV */ + +/* BEGIN: controls of the behavior of the testing app: */ +#define ENABLE_DEBUG 1 +#include "debug.h" + +#ifndef STOP_ON_FAILURE +#define STOP_ON_FAILURE 0 +#endif + +#ifndef DETAILED_OUTPUT +#define DETAILED_OUTPUT 0 +#endif +/* END: controls of the behavior of the testing app: */ + +/* In order to run the periph_uart test all of the following needs to be true: + * - periph_uart needs to be used + * - an I/O mapping for the UART at D0/D1 needs to be provided + * - this UART dev is not busy with stdio + */ +#if defined(ARDUINO_UART_D0D1) && defined(MODULE_PERIPH_UART) +# define ENABLE_UART_TEST (STDIO_UART_DEV != ARDUINO_UART_D0D1) +# define UART_TEST_DEV ARDUINO_UART_D0D1 +#else +# define ENABLE_UART_TEST 0 +#endif + +/* In order to run the periph_pwm test, all of the following needs to be true: + * - periph_pwm needs to be used (so that we actually have something to test) + * - periph_adc needs to be used (so that we can validate the output duty cycle) + * - At least one of: + * - Arduino I/O mapping for PWM on D5 *AND* analog pin A2 is provided + * - Arduino I/O mapping for PWM on D6 *AND* analog pin A1 is provided + */ +#if defined(MODULE_PERIPH_PWM) && defined(MODULE_PERIPH_ADC) +# if defined(ARDUINO_PIN_5_PWM_DEV) && defined(ARDUINO_A2) +# define ENABLE_PWM_TEST_D5 1 +# else +# define ENABLE_PWM_TEST_D5 0 +# endif +# if defined(ARDUINO_PIN_6_PWM_DEV) && defined(ARDUINO_A1) +# define ENABLE_PWM_TEST_D6 1 +# else +# define ENABLE_PWM_TEST_D6 0 +# endif +# define ENABLE_PWM_TEST (ENABLE_PWM_TEST_D5 || ENABLE_PWM_TEST_D6) +#else +# define ENABLE_PWM_TEST 0 +# define ENABLE_PWM_TEST_D5 0 +# define ENABLE_PWM_TEST_D6 0 +#endif + +/* In order to run the periph_adc test, we need: + * - periph_adc support (so that we have something to test) + * - Arduino I/O mapping for ADC (so that we know which line to test) + * - The PCF857x driver (so that we can control the R-2R resistor ladder + * connected to the GPIO expander. + */ +#if defined(MODULE_PERIPH_ADC) && defined(ARDUINO_A0) && defined(MODULE_PCF857X) && 0 +// TODO: Re-anble when PCB is fixed +# define ENABLE_ADC_TEST 1 +#else +# define ENABLE_ADC_TEST 0 +#endif + +/* We want the code to be compile-tested even when tests are disabled. We + * provide dummy parameters for the tests when they are off. The actual + * values don't matter, as the tests will be disabled anyway when the + * parameters are missing. But having the compiler checking the syntax and + * doing static analysis is useful in any case. */ +#ifndef ARDUINO_PIN_5_PWM_DEV +# define ARDUINO_PIN_5_PWM_DEV -1 +# define ARDUINO_PIN_5_PWM_CHAN -1 +#endif +#ifndef ARDUINO_PIN_6_PWM_DEV +# define ARDUINO_PIN_6_PWM_DEV -1 +# define ARDUINO_PIN_6_PWM_CHAN -1 +#endif +#ifndef ARDUINO_A0 +# define ARDUINO_A0 -1 +#endif +#ifndef ARDUINO_A1 +# define ARDUINO_A1 -1 +#endif +#ifndef ARDUINO_A2 +# define ARDUINO_A2 -1 +#endif +#ifndef UART_TEST_DEV +# define UART_TEST_DEV UART_DEV(0) +#endif +#ifndef TIMER +# define TIMER TIMER_DEV(0) +#endif + +/* A higher clock frequency is beneficial in being able to actually measure the + * difference of half a SPI clock cycle when clock phase is 1, compared to + * clock phase being 0. Most MCUs can clock their timers from the core clock + * directly, so the CPU clock is the highest clock frequency available. But + * some can't, so we handle them here explicitly. */ +#ifndef TIMER_FREQ_SPI_TEST +# if defined(CPU_SAM3) +# define TIMER_FREQ_SPI_TEST CLOCK_CORECLOCK / 4 +# elif defined(CPU_NRF52) || defined(CPU_NRF51) +# define TIMER_FREQ_SPI_TEST MHZ(16) +# else +# define TIMER_FREQ_SPI_TEST CLOCK_CORECLOCK +# endif +#endif + +/* for the UART test a slower frequency is more beneficial. We assume the + * timer to be 16 bit (tossing away the upper 16 bit of 32 bit timers) to + * ease the test. But the duration for transferring 8 bytes of UART data in + * ticks easily overflows 16 bit at higher frequencies, so we just go for + * a lower frequency instead. */ +#ifndef TIMER_FREQ_UART_TEST +# if defined(__AVR__) +# define TIMER_FREQ_UART_TEST CLOCK_CORECLOCK / 64 +# else +# define TIMER_FREQ_UART_TEST MHZ(1) +# endif +#endif + +static const char testdata[8] = "Selftest"; +static const spi_t spi_buses[] = { +#ifdef ARDUINO_SPI_D11D12D13 + ARDUINO_SPI_D11D12D13, +#endif +#ifdef ARDUINO_SPI_ISP + ARDUINO_SPI_ISP, +#endif +}; +static const gpio_t spi_clk_check_pins[] = { +#ifdef ARDUINO_SPI_D11D12D13 + PCF857X_GPIO_PIN(0, 2), +#endif +#ifdef ARDUINO_SPI_ISP + PCF857X_GPIO_PIN(0, 3), +#endif +}; + +static struct { + char data[8]; + uint8_t pos; +} serial_buf; + +/* This module is only used when both periph_i2c and the Arduino I/O + * mapping are present. If this module is not used, this test is optimized + * out as dead branch. We still want the compile test, though. */ +static const pcf857x_params_t params = { +#ifdef MODULE_PCF857X + .dev = ARDUINO_I2C_UNO, + .exp = PCF857X_EXP_PCF8574, +#endif +}; + +static pcf857x_t egpios; + +/* Some tests may pass due to luck (e.g. when expecting a pull up but input + * is floating, it will still get lucky from time to time). We repeat those + * in a loop to have some confidence that the test is consistently passing */ +static const unsigned flaky_test_repetitions = 100; + +/* We are trying to not spent much memory on error message for compatibility + * with as many boards as possible. The idea is that the line number where a + * test failed and careful commenting in the code provides the same level of + * insight with only a little inconvenience of having to open the editor. */ +static void print_test_failed(uint16_t line) +{ + printf("FAILURE in " __FILE__ ":%" PRIu16 "\n", line); +} + +static bool do_test(bool failed, uint16_t line) +{ + if (failed) { + print_test_failed(line); + if (STOP_ON_FAILURE) { + printf("Stopping, as STOP_ON_FAILURE==1\n"); + ARCHITECTURE_BREAKPOINT(1); + while (1) { + /* stop */ + } + } + } + + return failed; +} + +static void do_assert(bool failed, uint16_t line) +{ + if (failed) { + printf("CRITICAL "); + print_test_failed(line); + ARCHITECTURE_BREAKPOINT(1); + while (1) { + /* stop */ + } + } +} + +static void print_result(bool failed) +{ + if (failed) { + printf("[FAILED]\n"); + } + else { + printf("[OK]\n"); + } +} + +static void print_skipped(void) +{ + printf("(skipped)\n"); +} + +static void _print_start(const char *name, const char *detail, uint16_t line) +{ + if (DETAILED_OUTPUT) { + printf("Starting test for %s (%s) at " __FILE__ ":%" PRIu16"\n", name, + detail, line); + } + else { + printf("Starting test for %s at " __FILE__ ":%" PRIu16"\n", name, + line); + } +} + +#if DETAILED_OUTPUT +# define print_start(name, detail) _print_start(name, detail, __LINE__) +#else +# define print_start(name, detail) _print_start(name, NULL, __LINE__) +#endif + +#define TEST(x) do_test(!(x), __LINE__) + +#define ASSERT(x) do_assert(!(x), __LINE__) + +static void stupid_delay(unsigned count) +{ + while (count > 0) { + /* tell optimizer that the value of `count` is used and changed, so + * that the down-counting loop is not detected as dead code */ + __asm__ volatile ( + "" + /* outputs: */ + : "+r"(count) + /* inputs: */ + : + /* clobbers: */ + ); + count--; + } +} + +static void brief_delay(void) +{ + stupid_delay(100); +} + +static void long_delay(void) +{ + stupid_delay(10000); +} + +static bool periph_gpio_test_push_pull(void) +{ + bool failed = false; + print_start("GPIO", "push-pull"); + ASSERT(gpio_init(ARDUINO_PIN_3, GPIO_IN) == 0); + ASSERT(gpio_init(ARDUINO_PIN_4, GPIO_OUT) == 0); + + gpio_clear(ARDUINO_PIN_4); + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + failed |= TEST(gpio_read(ARDUINO_PIN_3) == 0); + } + + gpio_set(ARDUINO_PIN_4); + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + failed |= TEST(gpio_read(ARDUINO_PIN_3) != 0); + } + + print_result(failed); + + return failed; +} + +static bool periph_gpio_test_input_pull_up(void) +{ + bool failed = false; + print_start("GPIO", "input pull-up"); + ASSERT(gpio_init(ARDUINO_PIN_4, GPIO_IN) == 0); + if (gpio_init(ARDUINO_PIN_3, GPIO_IN_PU) == 0) { + /* give pull resistor a little time to pull */ + brief_delay(); + /* pull up should pull both D3 and D4 up, as D4 is connected to D3 via + * the testing shield */ + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + failed |= TEST(gpio_read(ARDUINO_PIN_3) != 0); + failed |= TEST(gpio_read(ARDUINO_PIN_4) != 0); + } + + /* push/pull on D4 should still be able to force down D3 */ + ASSERT(gpio_init(ARDUINO_PIN_4, GPIO_OUT) == 0); + gpio_clear(ARDUINO_PIN_4); + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + failed |= TEST(gpio_read(ARDUINO_PIN_3) == 0); + } + print_result(failed); + } + else { + print_skipped(); + } + + return failed; +} + +static bool periph_gpio_test_input_pull_down(void) +{ + bool failed = false; + print_start("GPIO", "input pull-down"); + ASSERT(gpio_init(ARDUINO_PIN_4, GPIO_IN) == 0); + if (gpio_init(ARDUINO_PIN_3, GPIO_IN_PD) == 0) { + /* give pull resistor a little time to pull */ + brief_delay(); + /* pull down should pull both D3 and D4 down, as D4 is connected to D3 + * via the testing shield */ + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + failed |= TEST(gpio_read(ARDUINO_PIN_3) == 0); + failed |= TEST(gpio_read(ARDUINO_PIN_4) == 0); + } + + /* push/pull on D4 should still be able to force up D3 */ + ASSERT(gpio_init(ARDUINO_PIN_4, GPIO_OUT) == 0); + gpio_set(ARDUINO_PIN_4); + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + failed |= TEST(gpio_read(ARDUINO_PIN_3) != 0); + } + print_result(failed); + } + else { + print_skipped(); + } + + return failed; +} + +static bool periph_gpio_test_open_drain_pull_up(void) +{ + bool failed = false; + print_start("GPIO", "open-drain pull-up"); + + ASSERT(gpio_init(ARDUINO_PIN_4, GPIO_IN) == 0); + if (gpio_init(ARDUINO_PIN_3, GPIO_OD_PU) == 0) { + gpio_set(ARDUINO_PIN_3); + /* give pull resistor a little time to pull */ + brief_delay(); + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + failed |= TEST(gpio_read(ARDUINO_PIN_4) != 0); + } + gpio_clear(ARDUINO_PIN_3); + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + failed |= TEST(gpio_read(ARDUINO_PIN_4) == 0); + } + print_result(failed); + } + else { + print_skipped(); + } + + return failed; +} + +static bool periph_gpio_test_open_drain_no_pull_up(void) +{ + bool failed = false; + print_start("GPIO", "open-drain no-pull-up"); + + /* we cannot test without pull up, but the input pin may have the pull + * up just as well */ + if ((gpio_init(ARDUINO_PIN_3, GPIO_OD) == 0) + && (gpio_init(ARDUINO_PIN_4, GPIO_IN_PU))) { + gpio_set(ARDUINO_PIN_3); + /* give pull resistor a little time to pull */ + brief_delay(); + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + failed |= TEST(gpio_read(ARDUINO_PIN_4) != 0); + } + gpio_clear(ARDUINO_PIN_3); + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + failed |= TEST(gpio_read(ARDUINO_PIN_4) == 0); + } + print_result(failed); + } + else { + print_skipped(); + } + + return failed; +} + +static bool periph_gpio_test(void) +{ + bool failed = false; + + failed |= periph_gpio_test_push_pull(); + failed |= periph_gpio_test_input_pull_up(); + failed |= periph_gpio_test_input_pull_down(); + failed |= periph_gpio_test_open_drain_pull_up(); + failed |= periph_gpio_test_open_drain_no_pull_up(); + + return failed; +} + +static void gpio_cb(void *arg) +{ + atomic_uint *cnt = arg; + atomic_fetch_add(cnt, 1); +} + +static bool periph_gpio_irq_test_falling(void) +{ + bool failed = false; + print_start("GPIO-IRQ", "falling-edge"); + atomic_uint cnt = 0; + ASSERT(gpio_init(ARDUINO_PIN_4, GPIO_OUT) == 0); + gpio_clear(ARDUINO_PIN_4); + ASSERT(gpio_init_int(ARDUINO_PIN_3, GPIO_IN, GPIO_FALLING, gpio_cb, &cnt) == 0); + + /* no stray IRQ */ + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 0); + + /* no IRQ on false edge */ + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 0); + + /* one IRQ on matching edge */ + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 1); + + /* still no IRQ on false edge */ + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 1); + + /* another IRQ on matching edge */ + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 2); + + /* no IRQs while disabled */ + gpio_irq_disable(ARDUINO_PIN_3); + gpio_set(ARDUINO_PIN_4); + brief_delay(); + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + gpio_set(ARDUINO_PIN_4); + brief_delay(); + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 2); + + /* no stray IRQs when re-enabled */ + gpio_irq_enable(ARDUINO_PIN_3); + failed |= TEST(atomic_load(&cnt) == 2); + + /* still no IRQ on false edge */ + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 2); + + /* another IRQ on matching edge */ + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 3); + + print_result(failed); + return failed; +} + +static bool periph_gpio_irq_test_rising(void) +{ + bool failed = false; + print_start("GPIO-IRQ", "rising-edge"); + atomic_uint cnt = 0; + ASSERT(gpio_init(ARDUINO_PIN_4, GPIO_OUT) == 0); + gpio_set(ARDUINO_PIN_4); + ASSERT(gpio_init_int(ARDUINO_PIN_3, GPIO_IN, GPIO_RISING, gpio_cb, &cnt) == 0); + + /* no stray IRQ */ + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 0); + + /* no IRQ on false edge */ + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 0); + + /* one IRQ on matching edge */ + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 1); + + /* still no IRQ on false edge */ + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 1); + + /* another IRQ on matching edge */ + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 2); + + /* no IRQs while disabled */ + gpio_irq_disable(ARDUINO_PIN_3); + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + gpio_set(ARDUINO_PIN_4); + brief_delay(); + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 2); + + /* no stray IRQs when re-enabled */ + gpio_irq_enable(ARDUINO_PIN_3); + failed |= TEST(atomic_load(&cnt) == 2); + + /* still no IRQ on false edge */ + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 2); + + /* another IRQ on matching edge */ + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 3); + + print_result(failed); + return failed; +} + +static bool periph_gpio_irq_test_both(void) +{ + bool failed = false; + print_start("GPIO-IRQ", "both-edges"); + atomic_uint cnt = 0; + ASSERT(gpio_init(ARDUINO_PIN_4, GPIO_OUT) == 0); + gpio_set(ARDUINO_PIN_4); + ASSERT(gpio_init_int(ARDUINO_PIN_3, GPIO_IN, GPIO_BOTH, gpio_cb, &cnt) == 0); + + /* no stray IRQ */ + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 0); + + /* IRQ on falling edge */ + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 1); + + /* another IRQ on rising edge */ + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 2); + + /* and another IRQ on falling edge */ + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 3); + + /* and another IRQ on rising edge */ + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 4); + + /* no IRQs while disabled */ + gpio_irq_disable(ARDUINO_PIN_3); + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + gpio_set(ARDUINO_PIN_4); + brief_delay(); + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 4); + + /* no stray IRQs when re-enabled */ + gpio_irq_enable(ARDUINO_PIN_3); + failed |= TEST(atomic_load(&cnt) == 4); + + /* an IRQ on falling edge */ + gpio_clear(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 5); + + /* another IRQ on rising edge */ + gpio_set(ARDUINO_PIN_4); + brief_delay(); + failed |= TEST(atomic_load(&cnt) == 6); + + print_result(failed); + return failed; +} + +static bool periph_gpio_irq_test(void) +{ + bool failed = false; + + failed |= periph_gpio_irq_test_falling(); + failed |= periph_gpio_irq_test_rising(); + failed |= periph_gpio_irq_test_both(); + + return failed; +} + +static bool periph_i2c_test(void) +{ + bool failed = false; + print_start("I2C", "GPIO extender"); + ASSERT(gpio_init(ARDUINO_PIN_8, GPIO_IN) == 0); + ASSERT(gpio_init(ARDUINO_PIN_9, GPIO_OUT) == 0); + ASSERT(pcf857x_gpio_init(&egpios, PCF857X_GPIO_PIN(0, 0), GPIO_OUT) == 0); + ASSERT(pcf857x_gpio_init(&egpios, PCF857X_GPIO_PIN(0, 1), GPIO_IN) == 0); + + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + gpio_set(ARDUINO_PIN_9); + failed |= TEST(pcf857x_gpio_read(&egpios, PCF857X_GPIO_PIN(0, 1)) != 0); + gpio_clear(ARDUINO_PIN_9); + failed |= TEST(pcf857x_gpio_read(&egpios, PCF857X_GPIO_PIN(0, 1)) == 0); + } + + for (unsigned i = 0; i < flaky_test_repetitions; i++) { + pcf857x_gpio_set(&egpios, PCF857X_GPIO_PIN(0, 0)); + failed |= TEST(gpio_read(ARDUINO_PIN_8) != 0); + pcf857x_gpio_clear(&egpios, PCF857X_GPIO_PIN(0, 0)); + failed |= TEST(gpio_read(ARDUINO_PIN_8) == 0); + } + + print_result(failed); + return failed; +} + +static void uart_rx_cb(void *arg, uint8_t data) +{ + (void)arg; + if (serial_buf.pos < sizeof(serial_buf.data)) { + serial_buf.data[serial_buf.pos] = data; + } + serial_buf.pos++; +} + +static bool periph_uart_rxtx_test(uint32_t symbolrate) +{ + bool failed = 0; + uint16_t duration_ticks = 0; + uint16_t bit_ticks = 0; + uint16_t start; + memset(&serial_buf, 0, sizeof(serial_buf)); + + ASSERT(uart_init(UART_TEST_DEV, symbolrate, uart_rx_cb, NULL) == 0); + + if (IS_USED(MODULE_PERIPH_TIMER)) { + bit_ticks = TIMER_FREQ_UART_TEST / symbolrate; + duration_ticks = 9ULL * sizeof(testdata) * bit_ticks; + } + + /* test that data send matches data received */ + if (IS_USED(MODULE_PERIPH_TIMER)) { + start = timer_read(TIMER); + } + uart_write(UART_TEST_DEV, (void *)testdata, sizeof(testdata)); + if (IS_USED(MODULE_PERIPH_TIMER)) { + uint16_t stop = timer_read(TIMER); + /* expecting actual duration within 75% to 200% of the expected. */ + failed |= TEST(stop - start > duration_ticks - (duration_ticks >> 2)); + failed |= TEST(stop - start < (duration_ticks << 1)); + if (failed) { + DEBUG("%" PRIu32 " Bd, expected %" PRIu16 " ticks, got %" PRIu16 + " ticks\n", + symbolrate, duration_ticks, (uint16_t)(stop - start)); + } + } + long_delay(); + failed |= TEST(memcmp(testdata, serial_buf.data, sizeof(serial_buf.data)) == 0); + failed |= TEST(serial_buf.pos == sizeof(testdata)); + + /* test that no data is received when UART is off */ + uart_poweroff(UART_TEST_DEV); + uart_write(UART_TEST_DEV, (void *)testdata, sizeof(testdata)); + long_delay(); + failed |= TEST(serial_buf.pos == sizeof(testdata)); + + /* test that data is received again when UART is back on */ + memset(&serial_buf, 0, sizeof(serial_buf)); + uart_poweron(UART_TEST_DEV); + long_delay(); + /* no data expected yet */ + failed |= TEST(serial_buf.pos == 0); + /* now writing with device back online */ + uart_write(UART_TEST_DEV, (void *)testdata, sizeof(testdata)); + long_delay(); + failed |= TEST(memcmp(testdata, serial_buf.data, sizeof(serial_buf.data)) == 0); + failed |= TEST(serial_buf.pos == sizeof(testdata)); + + return failed; +} + +static bool periph_uart_test_slow(void) +{ + bool failed = false; + print_start("UART", "slow"); + failed |= periph_uart_rxtx_test(9600); + print_result(failed); + return failed; +} + +static bool periph_uart_test_fast(void) +{ + bool failed = false; + print_start("UART", "fast"); + failed |= periph_uart_rxtx_test(115200); + print_result(failed); + return failed; +} + +static bool periph_uart_test(void) +{ + bool failed = false; + + if (IS_USED(MODULE_PERIPH_TIMER)) { + ASSERT(timer_init(TIMER, TIMER_FREQ_UART_TEST, NULL, NULL) == 0); + timer_start(TIMER); + } + + failed |= periph_uart_test_slow(); + failed |= periph_uart_test_fast(); + return failed; +} + +static bool periph_spi_rxtx_test(spi_t bus, spi_mode_t mode, spi_clk_t clk, + uint32_t clk_hz, gpio_t clk_check, bool idle_level, + const char *test_in_detail) +{ + (void)test_in_detail; + bool failed = 0; + print_start("SPI", test_in_detail); + uint16_t byte_transfer_ticks = 0; + memset(&serial_buf, 0, sizeof(serial_buf)); + + if (IS_USED(MODULE_PERIPH_TIMER)) { + byte_transfer_ticks = 8ULL * TIMER_FREQ_UART_TEST / clk_hz; + } + + /* D10 is C̅S̅, D7 is connected to C̅S̅ */ + spi_cs_t cs = ARDUINO_PIN_10; + gpio_t cs_check = ARDUINO_PIN_7; + gpio_init(cs_check, GPIO_IN); + spi_init_cs(bus, cs); + + spi_acquire(bus, cs, mode, clk); + + if (IS_USED(MODULE_PCF857X)) { + failed |= TEST(idle_level == (bool)pcf857x_gpio_read(&egpios, clk_check)); + } + + /* C̅S̅ should still be HIGH while no chip is selected */ + failed |= TEST(gpio_read(cs_check) != 0); + + for (uint8_t i = 0; i < UINT8_MAX; i++) { + uint16_t start = 0; + if (IS_USED(MODULE_PERIPH_TIMER)) { + start = timer_read(TIMER); + } + uint8_t received = spi_transfer_byte(bus, cs, true, i); + uint16_t stop = 0; + if (IS_USED(MODULE_PERIPH_TIMER)) { + stop = timer_read(TIMER); + } + failed |= TEST(received == i); + uint16_t byte_time = (uint16_t)(stop - start); + /* We allow the actual SPI clock to be slower than requested, but not + * faster. So the transfer needs to take *at least* the theoretical + * time. Given the overhead of, this already has some room for error */ + failed |= TEST(byte_time >= byte_transfer_ticks); + /* C̅S̅ should be still LOW while chip is selected */ + failed |= TEST(gpio_read(cs_check) == 0); + } + + failed |= TEST(spi_transfer_byte(bus, cs, false, UINT8_MAX) == UINT8_MAX); + /* C̅S̅ should be again HIGH while now that no chip is selected */ + failed |= TEST(gpio_read(cs_check) != 0); + + /* no also test for different sizes */ + for (unsigned i = 1; i <= sizeof(testdata); i++) { + uint8_t target[sizeof(testdata) + 4]; + memset(target, 0x55, sizeof(target)); + /* C̅S̅ should be HIGH before chip is selected */ + failed |= TEST(gpio_read(cs_check) != 0); + spi_transfer_bytes(bus, cs, false, testdata, target, i); + /* C̅S̅ should be HIGH again after transfer */ + failed |= TEST(gpio_read(cs_check) != 0); + + /* first part of data read should be the test data */ + for (unsigned j = 0; j < i; j++) { + failed |= TEST(target[j] == testdata[j]); + } + + /* rest of the target should be the canary value */ + for (unsigned j = i; j < sizeof(testdata); j++) { + failed |= TEST(target[j] == 0x55); + } + } + + if (IS_USED(MODULE_PCF857X)) { + failed |= TEST(idle_level == (bool)pcf857x_gpio_read(&egpios, clk_check)); + } + + spi_release(bus); + print_result(failed); + + return failed; +} + +static bool periph_spi_test(void) +{ + if (IS_USED(MODULE_PERIPH_TIMER)) { + ASSERT(timer_init(TIMER, TIMER_FREQ_SPI_TEST, NULL, NULL) == 0); + timer_start(TIMER); + } + + bool failed = false; + static const spi_clk_t clocks[] = { SPI_CLK_100KHZ, SPI_CLK_1MHZ, SPI_CLK_10MHZ }; + static const uint32_t clk_hzs[] = { KHZ(100), MHZ(1), MHZ(10) }; + + if (IS_USED(MODULE_PCF857X)) { + for (int i = 0; i < (int)ARRAY_SIZE(spi_clk_check_pins); i++) { + ASSERT(pcf857x_gpio_init(&egpios, spi_clk_check_pins[i], GPIO_IN) == 0); + } + } + + /* using a signed comparison here to also compile when no SPI buses are + * available for testing */ + for (int i = 0; i < (int)ARRAY_SIZE(spi_buses); i++) { + spi_t bus = spi_buses[i]; + gpio_t clk_check = spi_clk_check_pins[i]; + for (unsigned j = 0; j < ARRAY_SIZE(clocks); j++) { + spi_clk_t clk = clocks[j]; + uint32_t clk_hz = clk_hzs[j]; + failed |= periph_spi_rxtx_test(bus, SPI_MODE_0, clk, clk_hz, clk_check, false, "mode 0"); + failed |= periph_spi_rxtx_test(bus, SPI_MODE_1, clk, clk_hz, clk_check, false, "mode 1"); + failed |= periph_spi_rxtx_test(bus, SPI_MODE_2, clk, clk_hz, clk_check, true, "mode 2"); + failed |= periph_spi_rxtx_test(bus, SPI_MODE_3, clk, clk_hz, clk_check, true, "mode 3"); + } + } + return failed; +} + +static bool periph_pwm_test_chan(pwm_t pwm_dev, uint8_t pwm_chan, pwm_mode_t pwm_mode, adc_t adc_line) +{ + bool failed = false; + print_start("PWM", "duty cycle (via ADC)"); + + failed |= TEST(pwm_init(pwm_dev, pwm_mode, CLOCK_CORECLOCK, 256) > 0); + if (failed) { + /* pwm mode not supported, most likely */ + return failed; + } + + ASSERT(adc_init(adc_line) == 0); + + for (uint16_t i = 0; i <= UINT8_MAX; i++) { + pwm_set(pwm_dev, pwm_chan, i); + /* give voltage at ADC some time to settle */ + brief_delay(); + uint16_t sample = adc_sample(adc_line, ADC_RES_10BIT); + uint16_t expected = i << 2; + + /* let's allow for quite some error here */ + const uint16_t delta = 64; + uint16_t lower = expected <= delta ? 0 : expected - delta; + uint16_t upper = MIN(1023, expected + delta); + bool test_failed = TEST((lower <= sample) && (upper >= sample)); + if (test_failed) { + printf("%" PRIu16 " <= %" PRIu16 " <= %" PRIu16": FAILED\n", + lower, sample, upper); + } + failed |= test_failed; + } + + print_result(failed); + return failed; +} + +static bool periph_pwm_test(void) +{ + bool failed = false; + static const pwm_mode_t modes[] = { PWM_LEFT, PWM_RIGHT, PWM_CENTER }; + + for (unsigned i = 0; i < ARRAY_SIZE(modes); i++) { + if (ENABLE_PWM_TEST_D5) { + failed |= periph_pwm_test_chan(ARDUINO_PIN_5_PWM_DEV, ARDUINO_PIN_5_PWM_CHAN, modes[i], ARDUINO_A2); + } + if (ENABLE_PWM_TEST_D6) { + failed |= periph_pwm_test_chan(ARDUINO_PIN_6_PWM_DEV, ARDUINO_PIN_6_PWM_CHAN, modes[i], ARDUINO_A1); + } + } + + return failed; +} + +static void r_2r_dac_init(void) +{ + ASSERT(pcf857x_gpio_init(&egpios, PCF857X_GPIO_PIN(0, 4), GPIO_OUT) == 0); + ASSERT(pcf857x_gpio_init(&egpios, PCF857X_GPIO_PIN(0, 5), GPIO_OUT) == 0); + ASSERT(pcf857x_gpio_init(&egpios, PCF857X_GPIO_PIN(0, 6), GPIO_OUT) == 0); + ASSERT(pcf857x_gpio_init(&egpios, PCF857X_GPIO_PIN(0, 7), GPIO_OUT) == 0); +} + +static void r_2r_dac_write(uint8_t val) +{ + pcf857x_gpio_write(&egpios, PCF857X_GPIO_PIN(0, 4), val & (1U << 3)); + pcf857x_gpio_write(&egpios, PCF857X_GPIO_PIN(0, 5), val & (1U << 2)); + pcf857x_gpio_write(&egpios, PCF857X_GPIO_PIN(0, 6), val & (1U << 1)); + pcf857x_gpio_write(&egpios, PCF857X_GPIO_PIN(0, 7), val & (1U << 0)); +} + +static bool periph_adc_test(void) +{ + bool failed = false; + print_start("ADC", "sample R2R DAC output"); + + adc_init(ARDUINO_A0); + r_2r_dac_init(); + + for (uint8_t i = 0; i < 16; i++) { + r_2r_dac_write(i); + uint16_t sample = adc_sample(ARDUINO_A0, ADC_RES_10BIT); + uint16_t expected = i << 6; + + /* the resistors are said to be rather accurate, so lets be a bit + * more strict here */ + const uint16_t delta = 16; + uint16_t lower = expected <= delta ? 0 : expected - delta; + uint16_t upper = MIN(1023, expected + delta); + bool test_failed = TEST((lower <= sample) && (upper >= sample)); + if (test_failed) { + printf("%" PRIu16 " <= %" PRIu16 " <= %" PRIu16": FAILED\n", + lower, sample, upper); + } + failed |= test_failed; + } + + print_result(failed); + return failed; +} + +int main(void) +{ + bool failed = false; + + printf("self-testing peripheral drivers\n" + "===============================\n"); + + /* the GPIO extender is used by the I2C test and the ADC test, so only + * initialize it once here */ + if (IS_USED(MODULE_PCF857X)) { + ASSERT(pcf857x_init(&egpios, ¶ms) == PCF857X_OK); + } + + if (IS_USED(MODULE_PERIPH_GPIO)) { + failed |= periph_gpio_test(); + } + + if (IS_USED(MODULE_PERIPH_GPIO_IRQ)) { + failed |= periph_gpio_irq_test(); + } + + if (IS_USED(MODULE_PCF857X) && IS_USED(MODULE_PERIPH_GPIO)) { + failed |= periph_i2c_test(); + } + + if (ENABLE_UART_TEST) { + failed |= periph_uart_test(); + } + + if (IS_USED(MODULE_PERIPH_SPI)) { + failed |= periph_spi_test(); + } + + if (ENABLE_PWM_TEST) { + failed |= periph_pwm_test(); + } + + if (ENABLE_ADC_TEST) { + failed |= periph_adc_test(); + } + + if (failed) { + printf("\n\nSOME TESTS FAILED\n"); + return 1; + } + + printf("\n\nALL TESTS SUCCEEDED\n"); + return 0; +} diff --git a/tests/periph/selftest_shield/tests-with-config/01-run.py b/tests/periph/selftest_shield/tests-with-config/01-run.py new file mode 100755 index 0000000000..606d413ed1 --- /dev/null +++ b/tests/periph/selftest_shield/tests-with-config/01-run.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2022 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('ALL TESTS SUCCEEDED') + + +if __name__ == "__main__": + sys.exit(run(testfunc)) diff --git a/tests/periph/selftest_shield/tests/01-run.py b/tests/periph/selftest_shield/tests/01-run.py new file mode 100755 index 0000000000..005393cb48 --- /dev/null +++ b/tests/periph/selftest_shield/tests/01-run.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2023 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. + +# @author Marian Buschsieweke + +import sys +from testrunner import run + + +def testfunc(child): + child.expect("self-testing peripheral drivers") + child.expect("ALL TESTS SUCCEEDED") + + +if __name__ == "__main__": + sys.exit(run(testfunc))