From dfdd0761254a412429602d3850355ebe8e7de366 Mon Sep 17 00:00:00 2001 From: iosabi Date: Sun, 20 Dec 2020 19:17:44 +0100 Subject: [PATCH] cpu/qn908x: Implement blocking SPI support. This patch implements the basic support the last of the FLEXCOMM modes, Serial Peripheral Interface, in a simple blocking mode with busy wait, which is enough to test all the SPI functionality end-to-end. Tested reading and writing registers on a SPI peripheral, and checked with the oscilloscope that the frequencies were as expected. Results from `tests/periph_spi`: ``` > init 0 0 2 -1 0 SPI_DEV(0) initialized: mode: 0, clk: 2, cs_port: -1, cs_pin: 0 > bench 1 - write 1000 times 1 byte: 16002 16009 2 - write 1000 times 2 byte: 18001 18008 3 - write 1000 times 100 byte: 802000 802007 4 - write 1000 times 1 byte to register: 24003 24010 5 - write 1000 times 2 byte to register: 26001 26008 6 - write 1000 times 100 byte to register: 810001 810008 7 - read 1000 times 2 byte: 23003 23009 8 - read 1000 times 100 byte: 807002 807009 9 - read 1000 times 2 byte from register: 32002 32009 10 - read 1000 times 100 byte from register: 816002 816009 11 - transfer 1000 times 2 byte: 23003 23009 12 - transfer 1000 times 100 byte: 807003 807010 13 - transfer 1000 times 2 byte to register: 32003 32009 14 - transfer 1000 times 100 byte to register:816002 816009 15 - acquire/release 1000 times: 7222 7228 -- - SUM: 5059250 5059351 ``` --- cpu/qn908x/Kconfig | 1 + cpu/qn908x/Makefile.features | 1 + cpu/qn908x/doc.txt | 49 +++++ cpu/qn908x/include/gpio_mux.h | 10 + cpu/qn908x/include/periph_cpu.h | 86 ++++++++ cpu/qn908x/periph/spi.c | 341 ++++++++++++++++++++++++++++++++ 6 files changed, 488 insertions(+) create mode 100644 cpu/qn908x/periph/spi.c diff --git a/cpu/qn908x/Kconfig b/cpu/qn908x/Kconfig index cf4a1ddce4..98f7b03062 100644 --- a/cpu/qn908x/Kconfig +++ b/cpu/qn908x/Kconfig @@ -15,6 +15,7 @@ config CPU_FAM_QN908X select HAS_PERIPH_GPIO_IRQ select HAS_PERIPH_I2C_RECONFIGURE select HAS_PERIPH_RTC + select HAS_PERIPH_SPI_RECONFIGURE select HAS_PERIPH_WDT select HAS_PERIPH_WDT_CB diff --git a/cpu/qn908x/Makefile.features b/cpu/qn908x/Makefile.features index fbd0f167eb..b8fe32e529 100644 --- a/cpu/qn908x/Makefile.features +++ b/cpu/qn908x/Makefile.features @@ -6,6 +6,7 @@ FEATURES_PROVIDED += periph_cpuid FEATURES_PROVIDED += periph_gpio periph_gpio_irq FEATURES_PROVIDED += periph_i2c_reconfigure FEATURES_PROVIDED += periph_rtc +FEATURES_PROVIDED += periph_spi_reconfigure FEATURES_PROVIDED += periph_wdt periph_wdt_cb include $(RIOTCPU)/cortexm_common/Makefile.features diff --git a/cpu/qn908x/doc.txt b/cpu/qn908x/doc.txt index fb9bcc2160..d8754333e4 100644 --- a/cpu/qn908x/doc.txt +++ b/cpu/qn908x/doc.txt @@ -137,6 +137,55 @@ same time since they are both the same FLEXCOMM1 interface. #define I2C_NUMOF ARRAY_SIZE(i2c_config) +@defgroup cpu_qn908x_spi NXP QN908x Serial Peripheral Interface (SPI) +@ingroup cpu_qn908x +@brief NXP QN908x timer driver + +Two of the FLEXCOMM interfaces in this chip can be used as SPI interfaces named +SPI0 and SPI1, which correspond to FLEXCOMM2 and FLEXCOMM3. Note that FLEXCOMM2 +(SPI0) is also shared with the I2C peripheral I2C1 and both can't be used at +the same time. + +The SPI flexcomm clock is directly driven from the AHB bus, so its clock is +limited by the core CPU clock and the AHB divisor on the higher side with an +optional frequency divider of up to 65536 to generate lower clock frequencies. + +Multiple peripherals can be connected to the same SPI bus, using different CS +pins, with a maximum of 4 hardware CS peripherals per bus and any number of +software CS peripherals. + +This driver uses the [OSHA SPI Signal Names]( +https://www.oshwa.org/a-resolution-to-redefine-spi-signal-names/) and while it +only implements the Controller mode, the hardware is capable of operating in +Peripheral mode as well so we use the COPI/CIPO names. + +### SPI configuration example (for periph_conf.h) ### + +The following example uses only one hardware CS (number 0) and leaves the rest +unused. Check the user manual for the full list of CS pins available. + +When configuring the CS line on a driver, you should pass a @ref SPI_HWCS to use +the hardware CS mode defined in this configuration. To use any other GPIO as a +CS line selected by software it is also possible to pass a @ref GPIO_PIN pin. + +@code + static const spi_conf_t spi_config[] = { + { + .dev = SPI0, + .cipo_pin = GPIO_PIN(PORT_A, 5), + .copi_pin = GPIO_PIN(PORT_A, 4), + .clk_pin = GPIO_PIN(PORT_A, 30), + .cs_pin = { + GPIO_PIN(PORT_A, 3), /* Use as SPI_HWCS(0) */ + GPIO_UNDEF, + GPIO_UNDEF, + GPIO_UNDEF + }, + }, + }; +@endcode + + @defgroup cpu_qn908x_timer NXP QN908x Standard counter/timers (CTIMER) @ingroup cpu_qn908x @brief NXP QN908x timer driver diff --git a/cpu/qn908x/include/gpio_mux.h b/cpu/qn908x/include/gpio_mux.h index 0a152a05cf..19c77b80ef 100644 --- a/cpu/qn908x/include/gpio_mux.h +++ b/cpu/qn908x/include/gpio_mux.h @@ -56,6 +56,16 @@ extern "C" { #error "GPIO_T_ADDR(GPIO_PIN(1, x)) must be the GPIOB address" #endif +/** + * @brief Return whether the given pin is a CSHW pin. + */ +#define GPIO_T_IS_HWCS(pin) (((pin) & 0xff00u) == 0x8000) + +/** + * @brief Return the given CSHW number from the gpio_t pin. + */ +#define GPIO_T_HWCS(pin) ((pin) & 0x0003u) + /** * @brief Configure the pin mux to the given function. * diff --git a/cpu/qn908x/include/periph_cpu.h b/cpu/qn908x/include/periph_cpu.h index 6b416c4595..d674c4615b 100644 --- a/cpu/qn908x/include/periph_cpu.h +++ b/cpu/qn908x/include/periph_cpu.h @@ -384,6 +384,92 @@ typedef struct { #define i2c_pin_scl(dev) i2c_config[dev].pin_scl /** @} */ +/** + * @brief Use some common SPI functions + * @{ + */ +#define PERIPH_SPI_NEEDS_TRANSFER_BYTE +#define PERIPH_SPI_NEEDS_TRANSFER_REG +#define PERIPH_SPI_NEEDS_TRANSFER_REGS +/** @} */ + +#ifndef DOXYGEN +/** + * @brief Define a CPU specific SPI hardware chip select line macro + * + * GPIO numbers use the lower 5 bits and the bit 12. We define the CS numbers + * to have the bit 15 set. + */ +#define SPI_HWCS(x) (1u << 15u | (x)) + +/** + * @brief Number of HW CS pins supported + */ +#define SPI_HWCS_NUMOF 4 + +/** + * @brief SPI mode select helper macro + * + * The polarity is determined by the bits CPOL and CPHA in the SPI CFG register. + */ +#define SPI_MODE_SEL(pol, pha) (SPI_CFG_CPOL(pol) | SPI_CFG_CPHA(pha)) + +/** + * @name Override the SPI mode bitmask + * + * Override the SPI mode value so we can use it directly as a bitmask to CFG. + * @{ + */ +#define HAVE_SPI_MODE_T +typedef enum { + SPI_MODE_0 = SPI_MODE_SEL(0, 0), /**< mode 0 */ + SPI_MODE_1 = SPI_MODE_SEL(0, 1), /**< mode 1 */ + SPI_MODE_2 = SPI_MODE_SEL(1, 0), /**< mode 2 */ + SPI_MODE_3 = SPI_MODE_SEL(1, 1) /**< mode 3 */ +} spi_mode_t; +/** @} */ + +/** + * @name Override SPI speed values + * + * The speed is configured at run time based on the AHB clock speed using an + * arbitrary divider between /1 and /65536. The standard macro values just map + * to the frequency in Hz. The maximum possible speed is 32 MHz assuming a + * core clock and AHB bus clock of 32 MHz. + * @{ + */ +#define HAVE_SPI_CLK_T +typedef enum { + SPI_CLK_100KHZ = 100000u, /**< drive the SPI bus with 100KHz */ + SPI_CLK_400KHZ = 400000u, /**< drive the SPI bus with 400KHz */ + SPI_CLK_1MHZ = 1000000u, /**< drive the SPI bus with 1MHz */ + SPI_CLK_5MHZ = 5000000u, /**< drive the SPI bus with 5MHz */ + SPI_CLK_10MHZ = 10000000u /**< drive the SPI bus with 10MHz */ +} spi_clk_t; +/** @} */ + +/** + * @brief SPI pin getters + * @{ + */ +#define spi_pin_mosi(bus) spi_config[bus].copi_pin +#define spi_pin_miso(bus) spi_config[bus].cipo_pin +#define spi_pin_clk(bus) spi_config[bus].clk_pin +/** @} */ + +/** + * @brief SPI module configuration options + */ +typedef struct { + SPI_Type *dev; /**< SPI device to use */ + gpio_t cipo_pin; /**< Controller Input Peripheral Output */ + gpio_t copi_pin; /**< Controller Output Peripheral Input */ + gpio_t clk_pin; /**< CLK pin */ + gpio_t cs_pin[SPI_HWCS_NUMOF]; /**< pins used for HW cs lines */ +} spi_conf_t; + +#endif /* ifndef DOXYGEN */ + /** * @brief UART module configuration options * diff --git a/cpu/qn908x/periph/spi.c b/cpu/qn908x/periph/spi.c new file mode 100644 index 0000000000..66dd689c17 --- /dev/null +++ b/cpu/qn908x/periph/spi.c @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2020 iosabi + * + * 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_qn908x + * @ingroup drivers_periph_spi + * + * @{ + * + * @file + * @brief Low-level SPI driver implementation + * + * @author iosabi + * + * @} + */ + +#include "assert.h" +#include "bitarithm.h" +#include "mutex.h" + +#include "cpu.h" +#include "periph_conf.h" +#include "periph/spi.h" + +#include "vendor/drivers/fsl_clock.h" +#include "flexcomm.h" +#include "gpio_mux.h" + +#define ENABLE_DEBUG 0 +#include "debug.h" + +typedef struct { + uint8_t *in; /**< The RX buffer pointer or NULL if unused. */ + uint32_t in_len; /**< The remaining bytes to receive or 0 if unused. */ + + const uint8_t *out; /**< The TX buffer pointer or NULL if unused. */ + + /** + * @brief The remaining transfer length. + * + * This value is set even if we are not transferring any data, in which case + * it indicates the remaining 8-bit clock pulses needed to be sent to the + * FIFO to finish the transfer. + */ + uint32_t tr_len; + + uint32_t tx_mask; /** FIFOWR mask used when transmitting. */ +} spi_pending_transfer_t; + +/** + * @brief Mutex for accessing each SPI bus. + */ +static mutex_t locks[SPI_NUMOF]; + +/** + * @brief Bitmask of Port A pins that use Function 4 for the FLEXCOMM2. + * + * SPI pins are either function 4 or 5 depending on the pin and flexcomm. + * All FLEXCOMM3 possible pins are mapped to function 5, while in the + * case of FLEXCOMM2 some are in function 4. Some pins can act as a function + * in FLEXCOMM2 (function 4) while act as another function in FLEXCOM3 (function + * 5) + */ +static const uint32_t _spi_func5_mask_fc2 = + (1u << 0) | /* FC2_SSEL3 */ + (1u << 1) | /* FC2_SSEL2 */ + (1u << 2) | /* FC2_SSEL1 */ + (1u << 3) | /* FC2_SSEL0 */ + (1u << 4) | /* FC2_COPI */ + (1u << 5); /* FC2_CIPO */ + +/** + * @brief Set the clock divided for the target frequency. + */ +static void _spi_controller_set_speed(SPI_Type *spi_bus, uint32_t speed_hz) +{ + /* The SPI clock source is based on the FLEXCOMM clock with a simple + * frequency divider between /1 and /65536. */ + const uint32_t bus_freq = CLOCK_GetFreq(kCLOCK_BusClk); + uint32_t divider = (bus_freq + speed_hz / 2) / speed_hz; + + if (divider == 0) { + divider = 1; + } + else if (divider > (1u << 16)) { + divider = 1u << 16; + } + DEBUG("[spi] clock requested: %" PRIu32 " Hz, actual: %" PRIu32 + " Hz, divider: /%" PRIu32 "\n", speed_hz, bus_freq / divider, + divider); + /* The value stored in DIV is always (divider - 1), meaning that a value of + * 0 divides by 1. */ + spi_bus->DIV = divider - 1; +} + +void spi_init(spi_t bus) +{ + assert(bus < SPI_NUMOF); + const spi_conf_t *const conf = &spi_config[bus]; + SPI_Type *const spi_bus = conf->dev; + + int flexcomm_num = flexcomm_init((FLEXCOMM_Type *)spi_bus, FLEXCOMM_ID_SPI); + DEBUG("[spi] init: bus=%u, flexcomm=%d\n", (unsigned)bus, flexcomm_num); + assert(flexcomm_num >= 0); + + /* Set controller mode, but don't enable it. All CS are active low. MSB + * first bit order (standard). */ + spi_bus->CFG = SPI_CFG_MASTER_MASK; + /* Configure to use the RX and TX FIFO. */ + spi_bus->FIFOCFG = SPI_FIFOCFG_ENABLETX_MASK | SPI_FIFOCFG_ENABLERX_MASK; + locks[bus] = (mutex_t)MUTEX_INIT_LOCKED; + spi_init_pins(bus); +} + +void spi_init_pins(spi_t bus) +{ + assert(bus < SPI_NUMOF); + const spi_conf_t *const conf = &spi_config[bus]; + + const uint32_t mask = conf->dev == (SPI_Type *)FLEXCOMM2_BASE + ? _spi_func5_mask_fc2 + : 0xffffffff; + gpio_init_mux(conf->copi_pin, + ((1u << GPIO_T_PIN(conf->copi_pin)) & mask) ? 5 : 4); + gpio_init_mux(conf->cipo_pin, + ((1u << GPIO_T_PIN(conf->cipo_pin)) & mask) ? 5 : 4); + gpio_init_mux(conf->clk_pin, + ((1u << GPIO_T_PIN(conf->clk_pin)) & mask) ? 5 : 4); + /* Enables the SPI block and sets it to idle. */ + conf->dev->CFG |= SPI_CFG_ENABLE_MASK; + mutex_unlock(&locks[bus]); +} + +int spi_init_cs(spi_t bus, spi_cs_t cs) +{ + /* Initializing the CS pin doesn't require to acquire the mutex since each + * peripheral has its own independent CS pin. */ + if (bus >= SPI_NUMOF) { + return SPI_NODEV; + } + const spi_conf_t *const conf = &spi_config[bus]; + gpio_t pin = cs; + + if (GPIO_T_IS_HWCS(cs)) { + /* The gpio_t value comes from the board config rather than the cs + * variable itself when a HWCS number is passed. */ + pin = conf->cs_pin[GPIO_T_HWCS(cs)]; + } + if (!gpio_is_valid(pin)) { + return SPI_NOCS; + } + DEBUG("[spi] init_cs: cs=0x%.4" PRIx16 " pin=0x%.4" PRIx16 "\n", cs, pin); + + if (GPIO_T_IS_HWCS(cs)) { + const uint32_t mask = conf->dev == (SPI_Type *)FLEXCOMM2_BASE + ? _spi_func5_mask_fc2 + : 0xffffffff; + gpio_init_mux(pin, ((1u << GPIO_T_PIN(pin)) & mask) ? 5 : 4); + } + else { + gpio_init(pin, GPIO_OUT); + gpio_set(pin); + } + + return SPI_OK; +} + +#ifdef MODULE_PERIPH_SPI_RECONFIGURE +void spi_deinit_pins(spi_t bus) +{ + assert(bus < SPI_NUMOF); + mutex_lock(&locks[bus]); + const spi_conf_t *const conf = &spi_config[bus]; + + /* Disables the SPI block. It must be already idle. */ + conf->dev->CFG &= ~SPI_CFG_ENABLE_MASK; + + gpio_init(conf->copi_pin, GPIO_IN); + gpio_init(conf->cipo_pin, GPIO_IN); + gpio_init(conf->clk_pin, GPIO_IN); +} +#endif /* MODULE_PERIPH_SPI_RECONFIGURE */ + +int spi_acquire(spi_t bus, spi_cs_t cs, spi_mode_t mode, spi_clk_t clk) +{ + const spi_conf_t *const conf = &spi_config[bus]; + + mutex_lock(&locks[bus]); + + /* Set SPI clock speed. This silently chooses the closest frequency, no + * matter how far it is from the requested one. */ + _spi_controller_set_speed(conf->dev, clk); + + if ((mode & ~(SPI_CFG_CPHA_MASK | SPI_CFG_CPOL_MASK)) != 0) { + return SPI_NOMODE; + } + + DEBUG("[spi] acquire: mode CPHA=%d CPOL=%d, cs=0x%" PRIx32 "\n", + !!(mode & SPI_CFG_CPHA_MASK), !!(mode & SPI_CFG_CPOL_MASK), + (uint32_t)cs); + + conf->dev->CFG = + (conf->dev->CFG & ~(SPI_CFG_CPHA_MASK | SPI_CFG_CPOL_MASK)) | mode; + + return SPI_OK; +} + +void spi_release(spi_t bus) +{ + assert(bus < SPI_NUMOF); + DEBUG("[spi] release\n"); + mutex_unlock(&locks[bus]); +} + +/** + * @brief: Wait for the FIFO to be empty. + */ +static void _spi_wait_txempty(SPI_Type *spi_bus) +{ + while (!(spi_bus->FIFOSTAT & SPI_FIFOSTAT_TXEMPTY_MASK)) {} +} + +/** + * @brief Bitmask for the FIFOWR register with all the HWCS deasserted. + */ +#define SPI_HWCS_DEASSERT_ALL \ + (((1u << SPI_HWCS_NUMOF) - 1) << SPI_FIFOWR_TXSSEL0_N_SHIFT) + +/** + * @brief Initialize a SPI transfer given the transfer parameters. + */ +static void _spi_config_transfer(spi_pending_transfer_t *tr, spi_cs_t cs, + bool cont, const void *out, void *in, + size_t len) +{ + tr->in = in; + tr->in_len = in ? len : 0; + tr->out = out; + tr->tr_len = len; + tr->tx_mask = SPI_HWCS_DEASSERT_ALL; + if (GPIO_T_IS_HWCS(cs)) { + /* Flag that the TX should assert this HWCS by clearing the bit. */ + tr->tx_mask &= ~(1u << (SPI_FIFOWR_TXSSEL0_N_SHIFT + GPIO_T_HWCS(cs))); + if (!cont) { + /* Flag the End of Transfer (EOT) in the mask. This will only be + * used in the last byte. */ + tr->tx_mask |= SPI_FIFOWR_EOT_MASK; + } + } + if (!in) { + /* Ignores the RX side when the @p in is NULL so we don't need to read + * the FIFO at all. */ + tr->tx_mask |= SPI_FIFOWR_RXIGNORE_MASK; + } + tr->tx_mask |= SPI_FIFOWR_LEN(7); /* Data transfers of 8 bits. */ +} + +/** + * @brief Perform a blocking SPI transfer. + */ +static void _spi_transfer_blocking(spi_t bus, spi_pending_transfer_t *tr) +{ + SPI_Type *const spi_bus = spi_config[bus].dev; + + /* Configure to use the RX and TX fifo, and empty them. */ + spi_bus->FIFOCFG = SPI_FIFOCFG_ENABLETX_MASK + | SPI_FIFOCFG_ENABLERX_MASK + | SPI_FIFOCFG_EMPTYTX_MASK | SPI_FIFOCFG_EMPTYRX_MASK; + spi_bus->FIFOSTAT = SPI_FIFOSTAT_TXERR_MASK | SPI_FIFOSTAT_RXERR_MASK; + + while (tr->in_len || tr->tr_len) { + /* Read from RX FIFO if possible. */ + if (spi_bus->FIFOSTAT & SPI_FIFOSTAT_RXNOTEMPTY_MASK) { + uint32_t rd = spi_bus->FIFORD; + if (tr->in_len) { + *(tr->in++) = (uint8_t)rd; + tr->in_len--; + } + } + + /* Write when able to write and we have data to send or bogus (0) bytes + * to send when in receive-only mode. */ + if ((spi_bus->FIFOSTAT & SPI_FIFOSTAT_TXNOTFULL_MASK) && tr->tr_len) { + uint32_t wr = tr->tx_mask; + if (tr->out) { + wr |= *(tr->out++); + } + + /* If this is *not* the last byte, remove the EOT flag if any. */ + tr->tr_len--; + if (tr->tr_len) { + wr &= ~SPI_FIFOWR_EOT_MASK; + } + /* Push the data to the FIFO. */ + spi_bus->FIFOWR = wr; + } + } + _spi_wait_txempty(spi_bus); +} + +void spi_transfer_bytes(spi_t bus, spi_cs_t cs, bool cont, + const void *out, void *in, size_t len) +{ + spi_pending_transfer_t tr; + + _spi_config_transfer(&tr, cs, cont, out, in, len); + + /* At least one of input or one output buffer is given */ + assert(bus < SPI_NUMOF); + + if (!GPIO_T_IS_HWCS(cs)) { + /* Assert CS using a gpio. */ + gpio_clear((gpio_t)cs); + } + + DEBUG("[spi] transfer: cs=0x%.4" PRIx16 " cont=%d len=%" PRIu32 "\n", + cs, cont, (uint32_t)len); + _spi_transfer_blocking(bus, &tr); + + /* Deassert the CS only in gpio mode. HWCS deassert are handled by the + * hardware when EOT is set in the mask. */ + if (!cont && !GPIO_T_IS_HWCS(cs)) { + gpio_set((gpio_t)cs); + } +} + +/* ISR routine called for FLEXCOMM devices configured as SPI. */ +void isr_flexcomm_spi(USART_Type *dev, uint32_t flexcomm_num) +{ + // TODO: Set up async mode with interrupts. + (void)dev; + (void)flexcomm_num; + + cortexm_isr_end(); +}