From 246e411790df9f4c18c336c978ba675b10a0382e Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sun, 20 Sep 2026 14:20:21 -0700 Subject: [PATCH 1/7] Read DPv3 identity and discovery registers. DPv3 banks the DPIDR address and adds the address-width and discovery-base registers needed to locate its access ports. Treating DPIDR as independent of SELECT would return another register after reading a nonzero bank. Select the correct bank for immediate and queued DP reads, expose the DPv3 registers, and model them in the behavioral target. Earlier debug ports and baseline JTAG reject registers they do not implement. --- armdebug/conn_integration_test.go | 9 ++++ dap/adiv6_test.go | 66 +++++++++++++++++++++++++++ dap/banked_test.go | 8 ++-- dap/doc.go | 2 +- dap/sim/target.go | 76 +++++++++++++++++++++++-------- dap/swddp.go | 22 ++++++--- dap/txn.go | 5 +- dap/txn_test.go | 10 ++-- dap/types.go | 16 ++++++- docs/capabilities.md | 3 +- docs/ports/dap.md | 22 +++++++++ 11 files changed, 198 insertions(+), 41 deletions(-) create mode 100644 dap/adiv6_test.go diff --git a/armdebug/conn_integration_test.go b/armdebug/conn_integration_test.go index a579e85..8717c5d 100644 --- a/armdebug/conn_integration_test.go +++ b/armdebug/conn_integration_test.go @@ -57,6 +57,15 @@ func TestHILArmConnection(t *testing.T) { t.Fatal(err) } t.Logf("provider=%s probe=%+v DPIDR=%#08x requested_clock_hz=100000", selection.Provider, c.Info(), id) + if id>>12&15 == 3 { + for _, reg := range []dap.DPRegister{dap.DPIDR1, dap.BASEPTR0, dap.BASEPTR1, dap.DPIDR} { + value, err := c.Port().ReadDP(ctx, reg) + if err != nil { + t.Fatal(err) + } + t.Logf("%s=%#08x", reg, value) + } + } if inspectMemory { inspectMemoryHIL(t, ctx, c, ap) } diff --git a/dap/adiv6_test.go b/dap/adiv6_test.go new file mode 100644 index 0000000..106c77b --- /dev/null +++ b/dap/adiv6_test.go @@ -0,0 +1,66 @@ +package dap_test + +import ( + "github.com/jon/ostiole/dap" + dapsim "github.com/jon/ostiole/dap/sim" + "github.com/jon/ostiole/swd" + swdsim "github.com/jon/ostiole/swd/sim" + "testing" +) + +func TestDPv3BankedIdentity(t *testing.T) { + target := dapsim.New(0x4c013477) + for reg, value := range map[dap.DPRegister]uint32{dap.DPIDR1: 32, dap.BASEPTR0: 0x1001, dap.BASEPTR1: 0, dap.DLCR: 0} { + if err := target.SetDPRegister(reg, value); err != nil { + t.Fatal(err) + } + } + dp := dap.NewDebugPort(dap.SWDP(swd.New(swdsim.New(target)))) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + defer func() { + if err := dp.Release(t.Context()); err != nil { + t.Error(err) + } + }() + for _, reg := range []dap.DPRegister{dap.DPIDR1, dap.BASEPTR0, dap.BASEPTR1, dap.DPIDR} { + got, err := dp.ReadDP(t.Context(), reg) + want := map[dap.DPRegister]uint32{dap.DPIDR1: 32, dap.BASEPTR0: 0x1001, dap.BASEPTR1: 0, dap.DPIDR: 0x4c013477}[reg] + if err != nil || got != want { + t.Fatalf("%s = %#x, %v; want %#x", reg, got, err, want) + } + } + txn := dp.NewTxn() + base := txn.ReadDP(dap.BASEPTR0) + id := txn.ReadDP(dap.DPIDR) + if err := txn.Commit(t.Context()); err != nil { + t.Fatal(err) + } + if got, err := base.Value(); err != nil || got != 0x1001 { + t.Fatalf("base = %#x, %v", got, err) + } + if got, err := id.Value(); err != nil || got != 0x4c013477 { + t.Fatalf("identity = %#x, %v", got, err) + } +} + +func TestADIv6RegistersRejectADIv5(t *testing.T) { + target := newWaitTarget() + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + before := len(target.requests) + for _, reg := range []dap.DPRegister{dap.DPIDR1, dap.BASEPTR0, dap.BASEPTR1, dap.SELECT1} { + if _, err := dp.ReadDP(t.Context(), reg); err == nil { + t.Fatalf("read %s accepted", reg) + } + if err := dp.WriteDP(t.Context(), reg, 0); err == nil { + t.Fatalf("write %s accepted", reg) + } + } + if len(target.requests) != before { + t.Fatal("invalid registers sent traffic") + } +} diff --git a/dap/banked_test.go b/dap/banked_test.go index 01efb8a..759b772 100644 --- a/dap/banked_test.go +++ b/dap/banked_test.go @@ -75,9 +75,9 @@ func TestBankedDPAccessRejectsUnsupportedVersionWithoutTraffic(t *testing.T) { } } -func TestBankedDPAccessRejectsDPv3WithoutTraffic(t *testing.T) { +func TestBankedDPAccessRejectsFutureVersionWithoutTraffic(t *testing.T) { target := newWaitTarget() - target.dpidrOverride = 0x2ba03477 + target.dpidrOverride = 0x2ba04477 dp := newDebugPort(t, target) if _, err := dp.Connect(t.Context()); err != nil { t.Fatal(err) @@ -85,10 +85,10 @@ func TestBankedDPAccessRejectsDPv3WithoutTraffic(t *testing.T) { before := len(target.requests) if _, err := dp.ReadDP(t.Context(), dap.DLCR); err == nil { - t.Fatal("DPv3 DLCR read succeeded through the ADIv5 address map") + t.Fatal("DPv4 DLCR read succeeded through the ADIv5 address map") } if got := len(target.requests); got != before { - t.Fatalf("requests after rejected DPv3 read = %d, want %d", got, before) + t.Fatalf("requests after rejected DPv4 read = %d, want %d", got, before) } } diff --git a/dap/doc.go b/dap/doc.go index 75ad41a..f809785 100644 --- a/dap/doc.go +++ b/dap/doc.go @@ -33,7 +33,7 @@ // direct transfers can invalidate its cached register selection and response // state. // -// ReadDP and WriteDP accept logical ADIv5 register names. They distinguish +// ReadDP and WriteDP accept logical debug-port register names. They distinguish // operations which share a physical offset and enforce direction and availability. // SWD manages DPBANKSEL without exposing a current-bank operation. Baseline // JTAG supports readable SELECT, rejects banked registers and DPIDR, and accepts diff --git a/dap/sim/target.go b/dap/sim/target.go index ba9f5ce..4f62ee4 100644 --- a/dap/sim/target.go +++ b/dap/sim/target.go @@ -1,4 +1,4 @@ -// Package sim provides behavioral ADIv5 targets for the SWD simulator. +// Package sim provides behavioral ADIv5 and ADIv6 targets for the SWD simulator. package sim import ( @@ -33,13 +33,16 @@ const ( // Target models the initial SW-DP register state. type Target struct { - dpidr uint32 - ctrlStat uint32 - dpBanks [16]uint32 - dpBankSet [16]bool - selectDP uint32 - rdbuff uint32 - aps map[dap.APSel]*accessPort + dpidr uint32 + ctrlStat uint32 + dpBanks [16]uint32 + dpIDBanks [4]uint32 + dpIDBankSet [4]bool + selectHigh uint32 + dpBankSet [16]bool + selectDP uint32 + rdbuff uint32 + aps map[dap.APSel]*accessPort } type accessPort struct { @@ -95,6 +98,9 @@ func (t *Target) ObserveLineReset() { return } t.dpBanks[1] = 0 + if t.dpidr>>12&15 == 3 { + t.selectDP &^= 15 + } if t.OverrunDetectEnabled() { t.ctrlStat |= stickyOverrun } @@ -135,6 +141,9 @@ func (t *Target) SetDPRegister(reg dap.DPRegister, value uint32) error { if t == nil { return errors.New("dap/sim: nil target") } + if reg >= dap.DPIDR1 && reg <= dap.BASEPTR1 { + return t.setIdentityRegister(reg, value) + } var bank uint8 switch reg { case dap.DLCR: @@ -345,7 +354,7 @@ func (t *Target) Read(ctx context.Context, req swdsim.Request) (uint32, error) { } switch req.Addr { case 0x00: - return t.dpidr, nil + return t.readIdentityRegister(), nil case 0x04: bank := uint8(t.selectDP & 0x0f) if bank == 0 { @@ -379,16 +388,7 @@ func (t *Target) Write(ctx context.Context, req swdsim.Request, value uint32) er case 0x00: t.clearSticky(value) case 0x04: - bank := uint8(t.selectDP & 0x0f) - switch bank { - case 0: - t.setPower(value) - case 1: - if value&dlcrTurnaroundMask != 0 { - return errors.New("dap/sim: variable SWD turnaround is not modeled") - } - t.dpBanks[bank] = value - } + return t.writeBankedRegister(value) case 0x08: t.selectDP = value default: @@ -640,3 +640,41 @@ func (t *Target) setPower(value uint32) { t.ctrlStat |= systemPowerAck } } + +func (t *Target) setIdentityRegister(reg dap.DPRegister, value uint32) error { + bank := int(reg-dap.DPIDR1) + 1 + if t.dpidr>>12&15 != 3 || t.dpIDBankSet[bank] { + return fmt.Errorf("dap/sim: %s fixture unavailable", reg) + } + t.dpIDBanks[bank], t.dpIDBankSet[bank] = value, true + return nil +} + +func (t *Target) readIdentityRegister() uint32 { + bank := t.selectDP & 15 + if t.dpidr>>12&15 == 3 && bank != 0 { + if bank < 4 { + return t.dpIDBanks[bank] + } + return 0 + } + return t.dpidr +} + +func (t *Target) writeBankedRegister(value uint32) error { + bank := uint8(t.selectDP & 0x0f) + switch bank { + case 0: + t.setPower(value) + case 5: + if t.dpidr>>12&15 == 3 { + t.selectHigh = value + } + case 1: + if value&dlcrTurnaroundMask != 0 { + return errors.New("dap/sim: variable SWD turnaround is not modeled") + } + t.dpBanks[bank] = value + } + return nil +} diff --git a/dap/swddp.go b/dap/swddp.go index 3dca4fd..1e4eba0 100644 --- a/dap/swddp.go +++ b/dap/swddp.go @@ -101,9 +101,9 @@ func (dp *DebugPort) SetMaxWaits(maxWaits uint) error { return nil } -// ReadDP reads one logical ADIv5 debug-port register. Bank-independent and +// ReadDP reads one logical debug-port register. Bank-independent and // bank-zero registers remain distinct. Nonzero banks require an active SW-DP -// DPv1 or DPv2 connection. Baseline JTAG-DP has no banked DP registers or DPIDR; +// DPv1, DPv2, or DPv3 connection. Baseline JTAG-DP has no banked DP registers or DPIDR; // it supplies IDCODE instead. The debug port must be connected. func (dp *DebugPort) ReadDP(ctx context.Context, reg DPRegister) (uint32, error) { if err := dp.requireOperational(ctx); err != nil { @@ -149,7 +149,7 @@ func (dp *DebugPort) readDPRegister(ctx context.Context, reg DPRegister, info dp return value, nil } -// WriteDP writes one logical ADIv5 debug-port register. The binding owns +// WriteDP writes one logical debug-port register. The binding owns // CTRL/STAT.ORUNDETECT: preserve its SWD value and keep it clear for JTAG. // JTAG pushed-operation and transaction-counter modes are rejected, as are // unsupported SWD turnaround settings. Release does @@ -194,7 +194,7 @@ func (dp *DebugPort) validateDPRegister(reg DPRegister, write bool) (dpRegisterI if reg == IDCODE { return dpRegisterInfo{}, errors.New("dap: IDCODE is unavailable on SW-DP") } - info, ok := describeDPRegister(reg) + info, ok := dp.describeRegister(reg) if !ok { return dpRegisterInfo{}, fmt.Errorf("dap: invalid DP register %#04x", uint16(reg)) } @@ -212,6 +212,14 @@ func (dp *DebugPort) validateDPRegister(reg DPRegister, write bool) (dpRegisterI return info, nil } +func (dp *DebugPort) describeRegister(reg DPRegister) (dpRegisterInfo, bool) { + info, ok := describeDPRegister(reg) + if reg == DPIDR && dp.reentryID.dpidr.Version == 3 { + info.bankIndependent = false + } + return info, ok +} + func (dp *DebugPort) validateDPWrite(reg DPRegister, value uint32) (dpRegisterInfo, error) { info, err := dp.validateDPRegister(reg, true) if err != nil { @@ -236,8 +244,8 @@ func (dp *DebugPort) validateBankedDPRegister(info dpRegisterInfo) error { if dp.state.session != sessionConnected || !dp.identified { return errors.New("dap: banked DP access requires an active connection") } - if dp.identity.dpidr.Version > 2 { - return fmt.Errorf("dap: ADIv5 banked DP access does not support DPv%d", dp.identity.dpidr.Version) + if dp.identity.dpidr.Version > 3 { + return fmt.Errorf("dap: banked DP access does not support DPv%d", dp.identity.dpidr.Version) } if dp.identity.dpidr.Version < info.minVersion { return fmt.Errorf("dap: %s requires DPv%d or later", info.name, info.minVersion) @@ -294,7 +302,7 @@ func (dp *DebugPort) recordDPWriteState(reg DPRegister, value uint32) { } func (dp *DebugPort) recordDPRead(reg DPRegister, value uint32) { - if reg != DPIDR { + if dpRegisterOffset(reg) != 0 { dp.state.settleDPWrite() } if reg == CTRLSTAT && dp.state.selectDP.valid && dp.state.dpBank() == 0 { diff --git a/dap/txn.go b/dap/txn.go index 41e5f2b..deb1d08 100644 --- a/dap/txn.go +++ b/dap/txn.go @@ -293,13 +293,14 @@ type txnStep struct { } type swdTxnPlanner struct { + dp *DebugPort selectDP selectState selectPending bool steps []txnStep } func newSWDTxnPlanner(dp *DebugPort) *swdTxnPlanner { - return &swdTxnPlanner{selectDP: dp.state.selectDP, selectPending: dp.state.selectPending} + return &swdTxnPlanner{dp: dp, selectDP: dp.state.selectDP, selectPending: dp.state.selectPending} } func (p *swdTxnPlanner) plan(ops []txnOp) []txnStep { @@ -379,7 +380,7 @@ func (p *swdTxnPlanner) lowerAPWriteSequence(index int, op txnOp) { } func (p *swdTxnPlanner) lowerDP(index int, op txnOp) { - info, _ := describeDPRegister(op.dpReg) + info, _ := p.dp.validateDPRegister(op.dpReg, op.kind == txnWriteDP) if !info.bankIndependent { p.selectBank(index, info.bank) } diff --git a/dap/txn_test.go b/dap/txn_test.go index 038939f..14f0c33 100644 --- a/dap/txn_test.go +++ b/dap/txn_test.go @@ -949,9 +949,9 @@ func TestDebugPortTransactionRejectsOverrunChangeBeforeWireTraffic(t *testing.T) } } -func TestDebugPortTransactionRejectsDPv3BankWithoutTraffic(t *testing.T) { +func TestDebugPortTransactionRejectsFutureBankWithoutTraffic(t *testing.T) { target := newWaitTarget() - target.dpidrOverride = 0x2ba03477 + target.dpidrOverride = 0x2ba04477 dp := newDebugPort(t, target) if _, err := dp.Connect(t.Context()); err != nil { t.Fatal(err) @@ -961,13 +961,13 @@ func TestDebugPortTransactionRejectsDPv3BankWithoutTraffic(t *testing.T) { txn := dp.NewTxn() result := txn.ReadDP(dap.DLCR) if err := txn.Commit(t.Context()); err == nil { - t.Fatal("transaction accepted an ADIv5 banked address on DPv3") + t.Fatal("transaction accepted an ADIv5 banked address on DPv4") } if got := len(target.requests); got != before { - t.Fatalf("requests after rejected DPv3 transaction = %d, want %d", got, before) + t.Fatalf("requests after rejected DPv4 transaction = %d, want %d", got, before) } if _, err := result.Value(); err == nil || errors.Is(err, dap.ErrNotExecuted) { - t.Fatalf("DPv3 result error = %v, want address validation error", err) + t.Fatalf("DPv4 result error = %v, want address validation error", err) } } diff --git a/dap/types.go b/dap/types.go index 73a41e3..fea5038 100644 --- a/dap/types.go +++ b/dap/types.go @@ -2,11 +2,11 @@ package dap import "fmt" -// DPRegister identifies one logical ADIv5 debug-port register. Registers which +// DPRegister identifies one logical debug-port register. Registers which // share a physical SWD offset remain distinct values. type DPRegister uint16 -// ADIv5 debug-port registers. +// Debug-port registers. DPIDR1, BASEPTR0, BASEPTR1, and SELECT1 require DPv3. const ( DPIDR DPRegister = iota + 1 ABORT @@ -19,6 +19,10 @@ const ( RESEND RDBUFF IDCODE + DPIDR1 + BASEPTR0 + BASEPTR1 + SELECT1 ) type dpRegisterInfo struct { @@ -54,6 +58,14 @@ func describeDPRegister(reg DPRegister) (dpRegisterInfo, bool) { func describeBankedDPRegister(reg DPRegister) (dpRegisterInfo, bool) { switch reg { + case DPIDR1: + return dpRegisterInfo{name: "DPIDR1", bank: 1, readable: true, minVersion: 3}, true + case BASEPTR0: + return dpRegisterInfo{name: "BASEPTR0", bank: 2, readable: true, minVersion: 3}, true + case BASEPTR1: + return dpRegisterInfo{name: "BASEPTR1", bank: 3, readable: true, minVersion: 3}, true + case SELECT1: + return dpRegisterInfo{name: "SELECT1", offset: 4, bank: 5, writable: true, minVersion: 3}, true case DLCR: return dpRegisterInfo{name: "DLCR", offset: 0x04, bank: 1, readable: true, writable: true, minVersion: 1}, true case TARGETID: diff --git a/docs/capabilities.md b/docs/capabilities.md index 5582c57..14d49a4 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -250,7 +250,8 @@ See [JTAG](protocols/jtag.md) for effects and ownership. | Managed target-memory writes | Yes | `WriteScalar` and `WriteBlock` are effectful. The caller selects the address; the API checks alignment and range, not whether that address is safe to modify. `WriteRawAP` remains an unmanaged escape hatch. | | Block reads | Yes | Accepts empty, unaligned, and mixed-width ranges. No auto-incrementing word run crosses a 1 KiB TAR boundary. If the MEM-AP does not accept single address increment, the reader writes TAR before each word. It uses the ordinary DAP WAIT policy. If selection, framing, or cleanup becomes uncertain, repair is required. A FAULT returns only the confirmed prefix. Cancellation and transport or protocol failures can also interrupt the read. Unread destination bytes remain untouched. | | Block writes | Yes | Uses the block-read geometry, bounded chunks, and the binding's WAIT policy. If single address increment is unavailable, `WriteBlock` writes TAR before each word. Accepted writes are never replayed. SWD confirms buffered chunks through RDBUFF; sequential JTAG checks CTRL/STAT after each write and can return a confirmed prefix within a chunk. An uncertain write reports `ErrIndeterminate` and invalidates the MEM-AP without replay. | -| Later JTAG-DP versions and ADIv6 | No | JTAG uses the original ADIv5 register set, without version detection or banked DP registers. | +| DPv3 discovery registers | Yes | SW-DP supports DPIDR1, BASEPTR0/1, SELECT1, and bank-zero DPIDR reads. AP access still uses the ADIv5 layout. | +| Later JTAG-DP versions | No | JTAG uses the original ADIv5 register set, without version detection or banked DP registers. | | Behavioral simulation | Yes | DP identity/power, posted AP access, and byte-addressed MEM-AP reads and writes in either target byte order. AP fixtures take `dap.APSel` values and reject duplicate selectors, zero APIDRs, non-MEM-AP identities passed to `AddMEMAP`, and unaligned target-word addresses. | | DAP-composed SWD entry | HIL | The FT232H/Cortex-M AP, transaction, and MEM-AP tests each counted one SWD connection performed by `DebugPort.Connect`; the reconnect test counted two. | | JTAG-DP and AP1 memory identity | HIL | On Nostalgia, FT4232H `01691`/A at 100 kHz with Arm `0x5ba00477`/IR4 and Xilinx `0x14730093`/IR12: two fresh direct-driver sessions and two fresh discovered-probe sessions passed. AP1 IDR was `0x44770002`; component words at `0x80410ff0` through `0x80410ffc` were `0x0d`, `0x90`, `0x05`, `0xb1`. Every session restored CSW/TAR and owned power state, released the chain, and closed the probe; fresh sessions found the same inherited power/control state. Board activation was external; no halt, target reset, or target-memory write was exercised. See the [DAP bench procedure](ports/dap.md#ftdi-jtag-dp-bench). | diff --git a/docs/ports/dap.md b/docs/ports/dap.md index 6ae7e78..007ac30 100644 --- a/docs/ports/dap.md +++ b/docs/ports/dap.md @@ -574,3 +574,25 @@ cancellation, read failures, WAIT retries, and continued memory access. Shared SWD/JTAG simulations exercise large addresses in both byte orders. The [CoreSight guide](../coresight.md#hardware-evidence) records the advertised addresses and identity reads observed on the micro:bit and ZCU104 benches. + +## DPv3 discovery registers + +On SW-DP version 3, `ReadDP` supports `DPIDR1`, `BASEPTR0`, and `BASEPTR1`; +`WriteDP` supports `SELECT1`. DPIDR reads select bank zero on this version. +These registers are rejected on earlier debug ports and baseline JTAG-DP. +Reading the discovery registers alone does not discover or acquire an AP. + +```go +width, err := dp.ReadDP(ctx, dap.DPIDR1) +if err != nil { + return err +} +base, err := dp.ReadDP(ctx, dap.BASEPTR0) +if err != nil { + return err +} +fmt.Printf("DPIDR1=%#08x BASEPTR0=%#08x\n", width, base) +``` + +The caller retains the connected debug port and must release it afterward. +AP addressing and MEM-AP access currently use the ADIv5 register layout. From c2739152828c353c1a4c0563b611b5fac5bfb1b8 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sun, 20 Sep 2026 14:26:47 -0700 Subject: [PATCH 2/7] Address ADIv6 access ports through their debug-space bases. ADIv6 uses aligned AP base addresses and 4 KiB register windows instead of ADIv5 APSEL indices. Reusing an index would select the wrong register. Keep those selectors distinct, check them against the connected DP and its advertised address width, and select both address words when needed. Immediate AP reads and writes retain posted completion and existing recovery. Queued AP operations and MEM-AP acquisition remain ADIv5-only. --- armdebug/conn_integration_test.go | 16 +++ cmsisdap/session_integration_test.go | 2 +- dap/ap.go | 76 ++++++++------ dap/ap_integration_test.go | 2 +- dap/apv2.go | 62 ++++++++++++ dap/apv2_test.go | 143 +++++++++++++++++++++++++++ dap/doc.go | 8 +- dap/jtag_integration_test.go | 4 +- dap/jtag_memory_test.go | 4 +- dap/lifecycle.go | 19 +++- dap/memap.go | 3 + dap/memap_debugbase_test.go | 2 +- dap/memap_integration_test.go | 2 +- dap/memap_test.go | 2 +- dap/sim/debugbase.go | 2 +- dap/sim/target.go | 51 +++++++--- dap/swddp.go | 9 +- dap/txn.go | 27 +++-- dap/txn_sequential_test.go | 8 +- docs/architecture.md | 6 +- docs/capabilities.md | 2 +- docs/composition.md | 8 +- docs/ports/dap.md | 23 ++++- jlink/session_integration_test.go | 2 +- 24 files changed, 399 insertions(+), 84 deletions(-) create mode 100644 dap/apv2.go create mode 100644 dap/apv2_test.go diff --git a/armdebug/conn_integration_test.go b/armdebug/conn_integration_test.go index 8717c5d..29af94c 100644 --- a/armdebug/conn_integration_test.go +++ b/armdebug/conn_integration_test.go @@ -65,6 +65,22 @@ func TestHILArmConnection(t *testing.T) { } t.Logf("%s=%#08x", reg, value) } + + } + if selected := os.Getenv("OSTIOLE_ARMDEBUG_HIL_AP_BASE"); selected != "" { + base, err := strconv.ParseUint(selected, 0, 64) + if err != nil { + t.Fatal(err) + } + sel, err := dap.APAt(base) + if err != nil { + t.Fatal(err) + } + id, err := c.Port().ReadAPIDR(ctx, sel) + if err != nil { + t.Fatal(err) + } + t.Logf("AP@%#x IDR=%#08x", base, id.Raw) } if inspectMemory { inspectMemoryHIL(t, ctx, c, ap) diff --git a/cmsisdap/session_integration_test.go b/cmsisdap/session_integration_test.go index f3e4348..2e1ebdc 100644 --- a/cmsisdap/session_integration_test.go +++ b/cmsisdap/session_integration_test.go @@ -191,7 +191,7 @@ func observeCMSISDAPTarget(t *testing.T, ctx context.Context, readyOnOpen bool) func assertCMSISDAPAPRegister(t *testing.T, ctx context.Context, debugPort *dap.DebugPort, address uint8, want uint32) { t.Helper() - got, err := debugPort.ReadRawAP(ctx, dap.NewAPSel(0).Address(address)) + got, err := debugPort.ReadRawAP(ctx, dap.NewAPSel(0).Address(uint16(address))) if err != nil { t.Fatal(err) } diff --git a/dap/ap.go b/dap/ap.go index 0295665..3b341f7 100644 --- a/dap/ap.go +++ b/dap/ap.go @@ -7,9 +7,11 @@ import ( ) // APSel identifies one access port. Its zero value is invalid; construct a -// selector with NewAPSel. +// selector with NewAPSel or APAt. type APSel struct { index uint16 + base uint64 + v2 bool } // NewAPSel returns the selector for one ADIv5 access port. @@ -17,11 +19,11 @@ func NewAPSel(value uint8) APSel { return APSel{index: uint16(value) + 1} } -// Value returns the architectural selector value. The zero APSel returns an -// error. +// Value returns an ADIv5 selector value. +// It returns an error for ADIv6 or zero selectors. func (sel APSel) Value() (uint8, error) { - if sel.index == 0 { - return 0, errors.New("dap: zero APSel is invalid") + if sel.index == 0 || sel.v2 { + return 0, errors.New("dap: selector has no ADIv5 APSEL value") } return uint8(sel.index - 1), nil } @@ -30,19 +32,20 @@ func (sel APSel) Value() (uint8, error) { // invalid; derive an address from an APSel with Address. type APAddress struct { sel APSel - value uint8 + value uint16 } -// Address returns a complete ADIv5 access-port register address without +// Address returns a complete access-port register address without // sending traffic. The operation which uses the address reports an error if -// value is not four-byte aligned or sel is the zero value. -func (sel APSel) Address(value uint8) APAddress { +// value is not four-byte aligned, exceeds the register window (256 bytes for +// ADIv5, 4 KiB for ADIv6), or sel is invalid. +func (sel APSel) Address(value uint16) APAddress { return APAddress{sel: sel, value: value} } const apIDRAddress = uint8(0xfc) -// APIDRInfo contains the fields of an ADIv5 access-port identification +// APIDRInfo contains the fields of an Arm access-port identification // register. type APIDRInfo struct { Raw uint32 @@ -100,22 +103,22 @@ func (dp *DebugPort) ReadRawAP(ctx context.Context, addr APAddress) (uint32, err } func (dp *DebugPort) readAP(ctx context.Context, sel APSel, addr uint8) (uint32, error) { - _, value, err := dp.readAPEffect(ctx, sel, addr) + _, value, err := dp.readAPEffect(ctx, sel, sel.register(addr)) return value, err } -func (dp *DebugPort) readAPEffect(ctx context.Context, sel APSel, addr uint8) (bool, uint32, error) { - if err := validateRawAPAddress(addr, false); err != nil { +func (dp *DebugPort) readAPEffect(ctx context.Context, sel APSel, addr uint16) (bool, uint32, error) { + if _, err := validateAPAddress(APAddress{sel: sel, value: addr}, false); err != nil { return false, 0, err } if err := dp.selectAP(ctx, sel, addr); err != nil { return false, 0, err } if dp.jtag != nil { - result := dp.jtag.accessAP(ctx, apTransferRequest(addr&0x0c, true), 0) + result := dp.jtag.accessAP(ctx, apTransferRequest(uint8(addr&0x0c), true), 0) return result.outcome != transferUnsent && result.outcome != transferRejected, result.data, jtagResultError(result) } - return dp.conn.readAP(ctx, addr) + return dp.conn.readAP(ctx, uint8(addr&0x0c)) } // WriteRawAP writes the register at one complete access-port address and waits @@ -142,29 +145,32 @@ func (dp *DebugPort) WriteRawAP(ctx context.Context, addr APAddress, value uint3 } func (dp *DebugPort) writeAP(ctx context.Context, sel APSel, addr uint8, value uint32) error { - _, err := dp.writeAPEffect(ctx, sel, addr, value) + _, err := dp.writeAPEffect(ctx, sel, sel.register(addr), value) return err } -func (dp *DebugPort) writeAPEffect(ctx context.Context, sel APSel, addr uint8, value uint32) (bool, error) { - if err := validateRawAPAddress(addr, true); err != nil { +func (dp *DebugPort) writeAPEffect(ctx context.Context, sel APSel, addr uint16, value uint32) (bool, error) { + if _, err := validateAPAddress(APAddress{sel: sel, value: addr}, true); err != nil { return false, err } if err := dp.selectAP(ctx, sel, addr); err != nil { return false, err } if dp.jtag != nil { - result := dp.jtag.accessAP(ctx, apTransferRequest(addr&0x0c, false), value) + result := dp.jtag.accessAP(ctx, apTransferRequest(uint8(addr&0x0c), false), value) return result.outcome != transferUnsent && result.outcome != transferRejected, jtagResultError(result) } - return dp.conn.writeAP(ctx, addr, value) + return dp.conn.writeAP(ctx, uint8(addr&0x0c), value) } -func (dp *DebugPort) selectAP(ctx context.Context, sel APSel, addr uint8) error { - selection, err := validateAPSel(sel) +func (dp *DebugPort) selectAP(ctx context.Context, sel APSel, addr uint16) error { + selection, err := dp.validateSelector(sel) if err != nil { return err } + if sel.v2 { + return dp.selectAPAddress(ctx, selection+uint64(addr)) + } value := uint32(selection)<<24 | uint32(addr&0xf0) if !dp.state.selectDP.valid || dp.state.selectDP.value != value { if err := dp.writeDP(ctx, SELECT, value); err != nil { @@ -174,25 +180,39 @@ func (dp *DebugPort) selectAP(ctx context.Context, sel APSel, addr uint8) error return dp.confirmPendingSELECT(ctx) } -func validateAPSel(sel APSel) (uint8, error) { - return sel.Value() +func validateAPSel(sel APSel) (uint64, error) { + if sel.v2 { + return sel.base, nil + } + index, err := sel.Value() + return uint64(index), err } -func validateAPAddress(addr APAddress, write bool) (uint8, error) { +func validateAPAddress(addr APAddress, write bool) (uint16, error) { if _, err := validateAPSel(addr.sel); err != nil { return 0, err } - if err := validateRawAPAddress(addr.value, write); err != nil { + limit := uint16(0xff) + if addr.sel.v2 { + limit = 0xfff + } + if addr.value > limit { + return 0, fmt.Errorf("dap: AP register offset %#x exceeds %#x", addr.value, limit) + } + if write && addr.value == addr.sel.register(apIDRAddress) { + return 0, errors.New("dap: APIDR is read-only") + } + if err := validateRawAPAddress(addr.value, false); err != nil { return 0, err } return addr.value, nil } -func validateRawAPAddress(addr uint8, write bool) error { +func validateRawAPAddress(addr uint16, write bool) error { if addr&3 != 0 { return fmt.Errorf("dap: unaligned AP address %#02x", addr) } - if write && addr == apIDRAddress { + if write && addr == uint16(apIDRAddress) { return errors.New("dap: APIDR is read-only") } return nil diff --git a/dap/ap_integration_test.go b/dap/ap_integration_test.go index d429df8..b70d708 100644 --- a/dap/ap_integration_test.go +++ b/dap/ap_integration_test.go @@ -23,7 +23,7 @@ func TestAccessAPOverFTDI(t *testing.T) { defer cancel() dp := openHardwareDebugPort(t, ctx) - const hardwareAPCSW = uint8(0x00) + const hardwareAPCSW = uint16(0x00) var ( savedCSW uint32 saved bool diff --git a/dap/apv2.go b/dap/apv2.go new file mode 100644 index 0000000..d70952a --- /dev/null +++ b/dap/apv2.go @@ -0,0 +1,62 @@ +package dap + +import ( + "context" + "errors" + "fmt" +) + +// APAt returns a selector for a 4 KiB aligned ADIv6 access-port base. +// Address zero is valid. The connected debug port validates its address width. +func APAt(base uint64) (APSel, error) { + if base&0xfff != 0 { + return APSel{}, errors.New("dap: AP base must be 4 KiB aligned") + } + return APSel{base: base, v2: true}, nil +} + +// BaseAddress returns an ADIv6 selector's base. +// It returns an error for ADIv5 or zero selectors. +func (sel APSel) BaseAddress() (uint64, error) { + if !sel.v2 { + return 0, errors.New("dap: selector has no ADIv6 base address") + } + return sel.base, nil +} + +func (sel APSel) register(addr uint8) uint16 { + if sel.v2 { + return 0xd00 | uint16(addr) + } + return uint16(addr) +} + +func (dp *DebugPort) validateSelector(sel APSel) (uint64, error) { + value, err := validateAPSel(sel) + if err != nil { + return 0, err + } + if dp.reentryID.dpidr.Version > 3 || sel.v2 != (dp.reentryID.dpidr.Version == 3) { + return 0, errors.New("dap: AP selector does not match debug-port architecture") + } + if sel.v2 && value>>dp.addressBits != 0 { + return 0, fmt.Errorf("dap: AP base %#x exceeds %d bits", value, dp.addressBits) + } + return value, nil +} + +func (dp *DebugPort) selectAPAddress(ctx context.Context, address uint64) error { + bits := dp.addressBits + if dp.reentryID.dpidr.Version == 3 && bits > 32 { + if err := dp.writeDP(ctx, SELECT1, uint32(address>>32)); err != nil { + return err + } + if _, err := dp.readDP(ctx, RDBUFF); err != nil { + return err + } + } + if err := dp.writeDP(ctx, SELECT, uint32(address)&^15); err != nil { + return err + } + return dp.confirmPendingSELECT(ctx) +} diff --git a/dap/apv2_test.go b/dap/apv2_test.go new file mode 100644 index 0000000..ffad3fa --- /dev/null +++ b/dap/apv2_test.go @@ -0,0 +1,143 @@ +package dap_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/jon/ostiole/dap" + dapsim "github.com/jon/ostiole/dap/sim" + "github.com/jon/ostiole/swd" + swdsim "github.com/jon/ostiole/swd/sim" +) + +func TestAPv2Addressing(t *testing.T) { + target := dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, 40); err != nil { + t.Fatal(err) + } + low, err := dap.APAt(0x2000) + if err != nil { + t.Fatal(err) + } + high, err := dap.APAt(0x100002000) + if err != nil { + t.Fatal(err) + } + for sel, id := range map[dap.APSel]uint32{low: 0x34770008, high: 0x24770011} { + if err := target.AddAP(sel, id); err != nil { + t.Fatal(err) + } + } + dp := dap.NewDebugPort(dap.SWDP(swd.New(swdsim.New(target)))) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + defer func() { + if err := dp.Release(t.Context()); err != nil { + t.Error(err) + } + }() + for _, sel := range []dap.APSel{low, high, low} { + want := uint32(0x34770008) + if sel == high { + want = 0x24770011 + } + got, err := dp.ReadAPIDR(t.Context(), sel) + if err != nil || got.Raw != want { + t.Fatalf("AP IDR = %#x, %v; want %#x", got.Raw, err, want) + } + + } +} + +func TestAPv2RejectsInvalidAddressesBeforeTraffic(t *testing.T) { + target := newWaitTarget() + target.Target = dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, 20); err != nil { + t.Fatal(err) + } + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + before := len(target.requests) + base, _ := dap.APAt(0x2000) + outside, _ := dap.APAt(1 << 20) + for _, addr := range []dap.APAddress{base.Address(0x1000), base.Address(3), outside.Address(0), dap.NewAPSel(0).Address(0xfc)} { + if _, err := dp.ReadRawAP(t.Context(), addr); err == nil { + t.Fatalf("accepted %+v", addr) + } + } + if err := dp.WriteRawAP(t.Context(), base.Address(0xdfc), 1); err == nil { + t.Fatal("wrote APIDR") + } + if len(target.requests) != before { + t.Fatal("rejected access sent traffic") + } + if _, err := dap.APAt(1); err == nil { + t.Fatal("unaligned AP accepted") + } + zero, err := dap.APAt(0) + if err != nil || zero == (dap.APSel{}) || zero == dap.NewAPSel(0) { + t.Fatal("valid AP zero aliases another selector") + } + if _, err := zero.Value(); err == nil { + t.Fatal("APv2 exposed APSEL") + } + if _, err := dap.NewAPSel(0).BaseAddress(); err == nil { + t.Fatal("APv1 exposed a base") + } +} + +func TestAPv2ReleaseClearsUpperSelection(t *testing.T) { + for _, fail := range []bool{false, true} { + t.Run(fmt.Sprintf("failure=%v", fail), func(t *testing.T) { checkAPv2ReleaseClearsUpperSelection(t, fail) }) + } +} + +func checkAPv2ReleaseClearsUpperSelection(t *testing.T, fail bool) { + t.Helper() + target := newWaitTarget() + target.Target = dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, 40); err != nil { + t.Fatal(err) + } + low, _ := dap.APAt(0) + high, _ := dap.APAt(1 << 32) + for sel, id := range map[dap.APSel]uint32{low: 0x34770008, high: 0x24770011} { + if err := target.AddAP(sel, id); err != nil { + t.Fatal(err) + } + } + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := dp.ReadAPIDR(t.Context(), high); err != nil { + t.Fatal(err) + } + if fail { + target.writeErrFor = dpWrite(4) + target.writeErr = errors.New("lost SELECT1 response") + if err := dp.Release(t.Context()); err == nil { + t.Fatal("release succeeded despite a transfer failure") + } + before := len(target.requests) + assertRepairBlocksTraffic(t, dp, target, before) + } + if err := dp.Release(t.Context()); err != nil { + t.Fatal(err) + } + // Inspect the low AP after release without replacing SELECT1. + if err := target.Write(t.Context(), dpWrite(8), 0xdf0); err != nil { + t.Fatal(err) + } + if _, err := target.Read(t.Context(), apRead(12)); err != nil { + t.Fatal(err) + } + got, err := target.Read(t.Context(), dpRead(12)) + if err != nil || got != 0x34770008 { + t.Fatalf("released selection reads %#x, %v; want the low AP", got, err) + } +} diff --git a/dap/doc.go b/dap/doc.go index f809785..8020775 100644 --- a/dap/doc.go +++ b/dap/doc.go @@ -40,9 +40,11 @@ // only the architectural DAPABORT value for ABORT. Later JTAG-DP versions and // version detection are not implemented. // -// NewAPSel constructs an access-port selector; the zero APSel is invalid. -// APSel.Address combines a selector with a complete eight-bit ADIv5 AP address; -// the resulting APAddress also has an invalid zero value. ReadAPIDR reads and +// NewAPSel constructs an ADIv5 index selector; APAt constructs an ADIv6 +// base-address selector. +// The zero APSel is invalid. APSel.Address combines a selector with a complete +// register offset (eight bits for ADIv5, twelve for ADIv6). The resulting +// APAddress also has an invalid zero value. ReadAPIDR reads and // decodes the common read-only AP identity. ReadRawAP and WriteRawAP reject // invalid or unaligned addresses before traffic. Raw access has the effects // defined by the selected AP class; writing a MEM-AP data register can write diff --git a/dap/jtag_integration_test.go b/dap/jtag_integration_test.go index aece42d..a5b78a5 100644 --- a/dap/jtag_integration_test.go +++ b/dap/jtag_integration_test.go @@ -99,7 +99,7 @@ func exerciseJTAGDPMemory(t *testing.T, ctx context.Context, discovered bool) (u t.Logf("AP1 IDR=%#08x", idr.Raw) saved := make(map[uint8]uint32) for _, addr := range []uint8{0, 4} { - saved[addr], err = dp.ReadRawAP(ctx, sel.Address(addr)) + saved[addr], err = dp.ReadRawAP(ctx, sel.Address(uint16(addr))) if err != nil { t.Fatal(err) } @@ -123,7 +123,7 @@ func exerciseJTAGDPMemory(t *testing.T, ctx context.Context, discovered bool) (u t.Fatal(err) } for addr, want := range saved { - got, err := dp.ReadRawAP(ctx, sel.Address(addr)) + got, err := dp.ReadRawAP(ctx, sel.Address(uint16(addr))) if err != nil || got != want { t.Fatalf("restored AP1 register %#x = %#08x, expected %#08x: %v", addr, got, want, err) } diff --git a/dap/jtag_memory_test.go b/dap/jtag_memory_test.go index 4c3ae58..3d0db55 100644 --- a/dap/jtag_memory_test.go +++ b/dap/jtag_memory_test.go @@ -78,7 +78,7 @@ func exerciseSharedMEMAP(t *testing.T, link string, bigEndian bool) { t.Fatal(err) } for addr, value := range map[uint8]uint32{0: 0xa5000051, 4: 0x9988, 8: 1} { - if err := dp.WriteRawAP(t.Context(), sel.Address(addr), value); err != nil { + if err := dp.WriteRawAP(t.Context(), sel.Address(uint16(addr)), value); err != nil { t.Fatal(err) } } @@ -95,7 +95,7 @@ func exerciseSharedMEMAP(t *testing.T, link string, bigEndian bool) { t.Fatal(err) } for addr, want := range map[uint8]uint32{0: 0xa5000051, 4: 0x9988, 8: 1} { - if got, err := dp.ReadRawAP(t.Context(), sel.Address(addr)); err != nil || got != want { + if got, err := dp.ReadRawAP(t.Context(), sel.Address(uint16(addr))); err != nil || got != want { t.Fatalf("restored AP %#x = %#x, %v", addr, got, err) } } diff --git a/dap/lifecycle.go b/dap/lifecycle.go index 07a9024..ded231f 100644 --- a/dap/lifecycle.go +++ b/dap/lifecycle.go @@ -119,6 +119,18 @@ func (dp *DebugPort) initialize(ctx context.Context, raw uint32) (Identity, uint if err != nil { return Identity{}, 0, err } + if dpidr.Version == 3 { + width, err := dp.readDP(ctx, DPIDR1) + if err != nil { + return Identity{}, 0, err + } + dp.addressBits = uint8(width & 0x7f) + switch dp.addressBits { + case 12, 20, 32, 40, 48, 52: + default: + return Identity{}, 0, fmt.Errorf("dap: unsupported ADIv6 address width %d", dp.addressBits) + } + } return info, state, nil } @@ -168,11 +180,8 @@ func (dp *DebugPort) Release(ctx context.Context) error { return fmt.Errorf("dap: restore protocol state for release: %w", err) } } - if err := dp.writeDP(releaseCtx, SELECT, 0); err != nil { - return err - } - if _, err := dp.readDP(releaseCtx, RDBUFF); err != nil { - return fmt.Errorf("dap: confirm SELECT while releasing debug port: %w", err) + if err := dp.selectAPAddress(releaseCtx, 0); err != nil { + return fmt.Errorf("dap: clear selection while releasing debug port: %w", err) } if err := dp.releasePower(releaseCtx); err != nil { return err diff --git a/dap/memap.go b/dap/memap.go index 7cbc770..bc3f3cd 100644 --- a/dap/memap.go +++ b/dap/memap.go @@ -69,6 +69,9 @@ type MemAP struct { // It performs AP traffic. A returned MemAP must be paired with MemAP.Release. // The debug port must be connected and must not have cleanup pending. func OpenMemAP(ctx context.Context, dp *DebugPort, sel APSel) (*MemAP, error) { + if sel.v2 { + return nil, errors.New("dap: ADIv6 MEM-AP access is not supported") + } selection, err := validateAPSel(sel) if err != nil { return nil, err diff --git a/dap/memap_debugbase_test.go b/dap/memap_debugbase_test.go index d4d9e8d..0b6883d 100644 --- a/dap/memap_debugbase_test.go +++ b/dap/memap_debugbase_test.go @@ -150,7 +150,7 @@ func TestReadMEMAPDebugBaseFailureAndRetry(t *testing.T) { } releaseDebugBaseTest(t, mem.Release) for _, reg := range []uint8{0, 4, 8} { - got, readErr := dp.ReadRawAP(t.Context(), apSel(17).Address(reg)) + got, readErr := dp.ReadRawAP(t.Context(), apSel(17).Address(uint16(reg))) if readErr != nil || got != 0 { t.Fatalf("restored register %#x=%#x, %v", reg, got, readErr) } diff --git a/dap/memap_integration_test.go b/dap/memap_integration_test.go index bcfd8aa..5b5993e 100644 --- a/dap/memap_integration_test.go +++ b/dap/memap_integration_test.go @@ -62,7 +62,7 @@ func TestReadMEMAPWordOverFTDI(t *testing.T) { func assertHardwareAPRegister(t *testing.T, ctx context.Context, dp *dap.DebugPort, address uint8, want uint32) { t.Helper() - got, err := dp.ReadRawAP(ctx, hardwareAP.Address(address)) + got, err := dp.ReadRawAP(ctx, hardwareAP.Address(uint16(address))) if err != nil { t.Fatal(err) } diff --git a/dap/memap_test.go b/dap/memap_test.go index 4b839e3..7828c82 100644 --- a/dap/memap_test.go +++ b/dap/memap_test.go @@ -848,7 +848,7 @@ func TestNilMEMAPClient(t *testing.T) { func assertAPRegister(t *testing.T, dp *dap.DebugPort, address uint8, want uint32) { t.Helper() - got, err := dp.ReadRawAP(t.Context(), apSel(0).Address(address)) + got, err := dp.ReadRawAP(t.Context(), apSel(0).Address(uint16(address))) if err != nil { t.Fatal(err) } diff --git a/dap/sim/debugbase.go b/dap/sim/debugbase.go index 71000cc..c2f7452 100644 --- a/dap/sim/debugbase.go +++ b/dap/sim/debugbase.go @@ -15,7 +15,7 @@ func (t *Target) SetMEMAPDebugBase(sel dap.APSel, low, high uint32) error { if t == nil { return errors.New("dap/sim: nil target") } - selection, err := sel.Value() + selection, err := selectorValue(sel) if err != nil { return err } diff --git a/dap/sim/target.go b/dap/sim/target.go index 4f62ee4..c7a6147 100644 --- a/dap/sim/target.go +++ b/dap/sim/target.go @@ -177,7 +177,7 @@ func (t *Target) AddAP(sel dap.APSel, idr uint32) error { if idr == 0 { return errors.New("dap/sim: APIDR must be nonzero") } - selection, err := sel.Value() + selection, err := selectorValue(sel) if err != nil { return err } @@ -197,7 +197,7 @@ func (t *Target) AddMEMAP(sel dap.APSel, idr uint32, words map[uint32]uint32) er if idr == 0 || dap.DecodeAPIDR(idr).Class != 8 { return errors.New("dap/sim: MEM-AP requires a nonzero class-8 APIDR") } - selection, err := sel.Value() + selection, err := selectorValue(sel) if err != nil { return err } @@ -227,7 +227,7 @@ func (t *Target) SetMEMAPCFG(sel dap.APSel, cfg uint32) error { if t == nil { return errors.New("dap/sim: nil target") } - selection, err := sel.Value() + selection, err := selectorValue(sel) if err != nil { return err } @@ -254,7 +254,7 @@ func (t *Target) SetMEMAPSizes(sel dap.APSel, sizes ...dap.TransferSize) error { if t == nil { return errors.New("dap/sim: nil target") } - selection, err := sel.Value() + selection, err := selectorValue(sel) if err != nil { return err } @@ -284,7 +284,7 @@ func (t *Target) SetMEMAPBytes(sel dap.APSel, addr uint64, data []byte) error { if t == nil { return errors.New("dap/sim: nil target") } - selection, err := sel.Value() + selection, err := selectorValue(sel) if err != nil { return err } @@ -306,7 +306,7 @@ func (t *Target) MEMAPBytes(sel dap.APSel, addr uint64, size int) ([]byte, error if t == nil { return nil, errors.New("dap/sim: nil target") } - selection, err := sel.Value() + selection, err := selectorValue(sel) if err != nil { return nil, err } @@ -409,12 +409,15 @@ func validateRequest(req swdsim.Request, read bool) error { func (t *Target) readAP(req swdsim.Request) (uint32, error) { posted := t.rdbuff - ap := t.aps[dap.NewAPSel(uint8(t.selectDP>>24))] + ap := t.aps[t.selectedAP()] if ap == nil { t.rdbuff = 0 return posted, nil } - reg := t.apReg(req) + reg, err := t.apReg(req) + if err != nil { + return 0, err + } value, err := ap.readRegister(reg) if err != nil { return 0, err @@ -424,11 +427,15 @@ func (t *Target) readAP(req swdsim.Request) (uint32, error) { } func (t *Target) writeAP(req swdsim.Request, value uint32) error { - ap := t.aps[dap.NewAPSel(uint8(t.selectDP>>24))] + ap := t.aps[t.selectedAP()] if ap == nil { return nil } - return ap.writeRegister(t.apReg(req), value) + reg, err := t.apReg(req) + if err != nil { + return err + } + return ap.writeRegister(reg, value) } func (ap *accessPort) readRegister(reg uint8) (uint32, error) { @@ -592,9 +599,12 @@ func (ap *accessPort) writeMemoryValue(addr uint64, width int, value uint64) { } } -func (t *Target) apReg(req swdsim.Request) uint8 { +func (t *Target) apReg(req swdsim.Request) (uint8, error) { + if t.dpidr>>12&15 == 3 && t.selectDP&0xf00 != 0xd00 { + return 0, errors.New("dap/sim: AP register outside modeled register window") + } bank := uint8(t.selectDP>>4) & 0x0f - return bank<<4 | req.Addr + return bank<<4 | req.Addr, nil } func transferSizeEncoding(size dap.TransferSize) (uint8, bool) { @@ -678,3 +688,20 @@ func (t *Target) writeBankedRegister(value uint32) error { } return nil } + +func selectorValue(sel dap.APSel) (uint64, error) { + if base, err := sel.BaseAddress(); err == nil { + return base, nil + } + index, err := sel.Value() + return uint64(index), err +} + +func (t *Target) selectedAP() dap.APSel { + if t.dpidr>>12&15 == 3 { + address := uint64(t.selectHigh)<<32 | uint64(t.selectDP&^0xfff) + sel, _ := dap.APAt(address) + return sel + } + return dap.NewAPSel(uint8(t.selectDP >> 24)) +} diff --git a/dap/swddp.go b/dap/swddp.go index 1e4eba0..72bd6c3 100644 --- a/dap/swddp.go +++ b/dap/swddp.go @@ -55,6 +55,7 @@ type DebugPort struct { cleanupTimeout time.Duration identity Identity identified bool + addressBits uint8 reentryID Identity reentryKnown bool state debugPortState @@ -241,13 +242,13 @@ func (dp *DebugPort) validateDPWrite(reg DPRegister, value uint32) (dpRegisterIn } func (dp *DebugPort) validateBankedDPRegister(info dpRegisterInfo) error { - if dp.state.session != sessionConnected || !dp.identified { + if dp.state.session == sessionIdle || !dp.reentryKnown { return errors.New("dap: banked DP access requires an active connection") } - if dp.identity.dpidr.Version > 3 { - return fmt.Errorf("dap: banked DP access does not support DPv%d", dp.identity.dpidr.Version) + if dp.reentryID.dpidr.Version > 3 { + return fmt.Errorf("dap: banked DP access does not support DPv%d", dp.reentryID.dpidr.Version) } - if dp.identity.dpidr.Version < info.minVersion { + if dp.reentryID.dpidr.Version < info.minVersion { return fmt.Errorf("dap: %s requires DPv%d or later", info.name, info.minVersion) } return nil diff --git a/dap/txn.go b/dap/txn.go index deb1d08..dea0303 100644 --- a/dap/txn.go +++ b/dap/txn.go @@ -67,7 +67,7 @@ type txnOp struct { kind txnOpKind dpReg DPRegister apSel APSel - apAddr uint8 + apAddr uint16 data uint32 values []uint32 preserveAP bool @@ -113,7 +113,7 @@ func (t *Txn) WriteDP(reg DPRegister, value uint32) *WriteResult { // ReadAPIDR queues a read of one access-port identification register. The // result value is the raw register encoding. func (t *Txn) ReadAPIDR(sel APSel) *ReadResult { - return &ReadResult{result: t.queue(txnOp{kind: txnReadAPIDR, apSel: sel, apAddr: apIDRAddress})} + return &ReadResult{result: t.queue(txnOp{kind: txnReadAPIDR, apSel: sel, apAddr: sel.register(apIDRAddress)})} } // ReadRawAP queues one posted access-port read. A read that completes or might @@ -132,20 +132,20 @@ func (t *Txn) WriteRawAP(addr APAddress, value uint32) *WriteResult { } func (t *Txn) readAP(sel APSel, addr uint8) *ReadResult { - return &ReadResult{result: t.queue(txnOp{kind: txnReadRawAP, apSel: sel, apAddr: addr, preserveAP: true})} + return &ReadResult{result: t.queue(txnOp{kind: txnReadRawAP, apSel: sel, apAddr: sel.register(addr), preserveAP: true})} } func (t *Txn) writeAP(sel APSel, addr uint8, value uint32) *WriteResult { - return &WriteResult{result: t.queue(txnOp{kind: txnWriteRawAP, apSel: sel, apAddr: addr, data: value, preserveAP: true})} + return &WriteResult{result: t.queue(txnOp{kind: txnWriteRawAP, apSel: sel, apAddr: sel.register(addr), data: value, preserveAP: true})} } func (t *Txn) writeAPSequence(sel APSel, addr uint8, values []uint32) *WriteResult { - op := txnOp{kind: txnWriteAPSequence, apSel: sel, apAddr: addr, values: append([]uint32(nil), values...), preserveAP: true} + op := txnOp{kind: txnWriteAPSequence, apSel: sel, apAddr: sel.register(addr), values: append([]uint32(nil), values...), preserveAP: true} return &WriteResult{result: t.queue(op)} } func (t *Txn) readAPSequential(sel APSel, addr uint8) *ReadResult { - return &ReadResult{result: t.queue(txnOp{kind: txnReadAPSequential, apSel: sel, apAddr: addr, preserveAP: true})} + return &ReadResult{result: t.queue(txnOp{kind: txnReadAPSequential, apSel: sel, apAddr: sel.register(addr), preserveAP: true})} } func (t *Txn) queue(op txnOp) *txnResult { @@ -218,7 +218,7 @@ func (t *Txn) validate() error { case txnWriteDP: _, op.err = t.dp.validateDPWrite(op.dpReg, op.data) case txnReadAPIDR: - _, op.err = validateAPSel(op.apSel) + _, op.err = t.dp.validateSelector(op.apSel) case txnReadRawAP, txnReadAPSequential: _, op.err = validateAPAddress(APAddress{sel: op.apSel, value: op.apAddr}, false) case txnWriteRawAP, txnWriteAPSequence: @@ -227,6 +227,13 @@ func (t *Txn) validate() error { op.err = errors.Join(op.err, errors.New("dap: empty access-port write sequence")) } } + if op.apSel != (APSel{}) { + _, err := t.dp.validateSelector(op.apSel) + op.err = errors.Join(op.err, err) + } + if op.apSel.v2 { + op.err = errors.New("dap: ADIv6 AP transactions are not supported") + } if op.err != nil { errs = append(errs, op.err) } @@ -341,7 +348,7 @@ func (p *swdTxnPlanner) lowerSequentialAP(start int, ops []txnOp) { p.selectValue(start, value) p.settleSELECT(start) for i := range ops { - step := txnStep{req: apTransferRequest(ops[i].apAddr&0x0c, true), op: start + i, apRead: true} + step := txnStep{req: apTransferRequest(uint8(ops[i].apAddr&0x0c), true), op: start + i, apRead: true} if i > 0 { step.op-- step.deliver = true @@ -360,7 +367,7 @@ func (p *swdTxnPlanner) lowerAPWriteSequence(index int, op txnOp) { p.settleSELECT(index) for i, data := range op.values { p.steps = append(p.steps, txnStep{ - req: apTransferRequest(op.apAddr&0x0c, false), + req: apTransferRequest(uint8(op.apAddr&0x0c), false), data: data, op: index, operationStarted: i > 0, @@ -433,7 +440,7 @@ func (p *swdTxnPlanner) lowerAP(index int, op txnOp) { p.settleSELECT(index) read := op.kind == txnReadAPIDR || op.kind == txnReadRawAP invalidatesAP := op.kind == txnReadRawAP || op.kind == txnWriteRawAP - req := apTransferRequest(addr&0x0c, read) + req := apTransferRequest(uint8(addr&0x0c), read) p.steps = append(p.steps, txnStep{ apRead: op.kind == txnReadRawAP, apWrite: op.kind == txnWriteRawAP, diff --git a/dap/txn_sequential_test.go b/dap/txn_sequential_test.go index 65902ea..b41a0aa 100644 --- a/dap/txn_sequential_test.go +++ b/dap/txn_sequential_test.go @@ -6,9 +6,9 @@ func TestTxnPlannerPipelinesSequentialAPReads(t *testing.T) { dp := &DebugPort{} sel := NewAPSel(2) ops := []txnOp{ - {kind: txnReadAPSequential, apSel: sel, apAddr: memAPDRW}, - {kind: txnReadAPSequential, apSel: sel, apAddr: memAPDRW}, - {kind: txnReadAPSequential, apSel: sel, apAddr: memAPDRW}, + {kind: txnReadAPSequential, apSel: sel, apAddr: uint16(memAPDRW)}, + {kind: txnReadAPSequential, apSel: sel, apAddr: uint16(memAPDRW)}, + {kind: txnReadAPSequential, apSel: sel, apAddr: uint16(memAPDRW)}, } steps := newSWDTxnPlanner(dp).plan(ops) if len(steps) != 6 { @@ -35,7 +35,7 @@ func TestTxnPlannerPipelinesSequentialAPReads(t *testing.T) { func TestTxnPlannerBuffersAPWriteSequence(t *testing.T) { dp := &DebugPort{} - ops := []txnOp{{kind: txnWriteAPSequence, apSel: NewAPSel(3), apAddr: memAPDRW, values: []uint32{1, 2, 3}}} + ops := []txnOp{{kind: txnWriteAPSequence, apSel: NewAPSel(3), apAddr: uint16(memAPDRW), values: []uint32{1, 2, 3}}} steps := newSWDTxnPlanner(dp).plan(ops) if len(steps) != 6 { t.Fatalf("write steps = %+v, want SELECT, its barrier, three AP writes, and RDBUFF", steps) diff --git a/docs/architecture.md b/docs/architecture.md index 97062ed..afd4444 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -264,8 +264,10 @@ bounds each independent recovery attempt: one second by default for SWD, thirty for JTAG. No driver is reopened automatically after a poisoned exchange. -`NewAPSel` constructs an AP selector whose zero value is invalid. -`APSel.Address` combines it with a complete eight-bit register address; the +`NewAPSel` constructs an ADIv5 index; `APAt` constructs an ADIv6 base-address +selector. Both return `APSel` values; the zero `APSel` remains invalid. +`APSel.Address` combines a selector with an eight-bit ADIv5 or twelve-bit +ADIv6 register offset; the resulting `APAddress` also has an invalid zero value. `ReadAPIDR` reads and decodes the common read-only AP identity. Raw AP access rejects invalid or unaligned addresses before traffic. Register names and effects remain diff --git a/docs/capabilities.md b/docs/capabilities.md index 14d49a4..943bea2 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -250,7 +250,7 @@ See [JTAG](protocols/jtag.md) for effects and ownership. | Managed target-memory writes | Yes | `WriteScalar` and `WriteBlock` are effectful. The caller selects the address; the API checks alignment and range, not whether that address is safe to modify. `WriteRawAP` remains an unmanaged escape hatch. | | Block reads | Yes | Accepts empty, unaligned, and mixed-width ranges. No auto-incrementing word run crosses a 1 KiB TAR boundary. If the MEM-AP does not accept single address increment, the reader writes TAR before each word. It uses the ordinary DAP WAIT policy. If selection, framing, or cleanup becomes uncertain, repair is required. A FAULT returns only the confirmed prefix. Cancellation and transport or protocol failures can also interrupt the read. Unread destination bytes remain untouched. | | Block writes | Yes | Uses the block-read geometry, bounded chunks, and the binding's WAIT policy. If single address increment is unavailable, `WriteBlock` writes TAR before each word. Accepted writes are never replayed. SWD confirms buffered chunks through RDBUFF; sequential JTAG checks CTRL/STAT after each write and can return a confirmed prefix within a chunk. An uncertain write reports `ErrIndeterminate` and invalidates the MEM-AP without replay. | -| DPv3 discovery registers | Yes | SW-DP supports DPIDR1, BASEPTR0/1, SELECT1, and bank-zero DPIDR reads. AP access still uses the ADIv5 layout. | +| DPv3 discovery registers | Yes | SW-DP supports DPIDR1, BASEPTR0/1, SELECT1, and bank-zero DPIDR reads. Immediate AP access supports ADIv6 base addresses and 4 KiB register windows. Queued AP operations and MEM-AP acquisition still require ADIv5. | | Later JTAG-DP versions | No | JTAG uses the original ADIv5 register set, without version detection or banked DP registers. | | Behavioral simulation | Yes | DP identity/power, posted AP access, and byte-addressed MEM-AP reads and writes in either target byte order. AP fixtures take `dap.APSel` values and reject duplicate selectors, zero APIDRs, non-MEM-AP identities passed to `AddMEMAP`, and unaligned target-word addresses. | | DAP-composed SWD entry | HIL | The FT232H/Cortex-M AP, transaction, and MEM-AP tests each counted one SWD connection performed by `DebugPort.Connect`; the reconnect test counted two. | diff --git a/docs/composition.md b/docs/composition.md index bba1912..3fdf642 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -29,7 +29,7 @@ data-register write can write target memory. | Connect SWD or transfer DP/AP registers | `swd.New`, `Conn.Connect`, `Conn.ReadDP`, `Conn.WriteDP`, `Conn.ReadAP`, `Conn.WriteAP`, `Conn.NewBatch`, `Conn.Release` | `examples/trivial/swd-dpidr` | | Enter SWD, decode a DPIDR, and manage SW-DP power | `dap.NewDebugPort`, `DebugPort.Connect`, `DebugPort.Release` | `ost dap dp id` | | Identify one explicitly selected AP | `DebugPort.ReadAPIDR`, `DecodeAPIDR` | `examples/simple/ap-id` | -| Access another AP register by its full ADIv5 address | `DebugPort.ReadRawAP`, `DebugPort.WriteRawAP` | Package tests | +| Access another AP register by its full register address | `DebugPort.ReadRawAP`, `DebugPort.WriteRawAP` | Package tests | | Read or write one aligned target scalar through a MEM-AP | `dap.OpenMemAP`, `MemAP.ReadScalar`, `MemAP.WriteScalar`, `MemAP.Release` | `examples/simple/cortexm-info` uses `ReadWord`. | | Read or write arbitrary target bytes through a MEM-AP | `dap.OpenMemAP`, `MemAP.ReadBlock`, `MemAP.WriteBlock`, `MemAP.Release` | Package tests | | Obtain a MEM-AP's advertised debug entry | `MemAP.ReadDebugBase` | `examples/simple/coresight-info` | @@ -635,8 +635,10 @@ exclusive, serialized use of its `swd.Conn`; direct transfers on that connection can invalidate cached DAP state. DP, AP, transaction, and MEM-AP operations require an active connection. `ReadDP` and `WriteDP` take logical ADIv5 register names and manage DPBANKSEL without exposing a current-bank -API. `NewAPSel` constructs an AP selector whose zero value is invalid. -`APSel.Address` combines it with a complete eight-bit register address; the +API. `NewAPSel` constructs an ADIv5 index; `APAt` constructs an ADIv6 base-address +selector. Both return `APSel` values; the zero `APSel` remains invalid. +`APSel.Address` combines a selector with an eight-bit ADIv5 or twelve-bit +ADIv6 register offset; the resulting `APAddress` also has an invalid zero value. `ReadAPIDR` reads and decodes the common read-only AP identity. `EnumerateAPs` scans every ADIv5 AP selector without reading class-specific registers. Raw AP access rejects diff --git a/docs/ports/dap.md b/docs/ports/dap.md index 007ac30..ee7341c 100644 --- a/docs/ports/dap.md +++ b/docs/ports/dap.md @@ -595,4 +595,25 @@ fmt.Printf("DPIDR1=%#08x BASEPTR0=%#08x\n", width, base) ``` The caller retains the connected debug port and must release it afterward. -AP addressing and MEM-AP access currently use the ADIv5 register layout. +`dap.APAt(base)` constructs an ADIv6 selector; `NewAPSel(index)` remains the +ADIv5 constructor. `ReadAPIDR` selects the appropriate IDR offset. Raw +register access accepts a twelve-bit ADIv6 offset, and rejects a selector for +the wrong architecture or an address beyond DPIDR1.ASIZE before AP traffic. +Queued AP operations and MEM-AP acquisition still require ADIv5. + +```go +ap, err := dap.APAt(0x2000) +if err != nil { + return err +} +id, err := dp.ReadAPIDR(ctx, ap) +if err != nil { + return err +} +fmt.Printf("AP IDR=%#08x\n", id.Raw) +``` + +`APSel.Address` now accepts `uint16` rather than `uint8`. Untyped constants +remain unchanged; callers with a typed byte offset change +`ap.Address(offset)` to `ap.Address(uint16(offset))`. `Value` returns only an +ADIv5 index; use `BaseAddress` for an ADIv6 selector. diff --git a/jlink/session_integration_test.go b/jlink/session_integration_test.go index 890c2ff..a8e8f74 100644 --- a/jlink/session_integration_test.go +++ b/jlink/session_integration_test.go @@ -212,7 +212,7 @@ func observeJLinkTarget(t *testing.T, ctx context.Context, readyOnOpen bool) tar func assertAPRegister(t *testing.T, ctx context.Context, debugPort *dap.DebugPort, address uint8, want uint32) { t.Helper() - got, err := debugPort.ReadRawAP(ctx, dap.NewAPSel(0).Address(address)) + got, err := debugPort.ReadRawAP(ctx, dap.NewAPSel(0).Address(uint16(address))) if err != nil { t.Fatal(err) } From 88e49657f6f9fada9cf766afd9627b2a5cf17430 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sun, 20 Sep 2026 14:33:25 -0700 Subject: [PATCH 3/7] Access ADIv6 memory through existing managed owners. ADIv6 MEM-APs place their registers at offset 0xd00. Use that layout inside the existing memory client and accept base-address selectors in armdebug so callers retain the same acquisition and cleanup rules. Share sequential transaction execution with JTAG while retaining packed ADIv5 SWD transfers. Complete each AP operation before advancing, preserve confirmed block prefixes, and keep failed restoration retryable. Reject error modes that could suppress failures the client needs to observe. --- armdebug/conn_integration_test.go | 44 +++++--- armdebug/memap.go | 7 +- armdebug/memap_test.go | 45 ++++++++ dap/adiv6_test.go | 3 +- dap/apv2.go | 11 ++ dap/doc.go | 5 +- dap/jtag_txn.go | 67 ------------ dap/lifecycle.go | 3 + dap/memap.go | 13 ++- dap/memap_v2_test.go | 168 ++++++++++++++++++++++++++++++ dap/swddp.go | 3 + dap/txn.go | 26 +++-- dap/txn_serial.go | 122 ++++++++++++++++++++++ dap/txn_v3_test.go | 134 ++++++++++++++++++++++++ docs/architecture.md | 7 +- docs/capabilities.md | 2 +- docs/ports/dap.md | 28 ++++- 17 files changed, 579 insertions(+), 109 deletions(-) delete mode 100644 dap/jtag_txn.go create mode 100644 dap/memap_v2_test.go create mode 100644 dap/txn_serial.go create mode 100644 dap/txn_v3_test.go diff --git a/armdebug/conn_integration_test.go b/armdebug/conn_integration_test.go index 29af94c..6f2400e 100644 --- a/armdebug/conn_integration_test.go +++ b/armdebug/conn_integration_test.go @@ -11,6 +11,7 @@ import ( "time" "github.com/jon/ostiole/armdebug" + "github.com/jon/ostiole/coresight" "github.com/jon/ostiole/dap" "github.com/jon/ostiole/discover" _ "github.com/jon/ostiole/discover/probes" @@ -67,7 +68,18 @@ func TestHILArmConnection(t *testing.T) { } } + + if inspectMemory { + inspectMemoryHIL(t, ctx, c, ap) + } +} + +func memorySelectionHIL(t *testing.T) (dap.APSel, bool) { + t.Helper() if selected := os.Getenv("OSTIOLE_ARMDEBUG_HIL_AP_BASE"); selected != "" { + if os.Getenv("OSTIOLE_ARMDEBUG_HIL_AP") != "" { + t.Fatal("select either an AP index or base") + } base, err := strconv.ParseUint(selected, 0, 64) if err != nil { t.Fatal(err) @@ -76,19 +88,8 @@ func TestHILArmConnection(t *testing.T) { if err != nil { t.Fatal(err) } - id, err := c.Port().ReadAPIDR(ctx, sel) - if err != nil { - t.Fatal(err) - } - t.Logf("AP@%#x IDR=%#08x", base, id.Raw) - } - if inspectMemory { - inspectMemoryHIL(t, ctx, c, ap) + return sel, true } -} - -func memorySelectionHIL(t *testing.T) (dap.APSel, bool) { - t.Helper() selected := os.Getenv("OSTIOLE_ARMDEBUG_HIL_AP") if selected == "" { return dap.APSel{}, false @@ -102,10 +103,6 @@ func memorySelectionHIL(t *testing.T) (dap.APSel, bool) { func inspectMemoryHIL(t *testing.T, ctx context.Context, c *armdebug.Conn, ap dap.APSel) { t.Helper() - index, err := ap.Value() - if err != nil { - t.Fatal(err) - } id, err := c.Port().ReadAPIDR(ctx, ap) if err != nil { t.Fatal(err) @@ -118,7 +115,20 @@ func inspectMemoryHIL(t *testing.T, ctx context.Context, c *armdebug.Conn, ap da if err != nil { t.Fatal(err) } - t.Logf("AP%d IDR=%#08x CPUID=%#08x", index, id.Raw, processor.Raw) + base, present, err := memory.ReadDebugBase(ctx) + if err != nil { + t.Fatal(err) + } + t.Logf("%s IDR=%#08x CPUID=%#08x debug_base=%#x present=%v", ap, id.Raw, processor.Raw, base, present) + if present && os.Getenv("OSTIOLE_ARMDEBUG_HIL_WALK") == "1" { + visits, err := coresight.Walk(ctx, memory, base, coresight.WalkLimits{MaxDepth: 8, MaxComponents: 64, MaxEntries: 256}) + for _, visit := range visits { + t.Logf("ROM component=%+v error=%v", visit.Component, visit.Err) + } + if err != nil { + t.Fatal(err) + } + } } func closeHIL(t *testing.T, c *armdebug.Conn) { diff --git a/armdebug/memap.go b/armdebug/memap.go index 9a8ee32..b1741a0 100644 --- a/armdebug/memap.go +++ b/armdebug/memap.go @@ -3,7 +3,6 @@ package armdebug import ( "context" "errors" - "fmt" "github.com/jon/ostiole/dap" ) @@ -28,13 +27,9 @@ func (c *Conn) OpenMemAP(ctx context.Context, ap dap.APSel) (*dap.MemAP, error) if err := ctx.Err(); err != nil { return nil, err } - index, err := ap.Value() - if err != nil { - return nil, err - } for _, owned := range c.memories { if owned.selection == ap { - return nil, fmt.Errorf("armdebug: AP %d is already owned", index) + return nil, errors.New("armdebug: access port is already owned") } } client, err := dap.OpenMemAP(ctx, c.port, ap) diff --git a/armdebug/memap_test.go b/armdebug/memap_test.go index b9baf45..b0ab8d1 100644 --- a/armdebug/memap_test.go +++ b/armdebug/memap_test.go @@ -7,6 +7,7 @@ import ( "github.com/jon/ostiole/armdebug" "github.com/jon/ostiole/dap" + dapsim "github.com/jon/ostiole/dap/sim" "github.com/jon/ostiole/probe" swdsim "github.com/jon/ostiole/swd/sim" ) @@ -124,3 +125,47 @@ func TestMemAPCleanupFailureKeepsProbe(t *testing.T) { t.Fatalf("cleanup retry: %v", err) } } + +func TestOwnedAPv2Memory(t *testing.T) { + b := newBench() + b.target.Target = dapsim.New(0x4c013477) + if err := b.target.SetDPRegister(dap.DPIDR1, 20); err != nil { + t.Fatal(err) + } + sel, err := dap.APAt(0x2000) + if err != nil { + t.Fatal(err) + } + if err := b.target.AddMEMAP(sel, 0x34770008, map[uint32]uint32{0x100: 7}); err != nil { + t.Fatal(err) + } + c, err := armdebug.Connect(t.Context(), probe.New(probe.Info{}, b), config()) + if err != nil { + t.Fatal(err) + } + mem, err := c.OpenMemAP(t.Context(), sel) + if err != nil { + t.Fatal(err) + } + if got, err := mem.ReadWord(t.Context(), 0x100); err != nil || got != 7 { + t.Fatalf("read=%#x,%v", got, err) + } + before := b.transfers + if _, err := c.OpenMemAP(t.Context(), sel); err == nil || b.transfers != before { + t.Fatal("duplicate AP reached hardware") + } + failure := errors.New("restore failed") + b.target.beforeWrite = func(req swdsim.Request, selected, value uint32) error { + if req.AP && req.Addr == 4 && selected&^15 == 0x2d00 { + return failure + } + return nil + } + if err := c.Close(); !errors.Is(err, failure) || b.closes != 0 { + t.Fatalf("close=%v, probe closes=%d", err, b.closes) + } + b.target.beforeWrite = nil + if err := c.Close(); err != nil || b.closes != 1 { + t.Fatalf("retry=%v, probe closes=%d", err, b.closes) + } +} diff --git a/dap/adiv6_test.go b/dap/adiv6_test.go index 106c77b..d7ee57a 100644 --- a/dap/adiv6_test.go +++ b/dap/adiv6_test.go @@ -1,11 +1,12 @@ package dap_test import ( + "testing" + "github.com/jon/ostiole/dap" dapsim "github.com/jon/ostiole/dap/sim" "github.com/jon/ostiole/swd" swdsim "github.com/jon/ostiole/swd/sim" - "testing" ) func TestDPv3BankedIdentity(t *testing.T) { diff --git a/dap/apv2.go b/dap/apv2.go index d70952a..0ece96d 100644 --- a/dap/apv2.go +++ b/dap/apv2.go @@ -60,3 +60,14 @@ func (dp *DebugPort) selectAPAddress(ctx context.Context, address uint64) error } return dp.confirmPendingSELECT(ctx) } + +// String formats an ADIv5 index or an ADIv6 base address. +func (sel APSel) String() string { + if sel.v2 { + return fmt.Sprintf("AP@%#x", sel.base) + } + if sel.index != 0 { + return fmt.Sprintf("AP%d", sel.index-1) + } + return "invalid AP" +} diff --git a/dap/doc.go b/dap/doc.go index 8020775..9a0afb5 100644 --- a/dap/doc.go +++ b/dap/doc.go @@ -78,8 +78,9 @@ // // A Txn queues an ordered group of DP and AP operations. Commit validates the // complete queue, settles any earlier immediate DP write, then sends queued -// traffic through a private SWD or JTAG executor. SWD retains its packed frames; -// JTAG executes logical operations sequentially and checks CTRL/STAT after each +// traffic through a private SWD or JTAG executor. ADIv5 SWD retains packed +// frames; ADIv6 and JTAG complete each logical AP operation in order. JTAG +// checks CTRL/STAT after each // AP operation, because its acknowledgement combines OK and FAULT. // ReadResult.Value reports data from a queued read; WriteResult.Err reports // completion of a queued write. DP writes and AP operations settle diff --git a/dap/jtag_txn.go b/dap/jtag_txn.go deleted file mode 100644 index 3a1f5a6..0000000 --- a/dap/jtag_txn.go +++ /dev/null @@ -1,67 +0,0 @@ -package dap - -import ( - "context" - "errors" -) - -func (e *jtagExecutor) executeTxn(ctx context.Context, txn *Txn) error { - for i := range txn.ops { - op := &txn.ops[i] - value, err := e.executeOp(ctx, op) - op.result.resolve(value, err) - if err != nil { - txn.resolveSuffix(i + 1) - return err - } - } - return nil -} - -func (e *jtagExecutor) executeOp(ctx context.Context, op *txnOp) (uint32, error) { - if err := ctx.Err(); err != nil { - return 0, err - } - switch op.kind { - case txnReadDP: - return e.dp.readDP(ctx, op.dpReg) - case txnWriteDP: - return 0, e.dp.writeDP(ctx, op.dpReg, op.data) - case txnWriteAPSequence: - return 0, e.writeSequence(ctx, op) - default: - return e.executeAP(ctx, op) - } -} - -func (e *jtagExecutor) executeAP(ctx context.Context, op *txnOp) (uint32, error) { - generation := e.dp.state.apGeneration - var value uint32 - var possible bool - var err error - if op.kind == txnWriteRawAP { - possible, err = e.dp.writeAPEffect(ctx, op.apSel, op.apAddr, op.data) - } else { - possible, value, err = e.dp.readAPEffect(ctx, op.apSel, op.apAddr) - } - raw := op.kind == txnWriteRawAP || op.kind == txnReadRawAP - if possible && raw && (!op.preserveAP || errors.Is(err, ErrIndeterminate)) && e.dp.state.apGeneration == generation { - e.dp.state.invalidateAP() - } - return value, err -} - -func (e *jtagExecutor) writeSequence(ctx context.Context, op *txnOp) error { - for _, value := range op.values { - possible, err := e.dp.writeAPEffect(ctx, op.apSel, op.apAddr, value) - if possible { - op.accepted++ - } - if err != nil { - op.uncertainWrite = possible - return err - } - op.confirmed++ - } - return nil -} diff --git a/dap/lifecycle.go b/dap/lifecycle.go index ded231f..e699afa 100644 --- a/dap/lifecycle.go +++ b/dap/lifecycle.go @@ -120,6 +120,9 @@ func (dp *DebugPort) initialize(ctx context.Context, raw uint32) (Identity, uint return Identity{}, 0, err } if dpidr.Version == 3 { + if state&(1<<24) != 0 { + return Identity{}, 0, errors.New("dap: DP ERRMODE is not supported") + } width, err := dp.readDP(ctx, DPIDR1) if err != nil { return Identity{}, 0, err diff --git a/dap/memap.go b/dap/memap.go index bc3f3cd..0c61aac 100644 --- a/dap/memap.go +++ b/dap/memap.go @@ -69,9 +69,6 @@ type MemAP struct { // It performs AP traffic. A returned MemAP must be paired with MemAP.Release. // The debug port must be connected and must not have cleanup pending. func OpenMemAP(ctx context.Context, dp *DebugPort, sel APSel) (*MemAP, error) { - if sel.v2 { - return nil, errors.New("dap: ADIv6 MEM-AP access is not supported") - } selection, err := validateAPSel(sel) if err != nil { return nil, err @@ -94,6 +91,9 @@ func OpenMemAP(ctx context.Context, dp *DebugPort, sel APSel) (*MemAP, error) { if err != nil { return nil, err } + if err := validateMemAPErrorMode(sel, cfg, csw); err != nil { + return nil, err + } var tarhi uint32 if cfg&cfgLargeAddr != 0 { tarhi, err = dp.readAP(ctx, sel, memAPTARHI) @@ -417,3 +417,10 @@ func (m *MemAP) prepareRelease(ctx context.Context) (context.Context, context.Ca } return releaseCtx, cancel, nil } + +func validateMemAPErrorMode(sel APSel, cfg, csw uint32) error { + if sel.v2 && (cfg&0xf00 > 0x100 || csw&(3<<16) != 0) { + return errors.New("dap: unsupported ADIv6 MEM-AP error handling") + } + return nil +} diff --git a/dap/memap_v2_test.go b/dap/memap_v2_test.go new file mode 100644 index 0000000..55201af --- /dev/null +++ b/dap/memap_v2_test.go @@ -0,0 +1,168 @@ +package dap_test + +import ( + "bytes" + "errors" + "fmt" + "testing" + + "github.com/jon/ostiole/dap" + dapsim "github.com/jon/ostiole/dap/sim" +) + +func TestAPv2MemoryAndRestoration(t *testing.T) { + dp, sel := newAPv2MemoryPort(t) + defer func() { + if err := dp.Release(t.Context()); err != nil { + t.Error(err) + } + }() + for offset, value := range map[uint16]uint32{0xd00: 0x23000042, 0xd04: 0x20000100} { + if err := dp.WriteRawAP(t.Context(), sel.Address(offset), value); err != nil { + t.Fatal(err) + } + } + mem, err := dap.OpenMemAP(t.Context(), dp, sel) + if err != nil { + t.Fatal(err) + } + if got, err := mem.ReadWord(t.Context(), 0xe000ed00); err != nil || got != 0x411fd210 { + t.Fatalf("CPUID=%#x,%v", got, err) + } + if base, present, err := mem.ReadDebugBase(t.Context()); err != nil || !present || base != 0xe00ff000 { + t.Fatalf("base=%#x,%v,%v", base, present, err) + } + checkAPv2BlockRoundTrip(t, mem) + if err := mem.Release(t.Context()); err != nil { + t.Fatal(err) + } + for offset, want := range map[uint16]uint32{0xd00: 0x23000042, 0xd04: 0x20000100} { + got, err := dp.ReadRawAP(t.Context(), sel.Address(offset)) + if err != nil || got != want { + t.Fatalf("restore %#x=%#x,%v", offset, got, err) + } + } +} + +func TestAPv2ReleaseRetriesAfterFramingLoss(t *testing.T) { + target := newWaitTarget() + target.Target = dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, 40); err != nil { + t.Fatal(err) + } + sel, _ := dap.APAt(0x100002000) + if err := target.AddMEMAP(sel, 0x34770008, map[uint32]uint32{0x100: 7}); err != nil { + t.Fatal(err) + } + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + if err := dp.WriteRawAP(t.Context(), sel.Address(0xd04), 0x200); err != nil { + t.Fatal(err) + } + mem, err := dap.OpenMemAP(t.Context(), dp, sel) + if err != nil { + t.Fatal(err) + } + failure := errors.New("lost USB response") + target.writeErrFor = apWrite(4) + target.writeErr = failure + if _, err := mem.ReadWord(t.Context(), 0x100); !errors.Is(err, failure) { + t.Fatalf("read=%v", err) + } + target.writeErr = failure + if err := mem.Release(t.Context()); err == nil { + t.Fatal("cleanup succeeded despite continuing transfer failure") + } + target.writeErr = nil + if err := mem.Release(t.Context()); err != nil { + t.Fatal(err) + } + if err := dp.Release(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + got, err := dp.ReadRawAP(t.Context(), sel.Address(0xd04)) + if err != nil || got != 0x200 { + t.Fatalf("restored TAR=%#x,%v", got, err) + } + if err := dp.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestAPv2RejectsDeferredErrorModes(t *testing.T) { + for _, csw := range []uint32{1 << 16, 1 << 17} { + t.Run(fmt.Sprintf("%x", csw), func(t *testing.T) { + target := newWaitTarget() + target.Target = dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, 20); err != nil { + t.Fatal(err) + } + sel, _ := dap.APAt(0x2000) + if err := target.AddMEMAP(sel, 0x34770008, nil); err != nil { + t.Fatal(err) + } + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + if err := dp.WriteRawAP(t.Context(), sel.Address(0xd00), csw); err != nil { + t.Fatal(err) + } + if _, err := dap.OpenMemAP(t.Context(), dp, sel); err == nil { + t.Fatal("accepted a MEM-AP that can hide errors") + } + before := len(target.requests) + if err := dp.WriteDP(t.Context(), dap.CTRLSTAT, 1<<24|1); err == nil { + t.Fatal("accepted DP ERRMODE") + } + if len(target.requests) != before { + t.Fatal("invalid DP mode reached hardware") + } + if err := dp.Release(t.Context()); err != nil { + t.Fatal(err) + } + }) + } +} + +func newAPv2MemoryPort(t *testing.T) (*dap.DebugPort, dap.APSel) { + t.Helper() + target := newWaitTarget() + target.Target = dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, 40); err != nil { + t.Fatal(err) + } + sel, err := dap.APAt(0x100002000) + if err != nil { + t.Fatal(err) + } + if err := target.AddMEMAP(sel, 0x34770008, map[uint32]uint32{0xe000ed00: 0x411fd210}); err != nil { + t.Fatal(err) + } + if err := target.SetMEMAPDebugBase(sel, 0xe00ff003, 0); err != nil { + t.Fatal(err) + } + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + + return dp, sel +} + +func checkAPv2BlockRoundTrip(t *testing.T, mem *dap.MemAP) { + t.Helper() + data := bytes.Repeat([]byte{1, 2, 3, 4}, 32) + if n, err := mem.WriteBlock(t.Context(), 0x200003e1, data); err != nil || n != len(data) { + t.Fatalf("write=%d,%v", n, err) + } + got := make([]byte, len(data)) + if n, err := mem.ReadBlock(t.Context(), 0x200003e1, got); err != nil || n != len(data) || !bytes.Equal(got, data) { + t.Fatalf("read=%d,%v %x", n, err, got) + } +} diff --git a/dap/swddp.go b/dap/swddp.go index 72bd6c3..4941685 100644 --- a/dap/swddp.go +++ b/dap/swddp.go @@ -235,6 +235,9 @@ func (dp *DebugPort) validateDPWrite(reg DPRegister, value uint32) (dpRegisterIn if reg == CTRLSTAT && (value&overrunDetect != 0) != (dp.state.response == responseOverrun) { return dpRegisterInfo{}, errors.New("dap: write CTRL/STAT: ORUNDETECT is owned by the SWD connection") } + if reg == CTRLSTAT && dp.reentryID.dpidr.Version == 3 && value&(1<<24) != 0 { + return dpRegisterInfo{}, errors.New("dap: DP ERRMODE is not supported") + } if reg == DLCR && value&dlcrTurnaroundMask != 0 { return dpRegisterInfo{}, errors.New("dap: write DLCR: variable turnaround requires unsupported SWD framing") } diff --git a/dap/txn.go b/dap/txn.go index dea0303..f9ef168 100644 --- a/dap/txn.go +++ b/dap/txn.go @@ -78,12 +78,12 @@ type txnOp struct { uncertainWrite bool } -// Txn queues an ordered, single-use sequence of ADIv5 DP and AP operations. +// Txn queues an ordered, single-use sequence of DP and AP operations. // Calls sharing the transaction, its DebugPort, or the underlying connection // or chain must be serialized. Queued operations have the same effects, // WAIT behavior, and lifecycle requirements as the corresponding DebugPort // methods. Commit can pack their physical SWD requests while preserving -// logical result order. JTAG completes and checks each AP operation in order. +// logical result order. JTAG and ADIv6 complete each AP operation in order. type Txn struct { dp *DebugPort ops []txnOp @@ -191,8 +191,8 @@ func (t *Txn) Commit(ctx context.Context) error { t.resolveInvalid() return err } - if t.dp.jtag != nil { - return t.dp.jtag.executeTxn(ctx, t) + if t.dp.jtag != nil || t.dp.reentryID.dpidr.Version == 3 { + return t.dp.executeSequentialTxn(ctx, t) } return t.dp.conn.executeTxn(ctx, t) } @@ -231,9 +231,6 @@ func (t *Txn) validate() error { _, err := t.dp.validateSelector(op.apSel) op.err = errors.Join(op.err, err) } - if op.apSel.v2 { - op.err = errors.New("dap: ADIv6 AP transactions are not supported") - } if op.err != nil { errs = append(errs, op.err) } @@ -365,6 +362,10 @@ func (p *swdTxnPlanner) lowerAPWriteSequence(index int, op txnOp) { value := uint32(selection)<<24 | uint32(op.apAddr&0xf0) p.selectValue(index, value) p.settleSELECT(index) + p.appendAPWriteSequence(index, op) +} + +func (p *swdTxnPlanner) appendAPWriteSequence(index int, op txnOp) { for i, data := range op.values { p.steps = append(p.steps, txnStep{ req: apTransferRequest(uint8(op.apAddr&0x0c), false), @@ -438,11 +439,16 @@ func (p *swdTxnPlanner) lowerAP(index int, op txnOp) { value := uint32(selection)<<24 | uint32(addr&0xf0) p.selectValue(index, value) p.settleSELECT(index) - read := op.kind == txnReadAPIDR || op.kind == txnReadRawAP + p.appendAP(index, op) +} + +func (p *swdTxnPlanner) appendAP(index int, op txnOp) { + addr := op.apAddr + read := op.kind == txnReadAPIDR || op.kind == txnReadRawAP || op.kind == txnReadAPSequential invalidatesAP := op.kind == txnReadRawAP || op.kind == txnWriteRawAP req := apTransferRequest(uint8(addr&0x0c), read) p.steps = append(p.steps, txnStep{ - apRead: op.kind == txnReadRawAP, + apRead: op.kind == txnReadRawAP || op.kind == txnReadAPSequential, apWrite: op.kind == txnWriteRawAP, req: req, data: op.data, @@ -456,7 +462,7 @@ func (p *swdTxnPlanner) lowerAP(index int, op txnOp) { deliver: true, deliverValue: read, operationStarted: true, - apRead: op.kind == txnReadRawAP, + apRead: op.kind == txnReadRawAP || op.kind == txnReadAPSequential, apWrite: op.kind == txnWriteRawAP, completesWrite: op.kind == txnWriteRawAP, invalidatesAP: invalidatesAP && !op.preserveAP, diff --git a/dap/txn_serial.go b/dap/txn_serial.go new file mode 100644 index 0000000..3b4deb4 --- /dev/null +++ b/dap/txn_serial.go @@ -0,0 +1,122 @@ +package dap + +import ( + "context" + "errors" +) + +func (dp *DebugPort) executeSequentialTxn(ctx context.Context, txn *Txn) error { + if dp.conn != nil { + if err := dp.settlePreviousDPWrite(ctx); err != nil { + txn.resolveSuffix(0) + return err + } + } + for i := range txn.ops { + op := &txn.ops[i] + value, err := dp.executeOp(ctx, op) + op.result.resolve(value, err) + if err != nil { + txn.resolveSuffix(i + 1) + return err + } + } + return nil +} + +func (dp *DebugPort) executeOp(ctx context.Context, op *txnOp) (uint32, error) { + if err := ctx.Err(); err != nil { + return 0, err + } + if dp.conn != nil && op.kind != txnWriteAPSequence { + return dp.executeSWDOp(ctx, op) + } + switch op.kind { + case txnReadDP: + return dp.readDP(ctx, op.dpReg) + case txnWriteDP: + err := dp.writeDP(ctx, op.dpReg, op.data) + if err == nil && dp.conn != nil { + err = dp.settlePreviousDPWrite(ctx) + } + return 0, err + case txnWriteAPSequence: + return 0, dp.writeSequence(ctx, op) + default: + return dp.executeAP(ctx, op) + } +} + +func (dp *DebugPort) executeAP(ctx context.Context, op *txnOp) (uint32, error) { + generation := dp.state.apGeneration + var value uint32 + var possible bool + var err error + if op.kind == txnWriteRawAP { + possible, err = dp.writeAPEffect(ctx, op.apSel, op.apAddr, op.data) + } else { + possible, value, err = dp.readAPEffect(ctx, op.apSel, op.apAddr) + } + raw := op.kind == txnWriteRawAP || op.kind == txnReadRawAP + if possible && raw && (!op.preserveAP || errors.Is(err, ErrIndeterminate)) && dp.state.apGeneration == generation { + dp.state.invalidateAP() + } + return value, err +} + +func (dp *DebugPort) writeSequence(ctx context.Context, op *txnOp) error { + if dp.conn != nil { + return dp.writeSWDSequence(ctx, op) + } + for _, value := range op.values { + possible, err := dp.writeAPEffect(ctx, op.apSel, op.apAddr, value) + if possible { + op.accepted++ + } + if err != nil { + op.uncertainWrite = possible + return err + } + op.confirmed++ + } + return nil +} + +func (dp *DebugPort) executeSWDOp(ctx context.Context, op *txnOp) (uint32, error) { + txn := &Txn{dp: dp, ops: []txnOp{*op}} + if op.kind == txnReadDP || op.kind == txnWriteDP { + err := dp.conn.executeTxn(ctx, txn) + return op.result.value, err + } + if err := dp.selectAP(ctx, op.apSel, op.apAddr); err != nil { + return 0, err + } + planner := newSWDTxnPlanner(dp) + if op.kind == txnWriteAPSequence { + planner.appendAPWriteSequence(0, *op) + } else { + planner.appendAP(0, *op) + } + err := (&swdTxn{Txn: txn}).execute(ctx, planner.steps) + txn.recordSWDCompletions() + *op = txn.ops[0] + op.result.err = classifyPortError(op.result.err) + return op.result.value, classifyPortError(err) +} + +func (dp *DebugPort) writeSWDSequence(ctx context.Context, op *txnOp) error { + for _, value := range op.values { + part := *op + part.values = []uint32{value} + part.result = &txnResult{} + part.accepted, part.confirmed, part.uncertainWrite = 0, 0, false + _, err := dp.executeSWDOp(ctx, &part) + op.accepted += part.accepted + op.confirmed += part.confirmed + if err != nil { + op.uncertainWrite = part.uncertainWrite + return err + } + } + return nil +} diff --git a/dap/txn_v3_test.go b/dap/txn_v3_test.go new file mode 100644 index 0000000..c9931a0 --- /dev/null +++ b/dap/txn_v3_test.go @@ -0,0 +1,134 @@ +package dap_test + +import ( + "errors" + "testing" + + "github.com/jon/ostiole/dap" + dapsim "github.com/jon/ostiole/dap/sim" + "github.com/jon/ostiole/swd" + swdsim "github.com/jon/ostiole/swd/sim" +) + +func TestDPv3TransactionWriteFailures(t *testing.T) { + for _, mode := range []string{"AP transport", "DP transport", "AP rejected", "selection failed"} { + t.Run(mode, func(t *testing.T) { checkDPv3TransactionWriteFailures(t, mode) }) + } +} + +func checkDPv3TransactionWriteFailures(t *testing.T, mode string) { + t.Helper() + target := newWaitTarget() + target.Target = dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, 20); err != nil { + t.Fatal(err) + } + sel, _ := dap.APAt(0x2000) + if err := target.AddMEMAP(sel, 0x34770008, nil); err != nil { + t.Fatal(err) + } + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + defer func() { + if err := dp.Release(t.Context()); err != nil { + t.Error(err) + } + }() + failure := errors.New("lost response after write") + target.writeErrFor, target.writeErr = apWrite(4), failure + if mode == "DP transport" || mode == "selection failed" { + target.writeErrFor = dpWrite(8) + } + if mode == "AP rejected" { + target.writeErr = nil + target.armFault(apWrite(4)) + } + txn := dp.NewTxn() + prefix := txn.ReadDP(dap.DPIDR) + var write *dap.WriteResult + if mode == "DP transport" { + write = txn.WriteDP(dap.SELECT, 0) + } else { + write = txn.WriteRawAP(sel.Address(0xd04), 0x200) + } + suffix := txn.ReadDP(dap.DPIDR) + err := txn.Commit(t.Context()) + wantUncertain := mode == "AP transport" || mode == "DP transport" + if err == nil || errors.Is(err, dap.ErrIndeterminate) != wantUncertain { + t.Fatalf("Commit=%v; want uncertainty=%v", err, wantUncertain) + } + if errors.Is(write.Err(), dap.ErrIndeterminate) != wantUncertain { + t.Fatalf("write=%v; want uncertainty=%v", write.Err(), wantUncertain) + } + assertTxnValue(t, prefix, 0x4c013477) + if _, err := suffix.Value(); !errors.Is(err, dap.ErrNotExecuted) { + t.Fatalf("suffix=%v", err) + } +} + +func TestDPv3BlockWriteKeepsConfirmedPrefix(t *testing.T) { + target := &observedBlockWriteTarget{waitTarget: newWaitTarget()} + target.Target = dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, 20); err != nil { + t.Fatal(err) + } + sel, _ := dap.APAt(0x2000) + if err := target.AddMEMAP(sel, 0x34770008, nil); err != nil { + t.Fatal(err) + } + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + mem, err := dap.OpenMemAP(t.Context(), dp, sel) + if err != nil { + t.Fatal(err) + } + target.failAfter, target.failErr = 3, errors.New("lost write response") + n, err := mem.WriteBlock(t.Context(), 0x200, make([]byte, 32)) + if n != 8 || !errors.Is(err, dap.ErrIndeterminate) || target.writes != 3 { + t.Fatalf("write=%d,%v; executions=%d", n, err, target.writes) + } + if err := mem.Release(t.Context()); err != nil { + t.Fatal(err) + } + if err := dp.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestDPv3BlockWriteParityConfirmsWord(t *testing.T) { + target := &parityAfterDRWTarget{Target: dapsim.New(0x4c013477)} + if err := target.SetDPRegister(dap.DPIDR1, 20); err != nil { + t.Fatal(err) + } + sel, _ := dap.APAt(0x2000) + if err := target.AddMEMAP(sel, 0x34770008, nil); err != nil { + t.Fatal(err) + } + target.wire = &readParityWire{inner: swdsim.New(target)} + dp := dap.NewDebugPort(dap.SWDP(swd.New(target.wire))) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + mem, err := dap.OpenMemAP(t.Context(), dp, sel) + if err != nil { + t.Fatal(err) + } + n, err := mem.WriteBlock(t.Context(), 0x200, []byte{1, 2, 3, 4, 5, 6, 7, 8}) + if n != 4 || !errors.Is(err, swd.ErrParity) || errors.Is(err, dap.ErrIndeterminate) { + t.Fatalf("write=%d,%v; want one confirmed word and parity", n, err) + } + got, err := mem.ReadWord(t.Context(), 0x200) + if err != nil || got != 0x04030201 { + t.Fatalf("memory=%#x,%v", got, err) + } + if err := mem.Release(t.Context()); err != nil { + t.Fatal(err) + } + if err := dp.Release(t.Context()); err != nil { + t.Fatal(err) + } +} diff --git a/docs/architecture.md b/docs/architecture.md index afd4444..c5300f7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -221,7 +221,7 @@ SW-DP connection establishes only DPIDR; JTAG-DP establishes only IDCODE. Construct the opaque binding with `dap.SWDP(conn)` or `dap.JTAGDP(chain, tapIndex)` and pass it to `dap.NewDebugPort`. The zero binding is invalid; constructors send no traffic. The debug port enters the -bound protocol before applying ADIv5 policy. +bound protocol before applying DAP policy. Public DP, AP, transaction, and MEM-AP operations remain blocked until that connection is active. The debug port validates register availability and direction for the binding. On SWD it validates @@ -250,6 +250,11 @@ Connection setup validates the context and options before protocol entry. Power acquisition starts only after entry establishes the identity and control state; failed setup and ordinary release use the same link cleanup. +ADIv6 SW-DP selects AP register addresses through SELECT and SELECT1, using +the width advertised by DPIDR1. Its transactions complete each operation +before sending the next. ADIv6 MEM-APs use the existing restoration and +managed ownership rules. + The private JTAG executor owns DPACC/APACC framing and its delayed-response pipeline. It polls an accepted request to completion without replaying it, then checks CTRL/STAT after each AP operation before another AP operation is diff --git a/docs/capabilities.md b/docs/capabilities.md index 943bea2..23667ec 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -250,7 +250,7 @@ See [JTAG](protocols/jtag.md) for effects and ownership. | Managed target-memory writes | Yes | `WriteScalar` and `WriteBlock` are effectful. The caller selects the address; the API checks alignment and range, not whether that address is safe to modify. `WriteRawAP` remains an unmanaged escape hatch. | | Block reads | Yes | Accepts empty, unaligned, and mixed-width ranges. No auto-incrementing word run crosses a 1 KiB TAR boundary. If the MEM-AP does not accept single address increment, the reader writes TAR before each word. It uses the ordinary DAP WAIT policy. If selection, framing, or cleanup becomes uncertain, repair is required. A FAULT returns only the confirmed prefix. Cancellation and transport or protocol failures can also interrupt the read. Unread destination bytes remain untouched. | | Block writes | Yes | Uses the block-read geometry, bounded chunks, and the binding's WAIT policy. If single address increment is unavailable, `WriteBlock` writes TAR before each word. Accepted writes are never replayed. SWD confirms buffered chunks through RDBUFF; sequential JTAG checks CTRL/STAT after each write and can return a confirmed prefix within a chunk. An uncertain write reports `ErrIndeterminate` and invalidates the MEM-AP without replay. | -| DPv3 discovery registers | Yes | SW-DP supports DPIDR1, BASEPTR0/1, SELECT1, and bank-zero DPIDR reads. Immediate AP access supports ADIv6 base addresses and 4 KiB register windows. Queued AP operations and MEM-AP acquisition still require ADIv5. | +| DPv3 discovery registers | Yes | SW-DP supports DPIDR1, BASEPTR0/1, SELECT1, and bank-zero DPIDR reads. Immediate AP access supports ADIv6 base addresses and 4 KiB register windows. Queued AP operations, scalar/block MEM-AP access, and managed ownership support ADIv6 with sequential completion. | | Later JTAG-DP versions | No | JTAG uses the original ADIv5 register set, without version detection or banked DP registers. | | Behavioral simulation | Yes | DP identity/power, posted AP access, and byte-addressed MEM-AP reads and writes in either target byte order. AP fixtures take `dap.APSel` values and reject duplicate selectors, zero APIDRs, non-MEM-AP identities passed to `AddMEMAP`, and unaligned target-word addresses. | | DAP-composed SWD entry | HIL | The FT232H/Cortex-M AP, transaction, and MEM-AP tests each counted one SWD connection performed by `DebugPort.Connect`; the reconnect test counted two. | diff --git a/docs/ports/dap.md b/docs/ports/dap.md index ee7341c..c8be9bd 100644 --- a/docs/ports/dap.md +++ b/docs/ports/dap.md @@ -599,7 +599,10 @@ The caller retains the connected debug port and must release it afterward. ADIv5 constructor. `ReadAPIDR` selects the appropriate IDR offset. Raw register access accepts a twelve-bit ADIv6 offset, and rejects a selector for the wrong architecture or an address beyond DPIDR1.ASIZE before AP traffic. -Queued AP operations and MEM-AP acquisition still require ADIv5. +Queued AP operations and `OpenMemAP` also accept ADIv6 selectors. ADIv6 +transactions complete each operation before sending the next; ADIv5 SWD +retains its packed execution. The debug port rejects active DP ERRMODE, and +MEM-AP acquisition rejects error modes which can suppress or defer errors. ```go ap, err := dap.APAt(0x2000) @@ -617,3 +620,26 @@ fmt.Printf("AP IDR=%#08x\n", id.Raw) remain unchanged; callers with a typed byte offset change `ap.Address(offset)` to `ap.Address(uint16(offset))`. `Value` returns only an ADIv5 index; use `BaseAddress` for an ADIv6 selector. + +The same managed owner can acquire an ADIv6 MEM-AP: + +```go +ap, err := dap.APAt(0x2000) +if err != nil { + return err +} +memory, err := connection.OpenMemAP(ctx, ap) +if err != nil { + return err +} +processor, err := cortexm.Identify(ctx, memory) +if err != nil { + return err +} +fmt.Printf("CPUID=%#08x\n", processor.Raw) +``` + +Here `connection` is an open `armdebug.Conn`. Its `Close` restores acquired +MEM-AP state before releasing the debug port and probe; retain the owner and +retry if cleanup fails. Target-memory reads preserve the inherited access +attributes. This does not acquire or halt the processor. From 45812c1a91122c1096afa4e5000122b84a3c2910 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sun, 20 Sep 2026 14:37:32 -0700 Subject: [PATCH 4/7] Inspect ADIv6 debug components through the CoreSight walker. An ADIv6 debug port advertises a component tree in its own address space, separate from memory reached through a MEM-AP. Expose a borrowed word reader and decode BASEPTR0/1 so the existing bounded CoreSight walker can discover AP bases without a second ROM parser or a hard-coded scan. Keep raw-access invalidation visible, reject unsupported widths and malformed bases, and leave cleanup with the debug-port owner. --- armdebug/conn_integration_test.go | 24 +++++++ coresight/debugspace_test.go | 57 ++++++++++++++++ dap/debugspace.go | 78 ++++++++++++++++++++++ dap/debugspace_test.go | 107 ++++++++++++++++++++++++++++++ dap/sim/debugspace.go | 21 ++++++ dap/sim/target.go | 5 ++ docs/capabilities.md | 7 ++ docs/ports/dap.md | 31 +++++++++ 8 files changed, 330 insertions(+) create mode 100644 coresight/debugspace_test.go create mode 100644 dap/debugspace.go create mode 100644 dap/debugspace_test.go create mode 100644 dap/sim/debugspace.go diff --git a/armdebug/conn_integration_test.go b/armdebug/conn_integration_test.go index 6f2400e..af5afe1 100644 --- a/armdebug/conn_integration_test.go +++ b/armdebug/conn_integration_test.go @@ -69,6 +69,9 @@ func TestHILArmConnection(t *testing.T) { } + if os.Getenv("OSTIOLE_ARMDEBUG_HIL_DEBUG_SPACE") == "1" { + inspectDebugSpaceHIL(t, ctx, c.Port().DebugSpace()) + } if inspectMemory { inspectMemoryHIL(t, ctx, c, ap) } @@ -142,3 +145,24 @@ func closeHIL(t *testing.T, c *armdebug.Conn) { } t.Errorf("cleanup remains pending after three attempts: %v", err) } + +func inspectDebugSpaceHIL(t *testing.T, ctx context.Context, space dap.DebugSpace) { + t.Helper() + base, present, err := space.ReadDebugBase(ctx) + if err != nil || !present { + t.Fatalf("debug root=%#x,%v,%v", base, present, err) + } + visits, err := coresight.Walk(ctx, space, base, coresight.WalkLimits{MaxDepth: 8, MaxComponents: 64, MaxEntries: 256}) + for _, visit := range visits { + if visit.Component != nil { + t.Logf("debug-space base=%#x CIDR=%#08x PIDR=%#x DEVARCH=%#08x", visit.Component.Base, visit.Component.CIDR, visit.Component.PIDR, visit.Component.DEVARCH) + } + if visit.Err != nil { + t.Log(visit.Err) + } + } + t.Logf("debug-space visits=%d complete=%v", len(visits), err == nil) + if err != nil { + t.Fatal(err) + } +} diff --git a/coresight/debugspace_test.go b/coresight/debugspace_test.go new file mode 100644 index 0000000..4ffc376 --- /dev/null +++ b/coresight/debugspace_test.go @@ -0,0 +1,57 @@ +package coresight_test + +import ( + "errors" + "testing" + + "github.com/jon/ostiole/coresight" + "github.com/jon/ostiole/dap" + "github.com/jon/ostiole/dap/sim" + "github.com/jon/ostiole/swd" + swdsim "github.com/jon/ostiole/swd/sim" +) + +func TestWalkThroughDebugSpace(t *testing.T) { + target := sim.New(0x4c013477) + for reg, value := range map[dap.DPRegister]uint32{dap.DPIDR1: 20, dap.BASEPTR0: 1} { + if err := target.SetDPRegister(reg, value); err != nil { + t.Fatal(err) + } + } + root := memoryAt(0, 1) + root.words[0] = 0x2003 + root.words[4] = 0 + child := memoryAt(0x2000, 9) + child.words[0x2fbc] = 0x47700a17 + for _, memory := range []*componentMemory{root, child} { + for address, value := range memory.words { + if err := target.SetDebugWord(address, value); err != nil { + t.Fatal(err) + } + } + } + dp := dap.NewDebugPort(dap.SWDP(swd.New(swdsim.New(target)))) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { releaseSimulation(t, dp.Release) }) + space := dp.DebugSpace() + base, present, err := space.ReadDebugBase(t.Context()) + if err != nil || !present || base != 0 { + t.Fatalf("root=%#x,%v,%v", base, present, err) + } + visits, err := coresight.Walk(t.Context(), space, base, walkLimits()) + if err != nil || len(visits) != 2 { + t.Fatalf("walk=%+v,%v", visits, err) + } + arch, ok := visits[1].Component.Architecture() + if !ok || arch.ID != 0x0a17 || visits[1].Component.Base != 0x2000 { + t.Fatalf("AP=%+v", visits[1]) + } + limits := walkLimits() + limits.MaxComponents = 1 + visits, err = coresight.Walk(t.Context(), space, base, limits) + if !errors.Is(err, coresight.ErrWalkLimit) || len(visits) != 1 { + t.Fatalf("bounded walk=%+v,%v", visits, err) + } +} diff --git a/dap/debugspace.go b/dap/debugspace.go new file mode 100644 index 0000000..41d70c4 --- /dev/null +++ b/dap/debugspace.go @@ -0,0 +1,78 @@ +package dap + +import ( + "context" + "errors" + "fmt" +) + +// DebugSpace borrows the ADIv6 debug address space of a connected SW-DP. +// It owns no resources. Calls sharing its debug port must be serialized. +// Its zero value is invalid. +type DebugSpace struct{ dp *DebugPort } + +// DebugSpace returns a borrowed reader without traffic. Reads require DPv3. +// ReadScalar accesses AP registers, not memory behind a MEM-AP. Those reads +// can have component-specific effects and invalidate existing MemAP clients. +// Inspect the debug space before acquiring MEM-APs. +// The caller remains responsible for releasing dp. +func (dp *DebugPort) DebugSpace() DebugSpace { return DebugSpace{dp: dp} } + +func (s DebugSpace) check(ctx context.Context) error { + if err := s.dp.requireConnected(ctx); err != nil { + return err + } + if s.dp.reentryID.dpidr.Version != 3 { + return errors.New("dap: debug address space requires DPv3") + } + return ctx.Err() +} + +// ReadScalar reads one aligned 32-bit debug-space word. Other sizes are +// rejected before traffic. Ordinary raw-AP failure and cleanup rules apply. +func (s DebugSpace) ReadScalar(ctx context.Context, address uint64, size TransferSize) (uint64, error) { + if err := s.check(ctx); err != nil { + return 0, err + } + if size != Size32 || address&3 != 0 { + return 0, errors.New("dap: debug-space reads require aligned 32-bit words") + } + sel, err := APAt(address &^ 0xfff) + if err != nil { + return 0, err + } + value, err := s.dp.ReadRawAP(ctx, sel.Address(uint16(address&0xfff))) + return uint64(value), err +} + +// ReadDebugBase reads the DP's advertised discovery root. It is in the debug +// address space, distinct from a MEM-AP's target-memory debug base. Address +// zero is valid when present is true. An absent entry returns (0, false, nil); +// malformed or failed reads never return a partially assembled address. +func (s DebugSpace) ReadDebugBase(ctx context.Context) (address uint64, present bool, err error) { + if err := s.check(ctx); err != nil { + return 0, false, err + } + low, err := s.dp.ReadDP(ctx, BASEPTR0) + if err != nil { + return 0, false, err + } + if low&0xffe != 0 { + return 0, false, fmt.Errorf("dap: reserved bits in BASEPTR0 %#x", low) + } + if low&1 == 0 { + return 0, false, nil + } + var high uint32 + if s.dp.addressBits > 32 { + high, err = s.dp.ReadDP(ctx, BASEPTR1) + if err != nil { + return 0, false, err + } + } + address = uint64(high)<<32 | uint64(low&^0xfff) + if address>>s.dp.addressBits != 0 { + return 0, false, errors.New("dap: debug base exceeds address width") + } + return address, true, nil +} diff --git a/dap/debugspace_test.go b/dap/debugspace_test.go new file mode 100644 index 0000000..01a5f49 --- /dev/null +++ b/dap/debugspace_test.go @@ -0,0 +1,107 @@ +package dap_test + +import ( + "github.com/jon/ostiole/dap" + dapsim "github.com/jon/ostiole/dap/sim" + "testing" +) + +func TestDebugSpaceBaseAndReads(t *testing.T) { + target := newWaitTarget() + target.Target = dapsim.New(0x4c013477) + for reg, value := range map[dap.DPRegister]uint32{dap.DPIDR1: 40, dap.BASEPTR0: 0x2001, dap.BASEPTR1: 1} { + if err := target.SetDPRegister(reg, value); err != nil { + t.Fatal(err) + } + } + if err := target.SetDebugWord(0x100002ff0, 0xd); err != nil { + t.Fatal(err) + } + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + space := dp.DebugSpace() + base, present, err := space.ReadDebugBase(t.Context()) + if err != nil || !present || base != 0x100002000 { + t.Fatalf("base=%#x,%v,%v", base, present, err) + } + if got, err := space.ReadScalar(t.Context(), base+0xff0, dap.Size32); err != nil || got != 0xd { + t.Fatalf("word=%#x,%v", got, err) + } + before := len(target.requests) + for _, size := range []dap.TransferSize{0, dap.Size8, dap.Size16, dap.Size64} { + if _, err := space.ReadScalar(t.Context(), base, size); err == nil { + t.Fatal("unsupported size accepted") + } + } + if _, err := space.ReadScalar(t.Context(), 1, dap.Size32); err == nil { + t.Fatal("unaligned read") + } + if _, err := space.ReadScalar(t.Context(), 1<<40, dap.Size32); err == nil { + t.Fatal("out-of-range read") + } + if len(target.requests) != before { + t.Fatal("invalid read sent traffic") + } + if err := dp.Release(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := space.ReadScalar(t.Context(), base, dap.Size32); err == nil { + t.Fatal("released space read") + } +} + +func TestDebugSpaceBaseValidation(t *testing.T) { + for _, tt := range []struct { + name string + low uint32 + present, invalid bool + }{ + {"zero", 1, true, false}, {"absent", 0, false, false}, {"reserved", 3, false, true}, {"outside", 0x100001, false, true}, + } { + t.Run(tt.name, func(t *testing.T) { + target := newWaitTarget() + target.Target = dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, 20); err != nil { + t.Fatal(err) + } + if err := target.SetDPRegister(dap.BASEPTR0, tt.low); err != nil { + t.Fatal(err) + } + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + address, present, err := dp.DebugSpace().ReadDebugBase(t.Context()) + if address != 0 || present != tt.present || (err != nil) != tt.invalid { + t.Fatalf("base=%#x,%v,%v", address, present, err) + } + if err := dp.Release(t.Context()); err != nil { + t.Fatal(err) + } + }) + } +} + +func TestDebugSpaceRejectsUnavailableOwner(t *testing.T) { + var zero dap.DebugSpace + if _, _, err := zero.ReadDebugBase(t.Context()); err == nil { + t.Fatal("zero reader accepted") + } + target := newWaitTarget() + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + before := len(target.requests) + if _, _, err := dp.DebugSpace().ReadDebugBase(t.Context()); err == nil { + t.Fatal("ADIv5 space accepted") + } + if len(target.requests) != before { + t.Fatal("ADIv5 request sent traffic") + } + if err := dp.Release(t.Context()); err != nil { + t.Fatal(err) + } +} diff --git a/dap/sim/debugspace.go b/dap/sim/debugspace.go new file mode 100644 index 0000000..35c3e65 --- /dev/null +++ b/dap/sim/debugspace.go @@ -0,0 +1,21 @@ +package sim + +import "errors" + +// SetDebugWord sets a read-only word in the ADIv6 debug address space. It can +// supply ROM entries or component identities independently of MEM-AP memory. +// Configure DPIDR1 before adding words. Duplicate addresses replace prior data. +func (t *Target) SetDebugWord(address uint64, value uint32) error { + if t == nil || t.dpidr>>12&15 != 3 { + return errors.New("dap/sim: debug words require DPv3") + } + bits := t.dpIDBanks[1] & 0x7f + if bits == 0 || bits > 64 || address&3 != 0 || address>>bits != 0 { + return errors.New("dap/sim: invalid debug word address") + } + if t.debugWords == nil { + t.debugWords = make(map[uint64]uint32) + } + t.debugWords[address] = value + return nil +} diff --git a/dap/sim/target.go b/dap/sim/target.go index c7a6147..76d3015 100644 --- a/dap/sim/target.go +++ b/dap/sim/target.go @@ -33,6 +33,7 @@ const ( // Target models the initial SW-DP register state. type Target struct { + debugWords map[uint64]uint32 dpidr uint32 ctrlStat uint32 dpBanks [16]uint32 @@ -409,6 +410,10 @@ func validateRequest(req swdsim.Request, read bool) error { func (t *Target) readAP(req swdsim.Request) (uint32, error) { posted := t.rdbuff + if value, ok := t.debugWords[uint64(t.selectHigh)<<32|uint64(t.selectDP&^15)|uint64(req.Addr)]; ok && t.dpidr>>12&15 == 3 { + t.rdbuff = value + return posted, nil + } ap := t.aps[t.selectedAP()] if ap == nil { t.rdbuff = 0 diff --git a/docs/capabilities.md b/docs/capabilities.md index 23667ec..ed37f5e 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -266,6 +266,13 @@ the packages do not add locking. The [Arm Debug Access Port guide](ports/dap.md) describes ADIv5 register access, posted transactions, power handshakes, and the current bench result. +## ADIv6 debug-space inspection + +`DebugPort.DebugSpace` supplies a borrowed aligned-word reader for DPv3's +debug address space and reads its advertised BASEPTR0/1 root. It composes with +the bounded CoreSight walker to identify AP bases. Perform this inspection +before acquiring MEM-APs; raw debug-space reads invalidate existing clients. + ## CoreSight component inspection `coresight.Identify` reads CIDR and PIDR at an explicit 4 KiB aligned page, diff --git a/docs/ports/dap.md b/docs/ports/dap.md index c8be9bd..538b524 100644 --- a/docs/ports/dap.md +++ b/docs/ports/dap.md @@ -643,3 +643,34 @@ Here `connection` is an open `armdebug.Conn`. Its `Close` restores acquired MEM-AP state before releasing the debug port and probe; retain the owner and retry if cleanup fails. Target-memory reads preserve the inherited access attributes. This does not acquire or halt the processor. + +## Discovering ADIv6 access ports + +`DebugPort.DebugSpace` borrows the DP's debug address space. Its +`ReadDebugBase` reads BASEPTR0/1, and its aligned `Size32` reader composes with +`coresight.Identify` and `coresight.Walk`: + +```go +space := dp.DebugSpace() +base, present, err := space.ReadDebugBase(ctx) +if err != nil { + return err +} +if !present { + return errors.New("debug port advertises no discovery root") +} +visits, err := coresight.Walk(ctx, space, base, coresight.WalkLimits{ + MaxDepth: 8, MaxComponents: 64, MaxEntries: 256, +}) +``` + +The visits retain component bases and architecture IDs. A present Arm MEM-AP +architecture identifies an AP base that can be passed to `dap.APAt`. Keep +partial results and the walk error if inspection stops. The walker follows +only advertised entries within its bounds; it does not scan the address space +or acquire component power. + +Inspect this space before acquiring MEM-APs: raw AP reads invalidate existing +MEM-AP clients and can have register-specific effects. The reader owns no +cleanup; the caller still releases the debug port. The DP's discovery base +and a MEM-AP's debug base belong to different address spaces. From 9fdf47c8a5ccae8078a906b33ccff500581c9f0a Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sun, 20 Sep 2026 14:53:03 -0700 Subject: [PATCH 5/7] Inspect ADIv6 debug and memory spaces from the CoreSight example. The example previously accepted only an ADIv5 AP index. Add explicit ADIv6 AP-base and DP debug-space selections so the same bounded walker can inspect the advertised AP hierarchy or memory behind a chosen AP. Keep selection and output in the example while the borrowed readers own addressing. Record the RP2350 discovery and memory observations with the commands, cleanup results, and limits of the bench evidence. --- docs/coresight.md | 54 ++++++++++++++++++ docs/ports/dap.md | 18 +++--- examples/README.md | 5 ++ examples/simple/coresight-info/main.go | 76 +++++++++++++++++++++----- 4 files changed, 130 insertions(+), 23 deletions(-) diff --git a/docs/coresight.md b/docs/coresight.md index 62142e5..392ab72 100644 --- a/docs/coresight.md +++ b/docs/coresight.md @@ -278,3 +278,57 @@ unlocks, processor control, or board activation were performed. The observed tables were class 1. Class 9 layouts and power-domain skips have ordinary test coverage; large addresses and both memory byte orders also have public MEM-AP simulation coverage. Those cases were not exercised on hardware. + +## ADIv6 and the RP2350 + +The example accepts `-ap-base` for an ADIv6 MEM-AP, or `-debug-space` to +inspect the DP's own advertised tree. Choose exactly one of `-ap`, +`-ap-base`, and `-debug-space`. `-base` still overrides the root within the +selected address space. + +```sh +go run ./examples/simple/coresight-info \ + -provider jlink -serial 000802011345 -debug-space -walk + +go run ./examples/simple/coresight-info \ + -provider jlink -serial 000802011345 -ap-base 0x2000 -walk +``` + +On September 20, 2026, Nostalgia exercised the RP2350 through J-Link EDU Mini +V2 serial `000802011345`, with SWD requested at 100 kHz. Two fresh sessions +for each path used: + +```sh +OSTIOLE_ARMDEBUG_HIL=1 \ + OSTIOLE_PROBE_HIL_PROVIDER=jlink \ + OSTIOLE_PROBE_HIL_SERIAL=000802011345 \ + OSTIOLE_ARMDEBUG_HIL_DEBUG_SPACE=1 \ + go test -tags=integration ./armdebug -run '^TestHILArmConnection$' -count=2 -v + +for ap_base in 0x2000 0x4000; do + OSTIOLE_ARMDEBUG_HIL=1 \ + OSTIOLE_PROBE_HIL_PROVIDER=jlink \ + OSTIOLE_PROBE_HIL_SERIAL=000802011345 \ + OSTIOLE_ARMDEBUG_HIL_AP_BASE="$ap_base" \ + OSTIOLE_ARMDEBUG_HIL_WALK=1 \ + go test -tags=integration ./armdebug -run '^TestHILArmConnection$' -count=2 -v +done +``` + +The integration walks used depth 8, 64 visits, 256 entry reads, and a +10-second deadline. The debug port reported DPIDR `0x4c013477`, DPIDR1 +`0x94` (20 address bits), and a present discovery root at address zero. Its +class 9 ROM table led to six children, including MEM-APs at `0x2000` and +`0x4000` with DEVARCH `0x47700a17`. The walk completed with seven identities. + +Both MEM-APs returned IDR `0x34770008`, CPUID `0x411fd210`, and debug base +`0xe00ff000`. Each target-memory walk completed with seven identities. +The example also completed all three paths with its existing depth 8, +256-visit and 4096-entry limits. + +All connection cleanup calls returned successfully. Restored state was +not independently measured after close. These runs performed no target-memory +writes, halt, reset, component unlock, or component power requests. The walks +cover advertised entries, not every component in the RP2350 debug address +space. Addresses above 32 bits, memory writes, and injected failures have +simulation coverage but were not exercised on this bench. diff --git a/docs/ports/dap.md b/docs/ports/dap.md index 538b524..647703a 100644 --- a/docs/ports/dap.md +++ b/docs/ports/dap.md @@ -520,16 +520,14 @@ acknowledgements, physical WAIT responses, auto-increment across 1 KiB, or 64-bit transfers. Those are better experiments than collecting more CPUID values from the same board. -## ADIv6 is a different job - -ADIv6 uses DPv3 and APv2. Arm -[IHI 0074F, _Arm Debug Interface Architecture Specification -ADIv6.0_](https://developer.arm.com/documentation/ihi0074/f) says directly -that DPv3 is not fully backward compatible with earlier DP versions; APv2 also -has a different common programmer's model and address space. Use IHI 0074F, -not this ADIv5 note, when implementing either one. Treating ADIv6 as a few -additional ADIv5 register constants would hide the actual compatibility -boundary. +## ADIv6 architecture + +ADIv6 uses DPv3 and APv2, with base-address selection and a 4 KiB AP register +map. Ostiole supports this path over SWD; baseline JTAG-DP retains its ADIv5 +register model. See the [DPv3 registers](#dpv3-discovery-registers) and +[debug-space inspection](#discovering-adiv6-access-ports) below. Arm +[IHI 0074, Arm Debug Interface Architecture Specification ADIv6.0](https://developer.arm.com/documentation/ihi0074/) +defines these registers and their distinct address spaces. ## MEM-AP debug base diff --git a/examples/README.md b/examples/README.md index 25ffc00..7674369 100644 --- a/examples/README.md +++ b/examples/README.md @@ -28,3 +28,8 @@ an example has been implemented. - [`simple/coresight-info`](simple/coresight-info) reads the advertised debug entry of a selected MEM-AP, or a known component page, through a managed SWD connection. Add `-walk` for bounded ROM traversal with partial-result reporting. + +For ADIv6 SW-DP targets, `coresight-info -debug-space -walk` inspects the DP's +advertised discovery tree. Use `-ap-base ADDRESS` instead of `-ap INDEX` to +inspect memory through one ADIv6 MEM-AP. The same probe selection and cleanup +rules apply. See the [RP2350 procedure](../docs/coresight.md#adiv6-and-the-rp2350). diff --git a/examples/simple/coresight-info/main.go b/examples/simple/coresight-info/main.go index 1ccd457..dc16311 100644 --- a/examples/simple/coresight-info/main.go +++ b/examples/simple/coresight-info/main.go @@ -28,7 +28,9 @@ func run() (err error) { provider := flag.String("provider", "", "exact probe provider") serial := flag.String("serial", "", "exact probe serial") function := flag.String("function", "", "exact probe function") - ap := flag.Int("ap", -1, "required MEM-AP index (0..255)") + apBase := flag.String("ap-base", "", "ADIv6 MEM-AP base address") + debugSpace := flag.Bool("debug-space", false, "inspect the ADIv6 DP debug address space") + ap := flag.Int("ap", -1, "ADIv5 MEM-AP index (0..255)") address := flag.String("base", "", "override the MEM-AP debug base with a known identification page") walk := flag.Bool("walk", false, "walk ROM tables with depth 8, 256 visits, and 4096 entry reads") flag.Parse() @@ -36,8 +38,12 @@ func run() (err error) { if err != nil { return err } - if *ap < 0 || *ap > 255 || flag.NArg() != 0 { - return errors.New("require -ap 0..255 and no positional arguments") + selection, err := selectAP(*ap, *apBase, *debugSpace) + if err != nil { + return err + } + if flag.NArg() != 0 { + return errors.New("no positional arguments permitted") } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() @@ -50,24 +56,68 @@ func run() (err error) { if err != nil { return err } - memory, err := c.OpenMemAP(ctx, dap.NewAPSel(uint8(*ap))) - if err != nil { - return err + var memory inspectionReader + if *debugSpace { + memory = c.Port().DebugSpace() + } else { + memory, err = c.OpenMemAP(ctx, selection) + if err != nil { + return err + } } - if *address == "" { - var present bool - base, present, err = memory.ReadDebugBase(ctx) + return inspectRoot(ctx, memory, *address, base, *walk) +} + +type inspectionReader interface { + coresight.ScalarReader + ReadDebugBase(context.Context) (uint64, bool, error) +} + +func inspectRoot(ctx context.Context, memory inspectionReader, override string, base uint64, walk bool) error { + if override == "" { + value, present, err := memory.ReadDebugBase(ctx) if err != nil { return err } if !present { - return errors.New("selected MEM-AP advertises no debug entry") + return errors.New("selected address space advertises no debug entry") } + base = value + } + return inspect(ctx, memory, base, walk) +} + +func selectAP(index int, base string, debugSpace bool) (dap.APSel, error) { + choices := 0 + if index != -1 { + choices++ + } + if base != "" { + choices++ + } + if debugSpace { + choices++ + } + if choices != 1 { + return dap.APSel{}, errors.New("select exactly one of -ap, -ap-base, or -debug-space") + } + if debugSpace { + return dap.APSel{}, nil + } + if base != "" { + value, err := parseBase(base) + if err != nil { + return dap.APSel{}, err + } + return dap.APAt(value) + } + if index < 0 || index > 255 { + return dap.APSel{}, errors.New("-ap must be 0..255") } - return inspect(ctx, memory, base, *walk) + return dap.NewAPSel(uint8(index)), nil } -func inspect(ctx context.Context, memory *dap.MemAP, base uint64, walk bool) error { +func inspect(ctx context.Context, memory coresight.ScalarReader, base uint64, walk bool) error { if walk { return printWalk(ctx, memory, base) } @@ -109,7 +159,7 @@ func parseBase(value string) (uint64, error) { return base, nil } -func printWalk(ctx context.Context, memory *dap.MemAP, base uint64) error { +func printWalk(ctx context.Context, memory coresight.ScalarReader, base uint64) error { limits := coresight.WalkLimits{MaxDepth: 8, MaxComponents: 256, MaxEntries: 4096} visits, err := coresight.Walk(ctx, memory, base, limits) for i, visit := range visits { From be2beb57d5fa7b574c4ac93da149b85cd7f18f78 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sun, 20 Sep 2026 16:45:30 -0700 Subject: [PATCH 6/7] Reject raw DP selections beyond the advertised address width. ADIv6 AP selectors checked the debug port's address width, but raw SELECT and SELECT1 writes could still send reserved address bits. Apply the same limit to immediate and queued DP writes before any traffic, while allowing the independent DPBANKSEL field. --- dap/apv2.go | 11 +++++++++ dap/apv2_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++ dap/swddp.go | 2 +- docs/ports/dap.md | 2 ++ 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/dap/apv2.go b/dap/apv2.go index 0ece96d..8964ab9 100644 --- a/dap/apv2.go +++ b/dap/apv2.go @@ -71,3 +71,14 @@ func (sel APSel) String() string { } return "invalid AP" } + +func (dp *DebugPort) validateSelectionAddress(reg DPRegister, value uint32) error { + address := uint64(value &^ 15) + if reg == SELECT1 { + address = uint64(value) << 32 + } + if dp.reentryID.dpidr.Version == 3 && (reg == SELECT || reg == SELECT1) && address>>dp.addressBits != 0 { + return fmt.Errorf("dap: %s address exceeds %d bits", reg, dp.addressBits) + } + return nil +} diff --git a/dap/apv2_test.go b/dap/apv2_test.go index ffad3fa..146cc7d 100644 --- a/dap/apv2_test.go +++ b/dap/apv2_test.go @@ -141,3 +141,63 @@ func checkAPv2ReleaseClearsUpperSelection(t *testing.T, fail bool) { t.Fatalf("released selection reads %#x, %v; want the low AP", got, err) } } + +func TestDPv3RawSelectionAddressWidth(t *testing.T) { + for _, width := range []uint32{12, 20, 32, 40, 48, 52} { + t.Run(fmt.Sprint(width), func(t *testing.T) { checkDPv3RawSelectionAddressWidth(t, width) }) + } +} + +func checkDPv3RawSelectionAddressWidth(t *testing.T, width uint32) { + t.Helper() + target := newWaitTarget() + target.Target = dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, width); err != nil { + t.Fatal(err) + } + dp := newDebugPort(t, target) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + defer func() { + if err := dp.Release(t.Context()); err != nil { + t.Error(err) + } + }() + for _, reg := range []dap.DPRegister{dap.SELECT, dap.SELECT1} { + bits := width + if reg == dap.SELECT1 { + bits = 0 + if width > 32 { + bits = width - 32 + } + } + valid := uint32((uint64(1) << bits) - 1) + if err := dp.WriteDP(t.Context(), reg, valid); err != nil { + t.Fatalf("valid %s=%#x rejected: %v", reg, valid, err) + } + if _, err := dp.ReadDP(t.Context(), dap.RDBUFF); err != nil { + t.Fatal(err) + } + if bits >= 32 { + continue + } + before := len(target.requests) + invalid := uint32(1) << bits + if err := dp.WriteDP(t.Context(), reg, invalid); err == nil { + t.Fatalf("accepted %s bit %d", reg, bits) + } + txn := dp.NewTxn() + prefix := txn.ReadDP(dap.DPIDR) + write := txn.WriteDP(reg, invalid) + if err := txn.Commit(t.Context()); err == nil || write.Err() == nil { + t.Fatalf("queued invalid %s accepted", reg) + } + if _, err := prefix.Value(); !errors.Is(err, dap.ErrNotExecuted) { + t.Fatalf("invalid queue executed prefix: %v", err) + } + if len(target.requests) != before { + t.Fatal("invalid selection sent traffic") + } + } +} diff --git a/dap/swddp.go b/dap/swddp.go index 4941685..42d1dfe 100644 --- a/dap/swddp.go +++ b/dap/swddp.go @@ -241,7 +241,7 @@ func (dp *DebugPort) validateDPWrite(reg DPRegister, value uint32) (dpRegisterIn if reg == DLCR && value&dlcrTurnaroundMask != 0 { return dpRegisterInfo{}, errors.New("dap: write DLCR: variable turnaround requires unsupported SWD framing") } - return info, nil + return info, dp.validateSelectionAddress(reg, value) } func (dp *DebugPort) validateBankedDPRegister(info dpRegisterInfo) error { diff --git a/docs/ports/dap.md b/docs/ports/dap.md index 647703a..c80b84c 100644 --- a/docs/ports/dap.md +++ b/docs/ports/dap.md @@ -579,6 +579,8 @@ On SW-DP version 3, `ReadDP` supports `DPIDR1`, `BASEPTR0`, and `BASEPTR1`; `WriteDP` supports `SELECT1`. DPIDR reads select bank zero on this version. These registers are rejected on earlier debug ports and baseline JTAG-DP. Reading the discovery registers alone does not discover or acquire an AP. +Immediate and queued `SELECT` and `SELECT1` writes reject address bits beyond +DPIDR1.ASIZE before traffic; the low DPBANKSEL bits remain independent. ```go width, err := dp.ReadDP(ctx, dap.DPIDR1) From b6349d17cf489a1f58493dc538ca22c8f0c02ae8 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sun, 20 Sep 2026 16:52:48 -0700 Subject: [PATCH 7/7] Validate simulated debug-port versions and addresses. Fixture setup accepted selectors for incompatible or unsupported DP versions and ADIv6 addresses beyond the configured width. The public DAP API could not reach those fixtures despite their successful setup. Validate selectors against the target's DPIDR during fixture setup and updates, and check ADIv6 addresses against its supported address width. DPv3 fixtures require DPIDR1 to be configured first. --- dap/sim/debugbase.go | 2 +- dap/sim/debugspace.go | 12 ++++- dap/sim/target.go | 34 +++++++++---- dap/sim/target_test.go | 105 +++++++++++++++++++++++++++++++++++++++++ docs/capabilities.md | 2 +- 5 files changed, 141 insertions(+), 14 deletions(-) diff --git a/dap/sim/debugbase.go b/dap/sim/debugbase.go index c2f7452..794f5bd 100644 --- a/dap/sim/debugbase.go +++ b/dap/sim/debugbase.go @@ -15,7 +15,7 @@ func (t *Target) SetMEMAPDebugBase(sel dap.APSel, low, high uint32) error { if t == nil { return errors.New("dap/sim: nil target") } - selection, err := selectorValue(sel) + selection, err := t.selectorValue(sel) if err != nil { return err } diff --git a/dap/sim/debugspace.go b/dap/sim/debugspace.go index 35c3e65..25522f6 100644 --- a/dap/sim/debugspace.go +++ b/dap/sim/debugspace.go @@ -9,8 +9,7 @@ func (t *Target) SetDebugWord(address uint64, value uint32) error { if t == nil || t.dpidr>>12&15 != 3 { return errors.New("dap/sim: debug words require DPv3") } - bits := t.dpIDBanks[1] & 0x7f - if bits == 0 || bits > 64 || address&3 != 0 || address>>bits != 0 { + if address&3 != 0 || !t.validDebugAddress(address) { return errors.New("dap/sim: invalid debug word address") } if t.debugWords == nil { @@ -19,3 +18,12 @@ func (t *Target) SetDebugWord(address uint64, value uint32) error { t.debugWords[address] = value return nil } + +func (t *Target) validDebugAddress(address uint64) bool { + switch bits := t.dpIDBanks[1] & 0x7f; bits { + case 12, 20, 32, 40, 48, 52: + return address>>bits == 0 + default: + return false + } +} diff --git a/dap/sim/target.go b/dap/sim/target.go index 76d3015..ce5b1b3 100644 --- a/dap/sim/target.go +++ b/dap/sim/target.go @@ -170,7 +170,9 @@ func (t *Target) SetDPRegister(reg dap.DPRegister, value uint32) error { } // AddAP adds an access port with the supplied identification register. It -// rejects a zero APIDR and an existing selector. +// rejects a zero APIDR, an existing selector, and a selector for a different +// DAP architecture. AP fixtures require DPv0 through DPv3. For DPv3, configure +// DPIDR1 before adding fixtures; AP bases must fit its supported address width. func (t *Target) AddAP(sel dap.APSel, idr uint32) error { if t == nil { return errors.New("dap/sim: nil target") @@ -178,7 +180,7 @@ func (t *Target) AddAP(sel dap.APSel, idr uint32) error { if idr == 0 { return errors.New("dap/sim: APIDR must be nonzero") } - selection, err := selectorValue(sel) + selection, err := t.selectorValue(sel) if err != nil { return err } @@ -190,7 +192,9 @@ func (t *Target) AddAP(sel dap.APSel, idr uint32) error { } // AddMEMAP adds a memory access port initialized from aligned words. It rejects -// a non-MEM-AP identity, an existing selector, and unaligned fixtures. +// a non-MEM-AP identity, an existing selector, unaligned fixtures, and a +// selector for a different DAP architecture. The DP version and address-width +// requirements are the same as AddAP. func (t *Target) AddMEMAP(sel dap.APSel, idr uint32, words map[uint32]uint32) error { if t == nil { return errors.New("dap/sim: nil target") @@ -198,7 +202,7 @@ func (t *Target) AddMEMAP(sel dap.APSel, idr uint32, words map[uint32]uint32) er if idr == 0 || dap.DecodeAPIDR(idr).Class != 8 { return errors.New("dap/sim: MEM-AP requires a nonzero class-8 APIDR") } - selection, err := selectorValue(sel) + selection, err := t.selectorValue(sel) if err != nil { return err } @@ -228,7 +232,7 @@ func (t *Target) SetMEMAPCFG(sel dap.APSel, cfg uint32) error { if t == nil { return errors.New("dap/sim: nil target") } - selection, err := selectorValue(sel) + selection, err := t.selectorValue(sel) if err != nil { return err } @@ -255,7 +259,7 @@ func (t *Target) SetMEMAPSizes(sel dap.APSel, sizes ...dap.TransferSize) error { if t == nil { return errors.New("dap/sim: nil target") } - selection, err := selectorValue(sel) + selection, err := t.selectorValue(sel) if err != nil { return err } @@ -285,7 +289,7 @@ func (t *Target) SetMEMAPBytes(sel dap.APSel, addr uint64, data []byte) error { if t == nil { return errors.New("dap/sim: nil target") } - selection, err := selectorValue(sel) + selection, err := t.selectorValue(sel) if err != nil { return err } @@ -307,7 +311,7 @@ func (t *Target) MEMAPBytes(sel dap.APSel, addr uint64, size int) ([]byte, error if t == nil { return nil, errors.New("dap/sim: nil target") } - selection, err := selectorValue(sel) + selection, err := t.selectorValue(sel) if err != nil { return nil, err } @@ -694,8 +698,18 @@ func (t *Target) writeBankedRegister(value uint32) error { return nil } -func selectorValue(sel dap.APSel) (uint64, error) { - if base, err := sel.BaseAddress(); err == nil { +func (t *Target) selectorValue(sel dap.APSel) (uint64, error) { + if version := t.dpidr >> 12 & 15; version > 3 { + return 0, fmt.Errorf("dap/sim: AP fixtures do not support DPv%d", version) + } + if t.dpidr>>12&15 == 3 { + base, err := sel.BaseAddress() + if err != nil { + return 0, err + } + if !t.validDebugAddress(base) { + return 0, errors.New("dap/sim: AP base requires a configured, supported DPIDR1 address width") + } return base, nil } index, err := sel.Value() diff --git a/dap/sim/target_test.go b/dap/sim/target_test.go index 4a1bacb..6397967 100644 --- a/dap/sim/target_test.go +++ b/dap/sim/target_test.go @@ -424,3 +424,108 @@ func TestNilTargetRejectsAccessPortFixtures(t *testing.T) { t.Fatal("AddMEMAP() succeeded on a nil target") } } + +func TestTargetRejectsMismatchedAPArchitecture(t *testing.T) { + for _, memory := range []bool{false, true} { + for version := range uint32(16) { + checkTargetAPArchitecture(t, memory, 0x2ba00477|version<<12) + } + } +} + +func checkTargetAPArchitecture(t *testing.T, memory bool, dpidr uint32) { + t.Helper() + v6, err := dap.APAt(0x2000) + if err != nil { + t.Fatal(err) + } + target := New(dpidr) + valid, invalid := dap.NewAPSel(0), v6 + if dpidr>>12&15 == 3 { + valid, invalid = invalid, valid + if err := target.SetDPRegister(dap.DPIDR1, 20); err != nil { + t.Fatal(err) + } + } + add := target.AddAP + if memory { + add = func(sel dap.APSel, idr uint32) error { return target.AddMEMAP(sel, idr, nil) } + } + if err := add(invalid, 0x34770008); err == nil || len(target.aps) != 0 { + t.Fatalf("DPIDR=%#x memory=%v accepted %s or changed fixtures", dpidr, memory, invalid) + } + if dpidr>>12&15 > 3 { + if err := add(valid, 0x34770008); err == nil || len(target.aps) != 0 { + t.Fatalf("unsupported DPIDR=%#x memory=%v accepted a fixture", dpidr, memory) + } + return + } + if err := add(valid, 0x34770008); err != nil { + t.Fatal(err) + } + dp := dap.NewDebugPort(dap.SWDP(swd.New(swdsim.New(target)))) + if _, err := dp.Connect(t.Context()); err != nil { + t.Fatal(err) + } + id, err := dp.ReadAPIDR(t.Context(), valid) + if err != nil || id.Raw != 0x34770008 { + t.Fatalf("valid fixture is unreachable: %#x, %v", id.Raw, err) + } + if err := dp.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestTargetValidatesADIv6FixtureAddressWidth(t *testing.T) { + for width := range uint32(128) { + checkADIv6FixtureAddressWidth(t, width) + } +} + +func checkADIv6FixtureAddressWidth(t *testing.T, width uint32) { + t.Helper() + target := New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, width); err != nil { + t.Fatal(err) + } + validWidth := width == 12 || width == 20 || width == 32 || width == 40 || width == 48 || width == 52 + base := uint64(0) + if validWidth { + base = uint64(1)<