From 17e8070c50683e377ef84850a2a0efdb7815714d Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Sun, 15 Feb 2026 18:13:26 -0700 Subject: [PATCH] Add SWD Operations guide and update reference pages for v2026.02.15 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- astro.config.mjs | 1 + src/content/docs/guides/swd-operations.mdx | 262 ++++++++++++++++++++ src/content/docs/index.mdx | 4 +- src/content/docs/reference/exceptions.mdx | 26 ++ src/content/docs/reference/method-index.mdx | 13 +- src/content/docs/reference/types.mdx | 43 ++++ 6 files changed, 346 insertions(+), 3 deletions(-) create mode 100644 src/content/docs/guides/swd-operations.mdx 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