Allow mixed speed and mode on a SPI bus by CR1 caching
This commit is contained in:
parent
4778ad6c0f
commit
343e9b3a67
|
@ -75,7 +75,11 @@ void bmp280BusInit(busDevice_t *busdev)
|
|||
IOHi(busdev->busdev_u.spi.csnPin); // Disable
|
||||
IOInit(busdev->busdev_u.spi.csnPin, OWNER_BARO_CS, 0);
|
||||
IOConfigGPIO(busdev->busdev_u.spi.csnPin, IOCFG_OUT_PP);
|
||||
spiSetDivisor(busdev->busdev_u.spi.instance, SPI_CLOCK_STANDARD); // XXX
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(busdev, SPI_MODE0_POL_LOW_EDGE_1ST, SPI_CLOCK_STANDARD); // BMP280 supports Mode 0 or 3
|
||||
#else
|
||||
spiBusSetDivisor(busdev, SPI_CLOCK_STANDARD);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
UNUSED(busdev);
|
||||
|
@ -93,6 +97,8 @@ void bmp280BusDeinit(busDevice_t *busdev)
|
|||
#endif
|
||||
}
|
||||
|
||||
#include "drivers/time.h"
|
||||
|
||||
bool bmp280Detect(baroDev_t *baro)
|
||||
{
|
||||
delay(20);
|
||||
|
|
|
@ -255,7 +255,11 @@ bool lpsDetect(baroDev_t *baro)
|
|||
IOInit(busdev->busdev_u.spi.csnPin, OWNER_BARO_CS, 0);
|
||||
IOConfigGPIO(busdev->busdev_u.spi.csnPin, IOCFG_OUT_PP);
|
||||
IOHi(busdev->busdev_u.spi.csnPin); // Disable
|
||||
spiSetDivisor(busdev->busdev_u.spi.instance, SPI_CLOCK_STANDARD); // Baro can work only on up to 10Mhz SPI bus
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(busdev, SPI_MODE3_POL_HIGH_EDGE_2ND, SPI_CLOCK_STANDARD); // Baro can work only on up to 10Mhz SPI bus
|
||||
#else
|
||||
spiBusSetDivisor(busdev, SPI_CLOCK_STANDARD); // Baro can work only on up to 10Mhz SPI bus
|
||||
#endif
|
||||
|
||||
uint8_t temp = 0x00;
|
||||
lpsReadCommand(&baro->busdev, LPS_WHO_AM_I, &temp, 1);
|
||||
|
|
|
@ -71,7 +71,11 @@ void ms5611BusInit(busDevice_t *busdev)
|
|||
IOHi(busdev->busdev_u.spi.csnPin); // Disable
|
||||
IOInit(busdev->busdev_u.spi.csnPin, OWNER_BARO_CS, 0);
|
||||
IOConfigGPIO(busdev->busdev_u.spi.csnPin, IOCFG_OUT_PP);
|
||||
spiSetDivisor(busdev->busdev_u.spi.instance, SPI_CLOCK_STANDARD); // XXX
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(busdev, SPI_MODE3_POL_HIGH_EDGE_2ND, SPI_CLOCK_STANDARD);
|
||||
#else
|
||||
spiBusSetDivisor(busdev, SPI_CLOCK_STANDARD); // XXX
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
UNUSED(busdev);
|
||||
|
|
|
@ -104,7 +104,11 @@ void qmp6988BusInit(busDevice_t *busdev)
|
|||
IOHi(busdev->busdev_u.spi.csnPin);
|
||||
IOInit(busdev->busdev_u.spi.csnPin, OWNER_BARO_CS, 0);
|
||||
IOConfigGPIO(busdev->busdev_u.spi.csnPin, IOCFG_OUT_PP);
|
||||
spiSetDivisor(busdev->busdev_u.spi.instance, SPI_CLOCK_STANDARD);
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(busdev, SPI_MODE3_POL_HIGH_EDGE_2ND, SPI_CLOCK_STANDARD);
|
||||
#else
|
||||
spiBusSetDivisor(busdev, SPI_CLOCK_STANDARD);
|
||||
#endif
|
||||
}
|
||||
#else
|
||||
UNUSED(busdev);
|
||||
|
|
|
@ -36,8 +36,13 @@ bool busWriteRegister(const busDevice_t *busdev, uint8_t reg, uint8_t data)
|
|||
switch (busdev->bustype) {
|
||||
#ifdef USE_SPI
|
||||
case BUSTYPE_SPI:
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
// XXX Watch out fastpath users, if any
|
||||
return spiBusTransactionWriteRegister(busdev, reg & 0x7f, data);
|
||||
#else
|
||||
return spiBusWriteRegister(busdev, reg & 0x7f, data);
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_I2C
|
||||
case BUSTYPE_I2C:
|
||||
return i2cBusWriteRegister(busdev, reg, data);
|
||||
|
@ -57,8 +62,13 @@ bool busReadRegisterBuffer(const busDevice_t *busdev, uint8_t reg, uint8_t *data
|
|||
switch (busdev->bustype) {
|
||||
#ifdef USE_SPI
|
||||
case BUSTYPE_SPI:
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
// XXX Watch out fastpath users, if any
|
||||
return spiBusTransactionReadRegisterBuffer(busdev, reg | 0x80, data, length);
|
||||
#else
|
||||
return spiBusReadRegisterBuffer(busdev, reg | 0x80, data, length);
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_I2C
|
||||
case BUSTYPE_I2C:
|
||||
return i2cBusReadRegisterBuffer(busdev, reg, data, length);
|
||||
|
|
|
@ -33,11 +33,18 @@ typedef enum {
|
|||
BUSTYPE_GYRO_AUTO // Only used by acc/gyro bus auto detection code
|
||||
} busType_e;
|
||||
|
||||
struct spiDevice_s;
|
||||
|
||||
typedef struct busDevice_s {
|
||||
busType_e bustype;
|
||||
union {
|
||||
struct deviceSpi_s {
|
||||
SPI_TypeDef *instance;
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
struct SPIDevice_s *device; // Back ptr to controller for this device.
|
||||
// Cached SPI_CR1 for spiBusTransactionXXX
|
||||
uint16_t modeCache; // XXX cr1Value may be a better name?
|
||||
#endif
|
||||
#if defined(USE_HAL_DRIVER)
|
||||
SPI_HandleTypeDef* handle; // cached here for efficiency
|
||||
#endif
|
||||
|
|
|
@ -174,7 +174,6 @@ bool spiBusWriteRegister(const busDevice_t *bus, uint8_t reg, uint8_t data)
|
|||
}
|
||||
|
||||
bool spiBusRawReadRegisterBuffer(const busDevice_t *bus, uint8_t reg, uint8_t *data, uint8_t length)
|
||||
|
||||
{
|
||||
IOLo(bus->busdev_u.spi.csnPin);
|
||||
spiTransferByte(bus->busdev_u.spi.instance, reg);
|
||||
|
@ -218,4 +217,49 @@ void spiBusSetInstance(busDevice_t *bus, SPI_TypeDef *instance)
|
|||
bus->bustype = BUSTYPE_SPI;
|
||||
bus->busdev_u.spi.instance = instance;
|
||||
}
|
||||
|
||||
void spiBusSetDivisor(busDevice_t *bus, uint16_t divisor)
|
||||
{
|
||||
spiSetDivisor(bus->busdev_u.spi.instance, divisor);
|
||||
// bus->busdev_u.spi.modeCache = bus->busdev_u.spi.instance->CR1;
|
||||
}
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
// Separate set of spiBusTransactionXXX to keep fast path for acc/gyros.
|
||||
|
||||
void spiBusTransactionBegin(const busDevice_t *bus)
|
||||
{
|
||||
spiBusTransactionSetup(bus);
|
||||
IOLo(bus->busdev_u.spi.csnPin);
|
||||
}
|
||||
|
||||
void spiBusTransactionEnd(const busDevice_t *bus)
|
||||
{
|
||||
IOHi(bus->busdev_u.spi.csnPin);
|
||||
}
|
||||
|
||||
bool spiBusTransactionTransfer(const busDevice_t *bus, const uint8_t *txData, uint8_t *rxData, int length)
|
||||
{
|
||||
spiBusTransactionSetup(bus);
|
||||
return spiBusTransfer(bus, txData, rxData, length);
|
||||
}
|
||||
|
||||
bool spiBusTransactionWriteRegister(const busDevice_t *bus, uint8_t reg, uint8_t data)
|
||||
{
|
||||
spiBusTransactionSetup(bus);
|
||||
return spiBusWriteRegister(bus, reg, data);
|
||||
}
|
||||
|
||||
uint8_t spiBusTransactionReadRegister(const busDevice_t *bus, uint8_t reg)
|
||||
{
|
||||
spiBusTransactionSetup(bus);
|
||||
return spiBusReadRegister(bus, reg);
|
||||
}
|
||||
|
||||
bool spiBusTransactionReadRegisterBuffer(const busDevice_t *bus, uint8_t reg, uint8_t *data, uint8_t length)
|
||||
{
|
||||
spiBusTransactionSetup(bus);
|
||||
return spiBusReadRegisterBuffer(bus, reg, data, length);
|
||||
}
|
||||
#endif // USE_SPI_TRANSACTION
|
||||
#endif
|
||||
|
|
|
@ -69,6 +69,21 @@ typedef enum {
|
|||
#endif
|
||||
} SPIClockDivider_e;
|
||||
|
||||
// De facto standard mode
|
||||
// See https://en.wikipedia.org/wiki/Serial_Peripheral_Interface
|
||||
//
|
||||
// Mode CPOL CPHA
|
||||
// 0 0 0
|
||||
// 1 0 1
|
||||
// 2 1 0
|
||||
// 3 1 1
|
||||
typedef enum {
|
||||
SPI_MODE0_POL_LOW_EDGE_1ST = 0,
|
||||
SPI_MODE1_POL_LOW_EDGE_2ND,
|
||||
SPI_MODE2_POL_HIGH_EDGE_1ST,
|
||||
SPI_MODE3_POL_HIGH_EDGE_2ND
|
||||
} SPIMode_e;
|
||||
|
||||
typedef enum SPIDevice {
|
||||
SPIINVALID = -1,
|
||||
SPIDEV_1 = 0,
|
||||
|
@ -124,6 +139,16 @@ void spiBusWriteRegisterBuffer(const busDevice_t *bus, uint8_t reg, const uint8_
|
|||
uint8_t spiBusRawReadRegister(const busDevice_t *bus, uint8_t reg);
|
||||
uint8_t spiBusReadRegister(const busDevice_t *bus, uint8_t reg);
|
||||
void spiBusSetInstance(busDevice_t *bus, SPI_TypeDef *instance);
|
||||
void spiBusSetDivisor(busDevice_t *bus, SPIClockDivider_e divider);
|
||||
|
||||
void spiBusTransactionInit(busDevice_t *bus, SPIMode_e mode, SPIClockDivider_e divider);
|
||||
void spiBusTransactionSetup(const busDevice_t *bus);
|
||||
void spiBusTransactionBegin(const busDevice_t *bus);
|
||||
void spiBusTransactionEnd(const busDevice_t *bus);
|
||||
bool spiBusTransactionWriteRegister(const busDevice_t *bus, uint8_t reg, uint8_t data);
|
||||
uint8_t spiBusTransactionReadRegister(const busDevice_t *bus, uint8_t reg);
|
||||
bool spiBusTransactionReadRegisterBuffer(const busDevice_t *bus, uint8_t reg, uint8_t *data, uint8_t length);
|
||||
bool spiBusTransactionTransfer(const busDevice_t *bus, const uint8_t *txData, uint8_t *rxData, int length);
|
||||
|
||||
struct spiPinConfig_s;
|
||||
void spiPinConfigure(const struct spiPinConfig_s *pConfig);
|
||||
|
|
|
@ -70,6 +70,9 @@ typedef struct SPIDevice_s {
|
|||
DMA_HandleTypeDef hdma;
|
||||
uint8_t dmaIrqHandler;
|
||||
#endif
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
uint16_t cr1SoftCopy; // Copy of active CR1 value for this SPI instance
|
||||
#endif
|
||||
} spiDevice_t;
|
||||
|
||||
extern spiDevice_t spiDevice[SPIDEV_COUNT];
|
||||
|
|
|
@ -73,6 +73,18 @@
|
|||
|
||||
#define SPI_DEFAULT_TIMEOUT 10
|
||||
|
||||
static LL_SPI_InitTypeDef defaultInit =
|
||||
{
|
||||
.TransferDirection = SPI_DIRECTION_2LINES,
|
||||
.Mode = SPI_MODE_MASTER,
|
||||
.DataWidth = SPI_DATASIZE_8BIT,
|
||||
.NSS = SPI_NSS_SOFT,
|
||||
.BaudRate = SPI_BAUDRATEPRESCALER_8,
|
||||
.BitOrder = SPI_FIRSTBIT_MSB,
|
||||
.CRCPoly = 7,
|
||||
.CRCCalculation = SPI_CRCCALCULATION_DISABLE,
|
||||
};
|
||||
|
||||
void spiInitDevice(SPIDevice device)
|
||||
{
|
||||
spiDevice_t *spi = &(spiDevice[device]);
|
||||
|
@ -81,6 +93,7 @@ void spiInitDevice(SPIDevice device)
|
|||
return;
|
||||
}
|
||||
|
||||
#ifndef USE_SPI_TRANSACTION
|
||||
#ifdef SDCARD_SPI_INSTANCE
|
||||
if (spi->dev == SDCARD_SPI_INSTANCE) {
|
||||
spi->leadingEdge = true;
|
||||
|
@ -90,6 +103,7 @@ void spiInitDevice(SPIDevice device)
|
|||
if (spi->dev == RX_SPI_INSTANCE) {
|
||||
spi->leadingEdge = true;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Enable SPI clock
|
||||
|
@ -110,22 +124,20 @@ void spiInitDevice(SPIDevice device)
|
|||
LL_SPI_Disable(spi->dev);
|
||||
LL_SPI_DeInit(spi->dev);
|
||||
|
||||
LL_SPI_InitTypeDef init =
|
||||
#ifndef USE_SPI_TRANSACTION
|
||||
if (spi->leadingEdge) {
|
||||
defaultInit.ClockPolarity = SPI_POLARITY_LOW;
|
||||
defaultInit.ClockPhase = SPI_PHASE_1EDGE;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
.TransferDirection = SPI_DIRECTION_2LINES,
|
||||
.Mode = SPI_MODE_MASTER,
|
||||
.DataWidth = SPI_DATASIZE_8BIT,
|
||||
.ClockPolarity = spi->leadingEdge ? SPI_POLARITY_LOW : SPI_POLARITY_HIGH,
|
||||
.ClockPhase = spi->leadingEdge ? SPI_PHASE_1EDGE : SPI_PHASE_2EDGE,
|
||||
.NSS = SPI_NSS_SOFT,
|
||||
.BaudRate = SPI_BAUDRATEPRESCALER_8,
|
||||
.BitOrder = SPI_FIRSTBIT_MSB,
|
||||
.CRCPoly = 7,
|
||||
.CRCCalculation = SPI_CRCCALCULATION_DISABLE,
|
||||
};
|
||||
defaultInit.ClockPolarity = SPI_POLARITY_HIGH;
|
||||
defaultInit.ClockPhase = SPI_PHASE_2EDGE;
|
||||
}
|
||||
|
||||
LL_SPI_SetRxFIFOThreshold(spi->dev, SPI_RXFIFO_THRESHOLD_QF);
|
||||
|
||||
LL_SPI_Init(spi->dev, &init);
|
||||
LL_SPI_Init(spi->dev, &defaultInit);
|
||||
LL_SPI_Enable(spi->dev);
|
||||
}
|
||||
|
||||
|
@ -217,7 +229,7 @@ bool spiTransfer(SPI_TypeDef *instance, const uint8_t *txData, uint8_t *rxData,
|
|||
return true;
|
||||
}
|
||||
|
||||
void spiSetDivisor(SPI_TypeDef *instance, uint16_t divisor)
|
||||
static uint16_t spiDivisorToBRbits(SPI_TypeDef *instance, uint16_t divisor)
|
||||
{
|
||||
#if !(defined(STM32F1) || defined(STM32F3))
|
||||
// SPI2 and SPI3 are on APB1/AHB1 which PCLK is half that of APB2/AHB2.
|
||||
|
@ -225,12 +237,71 @@ void spiSetDivisor(SPI_TypeDef *instance, uint16_t divisor)
|
|||
if (instance == SPI2 || instance == SPI3) {
|
||||
divisor /= 2; // Safe for divisor == 0 or 1
|
||||
}
|
||||
#else
|
||||
UNUSED(instance);
|
||||
#endif
|
||||
|
||||
divisor = constrain(divisor, 2, 256);
|
||||
|
||||
return (ffs(divisor) - 2) << SPI_CR1_BR_Pos;
|
||||
}
|
||||
|
||||
void spiSetDivisor(SPI_TypeDef *instance, uint16_t divisor)
|
||||
{
|
||||
LL_SPI_Disable(instance);
|
||||
LL_SPI_SetBaudRatePrescaler(instance, (ffs(divisor) - 2) << SPI_CR1_BR_Pos);
|
||||
LL_SPI_SetBaudRatePrescaler(instance, spiDivisorToBRbits(instance, divisor));
|
||||
LL_SPI_Enable(instance);
|
||||
}
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
void spiBusTransactionInit(busDevice_t *bus, SPIMode_e mode, SPIClockDivider_e divisor)
|
||||
{
|
||||
switch (mode) {
|
||||
case SPI_MODE0_POL_LOW_EDGE_1ST:
|
||||
defaultInit.ClockPolarity = SPI_POLARITY_LOW;
|
||||
defaultInit.ClockPhase = SPI_PHASE_1EDGE;
|
||||
break;
|
||||
case SPI_MODE1_POL_LOW_EDGE_2ND:
|
||||
defaultInit.ClockPolarity = SPI_POLARITY_LOW;
|
||||
defaultInit.ClockPhase = SPI_PHASE_2EDGE;
|
||||
break;
|
||||
case SPI_MODE2_POL_HIGH_EDGE_1ST:
|
||||
defaultInit.ClockPolarity = SPI_POLARITY_HIGH;
|
||||
defaultInit.ClockPhase = SPI_PHASE_1EDGE;
|
||||
break;
|
||||
case SPI_MODE3_POL_HIGH_EDGE_2ND:
|
||||
defaultInit.ClockPolarity = SPI_POLARITY_HIGH;
|
||||
defaultInit.ClockPhase = SPI_PHASE_2EDGE;
|
||||
break;
|
||||
}
|
||||
|
||||
LL_SPI_Disable(bus->busdev_u.spi.instance);
|
||||
LL_SPI_DeInit(bus->busdev_u.spi.instance);
|
||||
|
||||
LL_SPI_Init(bus->busdev_u.spi.instance, &defaultInit);
|
||||
LL_SPI_SetBaudRatePrescaler(bus->busdev_u.spi.instance, spiDivisorToBRbits(bus->busdev_u.spi.instance, divisor));
|
||||
|
||||
// Configure for 8-bit reads. XXX Is this STM32F303xC specific?
|
||||
LL_SPI_SetRxFIFOThreshold(bus->busdev_u.spi.instance, SPI_RXFIFO_THRESHOLD_QF);
|
||||
|
||||
LL_SPI_Enable(bus->busdev_u.spi.instance);
|
||||
|
||||
bus->busdev_u.spi.device = &spiDevice[spiDeviceByInstance(bus->busdev_u.spi.instance)];
|
||||
bus->busdev_u.spi.modeCache = bus->busdev_u.spi.instance->CR1;
|
||||
}
|
||||
|
||||
void spiBusTransactionSetup(const busDevice_t *bus)
|
||||
{
|
||||
// We rely on MSTR bit to detect valid modeCache
|
||||
|
||||
if (bus->busdev_u.spi.modeCache && bus->busdev_u.spi.modeCache != bus->busdev_u.spi.device->cr1SoftCopy) {
|
||||
bus->busdev_u.spi.instance->CR1 = bus->busdev_u.spi.modeCache;
|
||||
bus->busdev_u.spi.device->cr1SoftCopy = bus->busdev_u.spi.modeCache;
|
||||
|
||||
// SCK seems to require some time to switch to a new initial level after CR1 is written.
|
||||
// Here we buy some time in addition to the software copy save above.
|
||||
__asm__("nop");
|
||||
}
|
||||
}
|
||||
#endif // USE_SPI_TRANSACTION
|
||||
#endif
|
||||
|
|
|
@ -34,6 +34,16 @@
|
|||
#include "drivers/io.h"
|
||||
#include "drivers/rcc.h"
|
||||
|
||||
static SPI_InitTypeDef defaultInit = {
|
||||
.SPI_Mode = SPI_Mode_Master,
|
||||
.SPI_Direction = SPI_Direction_2Lines_FullDuplex,
|
||||
.SPI_DataSize = SPI_DataSize_8b,
|
||||
.SPI_NSS = SPI_NSS_Soft,
|
||||
.SPI_FirstBit = SPI_FirstBit_MSB,
|
||||
.SPI_CRCPolynomial = 7,
|
||||
.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8,
|
||||
};
|
||||
|
||||
void spiInitDevice(SPIDevice device)
|
||||
{
|
||||
spiDevice_t *spi = &(spiDevice[device]);
|
||||
|
@ -42,6 +52,7 @@ void spiInitDevice(SPIDevice device)
|
|||
return;
|
||||
}
|
||||
|
||||
#ifndef USE_SPI_TRANSACTION
|
||||
#ifdef SDCARD_SPI_INSTANCE
|
||||
if (spi->dev == SDCARD_SPI_INSTANCE) {
|
||||
spi->leadingEdge = true;
|
||||
|
@ -51,6 +62,7 @@ void spiInitDevice(SPIDevice device)
|
|||
if (spi->dev == RX_SPI_INSTANCE) {
|
||||
spi->leadingEdge = true;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Enable SPI clock
|
||||
|
@ -76,21 +88,15 @@ void spiInitDevice(SPIDevice device)
|
|||
// Init SPI hardware
|
||||
SPI_I2S_DeInit(spi->dev);
|
||||
|
||||
SPI_InitTypeDef spiInit;
|
||||
spiInit.SPI_Mode = SPI_Mode_Master;
|
||||
spiInit.SPI_Direction = SPI_Direction_2Lines_FullDuplex;
|
||||
spiInit.SPI_DataSize = SPI_DataSize_8b;
|
||||
spiInit.SPI_NSS = SPI_NSS_Soft;
|
||||
spiInit.SPI_FirstBit = SPI_FirstBit_MSB;
|
||||
spiInit.SPI_CRCPolynomial = 7;
|
||||
spiInit.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;
|
||||
|
||||
#ifndef USE_SPI_TRANSACTION
|
||||
if (spi->leadingEdge) {
|
||||
spiInit.SPI_CPOL = SPI_CPOL_Low;
|
||||
spiInit.SPI_CPHA = SPI_CPHA_1Edge;
|
||||
} else {
|
||||
spiInit.SPI_CPOL = SPI_CPOL_High;
|
||||
spiInit.SPI_CPHA = SPI_CPHA_2Edge;
|
||||
defaultInit.SPI_CPOL = SPI_CPOL_Low;
|
||||
defaultInit.SPI_CPHA = SPI_CPHA_1Edge;
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
defaultInit.SPI_CPOL = SPI_CPOL_High;
|
||||
defaultInit.SPI_CPHA = SPI_CPHA_2Edge;
|
||||
}
|
||||
|
||||
#ifdef STM32F303xC
|
||||
|
@ -98,7 +104,7 @@ void spiInitDevice(SPIDevice device)
|
|||
SPI_RxFIFOThresholdConfig(spi->dev, SPI_RxFIFOThreshold_QF);
|
||||
#endif
|
||||
|
||||
SPI_Init(spi->dev, &spiInit);
|
||||
SPI_Init(spi->dev, &defaultInit);
|
||||
SPI_Cmd(spi->dev, ENABLE);
|
||||
}
|
||||
|
||||
|
@ -158,7 +164,9 @@ bool spiTransfer(SPI_TypeDef *instance, const uint8_t *txData, uint8_t *rxData,
|
|||
#else
|
||||
SPI_I2S_SendData(instance, b);
|
||||
#endif
|
||||
|
||||
spiTimeout = 1000;
|
||||
|
||||
while (SPI_I2S_GetFlagStatus(instance, SPI_I2S_FLAG_RXNE) == RESET) {
|
||||
if ((spiTimeout--) == 0)
|
||||
return spiTimeoutUserCallback(instance);
|
||||
|
@ -175,27 +183,86 @@ bool spiTransfer(SPI_TypeDef *instance, const uint8_t *txData, uint8_t *rxData,
|
|||
return true;
|
||||
}
|
||||
|
||||
void spiSetDivisor(SPI_TypeDef *instance, uint16_t divisor)
|
||||
static uint16_t spiDivisorToBRbits(SPI_TypeDef *instance, uint16_t divisor)
|
||||
{
|
||||
#define BR_BITS ((BIT(5) | BIT(4) | BIT(3)))
|
||||
|
||||
#if !(defined(STM32F1) || defined(STM32F3))
|
||||
// SPI2 and SPI3 are on APB1/AHB1 which PCLK is half that of APB2/AHB2.
|
||||
|
||||
if (instance == SPI2 || instance == SPI3) {
|
||||
divisor /= 2; // Safe for divisor == 0 or 1
|
||||
}
|
||||
#else
|
||||
UNUSED(instance);
|
||||
#endif
|
||||
|
||||
divisor = constrain(divisor, 2, 256);
|
||||
|
||||
SPI_Cmd(instance, DISABLE);
|
||||
return (ffs(divisor) - 2) << 3; // SPI_CR1_BR_Pos
|
||||
}
|
||||
|
||||
static void spiSetDivisorBRreg(SPI_TypeDef *instance, uint16_t divisor)
|
||||
{
|
||||
#define BR_BITS ((BIT(5) | BIT(4) | BIT(3)))
|
||||
const uint16_t tempRegister = (instance->CR1 & ~BR_BITS);
|
||||
instance->CR1 = tempRegister | ((ffs(divisor) - 2) << 3);
|
||||
|
||||
SPI_Cmd(instance, ENABLE);
|
||||
|
||||
instance->CR1 = tempRegister | spiDivisorToBRbits(instance, divisor);
|
||||
#undef BR_BITS
|
||||
}
|
||||
|
||||
void spiSetDivisor(SPI_TypeDef *instance, uint16_t divisor)
|
||||
{
|
||||
SPI_Cmd(instance, DISABLE);
|
||||
spiSetDivisorBRreg(instance, divisor);
|
||||
SPI_Cmd(instance, ENABLE);
|
||||
}
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
|
||||
void spiBusTransactionInit(busDevice_t *bus, SPIMode_e mode, SPIClockDivider_e divider)
|
||||
{
|
||||
switch (mode) {
|
||||
case SPI_MODE0_POL_LOW_EDGE_1ST:
|
||||
defaultInit.SPI_CPOL = SPI_CPOL_Low;
|
||||
defaultInit.SPI_CPHA = SPI_CPHA_1Edge;
|
||||
break;
|
||||
case SPI_MODE1_POL_LOW_EDGE_2ND:
|
||||
defaultInit.SPI_CPOL = SPI_CPOL_Low;
|
||||
defaultInit.SPI_CPHA = SPI_CPHA_2Edge;
|
||||
break;
|
||||
case SPI_MODE2_POL_HIGH_EDGE_1ST:
|
||||
defaultInit.SPI_CPOL = SPI_CPOL_High;
|
||||
defaultInit.SPI_CPHA = SPI_CPHA_1Edge;
|
||||
break;
|
||||
case SPI_MODE3_POL_HIGH_EDGE_2ND:
|
||||
defaultInit.SPI_CPOL = SPI_CPOL_High;
|
||||
defaultInit.SPI_CPHA = SPI_CPHA_2Edge;
|
||||
break;
|
||||
}
|
||||
|
||||
// Initialize the SPI instance to setup CR1
|
||||
|
||||
SPI_Init(bus->busdev_u.spi.instance, &defaultInit);
|
||||
spiSetDivisorBRreg(bus->busdev_u.spi.instance, divider);
|
||||
#ifdef STM32F303xC
|
||||
// Configure for 8-bit reads.
|
||||
SPI_RxFIFOThresholdConfig(bus->busdev_u.spi.instance, SPI_RxFIFOThreshold_QF);
|
||||
#endif
|
||||
|
||||
bus->busdev_u.spi.modeCache = bus->busdev_u.spi.instance->CR1;
|
||||
bus->busdev_u.spi.device = &spiDevice[spiDeviceByInstance(bus->busdev_u.spi.instance)];
|
||||
}
|
||||
|
||||
void spiBusTransactionSetup(const busDevice_t *bus)
|
||||
{
|
||||
// We rely on MSTR bit to detect valid modeCache
|
||||
|
||||
if (bus->busdev_u.spi.modeCache && bus->busdev_u.spi.modeCache != bus->busdev_u.spi.device->cr1SoftCopy) {
|
||||
bus->busdev_u.spi.instance->CR1 = bus->busdev_u.spi.modeCache;
|
||||
bus->busdev_u.spi.device->cr1SoftCopy = bus->busdev_u.spi.modeCache;
|
||||
|
||||
// SCK seems to require some time to switch to a new initial level after CR1 is written.
|
||||
// Here we buy some time in addition to the software copy save above.
|
||||
__asm__("nop");
|
||||
}
|
||||
}
|
||||
#endif // USE_SPI_TRANSACTION
|
||||
#endif
|
||||
|
|
|
@ -366,7 +366,7 @@ static bool ak8963Init(magDev_t *mag)
|
|||
return true;
|
||||
}
|
||||
|
||||
void ak8963BusInit(const busDevice_t *busdev)
|
||||
void ak8963BusInit(busDevice_t *busdev)
|
||||
{
|
||||
switch (busdev->bustype) {
|
||||
#ifdef USE_MAG_AK8963
|
||||
|
@ -380,7 +380,11 @@ void ak8963BusInit(const busDevice_t *busdev)
|
|||
IOHi(busdev->busdev_u.spi.csnPin); // Disable
|
||||
IOInit(busdev->busdev_u.spi.csnPin, OWNER_COMPASS_CS, 0);
|
||||
IOConfigGPIO(busdev->busdev_u.spi.csnPin, IOCFG_OUT_PP);
|
||||
spiSetDivisor(busdev->busdev_u.spi.instance, SPI_CLOCK_STANDARD);
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(busdev, SPI_MODE3_POL_HIGH_EDGE_2ND, SPI_CLOCK_STANDARD);
|
||||
#else
|
||||
spiBusSetDivisor(busdev, SPI_CLOCK_STANDARD);
|
||||
#endif
|
||||
break;
|
||||
#endif
|
||||
|
||||
|
|
|
@ -190,7 +190,11 @@ static void hmc5883SpiInit(busDevice_t *busdev)
|
|||
IOInit(busdev->busdev_u.spi.csnPin, OWNER_COMPASS_CS, 0);
|
||||
IOConfigGPIO(busdev->busdev_u.spi.csnPin, IOCFG_OUT_PP);
|
||||
|
||||
spiSetDivisor(busdev->busdev_u.spi.instance, SPI_CLOCK_STANDARD);
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(busdev, SPI_MODE3_POL_HIGH_EDGE_2ND, SPI_CLOCK_STANDARD);
|
||||
#else
|
||||
spiBusSetDivisor(busdev, SPI_CLOCK_STANDARD);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
|
@ -68,10 +68,14 @@ bool flashInit(const flashConfig_t *flashConfig)
|
|||
IOConfigGPIO(busdev->busdev_u.spi.csnPin, SPI_IO_CS_CFG);
|
||||
IOHi(busdev->busdev_u.spi.csnPin);
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(busdev, SPI_MODE3_POL_HIGH_EDGE_2ND, SPI_CLOCK_FAST);
|
||||
#else
|
||||
#ifndef FLASH_SPI_SHARED
|
||||
//Maximum speed for standard READ command is 20mHz, other commands tolerate 25mHz
|
||||
//spiSetDivisor(busdev->busdev_u.spi.instance, SPI_CLOCK_FAST);
|
||||
spiSetDivisor(busdev->busdev_u.spi.instance, SPI_CLOCK_STANDARD*2);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
flashDevice.busdev = busdev;
|
||||
|
@ -87,7 +91,11 @@ bool flashInit(const flashConfig_t *flashConfig)
|
|||
in[1] = 0;
|
||||
|
||||
// Clearing the CS bit terminates the command early so we don't have to read the chip UID:
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionTransfer(busdev, out, in, sizeof(out));
|
||||
#else
|
||||
spiBusTransfer(busdev, out, in, sizeof(out));
|
||||
#endif
|
||||
|
||||
// Manufacturer, memory type, and capacity
|
||||
uint32_t chipID = (in[1] << 16) | (in[2] << 8) | (in[3]);
|
||||
|
|
|
@ -80,6 +80,7 @@ STATIC_ASSERT(M25P16_PAGESIZE < FLASH_MAX_PAGE_SIZE, M25P16_PAGESIZE_too_small);
|
|||
|
||||
const flashVTable_t m25p16_vTable;
|
||||
|
||||
#ifndef USE_SPI_TRANSACTION
|
||||
static void m25p16_disable(busDevice_t *bus)
|
||||
{
|
||||
IOHi(bus->busdev_u.spi.csnPin);
|
||||
|
@ -91,12 +92,17 @@ static void m25p16_enable(busDevice_t *bus)
|
|||
__NOP();
|
||||
IOLo(bus->busdev_u.spi.csnPin);
|
||||
}
|
||||
#endif
|
||||
|
||||
static void m25p16_transfer(busDevice_t *bus, const uint8_t *txData, uint8_t *rxData, int len)
|
||||
{
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionTransfer(bus, txData, rxData, len);
|
||||
#else
|
||||
m25p16_enable(bus);
|
||||
spiTransfer(bus->busdev_u.spi.instance, txData, rxData, len);
|
||||
m25p16_disable(bus);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -104,11 +110,13 @@ static void m25p16_transfer(busDevice_t *bus, const uint8_t *txData, uint8_t *rx
|
|||
*/
|
||||
static void m25p16_performOneByteCommand(busDevice_t *bus, uint8_t command)
|
||||
{
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
m25p16_transfer(bus, &command, NULL, 1);
|
||||
#else
|
||||
m25p16_enable(bus);
|
||||
|
||||
spiTransferByte(bus->busdev_u.spi.instance, command);
|
||||
|
||||
m25p16_disable(bus);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -269,13 +277,20 @@ static void m25p16_pageProgramContinue(flashDevice_t *fdevice, const uint8_t *da
|
|||
|
||||
m25p16_writeEnable(fdevice);
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionBegin(fdevice->busdev);
|
||||
#else
|
||||
m25p16_enable(fdevice->busdev);
|
||||
#endif
|
||||
|
||||
spiTransfer(fdevice->busdev->busdev_u.spi.instance, command, NULL, fdevice->isLargeFlash ? 5 : 4);
|
||||
|
||||
spiTransfer(fdevice->busdev->busdev_u.spi.instance, data, NULL, length);
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionEnd(fdevice->busdev);
|
||||
#else
|
||||
m25p16_disable(fdevice->busdev);
|
||||
#endif
|
||||
|
||||
fdevice->currentWriteAddress += length;
|
||||
}
|
||||
|
@ -327,12 +342,20 @@ static int m25p16_readBytes(flashDevice_t *fdevice, uint32_t address, uint8_t *b
|
|||
return 0;
|
||||
}
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionBegin(fdevice->busdev);
|
||||
#else
|
||||
m25p16_enable(fdevice->busdev);
|
||||
#endif
|
||||
|
||||
spiTransfer(fdevice->busdev->busdev_u.spi.instance, command, NULL, fdevice->isLargeFlash ? 5 : 4);
|
||||
spiTransfer(fdevice->busdev->busdev_u.spi.instance, NULL, buffer, length);
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionEnd(fdevice->busdev);
|
||||
#else
|
||||
m25p16_disable(fdevice->busdev);
|
||||
#endif
|
||||
|
||||
return length;
|
||||
}
|
||||
|
|
|
@ -69,7 +69,11 @@ static void w25m_dieSelect(busDevice_t *busdev, int die)
|
|||
|
||||
uint8_t command[2] = { W25M_INSTRUCTION_SOFTWARE_DIE_SELECT, die };
|
||||
|
||||
#ifdef SPI_BUS_TRANSACTION
|
||||
spiBusTransactionTransfer(busdev, command, NULL, 2);
|
||||
#else
|
||||
spiBusTransfer(busdev, command, NULL, 2);
|
||||
#endif
|
||||
|
||||
activeDie = die;
|
||||
}
|
||||
|
|
|
@ -174,8 +174,12 @@
|
|||
|
||||
// On shared SPI buss we want to change clock for OSD chip and restore for other devices.
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
#define __spiBusTransactionBegin(busdev) spiBusTransactionBegin(busdev)
|
||||
#define __spiBusTransactionEnd(busdev) spiBusTransactionEnd(busdev)
|
||||
#else
|
||||
#ifdef MAX7456_SPI_CLK
|
||||
#define __spiBusTransactionBegin(busdev) {spiSetDivisor((busdev)->busdev_u.spi.instance, max7456SpiClock);IOLo((busdev)->busdev_u.spi.csnPin);}
|
||||
#define __spiBusTransactionBegin(busdev) {spiBusSetDivisor(busdev, max7456SpiClock);IOLo((busdev)->busdev_u.spi.csnPin);}
|
||||
#else
|
||||
#define __spiBusTransactionBegin(busdev) IOLo((busdev)->busdev_u.spi.csnPin)
|
||||
#endif
|
||||
|
@ -185,6 +189,7 @@
|
|||
#else
|
||||
#define __spiBusTransactionEnd(busdev) IOHi((busdev)->busdev_u.spi.csnPin)
|
||||
#endif
|
||||
#endif
|
||||
|
||||
busDevice_t max7456BusDevice;
|
||||
busDevice_t *busdev = &max7456BusDevice;
|
||||
|
@ -476,7 +481,11 @@ bool max7456Init(const max7456Config_t *max7456Config, const vcdProfile_t *pVcdP
|
|||
UNUSED(cpuOverclock);
|
||||
#endif
|
||||
|
||||
spiSetDivisor(busdev->busdev_u.spi.instance, max7456SpiClock);
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(busdev, SPI_MODE3_POL_HIGH_EDGE_2ND, max7456SpiClock);
|
||||
#else
|
||||
spiBusSetDivisor(busdev, max7456SpiClock);
|
||||
#endif
|
||||
|
||||
// force soft reset on Max7456
|
||||
__spiBusTransactionBegin(busdev);
|
||||
|
|
|
@ -54,11 +54,13 @@ void rxSpiDevicePreInit(const rxSpiConfig_t *rxSpiConfig)
|
|||
|
||||
bool rxSpiDeviceInit(const rxSpiConfig_t *rxSpiConfig)
|
||||
{
|
||||
if (!rxSpiConfig->spibus) {
|
||||
SPI_TypeDef *instance = spiInstanceByDevice(SPI_CFG_TO_DEV(rxSpiConfig->spibus));
|
||||
|
||||
if (!instance) {
|
||||
return false;
|
||||
}
|
||||
|
||||
spiBusSetInstance(busdev, spiInstanceByDevice(SPI_CFG_TO_DEV(rxSpiConfig->spibus)));
|
||||
spiBusSetInstance(busdev, instance);
|
||||
|
||||
const IO_t rxCsPin = IOGetByTag(rxSpiConfig->csnTag);
|
||||
IOInit(rxCsPin, OWNER_RX_SPI_CS, 0);
|
||||
|
@ -66,8 +68,11 @@ bool rxSpiDeviceInit(const rxSpiConfig_t *rxSpiConfig)
|
|||
busdev->busdev_u.spi.csnPin = rxCsPin;
|
||||
|
||||
IOHi(rxCsPin);
|
||||
|
||||
spiSetDivisor(busdev->busdev_u.spi.instance, SPI_CLOCK_STANDARD);
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(busdev, SPI_MODE0_POL_LOW_EDGE_1ST, SPI_CLOCK_STANDARD);
|
||||
#else
|
||||
spiBusSetDivisor(busdev, SPI_CLOCK_STANDARD);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -55,6 +55,9 @@
|
|||
/* Operational speed <= 25MHz */
|
||||
#define SDCARD_SPI_FULL_SPEED_CLOCK_DIVIDER SPI_CLOCK_FAST
|
||||
|
||||
#define SDCARD_SPI_MODE SPI_MODE0_POL_LOW_EDGE_1ST
|
||||
//#define SDCARD_SPI_MODE SPI_MODE3_POL_HIGH_EDGE_2ND
|
||||
|
||||
/* Break up 512-byte SD card sectors into chunks of this size when writing without DMA to reduce the peak overhead
|
||||
* per call to sdcard_poll().
|
||||
*/
|
||||
|
@ -71,7 +74,11 @@ static bool sdcardSpi_isFunctional(void)
|
|||
|
||||
static void sdcard_select(void)
|
||||
{
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionBegin(&sdcard.busdev);
|
||||
#else
|
||||
IOLo(sdcard.busdev.busdev_u.spi.csnPin);
|
||||
#endif
|
||||
}
|
||||
|
||||
static void sdcard_deselect(void)
|
||||
|
@ -82,7 +89,12 @@ static void sdcard_deselect(void)
|
|||
while (spiBusIsBusBusy(&sdcard.busdev)) {
|
||||
}
|
||||
|
||||
delayMicroseconds(10);
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionEnd(&sdcard.busdev);
|
||||
#else
|
||||
IOHi(sdcard.busdev.busdev_u.spi.csnPin);
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -99,7 +111,11 @@ static void sdcard_reset(void)
|
|||
}
|
||||
|
||||
if (sdcard.state >= SDCARD_STATE_READY) {
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(&sdcard.busdev, SDCARD_SPI_MODE, SDCARD_SPI_INITIALIZATION_CLOCK_DIVIDER);
|
||||
#else
|
||||
spiSetDivisor(sdcard.busdev.busdev_u.spi.instance, SDCARD_SPI_INITIALIZATION_CLOCK_DIVIDER);
|
||||
#endif
|
||||
}
|
||||
|
||||
sdcard.failureCount++;
|
||||
|
@ -535,14 +551,18 @@ static void sdcardSpi_init(const sdcardConfig_t *config, const spiPinConfig_t *s
|
|||
}
|
||||
|
||||
// Max frequency is initially 400kHz
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(&sdcard.busdev, SDCARD_SPI_MODE, SDCARD_SPI_INITIALIZATION_CLOCK_DIVIDER);
|
||||
#else
|
||||
spiSetDivisor(sdcard.busdev.busdev_u.spi.instance, SDCARD_SPI_INITIALIZATION_CLOCK_DIVIDER);
|
||||
#endif
|
||||
|
||||
// SDCard wants 1ms minimum delay after power is applied to it
|
||||
delay(1000);
|
||||
|
||||
// Transmit at least 74 dummy clock cycles with CS high so the SD card can start up
|
||||
IOHi(sdcard.busdev.busdev_u.spi.csnPin);
|
||||
|
||||
spiBusRawTransfer(&sdcard.busdev, NULL, NULL, SDCARD_INIT_NUM_DUMMY_BYTES);
|
||||
|
||||
// Wait for that transmission to finish before we enable the SDCard, so it receives the required number of cycles:
|
||||
|
@ -700,7 +720,12 @@ static bool sdcardSpi_poll(void)
|
|||
}
|
||||
|
||||
// Now we're done with init and we can switch to the full speed clock (<25MHz)
|
||||
|
||||
#ifdef USE_SPI_TRANSACTION
|
||||
spiBusTransactionInit(&sdcard.busdev, SDCARD_SPI_MODE, SDCARD_SPI_FULL_SPEED_CLOCK_DIVIDER);
|
||||
#else
|
||||
spiSetDivisor(sdcard.busdev.busdev_u.spi.instance, SDCARD_SPI_FULL_SPEED_CLOCK_DIVIDER);
|
||||
#endif
|
||||
|
||||
sdcard.multiWriteBlocksRemain = 0;
|
||||
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
|
||||
#if defined(STM32F40_41xxx) || defined(STM32F411xE)
|
||||
#define USE_OVERCLOCK
|
||||
#define USE_SPI_TRANSACTION
|
||||
#endif
|
||||
|
||||
#endif // STM32F4
|
||||
|
@ -83,6 +84,7 @@
|
|||
#define USE_PERSISTENT_MSC_RTC
|
||||
#define USE_MCO
|
||||
#define USE_DMA_SPEC
|
||||
#define USE_SPI_TRANSACTION
|
||||
#endif // STM32F7
|
||||
|
||||
#if defined(STM32F4) || defined(STM32F7)
|
||||
|
|
|
@ -147,7 +147,10 @@ void delay(uint32_t) {}
|
|||
bool busReadRegisterBuffer(const busDevice_t*, uint8_t, uint8_t*, uint8_t) {return true;}
|
||||
bool busWriteRegister(const busDevice_t*, uint8_t, uint8_t) {return true;}
|
||||
|
||||
void spiSetDivisor() {
|
||||
void spiBusSetDivisor() {
|
||||
}
|
||||
|
||||
void spiBusTransactionInit() {
|
||||
}
|
||||
|
||||
void spiPreinitByIO() {
|
||||
|
|
|
@ -149,7 +149,7 @@ void delayMicroseconds(uint32_t) {}
|
|||
bool busReadRegisterBuffer(const busDevice_t*, uint8_t, uint8_t*, uint8_t) {return true;}
|
||||
bool busWriteRegister(const busDevice_t*, uint8_t, uint8_t) {return true;}
|
||||
|
||||
void spiSetDivisor() {
|
||||
void spiBusSetDivisor() {
|
||||
}
|
||||
|
||||
void spiPreinitByIO() {
|
||||
|
|
Loading…
Reference in New Issue