diff --git a/README.md b/README.md index 8d4943d..35ad837 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ add 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 -halt/resume control and halted register access over word memory; see +halt/resume control, stepping, and halted register access over word memory; see [Cortex-M control](docs/cortexm.md). The FTDI path uses the standard H-series MPSSE port and endpoint layout. @@ -140,7 +140,8 @@ The `arm-info`, `coresight-info`, and `cortexm-control` examples accept The inspection examples and `ost` commands avoid reset, halt, target-memory writes, and persistent changes. The separately gated `cortexm-control` example enables halting debug, halts a Cortex-M0, reads PC, SP, R0, and R4, then resumes -it. The `dap.MemAP` API +it. Add `-step` to perform one architectural step before resume. The +`dap.MemAP` API does expose effectful scalar writes; callers choose the addresses and own the consequences. Establishing an ADIv5 connection also changes volatile debug-port control state; the connection releases its own power requests before return. diff --git a/docs/architecture.md b/docs/architecture.md index da98675..7d18883 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -337,8 +337,9 @@ component identity](coresight.md) for its register and failure boundaries. `target/cortexm` identifies processors through a word reader. Cortex-M0 control also requires a word writer that waits for each access to complete. -The target owns DHCSR control, its halt requests, and pending register -transfers. Release settles a pending transfer before restoring debug control; +The target owns DHCSR control, its halt requests, and pending register and +step operations. Stepping checks DFSR to preserve competing stops. Release +settles pending operations before restoring debug control; the target must be released before the memory owner. Register writes persist after release. It does not know about USB, adapters, or wire protocols. See @@ -386,7 +387,7 @@ replaceable while exercising the public protocol and DAP layers. The inspection examples and `ost` commands do not reset or halt the target, write target memory, or change persistent state. The explicitly gated `cortexm-control` example enables debug, halts Cortex-M0, reads PC, SP, R0, -and R4, then resumes it. +and R4, then resumes it. Its `-step` option steps once while halted. The `dap.MemAP` API does expose scalar and block target-memory writes; applications choose the affected addresses and own the consequences. diff --git a/docs/capabilities.md b/docs/capabilities.md index 8f437a4..520be90 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -303,7 +303,7 @@ layouts and power-domain 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). | -| Step | No | No single-step API exists. | +| 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). | | Register reads | Yes | Halted Cortex-M0 R0–R12, SP, LR, PC, XPSR, MSP, and PSP through `ReadRegister`. Two fresh CMSIS-DAP micro:bit sessions read all 19 registers; transfer failures and cleanup have behavioral coverage. | | Register writes | Yes | Halted Cortex-M0 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 micro:bit sessions wrote and restored R4, SP, MSP, PSP, and PC before resuming; see the [register bench](cortexm.md#register-bench). | | Reset | No | No architectural or pin-reset operation exists. | @@ -329,6 +329,7 @@ Available examples: `examples/simple/cortexm-control` separately demonstrates effectful Cortex-M0 halt/resume with PC, SP, R0, and R4 reads, and requires `-allow-control`. +Its optional `-step` performs one architectural step before resume. Available `ost` commands: diff --git a/docs/composition.md b/docs/composition.md index 5a5eeed..b8d4a0e 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -37,6 +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` | | Read or write a halted Cortex-M0 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 a36512a..5276acb 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -65,8 +65,46 @@ The package does not restore those indicators or clear DFSR event flags. The implementation follows Arm DDI 0419E, sections C1.5 and C1.6.3–C1.6.5 of the [Armv6-M Architecture Reference Manual](https://documentation-service.arm.com/static/5f8ff05ef86e16515cdbf826). -It does not implement reset, single-step, breakpoints, or -watchpoints. +It does not implement reset, breakpoints, or watchpoints. + +## Stepping + +`Step(ctx)` performs one 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 { + // Retain core and its memory owner for Release. + return err +} +pc, err := core.ReadRegister(ctx, cortexm.PC) +``` + +Stepping does not change interrupt masking. An architectural step can enter +an exception handler instead of retiring an instruction. A breakpoint, +watchpoint, vector catch, or external halt can also interrupt it. The target +checks DFSR before launch and rejects any existing flags for those events; +it preserves all DFSR flags. After launch, it requires a fresh halt with the +HALTED reason and no competing event before claiming that stop. A competing +stop returns an error and remains unowned. + +Once launch is attempted, any failure leaves only `Release` available. Release +never repeats the step. After a confirmed launch, it waits for a fresh halt +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. + +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. ## Register reads @@ -142,12 +180,12 @@ err = errors.Join(err, cleanupErr) The [control example](../examples/simple/cortexm-control/main.go) selects one probe and AP, halts, prints PC, SP, R0, and R4, resumes, then releases the -target before closing the -connection. It requires explicit consent to control execution: +target before closing the connection. With `-step`, it also steps once and +prints the resulting PC. It requires explicit consent to control execution: ```sh go run ./examples/simple/cortexm-control \ - -provider cmsisdap -serial SERIAL -ap 0 -clock 1000000 -allow-control + -provider cmsisdap -serial SERIAL -ap 0 -clock 1000000 -allow-control -step ``` Hardware-independent tests model DHCSR control and execution state, including @@ -241,3 +279,40 @@ the temporary PC or stack values. Writes to the other general registers and LR, process-stack selection, inherited halts, and failure cleanup have behavioral test coverage only. XPSR writes, stepping, reset, and state after Arm owner close were not tested. + +### Step bench + +`TestHILCortexM0Step` uses the same micro:bit and verified counter firmware, +with a separate gate for stepping: + +```sh +OSTIOLE_CORTEXM_HIL_CONTROL=1 \ +OSTIOLE_CORTEXM_HIL_STEP=1 \ +OSTIOLE_CORTEXM_HIL_PROGRAM=sha256:ee294cc06ab6e8228161b49506675b065c0148b26421cf1f83c8e45e35cd4e5d \ +go test -tags integration ./target/cortexm -run '^TestHILCortexM0Step$' -count=1 -v +``` + +On September 26, 2026, two fresh sessions passed on Nostalgia through +CMSIS-DAP, 1 MHz SWD, and AP0, with CPUID `0x410cc200`. Each checked twelve +consecutive steps through the counter loop: + +- At PC `0xc6`, `adds r0, #1` advanced PC to `0xc8` and incremented R0, + leaving RAM unchanged. +- At PC `0xc8`, `str r0, [r1]` advanced PC to `0xca` and copied R0 to the + counter at `0x20000000`, leaving R0 unchanged. +- At PC `0xca`, the branch returned PC to `0xc6`, leaving R0 and RAM unchanged. + +After every step, `Halted` confirmed Debug state with stepping and interrupt +masking disabled. The counter then remained unchanged across ten samples +20 milliseconds apart while halted, and advanced after resume. Each session +halted again, checked one further step, and released from that halt. The +counter advanced after release. DHCSR was `0x01000000` before acquisition and +after release in both sessions; initially disabled debug and running state +were restored. Both target releases and Arm owner closes completed. The +control example also completed a step with `-allow-control -step`. + +Stepping's register and memory effects were intentional and were not rolled +back. The firmware disables configurable interrupts, so these runs do not +establish exception entry, competing debug events, sleeping instructions, or +failure cleanup on hardware. Those control failures have behavioral coverage; +state after Arm owner close was not measured. diff --git a/examples/simple/cortexm-control/main.go b/examples/simple/cortexm-control/main.go index cb9089c..3c0395c 100644 --- a/examples/simple/cortexm-control/main.go +++ b/examples/simple/cortexm-control/main.go @@ -27,7 +27,8 @@ func run() error { provider := flag.String("provider", "", "required probe provider") serial := flag.String("serial", "", "required probe serial") ap := flag.Int("ap", -1, "required MEM-AP index (0..255)") - allow := flag.Bool("allow-control", false, "allow enabling debug, halting, and resuming the processor") + allow := flag.Bool("allow-control", false, "allow enabling debug, halting, stepping, and resuming the processor") + step := flag.Bool("step", false, "perform one architectural step while halted") clock := flag.Uint64("clock", 1_000_000, "maximum SWD clock in Hz") flag.Parse() if *clock < 1000 || *clock > 1<<32-1 { @@ -37,10 +38,10 @@ func run() error { return errors.New("require -allow-control, -provider, -serial, and -ap 0..255") } selection := discover.Selection{Provider: discover.ProviderID(*provider), Serial: *serial} - return runControl(selection, dap.NewAPSel(uint8(*ap)), uint32(*clock)) + return runControl(selection, dap.NewAPSel(uint8(*ap)), uint32(*clock), *step) } -func runControl(selection discover.Selection, ap dap.APSel, clock uint32) (err error) { +func runControl(selection discover.Selection, ap dap.APSel, clock uint32, step bool) (err error) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() c, err := armdebug.Open(ctx, selection, armdebug.Config{ @@ -61,10 +62,10 @@ func runControl(selection discover.Selection, ap dap.APSel, clock uint32) (err e if err != nil { return err } - return control(ctx, core) + return control(ctx, core, step) } -func control(ctx context.Context, core *cortexm.Target) error { +func control(ctx context.Context, core *cortexm.Target, step bool) error { if err := core.Halt(ctx); err != nil { return err } @@ -79,6 +80,16 @@ func control(ctx context.Context, core *cortexm.Target) error { } fmt.Printf("%s=%#08x\n", reg.name, value) } + if step { + if err := core.Step(ctx); err != nil { + return err + } + pc, err := core.ReadRegister(ctx, cortexm.PC) + if err != nil { + return err + } + fmt.Printf("stepped; PC=%#08x\n", pc) + } if err := core.Resume(ctx); err != nil { return err } diff --git a/target/cortexm/control.go b/target/cortexm/control.go index 644c807..92fc1c1 100644 --- a/target/cortexm/control.go +++ b/target/cortexm/control.go @@ -42,6 +42,7 @@ type Target struct { resumeUncertain bool registerPending bool registerLost bool + step stepPhase } // Acquire enables Cortex-M0 halting debug without requesting a halt. It reads @@ -119,7 +120,9 @@ func (t *Target) Identity() Identity { // a completed resume. An unconfirmed control change, or a new halt while // restoring disabled debug, can prevent cleanup until execution resumes. // Pending register transfers must settle first. Reset or loss of Debug state -// during a transfer prevents automatic cleanup. +// during a transfer prevents automatic cleanup. An accepted step must return +// halted before stepping can be disabled; an unconfirmed step launch prevents +// automatic cleanup. A competing debug event leaves its halt unowned. func (t *Target) Release(ctx context.Context) error { if t == nil || t.memory == nil { return nil @@ -133,6 +136,11 @@ func (t *Target) Release(ctx context.Context) error { return err } } + if t.step != stepIdle { + if err := t.settleStep(ctx); err != nil { + return err + } + } if t.changed { if err := t.restore(ctx); err != nil { return fmt.Errorf("cortexm: restore debug control: %w", err) diff --git a/target/cortexm/identity.go b/target/cortexm/identity.go index aa4e021..0b3ed20 100644 --- a/target/cortexm/identity.go +++ b/target/cortexm/identity.go @@ -1,5 +1,5 @@ // Package cortexm identifies Cortex-M processors and provides Cortex-M0 -// halting debug and register access through target memory. +// halting debug, stepping, and register access through target memory. package cortexm import ( diff --git a/target/cortexm/step.go b/target/cortexm/step.go new file mode 100644 index 0000000..ebfdb13 --- /dev/null +++ b/target/cortexm/step.go @@ -0,0 +1,146 @@ +package cortexm + +import ( + "context" + "errors" + "time" +) + +type stepPhase uint8 + +const ( + stepIdle stepPhase = iota + stepUncertain + stepRunning + stepStopped + stepRestoring + stepLost + dfsrAddress = uint32(0xe000ed30) +) + +// Step performs one 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. +// +// The caller controls cancellation and deadlines. Once launch is attempted, +// 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. +func (t *Target) Step(ctx context.Context) error { + if err := t.active(ctx); err != nil { + return err + } + if !t.haltOwned { + return errors.New("cortexm: no owned halt to step") + } + if err := t.waitRegister(ctx); err != nil { + return err + } + if !t.haltOwned { + return errors.New("cortexm: halt ownership was lost") + } + if err := ctx.Err(); err != nil { + return err + } + reason, err := t.memory.ReadWord(ctx, dfsrAddress) + if err != nil { + t.closing = true + return err + } + if reason&0x1e != 0 { + return errors.New("cortexm: competing debug event flags prevent stepping") + } + if err := ctx.Err(); err != nil { + return err + } + t.step = stepUncertain + t.haltOwned = false + if err := t.memory.WriteWord(ctx, dhcsrAddress, debugKey|cDebugEnable|cStep); err != nil { + t.closing = true + return err + } + t.step = stepRunning + if err := t.settleStep(ctx); err != nil { + t.closing = true + return err + } + if !t.haltOwned { + t.closing = true + return errors.New("cortexm: step stopped on an unowned debug event") + } + return nil +} + +func (t *Target) settleStep(ctx context.Context) error { + for { + if err := ctx.Err(); err != nil { + return err + } + value, err := t.stepStatus(ctx) + if err != nil { + return err + } + if t.step == stepRestoring && value&15 == cDebugEnable|cHalt && value&sHalt != 0 { + t.step = stepIdle + return nil + } + if value&(cHalt|sHalt) == cHalt|sHalt { + if t.step == stepRunning { + t.step = stepStopped + } + if err := t.finishStep(ctx); err != nil { + return err + } + } + timer := time.NewTimer(time.Millisecond) + select { + case <-ctx.Done(): + timer.Stop() + return ctx.Err() + case <-timer.C: + } + } +} + +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) + if err != nil { + return 0, err + } + 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 + } + if invalid { + t.step = stepLost + return 0, errors.New("cortexm: debug control changed during step") + } + return value, nil +} + +func (t *Target) finishStep(ctx context.Context) error { + if t.step == stepStopped { + reason, err := t.memory.ReadWord(ctx, dfsrAddress) + if err != nil { + return err + } + t.haltOwned = reason&0x1f == 1 + if !t.haltOwned { + t.changed = t.saved&cDebugEnable == 0 + } + t.step = stepRestoring + } + if err := ctx.Err(); err != nil { + return err + } + return t.memory.WriteWord(ctx, dhcsrAddress, debugKey|cDebugEnable|cHalt) +} diff --git a/target/cortexm/step_failure_test.go b/target/cortexm/step_failure_test.go new file mode 100644 index 0000000..e470ae0 --- /dev/null +++ b/target/cortexm/step_failure_test.go @@ -0,0 +1,238 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/jon/ostiole/target/cortexm" +) + +func TestStepPreservesCompetingEvents(t *testing.T) { + for _, before := range []bool{false, true} { + for _, reason := range []uint32{2, 4, 8, 16} { + m := newStepMemory() + 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("stepped with stale competing event") + } + } else { + writes = m.writes + if err := core.Resume(t.Context()); err == nil || writes != m.writes { + t.Fatal("resumed competing stop") + } + if err := core.Release(t.Context()); err != nil || !m.halted || m.control != debugEnable|haltRequest { + t.Fatalf("release: %v", err) + } + } + if m.reasons&reason == 0 { + t.Fatal("cleared competing evidence") + } + } + } +} + +func TestStepLaunchIsNeverReplayed(t *testing.T) { + for _, after := range []bool{false, true} { + m := newStepMemory() + 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 cleaned up") + } + if err := core.Step(t.Context()); err == nil || m.writes != writes { + t.Fatal("uncertain launch replayed") + } + } +} + +func TestStepIgnoredLaunchIsNotCompletion(t *testing.T) { + m := newStepMemory() + core := acquireStep(t, m) + m.ignoreWrites = true + if err := core.Step(t.Context()); err == nil { + t.Fatal("ignored step succeeded") + } + writes := m.writes + m.ignoreWrites = false + if err := core.Release(t.Context()); err == nil || m.writes != writes || !m.halted { + t.Fatal("ignored step resumed") + } +} + +func TestStepCompletionFailuresRetryCleanup(t *testing.T) { + for _, phase := range []string{"poll", "reason", "normalize-before", "normalize-after", "confirm", "cancel"} { + t.Run(phase, func(t *testing.T) { + m := newStepMemory() + core := acquireStep(t, m) + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + want := errMemory + switch phase { + case "poll": + m.failRead = m.reads + 3 + case "reason": + m.failRead = m.reads + 4 + case "normalize-before", "normalize-after": + m.failWrite = m.writes + 2 + m.afterWrite = phase == "normalize-after" + case "confirm": + m.failRead = m.reads + 5 + case "cancel": + m.onStep = cancel + want = context.Canceled + } + if err := core.Step(ctx); !errors.Is(err, want) { + t.Fatalf("step: %v", err) + } + if _, err := core.ReadRegister(t.Context(), cortexm.PC); err == nil { + t.Fatal("ordinary call after failure") + } + m.onStep = nil + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.launches != 1 || m.steps != 1 || m.control != 0 || m.halted { + t.Fatal("failed cleanup or replay") + } + }) + } +} + +func TestStepLostStateBlocksCleanup(t *testing.T) { + for _, change := range []string{"reset", "debug", "mask"} { + m := newStepMemory() + core := acquireStep(t, m) + m.onStep = func() { + switch change { + case "reset": + m.reset = true + case "debug": + m.control = 0 + case "mask": + m.control |= 8 + } + } + if err := core.Step(t.Context()); err == nil { + t.Fatal("lost state accepted") + } + writes := m.writes + m.control, m.halted = debugEnable|stepRequest|haltRequest, true + if err := core.Release(t.Context()); err == nil || m.writes != writes { + t.Fatal("lost step state restored") + } + } +} + +func TestStepWaitsForRegisterTransfer(t *testing.T) { + m := newStepMemory() + core := acquireStep(t, m) + m.pending, m.block = true, true + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Millisecond) + err := core.Step(ctx) + cancel() + if !errors.Is(err, context.DeadlineExceeded) || m.launches != 0 { + t.Fatal("step passed busy register transfer") + } + m.block = false + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestStepNormalizationCanBeRetried(t *testing.T) { + m := newStepMemory() + core := acquireStep(t, m) + m.onStep = func() { m.ignoreWrites = true } + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Millisecond) + err := core.Step(ctx) + cancel() + if !errors.Is(err, context.DeadlineExceeded) || !m.halted { + t.Fatal(err) + } + m.ignoreWrites = false + m.onStep = nil + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.steps != 1 || m.launches != 1 || m.halted { + t.Fatal("normalization replayed step or failed cleanup") + } +} + +func TestStepCompetingHaltPreventsDisabledDebugRestoration(t *testing.T) { + m := newStepMemory() + core := acquireStep(t, m) + m.onStep = func() { m.reasons |= 2 } + if err := core.Step(t.Context()); err == nil { + t.Fatal("competing halt accepted") + } + writes := m.writes + if err := core.Release(t.Context()); err == nil || m.writes != writes || !m.halted { + t.Fatal("resumed competing stop to restore debug") + } + m.control, m.halted = debugEnable, false + if err := core.Release(t.Context()); err != nil || m.control != 0 { + t.Fatalf("later cleanup: %v", err) + } +} + +func TestStepMayEnterAnException(t *testing.T) { + m := newStepMemory() + 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) + } + if pc, err := core.ReadRegister(t.Context(), cortexm.PC); err != nil || pc != 0x100 { + t.Fatal("exception entry rejected") + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestStepLosingCompletedHaltBlocksCleanup(t *testing.T) { + for _, reasonRead := range []bool{false, true} { + t.Run("lost-halt", func(t *testing.T) { + m := newStepMemory() + core := acquireStep(t, m) + if reasonRead { + m.failRead = m.reads + 4 + } else { + m.failWrite = m.writes + 2 + } + if err := core.Step(t.Context()); !errors.Is(err, errMemory) { + t.Fatal(err) + } + m.control, m.halted = debugEnable|stepRequest, false + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Millisecond) + err := core.Release(ctx) + cancel() + if err == nil { + t.Fatal("accepted running state during restoration") + } + writes := m.writes + m.control, m.halted, m.reasons = debugEnable|stepRequest|haltRequest, true, 2 + if err := core.Release(t.Context()); err == nil || m.writes != writes || !m.halted { + t.Fatal("cleanup claimed a later independent stop") + } + }) + } +} diff --git a/target/cortexm/step_integration_test.go b/target/cortexm/step_integration_test.go new file mode 100644 index 0000000..206e39e --- /dev/null +++ b/target/cortexm/step_integration_test.go @@ -0,0 +1,127 @@ +//go:build integration + +package cortexm_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/jon/ostiole/dap" + "github.com/jon/ostiole/target/cortexm" +) + +func TestHILCortexM0Step(t *testing.T) { + if os.Getenv("OSTIOLE_CORTEXM_HIL_CONTROL") != "1" || os.Getenv("OSTIOLE_CORTEXM_HIL_STEP") != "1" { + t.Skip("require OSTIOLE_CORTEXM_HIL_CONTROL=1 and OSTIOLE_CORTEXM_HIL_STEP=1") + } + if os.Getenv("OSTIOLE_CORTEXM_HIL_PROGRAM") != "sha256:ee294cc06ab6e8228161b49506675b065c0148b26421cf1f83c8e45e35cd4e5d" { + t.Fatal("require the documented counter image identity in OSTIOLE_CORTEXM_HIL_PROGRAM") + } + for range 2 { + if !t.Run("session", stepHIL) { + return + } + } +} + +func stepHIL(t *testing.T) { + t.Helper() + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) + defer cancel() + c := openControlBench(t, ctx) + var core *cortexm.Target + t.Cleanup(func() { releaseControlBench(t, core, c) }) + memory, err := c.OpenMemAP(ctx, dap.NewAPSel(0)) + if err != nil { + t.Fatal(err) + } + checkRegisterFirmware(t, ctx, memory) + before, err := memory.ReadWord(ctx, dhcsr) + if err != nil { + t.Fatal(err) + } + if before&haltStatus != 0 { + t.Fatal("bench is already halted; refusing to resume it") + } + checkCounterHIL(t, ctx, memory, 0x20000000, "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) + } + if r1 := readRegisterHIL(t, ctx, core, cortexm.R1); r1 != 0x20000000 { + t.Fatalf("unexpected counter address in R1: %#x", r1) + } + for n := range 12 { + checkCounterStepHIL(t, ctx, core, memory, n) + } + checkCounterHIL(t, ctx, memory, 0x20000000, "after steps, still halted", true) + if err := core.Resume(ctx); err != nil { + t.Fatal(err) + } + checkCounterHIL(t, ctx, memory, 0x20000000, "resumed", false) + if err := core.Halt(ctx); err != nil { + t.Fatal(err) + } + checkCounterStepHIL(t, ctx, core, memory, 12) + if err := core.Release(ctx); err != nil { + t.Fatal(err) + } + after, err := memory.ReadWord(ctx, dhcsr) + if err != nil { + t.Fatal(err) + } + mask := debugEnable | haltStatus + if before&debugEnable != 0 { + mask |= 12 + } + if after&mask != before&mask { + t.Fatalf("DHCSR before=%#x after=%#x", before, after) + } + checkCounterHIL(t, ctx, memory, 0x20000000, "released after another step", false) + t.Logf("micro:bit CMSIS-DAP 1 MHz AP0 CPUID=%#x DHCSR before=%#x after=%#x", core.Identity().Raw, before, after) +} + +func checkCounterStepHIL(t *testing.T, ctx context.Context, core *cortexm.Target, memory *dap.MemAP, n int) { + t.Helper() + pc := readRegisterHIL(t, ctx, core, cortexm.PC) + r0 := readRegisterHIL(t, ctx, core, cortexm.R0) + counter, err := memory.ReadWord(ctx, 0x20000000) + if err != nil { + t.Fatal(err) + } + nextPC, nextR0, nextCounter := pc, r0, counter + switch pc { + case 0xc6: + nextPC = 0xc8 + nextR0++ + case 0xc8: + nextPC = 0xca + nextCounter = r0 + case 0xca: + nextPC = 0xc6 + default: + t.Fatalf("PC outside counter loop: %#x", pc) + } + if err := core.Step(ctx); err != nil { + t.Fatal(err) + } + gotPC := readRegisterHIL(t, ctx, core, cortexm.PC) + gotR0 := readRegisterHIL(t, ctx, core, cortexm.R0) + gotCounter, err := memory.ReadWord(ctx, 0x20000000) + if err != nil { + t.Fatal(err) + } + halted, err := core.Halted(ctx) + if err != nil || !halted { + t.Fatalf("halted=%v err=%v", halted, err) + } + if gotPC != nextPC || gotR0 != nextR0 || gotCounter != nextCounter { + t.Fatalf("step %d: PC %#x -> %#x (want %#x), R0 %#x -> %#x (want %#x), RAM %#x -> %#x (want %#x)", n, pc, gotPC, nextPC, r0, gotR0, nextR0, counter, gotCounter, nextCounter) + } + t.Logf("step %d: PC %#x -> %#x, R0 %#x -> %#x, RAM %#x -> %#x", n, pc, gotPC, r0, gotR0, counter, gotCounter) +} diff --git a/target/cortexm/step_memory_test.go b/target/cortexm/step_memory_test.go new file mode 100644 index 0000000..2b4c899 --- /dev/null +++ b/target/cortexm/step_memory_test.go @@ -0,0 +1,72 @@ +package cortexm_test + +import ( + "context" + "errors" +) + +const stepRequest = uint32(4) + +type stepMemory struct { + *registerMemory + reasons uint32 + stepping bool + stepDelay, stepRemaining int + blockStep bool + launches, steps int + onStep func() +} + +func newStepMemory() *stepMemory { return &stepMemory{registerMemory: newRegisterMemory(), reasons: 1} } + +func (m *stepMemory) ReadWord(ctx context.Context, addr uint32) (uint32, error) { + if addr == 0xe000ed30 { + if err := ctx.Err(); err != nil { + return 0, err + } + m.reads++ + if m.reads == m.failRead { + return 0, errMemory + } + return m.reasons, nil + } + if ctx.Err() == nil && addr == dhcsr && m.stepping && !m.blockStep { + if m.stepRemaining == 0 { + m.finishStep() + } else { + m.stepRemaining-- + } + } + return m.registerMemory.ReadWord(ctx, addr) +} + +func (m *stepMemory) WriteWord(ctx context.Context, addr, value uint32) error { + if addr != dhcsr { + return m.registerMemory.WriteWord(ctx, addr, value) + } + if !m.halted && m.control&debugEnable != 0 && (m.control^value)&12 != 0 { + return errors.New("changed step/mask control while running") + } + wasHalted, writes := m.halted, m.writes + err := m.registerMemory.WriteWord(ctx, addr, value) + 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.stepDelay == 0 && !m.blockStep { + m.finishStep() + } + } + return err +} + +func (m *stepMemory) finishStep() { + m.stepping = false + m.steps++ + m.registers[0]++ + m.registers[15] += 2 + m.control |= haltRequest + m.halted = true + if m.onStep != nil { + m.onStep() + } +} diff --git a/target/cortexm/step_test.go b/target/cortexm/step_test.go new file mode 100644 index 0000000..b7d9b0b --- /dev/null +++ b/target/cortexm/step_test.go @@ -0,0 +1,173 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/jon/ostiole/target/cortexm" +) + +func acquireStep(t *testing.T, m *stepMemory) *cortexm.Target { + t.Helper() + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + if err := core.Halt(t.Context()); err != nil { + t.Fatal(err) + } + return core +} + +func TestStepReturnsHalted(t *testing.T) { + for _, delay := range []int{0, 2} { + t.Run("step", func(t *testing.T) { + m := newStepMemory() + m.stepDelay = delay + core := acquireStep(t, m) + firstPC := m.registers[15] + for n := 1; n <= 3; n++ { + if err := core.Step(t.Context()); err != nil { + t.Fatal(err) + } + pc, err := core.ReadRegister(t.Context(), cortexm.PC) + if err != nil || pc != firstPC+uint32(n)*2 || !m.halted || m.control != debugEnable|haltRequest { + t.Fatalf("PC=%#x err=%v control=%#x", pc, err, m.control) + } + } + if m.steps != 3 || m.launches != 3 { + t.Fatal("step replay") + } + 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.Fatalf("release: %v", err) + } + }) + } +} + +func TestStepRequiresOwnedHalt(t *testing.T) { + for _, inherited := range []bool{false, true} { + m := newStepMemory() + if inherited { + m.control, m.halted = debugEnable|haltRequest, true + } + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + writes := m.writes + if err := core.Step(t.Context()); err == nil || m.writes != writes { + t.Fatal("unowned step") + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + } + var core *cortexm.Target + if err := core.Step(t.Context()); err == nil { + t.Fatal("nil target step") + } + var zero cortexm.Target + if err := zero.Step(t.Context()); err == nil { + t.Fatal("zero target step") + } +} + +func TestStepCancellationBeforeLaunch(t *testing.T) { + for _, atRead := range []bool{false, true} { + m := newStepMemory() + core := acquireStep(t, m) + ctx, cancel := context.WithCancel(t.Context()) + if atRead { + m.onRead = cancel + } else { + cancel() + } + writes := m.writes + if err := core.Step(ctx); !errors.Is(err, context.Canceled) || m.writes != writes { + t.Fatalf("step: %v", err) + } + m.onRead = nil + if err := core.Step(t.Context()); err != nil { + t.Fatal(err) + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + } +} + +func TestStepPendingCleanup(t *testing.T) { + m := newStepMemory() + 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 control while step runs") + } + if err := core.Step(t.Context()); err == nil { + t.Fatal("ordinary step during cleanup") + } + m.blockStep = false + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.launches != 1 || m.steps != 1 || m.control != 0 || m.halted { + t.Fatal("cleanup replay or failed restoration") + } +} + +func TestStepObservesLostOwnership(t *testing.T) { + m := newStepMemory() + core := acquireStep(t, m) + m.control, m.halted = debugEnable, false + if err := core.Step(t.Context()); err == nil { + t.Fatal("step without halt") + } + m.control, m.halted = debugEnable|haltRequest, true + writes := m.writes + if err := core.Step(t.Context()); err == nil || m.writes != writes { + t.Fatal("step after lost ownership") + } +} + +func TestStepPreservesCallerDeadline(t *testing.T) { + for _, timeout := range []time.Duration{0, time.Second, time.Minute} { + t.Run(timeout.String(), func(t *testing.T) { + ctx := t.Context() + if timeout != 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, timeout) + defer cancel() + } + m := &deadlineMemory{Memory: newStepMemory(), t: t, ctx: ctx} + core, err := cortexm.Acquire(ctx, m) + if err != nil { + t.Fatal(err) + } + if err := core.Halt(ctx); err != nil { + t.Fatal(err) + } + if err := core.Step(ctx); err != nil { + t.Fatal(err) + } + if err := core.Release(ctx); err != nil { + t.Fatal(err) + } + }) + } +}