From cfd79338f42dd2bdda7f92adeb8207b8c64942ec Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sat, 14 Feb 2026 18:31:49 -0700 Subject: [PATCH] Add Getting Started and core guide documentation (10 pages) --- src/content/docs/getting-started/cli.mdx | 189 +++++++- .../docs/getting-started/first-connection.mdx | 208 ++++++++- .../docs/getting-started/installation.mdx | 147 +++++- .../docs/getting-started/quick-start.mdx | 422 +++++++++++++++++- src/content/docs/guides/async-vs-sync.mdx | 234 +++++++++- src/content/docs/guides/error-handling.mdx | 321 ++++++++++++- src/content/docs/guides/memory-operations.mdx | 306 ++++++++++++- src/content/docs/guides/register-access.mdx | 322 ++++++++++++- src/content/docs/guides/session-lifecycle.mdx | 279 +++++++++++- src/content/docs/guides/target-control.mdx | 294 +++++++++++- 10 files changed, 2712 insertions(+), 10 deletions(-) diff --git a/src/content/docs/getting-started/cli.mdx b/src/content/docs/getting-started/cli.mdx index 0e2da7f..0680114 100644 --- a/src/content/docs/getting-started/cli.mdx +++ b/src/content/docs/getting-started/cli.mdx @@ -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. + + + +## `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 `.`) + + + +## `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. +``` + + + +## 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 diff --git a/src/content/docs/getting-started/first-connection.mdx b/src/content/docs/getting-started/first-connection.mdx index 5ea50c6..dd8af5e 100644 --- a/src/content/docs/getting-started/first-connection.mdx +++ b/src/content/docs/getting-started/first-connection.mdx @@ -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. + + + + ```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()) + ``` + + + ```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}") + ``` + + + +### 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. + + + + ```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()) + ``` + + + ```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}") + ``` + + + +### 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 +) +``` + + + +## 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. + + + +## 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. + + + + ```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") + ``` + + + ```python + with Session.connect_sync() as ocd: + version = ocd.command("version") + print(version) + + ocd.command("adapter speed 4000") + ``` + + + +## 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 diff --git a/src/content/docs/getting-started/installation.mdx b/src/content/docs/getting-started/installation.mdx index 74ba085..6e6b254 100644 --- a/src/content/docs/getting-started/installation.mdx +++ b/src/content/docs/getting-started/installation.mdx @@ -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 + + + + ```bash + uv add openocd-python + ``` + + + ```bash + pip install openocd-python + ``` + + + ```bash + pipx install openocd-python + ``` + + + +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. + + + + ```bash + sudo pacman -S openocd + ``` + + + ```bash + sudo apt install openocd + ``` + + + ```bash + brew install openocd + ``` + + + Download the latest release from [openocd.org](https://openocd.org/pages/getting-openocd.html) and add the `bin` directory to your system `PATH`. + + + ```bash + git clone https://github.com/openocd-org/openocd.git + cd openocd + ./bootstrap + ./configure + make + sudo make install + ``` + + + +Verify OpenOCD is installed and accessible: + +```bash +openocd --version +``` + + + +## 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. + + + +## 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 diff --git a/src/content/docs/getting-started/quick-start.mdx b/src/content/docs/getting-started/quick-start.mdx index 2ba329b..7a59562 100644 --- a/src/content/docs/getting-started/quick-start.mdx +++ b/src/content/docs/getting-started/quick-start.mdx @@ -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. + + + +## Connect and read target state + +The simplest possible script: connect, read the target state, and print it. + + + + ```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()) + ``` + + + ```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}") + ``` + + + +Output when the target is halted: + +``` +Target: stm32f1x.cpu +State: halted +PC: 0x08001234 +``` + +## Halt, step, and resume + +Control the target execution state. + + + + ```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()) + ``` + + + ```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") + ``` + + + +## Read memory + +Read memory at various widths and display a hexdump. + + + + ```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()) + ``` + + + ```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}") + ``` + + + +## Read and write registers + +Access CPU registers by name, with convenience methods for common ARM Cortex-M registers. + + + + ```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()) + ``` + + + ```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() + ``` + + + +## Program flash + +Write a firmware image to flash memory with automatic erase and verification. + + + + ```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()) + ``` + + + ```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") + ``` + + + + + +## 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. + + + + ```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()) + ``` + + + ```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() + ``` + + + +## JTAG chain scan + +Discover all TAPs (Test Access Ports) on the JTAG chain. + + + + ```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()) + ``` + + + ```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}" + ) + ``` + + + +## 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: + + + + ```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()) + ``` + + + ```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}") + ``` + + + +## 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 diff --git a/src/content/docs/guides/async-vs-sync.mdx b/src/content/docs/guides/async-vs-sync.mdx index 6c6dc36..93801d6 100644 --- a/src/content/docs/guides/async-vs-sync.mdx +++ b/src/content/docs/guides/async-vs-sync.mdx @@ -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 + + + +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 diff --git a/src/content/docs/guides/error-handling.mdx b/src/content/docs/guides/error-handling.mdx index 8719c97..18b7e93 100644 --- a/src/content/docs/guides/error-handling.mdx +++ b/src/content/docs/guides/error-handling.mdx @@ -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, +) +``` + + + +## 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") +``` + + + +### 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 diff --git a/src/content/docs/guides/memory-operations.mdx b/src/content/docs/guides/memory-operations.mdx index bd14fbf..4e30ee3 100644 --- a/src/content/docs/guides/memory-operations.mdx +++ b/src/content/docs/guides/memory-operations.mdx @@ -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] +``` + + + + ```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) + ``` + + + ```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) + ``` + + + + + +### read_bytes + +For bulk data, `read_bytes()` returns raw `bytes`: + +```python +async def read_bytes(addr: int, size: int) -> bytes +``` + + + + ```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()}") + ``` + + + ```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()}") + ``` + + + +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 +``` + + + + ```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]) + ``` + + + ```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]) + ``` + + + +### write_bytes + +For bulk data, `write_bytes()` accepts a `bytes` object: + +```python +async def write_bytes(addr: int, data: bytes) -> None +``` + + + + ```python + async with Session.connect() as ocd: + payload = b"Hello, target!" + await ocd.memory.write_bytes(0x20000000, payload) + ``` + + + ```python + with Session.connect_sync() as ocd: + payload = b"Hello, target!" + ocd.memory.write_bytes(0x20000000, payload) + ``` + + + + + +## 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 +``` + + + + ```python + async with Session.connect() as ocd: + dump = await ocd.memory.hexdump(0x08000000, 64) + print(dump) + ``` + + + ```python + with Session.connect_sync() as ocd: + dump = ocd.memory.hexdump(0x08000000, 64) + print(dump) + ``` + + + +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] +``` + + + + ```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}") + ``` + + + ```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}") + ``` + + + +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. + + + +## File dump + +`dump()` reads memory and writes the raw bytes to a file: + +```python +async def dump(addr: int, size: int, path: Path) -> None +``` + + + + ```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")) + ``` + + + ```python + from pathlib import Path + + with Session.connect_sync() as ocd: + ocd.memory.dump(0x08000000, 128 * 1024, Path("flash_dump.bin")) + ``` + + + +## 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 diff --git a/src/content/docs/guides/register-access.mdx b/src/content/docs/guides/register-access.mdx index 38f8417..230dcd1 100644 --- a/src/content/docs/guides/register-access.mdx +++ b/src/content/docs/guides/register-access.mdx @@ -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`. + + + +## Reading a single register + +`read()` takes a register name and returns its integer value: + +```python +async def read(name: str) -> int +``` + + + + ```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}") + ``` + + + ```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}") + ``` + + + +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 +``` + + + + ```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() + ``` + + + ```python + with Session.connect_sync() as ocd: + ocd.target.halt() + + ocd.registers.write("r0", 0x42) + ocd.registers.write("pc", 0x08001000) + + ocd.target.resume() + ``` + + + +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] +``` + + + + ```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}") + ``` + + + ```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}") + ``` + + + +This issues one `reg ` 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] +``` + + + + ```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}") + ``` + + + ```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}") + ``` + + + +## 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")` | + + + + ```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}") + ``` + + + ```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}") + ``` + + + +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: + + + + ```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()) + ``` + + + ```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() + ``` + + + +## 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 diff --git a/src/content/docs/guides/session-lifecycle.mdx b/src/content/docs/guides/session-lifecycle.mdx index 88fe0b2..1802d7a 100644 --- a/src/content/docs/guides/session-lifecycle.mdx +++ b/src/content/docs/guides/session-lifecycle.mdx @@ -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. + + + + ```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()) + ``` + + + ```python + from openocd import Session + + with Session.connect_sync(host="localhost", port=6666) as ocd: + print(ocd.command("version")) + ``` + + + +### 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. + + + + ```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()) + ``` + + + ```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) + ``` + + + +## 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. + + + + ```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}]") + ``` + + + ```python + with Session.connect_sync() as ocd: + version = ocd.command("version") + ocd.command("adapter speed 8000") + ``` + + + +## 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. + + + +## 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 diff --git a/src/content/docs/guides/target-control.mdx b/src/content/docs/guides/target-control.mdx index 459e718..83df860 100644 --- a/src/content/docs/guides/target-control.mdx +++ b/src/content/docs/guides/target-control.mdx @@ -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: + + + + ```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}") + ``` + + + ```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}") + ``` + + + +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: + + + + ```python + state = await ocd.target.halt() + print(f"Halted at PC=0x{state.current_pc:08X}") + ``` + + + ```python + state = ocd.target.halt() + print(f"Halted at PC=0x{state.current_pc:08X}") + ``` + + + +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: + + + + ```python + # Resume from current PC + await ocd.target.resume() + + # Resume from a specific address + await ocd.target.resume(address=0x08000000) + ``` + + + ```python + ocd.target.resume() + ocd.target.resume(address=0x08000000) + ``` + + + +`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: + + + + ```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) + ``` + + + ```python + state = ocd.target.step() + print(f"Stepped to PC=0x{state.current_pc:08X}") + + state = ocd.target.step(address=0x08001000) + ``` + + + +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 | + + + + ```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") + ``` + + + ```python + ocd.target.reset(mode="halt") + ocd.target.reset(mode="run") + ocd.target.reset(mode="init") + ``` + + + +`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: + + + + ```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") + ``` + + + ```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") + ``` + + + +| 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: + + + + ```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()) + ``` + + + ```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() + ``` + + + +## 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