Table of Contents
KEY TAKEAWAYS
- A brownout occurs when the supply voltage drops below the safe operating level without reaching zero — more dangerous than a clean power-off because the CPU may execute corrupted instructions
- The internal Brown-Out Detector (BOD) uses a voltage comparator with hysteresis to hold the MCU in reset during low-voltage conditions
- EEPROM and Flash writes during a brownout can permanently corrupt stored data — always check voltage stability before non-volatile memory operations
- Implementing a safe shutdown sequence (save state, disable outputs, enter safe mode) prevents hardware damage and data loss during power failures
What Is a Brownout?
A brownout is a partial voltage drop — the supply voltage sags below the minimum operating level but does not reach zero. Unlike a clean power-off where everything stops definitively, a brownout leaves the microcontroller in a twilight zone: the CPU may still be running, but at the wrong speed, reading incorrect values from registers, or executing corrupted instructions from flash memory.
Brownouts happen in the real world more often than complete power failures:
- A motor starting up pulls current from a shared power rail, momentarily dropping the voltage
- A battery-powered device’s supply dips when the radio transmits at full power
- A loose connector creates intermittent contact
- A solar-powered system experiences a cloud passing over the panel
- A coin cell battery’s voltage gradually declines near end-of-life
Why Brownouts Are Dangerous
When VDD drops below the minimum operating voltage (typically 2.0V for a 3.3V MCU), several things can go wrong simultaneously:
1. Flash Memory Read Errors
Flash memory requires a minimum voltage to reliably read data. Below this threshold, the memory cells’ charge may be misinterpreted — a stored 1 reads as 0 or vice versa. The CPU fetches corrupted instructions and executes random operations. This is not a crash; it is worse — the CPU is running, but doing unpredictable things like toggling outputs, writing to random memory addresses, or entering infinite loops.
2. EEPROM and Flash Write Corruption
If the MCU is writing to non-volatile memory (EEPROM or flash) when a brownout occurs, the write operation may complete only partially. This can corrupt:
- Calibration data — requiring factory recalibration
- Configuration settings — device starts up with wrong parameters
- The flash page being written — which could include firmware code, effectively bricking the device
3. Output Glitches
GPIO output levels become unpredictable during a brownout. A pin configured to drive a motor or relay may toggle randomly, potentially causing physical damage to connected hardware.
How the Brown-Out Detector (BOD) Works
The internal BOD circuit is essentially a voltage comparator connected to the supply rail. It compares VDD against an internal reference voltage. When VDD drops below the BOD threshold, the circuit asserts the internal reset signal, holding the MCU in reset until VDD recovers above the threshold (plus a hysteresis margin to prevent oscillation).
The hysteresis is critical. Without it, if VDD hovers around the threshold, the BOD would trigger and release rapidly, causing the MCU to reset-run-reset-run in rapid succession. With hysteresis (typically 50-100mV), VDD must rise significantly above the trigger threshold before the reset is released.
Most microcontrollers let you select the BOD threshold from several options. For example:
- Level 0: BOD disabled (lowest power consumption, no protection)
- Level 1: 2.2V threshold
- Level 2: 2.7V threshold
- Level 3: 2.9V threshold (most conservative, highest protection)
Choose the threshold based on your supply voltage and the minimum operating voltage of external components. If your system runs at 3.3V and your most voltage-sensitive component requires 2.7V minimum, set the BOD threshold at 2.7V.
Configuring the Brown-Out Detector in C
BOD configuration varies by microcontroller family. On some chips (like many AVR devices), the BOD level is set via fuse bits at programming time, not at runtime. On ARM Cortex-M devices, it is typically configured through option bytes or runtime registers.
Here is a runtime BOD configuration for a typical ARM Cortex-M MCU:
#include <stdint.h>
/* Power control registers */
#define PWR_BASE 0x40007000UL
#define PWR_CR (*(volatile uint32_t *)(PWR_BASE + 0x00))
#define PWR_CSR (*(volatile uint32_t *)(PWR_BASE + 0x04))
/* Brown-out detection bit definitions (from datasheet) */
#define PWR_CR_PLS_MASK (7U << 5) /* PVD level selection bits */
#define PWR_CR_PLS_2V2 (0U << 5) /* PVD threshold = 2.2V */
#define PWR_CR_PLS_2V3 (1U << 5) /* PVD threshold = 2.3V */
#define PWR_CR_PLS_2V4 (2U << 5) /* PVD threshold = 2.4V */
#define PWR_CR_PLS_2V5 (3U << 5) /* PVD threshold = 2.5V */
#define PWR_CR_PLS_2V6 (4U << 5) /* PVD threshold = 2.6V */
#define PWR_CR_PLS_2V7 (5U << 5) /* PVD threshold = 2.7V */
#define PWR_CR_PLS_2V8 (6U << 5) /* PVD threshold = 2.8V */
#define PWR_CR_PLS_2V9 (7U << 5) /* PVD threshold = 2.9V */
#define PWR_CR_PVDE (1U << 4) /* Power Voltage Detector enable */
#define PWR_CSR_PVDO (1U << 2) /* PVD output: 1 = VDD below threshold */
/* EXTI and NVIC for PVD interrupt */
#define EXTI_BASE 0x40010400UL
#define EXTI_IMR (*(volatile uint32_t *)(EXTI_BASE + 0x00))
#define EXTI_RTSR (*(volatile uint32_t *)(EXTI_BASE + 0x08))
#define EXTI_FTSR (*(volatile uint32_t *)(EXTI_BASE + 0x0C))
#define EXTI_PR (*(volatile uint32_t *)(EXTI_BASE + 0x14))
#define PVD_EXTI_LINE (1U << 16) /* PVD is connected to EXTI line 16 */
void bod_init(uint32_t threshold_level)
{
/* Enable power interface clock */
RCC_APB1ENR |= (1U << 28);
/* Configure PVD threshold level */
uint32_t cr = PWR_CR;
cr &= ~PWR_CR_PLS_MASK; /* Clear current threshold */
cr |= threshold_level; /* Set new threshold */
cr |= PWR_CR_PVDE; /* Enable PVD */
PWR_CR = cr;
/* Configure EXTI line 16 (PVD) for both rising and falling edges */
EXTI_IMR |= PVD_EXTI_LINE; /* Unmask PVD interrupt */
EXTI_RTSR |= PVD_EXTI_LINE; /* Rising edge: VDD drops below threshold */
EXTI_FTSR |= PVD_EXTI_LINE; /* Falling edge: VDD recovers above threshold */
/* Enable PVD interrupt in NVIC (IRQ number 1 on many STM32) */
/* NVIC_EnableIRQ(PVD_IRQn); — use your CMSIS function here */
}
int is_voltage_low(void)
{
return (PWR_CSR & PWR_CSR_PVDO) ? 1 : 0;
}Protecting EEPROM Writes During Brownouts
The most critical protection is ensuring non-volatile memory writes never start when the voltage is marginal. Here is a safe EEPROM write function that checks voltage before and during the write:
typedef enum {
EEPROM_OK,
EEPROM_LOW_VOLTAGE,
EEPROM_WRITE_FAILED,
EEPROM_VERIFY_FAILED
} eeprom_status_t;
eeprom_status_t safe_eeprom_write(uint32_t address, const uint8_t *data, uint32_t length)
{
/* Pre-check: is voltage above BOD threshold? */
if (is_voltage_low()) {
return EEPROM_LOW_VOLTAGE;
}
/* Disable interrupts during EEPROM write to prevent
* timing interference and ensure atomic operation */
__disable_irq();
for (uint32_t i = 0; i < length; i++) {
/* Check voltage before EACH byte write */
if (is_voltage_low()) {
__enable_irq();
return EEPROM_LOW_VOLTAGE;
}
/* Perform the actual EEPROM write
* (implementation depends on MCU — this is conceptual) */
if (eeprom_write_byte(address + i, data[i]) != 0) {
__enable_irq();
return EEPROM_WRITE_FAILED;
}
}
__enable_irq();
/* Verify: read back and compare */
for (uint32_t i = 0; i < length; i++) {
uint8_t readback = eeprom_read_byte(address + i);
if (readback != data[i]) {
return EEPROM_VERIFY_FAILED;
}
}
return EEPROM_OK;
}
/* Write with retry and wear leveling */
eeprom_status_t safe_eeprom_write_with_retry(uint32_t address,
const uint8_t *data,
uint32_t length,
uint8_t max_retries)
{
for (uint8_t attempt = 0; attempt < max_retries; attempt++) {
eeprom_status_t status = safe_eeprom_write(address, data, length);
if (status == EEPROM_OK) {
return EEPROM_OK;
}
if (status == EEPROM_LOW_VOLTAGE) {
/* Voltage too low — do not retry, wait for stable power */
return EEPROM_LOW_VOLTAGE;
}
/* Write or verify failed — retry after a short delay */
delay_ms(10);
}
return EEPROM_WRITE_FAILED;
}Implementing a Safe Shutdown Sequence
When the PVD interrupt fires (voltage dropping below threshold), you have a limited amount of time before the voltage drops too low for the MCU to function. A typical supercapacitor or bulk capacitor on the power rail might give you 1-50 ms of runtime. Use this time wisely:
/* Flags for shutdown coordination */
static volatile int shutdown_requested = 0;
/* PVD Interrupt Handler — called when voltage crosses the threshold */
void PVD_IRQHandler(void)
{
/* Clear the EXTI pending bit */
EXTI_PR = PVD_EXTI_LINE;
if (is_voltage_low()) {
/* Voltage dropped below threshold — initiate shutdown */
shutdown_requested = 1;
} else {
/* Voltage recovered — cancel shutdown */
shutdown_requested = 0;
}
}
/*
* Safe shutdown sequence — called from main loop when
* shutdown_requested is set.
*
* Priority order (most critical first):
* 1. Stop outputs that could cause hardware damage
* 2. Save critical state to non-volatile memory
* 3. Signal other systems (if time allows)
* 4. Enter safe state
*/
void safe_shutdown(void)
{
/* Step 1: Immediately disable all outputs that drive external hardware.
* A motor driver or relay with random GPIO states can cause damage. */
disable_motor_outputs();
disable_relay_outputs();
set_all_gpio_safe_state();
/* Step 2: Save critical runtime state to EEPROM/backup registers.
* Only save what you need for recovery — keep it minimal
* because time is limited. */
backup_data_t state;
state.operating_mode = current_mode;
state.uptime_seconds = get_uptime();
state.error_code = last_error;
state.magic = BACKUP_MAGIC; /* Validation marker */
/* Write to backup registers (faster than EEPROM, survives reset) */
write_backup_registers(&state, sizeof(state));
/* Step 3: Send a "power fail" message if UART is fast enough.
* At 115200 baud, one byte takes ~87 µs. A short message is feasible. */
uart_send_string("!PFrn"); /* "Power Fail" — keep it short */
/* Step 4: Enter the lowest power state possible */
__disable_irq();
while (1) {
__asm volatile ("wfi"); /* Wait for reset or power recovery */
}
}
/* In main loop: check for shutdown request */
int main(void)
{
system_init();
bod_init(PWR_CR_PLS_2V7); /* Set threshold to 2.7V */
watchdog_init();
while (1) {
if (shutdown_requested) {
safe_shutdown(); /* Does not return */
}
/* Normal operation */
process_application();
watchdog_feed();
}
}Software Brownout Detection via ADC
Some microcontrollers have a built-in ADC channel connected to the supply voltage (often called VREFINT or VDD/3). You can use this to monitor the supply voltage in software with much finer resolution than the BOD thresholds:
/* Many MCUs have an internal ADC channel for VDD measurement.
* On STM32, VREFINT (internal reference, typically 1.2V) is on ADC channel 17.
* By measuring VREFINT, you can calculate VDD. */
#define VREFINT_CAL_ADDR 0x1FFFF7BAUL /* Factory calibration value address */
#define VREFINT_CAL_VREF 3300 /* Calibration was done at 3.3V (3300 mV) */
uint32_t measure_vdd_mv(void)
{
/* Read the factory calibration value (ADC reading at 3.3V) */
uint16_t vrefint_cal = *(volatile uint16_t *)VREFINT_CAL_ADDR;
/* Read the current VREFINT ADC value */
uint16_t vrefint_raw = adc_read_channel(17); /* Channel 17 = VREFINT */
/* Calculate VDD in millivolts:
* VDD = VREFINT_CAL_VREF * VREFINT_CAL / VREFINT_RAW
*
* Logic: VREFINT voltage is constant (~1.2V).
* If VDD drops, ADC reading of VREFINT goes UP
* (because ADC reference is VDD, and VREFINT stays constant).
*/
if (vrefint_raw == 0) return 0;
return (uint32_t)VREFINT_CAL_VREF * vrefint_cal / vrefint_raw;
}
/* Define voltage thresholds in millivolts */
#define VDD_WARNING_MV 3000 /* Below 3.0V: warn user */
#define VDD_CRITICAL_MV 2800 /* Below 2.8V: save state, reduce power */
#define VDD_SHUTDOWN_MV 2500 /* Below 2.5V: immediate safe shutdown */
typedef enum {
POWER_STATUS_OK,
POWER_STATUS_WARNING,
POWER_STATUS_CRITICAL,
POWER_STATUS_SHUTDOWN
} power_status_t;
power_status_t check_power_status(void)
{
uint32_t vdd = measure_vdd_mv();
if (vdd < VDD_SHUTDOWN_MV) {
return POWER_STATUS_SHUTDOWN;
} else if (vdd < VDD_CRITICAL_MV) {
return POWER_STATUS_CRITICAL;
} else if (vdd < VDD_WARNING_MV) {
return POWER_STATUS_WARNING;
}
return POWER_STATUS_OK;
}
/* Integrated into main loop: */
void power_monitor_task(void)
{
static power_status_t last_status = POWER_STATUS_OK;
power_status_t status = check_power_status();
if (status != last_status) {
switch (status) {
case POWER_STATUS_WARNING:
/* Reduce non-essential activity to save power */
reduce_display_brightness();
increase_sleep_intervals();
break;
case POWER_STATUS_CRITICAL:
/* Disable power-hungry peripherals */
disable_wifi_radio();
disable_led_indicators();
save_critical_state(); /* Last chance to save data */
break;
case POWER_STATUS_SHUTDOWN:
safe_shutdown(); /* Does not return */
break;
case POWER_STATUS_OK:
/* Voltage recovered — restore normal operation */
restore_normal_operation();
break;
}
last_status = status;
}
}This software approach gives you multiple warning levels and fine control over how your system responds to declining power. It complements the hardware BOD, which serves as the last-resort safety net. For details on ADC configuration, see ADC and DAC in Microcontrollers.
External Brownout Detection Circuits
For critical applications where the internal BOD is not precise enough, external voltage supervisor ICs provide more accurate thresholds, faster response times, and additional features:
- MAX809/MAX810 — Simple 3-pin voltage supervisors. Output goes low when VDD drops below threshold (available in various voltage options). Connect the output to the MCU’s NRST pin.
- TPS3839 — Ultra-low power (150 nA) supervisor, ideal for battery-powered devices.
- TL7705 — Provides an early warning output (RESET goes low before the MCU’s minimum voltage is reached), giving firmware time to save state.
The typical circuit is straightforward: VDD connects to the supervisor’s input, and the open-drain output connects to the MCU’s NRST pin (with a pull-up resistor). When VDD drops below the supervisor’s threshold, NRST is pulled low and the MCU resets. See Power Supply and Voltage Regulators for complete power circuit design.
BOD and Sleep Modes
The Brown-Out Detector consumes power — typically 20-50 µA. In deep sleep modes where your total budget might be 1-5 µA, the BOD can be the dominant power consumer. Some microcontrollers allow you to disable the BOD during sleep to save power, automatically re-enabling it on wake-up.
However, this creates a vulnerability window: if a brownout occurs while the MCU is asleep with BOD disabled, the MCU may wake up into an unstable state. The trade-off is:
- BOD enabled in sleep — safe but higher power consumption
- BOD disabled in sleep — lower power but risk of brownout during sleep
For battery-powered devices with a regulated supply, disabling BOD during sleep is usually safe because the regulator maintains a stable output. For devices powered directly from a battery (no regulator), keep BOD enabled. For more on power management strategies, see Sleep Modes and Low-Power Techniques and Battery Technologies for Embedded Devices.
Real-World War Story: The Case of the Corrupted Calibration
A temperature monitoring product was deployed in a factory. Every few months, one or two units would start reporting temperatures that were off by 5-10 degrees. The root cause: the factory’s large machines caused voltage dips on the AC mains when they started up. The wall adapter’s output would drop from 5V to 3.8V for a few milliseconds — not enough to trigger the POR, but enough to corrupt the flash page that stored the sensor calibration coefficients.
The fix was threefold:
- Enable the BOD at 2.7V to reset cleanly during voltage dips
- Store calibration data with a CRC checksum and maintain a backup copy in a separate flash page
- On startup, verify the CRC and restore from backup if corrupted
Here is the redundant storage pattern:
#include <stdint.h>
#include <string.h>
typedef struct {
float temp_offset;
float temp_scale;
float humidity_offset;
uint32_t serial_number;
uint32_t calibration_date;
uint32_t crc32; /* Must be last field */
} calibration_t;
/* Two copies in separate flash pages for redundancy */
#define CAL_PRIMARY_ADDR 0x0800F000UL
#define CAL_BACKUP_ADDR 0x0800F800UL
uint32_t compute_crc32(const void *data, uint32_t length)
{
const uint8_t *bytes = (const uint8_t *)data;
uint32_t crc = 0xFFFFFFFF;
for (uint32_t i = 0; i < length; i++) {
crc ^= bytes[i];
for (int bit = 0; bit < 8; bit++) {
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc = crc >> 1;
}
}
return ~crc;
}
int validate_calibration(const calibration_t *cal)
{
/* CRC is computed over all fields EXCEPT the CRC itself */
uint32_t expected_crc = compute_crc32(cal, sizeof(*cal) - sizeof(cal->crc32));
return (cal->crc32 == expected_crc) ? 1 : 0;
}
int load_calibration(calibration_t *cal)
{
const calibration_t *primary = (const calibration_t *)CAL_PRIMARY_ADDR;
const calibration_t *backup = (const calibration_t *)CAL_BACKUP_ADDR;
/* Try primary copy first */
if (validate_calibration(primary)) {
memcpy(cal, primary, sizeof(*cal));
return 1;
}
/* Primary corrupted — try backup */
if (validate_calibration(backup)) {
memcpy(cal, backup, sizeof(*cal));
/* Restore primary from backup */
flash_erase_page(CAL_PRIMARY_ADDR);
flash_write(CAL_PRIMARY_ADDR, (const uint8_t *)backup, sizeof(*cal));
uart_send_string("WARNING: Primary calibration restored from backuprn");
return 1;
}
/* Both copies corrupted — cannot operate accurately */
uart_send_string("ERROR: Calibration data corrupted. Factory reset required.rn");
return 0;
}
void save_calibration(const calibration_t *cal)
{
calibration_t to_save;
memcpy(&to_save, cal, sizeof(to_save));
/* Compute CRC before saving */
to_save.crc32 = compute_crc32(&to_save, sizeof(to_save) - sizeof(to_save.crc32));
/* Check voltage before flash operations */
if (is_voltage_low()) {
uart_send_string("ERROR: Low voltage — aborting calibration savern");
return;
}
/* Write to both pages */
flash_erase_page(CAL_PRIMARY_ADDR);
flash_write(CAL_PRIMARY_ADDR, (const uint8_t *)&to_save, sizeof(to_save));
flash_erase_page(CAL_BACKUP_ADDR);
flash_write(CAL_BACKUP_ADDR, (const uint8_t *)&to_save, sizeof(to_save));
}Summary
Brownout detection is one of those topics that separates hobby projects from production firmware. In a lab environment, you have a clean, stable power supply and brownouts never happen. In the real world — factories, vehicles, battery-powered field devices — they are a fact of life.
Key takeaways for your designs:
- Always enable the Brown-Out Detector in production firmware
- Set the BOD threshold above the minimum operating voltage of your most sensitive component
- Never write to EEPROM or flash without checking voltage first
- Use CRC checksums and redundant storage for critical non-volatile data
- Implement a safe shutdown sequence: disable outputs first, then save state
- Consider external voltage supervisors for mission-critical applications
- Use ADC-based voltage monitoring for graduated responses (warning → critical → shutdown)
For the complete picture of reset handling, also read Reset Systems in Microcontrollers. To understand how brownout detection fits into overall system power design, see Power Supply and Voltage Regulators and Power Consumption in Embedded Systems.

Vivek Bhageria — Lead Firmware R&D Engineer, 12+ years. Ex-Bosch (automotive powertrain), MusicTribe (real-time audio), medical devices. M.Tech BITS Pilani. I write at NerdyElectronics — practical, register-level embedded systems for engineers who want to understand what’s actually happening under the hood.





