Skip to content

Flash Programming

This example demonstrates a complete firmware update workflow: inspect the flash layout, erase, program a firmware image, verify, and reset the target.

import asyncio
import logging
import sys
from pathlib import Path
from openocd import Session, FlashError, ConnectionError
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
log = logging.getLogger("flash-program")
async def flash_firmware(firmware_path: Path):
"""Program a firmware image and verify it."""
if not firmware_path.exists():
log.error("Firmware file not found: %s", firmware_path)
return False
file_size = firmware_path.stat().st_size
log.info("Firmware: %s (%d bytes)", firmware_path.name, file_size)
try:
session = await Session.connect(timeout=5.0)
except ConnectionError as e:
log.error("Cannot connect to OpenOCD: %s", e)
return False
async with session:
# --- Halt the target first ---
log.info("Halting target...")
await session.target.halt()
# --- Inspect flash layout ---
banks = await session.flash.banks()
log.info("Found %d flash bank(s):", len(banks))
for bank in banks:
log.info(" Bank #%d: %s @ 0x%08X, size=0x%X (%d KB)",
bank.index, bank.name, bank.base, bank.size,
bank.size // 1024)
# Get detailed sector info for bank 0
bank_info = await session.flash.info(0)
log.info("Bank 0 has %d sectors", len(bank_info.sectors))
if bank_info.sectors:
first = bank_info.sectors[0]
last = bank_info.sectors[-1]
log.info(" First sector: offset=0x%X, size=0x%X", first.offset, first.size)
log.info(" Last sector: offset=0x%X, size=0x%X", last.offset, last.size)
# Check if firmware fits
if file_size > bank_info.size:
log.error("Firmware (%d bytes) is larger than flash bank (%d bytes)",
file_size, bank_info.size)
return False
# --- Erase ---
log.info("Erasing flash bank 0...")
try:
await session.flash.erase_all(bank=0)
log.info("Erase complete")
except FlashError as e:
log.error("Erase failed: %s", e)
return False
# --- Program ---
log.info("Programming %s...", firmware_path.name)
try:
await session.flash.write_image(
firmware_path,
erase=False, # Already erased above
verify=True, # Built-in post-write verification
)
log.info("Programming and verification complete")
except FlashError as e:
log.error("Programming failed: %s", e)
return False
# --- Optional: second verification pass ---
log.info("Running standalone verification...")
if firmware_path.suffix in (".bin",):
matches = await session.flash.verify(bank=0, path=firmware_path)
if matches:
log.info("Standalone verification PASSED")
else:
log.error("Standalone verification FAILED")
return False
# --- Reset and run ---
log.info("Resetting target to run new firmware...")
await session.target.reset(mode="run")
# Brief pause, then check state
await asyncio.sleep(0.5)
state = await session.target.state()
log.info("Target state: %s", state.state)
log.info("Firmware update complete")
return True
async def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <firmware.bin|hex|elf>")
sys.exit(1)
firmware = Path(sys.argv[1])
success = await flash_firmware(firmware)
sys.exit(0 if success else 1)
asyncio.run(main())
import logging
import sys
from pathlib import Path
from openocd import Session, FlashError, ConnectionError
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
log = logging.getLogger("flash-program")
def flash_firmware(firmware_path: Path) -> bool:
"""Program a firmware image and verify it."""
if not firmware_path.exists():
log.error("Firmware file not found: %s", firmware_path)
return False
file_size = firmware_path.stat().st_size
log.info("Firmware: %s (%d bytes)", firmware_path.name, file_size)
try:
session = Session.connect_sync(timeout=5.0)
except ConnectionError as e:
log.error("Cannot connect to OpenOCD: %s", e)
return False
with session:
log.info("Halting target...")
session.target.halt()
# Inspect flash
banks = session.flash.banks()
log.info("Found %d flash bank(s)", len(banks))
bank_info = session.flash.info(0)
log.info("Bank 0: %d sectors, %d KB",
len(bank_info.sectors), bank_info.size // 1024)
if file_size > bank_info.size:
log.error("Firmware too large for flash bank")
return False
# Erase
log.info("Erasing...")
try:
session.flash.erase_all(bank=0)
except FlashError as e:
log.error("Erase failed: %s", e)
return False
# Program with built-in verify
log.info("Programming %s...", firmware_path.name)
try:
session.flash.write_image(firmware_path, erase=False, verify=True)
log.info("Programming complete")
except FlashError as e:
log.error("Programming failed: %s", e)
return False
# Reset and run
log.info("Resetting target...")
session.target.reset(mode="run")
log.info("Done")
return True
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <firmware.bin|hex|elf>")
sys.exit(1)
success = flash_firmware(Path(sys.argv[1]))
sys.exit(0 if success else 1)
main()
INFO: Firmware: firmware.hex (32768 bytes)
INFO: Halting target...
INFO: Found 1 flash bank(s):
INFO: Bank #0: stm32f1x.flash @ 0x08000000, size=0x20000 (128 KB)
INFO: Bank 0 has 128 sectors
INFO: First sector: offset=0x0, size=0x400
INFO: Last sector: offset=0x1FC00, size=0x400
INFO: Erasing flash bank 0...
INFO: Erase complete
INFO: Programming firmware.hex...
INFO: Programming and verification complete
INFO: Running standalone verification...
INFO: Standalone verification PASSED
INFO: Resetting target to run new firmware...
INFO: Target state: running
INFO: Firmware update complete

For faster updates when only part of the flash changes, erase only the affected sectors instead of the whole bank:

async with await Session.connect() as session:
await session.target.halt()
bank = await session.flash.info(0)
firmware_size = Path("firmware.bin").stat().st_size
# Calculate which sectors the firmware occupies
last_sector = 0
cumulative = 0
for sector in bank.sectors:
cumulative = sector.offset + sector.size
if cumulative >= firmware_size:
last_sector = sector.index
break
log.info("Erasing sectors 0-%d (covers %d bytes)", last_sector, cumulative)
await session.flash.erase_sector(bank=0, first=0, last=last_sector)
await session.flash.write_image(Path("firmware.bin"), erase=False, verify=True)

After programming, protect the bootloader region so it cannot be accidentally overwritten:

async with await Session.connect() as session:
await session.target.halt()
# Program the full image
await session.flash.write_image(Path("firmware.hex"), erase=True, verify=True)
# Protect the first 2 sectors (bootloader)
await session.flash.protect(bank=0, first=0, last=1, on=True)
log.info("Bootloader sectors protected")