In this task, we'll set up SPI communication between two microcontrollers, one acting as the master and the other as the slave. The goal is to toggle an LED connected to the slave microcontroller by pressing a push-button connected to the master.
Components Needed
Push-Button (Master Side)
LED (Slave Side)
SPI Connections between master and slave
Master Pin | Slave Pin |
---|---|
SCK(Clock) | SCK |
MOSI(Master Out Slave In) | MOSI |
MISO(Master In Slave Out) | MISO |
SC/CS(Slave Select) | SS |
Note: If you only need one-way communication (Master → Slave), you can omit the MISO line.
Important: The CS/SS line also needs level shifting if coming from a higher voltage device.
How It Works
Expected Behavior
Implementation (Pseudocode)
Master (pseudocode)
Slave (pseudocode)
Best Practices
So, by connecting and configuring the push-button switch, LED, and SPI communication, we can implement the task.
Below are the solutions to the given task using different microcontrollers
We’re using an STM32 NUCLEO-F103RB board both as master and slave, which operates at a 3.3V logic level.
Common SPI Lines
Master Board (NUCLEO-F103RB)
Slave Board (NUCLEO-F103RB)
Circuit Diagram
A) STM32 as a Master:
B) STM32 as a Slave:
Project Setup in STM32CubeIDE
SystemClock_Config
).SPI_MODE_MASTER
)SPI_DATASIZE_8BIT
)SPI_POLARITY_LOW
)SPI_PHASE_1EDGE
)SPI_NSS_SOFT
)SPI_FIRSTBIT_MSB
)HAL_Init()
→ Initializes HAL and system tick.SystemClock_Config()
→ Configures system clock (HSI + PLL).MX_GPIO_Init()
→ Initializes GPIO ports.MX_USART2_UART_Init()
→ Configures UART2.MX_SPI2_Init()
→ Configures SPI2.SPI Initialization (Generated by CubeMX)
/* SPI2 parameter configuration*/
hspi2.Instance = SPI2;
hspi2.Init.Mode = SPI_MODE_MASTER;
hspi2.Init.Direction = SPI_DIRECTION_2LINES;
hspi2.Init.DataSize = SPI_DATASIZE_8BIT;
hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi2.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi2.Init.NSS = SPI_NSS_SOFT;
hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_32;
hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi2.Init.CRCPolynomial = 10;
if (HAL_SPI_Init(&hspi2) != HAL_OK)
{
Error_Handler();
}
Initializes SPI2 in master mode with 2-line (MOSI) communication, 8-bit data, low polarity, 1st edge clock phase, software-controlled chip select (CS), and a baud rate prescaler of 32.
Header includes and Private defines
#include <stdbool.h>
#define SPI2_CS_PIN GPIO_PIN_12
#define SPI2_CS_PORT GPIOB
#define SWITCH_PIN GPIO_PIN_0
#define SWITCH_PORT GPIOA
Defines pin and port mappings for SPI2 chip select (CS) and a switch input (GPIO).
SPI Transmission
void SPI2_TransmitByte(uint8_t data) {
SPI2_CS_Enable(true); // Pull CS low
HAL_SPI_Transmit(&hspi2, &data, 1, HAL_MAX_DELAY);
SPI2_CS_Enable(false); // Pull CS high
}
Transmits a single byte over SPI2, toggling the CS pin (low before, high after transmission).
CS Control
void SPI2_CS_Enable(bool enable) {
HAL_GPIO_WritePin(SPI2_CS_PORT, SPI2_CS_PIN, enable ? GPIO_PIN_RESET : GPIO_PIN_SET);
}
Controls the SPI2 CS pin state (low = enable, high = disable).
Main Loop (Button-Triggered SPI Transmission)
uint8_t txData = 0x00;
while (1) {
if (HAL_GPIO_ReadPin(SWITCH_PORT, SWITCH_PIN) == GPIO_PIN_RESET) {
SPI2_TransmitByte(txData); // Send data
txData ^= 1; // Toggle between 0x00 and 0x01
HAL_Delay(300); // Simple debounce
}
}
Checks a button press; if triggered, sends a toggling byte (0x00/0x01
) via SPI and adds a 300ms debounce delay.
Expected Output
0x00 or 0x01
alternately over SPI.The complete STM32CubeIDE project (including .ioc
configuration, main.c
, and HAL files) is available here:
📥 Download Project
Project Setup in STM32CubeIDE
SystemClock_Config
).HAL_Init()
→ Initializes HAL and system tick.SystemClock_Config()
→ Configures system clock (HSI + PLL).MX_GPIO_Init()
→ Initializes GPIO ports.MX_USART2_UART_Init()
→ Configures UART2.MX_SPI2_Init()
→ Configures SPI2.SPI Initialization (Generated by CubeMX)
/* SPI2 parameter configuration*/
hspi2.Instance = SPI2;
hspi2.Init.Mode = SPI_MODE_SLAVE;
hspi2.Init.Direction = SPI_DIRECTION_2LINES_RXONLY;
hspi2.Init.DataSize = SPI_DATASIZE_8BIT;
hspi2.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi2.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi2.Init.NSS = SPI_NSS_HARD_INPUT;
hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi2.Init.TIMode = SPI_TIMODE_DISABLE;
hspi2.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi2.Init.CRCPolynomial = 10;
if (HAL_SPI_Init(&hspi2) != HAL_OK)
{
Error_Handler();
}
Initializes SPI2 in slave mode with 2-line receive-only communication, 8-bit data, low polarity, 1st edge clock phase, and hardware-controlled chip select.
Private defines
#define LED_PORT GPIOA
#define LED_PIN GPIO_PIN_6
#define SPI_SLAVE_NSS_PIN GPIO_PIN_12
#define SPI_SLAVE_NSS_PORT GPIOB
Defines pin and port mappings for SPI2 chip select (CS) and an LED output (GPIO).
SPI Reception Callback
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
dataReady = 1;
// Restart reception
HAL_SPI_Receive_IT(&hspi2, &receivedData, 1);
}
Handles received data and immediately restarts reception in interrupt mode.
Main Loop (Button-Triggered SPI Transmission)
while (1) {
if (dataReady) {
dataReady = 0;
// Control LED based on received data
if (receivedData == 0x01) {
HAL_GPIO_WritePin(LED_PORT, LED_PIN, 1); // LED ON
} else if (receivedData == 0x00) {
HAL_GPIO_WritePin(LED_PORT, LED_PIN, 0); // LED OFF
}
}
HAL_Delay(1);
}
Controls an LED based on received SPI data (0x01 turns LED on, 0x00 turns it off).
Expected Output
The complete STM32CubeIDE project (including .ioc
configuration, main.c
, and HAL files) is available here:
📥 Download Project
We are using the ESP32 DevKit v4 development board and programming it using the Arduino IDE.
Solution of the given task:
In the ESP32 master code, the SPI.h
library is used for SPI communication.
Master Code
#include <SPI.h>
// Pin definitions
#define SS_PIN 5 // Slave Select (CS) pin
#define SWITCH_PIN 15 // Push button pin
// State variables
bool ledState = false; // LED ON/OFF state to send
const uint8_t DEBOUNCE_DELAY = 50; // Debounce time in ms
// Button state tracking
unsigned long lastDebounceTime = 0;
uint8_t lastButtonState = HIGH; // Last stable button state
uint8_t buttonState = HIGH; // Current button state
void setup() {
pinMode(SS_PIN, OUTPUT); // SS pin as output
pinMode(SWITCH_PIN, INPUT_PULLUP); // Button with pull-up
Serial.begin(115200); // Debug output
// Initialize SPI (SCLK=18, MISO=19, MOSI=23, SS=5)
SPI.begin(18, 19, 23, SS_PIN);
digitalWrite(SS_PIN, HIGH); // Keep slave deselected
}
void loop() {
// If button is pressed after debounce
if (isButtonPressed()) {
ledState = !ledState; // Toggle state
digitalWrite(SS_PIN, LOW); // Select slave
// Send command: 0x01 = LED ON, 0x00 = LED OFF
SPI.transfer(ledState ? 0x01 : 0x00);
Serial.printf("Sent: 0x%02X\n", ledState ? 0x01 : 0x00);
digitalWrite(SS_PIN, HIGH); // Deselect slave
}
}
// Debounce function for push button
bool isButtonPressed() {
int reading = digitalRead(SWITCH_PIN);
if (reading != lastButtonState) { // If state changed
lastDebounceTime = millis(); // Reset debounce timer
}
lastButtonState = reading;
// If stable for longer than debounce delay
if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
if (reading != buttonState) { // New stable state
buttonState = reading;
if (buttonState == LOW) { // Button pressed
return true;
}
}
}
return false;
}
Code Explanation
Setup
digitalWrite(SS_PIN, HIGH);
Loop
ledState
(ON ↔ OFF).digitalWrite(SS_PIN, LOW);
).0x01
if ON, 0x00
if OFF via SPI.transfer()
. digitalWrite(SS_PIN, HIGH);
isButtonPressed()
DEBOUNCE_DELAY.
For the slave code, we used #include "driver/spi_slave.h"
driver file.
Slave Code
#include "driver/spi_slave.h"
// Pin definitions
#define LED_PIN 15 // LED output pin
#define PIN_MOSI 23 // Master Out, Slave In(MOSI)
#define PIN_MISO 19 // Master In, Slave Out(MIS)
#define PIN_SCLK 18 // Serial Clock
#define PIN_CS 5 // Chip Select (from master)
// Buffers
uint8_t recv_buf[1]; // Data received from master
uint8_t send_buf[1] = {0xAA}; // Dummy byte to send back
uint8_t data = 0; // Stores received byte
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH); // Turn LED on at start
// Configure SPI bus pins
spi_bus_config_t buscfg = {
.mosi_io_num = PIN_MOSI,
.miso_io_num = PIN_MISO,
.sclk_io_num = PIN_SCLK,
.quadwp_io_num = -1, // Not used
.quadhd_io_num = -1, // Not used
.max_transfer_sz = 1 // Max transfer size = 1 byte
};
// Configure SPI slave interface
spi_slave_interface_config_t slvcfg = {
.spics_io_num = PIN_CS, // CS pin
.flags = 0,
.queue_size = 1, // One transaction at a time
.mode = 0, // SPI mode 0 (CPOL=0, CPHA=0)
.post_setup_cb = NULL,
.post_trans_cb = NULL
};
// Initialize SPI slave driver
if (spi_slave_initialize(VSPI_HOST, &buscfg, &slvcfg, SPI_DMA_DISABLED) != ESP_OK) {
Serial.println("SPI Slave init failed!");
return;
}
Serial.println("SPI Slave Ready");
}
void loop() {
// Define SPI transaction (1 byte in/out)
spi_slave_transaction_t trans = {
.length = 8, // 8 bits
.tx_buffer = send_buf, // Send dummy byte
.rx_buffer = recv_buf // Receive data
};
// Block until master sends data
if (spi_slave_transmit(VSPI_HOST, &trans, portMAX_DELAY) == ESP_OK) {
data = recv_buf[0]; // Store received byte
Serial.printf("Received: 0x%02X\n", data);
// LED control: ON if 0x01 received, else OFF
digitalWrite(LED_PIN, data == 0x01 ? HIGH : LOW);
}
}
Code Explanation
Setup Loop
recv_buf
), one for sending (send_buf = 0xAA
).Loop Loop
spi_slave_transmit
).recv_buf[0].
0x01
, LED turns ON; otherwise, LED turns OFF. 0xAA
to the master (dummy response).We are using the Arduino UNO development board and programming it using the Arduino IDE.
In both Arduino UNO master and slave codes, the SPI.h
library is used for SPI communication.
Solution of the given task:
First, let's establish the hardware connection.
Circuit Diagram
ledState
(ON/OFF) and sends 0x01(ON) or 0x00(OFF) to the slave via SPI.Code (Master)
#include <SPI.h>
#define SS_PIN 10 // Slave Select pin for SPI
#define SWITCH_PIN 2 // Push button pin
// Variables for button handling
bool ledState = false; // Tracks the LED state (ON/OFF)
const uint8_t DEBOUNCE_DELAY = 50; // Debounce delay in milliseconds
// Button state variables
unsigned long lastDebounceTime = 0; // Last recorded debounce time
uint8_t lastButtonState = HIGH; // Last stable state of the button
uint8_t buttonState = HIGH; // Current state of the button
void setup() {
pinMode(SS_PIN, OUTPUT); // Set Slave Select (SS) pin as output
pinMode(SWITCH_PIN, INPUT_PULLUP); // Set push button pin as input with pull-up resistor
// Initialize SPI communication
SPI.begin(); // Start SPI
digitalWrite(SS_PIN, HIGH); // Ensure SS is HIGH (deselect the slave)
Serial.begin(115200);
}
void loop() {
// Check if the button is pressed
if (isButtonPressed()) {
// Toggle the LED state
ledState = !ledState;
// Begin SPI communication with the slave
digitalWrite(SS_PIN, LOW); // Pull SS low to select the slave
if (ledState) {
SPI.transfer(0x01); // Send ON command (0x01) to the slave
} else {
SPI.transfer(0x00); // Send OFF command (0x00) to the slave
}
digitalWrite(SS_PIN, HIGH); // Deselect the slave by setting SS high
}
}
/**
* Checks if the button is pressed with debouncing.
*
* @return true if a valid button press is detected, false otherwise.
*/
bool isButtonPressed() {
int reading = digitalRead(SWITCH_PIN);
// If the button state has changed since the last read
if (reading != lastButtonState) {
lastDebounceTime = millis(); // Update debounce timer
}
lastButtonState = reading;
// If the button state remains stable for the debounce delay period
if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) {
// If the button state has changed
if (reading != buttonState) {
buttonState = reading;
// Return true if the button is pressed (LOW state)
if (buttonState == LOW) {
return true;
}
}
}
// Return false if no valid press is detected
return false;
}
Code Explanation(Master)
SWITCH_PIN
) as input with an internal pull-up resistor.SS_PIN
) as an output and ensures it starts in the inactive state (HIGH). isButtonPressed()
function.isButtonPressed()
)DEBOUNCE_DELAY
).First, let's establish the hardware connection.
Arduino UNO Circuit Diagram
#include <SPI.h>
#define LED_PIN 3 // LED connected to Pin 3
volatile byte receivedData = 0; // Variable to store received data from SPI
bool flag = 0; // Flag to indicate data received
void setup() {
pinMode(LED_PIN, OUTPUT); // Set LED pin as output
digitalWrite(LED_PIN, LOW); // Ensure LED is OFF initially
Serial.begin(115200);
// Configure SPI in slave mode
pinMode(MISO, OUTPUT); // Set MISO (Master In Slave Out) as output
SPCR |= _BV(SPE); // Enable SPI in slave mode
// Attach SPI interrupt for data reception
SPI.attachInterrupt(); // Enable SPI interrupt
}
/**
* Interrupt Service Routine (ISR) for SPI communication
* This gets triggered whenever data is received via SPI.
*/
ISR(SPI_STC_vect) {
receivedData = SPDR; // Read received data from SPI Data Register
flag = 1; // Set flag to indicate data received
}
void loop() {
// Check if data has been received
if (flag) {
// Control LED based on received data
if (receivedData == 0x01) {
digitalWrite(LED_PIN, HIGH); // Turn ON LED if data is 0x01
} else if (receivedData == 0x00) {
digitalWrite(LED_PIN, LOW); // Turn OFF LED if data is 0x00
}
flag = 0; // Reset flag after processing the data
}
}
1. Setup:
2. Interrupt Routine:
SPDR
) and sets the flag to indicate data reception.3. Main Loop:
loop()
function continuously checks the flag.Hardware Setup
We can see in the video, that as we click the button on the master the LED connected to the slave is toggling.