Chapter 133: SPI Multiple Device Management
Chapter Objectives
After completing this chapter, you will be able to:
- Understand how multiple SPI slave devices can share a single SPI bus.
- Learn to configure and manage individual Chip Select (CS) lines for multiple devices.
- Implement SPI communication with multiple peripherals on an ESP32 using ESP-IDF.
- Distinguish and apply device-specific configurations (e.g., SPI mode, clock speed) when adding multiple devices to the same bus.
- Handle SPI transactions targeted at specific devices on a shared bus using their unique handles.
- Identify and troubleshoot common issues related to multi-device SPI setups.
- Appreciate the resource-saving benefits of using a shared SPI bus.
Introduction
In Chapter 132, we explored the fundamentals of the Serial Peripheral Interface (SPI) and learned how to communicate with a single SPI slave device using an ESP32. While this is foundational, many embedded systems require interaction with multiple peripheral devices. Imagine a weather station project: you might need to read data from an SPI-based temperature sensor, an SPI pressure sensor, and perhaps write data to an SPI-driven display or save logs to an SPI flash memory chip. Connecting each of these to a separate SPI bus on the microcontroller would consume a significant number of GPIO pins and potentially exhaust the available SPI controllers.
Fortunately, the SPI protocol is inherently designed to support multiple slave devices on a single bus. This chapter focuses on how to effectively manage and communicate with several SPI peripherals sharing common MOSI, MISO, and SCLK lines, using individual Chip Select lines to address each device. We will delve into the ESP-IDF mechanisms that facilitate this, allowing for efficient and organized multi-device SPI communication.
Theory
Sharing the SPI Bus
The core principle enabling multiple devices on one SPI bus is the sharing of the data and clock lines, coupled with a dedicated selection line for each slave device.
- Shared Lines:
- SCLK (Serial Clock): The master (ESP32) generates a single clock signal that is distributed to all slave devices connected to the bus.
- MOSI (Master Out, Slave In): The master’s data output line is connected to the data input line of all slaves.
- MISO (Master In, Slave Out): The data output lines of all slave devices are typically connected together and then to the master’s data input line. This requires slave devices to tri-state (put into a high-impedance state) their MISO line when they are not selected.
- Dedicated Lines:
- CS (Chip Select) / SS (Slave Select): Each slave device on the bus requires a unique CS line controlled by the master. To communicate with a specific slave, the master asserts (usually pulls low) the CS line of that particular slave. Only the slave whose CS line is active will respond to the SCLK and MOSI signals and drive the MISO line. All other slave devices on the bus will ignore the SCLK and MOSI signals and keep their MISO lines in a high-impedance state, preventing bus contention.
ESP-IDF Management of Multiple Devices
The ESP-IDF spi_master driver is designed to handle multiple devices on a single SPI bus gracefully. When you initialize an SPI bus using spi_bus_initialize(), you are setting up the shared physical lines (MOSI, MISO, SCLK). Subsequently, each slave device is “added” to this bus using spi_bus_add_device().
%%{ init: { 'theme': 'base', 'themeVariables': { 'fontFamily': 'Open Sans' } } }%%
graph TB
subgraph "ESP-IDF Application Code"
direction LR
A[Start: Setup SPI Bus] --> B("1- Define <i>spi_bus_config_t</i> <br> (MOSI, MISO, SCLK pins)");
B --> C{"<i>spi_bus_initialize(host, &bus_config, dma_chan)</i>"};
end
C -- Success --> SharedBus["Shared SPI Bus Context <br> (Host: e.g., SPI2_HOST)"];
C -- Error --> BusInitError(["Error Handling: Bus Init Failed"]);
SharedBus --> DA(2- Add Device A);
DA --> DA_Cfg("Define <i>spi_device_interface_config_t</i> dev_a_cfg <br> - <i>spics_io_num = CS_PIN_A</i> <br> - <i>mode = 0</i> <br> - <i>clock_speed_hz = 5MHz</i>");
DA_Cfg --> DA_Add{"<i>spi_bus_add_device(host, &dev_a_cfg, &handle_a)</i>"};
DA_Add -- Success --> HandleA[<i>spi_device_handle_t handle_a</i>];
DA_Add -- Error --> AddDevAError(["Error Handling: Add Device A Failed"]);
SharedBus --> DB(3- Add Device B);
DB --> DB_Cfg("Define <i>spi_device_interface_config_t</i> dev_b_cfg <br> - <i>spics_io_num = CS_PIN_B</i> <br> - <i>mode = 1</i> <br> - <i>clock_speed_hz = 10MHz</i>");
DB_Cfg --> DB_Add{"<i>spi_bus_add_device(host, &dev_b_cfg, &handle_b)</i>"};
DB_Add -- Success --> HandleB[<i>spi_device_handle_t handle_b</i>];
DB_Add -- Error --> AddDevBError(["Error Handling: Add Device B Failed"]);
subgraph "Perform Transactions"
direction TB
HandleA --> TransactA("4- Transact with Device A <br> <i>spi_transaction_t t_a;</i> <br> <i>spi_device_transmit(handle_a, &t_a)</i>");
TransactA --> DriverA["Driver uses <b>handle_a</b> config:<br>- Activates CS_PIN_A<br>- Sets SPI Mode 0<br>- Sets Clock to 5MHz"];
HandleB --> TransactB("5- Transact with Device B <br> <i>spi_transaction_t t_b;</i> <br> <i>spi_device_transmit(handle_b, &t_b)</i>");
TransactB --> DriverB["Driver uses <b>handle_b</b> config:<br>- Activates CS_PIN_B<br>- Sets SPI Mode 1<br>- Sets Clock to 10MHz"];
end
classDef default fill:#f9f9f9,stroke:#333,stroke-width:1px,color:#333,font-family:'Open Sans';
classDef start fill:#EDE9FE,stroke:#5B21B6,stroke-width:2px,color:#5B21B6;
classDef process fill:#DBEAFE,stroke:#2563EB,stroke-width:1px,color:#1E40AF;
classDef decision fill:#FEF3C7,stroke:#D97706,stroke-width:1px,color:#92400E;
classDef handle fill:#D1FAE5,stroke:#059669,stroke-width:2px,color:#065F46;
classDef error fill:#FEE2E2,stroke:#DC2626,stroke-width:1px,color:#991B1B;
classDef busCtx fill:#FFFBEB,stroke:#F59E0B,stroke-width:1.5px,color:#B45309;
classDef driverAction fill:#E0F2FE,stroke:#0EA5E9,stroke-width:1.5px,color:#0369A1;
class A start;
class B,DA_Cfg,DB_Cfg,TransactA,TransactB process;
class C,DA_Add,DB_Add decision;
class HandleA,HandleB handle;
class BusInitError,AddDevAError,AddDevBError error;
class SharedBus busCtx;
class DriverA,DriverB driverAction;
class DA process,;
class DB process;
%% Styling for placeholder to make it invisible
class placeholder fill:transparent,stroke:transparent;
linkStyle default interpolate basis;
spi_device_handle_t: Each call tospi_bus_add_device()for a unique peripheral returns aspi_device_handle_t. This handle is crucial as it encapsulates all the specific configurations for that particular slave device, such as:- Its unique
spics_io_num(Chip Select GPIO pin). - Its required
clock_speed_hz. - Its specific SPI
mode(CPOL/CPHA). - Other parameters like
command_bits,address_bits,dummy_bits,queue_size, etc., defined inspi_device_interface_config_t.
- Its unique
When you want to perform an SPI transaction (e.g., using spi_device_transmit() or spi_device_polling_transmit()), you pass the specific spi_device_handle_t for the target slave device. The driver then uses the configuration associated with that handle to:
- Assert the correct CS line.
- Configure the SPI controller for the device’s required clock speed and SPI mode.
- Perform the data transfer.
- De-assert the CS line.
This allows different devices on the same bus to operate with potentially different SPI settings without manual reconfiguration of the bus for each transaction.
Considerations for Multi-Device SPI
| Consideration | Description & Impact | ESP-IDF Handling / Best Practice |
|---|---|---|
| Device-Specific Configurations | Each slave device can have its own SPI mode (CPOL/CPHA) and maximum clock speed. Mixing devices with different requirements on the same bus is common. |
|
| CS Pin Availability | Each slave device requires a dedicated Chip Select (CS) GPIO pin on the master (ESP32). This is often the primary limiting factor for the number of devices. |
|
| Bus Loading & Signal Integrity | Each connected device adds capacitive load to the shared SCLK, MOSI, and MISO lines. Too many devices or long traces can degrade signals, limiting maximum reliable clock speed. |
|
| Pull-up Resistors on CS Lines | Ensures CS lines are in a defined inactive (high, for active-low CS) state when not driven by the master, preventing accidental slave selection, especially during startup or if master GPIOs are tri-stated. |
|
| MISO Line Tri-stating | Slave devices must tri-state (high-impedance) their MISO output when not selected (CS is inactive). Failure to do so causes bus contention if multiple slaves try to drive MISO simultaneously. |
|
- Clock Speed and SPI Mode: As mentioned, the ESP-IDF driver allows each device added to the bus to have its own clock speed and SPI mode. The driver reconfigures these parameters for the bus temporarily for the duration of a transaction with a specific device.
- Bus Loading: Each device added to the SPI bus adds a small capacitive load to the shared MOSI, MISO, and SCLK lines. While ESP32 GPIOs have reasonable drive strength, connecting a very large number of devices or using long traces can degrade signal integrity, potentially limiting the maximum reliable clock speed. For most typical applications with a handful of devices on a PCB, this is not an issue.
- CS Pin Availability: The primary limiting factor for the number of SPI devices you can connect to a single bus is the number of available GPIO pins on your ESP32 to use as CS lines.
- Pull-up Resistors on CS Lines: While the ESP-IDF driver manages the state of CS pins, it’s often good practice, especially in noisy environments or if CS lines are long, to have external pull-up resistors on each CS line. This ensures that CS lines are in a defined inactive (high) state when not actively driven low by the master, preventing accidental selection of slaves, particularly during system startup or if the master’s GPIOs are momentarily tri-stated. The internal pull-ups on ESP32 GPIOs can also be enabled, but external ones offer more robust control over the pull-up strength.
Practical Examples
Example 1: Communicating with Two Simulated SPI Devices
This example demonstrates how to configure the ESP32 to communicate with two different “simulated” SPI devices on the same bus. We’ll use two different Chip Select pins. To simulate distinct devices and verify correct selection, we’ll perform loopback tests (MOSI to MISO connection) and expect to receive the data we send only when the corresponding CS is active.
Hardware Setup (Loopback for Two “Devices”):
- You’ll need one ESP32 board.
- Connect the designated MOSI pin to the MISO pin (this creates a single loopback path).
- We will use two different GPIOs for CS signals (e.g., CS_A and CS_B).
- A logic analyzer connected to SCLK, MOSI, MISO, CS_A, and CS_B would be very helpful to observe the independent selection.
Project Setup (VS Code with ESP-IDF Extension):
- Create a new ESP-IDF project or use the one from Chapter 132.
- Ensure
main/CMakeLists.txtincludesspi_master,driver, andesp_loginREQUIRESorPRIV_REQUIRES.idf_component_register(SRCS "main.c" INCLUDE_DIRS "." REQUIRES spi_master driver esp_log) - Copy the following code into
main/main.c.
Code (main/main.c):
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "esp_log.h"
static const char *TAG = "SPI_MULTI_DEVICE_EXAMPLE";
// Define SPI host
#define SPI_HOST_ID SPI2_HOST // Using SPI2_HOST (HSPI/FSPI/GPSPI depending on variant)
// Common SPI bus pins
#define PIN_NUM_MOSI 23 // Example for ESP32 DevKitC
#define PIN_NUM_MISO 19 // Example for ESP32 DevKitC (Connect this to MOSI for loopback)
#define PIN_NUM_SCLK 18 // Example for ESP32 DevKitC
// CS pins for two different devices
#define PIN_NUM_CS_DEVICE_A 5 // Example for ESP32 DevKitC
#define PIN_NUM_CS_DEVICE_B 4 // Example, ensure this is a free GPIO
// SPI device handles
spi_device_handle_t spi_device_a;
spi_device_handle_t spi_device_b;
void app_main(void)
{
esp_err_t ret;
ESP_LOGI(TAG, "Initializing SPI bus (SPI%d_HOST)...", SPI_HOST_ID + 1); // SPI2_HOST is 1, SPI3_HOST is 2 etc. (driver specific enum)
// Configuration for the SPI bus (shared by both devices)
spi_bus_config_t buscfg = {
.mosi_io_num = PIN_NUM_MOSI,
.miso_io_num = PIN_NUM_MISO, // MISO is connected to MOSI for loopback
.sclk_io_num = PIN_NUM_SCLK,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.max_transfer_sz = 64 // Max transfer size in bytes
};
// Initialize the SPI bus
ret = spi_bus_initialize(SPI_HOST_ID, &buscfg, SPI_DMA_CH_AUTO);
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "SPI bus initialized.");
// --- Configure and add Device A ---
ESP_LOGI(TAG, "Adding Device A to SPI bus...");
spi_device_interface_config_t devcfg_a = {
.clock_speed_hz = 5 * 1000 * 1000, // Device A: Clock out at 5 MHz
.mode = 0, // Device A: SPI mode 0
.spics_io_num = PIN_NUM_CS_DEVICE_A, // CS pin for Device A
.queue_size = 3, // Queue 3 transactions for Device A
.input_delay_ns = 0, // Optional: MISO input delay
};
ret = spi_bus_add_device(SPI_HOST_ID, &devcfg_a, &spi_device_a);
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "Device A added.");
// --- Configure and add Device B ---
ESP_LOGI(TAG, "Adding Device B to SPI bus...");
spi_device_interface_config_t devcfg_b = {
.clock_speed_hz = 10 * 1000 * 1000, // Device B: Clock out at 10 MHz (different from A)
.mode = 1, // Device B: SPI mode 1 (different from A)
.spics_io_num = PIN_NUM_CS_DEVICE_B, // CS pin for Device B
.queue_size = 3, // Queue 3 transactions for Device B
.input_delay_ns = 0,
};
ret = spi_bus_add_device(SPI_HOST_ID, &devcfg_b, &spi_device_b);
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "Device B added.");
// --- Transaction with Device A ---
char send_buffer_a[32] = "Data for Device A";
char recv_buffer_a[32] = {0};
spi_transaction_t t_a;
memset(&t_a, 0, sizeof(t_a));
t_a.length = strlen(send_buffer_a) * 8; // Length in bits
t_a.tx_buffer = send_buffer_a;
t_a.rx_buffer = recv_buffer_a;
ESP_LOGI(TAG, "Performing transaction with Device A (CS: %d, Mode: %d, Speed: %d Hz)...",
devcfg_a.spics_io_num, devcfg_a.mode, devcfg_a.clock_speed_hz);
ESP_LOGI(TAG, "Device A Sending: %s", send_buffer_a);
ret = spi_device_polling_transmit(spi_device_a, &t_a); // Using polling transmit for simplicity
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "Device A Received: %s", recv_buffer_a);
if (memcmp(send_buffer_a, recv_buffer_a, strlen(send_buffer_a)) == 0) {
ESP_LOGI(TAG, "Loopback for Device A successful!");
} else {
ESP_LOGW(TAG, "Loopback for Device A failed or partial match.");
}
vTaskDelay(pdMS_TO_TICKS(100)); // Small delay
// --- Transaction with Device B ---
char send_buffer_b[32] = "Payload for Device B!";
char recv_buffer_b[32] = {0};
spi_transaction_t t_b;
memset(&t_b, 0, sizeof(t_b));
t_b.length = strlen(send_buffer_b) * 8; // Length in bits
t_b.tx_buffer = send_buffer_b;
t_b.rx_buffer = recv_buffer_b;
ESP_LOGI(TAG, "Performing transaction with Device B (CS: %d, Mode: %d, Speed: %d Hz)...",
devcfg_b.spics_io_num, devcfg_b.mode, devcfg_b.clock_speed_hz);
ESP_LOGI(TAG, "Device B Sending: %s", send_buffer_b);
ret = spi_device_polling_transmit(spi_device_b, &t_b);
ESP_ERROR_CHECK(ret);
ESP_LOGI(TAG, "Device B Received: %s", recv_buffer_b);
if (memcmp(send_buffer_b, recv_buffer_b, strlen(send_buffer_b)) == 0) {
ESP_LOGI(TAG, "Loopback for Device B successful!");
} else {
ESP_LOGW(TAG, "Loopback for Device B failed or partial match.");
}
// Note: In a real scenario with actual different slave devices,
// the loopback (MOSI to MISO) would not be shared. Each device would have its own MISO.
// For this simulation, we rely on the CS line ensuring only one "logical" device is active.
// With a logic analyzer, you'd see CS_A go low for the first transaction,
// and CS_B go low for the second, while SCLK/MOSI operate according to their respective device configs.
ESP_LOGI(TAG, "SPI multi-device example finished.");
// Optional: Remove devices and free bus if no longer needed
// ret = spi_bus_remove_device(spi_device_a); ESP_ERROR_CHECK(ret);
// ret = spi_bus_remove_device(spi_device_b); ESP_ERROR_CHECK(ret);
// ret = spi_bus_free(SPI_HOST_ID); ESP_ERROR_CHECK(ret);
}
Build Instructions (VS Code):
- Connect your ESP32 board. Ensure MOSI (e.g., GPIO23) is physically connected to MISO (e.g., GPIO19).
- In VS Code, select the correct ESP-IDF target and COM port.
- Build (
Ctrl+E B), Flash (Ctrl+E F), and Monitor (Ctrl+E M).
Run/Flash/Observe:
- The monitor output should show logs for initializing the bus, adding Device A, then Device B.
- It will then perform a transaction with Device A, print sent/received data, and then do the same for Device B.
- With the MOSI-MISO loopback, both transactions should report success.
- Using a Logic Analyzer: This is where the real verification happens for multi-device setups.
- Probe SCLK, MOSI, MISO,
PIN_NUM_CS_DEVICE_A, andPIN_NUM_CS_DEVICE_B. - During the transaction with Device A:
PIN_NUM_CS_DEVICE_Ashould go low.PIN_NUM_CS_DEVICE_Bshould remain high.- SCLK should run at 5 MHz (or as configured for Device A).
- SPI mode 0 waveforms should be visible.
- During the transaction with Device B:
PIN_NUM_CS_DEVICE_Bshould go low.PIN_NUM_CS_DEVICE_Ashould remain high.- SCLK should run at 10 MHz (or as configured for Device B).
- SPI mode 1 waveforms should be visible.
- Data on MOSI should match
send_buffer_aandsend_buffer_brespectively, and MISO should mirror MOSI due to the loopback.
- Probe SCLK, MOSI, MISO,
Tip: The
input_delay_nsfield inspi_device_interface_config_tcan be important for high-speed communication with certain slave devices. It tells the ESP32 SPI controller how long to wait after the SCLK edge before sampling the MISO line. For most devices and moderate speeds, 0 is fine. Consult the slave device datasheet if you encounter issues at high speeds.
Variant Notes
The principles of managing multiple SPI devices are consistent across ESP32 variants (ESP32, ESP32-S2, ESP32-S3, ESP32-C3, ESP32-C6, ESP32-H2) when using the spi_master driver.
- SPI Controllers:
- ESP32, ESP32-S2, ESP32-S3: Typically offer two general-purpose SPI controllers (
SPI2_HOST,SPI3_HOST). You can have multiple devices onSPI2_HOSTand/or multiple devices onSPI3_HOSTindependently. - ESP32-C3, ESP32-C6, ESP32-H2: Typically offer one general-purpose SPI controller (usually
SPI2_HOST, also named FSPI or GPSPI). All your user SPI peripherals would share this single bus.
- ESP32, ESP32-S2, ESP32-S3: Typically offer two general-purpose SPI controllers (
- GPIO Availability: The main constraint is the number of GPIO pins available for CS signals. Each device requires its own dedicated CS pin. RISC-V based variants (C-series, H-series) might have fewer GPIOs overall compared to the dual-core Xtensa variants (original ESP32, S2, S3), so plan your pin assignments carefully.
- DMA Channels:
SPI_DMA_CH_AUTOis generally effective. The number of available DMA channels for SPI might vary slightly, but the driver handles this. If you were to manage DMA channels manually (not recommended for beginners), you’d need to consult the TRM for specifics. - IOMUX vs. GPIO Matrix: All variants allow flexible routing of SPI signals (MOSI, MISO, SCLK, CS) to most GPIO pins via the GPIO matrix. While some pins might have default IOMUX functions for SPI, you are not strictly limited to them.
The example code uses SPI2_HOST. If you were using an ESP32 variant with SPI3_HOST available and wanted to use that instead (or in addition), you would initialize and add devices to SPI3_HOST similarly.
Common Mistakes & Troubleshooting Tips
| Mistake / Issue | Symptom(s) | Troubleshooting / Solution |
|---|---|---|
| Chip Select (CS) Pin Conflicts or Mismanagement |
|
|
| Using the Wrong Device Handle |
|
|
| Incorrect Device-Specific Configuration |
|
|
| MISO Line Contention |
|
|
| Exceeding Max Transfer Size (max_transfer_sz) |
|
|
Exercises
- Extend to Three Devices:
- Modify the provided practical example to include a third simulated SPI device (Device C).
- Assign a unique CS pin for Device C.
- Configure Device C with a different SPI mode (e.g., Mode 2) and clock speed (e.g., 1 MHz) than Devices A and B.
- Perform a transaction with Device C and verify its loopback data.
- If using a logic analyzer, observe the CS line, clock speed, and SPI mode for transactions with Device C.
- Alternating Device Communication:
- Write a program that continuously communicates with Device A and Device B in an alternating fashion within a loop (e.g., A, B, A, B,…).
- Introduce a small delay (e.g., 500ms) between each device’s transaction.
- Send slightly different data in each iteration to make observation easier (e.g., include a counter in the payload).
- Multi-Device System Design (Conceptual):
- Imagine you are building a device using an ESP32-S3. It needs to:
- Read temperature from a sensor that uses SPI Mode 0, max 2 MHz clock (e.g., a common thermocouple interface like MAX31855).
- Display this temperature on a small OLED display that uses SPI Mode 3, max 10 MHz clock (e.g., SSD1306 or SH1106 based).
- Log the temperature every minute to an SPI NOR Flash chip that uses SPI Mode 0, max 20 MHz clock (e.g., W25Q32).
- All three devices must share the same SPI bus (e.g.,
SPI2_HOST). - List the key parameters you would set in
spi_device_interface_config_tfor each of these three devices (spics_io_num(choose hypothetical GPIOs),mode,clock_speed_hz). - Briefly outline the functions you would need for interacting with each device (e.g.,
read_temperature(),display_update(),log_to_flash()).
- Imagine you are building a device using an ESP32-S3. It needs to:
- Impact of
queue_size:- The
queue_sizeparameter inspi_device_interface_config_tdetermines how many transactions can be queued for a device usingspi_device_queue_trans()beforespi_device_get_trans_result()is called. - Research or reason about how a larger
queue_sizemight be beneficial when dealing with multiple devices, especially if some devices are fast and others are slow, or if the CPU needs to perform other tasks between initiating SPI transfers. - Conversely, what are the resource implications (e.g., memory) of a larger
queue_size?
- The
Summary
- Multiple SPI slave devices can efficiently share a single SPI bus (MOSI, MISO, SCLK lines).
- Each slave device requires a unique Chip Select (CS) line, managed by the master, to enable communication with that specific device.
- The ESP-IDF
spi_masterdriver simplifies multi-device management by associating device-specific configurations (mode, speed, CS pin) with aspi_device_handle_t. - Transactions are targeted to a specific device by providing its unique handle to functions like
spi_device_transmit()orspi_device_polling_transmit(). - The ESP-IDF driver automatically handles asserting the correct CS line and configuring the SPI peripheral for the target device’s settings for each transaction.
- The primary constraint on the number of devices is the availability of GPIOs for CS lines.
- Careful pin assignment, correct handle usage, and accurate per-device configuration are key to successful multi-device SPI implementations.
Further Reading
- ESP-IDF SPI Master Driver Documentation:
- ESP-IDF SPI Master API Reference (Ensure to check for your specific ESP32 variant in the URL path if needed).
- ESP-IDF SPI Examples:
- Explore the examples in
$IDF_PATH/examples/peripherals/spi_master/, which may include more complex scenarios or interactions with specific types of SPI devices.
- Explore the examples in
- Datasheets of SPI Peripherals: When working with real SPI devices, their datasheets are invaluable for understanding command structures, SPI modes, timing requirements, and register maps.

