SPI Functions — allow applications and other packages to access SPI devices


#include <cyg/io/spi.h>

CYG_SPI_OP_RETURN_TYPE cyg_spi_transfer(cyg_spi_device* device, cyg_bool polled, cyg_uint32 count, const cyg_uint8* tx_data, cyg_uint8* rx_data);

CYG_SPI_OP_RETURN_TYPE cyg_spi_tick(cyg_spi_device* device, cyg_bool polled, cyg_uint32 count);

int cyg_spi_get_config(cyg_spi_device* device, cyg_uint32 key, void* buf, cyg_uint32* len);

int cyg_spi_set_config(cyg_spi_device* device, cyg_uint32 key, const void* buf, cyg_uint32* len);

CYG_SPI_OP_RETURN_TYPE cyg_spi_transaction_begin(cyg_spi_device* device);

cyg_bool cyg_spi_transaction_begin_nb(cyg_spi_device* device);

CYG_SPI_OP_RETURN_TYPE cyg_spi_transaction_transfer(cyg_spi_device* device, cyg_bool polled, cyg_uint32 count, const cyg_uint8* tx_data, cyg_uint8* rx_data, cyg_bool drop_cs);

CYG_SPI_OP_RETURN_TYPE cyg_spi_transaction_tick(cyg_spi_device* device, cyg_bool polled, cyg_uint32 count);

CYG_SPI_OP_RETURN_TYPE cyg_spi_transaction_end(cyg_spi_device* device);


All SPI functions take a pointer to a cyg_spi_device structure as their first argument. This is an opaque data structure, usually provided by the platform HAL. It contains the information needed by the SPI bus driver to interact with the device, for example the required clock rate and polarity.

An SPI transfer involves the following stages:

  1. Perform thread-level locking on the bus. Only one thread at a time is allowed to access an SPI bus. This eliminates the need to worry about locking at the bus driver level. If a platform involves multiple SPI buses then each one will have its own lock. Prepare the bus for transfers to the specified device, for example by making sure it will tick at the right clock rate.
  2. Assert the chip select on the specified device, then transfer data to and from the device. There may be a single data transfer or a sequence. It may or may not be necessary to keep the chip select asserted throughout a sequence.
  3. Optionally generate some number of clock ticks without asserting a chip select, for those devices which need this to complete an operation.
  4. Return the bus to a quiescent state. Then unlock the bus, allowing other threads to perform SPI operations on devices attached to this bus.

The simple functions cyg_spi_transfer and cyg_spi_tick perform all these steps in a single call. These are suitable for simple I/O operations. The alternative transaction-oriented functions each perform just one of these steps. This makes it possible to perform multiple transfers while only locking and unlocking the bus once, as required for more complicated devices.

With the exception of cyg_spi_transaction_begin_nb all the functions will block until completion. The type of the return values depends on whether the underlying hardware driver can return error conditions or not. If it can, this will be indicated with the C preprocessor define CYGINT_IO_SPI_DRV_REPORTS_ERRORS, and the return type CYG_SPI_OP_RETURN_TYPE will be a standard error code as defined in the C/POSIX header <errno.h>. Alternatively for drivers which do not return errors, the return value's type will be void, or in other words, no return value. If there are any errors, they would be generated by an SPI bus peripheral, and not by the attached SPI device. The SPI bus does not receive any feedback from a device about possible errors, instead those have to be handled by software at a higher level.

If a driver is capable of indicating errors, and an error value is returned, this may imply the SPI operation requested may not have started, may not have completed, or may only have partially completed. The exact interpretation will depend on both hardware properties and low-level driver implementation. SPI API users are free to ignore all return values, in which case the usage of the SPI API will be identical, irrespective of the setting of CYGINT_IO_SPI_DRV_REPORTS_ERRORS.

An SPI transfer will always take a predictable amount of time, depending on the transfer size and the clock rate. If a thread cannot afford the time it will take to perform a complete large transfer then a number of smaller transfers can be used instead.

SPI operations should always be performed at thread-level or during system initialization, and not inside an ISR or DSR. This greatly simplifies locking. Also a typical ISR or DSR should not perform a blocking operation such as an SPI transfer.

SPI transfers can happen in either polled or interrupt-driven mode. Typically polled mode should be used during system initialization, before the scheduler has been started and interrupts have been enabled. Polled mode should also be used in single-threaded applications such as RedBoot. A typical multi-threaded application should normally use interrupt-driven mode because this allows for more efficient use of cpu cycles. Polled mode may be used in a multi-threaded application but this is generally undesirable: the cpu will spin while waiting for a transfer to complete, wasting cycles; also the current thread may get preempted or timesliced, making the timing of an SPI transfer much less predictable. On some hardware interrupt-driven mode is impossible or would be very inefficient. In such cases the bus drivers will only support polled mode and will ignore the polled argument.

Simple Transfers

cyg_spi_transfer can be used for SPI operations to simple devices. It takes the following arguments:

cyg_spi_device* device
This identifies the SPI device that should be used.
cyg_bool polled
Polled mode should be used during system initialization and in a single-threaded application. Interrupt-driven mode should normally be used in a multi-threaded application.
cyg_uint32 count
This identifies the number of data items to be transferred. Usually each data item is a single byte, but some devices use a larger size up to 16 bits.
const cyg_uint8* tx_data
The data to be transferred to the device. If the device will only output data and ignore its input then a null pointer can be used. Otherwise the array should contain count data items, usually bytes. For devices where each data item is larger than one byte the argument will be interpreted as an array of shorts instead, and should be aligned to a 2-byte boundary. The bottom n bits of each short will be sent to the device. The buffer need not be aligned to a cache-line boundary, even for SPI devices which use DMA transfers, but some bus drivers may provide better performance if the buffer is suitably aligned. The buffer will not be modified by the transfer.
cyg_uint8* rx_data
A buffer for the data to be received from the device. If the device does not generate any output then a null pointer can be used. The same size and alignment rules apply as for tx_data.

cyg_spi_transfer performs all the stages of an SPI transfer: locking the bus; setting it up correctly for the specified device; asserting the chip select and transferring the data; dropping the chip select at the end of the transfer; returning the bus to a quiescent state; and unlocking the bus.

Additional Clock Ticks

Some devices require a number of clock ticks on the SPI bus between transfers so that they can complete some internal processing. These ticks must happen at the appropriate clock rate but no chip select should be asserted and no data transfer will happen. cyg_spi_tick provides this functionality. The device argument identifies the SPI bus, the required clock rate and the size of each data item. The polled argument has the usual meaning. The count argument specifies the number of data items that would be transferred, which in conjunction with the size of each data item determines the number of clock ticks.


A transaction-oriented API is available for interacting with more complicated devices. This provides separate functions for each of the steps in an SPI transfer.

cyg_spi_transaction_begin must be used at the start of a transaction. This performs thread-level locking on the bus, blocking if it is currently in use by another thread. Then it prepares the bus for transfers to the specified device, for example by making sure it will tick at the right clock rate.

cyg_spi_transaction_begin_nb is a non-blocking variant, useful for threads which cannot afford to block for an indefinite period. If the bus is currently locked the function returns false immediately. If the bus is not locked then it acts as cyg_spi_transaction_begin and returns true.

Once the bus has been locked it is possible to perform one or more data transfers by calling cyg_spi_transaction_transfer. This takes the same arguments as cyg_spi_transfer, plus an additional one drop_cs. A non-zero value specifies that the device's chip select should be dropped at the end of the transfer, otherwise the chip select remains asserted. It is essential that the chip select be dropped in the final transfer of a transaction. If the protocol makes this difficult then cyg_spi_transaction_tick can be used to generate dummy ticks with all chip selects dropped.

If the device requires additional clock ticks in the middle of a transaction without being selected, cyg_spi_transaction_tick can be used. This will drop the device's chip select if necessary, then generate the appropriate number of ticks. The arguments are the same as for cyg_spi_tick.

cyg_spi_transaction_end should be called at the end of a transaction. It returns the SPI bus to a quiescent state, then unlocks it so that other threads can perform I/O.

A typical transaction might involve the following. First a command should be sent to the device, consisting of four bytes. The device will then respond with a single status byte, zero for failure, non-zero for success. If successful then the device can accept another n bytes of data, and will generate a 2-byte response including a checksum. The device's chip select should remain asserted throughout. The code for this would look something like:

#include <cyg/io/spi.h>
#include <cyg/hal/hal_io.h>    // Defines the SPI devices
    // Interrupt-driven transfer, four bytes of command
    cyg_spi_transaction_transfer(&hal_spi_eprom, 0, 4, command, NULL, 0);
    // Read back the status
    cyg_spi_transaction_transfer(&hal_spi_eprom, 0, 1, NULL, status, 0);
    if (!status[0]) {
        // Command failed, generate some extra ticks to drop the chip select
        cyg_spi_transaction_tick(&hal_spi_eprom, 0, 1);
    } else {
        // Transfer the data, then read back the final status. The
        // chip select should be dropped at the end of this.
        cyg_spi_transaction_transfer(&hal_spi_eprom, 0, n, data, NULL, 0);
        cyg_spi_transaction_transfer(&hal_spi_eprom, 0, 2, NULL, status, 1);
        // Code for checking the final status should go here
    // Transaction complete so clean up

A number of variations are possible. For example the command and status could be packed into the beginning and end of two 5-byte arrays, allowing a single transfer.

Device Configuration

The functions cyg_spi_get_config and cyg_spi_set_config can be used to examine and change parameters associated with SPI transfers. The only keys that are defined for all devices are CYG_IO_GET_CONFIG_SPI_CLOCKRATE and CYG_IO_SET_CONFIG_SPI_CLOCKRATE. Some types of device, for example MMC cards, support a range of clock rates. The cyg_spi_device structure will be initialized with a low clock rate. During system initialization the device will be queried for the optimal clock rate, and the cyg_spi_device should then be updated. The argument should be a clock rate in Hertz. For example the following code switches communication to 1Mbit/s:

cyg_uint32    new_clock_rate = 1000000;
cyg_uint32    len            = sizeof(cyg_uint32);
if (cyg_spi_set_config(&hal_mmc_device,
                       (const void *)&new_clock_rate, &len)) {
    // Error recovery code

If an SPI bus driver does not support the exact clock rate specified it will normally use the nearest valid one. SPI bus drivers may define additional keys appropriate for specific hardware. This means that the valid keys are not known by the generic code, and theoretically it is possible to use a key that is not valid for the SPI bus to which the device is attached. It is also possible that the argument used with one of these keys is invalid. Hence both cyg_spi_get_config and cyg_spi_set_config can return error codes. The return value will be 0 for success, non-zero for failure. The SPI bus driver's documentation should be consulted for further details.

Both configuration functions will lock the bus, in the same way as cyg_spi_transfer. Changing the clock rate in the middle of a transfer or manipulating other parameters would have unexpected consequences.