Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
67 changes: 65 additions & 2 deletions docs/protocols/swd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
10 changes: 10 additions & 0 deletions jlink/session_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

Expand Down
55 changes: 55 additions & 0 deletions swd/dormant.go
Original file line number Diff line number Diff line change
@@ -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<<uint(bit)) != 0)
}
if _, err := c.exchange(ctx, entry); err != nil {
return err
}
// Send the 128-bit alert least-significant byte first. Keep each exchange
// within the 136 clocks already required by JTAG-to-SWD entry.
alert := &sequence{}
alert.appendN(8, true, true)
for _, value := range []byte{
0x92, 0xf3, 0x09, 0x62, 0x95, 0x2d, 0x85, 0x86,
0xe9, 0xaf, 0xdd, 0xe3, 0xa2, 0x0e, 0xbc, 0x19,
} {
alert.appendByte(true, value)
}
if _, err := c.exchange(ctx, alert); err != nil {
return err
}
activation := &sequence{}
activation.appendN(4, true, false)
activation.appendByte(true, 0x1a)
activation.appendN(56, true, true)
activation.appendN(8, true, false)
_, err := c.exchange(ctx, activation)
return err
}
27 changes: 16 additions & 11 deletions swd/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ const (
clearStickyOverrun = uint32(1 << 4)
)

// Connect enters SWD, reads and validates DPIDR before configuration, clears
// supported sticky state, selects DP bank zero, and establishes the target's
// response grammar. It keeps an inherited ORUNDETECT setting or tries to enable
// it, and uses overrun framing only if the bit reads back as set. Release later
// restores the setting found during bootstrap but does not restore SELECT or
// cleared sticky state. Connect cleans up a failed attempt when possible; a
// joined cleanup error leaves Release available for retry. Calling Connect
// again repairs framing and rejects a changed DPIDR.
// Connect tries JTAG-to-SWD, then retries the initial DPIDR read once
// through dormant activation if its invalid-ACK response completed without
// a wire error. Other identity-read errors do not trigger activation. It
// reads and validates DPIDR before configuration, clears supported sticky
// state, selects DP bank zero, and establishes the target's response
// grammar. It keeps an inherited ORUNDETECT setting or tries to enable it,
// and uses overrun framing only if the bit reads back as set. Release later
// restores the setting found during bootstrap but does not restore SELECT
// or cleared sticky state. Connect cleans up a failed attempt when
// possible; a joined cleanup error leaves Release available for retry.
// Calling Connect again repairs framing and rejects a changed DPIDR.
func (c *Conn) Connect(ctx context.Context) (dpidr uint32, err error) {
if c == nil || c.wire == nil {
return 0, errors.New("swd: nil connection")
Expand Down Expand Up @@ -93,8 +96,10 @@ func (c *Conn) cleanupFailedConnect(connectErr *error) {
}
}

// Release restores the ORUNDETECT setting found by Connect. A failed release
// retains enough state for another call to retry. Release is harmless on an
// Release restores the ORUNDETECT setting found by Connect. A failed
// release retains enough state for another call to retry. Framing repair
// uses the same dormant fallback as Connect. Release leaves SWD selected;
// it does not restore the prior interface mode. Release is harmless on an
// idle or nil connection.
func (c *Conn) Release(ctx context.Context) error {
if c == nil || c.wire == nil || c.state == connectionIdle {
Expand Down Expand Up @@ -185,7 +190,7 @@ func (c *Conn) finishRelease() {
}

func (c *Conn) bootstrap(ctx context.Context) (uint32, error) {
dpidr, err := c.readRaw(ctx, 0x00)
dpidr, err := c.readEntryIdentity(ctx)
if err != nil {
c.requireRepair()
return 0, fmt.Errorf("swd: read DPIDR: %w", err)
Expand Down
Loading
Loading