drivers: spi_nrfx_*: Add support for optional WAKE line

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 <andrzej.glabek@nordicsemi.no>
This commit is contained in:
Andrzej Głąbek 2023-07-31 14:16:17 +02:00 committed by Carles Cufí
parent f132f55e32
commit 7974ff2665
8 changed files with 291 additions and 2 deletions

View file

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

View file

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

View file

@ -0,0 +1,75 @@
/*
* Copyright (c) 2023, Nordic Semiconductor ASA
*
* SPDX-License-Identifier: Apache-2.0
*/
#include "spi_nrfx_common.h"
#include <zephyr/kernel.h>
#include <nrfx_gpiote.h>
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;
}

View file

@ -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 <stdint.h>
#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_ */

View file

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

View file

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

View file

@ -6,6 +6,7 @@
#include <zephyr/drivers/spi.h>
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/drivers/gpio.h>
#include <soc.h>
#include <nrfx_spis.h>
@ -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, \

View file

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