In this task, we are implementing that when a push-button connected to the Master microcontroller is pressed, the Slave microcontroller toggles an LED connected to it using I2C communication.
For this, we have to establish I2C communication between the master and the slave.
I²C (Inter-Integrated Circuit) is a serial communication protocol that uses only two lines
Key characteristics:
Note: When interfacing I²C devices operating at different voltages (e.g., 5 V ↔ 3.3 V), always use a voltage level shifter to ensure safe logic levels and reliable communication.
So, by connecting and configuring the master and slave devices and the I2C 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 for both as master and slave, which operates at a 3.3V logic level.
Circuit Diagram
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_I2C1_Init()
→ Configures I2C1.I2C Initialization (MX_I2C1_Init)
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000; // 100kHz
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
if (HAL_I2C_Init(&hi2c1) != HAL_OK) {
Error_Handler();
}
Initializes I2C1 peripheral with 100kHz clock speed, standard duty cycle, and 7-bit addressing mode.
GPIO Initialization(MX_GPIO_Init)
/*Configure GPIO pin : PA0 */
GPIO_InitStruct.Pin = GPIO_PIN_0;
GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
GPIO_InitStruct.Pull = GPIO_PULLUP;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
This code initializes GPIO pin PA0 as an input with a pull-up resistor enabled.
Private defines and Variables
#define SWITCH_PORT GPIOA
#define SWITCH_PIN GPIO_PIN_0
#define SLAVE_ADDRESS 0x08 // slave address
uint8_t ledState = 0; // LED state
// Switch variables
uint8_t g_debounceDuration = 50; // Minimum time to debounce button (in milliseconds)
uint8_t g_previousButtonState = 1; // Previous state of the button
uint8_t g_currentButtonState = 1; // Current state of the button
uint32_t g_lastDebounceTime = 0; // Time when button state last changed
Defines button pin (PA0) and slave address (0x08). Tracks LED state and debounce timing variables (50ms threshold).
Private Function
//Checks for a debounced button press and returns true if detected, false otherwise.
uint8_t debouncedButtonPressCheck(GPIO_TypeDef *GPIOx, uint16_t GPIO_Pin,
uint8_t expectedState) {
uint8_t buttonReading = HAL_GPIO_ReadPin(GPIOx, GPIO_Pin);
// If the button state has changed, reset the debounce timer
if (buttonReading != g_previousButtonState) {
g_lastDebounceTime = HAL_GetTick();
}
g_previousButtonState = buttonReading;
// If the state has remained stable beyond the debounce duration, consider it valid
if ((HAL_GetTick() - g_lastDebounceTime) > g_debounceDuration) {
if (buttonReading != g_currentButtonState) {
g_currentButtonState = buttonReading;
if (g_currentButtonState == expectedState) {
return 1; // Return true if the desired state is detected
}
}
}
return 0; // Return false if no valid press is detected
}
Reads button state, ignores noise for 50ms, and returns true if pressed. Ensures reliable button detection.
Main Loop Logic
while (1)
{
// check if switch is pressed or not
if (debouncedButtonPressCheck(SWITCH_PORT, SWITCH_PIN, 0))
{
ledState = !ledState;
HAL_I2C_Master_Transmit(&hi2c1, SLAVE_ADDRESS << 1, &ledState, 1, HAL_MAX_DELAY);
}
}
Checks for button presses. If pressed, toggles ledState
and sends it via I2C to the slave. Runs indefinitely.
Key HAL Functions Used
HAL_I2C_Init
, HAL_GPIO_ReadPin
, and HAL_I2C_Master_Transmit handle I2C
and GPIO. HAL_GetTick
manages debounce timing.
Expected Output
When the button is pressed, the master toggles ledState
and sends it to the slave. The slave should respond by toggling its LED.
The complete STM32CubeIDE project (including .ioc
configuration, main.c
, and HAL files) is available here:
📥 Download Project
Circuit Diagram
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_I2C1_Init()
→ Configures I2C1.I2C Initialization (Slave Mode)
hi2c1.Instance = I2C1;
hi2c1.Init.ClockSpeed = 100000;
hi2c1.Init.DutyCycle = I2C_DUTYCYCLE_2;
hi2c1.Init.OwnAddress1 = 16; //SLAVE_ADDRESS<<1 i.e. 0x08<<1
hi2c1.Init.AddressingMode = I2C_ADDRESSINGMODE_7BIT;
hi2c1.Init.DualAddressMode = I2C_DUALADDRESS_DISABLE;
hi2c1.Init.OwnAddress2 = 0;
hi2c1.Init.GeneralCallMode = I2C_GENERALCALL_DISABLE;
hi2c1.Init.NoStretchMode = I2C_NOSTRETCH_DISABLE;
if (HAL_I2C_Init(&hi2c1) != HAL_OK)
{
Error_Handler();
}
It initializes the I2C1 peripheral with specified parameters (100 kHz clock, 7-bit addressing, own address 0x08<<1, no dual address, no general call, and no clock stretching), and calls Error_Handler()
if initialization fails.
GPIO Initialization(MX_GPIO_Init)
/*Configure GPIO pin : PA6 */
GPIO_InitStruct.Pin = GPIO_PIN_6;
GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
This code initializes GPIO pin PA6 as a low-speed push-pull output with no pull-up/pull-down resistors.
The slave uses interrupt-driven I2C communication to listen for master commands:
Key Callbacks
HAL_I2C_AddrCallback
: Triggered when the slave address is matched.HAL_I2C_SlaveRxCpltCallback
: Executed when data reception completes.HAL_I2C_ListenCpltCallback
: Re-enables listening after each transaction.1. Address Match Callback
void HAL_I2C_AddrCallback(I2C_HandleTypeDef *hi2c, uint8_t TransferDirection, uint16_t AddrMatchCode) {
if (hi2c->Instance == I2C1 && TransferDirection == I2C_DIRECTION_TRANSMIT) {
HAL_I2C_Slave_Sequential_Receive_IT(hi2c, &receivedData, 1, I2C_LAST_FRAME);
}
}
Triggered when the master addresses this slave. If the direction is transmit (master writing), it prepares to receive 1 byte of data.
2. Receive Complete Callback
void HAL_I2C_SlaveRxCpltCallback(I2C_HandleTypeDef *hi2c) {
if (hi2c->Instance == I2C1) {
// Toggle LED based on received data (1=ON, 0=OFF)
HAL_GPIO_WritePin(LED_PORT, LED_PIN, (receivedData == 1) ? GPIO_PIN_SET : GPIO_PIN_RESET);
}
}
Executed after data reception finishes. Updates the LED state (ON/OFF) based on the received value (1 or 0).
3. Listen Complete Callback
void HAL_I2C_ListenCpltCallback(I2C_HandleTypeDef *hi2c) {
if (hi2c->Instance == I2C1) {
HAL_I2C_EnableListen_IT(hi2c); // Re-enable listening
}
}
Re-enables listening after each transaction to ensure continuous operation. Acts as a reset for the next I2C communication.
Main Loop
The slave runs in an infinite loop, relying entirely on I2C interrupts:
// Enable interrupt-based listening
HAL_I2C_EnableListen_IT(&hi2c1);
while (1) {
// All logic is handled in callbacks
}
Expected Behavior
Key HAL Functions Used
HAL_I2C_EnableListen_IT()
HAL_I2C_Slave_Sequential_Receive_IT()
HAL_GPIO_WritePin()
HAL_I2C_AddrCallback
→ Triggered on address match.HAL_I2C_SlaveRxCpltCallback
→ Called after data reception.HAL_I2C_ListenCpltCallback
→ Re-enables listening after transaction.Download Project
The complete STM32CubeIDE project (including .ioc
configuration and callbacks) is available here:
📥 Download Project
We are using the ESP32 DevKit v4 development board and programming it using the Arduino IDE.
In both ESP32 master and slave codes, the Wire.h
library is used for I²C communication.
By default, the ESP32 Arduino core assigns:
In the given task, we used the default I²C Pins.
Solution of the given task:
The I²C slave can be any microcontroller (e.g., Arduino UNO, ESP32, STM32) programmed with the same slave address (0x08). Only the slave code changes.
#include "Wire.h"
#define switchPin 15 // Push-button connected to GPIO 15
#define I2C_DEV_ADDR 0x08 // I2C Slave address
uint8_t ledState = 0; // Variable to hold LED state (0=OFF, 1=ON)
uint8_t error; // Error code for I2C transmission
unsigned long debounce_delay = 50; // Debounce time (ms)
int last_switch_state = 1; // Last button state (1=not pressed, 0=pressed)
int current_switch_state = 1; // Stable button state after debounce
unsigned long last_debounce_time = 0; // Last time button state changed
// Function to disable ESP32's internal pull-up/pull-down on I2C pins
void ensureNoInternalPullups(int sda = 21, int scl = 22) {
gpio_pullup_dis((gpio_num_t)sda); // Disable pull-up on SDA
gpio_pulldown_dis((gpio_num_t)sda); // Disable pull-down on SDA
gpio_pullup_dis((gpio_num_t)scl); // Disable pull-up on SCL
gpio_pulldown_dis((gpio_num_t)scl); // Disable pull-down on SCL
}
void setup() {
Serial.begin(115200); // Start serial monitor
pinMode(switchPin, INPUT_PULLUP); // Configure button with internal pull-up
Wire.begin(); // Initialize I2C as Master
ensureNoInternalPullups(); // Ensure no extra pull-ups/pull-downs
}
void loop() {
// Check if button is pressed with debounce handling
if (is_debounced_press(switchPin)) {
ledState = !ledState; // Toggle LED state variable
// Send LED state to slave via I2C
Wire.beginTransmission(I2C_DEV_ADDR);
Wire.write(ledState); // Send 1 byte (LED state)
uint8_t error = Wire.endTransmission(true);
// Check transmission status
if (error == 0) {
Serial.println("Successful data transmission");
} else {
Serial.println("ERROR in data transmission");
Serial.println(error); // Print error code
}
}
}
// Debounce function for button press detection
bool is_debounced_press(int switch_pin) {
int reading = digitalRead(switch_pin); // Read current button state
// If button state changed → reset debounce timer
if (reading != last_switch_state) {
last_debounce_time = millis();
}
last_switch_state = reading; // Update last read state
// If stable longer than debounce delay(50ms) → update confirmed state
if ((millis() - last_debounce_time) > debounce_delay) {
if (reading != current_switch_state) {
current_switch_state = reading;
// Return true if stable state is button press (LOW)
if (current_switch_state == 0) {
last_switch_state = reading;
return true;
}
}
}
return false; // No valid press detected
}
is_debounced_press()
checks the push-button input:
#include "Wire.h"
#define I2C_DEV_SLAVE_ADDR 0x08 // I2C slave address
#define LED_PIN 2 // LED connected to GPIO 2
// Callback: runs when master sends data
void onReceive(int len) {
while (Wire.available()) {
uint8_t received = Wire.read(); // Read received byte
// Control LED based on received value
if (received == 1) {
digitalWrite(LED_PIN, HIGH); // Turn LED ON
} else if (received == 0) {
digitalWrite(LED_PIN, LOW); // Turn LED OFF
}
}
}
// Disable ESP32’s internal pull-ups/pull-downs on I2C pins
void ensureNoInternalPullups(int sda = 21, int scl = 22) {
gpio_pullup_dis((gpio_num_t)sda);
gpio_pulldown_dis((gpio_num_t)sda);
gpio_pullup_dis((gpio_num_t)scl);
gpio_pulldown_dis((gpio_num_t)scl);
}
void setup() {
Serial.begin(115200); // Start serial monitor
Serial.setDebugOutput(true); // Enable debug output (optional)
Wire.onReceive(onReceive); // Register receive event handler
Wire.begin((uint8_t)I2C_DEV_SLAVE_ADDR); // Start I2C in slave mode
ensureNoInternalPullups(); // Disable internal pulls
pinMode(LED_PIN, OUTPUT); // Set LED pin as output
}
void loop() {
// Nothing here; slave responds only when master sends data
}
onReceive
is a callback function that runs automatically whenever the master sends data.loop()
does nothing because the slave only reacts when data arrives from the master.We are using the Arduino UNO development board and programming it using the Arduino IDE.
In both Arduino UNO master and slave codes, the Wire.h
library is used for I²C communication.
Solution of the given task:
First, let's establish the hardware connection.
Circuit Diagram
#include <Wire.h>
#define slaveAddress 0x08 // slave address
#define switchPin 2
uint8_t ledState = 0; // LED state
uint8_t error;
unsigned long debounce_delay = 50; // Debounce delay in milliseconds
int last_switch_state = 1; // Previous switch state
int current_switch_state = 1; // Current switch state
unsigned long last_debounce_time = 0; // Timestamp of the last switch state change
void setup() {
Serial.begin(115200);
// Initialize arduino as I2C master
Wire.begin();
// Disable internal pull-ups on SDA/SCL
pinMode(A4, INPUT); // SDA
pinMode(A5, INPUT); // SCL
digitalWrite(A4, LOW); // ensure pull-up is off
digitalWrite(A5, LOW); // ensure pull-up is off
pinMode(switchPin, INPUT_PULLUP);
}
void loop() {
// check if switch is pressed or not
if (is_debounced_press(switchPin)) {
ledState = !(ledState);
Wire.beginTransmission(slaveAddress);
Wire.write(ledState); // Store the state of LED in i2c buffer
error = Wire.endTransmission(); // send data to slave
if (error == 0) {
Serial.println("Successful data transmission");
} else {
Serial.println("ERROR in data transmission");
}
}
}
bool is_debounced_press(int switch_pin) {
int reading = digitalRead(switch_pin);
// If the button state has changed, reset the debounce timer
if (reading != last_switch_state) {
last_debounce_time = millis();
}
last_switch_state = reading; // Update the previous state
// If the button state is stable for longer than the debounce delay, update the state.
if ((millis() - last_debounce_time) > debounce_delay) {
if (reading != current_switch_state) {
current_switch_state = reading;
// Return true if the button is pressed (LOW state)
if (current_switch_state == 0) {
last_switch_state = reading;
return true;
}
}
}
return false; // No valid press detected
}
Wire.begin()
: Initializes the Arduino as I²C master.is_debounced_press()
ensures that noise or mechanical bounce does not cause multiple false triggers.ledState
. Wire.beginTransmission(slaveAddress)
to start communication with the slave.Wire.write(ledState)
sends the new LED state (0 or 1).Wire.endTransmission()
completes the transaction and returns an error code (0 = success).First, let's establish the hardware connection.
Circuit Diagram
#include <Wire.h>
#define ledPin 8
#define slaveAddress 0x08
void setup() {
// Initialize arduino as I2C slave (Slave address is 0x08)
Wire.begin(slaveAddress);
// Disable internal pull-ups on SDA/SCL
pinMode(A4, INPUT); // SDA
pinMode(A5, INPUT); // SCL
digitalWrite(A4, LOW); // ensure pull-up is off
digitalWrite(A5, LOW); // ensure pull-up is off
Wire.onReceive(receiveEvent); // Attach a function to handle incoming data
pinMode(ledPin, OUTPUT);
}
void loop() {
}
// Function to handle incoming data
void receiveEvent(int bytes) {
while (Wire.available()) {
int received = Wire.read(); // Read the incoming byte
if (received == 1) {
digitalWrite(ledPin, HIGH);
} else if (received == 0) {
digitalWrite(ledPin, LOW);
}
}
}
Wire.begin(slaveAddress):
Sets the device as an I²C slave with address 0x08.Wire.onReceive():
Registers the function receiveEvent()
to handle incoming data.receiveEvent(int bytes):
Wire.read()
.