diff --git a/cpu/nrf52/Makefile.dep b/cpu/nrf52/Makefile.dep index ebf5dff9fe..2787f51a1b 100644 --- a/cpu/nrf52/Makefile.dep +++ b/cpu/nrf52/Makefile.dep @@ -5,5 +5,12 @@ ifneq (,$(filter nrf802154,$(USEMODULE))) USEMODULE += netdev_ieee802154 endif +# The nrf52832 requires gpio IRQ with SPI to work around errata 58 +ifneq (,$(filter nrf52832xxaa,$(CPU_MODEL))) + ifneq (,$(filter periph_spi,$(USEMODULE))) + FEATURES_REQUIRED += periph_gpio_irq + endif +endif + include $(RIOTCPU)/nrf5x_common/Makefile.dep include $(RIOTCPU)/cortexm_common/Makefile.dep diff --git a/cpu/nrf52/include/periph_cpu.h b/cpu/nrf52/include/periph_cpu.h index f1e136476d..5a5742324a 100644 --- a/cpu/nrf52/include/periph_cpu.h +++ b/cpu/nrf52/include/periph_cpu.h @@ -27,6 +27,14 @@ extern "C" { #endif +/** + * @brief Enable the workaround for the SPI single byte transmit errata (No. + * 58 on the nrf52832) + */ +#ifdef CPU_MODEL_NRF52832XXAA +#define ERRATA_SPI_SINGLE_BYTE_WORKAROUND (1) +#endif + /** * @brief System core clock speed, fixed to 64MHz for all NRF52x CPUs */ @@ -54,6 +62,14 @@ extern "C" { */ #define ADC_NUMOF (9U) +/** + * @brief SPI temporary buffer size for storing const data in RAM before + * initiating DMA transfer + */ +#ifndef CONFIG_SPI_MBUF_SIZE +#define CONFIG_SPI_MBUF_SIZE 64 +#endif + /** * @brief nRF52 specific naming of ADC lines (for convenience) */ @@ -180,6 +196,20 @@ typedef struct { } uart_conf_t; #endif +/** + * @brief SPI configuration values + */ +typedef struct { + NRF_SPIM_Type *dev; /**< SPI device used */ + gpio_t sclk; /**< CLK pin */ + gpio_t mosi; /**< MOSI pin */ + gpio_t miso; /**< MISO pin */ +#if ERRATA_SPI_SINGLE_BYTE_WORKAROUND + uint8_t ppi; /**< PPI channel */ +#endif +} spi_conf_t; + + /** * @brief Common SPI/I2C interrupt callback * diff --git a/cpu/nrf52/periph/spi.c b/cpu/nrf52/periph/spi.c new file mode 100644 index 0000000000..9376409b37 --- /dev/null +++ b/cpu/nrf52/periph/spi.c @@ -0,0 +1,258 @@ +/* + * Copyright (C) 2014-2016 Freie Universität Berlin + * Copyright (C) 2020 Inria + * Copyright (C) 2020 Koen Zandberg + * + * 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 cpu_nrf52 + * @ingroup drivers_periph_spi + * @{ + * + * @file + * @brief Low-level SPI driver implementation based on the SPIM peripheral + * + * @author Hauke Petersen + * @author Frank Holtz + * @author Jan Wagner + * @author Koen Zandberg + * + * @} + */ + +#include "cpu.h" +#include "mutex.h" +#include "assert.h" +#include "periph/spi.h" +#include "periph/gpio.h" +#include "periph_cpu.h" +#include + +#define RAM_MASK (0x20000000) + +/** + * @brief array holding one pre-initialized mutex for each SPI device + */ +static mutex_t locks[SPI_NUMOF]; + +/** + * @brief array with a busy mutex for each SPI device, used to block the + * thread until the transfer is done + */ +static mutex_t busy[SPI_NUMOF]; + +static uint8_t _mbuf[SPI_NUMOF][CONFIG_SPI_MBUF_SIZE]; + +static void spi_isr_handler(void *arg); + +static inline NRF_SPIM_Type *dev(spi_t bus) +{ + return (NRF_SPIM_Type *)spi_config[bus].dev; +} + +static inline bool _in_ram(const uint8_t *data) +{ + return ((uint32_t)data & RAM_MASK); +} + +#ifdef ERRATA_SPI_SINGLE_BYTE_WORKAROUND +void spi_gpio_handler(void *arg) +{ + spi_t bus = (spi_t)arg; + + /** + * Immediately disable the IRQ, we only care about one PPI event per + * transfer + */ + gpio_irq_disable(spi_config[bus].sclk); +} +#endif + +/** + * @brief Work-around for transmitting 1 byte with SPIM on the nrf52832. + * @warning Must not be used when transmitting multiple bytes. + * @warning After this workaround is used, the user must reset the PPI channel + * and the GPIOTE channel before attempting to transmit multiple bytes. + * + * @param bus The SPI instance that is in use. + */ +static void _setup_workaround_for_ftpan_58(spi_t bus) +{ +#ifdef ERRATA_SPI_SINGLE_BYTE_WORKAROUND + gpio_init_int(spi_config[bus].sclk, GPIO_OUT, GPIO_BOTH, + spi_gpio_handler, (void *)bus); + gpio_irq_disable(spi_config[bus].sclk); + uint8_t channel = gpio_int_get_exti(spi_config[bus].sclk); + assert(channel != 0xff); + + // Stop the spim instance when SCK toggles. + NRF_PPI->CH[spi_config[bus].ppi].EEP = + (uint32_t)&NRF_GPIOTE->EVENTS_IN[channel]; + NRF_PPI->CH[spi_config[bus].ppi].TEP = (uint32_t)&dev(bus)->TASKS_STOP; +#else + (void)bus; +#endif +} + +static void _enable_workaround(spi_t bus) +{ +#ifdef ERRATA_SPI_SINGLE_BYTE_WORKAROUND + /** + * The spim instance cannot be stopped mid-byte, so it will finish + * transmitting the first byte and then stop. Effectively ensuring + * that only 1 byte is transmitted. + */ + NRF_PPI->CHENSET = 1U << spi_config[bus].ppi; + gpio_irq_enable(spi_config[bus].sclk); +#else + (void)bus; +#endif +} + +static void _clear_workaround(spi_t bus) +{ +#ifdef ERRATA_SPI_SINGLE_BYTE_WORKAROUND + NRF_PPI->CHENCLR = 1U << spi_config[bus].ppi; +#else + (void)bus; +#endif +} + +void spi_init(spi_t bus) +{ + assert(bus < SPI_NUMOF); + + /* initialize mutex */ + mutex_init(&locks[bus]); + mutex_init(&busy[bus]); + mutex_lock(&busy[bus]); + /* initialize pins */ + spi_init_pins(bus); +} + +void spi_init_pins(spi_t bus) +{ + gpio_init(spi_config[bus].sclk, GPIO_OUT); + gpio_init(spi_config[bus].mosi, GPIO_OUT); + gpio_init(spi_config[bus].miso, GPIO_IN); + /* select pins for the SPI device */ + SPI_SCKSEL = spi_config[bus].sclk; + SPI_MOSISEL = spi_config[bus].mosi; + SPI_MISOSEL = spi_config[bus].miso; + _setup_workaround_for_ftpan_58(bus); + spi_twi_irq_register_spi(dev(bus), spi_isr_handler, (void *)bus); +} + +int spi_acquire(spi_t bus, spi_cs_t cs, spi_mode_t mode, spi_clk_t clk) +{ + (void)cs; + + mutex_lock(&locks[bus]); + /* configure bus */ + dev(bus)->CONFIG = mode; + dev(bus)->FREQUENCY = clk; + /* enable the bus */ + dev(bus)->ENABLE = SPIM_ENABLE_ENABLE_Enabled; + + return SPI_OK; +} + +void spi_release(spi_t bus) +{ + /* power off everything */ + dev(bus)->ENABLE = 0; + mutex_unlock(&locks[bus]); +} + +static size_t _transfer(spi_t bus, const uint8_t *out_buf, uint8_t *in_buf, + size_t remaining_len) +{ + uint8_t transfer_len = remaining_len > UINT8_MAX ? UINT8_MAX : remaining_len; + const uint8_t *out_mbuf = out_buf; + + /** + * Copy the out buffer in case it resides in flash, EasyDMA only works from + * RAM + */ + if (out_buf && !_in_ram(out_buf)) { + /* The SPI MBUF can be smaller than UINT8_MAX */ + transfer_len = transfer_len > CONFIG_SPI_MBUF_SIZE + ? CONFIG_SPI_MBUF_SIZE : transfer_len; + memcpy(_mbuf[bus], out_buf, transfer_len); + out_mbuf = _mbuf[bus]; + } + + uint8_t out_len = (out_buf) ? transfer_len : 0; + uint8_t in_len = (in_buf) ? transfer_len : 0; + + dev(bus)->TXD.PTR = (uint32_t)out_mbuf; + dev(bus)->RXD.PTR = (uint32_t)in_buf; + + dev(bus)->TXD.MAXCNT = out_len; + dev(bus)->RXD.MAXCNT = in_len; + + /* clear any spurious END events */ + dev(bus)->EVENTS_END = 0; + dev(bus)->TASKS_START = 1; + return transfer_len; +} + +void spi_transfer_bytes(spi_t bus, spi_cs_t cs, bool cont, + const void *out, void *in, size_t len) +{ + const uint8_t *out_buf = out; + uint8_t *in_buf = in; + + assert(out_buf || in_buf); + + if (cs != SPI_CS_UNDEF) { + gpio_clear((gpio_t)cs); + } + + /* Enable the workaround when the length is only 1 byte */ + size_t _len = len; + if (_len == 1) { + _enable_workaround(bus); + } + + /* Enable IRQ */ + dev(bus)->INTENSET = SPIM_INTENSET_END_Msk; + + do { + size_t transfer_len = _transfer(bus, out_buf, in_buf, len); + /* Block until the irq releases the mutex, then lock it again for the + * next transfer */ + mutex_lock(&busy[bus]); + out_buf += out_buf ? transfer_len : 0; + in_buf += in_buf ? transfer_len : 0; + len -= transfer_len; + } while (len); + + /* Disable IRQ */ + dev(bus)->INTENCLR = SPIM_INTENCLR_END_Msk; + + /** + * While we could always disable the workaround, only doing this when + * required spares us some cycles by not having to write to volatile + * registers + */ + if (_len == 1) { + _clear_workaround(bus); + } + + if ((cs != SPI_CS_UNDEF) && (!cont)) { + gpio_set((gpio_t)cs); + } +} + +void spi_isr_handler(void *arg) +{ + spi_t bus = (spi_t)arg; + + mutex_unlock(&busy[bus]); + dev(bus)->EVENTS_END = 0; +}