Merge feat/content-a: Getting Started and core guides (10 pages)

This commit is contained in:
Ryan Malloy 2026-02-14 18:41:04 -07:00
commit fd424643f5
10 changed files with 2712 additions and 10 deletions

View File

@ -3,4 +3,191 @@ title: CLI Reference
description: Command-line interface for openocd-python
---
Content coming soon.
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
openocd-python ships with a command-line tool for quick diagnostics and interactive debugging. It connects to an already-running OpenOCD instance over the TCL RPC protocol.
## Usage
```bash
openocd-python [--host HOST] [--port PORT] COMMAND
```
### Global options
| Option | Default | Description |
|--------|---------|-------------|
| `--host` | `localhost` | OpenOCD host address |
| `--port` | `6666` | OpenOCD TCL RPC port |
| `--version` | | Print the package version and exit |
### Available commands
| Command | Description |
|---------|-------------|
| `info` | Show target state, transport, adapter, and speed |
| `repl` | Interactive OpenOCD command shell |
| `read` | Read memory and display as hexdump |
| `scan` | Scan and display the JTAG chain |
## `info` -- Target information
Displays the current target state and adapter configuration in a single overview.
```bash
openocd-python info
```
Example output:
```
=== OpenOCD Target Info ===
Target: stm32f1x.cpu
State: halted
PC: 0x08001234
Transport: swd
Adapter: cmsis-dap
Speed: 4000 kHz
```
The `info` command queries four things:
- **Target name and state** via the `targets` command
- **Program counter** (only when halted) via `reg pc`
- **Transport** (e.g. swd, jtag) via `transport select`
- **Adapter name and speed** via `adapter name` and `adapter speed`
If any query fails (for example, no target is configured), that section is skipped rather than causing an error.
## `repl` -- Interactive command shell
Opens an interactive prompt where you can type raw OpenOCD TCL commands and see the responses.
```bash
openocd-python repl
```
```
OpenOCD REPL (type 'quit' or Ctrl-D to exit)
ocd> targets
TargetName Type Endian TapName State
-- ------------------ ---------- ------ ------------------ -----
0* stm32f1x.cpu cortex_m little stm32f1x.cpu halted
ocd> reg pc
pc (/32): 0x08001234
ocd> adapter speed
4000
ocd> quit
```
### REPL options
| Option | Default | Description |
|--------|---------|-------------|
| `--timeout` | `10.0` | Command timeout in seconds |
Exit the REPL by typing `quit`, `exit`, `q`, or pressing Ctrl-D.
<Aside type="tip">
The REPL is useful for exploring OpenOCD commands interactively before writing them into scripts. Any command you can type in the OpenOCD telnet interface (port 4444) also works here via the TCL RPC protocol.
</Aside>
## `read` -- Memory hexdump
Reads a block of memory and displays it as a formatted hexdump with both hex and ASCII columns.
```bash
openocd-python read ADDRESS [SIZE]
```
| Argument | Default | Description |
|----------|---------|-------------|
| `ADDRESS` | (required) | Start address in hex (e.g. `0x08000000`) |
| `SIZE` | `64` | Number of bytes to read |
Example:
```bash
openocd-python read 0x08000000 64
```
```
08000000: 00 50 00 20 A1 01 00 08 AB 01 00 08 AD 01 00 08 |.P. ............|
08000010: AF 01 00 08 B1 01 00 08 B3 01 00 08 00 00 00 00 |................|
08000020: 00 00 00 00 00 00 00 00 00 00 00 00 B5 01 00 08 |................|
08000030: B7 01 00 08 00 00 00 00 B9 01 00 08 BB 01 00 08 |................|
```
The hexdump format shows 16 bytes per line with:
- Address on the left
- Two groups of 8 hex bytes separated by a gap
- ASCII representation on the right (non-printable bytes shown as `.`)
<Aside type="note">
The address is parsed with Python's `int(addr, 0)`, so you can use hex (`0x08000000`), decimal (`134217728`), or octal (`0o1000000000`) notation.
</Aside>
## `scan` -- JTAG chain scan
Scans the JTAG chain and displays all discovered TAPs (Test Access Ports) with their IDCODEs and IR lengths.
```bash
openocd-python scan
```
Example output:
```
TAP Name IDCODE IR Enabled
--------------------------------------------------
stm32f1x.cpu 0x3BA00477 4 yes
stm32f1x.bs 0x06414041 5 yes
```
If no TAPs are found, the command prints:
```
No TAPs found on the JTAG chain.
```
<Aside type="caution">
The `scan` command requires JTAG transport. If your target is configured for SWD, the scan may not return any TAPs since SWD does not have a scan chain in the JTAG sense.
</Aside>
## Connecting to a remote instance
All commands accept `--host` and `--port` to target a remote OpenOCD instance:
```bash
# OpenOCD running on a Raspberry Pi
openocd-python --host 192.168.1.50 --port 6666 info
# OpenOCD on a non-standard port
openocd-python --port 7777 repl
```
## Running with uv
If you installed openocd-python in a project managed by `uv`, use `uv run`:
```bash
uv run openocd-python info
uv run openocd-python read 0x08000000 128
```
Or run it directly without installation using `uvx`:
```bash
uvx openocd-python info
```
## Next steps
- [First Connection](/getting-started/first-connection/) -- use the Python API for programmatic access
- [Quick Start](/getting-started/quick-start/) -- common tasks as Python scripts
- [Memory Operations](/guides/memory-operations/) -- the full memory read/write API behind the `read` command

View File

@ -3,4 +3,210 @@ title: First Connection
description: Connect to an OpenOCD instance for the first time
---
Content coming soon.
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
openocd-python provides two ways to talk to OpenOCD: connect to an already-running instance, or spawn a new OpenOCD process and connect to it. Both use the `Session` class as the entry point.
## Prerequisites
Before connecting, you need a running OpenOCD instance or a valid OpenOCD configuration file. Start OpenOCD in a separate terminal:
```bash
openocd -f interface/cmsis-dap.cfg -f target/stm32f1x.cfg
```
You should see output ending with something like:
```
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
Info : Listening on port 3333 for gdb connections
```
Port 6666 is the TCL RPC port that openocd-python uses.
## Connect to a running instance
The most common pattern is connecting to an OpenOCD instance that is already running. Use `Session.connect()` for async code or `Session.connect_sync()` for synchronous scripts.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
async with Session.connect() as ocd:
state = await ocd.target.state()
print(f"Target: {state.name}")
print(f"State: {state.state}")
if state.current_pc is not None:
print(f"PC: 0x{state.current_pc:08X}")
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.connect_sync() as ocd:
state = ocd.target.state()
print(f"Target: {state.name}")
print(f"State: {state.state}")
if state.current_pc is not None:
print(f"PC: 0x{state.current_pc:08X}")
```
</TabItem>
</Tabs>
### Connection parameters
`Session.connect()` accepts three optional arguments:
| Parameter | Default | Description |
|-----------|---------|-------------|
| `host` | `"localhost"` | Hostname or IP address of the OpenOCD instance |
| `port` | `6666` | TCL RPC port number |
| `timeout` | `10.0` | Connection timeout in seconds |
```python
# Connect to OpenOCD on a remote machine
async with Session.connect(host="192.168.1.50", port=6666, timeout=5.0) as ocd:
state = await ocd.target.state()
```
## Spawn and connect
If you want openocd-python to manage the OpenOCD process for you, use `Session.start()`. This spawns OpenOCD as a subprocess, waits for the TCL RPC port to become available, and connects to it. When the context manager exits, the process is stopped automatically.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
async with Session.start("interface/cmsis-dap.cfg -f target/stm32f1x.cfg") as ocd:
state = await ocd.target.state()
print(f"Target: {state.name}, State: {state.state}")
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.start_sync("interface/cmsis-dap.cfg -f target/stm32f1x.cfg") as ocd:
state = ocd.target.state()
print(f"Target: {state.name}, State: {state.state}")
```
</TabItem>
</Tabs>
### Start parameters
`Session.start()` accepts these arguments:
| Parameter | Default | Description |
|-----------|---------|-------------|
| `config` | (required) | Config file path or `-f`/`-c` flags as a string |
| `tcl_port` | `6666` | TCL RPC port for the spawned process |
| `openocd_bin` | `None` | Path to OpenOCD binary (auto-detected from `PATH` if `None`) |
| `timeout` | `10.0` | Seconds to wait for OpenOCD to start and become ready |
| `extra_args` | `None` | Additional CLI arguments passed to OpenOCD |
The `config` parameter is flexible. You can pass a plain filename (which gets wrapped with `-f`), or include `-f` and `-c` flags explicitly:
```python
# Plain config file -- automatically wrapped as "-f interface/cmsis-dap.cfg"
await Session.start("interface/cmsis-dap.cfg")
# Multiple config files with explicit flags
await Session.start("-f interface/cmsis-dap.cfg -f target/stm32f1x.cfg")
# Inline TCL commands
await Session.start("-f interface/cmsis-dap.cfg -f target/stm32f1x.cfg -c 'adapter speed 4000'")
# With extra args
await Session.start(
"interface/cmsis-dap.cfg -f target/stm32f1x.cfg",
extra_args=["-d2"], # debug level 2
)
```
<Aside type="caution">
`Session.start()` requires OpenOCD to be installed and available on your `PATH`. If OpenOCD is not on `PATH`, pass the full binary path via `openocd_bin="/usr/local/bin/openocd"`.
</Aside>
## The TCL RPC protocol
openocd-python communicates over OpenOCD's TCL RPC protocol on port 6666. The protocol is straightforward:
1. The client sends a command string followed by a `\x1a` (ASCII SUB) byte
2. The server responds with the result string followed by a `\x1a` byte
The library uses a **dual-socket design**: one TCP connection for command/response pairs, and a separate connection for asynchronous notifications (target halt events, reset events, etc.). This prevents notification messages from corrupting the command stream.
<Aside type="note">
The TCL RPC port (6666) is different from the telnet port (4444) and the GDB server port (3333). openocd-python only uses the TCL RPC port.
</Aside>
## Sending raw commands
Every `Session` (and `SyncSession`) exposes a `command()` method that sends an arbitrary OpenOCD TCL command and returns the raw response string. This is the escape hatch when you need functionality that is not yet wrapped by a subsystem.
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
version = await ocd.command("version")
print(version)
# Any valid OpenOCD TCL command works
await ocd.command("adapter speed 4000")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
version = ocd.command("version")
print(version)
ocd.command("adapter speed 4000")
```
</TabItem>
</Tabs>
## Context managers and cleanup
Both `Session.connect()` and `Session.start()` return a `Session` object that acts as an async context manager. Using `async with` (or `with` for sync) ensures the connection is closed and any spawned OpenOCD process is terminated when you are done:
```python
# The connection is closed automatically at the end of the block
async with Session.connect() as ocd:
await ocd.target.halt()
# Connection is now closed
# If you started OpenOCD, the process is also terminated
async with Session.start("interface/cmsis-dap.cfg -f target/stm32f1x.cfg") as ocd:
await ocd.target.halt()
# OpenOCD process has been stopped
```
If you need manual lifecycle control, you can call `close()` directly:
```python
ocd = await Session.connect()
try:
state = await ocd.target.state()
finally:
await ocd.close()
```
## Next steps
- [Quick Start](/getting-started/quick-start/) -- complete working examples for common tasks
- [Session Lifecycle](/guides/session-lifecycle/) -- deep dive into session management, lazy subsystems, and the process manager
- [Async vs Sync](/guides/async-vs-sync/) -- understand when to use each API style

View File

@ -3,4 +3,149 @@ title: Installation
description: Install openocd-python and its dependencies
---
Content coming soon.
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
openocd-python requires **Python 3.11 or later** and a working OpenOCD installation. The library itself has a single dependency: `cmsis-svd` for SVD register decoding.
## Install the package
<Tabs>
<TabItem label="uv">
```bash
uv add openocd-python
```
</TabItem>
<TabItem label="pip">
```bash
pip install openocd-python
```
</TabItem>
<TabItem label="pipx (CLI only)">
```bash
pipx install openocd-python
```
</TabItem>
</Tabs>
The package installs both the Python library (`import openocd`) and a CLI tool (`openocd-python`).
## Dependencies
| Package | Version | Purpose |
|---------|---------|---------|
| `cmsis-svd` | >= 0.4 | SVD file parsing for peripheral register decoding |
No other Python dependencies are required. The library uses only the standard library for networking (`asyncio`), subprocess management, and data types.
## Install OpenOCD
openocd-python communicates with OpenOCD over its TCL RPC interface. You need OpenOCD installed and either already running or available on your `PATH` so the library can spawn it.
<Tabs>
<TabItem label="Arch Linux">
```bash
sudo pacman -S openocd
```
</TabItem>
<TabItem label="Debian / Ubuntu">
```bash
sudo apt install openocd
```
</TabItem>
<TabItem label="macOS">
```bash
brew install openocd
```
</TabItem>
<TabItem label="Windows">
Download the latest release from [openocd.org](https://openocd.org/pages/getting-openocd.html) and add the `bin` directory to your system `PATH`.
</TabItem>
<TabItem label="From source">
```bash
git clone https://github.com/openocd-org/openocd.git
cd openocd
./bootstrap
./configure
make
sudo make install
```
</TabItem>
</Tabs>
Verify OpenOCD is installed and accessible:
```bash
openocd --version
```
<Aside type="note">
openocd-python supports OpenOCD 0.11 and later. Some commands (like `adapter name`) require OpenOCD 0.12+. The library handles version differences automatically, falling back to older command variants when needed.
</Aside>
## Verify the installation
After installing both openocd-python and OpenOCD, run a quick check to confirm everything is working.
First, start OpenOCD with your target configuration. For example, with a CMSIS-DAP probe and an STM32F1 target:
```bash
openocd -f interface/cmsis-dap.cfg -f target/stm32f1x.cfg
```
Then, in another terminal, verify the Python package can connect:
```python
from openocd import Session
with Session.connect_sync() as ocd:
state = ocd.target.state()
print(f"Target: {state.name}, State: {state.state}")
```
You can also verify using the CLI:
```bash
openocd-python info
```
This prints the target name, state, transport, adapter, and clock speed.
<Aside type="tip">
If you get a `ConnectionError`, check that OpenOCD is running and that the TCL RPC port (default 6666) is not blocked by a firewall. The TCL RPC port is distinct from the GDB server port (3333) and the telnet port (4444).
</Aside>
## Development installation
To work on openocd-python itself, clone the repository and install the development dependencies:
```bash
git clone https://git.supported.systems/ryan/openocd-python.git
cd openocd-python
uv sync --extra dev
```
The `dev` extras include:
| Package | Purpose |
|---------|---------|
| `pytest` >= 8.0 | Test runner |
| `pytest-asyncio` >= 0.24 | Async test support |
| `ruff` >= 0.8 | Linter and formatter |
Run the test suite (no hardware needed -- all tests use a mock OpenOCD server):
```bash
uv run pytest
```
Run the linter:
```bash
uv run ruff check src/ tests/
```
## Next steps
- [First Connection](/getting-started/first-connection/) -- connect to OpenOCD and run your first command
- [Quick Start](/getting-started/quick-start/) -- complete working examples for common tasks
- [CLI Reference](/getting-started/cli/) -- use the `openocd-python` command-line tool

View File

@ -3,4 +3,424 @@ title: Quick Start
description: Get up and running with openocd-python in minutes
---
Content coming soon.
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
This page contains complete, runnable examples for the most common tasks. Each example assumes OpenOCD is already running on `localhost:6666`. Both async and sync versions are provided.
<Aside type="note">
All examples import from `openocd`, which is the package name. The installable package is `openocd-python`, but the import is just `openocd`.
</Aside>
## Connect and read target state
The simplest possible script: connect, read the target state, and print it.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
async with Session.connect() as ocd:
state = await ocd.target.state()
print(f"Target: {state.name}")
print(f"State: {state.state}")
if state.current_pc is not None:
print(f"PC: 0x{state.current_pc:08X}")
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.connect_sync() as ocd:
state = ocd.target.state()
print(f"Target: {state.name}")
print(f"State: {state.state}")
if state.current_pc is not None:
print(f"PC: 0x{state.current_pc:08X}")
```
</TabItem>
</Tabs>
Output when the target is halted:
```
Target: stm32f1x.cpu
State: halted
PC: 0x08001234
```
## Halt, step, and resume
Control the target execution state.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
async with Session.connect() as ocd:
# Halt the target
state = await ocd.target.halt()
print(f"Halted at PC=0x{state.current_pc:08X}")
# Single-step one instruction
state = await ocd.target.step()
print(f"Stepped to PC=0x{state.current_pc:08X}")
# Resume execution
await ocd.target.resume()
print("Target resumed")
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.connect_sync() as ocd:
state = ocd.target.halt()
print(f"Halted at PC=0x{state.current_pc:08X}")
state = ocd.target.step()
print(f"Stepped to PC=0x{state.current_pc:08X}")
ocd.target.resume()
print("Target resumed")
```
</TabItem>
</Tabs>
## Read memory
Read memory at various widths and display a hexdump.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
async with Session.connect() as ocd:
# Read 4 words (32-bit) from the vector table
words = await ocd.memory.read_u32(0x08000000, count=4)
for i, w in enumerate(words):
print(f" [0x{0x08000000 + i*4:08X}] = 0x{w:08X}")
# Read raw bytes
data = await ocd.memory.read_bytes(0x08000000, 32)
print(f"\nFirst 32 bytes: {data.hex()}")
# Pretty hexdump
dump = await ocd.memory.hexdump(0x08000000, 64)
print(f"\n{dump}")
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.connect_sync() as ocd:
words = ocd.memory.read_u32(0x08000000, count=4)
for i, w in enumerate(words):
print(f" [0x{0x08000000 + i*4:08X}] = 0x{w:08X}")
data = ocd.memory.read_bytes(0x08000000, 32)
print(f"\nFirst 32 bytes: {data.hex()}")
dump = ocd.memory.hexdump(0x08000000, 64)
print(f"\n{dump}")
```
</TabItem>
</Tabs>
## Read and write registers
Access CPU registers by name, with convenience methods for common ARM Cortex-M registers.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
async with Session.connect() as ocd:
await ocd.target.halt()
# Read individual registers
pc = await ocd.registers.pc()
sp = await ocd.registers.sp()
lr = await ocd.registers.lr()
print(f"PC=0x{pc:08X} SP=0x{sp:08X} LR=0x{lr:08X}")
# Read several at once
values = await ocd.registers.read_many(["r0", "r1", "r2", "r3"])
for name, val in values.items():
print(f" {name} = 0x{val:08X}")
# Read all registers
all_regs = await ocd.registers.read_all()
print(f"\n{len(all_regs)} registers available")
# Write a register
await ocd.registers.write("r0", 0x42)
await ocd.target.resume()
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.connect_sync() as ocd:
ocd.target.halt()
pc = ocd.registers.pc()
sp = ocd.registers.sp()
lr = ocd.registers.lr()
print(f"PC=0x{pc:08X} SP=0x{sp:08X} LR=0x{lr:08X}")
values = ocd.registers.read_many(["r0", "r1", "r2", "r3"])
for name, val in values.items():
print(f" {name} = 0x{val:08X}")
all_regs = ocd.registers.read_all()
print(f"\n{len(all_regs)} registers available")
ocd.registers.write("r0", 0x42)
ocd.target.resume()
```
</TabItem>
</Tabs>
## Program flash
Write a firmware image to flash memory with automatic erase and verification.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from pathlib import Path
from openocd import Session
async def main():
async with Session.connect() as ocd:
firmware = Path("build/firmware.bin")
# Program flash (erases affected sectors, writes, then verifies)
await ocd.flash.write_image(firmware, erase=True, verify=True)
print("Flash programming complete")
# Reset and run the new firmware
await ocd.target.reset(mode="run")
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from pathlib import Path
from openocd import Session
with Session.connect_sync() as ocd:
firmware = Path("build/firmware.bin")
ocd.flash.write_image(firmware, erase=True, verify=True)
print("Flash programming complete")
ocd.target.reset(mode="run")
```
</TabItem>
</Tabs>
<Aside type="tip">
`write_image` supports `.bin`, `.hex`, and `.elf` file formats. OpenOCD determines the format automatically from the file contents.
</Aside>
## SVD register decoding
Load an SVD file to decode peripheral registers into named bitfields. This is especially useful for reading GPIO, timer, and peripheral configuration registers.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from pathlib import Path
from openocd import Session
async def main():
async with Session.connect() as ocd:
await ocd.target.halt()
# Load the SVD file for your chip
await ocd.svd.load(Path("STM32F103xx.svd"))
# List available peripherals
peripherals = ocd.svd.list_peripherals()
print(f"Peripherals: {', '.join(peripherals[:5])}...")
# Read and decode a specific register
odr = await ocd.svd.read_register("GPIOA", "ODR")
print(odr)
# GPIOA.ODR @ 0x4001080C = 0x00000010
# [ 0] ODR0 = 0x0
# [ 1] ODR1 = 0x0
# ...
# [ 4] ODR4 = 0x1
# ...
# Decode a value without reading hardware
decoded = ocd.svd.decode("GPIOA", "CRL", 0x44444444)
print(decoded)
await ocd.target.resume()
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from pathlib import Path
from openocd import Session
with Session.connect_sync() as ocd:
ocd.target.halt()
ocd.svd.load(Path("STM32F103xx.svd"))
peripherals = ocd.svd.list_peripherals()
print(f"Peripherals: {', '.join(peripherals[:5])}...")
odr = ocd.svd.read_register("GPIOA", "ODR")
print(odr)
decoded = ocd.svd.decode("GPIOA", "CRL", 0x44444444)
print(decoded)
ocd.target.resume()
```
</TabItem>
</Tabs>
## JTAG chain scan
Discover all TAPs (Test Access Ports) on the JTAG chain.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
async with Session.connect() as ocd:
taps = await ocd.jtag.scan_chain()
for tap in taps:
print(
f" {tap.name:<25s} IDCODE=0x{tap.idcode:08X} "
f"IR={tap.ir_length} enabled={tap.enabled}"
)
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.connect_sync() as ocd:
taps = ocd.jtag.scan_chain()
for tap in taps:
print(
f" {tap.name:<25s} IDCODE=0x{tap.idcode:08X} "
f"IR={tap.ir_length} enabled={tap.enabled}"
)
```
</TabItem>
</Tabs>
## Error handling
Catch specific exceptions for different failure modes.
```python
from openocd import (
Session,
ConnectionError,
TargetError,
TargetNotHaltedError,
TimeoutError,
)
with Session.connect_sync() as ocd:
try:
ocd.target.halt()
pc = ocd.registers.pc()
print(f"PC = 0x{pc:08X}")
except TargetNotHaltedError:
print("Target must be halted to read registers")
except TimeoutError:
print("Operation timed out")
except TargetError as e:
print(f"Target error: {e}")
```
See the [Error Handling guide](/guides/error-handling/) for the full exception hierarchy.
## Spawning OpenOCD from Python
Instead of starting OpenOCD manually, let the library manage it:
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
config = "-f interface/cmsis-dap.cfg -f target/stm32f1x.cfg"
async with Session.start(config, timeout=15.0) as ocd:
state = await ocd.target.state()
print(f"Target: {state.name}, State: {state.state}")
# OpenOCD process stops when the context manager exits
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
config = "-f interface/cmsis-dap.cfg -f target/stm32f1x.cfg"
with Session.start_sync(config, timeout=15.0) as ocd:
state = ocd.target.state()
print(f"Target: {state.name}, State: {state.state}")
```
</TabItem>
</Tabs>
## Next steps
- [Session Lifecycle](/guides/session-lifecycle/) -- connection management in depth
- [Target Control](/guides/target-control/) -- halt, resume, step, and reset
- [Memory Operations](/guides/memory-operations/) -- typed reads and writes at any width
- [Register Access](/guides/register-access/) -- CPU register manipulation
- [Error Handling](/guides/error-handling/) -- exception hierarchy and recovery

View File

@ -3,4 +3,236 @@ title: Async vs Sync
description: Choosing between async and synchronous APIs
---
Content coming soon.
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
openocd-python is **async-first**: every subsystem is implemented as an async class using `asyncio`. For callers who do not need or want async, a complete set of synchronous wrappers is provided. The two APIs have identical functionality -- the sync wrappers simply call `run_until_complete()` on the underlying async methods.
## The two API surfaces
Every subsystem exists as a pair:
| Async class | Sync wrapper |
|-------------|-------------|
| `Session` | `SyncSession` |
| `Target` | `SyncTarget` |
| `Memory` | `SyncMemory` |
| `Registers` | `SyncRegisters` |
| `Flash` | `SyncFlash` |
| `JTAGController` | `SyncJTAGController` |
| `BreakpointManager` | `SyncBreakpointManager` |
| `SVDManager` | `SyncSVDManager` |
The async classes are the primary implementation. The `Sync*` wrappers delegate every method call through an event loop using `loop.run_until_complete()`.
## Async usage
Use the async API when:
- You are inside an `async def` function already
- You are building on top of an async framework (FastAPI, aiohttp, etc.)
- You need to run multiple OpenOCD operations concurrently
- You are integrating with other async I/O (serial ports, network services, etc.)
```python
import asyncio
from openocd import Session
async def main():
async with Session.connect() as ocd:
# All subsystem methods use await
state = await ocd.target.state()
print(f"State: {state.state}")
if state.state == "halted":
pc = await ocd.registers.pc()
dump = await ocd.memory.hexdump(pc, 32)
print(dump)
await ocd.target.resume()
asyncio.run(main())
```
### Async with FastAPI
```python
from fastapi import FastAPI
from openocd import Session
app = FastAPI()
@app.get("/target/state")
async def get_target_state():
async with Session.connect() as ocd:
state = await ocd.target.state()
return {
"name": state.name,
"state": state.state,
"pc": state.current_pc,
}
```
## Sync usage
Use the sync API when:
- You are writing a simple script
- You are working in a REPL or Jupyter notebook
- Your codebase is synchronous
- You do not need concurrent I/O
```python
from openocd import Session
with Session.connect_sync() as ocd:
# No await needed -- methods block until complete
state = ocd.target.state()
print(f"State: {state.state}")
if state.state == "halted":
pc = ocd.registers.pc()
dump = ocd.memory.hexdump(pc, 32)
print(dump)
ocd.target.resume()
```
The sync entry points are `Session.connect_sync()` and `Session.start_sync()`. They return a `SyncSession` instead of a `Session`.
## How the sync wrapper works
When you call `Session.connect_sync()`, three things happen:
1. **Event loop creation**: `_get_or_create_loop()` gets or creates an `asyncio` event loop. If there is no running loop, it uses the existing one (or creates a new one). If there *is* already a running loop, it raises a `RuntimeError`.
2. **Async method execution**: The async `Session.connect()` is run via `loop.run_until_complete()`.
3. **SyncSession wrapping**: The resulting `Session` is wrapped in a `SyncSession` that stores both the session and the loop.
Every method on `SyncSession` (and the `Sync*` subsystem wrappers) follows the same pattern:
```python
# Inside SyncTarget
def halt(self) -> TargetState:
return self._loop.run_until_complete(self._target.halt())
```
This is straightforward delegation -- no additional logic, no caching, no threading.
## The async context guard
<Aside type="caution">
Never call the sync API from inside an already-running async context. The `_get_or_create_loop()` function detects this and raises a `RuntimeError` to prevent deadlocks.
</Aside>
The guard works by checking for an active event loop:
```python
def _get_or_create_loop() -> asyncio.AbstractEventLoop:
try:
asyncio.get_running_loop()
except RuntimeError:
pass # No running loop -- safe to proceed
else:
raise RuntimeError(
"Cannot use sync API from an async context. "
"Use the async Session.start()/connect() instead."
)
# ... create or get event loop
```
This means the following will fail:
```python
import asyncio
from openocd import Session
async def bad_idea():
# This raises RuntimeError!
with Session.connect_sync() as ocd:
ocd.target.state()
asyncio.run(bad_idea())
```
The fix is to use the async API inside async contexts:
```python
import asyncio
from openocd import Session
async def correct():
async with Session.connect() as ocd:
await ocd.target.state()
asyncio.run(correct())
```
## When to use which
| Scenario | API | Why |
|----------|-----|-----|
| Simple automation script | Sync | Less boilerplate, no `asyncio.run()` needed |
| Jupyter notebook | Sync | Notebooks have their own event loop complications |
| pytest test (sync) | Sync | Straightforward test functions |
| pytest test (async) | Async | With `pytest-asyncio` and `asyncio_mode = "auto"` |
| FastAPI / aiohttp endpoint | Async | Already in an async context |
| MCP server tool | Async | FastMCP tools are async |
| Concurrent multi-target | Async | `asyncio.gather()` across multiple sessions |
| CI/CD flash script | Sync | Simple, linear flow |
## Mixing async and sync
You cannot mix the two styles within a single session. A `Session` is always used with `await`, and a `SyncSession` is always used without. However, you can have separate sessions of different types in the same program, as long as the sync calls happen outside any running event loop:
```python
import asyncio
from openocd import Session
# Sync session for quick setup
with Session.connect_sync() as ocd:
ocd.target.halt()
ocd.target.reset(mode="halt")
# Async session for complex operations
async def do_work():
async with Session.connect() as ocd:
state = await ocd.target.state()
# ... more async work
asyncio.run(do_work())
```
## Type differences
The async and sync APIs return identical data types. `TargetState`, `Register`, `FlashBank`, `TAPInfo`, and all other dataclasses are shared between both APIs. The only difference is at the session and subsystem level:
```python
# Async
ocd: Session
ocd.target # -> Target
ocd.memory # -> Memory
ocd.registers # -> Registers
# Sync
ocd: SyncSession
ocd.target # -> SyncTarget
ocd.memory # -> SyncMemory
ocd.registers # -> SyncRegisters
```
The return types of methods are identical:
```python
# Both return TargetState
state = await ocd.target.state() # async
state = ocd.target.state() # sync
# Both return list[int]
words = await ocd.memory.read_u32(0x08000000, count=4) # async
words = ocd.memory.read_u32(0x08000000, count=4) # sync
```
## Next steps
- [Session Lifecycle](/guides/session-lifecycle/) -- how sessions are created and torn down
- [Error Handling](/guides/error-handling/) -- exception handling works the same in both APIs
- [Target Control](/guides/target-control/) -- examples showing both async and sync patterns

View File

@ -3,4 +3,323 @@ title: Error Handling
description: Exception hierarchy and error recovery patterns
---
Content coming soon.
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
openocd-python uses a structured exception hierarchy rooted at `OpenOCDError`. Every exception the library raises is a subclass of this base, so you can catch broadly or narrowly depending on your needs.
## Exception hierarchy
```
OpenOCDError
├── ConnectionError # TCP connection failures
├── TimeoutError # Deadline exceeded
├── TargetError # Target not responding or returned an error
│ └── TargetNotHaltedError # Operation requires halted target
├── FlashError # Flash operation failed
├── JTAGError # JTAG communication error
├── SVDError # SVD file or parsing error
├── ProcessError # OpenOCD subprocess failed
└── BreakpointError # Breakpoint/watchpoint operation failed
```
All exceptions are importable from the top-level `openocd` package:
```python
from openocd import (
OpenOCDError,
ConnectionError,
TimeoutError,
TargetError,
TargetNotHaltedError,
FlashError,
JTAGError,
SVDError,
ProcessError,
)
```
<Aside type="note">
`BreakpointError` is defined in `openocd.breakpoints` rather than `openocd.errors`, but it still inherits from `OpenOCDError`. Import it as `from openocd.breakpoints import BreakpointError`.
</Aside>
## Exception details
### OpenOCDError
The base class for all library exceptions. Catching this handles any error that openocd-python can raise:
```python
from openocd import OpenOCDError, Session
with Session.connect_sync() as ocd:
try:
state = ocd.target.state()
except OpenOCDError as e:
print(f"Something went wrong: {e}")
```
### ConnectionError
Raised when a TCP connection to OpenOCD cannot be established. Common causes:
- OpenOCD is not running
- Wrong host or port
- Firewall blocking the connection
- Network unreachable
```python
from openocd import ConnectionError, Session
try:
with Session.connect_sync(host="192.168.1.99", port=6666) as ocd:
ocd.target.state()
except ConnectionError as e:
print(f"Cannot reach OpenOCD: {e}")
```
Also raised if a command is sent after the connection has been closed, or if OpenOCD closes the connection unexpectedly during a read.
### TimeoutError
Raised when an operation exceeds its deadline. This can happen during:
- Initial connection (`Session.connect()` with `timeout` parameter)
- Waiting for OpenOCD to start (`Session.start()` with `timeout` parameter)
- Individual command responses (the `TclRpcConnection` timeout)
- `target.wait_halt()` when the target does not halt in time
```python
from openocd import TimeoutError, Session
with Session.connect_sync() as ocd:
try:
# Wait up to 2 seconds for the target to halt
ocd.target.wait_halt(timeout_ms=2000)
except TimeoutError:
print("Target did not halt within 2 seconds")
```
### TargetError
Raised when a target command fails. The error message contains the raw OpenOCD response for diagnosis. This covers halt, resume, step, reset, memory read/write, and register access failures.
```python
from openocd import TargetError, Session
with Session.connect_sync() as ocd:
try:
ocd.target.halt()
except TargetError as e:
print(f"Target command failed: {e}")
```
### TargetNotHaltedError
A subclass of `TargetError`, raised specifically when an operation requires a halted target but the target is running. This is most commonly encountered when reading or writing registers.
```python
from openocd import TargetNotHaltedError, Session
with Session.connect_sync() as ocd:
try:
pc = ocd.registers.pc()
except TargetNotHaltedError:
print("Halt the target first before reading registers")
ocd.target.halt()
pc = ocd.registers.pc()
```
Because `TargetNotHaltedError` is a subclass of `TargetError`, catching `TargetError` will also catch it:
```python
try:
pc = ocd.registers.pc()
except TargetNotHaltedError:
# Handle the specific case
ocd.target.halt()
pc = ocd.registers.pc()
except TargetError:
# Handle other target errors
print("Unexpected target error")
```
<Aside type="caution">
Order matters when catching exceptions. Always put the more specific exception (`TargetNotHaltedError`) before the more general one (`TargetError`), or the specific handler will never run.
</Aside>
### FlashError
Raised when a flash operation fails -- programming, erasing, verifying, or reading flash memory. The error message includes the raw OpenOCD response.
```python
from openocd import FlashError, Session
from pathlib import Path
with Session.connect_sync() as ocd:
try:
ocd.flash.write_image(Path("firmware.bin"))
except FlashError as e:
print(f"Flash programming failed: {e}")
```
Also raised for:
- Invalid sector ranges (e.g., `first > last` in `erase_sector`)
- Verification mismatches after `write_image` with `verify=True`
- Unparseable flash info output
### JTAGError
Raised when JTAG communication fails. This covers chain scan errors, TAP state transitions, and raw scan operations.
```python
from openocd import JTAGError, Session
with Session.connect_sync() as ocd:
try:
taps = ocd.jtag.scan_chain()
except JTAGError as e:
print(f"JTAG error: {e}")
```
### SVDError
Raised when SVD-related operations fail:
- SVD file not found or cannot be parsed
- Peripheral name not found in the loaded SVD
- Register name not found within a peripheral
- No SVD file loaded when trying to list or decode
```python
from openocd import SVDError, Session
from pathlib import Path
with Session.connect_sync() as ocd:
try:
ocd.svd.load(Path("nonexistent.svd"))
except SVDError as e:
print(f"SVD error: {e}")
```
### ProcessError
Raised when the OpenOCD subprocess fails to start or exits unexpectedly. This only applies when using `Session.start()`.
```python
from openocd import ProcessError, Session
try:
with Session.start_sync("nonexistent_config.cfg") as ocd:
pass
except ProcessError as e:
print(f"OpenOCD failed to start: {e}")
```
Common causes:
- OpenOCD binary not found on `PATH`
- Invalid configuration file
- OpenOCD exits before the TCL RPC port is ready
- Permission errors
### BreakpointError
Raised when a breakpoint or watchpoint operation fails. Defined in `openocd.breakpoints` but inherits from `OpenOCDError`.
```python
from openocd.breakpoints import BreakpointError
from openocd import Session
with Session.connect_sync() as ocd:
try:
ocd.breakpoints.add(0x08001234, length=2, hw=True)
except BreakpointError as e:
print(f"Breakpoint error: {e}")
```
## Catching patterns
### Broad catch -- handle any library error
```python
from openocd import OpenOCDError, Session
with Session.connect_sync() as ocd:
try:
ocd.target.halt()
pc = ocd.registers.pc()
data = ocd.memory.read_u32(pc, count=4)
except OpenOCDError as e:
print(f"Operation failed: {e}")
```
### Narrow catch -- handle specific failure modes
```python
from openocd import (
Session,
ConnectionError,
TargetError,
TargetNotHaltedError,
TimeoutError,
)
try:
with Session.connect_sync() as ocd:
ocd.target.halt()
pc = ocd.registers.pc()
ocd.target.resume()
except ConnectionError:
print("Could not connect to OpenOCD")
except TargetNotHaltedError:
print("Target is not halted")
except TimeoutError:
print("Operation timed out")
except TargetError as e:
print(f"Target error: {e}")
```
### Retry pattern
```python
from openocd import Session, TimeoutError
with Session.connect_sync() as ocd:
for attempt in range(3):
try:
ocd.target.halt()
ocd.target.wait_halt(timeout_ms=1000)
break
except TimeoutError:
if attempt == 2:
raise
print(f"Attempt {attempt + 1} timed out, retrying...")
ocd.target.reset(mode="halt")
```
### Recovery from TargetNotHaltedError
A common pattern is to attempt a register read and automatically halt if needed:
```python
from openocd import Session, TargetNotHaltedError
def safe_read_pc(ocd) -> int:
try:
return ocd.registers.pc()
except TargetNotHaltedError:
ocd.target.halt()
return ocd.registers.pc()
with Session.connect_sync() as ocd:
pc = safe_read_pc(ocd)
print(f"PC = 0x{pc:08X}")
```
## Error responses from OpenOCD
Internally, most subsystems detect errors by checking for the word "error" in the OpenOCD response string. This is because OpenOCD's TCL RPC protocol does not use structured error codes -- all errors are communicated as plain text in the response body.
The library wraps these text responses in the appropriate exception type so you do not need to parse them yourself. The original OpenOCD message is preserved in the exception's string representation.
## Next steps
- [Target Control](/guides/target-control/) -- which methods raise which exceptions
- [Memory Operations](/guides/memory-operations/) -- error handling for memory reads and writes
- [Session Lifecycle](/guides/session-lifecycle/) -- connection and process errors

View File

@ -3,4 +3,308 @@ title: Memory Operations
description: Read and write target memory at various widths
---
Content coming soon.
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
The `Memory` subsystem provides typed reads and writes at 8, 16, 32, and 64-bit widths, plus raw byte access, hexdump formatting, memory search, and file dump utilities. Access it through `session.memory`.
All memory operations use OpenOCD's `read_memory` and `write_memory` TCL commands, which provide reliable structured I/O.
## Typed reads
Read one or more values at a specific width. All read methods return a `list[int]`.
### read_u8 / read_u16 / read_u32 / read_u64
```python
async def read_u8(addr: int, count: int = 1) -> list[int]
async def read_u16(addr: int, count: int = 1) -> list[int]
async def read_u32(addr: int, count: int = 1) -> list[int]
async def read_u64(addr: int, count: int = 1) -> list[int]
```
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
# Read a single 32-bit word
values = await ocd.memory.read_u32(0x08000000)
stack_pointer = values[0]
print(f"Initial SP: 0x{stack_pointer:08X}")
# Read 8 consecutive 32-bit words (vector table)
vectors = await ocd.memory.read_u32(0x08000000, count=8)
for i, v in enumerate(vectors):
print(f" Vector[{i}] = 0x{v:08X}")
# Read 16-bit values (useful for Thumb disassembly)
instructions = await ocd.memory.read_u16(0x08001000, count=4)
# Read individual bytes
header = await ocd.memory.read_u8(0x20000000, count=16)
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
values = ocd.memory.read_u32(0x08000000)
stack_pointer = values[0]
print(f"Initial SP: 0x{stack_pointer:08X}")
vectors = ocd.memory.read_u32(0x08000000, count=8)
for i, v in enumerate(vectors):
print(f" Vector[{i}] = 0x{v:08X}")
instructions = ocd.memory.read_u16(0x08001000, count=4)
header = ocd.memory.read_u8(0x20000000, count=16)
```
</TabItem>
</Tabs>
<Aside type="note">
All read methods return a list even when `count=1`. This keeps the return type consistent and avoids the need to check whether you got a single value or a list.
</Aside>
### read_bytes
For bulk data, `read_bytes()` returns raw `bytes`:
```python
async def read_bytes(addr: int, size: int) -> bytes
```
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
data = await ocd.memory.read_bytes(0x08000000, 1024)
print(f"Read {len(data)} bytes")
print(f"First 4 bytes: {data[:4].hex()}")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
data = ocd.memory.read_bytes(0x08000000, 1024)
print(f"Read {len(data)} bytes")
print(f"First 4 bytes: {data[:4].hex()}")
```
</TabItem>
</Tabs>
Internally, `read_bytes` reads using 8-bit width and converts the result to a `bytes` object.
## Typed writes
Write one or more values at a specific width. Accepts either a single integer or a list of integers.
### write_u8 / write_u16 / write_u32
```python
async def write_u8(addr: int, values: int | list[int]) -> None
async def write_u16(addr: int, values: int | list[int]) -> None
async def write_u32(addr: int, values: int | list[int]) -> None
```
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
# Write a single 32-bit word
await ocd.memory.write_u32(0x20000000, 0xDEADBEEF)
# Write multiple 32-bit words
await ocd.memory.write_u32(0x20000000, [0x11111111, 0x22222222, 0x33333333])
# Write 16-bit values
await ocd.memory.write_u16(0x20001000, [0x1234, 0x5678])
# Write individual bytes
await ocd.memory.write_u8(0x20002000, [0x48, 0x65, 0x6C, 0x6C, 0x6F])
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
ocd.memory.write_u32(0x20000000, 0xDEADBEEF)
ocd.memory.write_u32(0x20000000, [0x11111111, 0x22222222, 0x33333333])
ocd.memory.write_u16(0x20001000, [0x1234, 0x5678])
ocd.memory.write_u8(0x20002000, [0x48, 0x65, 0x6C, 0x6C, 0x6F])
```
</TabItem>
</Tabs>
### write_bytes
For bulk data, `write_bytes()` accepts a `bytes` object:
```python
async def write_bytes(addr: int, data: bytes) -> None
```
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
payload = b"Hello, target!"
await ocd.memory.write_bytes(0x20000000, payload)
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
payload = b"Hello, target!"
ocd.memory.write_bytes(0x20000000, payload)
```
</TabItem>
</Tabs>
<Aside type="caution">
Memory writes go directly to RAM or peripheral registers. Writing to flash addresses requires the Flash subsystem -- direct memory writes to flash will fail on most targets. Writing to wrong addresses can crash the target or corrupt data.
</Aside>
## Hexdump
`hexdump()` reads memory and returns a formatted string with hex and ASCII columns, 16 bytes per line:
```python
async def hexdump(addr: int, size: int) -> str
```
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
dump = await ocd.memory.hexdump(0x08000000, 64)
print(dump)
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
dump = ocd.memory.hexdump(0x08000000, 64)
print(dump)
```
</TabItem>
</Tabs>
Output format:
```
08000000: 00 50 00 20 A1 01 00 08 AB 01 00 08 AD 01 00 08 |.P. ............|
08000010: AF 01 00 08 B1 01 00 08 B3 01 00 08 00 00 00 00 |................|
08000020: 00 00 00 00 00 00 00 00 00 00 00 00 B5 01 00 08 |................|
08000030: B7 01 00 08 00 00 00 00 B9 01 00 08 BB 01 00 08 |................|
```
Each line shows:
- **Address** (8 hex digits)
- **Hex bytes** in two groups of 8, separated by a gap
- **ASCII** representation (non-printable bytes displayed as `.`)
## Memory search
`search()` scans a memory range for a byte pattern and returns all matching addresses:
```python
async def search(pattern: bytes, start: int, end: int) -> list[int]
```
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
# Search for a magic number in flash
matches = await ocd.memory.search(
b"\xDE\xAD\xBE\xEF",
start=0x08000000,
end=0x08020000,
)
for addr in matches:
print(f" Found at 0x{addr:08X}")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
matches = ocd.memory.search(
b"\xDE\xAD\xBE\xEF",
start=0x08000000,
end=0x08020000,
)
for addr in matches:
print(f" Found at 0x{addr:08X}")
```
</TabItem>
</Tabs>
The search reads memory in 4096-byte chunks with an overlap of `len(pattern) - 1` bytes to handle patterns that span chunk boundaries. This is a client-side search since OpenOCD has no native memory search command.
<Aside type="tip">
For large memory regions, the search may take a while since every chunk requires a round-trip to OpenOCD. Narrow the search range as much as possible.
</Aside>
## File dump
`dump()` reads memory and writes the raw bytes to a file:
```python
async def dump(addr: int, size: int, path: Path) -> None
```
<Tabs>
<TabItem label="Async">
```python
from pathlib import Path
async with Session.connect() as ocd:
# Dump the first 128KB of flash to a file
await ocd.memory.dump(0x08000000, 128 * 1024, Path("flash_dump.bin"))
```
</TabItem>
<TabItem label="Sync">
```python
from pathlib import Path
with Session.connect_sync() as ocd:
ocd.memory.dump(0x08000000, 128 * 1024, Path("flash_dump.bin"))
```
</TabItem>
</Tabs>
## How it works
Under the hood, all memory operations use the OpenOCD TCL RPC `read_memory` and `write_memory` commands:
```
read_memory 0x08000000 32 4 -> "00500020 080001A1 080001AB 080001AD"
write_memory 0x20000000 32 {0xDEADBEEF}
```
The response from `read_memory` is a space-separated list of hex values. The library parses these into Python integers. If the response contains "error", a `TargetError` is raised.
## Method summary
| Method | Returns | Description |
|--------|---------|-------------|
| `read_u8(addr, count=1)` | `list[int]` | Read 8-bit values |
| `read_u16(addr, count=1)` | `list[int]` | Read 16-bit values |
| `read_u32(addr, count=1)` | `list[int]` | Read 32-bit values |
| `read_u64(addr, count=1)` | `list[int]` | Read 64-bit values |
| `read_bytes(addr, size)` | `bytes` | Read raw bytes |
| `write_u8(addr, values)` | `None` | Write 8-bit values |
| `write_u16(addr, values)` | `None` | Write 16-bit values |
| `write_u32(addr, values)` | `None` | Write 32-bit values |
| `write_bytes(addr, data)` | `None` | Write raw bytes |
| `search(pattern, start, end)` | `list[int]` | Search for byte pattern |
| `dump(addr, size, path)` | `None` | Dump memory to file |
| `hexdump(addr, size)` | `str` | Formatted hex+ASCII dump |
## Errors
All memory operations raise `TargetError` if the OpenOCD command fails (e.g., target not halted, invalid address, bus fault).
## Next steps
- [Register Access](/guides/register-access/) -- CPU register read/write
- [Target Control](/guides/target-control/) -- halt the target before memory operations
- [CLI Reference](/getting-started/cli/) -- the `read` command uses `hexdump()` internally

View File

@ -3,4 +3,324 @@ title: Register Access
description: Read and write CPU registers by name or number
---
Content coming soon.
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
The `Registers` subsystem reads and writes CPU registers by name using OpenOCD's `reg` command. It includes convenience accessors for common ARM Cortex-M registers. Access it through `session.registers`.
<Aside type="caution">
Register access requires the target to be halted. If the target is running, all register operations raise `TargetNotHaltedError`. Halt the target first with `session.target.halt()`.
</Aside>
## Reading a single register
`read()` takes a register name and returns its integer value:
```python
async def read(name: str) -> int
```
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
await ocd.target.halt()
pc = await ocd.registers.read("pc")
r0 = await ocd.registers.read("r0")
xpsr = await ocd.registers.read("xPSR")
print(f"PC = 0x{pc:08X}")
print(f"r0 = 0x{r0:08X}")
print(f"xPSR = 0x{xpsr:08X}")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
ocd.target.halt()
pc = ocd.registers.read("pc")
r0 = ocd.registers.read("r0")
xpsr = ocd.registers.read("xPSR")
print(f"PC = 0x{pc:08X}")
print(f"r0 = 0x{r0:08X}")
print(f"xPSR = 0x{xpsr:08X}")
```
</TabItem>
</Tabs>
Internally, `read("pc")` sends the command `reg pc` and parses the response:
```
pc (/32): 0x08001234
```
The regex pattern matches the register name, bit width, and hex value from this format.
## Writing a register
`write()` sets a register to a specific value:
```python
async def write(name: str, value: int) -> None
```
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
await ocd.target.halt()
# Set r0 to a test value
await ocd.registers.write("r0", 0x42)
# Move PC to a different address
await ocd.registers.write("pc", 0x08001000)
await ocd.target.resume()
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
ocd.target.halt()
ocd.registers.write("r0", 0x42)
ocd.registers.write("pc", 0x08001000)
ocd.target.resume()
```
</TabItem>
</Tabs>
The value is sent as a hex string: `reg r0 0x42`.
## Reading multiple registers
### read_many
`read_many()` reads several registers by name and returns a dictionary:
```python
async def read_many(names: list[str]) -> dict[str, int]
```
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
await ocd.target.halt()
values = await ocd.registers.read_many(["r0", "r1", "r2", "r3"])
for name, val in values.items():
print(f" {name} = 0x{val:08X}")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
ocd.target.halt()
values = ocd.registers.read_many(["r0", "r1", "r2", "r3"])
for name, val in values.items():
print(f" {name} = 0x{val:08X}")
```
</TabItem>
</Tabs>
This issues one `reg <name>` command per register sequentially. For reading all registers at once, use `read_all()` instead.
### read_all
`read_all()` reads every register in a single `reg` command and returns a dictionary of `Register` dataclasses:
```python
async def read_all() -> dict[str, Register]
```
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
await ocd.target.halt()
all_regs = await ocd.registers.read_all()
for name, reg in all_regs.items():
dirty = " (dirty)" if reg.dirty else ""
print(f" ({reg.number:>3d}) {reg.name:<12s} /{reg.size:<3d} = 0x{reg.value:08X}{dirty}")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
ocd.target.halt()
all_regs = ocd.registers.read_all()
for name, reg in all_regs.items():
dirty = " (dirty)" if reg.dirty else ""
print(f" ({reg.number:>3d}) {reg.name:<12s} /{reg.size:<3d} = 0x{reg.value:08X}{dirty}")
```
</TabItem>
</Tabs>
## The Register dataclass
`read_all()` returns `Register` objects, a frozen dataclass with five fields:
```python
@dataclass(frozen=True)
class Register:
name: str # Register name (e.g. "r0", "pc", "xPSR")
number: int # Register number in OpenOCD's numbering
value: int # Current value
size: int # Width in bits (e.g. 32)
dirty: bool # Whether the value has been modified since last commit
```
The `dirty` flag indicates that the register value has been written by the debugger but not yet committed to the target. This happens when you write a register and then inspect it before resuming.
OpenOCD's `reg` (list all) output looks like:
```
(0) r0 (/32): 0x00000000
(1) r1 (/32): 0x00000042
...
(16) xPSR (/32): 0x61000000 (dirty)
```
The library parses each line using a regex that extracts the register number, name, bit width, value, and optional dirty flag.
## ARM Cortex-M shortcuts
For the most commonly accessed ARM Cortex-M registers, convenience methods are provided:
| Method | Equivalent |
|--------|-----------|
| `pc()` | `read("pc")` |
| `sp()` | `read("sp")` |
| `lr()` | `read("lr")` |
| `xpsr()` | `read("xPSR")` |
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
await ocd.target.halt()
pc = await ocd.registers.pc()
sp = await ocd.registers.sp()
lr = await ocd.registers.lr()
xpsr = await ocd.registers.xpsr()
print(f"PC = 0x{pc:08X}")
print(f"SP = 0x{sp:08X}")
print(f"LR = 0x{lr:08X}")
print(f"xPSR = 0x{xpsr:08X}")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
ocd.target.halt()
pc = ocd.registers.pc()
sp = ocd.registers.sp()
lr = ocd.registers.lr()
xpsr = ocd.registers.xpsr()
print(f"PC = 0x{pc:08X}")
print(f"SP = 0x{sp:08X}")
print(f"LR = 0x{lr:08X}")
print(f"xPSR = 0x{xpsr:08X}")
```
</TabItem>
</Tabs>
These are thin wrappers that call `read()` with the appropriate register name. They exist for readability and to avoid typos in register name strings.
## Practical example: stack trace inspection
Read the stack pointer and inspect the stack contents alongside register values:
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def inspect_stack():
async with Session.connect() as ocd:
await ocd.target.halt()
sp = await ocd.registers.sp()
pc = await ocd.registers.pc()
lr = await ocd.registers.lr()
print(f"PC = 0x{pc:08X}")
print(f"LR = 0x{lr:08X}")
print(f"SP = 0x{sp:08X}")
# Read 16 words from the stack
print("\nStack contents:")
stack = await ocd.memory.read_u32(sp, count=16)
for i, val in enumerate(stack):
print(f" [SP+0x{i*4:02X}] = 0x{val:08X}")
await ocd.target.resume()
asyncio.run(inspect_stack())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.connect_sync() as ocd:
ocd.target.halt()
sp = ocd.registers.sp()
pc = ocd.registers.pc()
lr = ocd.registers.lr()
print(f"PC = 0x{pc:08X}")
print(f"LR = 0x{lr:08X}")
print(f"SP = 0x{sp:08X}")
print("\nStack contents:")
stack = ocd.memory.read_u32(sp, count=16)
for i, val in enumerate(stack):
print(f" [SP+0x{i*4:02X}] = 0x{val:08X}")
ocd.target.resume()
```
</TabItem>
</Tabs>
## Method summary
| Method | Returns | Description |
|--------|---------|-------------|
| `read(name)` | `int` | Read a single register by name |
| `write(name, value)` | `None` | Write a value to a register |
| `read_all()` | `dict[str, Register]` | Read all registers |
| `read_many(names)` | `dict[str, int]` | Read several registers by name |
| `pc()` | `int` | Read the program counter |
| `sp()` | `int` | Read the stack pointer |
| `lr()` | `int` | Read the link register |
| `xpsr()` | `int` | Read the xPSR status register |
## Errors
| Exception | When |
|-----------|------|
| `TargetNotHaltedError` | Target is running (register access requires halt) |
| `TargetError` | Register not found or command failed |
## Next steps
- [Target Control](/guides/target-control/) -- halt the target before register access
- [Memory Operations](/guides/memory-operations/) -- read memory at the address a register points to
- [Error Handling](/guides/error-handling/) -- handling TargetNotHaltedError

View File

@ -3,4 +3,281 @@ title: Session Lifecycle
description: Understanding session creation, connection, and teardown
---
Content coming soon.
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
`Session` is the single entry point to openocd-python. It manages the TCP connection to OpenOCD, optionally manages an OpenOCD subprocess, and provides lazy access to every subsystem (target, memory, registers, flash, JTAG, breakpoints, RTT, SVD, transport, and events).
## Creating a session
There are two factory methods, each with an async and a sync variant:
| Method | Returns | Purpose |
|--------|---------|---------|
| `Session.connect()` | `Session` | Connect to an already-running OpenOCD |
| `Session.start()` | `Session` | Spawn an OpenOCD process, then connect |
| `Session.connect_sync()` | `SyncSession` | Sync wrapper around `connect()` |
| `Session.start_sync()` | `SyncSession` | Sync wrapper around `start()` |
### connect() -- attach to a running instance
```python
@classmethod
async def connect(
cls,
host: str = "localhost",
port: int = 6666,
timeout: float = 10.0,
) -> Session
```
Creates a `TclRpcConnection`, opens a TCP socket to the given host and port, and returns a `Session`. The timeout applies to the initial TCP connection attempt.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
async with Session.connect(host="localhost", port=6666) as ocd:
print(await ocd.command("version"))
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.connect_sync(host="localhost", port=6666) as ocd:
print(ocd.command("version"))
```
</TabItem>
</Tabs>
### start() -- spawn and manage OpenOCD
```python
@classmethod
async def start(
cls,
config: str | Path,
*,
tcl_port: int = 6666,
openocd_bin: str | None = None,
timeout: float = 10.0,
extra_args: list[str] | None = None,
) -> Session
```
This method:
1. Creates an `OpenOCDProcess` instance
2. Spawns OpenOCD with the given config and port settings
3. Polls the TCL RPC port until it accepts connections (or the timeout expires)
4. Opens a `TclRpcConnection` to the now-ready port
5. Returns a `Session` that owns both the connection and the process
If the TCP connection fails after OpenOCD starts, the process is automatically stopped before the exception propagates.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
async with Session.start(
"-f interface/cmsis-dap.cfg -f target/stm32f1x.cfg",
tcl_port=6666,
timeout=15.0,
extra_args=["-d2"],
) as ocd:
state = await ocd.target.state()
print(state.name, state.state)
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.start_sync(
"-f interface/cmsis-dap.cfg -f target/stm32f1x.cfg",
tcl_port=6666,
timeout=15.0,
) as ocd:
state = ocd.target.state()
print(state.name, state.state)
```
</TabItem>
</Tabs>
## Context manager cleanup
`Session` implements `__aenter__` / `__aexit__` (and `SyncSession` implements `__enter__` / `__exit__`). When the context manager exits, `close()` is called, which:
1. Closes the TCP connection to OpenOCD
2. If this session spawned an OpenOCD process, terminates it (sends SIGTERM, waits up to 5 seconds, then sends SIGKILL if needed)
```python
async with Session.start(config) as ocd:
await ocd.target.halt()
# At this point:
# - TCP connection is closed
# - OpenOCD process has been terminated
```
For manual lifecycle management without a context manager:
```python
ocd = await Session.connect()
try:
await ocd.target.state()
finally:
await ocd.close()
```
## Lazy subsystem initialization
Session exposes ten subsystem properties. Each is created on first access -- not at connection time. This means connecting to OpenOCD is fast and you only pay the cost of subsystems you actually use.
| Property | Type | Purpose |
|----------|------|---------|
| `target` | `Target` | Halt, resume, step, reset, state queries |
| `memory` | `Memory` | Read/write memory at various widths |
| `registers` | `Registers` | CPU register read/write |
| `flash` | `Flash` | Flash programming, erase, verify |
| `jtag` | `JTAGController` | JTAG chain scanning, TAP state control |
| `breakpoints` | `BreakpointManager` | Breakpoint and watchpoint management |
| `rtt` | `RTTManager` | Real-Time Transfer communication |
| `svd` | `SVDManager` | SVD-based peripheral register decoding |
| `transport` | `Transport` | Transport selection and adapter configuration |
Each subsystem holds a reference to the shared `TclRpcConnection`. The `SVDManager` is special -- it also receives a reference to the `Memory` subsystem so it can read hardware registers.
```python
async with Session.connect() as ocd:
# No subsystems created yet
state = await ocd.target.state()
# Target subsystem now exists
pc = await ocd.registers.pc()
# Registers subsystem now exists
# Memory, flash, jtag, etc. are still None internally
```
The `SyncSession` wrapper mirrors this pattern. Each sync property creates the corresponding `Sync*` wrapper on first access:
```python
with Session.connect_sync() as ocd:
# ocd is a SyncSession
state = ocd.target.state() # Creates SyncTarget wrapping Target
pc = ocd.registers.pc() # Creates SyncRegisters wrapping Registers
```
## Raw command escape hatch
Both `Session` and `SyncSession` expose a `command()` method for sending arbitrary OpenOCD TCL commands:
```python
async def command(self, cmd: str) -> str
```
This sends the command string over the TCL RPC connection and returns the raw response. Use it when you need functionality not covered by the typed subsystems.
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
# Get OpenOCD version
version = await ocd.command("version")
# Set adapter speed directly
await ocd.command("adapter speed 8000")
# Run arbitrary TCL
await ocd.command("set x [expr {1 + 2}]")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
version = ocd.command("version")
ocd.command("adapter speed 8000")
```
</TabItem>
</Tabs>
## OpenOCDProcess internals
When using `Session.start()`, the library creates an `OpenOCDProcess` that manages the subprocess.
### Process properties
| Property | Type | Description |
|----------|------|-------------|
| `pid` | `int \| None` | Process ID of the OpenOCD subprocess |
| `running` | `bool` | Whether the process is still alive |
| `tcl_port` | `int` | The TCL RPC port this process was started on |
### Binary discovery
If `openocd_bin` is not provided, the library uses `shutil.which("openocd")` to find OpenOCD on the system `PATH`. If not found, a `ProcessError` is raised before any process is spawned.
### Config string parsing
The `config` parameter accepts several formats. The process builder parses the string and wraps bare filenames with `-f` flags:
```python
# These are equivalent:
await Session.start("interface/cmsis-dap.cfg")
await Session.start("-f interface/cmsis-dap.cfg")
# Multiple configs:
await Session.start("-f interface/cmsis-dap.cfg -f target/stm32f1x.cfg")
# Inline commands:
await Session.start("-f interface/cmsis-dap.cfg -c 'adapter speed 4000'")
```
The TCL port flag (`-c "tcl_port 6666"`) is always appended automatically based on the `tcl_port` parameter.
### Readiness polling
After spawning the process, `wait_ready()` polls the TCL RPC port at 250ms intervals until a TCP connection succeeds or the timeout expires. If the process exits before becoming ready, a `ProcessError` is raised with the last 500 bytes of stderr output.
### Shutdown sequence
`stop()` terminates the process gracefully:
1. Send `SIGTERM`
2. Wait up to 5 seconds for the process to exit
3. If still alive, send `SIGKILL` and wait
## Event callbacks
`Session` provides shortcut methods for registering callbacks on common target events:
```python
async with Session.connect() as ocd:
ocd.on_halt(lambda msg: print(f"Target halted: {msg}"))
ocd.on_reset(lambda msg: print(f"Target reset: {msg}"))
# Events are delivered on the notification socket
# Enable notifications to start receiving them
await ocd._conn.enable_notifications()
```
These callbacks filter the raw notification stream by keyword ("halted" or "reset" respectively). The notifications arrive on a separate TCP connection to prevent them from interleaving with command responses.
<Aside type="note">
The dual-socket design means notification delivery is independent of command traffic. The primary socket handles request/response pairs exclusively, while the notification socket runs an async background task that dispatches messages to registered callbacks.
</Aside>
## Next steps
- [Async vs Sync](/guides/async-vs-sync/) -- when to use each API style
- [Error Handling](/guides/error-handling/) -- what can go wrong and how to catch it
- [Target Control](/guides/target-control/) -- the Target subsystem in detail

View File

@ -3,4 +3,296 @@ title: Target Control
description: Halt, resume, reset, and step through targets
---
Content coming soon.
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
The `Target` subsystem controls the execution state of the debug target -- halting, resuming, single-stepping, resetting, and querying the current state. Access it through `session.target`.
## TargetState dataclass
Most target operations return a `TargetState`, a frozen dataclass with three fields:
```python
@dataclass(frozen=True)
class TargetState:
name: str
state: Literal["running", "halted", "reset", "debug-running", "unknown"]
current_pc: int | None = None
```
| Field | Description |
|-------|-------------|
| `name` | Target name as reported by OpenOCD (e.g. `"stm32f1x.cpu"`) |
| `state` | Current execution state |
| `current_pc` | Program counter value when halted, `None` otherwise |
The `current_pc` is only populated when `state` is `"halted"`. If the target is halted but the PC cannot be read for any reason, `current_pc` will be `None` and a debug log message is emitted.
## Querying target state
Call `state()` to get the current execution state without changing it:
<Tabs>
<TabItem label="Async">
```python
async with Session.connect() as ocd:
state = await ocd.target.state()
print(f"Target: {state.name}")
print(f"State: {state.state}")
if state.state == "halted" and state.current_pc is not None:
print(f"PC: 0x{state.current_pc:08X}")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as ocd:
state = ocd.target.state()
print(f"Target: {state.name}")
print(f"State: {state.state}")
if state.state == "halted" and state.current_pc is not None:
print(f"PC: 0x{state.current_pc:08X}")
```
</TabItem>
</Tabs>
Internally, `state()` sends the `targets` command to OpenOCD and parses the tabular output using a regex. The output looks like:
```
TargetName Type Endian TapName State
-- ------------------ ---------- ------ ------------------ -----
0* stm32f1x.cpu cortex_m little stm32f1x.cpu halted
```
## Halting the target
`halt()` stops the target and returns the resulting state:
<Tabs>
<TabItem label="Async">
```python
state = await ocd.target.halt()
print(f"Halted at PC=0x{state.current_pc:08X}")
```
</TabItem>
<TabItem label="Sync">
```python
state = ocd.target.halt()
print(f"Halted at PC=0x{state.current_pc:08X}")
```
</TabItem>
</Tabs>
If the target is already halted, `halt()` succeeds silently (OpenOCD reports "already halted", which the library does not treat as an error). A `TargetError` is raised only if the halt command actually fails.
## Resuming execution
`resume()` starts the target running from the current PC, or from a specific address:
<Tabs>
<TabItem label="Async">
```python
# Resume from current PC
await ocd.target.resume()
# Resume from a specific address
await ocd.target.resume(address=0x08000000)
```
</TabItem>
<TabItem label="Sync">
```python
ocd.target.resume()
ocd.target.resume(address=0x08000000)
```
</TabItem>
</Tabs>
`resume()` returns `None`. If you need to verify the target started running, call `state()` afterward.
## Single-stepping
`step()` executes one instruction and returns the resulting state:
<Tabs>
<TabItem label="Async">
```python
state = await ocd.target.step()
print(f"Stepped to PC=0x{state.current_pc:08X}")
# Step from a specific address
state = await ocd.target.step(address=0x08001000)
```
</TabItem>
<TabItem label="Sync">
```python
state = ocd.target.step()
print(f"Stepped to PC=0x{state.current_pc:08X}")
state = ocd.target.step(address=0x08001000)
```
</TabItem>
</Tabs>
Like `halt()`, `step()` returns a `TargetState` with the updated program counter.
## Resetting the target
`reset()` issues a target reset with one of three modes:
| Mode | Behavior |
|------|----------|
| `"halt"` | Reset and halt at the reset vector (default) |
| `"run"` | Reset and immediately resume execution |
| `"init"` | Reset and run OpenOCD init scripts |
<Tabs>
<TabItem label="Async">
```python
# Reset and halt (most common for debugging)
await ocd.target.reset(mode="halt")
# Reset and run (for production flash-and-go)
await ocd.target.reset(mode="run")
# Reset and run init scripts
await ocd.target.reset(mode="init")
```
</TabItem>
<TabItem label="Sync">
```python
ocd.target.reset(mode="halt")
ocd.target.reset(mode="run")
ocd.target.reset(mode="init")
```
</TabItem>
</Tabs>
`reset()` returns `None`. After a `reset("halt")`, the target should be halted at the reset vector. Query `state()` to confirm and read the PC.
## Waiting for halt
`wait_halt()` blocks until the target halts or the timeout expires. This is useful after setting a breakpoint and resuming:
<Tabs>
<TabItem label="Async">
```python
await ocd.target.resume()
try:
state = await ocd.target.wait_halt(timeout_ms=5000)
print(f"Target halted at PC=0x{state.current_pc:08X}")
except TimeoutError:
print("Target did not halt within 5 seconds")
```
</TabItem>
<TabItem label="Sync">
```python
ocd.target.resume()
try:
state = ocd.target.wait_halt(timeout_ms=5000)
print(f"Target halted at PC=0x{state.current_pc:08X}")
except TimeoutError:
print("Target did not halt within 5 seconds")
```
</TabItem>
</Tabs>
| Parameter | Default | Description |
|-----------|---------|-------------|
| `timeout_ms` | `5000` | Maximum wait time in milliseconds |
The timeout is enforced by OpenOCD (the `wait_halt` command), not by the Python library. A `TimeoutError` is raised if the response contains "timed out" or "time out". Any other error response raises a `TargetError`.
## Complete workflow example
A typical debug workflow that halts, inspects, modifies, and resumes:
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session, TargetNotHaltedError, TimeoutError
async def debug_session():
async with Session.connect() as ocd:
# Reset and halt at the reset vector
await ocd.target.reset(mode="halt")
state = await ocd.target.state()
print(f"Reset vector: 0x{state.current_pc:08X}")
# Step through the first 5 instructions
for i in range(5):
state = await ocd.target.step()
print(f" Step {i+1}: PC=0x{state.current_pc:08X}")
# Set a breakpoint and run to it
await ocd.breakpoints.add(0x08001000)
await ocd.target.resume()
try:
state = await ocd.target.wait_halt(timeout_ms=3000)
print(f"Hit breakpoint at PC=0x{state.current_pc:08X}")
except TimeoutError:
print("Breakpoint not hit, halting manually")
await ocd.target.halt()
# Clean up breakpoint and resume
await ocd.breakpoints.remove(0x08001000)
await ocd.target.resume()
asyncio.run(debug_session())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session, TargetNotHaltedError, TimeoutError
with Session.connect_sync() as ocd:
ocd.target.reset(mode="halt")
state = ocd.target.state()
print(f"Reset vector: 0x{state.current_pc:08X}")
for i in range(5):
state = ocd.target.step()
print(f" Step {i+1}: PC=0x{state.current_pc:08X}")
ocd.breakpoints.add(0x08001000)
ocd.target.resume()
try:
state = ocd.target.wait_halt(timeout_ms=3000)
print(f"Hit breakpoint at PC=0x{state.current_pc:08X}")
except TimeoutError:
print("Breakpoint not hit, halting manually")
ocd.target.halt()
ocd.breakpoints.remove(0x08001000)
ocd.target.resume()
```
</TabItem>
</Tabs>
## Method summary
| Method | Returns | Description |
|--------|---------|-------------|
| `state()` | `TargetState` | Query current state without changing it |
| `halt()` | `TargetState` | Halt the target |
| `resume(address=None)` | `None` | Resume execution |
| `step(address=None)` | `TargetState` | Single-step one instruction |
| `reset(mode="halt")` | `None` | Reset the target |
| `wait_halt(timeout_ms=5000)` | `TargetState` | Block until target halts |
## Errors
| Exception | When |
|-----------|------|
| `TargetError` | Any target command fails (halt, resume, step, reset) |
| `TimeoutError` | `wait_halt` exceeds its deadline |
## Next steps
- [Register Access](/guides/register-access/) -- read and write CPU registers (requires halted target)
- [Memory Operations](/guides/memory-operations/) -- read and write target memory
- [Error Handling](/guides/error-handling/) -- full exception reference