Skip to content

Commit

Permalink
Polish up SPI docs
Browse files Browse the repository at this point in the history
  • Loading branch information
multiplemonomials committed Jun 20, 2023
1 parent 132c1ab commit cef0d2f
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 23 deletions.
149 changes: 132 additions & 17 deletions drivers/include/drivers/SPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ const use_gpio_ssel_t use_gpio_ssel;
*
* @note Synchronization level: Thread safe
*
* %SPI allows you to send bytes to slave devices using single-word writes, a transaction, or an asynchronous API.
* %SPI allows you to transfer data to and from peripheral devices using single-word transfers, transactions,
* or an asynchronous API.
*
* Here's how to talk to a chip using the single-word API:
* @code{.cpp}
Expand All @@ -72,6 +73,7 @@ const use_gpio_ssel_t use_gpio_ssel;
*
* int main() {
* device.format(8, 0);
*
* device.lock();
* device.select();
* int response = device.write(0x0A);
Expand All @@ -88,17 +90,44 @@ const use_gpio_ssel_t use_gpio_ssel;
* SPI device(SPI_MOSI, SPI_MISO, SPI_SCLK, SPI_CS, use_gpio_ssel)
*
* int main() {
* device.format(8, 0);
*
* uint8_t command[2] = {0x0A, 0x0B};
* uint8_t response[2];
* int result = device.write(command, sizeof(command), response, sizeof(response));
* }
* @endcode
*
* <h1>Hardware vs Software Chip Selects</h1>
* <p>Most ARM chips have specific pins marked as "hardware chip selects". This means that they are connected
* to the %SPI peripheral and are automatically brought low by hardware when data is sent. However, chips
* often only have only one pin for each SPI bus that can be used as a hardware chip select. If you wish to
* use multiple peripheral devices with one SPI bus, or just don't have access to the HW CS pin, you must use the
* %SPI object in "GPIO chip select" mode.</p>
*
* <p>To use GPIO CS mode, simply pass the CS pin as the 4th constructor parameter, then pass the
* special constant \c use_gpio_ssel as the 5th parameter. This puts the object in GPIO mode, where
* it will operate the CS line as a regular GPIO pin before and after doing an SPI transfer. This mode
* is marginally slower than HW CS mode, but otherwise should offer the same functionality. In fact,
* since the pin mapping is more flexible, it may be advisable to use GPIO CS mode by default.</p>
*
* \warning It is not recommended to control the CS line using a separate DigitalOut instance!
* This was needed in very old Mbed versions but should now be considered a legacy practice.
* Not only does it make it difficult to use the asynchronous API, but it can also lead to corner
* cases which corrupt data. The CS line, whether done through GPIO or HW, should always be managed through
* the SPI class.
*
* <h1>Sharing a Bus</h1>
* <p>Multiple %SPI devices may share the same physical bus, so long as each has its own dedicated chip select
* (CS) pin. To implement this sharing, each chip should create its own instance of the SPI class, passing
* the same MOSI, MISO, and SCLK pins but a different CS pin. Mbed OS will internally share the %SPI hardware
* between these objects. Note that this is <i>completely different</i> from how the I2C class handles
* sharing.</p>
*
* <h1>Frame/Word Size</h1>
* <p>Mbed OS supports configuration of the SPI frame size, also known as the word size, which controls how
* <p>Mbed OS supports configuration of the %SPI frame size, also known as the word size, which controls how
* many bits are sent in one transfer operation (one call to SPI::write()). This parameter should match
* the "register size" of the SPI peripheral that you are talking to. For example, if you're working with a chip
* the "register size" of the %SPI peripheral that you are talking to. For example, if you're working with a chip
* which uses 16-bit registers, you should set the frame size to 16 using SPI::format(). Some Mbed devices also
* support 32-bit frames -- use the \c DEVICE_SPI_32BIT_WORDS feature macro to test if yours does.</p>
*
Expand All @@ -111,14 +140,14 @@ const use_gpio_ssel_t use_gpio_ssel;
* <p>It should be noted that changing the frame size can perform an apparent "endian swap" on data being
* transmitted. For example, suppose you have the 32-bit integer 0x01020408. On a little-endian processor,
* this will be encoded with the LSByte <i>first</i> in memory: <tt>08 04 02 01</tt>. If you send that
* integer using one-byte word size, it will appear as such:</p>
* integer using one-byte word size, it will appear as such. Consider the following example:</p>
* @code{.cpp}
* spi.format(8, 0);
* uint32_t myInteger = 0x01020408;
* spi.write(reinterpret_cast<const char *>(&myInteger), sizeof(myInteger), nullptr, 0);
* @endcode
*
* <p>If you used a logic analyzer to view the bytes on the MOSI line, then ran this code, it will show bytes
* <p>If you ran this code, then used a logic analyzer to view the bytes on the MOSI line, it would show bytes
* matching the layout of the integer in memory:</p>
* <pre>
* MOSI -> 08 04 02 01
Expand All @@ -132,19 +161,99 @@ const use_gpio_ssel_t use_gpio_ssel;
* @endcode
*
* <p>In this case, the complete 32-bit integer is sent as a single unit, from its MSBit to its LSBit, without
* breaking it into bytes. This will send the data in an order different from the order in memory:</p>
* breaking it into bytes. This will send the data in an order different from the order in memory. Viewed as
* bytes, it would look like:</p>
*
* <pre>
* MOSI -> 01 02 04 08
* </pre>
*
* <p>In effect, it's almost as if you did an endian swap on the data before sending it, but in fact it's standard
* SPI behavior when using frame sizes greater than one byte. This same rule applies to receiving data,
* so be sure to check examples in the datasheet to determine what frame size to use and whether byte
* swapping is needed when working with an external chip.</p>
* <p>But viewed by a peripheral chip which uses 32-bit SPI words, it would look like:</p>
*
* <pre>
* MOSI -> 0x01020408
* </pre>
*
* <p>When viewed as bytes, it's almost as if you did an endian swap on the data before sending it,
* but in fact it's standard %SPI behavior when using frame sizes greater than one byte. This same rule
* applies to receiving data, so be sure to check examples in the datasheet to determine what frame
* size to use and whether byte swapping is needed when working with an external chip.</p>
*
* <p>Note: Some Mbed targets support frame sizes that are not standard integer sizes, e.g. 4 bits, 7 bits, or
* 24 bits. However, the behavior of these frame sizes is not well defined and may differ across targets
* or across API calls (e.g. single-byte vs transaction vs async). More work is needed to make these consistent.</p>
*
* <h1>Asynchronous API</h1>
* <p>On many processors, Mbed OS also supports asynchronous %SPI. This feature allows you to run %SPI
* transfers completely in the background, while other threads execute in the foreground. This can
* be extremely useful if you need to send large amounts of data over the %SPI bus but don't want to
* block your main thread for ages. To see if your processor supports async %SPI, look for the
* DEVICE_SPI_ASYNCH macro.</p>
*
* <p>The asynchronous API has two different modes: nonblocking (where your thread can keep running, and
* the transfer calls a callback when finished) and blocking (where your thread blocks for the duration of
* the transfer but others can execute). Here's a sample of how to send the same data as above
* using the blocking async API:</p>
*
* @code
* #include "mbed.h"
*
* SPI device(SPI_MOSI, SPI_MISO, SPI_SCLK, SPI_CS, use_gpio_ssel)
*
* int main() {
* device.format(8, 0);
*
* uint8_t command[2] = {0x0A, 0x0B};
* uint8_t response[2];
* int result = device.transfer_and_wait(command, sizeof(command), response, sizeof(response));
* }
* @endcode
*
* <p>This code will cause the data in \c command to be sent to the device and the response to be received into
* \c response . During the transfer, the current thread is paused, but other threads can execute.
* The non-blocking API does not pause the current thread, but is a bit more complicated to use.
* See the SPI::transfer_and_wait() implementation in the header file for an example.</p>
*
* <h3>Async: DMA vs Interrupts</h3>
* <p>Some processors only provide asynchronous %SPI via interrupts, some only support DMA, and some offer both.
* Using interrupts is simpler and generally doesn't require additional resources from the chip, however,
* some CPU time will be spent servicing the interrupt while the transfer is running. This can become very
* performance-intensive -- on some chips, running async %SPI at frequencies of just a few MHz can be enough
* to make the interrupt use 100% of CPU time. In contrast, DMA can require additional resources to be allocated,
* (e.g. chips with few DMA channels might only support DMA on one or two %SPI busses at a time), but
* can run the bus at full speed without any CPU overhead. Generally, DMA should be preferred if available,
* especially if medium to fast bus speeds are needed.</p>
*
* <p>Consult your chip documentation and the Mbed port docs for your target to find out what is needed to enable
* DMA support. For example, for STMicro targets, see
* <a href="https://github.com/mbed-ce/mbed-os/wiki/STM32-DMA-SPI-Support">here</a>.To select DMA or interrupts,
* use the SPI::set_dma_usage() function. By default, interrupt %SPI will be used unless you change the
* setting.</p>
*
* <h3>Async: Queueing</h3>
* <p>The async %SPI system supports an optional transaction queueing mechanism. When this is enabled,
* Mbed will allow multiple transactions to be queued up on a single bus, and will execute each one
* and deliver the appropriate callback in series. This is mainly useful for the non-blocking
* async api (SPI::transfer()), though you can also use it with the blocking API by having multiple
* threads call it at once.</p>
*
* <p>The transaction queue size defaults to 2 on most devices, but you can change that using the
* \c drivers.spi_transaction_queue_len option, e.g.</p>
* \code{.json}
* {
* "target_overrides": {
* "*": {
* "drivers.spi_transaction_queue_len": 3
* }
* }
* }
* \endcode
*
* <p>To save a little bit of memory, you can also set the queue length to 0 to disable the queueing mechanism.</p>
*
* \warning Currently, the value set by SPI::set_default_write_value() is
* <a href="https://github.com/ARMmbed/mbed-os/issues/13941">not respected</a> by the asynchronous %SPI
* code. This needs to be fixed.
*/
class SPI : private NonCopyable<SPI> {

Expand Down Expand Up @@ -305,7 +414,7 @@ class SPI : private NonCopyable<SPI> {
/** 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.
* exclusive access; the Slave Select line will not activate until data is transferred.
*/
void select(void);

Expand Down Expand Up @@ -338,9 +447,12 @@ class SPI : private NonCopyable<SPI> {
* @param callback The event callback function.
* @param event The event mask of events to modify. @see spi_api.h for SPI events.
*
* \warning Be sure to call this function with pointer types matching the frame size set for the SPI bus,
* or undefined behavior may occur!
*
* @return Operation result.
* @retval 0 If the transfer has started.
* @retval -1 if the transfer could not be enqueued (increase TRANSACTION_QUEUE_SIZE_SPI macro)
* @retval -1 if the transfer could not be enqueued (increase drivers.spi_transaction_queue_len option)
*/
template<typename WordT>
typename std::enable_if<std::is_integral<WordT>::value, int>::type
Expand All @@ -367,7 +479,10 @@ class SPI : private NonCopyable<SPI> {
* @param rx_length The length of RX buffer in bytes If 0, no reception is done.
* @param timeout timeout value. Use #rtos::Kernel::wait_for_u32_forever to wait forever (the default).
*
* @return -1 if the transfer could not be enqueued (increase TRANSACTION_QUEUE_SIZE_SPI macro)
* \warning Be sure to call this function with pointer types matching the frame size set for the SPI bus,
* or undefined behavior may occur!
*
* @return -1 if the transfer could not be enqueued (increase drivers.spi_transaction_queue_len option)
* @return 1 on timeout
* @return 2 on other error
* @return 0 on success
Expand Down Expand Up @@ -501,7 +616,7 @@ class SPI : private NonCopyable<SPI> {
void unlock_deep_sleep();


#if TRANSACTION_QUEUE_SIZE_SPI
#if MBED_CONF_DRIVERS_SPI_TRANSACTION_QUEUE_LEN
/** Start a new transaction.
*
* @param data Transaction data.
Expand All @@ -512,7 +627,7 @@ class SPI : private NonCopyable<SPI> {
*/
void dequeue_transaction();

#endif // TRANSACTION_QUEUE_SIZE_SPI
#endif // MBED_CONF_DRIVERS_SPI_TRANSACTION_QUEUE_LEN
#endif // !defined(DOXYGEN_ONLY)
#endif // DEVICE_SPI_ASYNCH

Expand Down Expand Up @@ -541,9 +656,9 @@ class SPI : private NonCopyable<SPI> {
SingletonPtr<rtos::Mutex> mutex;
/* Current user of the SPI */
SPI *owner = nullptr;
#if DEVICE_SPI_ASYNCH && TRANSACTION_QUEUE_SIZE_SPI
#if DEVICE_SPI_ASYNCH && MBED_CONF_DRIVERS_SPI_TRANSACTION_QUEUE_LEN
/* Queue of pending transfers */
SingletonPtr<CircularBuffer<Transaction<SPI>, TRANSACTION_QUEUE_SIZE_SPI> > transaction_buffer;
SingletonPtr<CircularBuffer<Transaction<SPI>, MBED_CONF_DRIVERS_SPI_TRANSACTION_QUEUE_LEN> > transaction_buffer;
#endif
};

Expand Down
12 changes: 6 additions & 6 deletions drivers/source/SPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ void SPI::_do_construct()
}
core_util_critical_section_exit();

#if DEVICE_SPI_ASYNCH && TRANSACTION_QUEUE_SIZE_SPI
#if DEVICE_SPI_ASYNCH && MBED_CONF_DRIVERS_SPI_TRANSACTION_QUEUE_LEN
// prime the SingletonPtr, so we don't have a problem trying to
// construct the buffer if asynch operation initiated from IRQ
_peripheral->transaction_buffer.get();
Expand Down Expand Up @@ -291,14 +291,14 @@ void SPI::abort_transfer()
{
spi_abort_asynch(&_peripheral->spi);
unlock_deep_sleep();
#if TRANSACTION_QUEUE_SIZE_SPI
#if MBED_CONF_DRIVERS_SPI_TRANSACTION_QUEUE_LEN
dequeue_transaction();
#endif
}

void SPI::clear_transfer_buffer()
{
#if TRANSACTION_QUEUE_SIZE_SPI
#if MBED_CONF_DRIVERS_SPI_TRANSACTION_QUEUE_LEN
_peripheral->transaction_buffer->reset();
#endif
}
Expand All @@ -320,7 +320,7 @@ int SPI::set_dma_usage(DMAUsage usage)

int SPI::queue_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 TRANSACTION_QUEUE_SIZE_SPI
#if MBED_CONF_DRIVERS_SPI_TRANSACTION_QUEUE_LEN
transaction_t t;

t.tx_buffer = const_cast<void *>(tx_buffer);
Expand Down Expand Up @@ -373,7 +373,7 @@ void SPI::unlock_deep_sleep()
}
}

#if TRANSACTION_QUEUE_SIZE_SPI
#if MBED_CONF_DRIVERS_SPI_TRANSACTION_QUEUE_LEN

void SPI::start_transaction(transaction_t *data)
{
Expand All @@ -400,7 +400,7 @@ void SPI::irq_handler_asynch(void)
unlock_deep_sleep();
_callback.call(event & SPI_EVENT_ALL);
}
#if TRANSACTION_QUEUE_SIZE_SPI
#if MBED_CONF_DRIVERS_SPI_TRANSACTION_QUEUE_LEN
if (event & (SPI_EVENT_ALL | SPI_EVENT_INTERNAL_TRANSFER_COMPLETE)) {
// SPI peripheral is free (event happened), dequeue transaction
dequeue_transaction();
Expand Down

0 comments on commit cef0d2f

Please sign in to comment.