diff --git a/tests/periph/selftest_shield/Makefile b/tests/periph/selftest_shield/Makefile new file mode 100644 index 0000000000000..d3166a9b86f74 --- /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 0000000000000..7d1378f0798a3 --- /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 0000000000000..8c1706ceff863 --- /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 0000000000000..5c2678ab5e98d --- /dev/null +++ b/tests/periph/selftest_shield/main.c @@ -0,0 +1,1052 @@ +/* + * 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 "architecture.h" +#include "arduino_iomap.h" +#include "irq.h" +#include "macros/units.h" +#include "macros/utils.h" +#include "modules.h" +#include "mutex.h" +#include "pcf857x.h" +#include "periph/adc.h" +#include "periph/gpio.h" +#include "periph/i2c.h" +#include "periph/pwm.h" +#include "periph/spi.h" +#include "periph/timer.h" +#include "periph/uart.h" +#include "ringbuffer.h" +#include "stdio_uart.h" /* for STDIO_UART_DEV */ +#include "time_units.h" + +/* 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, 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 0000000000000..606d413ed19dc --- /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 0000000000000..005393cb48a04 --- /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))