ESP32-CAM Flash LED Control and Brownout-Free Power Supply Design
Two complaints come up again and again with the ESP32-CAM: the white flash LED is blindingly bright or stays on when it should not, and the board resets with the message Brownout detector was triggered. Both are related to current. The LED draws a large current on its own, and Wi-Fi transmission adds sharp current peaks on top of the camera load.
In this tutorial you will drive the flash LED with PWM so you can set its brightness, take photos with a short flash pulse, and keep the LED dark during deep sleep. Then you will design a power supply arrangement that keeps the 3.3 V rail stable so brownout resets stop.
How the Flash LED Is Wired
The flash LED is a high-power white LED switched by a transistor whose base or gate is driven from GPIO 4, through a current-limiting resistor. HIGH on GPIO 4 turns the LED on. Because a transistor does the switching, GPIO 4 itself only supplies a small control current, so it can be driven with PWM to control brightness.
GPIO 4 is also the DATA1 line of the microSD card in 4-bit mode. That is why the flash flickers when you write to the SD card. If you need the flash and the SD card together, start SD_MMC in 1-bit mode with SD_MMC.begin("/sdcard", true), which leaves GPIO 4 free.
PWM Brightness and Flash Photos
The ESP32 LEDC peripheral generates PWM. The camera driver already uses LEDC channel 0 and timer 0 to make the 20 MHz XCLK, so the flash must use a different channel. In Arduino core 3.x, ledcAttach(pin, freq, bits) picks a free channel for you, and ledcWrite(pin, duty) sets the duty. In core 2.x the equivalent is ledcSetup(2, 5000, 8), ledcAttachPin(4, 2) and ledcWrite(2, duty).
The sketch below accepts commands over the Serial Monitor: a digit from 0 to 9 sets the torch brightness and f takes a photo with a flash pulse, printing the JPEG size so you can compare flash and no-flash shots.
#include "esp_camera.h"
#include "driver/rtc_io.h"
// AI-Thinker ESP32-CAM pin map
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
#define FLASH_PIN 4
#define FLASH_FREQ 5000 // 5 kHz PWM, no visible flicker
#define FLASH_BITS 8 // duty 0..255
bool initCamera() {
camera_config_t c = {};
c.ledc_channel = LEDC_CHANNEL_0; // used for XCLK, keep flash off it
c.ledc_timer = LEDC_TIMER_0;
c.pin_d0 = Y2_GPIO_NUM; c.pin_d1 = Y3_GPIO_NUM;
c.pin_d2 = Y4_GPIO_NUM; c.pin_d3 = Y5_GPIO_NUM;
c.pin_d4 = Y6_GPIO_NUM; c.pin_d5 = Y7_GPIO_NUM;
c.pin_d6 = Y8_GPIO_NUM; c.pin_d7 = Y9_GPIO_NUM;
c.pin_xclk = XCLK_GPIO_NUM; c.pin_pclk = PCLK_GPIO_NUM;
c.pin_vsync = VSYNC_GPIO_NUM; c.pin_href = HREF_GPIO_NUM;
c.pin_sccb_sda = SIOD_GPIO_NUM; c.pin_sccb_scl = SIOC_GPIO_NUM;
c.pin_pwdn = PWDN_GPIO_NUM; c.pin_reset = RESET_GPIO_NUM;
c.xclk_freq_hz = 20000000;
c.pixel_format = PIXFORMAT_JPEG;
c.frame_size = FRAMESIZE_SVGA;
c.jpeg_quality = 12;
c.fb_count = 1;
c.fb_location = CAMERA_FB_IN_PSRAM;
c.grab_mode = CAMERA_GRAB_LATEST;
return esp_camera_init(&c) == ESP_OK;
}
void setFlash(uint8_t duty) {
ledcWrite(FLASH_PIN, duty); // core 3.x: write by pin
}
void flashPhoto() {
setFlash(255); // full brightness
delay(150); // let auto exposure adapt
camera_fb_t *fb = esp_camera_fb_get();
if (fb) esp_camera_fb_return(fb); // discard frame exposed before flash
fb = esp_camera_fb_get(); // this frame is lit by the flash
setFlash(0); // flash off as soon as possible
if (!fb) {
Serial.println("Capture failed");
return;
}
Serial.printf("Flash photo: %u bytes\n", fb->len);
esp_camera_fb_return(fb);
}
void setup() {
Serial.begin(115200);
rtc_gpio_hold_dis(GPIO_NUM_4); // release hold left by deep sleep
if (!initCamera()) {
Serial.println("Camera init failed");
while (true) delay(1000);
}
ledcAttach(FLASH_PIN, FLASH_FREQ, FLASH_BITS);
setFlash(0);
Serial.println("Send 0-9 for torch level, f for flash photo, s to sleep");
}
void loop() {
if (!Serial.available()) return;
char cmd = Serial.read();
if (cmd >= '0' && cmd <= '9') {
uint8_t duty = (cmd - '0') * 255 / 9;
setFlash(duty);
Serial.printf("Torch duty %u/255\n", duty);
} else if (cmd == 'f') {
flashPhoto();
} else if (cmd == 's') {
setFlash(0);
ledcDetach(FLASH_PIN);
pinMode(FLASH_PIN, OUTPUT);
digitalWrite(FLASH_PIN, LOW);
rtc_gpio_hold_en(GPIO_NUM_4); // keep LED off while asleep
Serial.println("Sleeping for 10 s");
esp_sleep_enable_timer_wakeup(10ULL * 1000000ULL);
esp_deep_sleep_start();
}
}
Key Points in the Code
- The flash is turned on before the capture, and one frame is discarded, because the frame already in progress was exposed without light.
- The LED is switched off immediately after the second
esp_camera_fb_get(), keeping the high current pulse short. - During deep sleep the GPIO output stops being driven, and the transistor can turn the LED on dimly.
rtc_gpio_hold_en()latches the pin LOW;rtc_gpio_hold_dis()at boot releases it. - Low duty values such as 20 to 40 give a useful torch for streaming without the heat and current of full brightness.
Power Supply Design to Prevent Brownouts
The brownout detector resets the ESP32 when its supply drops below a threshold, protecting it from executing code at an unsafe voltage. On the ESP32-CAM, the 3.3 V rail comes from an AMS1117 linear regulator fed by the 5V pin. A linear regulator needs its input to stay comfortably above 3.3 V, so any sag on the 5 V side, from thin wires, a weak adapter or a long USB cable, appears directly on the 3.3 V rail when Wi-Fi transmits.
| Do this | Instead of this |
|---|---|
| Feed 5 V into the 5V pin from a supply rated 2 A | Powering from the 3.3 V pin of a USB-TTL adapter |
| Short, thick wires straight to the header pins | Long jumper wires through a breadboard |
| Electrolytic capacitor of a few hundred microfarads plus 100 nF across 5V and GND near the board | No bulk capacitance at the board |
| Separate supply for servos, motors and relays, with a common ground | Running motors from the same 5V pin |
| A good phone charger and short data cable for the MB adapter | Laptop USB ports through a hub |
You will find sketches that disable the brownout detector with WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0). This hides the symptom: the board keeps running through voltage dips, and the result is corrupt images, Wi-Fi drops and occasional flash corruption. Fix the supply instead and keep the detector enabled.
Troubleshooting
- Flash glows faintly during sleep: add the
rtc_gpio_hold_enlines, or an external pull-down on GPIO 4. - Camera fails after adding the flash code: the PWM was attached to LEDC channel 0. Let
ledcAttachchoose, or in core 2.x use channel 2 or higher. - Brownout only when the flash fires: the LED pulse plus Wi-Fi is too much for the supply; add bulk capacitance or lower the flash duty.
- Photo still dark with flash: increase the delay before capture so auto exposure can adapt, or discard two frames.
Summary
The flash LED on GPIO 4 can be dimmed with LEDC PWM on any channel except the one used for XCLK, pulsed only during capture, and held off during deep sleep. Brownout resets come from a sagging 5 V input to the on-board regulator, so use a proper 5 V supply, short wires and bulk capacitance, keep motors on their own supply, and leave the brownout detector enabled.