---
title: Flash Programming
description: Read, write, erase, verify, and protect on-chip flash memory banks through OpenOCD.
---

import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';

The `Flash` subsystem wraps OpenOCD's `flash` command family for programming on-chip flash memory. It handles bank enumeration, sector-level erase, raw byte read/write through temporary files, high-level firmware image programming, verification, and write protection.

Access it through the session:

```python
# Async
session.flash

# Sync
sync_session.flash
```

## Listing flash banks

`banks()` returns a list of `FlashBank` descriptors for every flash bank OpenOCD has configured. These come without detailed sector information -- use `info()` for that.

<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session

async def main():
    async with await Session.connect() as session:
        banks = await session.flash.banks()
        for bank in banks:
            print(f"Bank #{bank.index}: {bank.name}")
            print(f"  Base: 0x{bank.base:08X}, Size: 0x{bank.size:X}")
            print(f"  Bus width: {bank.bus_width}, Chip width: {bank.chip_width}")
            print(f"  Target: {bank.target}")

asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session

with Session.connect_sync() as session:
    banks = session.flash.banks()
    for bank in banks:
        print(f"Bank #{bank.index}: {bank.name}")
        print(f"  Base: 0x{bank.base:08X}, Size: 0x{bank.size:X}")
        print(f"  Bus width: {bank.bus_width}, Chip width: {bank.chip_width}")
        print(f"  Target: {bank.target}")
```
</TabItem>
</Tabs>

## Getting bank details with sectors

`info(bank=0)` returns a `FlashBank` with its `sectors` list populated. Each sector is a `FlashSector` with index, offset, size, and protection status.

<Tabs>
<TabItem label="Async">
```python
async with await Session.connect() as session:
    bank = await session.flash.info(0)
    print(f"{bank.name}: {len(bank.sectors)} sectors")
    for sector in bank.sectors:
        prot = "protected" if sector.protected else "unprotected"
        print(f"  Sector {sector.index}: offset=0x{sector.offset:X}, "
              f"size=0x{sector.size:X}, {prot}")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as session:
    bank = session.flash.info(0)
    print(f"{bank.name}: {len(bank.sectors)} sectors")
    for sector in bank.sectors:
        prot = "protected" if sector.protected else "unprotected"
        print(f"  Sector {sector.index}: offset=0x{sector.offset:X}, "
              f"size=0x{sector.size:X}, {prot}")
```
</TabItem>
</Tabs>

## Reading flash

Two methods for reading flash content:

- **`read(bank, offset, size)`** -- returns raw `bytes` by writing to a temp file, then reading the data back through TCL or from the local filesystem.
- **`read_to_file(bank, path)`** -- dumps the entire bank directly to a file on disk.

<Tabs>
<TabItem label="Async">
```python
from pathlib import Path

async with await Session.connect() as session:
    # Read 256 bytes from the start of bank 0
    data = await session.flash.read(bank=0, offset=0, size=256)
    print(f"Read {len(data)} bytes: {data[:16].hex()}")

    # Dump the entire bank to a file
    await session.flash.read_to_file(bank=0, path=Path("flash_dump.bin"))
```
</TabItem>
<TabItem label="Sync">
```python
from pathlib import Path

with Session.connect_sync() as session:
    data = session.flash.read(bank=0, offset=0, size=256)
    print(f"Read {len(data)} bytes: {data[:16].hex()}")

    session.flash.read_to_file(bank=0, path=Path("flash_dump.bin"))
```
</TabItem>
</Tabs>

<Aside type="note">
`read()` uses a temporary file on the host as an intermediary because OpenOCD's `flash read_bank` command writes to a file rather than returning data inline. The temp file is cleaned up automatically.
</Aside>

## Writing flash

### Raw byte write

`write(bank, offset, data)` writes raw bytes to a flash bank at a given offset. Like `read()`, it uses a temporary file since OpenOCD's `flash write_bank` reads from a file.

<Tabs>
<TabItem label="Async">
```python
async with await Session.connect() as session:
    config_data = b"\x01\x02\x03\x04"
    await session.flash.write(bank=0, offset=0x1000, data=config_data)
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as session:
    config_data = b"\x01\x02\x03\x04"
    session.flash.write(bank=0, offset=0x1000, data=config_data)
```
</TabItem>
</Tabs>

<Aside type="caution">
`write()` does not erase sectors first. Flash memory can only transition bits from 1 to 0; writing to unerased flash produces incorrect data. Use `erase_sector()` or `erase_all()` before raw writes, or use `write_image()` which handles this automatically.
</Aside>

### Firmware image programming

`write_image(path, erase=True, verify=True)` is the high-level "flash and go" command. It handles erase, write, and verification in one operation. It accepts `.bin`, `.hex`, and `.elf` files.

<Tabs>
<TabItem label="Async">
```python
from pathlib import Path

async with await Session.connect() as session:
    firmware = Path("build/firmware.hex")
    await session.flash.write_image(firmware, erase=True, verify=True)
    print("Firmware programmed and verified")
```
</TabItem>
<TabItem label="Sync">
```python
from pathlib import Path

with Session.connect_sync() as session:
    firmware = Path("build/firmware.hex")
    session.flash.write_image(firmware, erase=True, verify=True)
    print("Firmware programmed and verified")
```
</TabItem>
</Tabs>

When `verify=True`, the method runs `verify_image` after writing. If the verification finds a mismatch, it raises `FlashError`.

## Erasing flash

### Erase a sector range

`erase_sector(bank, first, last)` erases sectors from `first` to `last` (both inclusive). Validates that `first <= last` and raises `FlashError` if the range is invalid.

<Tabs>
<TabItem label="Async">
```python
async with await Session.connect() as session:
    # Erase sectors 0 through 3 in bank 0
    await session.flash.erase_sector(bank=0, first=0, last=3)
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as session:
    session.flash.erase_sector(bank=0, first=0, last=3)
```
</TabItem>
</Tabs>

### Erase an entire bank

`erase_all(bank=0)` queries the bank info to find the last sector, then erases the full range.

```python
await session.flash.erase_all(bank=0)
```

## Write protection

`protect(bank, first, last, on)` sets or clears hardware write protection on a range of sectors.

<Tabs>
<TabItem label="Async">
```python
async with await Session.connect() as session:
    # Protect the bootloader (sectors 0-1)
    await session.flash.protect(bank=0, first=0, last=1, on=True)

    # Unprotect application sectors (2-7)
    await session.flash.protect(bank=0, first=2, last=7, on=False)
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as session:
    session.flash.protect(bank=0, first=0, last=1, on=True)
    session.flash.protect(bank=0, first=2, last=7, on=False)
```
</TabItem>
</Tabs>

## Verifying flash

`verify(bank, path)` compares flash contents against a reference binary file and returns `True` if they match, `False` otherwise.

<Tabs>
<TabItem label="Async">
```python
from pathlib import Path

async with await Session.connect() as session:
    matches = await session.flash.verify(bank=0, path=Path("golden.bin"))
    if matches:
        print("Flash contents match reference file")
    else:
        print("MISMATCH detected")
```
</TabItem>
<TabItem label="Sync">
```python
from pathlib import Path

with Session.connect_sync() as session:
    matches = session.flash.verify(bank=0, path=Path("golden.bin"))
    if matches:
        print("Flash contents match reference file")
    else:
        print("MISMATCH detected")
```
</TabItem>
</Tabs>

## Data types

### FlashBank

| Field | Type | Description |
|-------|------|-------------|
| `index` | `int` | Bank number |
| `name` | `str` | Bank name (e.g. `stm32f1x.flash`) |
| `base` | `int` | Base address |
| `size` | `int` | Total size in bytes |
| `bus_width` | `int` | Bus width |
| `chip_width` | `int` | Chip width |
| `target` | `str` | Associated target or driver name |
| `sectors` | `list[FlashSector]` | Sector list (empty from `banks()`, populated from `info()`) |

### FlashSector

| Field | Type | Description |
|-------|------|-------------|
| `index` | `int` | Sector number within the bank |
| `offset` | `int` | Byte offset from the bank base |
| `size` | `int` | Sector size in bytes |
| `protected` | `bool` | Whether write protection is enabled |

## Error handling

All flash operations raise `FlashError` on failure. The error message includes the OpenOCD response for diagnostics.

```python
from openocd import Session, FlashError

try:
    with Session.connect_sync() as session:
        session.flash.write_image(Path("firmware.bin"))
except FlashError as e:
    print(f"Flash operation failed: {e}")
```

## Method reference

| Method | Return Type | Description |
|--------|-------------|-------------|
| `banks()` | `list[FlashBank]` | List all configured flash banks |
| `info(bank=0)` | `FlashBank` | Detailed bank info with sectors |
| `read(bank, offset, size)` | `bytes` | Read raw flash via temp file |
| `read_to_file(bank, path)` | `None` | Dump entire bank to file |
| `write(bank, offset, data)` | `None` | Write raw bytes via temp file |
| `write_image(path, erase=True, verify=True)` | `None` | High-level flash programming |
| `erase_sector(bank, first, last)` | `None` | Erase a sector range |
| `erase_all(bank=0)` | `None` | Erase entire bank |
| `protect(bank, first, last, on)` | `None` | Set/clear write protection |
| `verify(bank, path)` | `bool` | Verify flash against a file |
