The ESP32-WROOM Errata Reality Check
If you have ever stared at a serial monitor watching your ESP32 randomly lock up, drop I2C sensors, or reboot with a cryptic brownout message, you are not writing bad code—you are likely hitting silicon errata. The ESP32-WROOM module family relies on the original ESP32 SoC, which has well-documented hardware bugs across its silicon revisions (v0 through v3). While Espressif has patched many issues in the ESP32-D0WD-V3 chip (found inside the modern ESP32-WROOM-32E), legacy quirks around I2C clock stretching, ADC2 Wi-Fi conflicts, and power-on brownouts still require explicit firmware and hardware workarounds.
The direct answer to surviving these chips is threefold: use ADC1 instead of ADC2, implement manual I2C bus recovery routines, and over-engineer your 3.3V decoupling. This guide breaks down the exact failure modes, the error strings they produce, and the compilable code to bypass them.
Difficulty Rating: Intermediate (Requires understanding of I2C bus states and power decoupling)
First 3 Things to Check When It Fails:
1. Measure the 3.3V rail with an oscilloscope for >100mV ripple during Wi-Fi TX bursts.
2. Verify external I2C pull-up resistors are 4.7kΩ or lower (internal pull-ups are too weak).
3. Ensure no code calls
analogRead() on ADC2 pins while WiFi.begin() is active.
Hardware & Pin Mapping for the Test Build
To demonstrate the workarounds, we are building a Wi-Fi-connected environmental logger that reads a BME280 sensor over I2C. This specific combination triggers the most common ESP32-WROOM errata: I2C bus lockups during Wi-Fi transmission and power rail sag.
Parts List
- MCU: ESP32-WROOM-32E DevKitC v4 (Ensure the metal can says "WROOM-32E", not the older 32D or bare 32).
- Sensor: BME280 Breakout (3.3V logic, I2C interface).
- Resistors: 2x 4.7kΩ through-hole resistors for I2C pull-ups.
- Capacitors: 1x 100µF low-ESR electrolytic + 1x 100nF ceramic (placed physically within 5mm of the module's 3V3 and GND pins).
Pin Mapping Table
| ESP32-WROOM-32E Pin | GPIO Number | Connected To | Errata Note |
|---|---|---|---|
| 3V3 | N/A | BME280 VCC, Pull-ups | Requires heavy decoupling to prevent brownout errata. |
| GND | N/A | BME280 GND | Keep return path short. |
| GPIO 21 | 21 | BME280 SDA | Default I2C SDA. Prone to locking low during Wi-Fi TX. |
| GPIO 22 | 22 | BME280 SCL | Default I2C SCL. |
| GPIO 34 | 34 | Battery Voltage Divider | Input only. Uses ADC1 (safe from Wi-Fi conflict). |
The Top 3 Silicon Bugs and How to Bypass Them
When debugging ESP32-WROOM errata, you will encounter specific error strings in the serial monitor. Here are the ranked causes and the exact fixes.
-
I2C Bus Lockup (Errata 3.12 / Wi-Fi Coexistence)
Exact Error String:E (1234) i2c: i2c_master_cmd_begin(1198): i2c transmission timeoutor the Arduino Wire library simply hangs indefinitely.
The Bug: When the Wi-Fi radio transmits, the internal DMA controller can interrupt the I2C peripheral mid-transaction. If the SDA line is pulled low by the ESP32 and the transaction is aborted, the slave device holds SDA low waiting for a clock pulse. The bus is now dead.
The Fix: Implement a manual bus recovery function that bit-bangs 9 SCL clock pulses to force the slave to release SDA, then reinitializes the Wire peripheral. -
ADC2 vs Wi-Fi Conflict
Exact Error String:E (567) adc: adc2_get_raw(285): adc2 is in use by Wi-Fi(or silent failures returning-1or4095in Arduino).
The Bug: The ESP32's Wi-Fi and Bluetooth subsystems share hardware resources with the ADC2 controller. If Wi-Fi is active, ADC2 reads are silently blocked or return garbage.
The Fix: Never use ADC2 pins (GPIO 0, 2, 4, 12-15, 25-27) for analog sensing if Wi-Fi or BLE is enabled. Route all analog sensors to ADC1 pins (GPIO 32-39). -
Brownout Detector False Triggers
Exact Error String:brownout detector was triggered(Printed in plain text by the ROM bootloader, followed by an immediate reboot).
The Bug: The ESP32's internal brownout detector (BOD) is highly sensitive to microsecond voltage droops. When the Wi-Fi PA (Power Amplifier) ramps up for transmission, it can pull >300mA for a few milliseconds. If your 3.3V regulator or USB cable cannot supply this transient current, the BOD trips.
The Fix: Add a 100µF low-ESR capacitor directly across the 3V3 and GND pins on the breakout board. Do not rely on the DevKit's onboard 10µF ceramic capacitor alone.
Compilable Workaround Code (Target: ESP32-WROOM-32E)
The following Arduino IDE code targets the ESP32-WROOM-32E. It includes the critical clearI2CBus() workaround to recover from SDA lockups, uses ADC1 for battery monitoring, and includes robust error handling for the BME280 sensor.
#include <Wire.h>
#include <WiFi.h>
#include <Adafruit_BME280.h>
// --- PIN DEFINITIONS ---
#define I2C_SDA_PIN 21
#define I2C_SCL_PIN 22
#define BATTERY_ADC_PIN 34 // ADC1_CH6 (Safe from Wi-Fi errata)
// --- CREDENTIALS ---
const char* ssid = "YOUR_WIFI_SSID";
const char* password = "YOUR_WIFI_PASSWORD";
Adafruit_BME280 bme;
// --- I2C BUS RECOVERY WORKAROUND ---
// Fixes ESP32 Errata 3.12: I2C lockup when Wi-Fi TX interrupts a transaction
void clearI2CBus() {
Serial.println("[ERRATA FIX] Attempting I2C bus recovery...");
// Detach the Wire library to take manual control of the pins
Wire.end();
pinMode(I2C_SCL_PIN, OUTPUT);
pinMode(I2C_SDA_PIN, INPUT_PULLUP);
// Send 9 clock pulses to force the slave to release SDA
for (int i = 0; i < 9; i++) {
digitalWrite(I2C_SCL_PIN, LOW);
delayMicroseconds(50);
digitalWrite(I2C_SCL_PIN, HIGH);
delayMicroseconds(50);
}
// Generate a STOP condition (SDA goes LOW to HIGH while SCL is HIGH)
pinMode(I2C_SDA_PIN, OUTPUT);
digitalWrite(I2C_SDA_PIN, LOW);
delayMicroseconds(50);
digitalWrite(I2C_SCL_PIN, HIGH);
delayMicroseconds(50);
digitalWrite(I2C_SDA_PIN, HIGH);
delayMicroseconds(50);
// Reinitialize Wire
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
Wire.setClock(100000); // Drop to 100kHz for better noise immunity
Serial.println("[ERRATA FIX] I2C bus reinitialized.");
}
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
Serial.println("Booting ESP32-WROOM-32E...");
// Initialize I2C with explicit pins and reduced clock speed
Wire.begin(I2C_SDA_PIN, I2C_SCL_PIN);
Wire.setClock(100000);
// Initialize BME280 with error handling
if (!bme.begin(0x76, &Wire)) {
Serial.println("[ERROR] Could not find BME280 sensor. Triggering bus recovery.");
clearI2CBus();
if (!bme.begin(0x76, &Wire)) {
Serial.println("[FATAL] BME280 not found after recovery. Check wiring.");
while (1) { delay(1000); } // Halt
}
}
// Initialize Wi-Fi (This is what usually triggers the I2C/ADC errata)
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
Serial.print("Connecting to Wi-Fi");
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nWi-Fi connected.");
}
void loop() {
// Read ADC1 (Avoids ADC2 Wi-Fi conflict)
int raw_adc = analogRead(BATTERY_ADC_PIN);
float voltage = (raw_adc / 4095.0) * 3.3 * 2.0; // Assuming 1:1 voltage divider
// Force an I2C transaction
float temp = bme.readTemperature();
// Check for I2C timeout / NaN (Symptom of bus lockup)
if (isnan(temp) || temp == 0.0) {
Serial.println("[ERROR] I2C read failed. SDA likely stuck low.");
clearI2CBus();
bme.begin(0x76, &Wire); // Re-init sensor after bus clear
} else {
Serial.printf("Temp: %.2f C | Battery: %.2f V\n", temp, voltage);
}
// Wi-Fi TX happens in the background, potentially interrupting the next I2C read
delay(2000);
}
Extending or Simplifying the Build
Depending on your project requirements, you can scale this architecture up or down while avoiding the underlying silicon bugs.
esp_deep_sleep_start(). Without the Wi-Fi radio ramping up, the brownout detector false-triggers disappear, and the I2C DMA interruption bug is never triggered. You can safely use ADC2 for battery monitoring in deep sleep wake stubs before Wi-Fi initializes.
ESP32-WROOM Errata FAQ
Does the ESP32-WROOM-32E fix all v1 silicon errata?
The ESP32-WROOM-32E contains the ESP32-D0WD-V3 chip (Revision 3). This revision fixes the critical v0/v1 bugs related to flash encryption boot loops and certain Wi-Fi memory leaks. However, it does not fix the fundamental hardware routing conflict between ADC2 and the Wi-Fi MAC, nor does it completely eliminate I2C bus lockups during heavy DMA usage. You still need firmware workarounds for I2C and must use ADC1 for analog sensing.
Why does my ESP32-WROOM keep printing "brownout detector was triggered"?
This plain-text error is generated by the mask ROM bootloader, not your Arduino sketch. It means the 3.3V rail dipped below ~2.4V for a few microseconds. This almost always happens during the initial Wi-Fi calibration or transmission burst, which draws a transient spike of 300mA to 500mA. If you are powering the DevKit via a PC USB port, the port may be current-limiting. Add a 100µF low-ESR capacitor directly across the 3V3 and GND header pins on the board to supply the transient current.
Can I use ADC2 for battery voltage monitoring while Wi-Fi is connected?
No. According to the Espressif Hardware Design Guidelines, the ADC2 peripheral is shared with the Wi-Fi and Bluetooth subsystems. When WiFi.begin() is called, the RF driver takes exclusive control of ADC2. Any calls to analogRead() on ADC2 pins (like GPIO 4, 12, 13, 14, 15, 25, 26, 27) will fail silently or return erratic data. Always route battery voltage dividers to ADC1 pins (GPIO 32 through 39).
How do I clear an I2C lockup without resetting the ESP32?
When the ESP32 I2C peripheral crashes mid-byte, the slave device is often left holding the SDA line low, waiting for the master to send the 9th clock pulse (ACK). Because the ESP32's hardware I2C state machine is locked, calling Wire.end() and Wire.begin() will not fix it. You must detach the Wire library, manually configure the SCL pin as a GPIO output, and toggle it HIGH and LOW 9 times. This forces the slave to clock out its remaining bits and release SDA. The clearI2CBus() function provided in the code block above automates this exact recovery sequence.






