diff --git a/armdebug/conn_integration_test.go b/armdebug/conn_integration_test.go index a579e85..af5afe1 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" @@ -57,6 +58,20 @@ 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 os.Getenv("OSTIOLE_ARMDEBUG_HIL_DEBUG_SPACE") == "1" { + inspectDebugSpaceHIL(t, ctx, c.Port().DebugSpace()) + } if inspectMemory { inspectMemoryHIL(t, ctx, c, ap) } @@ -64,6 +79,20 @@ func TestHILArmConnection(t *testing.T) { 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) + } + sel, err := dap.APAt(base) + if err != nil { + t.Fatal(err) + } + return sel, true + } selected := os.Getenv("OSTIOLE_ARMDEBUG_HIL_AP") if selected == "" { return dap.APSel{}, false @@ -77,10 +106,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) @@ -93,7 +118,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) { @@ -107,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/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/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/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/adiv6_test.go b/dap/adiv6_test.go new file mode 100644 index 0000000..d7ee57a --- /dev/null +++ b/dap/adiv6_test.go @@ -0,0 +1,67 @@ +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" +) + +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/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..8964ab9 --- /dev/null +++ b/dap/apv2.go @@ -0,0 +1,84 @@ +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) +} + +// 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" +} + +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 new file mode 100644 index 0000000..146cc7d --- /dev/null +++ b/dap/apv2_test.go @@ -0,0 +1,203 @@ +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) + } +} + +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/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/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/doc.go b/dap/doc.go index 75ad41a..9a0afb5 100644 --- a/dap/doc.go +++ b/dap/doc.go @@ -33,16 +33,18 @@ // 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 // 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 @@ -76,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_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/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 07a9024..e699afa 100644 --- a/dap/lifecycle.go +++ b/dap/lifecycle.go @@ -119,6 +119,21 @@ func (dp *DebugPort) initialize(ctx context.Context, raw uint32) (Identity, uint if err != nil { 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 + } + 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 +183,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..0c61aac 100644 --- a/dap/memap.go +++ b/dap/memap.go @@ -91,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) @@ -414,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_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/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/sim/debugbase.go b/dap/sim/debugbase.go index 71000cc..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 := sel.Value() + selection, err := t.selectorValue(sel) if err != nil { return err } diff --git a/dap/sim/debugspace.go b/dap/sim/debugspace.go new file mode 100644 index 0000000..25522f6 --- /dev/null +++ b/dap/sim/debugspace.go @@ -0,0 +1,29 @@ +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") + } + if address&3 != 0 || !t.validDebugAddress(address) { + 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 +} + +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 ba9f5ce..ce5b1b3 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,17 @@ 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 + debugWords map[uint64]uint32 + 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 +99,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 +142,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: @@ -160,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") @@ -168,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 := sel.Value() + selection, err := t.selectorValue(sel) if err != nil { return err } @@ -180,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") @@ -188,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 := sel.Value() + selection, err := t.selectorValue(sel) if err != nil { return err } @@ -218,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 := sel.Value() + selection, err := t.selectorValue(sel) if err != nil { return err } @@ -245,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 := sel.Value() + selection, err := t.selectorValue(sel) if err != nil { return err } @@ -275,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 := sel.Value() + selection, err := t.selectorValue(sel) if err != nil { return err } @@ -297,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 := sel.Value() + selection, err := t.selectorValue(sel) if err != nil { return nil, err } @@ -345,7 +359,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 +393,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: @@ -409,12 +414,19 @@ 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))] + 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 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 +436,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 +608,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) { @@ -640,3 +659,68 @@ 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 +} + +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() + 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/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)< 2 { - return fmt.Errorf("dap: ADIv5 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 @@ -294,7 +306,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..f9ef168 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 @@ -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 @@ -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 { @@ -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) } @@ -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,10 @@ 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.err != nil { errs = append(errs, op.err) } @@ -293,13 +297,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 { @@ -340,7 +345,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 @@ -357,9 +362,13 @@ 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(op.apAddr&0x0c, false), + req: apTransferRequest(uint8(op.apAddr&0x0c), false), data: data, op: index, operationStarted: i > 0, @@ -379,7 +388,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) } @@ -430,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(addr&0x0c, read) + 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, @@ -448,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_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/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_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/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/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/architecture.md b/docs/architecture.md index 97062ed..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 @@ -264,8 +269,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 5582c57..42db91f 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -250,8 +250,9 @@ 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. | -| 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. | +| 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 require DPv0 through DPv3 and `dap.APSel` values matching the simulated DP architecture. Configure DPIDR1 before adding DPv3 fixtures; their bases must fit its supported address width. All AP fixtures reject mismatched or 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). | | AP and MEM-AP access | HIL | Opt-in FTDI integration tests against an explicitly selected AP. One transaction clocked nine fixed requests in two SWDIO calls and received nine OK acknowledgements. A 64-byte block read matched scalar byte reads from the same SRAM range and counted 571 OK acknowledgements, no WAIT, FAULT, or invalid acknowledgement, and 563 fixed frames. Separately gated tests preserved that range, exercised 8-, 16-, and 32-bit scalar writes plus aligned 64-byte and unaligned 31-byte block writes, checked neighboring bytes, then restored and verified the original contents. The scalar-write test counted 3,130 OK acknowledgements and 3,122 fixed frames; the block-write test counted 777 OK acknowledgements and 769 fixed frames. Neither returned WAIT, FAULT, or an invalid acknowledgement. The selected range did not cross a TAR boundary, and the target did not advertise CFG.LD. | @@ -265,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/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/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 6ae7e78..c80b84c 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 architecture -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 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 @@ -574,3 +572,105 @@ 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. +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) +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. +`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 `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) +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. + +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. + +## 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. 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 { 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) }