Compare commits
8 Commits
b49b9ceef0
...
fe49fff171
| Author | SHA1 | Date | |
|---|---|---|---|
| fe49fff171 | |||
| fd424643f5 | |||
| 2f44f0f0a3 | |||
| cfd79338f4 | |||
| e6829d0a53 | |||
| 196f08804c | |||
| 073a6232e8 | |||
| 967e02b175 |
4
.env.example
Normal file
4
.env.example
Normal file
@ -0,0 +1,4 @@
|
||||
COMPOSE_PROJECT=openocd-python-docs
|
||||
MODE=prod
|
||||
DOMAIN=openocd-python.warehack.ing
|
||||
VITE_HMR_HOST=
|
||||
32
Dockerfile
Normal file
32
Dockerfile
Normal file
@ -0,0 +1,32 @@
|
||||
# --- Stage: deps ---
|
||||
FROM node:22-alpine AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci
|
||||
|
||||
# --- Stage: build ---
|
||||
FROM deps AS build
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# --- Stage: dev ---
|
||||
FROM node:22-alpine AS dev
|
||||
WORKDIR /app
|
||||
COPY --from=deps /app/node_modules ./node_modules
|
||||
COPY . .
|
||||
EXPOSE 4321
|
||||
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
|
||||
|
||||
# --- Stage: prod ---
|
||||
FROM caddy:2-alpine AS prod
|
||||
COPY --from=build /app/dist /srv
|
||||
COPY <<'CADDYFILE' /etc/caddy/Caddyfile
|
||||
:80 {
|
||||
root * /srv
|
||||
file_server
|
||||
encode gzip
|
||||
try_files {path} {path}/index.html =404
|
||||
header Cache-Control "public, max-age=3600"
|
||||
}
|
||||
CADDYFILE
|
||||
EXPOSE 80
|
||||
24
Makefile
Normal file
24
Makefile
Normal file
@ -0,0 +1,24 @@
|
||||
.PHONY: dev prod build logs down clean restart shell
|
||||
|
||||
dev:
|
||||
docker compose --profile dev up -d --build && docker compose --profile dev logs -f
|
||||
|
||||
prod:
|
||||
docker compose --profile prod up -d --build && sleep 3 && docker compose --profile prod logs --tail 50
|
||||
|
||||
build:
|
||||
docker compose --profile prod build --no-cache
|
||||
|
||||
logs:
|
||||
docker compose logs -f
|
||||
|
||||
down:
|
||||
docker compose --profile dev --profile prod down
|
||||
|
||||
clean:
|
||||
docker compose --profile dev --profile prod down -v --rmi local
|
||||
|
||||
restart: down prod
|
||||
|
||||
shell:
|
||||
docker compose exec docs-prod sh
|
||||
86
astro.config.mjs
Normal file
86
astro.config.mjs
Normal file
@ -0,0 +1,86 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import starlight from '@astrojs/starlight';
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://openocd-python.warehack.ing',
|
||||
telemetry: false,
|
||||
devToolbar: { enabled: false },
|
||||
integrations: [
|
||||
starlight({
|
||||
title: 'openocd-python',
|
||||
description: 'Typed async-first Python bindings for OpenOCD',
|
||||
logo: {
|
||||
src: './src/assets/logo.svg',
|
||||
alt: 'openocd-python',
|
||||
},
|
||||
favicon: '/favicon.svg',
|
||||
social: [
|
||||
{
|
||||
icon: 'github',
|
||||
label: 'GitHub',
|
||||
href: 'https://git.supported.systems/warehack.ing/openocd-python',
|
||||
},
|
||||
],
|
||||
customCss: ['./src/styles/custom.css'],
|
||||
sidebar: [
|
||||
{
|
||||
label: 'Getting Started',
|
||||
items: [
|
||||
{ label: 'Installation', slug: 'getting-started/installation' },
|
||||
{ label: 'First Connection', slug: 'getting-started/first-connection' },
|
||||
{ label: 'CLI Reference', slug: 'getting-started/cli' },
|
||||
{ label: 'Quick Start', slug: 'getting-started/quick-start' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Guides',
|
||||
items: [
|
||||
{ label: 'Session Lifecycle', slug: 'guides/session-lifecycle' },
|
||||
{ label: 'Async vs Sync', slug: 'guides/async-vs-sync' },
|
||||
{ label: 'Error Handling', slug: 'guides/error-handling' },
|
||||
{ label: 'Target Control', slug: 'guides/target-control' },
|
||||
{ label: 'Memory Operations', slug: 'guides/memory-operations' },
|
||||
{ label: 'Register Access', slug: 'guides/register-access' },
|
||||
{ label: 'Flash Programming', slug: 'guides/flash-programming' },
|
||||
{ label: 'Breakpoints & Watchpoints', slug: 'guides/breakpoints' },
|
||||
{ label: 'JTAG Operations', slug: 'guides/jtag-operations' },
|
||||
{ label: 'SVD Register Decoding', slug: 'guides/svd-decoding' },
|
||||
{ label: 'RTT Communication', slug: 'guides/rtt-communication' },
|
||||
{ label: 'Transport & Adapter', slug: 'guides/transport-adapter' },
|
||||
{ label: 'Event Callbacks', slug: 'guides/event-callbacks' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'API Reference',
|
||||
items: [
|
||||
{ label: 'Session API', slug: 'reference/session-api' },
|
||||
{ label: 'Types', slug: 'reference/types' },
|
||||
{ label: 'Exceptions', slug: 'reference/exceptions' },
|
||||
{ label: 'Connection Layer', slug: 'reference/connection-layer' },
|
||||
{ label: 'Method Index', slug: 'reference/method-index' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Examples',
|
||||
items: [
|
||||
{ label: 'Debug Session', slug: 'examples/debug-session' },
|
||||
{ label: 'Flash Programming', slug: 'examples/flash-programming' },
|
||||
{ label: 'SVD Inspection', slug: 'examples/svd-inspection' },
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
],
|
||||
vite: {
|
||||
server: {
|
||||
host: '0.0.0.0',
|
||||
...(process.env.VITE_HMR_HOST && {
|
||||
hmr: {
|
||||
host: process.env.VITE_HMR_HOST,
|
||||
protocol: 'wss',
|
||||
clientPort: 443,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
42
docker-compose.yml
Normal file
42
docker-compose.yml
Normal file
@ -0,0 +1,42 @@
|
||||
services:
|
||||
docs-dev:
|
||||
build:
|
||||
context: .
|
||||
target: dev
|
||||
profiles:
|
||||
- dev
|
||||
volumes:
|
||||
- ./src:/app/src
|
||||
- ./public:/app/public
|
||||
- ./astro.config.mjs:/app/astro.config.mjs
|
||||
env_file: .env
|
||||
labels:
|
||||
caddy: ${DOMAIN}
|
||||
caddy.reverse_proxy: "{{upstreams 4321}}"
|
||||
caddy.reverse_proxy.flush_interval: "-1"
|
||||
caddy.reverse_proxy.transport: "http"
|
||||
caddy.reverse_proxy.transport.read_timeout: "0"
|
||||
caddy.reverse_proxy.transport.write_timeout: "0"
|
||||
caddy.reverse_proxy.transport.keepalive: "5m"
|
||||
caddy.reverse_proxy.transport.keepalive_idle_conns: "10"
|
||||
caddy.reverse_proxy.stream_timeout: "24h"
|
||||
caddy.reverse_proxy.stream_close_delay: "5s"
|
||||
networks:
|
||||
- caddy
|
||||
|
||||
docs-prod:
|
||||
build:
|
||||
context: .
|
||||
target: prod
|
||||
profiles:
|
||||
- prod
|
||||
restart: unless-stopped
|
||||
labels:
|
||||
caddy: ${DOMAIN}
|
||||
caddy.reverse_proxy: "{{upstreams 80}}"
|
||||
networks:
|
||||
- caddy
|
||||
|
||||
networks:
|
||||
caddy:
|
||||
external: true
|
||||
7902
package-lock.json
generated
Normal file
7902
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
19
package.json
Normal file
19
package.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "openocd-python-docs",
|
||||
"type": "module",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "astro dev",
|
||||
"start": "astro dev",
|
||||
"build": "astro build",
|
||||
"preview": "astro preview",
|
||||
"astro": "astro"
|
||||
},
|
||||
"dependencies": {
|
||||
"astro": "^5.17.2",
|
||||
"@astrojs/starlight": "^0.35.0",
|
||||
"sharp": "^0.33.0",
|
||||
"astro-icon": "^1.1.0",
|
||||
"@iconify-json/lucide": "^1.2.0"
|
||||
}
|
||||
}
|
||||
25
public/favicon.svg
Normal file
25
public/favicon.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- Chip body -->
|
||||
<rect x="12" y="12" width="24" height="24" rx="3" stroke="#0d9488" stroke-width="2.5" fill="none"/>
|
||||
<!-- Inner die -->
|
||||
<rect x="18" y="18" width="12" height="12" rx="1.5" fill="#0d9488" opacity="0.15"/>
|
||||
<rect x="18" y="18" width="12" height="12" rx="1.5" stroke="#0d9488" stroke-width="1.5" fill="none"/>
|
||||
<!-- Pin 1 marker -->
|
||||
<circle cx="21" cy="21" r="1.5" fill="#0d9488" opacity="0.6"/>
|
||||
<!-- Top pins -->
|
||||
<line x1="18" y1="12" x2="18" y2="5" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="24" y1="12" x2="24" y2="5" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="30" y1="12" x2="30" y2="5" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Bottom pins -->
|
||||
<line x1="18" y1="36" x2="18" y2="43" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="24" y1="36" x2="24" y2="43" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="30" y1="36" x2="30" y2="43" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Left pins -->
|
||||
<line x1="12" y1="18" x2="5" y2="18" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="12" y1="24" x2="5" y2="24" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="12" y1="30" x2="5" y2="30" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Right pins -->
|
||||
<line x1="36" y1="18" x2="43" y2="18" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="36" y1="24" x2="43" y2="24" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="36" y1="30" x2="43" y2="30" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
25
src/assets/logo.svg
Normal file
25
src/assets/logo.svg
Normal file
@ -0,0 +1,25 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" fill="none">
|
||||
<!-- Chip body -->
|
||||
<rect x="12" y="12" width="24" height="24" rx="3" stroke="#0d9488" stroke-width="2.5" fill="none"/>
|
||||
<!-- Inner die -->
|
||||
<rect x="18" y="18" width="12" height="12" rx="1.5" fill="#0d9488" opacity="0.15"/>
|
||||
<rect x="18" y="18" width="12" height="12" rx="1.5" stroke="#0d9488" stroke-width="1.5" fill="none"/>
|
||||
<!-- Pin 1 marker -->
|
||||
<circle cx="21" cy="21" r="1.5" fill="#0d9488" opacity="0.6"/>
|
||||
<!-- Top pins -->
|
||||
<line x1="18" y1="12" x2="18" y2="5" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="24" y1="12" x2="24" y2="5" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="30" y1="12" x2="30" y2="5" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Bottom pins -->
|
||||
<line x1="18" y1="36" x2="18" y2="43" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="24" y1="36" x2="24" y2="43" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="30" y1="36" x2="30" y2="43" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Left pins -->
|
||||
<line x1="12" y1="18" x2="5" y2="18" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="12" y1="24" x2="5" y2="24" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="12" y1="30" x2="5" y2="30" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- Right pins -->
|
||||
<line x1="36" y1="18" x2="43" y2="18" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="36" y1="24" x2="43" y2="24" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="36" y1="30" x2="43" y2="30" stroke="#0d9488" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
7
src/content.config.ts
Normal file
7
src/content.config.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import { defineCollection } from 'astro:content';
|
||||
import { docsLoader } from '@astrojs/starlight/loaders';
|
||||
import { docsSchema } from '@astrojs/starlight/schema';
|
||||
|
||||
export const collections = {
|
||||
docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }),
|
||||
};
|
||||
204
src/content/docs/examples/debug-session.mdx
Normal file
204
src/content/docs/examples/debug-session.mdx
Normal file
@ -0,0 +1,204 @@
|
||||
---
|
||||
title: Debug Session
|
||||
description: Complete walkthrough of connecting to a target, reading state, inspecting registers and memory, single-stepping, and resuming.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
This example demonstrates a typical debug workflow: connect to OpenOCD, halt the target, inspect registers and memory, single-step through code, and resume execution.
|
||||
|
||||
## Async version
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import logging
|
||||
from openocd import Session, TargetError, TargetNotHaltedError, ConnectionError
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger("debug-session")
|
||||
|
||||
|
||||
async def main():
|
||||
# --- Connect ---
|
||||
try:
|
||||
session = await Session.connect(host="localhost", port=6666, timeout=5.0)
|
||||
except ConnectionError as e:
|
||||
log.error("Cannot connect to OpenOCD: %s", e)
|
||||
log.error("Make sure OpenOCD is running with TCL RPC enabled on port 6666")
|
||||
return
|
||||
|
||||
async with session:
|
||||
# --- Check initial state ---
|
||||
state = await session.target.state()
|
||||
log.info("Target: %s, State: %s", state.name, state.state)
|
||||
|
||||
# --- Halt ---
|
||||
if state.state != "halted":
|
||||
log.info("Halting target...")
|
||||
try:
|
||||
state = await session.target.halt()
|
||||
except TargetError as e:
|
||||
log.error("Failed to halt: %s", e)
|
||||
return
|
||||
log.info("Halted at PC=0x%08X", state.current_pc or 0)
|
||||
|
||||
# --- Read registers ---
|
||||
pc = await session.registers.pc()
|
||||
sp = await session.registers.sp()
|
||||
lr = await session.registers.lr()
|
||||
log.info("PC=0x%08X SP=0x%08X LR=0x%08X", pc, sp, lr)
|
||||
|
||||
# Read all general-purpose registers
|
||||
all_regs = await session.registers.read_all()
|
||||
for name, reg in sorted(all_regs.items(), key=lambda x: x[1].number):
|
||||
if reg.number <= 15: # r0-r15 on ARM
|
||||
log.info(" %-6s = 0x%08X%s",
|
||||
reg.name, reg.value,
|
||||
" (dirty)" if reg.dirty else "")
|
||||
|
||||
# --- Read memory around PC ---
|
||||
log.info("Memory at PC (8 words):")
|
||||
words = await session.memory.read_u32(pc, count=8)
|
||||
for i, word in enumerate(words):
|
||||
log.info(" 0x%08X: 0x%08X", pc + i * 4, word)
|
||||
|
||||
# --- Hexdump of stack ---
|
||||
log.info("Stack (64 bytes from SP):")
|
||||
hexdump = await session.memory.hexdump(sp, 64)
|
||||
for line in hexdump.splitlines():
|
||||
log.info(" %s", line)
|
||||
|
||||
# --- Single-step ---
|
||||
log.info("Single-stepping 3 instructions...")
|
||||
for i in range(3):
|
||||
state = await session.target.step()
|
||||
new_pc = state.current_pc or 0
|
||||
log.info(" Step %d: PC=0x%08X", i + 1, new_pc)
|
||||
|
||||
# --- Resume ---
|
||||
log.info("Resuming execution...")
|
||||
await session.target.resume()
|
||||
|
||||
# Verify the target is running
|
||||
state = await session.target.state()
|
||||
log.info("Target state after resume: %s", state.state)
|
||||
|
||||
log.info("Session closed")
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Sync version
|
||||
|
||||
```python
|
||||
import logging
|
||||
from openocd import Session, TargetError, ConnectionError
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger("debug-session")
|
||||
|
||||
|
||||
def main():
|
||||
# --- Connect ---
|
||||
try:
|
||||
session = Session.connect_sync(host="localhost", port=6666, timeout=5.0)
|
||||
except ConnectionError as e:
|
||||
log.error("Cannot connect to OpenOCD: %s", e)
|
||||
return
|
||||
|
||||
with session:
|
||||
# --- Check initial state ---
|
||||
state = session.target.state()
|
||||
log.info("Target: %s, State: %s", state.name, state.state)
|
||||
|
||||
# --- Halt ---
|
||||
if state.state != "halted":
|
||||
log.info("Halting target...")
|
||||
try:
|
||||
state = session.target.halt()
|
||||
except TargetError as e:
|
||||
log.error("Failed to halt: %s", e)
|
||||
return
|
||||
log.info("Halted at PC=0x%08X", state.current_pc or 0)
|
||||
|
||||
# --- Read registers ---
|
||||
pc = session.registers.pc()
|
||||
sp = session.registers.sp()
|
||||
lr = session.registers.lr()
|
||||
log.info("PC=0x%08X SP=0x%08X LR=0x%08X", pc, sp, lr)
|
||||
|
||||
# Read all general-purpose registers
|
||||
all_regs = session.registers.read_all()
|
||||
for name, reg in sorted(all_regs.items(), key=lambda x: x[1].number):
|
||||
if reg.number <= 15:
|
||||
log.info(" %-6s = 0x%08X%s",
|
||||
reg.name, reg.value,
|
||||
" (dirty)" if reg.dirty else "")
|
||||
|
||||
# --- Read memory around PC ---
|
||||
log.info("Memory at PC (8 words):")
|
||||
words = session.memory.read_u32(pc, count=8)
|
||||
for i, word in enumerate(words):
|
||||
log.info(" 0x%08X: 0x%08X", pc + i * 4, word)
|
||||
|
||||
# --- Hexdump of stack ---
|
||||
log.info("Stack (64 bytes from SP):")
|
||||
hexdump = session.memory.hexdump(sp, 64)
|
||||
for line in hexdump.splitlines():
|
||||
log.info(" %s", line)
|
||||
|
||||
# --- Single-step ---
|
||||
log.info("Single-stepping 3 instructions...")
|
||||
for i in range(3):
|
||||
state = session.target.step()
|
||||
new_pc = state.current_pc or 0
|
||||
log.info(" Step %d: PC=0x%08X", i + 1, new_pc)
|
||||
|
||||
# --- Resume ---
|
||||
log.info("Resuming execution...")
|
||||
session.target.resume()
|
||||
|
||||
state = session.target.state()
|
||||
log.info("Target state after resume: %s", state.state)
|
||||
|
||||
log.info("Session closed")
|
||||
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
## Expected output
|
||||
|
||||
```
|
||||
INFO:debug-session:Target: stm32f1x.cpu, State: running
|
||||
INFO:debug-session:Halting target...
|
||||
INFO:debug-session:Halted at PC=0x08001234
|
||||
INFO:debug-session:PC=0x08001234 SP=0x20004FF0 LR=0x08000A51
|
||||
INFO:debug-session: r0 = 0x00000001
|
||||
INFO:debug-session: r1 = 0x20000100
|
||||
INFO:debug-session: r2 = 0x00000000
|
||||
INFO:debug-session: ...
|
||||
INFO:debug-session:Memory at PC (8 words):
|
||||
INFO:debug-session: 0x08001234: 0x4B02B510
|
||||
INFO:debug-session: 0x08001238: 0x47984601
|
||||
INFO:debug-session: ...
|
||||
INFO:debug-session:Stack (64 bytes from SP):
|
||||
INFO:debug-session: 20004FF0: 08 00 0A 51 00 00 00 00 01 00 00 00 00 01 00 20 |...Q........... |
|
||||
INFO:debug-session: ...
|
||||
INFO:debug-session:Single-stepping 3 instructions...
|
||||
INFO:debug-session: Step 1: PC=0x08001236
|
||||
INFO:debug-session: Step 2: PC=0x08001238
|
||||
INFO:debug-session: Step 3: PC=0x0800123A
|
||||
INFO:debug-session:Resuming execution...
|
||||
INFO:debug-session:Target state after resume: running
|
||||
INFO:debug-session:Session closed
|
||||
```
|
||||
|
||||
## Key points
|
||||
|
||||
- Always check the target state before operating. Some operations (register reads, memory inspection) require a halted target.
|
||||
- The `TargetState` dataclass includes `current_pc` only when the target is halted. Check for `None` before using it.
|
||||
- Use `session.registers.read_all()` for a snapshot of all registers, or individual accessors like `session.registers.pc()` for specific values.
|
||||
- `session.memory.hexdump()` returns a formatted string -- it does not print directly. This lets you log or display it however you prefer.
|
||||
- The session context manager (`async with` / `with`) ensures the connection is closed cleanly on exit, even if an exception occurs.
|
||||
267
src/content/docs/examples/flash-programming.mdx
Normal file
267
src/content/docs/examples/flash-programming.mdx
Normal file
@ -0,0 +1,267 @@
|
||||
---
|
||||
title: Flash Programming
|
||||
description: End-to-end firmware update workflow with erase, program, verify, and reset.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
This example demonstrates a complete firmware update workflow: inspect the flash layout, erase, program a firmware image, verify, and reset the target.
|
||||
|
||||
## Async version
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from openocd import Session, FlashError, ConnectionError
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
||||
log = logging.getLogger("flash-program")
|
||||
|
||||
|
||||
async def flash_firmware(firmware_path: Path):
|
||||
"""Program a firmware image and verify it."""
|
||||
|
||||
if not firmware_path.exists():
|
||||
log.error("Firmware file not found: %s", firmware_path)
|
||||
return False
|
||||
|
||||
file_size = firmware_path.stat().st_size
|
||||
log.info("Firmware: %s (%d bytes)", firmware_path.name, file_size)
|
||||
|
||||
try:
|
||||
session = await Session.connect(timeout=5.0)
|
||||
except ConnectionError as e:
|
||||
log.error("Cannot connect to OpenOCD: %s", e)
|
||||
return False
|
||||
|
||||
async with session:
|
||||
# --- Halt the target first ---
|
||||
log.info("Halting target...")
|
||||
await session.target.halt()
|
||||
|
||||
# --- Inspect flash layout ---
|
||||
banks = await session.flash.banks()
|
||||
log.info("Found %d flash bank(s):", len(banks))
|
||||
for bank in banks:
|
||||
log.info(" Bank #%d: %s @ 0x%08X, size=0x%X (%d KB)",
|
||||
bank.index, bank.name, bank.base, bank.size,
|
||||
bank.size // 1024)
|
||||
|
||||
# Get detailed sector info for bank 0
|
||||
bank_info = await session.flash.info(0)
|
||||
log.info("Bank 0 has %d sectors", len(bank_info.sectors))
|
||||
if bank_info.sectors:
|
||||
first = bank_info.sectors[0]
|
||||
last = bank_info.sectors[-1]
|
||||
log.info(" First sector: offset=0x%X, size=0x%X", first.offset, first.size)
|
||||
log.info(" Last sector: offset=0x%X, size=0x%X", last.offset, last.size)
|
||||
|
||||
# Check if firmware fits
|
||||
if file_size > bank_info.size:
|
||||
log.error("Firmware (%d bytes) is larger than flash bank (%d bytes)",
|
||||
file_size, bank_info.size)
|
||||
return False
|
||||
|
||||
# --- Erase ---
|
||||
log.info("Erasing flash bank 0...")
|
||||
try:
|
||||
await session.flash.erase_all(bank=0)
|
||||
log.info("Erase complete")
|
||||
except FlashError as e:
|
||||
log.error("Erase failed: %s", e)
|
||||
return False
|
||||
|
||||
# --- Program ---
|
||||
log.info("Programming %s...", firmware_path.name)
|
||||
try:
|
||||
await session.flash.write_image(
|
||||
firmware_path,
|
||||
erase=False, # Already erased above
|
||||
verify=True, # Built-in post-write verification
|
||||
)
|
||||
log.info("Programming and verification complete")
|
||||
except FlashError as e:
|
||||
log.error("Programming failed: %s", e)
|
||||
return False
|
||||
|
||||
# --- Optional: second verification pass ---
|
||||
log.info("Running standalone verification...")
|
||||
if firmware_path.suffix in (".bin",):
|
||||
matches = await session.flash.verify(bank=0, path=firmware_path)
|
||||
if matches:
|
||||
log.info("Standalone verification PASSED")
|
||||
else:
|
||||
log.error("Standalone verification FAILED")
|
||||
return False
|
||||
|
||||
# --- Reset and run ---
|
||||
log.info("Resetting target to run new firmware...")
|
||||
await session.target.reset(mode="run")
|
||||
|
||||
# Brief pause, then check state
|
||||
await asyncio.sleep(0.5)
|
||||
state = await session.target.state()
|
||||
log.info("Target state: %s", state.state)
|
||||
|
||||
log.info("Firmware update complete")
|
||||
return True
|
||||
|
||||
|
||||
async def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <firmware.bin|hex|elf>")
|
||||
sys.exit(1)
|
||||
|
||||
firmware = Path(sys.argv[1])
|
||||
success = await flash_firmware(firmware)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Sync version
|
||||
|
||||
```python
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from openocd import Session, FlashError, ConnectionError
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
|
||||
log = logging.getLogger("flash-program")
|
||||
|
||||
|
||||
def flash_firmware(firmware_path: Path) -> bool:
|
||||
"""Program a firmware image and verify it."""
|
||||
|
||||
if not firmware_path.exists():
|
||||
log.error("Firmware file not found: %s", firmware_path)
|
||||
return False
|
||||
|
||||
file_size = firmware_path.stat().st_size
|
||||
log.info("Firmware: %s (%d bytes)", firmware_path.name, file_size)
|
||||
|
||||
try:
|
||||
session = Session.connect_sync(timeout=5.0)
|
||||
except ConnectionError as e:
|
||||
log.error("Cannot connect to OpenOCD: %s", e)
|
||||
return False
|
||||
|
||||
with session:
|
||||
log.info("Halting target...")
|
||||
session.target.halt()
|
||||
|
||||
# Inspect flash
|
||||
banks = session.flash.banks()
|
||||
log.info("Found %d flash bank(s)", len(banks))
|
||||
|
||||
bank_info = session.flash.info(0)
|
||||
log.info("Bank 0: %d sectors, %d KB",
|
||||
len(bank_info.sectors), bank_info.size // 1024)
|
||||
|
||||
if file_size > bank_info.size:
|
||||
log.error("Firmware too large for flash bank")
|
||||
return False
|
||||
|
||||
# Erase
|
||||
log.info("Erasing...")
|
||||
try:
|
||||
session.flash.erase_all(bank=0)
|
||||
except FlashError as e:
|
||||
log.error("Erase failed: %s", e)
|
||||
return False
|
||||
|
||||
# Program with built-in verify
|
||||
log.info("Programming %s...", firmware_path.name)
|
||||
try:
|
||||
session.flash.write_image(firmware_path, erase=False, verify=True)
|
||||
log.info("Programming complete")
|
||||
except FlashError as e:
|
||||
log.error("Programming failed: %s", e)
|
||||
return False
|
||||
|
||||
# Reset and run
|
||||
log.info("Resetting target...")
|
||||
session.target.reset(mode="run")
|
||||
log.info("Done")
|
||||
return True
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <firmware.bin|hex|elf>")
|
||||
sys.exit(1)
|
||||
|
||||
success = flash_firmware(Path(sys.argv[1]))
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
## Expected output
|
||||
|
||||
```
|
||||
INFO: Firmware: firmware.hex (32768 bytes)
|
||||
INFO: Halting target...
|
||||
INFO: Found 1 flash bank(s):
|
||||
INFO: Bank #0: stm32f1x.flash @ 0x08000000, size=0x20000 (128 KB)
|
||||
INFO: Bank 0 has 128 sectors
|
||||
INFO: First sector: offset=0x0, size=0x400
|
||||
INFO: Last sector: offset=0x1FC00, size=0x400
|
||||
INFO: Erasing flash bank 0...
|
||||
INFO: Erase complete
|
||||
INFO: Programming firmware.hex...
|
||||
INFO: Programming and verification complete
|
||||
INFO: Running standalone verification...
|
||||
INFO: Standalone verification PASSED
|
||||
INFO: Resetting target to run new firmware...
|
||||
INFO: Target state: running
|
||||
INFO: Firmware update complete
|
||||
```
|
||||
|
||||
## Selective sector erase
|
||||
|
||||
For faster updates when only part of the flash changes, erase only the affected sectors instead of the whole bank:
|
||||
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
await session.target.halt()
|
||||
|
||||
bank = await session.flash.info(0)
|
||||
firmware_size = Path("firmware.bin").stat().st_size
|
||||
|
||||
# Calculate which sectors the firmware occupies
|
||||
last_sector = 0
|
||||
cumulative = 0
|
||||
for sector in bank.sectors:
|
||||
cumulative = sector.offset + sector.size
|
||||
if cumulative >= firmware_size:
|
||||
last_sector = sector.index
|
||||
break
|
||||
|
||||
log.info("Erasing sectors 0-%d (covers %d bytes)", last_sector, cumulative)
|
||||
await session.flash.erase_sector(bank=0, first=0, last=last_sector)
|
||||
await session.flash.write_image(Path("firmware.bin"), erase=False, verify=True)
|
||||
```
|
||||
|
||||
## Protecting the bootloader
|
||||
|
||||
After programming, protect the bootloader region so it cannot be accidentally overwritten:
|
||||
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
await session.target.halt()
|
||||
|
||||
# Program the full image
|
||||
await session.flash.write_image(Path("firmware.hex"), erase=True, verify=True)
|
||||
|
||||
# Protect the first 2 sectors (bootloader)
|
||||
await session.flash.protect(bank=0, first=0, last=1, on=True)
|
||||
log.info("Bootloader sectors protected")
|
||||
```
|
||||
224
src/content/docs/examples/svd-inspection.mdx
Normal file
224
src/content/docs/examples/svd-inspection.mdx
Normal file
@ -0,0 +1,224 @@
|
||||
---
|
||||
title: SVD Inspection
|
||||
description: Load an SVD file, browse peripherals, and decode hardware register values into named bitfields.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
This example demonstrates using SVD metadata to inspect hardware registers on a live target. Instead of manually masking and shifting bits from raw hex values, the SVD subsystem decodes register contents into named fields with descriptions.
|
||||
|
||||
## Async version
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from openocd import Session, SVDError, ConnectionError
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
log = logging.getLogger("svd-inspect")
|
||||
|
||||
|
||||
async def main():
|
||||
svd_path = Path("STM32F103.svd")
|
||||
if not svd_path.exists():
|
||||
log.error("SVD file not found: %s", svd_path)
|
||||
log.error("Download from your vendor's CMSIS pack or cmsis-svd-data on GitHub")
|
||||
return
|
||||
|
||||
try:
|
||||
session = await Session.connect(timeout=5.0)
|
||||
except ConnectionError as e:
|
||||
log.error("Cannot connect to OpenOCD: %s", e)
|
||||
return
|
||||
|
||||
async with session:
|
||||
# --- Load SVD ---
|
||||
log.info("Loading SVD file: %s", svd_path)
|
||||
await session.svd.load(svd_path)
|
||||
|
||||
# --- Browse peripherals ---
|
||||
peripherals = session.svd.list_peripherals()
|
||||
log.info("Found %d peripherals", len(peripherals))
|
||||
log.info("First 10: %s", ", ".join(peripherals[:10]))
|
||||
|
||||
# --- Browse registers in GPIOA ---
|
||||
gpio_regs = session.svd.list_registers("GPIOA")
|
||||
log.info("\nGPIOA registers: %s", ", ".join(gpio_regs))
|
||||
|
||||
# --- Halt and read live registers ---
|
||||
await session.target.halt()
|
||||
|
||||
# Read and decode GPIOA.ODR (Output Data Register)
|
||||
log.info("\n--- GPIOA Output Data Register ---")
|
||||
odr = await session.svd.read_register("GPIOA", "ODR")
|
||||
log.info("%s", odr)
|
||||
|
||||
# Read and decode GPIOA.IDR (Input Data Register)
|
||||
log.info("\n--- GPIOA Input Data Register ---")
|
||||
idr = await session.svd.read_register("GPIOA", "IDR")
|
||||
log.info("%s", idr)
|
||||
|
||||
# --- Inspect clock configuration ---
|
||||
log.info("\n--- RCC Clock Control Register ---")
|
||||
rcc_cr = await session.svd.read_register("RCC", "CR")
|
||||
log.info("%s", rcc_cr)
|
||||
|
||||
# Check specific fields
|
||||
for field in rcc_cr.fields:
|
||||
if field.name in ("HSEON", "HSERDY", "PLLON", "PLLRDY"):
|
||||
status = "ON" if field.value else "OFF"
|
||||
log.info(" %s: %s - %s", field.name, status, field.description)
|
||||
|
||||
# --- Read an entire peripheral ---
|
||||
log.info("\n--- All USART1 registers ---")
|
||||
try:
|
||||
usart_regs = await session.svd.read_peripheral("USART1")
|
||||
for name, decoded in usart_regs.items():
|
||||
log.info("%s", decoded)
|
||||
log.info("") # blank line between registers
|
||||
except SVDError as e:
|
||||
log.warning("Could not read USART1: %s", e)
|
||||
|
||||
# --- Decode without hardware read ---
|
||||
log.info("\n--- Offline decode example ---")
|
||||
# Suppose we captured this value from a log file
|
||||
captured_value = 0x0300_0083
|
||||
decoded = session.svd.decode("RCC", "CR", captured_value)
|
||||
log.info("Decoding RCC.CR = 0x%08X:", captured_value)
|
||||
log.info("%s", decoded)
|
||||
|
||||
# --- Access individual field values programmatically ---
|
||||
log.info("\n--- Programmatic field access ---")
|
||||
odr = await session.svd.read_register("GPIOA", "ODR")
|
||||
pin_states = []
|
||||
for field in odr.fields:
|
||||
if field.value:
|
||||
pin_states.append(field.name)
|
||||
if pin_states:
|
||||
log.info("GPIOA pins driven high: %s", ", ".join(pin_states))
|
||||
else:
|
||||
log.info("No GPIOA pins driven high")
|
||||
|
||||
# Resume the target
|
||||
await session.target.resume()
|
||||
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Sync version
|
||||
|
||||
```python
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from openocd import Session, SVDError, ConnectionError
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
||||
log = logging.getLogger("svd-inspect")
|
||||
|
||||
|
||||
def main():
|
||||
svd_path = Path("STM32F103.svd")
|
||||
if not svd_path.exists():
|
||||
log.error("SVD file not found: %s", svd_path)
|
||||
return
|
||||
|
||||
try:
|
||||
session = Session.connect_sync(timeout=5.0)
|
||||
except ConnectionError as e:
|
||||
log.error("Cannot connect to OpenOCD: %s", e)
|
||||
return
|
||||
|
||||
with session:
|
||||
# Load SVD
|
||||
log.info("Loading SVD file: %s", svd_path)
|
||||
session.svd.load(svd_path)
|
||||
|
||||
# Browse
|
||||
peripherals = session.svd.list_peripherals()
|
||||
log.info("Found %d peripherals", len(peripherals))
|
||||
|
||||
gpio_regs = session.svd.list_registers("GPIOA")
|
||||
log.info("GPIOA registers: %s", ", ".join(gpio_regs))
|
||||
|
||||
# Halt and read
|
||||
session.target.halt()
|
||||
|
||||
log.info("\n--- GPIOA Output Data Register ---")
|
||||
odr = session.svd.read_register("GPIOA", "ODR")
|
||||
log.info("%s", odr)
|
||||
|
||||
log.info("\n--- RCC Clock Control Register ---")
|
||||
rcc_cr = session.svd.read_register("RCC", "CR")
|
||||
log.info("%s", rcc_cr)
|
||||
|
||||
for field in rcc_cr.fields:
|
||||
if field.name in ("HSEON", "HSERDY", "PLLON", "PLLRDY"):
|
||||
status = "ON" if field.value else "OFF"
|
||||
log.info(" %s: %s", field.name, status)
|
||||
|
||||
# Decode a captured value without reading hardware
|
||||
decoded = session.svd.decode("RCC", "CR", 0x0300_0083)
|
||||
log.info("\nOffline decode of RCC.CR = 0x03000083:")
|
||||
log.info("%s", decoded)
|
||||
|
||||
session.target.resume()
|
||||
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
## Expected output
|
||||
|
||||
```
|
||||
Loading SVD file: STM32F103.svd
|
||||
Found 51 peripherals
|
||||
First 10: ADC1, ADC2, AFIO, BKP, CAN, CRC, DAC, DMA1, DMA2, EXTI
|
||||
|
||||
GPIOA registers: BRR, BSRR, CRH, CRL, IDR, LCKR, ODR
|
||||
|
||||
--- GPIOA Output Data Register ---
|
||||
GPIOA.ODR @ 0x4001080C = 0x00000001
|
||||
[ 0:0] ODR0 = 0x1 Port output data bit 0
|
||||
[ 1:1] ODR1 = 0x0 Port output data bit 1
|
||||
[ 2:2] ODR2 = 0x0 Port output data bit 2
|
||||
[ 3:3] ODR3 = 0x0 Port output data bit 3
|
||||
...
|
||||
|
||||
--- GPIOA Input Data Register ---
|
||||
GPIOA.IDR @ 0x40010808 = 0x0000FFFD
|
||||
[ 0:0] IDR0 = 0x1 Port input data bit 0
|
||||
[ 1:1] IDR1 = 0x0 Port input data bit 1
|
||||
...
|
||||
|
||||
--- RCC Clock Control Register ---
|
||||
RCC.CR @ 0x40021000 = 0x03000083
|
||||
[ 0:0] HSION = 0x1 Internal high-speed clock enable
|
||||
[ 1:1] HSIRDY = 0x1 Internal high-speed clock ready flag
|
||||
[ 16:16] HSEON = 0x1 HSE clock enable
|
||||
[ 17:17] HSERDY = 0x1 External high-speed clock ready flag
|
||||
[ 24:24] PLLON = 0x1 PLL enable
|
||||
[ 25:25] PLLRDY = 0x1 PLL clock ready flag
|
||||
HSEON: ON - HSE clock enable
|
||||
HSERDY: ON - External high-speed clock ready flag
|
||||
PLLON: ON - PLL enable
|
||||
PLLRDY: ON - PLL clock ready flag
|
||||
|
||||
--- Offline decode example ---
|
||||
Decoding RCC.CR = 0x03000083:
|
||||
RCC.CR @ 0x40021000 = 0x03000083
|
||||
[ 0:0] HSION = 0x1 Internal high-speed clock enable
|
||||
...
|
||||
```
|
||||
|
||||
## Key points
|
||||
|
||||
- **`load()` is required first.** All other SVD methods raise `SVDError` if no SVD file has been loaded.
|
||||
- **`list_peripherals()` and `list_registers()` are synchronous** -- they operate on in-memory data after the initial parse. No `await` needed in async mode.
|
||||
- **`read_register()` reads from live hardware.** It computes the register's memory-mapped address from the SVD, reads 32 bits via the memory subsystem, and decodes the result. The target should be halted for consistent reads.
|
||||
- **`decode()` works offline.** Pass a raw integer value and it returns the same `DecodedRegister` structure without touching hardware. Useful for analyzing values from log files or crash dumps.
|
||||
- **`read_peripheral()` is fault-tolerant.** It silently skips write-only or otherwise unreadable registers and logs warnings. The returned dict contains only registers that were successfully read.
|
||||
- **`DecodedRegister.__str__`** produces formatted output. Each field shows its bit range, name, value, and description -- no manual bit manipulation needed.
|
||||
193
src/content/docs/getting-started/cli.mdx
Normal file
193
src/content/docs/getting-started/cli.mdx
Normal file
@ -0,0 +1,193 @@
|
||||
---
|
||||
title: CLI Reference
|
||||
description: Command-line interface for openocd-python
|
||||
---
|
||||
|
||||
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
|
||||
212
src/content/docs/getting-started/first-connection.mdx
Normal file
212
src/content/docs/getting-started/first-connection.mdx
Normal file
@ -0,0 +1,212 @@
|
||||
---
|
||||
title: First Connection
|
||||
description: Connect to an OpenOCD instance for the first time
|
||||
---
|
||||
|
||||
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
|
||||
151
src/content/docs/getting-started/installation.mdx
Normal file
151
src/content/docs/getting-started/installation.mdx
Normal file
@ -0,0 +1,151 @@
|
||||
---
|
||||
title: Installation
|
||||
description: Install openocd-python and its dependencies
|
||||
---
|
||||
|
||||
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
|
||||
426
src/content/docs/getting-started/quick-start.mdx
Normal file
426
src/content/docs/getting-started/quick-start.mdx
Normal file
@ -0,0 +1,426 @@
|
||||
---
|
||||
title: Quick Start
|
||||
description: Get up and running with openocd-python in minutes
|
||||
---
|
||||
|
||||
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
|
||||
238
src/content/docs/guides/async-vs-sync.mdx
Normal file
238
src/content/docs/guides/async-vs-sync.mdx
Normal file
@ -0,0 +1,238 @@
|
||||
---
|
||||
title: Async vs Sync
|
||||
description: Choosing between async and synchronous APIs
|
||||
---
|
||||
|
||||
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
|
||||
259
src/content/docs/guides/breakpoints.mdx
Normal file
259
src/content/docs/guides/breakpoints.mdx
Normal file
@ -0,0 +1,259 @@
|
||||
---
|
||||
title: Breakpoints and Watchpoints
|
||||
description: Set, remove, and list hardware/software breakpoints and data watchpoints.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The `BreakpointManager` subsystem wraps OpenOCD's `bp`, `rbp`, `wp`, and `rwp` commands to manage breakpoints that pause execution at a specific instruction address, and watchpoints that trigger on data memory access.
|
||||
|
||||
Access it through the session:
|
||||
|
||||
```python
|
||||
# Async
|
||||
session.breakpoints
|
||||
|
||||
# Sync
|
||||
sync_session.breakpoints
|
||||
```
|
||||
|
||||
## Hardware vs. software breakpoints
|
||||
|
||||
Breakpoints come in two flavors, each with different tradeoffs:
|
||||
|
||||
**Software breakpoints** replace the instruction at the target address with a special breakpoint instruction (e.g. `BKPT` on ARM). When the CPU executes that address, it traps into debug mode. The original instruction is restored transparently. Software breakpoints are plentiful but require writable memory -- they do not work in flash unless the debugger patches flash.
|
||||
|
||||
**Hardware breakpoints** use dedicated comparator registers built into the CPU core (e.g. the Flash Patch and Breakpoint unit on Cortex-M). They work on any memory region including flash, but the number available is limited -- typically 4 to 8 on Cortex-M devices.
|
||||
|
||||
<Aside type="tip">
|
||||
Use software breakpoints for code in RAM and hardware breakpoints for code in flash. OpenOCD automatically selects the right type in many configurations, but you can force hardware breakpoints with `hw=True`.
|
||||
</Aside>
|
||||
|
||||
## Setting breakpoints
|
||||
|
||||
`add(address, length=2, hw=False)` sets a breakpoint at the given instruction address.
|
||||
|
||||
The `length` parameter indicates the instruction size:
|
||||
- `2` for Thumb instructions (16-bit, the default on Cortex-M)
|
||||
- `4` for ARM instructions (32-bit)
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
import asyncio
|
||||
from openocd import Session
|
||||
|
||||
async def main():
|
||||
async with await Session.connect() as session:
|
||||
# Software breakpoint on a Thumb instruction
|
||||
await session.breakpoints.add(0x0800_1234, length=2)
|
||||
|
||||
# Hardware breakpoint on an ARM instruction
|
||||
await session.breakpoints.add(0x0800_5678, length=4, hw=True)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from openocd import Session
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
# Software breakpoint on a Thumb instruction
|
||||
session.breakpoints.add(0x0800_1234, length=2)
|
||||
|
||||
# Hardware breakpoint on an ARM instruction
|
||||
session.breakpoints.add(0x0800_5678, length=4, hw=True)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Removing breakpoints
|
||||
|
||||
`remove(address)` removes the breakpoint at the specified address.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
await session.breakpoints.add(0x0800_1234)
|
||||
# ... debug ...
|
||||
await session.breakpoints.remove(0x0800_1234)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.breakpoints.add(0x0800_1234)
|
||||
# ... debug ...
|
||||
session.breakpoints.remove(0x0800_1234)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Listing breakpoints
|
||||
|
||||
`list()` returns all active breakpoints as a list of `Breakpoint` dataclasses.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
await session.breakpoints.add(0x0800_1234)
|
||||
await session.breakpoints.add(0x0800_5678, hw=True)
|
||||
|
||||
bps = await session.breakpoints.list()
|
||||
for bp in bps:
|
||||
print(f"BP #{bp.number}: 0x{bp.address:08X} "
|
||||
f"type={bp.type} len={bp.length} enabled={bp.enabled}")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.breakpoints.add(0x0800_1234)
|
||||
session.breakpoints.add(0x0800_5678, hw=True)
|
||||
|
||||
bps = session.breakpoints.list()
|
||||
for bp in bps:
|
||||
print(f"BP #{bp.number}: 0x{bp.address:08X} "
|
||||
f"type={bp.type} len={bp.length} enabled={bp.enabled}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Data watchpoints
|
||||
|
||||
Watchpoints monitor data memory access rather than instruction execution. They trigger when the CPU reads from, writes to, or accesses a specific address range.
|
||||
|
||||
### Setting a watchpoint
|
||||
|
||||
`add_watchpoint(address, length, access="rw")` creates a data watchpoint.
|
||||
|
||||
The `access` parameter controls which operations trigger the watchpoint:
|
||||
- `"r"` -- read access only
|
||||
- `"w"` -- write access only
|
||||
- `"rw"` -- any access (read or write)
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
# Break on any write to a 4-byte variable
|
||||
await session.breakpoints.add_watchpoint(
|
||||
address=0x2000_0100,
|
||||
length=4,
|
||||
access="w"
|
||||
)
|
||||
|
||||
# Break on any access to a buffer
|
||||
await session.breakpoints.add_watchpoint(
|
||||
address=0x2000_0200,
|
||||
length=16,
|
||||
access="rw"
|
||||
)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.breakpoints.add_watchpoint(
|
||||
address=0x2000_0100, length=4, access="w"
|
||||
)
|
||||
session.breakpoints.add_watchpoint(
|
||||
address=0x2000_0200, length=16, access="rw"
|
||||
)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Aside type="caution">
|
||||
Watchpoint length must typically be a power of 2 on ARM targets (1, 2, 4, 8, ...). This is a hardware limitation of the DWT comparators. The number of available watchpoints is also limited, usually 2 to 4 on Cortex-M.
|
||||
</Aside>
|
||||
|
||||
### Removing a watchpoint
|
||||
|
||||
`remove_watchpoint(address)` removes the watchpoint at the given address.
|
||||
|
||||
```python
|
||||
await session.breakpoints.remove_watchpoint(0x2000_0100)
|
||||
```
|
||||
|
||||
### Listing watchpoints
|
||||
|
||||
`list_watchpoints()` returns all active watchpoints.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
await session.breakpoints.add_watchpoint(0x2000_0100, 4, "w")
|
||||
|
||||
wps = await session.breakpoints.list_watchpoints()
|
||||
for wp in wps:
|
||||
print(f"WP #{wp.number}: 0x{wp.address:08X} "
|
||||
f"len={wp.length} access={wp.access}")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.breakpoints.add_watchpoint(0x2000_0100, 4, "w")
|
||||
|
||||
wps = session.breakpoints.list_watchpoints()
|
||||
for wp in wps:
|
||||
print(f"WP #{wp.number}: 0x{wp.address:08X} "
|
||||
f"len={wp.length} access={wp.access}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Data types
|
||||
|
||||
### Breakpoint
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `number` | `int` | Breakpoint index |
|
||||
| `type` | `Literal["hw", "sw"]` | Hardware or software breakpoint |
|
||||
| `address` | `int` | Instruction address |
|
||||
| `length` | `int` | Instruction length in bytes (2 = Thumb, 4 = ARM) |
|
||||
| `enabled` | `bool` | Whether the breakpoint is active |
|
||||
|
||||
### Watchpoint
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `number` | `int` | Watchpoint index |
|
||||
| `address` | `int` | Watched memory address |
|
||||
| `length` | `int` | Size of watched region in bytes |
|
||||
| `access` | `Literal["r", "w", "rw"]` | Access type that triggers the watchpoint |
|
||||
|
||||
## Error handling
|
||||
|
||||
Breakpoint and watchpoint operations raise `BreakpointError` (a subclass of `OpenOCDError`) when OpenOCD reports a failure.
|
||||
|
||||
```python
|
||||
from openocd.breakpoints import BreakpointError
|
||||
|
||||
try:
|
||||
session.breakpoints.add(0x0800_1234, hw=True)
|
||||
except BreakpointError as e:
|
||||
print(f"Could not set breakpoint: {e}")
|
||||
```
|
||||
|
||||
Common failure causes:
|
||||
- No hardware breakpoint comparators available (reduce the number of HW breakpoints)
|
||||
- Target not halted when attempting to set a software breakpoint
|
||||
- Invalid address or length
|
||||
|
||||
## Method reference
|
||||
|
||||
| Method | Return Type | Description |
|
||||
|--------|-------------|-------------|
|
||||
| `add(address, length=2, hw=False)` | `None` | Set a breakpoint |
|
||||
| `remove(address)` | `None` | Remove a breakpoint |
|
||||
| `list()` | `list[Breakpoint]` | List active breakpoints |
|
||||
| `add_watchpoint(address, length, access="rw")` | `None` | Set a data watchpoint |
|
||||
| `remove_watchpoint(address)` | `None` | Remove a watchpoint |
|
||||
| `list_watchpoints()` | `list[Watchpoint]` | List active watchpoints |
|
||||
325
src/content/docs/guides/error-handling.mdx
Normal file
325
src/content/docs/guides/error-handling.mdx
Normal file
@ -0,0 +1,325 @@
|
||||
---
|
||||
title: Error Handling
|
||||
description: Exception hierarchy and error recovery patterns
|
||||
---
|
||||
|
||||
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
|
||||
192
src/content/docs/guides/event-callbacks.mdx
Normal file
192
src/content/docs/guides/event-callbacks.mdx
Normal file
@ -0,0 +1,192 @@
|
||||
---
|
||||
title: Event Callbacks
|
||||
description: Receive asynchronous notifications when the target halts, resumes, resets, or changes state.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The `EventManager` enables asynchronous target state notifications from OpenOCD. When enabled, OpenOCD pushes messages over a dedicated TCP connection whenever the target halts, resumes, resets, or a GDB client attaches/detaches. You register callbacks to react to these events without polling.
|
||||
|
||||
The `Session` class also provides convenience shortcuts (`on_halt`, `on_reset`) for the most common cases.
|
||||
|
||||
## Architecture: dual-socket design
|
||||
|
||||
OpenOCD's TCL RPC notification system uses a **separate TCP connection** from the command channel. This prevents notifications from interleaving with command responses on the same stream.
|
||||
|
||||
When you enable notifications:
|
||||
|
||||
1. A second TCP connection opens to the same OpenOCD host and port
|
||||
2. The command `tcl_notifications on` is sent on this second connection
|
||||
3. A background asyncio task reads notification messages from this dedicated socket
|
||||
4. Incoming messages are dispatched to registered callbacks
|
||||
|
||||
The command connection remains unaffected -- you can send commands and receive notifications simultaneously without race conditions.
|
||||
|
||||
## Quick start with Session shortcuts
|
||||
|
||||
The simplest way to react to events is through the `Session.on_halt()` and `Session.on_reset()` methods. These register notification callbacks that filter for specific keywords in the message.
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from openocd import Session
|
||||
|
||||
async def main():
|
||||
async with await Session.connect() as session:
|
||||
# Register event handlers
|
||||
session.on_halt(lambda msg: print(f"Target halted: {msg}"))
|
||||
session.on_reset(lambda msg: print(f"Target reset: {msg}"))
|
||||
|
||||
# Resume the target and wait for it to hit a breakpoint
|
||||
await session.breakpoints.add(0x0800_1234, hw=True)
|
||||
await session.target.resume()
|
||||
|
||||
# Give it time to trigger
|
||||
await asyncio.sleep(2.0)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
<Aside type="note">
|
||||
The `on_halt` and `on_reset` shortcuts register callbacks directly on the connection's notification system. They use substring matching: `on_halt` triggers when "halted" appears anywhere in the notification message, and `on_reset` triggers on "reset".
|
||||
</Aside>
|
||||
|
||||
## Using EventManager directly
|
||||
|
||||
For finer control, use the `EventManager` class. It supports multiple event types and allows registering and unregistering individual callbacks.
|
||||
|
||||
### Enabling notifications
|
||||
|
||||
`enable()` opens the notification socket and starts the background listener. Call this before registering callbacks.
|
||||
|
||||
```python
|
||||
from openocd.events import EventManager
|
||||
|
||||
async with await Session.connect() as session:
|
||||
events = EventManager(session._conn)
|
||||
await events.enable()
|
||||
print(f"Notifications enabled: {events.enabled}")
|
||||
```
|
||||
|
||||
### Registering callbacks
|
||||
|
||||
`on(event_type, callback)` registers a callback for a specific event. Matching is case-insensitive substring: a notification containing "halted" anywhere in its text triggers all callbacks registered for the `"halted"` event type.
|
||||
|
||||
```python
|
||||
from openocd.events import (
|
||||
EventManager,
|
||||
EVENT_HALTED,
|
||||
EVENT_RESUMED,
|
||||
EVENT_RESET,
|
||||
EVENT_GDB_ATTACHED,
|
||||
EVENT_GDB_DETACHED,
|
||||
)
|
||||
|
||||
def on_halted(msg: str) -> None:
|
||||
print(f"[HALT] {msg}")
|
||||
|
||||
def on_resumed(msg: str) -> None:
|
||||
print(f"[RESUME] {msg}")
|
||||
|
||||
events = EventManager(session._conn)
|
||||
await events.enable()
|
||||
|
||||
events.on(EVENT_HALTED, on_halted)
|
||||
events.on(EVENT_RESUMED, on_resumed)
|
||||
events.on(EVENT_RESET, lambda msg: print(f"[RESET] {msg}"))
|
||||
events.on(EVENT_GDB_ATTACHED, lambda msg: print(f"[GDB+] {msg}"))
|
||||
events.on(EVENT_GDB_DETACHED, lambda msg: print(f"[GDB-] {msg}"))
|
||||
```
|
||||
|
||||
### Unregistering callbacks
|
||||
|
||||
`off(event_type, callback)` removes a previously registered callback. If the callback was not registered, `off()` silently does nothing.
|
||||
|
||||
```python
|
||||
events.off(EVENT_HALTED, on_halted)
|
||||
```
|
||||
|
||||
## Known event types
|
||||
|
||||
The `events` module defines constants for known notification types:
|
||||
|
||||
| Constant | String Value | Trigger |
|
||||
|----------|-------------|---------|
|
||||
| `EVENT_HALTED` | `"halted"` | Target entered halted state |
|
||||
| `EVENT_RESUMED` | `"resumed"` | Target resumed execution |
|
||||
| `EVENT_RESET` | `"reset"` | Target was reset |
|
||||
| `EVENT_GDB_ATTACHED` | `"gdb-attached"` | GDB client connected |
|
||||
| `EVENT_GDB_DETACHED` | `"gdb-detached"` | GDB client disconnected |
|
||||
|
||||
These are not an exhaustive list -- OpenOCD may emit other notification strings depending on configuration. You can register callbacks for any substring pattern.
|
||||
|
||||
## Complete example
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from openocd import Session
|
||||
from openocd.events import EventManager, EVENT_HALTED, EVENT_RESUMED
|
||||
|
||||
halt_count = 0
|
||||
|
||||
def count_halts(msg: str) -> None:
|
||||
global halt_count
|
||||
halt_count += 1
|
||||
print(f"Halt #{halt_count}: {msg}")
|
||||
|
||||
async def main():
|
||||
async with await Session.connect() as session:
|
||||
# Set up event monitoring
|
||||
events = EventManager(session._conn)
|
||||
await events.enable()
|
||||
events.on(EVENT_HALTED, count_halts)
|
||||
events.on(EVENT_RESUMED, lambda m: print(f"Resumed: {m}"))
|
||||
|
||||
# Set a breakpoint and let the target run
|
||||
await session.breakpoints.add(0x0800_1234, hw=True)
|
||||
await session.target.resume()
|
||||
|
||||
# Monitor for 5 seconds
|
||||
await asyncio.sleep(5.0)
|
||||
|
||||
print(f"Total halts observed: {halt_count}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Callback behavior
|
||||
|
||||
Callbacks run synchronously within the notification reader's asyncio task. Keep these points in mind:
|
||||
|
||||
- Callbacks receive the **full notification message string** as their single argument.
|
||||
- Callbacks should be fast and non-blocking. Dispatch long-running work to a separate task.
|
||||
- Exceptions in callbacks are caught and logged -- they do not crash the notification loop.
|
||||
- Multiple callbacks for the same event type are called in registration order.
|
||||
- The same callback function will not be registered twice for the same event type.
|
||||
|
||||
```python
|
||||
# Fast callback -- good
|
||||
def on_halt(msg: str) -> None:
|
||||
print(f"Halted: {msg}")
|
||||
|
||||
# Dispatching slow work -- good
|
||||
def on_halt_with_work(msg: str) -> None:
|
||||
asyncio.create_task(analyze_state(msg))
|
||||
```
|
||||
|
||||
## EventManager API reference
|
||||
|
||||
| Member | Type | Description |
|
||||
|--------|------|-------------|
|
||||
| `enable()` | `async` | Send `tcl_notifications on`, open notification socket |
|
||||
| `on(event_type, callback)` | sync | Register a callback (case-insensitive substring match) |
|
||||
| `off(event_type, callback)` | sync | Unregister a callback |
|
||||
| `enabled` (property) | `bool` | Whether notifications are active |
|
||||
|
||||
## Session shortcuts
|
||||
|
||||
| Method | Trigger Keyword | Description |
|
||||
|--------|----------------|-------------|
|
||||
| `session.on_halt(callback)` | `"halted"` | Register a halt callback on the connection |
|
||||
| `session.on_reset(callback)` | `"reset"` | Register a reset callback on the connection |
|
||||
|
||||
These shortcuts register directly on the connection's notification handler and work whenever the notification listener is active.
|
||||
316
src/content/docs/guides/flash-programming.mdx
Normal file
316
src/content/docs/guides/flash-programming.mdx
Normal file
@ -0,0 +1,316 @@
|
||||
---
|
||||
title: Flash Programming
|
||||
description: Read, write, erase, verify, and protect on-chip flash memory banks through OpenOCD.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The `Flash` subsystem wraps OpenOCD's `flash` command family for programming on-chip flash memory. It handles bank enumeration, sector-level erase, raw byte read/write through temporary files, high-level firmware image programming, verification, and write protection.
|
||||
|
||||
Access it through the session:
|
||||
|
||||
```python
|
||||
# Async
|
||||
session.flash
|
||||
|
||||
# Sync
|
||||
sync_session.flash
|
||||
```
|
||||
|
||||
## Listing flash banks
|
||||
|
||||
`banks()` returns a list of `FlashBank` descriptors for every flash bank OpenOCD has configured. These come without detailed sector information -- use `info()` for that.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
import asyncio
|
||||
from openocd import Session
|
||||
|
||||
async def main():
|
||||
async with await Session.connect() as session:
|
||||
banks = await session.flash.banks()
|
||||
for bank in banks:
|
||||
print(f"Bank #{bank.index}: {bank.name}")
|
||||
print(f" Base: 0x{bank.base:08X}, Size: 0x{bank.size:X}")
|
||||
print(f" Bus width: {bank.bus_width}, Chip width: {bank.chip_width}")
|
||||
print(f" Target: {bank.target}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from openocd import Session
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
banks = session.flash.banks()
|
||||
for bank in banks:
|
||||
print(f"Bank #{bank.index}: {bank.name}")
|
||||
print(f" Base: 0x{bank.base:08X}, Size: 0x{bank.size:X}")
|
||||
print(f" Bus width: {bank.bus_width}, Chip width: {bank.chip_width}")
|
||||
print(f" Target: {bank.target}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Getting bank details with sectors
|
||||
|
||||
`info(bank=0)` returns a `FlashBank` with its `sectors` list populated. Each sector is a `FlashSector` with index, offset, size, and protection status.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
bank = await session.flash.info(0)
|
||||
print(f"{bank.name}: {len(bank.sectors)} sectors")
|
||||
for sector in bank.sectors:
|
||||
prot = "protected" if sector.protected else "unprotected"
|
||||
print(f" Sector {sector.index}: offset=0x{sector.offset:X}, "
|
||||
f"size=0x{sector.size:X}, {prot}")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
bank = session.flash.info(0)
|
||||
print(f"{bank.name}: {len(bank.sectors)} sectors")
|
||||
for sector in bank.sectors:
|
||||
prot = "protected" if sector.protected else "unprotected"
|
||||
print(f" Sector {sector.index}: offset=0x{sector.offset:X}, "
|
||||
f"size=0x{sector.size:X}, {prot}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Reading flash
|
||||
|
||||
Two methods for reading flash content:
|
||||
|
||||
- **`read(bank, offset, size)`** -- returns raw `bytes` by writing to a temp file, then reading the data back through TCL or from the local filesystem.
|
||||
- **`read_to_file(bank, path)`** -- dumps the entire bank directly to a file on disk.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
async with await Session.connect() as session:
|
||||
# Read 256 bytes from the start of bank 0
|
||||
data = await session.flash.read(bank=0, offset=0, size=256)
|
||||
print(f"Read {len(data)} bytes: {data[:16].hex()}")
|
||||
|
||||
# Dump the entire bank to a file
|
||||
await session.flash.read_to_file(bank=0, path=Path("flash_dump.bin"))
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
data = session.flash.read(bank=0, offset=0, size=256)
|
||||
print(f"Read {len(data)} bytes: {data[:16].hex()}")
|
||||
|
||||
session.flash.read_to_file(bank=0, path=Path("flash_dump.bin"))
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Aside type="note">
|
||||
`read()` uses a temporary file on the host as an intermediary because OpenOCD's `flash read_bank` command writes to a file rather than returning data inline. The temp file is cleaned up automatically.
|
||||
</Aside>
|
||||
|
||||
## Writing flash
|
||||
|
||||
### Raw byte write
|
||||
|
||||
`write(bank, offset, data)` writes raw bytes to a flash bank at a given offset. Like `read()`, it uses a temporary file since OpenOCD's `flash write_bank` reads from a file.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
config_data = b"\x01\x02\x03\x04"
|
||||
await session.flash.write(bank=0, offset=0x1000, data=config_data)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
config_data = b"\x01\x02\x03\x04"
|
||||
session.flash.write(bank=0, offset=0x1000, data=config_data)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Aside type="caution">
|
||||
`write()` does not erase sectors first. Flash memory can only transition bits from 1 to 0; writing to unerased flash produces incorrect data. Use `erase_sector()` or `erase_all()` before raw writes, or use `write_image()` which handles this automatically.
|
||||
</Aside>
|
||||
|
||||
### Firmware image programming
|
||||
|
||||
`write_image(path, erase=True, verify=True)` is the high-level "flash and go" command. It handles erase, write, and verification in one operation. It accepts `.bin`, `.hex`, and `.elf` files.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
async with await Session.connect() as session:
|
||||
firmware = Path("build/firmware.hex")
|
||||
await session.flash.write_image(firmware, erase=True, verify=True)
|
||||
print("Firmware programmed and verified")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
firmware = Path("build/firmware.hex")
|
||||
session.flash.write_image(firmware, erase=True, verify=True)
|
||||
print("Firmware programmed and verified")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
When `verify=True`, the method runs `verify_image` after writing. If the verification finds a mismatch, it raises `FlashError`.
|
||||
|
||||
## Erasing flash
|
||||
|
||||
### Erase a sector range
|
||||
|
||||
`erase_sector(bank, first, last)` erases sectors from `first` to `last` (both inclusive). Validates that `first <= last` and raises `FlashError` if the range is invalid.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
# Erase sectors 0 through 3 in bank 0
|
||||
await session.flash.erase_sector(bank=0, first=0, last=3)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.flash.erase_sector(bank=0, first=0, last=3)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Erase an entire bank
|
||||
|
||||
`erase_all(bank=0)` queries the bank info to find the last sector, then erases the full range.
|
||||
|
||||
```python
|
||||
await session.flash.erase_all(bank=0)
|
||||
```
|
||||
|
||||
## Write protection
|
||||
|
||||
`protect(bank, first, last, on)` sets or clears hardware write protection on a range of sectors.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
# Protect the bootloader (sectors 0-1)
|
||||
await session.flash.protect(bank=0, first=0, last=1, on=True)
|
||||
|
||||
# Unprotect application sectors (2-7)
|
||||
await session.flash.protect(bank=0, first=2, last=7, on=False)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.flash.protect(bank=0, first=0, last=1, on=True)
|
||||
session.flash.protect(bank=0, first=2, last=7, on=False)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Verifying flash
|
||||
|
||||
`verify(bank, path)` compares flash contents against a reference binary file and returns `True` if they match, `False` otherwise.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
async with await Session.connect() as session:
|
||||
matches = await session.flash.verify(bank=0, path=Path("golden.bin"))
|
||||
if matches:
|
||||
print("Flash contents match reference file")
|
||||
else:
|
||||
print("MISMATCH detected")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
matches = session.flash.verify(bank=0, path=Path("golden.bin"))
|
||||
if matches:
|
||||
print("Flash contents match reference file")
|
||||
else:
|
||||
print("MISMATCH detected")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Data types
|
||||
|
||||
### FlashBank
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `index` | `int` | Bank number |
|
||||
| `name` | `str` | Bank name (e.g. `stm32f1x.flash`) |
|
||||
| `base` | `int` | Base address |
|
||||
| `size` | `int` | Total size in bytes |
|
||||
| `bus_width` | `int` | Bus width |
|
||||
| `chip_width` | `int` | Chip width |
|
||||
| `target` | `str` | Associated target or driver name |
|
||||
| `sectors` | `list[FlashSector]` | Sector list (empty from `banks()`, populated from `info()`) |
|
||||
|
||||
### FlashSector
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `index` | `int` | Sector number within the bank |
|
||||
| `offset` | `int` | Byte offset from the bank base |
|
||||
| `size` | `int` | Sector size in bytes |
|
||||
| `protected` | `bool` | Whether write protection is enabled |
|
||||
|
||||
## Error handling
|
||||
|
||||
All flash operations raise `FlashError` on failure. The error message includes the OpenOCD response for diagnostics.
|
||||
|
||||
```python
|
||||
from openocd import Session, FlashError
|
||||
|
||||
try:
|
||||
with Session.connect_sync() as session:
|
||||
session.flash.write_image(Path("firmware.bin"))
|
||||
except FlashError as e:
|
||||
print(f"Flash operation failed: {e}")
|
||||
```
|
||||
|
||||
## Method reference
|
||||
|
||||
| Method | Return Type | Description |
|
||||
|--------|-------------|-------------|
|
||||
| `banks()` | `list[FlashBank]` | List all configured flash banks |
|
||||
| `info(bank=0)` | `FlashBank` | Detailed bank info with sectors |
|
||||
| `read(bank, offset, size)` | `bytes` | Read raw flash via temp file |
|
||||
| `read_to_file(bank, path)` | `None` | Dump entire bank to file |
|
||||
| `write(bank, offset, data)` | `None` | Write raw bytes via temp file |
|
||||
| `write_image(path, erase=True, verify=True)` | `None` | High-level flash programming |
|
||||
| `erase_sector(bank, first, last)` | `None` | Erase a sector range |
|
||||
| `erase_all(bank=0)` | `None` | Erase entire bank |
|
||||
| `protect(bank, first, last, on)` | `None` | Set/clear write protection |
|
||||
| `verify(bank, path)` | `bool` | Verify flash against a file |
|
||||
308
src/content/docs/guides/jtag-operations.mdx
Normal file
308
src/content/docs/guides/jtag-operations.mdx
Normal file
@ -0,0 +1,308 @@
|
||||
---
|
||||
title: JTAG Operations
|
||||
description: Scan chain enumeration, IR/DR scan operations, TAP state control, and boundary scan file execution.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The `JTAGController` provides direct access to the JTAG interface: chain discovery, register scanning, TAP state machine control, and boundary scan file execution. It acts as a facade that delegates to specialized submodules (`chain`, `scan`, `state`, `boundary`).
|
||||
|
||||
Access it through the session:
|
||||
|
||||
```python
|
||||
# Async
|
||||
session.jtag
|
||||
|
||||
# Sync
|
||||
sync_session.jtag
|
||||
```
|
||||
|
||||
<Aside type="note">
|
||||
Most users interact with OpenOCD at the target level (halt, resume, memory read/write) and never need raw JTAG commands. This subsystem is for situations where you need direct control over the JTAG protocol -- production testing, boundary scan, custom TAP access, or working with non-standard debug architectures.
|
||||
</Aside>
|
||||
|
||||
## Scan chain discovery
|
||||
|
||||
`scan_chain()` queries OpenOCD for every TAP (Test Access Port) on the JTAG chain and returns them as `TAPInfo` dataclasses.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
import asyncio
|
||||
from openocd import Session
|
||||
|
||||
async def main():
|
||||
async with await Session.connect() as session:
|
||||
taps = await session.jtag.scan_chain()
|
||||
for tap in taps:
|
||||
print(f"TAP: {tap.name}")
|
||||
print(f" Chip: {tap.chip}, TAP name: {tap.tap_name}")
|
||||
print(f" IDCODE: 0x{tap.idcode:08X}")
|
||||
print(f" IR length: {tap.ir_length} bits")
|
||||
print(f" Enabled: {tap.enabled}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from openocd import Session
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
taps = session.jtag.scan_chain()
|
||||
for tap in taps:
|
||||
print(f"TAP: {tap.name}")
|
||||
print(f" Chip: {tap.chip}, TAP name: {tap.tap_name}")
|
||||
print(f" IDCODE: 0x{tap.idcode:08X}")
|
||||
print(f" IR length: {tap.ir_length} bits")
|
||||
print(f" Enabled: {tap.enabled}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
A typical STM32 output:
|
||||
|
||||
```
|
||||
TAP: stm32f1x.cpu
|
||||
Chip: stm32f1x, TAP name: cpu
|
||||
IDCODE: 0x3BA00477
|
||||
IR length: 4 bits
|
||||
Enabled: True
|
||||
```
|
||||
|
||||
## Adding a new TAP
|
||||
|
||||
`new_tap(chip, tap, ir_len, expected_id=None)` declares a new TAP on the chain. This is typically done before scan chain initialization, but can be useful when dynamically configuring multi-device chains.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
# Declare a TAP with known IDCODE
|
||||
await session.jtag.new_tap(
|
||||
chip="fpga",
|
||||
tap="bs",
|
||||
ir_len=6,
|
||||
expected_id=0x0362D093
|
||||
)
|
||||
|
||||
# Declare a TAP without IDCODE verification
|
||||
await session.jtag.new_tap(chip="cpld", tap="cpu", ir_len=8)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.jtag.new_tap(
|
||||
chip="fpga", tap="bs", ir_len=6, expected_id=0x0362D093
|
||||
)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Scan operations
|
||||
|
||||
### Instruction register scan
|
||||
|
||||
`irscan(tap, instruction)` shifts an instruction into a TAP's instruction register (IR) and returns the value shifted out.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
# Select the IDCODE instruction (commonly 0x0E on ARM DAPs)
|
||||
shifted_out = await session.jtag.irscan("stm32f1x.cpu", 0x0E)
|
||||
print(f"IR shifted out: 0x{shifted_out:X}")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
shifted_out = session.jtag.irscan("stm32f1x.cpu", 0x0E)
|
||||
print(f"IR shifted out: 0x{shifted_out:X}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Data register scan
|
||||
|
||||
`drscan(tap, bits, value)` shifts a value of the specified bit width through the data register (DR) and returns the captured output.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
# Read IDCODE: shift 32 bits through DR after selecting IDCODE via IR
|
||||
await session.jtag.irscan("stm32f1x.cpu", 0x0E)
|
||||
idcode = await session.jtag.drscan("stm32f1x.cpu", 32, 0x0)
|
||||
print(f"IDCODE: 0x{idcode:08X}")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.jtag.irscan("stm32f1x.cpu", 0x0E)
|
||||
idcode = session.jtag.drscan("stm32f1x.cpu", 32, 0x0)
|
||||
print(f"IDCODE: 0x{idcode:08X}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
### Run-Test/Idle clocking
|
||||
|
||||
`runtest(cycles)` clocks the specified number of TCK pulses while the TAP controller is in the Run-Test/Idle state. Some devices require idle clocking between operations.
|
||||
|
||||
```python
|
||||
# Clock 100 TCK cycles in Run-Test/Idle
|
||||
await session.jtag.runtest(100)
|
||||
```
|
||||
|
||||
The cycle count must be non-negative; passing a negative value raises `JTAGError`.
|
||||
|
||||
## TAP state machine control
|
||||
|
||||
`pathmove(states)` walks the TAP controller through an explicit sequence of IEEE 1149.1 states. Each state must be a legal single-step transition from the previous one. OpenOCD validates the path and reports an error for illegal transitions.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
from openocd import Session, JTAGState
|
||||
|
||||
async with await Session.connect() as session:
|
||||
await session.jtag.pathmove([
|
||||
JTAGState.DRSELECT,
|
||||
JTAGState.DRCAPTURE,
|
||||
JTAGState.DRSHIFT,
|
||||
])
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from openocd import Session, JTAGState
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
session.jtag.pathmove([
|
||||
JTAGState.DRSELECT,
|
||||
JTAGState.DRCAPTURE,
|
||||
JTAGState.DRSHIFT,
|
||||
])
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The list must contain at least one state. An empty list raises `JTAGError`.
|
||||
|
||||
### JTAGState enum
|
||||
|
||||
The `JTAGState` enum defines all 16 IEEE 1149.1 TAP controller states:
|
||||
|
||||
| State | Description |
|
||||
|-------|-------------|
|
||||
| `RESET` | Test-Logic-Reset |
|
||||
| `IDLE` | Run-Test/Idle |
|
||||
| `DRSELECT` | Select-DR-Scan |
|
||||
| `DRCAPTURE` | Capture-DR |
|
||||
| `DRSHIFT` | Shift-DR |
|
||||
| `DREXIT1` | Exit1-DR |
|
||||
| `DRPAUSE` | Pause-DR |
|
||||
| `DREXIT2` | Exit2-DR |
|
||||
| `DRUPDATE` | Update-DR |
|
||||
| `IRSELECT` | Select-IR-Scan |
|
||||
| `IRCAPTURE` | Capture-IR |
|
||||
| `IRSHIFT` | Shift-IR |
|
||||
| `IREXIT1` | Exit1-IR |
|
||||
| `IRPAUSE` | Pause-IR |
|
||||
| `IREXIT2` | Exit2-IR |
|
||||
| `IRUPDATE` | Update-IR |
|
||||
|
||||
`JTAGState` is a `str` enum, so `JTAGState.IDLE.value` returns the string `"IDLE"`.
|
||||
|
||||
## Boundary scan files
|
||||
|
||||
### SVF execution
|
||||
|
||||
`svf(path, tap=None, quiet=False, progress=True)` executes a Serial Vector Format file. SVF files describe JTAG test vectors and are commonly used for FPGA configuration, board-level test, and CPLD programming.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
async with await Session.connect() as session:
|
||||
await session.jtag.svf(
|
||||
path=Path("board_test.svf"),
|
||||
tap="fpga.bs",
|
||||
quiet=False,
|
||||
progress=True,
|
||||
)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
session.jtag.svf(
|
||||
path=Path("board_test.svf"),
|
||||
tap="fpga.bs",
|
||||
quiet=False,
|
||||
progress=True,
|
||||
)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Parameters:
|
||||
- `path` -- path to the `.svf` file
|
||||
- `tap` -- restrict operations to a specific TAP (optional; when `None`, OpenOCD applies vectors to the appropriate TAP)
|
||||
- `quiet` -- suppress per-statement logging inside OpenOCD
|
||||
- `progress` -- show a progress indicator (default `True`)
|
||||
|
||||
### XSVF execution
|
||||
|
||||
`xsvf(tap, path)` executes a Xilinx-extended SVF file against a specific TAP.
|
||||
|
||||
```python
|
||||
from pathlib import Path
|
||||
|
||||
await session.jtag.xsvf("cpld.bs", Path("config.xsvf"))
|
||||
```
|
||||
|
||||
## Data types
|
||||
|
||||
### TAPInfo
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | `str` | Full TAP name (e.g. `stm32f1x.cpu`) |
|
||||
| `chip` | `str` | Chip portion of the name |
|
||||
| `tap_name` | `str` | TAP portion of the name |
|
||||
| `idcode` | `int` | Detected IDCODE |
|
||||
| `ir_length` | `int` | Instruction register length in bits |
|
||||
| `enabled` | `bool` | Whether the TAP is enabled |
|
||||
|
||||
## Error handling
|
||||
|
||||
All JTAG operations raise `JTAGError` on failure.
|
||||
|
||||
```python
|
||||
from openocd import JTAGError
|
||||
|
||||
try:
|
||||
await session.jtag.irscan("nonexistent.tap", 0x0E)
|
||||
except JTAGError as e:
|
||||
print(f"JTAG error: {e}")
|
||||
```
|
||||
|
||||
## Method reference
|
||||
|
||||
| Method | Return Type | Description |
|
||||
|--------|-------------|-------------|
|
||||
| `scan_chain()` | `list[TAPInfo]` | Enumerate TAPs on the chain |
|
||||
| `new_tap(chip, tap, ir_len, expected_id=None)` | `None` | Declare a new TAP |
|
||||
| `irscan(tap, instruction)` | `int` | Shift instruction into IR |
|
||||
| `drscan(tap, bits, value)` | `int` | Shift data through DR |
|
||||
| `runtest(cycles)` | `None` | Clock TCK in Run-Test/Idle |
|
||||
| `pathmove(states)` | `None` | Walk TAP through state sequence |
|
||||
| `svf(path, tap=None, quiet=False, progress=True)` | `None` | Execute SVF file |
|
||||
| `xsvf(tap, path)` | `None` | Execute XSVF file |
|
||||
310
src/content/docs/guides/memory-operations.mdx
Normal file
310
src/content/docs/guides/memory-operations.mdx
Normal file
@ -0,0 +1,310 @@
|
||||
---
|
||||
title: Memory Operations
|
||||
description: Read and write target memory at various widths
|
||||
---
|
||||
|
||||
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
|
||||
326
src/content/docs/guides/register-access.mdx
Normal file
326
src/content/docs/guides/register-access.mdx
Normal file
@ -0,0 +1,326 @@
|
||||
---
|
||||
title: Register Access
|
||||
description: Read and write CPU registers by name or number
|
||||
---
|
||||
|
||||
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
|
||||
237
src/content/docs/guides/rtt-communication.mdx
Normal file
237
src/content/docs/guides/rtt-communication.mdx
Normal file
@ -0,0 +1,237 @@
|
||||
---
|
||||
title: RTT Communication
|
||||
description: High-speed bidirectional data transfer between host and target using SEGGER Real-Time Transfer.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The `RTTManager` subsystem provides access to SEGGER Real-Time Transfer (RTT), a protocol for high-speed bidirectional communication between a debug host and an embedded target. RTT uses a shared control block in target RAM rather than dedicated hardware, making it significantly faster than semihosting while requiring no additional pins.
|
||||
|
||||
Access it through the session:
|
||||
|
||||
```python
|
||||
session.rtt
|
||||
```
|
||||
|
||||
## How RTT works
|
||||
|
||||
RTT places a small control block in the target's RAM that contains ring buffers for communication. The control block starts with a known identifier string (typically `"SEGGER RTT"`) so the debugger can locate it by scanning a RAM region.
|
||||
|
||||
Communication flows through numbered **channels**:
|
||||
- **Up-channels** (target to host): the firmware writes data that the host reads
|
||||
- **Down-channels** (host to target): the host writes data that the firmware reads
|
||||
|
||||
Channel 0 is conventionally used as a terminal for `printf`-style logging.
|
||||
|
||||
<Aside type="note">
|
||||
RTT requires firmware-side support. Your embedded application must include the SEGGER RTT library (or a compatible implementation) and initialize the control block. The RTT library is freely available from SEGGER and is included in many SDKs (Zephyr, nRF Connect SDK, etc.).
|
||||
</Aside>
|
||||
|
||||
## Setup and lifecycle
|
||||
|
||||
The typical RTT flow is: **setup** (configure search parameters), then **start** (find the control block and activate channels), then **read/write**, then **stop**.
|
||||
|
||||
### Configuring the search region
|
||||
|
||||
`setup(address, size, id_string="SEGGER RTT")` tells OpenOCD where to look for the RTT control block in target RAM.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
import asyncio
|
||||
from openocd import Session
|
||||
|
||||
async def main():
|
||||
async with await Session.connect() as session:
|
||||
# Search for the control block in the first 8KB of SRAM
|
||||
await session.rtt.setup(
|
||||
address=0x2000_0000,
|
||||
size=0x2000,
|
||||
id_string="SEGGER RTT"
|
||||
)
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from openocd import Session
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
# Use the raw command interface for sync RTT access
|
||||
session.command('rtt setup 0x20000000 0x2000 "SEGGER RTT"')
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Parameters:
|
||||
- `address` -- start address of the RAM region to search
|
||||
- `size` -- size of the search region in bytes
|
||||
- `id_string` -- RTT control block identifier (default `"SEGGER RTT"`)
|
||||
|
||||
<Aside type="tip">
|
||||
If you know the exact address of the RTT control block from your linker map file, set `size` to a small value like `0x100` to speed up the search. Look for the `_SEGGER_RTT` symbol in your `.map` file.
|
||||
</Aside>
|
||||
|
||||
### Starting RTT
|
||||
|
||||
`start()` scans the configured region for the control block and activates all discovered channels.
|
||||
|
||||
```python
|
||||
await session.rtt.start()
|
||||
```
|
||||
|
||||
Raises `OpenOCDError` if the control block is not found. Make sure the target firmware is running and has initialized RTT before calling `start()`.
|
||||
|
||||
### Stopping RTT
|
||||
|
||||
`stop()` deactivates RTT communication.
|
||||
|
||||
```python
|
||||
await session.rtt.stop()
|
||||
```
|
||||
|
||||
## Discovering channels
|
||||
|
||||
`channels()` returns a list of `RTTChannel` descriptors after `start()` has been called.
|
||||
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
await session.rtt.setup(address=0x2000_0000, size=0x2000)
|
||||
await session.rtt.start()
|
||||
|
||||
channels = await session.rtt.channels()
|
||||
for ch in channels:
|
||||
direction = "target->host" if ch.direction == "up" else "host->target"
|
||||
print(f"Channel {ch.index}: {ch.name} "
|
||||
f"(size={ch.size}, {direction})")
|
||||
```
|
||||
|
||||
Typical output:
|
||||
|
||||
```
|
||||
Channel 0: Terminal (size=1024, target->host)
|
||||
Channel 0: Terminal (size=16, host->target)
|
||||
```
|
||||
|
||||
## Reading data
|
||||
|
||||
`read(channel)` reads pending data from an up-channel (target to host). Returns an empty string if nothing is available.
|
||||
|
||||
```python
|
||||
data = await session.rtt.read(0)
|
||||
if data:
|
||||
print(f"Received: {data}")
|
||||
```
|
||||
|
||||
For continuous monitoring, poll in a loop:
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def rtt_monitor(session):
|
||||
await session.rtt.setup(address=0x2000_0000, size=0x2000)
|
||||
await session.rtt.start()
|
||||
|
||||
try:
|
||||
while True:
|
||||
data = await session.rtt.read(0)
|
||||
if data:
|
||||
print(data, end="", flush=True)
|
||||
await asyncio.sleep(0.05) # 50ms poll interval
|
||||
finally:
|
||||
await session.rtt.stop()
|
||||
```
|
||||
|
||||
## Writing data
|
||||
|
||||
`write(channel, data)` sends a string to a down-channel (host to target).
|
||||
|
||||
```python
|
||||
await session.rtt.write(0, "help\n")
|
||||
|
||||
# Wait for the target to process and respond
|
||||
await asyncio.sleep(0.1)
|
||||
response = await session.rtt.read(0)
|
||||
print(response)
|
||||
```
|
||||
|
||||
<Aside type="note">
|
||||
The `write()` method automatically escapes TCL special characters (`\`, `"`, `[`, `$`) to prevent injection through the TCL RPC layer. You can pass arbitrary string content safely.
|
||||
</Aside>
|
||||
|
||||
## Complete example
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from openocd import Session
|
||||
|
||||
async def main():
|
||||
async with await Session.connect() as session:
|
||||
# Make sure the target is running (RTT needs active firmware)
|
||||
await session.target.reset(mode="run")
|
||||
await asyncio.sleep(0.5) # Let firmware initialize
|
||||
|
||||
# Configure and start RTT
|
||||
await session.rtt.setup(
|
||||
address=0x2000_0000,
|
||||
size=0x4000,
|
||||
id_string="SEGGER RTT"
|
||||
)
|
||||
await session.rtt.start()
|
||||
|
||||
# List available channels
|
||||
channels = await session.rtt.channels()
|
||||
print(f"Found {len(channels)} RTT channels")
|
||||
|
||||
# Read for a few seconds
|
||||
for _ in range(20):
|
||||
data = await session.rtt.read(0)
|
||||
if data:
|
||||
print(f"[RTT] {data}", end="")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
await session.rtt.stop()
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
|
||||
## Data types
|
||||
|
||||
### RTTChannel
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `index` | `int` | Channel number |
|
||||
| `name` | `str` | Channel name (e.g. `Terminal`) |
|
||||
| `size` | `int` | Buffer size in bytes |
|
||||
| `direction` | `Literal["up", "down"]` | `up` = target-to-host, `down` = host-to-target |
|
||||
|
||||
## Error handling
|
||||
|
||||
RTT operations raise `OpenOCDError` on failure (there is no dedicated RTT exception type).
|
||||
|
||||
```python
|
||||
from openocd import OpenOCDError
|
||||
|
||||
try:
|
||||
await session.rtt.start()
|
||||
except OpenOCDError as e:
|
||||
print(f"RTT failed: {e}")
|
||||
```
|
||||
|
||||
Common failure causes:
|
||||
- Control block not found (firmware not running, wrong search address or size)
|
||||
- RTT not set up before calling `start()`
|
||||
- Channel index out of range
|
||||
|
||||
## Method reference
|
||||
|
||||
| Method | Return Type | Description |
|
||||
|--------|-------------|-------------|
|
||||
| `setup(address, size, id_string="SEGGER RTT")` | `None` | Configure control block search |
|
||||
| `start()` | `None` | Find control block, activate channels |
|
||||
| `stop()` | `None` | Deactivate RTT |
|
||||
| `channels()` | `list[RTTChannel]` | List discovered channels |
|
||||
| `read(channel)` | `str` | Read from an up-channel |
|
||||
| `write(channel, data)` | `None` | Write to a down-channel |
|
||||
283
src/content/docs/guides/session-lifecycle.mdx
Normal file
283
src/content/docs/guides/session-lifecycle.mdx
Normal file
@ -0,0 +1,283 @@
|
||||
---
|
||||
title: Session Lifecycle
|
||||
description: Understanding session creation, connection, and teardown
|
||||
---
|
||||
|
||||
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
|
||||
258
src/content/docs/guides/svd-decoding.mdx
Normal file
258
src/content/docs/guides/svd-decoding.mdx
Normal file
@ -0,0 +1,258 @@
|
||||
---
|
||||
title: SVD Register Decoding
|
||||
description: Load CMSIS-SVD files and decode hardware register values into named bitfields.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The `SVDManager` subsystem integrates CMSIS-SVD metadata with live hardware reads. Given an SVD file describing a microcontroller's peripheral registers, it can read a register from the target and decode its raw value into named bitfields -- turning opaque hex values into human-readable output.
|
||||
|
||||
Access it through the session:
|
||||
|
||||
```python
|
||||
# Async
|
||||
session.svd
|
||||
|
||||
# Sync
|
||||
sync_session.svd
|
||||
```
|
||||
|
||||
<Aside type="note">
|
||||
SVD (System View Description) files are XML documents that describe the memory-mapped registers of ARM Cortex-M microcontrollers. They are published by silicon vendors and define every peripheral, register, and bitfield. You can find SVD files in your vendor's CMSIS pack or on GitHub repositories like [cmsis-svd/cmsis-svd-data](https://github.com/cmsis-svd/cmsis-svd-data).
|
||||
</Aside>
|
||||
|
||||
## Prerequisites
|
||||
|
||||
The SVD subsystem requires the `cmsis-svd` package, which is included in the default dependencies:
|
||||
|
||||
```bash
|
||||
uv add openocd-python
|
||||
```
|
||||
|
||||
## Loading an SVD file
|
||||
|
||||
`load(svd_path)` parses an SVD XML file and indexes its peripherals and registers. The file parse runs in a background thread (via `asyncio.to_thread`) to avoid blocking the event loop on large SVD files.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
from openocd import Session
|
||||
|
||||
async def main():
|
||||
async with await Session.connect() as session:
|
||||
await session.svd.load(Path("STM32F103.svd"))
|
||||
print(f"SVD loaded: {session.svd.loaded}")
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from pathlib import Path
|
||||
from openocd import Session
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
session.svd.load(Path("STM32F103.svd"))
|
||||
print(f"SVD loaded: {session.svd.loaded}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Raises `SVDError` if the file is not found or cannot be parsed.
|
||||
|
||||
## Browsing peripherals and registers
|
||||
|
||||
After loading, enumerate what is available. Both `list_peripherals()` and `list_registers()` are synchronous methods (no `await` needed) since they operate on in-memory data.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
await session.svd.load(Path("STM32F103.svd"))
|
||||
|
||||
# List all peripherals
|
||||
peripherals = session.svd.list_peripherals()
|
||||
print(f"Found {len(peripherals)} peripherals:")
|
||||
for name in peripherals[:10]:
|
||||
print(f" {name}")
|
||||
|
||||
# List registers in a specific peripheral
|
||||
regs = session.svd.list_registers("GPIOA")
|
||||
print(f"\nGPIOA registers: {', '.join(regs)}")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.svd.load(Path("STM32F103.svd"))
|
||||
|
||||
peripherals = session.svd.list_peripherals()
|
||||
print(f"Found {len(peripherals)} peripherals:")
|
||||
for name in peripherals[:10]:
|
||||
print(f" {name}")
|
||||
|
||||
regs = session.svd.list_registers("GPIOA")
|
||||
print(f"\nGPIOA registers: {', '.join(regs)}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
## Reading and decoding a register
|
||||
|
||||
`read_register(peripheral, register)` is the primary method. It computes the register's memory-mapped address from the SVD metadata, reads 32 bits from that address on the target, and decodes the raw value into named bitfields.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
await session.svd.load(Path("STM32F103.svd"))
|
||||
await session.target.halt()
|
||||
|
||||
decoded = await session.svd.read_register("GPIOA", "ODR")
|
||||
print(decoded)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.svd.load(Path("STM32F103.svd"))
|
||||
session.target.halt()
|
||||
|
||||
decoded = session.svd.read_register("GPIOA", "ODR")
|
||||
print(decoded)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
The `DecodedRegister.__str__` method formats the output:
|
||||
|
||||
```
|
||||
GPIOA.ODR @ 0x4001080C = 0x00000001
|
||||
[ 0:0] ODR0 = 0x1 Port output data bit 0
|
||||
[ 1:1] ODR1 = 0x0 Port output data bit 1
|
||||
[ 2:2] ODR2 = 0x0 Port output data bit 2
|
||||
...
|
||||
```
|
||||
|
||||
Each line shows the bit range, field name, extracted value, and the description from the SVD metadata.
|
||||
|
||||
## Reading an entire peripheral
|
||||
|
||||
`read_peripheral(peripheral)` reads and decodes every register in a peripheral, returning a dict keyed by register name.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
await session.svd.load(Path("STM32F103.svd"))
|
||||
await session.target.halt()
|
||||
|
||||
all_regs = await session.svd.read_peripheral("GPIOA")
|
||||
for name, decoded in all_regs.items():
|
||||
print(f"\n{decoded}")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.svd.load(Path("STM32F103.svd"))
|
||||
session.target.halt()
|
||||
|
||||
all_regs = session.svd.read_peripheral("GPIOA")
|
||||
for name, decoded in all_regs.items():
|
||||
print(f"\n{decoded}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Aside type="tip">
|
||||
`read_peripheral()` silently skips registers that fail to read (e.g. write-only registers, reserved addresses) and logs a warning. The returned dict only contains successfully read registers.
|
||||
</Aside>
|
||||
|
||||
## Decoding without hardware
|
||||
|
||||
`decode(peripheral, register, value)` decodes a raw integer value using SVD metadata without performing any hardware read. Useful when you already have the value from a log file, a previous read, or a known reset value.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
await session.svd.load(Path("STM32F103.svd"))
|
||||
|
||||
# Decode a known value -- no target read needed
|
||||
decoded = session.svd.decode("RCC", "CR", 0x0300_0083)
|
||||
print(decoded)
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
session.svd.load(Path("STM32F103.svd"))
|
||||
|
||||
decoded = session.svd.decode("RCC", "CR", 0x0300_0083)
|
||||
print(decoded)
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
`decode()` is synchronous in both APIs -- it operates purely on in-memory data.
|
||||
|
||||
## Data types
|
||||
|
||||
### DecodedRegister
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `peripheral` | `str` | Peripheral name (e.g. `GPIOA`) |
|
||||
| `register` | `str` | Register name (e.g. `ODR`) |
|
||||
| `address` | `int` | Memory-mapped address |
|
||||
| `raw_value` | `int` | Raw 32-bit value read from hardware |
|
||||
| `fields` | `list[BitField]` | Decoded bitfields, sorted by bit offset |
|
||||
|
||||
### BitField
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | `str` | Field name from the SVD (e.g. `ODR0`) |
|
||||
| `offset` | `int` | Bit offset within the register |
|
||||
| `width` | `int` | Field width in bits |
|
||||
| `value` | `int` | Extracted field value |
|
||||
| `description` | `str` | Description from the SVD metadata |
|
||||
|
||||
## Error handling
|
||||
|
||||
```python
|
||||
from openocd import SVDError
|
||||
|
||||
try:
|
||||
session.svd.list_peripherals()
|
||||
except SVDError as e:
|
||||
print(f"No SVD loaded: {e}")
|
||||
|
||||
try:
|
||||
decoded = await session.svd.read_register("NONEXISTENT", "REG")
|
||||
except SVDError as e:
|
||||
print(f"Lookup failed: {e}")
|
||||
```
|
||||
|
||||
Common `SVDError` cases:
|
||||
- No SVD file loaded (call `load()` first)
|
||||
- Peripheral name not found in the SVD
|
||||
- Register name not found within the peripheral
|
||||
- SVD file does not exist or is malformed
|
||||
|
||||
Hardware read failures from `read_register` and `read_peripheral` raise `TargetError`, not `SVDError`.
|
||||
|
||||
## Method reference
|
||||
|
||||
| Method | Return Type | Description |
|
||||
|--------|-------------|-------------|
|
||||
| `load(svd_path)` | `None` | Parse an SVD XML file |
|
||||
| `loaded` (property) | `bool` | Whether an SVD is loaded |
|
||||
| `list_peripherals()` | `list[str]` | Sorted peripheral names |
|
||||
| `list_registers(peripheral)` | `list[str]` | Sorted register names for a peripheral |
|
||||
| `read_register(peripheral, register)` | `DecodedRegister` | Read from hardware and decode |
|
||||
| `read_peripheral(peripheral)` | `dict[str, DecodedRegister]` | Read all registers in a peripheral |
|
||||
| `decode(peripheral, register, value)` | `DecodedRegister` | Decode without hardware read |
|
||||
298
src/content/docs/guides/target-control.mdx
Normal file
298
src/content/docs/guides/target-control.mdx
Normal file
@ -0,0 +1,298 @@
|
||||
---
|
||||
title: Target Control
|
||||
description: Halt, resume, reset, and step through targets
|
||||
---
|
||||
|
||||
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
|
||||
155
src/content/docs/guides/transport-adapter.mdx
Normal file
155
src/content/docs/guides/transport-adapter.mdx
Normal file
@ -0,0 +1,155 @@
|
||||
---
|
||||
title: Transport and Adapter
|
||||
description: Query the debug transport, identify the adapter interface, and control the clock speed.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The `Transport` subsystem provides access to the debug transport layer -- the physical protocol used to communicate between the debug adapter and the target. OpenOCD supports several transports including JTAG, SWD, and SWIM, used with adapter hardware like CMSIS-DAP, ST-Link, and J-Link probes.
|
||||
|
||||
Access it through the session:
|
||||
|
||||
```python
|
||||
session.transport
|
||||
```
|
||||
|
||||
## Querying the active transport
|
||||
|
||||
`select()` returns the name of the currently active transport as a string.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
import asyncio
|
||||
from openocd import Session
|
||||
|
||||
async def main():
|
||||
async with await Session.connect() as session:
|
||||
transport = await session.transport.select()
|
||||
print(f"Active transport: {transport}")
|
||||
# Typical output: "swd" or "jtag"
|
||||
|
||||
asyncio.run(main())
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
from openocd import Session
|
||||
|
||||
with Session.connect_sync() as session:
|
||||
transport = session.command("transport select").strip()
|
||||
print(f"Active transport: {transport}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Common return values:
|
||||
|
||||
| Value | Protocol |
|
||||
|-------|----------|
|
||||
| `"jtag"` | IEEE 1149.1 JTAG |
|
||||
| `"swd"` | ARM Serial Wire Debug |
|
||||
| `"swim"` | STM8 Single Wire Interface Module |
|
||||
|
||||
## Listing available transports
|
||||
|
||||
`list()` returns all transports supported by the current adapter configuration.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
available = await session.transport.list()
|
||||
print(f"Available transports: {', '.join(available)}")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
response = session.command("transport list").strip()
|
||||
print(f"Available transports: {response}")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
A CMSIS-DAP adapter typically supports both `["jtag", "swd"]`, while an ST-Link V2 may report `["hla_swd"]`.
|
||||
|
||||
## Identifying the adapter
|
||||
|
||||
`adapter_info()` returns a description of the connected debug adapter. It tries the `adapter name` command first (OpenOCD 0.12+) and falls back to `adapter info` for older versions.
|
||||
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
info = await session.transport.adapter_info()
|
||||
print(f"Adapter: {info}")
|
||||
# Example: "cmsis-dap" or "st-link"
|
||||
```
|
||||
|
||||
## Adapter clock speed
|
||||
|
||||
`adapter_speed(khz=None)` gets or sets the debug adapter clock frequency in kHz. When called without arguments it returns the current speed. When called with a value it sets the speed and returns the new value.
|
||||
|
||||
<Tabs>
|
||||
<TabItem label="Async">
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
# Query current speed
|
||||
current = await session.transport.adapter_speed()
|
||||
print(f"Current speed: {current} kHz")
|
||||
|
||||
# Set to 4 MHz
|
||||
new_speed = await session.transport.adapter_speed(4000)
|
||||
print(f"Speed set to: {new_speed} kHz")
|
||||
```
|
||||
</TabItem>
|
||||
<TabItem label="Sync">
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
current = session.command("adapter speed").strip()
|
||||
print(f"Current speed: {current}")
|
||||
|
||||
session.command("adapter speed 4000")
|
||||
print("Speed set to 4000 kHz")
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
<Aside type="tip">
|
||||
Start with a lower clock speed (1000 kHz or less) when bringing up a new board, then increase once communication is stable. Maximum speed depends on the adapter, target, and board layout. CMSIS-DAP adapters typically support up to 10 MHz on SWD.
|
||||
</Aside>
|
||||
|
||||
<Aside type="caution">
|
||||
Changing the adapter speed while the target is actively being debugged can cause communication errors. Set the speed before performing debug operations, ideally during initial configuration.
|
||||
</Aside>
|
||||
|
||||
## Common adapter interfaces
|
||||
|
||||
| Adapter | Typical Transports | Notes |
|
||||
|---------|--------------------|-------|
|
||||
| CMSIS-DAP | JTAG, SWD | Open standard, wide device support |
|
||||
| ST-Link | SWD (HLA) | ST Microelectronics probes |
|
||||
| J-Link | JTAG, SWD | SEGGER probes, high performance |
|
||||
| FTDI | JTAG, SWD | FT2232-based adapters |
|
||||
| Raspberry Pi GPIO | JTAG, SWD | Direct bitbang via GPIO pins |
|
||||
|
||||
## Error handling
|
||||
|
||||
Transport and adapter operations raise `OpenOCDError` on failure.
|
||||
|
||||
```python
|
||||
from openocd import OpenOCDError
|
||||
|
||||
try:
|
||||
speed = await session.transport.adapter_speed(99999)
|
||||
except OpenOCDError as e:
|
||||
print(f"Speed setting failed: {e}")
|
||||
```
|
||||
|
||||
## Method reference
|
||||
|
||||
| Method | Return Type | Description |
|
||||
|--------|-------------|-------------|
|
||||
| `select()` | `str` | Get the active transport name |
|
||||
| `list()` | `list[str]` | List available transports |
|
||||
| `adapter_info()` | `str` | Get adapter description string |
|
||||
| `adapter_speed(khz=None)` | `int` | Get or set adapter speed in kHz |
|
||||
32
src/content/docs/index.mdx
Normal file
32
src/content/docs/index.mdx
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
title: openocd-python
|
||||
description: Typed async-first Python bindings for OpenOCD
|
||||
template: splash
|
||||
hero:
|
||||
tagline: Typed async-first Python bindings for OpenOCD — control targets, read memory, program flash, and decode SVD registers from Python.
|
||||
actions:
|
||||
- text: Get Started
|
||||
link: /getting-started/installation/
|
||||
icon: right-arrow
|
||||
- text: View on GitHub
|
||||
link: https://git.supported.systems/warehack.ing/openocd-python
|
||||
icon: external
|
||||
variant: minimal
|
||||
---
|
||||
|
||||
import { Card, CardGrid } from '@astrojs/starlight/components';
|
||||
|
||||
<CardGrid stagger>
|
||||
<Card title="Async-First Design" icon="rocket">
|
||||
Full async/await API with sync wrappers. Use `Session.connect()` for async or `Session.connect_sync()` for synchronous code.
|
||||
</Card>
|
||||
<Card title="9 Subsystems" icon="puzzle">
|
||||
Target control, memory, registers, flash, breakpoints, JTAG, SVD, RTT, and transport — all from one session.
|
||||
</Card>
|
||||
<Card title="Type-Safe" icon="approve-check">
|
||||
Frozen dataclasses for all return types. Explicit exception hierarchy. Full type annotations throughout.
|
||||
</Card>
|
||||
<Card title="Safety-Critical Ready" icon="setting">
|
||||
Defensive parsing, explicit error handling, and thorough test coverage. Used in avionics, medical, and automotive contexts.
|
||||
</Card>
|
||||
</CardGrid>
|
||||
196
src/content/docs/reference/connection-layer.mdx
Normal file
196
src/content/docs/reference/connection-layer.mdx
Normal file
@ -0,0 +1,196 @@
|
||||
---
|
||||
title: Connection Layer
|
||||
description: Internals of TclRpcConnection, TelnetConnection, and OpenOCDProcess.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The connection layer provides the transport between `openocd-python` and the OpenOCD process. Most users never interact with these classes directly -- the `Session` facade handles everything. This reference is for contributors, advanced users, and anyone debugging connection issues.
|
||||
|
||||
## Connection ABC
|
||||
|
||||
All connection backends implement the abstract base class in `openocd.connection.base`:
|
||||
|
||||
```python
|
||||
class Connection(ABC):
|
||||
async def connect(self, host: str, port: int) -> None: ...
|
||||
async def send(self, command: str) -> str: ...
|
||||
async def close(self) -> None: ...
|
||||
async def enable_notifications(self) -> None: ...
|
||||
def on_notification(self, callback: Callable[[str], None]) -> None: ...
|
||||
```
|
||||
|
||||
| Method | Description |
|
||||
|--------|-------------|
|
||||
| `connect(host, port)` | Open a TCP connection to the given host and port |
|
||||
| `send(command)` | Send a command string and return the response |
|
||||
| `close()` | Close the connection and release resources |
|
||||
| `enable_notifications()` | Enable asynchronous event notifications from OpenOCD |
|
||||
| `on_notification(callback)` | Register a callback for incoming notification messages |
|
||||
|
||||
## TclRpcConnection
|
||||
|
||||
**Module:** `openocd.connection.tcl_rpc`
|
||||
|
||||
The primary connection backend. Speaks OpenOCD's TCL RPC binary protocol on port 6666.
|
||||
|
||||
### Protocol
|
||||
|
||||
The TCL RPC protocol uses a simple framing scheme:
|
||||
|
||||
- **Client sends:** `command_bytes` + `\x1a`
|
||||
- **Server replies:** `response_bytes` + `\x1a`
|
||||
|
||||
The `\x1a` byte (ASCII SUB / Ctrl-Z) acts as an unambiguous message delimiter. Commands and responses are UTF-8 encoded strings.
|
||||
|
||||
### Constructor
|
||||
|
||||
```python
|
||||
TclRpcConnection(timeout: float = 10.0)
|
||||
```
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `timeout` | `float` | `10.0` | Timeout in seconds for connect and send operations |
|
||||
|
||||
### Dual-socket design
|
||||
|
||||
`TclRpcConnection` maintains **two separate TCP connections** to OpenOCD:
|
||||
|
||||
1. **Command socket** -- handles request/response pairs. An async lock serializes all commands to prevent interleaving.
|
||||
2. **Notification socket** -- opened by `enable_notifications()`. Sends `tcl_notifications on` and then exclusively reads unsolicited event messages.
|
||||
|
||||
This separation prevents notifications from corrupting the command response stream, which would happen if both shared a single socket with two concurrent readers.
|
||||
|
||||
### Command flow
|
||||
|
||||
1. Acquire the async lock
|
||||
2. Write `command.encode("utf-8") + b"\x1a"` to the command socket
|
||||
3. Read from the socket until `\x1a` is found in the response stream
|
||||
4. Any bytes after the separator are preserved in a remainder buffer for the next call
|
||||
5. Release the lock
|
||||
6. Return the response decoded as UTF-8
|
||||
|
||||
### Constants
|
||||
|
||||
| Constant | Value | Description |
|
||||
|----------|-------|-------------|
|
||||
| `SEPARATOR` | `b"\x1a"` | Message delimiter byte |
|
||||
| `DEFAULT_TIMEOUT` | `10.0` | Default timeout in seconds |
|
||||
| `MAX_RESPONSE_SIZE` | `10 * 1024 * 1024` | 10 MB guard against runaway reads |
|
||||
|
||||
### Notification loop
|
||||
|
||||
When `enable_notifications()` is called:
|
||||
|
||||
1. A second TCP connection opens to the same host:port
|
||||
2. `tcl_notifications on\x1a` is sent and the acknowledgement consumed
|
||||
3. A background `asyncio.Task` enters `_notification_loop()`, which reads messages delimited by `\x1a`
|
||||
4. Each message is dispatched to all registered callbacks
|
||||
5. If the notification connection drops, `_notification_failed` is set to `True` and subsequent `send()` calls log a warning
|
||||
|
||||
### Error conditions
|
||||
|
||||
- **Connection closed:** If `send()` reads zero bytes, `ConnectionError` is raised
|
||||
- **Response too large:** If the response buffer exceeds `MAX_RESPONSE_SIZE` without a separator, `ConnectionError` is raised (likely connected to the wrong port)
|
||||
- **Timeout:** If the response does not arrive within the configured timeout, `TimeoutError` is raised
|
||||
|
||||
## TelnetConnection
|
||||
|
||||
**Module:** `openocd.connection.telnet`
|
||||
|
||||
A fallback connection backend that speaks to OpenOCD's human-oriented telnet interface on port 4444.
|
||||
|
||||
### Protocol
|
||||
|
||||
- **Client sends:** `command\n`
|
||||
- **Server replies:** response text ending with `"> "` prompt
|
||||
|
||||
The telnet connection:
|
||||
- Reads until the `"> "` prompt after each command
|
||||
- Strips the echoed command from the first line of the response
|
||||
- Does **not** support notifications (`enable_notifications()` logs a warning and does nothing)
|
||||
|
||||
### Constructor
|
||||
|
||||
```python
|
||||
TelnetConnection(timeout: float = 10.0)
|
||||
```
|
||||
|
||||
### Limitations
|
||||
|
||||
| Feature | TclRpcConnection | TelnetConnection |
|
||||
|---------|------------------|------------------|
|
||||
| Default port | 6666 | 4444 |
|
||||
| Binary framing | `\x1a` delimiter | `"> "` prompt |
|
||||
| Notifications | Supported (dual-socket) | Not supported |
|
||||
| Output consistency | Structured | Varies by version |
|
||||
| Recommended | Yes | Fallback only |
|
||||
|
||||
<Aside type="caution">
|
||||
The telnet interface is designed for interactive human use. Its output formatting varies between OpenOCD versions, making regex parsing fragile. Use `TclRpcConnection` whenever possible.
|
||||
</Aside>
|
||||
|
||||
## OpenOCDProcess
|
||||
|
||||
**Module:** `openocd.process`
|
||||
|
||||
Spawns and manages an OpenOCD subprocess. Used internally by `Session.start()`.
|
||||
|
||||
### Constructor
|
||||
|
||||
```python
|
||||
OpenOCDProcess()
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `pid` | `int \| None` | Process ID, or `None` if not started |
|
||||
| `running` | `bool` | Whether the process is still alive |
|
||||
| `tcl_port` | `int` | The TCL RPC port (default 6666) |
|
||||
|
||||
### start()
|
||||
|
||||
```python
|
||||
async def start(
|
||||
config: str | list[str],
|
||||
extra_args: list[str] | None = None,
|
||||
tcl_port: int = 6666,
|
||||
openocd_bin: str | None = None,
|
||||
) -> None
|
||||
```
|
||||
|
||||
Build the command line and spawn `openocd` as an async subprocess.
|
||||
|
||||
The `config` parameter accepts:
|
||||
- A string like `"interface/cmsis-dap.cfg -f target/stm32f1x.cfg"` (automatically split and prefixed with `-f` where needed)
|
||||
- A pre-split list like `["-f", "my board/config.cfg"]` (used as-is, preserving paths with spaces)
|
||||
|
||||
The method always appends `-c "tcl_port <port>"` to ensure the TCL RPC port matches what `Session` will connect to.
|
||||
|
||||
OpenOCD binary detection:
|
||||
1. Uses `openocd_bin` if provided
|
||||
2. Otherwise calls `shutil.which("openocd")`
|
||||
3. Raises `ProcessError` if not found
|
||||
|
||||
### wait_ready()
|
||||
|
||||
```python
|
||||
async def wait_ready(timeout: float = 10.0) -> None
|
||||
```
|
||||
|
||||
Poll the TCL RPC port every 0.25 seconds until it accepts a TCP connection, or raise `TimeoutError`. Also checks whether the process has died and reports stderr output if so.
|
||||
|
||||
### stop()
|
||||
|
||||
```python
|
||||
async def stop() -> None
|
||||
```
|
||||
|
||||
Graceful shutdown sequence:
|
||||
1. Send `SIGTERM`
|
||||
2. Wait up to 5 seconds for the process to exit
|
||||
3. If still running after 5 seconds, send `SIGKILL`
|
||||
4. Wait for final exit
|
||||
304
src/content/docs/reference/exceptions.mdx
Normal file
304
src/content/docs/reference/exceptions.mdx
Normal file
@ -0,0 +1,304 @@
|
||||
---
|
||||
title: Exceptions
|
||||
description: Complete exception hierarchy rooted at OpenOCDError with guidance on when each is raised.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
All exceptions in `openocd-python` inherit from `OpenOCDError`, allowing callers to catch broadly or narrowly as needed. Import them from the top-level package:
|
||||
|
||||
```python
|
||||
from openocd import (
|
||||
OpenOCDError,
|
||||
ConnectionError,
|
||||
TimeoutError,
|
||||
TargetError,
|
||||
TargetNotHaltedError,
|
||||
FlashError,
|
||||
JTAGError,
|
||||
SVDError,
|
||||
ProcessError,
|
||||
)
|
||||
from openocd.breakpoints import BreakpointError
|
||||
```
|
||||
|
||||
## Hierarchy
|
||||
|
||||
```
|
||||
OpenOCDError
|
||||
+-- ConnectionError
|
||||
+-- TimeoutError
|
||||
+-- TargetError
|
||||
| +-- TargetNotHaltedError
|
||||
+-- FlashError
|
||||
+-- JTAGError
|
||||
+-- SVDError
|
||||
+-- ProcessError
|
||||
+-- BreakpointError
|
||||
```
|
||||
|
||||
<Aside type="note">
|
||||
`BreakpointError` is defined in `openocd.breakpoints` rather than `openocd.errors`, but it still inherits from `OpenOCDError`. It is not re-exported from the top-level `openocd` package to avoid a naming collision, so import it directly from `openocd.breakpoints`.
|
||||
</Aside>
|
||||
|
||||
## OpenOCDError
|
||||
|
||||
```python
|
||||
class OpenOCDError(Exception)
|
||||
```
|
||||
|
||||
Base exception for all `openocd-python` errors. Catch this to handle any library error in a single clause.
|
||||
|
||||
```python
|
||||
from openocd import OpenOCDError
|
||||
|
||||
try:
|
||||
async with await Session.connect() as session:
|
||||
await session.target.halt()
|
||||
data = await session.memory.read_u32(0x0800_0000)
|
||||
except OpenOCDError as e:
|
||||
print(f"Something went wrong: {e}")
|
||||
```
|
||||
|
||||
## ConnectionError
|
||||
|
||||
```python
|
||||
class ConnectionError(OpenOCDError)
|
||||
```
|
||||
|
||||
Raised when the library cannot establish or maintain a TCP connection to OpenOCD.
|
||||
|
||||
**When raised:**
|
||||
- `Session.connect()` or `Session.start()` cannot reach the TCL RPC port
|
||||
- The OpenOCD process closes the connection mid-session
|
||||
- A response exceeds `MAX_RESPONSE_SIZE` (10 MB) without a separator -- likely connected to the wrong port
|
||||
- The notification socket fails to open
|
||||
|
||||
**What to check:**
|
||||
- Is OpenOCD running and listening on the expected port?
|
||||
- Is the host/port correct?
|
||||
- Is a firewall blocking the connection?
|
||||
|
||||
```python
|
||||
from openocd import ConnectionError
|
||||
|
||||
try:
|
||||
session = await Session.connect(host="192.168.1.100", port=6666)
|
||||
except ConnectionError as e:
|
||||
print(f"Cannot reach OpenOCD: {e}")
|
||||
```
|
||||
|
||||
## TimeoutError
|
||||
|
||||
```python
|
||||
class TimeoutError(OpenOCDError)
|
||||
```
|
||||
|
||||
Raised when an operation exceeds its deadline.
|
||||
|
||||
**When raised:**
|
||||
- Connection attempt times out
|
||||
- A command does not receive a response within the configured timeout
|
||||
- `OpenOCDProcess.wait_ready()` exceeds its timeout waiting for the TCL port to accept connections
|
||||
|
||||
```python
|
||||
from openocd import TimeoutError
|
||||
|
||||
try:
|
||||
session = await Session.connect(timeout=2.0)
|
||||
except TimeoutError:
|
||||
print("OpenOCD did not respond within 2 seconds")
|
||||
```
|
||||
|
||||
## TargetError
|
||||
|
||||
```python
|
||||
class TargetError(OpenOCDError)
|
||||
```
|
||||
|
||||
Raised when a target operation fails -- the target is not responding or returned an error.
|
||||
|
||||
**When raised:**
|
||||
- `target.halt()`, `target.resume()`, `target.step()`, or `target.reset()` fails
|
||||
- `memory.read_*()` or `memory.write_*()` encounters an error response
|
||||
- `registers.read()` or `registers.write()` fails (for reasons other than "not halted")
|
||||
|
||||
```python
|
||||
from openocd import TargetError
|
||||
|
||||
try:
|
||||
state = await session.target.halt()
|
||||
except TargetError as e:
|
||||
print(f"Target operation failed: {e}")
|
||||
```
|
||||
|
||||
### TargetNotHaltedError
|
||||
|
||||
```python
|
||||
class TargetNotHaltedError(TargetError)
|
||||
```
|
||||
|
||||
A specialization of `TargetError` for operations that require a halted target.
|
||||
|
||||
**When raised:**
|
||||
- Register reads/writes when the target is still running
|
||||
- Any operation that OpenOCD rejects with "target not halted"
|
||||
|
||||
```python
|
||||
from openocd import TargetNotHaltedError
|
||||
|
||||
try:
|
||||
regs = await session.registers.read_all()
|
||||
except TargetNotHaltedError:
|
||||
print("Halt the target before reading registers")
|
||||
await session.target.halt()
|
||||
regs = await session.registers.read_all()
|
||||
```
|
||||
|
||||
## FlashError
|
||||
|
||||
```python
|
||||
class FlashError(OpenOCDError)
|
||||
```
|
||||
|
||||
Raised when a flash memory operation fails.
|
||||
|
||||
**When raised:**
|
||||
- `flash.banks()` or `flash.info()` cannot parse OpenOCD output
|
||||
- `flash.read()`, `flash.write()`, `flash.erase_sector()`, or `flash.erase_all()` encounters an error
|
||||
- `flash.write_image()` fails to program or verify the image
|
||||
- `flash.verify()` encounters an error (note: a mismatch returns `False` rather than raising)
|
||||
- `flash.protect()` fails
|
||||
- `flash.erase_sector()` receives an invalid sector range (`first > last`)
|
||||
|
||||
```python
|
||||
from openocd import FlashError
|
||||
|
||||
try:
|
||||
await session.flash.write_image(Path("firmware.bin"))
|
||||
except FlashError as e:
|
||||
print(f"Flash programming failed: {e}")
|
||||
```
|
||||
|
||||
## JTAGError
|
||||
|
||||
```python
|
||||
class JTAGError(OpenOCDError)
|
||||
```
|
||||
|
||||
Raised when a JTAG operation fails.
|
||||
|
||||
**When raised:**
|
||||
- `jtag.scan_chain()` encounters an error
|
||||
- `jtag.irscan()`, `jtag.drscan()`, or `jtag.runtest()` fails
|
||||
- `jtag.pathmove()` receives an empty state list or an invalid state transition
|
||||
- `jtag.svf()` or `jtag.xsvf()` encounters an error during file execution
|
||||
- `jtag.new_tap()` fails
|
||||
|
||||
```python
|
||||
from openocd import JTAGError
|
||||
|
||||
try:
|
||||
taps = await session.jtag.scan_chain()
|
||||
except JTAGError as e:
|
||||
print(f"JTAG chain error: {e}")
|
||||
```
|
||||
|
||||
## SVDError
|
||||
|
||||
```python
|
||||
class SVDError(OpenOCDError)
|
||||
```
|
||||
|
||||
Raised when SVD file loading or metadata lookup fails.
|
||||
|
||||
**When raised:**
|
||||
- `svd.load()` given a path that does not exist
|
||||
- `svd.load()` encounters a parse error in the XML
|
||||
- `svd.list_peripherals()` or `svd.list_registers()` called before `load()`
|
||||
- `svd.read_register()` or `svd.decode()` references a peripheral or register not in the SVD
|
||||
|
||||
```python
|
||||
from openocd import SVDError
|
||||
|
||||
try:
|
||||
await session.svd.load(Path("nonexistent.svd"))
|
||||
except SVDError as e:
|
||||
print(f"SVD error: {e}")
|
||||
```
|
||||
|
||||
## ProcessError
|
||||
|
||||
```python
|
||||
class ProcessError(OpenOCDError)
|
||||
```
|
||||
|
||||
Raised when OpenOCD subprocess management fails.
|
||||
|
||||
**When raised:**
|
||||
- `Session.start()` cannot find the `openocd` binary
|
||||
- The OpenOCD process exits unexpectedly during startup (error message includes stderr output)
|
||||
- An empty config string is passed
|
||||
|
||||
```python
|
||||
from openocd import ProcessError
|
||||
|
||||
try:
|
||||
session = await Session.start("target/stm32f1x.cfg")
|
||||
except ProcessError as e:
|
||||
print(f"OpenOCD failed to start: {e}")
|
||||
```
|
||||
|
||||
## BreakpointError
|
||||
|
||||
```python
|
||||
class BreakpointError(OpenOCDError)
|
||||
```
|
||||
|
||||
Raised when a breakpoint or watchpoint operation fails. Defined in `openocd.breakpoints`.
|
||||
|
||||
**When raised:**
|
||||
- `breakpoints.add()` or `breakpoints.remove()` encounters an error (e.g. no HW breakpoints available)
|
||||
- `breakpoints.add_watchpoint()` or `breakpoints.remove_watchpoint()` fails
|
||||
|
||||
```python
|
||||
from openocd.breakpoints import BreakpointError
|
||||
|
||||
try:
|
||||
await session.breakpoints.add(0x0800_1234, hw=True)
|
||||
except BreakpointError as e:
|
||||
print(f"Breakpoint error: {e}")
|
||||
```
|
||||
|
||||
## Catching at different levels
|
||||
|
||||
```python
|
||||
from openocd import (
|
||||
OpenOCDError,
|
||||
ConnectionError,
|
||||
TargetError,
|
||||
TargetNotHaltedError,
|
||||
)
|
||||
|
||||
try:
|
||||
async with await Session.connect() as session:
|
||||
await session.target.halt()
|
||||
pc = await session.registers.pc()
|
||||
data = await session.memory.read_u32(pc, 4)
|
||||
|
||||
except TargetNotHaltedError:
|
||||
# Most specific: target needs to be halted
|
||||
print("Target is running -- halt it first")
|
||||
|
||||
except TargetError as e:
|
||||
# Broader: any target-related failure
|
||||
print(f"Target problem: {e}")
|
||||
|
||||
except ConnectionError as e:
|
||||
# Connection lost
|
||||
print(f"Lost connection to OpenOCD: {e}")
|
||||
|
||||
except OpenOCDError as e:
|
||||
# Catch-all for anything from this library
|
||||
print(f"OpenOCD error: {e}")
|
||||
```
|
||||
142
src/content/docs/reference/method-index.mdx
Normal file
142
src/content/docs/reference/method-index.mdx
Normal file
@ -0,0 +1,142 @@
|
||||
---
|
||||
title: Method Index
|
||||
description: Comprehensive index of every public method grouped by subsystem.
|
||||
---
|
||||
|
||||
A quick-reference index of every public method in `openocd-python`, organized by subsystem. Each entry shows the method signature, return type, and a brief description.
|
||||
|
||||
## Session
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `connect(host="localhost", port=6666, timeout=10.0)` | `Session` | Connect to a running OpenOCD instance |
|
||||
| `start(config, *, tcl_port=6666, openocd_bin=None, timeout=10.0, extra_args=None)` | `Session` | Spawn OpenOCD and connect |
|
||||
| `connect_sync(host="localhost", port=6666, **kwargs)` | `SyncSession` | Sync version of `connect()` |
|
||||
| `start_sync(config, **kwargs)` | `SyncSession` | Sync version of `start()` |
|
||||
| `close()` | `None` | Close connection and stop subprocess |
|
||||
| `command(cmd)` | `str` | Send a raw command, return response |
|
||||
| `on_halt(callback)` | `None` | Register halt notification callback |
|
||||
| `on_reset(callback)` | `None` | Register reset notification callback |
|
||||
|
||||
**Properties:** `target`, `memory`, `registers`, `flash`, `jtag`, `breakpoints`, `rtt`, `svd`, `transport`
|
||||
|
||||
## Target
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `halt()` | `TargetState` | Halt the target |
|
||||
| `resume(address=None)` | `None` | Resume execution, optionally from an address |
|
||||
| `step(address=None)` | `TargetState` | Single-step one instruction |
|
||||
| `reset(mode="halt")` | `None` | Reset the target (`"run"`, `"halt"`, or `"init"`) |
|
||||
| `wait_halt(timeout_ms=5000)` | `TargetState` | Block until halted or timeout |
|
||||
| `state()` | `TargetState` | Query current target state |
|
||||
|
||||
## Memory
|
||||
|
||||
| Method | Return | 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 in memory |
|
||||
| `dump(addr, size, path)` | `None` | Dump memory region to file |
|
||||
| `hexdump(addr, size)` | `str` | Formatted hex+ASCII dump |
|
||||
|
||||
## Registers
|
||||
|
||||
| Method | Return | 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 register |
|
||||
|
||||
## Flash
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `banks()` | `list[FlashBank]` | List all configured flash banks |
|
||||
| `info(bank=0)` | `FlashBank` | Detailed bank info with sectors |
|
||||
| `read(bank, offset, size)` | `bytes` | Read raw flash content |
|
||||
| `read_to_file(bank, path)` | `None` | Dump entire bank to file |
|
||||
| `write(bank, offset, data)` | `None` | Write raw bytes to flash |
|
||||
| `write_image(path, erase=True, verify=True)` | `None` | High-level flash programming |
|
||||
| `erase_sector(bank, first, last)` | `None` | Erase a sector range |
|
||||
| `erase_all(bank=0)` | `None` | Erase entire bank |
|
||||
| `protect(bank, first, last, on)` | `None` | Set/clear write protection |
|
||||
| `verify(bank, path)` | `bool` | Verify flash against a file |
|
||||
|
||||
## BreakpointManager
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `add(address, length=2, hw=False)` | `None` | Set a breakpoint |
|
||||
| `remove(address)` | `None` | Remove a breakpoint |
|
||||
| `list()` | `list[Breakpoint]` | List active breakpoints |
|
||||
| `add_watchpoint(address, length, access="rw")` | `None` | Set a data watchpoint |
|
||||
| `remove_watchpoint(address)` | `None` | Remove a watchpoint |
|
||||
| `list_watchpoints()` | `list[Watchpoint]` | List active watchpoints |
|
||||
|
||||
## JTAGController
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `scan_chain()` | `list[TAPInfo]` | Enumerate TAPs on the chain |
|
||||
| `new_tap(chip, tap, ir_len, expected_id=None)` | `None` | Declare a new TAP |
|
||||
| `irscan(tap, instruction)` | `int` | Shift instruction into IR |
|
||||
| `drscan(tap, bits, value)` | `int` | Shift data through DR |
|
||||
| `runtest(cycles)` | `None` | Clock TCK in Run-Test/Idle |
|
||||
| `pathmove(states)` | `None` | Walk TAP through state sequence |
|
||||
| `svf(path, tap=None, *, quiet=False, progress=True)` | `None` | Execute SVF file |
|
||||
| `xsvf(tap, path)` | `None` | Execute XSVF file |
|
||||
|
||||
## SVDManager
|
||||
|
||||
| Method / Property | Return | Description |
|
||||
|-------------------|--------|-------------|
|
||||
| `load(svd_path)` | `None` | Parse an SVD XML file |
|
||||
| `loaded` (property) | `bool` | Whether an SVD is loaded |
|
||||
| `list_peripherals()` | `list[str]` | Sorted peripheral names |
|
||||
| `list_registers(peripheral)` | `list[str]` | Sorted register names |
|
||||
| `read_register(peripheral, register)` | `DecodedRegister` | Read from hardware and decode |
|
||||
| `read_peripheral(peripheral)` | `dict[str, DecodedRegister]` | Read all registers in a peripheral |
|
||||
| `decode(peripheral, register, value)` | `DecodedRegister` | Decode without hardware read |
|
||||
|
||||
## RTTManager
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `setup(address, size, id_string="SEGGER RTT")` | `None` | Configure control block search |
|
||||
| `start()` | `None` | Find control block, activate channels |
|
||||
| `stop()` | `None` | Deactivate RTT |
|
||||
| `channels()` | `list[RTTChannel]` | List discovered channels |
|
||||
| `read(channel)` | `str` | Read from an up-channel |
|
||||
| `write(channel, data)` | `None` | Write to a down-channel |
|
||||
|
||||
## Transport
|
||||
|
||||
| Method | Return | Description |
|
||||
|--------|--------|-------------|
|
||||
| `select()` | `str` | Get the active transport name |
|
||||
| `list()` | `list[str]` | List available transports |
|
||||
| `adapter_info()` | `str` | Get adapter description |
|
||||
| `adapter_speed(khz=None)` | `int` | Get or set adapter speed in kHz |
|
||||
|
||||
## EventManager
|
||||
|
||||
| Method / Property | Return | Description |
|
||||
|-------------------|--------|-------------|
|
||||
| `enable()` | `None` | Enable TCL notifications |
|
||||
| `on(event_type, callback)` | `None` | Register event callback |
|
||||
| `off(event_type, callback)` | `None` | Unregister event callback |
|
||||
| `enabled` (property) | `bool` | Whether notifications are active |
|
||||
235
src/content/docs/reference/session-api.mdx
Normal file
235
src/content/docs/reference/session-api.mdx
Normal file
@ -0,0 +1,235 @@
|
||||
---
|
||||
title: Session API
|
||||
description: Complete reference for Session and SyncSession, the main entry points to openocd-python.
|
||||
---
|
||||
|
||||
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
|
||||
|
||||
`Session` is the primary entry point for all interaction with OpenOCD. It manages the connection lifecycle and provides lazy access to every subsystem (target, memory, registers, flash, JTAG, breakpoints, RTT, SVD, transport).
|
||||
|
||||
`SyncSession` is the synchronous counterpart for use outside async contexts.
|
||||
|
||||
## Session (async)
|
||||
|
||||
### Constructor
|
||||
|
||||
```python
|
||||
Session.__init__(connection: TclRpcConnection, process: OpenOCDProcess | None = None)
|
||||
```
|
||||
|
||||
You rarely call this directly. Use the factory methods `connect()` or `start()` instead.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `connection` | `TclRpcConnection` | An established TCL RPC connection |
|
||||
| `process` | `OpenOCDProcess \| None` | Optional managed subprocess (closed on `close()`) |
|
||||
|
||||
### Factory methods
|
||||
|
||||
#### Session.connect()
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
async def connect(
|
||||
host: str = "localhost",
|
||||
port: int = 6666,
|
||||
timeout: float = 10.0,
|
||||
) -> Session
|
||||
```
|
||||
|
||||
Connect to an already-running OpenOCD instance.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `host` | `str` | `"localhost"` | OpenOCD host address |
|
||||
| `port` | `int` | `6666` | TCL RPC port |
|
||||
| `timeout` | `float` | `10.0` | Connection timeout in seconds |
|
||||
|
||||
**Returns:** `Session`
|
||||
|
||||
**Raises:** `ConnectionError`, `TimeoutError`
|
||||
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
state = await session.target.state()
|
||||
```
|
||||
|
||||
#### Session.start()
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
async def start(
|
||||
config: str | Path,
|
||||
*,
|
||||
tcl_port: int = 6666,
|
||||
openocd_bin: str | None = None,
|
||||
timeout: float = 10.0,
|
||||
extra_args: list[str] | None = None,
|
||||
) -> Session
|
||||
```
|
||||
|
||||
Spawn an OpenOCD process with the given configuration, wait for it to become ready, then connect.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `config` | `str \| Path` | *(required)* | Config file path or `-f`/`-c` flags string |
|
||||
| `tcl_port` | `int` | `6666` | TCL RPC port for the spawned process |
|
||||
| `openocd_bin` | `str \| None` | `None` | Custom OpenOCD binary path (auto-detected if `None`) |
|
||||
| `timeout` | `float` | `10.0` | Seconds to wait for OpenOCD readiness |
|
||||
| `extra_args` | `list[str] \| None` | `None` | Additional CLI arguments |
|
||||
|
||||
**Returns:** `Session`
|
||||
|
||||
**Raises:** `ProcessError`, `ConnectionError`, `TimeoutError`
|
||||
|
||||
```python
|
||||
async with await Session.start("interface/cmsis-dap.cfg -f target/stm32f1x.cfg") as session:
|
||||
await session.target.halt()
|
||||
```
|
||||
|
||||
The config parameter accepts several formats:
|
||||
- File path: `"board/stm32f1discovery.cfg"`
|
||||
- Flag string: `"-f interface/cmsis-dap.cfg -f target/stm32f1x.cfg"`
|
||||
- List form: `["-f", "interface/cmsis-dap.cfg", "-f", "target/stm32f1x.cfg"]`
|
||||
|
||||
#### Session.connect_sync()
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def connect_sync(
|
||||
host: str = "localhost",
|
||||
port: int = 6666,
|
||||
**kwargs,
|
||||
) -> SyncSession
|
||||
```
|
||||
|
||||
Synchronous version of `connect()`. Returns a `SyncSession`.
|
||||
|
||||
**Raises:** `RuntimeError` if called from inside a running async event loop.
|
||||
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
state = session.target.state()
|
||||
```
|
||||
|
||||
#### Session.start_sync()
|
||||
|
||||
```python
|
||||
@classmethod
|
||||
def start_sync(config: str | Path, **kwargs) -> SyncSession
|
||||
```
|
||||
|
||||
Synchronous version of `start()`. Returns a `SyncSession`.
|
||||
|
||||
### Context manager
|
||||
|
||||
`Session` implements `__aenter__` and `__aexit__`. On exit, it calls `close()`.
|
||||
|
||||
```python
|
||||
async with await Session.connect() as session:
|
||||
# session is open here
|
||||
pass
|
||||
# session.close() called automatically
|
||||
```
|
||||
|
||||
### Methods
|
||||
|
||||
#### close()
|
||||
|
||||
```python
|
||||
async def close() -> None
|
||||
```
|
||||
|
||||
Close the TCP connection and stop the OpenOCD subprocess if one was spawned via `start()`.
|
||||
|
||||
#### command()
|
||||
|
||||
```python
|
||||
async def command(cmd: str) -> str
|
||||
```
|
||||
|
||||
Send a raw OpenOCD command string and return the response. This is the escape hatch for commands not covered by the typed subsystem APIs.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `cmd` | `str` | Any valid OpenOCD command |
|
||||
|
||||
**Returns:** The raw response string from OpenOCD.
|
||||
|
||||
```python
|
||||
resp = await session.command("version")
|
||||
print(resp)
|
||||
```
|
||||
|
||||
#### on_halt()
|
||||
|
||||
```python
|
||||
def on_halt(callback: Callable[[str], None]) -> None
|
||||
```
|
||||
|
||||
Register a callback that fires when a notification containing "halted" is received.
|
||||
|
||||
#### on_reset()
|
||||
|
||||
```python
|
||||
def on_reset(callback: Callable[[str], None]) -> None
|
||||
```
|
||||
|
||||
Register a callback that fires when a notification containing "reset" is received.
|
||||
|
||||
### Properties (lazy subsystems)
|
||||
|
||||
Each property instantiates its subsystem on first access. All subsystems share the same underlying connection.
|
||||
|
||||
| Property | Async Type | Description |
|
||||
|----------|-----------|-------------|
|
||||
| `target` | `Target` | Halt, resume, step, reset, state queries |
|
||||
| `memory` | `Memory` | Read/write target memory at various widths |
|
||||
| `registers` | `Registers` | Read/write CPU registers |
|
||||
| `flash` | `Flash` | Flash bank enumeration, programming, erase, verify |
|
||||
| `jtag` | `JTAGController` | JTAG scan chain, IR/DR scan, boundary scan |
|
||||
| `breakpoints` | `BreakpointManager` | Set/remove breakpoints and watchpoints |
|
||||
| `rtt` | `RTTManager` | SEGGER RTT channel read/write |
|
||||
| `svd` | `SVDManager` | SVD file loading, register decoding |
|
||||
| `transport` | `Transport` | Transport and adapter queries |
|
||||
|
||||
---
|
||||
|
||||
## SyncSession
|
||||
|
||||
Wraps an async `Session` for synchronous use. Every method runs through `loop.run_until_complete()` on an internally managed event loop.
|
||||
|
||||
### Context manager
|
||||
|
||||
```python
|
||||
with Session.connect_sync() as session:
|
||||
print(session.target.state())
|
||||
```
|
||||
|
||||
Calls `close()` on exit.
|
||||
|
||||
### Methods
|
||||
|
||||
#### command()
|
||||
|
||||
```python
|
||||
def command(cmd: str) -> str
|
||||
```
|
||||
|
||||
Synchronous version of `Session.command()`.
|
||||
|
||||
### Properties (lazy sync subsystems)
|
||||
|
||||
| Property | Sync Type | Description |
|
||||
|----------|----------|-------------|
|
||||
| `target` | `SyncTarget` | Halt, resume, step, reset, state |
|
||||
| `memory` | `SyncMemory` | Read/write memory |
|
||||
| `registers` | `SyncRegisters` | Read/write registers |
|
||||
| `flash` | `SyncFlash` | Flash operations |
|
||||
| `jtag` | `SyncJTAGController` | JTAG operations |
|
||||
| `breakpoints` | `SyncBreakpointManager` | Breakpoints and watchpoints |
|
||||
| `svd` | `SyncSVDManager` | SVD loading and decoding |
|
||||
|
||||
<Aside type="caution">
|
||||
The sync API must never be called from inside a running async event loop. Doing so raises `RuntimeError` with a clear message. If you are in an async context, use the async `Session` directly.
|
||||
</Aside>
|
||||
304
src/content/docs/reference/types.mdx
Normal file
304
src/content/docs/reference/types.mdx
Normal file
@ -0,0 +1,304 @@
|
||||
---
|
||||
title: Types
|
||||
description: Complete reference for all frozen dataclass types and enums returned by the API.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
All types in `openocd-python` are **frozen dataclasses** -- they are immutable after construction. Nothing mutable leaves the API surface. Import them from the top-level package:
|
||||
|
||||
```python
|
||||
from openocd import (
|
||||
TargetState, Register, FlashSector, FlashBank,
|
||||
TAPInfo, JTAGState, MemoryRegion, BitField,
|
||||
DecodedRegister, Breakpoint, Watchpoint, RTTChannel,
|
||||
)
|
||||
```
|
||||
|
||||
## Target types
|
||||
|
||||
### TargetState
|
||||
|
||||
Snapshot of target execution state, returned by `target.halt()`, `target.step()`, `target.state()`, and `target.wait_halt()`.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class TargetState:
|
||||
name: str
|
||||
state: Literal["running", "halted", "reset", "debug-running", "unknown"]
|
||||
current_pc: int | None = None
|
||||
```
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `name` | `str` | *(required)* | Target name (e.g. `stm32f1x.cpu`) |
|
||||
| `state` | `Literal[...]` | *(required)* | One of `"running"`, `"halted"`, `"reset"`, `"debug-running"`, `"unknown"` |
|
||||
| `current_pc` | `int \| None` | `None` | Program counter (populated only when halted) |
|
||||
|
||||
## Register types
|
||||
|
||||
### Register
|
||||
|
||||
A single CPU register, returned by `registers.read_all()`.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Register:
|
||||
name: str
|
||||
number: int
|
||||
value: int
|
||||
size: int
|
||||
dirty: bool = False
|
||||
```
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `name` | `str` | *(required)* | Register name (e.g. `r0`, `pc`, `xPSR`) |
|
||||
| `number` | `int` | *(required)* | Register number in OpenOCD's numbering |
|
||||
| `value` | `int` | *(required)* | Current register value |
|
||||
| `size` | `int` | *(required)* | Register width in bits (typically 32) |
|
||||
| `dirty` | `bool` | `False` | Whether the register has been modified but not committed |
|
||||
|
||||
## Flash types
|
||||
|
||||
### FlashSector
|
||||
|
||||
One sector inside a flash bank.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class FlashSector:
|
||||
index: int
|
||||
offset: int
|
||||
size: int
|
||||
protected: bool
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `index` | `int` | Sector number within the bank |
|
||||
| `offset` | `int` | Byte offset from the bank base address |
|
||||
| `size` | `int` | Sector size in bytes |
|
||||
| `protected` | `bool` | `True` if write protection is enabled |
|
||||
|
||||
### FlashBank
|
||||
|
||||
A flash bank reported by OpenOCD, returned by `flash.banks()` and `flash.info()`.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class FlashBank:
|
||||
index: int
|
||||
name: str
|
||||
base: int
|
||||
size: int
|
||||
bus_width: int
|
||||
chip_width: int
|
||||
target: str
|
||||
sectors: list[FlashSector] = field(default_factory=list)
|
||||
```
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `index` | `int` | *(required)* | Bank number |
|
||||
| `name` | `str` | *(required)* | Bank name (e.g. `stm32f1x.flash`) |
|
||||
| `base` | `int` | *(required)* | Base address in memory |
|
||||
| `size` | `int` | *(required)* | Total bank size in bytes |
|
||||
| `bus_width` | `int` | *(required)* | Bus width |
|
||||
| `chip_width` | `int` | *(required)* | Chip width |
|
||||
| `target` | `str` | *(required)* | Associated target or driver name |
|
||||
| `sectors` | `list[FlashSector]` | `[]` | Sector list (populated only by `flash.info()`) |
|
||||
|
||||
## JTAG types
|
||||
|
||||
### TAPInfo
|
||||
|
||||
One TAP discovered on the JTAG chain, returned by `jtag.scan_chain()`.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class TAPInfo:
|
||||
name: str
|
||||
chip: str
|
||||
tap_name: str
|
||||
idcode: int
|
||||
ir_length: int
|
||||
enabled: bool
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | `str` | Full dotted name (e.g. `stm32f1x.cpu`) |
|
||||
| `chip` | `str` | Chip portion of the name (e.g. `stm32f1x`) |
|
||||
| `tap_name` | `str` | TAP portion of the name (e.g. `cpu`) |
|
||||
| `idcode` | `int` | Detected IDCODE value |
|
||||
| `ir_length` | `int` | Instruction register length in bits |
|
||||
| `enabled` | `bool` | Whether the TAP is enabled in the scan chain |
|
||||
|
||||
### JTAGState
|
||||
|
||||
Enum of all 16 IEEE 1149.1 TAP controller states. Used with `jtag.pathmove()`.
|
||||
|
||||
```python
|
||||
class JTAGState(str, Enum):
|
||||
RESET = "RESET"
|
||||
IDLE = "IDLE"
|
||||
DRSELECT = "DRSELECT"
|
||||
DRCAPTURE = "DRCAPTURE"
|
||||
DRSHIFT = "DRSHIFT"
|
||||
DREXIT1 = "DREXIT1"
|
||||
DRPAUSE = "DRPAUSE"
|
||||
DREXIT2 = "DREXIT2"
|
||||
DRUPDATE = "DRUPDATE"
|
||||
IRSELECT = "IRSELECT"
|
||||
IRCAPTURE = "IRCAPTURE"
|
||||
IRSHIFT = "IRSHIFT"
|
||||
IREXIT1 = "IREXIT1"
|
||||
IRPAUSE = "IRPAUSE"
|
||||
IREXIT2 = "IREXIT2"
|
||||
IRUPDATE = "IRUPDATE"
|
||||
```
|
||||
|
||||
`JTAGState` is a `str` enum, so each member's `.value` is its name as a string.
|
||||
|
||||
## Memory types
|
||||
|
||||
### MemoryRegion
|
||||
|
||||
A chunk of memory read from the target.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class MemoryRegion:
|
||||
address: int
|
||||
size: int
|
||||
data: bytes
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `address` | `int` | Start address of the region |
|
||||
| `size` | `int` | Size of the region in bytes |
|
||||
| `data` | `bytes` | Raw memory contents |
|
||||
|
||||
## SVD types
|
||||
|
||||
### BitField
|
||||
|
||||
One decoded bitfield inside a register, part of `DecodedRegister.fields`.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class BitField:
|
||||
name: str
|
||||
offset: int
|
||||
width: int
|
||||
value: int
|
||||
description: str
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | `str` | Field name from the SVD (e.g. `ODR0`) |
|
||||
| `offset` | `int` | Bit offset within the register |
|
||||
| `width` | `int` | Field width in bits |
|
||||
| `value` | `int` | Extracted field value |
|
||||
| `description` | `str` | Description from SVD metadata |
|
||||
|
||||
### DecodedRegister
|
||||
|
||||
A register value decoded into named bitfields via SVD, returned by `svd.read_register()` and `svd.decode()`.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class DecodedRegister:
|
||||
peripheral: str
|
||||
register: str
|
||||
address: int
|
||||
raw_value: int
|
||||
fields: list[BitField] = field(default_factory=list)
|
||||
```
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `peripheral` | `str` | *(required)* | Peripheral name (e.g. `GPIOA`) |
|
||||
| `register` | `str` | *(required)* | Register name (e.g. `ODR`) |
|
||||
| `address` | `int` | *(required)* | Memory-mapped address |
|
||||
| `raw_value` | `int` | *(required)* | Raw 32-bit value |
|
||||
| `fields` | `list[BitField]` | `[]` | Decoded bitfields, sorted by bit offset |
|
||||
|
||||
`DecodedRegister` implements `__str__` for formatted output:
|
||||
|
||||
```
|
||||
GPIOA.ODR @ 0x4001080C = 0x00000001
|
||||
[ 0:0] ODR0 = 0x1 Port output data bit 0
|
||||
[ 1:1] ODR1 = 0x0 Port output data bit 1
|
||||
```
|
||||
|
||||
Multi-bit fields show the range (e.g. `[7:4]`), single-bit fields show just the offset (e.g. `[0]` displayed as `[ 0:0]`).
|
||||
|
||||
## Breakpoint types
|
||||
|
||||
### Breakpoint
|
||||
|
||||
An active breakpoint, returned by `breakpoints.list()`.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Breakpoint:
|
||||
number: int
|
||||
type: Literal["hw", "sw"]
|
||||
address: int
|
||||
length: int
|
||||
enabled: bool
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `number` | `int` | Breakpoint index |
|
||||
| `type` | `Literal["hw", "sw"]` | Hardware or software breakpoint |
|
||||
| `address` | `int` | Instruction address |
|
||||
| `length` | `int` | Instruction length in bytes (2 = Thumb, 4 = ARM) |
|
||||
| `enabled` | `bool` | Whether the breakpoint is active |
|
||||
|
||||
### Watchpoint
|
||||
|
||||
An active data watchpoint, returned by `breakpoints.list_watchpoints()`.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class Watchpoint:
|
||||
number: int
|
||||
address: int
|
||||
length: int
|
||||
access: Literal["r", "w", "rw"]
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `number` | `int` | Watchpoint index |
|
||||
| `address` | `int` | Watched memory address |
|
||||
| `length` | `int` | Size of watched region in bytes |
|
||||
| `access` | `Literal["r", "w", "rw"]` | Access type: read, write, or both |
|
||||
|
||||
## RTT types
|
||||
|
||||
### RTTChannel
|
||||
|
||||
An RTT channel descriptor, returned by `rtt.channels()`.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class RTTChannel:
|
||||
index: int
|
||||
name: str
|
||||
size: int
|
||||
direction: Literal["up", "down"]
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `index` | `int` | Channel number |
|
||||
| `name` | `str` | Channel name (e.g. `Terminal`) |
|
||||
| `size` | `int` | Buffer size in bytes |
|
||||
| `direction` | `Literal["up", "down"]` | `up` = target-to-host, `down` = host-to-target |
|
||||
1
src/env.d.ts
vendored
Normal file
1
src/env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference path="../.astro/types.d.ts" />
|
||||
82
src/styles/custom.css
Normal file
82
src/styles/custom.css
Normal file
@ -0,0 +1,82 @@
|
||||
/* Custom theme for openocd-python docs */
|
||||
/* Hardware engineering palette: teal, slate, amber */
|
||||
|
||||
:root {
|
||||
/* Accent colors - teal */
|
||||
--sl-color-accent-low: #0d3b3f;
|
||||
--sl-color-accent: #0d9488;
|
||||
--sl-color-accent-high: #5eead4;
|
||||
|
||||
/* Text colors */
|
||||
--sl-color-white: #f8fafc;
|
||||
--sl-color-gray-1: #e2e8f0;
|
||||
--sl-color-gray-2: #cbd5e1;
|
||||
--sl-color-gray-3: #94a3b8;
|
||||
--sl-color-gray-4: #64748b;
|
||||
--sl-color-gray-5: #334155;
|
||||
--sl-color-gray-6: #1e293b;
|
||||
--sl-color-black: #0f172a;
|
||||
|
||||
/* Banner/badge accent - amber for warnings/highlights */
|
||||
--sl-color-orange-low: #451a03;
|
||||
--sl-color-orange: #f59e0b;
|
||||
--sl-color-orange-high: #fde68a;
|
||||
}
|
||||
|
||||
/* Light theme overrides */
|
||||
:root[data-theme='light'] {
|
||||
--sl-color-accent-low: #ccfbf1;
|
||||
--sl-color-accent: #0d9488;
|
||||
--sl-color-accent-high: #134e4a;
|
||||
|
||||
--sl-color-white: #0f172a;
|
||||
--sl-color-gray-1: #1e293b;
|
||||
--sl-color-gray-2: #334155;
|
||||
--sl-color-gray-3: #64748b;
|
||||
--sl-color-gray-4: #94a3b8;
|
||||
--sl-color-gray-5: #e2e8f0;
|
||||
--sl-color-gray-6: #f1f5f9;
|
||||
--sl-color-black: #f8fafc;
|
||||
}
|
||||
|
||||
/* Code blocks - darker slate with teal accents */
|
||||
:root {
|
||||
--sl-color-bg-code: #0c1222;
|
||||
}
|
||||
|
||||
/* Sidebar - subtle depth */
|
||||
.sidebar-pane {
|
||||
border-right: 1px solid var(--sl-color-gray-5);
|
||||
}
|
||||
|
||||
/* Links - teal hover with smooth transition */
|
||||
a:hover {
|
||||
color: var(--sl-color-accent-high);
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
/* Subtle PCB-trace pattern on hero area */
|
||||
.hero {
|
||||
background-image:
|
||||
linear-gradient(90deg, rgba(13, 148, 136, 0.03) 1px, transparent 1px),
|
||||
linear-gradient(rgba(13, 148, 136, 0.03) 1px, transparent 1px);
|
||||
background-size: 24px 24px;
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--sl-color-gray-6);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Inline code */
|
||||
:not(pre) > code {
|
||||
background: var(--sl-color-gray-6);
|
||||
padding: 0.15em 0.35em;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
5
tsconfig.json
Normal file
5
tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "astro/tsconfigs/strict",
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user