Compare commits

...

8 Commits

39 changed files with 14649 additions and 0 deletions

4
.env.example Normal file
View File

@ -0,0 +1,4 @@
COMPOSE_PROJECT=openocd-python-docs
MODE=prod
DOMAIN=openocd-python.warehack.ing
VITE_HMR_HOST=

32
Dockerfile Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

19
package.json Normal file
View 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
View 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
View 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
View 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() }),
};

View 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.

View 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")
```

View 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.

View 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

View 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

View 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

View 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

View 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

View 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 |

View 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

View 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.

View 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 |

View 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 |

View 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

View 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

View 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 |

View 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

View 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 |

View 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

View 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 |

View 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>

View 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

View 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}")
```

View 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 |

View 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>

View 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
View File

@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />

82
src/styles/custom.css Normal file
View 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
View File

@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": ["src/**/*"],
"exclude": ["node_modules"]
}