Add Getting Started and core guide documentation (10 pages)
This commit is contained in:
parent
e6829d0a53
commit
cfd79338f4
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user