The Serial Peripheral Interface
The Serial Peripheral Interface (often shored as SPI bus) is a widely used synchronous serial communication peripheral which communicates in full duplex mode using a master-slave architecture with a single master.
As the SPI is a Synchronous Serial bus, a clock signal is generated by one of the endpoint and provided to the others through a specific Serial Clock Line often shorted as SCL or CLK. The communication party which generates the clock is named Master while other Slaves.
In full-duplex buses, the communication is simultaneously bi-directional, i.e. data can flow from master to slave and from slave to master at the same time on two separate wires at the same time. The SPI has indeed two data lines:
- the Master Output Slave Input, often shorted as MOSI or SDO which stands for Serial Data Output,
- the Master Input Slave Output, often shorted as MISO or SDI which stands for Serial Data Input.
The SPI communication uses an additional wire for each slave to address them during communication. This wire is known as Chip Select (CS) or Slave Select (SS) more often nCS or nSS as it works in negate logic.
In embedded applications, SPI is could be used to interconnect microcontrollers but more often it is used to connect a micro to an external peripheral/device like Secure Digital, LED display, MEMS, etc. In this case the MCU acts as communication Master while the device as slave.
Basically in a single master, single slave communication the bus is composed by 4 wires (i.e. MISO, MOSI, SCL and nCS) thus the SPI is often nicknamed 4-wire bus. In this scenario the communication starts with the master lowering the chip select. At this point the master has to provide a clock signal having a proper frequency: slave and master will send and sample one bit per each clock cycle. As instance, in a 8-bit lenght communication the master will provide 8 clock pulses and master and slave will send and receive both 8 bits. To conclude the communication, the master will pull up the chip select.
The SPI communication is very simple and allows few settings. One of those configuration is the data lenght which usually is a multiple of 1 byte (e.g. 8-bit, 16-bit, 32-bit) but it is not always true.
SPI is designed to be independent from clock speed and potentially the clock speed could be pushed at Gigahertz. Anyway, small devices can operate within a specific range of clock rate and such information is often available in their manuals or datasheets. A good master SPI cell should thus be able to change its clock speed and we should thus expect that the clock speed is one of the SPI settings. Usually, it is and master has a wide range of allowed clock frequencies to be compliant with a wide range of devices.
SPI clock has also another couple of configuration to be taken in account:
- the Clock polarity (CPOL) which specifies the logic level of the clock signal when communication bus is its idle state. If CPOL is low clock line will be normally low and first clock edge will be the rising one, if CPOL is high clock will be normally high and first clock edge will be the falling one.
- the Clock phase (CPHA) which specifies if data is sampled on first or second clock edge.
Note that CPHA itself is not able to specify is the data is sampled on rising or falling edge: first and second clock edge are relative concept depending on the configuration of CPOL. As instance if CPHA set as 0 which means the data will be sampled on first clock edge: if CPOL is 0 first edge will be the falling one, if CPOL is 1 the first edge will be the rising one.
Basically we can have 4 CPOL and CPHA combination and their behavior is shown in the next figure:
A good master SPI should be able to operate in every condition since usually slaves are designed to be as cheap as possible with a simple circuitry, a small clock speed range, non configurable clock polarity and phase. This means we will (almost) always choose master configurations depending on slave needs.
Multi slave scenarios
There are two way to connect a master with more than a slave: the independent mode and the cooperative mode.
The independent mode is widely used when there are many different slave typologies. In this case the slaves share SCL, MOSI and MISO but each slave has its own nCS. In few words each slave can be selected independently and on each communication the master lowers the nCS of the slave it want communicate with: the chip select acts like a chip enable.
The cooperative mode is know also as daisy chain and in this case slaves share the same clock and slave select while MOSI and MISO are connected to create a chain of devices: the master output is connected to the first slave input, the first slave output to the second slave input, …, the last slave output to the master input. This kind of connection is particularly suitable with Serial Input Parallel Output Shift Registers (SIPO SR or simply SR): as instance connecting 4 16-bit SR in daisy chain we can obtain a system which acts like a single 64-bit SR.
The STM32 SPI
Across the various STM32 subfamilies there are three different hardware version of the SPI cell. We have already explained how to identify the driver version of your STM32 board here reporting them in a table. Anyway, as quick reference:
- STM32F1xx, STM32F2xx, STM32F4xx, STM32L0xx and STM32L1xx use SPIv1;
- STM32F0xx, STM32F3xx, STM32F7xx and STM32L4xx use SPIv2;
- STM32H7xx uses SPIv3.
The differences between various SPI versions are minimal and basically related to exposed configuration registers. STM32 allows:
- both master and slave mode
- to configure the baud-rate
- to communicate in full-duplex and half-duplex mode using a customization of the SPI named three wire SPI
- to select the data size. Note that possible choices depend on the hardware version. On SPIv1 available choices are 8 or 16-bit, in SPIv2 any size between 4 and 16-bit and in SPIv3 any size between 4 and 32-bit.
- to manage slave select automatically in hardware
- to configure clock phase and polarity
- to do hardware CRC but only in SPIv2 and SPIv3 and in the SPIv3 it is also possible to choose CRC size.
Note that STM32 offers many SPI instances (from 2 in the small packages up to 6 in larger packages). SPI cells are identified with a progressive number (i.e. SPI 1, SPI 2, SPI 3 and so on). Note also that SPI bus relies on GPIO alternate functions to be able to get in touch with the outside world.
The ChibiOS SPI driver
The current ChibiOS SPI driver allows only master mode allowing to exploit many of the feature offered by the STM32 SPI cell. It offers again both synchronous and asynchronous API.
Each API of the SPI Driver starts with the prefix “spi”. Function names are camel-case, pre-processor constants uppercase and variables lowercase.
Different driver same approach
Comparing the SPI driver with previously presented Serial Driver or ADC we can notice that there are some certitudes in ChibiOS: one of these is for sure the consistency of design patterns. The SPI driver is organized like every other simple driver of ChibiOS\HAL:
- The whole driver subsystem can be enabled disabled through the proper switch in the halconf.h. The switch name is HAL_USE_SPI.
- To use the driver we have then to assign it a SPI peripheral acting on mcuconf.h.
- Assigning a peripheral a new object will become available: SPID1 on SPI 1 assignation, SPID2 on SPI 2 and so on.
- Each SPIDx object represent a driver which implements a Finite State Machine.
- A driver to be used shall be initialized but this is done automatically on halInit();
- Each driver operation (e.g. a communication exchange) can be done only if driver has been properly started. This requires a call to spiStart().
- The spiStart() receives as usual two parameters: a pointer to the driver and a pointer to its configuration.
- If the driver is not used it can be stopped through the spiStop().
The following figure represent the state machine of the driver.
Configuring the SPI
The SPI configuration structure allows to properly exploit STM32 SPI features by configuring data size, baudrate and clock phase and polarity. The following structure has been copied out from SPIv2 but is identical to that from SPIv1 and extremely similar to SPIv3.
The first parameter is circular, a boolean which enables the circular mode. In this mode the SPI buffer is transmitted continuously. The driver will also execute a callback every time the whole buffer have been sent: the callback can be configured using the second parameter, end_cb, which is a pointer to a callable. This callback is particularly suitable for asynchronous APIs usage.
The next parameters is the identifier of the chip se depends on SPI_SELECT_MODE. According to the configuration we can have:
- one IO line,
- port identifier and pad mask (default),
- port and pin identifiers.
Those are used in software chip select mode. As said chip select can be controlled in hardware as well as in software. When chip select is controlled by software the driver need to know which pin or mask of pins are the chip selects. SPI_SELECT_MODE can be configured in halconf.h and by default it is set as SPI_SELECT_MODE_PAD.
The latest two parameters are hardware depending and represent 2 SPI registers. All the differences between various hardware version are circumscribed here:
- in SPIv1 and SPIv2 we have cr1 and cr2 which represent the value of SPI control register 1 (SPI_CR1) and SPI control register 2 (SPI_CR2). Even if they have the same name in both the hardware version, bit fields and related configurations are slightly different.
- in SPIv3 we have cfg1 and cfg2 which represent the value of SPI configuration register 1 (SPI_CFG1) and SPI configuration register 2 (SPI_CFG2).
To compose register values we can use the bitmasks already defined in the CMSIS header files. Such files contains all the definition of every bit field of every register of our MCU. It is possible to find under [chibios_root]\chibios_trunk\os\common\ext\ST\. Each MCU has is own CMSIS header file as the register map changes with microcontroller. As example the file for the STM32F401RE is [chibios_root]\chibios_trunk\os\common\ext\ST\STM32F4xx\stm32f401xe.h.
Anyway, it can be useful to take a look to SPI demo under testhal folder: here you can find some valid example to understand how to configure SPI. As instance, the following configuration have been copied by the SPI demo for STM32F3 discovery.
In this case circular mode is disabled, callback is unused, the chip select pin is PB12 and SPI is configured to work at 18 MHz with CPOL and CPHA both to zero.
Note that to compute the baud rate value user should refer to both SPI CR1 description and clock tree configuration which in ChibiOS can be configured in mcuconf.h. In this specific case the BR bit-field value is set to 0. The register description shows a formula to compute the baud rate which is
The SPI clock frequency depends on clock tree configuration. In this demo AHB bus is configured at its maximum speed (i.e 72 MHz), the APB1 and APB2 are configured both to half AHB speed (i.e. 36 MHz). The SPI speed is equal to the speed of the APB bus. Thus in this specific case we have
To put in place this configuration user has to call the spiStart specifying which SPI Driver we are actually going to use.
SPI driver configuration can be changed on the fly restarting the driver with a new configuration. As instance this is particularly useful when we have more slaves which works with different configuration (baud rate, clock polarity and phase, slave select and so on).
Select and unselect
The SPI offers a couple of API to select or deselect the slave. Basically selecting the slave we are lowering the nCS.
To use these API the driver shall be initialised and nCS shall not be configured as hardware managed. The sequence of SPI transaction would be
Note that a software managed chip select is actually a digital output pin thus nCS pin shall be configured as output push-pull while MISO, MOSI and SCL are configured in alternate mode (take a look to GPIO article if you are not familiar with).
As instance this code has been copied by a demo for STM32 Nucleo-64 F401RE where SPI pin are not configured at board level
The SPI driver offers both synchronous and asynchronous communication functions. We already introduced these concept but let me recall them.
A blocking function blocks the execution of the calling thread until the operation is completed. If the function has some internal wait they are not polled, instead the calling thread is suspended and the RTOS executes other threads. Such functions are also know as synchronous functions as the calling thread remains in sync with the operation.
A non-blocking function launches the operation and returns to the calling thread which continues to be executed. Such operation is executed in background, usually in hardware, launching a interrupt on operation complete event. Such IRQ usually is exposed by driver API in form of a callback. Such function are also know as asynchronous functions.
As the SPI is actually full duplex, basically each transaction is bidirectional. Thus maybe the spiStartExchange and the spiExhange can be considered the main communication functions of the SPI driver.
The first is asynchronous while the second is synchronous. Both the functions receive four parameters:
- A pointer to the SPI driver
- The transaction size in word where the word size is defined by the SPI configuration
- a pointer to a transmission buffer having proper size and pre-filled before the exchange call
- a pointer to a receiver buffer having proper size which will be filled by exchange call with SPI incoming data.
The synchronous call (i.e spiExhange) puts the calling thread in sleep state until the transaction is completed or aborted. The asynchronous call instead starts the communication and returns to the caller. In both cases when the operation is completed the end_cb (if is not NULL) is executed.
Send and Receive
The driver offers also some functions to communicate in one way: the spiStartSend, the spiStartReceive and their related synchronous version spiSend and spiReceive
This is actually a trick because the SPI always act in full duplex mode. Indeed the spiSend is actually an spiExchange which ignores received data. Similarly the spiRead is actually an spiExchange which uses dummy data as transmission data. Of course the same is for asynchronous API.
Ignore and Abort
The driver offer also some functions to ignore data on bus: the spiStartIgnore and spiIgnore.
These functions can be useful in certain case (e.g where the slave expects some data to push forward an internal state machine but the master is not interested to received data or is not transmitting anything).
The asynchronous API requires also another function which eventually is able to stop the ongoing activities: the spiAbort.
This function aborts ongoing operations if any.
The driver offers also a couple of API to acquire and release the bus: spiAcquireBus and spiReleaseBus.
Such functions are useful when the same SPI driver is used from two threads because they guarantee that none will use that SPI driver until it is acquired avoiding misbehaviour due to concurrent access. Note that these API requires SPI_USE_MUTUAL_EXCLUSION is set to TRUE in the halconf.h file.
Further readings and Hands-on
We have already planned a collection of example and exercises for the SPI driver. If you are interested in follow us on Facebook to be updated on our articles. Anyway, at this moment you could refer to the SPI demo under testhal to give it a try.
SPI is used by many devices and we wrote many article about them. For sure the quick reference could be:
- STM32, ChibiOS and a 8×8 LED Matrix
- 7-segment display and STM32 using ChibiOS
- A Radio Frequency transceiver library: nRF24L01 and ChibiOS/RT
You can also try the demo under testex for STM32F3 Discovery which uses L3GD20: this is a 3-axis gyroscope connected to the MCU through the SPI.
Previous and next
This article is part of a series of articles which are meant to be tutorials. I have composed them to be read in sequence. Here the previous and next article of this series: