Using STM32 SPI with ChibiOS

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 endpoints 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:

  1. the Master Output Slave Input, often shorted as MOSI or SDO which stands for Serial Data Output,
  2. 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.

SPI highlights

In embedded applications, the SPI could be used to interconnect microcontrollers but more often it is used to connect a micro to an external peripheral/device like Secure Digital Card, LED display, MEMS, etc. In this case, the MCU acts as communication Master while the device as a slave.

Basically, in a single master single slave communication, the bus is composed of 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. For instance, in an 8-bit length communication, the master will provide 8 clock pulses and master and slave will send and receive both 8 bits. The communication ends when the master pulls up the chip select.

A diagram of an SPI communication master slave with high clock polarity and low clock phase.

SPI communication is very simple and allows a few settings. One of those configurations is the data length 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 of 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. Indeed, this is usually an option and master has a wide range of allowed clock frequencies in order to be compliant with a wide range of devices.

SPI clock has also another couple of configurations to be taken into account:

  • the Clock polarity (CPOL), which specifies the logic level of the clock signal when the communication bus is its idle state. If CPOL is low, the clock line will be normally low and first clock edge will be the rising one. If CPOL is high, the 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 the first or second clock edge.

Note that CPHA itself is not able to specify if the data is sampled on rising or falling edge: first and second clock edges are a relative concept. They make sense only whereas CPOL decides. For instance, if CPHA is set as 0, the data will be sampled on the first clock edge. If CPOL is 0, the first edge will be the rising one, otherwise falling one. Thus we can state that:

  • if CPOL and CPHA are both 0 the clock is normally low and data is sampled on the rising edge
  • if CPOL is 0 and CPHA is 1 the clock is normally low and data is sampled on the falling edge
  • if CPOL is 1 and CPHA is 0 the clock is normally high and data is sampled on the falling edge
  • if CPOL and CPHA are both 1 the clock is normally high and data is sampled on the rising edge

These 4 CPOL and CPHA combinations and their behavior are shown in the next figure:

The behavior of SPI with different clock polarity (CPOL) and clock phase (CPHA) configuration

A good SPI master should be able to operate in every condition since usually slaves are designed to be as cheap as possible with 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 is two way to connect a master with more than a slave: the independent mode and the cooperative mode.

Two way to connect multiple slaves to a master: on the left the independent mode; on the right the dependent mode (aka daisy chain).

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 a few words each slave can be selected independently and on each communication, the master lowers the nCS of the slave it wants to communicate with: the chip select acts like a chip enable.

The cooperative mode is known 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): for instance, connecting 4 16-bit SR in a daisy chain, we can obtain a system which acts as a single 64-bit SR.


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 a quick reference:

  • STM32F1xx, STM32F2xxSTM32F4xxSTM32L0xx and STM32L1xx use SPIv1;
  • STM32F0xx, STM32F3xxSTM32F7xx 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 a 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 depending 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 an 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 represents 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 the 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 represents the state machine of the driver.

The SPI Driver state machine

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 has 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 are used to identify the chip select of the slave. They depend on SPI_SELECT_MODE. According to the configuration we can have:

  • one IO line,
  • a port identifier and a pad mask,
  • a port and a pin identifier (default).

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 is the chip select. SPI_SELECT_MODE can be configured in halconf.h. 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 contain 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 the 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. For instance, the following configuration has been copied by the SPI demo for STM32F3 discovery.

In this case, the circular mode is disabled, the 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

baudrate=\frac{f_{clk}}{2^{BR + 1}}

The SPI clock frequency depends on the 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

baudrate=\frac{36 MHz}{2^{0 + 1}}=18MHz

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. For instance, this is particularly useful when we have more slaves which work with a 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 initialized 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).

For instance, this code has been copied by a demo for STM32 Nucleo-64 F401RE where SPI pins are not configured at board level

Communication API

The SPI driver offers both synchronous and asynchronous communication functions. We already introduced these concepts but let me recall them.

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 known as synchronous functions as the calling thread remains in sync with the operation.

non-blocking function launches the operation and returns to the calling thread which continues to be executed. Such operation is executed in the background, usually in hardware, launching an interrupt on operation complete event. Such IRQ usually is exposed by driver API in the form of a callback. Such functions are also known as asynchronous functions.


As the SPI is actually a 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 functions receive four parameters:

  • A pointer to the SPI driver
  • The transaction size expressed in words whereas the word size is defined by the SPI configuration
  • a pointer to a transmission buffer having the proper size and pre-filled before the exchange call
  • a pointer to a receiver buffer having the proper size which will be filled by exchange call with SPI incoming data.

The synchronous call (i.e spiExhange) puts the calling thread in a 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 (when it is not equal to 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 a spiExchange which ignores received data. Similarly, the spiRead is actually a spiExchange which uses dummy data as transmission data. Of course the same is for asynchronous API.

Ignore and Abort

The driver offers also some functions to ignore data on the bus: the spiStartIgnore and spiIgnore.

These functions can be useful in certain cases (e.g where the slave expects some data to push forward an internal state machine but the master is not interested in receiving 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.

Mutual exclusion

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 misbehavior 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 articles about them. For sure the quick reference could be:

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:

Be the first to reply at Using STM32 SPI with ChibiOS

Leave a Reply