Hello! Welcome back to our module on synchronous communication.
In our last lesson, we established the fundamental differences between asynchronous (UART) and synchronous (SPI, I2C) protocols. The key takeaway was that synchronous protocols like SPI use a shared clock line to eliminate timing ambiguities, allowing for higher speeds and more robust communication. You learned that SPI uses a master-slave architecture with a dedicated Chip Select (CS) line to choose which slave to communicate with.
Today, we'll put that theory into practice. We'll address the learning outcome: Configure the SPI peripheral in master mode and manage chip select (CS) for multiple slaves. This lesson is foundational for interfacing with a huge variety of peripherals, from sensors to memory chips and displays, which is a core skill for any firmware engineer.
1. SPI from the Master's Perspective
As the master device, your STM32 is in charge. It dictates the communication speed and decides which slave to talk to and when. To configure the SPI peripheral in master mode, you need to define several key parameters.
First, let's watch a brief overview of the SPI protocol to refresh our memory, focusing on the roles of the different lines and the concepts of clock polarity and phase.
Getting Started with STM32 and Nucleo Part 5: How to Use SPI | Digi-Key Electronics
The video 'Getting Started with STM32 and Nucleo Part 5: How to Use SPI' from Digi-Key provides an excellent introduction to the core concepts we'll be configuring today.
Watch the segment from 00:29 to 04:37. Pay close attention to: The function of the four main lines: SCK, MOSI, MISO, and CS. The concept of Clock Polarity (CPOL) and Clock Phase (CPHA), which combine to define the four 'SPI Modes'.
As the video explained, the two most important configuration parameters that you must get right for your slave device are:
- Clock Polarity (CPOL): Defines the idle state of the clock line. CPOL=0 means the clock is idle LOW. CPOL=1 means the clock is idle HIGH.
- Clock Phase (CPHA): Defines on which clock edge the data is sampled. CPHA=0 means data is sampled on the first (leading) edge. CPHA=1 means data is sampled on the second (trailing) edge.
These two parameters combine to create four possible SPI modes (0, 1, 2, 3). You must read the datasheet of the slave device you are communicating with to determine which mode it requires. Mode 0 (CPOL=0, CPHA=0) is the most common.
2. Configuring SPI Master Mode in STM32CubeIDE
Now, let's apply this theory to configure the SPI1 peripheral on your STM32F103 board using STM32CubeIDE. Our goal is to set it up as a full-duplex master.
A critical decision is how to manage the Chip Select (CS) pin. While the hardware can control a single CS pin automatically (Hardware NSS mode), it's far more common and flexible to manage it manually using a standard GPIO pin. This approach allows you to control any number of slave devices. We will use this software-based approach.
The following resources provide a step-by-step guide. The video demonstrates the process visually, while the text provides a good reference with code examples.
Getting Started with STM32 and Nucleo Part 5: How to Use SPI | Digi-Key Electronics
Let's return to the Digi-Key video, which walks through the exact CubeMX configuration we need.
Watch the section from 09:04 to 11:08. Follow along as the presenter: Enables SPI1 in Full-Duplex Master mode. Disables Hardware NSS (Slave Select). This is key. Sets the Data Size to 8 bits and adjusts the Prescaler to control the clock speed. Sets CPOL and CPHA (in this case, leaving them at their defaults for Mode 0). Configures a separate GPIO pin as a GPIO_Output to be used for the manual CS.
For a supplementary text-based guide that you can refer back to, the following article is excellent.
STM32 SPI Communication Tutorial
The article 'STM32 SPI Communication Tutorial' from ControllersTech provides a clear walkthrough with screenshots and code, reinforcing what you saw in the video.
Read the 'SPI Configuration' section. Note the settings for 'Full Duplex Master', 'Data Size', 'Prescaler', and the CPOL/CPHA settings. This mirrors the video's steps and explains the reasoning for each choice.
By now, you should be comfortable with the configuration process in CubeMX. The main steps are:
- Set the SPI peripheral to Full-Duplex Master.
- Set the baud rate via the Prescaler. This must be less than or equal to the maximum clock speed specified in the slave device's datasheet.
- Set CPOL and CPHA to match the slave's required SPI mode.
- Disable NSS (Hardware Slave Select).
- Choose a standard GPIO pin and configure it as an Output Push-Pull pin. This will be your software-controlled CS pin.
3. A Deeper Dive: The Control Registers
STM32CubeIDE provides a convenient graphical interface, but as a professional engineer, you should understand what's happening at the hardware level. The settings you just configured in CubeMX directly manipulate bits in the SPI peripheral's control registers.
The main register is SPI_CR1. Let's explore how its bits correspond to the settings we chose.
Mastering SPI in STM32 | Register-Level Programming Tutorial #8
The video 'Mastering SPI in STM32 | Register-Level Programming Tutorial #8' from ControllersTech offers a fantastic register-level explanation.
Watch the segment from 04:38 to 11:40. You don't need to write any register code yourself, but focus on understanding the mapping: Bits 0 & 1 (CPHA, CPOL): Clock Phase and Polarity. Bit 2 (MSTR): Set to 1 for Master mode. Bits 3-5 (BR): Baud Rate control (the prescaler). Bits 8 & 9 (SSI, SSM): SSM=1 enables Software Slave Management, which is what we are doing.
This deeper knowledge is invaluable for debugging complex issues where you might need to inspect register values directly. You can see a direct correlation: clicking "Full-Duplex Master" in CubeMX ultimately sets the MSTR bit in SPI_CR1 to 1.
4. Managing Chip Select for Multiple Slaves
The reason we chose software slave management is its scalability. An SPI bus can have many slaves, but they all share the SCK, MOSI, and MISO lines. To ensure the master only talks to one slave at a time, each slave must have its own unique Chip Select line.

The communication protocol is simple:
- Before starting a transaction, pull the CS line of the desired slave LOW.
- Perform the SPI data transfer.
- After the transaction is complete, pull the slave's CS line HIGH.
This sequence is clearly visible on a logic analyzer.

A Professional Software Pattern
When your project involves multiple SPI slaves (e.g., an SD card, an accelerometer, and a display), managing the various CS pins can get messy. A clean, professional way to handle this is to create a small abstraction layer.
The following resource presents an excellent software pattern for this.
Working with STM32 and SPI: Dealing with multiple slaves
The article 'Working with STM32 and SPI: Dealing with multiple slaves' from EmbeddedExpert.io demonstrates a robust C-based pattern for managing CS pins.
Read through the code examples in these sections. You don't need to implement the full register-level driver, but focus on the design pattern: Section 0: The use of an enum (SlaveSelectTypedef) to create unique identifiers for each slave. Section 1: Initializing multiple GPIOs as output pins for the different slaves. Section 3: The Slave_Select and Slave_Deselect functions that use a switch statement to control the correct GPIO based on the enum passed as an argument. Section 4: How these select/deselect functions are wrapped around the SPI write/read calls.
Here is a simplified example of that pattern using HAL functions, which you can use in your own projects:
// 1. Define an enum for your slave devices in a header file (e.g., spi_slaves.h)
typedef enum {
SLAVE_DEVICE_SENSOR,
SLAVE_DEVICE_MEMORY,
SLAVE_DEVICE_DISPLAY
} SpiSlave_t;
// Assume CS pins are configured in CubeMX:
// SENSOR_CS_Pin -> PA2
// MEMORY_CS_Pin -> PA3
// DISPLAY_CS_Pin -> PA4
// 2. Create functions to select and deselect slaves
void SPI_SelectSlave(SpiSlave_t slave) {
switch(slave) {
case SLAVE_DEVICE_SENSOR:
HAL_GPIO_WritePin(SENSOR_CS_GPIO_Port, SENSOR_CS_Pin, GPIO_PIN_RESET); // Active LOW
break;
case SLAVE_DEVICE_MEMORY:
HAL_GPIO_WritePin(MEMORY_CS_GPIO_Port, MEMORY_CS_Pin, GPIO_PIN_RESET);
break;
case SLAVE_DEVICE_DISPLAY:
HAL_GPIO_WritePin(DISPLAY_CS_GPIO_Port, DISPLAY_CS_Pin, GPIO_PIN_RESET);
break;
}
}
void SPI_DeselectSlave(SpiSlave_t slave) {
switch(slave) {
case SLAVE_DEVICE_SENSOR:
HAL_GPIO_WritePin(SENSOR_CS_GPIO_Port, SENSOR_CS_Pin, GPIO_PIN_SET); // Inactive HIGH
break;
case SLAVE_DEVICE_MEMORY:
HAL_GPIO_WritePin(MEMORY_CS_GPIO_Port, MEMORY_CS_Pin, GPIO_PIN_SET);
break;
case SLAVE_DEVICE_DISPLAY:
HAL_GPIO_WritePin(DISPLAY_CS_GPIO_Port, DISPLAY_CS_Pin, GPIO_PIN_SET);
break;
}
}
// 3. Use these functions to wrap your SPI communication
void WriteToSensor(uint8_t* data, uint16_t size) {
SPI_SelectSlave(SLAVE_DEVICE_SENSOR);
// HAL_SPI_Transmit(...) call goes here
SPI_DeselectSlave(SLAVE_DEVICE_SENSOR);
}
This pattern makes your application code clean, readable, and less prone to errors, as you're working with logical device names (SLAVE_DEVICE_SENSOR) rather than raw GPIO ports and pins.
Conclusion
Congratulations! You have successfully learned how to configure one of the most important peripherals in the embedded world. You not only know how to set it up using the graphical tools but also understand the underlying register settings and have a professional software pattern for managing multiple devices.
Key Takeaways:
- SPI master mode configuration involves setting the clock speed (prescaler), duplex mode, and SPI mode (CPOL/CPHA).
- These settings must match the requirements in the slave device's datasheet.
- Using software-managed Chip Select (a standard GPIO output pin) is a flexible and scalable method for controlling one or more slaves.
- The communication sequence is always: Select (CS Low) -> Transact -> Deselect (CS High).
- An
enumandswitchstatement provide a clean, robust software pattern for managing CS lines for multiple slaves.
Preview of the Next Lesson:
With the SPI peripheral now configured, the next logical step is to send and receive data. In the upcoming lesson, you will implement blocking functions to transmit and receive data with an SPI peripheral. We will write the code that goes inside the Select/Deselect wrappers we designed today.