mirror of https://github.com/ARMmbed/mbed-os.git
SPI upgrade - per-peripheral mutex and GPIO-based SSEL
This commit takes some of the work done on the SPI class from #8445, and refines it, to provide the per-peripheral mutex functionality. This also implements GPIO-based SSEL, which exposes a new select()/deselect() API for users to group transfers, and should work on every platform (unlike the HAL-based SSEL). This requires users to use a new constructor to avoid backwards compatibility issues. To activate the per-peripheral mutex, the HAL must define SPI_COUNT and provide spi_get_peripheral_name(). (In #8445 this is a reworked spi_get_module, but the name is changed here to avoid a collision with existing HALs - this commit is designed to work without wider HAL changes). Fixes: #9149pull/9469/head
parent
857cd9fba1
commit
5e059b7d1d
189
drivers/SPI.cpp
189
drivers/SPI.cpp
|
@ -23,33 +23,99 @@
|
|||
|
||||
#if DEVICE_SPI
|
||||
|
||||
/* Backwards compatibility with HALs that don't provide this */
|
||||
MBED_WEAK SPIName spi_get_peripheral_name(PinName /*mosi*/, PinName /*miso*/, PinName /*mclk*/)
|
||||
{
|
||||
return (SPIName)1;
|
||||
}
|
||||
|
||||
namespace mbed {
|
||||
|
||||
#if DEVICE_SPI_ASYNCH && TRANSACTION_QUEUE_SIZE_SPI
|
||||
CircularBuffer<Transaction<SPI>, TRANSACTION_QUEUE_SIZE_SPI> SPI::_transaction_buffer;
|
||||
#endif
|
||||
SPI::spi_peripheral_s SPI::_peripherals[SPI_PERIPHERALS_USED];
|
||||
int SPI::_peripherals_used;
|
||||
|
||||
SPI::SPI(PinName mosi, PinName miso, PinName sclk, PinName ssel) :
|
||||
_spi(),
|
||||
#if DEVICE_SPI_ASYNCH
|
||||
_irq(this),
|
||||
_usage(DMA_USAGE_NEVER),
|
||||
_deep_sleep_locked(false),
|
||||
#endif
|
||||
_bits(8),
|
||||
_mode(0),
|
||||
_hz(1000000),
|
||||
_write_fill(SPI_FILL_CHAR)
|
||||
_mosi(mosi),
|
||||
_miso(miso),
|
||||
_sclk(sclk),
|
||||
_hw_ssel(ssel),
|
||||
_sw_ssel(NC)
|
||||
{
|
||||
_do_construct();
|
||||
}
|
||||
|
||||
SPI::SPI(PinName mosi, PinName miso, PinName sclk, PinName ssel, use_gpio_ssel_t) :
|
||||
#if DEVICE_SPI_ASYNCH
|
||||
_irq(this),
|
||||
#endif
|
||||
_mosi(mosi),
|
||||
_miso(miso),
|
||||
_sclk(sclk),
|
||||
_hw_ssel(NC),
|
||||
_sw_ssel(ssel, 1)
|
||||
{
|
||||
_do_construct();
|
||||
}
|
||||
|
||||
void SPI::_do_construct()
|
||||
{
|
||||
// No lock needed in the constructor
|
||||
spi_init(&_spi, mosi, miso, sclk, ssel);
|
||||
#if DEVICE_SPI_ASYNCH
|
||||
_usage = DMA_USAGE_NEVER;
|
||||
_deep_sleep_locked = false;
|
||||
#endif
|
||||
_select_count = 0;
|
||||
_bits = 8;
|
||||
_mode = 0;
|
||||
_hz = 1000000;
|
||||
_write_fill = SPI_FILL_CHAR;
|
||||
|
||||
SPIName name = spi_get_peripheral_name(_mosi, _miso, _sclk);
|
||||
|
||||
core_util_critical_section_enter();
|
||||
// lookup in a critical section if we already have it else initialize it
|
||||
|
||||
_peripheral = SPI::_lookup(name);
|
||||
if (!_peripheral) {
|
||||
_peripheral = SPI::_alloc();
|
||||
_peripheral->name = name;
|
||||
}
|
||||
core_util_critical_section_exit();
|
||||
// we don't need to _acquire at this stage.
|
||||
// this will be done anyway before any operation.
|
||||
}
|
||||
|
||||
SPI::~SPI()
|
||||
{
|
||||
if (_owner == this) {
|
||||
_owner = NULL;
|
||||
lock();
|
||||
/* Make sure a stale pointer isn't left in peripheral's owner field */
|
||||
if (_peripheral->owner == this) {
|
||||
_peripheral->owner = NULL;
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
SPI::spi_peripheral_s *SPI::_lookup(SPIName name)
|
||||
{
|
||||
SPI::spi_peripheral_s *result = NULL;
|
||||
core_util_critical_section_enter();
|
||||
for (int idx = 0; idx < _peripherals_used; idx++) {
|
||||
if (_peripherals[idx].name == name) {
|
||||
result = &_peripherals[idx];
|
||||
break;
|
||||
}
|
||||
}
|
||||
core_util_critical_section_exit();
|
||||
return result;
|
||||
}
|
||||
|
||||
SPI::spi_peripheral_s *SPI::_alloc()
|
||||
{
|
||||
MBED_ASSERT(_peripherals_used < SPI_PERIPHERALS_USED);
|
||||
return &_peripherals[_peripherals_used++];
|
||||
}
|
||||
|
||||
void SPI::format(int bits, int mode)
|
||||
|
@ -60,8 +126,8 @@ void SPI::format(int bits, int mode)
|
|||
// If changing format while you are the owner then just
|
||||
// update format, but if owner is changed then even frequency should be
|
||||
// updated which is done by acquire.
|
||||
if (_owner == this) {
|
||||
spi_format(&_spi, _bits, _mode, 0);
|
||||
if (_peripheral->owner == this) {
|
||||
spi_format(&_peripheral->spi, _bits, _mode, 0);
|
||||
} else {
|
||||
_acquire();
|
||||
}
|
||||
|
@ -75,69 +141,78 @@ void SPI::frequency(int hz)
|
|||
// If changing format while you are the owner then just
|
||||
// update frequency, but if owner is changed then even frequency should be
|
||||
// updated which is done by acquire.
|
||||
if (_owner == this) {
|
||||
spi_frequency(&_spi, _hz);
|
||||
if (_peripheral->owner == this) {
|
||||
spi_frequency(&_peripheral->spi, _hz);
|
||||
} else {
|
||||
_acquire();
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
SPI *SPI::_owner = NULL;
|
||||
SingletonPtr<PlatformMutex> SPI::_mutex;
|
||||
|
||||
// ignore the fact there are multiple physical spis, and always update if it wasn't us last
|
||||
void SPI::aquire()
|
||||
{
|
||||
lock();
|
||||
if (_owner != this) {
|
||||
spi_format(&_spi, _bits, _mode, 0);
|
||||
spi_frequency(&_spi, _hz);
|
||||
_owner = this;
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
// Note: Private function with no locking
|
||||
void SPI::_acquire()
|
||||
{
|
||||
if (_owner != this) {
|
||||
spi_format(&_spi, _bits, _mode, 0);
|
||||
spi_frequency(&_spi, _hz);
|
||||
_owner = this;
|
||||
if (_peripheral->owner != this) {
|
||||
spi_init(&_peripheral->spi, _mosi, _miso, _sclk, _hw_ssel);
|
||||
spi_format(&_peripheral->spi, _bits, _mode, 0);
|
||||
spi_frequency(&_peripheral->spi, _hz);
|
||||
_peripheral->owner = this;
|
||||
}
|
||||
}
|
||||
|
||||
int SPI::write(int value)
|
||||
{
|
||||
lock();
|
||||
_acquire();
|
||||
int ret = spi_master_write(&_spi, value);
|
||||
unlock();
|
||||
select();
|
||||
int ret = spi_master_write(&_peripheral->spi, value);
|
||||
deselect();
|
||||
return ret;
|
||||
}
|
||||
|
||||
int SPI::write(const char *tx_buffer, int tx_length, char *rx_buffer, int rx_length)
|
||||
{
|
||||
lock();
|
||||
_acquire();
|
||||
int ret = spi_master_block_write(&_spi, tx_buffer, tx_length, rx_buffer, rx_length, _write_fill);
|
||||
unlock();
|
||||
select();
|
||||
int ret = spi_master_block_write(&_peripheral->spi, tx_buffer, tx_length, rx_buffer, rx_length, _write_fill);
|
||||
deselect();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void SPI::_set_ssel(int val)
|
||||
{
|
||||
if (_sw_ssel.is_connected()) {
|
||||
_sw_ssel = val;
|
||||
}
|
||||
}
|
||||
|
||||
void SPI::lock()
|
||||
{
|
||||
_mutex->lock();
|
||||
_peripheral->mutex->lock();
|
||||
}
|
||||
|
||||
void SPI::select()
|
||||
{
|
||||
lock();
|
||||
if (_select_count++ == 0) {
|
||||
_acquire();
|
||||
_set_ssel(0);
|
||||
}
|
||||
}
|
||||
|
||||
void SPI::unlock()
|
||||
{
|
||||
_mutex->unlock();
|
||||
_peripheral->mutex->unlock();
|
||||
}
|
||||
|
||||
void SPI::deselect()
|
||||
{
|
||||
if (--_select_count == 0) {
|
||||
_set_ssel(1);
|
||||
}
|
||||
unlock();
|
||||
}
|
||||
|
||||
void SPI::set_default_write_value(char data)
|
||||
{
|
||||
// this does not actually need to lock the peripheral.
|
||||
lock();
|
||||
_write_fill = data;
|
||||
unlock();
|
||||
|
@ -147,7 +222,7 @@ void SPI::set_default_write_value(char data)
|
|||
|
||||
int SPI::transfer(const void *tx_buffer, int tx_length, void *rx_buffer, int rx_length, unsigned char bit_width, const event_callback_t &callback, int event)
|
||||
{
|
||||
if (spi_active(&_spi)) {
|
||||
if (spi_active(&_peripheral->spi)) {
|
||||
return queue_transfer(tx_buffer, tx_length, rx_buffer, rx_length, bit_width, callback, event);
|
||||
}
|
||||
start_transfer(tx_buffer, tx_length, rx_buffer, rx_length, bit_width, callback, event);
|
||||
|
@ -156,7 +231,7 @@ int SPI::transfer(const void *tx_buffer, int tx_length, void *rx_buffer, int rx_
|
|||
|
||||
void SPI::abort_transfer()
|
||||
{
|
||||
spi_abort_asynch(&_spi);
|
||||
spi_abort_asynch(&_peripheral->spi);
|
||||
unlock_deep_sleep();
|
||||
#if TRANSACTION_QUEUE_SIZE_SPI
|
||||
dequeue_transaction();
|
||||
|
@ -167,7 +242,7 @@ void SPI::abort_transfer()
|
|||
void SPI::clear_transfer_buffer()
|
||||
{
|
||||
#if TRANSACTION_QUEUE_SIZE_SPI
|
||||
_transaction_buffer.reset();
|
||||
_peripheral->transaction_buffer->reset();
|
||||
#endif
|
||||
}
|
||||
|
||||
|
@ -179,7 +254,7 @@ void SPI::abort_all_transfers()
|
|||
|
||||
int SPI::set_dma_usage(DMAUsage usage)
|
||||
{
|
||||
if (spi_active(&_spi)) {
|
||||
if (spi_active(&_peripheral->spi)) {
|
||||
return -1;
|
||||
}
|
||||
_usage = usage;
|
||||
|
@ -199,12 +274,12 @@ int SPI::queue_transfer(const void *tx_buffer, int tx_length, void *rx_buffer, i
|
|||
t.callback = callback;
|
||||
t.width = bit_width;
|
||||
Transaction<SPI> transaction(this, t);
|
||||
if (_transaction_buffer.full()) {
|
||||
if (_peripheral->transaction_buffer->full()) {
|
||||
return -1; // the buffer is full
|
||||
} else {
|
||||
core_util_critical_section_enter();
|
||||
_transaction_buffer.push(transaction);
|
||||
if (!spi_active(&_spi)) {
|
||||
_peripheral->transaction_buffer->push(transaction);
|
||||
if (!spi_active(&_peripheral->spi)) {
|
||||
dequeue_transaction();
|
||||
}
|
||||
core_util_critical_section_exit();
|
||||
|
@ -219,9 +294,10 @@ void SPI::start_transfer(const void *tx_buffer, int tx_length, void *rx_buffer,
|
|||
{
|
||||
lock_deep_sleep();
|
||||
_acquire();
|
||||
_set_ssel(0);
|
||||
_callback = callback;
|
||||
_irq.callback(&SPI::irq_handler_asynch);
|
||||
spi_master_transfer(&_spi, tx_buffer, tx_length, rx_buffer, rx_length, bit_width, _irq.entry(), event, _usage);
|
||||
spi_master_transfer(&_peripheral->spi, tx_buffer, tx_length, rx_buffer, rx_length, bit_width, _irq.entry(), event, _usage);
|
||||
}
|
||||
|
||||
void SPI::lock_deep_sleep()
|
||||
|
@ -250,7 +326,7 @@ void SPI::start_transaction(transaction_t *data)
|
|||
void SPI::dequeue_transaction()
|
||||
{
|
||||
Transaction<SPI> t;
|
||||
if (_transaction_buffer.pop(t)) {
|
||||
if (_peripheral->transaction_buffer->pop(t)) {
|
||||
SPI *obj = t.get_object();
|
||||
transaction_t *data = t.get_transaction();
|
||||
obj->start_transaction(data);
|
||||
|
@ -261,8 +337,9 @@ void SPI::dequeue_transaction()
|
|||
|
||||
void SPI::irq_handler_asynch(void)
|
||||
{
|
||||
int event = spi_irq_handler_asynch(&_spi);
|
||||
int event = spi_irq_handler_asynch(&_peripheral->spi);
|
||||
if (_callback && (event & SPI_EVENT_ALL)) {
|
||||
_set_ssel(1);
|
||||
unlock_deep_sleep();
|
||||
_callback.call(event & SPI_EVENT_ALL);
|
||||
}
|
||||
|
|
119
drivers/SPI.h
119
drivers/SPI.h
|
@ -23,9 +23,21 @@
|
|||
|
||||
#include "platform/PlatformMutex.h"
|
||||
#include "hal/spi_api.h"
|
||||
#include "drivers/DigitalOut.h"
|
||||
#include "platform/SingletonPtr.h"
|
||||
#include "platform/NonCopyable.h"
|
||||
|
||||
/* Backwards compatibility with HALs not providing this */
|
||||
#ifndef SPI_COUNT
|
||||
#define SPI_COUNT 1
|
||||
#endif
|
||||
|
||||
#if defined MBED_CONF_DRIVERS_SPI_COUNT_MAX && SPI_COUNT > MBED_CONF_DRIVERS_SPI_COUNT_MAX
|
||||
#define SPI_PERIPHERALS_USED MBED_CONF_DRIVERS_SPI_COUNT_MAX
|
||||
#else
|
||||
#define SPI_PERIPHERALS_USED SPI_COUNT
|
||||
#endif
|
||||
|
||||
#if DEVICE_SPI_ASYNCH
|
||||
#include "platform/CThunk.h"
|
||||
#include "hal/dma_api.h"
|
||||
|
@ -37,6 +49,9 @@
|
|||
namespace mbed {
|
||||
/** \addtogroup drivers */
|
||||
|
||||
struct use_gpio_ssel_t { };
|
||||
const use_gpio_ssel_t use_gpio_ssel;
|
||||
|
||||
/** A SPI Master, used for communicating with SPI slave devices.
|
||||
*
|
||||
* The default format is set to 8-bits, mode 0, and a clock frequency of 1MHz.
|
||||
|
@ -84,6 +99,11 @@ class SPI : private NonCopyable<SPI> {
|
|||
public:
|
||||
|
||||
/** Create a SPI master connected to the specified pins.
|
||||
*
|
||||
* @note This constructor passes the SSEL pin selection to the target HAL.
|
||||
* Not all targets support SSEL, so this cannot be relied on in portable code.
|
||||
* Portable code should use the alternative constructor that uses GPIO
|
||||
* for SSEL.
|
||||
*
|
||||
* @note You can specify mosi or miso as NC if not used.
|
||||
*
|
||||
|
@ -93,6 +113,23 @@ public:
|
|||
* @param ssel SPI Chip Select pin.
|
||||
*/
|
||||
SPI(PinName mosi, PinName miso, PinName sclk, PinName ssel = NC);
|
||||
|
||||
/** Create a SPI master connected to the specified pins.
|
||||
*
|
||||
* @note This constructor manipulates the SSEL pin as a GPIO output
|
||||
* using a DigitalOut object. This should work on any target, and permits
|
||||
* the use of select() and deselect() methods to keep the pin asserted
|
||||
* between transfers.
|
||||
*
|
||||
* @note You can specify mosi or miso as NC if not used.
|
||||
*
|
||||
* @param mosi SPI Master Out, Slave In pin.
|
||||
* @param miso SPI Master In, Slave Out pin.
|
||||
* @param sclk SPI Clock pin.
|
||||
* @param ssel SPI Chip Select pin.
|
||||
*/
|
||||
SPI(PinName mosi, PinName miso, PinName sclk, PinName ssel, use_gpio_ssel_t);
|
||||
|
||||
virtual ~SPI();
|
||||
|
||||
/** Configure the data transmission format.
|
||||
|
@ -149,6 +186,17 @@ public:
|
|||
*/
|
||||
virtual void unlock(void);
|
||||
|
||||
/** Assert the Slave Select line, acquiring exclusive access to this SPI bus.
|
||||
*
|
||||
* If use_gpio_ssel was not passed to the constructor, this only acquires
|
||||
* exclusive access; it cannot assert the Slave Select line.
|
||||
*/
|
||||
void select(void);
|
||||
|
||||
/** Deassert the Slave Select line, releasing exclusive access to this SPI bus.
|
||||
*/
|
||||
void deselect(void);
|
||||
|
||||
/** Set default write data.
|
||||
* SPI requires the master to send some data during a read operation.
|
||||
* Different devices may require different default byte values.
|
||||
|
@ -180,7 +228,7 @@ public:
|
|||
template<typename Type>
|
||||
int transfer(const Type *tx_buffer, int tx_length, Type *rx_buffer, int rx_length, const event_callback_t &callback, int event = SPI_EVENT_COMPLETE)
|
||||
{
|
||||
if (spi_active(&_spi)) {
|
||||
if (spi_active(&_peripheral->spi)) {
|
||||
return queue_transfer(tx_buffer, tx_length, rx_buffer, rx_length, sizeof(Type) * 8, callback, event);
|
||||
}
|
||||
start_transfer(tx_buffer, tx_length, rx_buffer, rx_length, sizeof(Type) * 8, callback, event);
|
||||
|
@ -274,7 +322,6 @@ private:
|
|||
|
||||
|
||||
#if TRANSACTION_QUEUE_SIZE_SPI
|
||||
|
||||
/** Start a new transaction.
|
||||
*
|
||||
* @param data Transaction data.
|
||||
|
@ -285,18 +332,35 @@ private:
|
|||
*/
|
||||
void dequeue_transaction();
|
||||
|
||||
/* Queue of pending transfers */
|
||||
static CircularBuffer<Transaction<SPI>, TRANSACTION_QUEUE_SIZE_SPI> _transaction_buffer;
|
||||
#endif
|
||||
|
||||
#endif //!defined(DOXYGEN_ONLY)
|
||||
|
||||
#endif //DEVICE_SPI_ASYNCH
|
||||
#endif // TRANSACTION_QUEUE_SIZE_SPI
|
||||
#endif // !defined(DOXYGEN_ONLY)
|
||||
#endif // DEVICE_SPI_ASYNCH
|
||||
|
||||
#if !defined(DOXYGEN_ONLY)
|
||||
protected:
|
||||
/* Internal SPI object identifying the resources */
|
||||
spi_t _spi;
|
||||
struct spi_peripheral_s {
|
||||
/* Internal SPI name identifying the resources. */
|
||||
SPIName name;
|
||||
/* Internal SPI object handling the resources' state. */
|
||||
spi_t spi;
|
||||
/* Used by lock and unlock for thread safety */
|
||||
SingletonPtr<PlatformMutex> mutex;
|
||||
/* Current user of the SPI */
|
||||
SPI *owner;
|
||||
#if DEVICE_SPI_ASYNCH && TRANSACTION_QUEUE_SIZE_SPI
|
||||
/* Queue of pending transfers */
|
||||
SingletonPtr<CircularBuffer<Transaction<SPI>, TRANSACTION_QUEUE_SIZE_SPI> > transaction_buffer;
|
||||
#endif
|
||||
};
|
||||
|
||||
// holds spi_peripheral_s per peripheral on the device.
|
||||
// Drawback: it costs ram size even if the device is not used, however
|
||||
// application can limit the allocation via JSON.
|
||||
static spi_peripheral_s _peripherals[SPI_PERIPHERALS_USED];
|
||||
static int _peripherals_used;
|
||||
|
||||
// Holds the reference to the associated peripheral.
|
||||
spi_peripheral_s *_peripheral;
|
||||
|
||||
#if DEVICE_SPI_ASYNCH
|
||||
/* Interrupt */
|
||||
|
@ -307,14 +371,17 @@ protected:
|
|||
DMAUsage _usage;
|
||||
/* Current sate of the sleep manager */
|
||||
bool _deep_sleep_locked;
|
||||
#endif
|
||||
#endif // DEVICE_SPI_ASYNCH
|
||||
|
||||
// Configuration.
|
||||
PinName _mosi;
|
||||
PinName _miso;
|
||||
PinName _sclk;
|
||||
PinName _hw_ssel;
|
||||
|
||||
// The Slave Select GPIO if we're doing it ourselves.
|
||||
DigitalOut _sw_ssel;
|
||||
|
||||
/* Take over the physical SPI and apply our settings (thread safe) */
|
||||
void aquire(void);
|
||||
/* Current user of the SPI */
|
||||
static SPI *_owner;
|
||||
/* Used by lock and unlock for thread safety */
|
||||
static SingletonPtr<PlatformMutex> _mutex;
|
||||
/* Size of the SPI frame */
|
||||
int _bits;
|
||||
/* Clock polairy and phase */
|
||||
|
@ -323,18 +390,30 @@ protected:
|
|||
int _hz;
|
||||
/* Default character used for NULL transfers */
|
||||
char _write_fill;
|
||||
/* Select count to handle re-entrant selection */
|
||||
int8_t _select_count;
|
||||
|
||||
private:
|
||||
void _do_construct();
|
||||
|
||||
/** Private acquire function without locking/unlocking.
|
||||
* Implemented in order to avoid duplicate locking and boost performance.
|
||||
*/
|
||||
void _acquire(void);
|
||||
void _set_ssel(int);
|
||||
|
||||
/** Private lookup in the static _peripherals table.
|
||||
*/
|
||||
static spi_peripheral_s *_lookup(SPIName name);
|
||||
/** Allocate an entry in the static _peripherals table.
|
||||
*/
|
||||
static spi_peripheral_s *_alloc();
|
||||
|
||||
#endif //!defined(DOXYGEN_ONLY)
|
||||
};
|
||||
|
||||
} // namespace mbed
|
||||
|
||||
#endif
|
||||
#endif // DEVICE_SPI || DOXYGEN_ONLY
|
||||
|
||||
#endif
|
||||
#endif // MBED_SPI_H
|
||||
|
|
|
@ -8,6 +8,10 @@
|
|||
"uart-serial-rxbuf-size": {
|
||||
"help": "Default RX buffer size for a UARTSerial instance (unit Bytes))",
|
||||
"value": 256
|
||||
},
|
||||
"spi_count_max": {
|
||||
"help": "The maximum number of SPI peripherals used at the same time. Determines RAM allocated for SPI peripheral management. If null, limit determined by hardware.",
|
||||
"value": null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,15 @@ extern "C" {
|
|||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a variant of the SPIName enum uniquely identifying a SPI peripheral of the device.
|
||||
* @param[in] mosi The pin to use for MOSI
|
||||
* @param[in] miso The pin to use for MISO
|
||||
* @param[in] sclk The pin to use for SCLK
|
||||
* @return An SPI peripheral identifier
|
||||
*/
|
||||
SPIName spi_get_peripheral_name(PinName mosi, PinName miso, PinName mclk);
|
||||
|
||||
/** Initialize the SPI peripheral
|
||||
*
|
||||
* Configures the pins used by SPI, sets a default format and frequency, and enables the peripheral
|
||||
|
|
Loading…
Reference in New Issue