diff --git a/astro.config.mjs b/astro.config.mjs
index b697d2b..06f20c1 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -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' },
diff --git a/src/content/docs/guides/swd-operations.mdx b/src/content/docs/guides/swd-operations.mdx
new file mode 100644
index 0000000..fb409d6
--- /dev/null
+++ b/src/content/docs/guides/swd-operations.mdx
@@ -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
+```
+
+
+
+## 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 (` dpreg`, ` 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.
+
+
+
+```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())
+```
+
+
+```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}")
+```
+
+
+
+## DP register access
+
+`dpreg(address, value=None)` reads or writes Debug Port registers. When `value` is `None`, it reads. When provided, it writes.
+
+
+
+```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}")
+```
+
+
+```python
+with Session.connect_sync() as session:
+ dpidr = session.swd.dpreg(0x0)
+ print(f"DPIDR: 0x{dpidr:08X}")
+```
+
+
+
+### 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}")
+```
+
+
+
+## AP register access
+
+`apreg(ap, address, value=None)` reads or writes Access Port registers. The `ap` parameter is the AP index (0, 1, 2...).
+
+
+
+```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)
+```
+
+
+```python
+with Session.connect_sync() as session:
+ ap_idr = session.swd.apreg(0, 0xFC)
+ print(f"AP #0 IDR: 0x{ap_idr:08X}")
+```
+
+
+
+## AP enumeration
+
+`list_aps()` probes the DAP to discover all Access Ports by reading their IDR registers. Returns a list of `APInfo` dataclasses.
+
+
+
+```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}")
+```
+
+
+```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}")
+```
+
+
+
+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) |
diff --git a/src/content/docs/index.mdx b/src/content/docs/index.mdx
index 1d34a70..e802192 100644
--- a/src/content/docs/index.mdx
+++ b/src/content/docs/index.mdx
@@ -20,8 +20,8 @@ import { Card, CardGrid } from '@astrojs/starlight/components';
Full async/await API with sync wrappers. Use `Session.connect()` for async or `Session.connect_sync()` for synchronous code.
-
- Target control, memory, registers, flash, breakpoints, JTAG, SVD, RTT, and transport — all from one session.
+
+ Target control, memory, registers, flash, breakpoints, JTAG, SWD/DAP, SVD, RTT, and transport — all from one session.
Frozen dataclasses for all return types. Explicit exception hierarchy. Full type annotations throughout.
diff --git a/src/content/docs/reference/exceptions.mdx b/src/content/docs/reference/exceptions.mdx
index 9a980d4..1bb85ca 100644
--- a/src/content/docs/reference/exceptions.mdx
+++ b/src/content/docs/reference/exceptions.mdx
@@ -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
diff --git a/src/content/docs/reference/method-index.mdx b/src/content/docs/reference/method-index.mdx
index 9ceadbc..b019796 100644
--- a/src/content/docs/reference/method-index.mdx
+++ b/src/content/docs/reference/method-index.mdx
@@ -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 |
diff --git a/src/content/docs/reference/types.mdx b/src/content/docs/reference/types.mdx
index 5ec0efa..e7d2724 100644
--- a/src/content/docs/reference/types.mdx
+++ b/src/content/docs/reference/types.mdx
@@ -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