diff --git a/dap/sim/debugbase_test.go b/dap/sim/debugbase_test.go index d99be83..9c379b6 100644 --- a/dap/sim/debugbase_test.go +++ b/dap/sim/debugbase_test.go @@ -12,23 +12,23 @@ func TestMEMAPDebugBaseRegisters(t *testing.T) { t.Fatal(err) } ap := target.aps[sel] - if got, err := ap.readRegister(0xf8); err != nil || got != 2 { + if got, err := ap.readRegister(t.Context(), 0xf8); err != nil || got != 2 { t.Fatalf("default BASE=%#x, %v", got, err) } if err := target.SetMEMAPDebugBase(sel, 0xe00ff003, 0x12345678); err != nil { t.Fatal(err) } - if got, err := ap.readRegister(0xf0); err != nil || got != 0 { + if got, err := ap.readRegister(t.Context(), 0xf0); err != nil || got != 0 { t.Fatalf("32-bit upper BASE=%#x, %v", got, err) } if err := target.SetMEMAPCFG(sel, 2); err != nil { t.Fatal(err) } for reg, want := range map[uint8]uint32{0xf8: 0xe00ff003, 0xf0: 0x12345678} { - if err := ap.writeRegister(reg, 0); err != nil { + if err := ap.writeRegister(t.Context(), reg, 0); err != nil { t.Fatal(err) } - if got, err := ap.readRegister(reg); err != nil || got != want { + if got, err := ap.readRegister(t.Context(), reg); err != nil || got != want { t.Fatalf("register %#x=%#x, %v; want %#x", reg, got, err, want) } } diff --git a/dap/sim/device.go b/dap/sim/device.go new file mode 100644 index 0000000..6d81b71 --- /dev/null +++ b/dap/sim/device.go @@ -0,0 +1,131 @@ +package sim + +import ( + "context" + "errors" + "fmt" + "reflect" + + "github.com/jon/ostiole/dap" +) + +// ErrBusFault reports a target-bus access which completed with an error. +// The accepted AP transfer sets STICKYERR; subsequent SWD requests report FAULT. +// It does not represent a request-phase WAIT or a host transport failure. +var ErrBusFault = errors.New("dap/sim: target bus fault") + +// MemoryDevice receives one complete target access at an absolute byte address. +// Data is in increasing address order, independent of MEM-AP byte order, and +// has length 1, 2, 4 or 8. Read fills data; Write consumes it without retaining +// the slice. Calls are serialized with their Target and all other shared views. +// Return ErrBusFault for a modeled bus error, including a rejected width. +// Other errors become ErrDeviceFailure. No access is replayed. +type MemoryDevice interface { + Read(ctx context.Context, addr uint64, data []byte) error + Write(ctx context.Context, addr uint64, data []byte) error +} + +type deviceMapping struct { + start, end uint64 + device MemoryDevice +} + +// MapMEMAPDevice maps size bytes of one MEM-AP to device before any target +// traffic. Mappings cannot overlap or change after traffic starts. Unmapped +// addresses retain the ordinary byte-memory fixture behavior. The mapping owns +// no cleanup; the caller owns device and serializes all uses of shared devices. +func (t *Target) MapMEMAPDevice(sel dap.APSel, addr, size uint64, device MemoryDevice) error { + if t == nil { + return errors.New("dap/sim: nil target") + } + if t.started { + return errors.New("dap/sim: device mapping after target traffic") + } + if _, err := t.selectorValue(sel); err != nil { + return err + } + ap := t.aps[sel] + if ap == nil || !ap.memAP { + return errors.New("dap/sim: device mapping requires a MEM-AP") + } + if size == 0 || addr+size-1 < addr { + return errors.New("dap/sim: empty or overflowing device mapping") + } + if nilDevice(device) { + return errors.New("dap/sim: nil memory device") + } + end := addr + size - 1 + if ap.overlapsDevice(addr, end) { + return errors.New("dap/sim: overlapping device mapping") + } + ap.devices = append(ap.devices, deviceMapping{addr, end, device}) + return nil +} + +func nilDevice(device MemoryDevice) bool { + if device == nil { + return true + } + v := reflect.ValueOf(device) + switch v.Kind() { + case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: + return v.IsNil() + default: + return false + } +} + +func (ap *accessPort) overlapsDevice(start, end uint64) bool { + for _, mapping := range ap.devices { + if start <= mapping.end && end >= mapping.start { + return true + } + } + return false +} + +func (ap *accessPort) deviceForAccess(addr uint64, width int) (MemoryDevice, error) { + end, err := memoryRangeEnd(addr, width) + if err != nil || addr%uint64(width) != 0 { + return nil, ErrBusFault + } + for _, mapping := range ap.devices { + if addr > mapping.end || end < mapping.start { + continue + } + if addr < mapping.start || end > mapping.end { + return nil, fmt.Errorf("%w: access crosses device mapping", ErrBusFault) + } + return mapping.device, nil + } + return nil, nil +} + +func (ap *accessPort) readMemory(ctx context.Context, addr uint64, width int) ([]byte, error) { + device, err := ap.deviceForAccess(addr, width) + if err != nil { + return nil, err + } + data := make([]byte, width) + if device != nil { + return data, deviceResult(device.Read(ctx, addr, data)) + } + for i := range data { + data[i] = ap.memory[addr+uint64(i)] + } + return data, nil +} + +func (ap *accessPort) writeMemory(ctx context.Context, addr uint64, data []byte) error { + device, err := ap.deviceForAccess(addr, len(data)) + if err != nil { + return err + } + if device != nil { + return deviceResult(device.Write(ctx, addr, data)) + } + for i, b := range data { + ap.memory[addr+uint64(i)] = b + } + return nil +} diff --git a/dap/sim/device_error.go b/dap/sim/device_error.go new file mode 100644 index 0000000..3466ba5 --- /dev/null +++ b/dap/sim/device_error.go @@ -0,0 +1,38 @@ +package sim + +import ( + "context" + "errors" + "fmt" +) + +// ErrDeviceFailure reports a host or model failure inside a memory callback. +// The accepted access might have taken effect. The error text retains the +// callback diagnostic; only context cancellation/deadline identity is retained. +// Callback errors never identify SWD acknowledgements or unsent requests. +var ErrDeviceFailure = errors.New("dap/sim: memory device failure") + +type deviceFailure struct { + cause error +} + +func (e deviceFailure) Error() string { + return fmt.Sprintf("%v: %v", ErrDeviceFailure, e.cause) +} + +// Is preserves cancellation without exposing protocol classifications from an +// accepted device access. Unwrapping the cause would let SWD treat a callback +// WAIT as permission to retry the accepted access. +func (e deviceFailure) Is(target error) bool { + if target == ErrDeviceFailure { + return true + } + return (target == context.Canceled || target == context.DeadlineExceeded) && errors.Is(e.cause, target) +} + +func deviceResult(err error) error { + if err == nil || errors.Is(err, ErrBusFault) { + return err + } + return deviceFailure{cause: err} +} diff --git a/dap/sim/device_test.go b/dap/sim/device_test.go new file mode 100644 index 0000000..f165846 --- /dev/null +++ b/dap/sim/device_test.go @@ -0,0 +1,534 @@ +package sim_test + +import ( + "context" + "errors" + "math" + "slices" + "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" +) + +type deviceAccess struct { + addr uint64 + data []byte + write bool +} + +type memoryDevice struct { + data map[uint64]byte + accesses []deviceAccess + err error + errAt uint64 + afterWrite bool + ctx context.Context +} + +func (d *memoryDevice) Read(ctx context.Context, addr uint64, data []byte) error { + d.ctx = ctx + d.accesses = append(d.accesses, deviceAccess{addr: addr, data: make([]byte, len(data))}) + if d.err != nil && (d.errAt == 0 || d.errAt == addr) { + return d.err + } + for i := range data { + data[i] = d.data[addr+uint64(i)] + } + return nil +} + +func (d *memoryDevice) Write(ctx context.Context, addr uint64, data []byte) error { + d.ctx = ctx + d.accesses = append(d.accesses, deviceAccess{addr: addr, data: slices.Clone(data), write: true}) + if !d.afterWrite && d.err != nil && (d.errAt == 0 || d.errAt == addr) { + return d.err + } + if d.data == nil { + d.data = make(map[uint64]byte) + } + for i, b := range data { + d.data[addr+uint64(i)] = b + } + if d.afterWrite && (d.errAt == 0 || d.errAt == addr) { + return d.err + } + return nil +} + +func mappedTarget(t *testing.T, addr, size uint64, device dapsim.MemoryDevice) *dapsim.Target { + t.Helper() + target := dapsim.New(0x2ba01477) + addMEMAPFixture(t, target, 0, memAPIDR, nil) + if err := target.MapMEMAPDevice(apSel(0), addr, size, device); err != nil { + t.Fatal(err) + } + return target +} + +func openedMemory(t *testing.T, target *dapsim.Target, sel dap.APSel) *dap.MemAP { + t.Helper() + dp := enteredDAP(t, target) + memory, err := dap.OpenMemAP(t.Context(), dp, sel) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := memory.Release(context.Background()); err != nil { + t.Errorf("release MEM-AP: %v", err) + } + }) + return memory +} + +func TestMappedDevicePreservesScalarWidthAndByteOrder(t *testing.T) { + for _, bigEndian := range []bool{false, true} { + for _, size := range []dap.TransferSize{dap.Size8, dap.Size16, dap.Size32, dap.Size64} { + t.Run(sizeName(size, bigEndian), func(t *testing.T) { + testMappedScalar(t, size, bigEndian) + }) + } + } +} + +func testMappedScalar(t *testing.T, size dap.TransferSize, bigEndian bool) { + t.Helper() + device := new(memoryDevice) + target := mappedTarget(t, 0x100, 16, device) + cfg := uint32(4) + if bigEndian { + cfg |= 1 + } + if err := target.SetMEMAPCFG(apSel(0), cfg); err != nil { + t.Fatal(err) + } + if err := target.SetMEMAPSizes(apSel(0), dap.Size8, dap.Size16, dap.Size32, dap.Size64); err != nil { + t.Fatal(err) + } + memory := openedMemory(t, target, apSel(0)) + addr := uint64(0x108) + if size == dap.Size8 { + addr++ + } + if size == dap.Size16 { + addr += 2 + } + value := uint64(0x8877665544332211) + if size != dap.Size64 { + value &= uint64(1)<<(uint(size)*8) - 1 + } + if err := memory.WriteScalar(t.Context(), addr, size, value); err != nil { + t.Fatal(err) + } + got, err := memory.ReadScalar(t.Context(), addr, size) + if err != nil || got != value { + t.Fatalf("ReadScalar = %#x, %v; want %#x", got, err, value) + } + checkScalarAccesses(t, device.accesses, addr, size, value, bigEndian) +} + +func checkScalarAccesses(t *testing.T, accesses []deviceAccess, addr uint64, size dap.TransferSize, value uint64, bigEndian bool) { + t.Helper() + if len(accesses) != 2 { + t.Fatalf("device accesses = %v, want one write and one read", accesses) + } + write, read := accesses[0], accesses[1] + if !write.write || read.write || write.addr != addr || read.addr != addr || len(write.data) != int(size) || len(read.data) != int(size) { + t.Fatalf("device accesses lost direction, address or width: %v", accesses) + } + for i, b := range write.data { + shift := i * 8 + if bigEndian { + shift = (int(size) - 1 - i) * 8 + } + if want := byte(value >> uint(shift)); b != want { + t.Fatalf("byte %d = %#x, want %#x", i, b, want) + } + } +} + +func sizeName(size dap.TransferSize, bigEndian bool) string { + name := map[dap.TransferSize]string{dap.Size8: "byte", dap.Size16: "halfword", dap.Size32: "word", dap.Size64: "doubleword"}[size] + if bigEndian { + return name + "/big" + } + return name + "/little" +} + +func TestMappedDeviceRejectsInvalidMappings(t *testing.T) { + device := new(memoryDevice) + target := mappedTarget(t, 0x100, 4, device) + var nilDevice *memoryDevice + for _, test := range []struct { + name string + sel dap.APSel + addr, size uint64 + device dapsim.MemoryDevice + }{ + {"zero selector", dap.APSel{}, 0, 4, device}, + {"absent AP", apSel(1), 0, 4, device}, + {"empty", apSel(0), 0, 0, device}, + {"overflow", apSel(0), math.MaxUint64, 2, device}, + {"nil", apSel(0), 0, 4, nil}, + {"typed nil", apSel(0), 0, 4, nilDevice}, + {"overlap", apSel(0), 0xff, 2, device}, + {"contained", apSel(0), 0x101, 1, device}, + {"containing", apSel(0), 0, 0x200, device}, + } { + t.Run(test.name, func(t *testing.T) { + if err := target.MapMEMAPDevice(test.sel, test.addr, test.size, test.device); err == nil { + t.Fatal("accepted invalid mapping") + } + }) + } + if err := (*dapsim.Target)(nil).MapMEMAPDevice(apSel(0), 0, 4, device); err == nil { + t.Fatal("nil target accepted mapping") + } + if err := target.MapMEMAPDevice(apSel(0), 0x104, 4, device); err != nil { + t.Fatalf("adjacent mapping: %v", err) + } + if err := target.MapMEMAPDevice(apSel(0), math.MaxUint64, 1, device); err != nil { + t.Fatalf("last address: %v", err) + } + _ = enteredDAP(t, target) + if err := target.MapMEMAPDevice(apSel(0), 0x200, 4, device); err == nil { + t.Fatal("mapping changed after traffic") + } +} + +func TestMappedDeviceFixtureAccessCannotBypassDevice(t *testing.T) { + device := new(memoryDevice) + target := mappedTarget(t, 0x102, 2, device) + for _, addr := range []uint64{0x100, 0x102, 0x103} { + if err := target.SetMEMAPBytes(apSel(0), addr, []byte{1, 2, 3, 4}); err == nil { + t.Fatalf("fixture write at %#x bypassed device", addr) + } + if _, err := target.MEMAPBytes(apSel(0), addr, 4); err == nil { + t.Fatalf("fixture read at %#x bypassed device", addr) + } + } + if err := target.SetMEMAPBytes(apSel(0), 0x104, []byte{1, 2}); err != nil { + t.Fatal(err) + } + if len(device.accesses) != 0 { + t.Fatal("fixture access invoked device") + } +} + +func TestMappedDeviceCrossingAccessFaultsBeforeEffects(t *testing.T) { + for _, read := range []bool{true, false} { + device := new(memoryDevice) + target := mappedTarget(t, 0x102, 2, device) + memory := openedMemory(t, target, apSel(0)) + var err error + if read { + _, err = memory.ReadWord(t.Context(), 0x100) + } else { + err = memory.WriteWord(t.Context(), 0x100, 7) + } + if !errors.Is(err, dap.ErrFault) { + t.Fatalf("crossing access error = %v, want DAP fault", err) + } + if len(device.accesses) != 0 { + t.Fatal("crossing access reached device") + } + data, err := target.MEMAPBytes(apSel(0), 0x100, 2) + if err != nil || !slices.Equal(data, []byte{0, 0}) { + t.Fatalf("ordinary prefix changed: %v, %v", data, err) + } + } +} + +func TestMappedDeviceFaultUsesDAPRecovery(t *testing.T) { + device := &memoryDevice{err: dapsim.ErrBusFault} + target := mappedTarget(t, 0x100, 4, device) + dp := enteredDAP(t, target) + memory, err := dap.OpenMemAP(t.Context(), dp, apSel(0)) + if err != nil { + t.Fatal(err) + } + _, err = memory.ReadWord(t.Context(), 0x100) + var fault *dap.FaultError + if !errors.As(err, &fault) || !fault.StateValid || fault.CTRLSTAT&(1<<5) == 0 { + t.Fatalf("read fault = %v, want captured STICKYERR", err) + } + if err := memory.Release(t.Context()); err != nil { + t.Fatal(err) + } + memory, err = dap.OpenMemAP(t.Context(), dp, apSel(0)) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := memory.Release(context.Background()); err != nil { + t.Error(err) + } + }) + device.err = nil + if err := memory.WriteWord(t.Context(), 0x100, 42); err != nil { + t.Fatalf("write after fault recovery: %v", err) + } + if got, err := memory.ReadWord(t.Context(), 0x100); err != nil || got != 42 { + t.Fatalf("read after recovery = %d, %v", got, err) + } +} + +func TestMappedDeviceReceivesContextAndCancellation(t *testing.T) { + device := new(memoryDevice) + target := mappedTarget(t, 0x100, 4, device) + memory := openedMemory(t, target, apSel(0)) + ctx, cancel := context.WithCancel(t.Context()) + if _, err := memory.ReadWord(ctx, 0x100); err != nil { + t.Fatal(err) + } + if device.ctx != ctx { + t.Fatal("device lost caller context") + } + cancel() + before := len(device.accesses) + if err := memory.WriteWord(ctx, 0x100, 1); !errors.Is(err, context.Canceled) { + t.Fatalf("canceled write = %v", err) + } + if len(device.accesses) != before { + t.Fatal("canceled write reached device") + } +} + +func TestMappedDeviceFaultFollowsAcceptedWrite(t *testing.T) { + for _, after := range []bool{false, true} { + device := &memoryDevice{err: dapsim.ErrBusFault, afterWrite: after} + target := mappedTarget(t, 0x100, 4, device) + conn := enteredConn(t, target) + selectAP(t, conn, 0, 0) + if err := conn.WriteAP(t.Context(), 0, 2|0x10); err != nil { + t.Fatal(err) + } + if err := conn.WriteAP(t.Context(), 4, 0x100); err != nil { + t.Fatal(err) + } + if err := conn.WriteAP(t.Context(), 0x0c, 42); err != nil { + t.Fatalf("accepted DRW write returned host error: %v", err) + } + ctrl, err := conn.ReadDP(t.Context(), 4) + if err != nil || ctrl&(1<<5) == 0 { + t.Fatalf("CTRL/STAT = %#x, %v; want STICKYERR", ctrl, err) + } + if _, err := conn.ReadDP(t.Context(), 0x0c); !errors.Is(err, swd.ErrFault) { + t.Fatalf("completion = %v, want SWD FAULT", err) + } + if len(device.accesses) != 1 { + t.Fatalf("faulting write was replayed: %v", device.accesses) + } + want := byte(0) + if after { + want = 42 + } + if device.data[0x100] != want { + t.Fatalf("write effect = %d, want %d", device.data[0x100], want) + } + if err := conn.WriteDP(t.Context(), 0, 0x1e); err != nil { + t.Fatal(err) + } + if tar := readPosted(t, conn, 0, 0, 4); tar != 0x100 { + t.Fatalf("fault incremented TAR: %#x", tar) + } + } +} + +func TestMappedDeviceModelErrorIsNotBusFault(t *testing.T) { + for _, failure := range []error{context.Canceled, errors.New("device model failed")} { + device := &memoryDevice{err: failure} + target := mappedTarget(t, 0x100, 4, device) + conn := enteredConn(t, target) + selectAP(t, conn, 0, 0) + if err := conn.WriteAP(t.Context(), 0, 2); err != nil { + t.Fatal(err) + } + if err := conn.WriteAP(t.Context(), 4, 0x100); err != nil { + t.Fatal(err) + } + if _, err := conn.ReadAP(t.Context(), 0x0c); !errors.Is(err, dapsim.ErrDeviceFailure) || errors.Is(err, swd.ErrFault) { + t.Fatalf("model failure = %v, want device failure", err) + } + ctrl, err := target.Read(t.Context(), swdsim.Request{Read: true, Addr: 4}) + if err != nil || ctrl&(1<<5) != 0 { + t.Fatalf("model failure set STICKYERR: %#x, %v", ctrl, err) + } + } +} + +func TestMappedDeviceBlockCrossesTARBoundary(t *testing.T) { + device := new(memoryDevice) + target := mappedTarget(t, 0x3fc, 12, device) + memory := openedMemory(t, target, apSel(0)) + data := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12} + if n, err := memory.WriteBlock(t.Context(), 0x3fc, data); err != nil || n != len(data) { + t.Fatalf("WriteBlock = %d, %v", n, err) + } + got := make([]byte, len(data)) + if n, err := memory.ReadBlock(t.Context(), 0x3fc, got); err != nil || n != len(got) || !slices.Equal(got, data) { + t.Fatalf("ReadBlock = %d, %v, %v", n, err, got) + } + for i, access := range device.accesses { + if want := uint64(0x3fc + (i%3)*4); access.addr != want || len(access.data) != 4 { + t.Fatalf("access %d = %v, want word at %#x", i, access, want) + } + } +} + +func TestMappedDeviceBlockFaultRetainsConfirmedPrefix(t *testing.T) { + device := &memoryDevice{data: map[uint64]byte{0x3fc: 1}, err: dapsim.ErrBusFault, errAt: 0x400} + target := mappedTarget(t, 0x3fc, 12, device) + memory := openedMemory(t, target, apSel(0)) + got := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + n, err := memory.ReadBlock(t.Context(), 0x3fc, got) + if n != 4 || !errors.Is(err, dap.ErrFault) || !slices.Equal(got[:4], []byte{1, 0, 0, 0}) { + t.Fatalf("ReadBlock = %d, %v, %v; want four confirmed bytes and FAULT", n, err, got) + } + for _, b := range got[4:] { + if b != 0xff { + t.Fatal("fault changed unconfirmed suffix") + } + } + if len(device.accesses) != 2 { + t.Fatalf("access after fault or replay: %v", device.accesses) + } +} + +func TestMappedDeviceReceivesLargeAddress(t *testing.T) { + const addr = uint64(0x12300000100) + device := new(memoryDevice) + target := mappedTarget(t, addr, 4, device) + if err := target.SetMEMAPCFG(apSel(0), 2); err != nil { + t.Fatal(err) + } + memory := openedMemory(t, target, apSel(0)) + if err := memory.WriteScalar(t.Context(), addr, dap.Size32, 7); err != nil { + t.Fatal(err) + } + if len(device.accesses) != 1 || device.accesses[0].addr != addr { + t.Fatalf("large address truncated: %v", device.accesses) + } +} + +func TestMappedDeviceBlockWriteFaultKeepsEarlierEffects(t *testing.T) { + device := &memoryDevice{err: dapsim.ErrBusFault, errAt: 0x400} + target := mappedTarget(t, 0x3fc, 12, device) + memory := openedMemory(t, target, apSel(0)) + n, err := memory.WriteBlock(t.Context(), 0x3fc, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}) + if n != 4 || !errors.Is(err, dap.ErrFault) { + t.Fatalf("WriteBlock = %d, %v; want four confirmed bytes and FAULT", n, err) + } + if len(device.accesses) != 2 || device.data[0x3fc] != 1 || device.data[0x400] != 0 || device.data[0x404] != 0 { + t.Fatalf("unexpected effects or replay: %v, %v", device.accesses, device.data) + } +} + +type wordDevice struct { + memoryDevice + rejected int +} + +func (d *wordDevice) Write(ctx context.Context, addr uint64, data []byte) error { + if len(data) != 4 { + d.rejected++ + return dapsim.ErrBusFault + } + return d.memoryDevice.Write(ctx, addr, data) +} + +func TestMappedDeviceRejectsNarrowWriteWithoutReadModifyWrite(t *testing.T) { + device := new(wordDevice) + target := mappedTarget(t, 0x100, 4, device) + memory := openedMemory(t, target, apSel(0)) + if err := memory.WriteScalar(t.Context(), 0x101, dap.Size8, 7); !errors.Is(err, dap.ErrFault) { + t.Fatalf("narrow write = %v, want FAULT", err) + } + if device.rejected != 1 || len(device.accesses) != 0 { + t.Fatalf("narrow write caused hidden access: %d, %v", device.rejected, device.accesses) + } +} + +func TestLineResetFreezesDeviceMappings(t *testing.T) { + target := dapsim.New(0x2ba01477) + addMEMAPFixture(t, target, 0, memAPIDR, nil) + target.ObserveLineReset() + if err := target.MapMEMAPDevice(apSel(0), 0x100, 4, new(memoryDevice)); err == nil { + t.Fatal("mapping accepted after line reset traffic") + } +} + +func TestMappedDeviceBlockCancellationKeepsConfirmedPrefix(t *testing.T) { + device := &memoryDevice{data: map[uint64]byte{0x3fc: 1}, err: context.Canceled, errAt: 0x400} + target := mappedTarget(t, 0x3fc, 12, device) + memory := openedMemory(t, target, apSel(0)) + got := []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff} + n, err := memory.ReadBlock(t.Context(), 0x3fc, got) + if n != 4 || !errors.Is(err, context.Canceled) || errors.Is(err, dap.ErrFault) { + t.Fatalf("ReadBlock = %d, %v; want four confirmed bytes and cancellation", n, err) + } + if !slices.Equal(got, []byte{1, 0, 0, 0, 0xff, 0xff, 0xff, 0xff}) || len(device.accesses) != 2 { + t.Fatalf("unconfirmed effects or replay: %v, %v", got, device.accesses) + } +} + +type onceFailingDevice struct { + memoryDevice + failure error +} + +func (d *onceFailingDevice) Write(ctx context.Context, addr uint64, data []byte) error { + if err := d.memoryDevice.Write(ctx, addr, data); err != nil { + return err + } + if len(d.accesses) == 1 { + return d.failure + } + return nil +} + +func TestDeviceCallbackFailureCannotRejectOrReplayAcceptedAPWrite(t *testing.T) { + for _, failure := range []error{swd.ErrWait, swd.ErrFault, swd.ErrParity, swd.ErrProtocol, swd.ErrNotExecuted} { + t.Run(failure.Error(), func(t *testing.T) { + device := new(onceFailingDevice) + target := mappedTarget(t, 0x100, 4, device) + dp := enteredDAP(t, target) + memory, err := dap.OpenMemAP(t.Context(), dp, apSel(0)) + if err != nil { + t.Fatal(err) + } + if err := memory.WriteWord(t.Context(), 0x100, 0); err != nil { + t.Fatal(err) + } + device.accesses = nil + device.failure = failure + err = dp.WriteRawAP(t.Context(), apSel(0).Address(0x0c), 7) + if !errors.Is(err, dapsim.ErrDeviceFailure) || errors.Is(err, failure) { + t.Fatalf("callback failure = %v, want host/model failure without protocol classification", err) + } + if len(device.accesses) != 1 || device.data[0x100] != 7 { + t.Fatalf("accepted write replayed or lost: %v, %v", device.accesses, device.data) + } + }) + } +} + +func TestDeviceCallbackFailureCannotBecomeReadAcknowledgement(t *testing.T) { + for _, failure := range []error{swd.ErrWait, swd.ErrFault, swd.ErrParity, swd.ErrProtocol, swd.ErrNotExecuted} { + device := &memoryDevice{err: failure} + target := mappedTarget(t, 0x100, 4, device) + dp := enteredDAP(t, target) + if err := dp.WriteRawAP(t.Context(), apSel(0).Address(0), 2); err != nil { + t.Fatal(err) + } + if err := dp.WriteRawAP(t.Context(), apSel(0).Address(4), 0x100); err != nil { + t.Fatal(err) + } + _, err := dp.ReadRawAP(t.Context(), apSel(0).Address(0x0c)) + if !errors.Is(err, dapsim.ErrDeviceFailure) || errors.Is(err, failure) || len(device.accesses) != 1 { + t.Fatalf("callback read = %v, %v; want one host/model failure", err, device.accesses) + } + } +} diff --git a/dap/sim/ram.go b/dap/sim/ram.go new file mode 100644 index 0000000..d3c3c78 --- /dev/null +++ b/dap/sim/ram.go @@ -0,0 +1,82 @@ +package sim + +import ( + "context" + "errors" +) + +// RAM is sparse byte-addressed memory which can be mapped through multiple +// MEM-APs. Its zero value is usable and contains no written bytes. Serialize all +// device accesses, +// fixture writes and snapshots, including accesses through other APs or targets. +// Mapping bounds belong to the Target; Bytes is a detached snapshot, not bus +// traffic. Unwritten bytes read as zero. +type RAM struct { + memory map[uint64]byte +} + +// SetBytes copies fixture bytes into RAM without a simulated bus access. +func (r *RAM) SetBytes(addr uint64, data []byte) error { + if r == nil { + return errors.New("dap/sim: nil RAM") + } + if _, err := memoryRangeEnd(addr, len(data)); err != nil { + return err + } + if r.memory == nil { + r.memory = make(map[uint64]byte) + } + for i, b := range data { + r.memory[addr+uint64(i)] = b + } + return nil +} + +// Bytes returns a detached copy of size bytes without a simulated bus access. +func (r *RAM) Bytes(addr uint64, size int) ([]byte, error) { + if r == nil { + return nil, errors.New("dap/sim: nil RAM") + } + if _, err := memoryRangeEnd(addr, size); err != nil { + return nil, err + } + data := make([]byte, size) + for i := range data { + data[i] = r.memory[addr+uint64(i)] + } + return data, nil +} + +// Read fills data from one aligned 1, 2, 4 or 8 byte bus access. +func (r *RAM) Read(ctx context.Context, addr uint64, data []byte) error { + if err := checkRAMAccess(ctx, addr, len(data)); err != nil { + return err + } + value, err := r.Bytes(addr, len(data)) + if err != nil { + return err + } + copy(data, value) + return nil +} + +// Write copies data from one aligned 1, 2, 4 or 8 byte bus access. +func (r *RAM) Write(ctx context.Context, addr uint64, data []byte) error { + if err := checkRAMAccess(ctx, addr, len(data)); err != nil { + return err + } + return r.SetBytes(addr, data) +} + +func checkRAMAccess(ctx context.Context, addr uint64, width int) error { + if err := ctx.Err(); err != nil { + return err + } + if width != 1 && width != 2 && width != 4 && width != 8 { + return ErrBusFault + } + if _, err := memoryRangeEnd(addr, width); err != nil || addr%uint64(width) != 0 { + return ErrBusFault + } + return nil +} diff --git a/dap/sim/ram_test.go b/dap/sim/ram_test.go new file mode 100644 index 0000000..8e7c6f7 --- /dev/null +++ b/dap/sim/ram_test.go @@ -0,0 +1,184 @@ +package sim_test + +import ( + "context" + "errors" + "math" + "slices" + "testing" + + "github.com/jon/ostiole/dap" + dapsim "github.com/jon/ostiole/dap/sim" +) + +func TestRAMSharesBytesThroughTwoAPsAndKeepsDevicesPrivate(t *testing.T) { + target := dapsim.New(0x2ba01477) + ram := new(dapsim.RAM) + for i := range uint8(2) { + addMEMAPFixture(t, target, i, memAPIDR, nil) + if err := target.MapMEMAPDevice(apSel(i), 0x100, 0x400, ram); err != nil { + t.Fatal(err) + } + if err := target.MapMEMAPDevice(apSel(i), 0xe000ed00, 4, new(memoryDevice)); err != nil { + t.Fatal(err) + } + } + dp := enteredDAP(t, target) + var memories [2]*dap.MemAP + for i := range memories { + m, err := dap.OpenMemAP(t.Context(), dp, apSel(uint8(i))) + if err != nil { + t.Fatal(err) + } + memories[i] = m + t.Cleanup(func() { + if err := m.Release(context.Background()); err != nil { + t.Error(err) + } + }) + } + if err := memories[0].WriteWord(t.Context(), 0x100, 0x12345678); err != nil { + t.Fatal(err) + } + if got, err := memories[1].ReadWord(t.Context(), 0x100); err != nil || got != 0x12345678 { + t.Fatalf("shared RAM read = %#x, %v", got, err) + } + if err := memories[1].WriteScalar(t.Context(), 0x101, dap.Size8, 0xaa); err != nil { + t.Fatal(err) + } + if got, err := memories[0].ReadWord(t.Context(), 0x100); err != nil || got != 0x1234aa78 { + t.Fatalf("peer narrow write = %#x, %v", got, err) + } + if err := memories[0].WriteWord(t.Context(), 0xe000ed00, 1); err != nil { + t.Fatal(err) + } + if got, err := memories[1].ReadWord(t.Context(), 0xe000ed00); err != nil || got != 0 { + t.Fatalf("core-private device leaked to peer: %d, %v", got, err) + } + data, err := ram.Bytes(0x100, 4) + if err != nil || !slices.Equal(data, []byte{0x78, 0xaa, 0x34, 0x12}) { + t.Fatalf("RAM snapshot = %v, %v", data, err) + } +} + +func TestRAMFixturesCopyAndRejectOverflow(t *testing.T) { + ram := new(dapsim.RAM) + data := []byte{1, 2, 3, 4} + if err := ram.SetBytes(0x100, data); err != nil { + t.Fatal(err) + } + data[0] = 9 + got, err := ram.Bytes(0x100, 4) + if err != nil || !slices.Equal(got, []byte{1, 2, 3, 4}) { + t.Fatalf("RAM fixture = %v, %v", got, err) + } + got[0] = 9 + again, err := ram.Bytes(0x100, 4) + if err != nil || again[0] != 1 { + t.Fatalf("snapshot aliases RAM: %v, %v", again, err) + } + if err := ram.SetBytes(math.MaxUint64, []byte{5, 6}); err == nil { + t.Fatal("overflowing fixture write succeeded") + } + if _, err := ram.Bytes(math.MaxUint64, 2); err == nil { + t.Fatal("overflowing snapshot succeeded") + } + if _, err := ram.Bytes(0, -1); err == nil { + t.Fatal("negative snapshot succeeded") + } + if err := ram.SetBytes(math.MaxUint64, []byte{5}); err != nil { + t.Fatal(err) + } + if got, err := ram.Bytes(math.MaxUint64, 1); err != nil || got[0] != 5 { + t.Fatalf("last byte = %v, %v", got, err) + } +} + +func TestRAMDeviceRejectsCancellationAndInvalidAccess(t *testing.T) { + ram := new(dapsim.RAM) + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if err := ram.Write(ctx, 0x100, []byte{1}); !errors.Is(err, context.Canceled) { + t.Fatalf("canceled write = %v", err) + } + if err := ram.Read(ctx, 0x100, []byte{1}); !errors.Is(err, context.Canceled) { + t.Fatalf("canceled read = %v", err) + } + for _, test := range []struct { + addr uint64 + size int + }{{0, 0}, {0, 3}, {1, 2}, {math.MaxUint64, 4}} { + if err := ram.Write(t.Context(), test.addr, make([]byte, test.size)); !errors.Is(err, dapsim.ErrBusFault) { + t.Fatalf("invalid write %+v = %v", test, err) + } + if err := ram.Read(t.Context(), test.addr, make([]byte, test.size)); !errors.Is(err, dapsim.ErrBusFault) { + t.Fatalf("invalid read %+v = %v", test, err) + } + } + if got, err := ram.Bytes(0x100, 1); err != nil || got[0] != 0 { + t.Fatalf("canceled write changed RAM: %v, %v", got, err) + } + if err := (*dapsim.RAM)(nil).SetBytes(0, []byte{1}); err == nil { + t.Fatal("nil RAM fixture succeeded") + } + if _, err := (*dapsim.RAM)(nil).Bytes(0, 1); err == nil { + t.Fatal("nil RAM snapshot succeeded") + } + if err := (*dapsim.RAM)(nil).Read(t.Context(), 0, []byte{1}); err == nil { + t.Fatal("nil RAM read succeeded") + } + if err := (*dapsim.RAM)(nil).Write(t.Context(), 0, []byte{1}); err == nil { + t.Fatal("nil RAM write succeeded") + } +} + +func TestRAMSharedADIv6ViewsUseTheirOwnByteOrder(t *testing.T) { + target := dapsim.New(0x4c013477) + if err := target.SetDPRegister(dap.DPIDR1, 32); err != nil { + t.Fatal(err) + } + ram := new(dapsim.RAM) + var selectors [2]dap.APSel + for i := range selectors { + sel, err := dap.APAt(uint64(0x2000 + i*0x2000)) + if err != nil { + t.Fatal(err) + } + selectors[i] = sel + if err := target.AddMEMAP(sel, memAPIDR, nil); err != nil { + t.Fatal(err) + } + if err := target.SetMEMAPCFG(sel, uint32(i)); err != nil { + t.Fatal(err) + } + if err := target.MapMEMAPDevice(sel, 0x100, 4, ram); err != nil { + t.Fatal(err) + } + } + dp := enteredDAP(t, target) + var memories [2]*dap.MemAP + for i, sel := range selectors { + m, err := dap.OpenMemAP(t.Context(), dp, sel) + if err != nil { + t.Fatal(err) + } + memories[i] = m + t.Cleanup(func() { + if err := m.Release(context.Background()); err != nil { + t.Error(err) + } + }) + } + if err := memories[0].WriteWord(t.Context(), 0x100, 0x12345678); err != nil { + t.Fatal(err) + } + if got, err := memories[1].ReadWord(t.Context(), 0x100); err != nil || got != 0x78563412 { + t.Fatalf("big-endian view = %#x, %v", got, err) + } + if err := memories[1].WriteScalar(t.Context(), 0x102, dap.Size16, 0xabcd); err != nil { + t.Fatal(err) + } + if got, err := memories[0].ReadWord(t.Context(), 0x100); err != nil || got != 0xcdab5678 { + t.Fatalf("little-endian peer = %#x, %v", got, err) + } +} diff --git a/dap/sim/target.go b/dap/sim/target.go index ce5b1b3..ba597cb 100644 --- a/dap/sim/target.go +++ b/dap/sim/target.go @@ -31,8 +31,10 @@ const ( dlcrTurnaroundMask = uint32(3 << 8) ) -// Target models the initial SW-DP register state. +// Target models SW-DP, AP and target-memory state. All calls, fixture changes, +// and uses of devices shared with other targets must be serialized. type Target struct { + started bool debugWords map[uint64]uint32 dpidr uint32 ctrlStat uint32 @@ -47,6 +49,7 @@ type Target struct { } type accessPort struct { + devices []deviceMapping regs map[uint8]uint32 memory map[uint64]byte memAP bool @@ -98,6 +101,7 @@ func (t *Target) ObserveLineReset() { if t == nil { return } + t.started = true t.dpBanks[1] = 0 if t.dpidr>>12&15 == 3 { t.selectDP &^= 15 @@ -120,6 +124,7 @@ func (t *Target) Acknowledge(ctx context.Context, req swdsim.Request) error { if err := ctx.Err(); err != nil { return err } + t.started = true sticky := stickyOverrun | stickyCompare | stickyError | writeDataError if t.ctrlStat&sticky == 0 || isStickyExempt(req, t.selectDP) { return nil @@ -284,7 +289,8 @@ func (t *Target) SetMEMAPSizes(sel dap.APSel, sizes ...dap.TransferSize) error { return nil } -// SetMEMAPBytes copies data into one simulated MEM-AP's target memory. +// SetMEMAPBytes copies data into one simulated MEM-AP's ordinary target memory. +// It rejects ranges overlapping mapped devices without calling the device. func (t *Target) SetMEMAPBytes(sel dap.APSel, addr uint64, data []byte) error { if t == nil { return errors.New("dap/sim: nil target") @@ -297,16 +303,21 @@ func (t *Target) SetMEMAPBytes(sel dap.APSel, addr uint64, data []byte) error { if ap == nil || !ap.memAP { return fmt.Errorf("dap/sim: AP %d is not a MEM-AP", selection) } - if _, err := memoryRangeEnd(addr, len(data)); err != nil { + end, err := memoryRangeEnd(addr, len(data)) + if err != nil { return err } + if len(data) != 0 && ap.overlapsDevice(addr, end) { + return errors.New("dap/sim: fixture write overlaps mapped device") + } for i := range data { ap.memory[addr+uint64(i)] = data[i] } return nil } -// MEMAPBytes returns a copy of one simulated MEM-AP's target memory range. +// MEMAPBytes returns a copy of one simulated MEM-AP's ordinary memory range. +// It rejects ranges overlapping mapped devices without calling the device. func (t *Target) MEMAPBytes(sel dap.APSel, addr uint64, size int) ([]byte, error) { if t == nil { return nil, errors.New("dap/sim: nil target") @@ -319,9 +330,13 @@ func (t *Target) MEMAPBytes(sel dap.APSel, addr uint64, size int) ([]byte, error if ap == nil || !ap.memAP { return nil, fmt.Errorf("dap/sim: AP %d is not a MEM-AP", selection) } - if _, err := memoryRangeEnd(addr, size); err != nil { + end, err := memoryRangeEnd(addr, size) + if err != nil { return nil, err } + if size != 0 && ap.overlapsDevice(addr, end) { + return nil, errors.New("dap/sim: fixture read overlaps mapped device") + } data := make([]byte, size) for i := range data { data[i] = ap.memory[addr+uint64(i)] @@ -354,8 +369,9 @@ func (t *Target) Read(ctx context.Context, req swdsim.Request) (uint32, error) { if err := ctx.Err(); err != nil { return 0, err } + t.started = true if req.AP { - return t.readAP(req) + return t.readAP(ctx, req) } switch req.Addr { case 0x00: @@ -386,8 +402,9 @@ func (t *Target) Write(ctx context.Context, req swdsim.Request, value uint32) er if err := ctx.Err(); err != nil { return err } + t.started = true if req.AP { - return t.writeAP(req, value) + return t.writeAP(ctx, req, value) } switch req.Addr { case 0x00: @@ -412,7 +429,7 @@ func validateRequest(req swdsim.Request, read bool) error { return nil } -func (t *Target) readAP(req swdsim.Request) (uint32, error) { +func (t *Target) readAP(ctx context.Context, req swdsim.Request) (uint32, error) { posted := t.rdbuff if value, ok := t.debugWords[uint64(t.selectHigh)<<32|uint64(t.selectDP&^15)|uint64(req.Addr)]; ok && t.dpidr>>12&15 == 3 { t.rdbuff = value @@ -427,7 +444,11 @@ func (t *Target) readAP(req swdsim.Request) (uint32, error) { if err != nil { return 0, err } - value, err := ap.readRegister(reg) + value, err := ap.readRegister(ctx, reg) + if errors.Is(err, ErrBusFault) { + t.ctrlStat |= stickyError + return posted, nil + } if err != nil { return 0, err } @@ -435,7 +456,7 @@ func (t *Target) readAP(req swdsim.Request) (uint32, error) { return posted, nil } -func (t *Target) writeAP(req swdsim.Request, value uint32) error { +func (t *Target) writeAP(ctx context.Context, req swdsim.Request, value uint32) error { ap := t.aps[t.selectedAP()] if ap == nil { return nil @@ -444,10 +465,15 @@ func (t *Target) writeAP(req swdsim.Request, value uint32) error { if err != nil { return err } - return ap.writeRegister(reg, value) + err = ap.writeRegister(ctx, reg, value) + if errors.Is(err, ErrBusFault) { + t.ctrlStat |= stickyError + return nil + } + return err } -func (ap *accessPort) readRegister(reg uint8) (uint32, error) { +func (ap *accessPort) readRegister(ctx context.Context, reg uint8) (uint32, error) { if !ap.memAP { return ap.regs[reg], nil } @@ -458,7 +484,7 @@ func (ap *accessPort) readRegister(reg uint8) (uint32, error) { return 0, nil } if reg == 0x0c { - return ap.readDRW() + return ap.readDRW(ctx) } if reg == 0 { ap.largePhase = largeDataIdle @@ -466,7 +492,7 @@ func (ap *accessPort) readRegister(reg uint8) (uint32, error) { return ap.regs[reg], nil } -func (ap *accessPort) writeRegister(reg uint8, value uint32) error { +func (ap *accessPort) writeRegister(ctx context.Context, reg uint8, value uint32) error { if !ap.memAP { ap.regs[reg] = value return nil @@ -481,7 +507,7 @@ func (ap *accessPort) writeRegister(reg uint8, value uint32) error { return nil } if reg == 0x0c { - return ap.writeDRW(value) + return ap.writeDRW(ctx, value) } if reg == 0 && ap.sizes&(1< 3 || addrInc == 0x20 || addrInc == 0x30 || addrInc == 0x10 && size != 2 { + if !ap.supportsDRWAddressing() { return 0, errors.New("dap/sim: DRW read has unsupported CSW.Size or AddrInc") } if size == 3 { @@ -512,7 +537,10 @@ func (ap *accessPort) readDRW() (uint32, error) { } switch ap.largePhase { case largeDataIdle: - value := ap.memoryValue(ap.targetAddress(), 8) + value, err := ap.memoryValue(ctx, ap.targetAddress(), 8) + if err != nil { + return 0, err + } ap.largeWord = uint32(value >> 32) ap.largePhase = largeDataRead return uint32(value), nil @@ -525,15 +553,17 @@ func (ap *accessPort) readDRW() (uint32, error) { } } width := 1 << size - value := uint32(ap.memoryValue(ap.targetAddress(), width)) + value, err := ap.memoryValue(ctx, ap.targetAddress(), width) + if err != nil { + return 0, err + } ap.incrementTAR(width) - return value << ap.laneShift(width), nil + return uint32(value) << ap.laneShift(width), nil } -func (ap *accessPort) writeDRW(value uint32) error { +func (ap *accessPort) writeDRW(ctx context.Context, value uint32) error { size := uint8(ap.regs[0] & 7) - addrInc := ap.regs[0] & 0x30 - if size > 3 || addrInc == 0x20 || addrInc == 0x30 || addrInc == 0x10 && size != 2 { + if !ap.supportsDRWAddressing() { return errors.New("dap/sim: DRW write has unsupported CSW.Size or AddrInc") } if size == 3 { @@ -546,20 +576,26 @@ func (ap *accessPort) writeDRW(value uint32) error { ap.largePhase = largeDataWrite return nil case largeDataWrite: - ap.writeMemoryValue(ap.targetAddress(), 8, uint64(ap.largeWord)|uint64(value)<<32) ap.largePhase = largeDataIdle - return nil + return ap.writeMemoryValue(ctx, ap.targetAddress(), 8, uint64(ap.largeWord)|uint64(value)<<32) default: return errors.New("dap/sim: 64-bit DRW write interrupted a read") } } width := 1 << size data := uint64(value >> ap.laneShift(width)) - ap.writeMemoryValue(ap.targetAddress(), width, data) + if err := ap.writeMemoryValue(ctx, ap.targetAddress(), width, data); err != nil { + return err + } ap.incrementTAR(width) return nil } +func (ap *accessPort) supportsDRWAddressing() bool { + size, inc := ap.regs[0]&7, ap.regs[0]&0x30 + return size <= 3 && (inc == 0 || inc == 0x10 && size == 2) +} + func (ap *accessPort) incrementTAR(width int) { if ap.regs[0]&0x30 != 0x10 { return @@ -584,28 +620,34 @@ func (ap *accessPort) laneShift(width int) uint { return uint(lane * 8) } -func (ap *accessPort) memoryValue(addr uint64, width int) uint64 { +func (ap *accessPort) memoryValue(ctx context.Context, addr uint64, width int) (uint64, error) { + data, err := ap.readMemory(ctx, addr, width) + if err != nil { + return 0, err + } var value uint64 if ap.regs[0xf4]&1 != 0 { for offset := range width { - value = value<<8 | uint64(ap.memory[addr+uint64(offset)]) + value = value<<8 | uint64(data[offset]) } - return value + return value, nil } for offset := range width { - value |= uint64(ap.memory[addr+uint64(offset)]) << uint(offset*8) + value |= uint64(data[offset]) << uint(offset*8) } - return value + return value, nil } -func (ap *accessPort) writeMemoryValue(addr uint64, width int, value uint64) { +func (ap *accessPort) writeMemoryValue(ctx context.Context, addr uint64, width int, value uint64) error { + data := make([]byte, width) for offset := range width { shift := offset * 8 if ap.regs[0xf4]&1 != 0 { shift = (width - 1 - offset) * 8 } - ap.memory[addr+uint64(offset)] = byte(value >> uint(shift)) + data[offset] = byte(value >> uint(shift)) } + return ap.writeMemory(ctx, addr, data) } func (t *Target) apReg(req swdsim.Request) (uint8, error) { diff --git a/dap/sim/target_test.go b/dap/sim/target_test.go index 6397967..1a8b4c7 100644 --- a/dap/sim/target_test.go +++ b/dap/sim/target_test.go @@ -108,14 +108,14 @@ func TestTargetSnapshotsSize64ReadData(t *testing.T) { ap := target.aps[sel] ap.regs[0] = 3 ap.regs[4] = 0x100 - low, err := ap.readDRW() + low, err := ap.readDRW(t.Context()) if err != nil { t.Fatal(err) } if err := target.SetMEMAPBytes(sel, 0x104, []byte{0xaa, 0xbb, 0xcc, 0xdd}); err != nil { t.Fatal(err) } - high, err := ap.readDRW() + high, err := ap.readDRW(t.Context()) if err != nil { t.Fatal(err) } @@ -133,16 +133,16 @@ func TestTargetAcceptsAutoIncrementOnlyForSize32(t *testing.T) { ap := target.aps[sel] for _, size := range []uint32{0, 1, 3} { ap.regs[0] = size | 0x10 - if _, err := ap.readDRW(); err == nil { + if _, err := ap.readDRW(t.Context()); err == nil { t.Errorf("DRW read accepted CSW.Size encoding %d with address increment", size) } - if err := ap.writeDRW(0); err == nil { + if err := ap.writeDRW(t.Context(), 0); err == nil { t.Errorf("DRW write accepted CSW.Size encoding %d with address increment", size) } } ap.regs[0] = 2 | 0x10 ap.regs[4] = 0x100 - if _, err := ap.readDRW(); err != nil { + if _, err := ap.readDRW(t.Context()); err != nil { t.Fatalf("Size32 incrementing DRW read: %v", err) } if ap.regs[4] != 0x104 { @@ -161,25 +161,25 @@ func TestTargetRequiresCSWToTerminateIncompleteSize64Transfer(t *testing.T) { } ap := target.aps[sel] ap.regs[0] = 3 - if _, err := ap.readDRW(); err != nil { + if _, err := ap.readDRW(t.Context()); err != nil { t.Fatal(err) } if err := target.Write(t.Context(), swdsim.Request{AP: true, Addr: 0x04}, 0x100); err == nil { t.Fatal("TAR write succeeded during incomplete Size64 read") } - if err := ap.writeDRW(0); err == nil { + if err := ap.writeDRW(t.Context(), 0); err == nil { t.Fatal("DRW write succeeded during incomplete Size64 read") } if err := target.Write(t.Context(), swdsim.Request{AP: true, Addr: 0x00}, 3); err != nil { t.Fatal(err) } - if err := ap.writeDRW(0x55667788); err != nil { + if err := ap.writeDRW(t.Context(), 0x55667788); err != nil { t.Fatal(err) } if _, err := target.Read(t.Context(), swdsim.Request{AP: true, Read: true, Addr: 0x04}); err == nil { t.Fatal("TAR read succeeded during incomplete Size64 write") } - if _, err := ap.readDRW(); err == nil { + if _, err := ap.readDRW(t.Context()); err == nil { t.Fatal("DRW read succeeded during incomplete Size64 write") } if _, err := target.Read(t.Context(), swdsim.Request{AP: true, Read: true, Addr: 0x00}); err != nil { diff --git a/docs/architecture.md b/docs/architecture.md index 9cea2ad..a1e2209 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -378,7 +378,12 @@ host-specific state. The public simulations implement the same boundaries consumed by production code. `swd/sim` provides a wire, and `dap/sim` provides DP, AP, and MEM-AP state -behind that wire. +behind that wire. A MEM-AP can map bounded target address ranges to a +`dap/sim.MemoryDevice`. Devices receive complete byte accesses; the MEM-AP still +owns byte order, address lanes, posted reads, and TAR incrementing. Mappings are +fixed before target traffic. `dap/sim.RAM` can supply shared byte storage +through several APs, while separate devices model private register windows at +the same address. Unmapped addresses keep their ordinary fixture memory. Production packages do not import their simulators. Tests and downstream programs may compose them explicitly, which keeps hardware-free behavior diff --git a/docs/capabilities.md b/docs/capabilities.md index fb18704..6078b3e 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -222,35 +222,35 @@ baseline JTAG-DP. See [JTAG](protocols/jtag.md) for effects and ownership. ## Debug Access Port and MEM-AP -| Capability | Implemented | Validation and boundary | -| ------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| DPIDR decoding | Yes | Validates the constant bit and exposes raw identity fields. | -| Connection identity | Yes | `Connect` returns an opaque `Identity`; `DPIDR()` and `IDCODE()` distinguish present identification registers. SW-DP supplies only DPIDR; JTAG-DP supplies only IDCODE. The last successful identity remains cached after release or cleanup failure. | -| Port binding | Yes | `NewDebugPort` takes an opaque `Port` constructed with `SWDP(conn)` or `JTAGDP(chain, tapIndex)`. Constructors send no traffic. JTAG requires a complete explicit layout and a zero-based, TDO-first index with a four- or eight-bit IR. The zero binding and invalid inputs fail before hardware access. | -| Baseline JTAG-DP | Yes | Supports IDCODE, CTRL/STAT, readable SELECT, RDBUFF, baseline ABORT, and AP access. The executor tracks delayed responses, polls accepted operations without replay, and checks CTRL/STAT after every AP operation. Inherited ORUNDETECT is temporarily disabled and restored; active pushed or counted modes are rejected. Exact chain revalidation precedes restoration after scan-state loss. Independent cleanup defaults to thirty seconds and can be configured with `WithCleanupTimeout`. | -| SW-DP connection | Yes | `DebugPort.Connect` uses the SWD connection's established DPIDR and response grammar, then requests acknowledged power. It records newly requested power bits before writing them and attempts bounded cleanup after failed setup. Failed cleanup remains retryable through `Release`. | -| SW-DP release | Yes | Restores SELECT to bank zero, settles that write through RDBUFF, clears only power requests acquired by the debug port, then releases the SWD connection. If framing is unknown, cleanup first performs bounded SWD re-entry and verifies DPIDR against the connection being cleaned up. Failed release can be retried and blocks ordinary operations in the meantime. | -| DP register access | Yes | Logical ADIv5 registers distinguish operations which share a wire offset, including DPIDR from ABORT and SELECT from RESEND. Access selects the required bank while preserving known AP fields. CTRL/STAT writes must preserve connection-owned ORUNDETECT. Wrong directions, unknown registers, unavailable DP versions, and writes requiring unsupported turnaround fail before traffic. DAPABORT invalidates AP-derived state. Access is blocked while cleanup is pending. | -| AP identity | Yes | `NewAPSel` constructs an AP selector whose zero value is invalid. `ReadAPIDR` reads the common read-only IDR, and `DecodeAPIDR` exposes its ADIv5 fields. | -| Raw AP access | Yes | `APSel.Address` combines a selector with a complete eight-bit register address; both types have invalid zero values. Immediate and queued operations reject invalid or unaligned addresses before traffic. Reads use RDBUFF and writes use a completion barrier. Either operation invalidates existing MEM-AP handles if it completes or might have completed. A request canceled before it is sent does not invalidate them. Raw access has the effects defined by the selected AP class; a MEM-AP data-register write can write target memory. APIDR writes fail before traffic. | -| Ordered transactions | Yes | A single-use queue validates every DP/AP operation before traffic and settles any earlier immediate DP write before the queue runs. Queued reads expose data through `ReadResult.Value`; queued writes expose completion through `WriteResult.Err`. DP writes and AP operations settle through RDBUFF. Ordinary fixed frames use SWD batches; DPIDR, CTRL/STAT, and ABORT remain physical boundaries. The queue retains a confirmed prefix after failure and distinguishes later unsent work from operations in a failed physical chunk. FT232H HIL completed nine requests in two SWDIO calls. | -| WAIT handling | Yes | SWD retries a rejected physical request after required STICKYORUN cleanup. JTAG polls the preceding accepted request; requests shifted during WAIT were discarded. `WithMaxWaits` bounds WAIT observations; zero uses only the operation context, and `SetMaxWaits` changes the bound while idle. Cancellation supplies the operation error, while independent cleanup failures remain visible. Pending AP work that exhausts its bound requires ABORT and AP-state invalidation, without replay. An accepted JTAG operation remains indeterminate, not unexecuted, when its completion cannot be established. | -| FAULT recovery | Yes | A FAULT is never replayed. With a known response grammar and bank-zero selection, the error includes the captured CTRL/STAT value and DAP clears only the sticky conditions reported there, then verifies that they are clear. A definitely abandoned AP write does not invalidate MEM-AP state; an uncertain effect does. Failed cleanup preserves the FAULT and blocks ordinary traffic until release repairs the port. | -| AP enumeration | Yes | Scans all 256 ADIv5 APSEL values in bounded transactions. IDR zero means absent; a FAULT returns the confirmed discoveries with the error. The current Cortex-M bench reports AP0 as `0x24770011` and AP1 as `0x02880000`; sparse numbering is covered by simulation. The scan used 32 SWDIO calls for 1,022 fixed frames, all with OK acknowledgements. | -| MEM-AP acquisition | Yes | `OpenMemAP` performs AP traffic, rejects an absent or non-MEM AP, and snapshots the state which `Release` restores. | -| MEM-AP debug entry | Yes | `ReadDebugBase` decodes ADIv5 and legacy BASE formats, distinguishes absence from address zero, and reads the upper word only for a present entry with CFG.LA. It preserves the memory client on success and does not access target memory. Behavioral tests cover formats, malformed values, cancellation, failure, retry, and shared SWD/JTAG access. | -| MEM-AP configuration | Yes | `OpenMemAP` reads CFG, models BE, LA, and LD, and includes TARHI in retryable restoration when large addresses are available. | -| Scalar target-memory access | Yes | `ReadScalar` and `WriteScalar` support aligned 8-, 16-, and 32-bit values and verify the implementation-defined CSW.Size before using the byte lane selected by CFG.BE. CFG.LA permits addresses above 32 bits; CFG.LD makes 64-bit access eligible for the same CSW check. Oversized write values fail before traffic, and writes finish with an AP completion barrier. If the first DRW access of a failed Size64 transfer might have started, ordinary traffic remains blocked until cleanup. `ReadWord` and `WriteWord` provide 32-bit convenience operations. | -| MEM-AP restoration | Yes | Saves and restores CSW, TAR, and TARHI when present; failed restoration remains retryable. MEM-AP restoration remains available while debug-port cleanup is pending. If framing is unknown, `Release` re-enters the bound protocol and verifies identity before restoration. It terminates a possibly incomplete Size64 transfer through CSW before touching TAR or TARHI. If DAPABORT interrupts cleanup, the next `Release` retries every saved value. The invalidated handle remains invalid. | -| Managed target-memory writes | Yes | `WriteScalar` and `WriteBlock` are effectful. The caller selects the address; the API checks alignment and range, not whether that address is safe to modify. `WriteRawAP` remains an unmanaged escape hatch. | -| Block reads | Yes | Accepts empty, unaligned, and mixed-width ranges. No auto-incrementing word run crosses a 1 KiB TAR boundary. If the MEM-AP does not accept single address increment, the reader writes TAR before each word. It uses the ordinary DAP WAIT policy. If selection, framing, or cleanup becomes uncertain, repair is required. A FAULT returns only the confirmed prefix. Cancellation and transport or protocol failures can also interrupt the read. Unread destination bytes remain untouched. | -| Block writes | Yes | Uses the block-read geometry, bounded chunks, and the binding's WAIT policy. If single address increment is unavailable, `WriteBlock` writes TAR before each word. Accepted writes are never replayed. SWD confirms buffered chunks through RDBUFF; sequential JTAG checks CTRL/STAT after each write and can return a confirmed prefix within a chunk. An uncertain write reports `ErrIndeterminate` and invalidates the MEM-AP without replay. | -| DPv3 discovery registers | Yes | SW-DP supports DPIDR1, BASEPTR0/1, SELECT1, and bank-zero DPIDR reads. Immediate AP access supports ADIv6 base addresses and 4 KiB register windows. Queued AP operations, 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. | +| Capability | Implemented | Validation and boundary | +| ------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| DPIDR decoding | Yes | Validates the constant bit and exposes raw identity fields. | +| Connection identity | Yes | `Connect` returns an opaque `Identity`; `DPIDR()` and `IDCODE()` distinguish present identification registers. SW-DP supplies only DPIDR; JTAG-DP supplies only IDCODE. The last successful identity remains cached after release or cleanup failure. | +| Port binding | Yes | `NewDebugPort` takes an opaque `Port` constructed with `SWDP(conn)` or `JTAGDP(chain, tapIndex)`. Constructors send no traffic. JTAG requires a complete explicit layout and a zero-based, TDO-first index with a four- or eight-bit IR. The zero binding and invalid inputs fail before hardware access. | +| Baseline JTAG-DP | Yes | Supports IDCODE, CTRL/STAT, readable SELECT, RDBUFF, baseline ABORT, and AP access. The executor tracks delayed responses, polls accepted operations without replay, and checks CTRL/STAT after every AP operation. Inherited ORUNDETECT is temporarily disabled and restored; active pushed or counted modes are rejected. Exact chain revalidation precedes restoration after scan-state loss. Independent cleanup defaults to thirty seconds and can be configured with `WithCleanupTimeout`. | +| SW-DP connection | Yes | `DebugPort.Connect` uses the SWD connection's established DPIDR and response grammar, then requests acknowledged power. It records newly requested power bits before writing them and attempts bounded cleanup after failed setup. Failed cleanup remains retryable through `Release`. | +| SW-DP release | Yes | Restores SELECT to bank zero, settles that write through RDBUFF, clears only power requests acquired by the debug port, then releases the SWD connection. If framing is unknown, cleanup first performs bounded SWD re-entry and verifies DPIDR against the connection being cleaned up. Failed release can be retried and blocks ordinary operations in the meantime. | +| DP register access | Yes | Logical ADIv5 registers distinguish operations which share a wire offset, including DPIDR from ABORT and SELECT from RESEND. Access selects the required bank while preserving known AP fields. CTRL/STAT writes must preserve connection-owned ORUNDETECT. Wrong directions, unknown registers, unavailable DP versions, and writes requiring unsupported turnaround fail before traffic. DAPABORT invalidates AP-derived state. Access is blocked while cleanup is pending. | +| AP identity | Yes | `NewAPSel` constructs an AP selector whose zero value is invalid. `ReadAPIDR` reads the common read-only IDR, and `DecodeAPIDR` exposes its ADIv5 fields. | +| Raw AP access | Yes | `APSel.Address` combines a selector with a complete eight-bit register address; both types have invalid zero values. Immediate and queued operations reject invalid or unaligned addresses before traffic. Reads use RDBUFF and writes use a completion barrier. Either operation invalidates existing MEM-AP handles if it completes or might have completed. A request canceled before it is sent does not invalidate them. Raw access has the effects defined by the selected AP class; a MEM-AP data-register write can write target memory. APIDR writes fail before traffic. | +| Ordered transactions | Yes | A single-use queue validates every DP/AP operation before traffic and settles any earlier immediate DP write before the queue runs. Queued reads expose data through `ReadResult.Value`; queued writes expose completion through `WriteResult.Err`. DP writes and AP operations settle through RDBUFF. Ordinary fixed frames use SWD batches; DPIDR, CTRL/STAT, and ABORT remain physical boundaries. The queue retains a confirmed prefix after failure and distinguishes later unsent work from operations in a failed physical chunk. FT232H HIL completed nine requests in two SWDIO calls. | +| WAIT handling | Yes | SWD retries a rejected physical request after required STICKYORUN cleanup. JTAG polls the preceding accepted request; requests shifted during WAIT were discarded. `WithMaxWaits` bounds WAIT observations; zero uses only the operation context, and `SetMaxWaits` changes the bound while idle. Cancellation supplies the operation error, while independent cleanup failures remain visible. Pending AP work that exhausts its bound requires ABORT and AP-state invalidation, without replay. An accepted JTAG operation remains indeterminate, not unexecuted, when its completion cannot be established. | +| FAULT recovery | Yes | A FAULT is never replayed. With a known response grammar and bank-zero selection, the error includes the captured CTRL/STAT value and DAP clears only the sticky conditions reported there, then verifies that they are clear. A definitely abandoned AP write does not invalidate MEM-AP state; an uncertain effect does. Failed cleanup preserves the FAULT and blocks ordinary traffic until release repairs the port. | +| AP enumeration | Yes | Scans all 256 ADIv5 APSEL values in bounded transactions. IDR zero means absent; a FAULT returns the confirmed discoveries with the error. The current Cortex-M bench reports AP0 as `0x24770011` and AP1 as `0x02880000`; sparse numbering is covered by simulation. The scan used 32 SWDIO calls for 1,022 fixed frames, all with OK acknowledgements. | +| MEM-AP acquisition | Yes | `OpenMemAP` performs AP traffic, rejects an absent or non-MEM AP, and snapshots the state which `Release` restores. | +| MEM-AP debug entry | Yes | `ReadDebugBase` decodes ADIv5 and legacy BASE formats, distinguishes absence from address zero, and reads the upper word only for a present entry with CFG.LA. It preserves the memory client on success and does not access target memory. Behavioral tests cover formats, malformed values, cancellation, failure, retry, and shared SWD/JTAG access. | +| MEM-AP configuration | Yes | `OpenMemAP` reads CFG, models BE, LA, and LD, and includes TARHI in retryable restoration when large addresses are available. | +| Scalar target-memory access | Yes | `ReadScalar` and `WriteScalar` support aligned 8-, 16-, and 32-bit values and verify the implementation-defined CSW.Size before using the byte lane selected by CFG.BE. CFG.LA permits addresses above 32 bits; CFG.LD makes 64-bit access eligible for the same CSW check. Oversized write values fail before traffic, and writes finish with an AP completion barrier. If the first DRW access of a failed Size64 transfer might have started, ordinary traffic remains blocked until cleanup. `ReadWord` and `WriteWord` provide 32-bit convenience operations. | +| MEM-AP restoration | Yes | Saves and restores CSW, TAR, and TARHI when present; failed restoration remains retryable. MEM-AP restoration remains available while debug-port cleanup is pending. If framing is unknown, `Release` re-enters the bound protocol and verifies identity before restoration. It terminates a possibly incomplete Size64 transfer through CSW before touching TAR or TARHI. If DAPABORT interrupts cleanup, the next `Release` retries every saved value. The invalidated handle remains invalid. | +| Managed target-memory writes | Yes | `WriteScalar` and `WriteBlock` are effectful. The caller selects the address; the API checks alignment and range, not whether that address is safe to modify. `WriteRawAP` remains an unmanaged escape hatch. | +| Block reads | Yes | Accepts empty, unaligned, and mixed-width ranges. No auto-incrementing word run crosses a 1 KiB TAR boundary. If the MEM-AP does not accept single address increment, the reader writes TAR before each word. It uses the ordinary DAP WAIT policy. If selection, framing, or cleanup becomes uncertain, repair is required. A FAULT returns only the confirmed prefix. Cancellation and transport or protocol failures can also interrupt the read. Unread destination bytes remain untouched. | +| Block writes | Yes | Uses the block-read geometry, bounded chunks, and the binding's WAIT policy. If single address increment is unavailable, `WriteBlock` writes TAR before each word. Accepted writes are never replayed. SWD confirms buffered chunks through RDBUFF; sequential JTAG checks CTRL/STAT after each write and can return a confirmed prefix within a chunk. An uncertain write reports `ErrIndeterminate` and invalidates the MEM-AP without replay. | +| DPv3 discovery registers | Yes | SW-DP supports DPIDR1, BASEPTR0/1, SELECT1, and bank-zero DPIDR reads. Immediate AP access supports ADIv6 base addresses and 4 KiB register windows. Queued AP operations, 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. Bounded mapped devices receive one complete access with its context, absolute address, and byte width. RAM can share one byte store through several APs, with detached fixture snapshots. Mapping crossings and device bus faults set STICKYERR after AP acceptance; later requests report FAULT. Other callback errors report ErrDeviceFailure without acknowledgement classifications or replay. Mapping is fixed before traffic, and fixture helpers reject mapped ranges. No bus latency or device execution is modeled by the DAP simulator. 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. | Connecting and using a MEM-AP changes volatile debug state; its write methods also change target memory. Applications must release the MEM-AP before the debug diff --git a/docs/ports/dap.md b/docs/ports/dap.md index 2641bd2..4d01f89 100644 --- a/docs/ports/dap.md +++ b/docs/ports/dap.md @@ -632,6 +632,84 @@ 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. +## Mapping simulated target devices + +`dap/sim.Target.MapMEMAPDevice` replaces a bounded range of a MEM-AP's ordinary +fixture memory with a `dap/sim.MemoryDevice`. Configure mappings before +connecting the simulated wire: + +```go +target := dapsim.New(0x2ba01477) +sel := dap.NewAPSel(0) +if err := target.AddMEMAP(sel, 0x00010001, nil); err != nil { + return err +} +if err := target.MapMEMAPDevice(sel, 0xe000ed00, 0x100, device); err != nil { + return err +} +conn := swd.New(swdsim.New(target)) +dp := dap.NewDebugPort(dap.SWDP(conn)) +``` + +Here `device` implements `MemoryDevice`, and the simulator packages are imported +as `dapsim` and `swdsim`. Connect `dp` and acquire MEM-APs through the ordinary +public APIs. Release each MEM-AP before releasing the DP, retaining owners if +cleanup fails. Mapping itself performs no traffic and owns no cleanup. + +Each device call receives the caller's context, the absolute target byte +address, and a fresh byte slice of length 1, 2, 4 or 8. The bytes follow +increasing target addresses in both MEM-AP byte orders. A read fills the slice; +a write consumes it without retaining it. The MEM-AP owns numeric byte order, +DRW lanes, posted reads, 64-bit transfer phases and TAR incrementing. Narrow +writes do not synthesize a device read/modify/write. A device can reject widths +it does not support. + +An access crossing a mapping boundary faults before any device or ordinary +memory effect. Empty, overflowing, overlapping and nil-device mappings are +rejected, and mappings cannot change once target traffic starts. `SetMEMAPBytes` +and `MEMAPBytes` reject any nonempty range overlapping a device; they neither +invoke callbacks nor silently access the hidden fixture bytes. Serialize target +traffic, fixture changes, and all views of shared devices. + +A device returns `dapsim.ErrBusFault` to model a completed bus error. The +accepted AP data phase sets DP STICKYERR, and a later request observes SWD +FAULT. A faulting DRW access does not increment TAR. The device decides whether +a rejected write had an effect; the simulator never replays it or undoes it. +Ordinary DAP recovery captures and clears sticky state, invalidating existing +MEM-AP clients. Release and reopen those clients before further access. Other +callback errors become `ErrDeviceFailure`, preserving their diagnostic text and +context cancellation/deadline identity while hiding protocol classifications. An +accepted access might already have taken effect. Request-phase WAIT injection +belongs to `swd/sim.Acknowledger`; a bus callback error cannot retroactively +change an acknowledgement already sent. Bus latency and pending completion are +not modeled. + +`dap/sim.RAM` supplies a shared byte store. Map the same value through several +APs; writes through one AP are then visible through the others. Its zero value +is usable. Use `SetBytes` to prepare fixtures and `Bytes` to read snapshots: + +```go +ram := new(dapsim.RAM) +if err := ram.SetBytes(0x20040000, []byte{0, 0, 0, 0}); err != nil { + return err +} +for _, sel := range []dap.APSel{dap.NewAPSel(0), dap.NewAPSel(1)} { + if err := target.MapMEMAPDevice(sel, 0x20040000, 0x100, ram); err != nil { + return err + } +} +snapshot, err := ram.Bytes(0x20040000, 4) +``` + +Both MEM-AP fixtures must already exist, and the mappings still precede traffic. +For ADIv6, use configured `dap.APAt` selectors instead of `dap.NewAPSel`. +`SetBytes` and `Bytes` operate directly on the shared store, copy their slices, +and perform no simulated bus traffic. Bounds for bus accesses come from each +mapping; fixture access to RAM itself can cover any non-overflowing byte range. +Use separate device values for core-private register windows at the same address +through different APs. Neither the shared store nor device mapping adds +processor execution or synchronization between cores. + ## Discovering ADIv6 access ports `DebugPort.DebugSpace` borrows the DP's debug address space. Its `ReadDebugBase` @@ -664,4 +742,4 @@ 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. [adi-v6-spec]: - https://documentation-service.arm.com/static/622222b2e6f58973271ebc21 + https://documentation-service.arm.com/static/62221ef4e6f58973271ebc1d