From 355adc055b3e6df1885ff05c99d23a1d4d401418 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sun, 13 Sep 2026 12:44:39 -0700 Subject: [PATCH] Connect dormant debug interfaces over SWD. Targets which require dormant activation do not answer the existing JTAG-to-SWD entry. Retry the initial DPIDR read once through dormant activation after a completed invalid-ACK response. Use the same path when release must repair framing before restoring owned state. Do not use dormant activation to retry other identity-read failures. Keep ordinary register calls at one attempt, and fit activation within the existing wire transfer size. The RP2350/J-Link bench now connects, confirms identity on reconnect, and completes release after line-reset repair. --- docs/architecture.md | 8 +- docs/capabilities.md | 5 +- docs/protocols/swd.md | 67 ++++++++- jlink/session_integration_test.go | 10 ++ swd/dormant.go | 55 +++++++ swd/lifecycle.go | 27 ++-- swd/sim/dormant_test.go | 232 ++++++++++++++++++++++++++++++ 7 files changed, 388 insertions(+), 16 deletions(-) create mode 100644 swd/dormant.go create mode 100644 swd/sim/dormant_test.go diff --git a/docs/architecture.md b/docs/architecture.md index d9f92a2..66787de 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -187,7 +187,13 @@ commands and response status. Each adapter owns its clock and transfer limits. acknowledgements, data parity, line reset, the JTAG-to-SWD selection sequence, and the CTRL/STAT.ORUNDETECT setting which selects the response grammar. -`swd.Conn.Connect` reads and validates DPIDR before configuration, clears +`swd.Conn.Connect` tries JTAG-to-SWD first. If the initial DPIDR read has an +invalid ACK and its trailing clocks complete, it tries JTAG-to-dormant and +dormant-to-SWD once before reading DPIDR again. Release uses the same fallback +when it must repair framing. Release leaves SWD selected rather than restoring +the prior interface mode. + +The connection reads and validates DPIDR before configuration, clears supported sticky state, writes zero to SELECT, settles it through RDBUFF, then reads CTRL/STAT. This establishes which response grammar applies before ordinary register access. It keeps an inherited ORUNDETECT setting or tries to diff --git a/docs/capabilities.md b/docs/capabilities.md index 46bb6e3..0d07126 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -197,9 +197,10 @@ packet, ownership, and current bench boundaries. | Automatic retries | No | A raw register call does not replay the requested transaction. In overrun mode it clears STICKYORUN before returning WAIT; retry policy belongs to the caller. | | Ordered raw queue | Yes | `swd.Batch` validates all queued DP/AP operations before traffic, sends them in order, resolves direction-specific results, and never replays the operation which first fails. | | Fixed-frame batching | Yes | In overrun mode the ordered queue packs complete 54-bit frames up to an optional wire limit; simple mode remains sequential. Operations in a failed physical chunk are indeterminate; later chunks remain unsent, and requested operations are never replayed. | -| Multidrop or dormant state | No | The public connection models one entered SWD target. | +| Dormant activation | Yes | `Connect`, and `Release` when repairing framing, try JTAG-to-dormant and dormant-to-SWD once after a completed invalid-ACK response to the initial DPIDR read. Each wire call uses at most 136 clocks. Ordinary register calls are not retried. | +| Multidrop selection | No | The public connection models one entered SWD target. | | Behavioral simulation | Yes | Protocol entry and line-reset effects, live overrun response grammar, DP/AP register transfers, packed fixed frames, transfer limits, and request-phase WAIT or FAULT injection. | -| Physical DPIDR read | HIL | Opt-in FTDI test and trivial example on Linux and macOS, plus an opt-in J-Link test on macOS. | +| Physical DPIDR read | HIL | Opt-in FTDI test and trivial example on Linux and macOS, plus an opt-in J-Link test on macOS. Two fresh RP2350/J-Link sessions read DPIDR `0x4c013477`, reconnected with matching identity, and completed release after line-reset repair; see the SWD guide for preparation and limits. | The public `swd.Wire` boundary is implemented by FTDI, J-Link, and CMSIS-DAP and can be borrowed through a generic `probe.Probe` owner. diff --git a/docs/protocols/swd.md b/docs/protocols/swd.md index c6e0a4f..71356db 100644 --- a/docs/protocols/swd.md +++ b/docs/protocols/swd.md @@ -9,8 +9,8 @@ ADIv5.2_](https://developer.arm.com/documentation/ihi0031/h) is the normative SWD specification. Use chapter B4 and section B5.2 for the protocol definition; this note is not a substitute for them. It is limited to details which are easy to misread and observations from hardware. It covers the point-to-point -protocol, not SWD protocol version 2 target selection, multidrop, or -dormant-state entry. +protocol and dormant activation, not SWD protocol version 2 target selection +or multidrop. ## A transfer @@ -172,6 +172,36 @@ The second high run leaves SWD in line-reset state. Arm points out that the two low idle clocks from a normal line reset are absent from the switching figure; a host can supply idle clocks before reading DPIDR. +IHI 0031H section B5.3 defines dormant operation. A dormant interface +ignores ordinary SWD requests until it receives the selection alert and +activation code. Ostiole first tries the JTAG-to-SWD sequence above. If the +initial DPIDR read returns an invalid ACK and the host completes the +undriven data phase and idle clocks, it tries this sequence once: + +1. Nine high clocks and the 31-bit JTAG-to-dormant code `0x33bbbbba`, + least-significant bit first. +2. Eight high clocks and the 128-bit selection alert + `0x19bc0ea2e3ddafe986852d956209f392`, least-significant bit first across + the whole value: byte `0x92` goes first. +3. Four low clocks, the eight-bit SWD activation code `0x1a` + least-significant bit first, 56 high clocks for line reset, and eight low + idle clocks. +4. Another DPIDR read, followed by the ordinary bootstrap only if identity + validation succeeds. + +The three activation exchanges use 40, 136, and 76 clocks, so the fallback +needs no larger wire transfer than existing JTAG-to-SWD entry. A parity, +WAIT, FAULT, or transport error does not trigger this fallback. Neither does +an invalid ACK whose trailing clocks failed. Each bootstrap has at most two +DPIDR attempts; failed Connect may also run a separate bootstrap during +bounded cleanup. Ordinary register calls still make one attempt. + +Release uses the same activation fallback when framing repair is needed and +checks the established identity before restoring owned state. It restores +ORUNDETECT but leaves SWD selected; it does not return the interface to +dormant mode. Activation does not halt or reset the processor, request +system power, or add ADIv6 AP addressing. + Multidrop SWD has another boundary worth stating plainly: there is no generic way to ask an unselected multidrop bus which target IDs are present. The host must already know which IDs to try. That is a protocol limitation, not a @@ -289,3 +319,36 @@ entry. It used 32,033 physical SWDIO calls. The experiment exercises Linux usbfs submission, completion notification and reaping, endpoint cancellation, and release on this bench; it is not a USB or SWD waveform capture and does not establish behavior for another host controller or FTDI product. + +## RP2350 dormant activation bench + +On macOS, a J-Link EDU Mini V2 (serial `000802011345`, firmware +`J-Link EDU Mini V2 compiled Jun 25 2026 10:27:52`) connected to an RP2350 +over SWD at 100 kHz. Before each of two fresh Ostiole sessions, OpenOCD 0.12.0 connected +to the debug port and shut down; its debug log showed SWD-to-dormant +followed by dormant-to-JTAG on shutdown. The preparation used no CPU target +or reset command: + +```sh +openocd -c 'adapter driver jlink' -c 'adapter serial 000802011345' \ + -c 'transport select swd' -c 'adapter speed 100' -c 'reset_config none' \ + -c 'gdb_port disabled' -c 'tcl_port disabled' -c 'telnet_port disabled' \ + -c 'swd newdap rp2350 dp -irlen 4' \ + -c 'dap create rp2350.dap -chain-position rp2350.dp -adiv6' \ + -c init -c shutdown +OSTIOLE_JLINK_HIL=1 OSTIOLE_JLINK_HIL_SERIAL=000802011345 \ + go test -tags=integration ./jlink -run '^TestHILJLinkSWDDPIDR$' -count=1 -v +``` + +Both sessions returned DPIDR `0x4c013477` (version 3, designer `0x23b`), +confirmed the same identity on reconnect, and completed SWD release after an +explicit line reset forced framing repair. The J-Link reported a 504-bit +transfer limit. Before dormant activation was implemented, the DPIDR test +returned an invalid ACK and cleanup remained pending. OpenOCD independently +reproduced that invalid ACK after JTAG-to-SWD and read the correct identity +after its dormant fallback. + +The runs did not independently measure the inherited ORUNDETECT value after +release, exercise an Ostiole ADIv6 AP, or read target memory. No processor +halt or reset was requested. OpenOCD performed its own debug-port +initialization; the bench was not power-cycled to test startup state. diff --git a/jlink/session_integration_test.go b/jlink/session_integration_test.go index 858ae13..890c2ff 100644 --- a/jlink/session_integration_test.go +++ b/jlink/session_integration_test.go @@ -77,6 +77,16 @@ func TestHILJLinkSWDDPIDR(t *testing.T) { if session.ClockHz() != 100_000 || session.MaxTransferBits() != 504 { t.Fatalf("J-Link SWD configuration = %d Hz, %d bits", session.ClockHz(), session.MaxTransferBits()) } + if confirmed, err := connection.Connect(ctx); err != nil || confirmed != raw { + t.Fatalf("reconnect DPIDR=%#08x, %v; want %#08x", confirmed, err, raw) + } + if err := connection.LineReset(ctx); err != nil { + t.Fatal(err) + } + if err := cleanup.releaseCurrent(ctx); err != nil { + t.Fatal(err) + } + t.Log("SWD release completed after line-reset repair") t.Logf("DPIDR=%#08x version=%d designer=%#03x clock=%d max_bits=%d", raw, info.Version, info.Designer, session.ClockHz(), session.MaxTransferBits()) } diff --git a/swd/dormant.go b/swd/dormant.go new file mode 100644 index 0000000..4cd1b74 --- /dev/null +++ b/swd/dormant.go @@ -0,0 +1,55 @@ +package swd + +import ( + "context" + "errors" + "fmt" +) + +func (c *Conn) readEntryIdentity(ctx context.Context) (uint32, error) { + dpidr, err := c.readRaw(ctx, 0x00) + // A joined error means the invalid-ACK data phase could not be completed. + // Leave that transport failure to the caller instead of clocking activation. + if err != ErrProtocol { + return dpidr, err + } + if err := ctx.Err(); err != nil { + return 0, err + } + if err := c.enterThroughDormant(ctx); err != nil { + return 0, errors.Join(ErrProtocol, fmt.Errorf("swd: dormant activation: %w", err)) + } + return c.readRaw(ctx, 0x00) +} + +func (c *Conn) enterThroughDormant(ctx context.Context) error { + // IHI 0031H B5.3: nine high clocks and the 31-bit JTAG-to-DS code. + entry := &sequence{} + entry.appendN(9, true, true) + for bit := range 31 { + entry.append(true, uint32(0x33bbbbba)&(1< 136 { + return errors.New("entry exceeds legacy wire limit") + } + if bits == w.failBits && (bits != 136 || output[1] == 0x92) { + return w.failErr + } + + return nil +}