The Short Answer: Which ESP32 Development Board Should You Buy?
If you are starting a new embedded IoT project today, buy the ESP32-S3-DevKitC-1 (N8R8 variant). While the classic ESP-WROOM-32 remains a budget staple, the ESP32-S3 offers native USB, Bluetooth 5.0 (LE), and vector instructions for edge AI, solving the most common hardware bottlenecks of the original architecture without a massive price jump.
Choosing between an ESP-WROOM-32, a generic ESP32, and the newer ESP32-S series development boards comes down to your specific peripheral needs. Use this decision path to lock in your hardware:
- IF you need Classic Bluetooth (A2DP audio streaming or legacy HID) AND lowest possible BOM cost → Choose ESP32-WROOM-32 (DevKit V1).
- IF you need native USB OTG, maximum GPIO count (43), and do NOT need Bluetooth → Choose ESP32-S2.
- IF you need native USB, Wi-Fi + BLE 5.0, higher clock speeds (240MHz), and vector instructions for audio/AI processing → Choose ESP32-S3-DevKitC-1 (N8R8). (This is the default recommendation for 90% of new builds).
Spec-Sheet Showdown: WROOM-32 vs. ESP32-S2 vs. ESP32-S3
The original ESP32-WROOM-32 module revolutionized hobbyist IoT, but its 2016 architecture shows its age. The S-series (S2 and S3) were built to address the original's limitations, specifically the lack of native USB and the complex strapping pin requirements.
| Feature | ESP32-WROOM-32 (Classic) | ESP32-S2 | ESP32-S3 (Recommended) |
|---|---|---|---|
| CPU Cores | 2x Xtensa LX6 (240MHz) | 1x Xtensa LX7 (240MHz) | 2x Xtensa LX7 (240MHz) |
| Native USB | No (Requires external UART bridge) | Yes (USB OTG) | Yes (USB OTG + Serial/JTAG) |
| Bluetooth | Classic BT + BLE 4.2 | None | BLE 5.0 (No Classic BT) |
| GPIO Count | 34 (Many restricted by strapping) | 43 | 45 |
| AI / Vector Ops | No | No | Yes (Accelerates neural nets) |
| Typical 2026 Price | $4.00 - $5.50 | $5.00 - $6.50 | $6.50 - $8.50 |
Note: The S3 drops Classic Bluetooth. If your project relies on older A2DP audio sinks, you must stick to the WROOM-32.
Parts List and Pin Mapping for the Recommended Build
For this guide, we are building a Wi-Fi connected environmental sensor. The code and wiring below specifically target the ESP32-S3-DevKitC-1 (N8R8). The "N8R8" designation is critical: it means 8MB of Quad SPI Flash and 8MB of Octal SPI PSRAM. Buying the cheaper N8 (no PSRAM) variant will cause out-of-memory panics if you attempt to load TLS certificates or audio buffers later.
Bill of Materials (BOM)
- MCU: ESP32-S3-DevKitC-1 (N8R8 variant) with pre-soldered headers.
- Sensor: Adafruit BME280 I2C Breakout (Product ID: 2652) or generic equivalent.
- Wiring: 22 AWG solid core jumper wires.
- Power: 5V/2A USB-C power supply (do not use a PC USB 2.0 port for initial flashing; voltage drop causes brownouts).
Pin Mapping Table
The ESP32-S3 allows flexible I2C pin mapping, but we avoid default strapping pins (like GPIO 0, 3, 45, 46) to prevent boot-loop issues when the sensor pulls the line low on startup.
| BME280 Pin | ESP32-S3-DevKitC-1 Pin | Notes |
|---|---|---|
| VIN / VCC | 3V3 | Do NOT use 5V; the BME280 is strictly 3.3V logic. |
| GND | GND | Common ground required for I2C stability. |
| SDA | GPIO 8 | Safe general-purpose pin, no boot strapping conflicts. |
| SCL | GPIO 9 | Safe general-purpose pin. |
Wiring and Flashing: Step-by-Step Setup
- Seat the MCU: Press the ESP32-S3-DevKitC-1 firmly into the center trench of your solderless breadboard, ensuring both rows of header pins are fully seated.
- Wire Power: Connect the BME280
VINto the ESP323V3pin, andGNDtoGND. - Wire Data: Connect BME280
SDAto ESP32GPIO 8, andSCLtoGPIO 9. - Verify with Multimeter: Before plugging in USB, set your multimeter to continuity mode. Probe the 3V3 and GND pins on the sensor breakout. You should read an open circuit (OL). If it beeps, you have a short—fix it before applying power or you will fry the onboard LDO.
- Connect USB: Plug a known data-capable USB-C cable into the S3 and your PC.
Complete Compilable Code: Wi-Fi MQTT Environmental Sensor
This firmware targets the ESP32-S3 using the Arduino IDE (ensure you have the esp32 board manager package v2.0.14 or newer installed). It reads the BME280 and publishes to an MQTT broker, featuring robust non-blocking error handling for both I2C and Wi-Fi dropouts.
Required Libraries (install via Arduino Library Manager): Adafruit BME280, Adafruit Unified Sensor, PubSubClient.
/*
* Target Board: ESP32-S3-DevKitC-1 (N8R8)
* Project: MQTT Environmental Sensor
* Author: ElectricalFlux
*/
#include
#include
#include
#include
#include
// --- PIN DEFINITIONS ---
#define PIN_I2C_SDA 8
#define PIN_I2C_SCL 9
// --- NETWORK CREDENTIALS ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
const char* mqtt_server = "192.168.1.100"; // Local broker IP
const int mqtt_port = 1883;
// --- OBJECTS ---
Adafruit_BME280 bme;
WiFiClient espClient;
PubSubClient client(espClient);
// --- TIMING VARIABLES ---
unsigned long lastMsg = 0;
const long READ_INTERVAL = 10000; // 10 seconds
void setup_wifi() {
delay(10);
Serial.print("Connecting to ");
Serial.println(ssid);
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
int attempts = 0;
while (WiFi.status() != WL_CONNECTED && attempts < 40) {
delay(500);
Serial.print(".");
attempts++;
}
if (WiFi.status() == WL_CONNECTED) {
Serial.println("\nWiFi connected. IP: ");
Serial.println(WiFi.localIP());
} else {
Serial.println("\nWiFi connection failed. Rebooting in 5s...");
delay(5000);
ESP.restart();
}
}
void reconnect_mqtt() {
int retries = 0;
while (!client.connected() && retries < 5) {
String clientId = "ESP32S3-" + String(random(0xffff), HEX);
Serial.print("Attempting MQTT connection...");
if (client.connect(clientId.c_str())) {
Serial.println("connected");
} else {
Serial.print("failed, rc=");
Serial.print(client.state());
retries++;
delay(2000);
}
}
}
void setup() {
Serial.begin(115200);
delay(1000); // Allow USB-CDC serial port to enumerate on S3
Serial.println("\n--- ESP32-S3 BME280 MQTT Boot ---");
// Initialize I2C with custom S3 pins
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
// BME280 Error Handling
if (!bme.begin(0x77, &Wire)) {
Serial.println("ERROR: Could not find a valid BME280 sensor at 0x77!");
Serial.println("Check wiring: SDA->GPIO8, SCL->GPIO9. Halting.");
while (1) { delay(100); } // Halt execution safely
}
Serial.println("BME280 initialized successfully.");
setup_wifi();
client.setServer(mqtt_server, mqtt_port);
}
void loop() {
if (!client.connected()) {
if (WiFi.status() != WL_CONNECTED) setup_wifi();
reconnect_mqtt();
}
client.loop();
unsigned long now = millis();
if (now - lastMsg > READ_INTERVAL) {
lastMsg = now;
float temp = bme.readTemperature();
float hum = bme.readHumidity();
// Sanity check for I2C read glitches (returns NaN on failure)
if (isnan(temp) || isnan(hum)) {
Serial.println("ERROR: BME280 read returned NaN. Resetting I2C bus.");
Wire.end();
Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL);
return;
}
char tempStr[8], humStr[8];
dtostrf(temp, 1, 2, tempStr);
dtostrf(hum, 1, 2, humStr);
client.publish("sensor/esp32s3/temperature", tempStr);
client.publish("sensor/esp32s3/humidity", humStr);
Serial.printf("Published: Temp=%sC, Hum=%s%%\n", tempStr, humStr);
}
}
Troubleshooting: "Failed to Connect" and Boot Failures
The ESP32-S3 handles serial bootloading differently than the classic WROOM-32. If you hit a wall during the upload phase, look for this exact error string in your Arduino IDE output:
A fatal error occurred: Failed to connect to ESP32-S3: No serial data received.
For troubleshooting steps visit: https://docs.espressif.com/projects/esptool/en/latest/troubleshooting.html
The First Three Things to Check
- The Manual Boot Sequence: Unlike older boards with auto-reset circuits, many S3 DevKits require manual bootloader entry. Fix: Press and hold the
BOOTbutton on the board, tap theRESETbutton, releaseRESET, then releaseBOOT. Click "Upload" in the IDE immediately after. - USB Cable Integrity: The S3 uses native USB on certain pins, but the DevKitC-1 routes a secondary UART bridge for flashing. If your cable is charge-only (missing the D+/D- data lines), the PC will provide power but no COM port. Fix: Swap to a verified data-sync USB-C cable.
- USB-CDC vs. UART Mode: In the Arduino IDE Tools menu, ensure
USB CDC On Bootis set toEnabledandUpload Modeis set toUART0 / Hardware CDC. Misconfiguring this routes Serial output to the wrong physical interface.
Ranked Causes for I2C Sensor Hangs
If the code compiles and flashes, but the serial monitor halts at ERROR: Could not find a valid BME280, check these in order:
- Cause 1 (60%): Wrong I2C address. Some cheap BME280 clones ship with the address tied to
0x76instead of0x77. Change thebme.begin(0x77)argument to0x76. - Cause 2 (30%): Missing pull-up resistors. The Adafruit breakout has them onboard, but raw modules do not. Add 4.7kΩ resistors between SDA/SCL and 3V3.
- Cause 3 (10%): Capacitive loading on long wires. If your jumper wires exceed 12 inches, the I2C clock edges degrade. Shorten the wires or drop the I2C clock speed via
Wire.setClock(10000);.
Extending and Simplifying the Build
Once you have the baseline MQTT telemetry flowing, you can scale the project up or strip it down based on your deployment environment.
How to Simplify (The Offline Route)
If you don't want to maintain an MQTT broker like Mosquitto, strip out the PubSubClient library entirely. Replace the MQTT publish block with the native WebServer.h library to host a simple local HTML dashboard. The ESP32-S3's dual cores can easily handle serving a basic HTTP GET request on port 80 while sampling the sensor on the secondary core via xTaskCreatePinnedToCore().
How to Extend (The Solar Off-Grid Route)
To run this board remotely, you need to leverage the S3's deep sleep capabilities.
- Add Power Storage: Wire a 3.7V 18650 Li-ion cell to a TP4056 charging module, and route the module's regulated 5V out to the ESP32's
5Vpin (bypassing the USB path). - Implement Deep Sleep: Add
esp_sleep_enable_timer_wakeup(900 * 1000000ULL);to your setup, followed byesp_deep_sleep_start();at the end of your loop. This drops current draw from ~80mA to roughly 12µA, allowing a single 18650 to run the node for months. - Hardware Note: Before deep sleeping, ensure you turn off the BME280's internal heater and set it to forced mode to prevent parasitic drain through the I2C lines.






