diff --git a/coresight/component.go b/coresight/component.go index 9d55ade..f271c46 100644 --- a/coresight/component.go +++ b/coresight/component.go @@ -1,4 +1,4 @@ -// Package coresight identifies debug components through target memory. +// Package coresight identifies debug components and inspects ROM tables through target memory. // It borrows the reader and neither writes target memory nor owns cleanup. package coresight @@ -11,7 +11,7 @@ import ( ) // ScalarReader reads aligned target scalars as numeric values, independently -// of target byte order. Identify uses only dap.Size32. A dap.MemAP implements +// of target byte order. Inspection uses only dap.Size32. A dap.MemAP implements // this interface; its owner remains responsible for serialization and release. type ScalarReader interface { ReadScalar(context.Context, uint64, dap.TransferSize) (uint64, error) diff --git a/coresight/memap_test.go b/coresight/memap_test.go index d7d67c5..1b39779 100644 --- a/coresight/memap_test.go +++ b/coresight/memap_test.go @@ -53,6 +53,7 @@ func TestIdentifyThroughMEMAP(t *testing.T) { if got.CIDR != 0xb105900d || got.PIDR != 0x24523bb906 || got.DEVARCH != 0x47721a14 { t.Fatalf("identity=%+v", got) } + verifyWalkThroughMEMAP(t, mem, target, ap, order) }) } } @@ -65,3 +66,24 @@ func releaseSimulation(t *testing.T, release func(context.Context) error) { t.Error(err) } } + +func verifyWalkThroughMEMAP(t *testing.T, mem *dap.MemAP, target *sim.Target, ap dap.APSel, order binary.ByteOrder) { + t.Helper() + const high = uint64(1) << 32 + for address, word := range walkMemory().words { + var bytes [4]byte + order.PutUint32(bytes[:], word) + if err := target.SetMEMAPBytes(ap, high+address, bytes[:]); err != nil { + t.Fatal(err) + } + } + visits, err := coresight.Walk(t.Context(), mem, high+0x10000, walkLimits()) + if err != nil || len(visits) != 4 { + t.Fatalf("visits=%+v err=%v", visits, err) + } + for i, base := range []uint64{0x10000, 0x20000, 0x30000, 0x40000} { + if visits[i].Component == nil || visits[i].Component.Base != high+base { + t.Fatalf("visit %d=%+v", i, visits[i]) + } + } +} diff --git a/coresight/rom.go b/coresight/rom.go new file mode 100644 index 0000000..1523336 --- /dev/null +++ b/coresight/rom.go @@ -0,0 +1,134 @@ +package coresight + +import ( + "context" + "errors" + "fmt" +) + +// ErrNotROMTable means the component does not advertise a supported ROM architecture. +var ErrNotROMTable = errors.New("coresight: not a ROM table") + +// ROMTable describes a ROM table's entry layout, derived from a component identity. +// It owns no reader or resources. Its zero value is invalid. +type ROMTable struct { + base uint64 + count int + stride uint64 + class uint8 +} + +// ROMTable interprets a valid identification snapshot without memory traffic. +// It recognizes class 1 and Arm class 9 ROM architecture 0x0af7, revision 0. +// Unknown class 9 entry formats or revisions return errors, not ErrNotROMTable. +func (c Component) ROMTable() (ROMTable, error) { + if c.Base&0xfff != 0 || c.CIDR&0xffff0fff != 0xb105000d { + return ROMTable{}, errors.New("coresight: invalid component snapshot") + } + table := ROMTable{base: c.Base, count: 960, stride: 4, class: c.Class()} + if c.Class() == 1 { + return table, nil + } + arch, present := c.Architecture() + if !present || arch.Architect != 0x23b || arch.ID != 0x0af7 { + return ROMTable{}, ErrNotROMTable + } + if arch.Revision != 0 || c.DEVID&0xf > 1 { + return ROMTable{}, fmt.Errorf("coresight: unsupported ROM revision %d or format %#x", arch.Revision, c.DEVID&0xf) + } + table.count = 512 + if c.DEVID&0xf == 1 { + table.count = 256 + table.stride = 8 + } + return table, nil +} + +// EntryCount returns the architectural entry capacity, not a discovered length. +// A zero return means the table is invalid. +func (t ROMTable) EntryCount() int { return t.count } + +// ROMEntry is a detached entry snapshot. Base and power metadata are meaningful +// only when Present is true. End marks a terminator, not an absent interior entry. +// PowerID is scoped to the containing table and is valid only with PowerIDValid. +// A power ID does not establish that the child is powered or safe to access. +type ROMEntry struct { + Raw uint64 + Base uint64 + Present bool + End bool + PowerID uint8 + PowerIDValid bool +} + +// ReadEntry reads one architectural entry using numeric 32-bit scalars. It +// validates the index and arguments before traffic, reads both words of a +// 64-bit entry before decoding, and returns a zero entry on failure. Reserved +// encodings, zero offsets in present entries, and address underflow or overflow fail. +// It does not read the child, request power, or acquire cleanup obligations. +func (t ROMTable) ReadEntry(ctx context.Context, reader ScalarReader, index int) (ROMEntry, error) { + if ctx == nil || reader == nil { + return ROMEntry{}, errors.New("coresight: nil context or scalar reader") + } + if index < 0 || index >= t.count { + return ROMEntry{}, fmt.Errorf("coresight: invalid ROM entry index %d", index) + } + address := t.base + uint64(index)*t.stride + low, err := readWord(ctx, reader, address) + if err != nil { + return ROMEntry{}, err + } + raw := uint64(low) + if t.stride == 8 { + high, err := readWord(ctx, reader, address+4) + if err != nil { + return ROMEntry{}, err + } + raw |= uint64(high) << 32 + } + entry, err := t.decode(raw) + if err != nil { + return ROMEntry{}, fmt.Errorf("coresight: ROM entry at %#x: %w", address, err) + } + return entry, nil +} + +func (t ROMTable) decode(raw uint64) (ROMEntry, error) { + e := ROMEntry{Raw: raw, End: raw == 0} + if e.End { + return e, nil + } + if t.class == 9 && raw&3 == 2 { + return e, nil + } + if raw&2 == 0 || t.class == 9 && raw&3 != 3 { + return ROMEntry{}, errors.New("invalid entry format or presence") + } + if raw&0xe08 != 0 || raw&4 == 0 && raw&0x1f0 != 0 { + return ROMEntry{}, errors.New("reserved entry bits are nonzero") + } + e.Present = raw&1 != 0 + if !e.Present { + return e, nil + } + var err error + e.Base, err = t.entryBase(raw) + if err != nil { + return ROMEntry{}, err + } + e.PowerID = uint8(raw >> 4 & 0x1f) + e.PowerIDValid = raw&4 != 0 + return e, nil +} + +func (t ROMTable) entryBase(raw uint64) (uint64, error) { + offset := int64(raw &^ 0xfff) + if t.stride == 4 { + offset = int64(int32(raw &^ 0xfff)) + } + base := t.base + uint64(offset) + if offset == 0 || offset > 0 && base < t.base || offset < 0 && base > t.base { + return 0, errors.New("zero offset or address outside uint64 range") + } + return base, nil +} diff --git a/coresight/rom_test.go b/coresight/rom_test.go new file mode 100644 index 0000000..ee4c065 --- /dev/null +++ b/coresight/rom_test.go @@ -0,0 +1,204 @@ +package coresight_test + +import ( + "context" + "errors" + "math" + "testing" + + "github.com/jon/ostiole/coresight" +) + +func romComponent(class uint8, format uint32) coresight.Component { + return coresight.Component{Base: 0x100000000, CIDR: 0xb105000d | uint32(class)<<12, DEVARCH: 0x47700af7, DEVID: format} +} + +func TestROMTableLayout(t *testing.T) { + for _, tt := range []struct { + class uint8 + format uint32 + count int + }{{1, 0, 960}, {9, 0, 512}, {9, 1, 256}} { + c := romComponent(tt.class, tt.format) + table, err := c.ROMTable() + if err != nil || table.EntryCount() != tt.count { + t.Fatalf("layout=%+v,%v", table, err) + } + m := &componentMemory{words: map[uint64]uint32{}} + stride := uint64(4) + if tt.format == 1 { + stride = 8 + } + address := c.Base + uint64(tt.count-1)*stride + m.words[address] = 0x1003 + if stride == 8 { + m.words[address+4] = 0 + } + entry, err := table.ReadEntry(t.Context(), m, tt.count-1) + if err != nil || entry.Base != c.Base+0x1000 || !entry.Present { + t.Fatalf("last=%+v,%v", entry, err) + } + before := len(m.reads) + for _, i := range []int{-1, tt.count} { + if _, err = table.ReadEntry(t.Context(), m, i); err == nil { + t.Fatal("invalid index accepted") + } + } + if len(m.reads) != before { + t.Fatal("invalid index reached memory") + } + } +} + +func TestROMTableRejectsInvalidIdentity(t *testing.T) { + for _, edit := range []func(*coresight.Component){ + func(c *coresight.Component) { c.CIDR = 0 }, func(c *coresight.Component) { c.Base++ }, + func(c *coresight.Component) { c.DEVID = 2 }, + func(c *coresight.Component) { c.DEVARCH |= 1 << 16 }, func(c *coresight.Component) { c.DEVARCH &^= 1 << 20 }, + func(c *coresight.Component) { c.DEVARCH ^= 1 << 21 }, func(c *coresight.Component) { c.DEVARCH++ }, + } { + c := romComponent(9, 0) + edit(&c) + if _, err := c.ROMTable(); err == nil { + t.Fatalf("accepted %+v", c) + } + } + c := romComponent(0xe, 0) + if _, err := c.ROMTable(); !errors.Is(err, coresight.ErrNotROMTable) { + t.Fatalf("non-table error=%v", err) + } + m := memoryAt(0, 1) + var table coresight.ROMTable + if _, err := table.ReadEntry(t.Context(), m, 0); err == nil || len(m.reads) != 0 { + t.Fatal("zero table read memory") + } +} + +func TestROMEntryFormats(t *testing.T) { + for _, tt := range []struct { + name string + class uint8 + format uint32 + raw uint64 + base uint64 + present, end, power, invalid bool + }{ + {name: "class1 end", class: 1, end: true}, + {name: "class1 absent", class: 1, raw: 0x1002}, + {name: "class1 positive", class: 1, raw: 0x1003, base: 0x100001000, present: true}, + {name: "class1 negative", class: 1, raw: 0xfffff003, base: 0xfffff000, present: true}, + {name: "power domain zero", class: 1, raw: 0x1007, base: 0x100001000, present: true, power: true}, + {name: "format zero", class: 1, raw: 0x1001, invalid: true}, + {name: "all ones is malformed", class: 1, raw: 0xffffffff, invalid: true}, + {name: "reserved", class: 1, raw: 0x1203, invalid: true}, + {name: "unqualified power", class: 1, raw: 0x1013, invalid: true}, + {name: "self", class: 1, raw: 3, invalid: true}, + {name: "class9 end", class: 9, end: true}, + {name: "class9 malformed end", class: 9, raw: 0x1000, invalid: true}, + {name: "class9 reserved presence", class: 9, raw: 0x1001, invalid: true}, + {name: "class9 absent unknown bits", class: 9, raw: 0xfffffffe}, + {name: "class9 present", class: 9, raw: 0x1003, base: 0x100001000, present: true}, + {name: "64-bit positive", class: 9, format: 1, raw: 0x200001003, base: 0x300001000, present: true}, + {name: "64-bit negative", class: 9, format: 1, raw: 0xfffffffffffff003, base: 0xfffff000, present: true}, + {name: "64-bit malformed end high", class: 9, format: 1, raw: 0x100000000, invalid: true}, + } { + t.Run(tt.name, func(t *testing.T) { + c := romComponent(tt.class, tt.format) + table, err := c.ROMTable() + if err != nil { + t.Fatal(err) + } + m := &componentMemory{words: map[uint64]uint32{c.Base: uint32(tt.raw), c.Base + 4: uint32(tt.raw >> 32)}} + got, err := table.ReadEntry(t.Context(), m, 0) + if (err != nil) != tt.invalid { + t.Fatalf("entry=%+v,%v", got, err) + } + if tt.invalid { + if got != (coresight.ROMEntry{}) { + t.Fatal("partial entry returned") + } + return + } + if got.Raw != tt.raw || got.Base != tt.base || got.Present != tt.present || got.End != tt.end || got.PowerIDValid != tt.power { + t.Fatalf("entry=%+v", got) + } + reads := 1 + if tt.format == 1 { + reads = 2 + } + if len(m.reads) != reads { + t.Fatalf("reads=%v", m.reads) + } + }) + } +} + +func TestROMEntryAddressBounds(t *testing.T) { + for _, tt := range []struct { + base, raw uint64 + valid bool + }{ + {0, 0xfffffffffffff003, false}, {math.MaxUint64 - 0xfff, 0x1003, false}, + {0x8000000000000000, 0x8000000000000003, true}, + {0, 0x7ffffffffffff003, true}, + } { + c := romComponent(9, 1) + c.Base = tt.base + table, err := c.ROMTable() + if err != nil { + t.Fatal(err) + } + m := &componentMemory{words: map[uint64]uint32{c.Base: uint32(tt.raw), c.Base + 4: uint32(tt.raw >> 32)}} + if _, err = table.ReadEntry(t.Context(), m, 0); (err == nil) != tt.valid { + t.Fatalf("base=%x raw=%x err=%v", tt.base, tt.raw, err) + } + } +} + +func TestROMEntryReadFailure(t *testing.T) { + c := romComponent(9, 1) + table, err := c.ROMTable() + if err != nil { + t.Fatal(err) + } + failure := errors.New("memory failure") + for _, failAt := range []int{1, 2} { + m := &componentMemory{words: map[uint64]uint32{c.Base: 0, c.Base + 4: 0}, failAt: failAt, failure: failure} + got, err := table.ReadEntry(t.Context(), m, 0) + if !errors.Is(err, failure) || got != (coresight.ROMEntry{}) || len(m.reads) != failAt { + t.Fatalf("entry=%+v,%v reads=%v", got, err, m.reads) + } + } + m := memoryAt(c.Base, 1) + var nilContext context.Context + if _, err = table.ReadEntry(nilContext, m, 0); err == nil { + t.Fatal("nil context accepted") + } + if _, err = table.ReadEntry(t.Context(), nil, 0); err == nil { + t.Fatal("nil reader accepted") + } +} + +func TestROMEntryCancellation(t *testing.T) { + c := romComponent(9, 1) + table, err := c.ROMTable() + if err != nil { + t.Fatal(err) + } + for _, before := range []bool{false, true} { + ctx, cancel := context.WithCancel(t.Context()) + m := &componentMemory{words: map[uint64]uint32{c.Base: 3}, cancel: cancel} + if before { + cancel() + } + entry, err := table.ReadEntry(ctx, m, 0) + cancel() + want := 1 + if before { + want = 0 + } + if !errors.Is(err, context.Canceled) || entry != (coresight.ROMEntry{}) || len(m.reads) != want { + t.Fatalf("entry=%+v,%v reads=%v", entry, err, m.reads) + } + } +} diff --git a/coresight/walk.go b/coresight/walk.go new file mode 100644 index 0000000..c224e13 --- /dev/null +++ b/coresight/walk.go @@ -0,0 +1,158 @@ +package coresight + +import ( + "context" + "errors" + "fmt" +) + +var ( + // ErrWalkLimit means an explicit traversal bound prevented further inspection. + ErrWalkLimit = errors.New("coresight: ROM walk limit reached") + // ErrRepeatedTable means a table was reached twice, through a cycle or duplicate reference. + ErrRepeatedTable = errors.New("coresight: repeated ROM table") + // ErrPowerDomain means a child was skipped because its entry names a power domain. + ErrPowerDomain = errors.New("coresight: power domain access not established") +) + +// WalkLimits bounds one entire walk. All limits are explicit; there are no defaults. +type WalkLimits struct { + MaxDepth int // Root depth is zero; zero permits only the root. + MaxComponents int // Maximum visits, including the root and skipped children. Must be positive. + MaxEntries int // Maximum entry reads across all tables, including absent entries and terminators. Must be positive. +} + +// Validate checks limits without traffic, for use before opening hardware. +func (l WalkLimits) Validate() error { + if l.MaxDepth < 0 || l.MaxComponents <= 0 || l.MaxEntries <= 0 { + return errors.New("coresight: require nonnegative depth and positive component and entry limits") + } + return nil +} + +// Visit records one component reached in depth-first entry order. Parent indexes +// the returned slice; Parent and Index are -1 for the root, whose Entry is zero. +// Otherwise Index identifies the entry in the parent table. Component is nil +// when identity was not obtained. Err records a skipped or failed component; +// table entry read errors and limits are reported by Walk's returned error. +type Visit struct { + Parent int + Index int + Entry ROMEntry + Component *Component + Err error +} + +// Walk identifies root and follows present ROM entries within limits. It returns +// the visits recorded so far and a non-nil error whenever inspection is incomplete. +// Power-domain children are recorded with ErrPowerDomain without being accessed; +// their accessible siblings are still inspected. Any other error stops all reads, +// preserving the underlying error. Repeated tables are rejected before rereading. +// A non-table root is a successful single visit. Unknown component architectures +// are leaves; recognized ROM tables with unsupported formats fail. +// +// The caller supplies a safe root address and retains reader ownership. Walk +// writes no target memory, requests no component power, and performs no unlocks. +// Walk uses an explicit stack; limits bound visits, entry reads, and hierarchy depth. +func Walk(ctx context.Context, reader ScalarReader, base uint64, limits WalkLimits) ([]Visit, error) { + if err := limits.Validate(); err != nil { + return nil, err + } + if ctx == nil || reader == nil || base&0xfff != 0 { + return nil, errors.New("coresight: invalid walk context, reader, or root address") + } + if err := ctx.Err(); err != nil { + return nil, err + } + w := walker{ctx: ctx, reader: reader, limits: limits, seen: map[uint64]bool{}} + err := w.inspect(base, Visit{Parent: -1, Index: -1}, 0) + for err == nil && len(w.stack) > 0 { + err = w.next() + } + return w.visits, errors.Join(append(w.skipped, err)...) +} + +type walkFrame struct { + table ROMTable + parent, next, depth int +} + +type walker struct { + ctx context.Context + reader ScalarReader + limits WalkLimits + entries int + visits []Visit + stack []walkFrame + seen map[uint64]bool + skipped []error +} + +func (w *walker) next() error { + if err := w.ctx.Err(); err != nil { + return err + } + top := len(w.stack) - 1 + frame := w.stack[top] + if frame.next == frame.table.EntryCount() { + w.stack = w.stack[:top] + return nil + } + if w.entries == w.limits.MaxEntries { + return fmt.Errorf("%w: entries at table %#x index %d", ErrWalkLimit, frame.table.base, frame.next) + } + w.entries++ + entry, err := frame.table.ReadEntry(w.ctx, w.reader, frame.next) + if err != nil { + return err + } + w.stack[top].next++ + if entry.End { + w.stack = w.stack[:top] + return nil + } + if !entry.Present { + return nil + } + return w.inspect(entry.Base, Visit{Parent: frame.parent, Index: frame.next, Entry: entry}, frame.depth+1) +} + +func (w *walker) inspect(base uint64, visit Visit, depth int) error { + if depth > w.limits.MaxDepth || len(w.visits) == w.limits.MaxComponents { + return fmt.Errorf("%w: component %#x at depth %d", ErrWalkLimit, base, depth) + } + index := len(w.visits) + w.visits = append(w.visits, visit) + if visit.Entry.PowerIDValid { + err := fmt.Errorf("component %#x power ID %d: %w", base, visit.Entry.PowerID, ErrPowerDomain) + w.visits[index].Err = err + w.skipped = append(w.skipped, err) + return nil + } + err := w.identify(base, index, depth) + if err != nil { + w.visits[index].Err = err + } + return err +} + +func (w *walker) identify(base uint64, index, depth int) error { + if w.seen[base] { + return fmt.Errorf("%w at %#x", ErrRepeatedTable, base) + } + component, err := Identify(w.ctx, w.reader, base) + if err != nil { + return err + } + w.visits[index].Component = &component + table, err := component.ROMTable() + if errors.Is(err, ErrNotROMTable) { + return nil + } + if err != nil { + return fmt.Errorf("coresight: table %#x: %w", base, err) + } + w.seen[base] = true + w.stack = append(w.stack, walkFrame{table: table, parent: index, depth: depth}) + return nil +} diff --git a/coresight/walk_integration_test.go b/coresight/walk_integration_test.go new file mode 100644 index 0000000..ddb4161 --- /dev/null +++ b/coresight/walk_integration_test.go @@ -0,0 +1,97 @@ +//go:build integration + +package coresight_test + +import ( + "context" + "errors" + "os" + "testing" + "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/jtag" + "github.com/jon/ostiole/probe" +) + +type romBench struct { + name string + selection discover.Selection + port armdebug.PortConfig + ap uint8 + count int + fault uint64 +} + +func TestHILROMWalk(t *testing.T) { + if os.Getenv("OSTIOLE_ROM_HIL") != "1" { + t.Skip("set OSTIOLE_ROM_HIL=1 for the micro:bit and externally enabled ZCU104 benches") + } + arm, _ := jtag.IDCODE(4, 0x5ba00477) + xilinx, _ := jtag.IDCODE(12, 0x14730093) + for _, bench := range []romBench{ + {"microbit", discover.Selection{Provider: "cmsisdap", Serial: "9900360140124e4500279015000000360000000097969901"}, armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000}), 0, 6, 0}, + {"zcu104", discover.Selection{Provider: "ftdi", Serial: "01691", Function: "A"}, armdebug.JTAGDP(probe.JTAGConfig{MaxClockHz: 100_000}, jtag.Layout{arm, xilinx}, 0), 1, 18, 0x803e0000}, + } { + t.Run(bench.name, func(t *testing.T) { + for session := range 2 { + if !t.Run("session", func(t *testing.T) { observeROMWalk(t, bench, session) }) { + return + } + } + }) + } +} + +func observeROMWalk(t *testing.T, bench romBench, session int) { + t.Helper() + ctx, cancel := context.WithTimeout(t.Context(), 120*time.Second) + defer cancel() + owner := openIdentityBench(t, ctx, bench.selection, bench.port) + memory, err := owner.OpenMemAP(ctx, dap.NewAPSel(bench.ap)) + if err != nil { + t.Fatal(err) + } + base, present, err := memory.ReadDebugBase(ctx) + if err != nil || !present { + t.Fatalf("BASE=%#x,%v,%v", base, present, err) + } + limits := coresight.WalkLimits{MaxDepth: 8, MaxComponents: 256, MaxEntries: 4096} + visits, err := coresight.Walk(ctx, memory, base, limits) + for i, v := range visits { + if v.Component != nil { + t.Logf("visit=%d parent=%d entry=%d base=%#x class=%#x part=%#x", i, v.Parent, v.Index, v.Component.Base, v.Component.Class(), v.Component.Part()) + } + if v.Err != nil { + t.Logf("visit=%d parent=%d entry=%d base=%#x: %v", i, v.Parent, v.Index, v.Entry.Base, v.Err) + } + } + t.Logf("session=%d AP%d 100 kHz root=%#x visits=%d complete=%t error=%v", session, bench.ap, base, len(visits), err == nil, err) + checkROMWalkObservation(t, bench, visits, err) +} + +func checkROMWalkObservation(t *testing.T, bench romBench, visits []coresight.Visit, err error) { + t.Helper() + if len(visits) != bench.count { + t.Fatalf("visits=%d, want %d", len(visits), bench.count) + } + if bench.fault == 0 { + if err != nil { + t.Fatal(err) + } + } else { + last := visits[len(visits)-1] + if !errors.Is(err, dap.ErrFault) || !errors.Is(last.Err, dap.ErrFault) || last.Entry.Base != bench.fault || last.Component != nil { + t.Fatalf("expected inaccessible component %#x; last=%+v err=%v", bench.fault, last, err) + } + visits = visits[:len(visits)-1] + } + for i, v := range visits { + if v.Component == nil || v.Err != nil { + t.Fatalf("identity %d=%+v", i, v) + } + } +} diff --git a/coresight/walk_test.go b/coresight/walk_test.go new file mode 100644 index 0000000..7b8e120 --- /dev/null +++ b/coresight/walk_test.go @@ -0,0 +1,274 @@ +package coresight_test + +import ( + "context" + "errors" + "slices" + "testing" + + "github.com/jon/ostiole/coresight" + "github.com/jon/ostiole/dap" +) + +func walkMemory() *componentMemory { + m := memoryAt(0x10000, 1) + for _, c := range []struct { + base uint64 + class uint8 + }{{0x20000, 9}, {0x30000, 0xe}, {0x40000, 0xe}} { + for a, v := range memoryAt(c.base, c.class).words { + m.words[a] = v + } + } + m.words[0x20000+0xfbc] = 0x47700af7 + m.words[0x20000+0xfc8] = 1 + m.words[0x10000] = 0x10003 + m.words[0x10004] = 0x30003 + m.words[0x10008] = 0 + m.words[0x20000] = 0x10003 + m.words[0x20004] = 0 + m.words[0x20008] = 0 + m.words[0x2000c] = 0 + return m +} + +func walkLimits() coresight.WalkLimits { + return coresight.WalkLimits{MaxDepth: 4, MaxComponents: 16, MaxEntries: 32} +} + +func TestWalkROMDepthFirst(t *testing.T) { + m := walkMemory() + visits, err := coresight.Walk(t.Context(), m, 0x10000, walkLimits()) + if err != nil { + t.Fatal(err) + } + var bases []uint64 + var parents []int + for _, v := range visits { + if v.Component == nil || v.Err != nil { + t.Fatalf("visit=%+v", v) + } + bases = append(bases, v.Component.Base) + parents = append(parents, v.Parent) + } + if !slices.Equal(bases, []uint64{0x10000, 0x20000, 0x30000, 0x40000}) || !slices.Equal(parents, []int{-1, 0, 1, 0}) { + t.Fatalf("bases=%x parents=%v", bases, parents) + } + if visits[0].Index != -1 || visits[3].Index != 1 || visits[2].Entry.Raw != 0x10003 { + t.Fatalf("visits=%+v", visits) + } +} + +func TestWalkSkipsPowerDomains(t *testing.T) { + m := walkMemory() + m.words[0x10000] = 0x101f7 + visits, err := coresight.Walk(t.Context(), m, 0x10000, walkLimits()) + if !errors.Is(err, coresight.ErrPowerDomain) || len(visits) != 3 { + t.Fatalf("visits=%+v err=%v", visits, err) + } + skipped := visits[1] + if skipped.Component != nil || !errors.Is(skipped.Err, coresight.ErrPowerDomain) || skipped.Entry.PowerID != 31 || !skipped.Entry.PowerIDValid { + t.Fatalf("skipped=%+v", skipped) + } + for _, a := range m.reads { + if a >= 0x20000 && a < 0x21000 { + t.Fatalf("accessed another power domain at %x", a) + } + } + if visits[2].Component.Base != 0x40000 { + t.Fatal("did not inspect accessible sibling") + } +} + +func TestWalkStopsAtRepeatedTables(t *testing.T) { + for _, cycle := range []bool{false, true} { + m := walkMemory() + if cycle { + m.words[0x20000] = 0xffff0003 + m.words[0x20004] = 0xffffffff + } else { + m.words[0x10004] = 0x10003 + } + visits, err := coresight.Walk(t.Context(), m, 0x10000, walkLimits()) + if !errors.Is(err, coresight.ErrRepeatedTable) { + t.Fatalf("visits=%+v err=%v", visits, err) + } + last := visits[len(visits)-1] + if last.Component != nil || !errors.Is(last.Err, coresight.ErrRepeatedTable) { + t.Fatalf("last=%+v", last) + } + repeated := uint64(0x20000) + if cycle { + repeated = 0x10000 + } + count := 0 + for _, a := range m.reads { + if a == repeated+0xff0 { + count++ + } + } + if count != 1 { + t.Fatalf("identified repeated table %d times", count) + } + } +} + +func TestWalkLimits(t *testing.T) { + for _, tt := range []struct { + name string + limits coresight.WalkLimits + count int + unread uint64 + }{ + {"depth", coresight.WalkLimits{MaxDepth: 0, MaxComponents: 16, MaxEntries: 32}, 1, 0x20ff0}, + {"components", coresight.WalkLimits{MaxDepth: 4, MaxComponents: 2, MaxEntries: 32}, 2, 0x30ff0}, + {"entries", coresight.WalkLimits{MaxDepth: 4, MaxComponents: 16, MaxEntries: 1}, 2, 0x20000}, + } { + t.Run(tt.name, func(t *testing.T) { + m := walkMemory() + visits, err := coresight.Walk(t.Context(), m, 0x10000, tt.limits) + if !errors.Is(err, coresight.ErrWalkLimit) || len(visits) != tt.count { + t.Fatalf("visits=%+v err=%v", visits, err) + } + if slices.Contains(m.reads, tt.unread) { + t.Fatalf("read beyond limit at %x", tt.unread) + } + }) + } +} + +func TestWalkValidationBeforeTraffic(t *testing.T) { + m := walkMemory() + for _, limits := range []coresight.WalkLimits{{}, {MaxDepth: -1, MaxComponents: 1, MaxEntries: 1}, {MaxComponents: -1, MaxEntries: 1}, {MaxComponents: 1, MaxEntries: -1}} { + if err := limits.Validate(); err == nil { + t.Fatal("invalid limits accepted") + } + if _, err := coresight.Walk(t.Context(), m, 0x10000, limits); err == nil { + t.Fatal("invalid walk accepted") + } + } + var nilContext context.Context + for _, ctx := range []context.Context{nilContext, t.Context()} { + if _, err := coresight.Walk(ctx, m, 1, walkLimits()); err == nil { + t.Fatal("invalid argument accepted") + } + } + if _, err := coresight.Walk(t.Context(), nil, 0x10000, walkLimits()); err == nil { + t.Fatal("nil reader accepted") + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if _, err := coresight.Walk(ctx, m, 0x10000, walkLimits()); !errors.Is(err, context.Canceled) { + t.Fatalf("err=%v", err) + } + if len(m.reads) != 0 { + t.Fatalf("reads=%v", m.reads) + } +} + +func TestWalkFailureRetainsPrefix(t *testing.T) { + failure := errors.New("target unavailable") + for _, failAt := range []int{1, 13, 14, 29, 31} { + m := walkMemory() + m.failAt = failAt + m.failure = failure + visits, err := coresight.Walk(t.Context(), m, 0x10000, walkLimits()) + if !errors.Is(err, failure) || len(m.reads) != failAt || len(visits) == 0 { + t.Fatalf("at %d: visits=%+v err=%v reads=%d", failAt, visits, err, len(m.reads)) + } + } + m := walkMemory() + m.words[0x10004] = 0x1001 + visits, err := coresight.Walk(t.Context(), m, 0x10000, walkLimits()) + if err == nil || len(visits) != 3 { + t.Fatalf("malformed: visits=%+v err=%v", visits, err) + } +} + +func TestWalkLeafAndUnsupportedTable(t *testing.T) { + m := walkMemory() + visits, err := coresight.Walk(t.Context(), m, 0x30000, walkLimits()) + if err != nil || len(visits) != 1 { + t.Fatalf("leaf=%+v,%v", visits, err) + } + m.words[0x20000+0xfc8] = 2 + visits, err = coresight.Walk(t.Context(), m, 0x20000, walkLimits()) + if err == nil || len(visits) != 1 || visits[0].Component == nil || visits[0].Err == nil { + t.Fatalf("unsupported=%+v,%v", visits, err) + } +} + +func TestWalkCountsAbsentEntriesAndTerminators(t *testing.T) { + m := walkMemory() + m.words[0x10000] = 0x1002 + m.words[0x10004] = 0 + limits := walkLimits() + limits.MaxEntries = 1 + visits, err := coresight.Walk(t.Context(), m, 0x10000, limits) + if !errors.Is(err, coresight.ErrWalkLimit) || len(visits) != 1 || slices.Contains(m.reads, 0x10004) { + t.Fatalf("visits=%+v err=%v", visits, err) + } + limits.MaxEntries = 2 + if _, err = coresight.Walk(t.Context(), m, 0x10000, limits); err != nil { + t.Fatal(err) + } +} + +func TestWalkFullTableNeedsNoTerminator(t *testing.T) { + m := memoryAt(0x10000, 1) + for i := range 960 { + m.words[0x10000+uint64(i)*4] = 0x1002 + } + limits := walkLimits() + limits.MaxEntries = 960 + visits, err := coresight.Walk(t.Context(), m, 0x10000, limits) + if err != nil || len(visits) != 1 || len(m.reads) != 12+960 { + t.Fatalf("visits=%+v err=%v reads=%d", visits, err, len(m.reads)) + } +} + +func TestWalkCancellationRetainsPowerSkips(t *testing.T) { + m := walkMemory() + m.words[0x10000] = 0x10007 + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + reader := &cancelWalkReader{componentMemory: m, cancel: cancel, address: 0x10004} + visits, err := coresight.Walk(ctx, reader, 0x10000, walkLimits()) + if !errors.Is(err, context.Canceled) || !errors.Is(err, coresight.ErrPowerDomain) || len(visits) != 2 { + t.Fatalf("visits=%+v err=%v", visits, err) + } + if slices.Contains(m.reads, 0x40ff0) { + t.Fatal("identified child after cancellation") + } +} + +type cancelWalkReader struct { + *componentMemory + cancel context.CancelFunc + address uint64 + after bool +} + +func (r *cancelWalkReader) ReadScalar(ctx context.Context, address uint64, size dap.TransferSize) (uint64, error) { + if address == r.address { + if r.after { + word, err := r.componentMemory.ReadScalar(ctx, address, size) + r.cancel() + return word, err + } + r.cancel() + return 0, ctx.Err() + } + return r.componentMemory.ReadScalar(ctx, address, size) +} + +func TestWalkCancellationBeforeNextEntry(t *testing.T) { + m := walkMemory() + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + reader := &cancelWalkReader{componentMemory: m, cancel: cancel, address: 0x10fdc, after: true} + visits, err := coresight.Walk(ctx, reader, 0x10000, walkLimits()) + if !errors.Is(err, context.Canceled) || len(visits) != 1 || visits[0].Component == nil || len(m.reads) != 12 { + t.Fatalf("visits=%+v err=%v reads=%v", visits, err, m.reads) + } +} diff --git a/docs/README.md b/docs/README.md index c7ae084..d002b6b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,8 +16,8 @@ today and how to assemble them without duplicating lower-level behavior. and direction-explicit sequence commands. - [Arm Debug Access Ports](ports/dap.md) describes the ADIv5 register window, posted AP access, power handshakes, and MEM-AP details worth testing. -- [CoreSight component identity](coresight.md) describes identification - registers, borrowed memory access, and the inspection example. +- [CoreSight component inspection](coresight.md) describes identification + registers, ROM entry decoding, bounded traversal, and the inspection example. - [Composition](composition.md) maps common tasks to the narrowest public package that implements them and gives coding agents a selection checklist. - [Capabilities](capabilities.md) distinguishes implemented behavior from diff --git a/docs/architecture.md b/docs/architecture.md index fdfb2b0..97062ed 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -50,7 +50,7 @@ debugger service. | `swd/sim` | Model SWD protocol entry, register transfers, fixed-frame packing, and transfer limits without hardware. | | `dap` | Bind SW-DP or baseline ADIv5 JTAG-DP, manage identity and power, execute ordered DP/AP transactions, and provide scalar or block MEM-AP access. | | `dap/sim` | Model the DP, AP, and byte-addressed target-memory state consumed by `dap`. | -| `coresight` | Identify one explicitly addressed debug component through borrowed scalar memory, without acquiring resources or writing target memory. | +| `coresight` | Identify debug components and walk ROM tables through borrowed scalar memory, with explicit bounds and no resource acquisition or target-memory writes. | | `target/cortexm` | Read and decode the architectural Cortex-M CPUID value. | | `examples/...` | Demonstrate public package compositions as executable programs. | | `cmd/ost` | Provide a small command hierarchy over the same public packages. | @@ -321,9 +321,11 @@ the awkward parts of posted and memory access. debug entry, including legacy encodings and the optional upper address word. It preserves the memory client's state on success and does not access target memory. `coresight` reads component identification through a scalar-memory -reader. It uses DAP transfer sizes but owns no DAP or MEM-AP state. See -[CoreSight component identity](coresight.md) for its register and failure -boundaries. +reader. It also derives ROM geometry from those identities, reads individual +entries, and walks hierarchies with explicit limits. Traversal skips +children with power-domain metadata and reports an incomplete result. It +uses DAP transfer sizes but owns no DAP or MEM-AP state. See [CoreSight +component identity](coresight.md) for its register and failure boundaries. `target/cortexm` depends only on a compatible word reader. It knows the CPUID address and encoding, but it does not know about USB, FTDI, or SWD. diff --git a/docs/capabilities.md b/docs/capabilities.md index 2c5bb30..5582c57 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -265,16 +265,28 @@ 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. -## CoreSight component identity +## CoreSight component inspection `coresight.Identify` reads CIDR and PIDR at an explicit 4 KiB aligned page, -then DEVARCH, DEVID, and DEVTYPE for class 9. It preserves unknown identifiers -and performs no target-memory writes. Deterministic tests cover invalid input, -malformed preambles, cancellation, every read failure, and 64-bit addresses; -MEM-AP simulation covers both byte orders. It does not walk ROM tables, unlock -components, or infer the cause of inaccessible memory. See -[CoreSight component identity](coresight.md) for the two-session micro:bit SWD -and ZCU104 JTAG hardware observations and their limits. +then DEVARCH, DEVID, and DEVTYPE for class 9. It preserves unknown +identifiers and performs no target-memory writes. Deterministic tests cover +invalid input, malformed preambles, cancellation, every read failure, and +64-bit addresses; MEM-AP simulation covers both byte orders. +`Component.ROMTable` recognizes class 1 and Arm class 9 ROM geometry; +`ReadEntry` decodes one entry, including its table-scoped power metadata, +without accessing the child. `Walk` follows tables with explicit depth, +component, and entry limits, preserves partial results, rejects repeated +tables, and skips power-domain children. It does not unlock components, or +infer the cause of inaccessible memory. See [CoreSight component +identity](coresight.md) for the two-session micro:bit SWD and ZCU104 JTAG +hardware observations and their limits. + +ROM traversal HIL completed six identities on the micro:bit. On ZCU104 it +returned seventeen identities and a failed visit at `0x803e0000`, then stopped. +Both observations repeated in fresh sessions with successful owner close. +See the [ROM traversal hardware evidence](coresight.md#rom-traversal-hardware-evidence) +for exact selections, bounds, and the incomplete ZCU104 result. Class 9 table +layouts and power-domain skips have hardware-independent test coverage. ## Cortex-M target operations @@ -300,7 +312,8 @@ Available examples: - `examples/simple/cortexm-info` reports DPIDR, AP IDR, and Cortex-M CPUID. - `examples/simple/coresight-info` reads the MEM-AP's advertised component identity, or an explicitly supplied page, through a managed SWD connection - and selected MEM-AP. + and selected MEM-AP. Its `-walk` option follows ROM entries with fixed + depth, component, and entry bounds. - `examples/simple/arm-info` reports the same identities through generic probe discovery and one Arm debug owner, with explicit AP selection. @@ -321,7 +334,7 @@ the volatile DAP and MEM-AP state described above. ## Not currently provided There is no CMSIS-DAP HID/v1 transport, automatic probe -discovery policy, ROM-table traversal, +discovery policy, multi-core or SoC attachment, general target control, semihosting, trace, debugger protocol server, firmware flashing, FPGA programming, or Windows host implementation. diff --git a/docs/composition.md b/docs/composition.md index 31ace0c..bba1912 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -34,6 +34,7 @@ data-register write can write target memory. | 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` | | Identify one debug component through scalar memory | `coresight.Identify` | `examples/simple/coresight-info` | +| Inspect ROM entries or a bounded component hierarchy | `Component.ROMTable`, `ROMTable.ReadEntry`, `coresight.Walk` | `examples/simple/coresight-info -walk` | | Identify a Cortex-M through any compatible word reader | `cortexm.Identify` | `examples/simple/cortexm-info` | | Test SWD and DAP behavior without hardware | `swd/sim`, `dap/sim` | Package tests | @@ -787,7 +788,11 @@ This borrows the same memory client and adds no cleanup owner. An advertised address still requires component power and access permissions. A caller that already knows another accessible identification page can pass that address directly to `coresight.Identify`. -The [component guide](coresight.md) describes the returned identity and errors. +Use `coresight.Walk` with explicit `WalkLimits` to follow ROM entries. Retain +partial visits when it returns an error, and leave power-domain children +skipped until access has been established separately. The same memory owner +retains cleanup responsibility. The [component guide](coresight.md) describes +entry decoding, traversal bounds, power metadata, and incomplete results. ## Keep policy at the application edge diff --git a/docs/coresight.md b/docs/coresight.md index 0f089ea..62142e5 100644 --- a/docs/coresight.md +++ b/docs/coresight.md @@ -1,10 +1,10 @@ -# CoreSight component identity +# CoreSight component inspection `coresight.Identify` reads one component's identification registers through a borrowed scalar-memory reader. A `dap.MemAP` implements that interface over SWD or JTAG. Obtain the advertised identification page from the selected MEM-AP with `ReadDebugBase`, or supply an explicitly known address. The package -does not walk ROM tables. +can also read individual ROM entries or walk a hierarchy with explicit limits. ```go base, present, err := memory.ReadDebugBase(ctx) @@ -70,6 +70,109 @@ fields, cancellation, failures at every register read, and the final aligned page of the 64-bit address space. Composition tests exercise the existing SWD/DAP simulator with both MEM-AP byte orders above 4 GiB. +## Reading ROM entries + +`Component.ROMTable` derives entry geometry from an identification snapshot +without accessing memory. It recognizes class 1 and Arm's class 9 ROM +architecture `0x0af7`, revision 0. Other component architectures return +`ErrNotROMTable`; an unsupported ROM revision or entry format returns an error. +A zero table is invalid. + +```go +table, err := component.ROMTable() +if err != nil { + return err +} +for i := 0; i < table.EntryCount(); i++ { + entry, err := table.ReadEntry(ctx, memory, i) + if err != nil { + return err + } + if entry.End { + break + } + if entry.Present { + fmt.Printf("entry=%d base=%#x power-ID=%d valid=%t\n", + i, entry.Base, entry.PowerID, entry.PowerIDValid) + } +} +``` + +Class 1 tables hold at most 960 32-bit entries. Class 9 DEVID.FORMAT selects +512 32-bit or 256 64-bit entries. A table that fills every slot needs no +additional terminator. `ReadEntry` reads both words of a 64-bit entry before +interpreting it and returns no partial entry on error. It applies signed +relative offsets without allowing address underflow or overflow. + +The decoder follows IHI 0029E D6.4.4 and D7.5.17. It rejects reserved +formats, nonzero reserved bits, zero offsets in present entries, and nonzero +class 9 terminators. Class 9 absence (`PRESENT=2`) leaves the remaining bits +uninterpreted. Class 1 FORMAT=0 entries are unsupported; all-ones entries +are malformed. `Raw` retains the complete entry value on success. Unknown +class 9 architectures are not interpreted as tables based on their part +number alone. + +Entry reads do not access the child. A valid power ID is scoped to the +containing table and does not establish that the child is powered. This API +does not request power. Callers must establish access before identifying a +child in another power domain. Reader ownership and cleanup remain as above. + +## Bounded traversal + +`Walk` identifies a root and follows present entries in depth-first order. A +root that is not a ROM table produces one successful visit. Limits apply to +the entire walk, with root depth zero. The component limit counts the root, +failed identities, and skipped power-domain children. The entry limit counts +absent entries and terminators as well as present entries. Validate limits +before opening hardware when they come from application arguments. + +```go +limits := coresight.WalkLimits{MaxDepth: 8, MaxComponents: 256, MaxEntries: 4096} +if err := limits.Validate(); err != nil { + return err +} +visits, err := coresight.Walk(ctx, memory, base, limits) +for _, visit := range visits { + if visit.Component != nil { + fmt.Printf("parent=%d entry=%d base=%#x class=%#x\n", + visit.Parent, visit.Index, visit.Component.Base, visit.Component.Class()) + } + if visit.Err != nil { + fmt.Printf("parent=%d entry=%d: %v\n", visit.Parent, visit.Index, visit.Err) + } +} +if err != nil { + return err +} +``` + +Each visit refers to its parent by index in the returned slice. The root has +`Parent=-1` and `Index=-1`. Other visits retain the decoded entry, including +power metadata scoped to the parent table. `Component` is nil if the identity +was not obtained. Absent entries and terminators have no visits; use individual +entry reads when their raw values matter. + +A power-domain child is recorded with `ErrPowerDomain` and skipped before any +child access. The walk continues through its accessible siblings but returns +a non-nil error, so those results cannot be mistaken for a complete inventory. +It does not test a power-control register, request power, or offer an option +to assume an advertised domain is accessible. + +Other failures stop the walk immediately, including malformed entries, +unsupported ROM formats, repeated tables, exhausted limits, and memory errors. +Repeated table references include cycles and duplicate references from separate +parents; they fail before another identity read. Ordinary component references +may repeat. Unknown component architectures remain leaves. A successful walk +covers the supported tables reached from this root, not every debug component +in the system. + +The returned error preserves underlying memory errors and matches +`ErrWalkLimit`, `ErrRepeatedTable`, or `ErrPowerDomain` when applicable. Earlier +visits remain available; an identity failure is recorded on its visit. An entry +read failure or exhausted limit is reported in the returned error, without a +child visit. Stop using a failed MEM-AP according to its recovery rules, +then release its owner with bounded, retryable cleanup. + ## Inspection example `examples/simple/coresight-info` opens a managed SWD connection, acquires the @@ -89,6 +192,20 @@ identification page. The example requests a 100 kHz clock and applies a ten-second operation deadline. The library also accepts memory clients reached through JTAG; the example configures SWD only. +Add `-walk` to follow the advertised root with depth 8, at most 256 visits, +and at most 4096 entry reads across the hierarchy: + +```sh +go run ./examples/simple/coresight-info \ + -provider cmsisdap -serial SERIAL -ap 0 -walk +``` + +`-base ADDRESS` also applies to walks. The output includes parent and entry +indexes, available identities, per-component errors, and a `complete` field. +Incomplete inspection exits unsuccessfully after printing its partial results +and attempting owner cleanup. These fixed bounds keep the example small; +library callers supply their own `WalkLimits`. + ## Hardware evidence On September 12, 2026, the macOS Nostalgia bench ran: @@ -125,3 +242,39 @@ The example passed on that micro:bit with its exact serial, both with the advertised address and with `-base 0xe00ff000`. These results cover the advertised entry and one known page on each bench, not ROM traversal, component register access, or physical large-address and big-endian support. + +## ROM traversal hardware evidence + +On September 12, 2026, Nostalgia ran: + +```sh +OSTIOLE_ROM_HIL=1 \ + go test -tags=integration -run '^TestHILROMWalk$' -count=1 -v ./coresight +``` + +The test uses the same exact probe selections and externally enabled ZCU104 +chain described above. Each path opens two fresh sessions at 100 kHz, reads +its MEM-AP's advertised root, and walks with depth 8, 256 visits, 4096 entry +reads, and a 120-second operation deadline. + +The micro:bit SWD AP0 walk completed with six identities. Its root at +`0xf0000000` led to the nested table at `0xe00ff000`, components at +`0xe000e000`, `0xe0001000`, and `0xe0002000`, and a component at `0xf0002000`. +The `coresight-info -walk` example also reported six visits and `complete=true` +using that probe's exact serial and its ten-second deadline. + +The ZCU104 JTAG AP1 walk was incomplete. From root `0x80000000`, it identified +sixteen children at `0x80100000` through `0x801f0000`, then stopped on a DAP +FAULT while reading CIDR at `0x803e0ff0`. The result retained those seventeen +identities and a failed eighteenth visit for root entry 16. The test checks +this access boundary; it does not count the inaccessible component as +identified or attempt later entries. The error does not distinguish a power +restriction from another cause of that target access fault. + +Both sessions reproduced each bench's result. Every owner reported successful +close, including after the ZCU104 fault. This does not independently measure +restored state after close. No target-memory writes, component power requests, +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. diff --git a/examples/README.md b/examples/README.md index 6eef7e5..25ffc00 100644 --- a/examples/README.md +++ b/examples/README.md @@ -27,3 +27,4 @@ an example has been implemented. reads DPIDR, AP IDR, and Cortex-M identity through one Arm debug owner. - [`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. diff --git a/examples/simple/coresight-info/main.go b/examples/simple/coresight-info/main.go index 8b0fdeb..1ccd457 100644 --- a/examples/simple/coresight-info/main.go +++ b/examples/simple/coresight-info/main.go @@ -30,6 +30,7 @@ func run() (err error) { function := flag.String("function", "", "exact probe function") ap := flag.Int("ap", -1, "required 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() base, err := parseBase(*address) if err != nil { @@ -63,6 +64,13 @@ func run() (err error) { return errors.New("selected MEM-AP advertises no debug entry") } } + return inspect(ctx, memory, base, *walk) +} + +func inspect(ctx context.Context, memory *dap.MemAP, base uint64, walk bool) error { + if walk { + return printWalk(ctx, memory, base) + } component, err := coresight.Identify(ctx, memory, base) if err != nil { return err @@ -100,3 +108,19 @@ func parseBase(value string) (uint64, error) { } return base, nil } + +func printWalk(ctx context.Context, memory *dap.MemAP, 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 { + fmt.Printf("visit=%d parent=%d entry=%d\n", i, visit.Parent, visit.Index) + if visit.Component != nil { + printComponent(*visit.Component) + } + if visit.Err != nil { + fmt.Printf("%v\n", visit.Err) + } + } + fmt.Printf("visits=%d complete=%t\n", len(visits), err == nil) + return err +}