What Are Option Bytes?
Option bytes reside in a dedicated flash memory bank, physically separate from the main program flash. They are organised as a set of 8โ24 bytes (depending on the family) that configure hardware behaviour before the CPU fetches the first instruction. On STM32F4, the option bytes map to FLASH_OPTCR and FLASH_OPTCR1 registers. On STM32L4/G4/U5, they are accessed through FLASH_OPTR, FLASH_PCROP1ER, FLASH_WRP1AR, and similar registers.
The key distinction: option byte changes take effect only after a system reset or a power-on cycle. Writing the byte is just the first step โ you must also trigger a reload.
RDP: Read Protection Levels
The RDP (Read Protection) byte is the most critical option byte. It controls access to the flash memory through the debug interface (SWD/JTAG) and the system bootloader. Three levels exist across all modern STM32 families:
| Level | RDP Value | Behaviour |
|---|---|---|
| Level 0 | 0xAA | No protection. Full debug access, bootloader read, all flash sectors accessible. |
| Level 1 | 0xBB (any value except 0xAA and 0xCC) | Flash memory inaccessible via debug or bootloader unless a mass erase is performed. CPU can still execute and read flash. Use this for shipping products to prevent firmware extraction. |
| Level 2 | 0xCC | Permanent read protection. Debug interface is permanently disabled. Irreversible. The chip becomes a black box โ no debug, no bootloader, no mass erase to revert. Use only when physical access to the device is untrusted. |
Register-Level RDP Programming (STM32F4)
/* Option byte programming sequence on STM32F4 */
FLASH->OPTKEYR = 0x08192A3B; /* unlock OPTKEY1 */
FLASH->OPTKEYR = 0x4C5D6E7F; /* unlock OPTKEY2 */
/* Wait for option byte unlock */
while (!(FLASH->SR & FLASH_SR_BSY)); /* wait if a previous operation is running */
FLASH->OPTCR = (FLASH->OPTCR & ~FLASH_OPTCR_RDP_Msk) /* clear RDP field */
| (0xBB << FLASH_OPTCR_RDP_Pos); /* Level 1 */
/* Launch option byte loading */
FLASH->OPTCR |= FLASH_OPTCR_OPTSTRT; /* start option byte programming */
while (FLASH->SR & FLASH_SR_BSY); /* wait for completion */
/* Required: system reset to apply the new option byte */
NVIC_SystemReset();
Register-Level RDP Programming (STM32L4/G4/U5)
/* On L4/G4/U5, option bytes use a dedicated register set */
/* Step 1: Unlock the flash control register */
FLASH->KEYR = 0x45670123; /* unlock KEY1 */
FLASH->KEYR = 0xCDEF89AB; /* unlock KEY2 */
/* Step 2: Unlock option bytes */
FLASH->OPTKEYR = 0x08192A3B; /* unlock OPTKEY1 */
FLASH->OPTKEYR = 0x4C5D6E7F; /* unlock OPTKEY2 */
/* Step 3: Set RDP to Level 1 (value 0xBB) */
FLASH->OPTR = (FLASH->OPTR & ~FLASH_OPTR_RDP_Msk) | (0xBB << FLASH_OPTR_RDP_Pos);
/* Step 4: Launch option byte programming */
FLASH->CR |= FLASH_CR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);
/* Step 5: System reset */
NVIC_SystemReset();
Critical warning: If you program RDP Level 1 without having a working firmware update path, you will need a mass erase via the bootloader to make changes โ which erases all flash contents, including the option bytes that locked it. Level 2 must be programmed at the very end of production and tested on sacrificial units first. Once Level 2 is set, no debugger will ever talk to that chip again.
BOR: Brown-Out Reset Level
The BOR (Brown-Out Reset) level sets the voltage threshold below which the STM32 is held in reset. This prevents the CPU from executing code at voltages where flash read operations are unreliable. On F4, the BOR level is configured through option bits in FLASH_OPTCR. On L4/G4/U5, it is in FLASH_OPTR.
| BOR Level | Threshold (F4) | Threshold (L4) | Use Case |
|---|---|---|---|
| Level 0 (off) | โ | โ | No BOR. Supply must be externally supervised. |
| Level 1 | ~2.10 V | ~1.80 V | Safe for 2.4โ2.7 V battery operation |
| Level 2 | ~2.30 V | ~2.00 V | Standard 3.3 V operation with margin |
| Level 3 | ~2.55 V | ~2.20 V | Conservative; use for noisy supplies or automotive |
| Level 4 | ~2.80 V | ~2.50 V | High threshold for 3.3 V ยฑ 5% supplies |
/* Set BOR Level 2 on STM32F4 (3.3 V operation) */
FLASH->OPTCR = (FLASH->OPTCR & ~FLASH_OPTCR_BOR_LEV_Msk)
| (2 << FLASH_OPTCR_BOR_LEV_Pos)
| FLASH_OPTCR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);
/* On STM32L4/U5 */
FLASH->OPTR = (FLASH->OPTR & ~FLASH_OPTR_BOR_LEV_Msk)
| (2 << FLASH_OPTR_BOR_LEV_Pos);
FLASH->CR |= FLASH_CR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);
The BOR resets the chip when VDD drops below the threshold and prevents startup until VDD rises above it (with hysteresis of approximately 100 mV). On battery-powered products, setting BOR too high causes premature shutdown; setting it too low risks flash read errors in partial VDD conditions.
Boot Mode and Pin Configuration
The boot pins (BOOT0, BOOT1/nBOOT1) determine where the CPU fetches the first instruction after reset. Option bytes allow overriding the pin state without changing the hardware. This is essential for production products that must boot from the main flash but need a fallback bootloader for field updates.
Key option bits for boot configuration:
- nBOOT0 โ inverted copy of the BOOT0 pin value. When the option byte nBOOT0 is programmed to
0, the BOOT0 pin is effectively pulled low (boot from main flash). - nBOOT1 โ selects the boot mode when BOOT0 = 1. When
1(default), boot from system memory (bootloader). When0, boot from SRAM. - BOOT_LOCK (L4/G4/U5) โ when set, the bootloader ignores the BOOT0 pin and always boots from the main flash. This prevents a glitch or physical manipulation from forcing the chip into bootloader mode.
- nSWBOOT0 (L4/G4/U5) โ when set, the boot configuration is fully controlled by option bytes (nBOOT0 + nBOOT1), ignoring the BOOT0 pin entirely.
/* Configure F4 to always boot from main flash, ignoring BOOT0 pin */
/* This is done via nBOOT0 option bit */
FLASH->OPTCR = (FLASH->OPTCR & ~FLASH_OPTCR_nBOOT0)
| FLASH_OPTCR_nBOOT0; /* nBOOT0 = 1, BOOT0 pinned low */
FLASH->OPTCR |= FLASH_OPTCR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);
/* On L4/G4/U5: ignore BOOT0 pin entirely, boot from flash */
FLASH->OPTR |= FLASH_OPTR_nBOOT0 /* BOOT0 internally pulled low */
| FLASH_OPTR_nSWBOOT0; /* software boot config enabled */
FLASH->CR |= FLASH_CR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);
OTP Area: One-Time Programmable Bytes
All STM32 families provide an OTP (One-Time Programmable) area within the flash memory. On F4, this is 512 bytes (16 banks of 32 bits each). On L4/G4/U5, it is typically 1024 bytes organised as 32 OTP words of 32 bits each, plus a single non-reusable lock byte per word.
Use cases for OTP:
- Unique device serial number (burned during production test)
- Factory MAC address or cryptographic key seed
- Calibration coefficients that must never change
- Production date and batch code
/* Writing OTP on STM32L4/G4 */
/* OTP base address: 0x1FFF7000 on L4, 0x1FFF7200 on G4 */
#define OTP_BASE 0x1FFF7000UL
#define OTP_LOCK_BASE 0x1FFF7010UL /* lock byte for each OTP word */
/* Write a 32-bit calibration value to OTP word 4 */
uint32_t *otp_word4 = (uint32_t *)(OTP_BASE + 4 * 4);
*otp_word4 = 0xDEADBEEF; /* direct write (flash controller handles it) */
/* Lock OTP word 4 to prevent future writes */
volatile uint8_t *lock4 = (uint8_t *)(OTP_LOCK_BASE + 4);
*lock4 = 0x00; /* writing 0 locks the word (cannot be undone) */
/* Reading back is straightforward */
uint32_t cal = *otp_word4; /* always readable */
OTP is one-time programmable per word. You can write each OTP word exactly once. The lock byte, when cleared to 0x00, permanently write-protects that word โ even the programmer cannot change it. There is no way to unlock an OTP word. Plan your OTP layout before any production run.
Write Protection (WRP)
The WRP (Write Protection) option bytes prevent accidental or malicious writes to selected flash sectors. In production, protect the bootloader and RDP configuration sectors. On STM32F4, protection is set per sector; on L4/G4/U5, per 2โ4 KB pages.
/* Protect sectors 0โ3 on F4 (bootloader area, 64 KB) */
FLASH->OPTCR = (FLASH->OPTCR & ~FLASH_OPTCR_WRP_Msk)
| (0x0F << FLASH_OPTCR_WRP_Pos); /* sectors 0โ3 */
FLASH->OPTCR |= FLASH_OPTCR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);
/* On STM32L4, protect pages 0โ7 using WRP1AR */
FLASH->WRP1AR = (0 << FLASH_WRP1AR_WRP1A_STRT_Pos) /* start page 0 */
| (7 << FLASH_WRP1AR_WRP1A_END_Pos) /* end page 7 */
| FLASH_WRP1AR_WRP1A_EN;
FLASH->CR |= FLASH_CR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);
PCROP: Proprietary Code Readout Protection
PCROP (Proprietary Code Readout Protection) is an extension available on L4/G4/U5 that protects selected flash pages from being read even by the CPU. This is different from RDP โ RDP protects against external debug reads, while PCROP prevents any bus master (CPU, DMA, ART accelerator) from reading the protected pages. Code stored in PCROP regions can still be executed by the CPU (instruction fetch) but cannot be read as data.
Use PCROP for:
- AES key tables and cryptographic primitives
- License-checking routines
- Bootloader that must remain confidential even from application code
Practical Example: Production Programming Flow
A typical production programming script (executed via STM32CubeProgrammer CLI or a custom flasher on the test jig) follows this sequence:
/* Phase 1: Flash the application binary */
/* (erase + write main flash) */
/* Phase 2: Program option bytes for production */
FLASH->OPTKEYR = 0x08192A3B;
FLASH->OPTKEYR = 0x4C5D6E7F;
/* Set BOR Level 2 for 3.3 V operation */
FLASH->OPTR = (FLASH->OPTR & ~FLASH_OPTR_BOR_LEV_Msk)
| (2 << FLASH_OPTR_BOR_LEV_Pos)
| FLASH_OPTR_nBOOT0 /* boot from flash */
| FLASH_OPTR_nSWBOOT0; /* ignore BOOT0 pin */
/* Protect bootloader pages from errant writes */
FLASH->WRP1AR = (0 << FLASH_WRP1AR_WRP1A_STRT_Pos)
| (3 << FLASH_WRP1AR_WRP1A_END_Pos)
| FLASH_WRP1AR_WRP1A_EN;
/* Write OTP with unique ID and calibration */
*(uint32_t *)(OTP_BASE + 0) = unique_id_lo;
*(uint32_t *)(OTP_BASE + 4) = unique_id_hi;
*(uint8_t *)(OTP_LOCK_BASE + 0) = 0x00; /* lock word 0 */
*(uint8_t *)(OTP_LOCK_BASE + 1) = 0x00; /* lock word 1 */
/* Set RDP Level 1 (last step โ after this, debug is locked) */
FLASH->OPTR = (FLASH->OPTR & ~FLASH_OPTR_RDP_Msk)
| (0xBB << FLASH_OPTR_RDP_Pos);
FLASH->CR |= FLASH_CR_OPTSTRT;
while (FLASH->SR & FLASH_SR_BSY);
/* Phase 3: System reset to apply */
NVIC_SystemReset();
The order is intentional: write protection first, OTP second, then RDP last. If RDP Level 2 is required, it must be absolutely the last operation โ every other option byte must be finalised before RDP2 is set, because there is no recovery.
Practical Checklist
| Check | Why |
|---|---|
| Option byte unlock sequence correct? | Wrong keys silently ignored; writes have no effect |
| OPTSTRT launched after every OPTCR change? | Without OPTSTRT, option bytes are not programmed |
| System reset performed after programming? | Option bytes only apply after POR or system reset |
| RDP Level 1 tested on sacrificial unit first? | Level 1 requires mass erase to change โ verify your update path |
| Level 2 planned or accidental? | Level 2 is permanent. One sacrificial unit to confirm, then proceed. |
| BOR level appropriate for supply voltage? | Too low = flash errors. Too high = early battery cutoff. |
| nSWBOOT0 + nBOOT0 configured for pin-independent boot? | Production boards may have BOOT0 floating or hard-wired incorrectly |
| OTP layout documented in production spec? | Each word is one shot. No erasing, no rewriting. |
| PCROP boundaries do not overlap interrupt vectors? | VTOR points to the start of flash; PCROP on sector 0 blocks vector reads |
| WRP covers bootloader but not OTA update region? | If the bootloader updates itself, it needs write access to its own pages |
How I Would Approach This on a Client Project
I worked with a client producing a battery-powered gas sensor that ran on an STM32L051. The production line was burning a unique serial number and calibration coefficients into OTP, then setting RDP Level 1 and nSWBOOT0 to avoid pin-dependent boot. The first batch of 200 units came back with corrupted calibration data after three months.
Root cause: the production script was writing OTP after setting RDP Level 1. The mass erase that accompanied the RDP transition (Level 1 requires mass erase when going from Level 0 to Level 1, since old Level 0 units needed their flash erased) was also clearing the OTP area. The fix: write OTP before setting RDP, and lock the OTP words immediately after writing. This also uncovered a second issue โ the test jig was reading back OTP before the lock byte was written, getting stale data from the previous unit because OTP retained its old value when the flash mass erase was bypassed for OTP.
Lesson: the order of option byte operations in production matters as much as the values themselves. Always write a production programming spec that defines the exact sequence, including which steps need a power cycle, and verify on sacrificial units before committing to volume.
On another project with STM32G4 for an automotive actuator, the client required RDP Level 2 for IP protection. We set all option bytes (BOR Level 3, WR protection, nSWBOOT0, PCROP for the crypto library) before the final RDP2 step. The last unit before RDP2 is programmed on the test jig is used for HALT debugging in system tests. After validation, the RDP2 bit is written โ and the unit is sealed forever.
Sources consulted:
- STM32F4 Reference Manual (RM0090) โ Chapter 3: Flash memory interface, Section 3.7: Option bytes
- STM32L4 Reference Manual (RM0351) โ Chapter 4: Flash, Section 4.3: Option bytes
- STM32G4 Reference Manual (RM0440) โ Chapter 4: Flash, Section 4.3: Option bytes
- STM32U5 Reference Manual (RM0456) โ Chapter 6: Flash, Section 6.6: Option bytes
- ST Application Note AN3241 โ Option byte programming for STM32F4 series
- ST Application Note AN4803 โ STM32L4 option byte programming
- ST Application Note AN2606 โ STM32 system memory boot mode
- ST Application Note AN4370 โ PCROP introduction
- CMSIS-Core 5.6.0 โ
stm32l4xx.h,stm32f4xx.hregister definitions

💬 Comments
Have questions or experience with STM32 option byte programming, RDP levels, OTP corruption, or production programming flows? Drop me an email. Include the article slug in the subject line.