Add SWD Operations guide and update reference pages for v2026.02.15

- New guide: SWD Operations (DAP discovery, DP/AP register access, AP enumeration)
- Reference: add DAPInfo, APInfo types and SWDError exception
- Method index: add SWDController section
- Homepage: 9 → 10 subsystems
- Sidebar: add SWD Operations entry
This commit is contained in:
Ryan Malloy 2026-02-15 18:13:26 -07:00
parent fe49fff171
commit 17e8070c50
6 changed files with 346 additions and 3 deletions

View File

@ -44,6 +44,7 @@ export default defineConfig({
{ label: 'Flash Programming', slug: 'guides/flash-programming' },
{ label: 'Breakpoints & Watchpoints', slug: 'guides/breakpoints' },
{ label: 'JTAG Operations', slug: 'guides/jtag-operations' },
{ label: 'SWD Operations', slug: 'guides/swd-operations' },
{ label: 'SVD Register Decoding', slug: 'guides/svd-decoding' },
{ label: 'RTT Communication', slug: 'guides/rtt-communication' },
{ label: 'Transport & Adapter', slug: 'guides/transport-adapter' },

View File

@ -0,0 +1,262 @@
---
title: SWD Operations
description: DAP discovery, DP/AP register access, Access Port enumeration, and SWD-specific debugging workflows.
---
import { Tabs, TabItem, Aside } from '@astrojs/starlight/components';
The `SWDController` provides access to SWD (Serial Wire Debug) operations through the DAP (Debug Access Port) interface: DAP discovery, DP register reads/writes, AP register reads/writes, and AP enumeration. Access it through the session:
```python
# Async
session.swd
# Sync
sync_session.swd
```
<Aside type="note">
Most users interact with OpenOCD at the target level (halt, resume, memory read/write) and never need raw DAP register commands. This subsystem is for situations where you need direct DP/AP access -- reading device IDs, probing unlisted APs, low-level debug port configuration, or working with custom debug architectures.
</Aside>
## SWD vs JTAG
SWD and JTAG are the two debug transport protocols supported by OpenOCD. The key architectural difference:
| | JTAG | SWD |
|---|---|---|
| **Model** | Scan chain: TAPs, IR/DR shifts | DAP-centric: DP + AP registers |
| **Topology** | Daisy-chained TAPs | Point-to-point (one DAP) |
| **Pins** | TDI, TDO, TCK, TMS (+ TRST) | SWDIO, SWCLK (2 pins) |
| **Subsystem** | `session.jtag` | `session.swd` |
| **OpenOCD model** | Global commands (`scan_chain`, `irscan`) | Named DAP objects (`<dap> dpreg`, `<dap> apreg`) |
Both transports share the higher-level subsystems (target, memory, registers, flash, etc.) -- those work identically regardless of transport. The transport-specific subsystems expose the wire-level protocol details.
## DAP architecture
An ARM CoreSight debug system has a **Debug Port** (DP) connected to one or more **Access Ports** (APs):
```
┌─────────────┐
│ DP │ ← DPIDR, TARGETID, CTRL/STAT
│ (SWDIO) │
├─────────────┤
│ AP #0 │ ← MEM-AP (AHB/APB/AXI bus access)
│ AP #1 │ ← JTAG-AP, additional MEM-AP, etc.
│ ... │
└─────────────┘
```
- **DP registers** (address 0x0 - 0x24): Debug port identification and control
- **AP registers** (per-AP, address 0x00 - 0xFC): Access port configuration and data transfer
## DAP discovery
`info()` queries the DAP for identification and topology. Returns a `DAPInfo` with the DPIDR value and discovered AP count.
<Tabs>
<TabItem label="Async">
```python
import asyncio
from openocd import Session
async def main():
async with await Session.connect() as session:
info = await session.swd.info()
print(f"DAP: {info.name}")
print(f" DPIDR: 0x{info.dpidr:08X}")
print(f" Access Ports: {info.ap_count}")
asyncio.run(main())
```
</TabItem>
<TabItem label="Sync">
```python
from openocd import Session
with Session.connect_sync() as session:
info = session.swd.info()
print(f"DAP: {info.name}")
print(f" DPIDR: 0x{info.dpidr:08X}")
print(f" Access Ports: {info.ap_count}")
```
</TabItem>
</Tabs>
## DP register access
`dpreg(address, value=None)` reads or writes Debug Port registers. When `value` is `None`, it reads. When provided, it writes.
<Tabs>
<TabItem label="Async">
```python
async with await Session.connect() as session:
# Read DPIDR (DP address 0x0)
dpidr = await session.swd.dpreg(0x0)
print(f"DPIDR: 0x{dpidr:08X}")
# Read CTRL/STAT (DP address 0x4)
ctrl_stat = await session.swd.dpreg(0x4)
print(f"CTRL/STAT: 0x{ctrl_stat:08X}")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as session:
dpidr = session.swd.dpreg(0x0)
print(f"DPIDR: 0x{dpidr:08X}")
```
</TabItem>
</Tabs>
### Convenience methods
Two commonly-needed DP registers have dedicated methods:
```python
# DP IDR (address 0x0) — identifies the debug port
dpidr = await session.swd.dpidr()
print(f"DPIDR: 0x{dpidr:08X}")
# TARGETID (address 0x24, DPv2+) — identifies the target device
target_id = await session.swd.target_id()
print(f"TARGETID: 0x{target_id:08X}")
```
<Aside type="tip">
`dpidr()` is equivalent to `dpreg(0x0)` and `target_id()` is equivalent to `dpreg(0x24)`. The convenience methods exist because these are the two most-queried DP registers in practice.
</Aside>
## AP register access
`apreg(ap, address, value=None)` reads or writes Access Port registers. The `ap` parameter is the AP index (0, 1, 2...).
<Tabs>
<TabItem label="Async">
```python
async with await Session.connect() as session:
# Read AP #0 IDR (identifies the AP type)
ap_idr = await session.swd.apreg(0, 0xFC)
print(f"AP #0 IDR: 0x{ap_idr:08X}")
# Read AP #0 BASE (ROM table address)
ap_base = await session.swd.apreg(0, 0xF8)
print(f"AP #0 BASE: 0x{ap_base:08X}")
# Write AP CSW register (configure transfer parameters)
await session.swd.apreg(0, 0x00, value=0x23000052)
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as session:
ap_idr = session.swd.apreg(0, 0xFC)
print(f"AP #0 IDR: 0x{ap_idr:08X}")
```
</TabItem>
</Tabs>
## AP enumeration
`list_aps()` probes the DAP to discover all Access Ports by reading their IDR registers. Returns a list of `APInfo` dataclasses.
<Tabs>
<TabItem label="Async">
```python
async with await Session.connect() as session:
aps = await session.swd.list_aps()
for ap in aps:
print(f"AP #{ap.index}: {ap.ap_type}")
print(f" IDR: 0x{ap.idr:08X}")
print(f" BASE: 0x{ap.base:08X}")
```
</TabItem>
<TabItem label="Sync">
```python
with Session.connect_sync() as session:
aps = session.swd.list_aps()
for ap in aps:
print(f"AP #{ap.index}: {ap.ap_type}")
print(f" IDR: 0x{ap.idr:08X}")
print(f" BASE: 0x{ap.base:08X}")
```
</TabItem>
</Tabs>
A typical STM32F1 output:
```
AP #0: MEM-AP
IDR: 0x04770031
BASE: 0xE00FF003
```
## Multi-DAP boards
Most boards have a single DAP, and the `SWDController` auto-discovers it via `dap names`. For multi-DAP boards (e.g. STM32H7 dual-core with separate DAPs for M7 and M4), pass the DAP name explicitly:
```python
# Auto-discover (single-DAP boards)
dpidr = await session.swd.dpidr()
# Explicit DAP name (multi-DAP boards)
m7_dpidr = await session.swd.dpidr(dap="stm32h7x.m7.dap")
m4_info = await session.swd.info(dap="stm32h7x.m4.dap")
```
Every `SWDController` method accepts an optional `dap` keyword argument. When omitted, the controller caches the result of `dap names` and reuses the first discovered DAP.
## Data types
### DAPInfo
Returned by `swd.info()`.
| Field | Type | Description |
|-------|------|-------------|
| `name` | `str` | DAP instance name (e.g. `stm32f1x.dap`) |
| `dpidr` | `int` | DP ID Register value |
| `ap_count` | `int` | Number of APs found in the `dap info` output |
| `raw_info` | `str` | Full `dap info` output for detailed parsing |
### APInfo
Returned by `swd.list_aps()`.
| Field | Type | Description |
|-------|------|-------------|
| `index` | `int` | AP number (0, 1, 2...) |
| `idr` | `int` | AP ID Register (from offset 0xFC) |
| `base` | `int` | ROM table base address (from offset 0xF8) |
| `ap_type` | `str` | `"MEM-AP"`, `"JTAG-AP"`, or `"unknown"` |
## Error handling
All SWD/DAP operations raise `SWDError` on failure.
```python
from openocd import SWDError
try:
dpidr = await session.swd.dpidr()
except SWDError as e:
print(f"SWD error: {e}")
```
Common failure causes:
- No DAP found (transport not set to SWD)
- Target powered off or not connected
- Invalid AP index
- DP/AP register read returns no data
## Method reference
| Method | Return Type | Description |
|--------|-------------|-------------|
| `info(dap=None)` | `DAPInfo` | Query DAP identification and topology |
| `list_aps(dap=None)` | `list[APInfo]` | Enumerate Access Ports |
| `dpreg(address, value=None, *, dap=None)` | `int` | Read or write a DP register |
| `apreg(ap, address, value=None, *, dap=None)` | `int` | Read or write an AP register |
| `dpidr(dap=None)` | `int` | Read the DP IDR (address 0x0) |
| `target_id(dap=None)` | `int` | Read TARGETID (DP address 0x24) |

View File

@ -20,8 +20,8 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
<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 title="10 Subsystems" icon="puzzle">
Target control, memory, registers, flash, breakpoints, JTAG, SWD/DAP, 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.

View File

@ -16,6 +16,7 @@ from openocd import (
TargetNotHaltedError,
FlashError,
JTAGError,
SWDError,
SVDError,
ProcessError,
)
@ -32,6 +33,7 @@ OpenOCDError
| +-- TargetNotHaltedError
+-- FlashError
+-- JTAGError
+-- SWDError
+-- SVDError
+-- ProcessError
+-- BreakpointError
@ -204,6 +206,30 @@ except JTAGError as e:
print(f"JTAG chain error: {e}")
```
## SWDError
```python
class SWDError(OpenOCDError)
```
Raised when an SWD/DAP operation fails.
**When raised:**
- `swd.info()` encounters an error querying the DAP
- `swd.dpreg()` or `swd.apreg()` fails (e.g. no hex value in response, error keyword in output)
- `swd.list_aps()` encounters a probe error
- `swd.dpidr()` or `swd.target_id()` fails
- No DAP instances found (transport may not be set to SWD)
```python
from openocd import SWDError
try:
dpidr = await session.swd.dpidr()
except SWDError as e:
print(f"SWD/DAP error: {e}")
```
## SVDError
```python

View File

@ -18,7 +18,7 @@ A quick-reference index of every public method in `openocd-python`, organized by
| `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`
**Properties:** `target`, `memory`, `registers`, `flash`, `jtag`, `swd`, `breakpoints`, `rtt`, `svd`, `transport`
## Target
@ -100,6 +100,17 @@ A quick-reference index of every public method in `openocd-python`, organized by
| `svf(path, tap=None, *, quiet=False, progress=True)` | `None` | Execute SVF file |
| `xsvf(tap, path)` | `None` | Execute XSVF file |
## SWDController
| Method | Return | Description |
|--------|--------|-------------|
| `info(dap=None)` | `DAPInfo` | Query DAP identification and topology |
| `list_aps(dap=None)` | `list[APInfo]` | Enumerate Access Ports |
| `dpreg(address, value=None, *, dap=None)` | `int` | Read or write a DP register |
| `apreg(ap, address, value=None, *, dap=None)` | `int` | Read or write an AP register |
| `dpidr(dap=None)` | `int` | Read the DP IDR (address 0x0) |
| `target_id(dap=None)` | `int` | Read TARGETID (DP address 0x24) |
## SVDManager
| Method / Property | Return | Description |

View File

@ -12,6 +12,7 @@ from openocd import (
TargetState, Register, FlashSector, FlashBank,
TAPInfo, JTAGState, MemoryRegion, BitField,
DecodedRegister, Breakpoint, Watchpoint, RTTChannel,
DAPInfo, APInfo,
)
```
@ -281,6 +282,48 @@ class Watchpoint:
| `length` | `int` | Size of watched region in bytes |
| `access` | `Literal["r", "w", "rw"]` | Access type: read, write, or both |
## SWD / DAP types
### DAPInfo
Debug Access Port information, returned by `swd.info()`.
```python
@dataclass(frozen=True)
class DAPInfo:
name: str
dpidr: int
ap_count: int
raw_info: str
```
| Field | Type | Description |
|-------|------|-------------|
| `name` | `str` | DAP instance name (e.g. `stm32f1x.dap`) |
| `dpidr` | `int` | DP ID Register value |
| `ap_count` | `int` | Number of access ports discovered |
| `raw_info` | `str` | Full `dap info` output for detailed parsing |
### APInfo
Access Port descriptor, returned by `swd.list_aps()`.
```python
@dataclass(frozen=True)
class APInfo:
index: int
idr: int
base: int
ap_type: str
```
| Field | Type | Description |
|-------|------|-------------|
| `index` | `int` | AP number (0, 1, 2...) |
| `idr` | `int` | AP ID Register (from apreg offset 0xFC) |
| `base` | `int` | ROM table base address (from apreg offset 0xF8) |
| `ap_type` | `str` | `"MEM-AP"`, `"JTAG-AP"`, or `"unknown"` |
## RTT types
### RTTChannel