From 08c7d9d7352cf2b74c68f5d46c134a0e77a39ca5 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Tue, 29 Sep 2026 08:29:00 -0700 Subject: [PATCH] Support Cortex-M33 architectural stepping. M33 halt/resume and register access were available, but stepping still rejected the architecture. Extend the existing step lifecycle with M33 permission, snap-stall, and restart checks while preserving competing stop evidence and caller deadlines. A step itself sets restart status. Accept it while execution is pending, but prevent automatic cleanup if restart status appears again after observing completion. Recheck the halt before clearing stepping so cleanup cannot claim a later independent stop. Never replay an uncertain launch. Two fresh RP2350 core-0 sessions verified PC, R0, and RAM across thirteen steps each, then restored inherited debug state through resume/release. --- README.md | 4 +- docs/README.md | 4 +- docs/architecture.md | 3 +- docs/capabilities.md | 9 +- docs/composition.md | 2 +- docs/cortexm.md | 64 ++++- target/cortexm/architecture.go | 10 - target/cortexm/control.go | 1 - target/cortexm/identity.go | 2 +- target/cortexm/m33_step_integration_test.go | 100 +++++++ target/cortexm/m33_step_test.go | 273 ++++++++++++++++++++ target/cortexm/m33_test.go | 24 -- target/cortexm/step.go | 47 +++- target/cortexm/step_integration_test.go | 21 +- target/cortexm/step_memory_test.go | 3 + 15 files changed, 489 insertions(+), 78 deletions(-) create mode 100644 target/cortexm/m33_step_integration_test.go create mode 100644 target/cortexm/m33_step_test.go diff --git a/README.md b/README.md index 135e285..257b772 100644 --- a/README.md +++ b/README.md @@ -70,8 +70,8 @@ posted access-port reads and a Cortex-M identity read through a MEM-AP. They compose the public packages explicitly without duplicating their framing. The `target/cortexm` package reads and decodes the architectural CPUID value through any compatible target-word reader. It also provides acquired Cortex-M0 and -Cortex-M33 halt/resume control and halted register access over word memory, plus -Cortex-M0 stepping; see [Cortex-M control](docs/cortexm.md). +Cortex-M33 halt/resume control, halted register access, and architectural +stepping over word memory; see [Cortex-M control](docs/cortexm.md). The FTDI path uses the standard H-series MPSSE port and endpoint layout. Descriptor-driven FTDI port binding is not implemented yet. J-Link instead diff --git a/docs/README.md b/docs/README.md index 9ccea61..fabfbe0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,8 +19,8 @@ and how to assemble them without duplicating lower-level behavior. - [CoreSight component inspection](coresight.md) describes identification registers, ROM entry decoding, bounded traversal, and the inspection example. - [Cortex-M control](cortexm.md) describes Cortex-M0/M33 acquisition, - halt/resume, register access, restoration, and the explicitly gated control - example. + halt/resume, register access, stepping, restoration, and the explicitly gated + control 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 7c15219..f022fb7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -335,7 +335,8 @@ before the memory owner. Register writes persist after release. It does not know about USB, adapters, or wire protocols. See [Cortex-M control](cortexm.md) for restoration and failure boundaries. Cortex-M33 control requires Secure invasive debug permission. Its stack register selectors use the halted security state; -register access does not change security state. Stepping remains Cortex-M0-only. +register access does not change security state. Both architectures support +stepping from an owned halt. ## Host implementations diff --git a/docs/capabilities.md b/docs/capabilities.md index 68d9114..62b1cc0 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -296,8 +296,9 @@ skips have hardware-independent test coverage. | 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; stepping remains M0-only. | +| 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. | | Reset | No | No architectural or pin-reset operation exists. | @@ -305,9 +306,9 @@ skips have hardware-independent test coverage. | 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 and excludes stepping. -Register access uses the halted security state. See -[Cortex-M control](cortexm.md) for its effects and cleanup limits. +Cortex-M33. M33 requires Secure invasive debug permission. Register access uses +the halted security state. See [Cortex-M control](cortexm.md) for its effects +and cleanup limits. ## Executable surfaces diff --git a/docs/composition.md b/docs/composition.md index 4cc0c49..2a8139d 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -37,7 +37,7 @@ data-register write can write target memory. | 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 from an owned halt | `Target.Step` | `examples/simple/cortexm-control -step` | +| Step a Cortex-M0/M33 from an owned halt | `Target.Step` | `examples/simple/cortexm-control -step` | | 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 120fb55..907ca42 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -86,9 +86,8 @@ can safely resume; clearing the bit is insufficient. Once observed, the target will not automatically resume the processor. Reset and recovery from that state remain outside this API. -`Acquire`, `Halt`, `Halted`, `Resume`, `Release`, `ReadRegister`, and -`WriteRegister` support Cortex-M33. `Step` rejects an acquired M33 before -further memory traffic, leaving its other operations available. +`Acquire`, `Halt`, `Halted`, `Resume`, `Release`, `ReadRegister`, +`WriteRegister`, and `Step` support Cortex-M0 and Cortex-M33. On RP2350, core 0 uses the ADIv6 MEM-AP at `0x2000`. Select it through the existing Arm debug owner, then use the target composition below: @@ -113,11 +112,11 @@ the [RP2350 datasheet][rp2350]. ## Stepping -`Step(ctx)` performs one Cortex-M0 architectural step from a halt owned by the -target. It returns halted with stepping disabled, retaining ownership for -another step, register access, or resume. It rejects a running processor or an -inherited halt, settles any pending register transfer before launch, and uses -the caller's context for cancellation and deadlines. +`Step(ctx)` performs one Cortex-M0 or Cortex-M33 architectural step from a halt +owned by the target. It returns halted with stepping disabled, retaining +ownership for another step, register access, or resume. It rejects a running +processor or an inherited halt, settles any pending register transfer before +launch, and uses the caller's context for cancellation and deadlines. ```go if err := core.Step(ctx); err != nil { @@ -141,14 +140,20 @@ before clearing C_STEP; it does not change stepping control while running. A failed write to clear C_STEP can be retried without restarting execution. An unconfirmed launch, ignored step request, reset, changed debug control, or loss of the completed halt can prevent automatic cleanup. A competing stop can -prevent restoring initially disabled debug until the processor runs again. -Retain both owners when release fails; this package provides no forced cleanup -operation. +prevent restoring initially disabled debug until the processor runs again. On +M33, the step's own restart is expected while waiting for completion. After +observing the completed halt, any further restart prevents automatic cleanup, +even if the core has already halted again. Permission and snap-stall checks +apply throughout; before clearing C_STEP, the target checks that the completed +halt is still present. Retain both owners when release fails; this package +provides no forced cleanup operation. Instructions, exception entry, elapsed time, and peripheral effects cannot be undone. Behavioral tests cover immediate and delayed completion, competing flags, cancellation, ignored writes, partial failures, and cleanup retries. The -[step bench](#step-bench) records physical instruction checks. +[micro:bit step bench](#step-bench) and [RP2350 step bench](#rp2350-step-bench) +record physical instruction checks. M33 step semantics follow Arm DDI 0553B.y +B13.4.2 and D1.2.38–D1.2.39. ## Register reads @@ -435,3 +440,38 @@ coverage. No security-state switch or core-1 control was performed. Temporary stack and PC values were not executed. State after Arm owner close was not independently measured. The previously loaded RAM program remains running; flash was untouched. + +## RP2350 step bench + +After preparing the +[core-0 RAM counter](../target/cortexm/testdata/rp2350-counter/README.md), run +the separately gated step test: + +```sh +OSTIOLE_RP2350_HIL_CONTROL=1 \ +OSTIOLE_RP2350_HIL_STEP=1 \ +OSTIOLE_RP2350_HIL_PROGRAM=c20737e61153b272322548e8e6db5c420f0c148d6707ca4412c309f70415065a \ +go test -tags integration ./target/cortexm -run '^TestHILRP2350Step$' -count=1 -v +``` + +The test checks CPUID and counter instructions before acquisition and refuses an +inherited halt. After halting, it checks Secure state, Thread mode, Thumb state, +and R1's counter address. It compares PC, R0, and RAM after each step through +the increment at `0x20040026`, store at `0x20040028`, and branch at +`0x2004002a`. Twelve steps precede resume; a further halt and step exercise +release from an owned stop. DSCSR is compared across the first twelve steps. The +test does not reload firmware or roll back execution. + +On Nostalgia, two fresh sessions through J-Link EDU Mini V2 `000802011345` at 1 +MHz and AP `0x2000` passed on RP2350 core 0, CPUID `0x411fd210`. Each checked 13 +steps, with PC/R0/RAM matching the expected instruction effects. The counter +stayed unchanged while halted and advanced after resume and release. DSCSR +remained `0x00030000`. Initially disabled debug and running state were restored +before Arm owner close; target release and owner close succeeded. + +The RAM program disables configurable interrupts and runs in Secure state. +Exception entry, competing debug events, permission loss, snap-stall, restart +after a completed halt, and failure cleanup have behavioral coverage. These +sessions do not establish sleeping-instruction behavior, Non-secure execution, +core-1 control, or cross-core coordination. State after Arm owner close was not +independently measured. Flash was untouched and the counter remains running. diff --git a/target/cortexm/architecture.go b/target/cortexm/architecture.go index fb9fec8..82e47c2 100644 --- a/target/cortexm/architecture.go +++ b/target/cortexm/architecture.go @@ -47,13 +47,3 @@ func (t *Target) readDHCSR(ctx context.Context) (uint32, error) { } return value, err } - -func (t *Target) activeM0(ctx context.Context) error { - if err := t.active(ctx); err != nil { - return err - } - if t.identity.Part != 0xc20 { - return errors.New("cortexm: stepping requires Cortex-M0") - } - return nil -} diff --git a/target/cortexm/control.go b/target/cortexm/control.go index c916230..180d6e5 100644 --- a/target/cortexm/control.go +++ b/target/cortexm/control.go @@ -53,7 +53,6 @@ type Target struct { // Cortex-M33 requires Secure invasive debug permission (S_SDE) and rejects // snap-stall state. It does not change authentication or security settings. // DHCSR reads consume sticky reset, retirement, and Cortex-M33 restart status. -// Stepping currently requires Cortex-M0. // // The caller controls cancellation and deadlines. Failed setup attempts // restoration with an independent five-second context. A non-nil target diff --git a/target/cortexm/identity.go b/target/cortexm/identity.go index d171be0..f4a125f 100644 --- a/target/cortexm/identity.go +++ b/target/cortexm/identity.go @@ -1,6 +1,6 @@ // Package cortexm identifies Cortex-M processors and provides Cortex-M0 and // Cortex-M33 halting debug and register access through target memory. -// Stepping currently requires Cortex-M0. +// Both support architectural stepping from an owned halt. package cortexm import ( diff --git a/target/cortexm/m33_step_integration_test.go b/target/cortexm/m33_step_integration_test.go new file mode 100644 index 0000000..3246888 --- /dev/null +++ b/target/cortexm/m33_step_integration_test.go @@ -0,0 +1,100 @@ +//go:build integration + +package cortexm_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/jon/ostiole/dap" + "github.com/jon/ostiole/target/cortexm" +) + +func TestHILRP2350Step(t *testing.T) { + if os.Getenv("OSTIOLE_RP2350_HIL_STEP") != "1" || os.Getenv("OSTIOLE_RP2350_HIL_CONTROL") != "1" { + t.Skip("require OSTIOLE_RP2350_HIL_STEP=1 and OSTIOLE_RP2350_HIL_CONTROL=1") + } + if os.Getenv("OSTIOLE_RP2350_HIL_PROGRAM") != "c20737e61153b272322548e8e6db5c420f0c148d6707ca4412c309f70415065a" { + t.Fatal("require the documented RP2350 counter binary identity") + } + for range 2 { + if !t.Run("session", m33StepHIL) { + return + } + } +} + +func m33StepHIL(t *testing.T) { + t.Helper() + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) + defer cancel() + ap, err := dap.APAt(0x2000) + if err != nil { + t.Fatal(err) + } + bench := controlBench{name: "RP2350 core 0", provider: "jlink", serial: "000802011345", ap: ap, cpuid: 0x411fd210} + c := bench.open(t, ctx) + var core *cortexm.Target + t.Cleanup(func() { releaseControlBench(t, core, c) }) + memory, err := c.OpenMemAP(ctx, ap) + if err != nil { + t.Fatal(err) + } + before := checkM33RegisterBench(t, ctx, memory) + checkCounterHIL(t, ctx, memory, 0x20040000, "before acquisition", false) + core, err = cortexm.Acquire(ctx, memory) + if err != nil { + t.Fatal(err) + } + if err := core.Halt(ctx); err != nil { + t.Fatal(err) + } + exerciseM33StepsHIL(t, ctx, core, memory) + checkCounterHIL(t, ctx, memory, 0x20040000, "after steps, halted", true) + if err := core.Resume(ctx); err != nil { + t.Fatal(err) + } + checkCounterHIL(t, ctx, memory, 0x20040000, "resumed", false) + if err := core.Halt(ctx); err != nil { + t.Fatal(err) + } + checkCounterStepAtHIL(t, ctx, core, memory, 12, 0x20040026, 0x20040000) + if err := core.Release(ctx); err != nil { + t.Fatal(err) + } + after, err := memory.ReadWord(ctx, dhcsr) + mask := debugEnable | haltStatus + if before&debugEnable != 0 { + mask |= 12 + } + if err != nil || after&mask != before&mask { + t.Fatalf("DHCSR before=%#x after=%#x: %v", before, after, err) + } + checkCounterHIL(t, ctx, memory, 0x20040000, "released after another step", false) + t.Logf("J-Link %s 1 MHz AP 0x2000 CPUID=%#x DHCSR before=%#x after=%#x", bench.serial, core.Identity().Raw, before, after) +} + +func exerciseM33StepsHIL(t *testing.T, ctx context.Context, core *cortexm.Target, memory *dap.MemAP) { + t.Helper() + const dscsr = uint32(0xe000ee08) + domain, err := memory.ReadWord(ctx, dscsr) + if err != nil || domain&(1<<16) == 0 { + t.Fatalf("require Secure counter state: DSCSR=%#x: %v", domain, err) + } + if r1 := readRegisterHIL(t, ctx, core, cortexm.R1); r1 != 0x20040000 { + t.Fatalf("counter address in R1=%#x", r1) + } + if xpsr := readRegisterHIL(t, ctx, core, cortexm.XPSR); xpsr&0x010001ff != 0x01000000 { + t.Fatalf("counter state XPSR=%#x", xpsr) + } + for n := range 12 { + checkCounterStepAtHIL(t, ctx, core, memory, n, 0x20040026, 0x20040000) + } + after, err := memory.ReadWord(ctx, dscsr) + if err != nil || after != domain { + t.Fatalf("DSCSR before=%#x after=%#x: %v", domain, after, err) + } + t.Logf("DSCSR preserved at %#x", domain) +} diff --git a/target/cortexm/m33_step_test.go b/target/cortexm/m33_step_test.go new file mode 100644 index 0000000..5d5459b --- /dev/null +++ b/target/cortexm/m33_step_test.go @@ -0,0 +1,273 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/jon/ostiole/target/cortexm" +) + +func newM33StepMemory() *stepMemory { + m := newStepMemory() + m.cpuid, m.status = 0x411fd210, secureDebug + return m +} + +func TestM33StepReturnsOwnedHalt(t *testing.T) { + for _, delay := range []int{0, 2} { + m := newM33StepMemory() + m.stepDelay = delay + core := acquireStep(t, m) + for range 3 { + if err := core.Step(t.Context()); err != nil { + t.Fatal(err) + } + if _, err := core.ReadRegister(t.Context(), cortexm.PC); err != nil { + t.Fatal(err) + } + } + if m.steps != 3 || m.launches != 3 || m.control != debugEnable|haltRequest || !m.halted { + t.Fatal("step did not return normalized owned halt") + } + if err := core.Resume(t.Context()); err != nil { + t.Fatal(err) + } + if err := core.Release(t.Context()); err != nil || m.control != 0 || m.halted { + t.Fatal("restoration failed", err) + } + } +} + +func TestM33StepRejectsUnownedOrUnsafeLaunch(t *testing.T) { + for _, change := range []string{"inherited", "restart", "permission", "snap", "event"} { + t.Run(change, func(t *testing.T) { + m := newM33StepMemory() + var core *cortexm.Target + if change == "inherited" { + m.control, m.halted = debugEnable|haltRequest, true + var err error + core, err = cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + } else { + core = acquireStep(t, m) + } + switch change { + case "restart": + m.status |= 1 << 26 + case "permission": + m.status = 0 + case "snap": + m.control |= 32 + case "event": + m.reasons |= 16 + } + writes := m.writes + if err := core.Step(t.Context()); err == nil || m.writes != writes || m.launches != 0 { + t.Fatal("unsafe step launched") + } + }) + } +} + +func TestM33StepCompletionRejectsPermissionAndSnap(t *testing.T) { + for _, change := range []string{"permission", "snap", "snap and mask"} { + t.Run(change, func(t *testing.T) { + m := newM33StepMemory() + core := acquireStep(t, m) + m.onStep = func() { + switch change { + case "permission": + m.status &^= secureDebug + case "snap": + m.control |= 32 + case "snap and mask": + m.control |= 32 | 8 + } + } + writes := m.writes + if err := core.Step(t.Context()); err == nil || m.writes != writes+1 { + t.Fatal("unsafe completion normalized", err) + } + writes = m.writes + if err := core.Release(t.Context()); err == nil || m.writes != writes { + t.Fatal("unsafe cleanup wrote control", err) + } + m.control &^= 32 | 8 + m.status = secureDebug + m.onStep = nil + err := core.Release(t.Context()) + if change == "permission" { + if err != nil || m.halted { + t.Fatal("permission recovery failed", err) + } + } else if err == nil || m.writes != writes || !m.halted { + t.Fatal("snap clearing allowed cleanup", err) + } + }) + } +} + +func TestM33StepRestartAfterCompletionBlocksCleanup(t *testing.T) { + for _, phase := range []string{"reason", "normalization", "during reason"} { + t.Run(phase, func(t *testing.T) { + m := newM33StepMemory() + core := acquireStep(t, m) + switch phase { + case "reason": + m.failRead = m.reads + 4 + case "normalization": + m.failWrite = m.writes + 2 + default: + n := m.reads + 3 + m.onRead = func() { + if m.reads == n { + m.status |= 1 << 26 + } + } + } + if err := core.Step(t.Context()); err == nil { + t.Fatal("completion failure ignored") + } + if m.launches != 1 || m.steps != 1 { + t.Fatal("step did not execute") + } + writes := m.writes + m.status |= 1 << 26 + m.onRead = nil + if err := core.Release(t.Context()); err == nil || m.writes != writes || !m.halted { + t.Fatal("new halt mistaken for completed step") + } + }) + } +} + +func TestM33StepCancellationSettlesWithoutReplay(t *testing.T) { + m := newM33StepMemory() + core := acquireStep(t, m) + m.blockStep = true + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Millisecond) + err := core.Step(ctx) + cancel() + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatal(err) + } + writes := m.writes + ctx, cancel = context.WithTimeout(t.Context(), 5*time.Millisecond) + err = core.Release(ctx) + cancel() + if !errors.Is(err, context.DeadlineExceeded) || m.writes != writes { + t.Fatal("cleanup changed running step") + } + m.blockStep = false + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.steps != 1 || m.launches != 1 || m.halted || m.control != 0 { + t.Fatal("cleanup replayed step or lost restoration") + } +} + +func TestM33StepPreservesCompetingEvents(t *testing.T) { + for _, before := range []bool{false, true} { + for _, reason := range []uint32{2, 4, 8, 16} { + m := newM33StepMemory() + m.control = debugEnable + core := acquireStep(t, m) + if before { + m.reasons |= reason + } else { + m.onStep = func() { m.reasons |= reason } + } + writes := m.writes + if err := core.Step(t.Context()); err == nil { + t.Fatal("competing event accepted") + } + if before { + if writes != m.writes || m.launches != 0 { + t.Fatal("launched with stale event") + } + } else { + writes = m.writes + if err := core.Resume(t.Context()); err == nil || m.writes != writes { + t.Fatal("competing halt resumed") + } + if err := core.Release(t.Context()); err != nil || !m.halted { + t.Fatal("competing halt not preserved", err) + } + } + if m.reasons&reason == 0 { + t.Fatal("cleared competing evidence") + } + } + } +} + +func TestM33StepUncertainLaunchNeverReplays(t *testing.T) { + for _, after := range []bool{false, true} { + m := newM33StepMemory() + core := acquireStep(t, m) + m.failWrite, m.afterWrite = m.writes+1, after + if err := core.Step(t.Context()); !errors.Is(err, errMemory) { + t.Fatal(err) + } + writes := m.writes + if err := core.Release(t.Context()); err == nil || m.writes != writes { + t.Fatal("uncertain launch resumed or replayed") + } + } +} + +func TestM33StepFailedNormalizationRetainsOwnership(t *testing.T) { + for _, after := range []bool{false, true} { + m := newM33StepMemory() + core := acquireStep(t, m) + m.failWrite, m.afterWrite = m.writes+2, after + if err := core.Step(t.Context()); !errors.Is(err, errMemory) { + t.Fatal(err) + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.steps != 1 || m.launches != 1 || m.halted || m.control != 0 { + t.Fatal("normalization replayed execution or failed restoration") + } + } +} + +func TestM33StepPermissionFailureRetainsCompletionBoundary(t *testing.T) { + m := newM33StepMemory() + core := acquireStep(t, m) + m.onStep = func() { m.status &^= secureDebug } + if err := core.Step(t.Context()); err == nil { + t.Fatal("lost permission accepted") + } + if m.steps != 1 || m.launches != 1 { + t.Fatal("step did not complete") + } + writes := m.writes + m.onStep = nil + m.status = secureDebug | 1<<26 + if err := core.Release(t.Context()); err == nil || m.writes != writes || !m.halted { + t.Fatal("permission recovery claimed a later halt", err) + } +} + +func TestM33StepMayEnterException(t *testing.T) { + m := newM33StepMemory() + core := acquireStep(t, m) + m.onStep = func() { m.registers[15] = 0x100; m.registers[16] = 0x0100000f } + if err := core.Step(t.Context()); err != nil { + t.Fatal(err) + } + pc, err := core.ReadRegister(t.Context(), cortexm.PC) + if err != nil || pc != 0x100 { + t.Fatal("exception entry rejected", err) + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } +} diff --git a/target/cortexm/m33_test.go b/target/cortexm/m33_test.go index 8ef5387..e7a7367 100644 --- a/target/cortexm/m33_test.go +++ b/target/cortexm/m33_test.go @@ -130,30 +130,6 @@ func TestM33RejectsControlChangesBeforeResume(t *testing.T) { } } -func TestM33DefersSteppingWithoutTraffic(t *testing.T) { - m := newM33Memory() - core, err := cortexm.Acquire(t.Context(), m) - if err != nil { - t.Fatal(err) - } - if err := core.Halt(t.Context()); err != nil { - t.Fatal(err) - } - reads, writes := m.reads, m.writes - if err := core.Step(t.Context()); err == nil { - t.Fatal("step accepted") - } - if m.reads != reads || m.writes != writes { - t.Fatal("unsupported operation reached memory") - } - if err := core.Resume(t.Context()); err != nil { - t.Fatal(err) - } - if err := core.Release(t.Context()); err != nil { - t.Fatal(err) - } -} - func TestM33HaltHonorsCancellationAndAllowsCleanup(t *testing.T) { m := newM33Memory() core, err := cortexm.Acquire(t.Context(), m) diff --git a/target/cortexm/step.go b/target/cortexm/step.go index 8688db4..0bf3eb9 100644 --- a/target/cortexm/step.go +++ b/target/cortexm/step.go @@ -18,9 +18,10 @@ const ( dfsrAddress = uint32(0xe000ed30) ) -// Step performs one Cortex-M0 architectural step from a halt owned by this -// target, then returns halted with stepping disabled. Exceptions can be taken and debug -// events can interrupt a step; success does not promise instruction retirement. +// Step performs one Cortex-M0 or Cortex-M33 architectural step from a halt +// owned by this target, then returns halted with stepping disabled. +// Exceptions can be taken and debug events can interrupt a step; success +// does not promise instruction retirement. // Interrupt masking is unchanged. Existing competing DFSR event flags prevent // stepping, and none of its flags are cleared. // @@ -28,9 +29,11 @@ const ( // any failure leaves only Release available. Release never repeats a step and // only clears stepping while halted. Unconfirmed launch, reset, or lost debug // control can prevent automatic cleanup. A competing halt is not owned -// and can prevent restoring initially disabled debug. Execution is not undone. +// and can prevent restoring initially disabled debug. On Cortex-M33, restart +// after observing the completed halt prevents automatic cleanup, even if the +// processor has already halted again. Execution is not undone. func (t *Target) Step(ctx context.Context) error { - if err := t.activeM0(ctx); err != nil { + if err := t.active(ctx); err != nil { return err } if !t.haltOwned { @@ -88,9 +91,6 @@ func (t *Target) settleStep(ctx context.Context) error { return nil } if value&(cHalt|sHalt) == cHalt|sHalt { - if t.step == stepRunning { - t.step = stepStopped - } if err := t.finishStep(ctx); err != nil { return err } @@ -109,22 +109,40 @@ func (t *Target) stepStatus(ctx context.Context) (uint32, error) { if t.step == stepUncertain || t.step == stepLost { return 0, errors.New("cortexm: step completion is unknown; cleanup cannot continue") } - value, err := t.memory.ReadWord(ctx, dhcsrAddress) + value, err := t.readDHCSR(ctx) if err != nil { return 0, err } + if err := t.observeStepState(value); err != nil { + return 0, err + } + if err := t.validateArchitectureControl(value); err != nil { + return 0, err + } + return value, nil +} + +func (t *Target) observeStepState(value uint32) error { invalid := value&cDebugEnable == 0 || value&(cMaskInts|sReset) != 0 if t.step != stepRestoring { invalid = invalid || value&cStep == 0 } if t.step == stepStopped || t.step == stepRestoring { - invalid = invalid || value&(cHalt|sHalt) != cHalt|sHalt + invalid = invalid || t.stepHaltLost(value) } if invalid { t.step = stepLost - return 0, errors.New("cortexm: debug control changed during step") + return errors.New("cortexm: debug control changed during step") } - return value, nil + if t.step == stepRunning && value&(cHalt|sHalt) == cHalt|sHalt { + t.step = stepStopped + } + return nil +} + +func (t *Target) stepHaltLost(value uint32) bool { + return value&(cHalt|sHalt) != cHalt|sHalt || + t.identity.Part == 0xd21 && value&sRestart != 0 } func (t *Target) finishStep(ctx context.Context) error { @@ -142,5 +160,10 @@ func (t *Target) finishStep(ctx context.Context) error { if err := ctx.Err(); err != nil { return err } + if t.identity.Part == 0xd21 { + if _, err := t.stepStatus(ctx); err != nil { + return err + } + } return t.memory.WriteWord(ctx, dhcsrAddress, debugKey|cDebugEnable|cHalt) } diff --git a/target/cortexm/step_integration_test.go b/target/cortexm/step_integration_test.go index 206e39e..43ea08c 100644 --- a/target/cortexm/step_integration_test.go +++ b/target/cortexm/step_integration_test.go @@ -87,23 +87,28 @@ func stepHIL(t *testing.T) { } func checkCounterStepHIL(t *testing.T, ctx context.Context, core *cortexm.Target, memory *dap.MemAP, n int) { + t.Helper() + 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) { t.Helper() pc := readRegisterHIL(t, ctx, core, cortexm.PC) r0 := readRegisterHIL(t, ctx, core, cortexm.R0) - counter, err := memory.ReadWord(ctx, 0x20000000) + counter, err := memory.ReadWord(ctx, addr) if err != nil { t.Fatal(err) } nextPC, nextR0, nextCounter := pc, r0, counter switch pc { - case 0xc6: - nextPC = 0xc8 + case start: + nextPC = start + 2 nextR0++ - case 0xc8: - nextPC = 0xca + case start + 2: + nextPC = start + 4 nextCounter = r0 - case 0xca: - nextPC = 0xc6 + case start + 4: + nextPC = start default: t.Fatalf("PC outside counter loop: %#x", pc) } @@ -112,7 +117,7 @@ func checkCounterStepHIL(t *testing.T, ctx context.Context, core *cortexm.Target } gotPC := readRegisterHIL(t, ctx, core, cortexm.PC) gotR0 := readRegisterHIL(t, ctx, core, cortexm.R0) - gotCounter, err := memory.ReadWord(ctx, 0x20000000) + gotCounter, err := memory.ReadWord(ctx, addr) if err != nil { t.Fatal(err) } diff --git a/target/cortexm/step_memory_test.go b/target/cortexm/step_memory_test.go index 2b4c899..0c75df0 100644 --- a/target/cortexm/step_memory_test.go +++ b/target/cortexm/step_memory_test.go @@ -52,6 +52,9 @@ func (m *stepMemory) WriteWord(ctx context.Context, addr, value uint32) error { if wasHalted && value&15 == debugEnable|stepRequest && m.writes != writes && m.control == debugEnable|stepRequest && !m.ignoreWrites { m.stepping, m.stepRemaining = true, m.stepDelay m.launches++ + if m.cpuid == 0x411fd210 { + m.status |= 1 << 26 + } if m.stepDelay == 0 && !m.blockStep { m.finishStep() }