1
0
mirror of https://github.com/RIOT-OS/RIOT.git synced 2024-12-29 04:50:03 +01:00

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

```
This commit is contained in:
iosabi 2020-12-20 19:17:44 +01:00
parent bd837fe98b
commit dfdd076125
6 changed files with 488 additions and 0 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

341
cpu/qn908x/periph/spi.c Normal file
View File

@ -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 <iosabi@protonmail.com>
*
* @}
*/
#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();
}