From 89ecdf0fe37ec473581572e3f8ecee1ecec41797 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Fri, 2 Oct 2026 04:26:33 +0000 Subject: [PATCH 1/3] Acquire and release explicit Cortex-M groups. Callers that acquire several targets currently have to track which acquisitions succeeded and which targets still need restoration. Own those targets over borrowed memory, retain per-member results, and restore independent members after failure. Keep lower owners live while restoration remains pending. --- docs/architecture.md | 6 +- docs/capabilities.md | 7 +- docs/composition.md | 9 ++ docs/cortexm.md | 29 ++++++ target/cortexm/group.go | 168 +++++++++++++++++++++++++++++++ target/cortexm/group_test.go | 190 +++++++++++++++++++++++++++++++++++ 6 files changed, 404 insertions(+), 5 deletions(-) create mode 100644 target/cortexm/group.go create mode 100644 target/cortexm/group_test.go diff --git a/docs/architecture.md b/docs/architecture.md index 64dbbc8..f2483db 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -63,8 +63,10 @@ their framing. A target owns one processor. Several targets may borrow distinct MEM-APs from one `armdebug.Conn`; all calls over that shared owner remain serialized. -Independent RP2350 core control has [physical evidence][dual-core]. It does not -provide group ownership, CTI routing, or a simultaneous memory snapshot. +`cortexm.Group` acquires and owns an explicit set of those targets, retaining +member outcomes and cleanup after partial failure. Independent RP2350 core +control has [physical evidence][dual-core]. Neither path owns CTI routing or +supplies a simultaneous memory snapshot. ## Discovery and opening diff --git a/docs/capabilities.md b/docs/capabilities.md index ed3f9b5..ca1a142 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -302,6 +302,7 @@ skips have hardware-independent test coverage. | Register reads | Yes | Halted Cortex-M0/M33 R0–R12, SP, LR, PC, XPSR, MSP, and PSP through `ReadRegister`. Two fresh sessions each on CMSIS-DAP/micro:bit and J-Link/RP2350 core 0 read all 19 registers; transfer failures and cleanup have behavioral coverage. | | Register writes | Yes | Halted Cortex-M0/M33 writes except XPSR; aligned SP/MSP/PSP and even PC values. Writes persist after release. Behavioral tests cover staging, uncertain selection, and pending cleanup. Two sessions each on micro:bit and RP2350 core 0 wrote and restored R4, SP, MSP, PSP, and PC before resuming; see the [micro:bit](cortexm.md#register-bench) and [RP2350](cortexm.md#rp2350-register-bench) register benches. | | Independent RP2350 cores | HIL | Two fresh J-Link sessions borrowed core-0/core-1 MEM-APs under one Arm owner. Each core halted and stepped while its peer advanced; both restored disabled debug and running state with inactive CTIs unchanged. See the [independent-core bench](cortexm.md#rp2350-independent-core-bench). This provides serialized per-core control, without group or CTI ownership. | +| Cortex-M group acquisition and release | Yes | `AcquireGroup` owns an explicit member list over borrowed memory and retains per-core acquisition and release results. Failed restoration keeps cleanup retryable; lower owners must remain live. Behavioral tests cover partial setup, independent cleanup, and retry; see [group ownership](cortexm.md#group-ownership). | | Reset | No | No architectural or pin-reset operation exists. | | Breakpoints or watchpoints | No | No target instrumentation API exists. | | Firmware or runtime loading | No | No ELF loader, image-placement policy, or flash driver exists. | @@ -346,9 +347,9 @@ SWD, and use the volatile DAP and MEM-AP state described above. ## Not currently provided There is no CMSIS-DAP HID/v1 transport, automatic probe discovery policy, -automatic SoC attachment, group or CTI control, general target control, -semihosting, trace, debugger protocol server, firmware flashing, FPGA -programming, or Windows host implementation. +automatic SoC attachment, CTI control, general target control, semihosting, +trace, debugger protocol server, firmware flashing, FPGA programming, or Windows +host implementation. Treat an absent capability as an explicit boundary. Do not infer it from the project description or recreate its lower-level protocol inside an application. diff --git a/docs/composition.md b/docs/composition.md index b47a859..a30a36b 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -38,6 +38,7 @@ data-register write can write target memory. | Identify a Cortex-M through any compatible word reader | `cortexm.Identify` | `examples/simple/cortexm-info` | | Acquire, halt, inspect registers, and resume a Cortex-M0 | `cortexm.Acquire`, `Target.Halt`, `Target.ReadRegister`, `Target.Resume`, `Target.Release` | `examples/simple/cortexm-control` | | Step a Cortex-M0/M33 from an owned halt | `Target.Step` | `examples/simple/cortexm-control -step` | +| Acquire and release explicit Cortex-M targets together | `cortexm.AcquireGroup`, `Group.Results`, `Group.Release` | [Group ownership](cortexm.md#group-ownership) | | Read or write a halted Cortex-M0/M33 register | `Target.ReadRegister`, `Target.WriteRegister` | [Register reads](cortexm.md#register-reads), [writes](cortexm.md#register-writes) | | Test SWD and DAP behavior without hardware | `swd/sim`, `dap/sim` | Package tests | @@ -750,6 +751,14 @@ release both targets before closing their memory owner. The [two-core composition and bench](cortexm.md#independent-rp2350-cores) show the per-core ownership boundary and physical observations. +Use `cortexm.AcquireGroup` when one owner should acquire those targets and +retain per-member results after partial failure. Supply explicit IDs and +borrowed memory clients; access the targets through group methods. Serialize +group and other connection calls. Release the group before closing the Arm +owner, and retain both when target restoration remains pending. See +[group ownership and control](cortexm.md#group-ownership) for the composition +and failure rules. + ## Release in reverse order A complete Cortex-M identity composition acquires and releases state in one of diff --git a/docs/cortexm.md b/docs/cortexm.md index 491fbee..1c545d2 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -308,6 +308,35 @@ routing can couple stops: inspect the bench's routing before expecting independent progress. This composition provides per-core control, without group ownership, coordinated stopping, or a simultaneous snapshot. +## Group ownership + +`AcquireGroup` copies an explicit list of `Member` values and acquires one +target for each, in membership order. Each `CoreID` is nonzero and unique within +that group; it is not a CPUID or AP address. The caller supplies distinct +physical processors and keeps their memory clients alive. Different IDs cannot +prove that two clients address different processors. + +Acquisition enables halting debug without requesting a halt. It has the effects +and architecture restrictions of `Acquire`. Invalid static membership fails +before traffic. On failure, acquisition stops and attempts cleanup with a fresh +five-second context, in addition to any failed target acquisition's own cleanup. +A non-nil group returned with an error retains obligations and permits only +release and cached results. Memory is borrowed on every return. + +`Release` restores pending targets in reverse membership order, attempting +independent members after a failure while its context permits. Use a fresh +bounded context after cancellation. Successful target releases are never +repeated. Keep the memory and Arm owners live until every member's +`CleanupPending` is false. Starting release blocks ordinary group operations; +nil and inactive groups require no cleanup. + +`Results` returns copied member outcomes in membership order without traffic. +`Attempted` distinguishes members reached from those not reached, and `Err` +retains their causes. Acquisition failure also records cleanup errors. Identity +remains available after release. Release updates each member's outcome. The +group never lends its owned targets. Calls and all other access over the shared +connection must be serialized by the caller. + ## Hardware procedure The opt-in integration test selects the CMSIS-DAP micro:bit with serial diff --git a/target/cortexm/group.go b/target/cortexm/group.go new file mode 100644 index 0000000..606c1a5 --- /dev/null +++ b/target/cortexm/group.go @@ -0,0 +1,168 @@ +package cortexm + +import ( + "context" + "errors" + "fmt" +) + +// CoreID names a processor within one Group. Zero is invalid; an ID is not a +// CPUID or an access-port address. +type CoreID uint32 + +// Member selects one distinct physical processor through borrowed memory. +// IDs must be nonzero and unique. The caller supplies distinct processors and +// keeps exclusive control of their debug registers until group release. +type Member struct { + ID CoreID + Memory Memory +} + +// CoreResult is a detached member outcome. Attempted means the member operation +// was called. CleanupPending requires retaining memory and lower owners. +// Identity survives successful release. +type CoreResult struct { + ID CoreID + Identity Identity + Attempted bool + CleanupPending bool + Err error +} + +type groupCore struct { + result CoreResult + target *Target +} + +// Group owns its member targets, borrowing their memory. Do not copy it. +// Serialize calls and all access over the shared memory connection. The group +// does not close memory or its lower owners. Its zero value is inactive. +type Group struct { + cores []groupCore + closing bool +} + +// AcquireGroup validates and copies membership, then acquires targets in that +// order without requesting halts. Target acquisition effects and restrictions +// apply to every member. Failure stops acquisition and attempts release with a +// fresh five-second context. A non-nil group returned with an error retains +// cleanup; only Release is available. Memory remains borrowed on every return. +func AcquireGroup(ctx context.Context, members []Member) (*Group, error) { + if err := validateMembers(members); err != nil { + return nil, err + } + if err := liveContext(ctx); err != nil { + return nil, err + } + members = append([]Member(nil), members...) + g := &Group{cores: make([]groupCore, len(members))} + for i, member := range members { + g.cores[i].result.ID = member.ID + } + for i, member := range members { + core := &g.cores[i] + core.result.Attempted = true + var err error + core.target, err = Acquire(ctx, member.Memory) + core.result.Identity = core.target.Identity() + core.result.CleanupPending = core.target != nil + core.result.Err = err + if err != nil { + cleanup, cancel := context.WithTimeout(context.Background(), acquireCleanupTimeout) + _, releaseErr := g.release(cleanup, true) + cancel() + if g.pending() { + return g, errors.Join(coreError(member.ID, err), releaseErr) + } + return nil, errors.Join(coreError(member.ID, err), releaseErr) + } + } + return g, nil +} + +func validateMembers(members []Member) error { + if len(members) == 0 { + return errors.New("cortexm: empty group membership") + } + seen := make(map[CoreID]bool, len(members)) + for _, member := range members { + if member.ID == 0 || seen[member.ID] || member.Memory == nil { + return errors.New("cortexm: require unique nonzero core IDs and non-nil memory") + } + seen[member.ID] = true + } + return nil +} + +// Results copies member outcomes in membership order without traffic. +// Acquisition failure includes any cleanup errors. Nil groups return no results. +// Changing the returned slice does not change group state. +func (g *Group) Results() []CoreResult { + if g == nil { + return nil + } + results := make([]CoreResult, len(g.cores)) + for i, core := range g.cores { + results[i] = core.result + } + return results +} + +// Release restores each pending target in reverse membership order and attempts +// independent members even after a failure, while ctx permits. It retains +// failures for retry and never repeats successful release. Keep lower owners +// live until every CleanupPending is false. Use a fresh bounded context after +// cancellation. Nil and inactive groups require no cleanup. Once release begins, +// only Release and cached Results remain available. +func (g *Group) Release(ctx context.Context) ([]CoreResult, error) { + return g.release(ctx, false) +} + +func (g *Group) release(ctx context.Context, preserve bool) ([]CoreResult, error) { + if g == nil { + return nil, nil + } + g.closing = true + if !preserve { + for i := range g.cores { + g.cores[i].result.Attempted = false + g.cores[i].result.Err = nil + } + } + if !g.pending() { + return g.Results(), nil + } + var errs []error + for i := len(g.cores) - 1; i >= 0; i-- { + core := &g.cores[i] + if !core.result.CleanupPending { + continue + } + if err := liveContext(ctx); err != nil { + core.result.Err = errors.Join(core.result.Err, err) + errs = append(errs, err) + break + } + core.result.Attempted = true + err := core.target.Release(ctx) + core.result.Err = errors.Join(core.result.Err, err) + core.result.CleanupPending = err != nil + if err != nil { + errs = append(errs, coreError(core.result.ID, err)) + } + } + return g.Results(), errors.Join(errs...) +} + +func (g *Group) pending() bool { + for _, core := range g.cores { + if core.result.CleanupPending { + return true + } + } + return false +} + +func coreError(id CoreID, err error) error { + return fmt.Errorf("cortexm: core %d: %w", id, err) +} diff --git a/target/cortexm/group_test.go b/target/cortexm/group_test.go new file mode 100644 index 0000000..583796e --- /dev/null +++ b/target/cortexm/group_test.go @@ -0,0 +1,190 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func groupMembers(memories ...cortexm.Memory) []cortexm.Member { + members := make([]cortexm.Member, len(memories)) + for i, memory := range memories { + members[i] = cortexm.Member{ID: cortexm.CoreID(i + 1), Memory: memory} + } + return members +} + +func TestGroupAcquireAndRelease(t *testing.T) { + a, b := newControlMemory(), newControlMemory() + b.control, b.halted = debugEnable|haltRequest, true + members := groupMembers(a, b) + g, err := cortexm.AcquireGroup(t.Context(), members) + if err != nil || g == nil { + t.Fatalf("group=%v error=%v", g, err) + } + members[0].ID = 99 + results := g.Results() + if len(results) != 2 || results[0].ID != 1 || results[1].ID != 2 { + t.Fatalf("results=%+v", results) + } + for _, result := range results { + if !result.Attempted || !result.CleanupPending || result.Identity.Raw != a.cpuid || result.Err != nil { + t.Fatalf("result=%+v", result) + } + } + results[0].ID = 99 + if g.Results()[0].ID != 1 { + t.Fatal("results expose owned membership") + } + if _, err := g.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestGroupReleasePreservesInheritedState(t *testing.T) { + a, b := newControlMemory(), newControlMemory() + b.control, b.halted = debugEnable|haltRequest, true + g, err := cortexm.AcquireGroup(t.Context(), groupMembers(a, b)) + if err != nil { + t.Fatal(err) + } + if _, err := g.Release(t.Context()); err != nil || a.control != 0 || !b.halted || b.control != 3 { + t.Fatalf("release=%v control=%#x/%#x halted=%v", err, a.control, b.control, b.halted) + } + writes := a.writes + b.writes + if _, err := g.Release(t.Context()); err != nil || a.writes+b.writes != writes { + t.Fatalf("repeated release=%v", err) + } + for _, result := range g.Results() { + if result.CleanupPending { + t.Fatalf("released result=%+v", result) + } + } +} + +func TestGroupRejectsMembershipBeforeTraffic(t *testing.T) { + m := newControlMemory() + for _, members := range [][]cortexm.Member{ + nil, {{ID: 0, Memory: m}}, {{ID: 1}}, + {{ID: 1, Memory: m}, {ID: 1, Memory: m}}, + } { + if g, err := cortexm.AcquireGroup(t.Context(), members); g != nil || err == nil { + t.Fatalf("members=%v group=%v error=%v", members, g, err) + } + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if _, err := cortexm.AcquireGroup(ctx, groupMembers(m)); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + var nilContext context.Context + if _, err := cortexm.AcquireGroup(nilContext, groupMembers(m)); err == nil { + t.Fatal("accepted nil context") + } + if m.reads != 0 || m.writes != 0 { + t.Fatal("invalid acquisition reached memory") + } +} + +func TestGroupAcquireFailureRestoresEarlierMembers(t *testing.T) { + a, b := newControlMemory(), newControlMemory() + b.cpuid = 0 + if g, err := cortexm.AcquireGroup(t.Context(), groupMembers(a, b)); g != nil || err == nil || a.control != 0 { + t.Fatalf("group=%v error=%v control=%#x", g, err, a.control) + } +} + +func TestGroupCopiesCompleteMembershipBeforeTraffic(t *testing.T) { + a, b, replacement := newControlMemory(), newControlMemory(), newControlMemory() + members := groupMembers(a, b) + a.onWrite = func() { members[1] = cortexm.Member{ID: 9, Memory: replacement} } + g, err := cortexm.AcquireGroup(t.Context(), members) + if err != nil { + t.Fatal(err) + } + if b.control != debugEnable || replacement.reads != 0 || g.Results()[1].ID != 2 { + t.Fatalf("selected control=%#x replacement reads=%d results=%+v", b.control, replacement.reads, g.Results()) + } + if _, err := g.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestGroupAcquireFailureRetainsCleanup(t *testing.T) { + a, b, c := newControlMemory(), newControlMemory(), newControlMemory() + a.failWrite = 2 + b.failRead, b.failWrite = 3, 2 + g, err := cortexm.AcquireGroup(t.Context(), groupMembers(a, b, c)) + if g == nil || !errors.Is(err, errMemory) { + t.Fatalf("group=%v error=%v", g, err) + } + results := g.Results() + if !results[0].CleanupPending || results[1].CleanupPending || results[2].Attempted { + t.Fatalf("results=%+v", results) + } + if c.reads != 0 || c.writes != 0 { + t.Fatal("acquired a later member after failure") + } + if _, err := g.Release(t.Context()); err != nil || a.control != 0 || b.control != 0 { + t.Fatalf("release=%v control=%#x/%#x", err, a.control, b.control) + } +} + +func TestGroupReleaseAttemptsIndependentMembersAndRetries(t *testing.T) { + a, b := newControlMemory(), newControlMemory() + g, err := cortexm.AcquireGroup(t.Context(), groupMembers(a, b)) + if err != nil { + t.Fatal(err) + } + var order []int + a.onWrite = func() { order = append(order, 1) } + b.onWrite = func() { order = append(order, 2) } + b.failRead = b.reads + 1 + results, err := g.Release(t.Context()) + if !errors.Is(err, errMemory) || a.control != 0 || results[0].CleanupPending || !results[1].CleanupPending { + t.Fatalf("results=%+v error=%v", results, err) + } + aWrites := a.writes + if _, err := g.Release(t.Context()); err != nil || a.writes != aWrites || b.control != 0 { + t.Fatalf("retry=%v writes=%d/%d control=%#x", err, a.writes, aWrites, b.control) + } + if len(order) != 2 || order[0] != 1 || order[1] != 2 { + t.Fatalf("cleanup order=%v", order) + } +} + +func TestGroupAcquisitionCleanupOutlivesCancellation(t *testing.T) { + a, b := newControlMemory(), newControlMemory() + ctx, cancel := context.WithCancel(t.Context()) + a.onWrite = cancel + g, err := cortexm.AcquireGroup(ctx, groupMembers(a, b)) + if g != nil || !errors.Is(err, context.Canceled) || a.control != 0 || b.reads != 0 { + t.Fatalf("group=%v error=%v control=%#x peer reads=%d", g, err, a.control, b.reads) + } +} + +func TestGroupCanceledReleaseAndInactiveValues(t *testing.T) { + var nilGroup *cortexm.Group + var nilContext context.Context + for _, g := range []*cortexm.Group{nilGroup, {}} { + if results, err := g.Release(nilContext); err != nil || len(results) != 0 || len(g.Results()) != 0 { + t.Fatalf("inactive results=%v error=%v", results, err) + } + } + m := newControlMemory() + g, err := cortexm.AcquireGroup(t.Context(), groupMembers(m)) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + results, err := g.Release(ctx) + if !errors.Is(err, context.Canceled) || !results[0].CleanupPending || results[0].Attempted { + t.Fatalf("results=%+v error=%v", results, err) + } + if _, err := g.Release(t.Context()); err != nil || m.control != 0 { + t.Fatalf("retry=%v control=%#x", err, m.control) + } +} From 0098318a494fbff6614db5a2d1fe8df9d45aa088 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Fri, 2 Oct 2026 04:26:34 +0000 Subject: [PATCH 2/3] Control selected Cortex-M group members. Independent targets require callers to track partial stop and resume progress. Report selected outcomes in membership order, preserve unowned halts, and retain uncertain members for cleanup without replaying writes. Observe execution separately from a retained halt-request claim. --- docs/architecture.md | 7 +- docs/capabilities.md | 2 +- docs/composition.md | 54 +++---- docs/cortexm.md | 62 +++++++- target/cortexm/group.go | 19 ++- target/cortexm/group_control.go | 142 +++++++++++++++++ target/cortexm/group_control_test.go | 229 +++++++++++++++++++++++++++ 7 files changed, 476 insertions(+), 39 deletions(-) create mode 100644 target/cortexm/group_control.go create mode 100644 target/cortexm/group_control_test.go diff --git a/docs/architecture.md b/docs/architecture.md index f2483db..a237bd3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -64,9 +64,10 @@ their framing. A target owns one processor. Several targets may borrow distinct MEM-APs from one `armdebug.Conn`; all calls over that shared owner remain serialized. `cortexm.Group` acquires and owns an explicit set of those targets, retaining -member outcomes and cleanup after partial failure. Independent RP2350 core -control has [physical evidence][dual-core]. Neither path owns CTI routing or -supplies a simultaneous memory snapshot. +member outcomes and cleanup after partial failure. It supplies selected halt, +resume, and status without lending raw targets. Independent RP2350 core control +has [physical evidence][dual-core]. Neither path owns CTI routing or supplies a +simultaneous memory snapshot. ## Discovery and opening diff --git a/docs/capabilities.md b/docs/capabilities.md index ca1a142..8a18594 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -302,7 +302,7 @@ skips have hardware-independent test coverage. | Register reads | Yes | Halted Cortex-M0/M33 R0–R12, SP, LR, PC, XPSR, MSP, and PSP through `ReadRegister`. Two fresh sessions each on CMSIS-DAP/micro:bit and J-Link/RP2350 core 0 read all 19 registers; transfer failures and cleanup have behavioral coverage. | | Register writes | Yes | Halted Cortex-M0/M33 writes except XPSR; aligned SP/MSP/PSP and even PC values. Writes persist after release. Behavioral tests cover staging, uncertain selection, and pending cleanup. Two sessions each on micro:bit and RP2350 core 0 wrote and restored R4, SP, MSP, PSP, and PC before resuming; see the [micro:bit](cortexm.md#register-bench) and [RP2350](cortexm.md#rp2350-register-bench) register benches. | | Independent RP2350 cores | HIL | Two fresh J-Link sessions borrowed core-0/core-1 MEM-APs under one Arm owner. Each core halted and stepped while its peer advanced; both restored disabled debug and running state with inactive CTIs unchanged. See the [independent-core bench](cortexm.md#rp2350-independent-core-bench). This provides serialized per-core control, without group or CTI ownership. | -| Cortex-M group acquisition and release | Yes | `AcquireGroup` owns an explicit member list over borrowed memory and retains per-core acquisition and release results. Failed restoration keeps cleanup retryable; lower owners must remain live. Behavioral tests cover partial setup, independent cleanup, and retry; see [group ownership](cortexm.md#group-ownership). | +| Cortex-M group ownership and control | Yes | Selected halt/resume/status preserve per-core outcomes and inherited halts, with retryable cleanup after partial failure. Behavioral tests cover cancellation, ambiguous writes, M33 restart, and permission loss; see [group ownership](cortexm.md#group-ownership). | | Reset | No | No architectural or pin-reset operation exists. | | Breakpoints or watchpoints | No | No target instrumentation API exists. | | Firmware or runtime loading | No | No ELF loader, image-placement policy, or flash driver exists. | diff --git a/docs/composition.md b/docs/composition.md index a30a36b..c0c8a3f 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -14,33 +14,33 @@ data-register write can write target memory. ## Find the right layer -| Task | Public API | Executable reference | -| -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | -| Discover and select among registered probe drivers | `discover.Probes`, `ProbeInventory.Select`, `Candidate.Open`, or `discover.OpenProbe` | `discover/probes` integration tests | -| List every host USB attachment | `usb.New`, `usb.AllDevices`, `Enumerator.List` | Package tests | -| List USB attachments understood by the FTDI driver | `usb.New`, `ftdi.SupportedDevices`, `Enumerator.List` | `ost ftdi list` | -| Read metadata from one J-Link | `usb.New`, `jlink.SupportedDevices`, `Enumerator.Open`, `jlink.Open`, `Session.Info` | Package tests | -| Read metadata from one CMSIS-DAP v2 probe | `usb.New`, `usb.AllDevices`, `cmsisdap.Candidates`, `Enumerator.Open`, `cmsisdap.Open`, `Session.Info` | Package tests | -| Open one FTDI MPSSE SWD port | `Enumerator.Open`, `ftdi.Open` | `examples/trivial/swd-dpidr` | -| Use an FTDI JTAG chain | `ftdi.Open` or `Probe.JTAG`, then `jtag.New` and `jtag.NewChain` | FTDI integration tests | -| Open one J-Link SWD session | `Enumerator.Open`, `jlink.Open`, `jlink.WithSWD` | Package tests | -| Use a J-Link JTAG chain | `jlink.Open` with `WithJTAG` or `Probe.JTAG`, then `jtag.New` and `jtag.NewChain` | J-Link integration tests | -| Open one CMSIS-DAP SWD session | `Enumerator.Open`, `cmsisdap.Open`, `cmsisdap.WithSWD` | Package tests | -| Connect SWD or transfer DP/AP registers | `swd.New`, `Conn.Connect`, `Conn.ReadDP`, `Conn.WriteDP`, `Conn.ReadAP`, `Conn.WriteAP`, `Conn.NewBatch`, `Conn.Release` | `examples/trivial/swd-dpidr` | -| Enter SWD, decode a DPIDR, and manage SW-DP power | `dap.NewDebugPort`, `DebugPort.Connect`, `DebugPort.Release` | `ost dap dp id` | -| Identify one explicitly selected AP | `DebugPort.ReadAPIDR`, `DecodeAPIDR` | `examples/simple/ap-id` | -| Access another AP register by its full register address | `DebugPort.ReadRawAP`, `DebugPort.WriteRawAP` | Package tests | -| Read or write one aligned target scalar through a MEM-AP | `dap.OpenMemAP`, `MemAP.ReadScalar`, `MemAP.WriteScalar`, `MemAP.Release` | `examples/simple/cortexm-info` uses `ReadWord`. | -| Read or write arbitrary target bytes through a MEM-AP | `dap.OpenMemAP`, `MemAP.ReadBlock`, `MemAP.WriteBlock`, `MemAP.Release` | Package tests | -| Obtain a MEM-AP's advertised debug entry | `MemAP.ReadDebugBase` | `examples/simple/coresight-info` | -| 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` | -| Acquire, halt, inspect registers, and resume a Cortex-M0 | `cortexm.Acquire`, `Target.Halt`, `Target.ReadRegister`, `Target.Resume`, `Target.Release` | `examples/simple/cortexm-control` | -| Step a Cortex-M0/M33 from an owned halt | `Target.Step` | `examples/simple/cortexm-control -step` | -| Acquire and release explicit Cortex-M targets together | `cortexm.AcquireGroup`, `Group.Results`, `Group.Release` | [Group ownership](cortexm.md#group-ownership) | -| Read or write a halted Cortex-M0/M33 register | `Target.ReadRegister`, `Target.WriteRegister` | [Register reads](cortexm.md#register-reads), [writes](cortexm.md#register-writes) | -| Test SWD and DAP behavior without hardware | `swd/sim`, `dap/sim` | Package tests | +| Task | Public API | Executable reference | +| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------- | +| Discover and select among registered probe drivers | `discover.Probes`, `ProbeInventory.Select`, `Candidate.Open`, or `discover.OpenProbe` | `discover/probes` integration tests | +| List every host USB attachment | `usb.New`, `usb.AllDevices`, `Enumerator.List` | Package tests | +| List USB attachments understood by the FTDI driver | `usb.New`, `ftdi.SupportedDevices`, `Enumerator.List` | `ost ftdi list` | +| Read metadata from one J-Link | `usb.New`, `jlink.SupportedDevices`, `Enumerator.Open`, `jlink.Open`, `Session.Info` | Package tests | +| Read metadata from one CMSIS-DAP v2 probe | `usb.New`, `usb.AllDevices`, `cmsisdap.Candidates`, `Enumerator.Open`, `cmsisdap.Open`, `Session.Info` | Package tests | +| Open one FTDI MPSSE SWD port | `Enumerator.Open`, `ftdi.Open` | `examples/trivial/swd-dpidr` | +| Use an FTDI JTAG chain | `ftdi.Open` or `Probe.JTAG`, then `jtag.New` and `jtag.NewChain` | FTDI integration tests | +| Open one J-Link SWD session | `Enumerator.Open`, `jlink.Open`, `jlink.WithSWD` | Package tests | +| Use a J-Link JTAG chain | `jlink.Open` with `WithJTAG` or `Probe.JTAG`, then `jtag.New` and `jtag.NewChain` | J-Link integration tests | +| Open one CMSIS-DAP SWD session | `Enumerator.Open`, `cmsisdap.Open`, `cmsisdap.WithSWD` | Package tests | +| Connect SWD or transfer DP/AP registers | `swd.New`, `Conn.Connect`, `Conn.ReadDP`, `Conn.WriteDP`, `Conn.ReadAP`, `Conn.WriteAP`, `Conn.NewBatch`, `Conn.Release` | `examples/trivial/swd-dpidr` | +| Enter SWD, decode a DPIDR, and manage SW-DP power | `dap.NewDebugPort`, `DebugPort.Connect`, `DebugPort.Release` | `ost dap dp id` | +| Identify one explicitly selected AP | `DebugPort.ReadAPIDR`, `DecodeAPIDR` | `examples/simple/ap-id` | +| Access another AP register by its full register address | `DebugPort.ReadRawAP`, `DebugPort.WriteRawAP` | Package tests | +| Read or write one aligned target scalar through a MEM-AP | `dap.OpenMemAP`, `MemAP.ReadScalar`, `MemAP.WriteScalar`, `MemAP.Release` | `examples/simple/cortexm-info` uses `ReadWord`. | +| Read or write arbitrary target bytes through a MEM-AP | `dap.OpenMemAP`, `MemAP.ReadBlock`, `MemAP.WriteBlock`, `MemAP.Release` | Package tests | +| Obtain a MEM-AP's advertised debug entry | `MemAP.ReadDebugBase` | `examples/simple/coresight-info` | +| 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` | +| Acquire, halt, inspect registers, and resume a Cortex-M0 | `cortexm.Acquire`, `Target.Halt`, `Target.ReadRegister`, `Target.Resume`, `Target.Release` | `examples/simple/cortexm-control` | +| Step a Cortex-M0/M33 from an owned halt | `Target.Step` | `examples/simple/cortexm-control -step` | +| Own an explicit Cortex-M group and control selected cores | `cortexm.AcquireGroup`, `Group.Halt`, `Group.Resume`, `Group.Status`, `Group.Release` | [Group ownership](cortexm.md#group-ownership) | +| Read or write a halted Cortex-M0/M33 register | `Target.ReadRegister`, `Target.WriteRegister` | [Register reads](cortexm.md#register-reads), [writes](cortexm.md#register-writes) | +| Test SWD and DAP behavior without hardware | `swd/sim`, `dap/sim` | Package tests | The examples are intentionally small, executable compositions of public packages. `ost` adds command parsing and output policy, but its internal diff --git a/docs/cortexm.md b/docs/cortexm.md index 1c545d2..f01bd13 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -333,9 +333,65 @@ nil and inactive groups require no cleanup. `Results` returns copied member outcomes in membership order without traffic. `Attempted` distinguishes members reached from those not reached, and `Err` retains their causes. Acquisition failure also records cleanup errors. Identity -remains available after release. Release updates each member's outcome. The -group never lends its owned targets. Calls and all other access over the shared -connection must be serialized by the caller. +remains available after release. Later operations update the selected members, +so cached results may describe different calls. The group never lends its owned +targets. Calls and all other access over the shared connection must be +serialized by the caller. + +### Selected control + +`Halt`, `Resume`, and `Status` take explicit core IDs and operate in membership +order, returning an outcome for every selected member. Empty, duplicate, and +unknown selections fail before traffic. An operation stops on its first failure, +retaining earlier successes and marking later members unattempted. An uncertain +or unavailable target puts the whole group into cleanup-only mode; a rejected +precondition or cancellation before a target attempt does not. + +`Halt` confirms each selected stop without claiming a halt already present. +`Resume` observes each selected core and releases only owned halt requests; +running or unowned cores are reported as `Skipped` without a control write. +`Status` observes without claiming a stop, consuming the sticky DHCSR indicators +documented for `Target.Halted`. M33 restart evidence relinquishes a halt claim. + +The outcome's `State` is `ExecutionUnknown` unless the operation establishes +`Running` or `Halted`. `HaltOwned` records the target's retained halt-request +claim; on a failed confirmation it does not prove a stop. Release outcomes have +unknown execution state. Cached results do not refresh hardware observations. +Sequential requests do not provide simultaneous stopping or an atomic memory +snapshot. The group does not configure CTIs; inherited routes can couple cores. + +For two memory clients already borrowed from one Arm owner: + +```go +group, err := cortexm.AcquireGroup(ctx, []cortexm.Member{ + {ID: 1, Memory: memory0}, + {ID: 2, Memory: memory1}, +}) +if err == nil { + var stops []cortexm.CoreResult + stops, err = group.Halt(ctx, 1, 2) + for _, stop := range stops { + log.Printf("core %d: state=%v owned=%t attempted=%t error=%v", + stop.ID, stop.State, stop.HaltOwned, stop.Attempted, stop.Err) + } + if err == nil { + _, err = group.Resume(ctx, 1) + } +} +cleanup, cancel := context.WithTimeout(context.Background(), 5*time.Second) +results, releaseErr := group.Release(cleanup) +cancel() +err = errors.Join(err, releaseErr) +if releaseErr == nil { + err = errors.Join(err, owner.Close()) +} +``` + +Inspect `results` and `err`. When release fails, retain `group` and `owner` for +another cleanup attempt rather than close the memory dependency. An ambiguous +write, new unowned halt, lost permission, or unusable memory can prevent +recovery. Successful release restores debug control; it does not undo executed +instructions. ## Hardware procedure diff --git a/target/cortexm/group.go b/target/cortexm/group.go index 606c1a5..b371155 100644 --- a/target/cortexm/group.go +++ b/target/cortexm/group.go @@ -19,12 +19,17 @@ type Member struct { } // CoreResult is a detached member outcome. Attempted means the member operation -// was called. CleanupPending requires retaining memory and lower owners. -// Identity survives successful release. +// was called; Skipped means Resume observed no owned halt to release. State is +// unknown unless this operation observed execution. HaltOwned is the target's +// retained halt-request claim, not proof that it stopped. CleanupPending requires +// retaining memory and lower owners. Identity survives successful release. type CoreResult struct { ID CoreID Identity Identity Attempted bool + Skipped bool + State ExecutionState + HaltOwned bool CleanupPending bool Err error } @@ -94,9 +99,10 @@ func validateMembers(members []Member) error { return nil } -// Results copies member outcomes in membership order without traffic. -// Acquisition failure includes any cleanup errors. Nil groups return no results. -// Changing the returned slice does not change group state. +// Results copies each member's most recent outcome in membership order without +// traffic; members may have been selected by different calls. Acquisition failure +// includes any cleanup errors. Nil groups return no results. Changing the +// returned slice does not change group state. func (g *Group) Results() []CoreResult { if g == nil { return nil @@ -104,6 +110,7 @@ func (g *Group) Results() []CoreResult { results := make([]CoreResult, len(g.cores)) for i, core := range g.cores { results[i] = core.result + results[i].HaltOwned = core.target != nil && core.target.haltOwned } return results } @@ -126,6 +133,8 @@ func (g *Group) release(ctx context.Context, preserve bool) ([]CoreResult, error if !preserve { for i := range g.cores { g.cores[i].result.Attempted = false + g.cores[i].result.Skipped = false + g.cores[i].result.State = ExecutionUnknown g.cores[i].result.Err = nil } } diff --git a/target/cortexm/group_control.go b/target/cortexm/group_control.go new file mode 100644 index 0000000..4baacd4 --- /dev/null +++ b/target/cortexm/group_control.go @@ -0,0 +1,142 @@ +package cortexm + +import ( + "context" + "errors" +) + +// ExecutionState is the execution state observed by a group operation. +// It describes an observation, not a simultaneous snapshot of its members. +type ExecutionState uint8 + +const ( + // ExecutionUnknown means the operation did not establish execution state. + ExecutionUnknown ExecutionState = iota + // Running means the processor was observed outside Debug state. + Running + // Halted means the processor was observed in Debug state. + Halted +) + +// Halt requests and confirms selected stops in membership order. An existing +// stop remains unowned. Empty, duplicate, and unknown selections fail before +// traffic. Failure stops the operation without resuming earlier successes; +// results include selected members not reached. An uncertain member leaves +// the entire group available only for Release and cached Results. +func (g *Group) Halt(ctx context.Context, ids ...CoreID) ([]CoreResult, error) { + return g.control(ctx, ids, haltCore) +} + +// Resume observes selected cores, then resumes only owned halt requests. +// Running or unowned cores are reported as Skipped without a control write. +// Failure retains partial progress and never replays an uncertain resume. +// Selection and failure rules are those of Halt. Execution is not undone. +func (g *Group) Resume(ctx context.Context, ids ...CoreID) ([]CoreResult, error) { + return g.control(ctx, ids, resumeCore) +} + +// Status observes selected cores in membership order without claiming stops. +// Like Target.Halted, it consumes sticky DHCSR status and can relinquish a halt +// claim on M33 restart. Selection and failure rules are those of Halt. +func (g *Group) Status(ctx context.Context, ids ...CoreID) ([]CoreResult, error) { + return g.control(ctx, ids, statusCore) +} + +type coreControl func(context.Context, *Target) (ExecutionState, bool, error) + +func (g *Group) control(ctx context.Context, ids []CoreID, call coreControl) ([]CoreResult, error) { + selected, err := g.selectCores(ids) + if err != nil { + return nil, err + } + for _, i := range selected { + core := &g.cores[i] + core.result.Attempted, core.result.Skipped = false, false + core.result.State, core.result.Err = ExecutionUnknown, nil + } + for _, i := range selected { + core := &g.cores[i] + if err = liveContext(ctx); err != nil { + core.result.Err = err + break + } + core.result.Attempted = true + core.result.State, core.result.Skipped, err = call(ctx, core.target) + core.result.Err = err + if err != nil { + g.closing = core.target.closing + break + } + } + results := g.selectedResults(selected) + for _, result := range results { + if result.Err != nil { + return results, coreError(result.ID, result.Err) + } + } + return results, nil +} + +func (g *Group) selectCores(ids []CoreID) ([]int, error) { + if g == nil || len(g.cores) == 0 || g.closing { + return nil, errors.New("cortexm: group is unavailable; release may be pending") + } + if len(ids) == 0 { + return nil, errors.New("cortexm: empty core selection") + } + wanted := make(map[CoreID]bool, len(ids)) + for _, id := range ids { + if id == 0 || wanted[id] { + return nil, errors.New("cortexm: require unique nonzero selected core IDs") + } + wanted[id] = true + } + var selected []int + for i, core := range g.cores { + if wanted[core.result.ID] { + selected = append(selected, i) + } + } + if len(selected) != len(ids) { + return nil, errors.New("cortexm: selected core is not a group member") + } + return selected, nil +} + +func (g *Group) selectedResults(selected []int) []CoreResult { + all := g.Results() + results := make([]CoreResult, len(selected)) + for i, index := range selected { + results[i] = all[index] + } + return results +} + +func haltCore(ctx context.Context, t *Target) (ExecutionState, bool, error) { + if err := t.Halt(ctx); err != nil { + return ExecutionUnknown, false, err + } + return Halted, false, nil +} + +func resumeCore(ctx context.Context, t *Target) (ExecutionState, bool, error) { + state, _, err := statusCore(ctx, t) + if err != nil || !t.haltOwned { + return state, err == nil, err + } + if err := t.Resume(ctx); err != nil { + return ExecutionUnknown, false, err + } + return Running, false, nil +} + +func statusCore(ctx context.Context, t *Target) (ExecutionState, bool, error) { + halted, err := t.Halted(ctx) + if err != nil { + return ExecutionUnknown, false, err + } + if halted { + return Halted, false, nil + } + return Running, false, nil +} diff --git a/target/cortexm/group_control_test.go b/target/cortexm/group_control_test.go new file mode 100644 index 0000000..f21a4ec --- /dev/null +++ b/target/cortexm/group_control_test.go @@ -0,0 +1,229 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func acquireControlGroup(t *testing.T, memories ...cortexm.Memory) *cortexm.Group { + t.Helper() + g, err := cortexm.AcquireGroup(t.Context(), groupMembers(memories...)) + if err != nil { + t.Fatal(err) + } + return g +} + +func TestGroupSelectedControlAndStatus(t *testing.T) { + a, b := newControlMemory(), newControlMemory() + g := acquireControlGroup(t, a, b) + results, err := g.Halt(t.Context(), 2, 1) + if err != nil || len(results) != 2 || results[0].ID != 1 || results[1].ID != 2 { + t.Fatalf("halt=%+v error=%v", results, err) + } + for _, result := range results { + if result.State != cortexm.Halted || !result.HaltOwned || !result.Attempted || result.Skipped { + t.Fatalf("halt=%+v", result) + } + } + if _, err := g.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestGroupSelectiveResumeAndLiveStatus(t *testing.T) { + a, b := newControlMemory(), newControlMemory() + g := acquireControlGroup(t, a, b) + if _, err := g.Halt(t.Context(), 1, 2); err != nil { + t.Fatal(err) + } + results, err := g.Resume(t.Context(), 1) + if err != nil || len(results) != 1 || results[0].State != cortexm.Running || results[0].HaltOwned || a.halted || !b.halted { + t.Fatalf("resume=%+v error=%v halted=%v/%v", results, err, a.halted, b.halted) + } + results, err = g.Status(t.Context(), 1, 2) + if err != nil || results[0].State != cortexm.Running || results[1].State != cortexm.Halted || !results[1].HaltOwned { + t.Fatalf("status=%+v error=%v", results, err) + } + if _, err := g.Release(t.Context()); err != nil || a.control != 0 || b.control != 0 { + t.Fatalf("release=%v control=%#x/%#x", err, a.control, b.control) + } + for _, result := range g.Results() { + if result.State != cortexm.ExecutionUnknown || result.HaltOwned { + t.Fatalf("release retained stale state=%+v", result) + } + } +} + +func TestGroupResumeSkipsUnownedStopsAndRunningCores(t *testing.T) { + a, b := newControlMemory(), newControlMemory() + a.control, a.halted = debugEnable|haltRequest, true + g := acquireControlGroup(t, a, b) + aWrites, bWrites := a.writes, b.writes + results, err := g.Resume(t.Context(), 1, 2) + if err != nil || !results[0].Skipped || !results[1].Skipped || results[0].State != cortexm.Halted || results[1].State != cortexm.Running { + t.Fatalf("resume=%+v error=%v", results, err) + } + if a.writes != aWrites || b.writes != bWrites { + t.Fatal("resume wrote an unowned core") + } + if results, err = g.Halt(t.Context(), 1); err != nil || results[0].HaltOwned { + t.Fatalf("inherited halt=%+v error=%v", results, err) + } + if _, err := g.Release(t.Context()); err != nil || !a.halted { + t.Fatalf("release=%v inherited halt=%v", err, a.halted) + } +} + +func TestGroupInvalidSelectionHasNoEffects(t *testing.T) { + m := newControlMemory() + g := acquireControlGroup(t, m) + for _, call := range []func(context.Context, ...cortexm.CoreID) ([]cortexm.CoreResult, error){g.Halt, g.Resume, g.Status} { + for _, ids := range [][]cortexm.CoreID{nil, {0}, {2}, {1, 1}, {1, 2}} { + reads, writes := m.reads, m.writes + if _, err := call(t.Context(), ids...); err == nil || m.reads != reads || m.writes != writes { + t.Fatalf("selection=%v error=%v reads/writes=%d/%d", ids, err, m.reads, m.writes) + } + } + if _, err := call(nil, 1); err == nil { + t.Fatal("accepted nil context") + } + } + if _, err := g.Release(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := g.Halt(t.Context(), 1); err == nil { + t.Fatal("halted a released group") + } + var nilGroup *cortexm.Group + for _, inactive := range []*cortexm.Group{nilGroup, {}} { + if _, err := inactive.Status(t.Context(), 1); err == nil { + t.Fatal("accepted an inactive group") + } + } +} + +func TestGroupHaltFailureRetainsPartialSuccess(t *testing.T) { + a, b, c := newControlMemory(), newControlMemory(), newControlMemory() + g := acquireControlGroup(t, a, b, c) + b.failRead = b.reads + 1 + cReads := c.reads + results, err := g.Halt(t.Context(), 3, 2, 1) + if !errors.Is(err, errMemory) || len(results) != 3 { + t.Fatalf("halt=%+v error=%v", results, err) + } + if !a.halted || !results[0].HaltOwned || results[0].State != cortexm.Halted || results[1].State != cortexm.ExecutionUnknown || !errors.Is(results[1].Err, errMemory) || results[2].Attempted || c.reads != cReads { + t.Fatalf("partial halt=%+v", results) + } + if _, err := g.Resume(t.Context(), 1); err == nil { + t.Fatal("ordinary control remained available after member failure") + } + if _, err := g.Release(t.Context()); err != nil || a.halted || a.control != 0 || b.control != 0 || c.control != 0 { + t.Fatalf("cleanup=%v controls=%#x/%#x/%#x", err, a.control, b.control, c.control) + } +} + +func TestGroupUncertainHaltNeverReplays(t *testing.T) { + for _, after := range []bool{false, true} { + a, b := newControlMemory(), newControlMemory() + g := acquireControlGroup(t, a, b) + a.failWrite, a.afterWrite = a.writes+1, after + results, err := g.Halt(t.Context(), 1, 2) + if !errors.Is(err, errMemory) || results[0].HaltOwned || results[1].Attempted { + t.Fatalf("after=%v results=%+v error=%v", after, results, err) + } + writes := a.writes + _, err = g.Release(t.Context()) + if after { + if err == nil || a.writes != writes || !g.Results()[0].CleanupPending { + t.Fatalf("uncertain halt cleanup=%v writes=%d/%d", err, a.writes, writes) + } + } else if err != nil || a.control != 0 { + t.Fatalf("rejected halt cleanup=%v control=%#x", err, a.control) + } + if b.control != 0 || g.Results()[1].CleanupPending { + t.Fatal("uncertain member prevented independent cleanup") + } + } +} + +func TestGroupCancellationAfterHaltRetainsPeerOutcome(t *testing.T) { + a, b := newControlMemory(), newControlMemory() + g := acquireControlGroup(t, a, b) + ctx, cancel := context.WithCancel(t.Context()) + a.onWrite = cancel + results, err := g.Halt(ctx, 1, 2) + if !errors.Is(err, context.Canceled) || !a.halted || results[1].Attempted || results[0].State != cortexm.ExecutionUnknown { + t.Fatalf("results=%+v error=%v", results, err) + } + if _, err := g.Release(t.Context()); err != nil || a.halted || a.control != 0 || b.control != 0 { + t.Fatalf("cleanup=%v controls=%#x/%#x", err, a.control, b.control) + } +} + +func TestGroupM33RestartLeavesCompetingStopUnowned(t *testing.T) { + m := newControlMemory() + m.cpuid, m.status = 0x411fd210, 1<<20 + g := acquireControlGroup(t, m) + if _, err := g.Halt(t.Context(), 1); err != nil { + t.Fatal(err) + } + m.status |= 1 << 26 + writes := m.writes + results, err := g.Resume(t.Context(), 1) + if err != nil || !results[0].Skipped || results[0].HaltOwned || m.writes != writes || !m.halted { + t.Fatalf("resume=%+v error=%v writes=%d/%d", results, err, m.writes, writes) + } + if _, err := g.Release(t.Context()); err == nil || !m.halted || m.writes != writes { + t.Fatalf("competing halt cleanup=%v", err) + } +} + +func TestGroupPartialResumeRetainsConfirmedAndUncertainEffects(t *testing.T) { + for _, after := range []bool{false, true} { + a, b, c := newControlMemory(), newControlMemory(), newControlMemory() + g := acquireControlGroup(t, a, b, c) + if _, err := g.Halt(t.Context(), 1, 2, 3); err != nil { + t.Fatal(err) + } + b.failWrite, b.afterWrite = b.writes+1, after + results, err := g.Resume(t.Context(), 1, 2, 3) + if !errors.Is(err, errMemory) || results[0].State != cortexm.Running || results[0].HaltOwned || results[1].State != cortexm.ExecutionUnknown || results[2].Attempted || !c.halted { + t.Fatalf("after=%v results=%+v error=%v", after, results, err) + } + bWrites := b.writes + _, err = g.Release(t.Context()) + if after && err != nil { + t.Fatal(err) + } + if !after && (err == nil || b.writes != bWrites) { + t.Fatalf("replayed uncertain resume: error=%v writes=%d/%d", err, b.writes, bWrites) + } + if a.halted || c.halted || a.control != 0 || c.control != 0 { + t.Fatal("confirmed members did not restore independently") + } + } +} + +func TestGroupPermissionLossKeepsCleanupRetryable(t *testing.T) { + a, b := newControlMemory(), newControlMemory() + a.cpuid, a.status = 0x411fd210, 1<<20 + g := acquireControlGroup(t, a, b) + if _, err := g.Halt(t.Context(), 1, 2); err != nil { + t.Fatal(err) + } + a.status = 0 + if _, err := g.Status(t.Context(), 1, 2); err == nil { + t.Fatal("accepted lost permission") + } + if results, err := g.Release(t.Context()); err == nil || !results[0].CleanupPending || results[1].CleanupPending || !a.halted { + t.Fatalf("release=%+v error=%v", results, err) + } + a.status = 1 << 20 + if _, err := g.Release(t.Context()); err != nil || a.halted || a.control != 0 { + t.Fatalf("retry=%v control=%#x", err, a.control) + } +} From 4331bf6c640743fd3c2343016c34601f7457f270 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Fri, 2 Oct 2026 04:26:35 +0000 Subject: [PATCH 3/3] Access Cortex-M group members through their owned targets. A group cannot lend raw targets without bypassing its lifecycle. Route register and step operations through its selected member and retain the same transfer, event, and cleanup rules. Exercise the composition on the prepared RP2350 counters without changing inherited CTI routing. --- docs/architecture.md | 10 +- docs/capabilities.md | 30 ++--- docs/composition.md | 1 + docs/cortexm.md | 54 ++++++++ target/cortexm/group_access.go | 43 ++++++ target/cortexm/group_access_test.go | 135 +++++++++++++++++++ target/cortexm/group_integration_test.go | 138 ++++++++++++++++++++ target/cortexm/register_integration_test.go | 8 +- target/cortexm/step_integration_test.go | 8 +- 9 files changed, 405 insertions(+), 22 deletions(-) create mode 100644 target/cortexm/group_access.go create mode 100644 target/cortexm/group_access_test.go create mode 100644 target/cortexm/group_integration_test.go diff --git a/docs/architecture.md b/docs/architecture.md index a237bd3..9cea2ad 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -64,10 +64,11 @@ their framing. A target owns one processor. Several targets may borrow distinct MEM-APs from one `armdebug.Conn`; all calls over that shared owner remain serialized. `cortexm.Group` acquires and owns an explicit set of those targets, retaining -member outcomes and cleanup after partial failure. It supplies selected halt, -resume, and status without lending raw targets. Independent RP2350 core control -has [physical evidence][dual-core]. Neither path owns CTI routing or supplies a -simultaneous memory snapshot. +member outcomes and cleanup after partial failure. It supplies selected control, +register access, and stepping without lending raw targets. Independent RP2350 +core control has [physical evidence][dual-core], and the [group bench][group] +exercises selected control and cleanup through one group. Neither path owns CTI +routing or supplies a simultaneous memory snapshot. ## Discovery and opening @@ -419,3 +420,4 @@ Callers should always complete the documented release sequence, including when the primary operation fails. [dual-core]: cortexm.md#rp2350-independent-core-bench +[group]: cortexm.md#rp2350-group-bench diff --git a/docs/capabilities.md b/docs/capabilities.md index 8a18594..fb18704 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -291,21 +291,21 @@ skips have hardware-independent test coverage. ## Cortex-M target operations -| Capability | Implemented | Validation and boundary | -| -------------------------------------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| CPUID read and decode | Yes | Accepts any aligned-word reader and validates a plausible Arm Cortex-M identity. | -| Physical identity read | HIL | Opt-in FTDI/SWD/DAP/MEM-AP integration test. | -| Cortex-M0 acquisition and halt/resume | HIL | Two CMSIS-DAP micro:bit sessions at a requested 1 MHz stopped a CPU counter during halt and observed progress after resume and release. Both restored initially disabled debug and running state before Arm debug owner close. Earlier sessions preserved initially enabled debug. Cleanup failures remain covered only by behavioral tests; see the [control evidence](cortexm.md#hardware-evidence). | -| Cortex-M33 acquisition and halt/resume | HIL | Two RP2350 core-0/J-Link sessions at 1 MHz stopped a RAM counter during halt, observed counter progress after resume and release, and restored disabled debug. Secure invasive debug permission is required. | -| Cortex-M0 step | HIL | `Target.Step` requires an owned halt and returns halted. Two fresh micro:bit sessions checked PC/R0/RAM across 13 steps each, resume, and release with disabled debug restored. Competing events and failure cleanup have behavioral coverage; see the [step bench](cortexm.md#step-bench). | -| Cortex-M33 step | HIL | Two fresh RP2350 core-0/J-Link sessions at 1 MHz checked PC/R0/RAM across 13 steps each, resume, and release with disabled debug restored. Secure counter state and DSCSR were preserved. Permission, snap-stall, restart after completion, and failure cleanup have behavioral coverage; see the [RP2350 step bench](cortexm.md#rp2350-step-bench). | -| Register reads | Yes | Halted Cortex-M0/M33 R0–R12, SP, LR, PC, XPSR, MSP, and PSP through `ReadRegister`. Two fresh sessions each on CMSIS-DAP/micro:bit and J-Link/RP2350 core 0 read all 19 registers; transfer failures and cleanup have behavioral coverage. | -| Register writes | Yes | Halted Cortex-M0/M33 writes except XPSR; aligned SP/MSP/PSP and even PC values. Writes persist after release. Behavioral tests cover staging, uncertain selection, and pending cleanup. Two sessions each on micro:bit and RP2350 core 0 wrote and restored R4, SP, MSP, PSP, and PC before resuming; see the [micro:bit](cortexm.md#register-bench) and [RP2350](cortexm.md#rp2350-register-bench) register benches. | -| Independent RP2350 cores | HIL | Two fresh J-Link sessions borrowed core-0/core-1 MEM-APs under one Arm owner. Each core halted and stepped while its peer advanced; both restored disabled debug and running state with inactive CTIs unchanged. See the [independent-core bench](cortexm.md#rp2350-independent-core-bench). This provides serialized per-core control, without group or CTI ownership. | -| Cortex-M group ownership and control | Yes | Selected halt/resume/status preserve per-core outcomes and inherited halts, with retryable cleanup after partial failure. Behavioral tests cover cancellation, ambiguous writes, M33 restart, and permission loss; see [group ownership](cortexm.md#group-ownership). | -| Reset | No | No architectural or pin-reset operation exists. | -| Breakpoints or watchpoints | No | No target instrumentation API exists. | -| Firmware or runtime loading | No | No ELF loader, image-placement policy, or flash driver exists. | +| Capability | Implemented | Validation and boundary | +| -------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| CPUID read and decode | Yes | Accepts any aligned-word reader and validates a plausible Arm Cortex-M identity. | +| Physical identity read | HIL | Opt-in FTDI/SWD/DAP/MEM-AP integration test. | +| Cortex-M0 acquisition and halt/resume | HIL | Two CMSIS-DAP micro:bit sessions at a requested 1 MHz stopped a CPU counter during halt and observed progress after resume and release. Both restored initially disabled debug and running state before Arm debug owner close. Earlier sessions preserved initially enabled debug. Cleanup failures remain covered only by behavioral tests; see the [control evidence](cortexm.md#hardware-evidence). | +| Cortex-M33 acquisition and halt/resume | HIL | Two RP2350 core-0/J-Link sessions at 1 MHz stopped a RAM counter during halt, observed counter progress after resume and release, and restored disabled debug. Secure invasive debug permission is required. | +| Cortex-M0 step | HIL | `Target.Step` requires an owned halt and returns halted. Two fresh micro:bit sessions checked PC/R0/RAM across 13 steps each, resume, and release with disabled debug restored. Competing events and failure cleanup have behavioral coverage; see the [step bench](cortexm.md#step-bench). | +| Cortex-M33 step | HIL | Two fresh RP2350 core-0/J-Link sessions at 1 MHz checked PC/R0/RAM across 13 steps each, resume, and release with disabled debug restored. Secure counter state and DSCSR were preserved. Permission, snap-stall, restart after completion, and failure cleanup have behavioral coverage; see the [RP2350 step bench](cortexm.md#rp2350-step-bench). | +| Register reads | Yes | Halted Cortex-M0/M33 R0–R12, SP, LR, PC, XPSR, MSP, and PSP through `ReadRegister`. Two fresh sessions each on CMSIS-DAP/micro:bit and J-Link/RP2350 core 0 read all 19 registers; transfer failures and cleanup have behavioral coverage. | +| Register writes | Yes | Halted Cortex-M0/M33 writes except XPSR; aligned SP/MSP/PSP and even PC values. Writes persist after release. Behavioral tests cover staging, uncertain selection, and pending cleanup. Two sessions each on micro:bit and RP2350 core 0 wrote and restored R4, SP, MSP, PSP, and PC before resuming; see the [micro:bit](cortexm.md#register-bench) and [RP2350](cortexm.md#rp2350-register-bench) register benches. | +| Independent RP2350 cores | HIL | Two fresh J-Link sessions borrowed core-0/core-1 MEM-APs under one Arm owner. Each core halted and stepped while its peer advanced; both restored disabled debug and running state with inactive CTIs unchanged. See the [independent-core bench](cortexm.md#rp2350-independent-core-bench). This provides serialized per-core control, without group or CTI ownership. | +| Cortex-M group ownership and control | HIL | `AcquireGroup` owns explicitly selected targets; selected halt/resume/status and member register/step access retain per-core outcomes and retryable cleanup. Two fresh RP2350/J-Link sessions exercised selected control, sequential stopping, three verified steps, and selective resume, with debug state restored and inactive CTIs unchanged before owner close. Failure paths have behavioral coverage; see the [group bench](cortexm.md#rp2350-group-bench). | +| Reset | No | No architectural or pin-reset operation exists. | +| Breakpoints or watchpoints | No | No target instrumentation API exists. | +| Firmware or runtime loading | No | No ELF loader, image-placement policy, or flash driver exists. | Identity covers Cortex-M; acquired halt/resume control accepts Cortex-M0 and Cortex-M33. M33 requires Secure invasive debug permission. Register access uses diff --git a/docs/composition.md b/docs/composition.md index c0c8a3f..5558754 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -39,6 +39,7 @@ data-register write can write target memory. | Acquire, halt, inspect registers, and resume a Cortex-M0 | `cortexm.Acquire`, `Target.Halt`, `Target.ReadRegister`, `Target.Resume`, `Target.Release` | `examples/simple/cortexm-control` | | Step a Cortex-M0/M33 from an owned halt | `Target.Step` | `examples/simple/cortexm-control -step` | | Own an explicit Cortex-M group and control selected cores | `cortexm.AcquireGroup`, `Group.Halt`, `Group.Resume`, `Group.Status`, `Group.Release` | [Group ownership](cortexm.md#group-ownership) | +| Access a group member's registers or step it | `Group.ReadRegister`, `Group.WriteRegister`, `Group.Step` | [Member access](cortexm.md#member-registers-and-stepping) | | Read or write a halted Cortex-M0/M33 register | `Target.ReadRegister`, `Target.WriteRegister` | [Register reads](cortexm.md#register-reads), [writes](cortexm.md#register-writes) | | Test SWD and DAP behavior without hardware | `swd/sim`, `dap/sim` | Package tests | diff --git a/docs/cortexm.md b/docs/cortexm.md index f01bd13..979959b 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -393,6 +393,29 @@ write, new unowned halt, lost permission, or unusable memory can prevent recovery. Successful release restores debug control; it does not undo executed instructions. +### Member registers and stepping + +`Group.ReadRegister`, `Group.WriteRegister`, and `Group.Step` select one member +and use its owned target. They retain that member's result in `Results`, with +the same validation, transfer, event, and cleanup rules as the corresponding +target method. Register access requires a halt but does not claim it; stepping +requires an owned halt. Invalid register values fail before memory traffic, and +uncertain transfers or steps block ordinary group calls. A step neither resumes +nor steps peers. Register writes and instruction effects persist after release. + +For an acquired group with an owned stop on core ID 1: + +```go +pc, err := group.ReadRegister(ctx, 1, cortexm.PC) +if err == nil { + err = group.Step(ctx, 1) +} +``` + +Use the same bounded group release and lower-owner retention shown above even +when access fails. The group has no policy for suspending inherited CTI routes +during stepping. + ## Hardware procedure The opt-in integration test selects the CMSIS-DAP micro:bit with serial @@ -663,4 +686,35 @@ Per-target cleanup failures have behavioral coverage; this bench does not inject failures. State after Arm owner close was not independently measured. Both loops remain running; preparation and instruction effects are not undone. +## RP2350 group bench + +`TestHILRP2350Group` uses the same prepared [RAM counters][dual-counter], J-Link +EDU Mini V2 `000802011345` at 1 MHz, and one Arm owner lending AP `0x2000` and +AP `0x4000`. Group IDs 1 and 2 select physical cores 0 and 1 respectively. +Preparation effects and inactive CTI requirements are those of the +independent-core bench above. The test does not prepare RAM or write routing. + +```sh +OSTIOLE_RP2350_HIL_GROUP=1 \ +OSTIOLE_RP2350_HIL_PROGRAM=bf878b47815bc5eaf6afb5279efaa6ff163832bd178c5b7b1604f80e6ad6cde9 \ +go test -tags integration ./target/cortexm -run '^TestHILRP2350Group$' -count=1 -v +``` + +Two fresh sessions read all 19 registers on each core through the group, halted +and stepped each while its peer advanced, and resumed each selected core. Both +counters stopped under sequential group requests. One additional PC/R0/RAM +verified step on core 0 left both counters stopped; resuming only core 0 left +core 1 stopped. Group release restored progress on both cores. + +Both sessions restored initially disabled debug and running state before Arm +owner close, with DHCSR `0x01100000` before/after, DSCSR `0x00030000` unchanged, +and inactive CTIs and gate `0x0f` unchanged. Group release and owner close +succeeded. State after close was not independently measured. The loops remain +running; instruction and preparation effects are not undone. + +Group register writes, inherited stops, partial failures, cancellation, lost +permissions, competing events, and cleanup retries have behavioral coverage. +This bench does not exercise register writes, inject failures, or establish CTI +propagation, simultaneous stopping, or Non-secure execution. + [dual-counter]: ../target/cortexm/testdata/rp2350-dual-counter/README.md diff --git a/target/cortexm/group_access.go b/target/cortexm/group_access.go new file mode 100644 index 0000000..a01e98b --- /dev/null +++ b/target/cortexm/group_access.go @@ -0,0 +1,43 @@ +package cortexm + +import "context" + +// ReadRegister reads a selected member through its owned Target. The halted +// register and cleanup rules of Target.ReadRegister apply. Results retains the +// member outcome; an error returns no valid register value. +func (g *Group) ReadRegister(ctx context.Context, id CoreID, reg Register) (uint32, error) { + var value uint32 + err := g.access(ctx, id, func(t *Target) error { + var err error + value, err = t.ReadRegister(ctx, reg) + return err + }) + if err != nil { + return 0, err + } + return value, nil +} + +// WriteRegister changes a selected member through its owned Target. The value +// validation, effects, and cleanup rules of Target.WriteRegister apply. Release +// does not undo register writes. Results retains the member outcome. +func (g *Group) WriteRegister(ctx context.Context, id CoreID, reg Register, value uint32) error { + return g.access(ctx, id, func(t *Target) error { return t.WriteRegister(ctx, reg, value) }) +} + +// Step steps a selected member from an owned halt, preserving Target.Step's +// event and cleanup rules. It neither steps nor resumes peers. Results retains +// the member outcome. This group does not own cross-trigger routing. +func (g *Group) Step(ctx context.Context, id CoreID) error { + return g.access(ctx, id, func(t *Target) error { return t.Step(ctx) }) +} + +func (g *Group) access(ctx context.Context, id CoreID, call func(*Target) error) error { + _, err := g.control(ctx, []CoreID{id}, func(_ context.Context, t *Target) (ExecutionState, bool, error) { + if err := call(t); err != nil { + return ExecutionUnknown, false, err + } + return Halted, false, nil + }) + return err +} diff --git a/target/cortexm/group_access_test.go b/target/cortexm/group_access_test.go new file mode 100644 index 0000000..939778c --- /dev/null +++ b/target/cortexm/group_access_test.go @@ -0,0 +1,135 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func TestGroupRegistersAndStepUseSelectedTarget(t *testing.T) { + a, b := newStepMemory(), newStepMemory() + g := acquireControlGroup(t, a, b) + if _, err := g.Halt(t.Context(), 1, 2); err != nil { + t.Fatal(err) + } + peerReads, peerWrites := b.reads, b.writes + want := a.registers[4] + if value, err := g.ReadRegister(t.Context(), 1, cortexm.R4); err != nil || value != want { + t.Fatalf("R4=%#x want=%#x error=%v", value, want, err) + } + if err := g.WriteRegister(t.Context(), 1, cortexm.R4, 0xfeed); err != nil || a.registers[4] != 0xfeed { + t.Fatalf("write=%v R4=%#x", err, a.registers[4]) + } + result := g.Results()[0] + if result.State != cortexm.Halted || !result.HaltOwned || !result.Attempted || result.Err != nil { + t.Fatalf("step result=%+v", result) + } + if b.reads != peerReads || b.writes != peerWrites || b.steps != 0 || !b.halted { + t.Fatal("member access touched its peer") + } + if _, err := g.Release(t.Context()); err != nil || a.registers[4] != 0xfeed { + t.Fatalf("release=%v R4=%#x", err, a.registers[4]) + } +} + +func TestGroupStepLeavesPeerStopped(t *testing.T) { + a, b := newStepMemory(), newStepMemory() + g := acquireControlGroup(t, a, b) + if _, err := g.Halt(t.Context(), 1, 2); err != nil { + t.Fatal(err) + } + pc, r0 := a.registers[15], a.registers[0] + peerReads, peerWrites := b.reads, b.writes + if err := g.Step(t.Context(), 1); err != nil || a.steps != 1 || a.registers[15] != pc+2 || a.registers[0] != r0+1 { + t.Fatalf("step=%v steps=%d PC=%#x R0=%#x", err, a.steps, a.registers[15], a.registers[0]) + } + if b.reads != peerReads || b.writes != peerWrites || b.steps != 0 || !b.halted { + t.Fatal("step touched its peer") + } + if _, err := g.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestGroupMemberPreconditionsDoNotInvalidatePeers(t *testing.T) { + a, b := newStepMemory(), newStepMemory() + g := acquireControlGroup(t, a, b) + reads, writes := a.reads, a.writes + if _, err := g.ReadRegister(t.Context(), 1, 0); err == nil { + t.Fatal("invalid register accepted") + } + if err := g.WriteRegister(t.Context(), 1, cortexm.PC, 1); err == nil { + t.Fatal("invalid PC accepted") + } + if a.reads != reads || a.writes != writes { + t.Fatal("invalid input reached target memory") + } + if err := g.Step(t.Context(), 1); err == nil || a.launches != 0 { + t.Fatalf("unowned step=%v launches=%d", err, a.launches) + } + if _, err := g.Halt(t.Context(), 2); err != nil { + t.Fatal(err) + } + if _, err := g.ReadRegister(t.Context(), 0, cortexm.R0); err == nil { + t.Fatal("invalid member accepted") + } + if _, err := g.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestGroupRegisterFailureRetainsTargetLifecycle(t *testing.T) { + a, b := newRegisterMemory(), newRegisterMemory() + g := acquireControlGroup(t, a, b) + if _, err := g.Halt(t.Context(), 1, 2); err != nil { + t.Fatal(err) + } + a.failWrite, a.afterWrite = a.writes+1, true + if _, err := g.ReadRegister(t.Context(), 1, cortexm.R4); !errors.Is(err, errMemory) { + t.Fatal(err) + } + transfers := a.transfers + if err := g.WriteRegister(t.Context(), 2, cortexm.R4, 0); err == nil { + t.Fatal("member access remained available after uncertain selection") + } + if _, err := g.Release(t.Context()); err != nil || a.transfers != transfers || a.control != 0 || b.control != 0 { + t.Fatalf("release=%v transfers=%d/%d control=%#x/%#x", err, a.transfers, transfers, a.control, b.control) + } +} + +func TestGroupStepFailureDoesNotReplayLaunch(t *testing.T) { + a, b := newStepMemory(), newStepMemory() + g := acquireControlGroup(t, a, b) + if _, err := g.Halt(t.Context(), 1, 2); err != nil { + t.Fatal(err) + } + a.failWrite, a.afterWrite = a.writes+1, true + if err := g.Step(t.Context(), 1); !errors.Is(err, errMemory) || a.launches != 1 { + t.Fatalf("step=%v launches=%d", err, a.launches) + } + if _, err := g.Release(t.Context()); err == nil || a.launches != 1 || b.control != 0 || g.Results()[1].CleanupPending { + t.Fatalf("release=%v launches=%d peer control=%#x", err, a.launches, b.control) + } +} + +func TestGroupCanceledMemberAccessHasNoTraffic(t *testing.T) { + m := newStepMemory() + g := acquireControlGroup(t, m) + ctx, cancel := context.WithCancel(t.Context()) + cancel() + reads, writes := m.reads, m.writes + if _, err := g.ReadRegister(ctx, 1, cortexm.R0); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + if err := g.WriteRegister(ctx, 1, cortexm.R4, 0); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + if err := g.Step(ctx, 1); !errors.Is(err, context.Canceled) || m.reads != reads || m.writes != writes { + t.Fatalf("step=%v reads/writes=%d/%d", err, m.reads, m.writes) + } + if _, err := g.Release(t.Context()); err != nil { + t.Fatal(err) + } +} diff --git a/target/cortexm/group_integration_test.go b/target/cortexm/group_integration_test.go new file mode 100644 index 0000000..a145855 --- /dev/null +++ b/target/cortexm/group_integration_test.go @@ -0,0 +1,138 @@ +//go:build integration + +package cortexm_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/jon/ostiole/armdebug" + "github.com/jon/ostiole/dap" + "github.com/jon/ostiole/target/cortexm" +) + +func TestHILRP2350Group(t *testing.T) { + if os.Getenv("OSTIOLE_RP2350_HIL_GROUP") != "1" { + t.Skip("require OSTIOLE_RP2350_HIL_GROUP=1") + } + if os.Getenv("OSTIOLE_RP2350_HIL_PROGRAM") != dualCounterProgram { + t.Fatal("require the documented two-core counter binary identity") + } + for range 2 { + if !t.Run("session", groupHIL) { + return + } + } +} + +func groupHIL(t *testing.T) { + t.Helper() + ctx, cancel := context.WithTimeout(t.Context(), 90*time.Second) + defer cancel() + bench := controlBench{name: "RP2350 group", provider: "jlink", serial: "000802011345"} + c := bench.open(t, ctx) + var cores [2]dualCoreBench + var g *cortexm.Group + t.Cleanup(func() { releaseGroupBench(t, g, c) }) + var members []cortexm.Member + for i, base := range []uint64{0x2000, 0x4000} { + ap, err := dap.APAt(base) + if err != nil { + t.Fatal(err) + } + cores[i].memory, err = c.OpenMemAP(ctx, ap) + if err != nil { + t.Fatal(err) + } + checkDualCoreBench(t, ctx, &cores[i], i) + members = append(members, cortexm.Member{ID: cortexm.CoreID(i + 1), Memory: cores[i].memory}) + } + checkDualCounters(t, ctx, &cores, "before group acquisition", [2]bool{}) + var err error + g, err = cortexm.AcquireGroup(ctx, members) + if err != nil { + t.Fatal(err) + } + exerciseGroupHIL(t, ctx, g, &cores) + if results, err := g.Release(ctx); err != nil { + t.Fatalf("group release=%+v: %v", results, err) + } + for i := range cores { + checkDualCoreRestored(t, ctx, &cores[i], i) + } + checkDualCounters(t, ctx, &cores, "group released", [2]bool{}) +} + +func exerciseGroupHIL(t *testing.T, ctx context.Context, g *cortexm.Group, cores *[2]dualCoreBench) { + t.Helper() + for i := range cores { + id := cortexm.CoreID(i + 1) + if results, err := g.Halt(ctx, id); err != nil || len(results) != 1 || !results[0].HaltOwned || results[0].State != cortexm.Halted { + t.Fatalf("group halt=%+v: %v", results, err) + } + stopped := [2]bool{} + stopped[i] = true + view := groupViewHIL{group: g, id: id} + checkDualCounters(t, ctx, cores, "selected core halted", stopped) + readRegistersHIL(t, ctx, view) + checkCounterStepAtHIL(t, ctx, view, cores[i].memory, i, uint32(0x20040026+i*0x40), uint32(0x20040000+i*4)) + checkDualCounters(t, ctx, cores, "selected core stepped", stopped) + if results, err := g.Resume(ctx, id); err != nil || results[0].Skipped || results[0].State != cortexm.Running { + t.Fatalf("group resume=%+v: %v", results, err) + } + checkDualCounters(t, ctx, cores, "selected core resumed", [2]bool{}) + } + results, err := g.Halt(ctx, 2, 1) + if err != nil || len(results) != 2 || results[0].ID != 1 || results[1].ID != 2 { + t.Fatalf("group halt=%+v: %v", results, err) + } + checkDualCounters(t, ctx, cores, "group halted sequentially", [2]bool{true, true}) + checkCounterStepAtHIL(t, ctx, groupViewHIL{group: g, id: 1}, cores[0].memory, 2, 0x20040026, 0x20040000) + checkDualCounters(t, ctx, cores, "core 0 stepped with peer halted", [2]bool{true, true}) + if results, err := g.Resume(ctx, 1); err != nil || results[0].Skipped { + t.Fatalf("selective resume=%+v: %v", results, err) + } + checkDualCounters(t, ctx, cores, "only core 0 resumed", [2]bool{false, true}) +} + +type groupViewHIL struct { + group *cortexm.Group + id cortexm.CoreID +} + +func (v groupViewHIL) ReadRegister(ctx context.Context, reg cortexm.Register) (uint32, error) { + return v.group.ReadRegister(ctx, v.id, reg) +} + +func (v groupViewHIL) Step(ctx context.Context) error { return v.group.Step(ctx, v.id) } + +func (v groupViewHIL) Halted(ctx context.Context) (bool, error) { + results, err := v.group.Status(ctx, v.id) + return err == nil && results[0].State == cortexm.Halted, err +} + +func releaseGroupBench(t *testing.T, g *cortexm.Group, c *armdebug.Conn) { + t.Helper() + var err error + for range 3 { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + _, err = g.Release(ctx) + cancel() + if err == nil { + break + } + } + if err != nil { + t.Errorf("group cleanup pending; retaining Arm owner: %v", err) + return + } + for range 3 { + if err = c.Close(); err == nil { + t.Log("group released and shared Arm debug owner closed") + return + } + } + t.Errorf("shared Arm debug owner cleanup remains pending: %v", err) +} diff --git a/target/cortexm/register_integration_test.go b/target/cortexm/register_integration_test.go index d5266cc..15d9934 100644 --- a/target/cortexm/register_integration_test.go +++ b/target/cortexm/register_integration_test.go @@ -112,7 +112,11 @@ func checkRegisterFirmware(t *testing.T, ctx context.Context, memory *dap.MemAP) } } -func readRegisterHIL(t *testing.T, ctx context.Context, core *cortexm.Target, reg cortexm.Register) uint32 { +type registerReaderHIL interface { + ReadRegister(context.Context, cortexm.Register) (uint32, error) +} + +func readRegisterHIL(t *testing.T, ctx context.Context, core registerReaderHIL, reg cortexm.Register) uint32 { t.Helper() value, err := core.ReadRegister(ctx, reg) if err != nil { @@ -121,7 +125,7 @@ func readRegisterHIL(t *testing.T, ctx context.Context, core *cortexm.Target, re return value } -func readRegistersHIL(t *testing.T, ctx context.Context, core *cortexm.Target) map[cortexm.Register]uint32 { +func readRegistersHIL(t *testing.T, ctx context.Context, core registerReaderHIL) map[cortexm.Register]uint32 { t.Helper() saved := make(map[cortexm.Register]uint32) for _, reg := range []cortexm.Register{ diff --git a/target/cortexm/step_integration_test.go b/target/cortexm/step_integration_test.go index 43ea08c..a719b30 100644 --- a/target/cortexm/step_integration_test.go +++ b/target/cortexm/step_integration_test.go @@ -91,7 +91,13 @@ func checkCounterStepHIL(t *testing.T, ctx context.Context, core *cortexm.Target checkCounterStepAtHIL(t, ctx, core, memory, n, 0xc6, 0x20000000) } -func checkCounterStepAtHIL(t *testing.T, ctx context.Context, core *cortexm.Target, memory *dap.MemAP, n int, start, addr uint32) { +type stepCoreHIL interface { + registerReaderHIL + Step(context.Context) error + Halted(context.Context) (bool, error) +} + +func checkCounterStepAtHIL(t *testing.T, ctx context.Context, core stepCoreHIL, memory *dap.MemAP, n int, start, addr uint32) { t.Helper() pc := readRegisterHIL(t, ctx, core, cortexm.PC) r0 := readRegisterHIL(t, ctx, core, cortexm.R0)