Hi everyone,
I'm trying to get I2C communication working between an ESP32-S3 (master) and an ESP32-C5 (slave). I'm using a Waveshare ESP32-S3 with 1.91-inch TFT as the master, and an ESP32-C5 MINI as the slave.
The slave code is attached below. It's a Wi-Fi sniffer that also acts as an I2C slave to expose device lists and control flood mode via I2C commands.
The Problem:
The ESP32-C5 slave never responds to the master's I2C requests. The master's Wire.requestFrom() always returns 0 bytes. I've verified:
-
Wiring (SDA=GPIO15, SCL=GPIO7 on C5; matching pins on S3)
-
Pull-up resistors (4.7kΩ to 3.3V)
-
Common ground
-
Both 100kHz and 400kHz bus speeds
-
Slave address 0x08
Slave Code Snippet (ESP32-C5):
cpp
#define I2C_SDA 15
#define I2C_SCL 7
#define I2C_SLAVE_ADDR 0x08
void initI2C() {
Wire.end();
Wire.begin(I2C_SLAVE_ADDR, I2C_SDA, I2C_SCL);
Wire.onReceive(i2cOnReceive);
Wire.onRequest(i2cOnRequest);
ESP_LOGI("I2C", "I2C slave started, address 0x%02X", I2C_SLAVE_ADDR);
}
The onRequest callback prepares a response buffer and writes it via Wire.write().
What I've Tried:
-
Using
Wire.setPins()beforeWire.begin() -
Different GPIO pins
-
Adding delays after
Wire.begin() -
The
Wire.slaveWrite()pre-load approach
None of these worked.
Has anyone successfully used ESP32-C5 as an I2C slave with the Arduino Wire library?
I found a few reports of similar issues:
-
An esp32.com thread describing the exact same symptom – master reads 0 bytes from C5 slave
-
GitHub issue #12739 where the C5 slave never responds, traced to missing clock enable in the Arduino core's I2C slave initialization
-
Espressif documentation notes that I2C Slave v1.0 has known issues and recommends using Slave Driver v2.0 via
CONFIG_I2C_ENABLE_SLAVE_DRIVER_VERSION_2
The GitHub issue suggests that on the C5, the I2C peripheral clocks aren't being enabled by the Wire library, so register writes silently fail. Manually enabling the clocks before Wire.begin() reportedly helps, but the address-ACK still doesn't complete.
Has anyone found a reliable workaround for this? Is this fixed in a newer version of the Arduino core (I'm using the latest from board manager). Or do I need to drop down to ESP-IDF and use the Slave Driver v2.0?
Any help would be greatly appreciated!
Thanks,
[SUNSET]
Full slave code for reference:
#include <WiFi.h>
#include <esp_wifi.h>
#include <esp_log.h>
#include <cstring>
#include <cstdlib>
#include <Wire.h>
// ========================== Macros ==========================
#define ENABLE_FLOOD 1
#define FLOOD_FRAME_TYPE 2
#define MAX_DEVICES 20
#define INACTIVE_TIMEOUT_MS 5000
#define PRINT_INTERVAL_MS 500
#define CHANNEL_SWITCH_INTERVAL_MS 10
// I2C slave pins and address
#define I2C_SDA 15
#define I2C_SCL 7
#define I2C_SLAVE_ADDR 0x08
// I2C command codes
#define CMD_SET_SRC_MAC_2G 0x01
#define CMD_SET_SRC_MAC_5G 0x02
#define CMD_SET_DST_MAC 0x03
#define CMD_SET_FLOOD_MODE 0x04
#define CMD_GET_DEV_COUNT 0x05
#define CMD_GET_DEV_LIST 0x06
#define CMD_SET_FLOOD_CHANNELS 0x07 // NEW
#define MAX_FLOOD_CHANNELS 20
// ============================================================
static const int scan_channels[] = {
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13,
36, 40, 44, 48, 52, 56, 60, 64, 100, 104, 108, 112, 116, 120, 124, 128, 132, 136, 140, 149, 153, 157, 161, 165
};
#define CHANNEL_COUNT (sizeof(scan_channels) / sizeof(scan_channels[0]))
struct device_info {
uint8_t mac[6];
int8_t rssi;
uint32_t last_seen;
uint16_t channel;
uint8_t frame_type;
};
static device_info devices[MAX_DEVICES];
static uint8_t device_count = 0;
static portMUX_TYPE mux = portMUX_INITIALIZER_UNLOCKED;
static volatile uint16_t g_current_channel = scan_channels[0];
#ifdef ENABLE_FLOOD
// Dynamic flood channel list (default: 11, 12, 36)
uint8_t flood_channels[MAX_FLOOD_CHANNELS] = { 0, 1, 2 };
uint8_t flood_channel_count = 3; // must be <= MAX_FLOOD_CHANNELS
// Flood source MACs (writable via I2C)
uint8_t flood_src_mac_2P4G[6] = { 0x00 };
uint8_t flood_src_mac_5P0G[6] = { 0x00 };
// Destination MAC (writable via I2C, default broadcast)
uint8_t flood_dst_mac[6] = { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF };
bool flood_mode = false;
uint32_t flood_packet_count = 0;
#endif
// I2C response state and TX buffer
static volatile uint8_t i2c_response_type = 0; // 0=none, 1=count, 2=list
static uint8_t i2c_tx_buffer[201]; // 1 + 20*10
static size_t i2c_tx_len = 0;
// ============================================================
extern "C" int ieee80211_raw_frame_sanity_check(int32_t arg, int32_t, int32_t) {
return 0;
}
// -------------------- Frame type decoder --------------------
static const char *getFrameTypeString(uint8_t encoded) {
uint8_t type = (encoded >> 4) & 0x03;
uint8_t subtype = encoded & 0x0F;
switch (type) {
case 0:
switch (subtype) {
case 0: return "AssocReq";
case 1: return "AssocResp";
case 2: return "ReassocReq";
case 3: return "ReassocResp";
case 4: return "ProbeReq";
case 5: return "ProbeResp";
case 6: return "Beacon";
case 7: return "ATIM";
case 8: return "Disassoc";
case 9: return "Auth";
case 10: return "Deauth";
case 11: return "Action";
case 12: return "ActionNoAck";
default: return "Mgmt?";
}
case 1:
switch (subtype) {
case 0: return "RTS";
case 1: return "CTS";
case 2: return "ACK";
case 3: return "CF-End";
case 4: return "CF-End+ACK";
case 5: return "PS-Poll";
case 6: return "BlockAckReq";
case 7: return "BlockAck";
default: return "Ctrl?";
}
case 2:
switch (subtype) {
case 0: return "Data";
case 1: return "Data+CF-ACK";
case 2: return "Data+CF-Poll";
case 3: return "Data+CF-ACK+CF-Poll";
case 4: return "Null";
case 5: return "CF-ACK";
case 6: return "CF-Poll";
case 7: return "CF-ACK+CF-Poll";
case 8: return "QoS Data";
case 9: return "QoS Data+CF-ACK";
case 10: return "QoS Data+CF-Poll";
case 11: return "QoS Data+CF-ACK+CF-Poll";
case 12: return "QoS Null";
default: return "Data?";
}
default: return "Invalid";
}
}
// -------------------- Sniffer callback --------------------
void sniffer_cb(void *buf, wifi_promiscuous_pkt_type_t type) {
#ifdef ENABLE_FLOOD
if (flood_mode) return;
#endif
wifi_promiscuous_pkt_t *pkt = (wifi_promiscuous_pkt_t *)buf;
int8_t rssi = pkt->rx_ctrl.rssi;
uint8_t *payload = (uint8_t *)pkt->payload;
uint8_t *mac = payload + 10;
// Ignore broadcast
if (mac[0] == 0xFF && mac[1] == 0xFF && mac[2] == 0xFF && mac[3] == 0xFF && mac[4] == 0xFF && mac[5] == 0xFF) {
return;
}
uint8_t fc0 = payload[0];
uint8_t frame_type_val = (fc0 & 0x0C) >> 2;
uint8_t subtype = (fc0 & 0xF0) >> 4;
uint8_t encoded_type = (frame_type_val << 4) | subtype;
uint16_t channel = g_current_channel;
portENTER_CRITICAL(&mux);
bool found = false;
for (int i = 0; i < device_count; i++) {
if (memcmp(devices[i].mac, mac, 6) == 0) {
devices[i].rssi = rssi;
devices[i].last_seen = millis();
devices[i].channel = channel;
devices[i].frame_type = encoded_type;
found = true;
break;
}
}
if (!found) {
if (device_count < MAX_DEVICES) {
memcpy(devices[device_count].mac, mac, 6);
devices[device_count].rssi = rssi;
devices[device_count].last_seen = millis();
devices[device_count].channel = channel;
devices[device_count].frame_type = encoded_type;
device_count++;
} else {
// Replace oldest
int oldest = 0;
uint32_t min_time = devices[0].last_seen;
for (int i = 1; i < MAX_DEVICES; i++) {
if (devices[i].last_seen < min_time) {
min_time = devices[i].last_seen;
oldest = i;
}
}
memcpy(devices[oldest].mac, mac, 6);
devices[oldest].rssi = rssi;
devices[oldest].last_seen = millis();
devices[oldest].channel = channel;
devices[oldest].frame_type = encoded_type;
}
}
portEXIT_CRITICAL(&mux);
}
// -------------------- WiFi initialization --------------------
void initWiFiSniffer() {
WiFi.mode(WIFI_MODE_STA);
wifi_country_t country = {
.cc = "00",
.schan = 1,
.nchan = 165,
.policy = WIFI_COUNTRY_POLICY_AUTO
};
esp_wifi_set_country(&country);
esp_wifi_set_promiscuous(true);
esp_wifi_set_promiscuous_rx_cb(&sniffer_cb);
esp_wifi_set_channel(scan_channels[0], WIFI_SECOND_CHAN_NONE);
g_current_channel = scan_channels[0];
ESP_LOGI("SNIFF", "Wi-Fi sniffer started on channel %d", scan_channels[0]);
#ifdef ENABLE_FLOOD
ESP_LOGI("FLOOD", "Flood enabled, 2.4G MAC: %02X:%02X:%02X:%02X:%02X:%02X",
flood_src_mac_2P4G[0], flood_src_mac_2P4G[1], flood_src_mac_2P4G[2],
flood_src_mac_2P4G[3], flood_src_mac_2P4G[4], flood_src_mac_2P4G[5]);
ESP_LOGI("FLOOD", "5G MAC: %02X:%02X:%02X:%02X:%02X:%02X",
flood_src_mac_5P0G[0], flood_src_mac_5P0G[1], flood_src_mac_5P0G[2],
flood_src_mac_5P0G[3], flood_src_mac_5P0G[4], flood_src_mac_5P0G[5]);
ESP_LOGI("FLOOD", "Flood channels: ");
for (int i = 0; i < flood_channel_count; i++) {
Serial.printf("%d ", flood_channels[i]);
}
Serial.println();
#endif
}
// -------------------- Device management --------------------
void cleanInactiveDevices() {
portENTER_CRITICAL(&mux);
uint32_t now = millis();
int i = 0;
while (i < device_count) {
if (now - devices[i].last_seen > INACTIVE_TIMEOUT_MS) {
device_count--;
if (i < device_count) {
memcpy(&devices[i], &devices[device_count], sizeof(device_info));
}
} else {
i++;
}
}
portEXIT_CRITICAL(&mux);
}
void printDevices() {
portENTER_CRITICAL(&mux);
ESP_LOGI("SNIFF", "=== Devices (%d) ===", device_count);
for (int i = 0; i < device_count; i++) {
const char *band = (devices[i].channel <= 14) ? "2.4G" : "5G";
const char *ftype = getFrameTypeString(devices[i].frame_type);
ESP_LOGI("SNIFF",
"MAC: %02X:%02X:%02X:%02X:%02X:%02X, RSSI: %d, Chan: %d (%s), Type: %s, Last: %lu ms ago",
devices[i].mac[0], devices[i].mac[1], devices[i].mac[2],
devices[i].mac[3], devices[i].mac[4], devices[i].mac[5],
devices[i].rssi, devices[i].channel, band, ftype,
millis() - devices[i].last_seen);
}
portEXIT_CRITICAL(&mux);
}
// -------------------- Flood frame sending --------------------
#ifdef ENABLE_FLOOD
static void send_flood_frame(int channel) {
if (!flood_mode) return;
uint8_t frame[128];
memset(frame, 0, sizeof(frame));
int frame_len = 0;
const uint8_t *dst_addr = flood_dst_mac;
uint8_t *src_mac = (channel <= 14) ? flood_src_mac_2P4G : flood_src_mac_5P0G;
#if (FLOOD_FRAME_TYPE == 1)
frame[0] = 0xD0;
frame[1] = 0x00;
memcpy(&frame[4], dst_addr, 6);
memcpy(&frame[10], src_mac, 6);
memcpy(&frame[16], src_mac, 6);
int pos = 24;
frame[pos++] = 0x00;
frame[pos++] = 0x00;
frame_len = pos;
#elif (FLOOD_FRAME_TYPE == 2)
frame[0] = 0xC0;
frame[1] = 0x00;
frame[2] = 0x00;
frame[3] = 0x00;
memcpy(&frame[4], dst_addr, 6);
memcpy(&frame[10], src_mac, 6);
memcpy(&frame[16], src_mac, 6);
frame[22] = 0x00;
frame[23] = 0x00;
int pos = 24;
frame[pos++] = 0x0C;
frame[pos++] = 0x00;
frame_len = pos;
#elif (FLOOD_FRAME_TYPE == 3)
frame[0] = 0x40;
frame[1] = 0x00;
frame[2] = 0x00;
frame[3] = 0x00;
memcpy(&frame[4], dst_addr, 6);
memcpy(&frame[10], src_mac, 6);
memcpy(&frame[16], dst_addr, 6);
int pos = 24;
frame[pos++] = 0x00;
frame[pos++] = 0x00;
frame_len = pos;
#else
// Beacon-like frame
frame[0] = 0x80;
frame[1] = 0x00;
memcpy(&frame[4], dst_addr, 6);
for (int i = 0; i < 6; i++) frame[10 + i] = src_mac[i];
memcpy(&frame[16], &frame[10], 6);
frame[22] = rand() & 0xFF;
frame[23] = rand() & 0xFF;
memset(&frame[24], 0, 8);
frame[32] = 0x64;
frame[33] = 0x00;
frame[34] = 0x01;
frame[35] = 0x00;
int pos = 36;
frame[pos++] = 0x00;
const char *prefix = "TP_LINK-";
uint8_t prefix_len = strlen(prefix);
uint8_t random_len = (rand() % 6) + 1;
uint8_t total_len = prefix_len + random_len;
frame[pos++] = total_len;
for (int i = 0; i < prefix_len; i++) frame[pos++] = prefix[i];
for (int i = 0; i < random_len; i++) frame[pos++] = 'A' + (rand() % 26);
frame_len = pos;
#endif
esp_err_t err = esp_wifi_80211_tx(WIFI_IF_STA, frame, frame_len, true);
if (err == ESP_OK) {
flood_packet_count++;
}
}
#endif
// -------------------- FreeRTOS tasks --------------------
// Task: Wi-Fi sniffing & channel hopping (and flood sending if enabled)
void wifiSniffTask(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
while (1) {
#ifdef ENABLE_FLOOD
static uint8_t flood_index = 0;
if (flood_channel_count > 0) {
int channel = flood_channels[flood_index % flood_channel_count];
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
g_current_channel = channel;
send_flood_frame(channel);
flood_index = (flood_index + 1) % flood_channel_count;
} else {
// No channels defined – fallback to a default channel
esp_wifi_set_channel(1, WIFI_SECOND_CHAN_NONE);
g_current_channel = 1;
}
#else
static uint8_t scan_index = 0;
int channel = scan_channels[scan_index];
esp_wifi_set_channel(channel, WIFI_SECOND_CHAN_NONE);
g_current_channel = channel;
scan_index = (scan_index + 1) % CHANNEL_COUNT;
#endif
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(CHANNEL_SWITCH_INTERVAL_MS));
}
}
// Task: Manage device list (cleanup and print)
void manageDevicesTask(void *pvParameters) {
TickType_t xLastWakeTime = xTaskGetTickCount();
while (1) {
printDevices();
cleanInactiveDevices();
vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(PRINT_INTERVAL_MS));
}
}
// ===== I2C slave callbacks =====
#ifdef ENABLE_FLOOD
// Helper to set flood channels from I2C data
static void setFloodChannels(uint8_t count, uint8_t *channels) {
if (count > MAX_FLOOD_CHANNELS) count = MAX_FLOOD_CHANNELS;
portENTER_CRITICAL(&mux);
flood_channel_count = count;
memcpy(flood_channels, channels, count);
portEXIT_CRITICAL(&mux);
ESP_LOGI("I2C", "Flood channels updated, count=%d", count);
}
#endif
void i2cOnReceive(int len) {
uint8_t cmd = 0;
if (Wire.available()) {
cmd = Wire.read();
len--;
} else {
return;
}
switch (cmd) {
case CMD_SET_SRC_MAC_2G:
if (len >= 6) {
uint8_t buf[6];
for (int i = 0; i < 6; i++) buf[i] = Wire.read();
portENTER_CRITICAL(&mux);
memcpy(flood_src_mac_2P4G, buf, 6);
portEXIT_CRITICAL(&mux);
}
break;
case CMD_SET_SRC_MAC_5G:
if (len >= 6) {
uint8_t buf[6];
for (int i = 0; i < 6; i++) buf[i] = Wire.read();
portENTER_CRITICAL(&mux);
memcpy(flood_src_mac_5P0G, buf, 6);
portEXIT_CRITICAL(&mux);
}
break;
case CMD_SET_DST_MAC:
if (len >= 6) {
uint8_t buf[6];
for (int i = 0; i < 6; i++) buf[i] = Wire.read();
portENTER_CRITICAL(&mux);
memcpy(flood_dst_mac, buf, 6);
portEXIT_CRITICAL(&mux);
}
break;
case CMD_SET_FLOOD_MODE:
if (len >= 1) {
uint8_t val = Wire.read();
portENTER_CRITICAL(&mux);
flood_mode = (val != 0);
portEXIT_CRITICAL(&mux);
}
break;
case CMD_SET_FLOOD_CHANNELS: // NEW
if (len >= 1) {
uint8_t count = Wire.read();
if (count > 0 && count <= MAX_FLOOD_CHANNELS && len >= count) {
uint8_t channels[MAX_FLOOD_CHANNELS];
for (int i = 0; i < count; i++) {
channels[i] = Wire.read();
}
setFloodChannels(count, channels);
} else {
// invalid data – ignore
ESP_LOGW("I2C", "Invalid flood channel data, count=%d, len=%d", count, len);
}
}
break;
case CMD_GET_DEV_COUNT:
i2c_response_type = 1;
break;
case CMD_GET_DEV_LIST:
i2c_response_type = 2;
break;
default:
// ignore unknown command
break;
}
}
void i2cOnRequest() {
ESP_LOGI("Req", "Request triggered");
if (i2c_response_type == 0) {
Wire.write(0xFF); // error indicator
return;
}
if (i2c_response_type == 1) {
uint8_t cnt;
portENTER_CRITICAL(&mux);
cnt = device_count;
portEXIT_CRITICAL(&mux);
Wire.write(cnt);
i2c_response_type = 0;
return;
}
if (i2c_response_type == 2) {
portENTER_CRITICAL(&mux);
uint8_t cnt = device_count;
i2c_tx_buffer[0] = cnt;
size_t offset = 1;
for (int i = 0; i < cnt && i < MAX_DEVICES; i++) {
memcpy(&i2c_tx_buffer[offset], devices[i].mac, 6);
offset += 6;
i2c_tx_buffer[offset++] = devices[i].rssi;
i2c_tx_buffer[offset++] = (devices[i].channel & 0xFF);
i2c_tx_buffer[offset++] = (devices[i].channel >> 8) & 0xFF;
i2c_tx_buffer[offset++] = devices[i].frame_type;
}
size_t total_len = 1 + cnt * 10;
if (total_len < sizeof(i2c_tx_buffer)) {
memset(&i2c_tx_buffer[total_len], 0, sizeof(i2c_tx_buffer) - total_len);
}
i2c_tx_len = sizeof(i2c_tx_buffer);
portEXIT_CRITICAL(&mux);
Wire.write(i2c_tx_buffer, i2c_tx_len);
i2c_response_type = 0;
return;
}
}
void initI2C() {
Wire.end();
Wire.begin(I2C_SLAVE_ADDR, I2C_SDA, I2C_SCL);
Wire.onReceive(i2cOnReceive);
Wire.onRequest(i2cOnRequest);
ESP_LOGI("I2C", "I2C slave started, address 0x%02X, SDA=%d, SCL=%d",
I2C_SLAVE_ADDR, I2C_SDA, I2C_SCL);
}
// ============================================================
void setup() {
delay(500);
Serial.begin(115200);
initI2C();
initWiFiSniffer();
// Create FreeRTOS tasks
xTaskCreate(wifiSniffTask, "WiFiSniff", 4096, NULL, 1, NULL);
xTaskCreate(manageDevicesTask, "ManageDev", 4096, NULL, 1, NULL);
}
void loop() {
// Empty – tasks handle everything
vTaskDelay(pdMS_TO_TICKS(1000));
}
Environment:
-
Master: Waveshare ESP32-S3 with 1.91-inch TFT
-
Slave: ESP32-C5 MINI
-
Arduino IDE version: [2.3.8]
