From 7974ff2665196c564215587e343dfc224d83ca19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20G=C5=82=C4=85bek?= Date: Mon, 31 Jul 2023 14:16:17 +0200 Subject: [PATCH] drivers: spi_nrfx_*: Add support for optional WAKE line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add option to use (by defining the `wake-gpios` devicetree properties) an additional signal line between SPI master and SPI slave that allows the latter to stay in low-power state and wake up only when a transfer is to occur. Signed-off-by: Andrzej Głąbek --- drivers/spi/CMakeLists.txt | 6 +- drivers/spi/Kconfig.nrfx | 9 ++ drivers/spi/spi_nrfx_common.c | 75 +++++++++++++++ drivers/spi/spi_nrfx_common.h | 17 ++++ drivers/spi/spi_nrfx_spi.c | 31 ++++++ drivers/spi/spi_nrfx_spim.c | 31 ++++++ drivers/spi/spi_nrfx_spis.c | 100 ++++++++++++++++++++ dts/bindings/spi/nordic,nrf-spi-common.yaml | 24 +++++ 8 files changed, 291 insertions(+), 2 deletions(-) create mode 100644 drivers/spi/spi_nrfx_common.c create mode 100644 drivers/spi/spi_nrfx_common.h diff --git a/drivers/spi/CMakeLists.txt b/drivers/spi/CMakeLists.txt index a60bce17df7..2eb499366ea 100644 --- a/drivers/spi/CMakeLists.txt +++ b/drivers/spi/CMakeLists.txt @@ -16,8 +16,10 @@ zephyr_library_sources_ifdef(CONFIG_SPI_SAM spi_sam.c) zephyr_library_sources_ifdef(CONFIG_SPI_SAM0 spi_sam0.c) zephyr_library_sources_ifdef(CONFIG_SPI_SIFIVE spi_sifive.c) zephyr_library_sources_ifdef(CONFIG_SPI_RV32M1_LPSPI spi_rv32m1_lpspi.c) -zephyr_library_sources_ifdef(CONFIG_SPI_NRFX_SPI spi_nrfx_spi.c) -zephyr_library_sources_ifdef(CONFIG_SPI_NRFX_SPIM spi_nrfx_spim.c) +zephyr_library_sources_ifdef(CONFIG_SPI_NRFX_SPI spi_nrfx_spi.c + spi_nrfx_common.c) +zephyr_library_sources_ifdef(CONFIG_SPI_NRFX_SPIM spi_nrfx_spim.c + spi_nrfx_common.c) zephyr_library_sources_ifdef(CONFIG_SPI_NRFX_SPIS spi_nrfx_spis.c) zephyr_library_sources_ifdef(CONFIG_SPI_LITESPI spi_litespi.c) zephyr_library_sources_ifdef(CONFIG_SPI_OC_SIMPLE spi_oc_simple.c) diff --git a/drivers/spi/Kconfig.nrfx b/drivers/spi/Kconfig.nrfx index bc287e18aca..d2030a1a2ea 100644 --- a/drivers/spi/Kconfig.nrfx +++ b/drivers/spi/Kconfig.nrfx @@ -66,4 +66,13 @@ config SPI_NRFX_RAM_BUFFER_SIZE supplying buffers located in flash to the driver, otherwise such transfers will fail. +config SPI_NRFX_WAKE_TIMEOUT_US + int "Maximum time to wait for SPI slave to wake up" + default 200 + help + Maximum amount of time (in microseconds) that SPI master should wait + for SPI slave to wake up after the WAKE line is asserted. Used only + by instances that have the WAKE line configured (see the wake-gpios + devicetree property). + endif # SPI_NRFX diff --git a/drivers/spi/spi_nrfx_common.c b/drivers/spi/spi_nrfx_common.c new file mode 100644 index 00000000000..1ef233cfab3 --- /dev/null +++ b/drivers/spi/spi_nrfx_common.c @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2023, Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "spi_nrfx_common.h" +#include +#include + +int spi_nrfx_wake_init(uint32_t wake_pin) +{ + nrfx_gpiote_input_config_t input_config = { + .pull = NRF_GPIO_PIN_PULLDOWN, + }; + uint8_t ch; + nrfx_gpiote_trigger_config_t trigger_config = { + .trigger = NRFX_GPIOTE_TRIGGER_HITOLO, + .p_in_channel = &ch, + }; + nrfx_err_t res; + + res = nrfx_gpiote_channel_alloc(&ch); + if (res != NRFX_SUCCESS) { + return -ENODEV; + } + + res = nrfx_gpiote_input_configure(wake_pin, + &input_config, + &trigger_config, + NULL); + if (res != NRFX_SUCCESS) { + nrfx_gpiote_channel_free(ch); + return -EIO; + } + + return 0; +} + +int spi_nrfx_wake_request(uint32_t wake_pin) +{ + nrf_gpiote_event_t trigger_event = nrfx_gpiote_in_event_get(wake_pin); + uint32_t start_cycles; + uint32_t max_wait_cycles = + DIV_ROUND_UP(CONFIG_SPI_NRFX_WAKE_TIMEOUT_US * + CONFIG_SYS_CLOCK_HW_CYCLES_PER_SEC, + 1000000); + int err = 0; + + /* Enable the trigger (a high-to-low transition) without its interrupt. + * The expected time to wait is quite short so it is not worth paying + * the overhead of context switching to handle the interrupt. + */ + nrfx_gpiote_trigger_enable(wake_pin, false); + /* Enable pull-up on the WAKE line. After the slave device sees the + * WAKE line going high, it will force the line to go low. This will + * be caught by the enabled trigger and the loop below waits for that. + */ + nrf_gpio_cfg_input(wake_pin, NRF_GPIO_PIN_PULLUP); + + start_cycles = k_cycle_get_32(); + while (!nrf_gpiote_event_check(NRF_GPIOTE, trigger_event)) { + uint32_t elapsed_cycles = k_cycle_get_32() - start_cycles; + + if (elapsed_cycles >= max_wait_cycles) { + err = -ETIMEDOUT; + break; + } + } + + nrfx_gpiote_trigger_disable(wake_pin); + nrf_gpio_cfg_input(wake_pin, NRF_GPIO_PIN_PULLDOWN); + + return err; +} diff --git a/drivers/spi/spi_nrfx_common.h b/drivers/spi/spi_nrfx_common.h new file mode 100644 index 00000000000..515ed5c6f1f --- /dev/null +++ b/drivers/spi/spi_nrfx_common.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2023, Nordic Semiconductor ASA + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_SPI_NRFX_COMMON_H_ +#define ZEPHYR_DRIVERS_SPI_NRFX_COMMON_H_ + +#include + +#define WAKE_PIN_NOT_USED UINT32_MAX + +int spi_nrfx_wake_init(uint32_t wake_pin); +int spi_nrfx_wake_request(uint32_t wake_pin); + +#endif /* ZEPHYR_DRIVERS_SPI_NRFX_COMMON_H_ */ diff --git a/drivers/spi/spi_nrfx_spi.c b/drivers/spi/spi_nrfx_spi.c index df07bf8ef63..fc4b66e176e 100644 --- a/drivers/spi/spi_nrfx_spi.c +++ b/drivers/spi/spi_nrfx_spi.c @@ -15,6 +15,7 @@ LOG_MODULE_REGISTER(spi_nrfx_spi, CONFIG_SPI_LOG_LEVEL); #include "spi_context.h" +#include "spi_nrfx_common.h" struct spi_nrfx_data { struct spi_context ctx; @@ -29,6 +30,7 @@ struct spi_nrfx_config { nrfx_spi_config_t def_config; void (*irq_connect)(void); const struct pinctrl_dev_config *pcfg; + uint32_t wake_pin; }; static void event_handler(const nrfx_spi_evt_t *p_event, void *p_context); @@ -231,6 +233,18 @@ static int transceive(const struct device *dev, if (error == 0) { dev_data->busy = true; + if (dev_config->wake_pin != WAKE_PIN_NOT_USED) { + error = spi_nrfx_wake_request(dev_config->wake_pin); + if (error == -ETIMEDOUT) { + LOG_WRN("Waiting for WAKE acknowledgment timed out"); + /* If timeout occurs, try to perform the transfer + * anyway, just in case the slave device was unable + * to signal that it was already awaken and prepared + * for the transfer. + */ + } + } + spi_context_buffers_setup(&dev_data->ctx, tx_bufs, rx_bufs, 1); spi_context_cs_control(&dev_data->ctx, true); @@ -363,6 +377,18 @@ static int spi_nrfx_init(const struct device *dev) return err; } + if (dev_config->wake_pin != WAKE_PIN_NOT_USED) { + err = spi_nrfx_wake_init(dev_config->wake_pin); + if (err == -ENODEV) { + LOG_ERR("Failed to allocate GPIOTE channel for WAKE"); + return err; + } + if (err == -EIO) { + LOG_ERR("Failed to configure WAKE pin"); + return err; + } + } + dev_config->irq_connect(); err = spi_context_cs_configure_all(&dev_data->ctx); @@ -413,7 +439,12 @@ static int spi_nrfx_init(const struct device *dev) }, \ .irq_connect = irq_connect##idx, \ .pcfg = PINCTRL_DT_DEV_CONFIG_GET(SPI(idx)), \ + .wake_pin = NRF_DT_GPIOS_TO_PSEL_OR(SPI(idx), wake_gpios, \ + WAKE_PIN_NOT_USED), \ }; \ + BUILD_ASSERT(!DT_NODE_HAS_PROP(SPI(idx), wake_gpios) || \ + !(DT_GPIO_FLAGS(SPI(idx), wake_gpios) & GPIO_ACTIVE_LOW), \ + "WAKE line must be configured as active high"); \ PM_DEVICE_DT_DEFINE(SPI(idx), spi_nrfx_pm_action); \ DEVICE_DT_DEFINE(SPI(idx), \ spi_nrfx_init, \ diff --git a/drivers/spi/spi_nrfx_spim.c b/drivers/spi/spi_nrfx_spim.c index d160a15e28b..55ff51cc7b3 100644 --- a/drivers/spi/spi_nrfx_spim.c +++ b/drivers/spi/spi_nrfx_spim.c @@ -22,6 +22,7 @@ LOG_MODULE_REGISTER(spi_nrfx_spim, CONFIG_SPI_LOG_LEVEL); #include "spi_context.h" +#include "spi_nrfx_common.h" #if defined(CONFIG_SOC_NRF52832) && !defined(CONFIG_SOC_NRF52832_ALLOW_SPIM_DESPITE_PAN_58) #error This driver is not available by default for nRF52832 because of Product Anomaly 58 \ @@ -59,6 +60,7 @@ struct spi_nrfx_config { #ifdef CONFIG_SOC_NRF52832_ALLOW_SPIM_DESPITE_PAN_58 bool anomaly_58_workaround; #endif + uint32_t wake_pin; }; static void event_handler(const nrfx_spim_evt_t *p_event, void *p_context); @@ -393,6 +395,18 @@ static int transceive(const struct device *dev, if (error == 0) { dev_data->busy = true; + if (dev_config->wake_pin != WAKE_PIN_NOT_USED) { + error = spi_nrfx_wake_request(dev_config->wake_pin); + if (error == -ETIMEDOUT) { + LOG_WRN("Waiting for WAKE acknowledgment timed out"); + /* If timeout occurs, try to perform the transfer + * anyway, just in case the slave device was unable + * to signal that it was already awaken and prepared + * for the transfer. + */ + } + } + spi_context_buffers_setup(&dev_data->ctx, tx_bufs, rx_bufs, 1); spi_context_cs_control(&dev_data->ctx, true); @@ -529,6 +543,18 @@ static int spi_nrfx_init(const struct device *dev) return err; } + if (dev_config->wake_pin != WAKE_PIN_NOT_USED) { + err = spi_nrfx_wake_init(dev_config->wake_pin); + if (err == -ENODEV) { + LOG_ERR("Failed to allocate GPIOTE channel for WAKE"); + return err; + } + if (err == -EIO) { + LOG_ERR("Failed to configure WAKE pin"); + return err; + } + } + dev_config->irq_connect(); err = spi_context_cs_configure_all(&dev_data->ctx); @@ -603,7 +629,12 @@ static int spi_nrfx_init(const struct device *dev) (.anomaly_58_workaround = \ SPIM_PROP(idx, anomaly_58_workaround),), \ ()) \ + .wake_pin = NRF_DT_GPIOS_TO_PSEL_OR(SPIM(idx), wake_gpios, \ + WAKE_PIN_NOT_USED), \ }; \ + BUILD_ASSERT(!DT_NODE_HAS_PROP(SPIM(idx), wake_gpios) || \ + !(DT_GPIO_FLAGS(SPIM(idx), wake_gpios) & GPIO_ACTIVE_LOW),\ + "WAKE line must be configured as active high"); \ PM_DEVICE_DT_DEFINE(SPIM(idx), spim_nrfx_pm_action); \ DEVICE_DT_DEFINE(SPIM(idx), \ spi_nrfx_init, \ diff --git a/drivers/spi/spi_nrfx_spis.c b/drivers/spi/spi_nrfx_spis.c index d2448af7a67..d6c3eb9ce0e 100644 --- a/drivers/spi/spi_nrfx_spis.c +++ b/drivers/spi/spi_nrfx_spis.c @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -18,6 +19,8 @@ LOG_MODULE_REGISTER(spi_nrfx_spis, CONFIG_SPI_LOG_LEVEL); struct spi_nrfx_data { struct spi_context ctx; const struct device *dev; + struct k_sem wake_sem; + struct gpio_callback wake_cb_data; }; struct spi_nrfx_config { @@ -26,6 +29,7 @@ struct spi_nrfx_config { void (*irq_connect)(void); uint16_t max_buf_len; const struct pinctrl_dev_config *pcfg; + struct gpio_dt_spec wake_gpio; }; static inline nrf_spis_mode_t get_nrf_spis_mode(uint16_t operation) @@ -130,6 +134,32 @@ static int prepare_for_transfer(const struct device *dev, return 0; } +static void wake_callback(const struct device *dev, struct gpio_callback *cb, + uint32_t pins) +{ + struct spi_nrfx_data *dev_data = + CONTAINER_OF(cb, struct spi_nrfx_data, wake_cb_data); + const struct spi_nrfx_config *dev_config = dev_data->dev->config; + + (void)gpio_pin_interrupt_configure_dt(&dev_config->wake_gpio, + GPIO_INT_DISABLE); + k_sem_give(&dev_data->wake_sem); +} + +static void wait_for_wake(struct spi_nrfx_data *dev_data, + const struct spi_nrfx_config *dev_config) +{ + /* If the WAKE line is low, wait until it goes high - this is a signal + * from the master that it wants to perform a transfer. + */ + if (gpio_pin_get_raw(dev_config->wake_gpio.port, + dev_config->wake_gpio.pin) == 0) { + (void)gpio_pin_interrupt_configure_dt(&dev_config->wake_gpio, + GPIO_INT_LEVEL_HIGH); + (void)k_sem_take(&dev_data->wake_sem, K_FOREVER); + } +} + static int transceive(const struct device *dev, const struct spi_config *spi_cfg, const struct spi_buf_set *tx_bufs, @@ -139,6 +169,7 @@ static int transceive(const struct device *dev, void *userdata) { struct spi_nrfx_data *dev_data = dev->data; + const struct spi_nrfx_config *dev_config = dev->config; int error; spi_context_lock(&dev_data->ctx, asynchronous, cb, userdata, spi_cfg); @@ -155,14 +186,41 @@ static int transceive(const struct device *dev, LOG_ERR("Only buffers located in RAM are supported"); error = -ENOTSUP; } else { + if (dev_config->wake_gpio.port) { + wait_for_wake(dev_data, dev_config); + + nrf_spis_enable(dev_config->spis.p_reg); + } + error = prepare_for_transfer(dev, tx_bufs ? tx_bufs->buffers[0].buf : NULL, tx_bufs ? tx_bufs->buffers[0].len : 0, rx_bufs ? rx_bufs->buffers[0].buf : NULL, rx_bufs ? rx_bufs->buffers[0].len : 0); if (error == 0) { + if (dev_config->wake_gpio.port) { + /* Set the WAKE line low (tie it to ground) + * to signal readiness to handle the transfer. + */ + gpio_pin_set_raw(dev_config->wake_gpio.port, + dev_config->wake_gpio.pin, + 0); + /* Set the WAKE line back high (i.e. disconnect + * output for its pin since it's configured in + * open drain mode) so that it can be controlled + * by the other side again. + */ + gpio_pin_set_raw(dev_config->wake_gpio.port, + dev_config->wake_gpio.pin, + 1); + } + error = spi_context_wait_for_completion(&dev_data->ctx); } + + if (dev_config->wake_gpio.port) { + nrf_spis_disable(dev_config->spis.p_reg); + } } spi_context_release(&dev_data->ctx, error); @@ -245,6 +303,42 @@ static int spi_nrfx_init(const struct device *dev) return -EBUSY; } + if (dev_config->wake_gpio.port) { + if (!device_is_ready(dev_config->wake_gpio.port)) { + return -ENODEV; + } + + /* In open drain mode, the output is disconnected when set to + * the high state, so the following will effectively configure + * the pin as an input only. + */ + err = gpio_pin_configure_dt(&dev_config->wake_gpio, + GPIO_INPUT | + GPIO_OUTPUT_HIGH | + GPIO_OPEN_DRAIN); + if (err < 0) { + return err; + } + + gpio_init_callback(&dev_data->wake_cb_data, wake_callback, + BIT(dev_config->wake_gpio.pin)); + err = gpio_add_callback(dev_config->wake_gpio.port, + &dev_data->wake_cb_data); + if (err < 0) { + return err; + } + + /* When the WAKE line is used, the SPIS peripheral is enabled + * only after the master signals that it wants to perform a + * transfer and it is disabled right after the transfer is done. + * Waiting for the WAKE line to go high, what can be done using + * the GPIO PORT event, instead of just waiting for the transfer + * with the SPIS peripheral enabled, significantly reduces idle + * power consumption. + */ + nrf_spis_disable(dev_config->spis.p_reg); + } + spi_context_unlock_unconditionally(&dev_data->ctx); return 0; @@ -270,6 +364,8 @@ static int spi_nrfx_init(const struct device *dev) SPI_CONTEXT_INIT_LOCK(spi_##idx##_data, ctx), \ SPI_CONTEXT_INIT_SYNC(spi_##idx##_data, ctx), \ .dev = DEVICE_DT_GET(SPIS(idx)), \ + .wake_sem = Z_SEM_INITIALIZER( \ + spi_##idx##_data.wake_sem, 0, 1), \ }; \ PINCTRL_DT_DEFINE(SPIS(idx)); \ static const struct spi_nrfx_config spi_##idx##z_config = { \ @@ -288,7 +384,11 @@ static int spi_nrfx_init(const struct device *dev) .irq_connect = irq_connect##idx, \ .pcfg = PINCTRL_DT_DEV_CONFIG_GET(SPIS(idx)), \ .max_buf_len = BIT_MASK(SPIS_PROP(idx, easydma_maxcnt_bits)), \ + .wake_gpio = GPIO_DT_SPEC_GET_OR(SPIS(idx), wake_gpios, {0}), \ }; \ + BUILD_ASSERT(!DT_NODE_HAS_PROP(SPIS(idx), wake_gpios) || \ + !(DT_GPIO_FLAGS(SPIS(idx), wake_gpios) & GPIO_ACTIVE_LOW),\ + "WAKE line must be configured as active high"); \ DEVICE_DT_DEFINE(SPIS(idx), \ spi_nrfx_init, \ NULL, \ diff --git a/dts/bindings/spi/nordic,nrf-spi-common.yaml b/dts/bindings/spi/nordic,nrf-spi-common.yaml index d9c0d2f9ae3..ed504a5902b 100644 --- a/dts/bindings/spi/nordic,nrf-spi-common.yaml +++ b/dts/bindings/spi/nordic,nrf-spi-common.yaml @@ -36,3 +36,27 @@ properties: description: | Maximum number of bits available in the EasyDMA MAXCNT register. This property must be set at SoC level DTS files. + + wake-gpios: + type: phandle-array + description: | + Optional bi-directional line that allows SPI master to indicate to SPI + slave (by setting the line high) that a transfer is to occur, so that + the latter can prepare (and indicate its readiness) for handling that + transfer when it is actually needed, and stay in any desired low-power + state otherwise. + The protocol is as follows: + - initially, SPI slave configures its WAKE line pin as an input and SPI + master keeps the line in the low state + - when a transfer is to be performed, SPI master configures its WAKE + line pin as an input with pull-up; this changes the line state to + high but allows SPI slave to override that state + - when SPI slave detects the high state of the WAKE line, it prepares + for the transfer and when everything is ready, it drives the WAKE + line low by configuring its pin as an output + - the generated high-to-low transition on the WAKE line is a signal + to SPI master that it can proceed with the transfer + - SPI slave releases the line by configuring its pin back to be an input + and SPI master again keeps the line in the low state + Please note that the line must be configured and properly handled on + both sides for the mechanism to work correctly.