From 9f2b8090f382b0a790ade55253782122f987b799 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sun, 27 Sep 2026 19:56:29 -0700 Subject: [PATCH] Support Cortex-M33 acquisition and halt/resume. Acquisition rejected the RP2350's Cortex-M33 even though its MEM-AP and identity paths were usable. Admit M33 with Secure invasive debug permission and retain the existing halt ownership and release rules. Reject snap-stall state and never automatically resume after observing it; clearing the bit cannot repair the affected memory-system state. Relinquish halt ownership when M33 restart status reveals a new stop. Keep register access and stepping limited to Cortex-M0. Exercise core-0 control with a RAM counter and retain the program, preparation procedure, and explicit hardware test. Fresh RP2350/J-Link sessions stop the counter during halt and observe counter progress after resume and release, restoring initially disabled debug. This does not establish cross-core control or recovery from restricted debug permissions. --- README.md | 6 +- docs/architecture.md | 21 +- docs/capabilities.md | 31 +- docs/cortexm.md | 118 ++++++-- target/cortexm/architecture.go | 59 ++++ target/cortexm/control.go | 32 +- target/cortexm/control_integration_test.go | 39 ++- target/cortexm/control_test.go | 4 +- target/cortexm/identity.go | 5 +- target/cortexm/m33_integration_test.go | 35 +++ target/cortexm/m33_test.go | 278 ++++++++++++++++++ target/cortexm/register.go | 6 +- target/cortexm/register_write.go | 4 +- target/cortexm/restore.go | 18 +- target/cortexm/run.go | 9 +- target/cortexm/step.go | 6 +- .../cortexm/testdata/rp2350-counter/README.md | 39 +++ .../cortexm/testdata/rp2350-counter/counter.S | 15 + .../testdata/rp2350-counter/counter.ld | 5 + .../testdata/rp2350-counter/prepare.cfg | 23 ++ 20 files changed, 675 insertions(+), 78 deletions(-) create mode 100644 target/cortexm/architecture.go create mode 100644 target/cortexm/m33_integration_test.go create mode 100644 target/cortexm/m33_test.go create mode 100644 target/cortexm/testdata/rp2350-counter/README.md create mode 100644 target/cortexm/testdata/rp2350-counter/counter.S create mode 100644 target/cortexm/testdata/rp2350-counter/counter.ld create mode 100644 target/cortexm/testdata/rp2350-counter/prepare.cfg diff --git a/README.md b/README.md index 4fee1b2..ac668b2 100644 --- a/README.md +++ b/README.md @@ -69,9 +69,9 @@ The [examples](examples) begin with a raw SWD debug-port identity read, then 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, stepping, and halted register access over word memory; see -[Cortex-M control](docs/cortexm.md). +any compatible target-word reader. It also provides acquired Cortex-M0 and +Cortex-M33 halt/resume control over word memory, plus Cortex-M0 stepping and +halted register access; 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/architecture.md b/docs/architecture.md index abc691e..8626516 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -23,7 +23,7 @@ USB host access Arm Debug Port and MEM-AP | v - Cortex-M identity and Cortex-M0 control + Cortex-M identity and Cortex-M0/M33 control | v examples and ost @@ -51,7 +51,7 @@ service. | `dap` | Bind SW-DP or baseline ADIv5 JTAG-DP, manage identity and power, execute ordered DP/AP transactions, and provide scalar or block MEM-AP access. | | `dap/sim` | Model the DP, AP, and byte-addressed target-memory state consumed by `dap`. | | `coresight` | Identify debug components and walk ROM tables through borrowed scalar memory, with explicit bounds and no resource acquisition or target-memory writes. | -| `target/cortexm` | Identify Cortex-M processors and own Cortex-M0 halting debug and register access over borrowed word memory. | +| `target/cortexm` | Identify Cortex-M processors and own Cortex-M0/M33 halting debug, with Cortex-M0 register access, over borrowed word memory. | | `examples/...` | Demonstrate public package compositions as executable programs. | | `cmd/ost` | Provide a small command hierarchy over the same public packages. | @@ -326,14 +326,15 @@ transfer sizes but owns no DAP or MEM-AP state. See [CoreSight 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 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 [Cortex-M control](cortexm.md) for -restoration and failure boundaries. +`target/cortexm` identifies processors through a word reader. Cortex-M0/M33 +control also requires a word writer that waits for each access to complete. 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 [Cortex-M control](cortexm.md) for +restoration and failure boundaries. Cortex-M33 control requires Secure invasive +debug permission and excludes register access and stepping. ## Host implementations diff --git a/docs/capabilities.md b/docs/capabilities.md index 3c2e821..d83ef25 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -291,20 +291,23 @@ 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-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. | -| 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 control currently accepts Cortex-M0 only. See -[Cortex-M control](cortexm.md) for its effects and cleanup limits. +| 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; register access and stepping remain M0-only. | +| 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. | +| 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 and excludes register +access and stepping. See [Cortex-M control](cortexm.md) for its effects and +cleanup limits. ## Executable surfaces diff --git a/docs/cortexm.md b/docs/cortexm.md index f29b8ba..5ce315f 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -1,16 +1,17 @@ # Cortex-M control `target/cortexm.Identify` reads CPUID through any aligned-word reader. `Acquire` -additionally enables halting debug on Cortex-M0 through a borrowed `Memory`, -whose `ReadWord` and `WriteWord` methods are supplied by `dap.MemAP`. Other -processor parts are rejected before a debug-register write. +additionally enables halting debug on Cortex-M0 or Cortex-M33 through a borrowed +`Memory`, whose `ReadWord` and `WriteWord` methods are supplied by `dap.MemAP`. +Other processor parts are rejected before a debug-register write. ## Ownership Acquire sends target traffic but does not request a halt. It preserves an inherited halt and rejects active stepping, interrupt masking, or an unfinished -halt transition. When debug is disabled, the other control bits are unknown; -acquisition initializes them to zero when enabling debug. +halt transition. When debug is disabled, the halt, step, and interrupt-mask +control bits are unknown; acquisition initializes them to zero when enabling +debug. The target requires exclusive control of the processor's debug registers. Do not use another debugger or write those registers through raw memory while it is @@ -52,7 +53,9 @@ debug was initially disabled, observing a new halt prevents restoration until the processor runs again. After an uncertain halt or resume write, cleanup likewise refuses to resume an observed halt: the memory interface cannot establish whether the failed write caused that stop. A later running observation -permits cleanup to continue. The package has no forced-resume escape hatch. +permits cleanup to continue. On Cortex-M33, the target also relinquishes halt +ownership when it observes sticky restart status, even if a new event has +already halted the core again. The package has no forced-resume escape hatch. Debug events racing with restoration of disabled debug can still affect execution. @@ -60,20 +63,60 @@ If halt readback shows that the request was lost, the target relinquishes halt ownership. Cleanup leaves an independent stop alone; restoring initially disabled debug waits until the processor is running. -DHCSR reads consume the sticky reset and instruction-retirement indicators. The -package does not restore those indicators or clear DFSR event flags. +DHCSR reads consume sticky reset and instruction-retirement indicators, and +Cortex-M33 restart status. 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][armv6m]. It does not implement reset, -breakpoints, or watchpoints. +The Cortex-M0 implementation follows Arm DDI 0419E, sections C1.5 and +C1.6.3–C1.6.5 of the [Armv6-M Architecture Reference Manual][armv6m]. It does +not implement reset, breakpoints, or watchpoints. + +## Cortex-M33 control + +Cortex-M33 acquisition requires Secure invasive debug permission, indicated by +DHCSR.S_SDE. Restricted Non-secure-only debug and implementations without the +Security Extension are not supported by this control path. Acquisition does not +unlock debug, write authentication settings, select a security bank, or change +the processor's security state. Permission must remain available for control and +restoration. + +The target rejects C_SNAPSTALL on acquisition and stops control if it observes +that bit later. Arm requires a system reset after snap-stall before execution +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`, and `Release` support Cortex-M33. +Register reads, register writes, and `Step` reject an acquired M33 before +further memory traffic, leaving its control operations available. Those +operations still support Cortex-M0. + +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: + +```go +ap, err := dap.APAt(0x2000) +if err != nil { + return err +} +memory, err := connection.OpenMemAP(ctx, ap) +if err != nil { + return err +} +``` + +This controls one processor. It does not stop the other core, coordinate shared +memory, or stop DMA and peripherals. The M33 register semantics follow DHCSR in +Arm DDI 0553B.y, section D1.2.39 of the [Armv8-M Architecture Reference +Manual][armv8m], and the [RP2350 datasheet][rp2350]. ## 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. +`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. ```go if err := core.Step(ctx); err != nil { @@ -178,10 +221,11 @@ err = errors.Join(err, cleanupErr) // On failure retain both owners for a later cleanup attempt. ``` -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. With `-step`, it also steps once and -prints the resulting PC. It requires explicit consent to control execution: +The Cortex-M0 [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. 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 \ @@ -316,3 +360,37 @@ failure cleanup on hardware. Those control failures have behavioral coverage; state after Arm owner close was not measured. [armv6m]: https://documentation-service.arm.com/static/5f8ff05ef86e16515cdbf826 +[armv8m]: + https://community.arm.com/cfs-file/__key/communityserver-discussions-components-files/471/DDI0553B_5F00_y_5F00_armv8m_5F00_arm.pdf +[rp2350]: https://datasheets.raspberrypi.com/rp2350/rp2350-datasheet.pdf + +## RP2350 hardware procedure + +`TestHILRP2350Control` selects J-Link EDU Mini V2 serial `000802011345`, +requests 1 MHz SWD, and opens core 0's ADIv6 MEM-AP at `0x2000`. It requires +CPUID `0x411fd210` and a known program whose aligned RAM counter is incremented +only by that core. An inherited halt fails the test without resuming it. The +[RAM counter](../target/cortexm/testdata/rp2350-counter/README.md) provides the +program and separate OpenOCD preparation instructions. + +```sh +OSTIOLE_RP2350_HIL_CONTROL=1 \ +OSTIOLE_RP2350_HIL_COUNTER=0x20040000 \ +OSTIOLE_RP2350_HIL_PROGRAM=c20737e61153b272322548e8e6db5c420f0c148d6707ca4412c309f70415065a \ +go test -tags integration ./target/cortexm -run '^TestHILRP2350Control$' -count=1 -v +``` + +On September 27, 2026, OpenOCD 0.12.0 loaded and verified the RAM counter on +Nostalgia's RP2350 bench. Two fresh Ostiole sessions observed progress before +acquisition, no counter changes during halt, and renewed progress after resume +and release from a second halt. Both restored initially disabled debug and +running state before closing the Arm owner; target release and owner close +succeeded. Two earlier sessions also preserved initially enabled debug. + +These observations cover core 0 with Secure invasive debug permitted and +configurable interrupts disabled. Failure recovery, permission denial, and +snap-stall rejection are covered only by behavioral tests. The test does not +establish core-1 control, cross-core coordination, Non-secure-only debug, M33 +register access or stepping. State after closing the Arm debug owner was not +measured. The RAM program remains running after the test; original execution +state is not recovered. diff --git a/target/cortexm/architecture.go b/target/cortexm/architecture.go new file mode 100644 index 0000000..e8a963b --- /dev/null +++ b/target/cortexm/architecture.go @@ -0,0 +1,59 @@ +package cortexm + +import ( + "context" + "errors" + "fmt" +) + +const ( + cSnapStall = uint32(1 << 5) + sSecureDebug = uint32(1 << 20) + sRestart = uint32(1 << 26) +) + +func controlIdentity(identity Identity) error { + if identity.Part == 0xc20 && identity.Architecture == 0xc || + identity.Part == 0xd21 && identity.Architecture == 0xf { + return nil + } + return fmt.Errorf("cortexm: control requires Cortex-M0 or Cortex-M33, got CPUID %#08x", identity.Raw) +} + +func (t *Target) validateArchitectureControl(value uint32) error { + if t.identity.Part != 0xd21 { + return nil + } + if value&cSnapStall != 0 { + t.snapStalled = true + } + if t.snapStalled { + return errors.New("cortexm: snap-stall state requires system reset before resuming") + } + if value&sSecureDebug == 0 { + return errors.New("cortexm: Cortex-M33 control requires Secure invasive debug permission") + } + return nil +} + +func (t *Target) readDHCSR(ctx context.Context) (uint32, error) { + value, err := t.memory.ReadWord(ctx, dhcsrAddress) + if err == nil && t.identity.Part == 0xd21 { + t.snapStalled = t.snapStalled || value&cSnapStall != 0 + if value&(cDebugEnable|sRestart) == cDebugEnable|sRestart && (t.haltOwned || t.resumeUncertain) { + t.haltOwned, t.resumeUncertain = false, false + t.changed = t.saved&cDebugEnable == 0 + } + } + 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: register access and stepping require Cortex-M0") + } + return nil +} diff --git a/target/cortexm/control.go b/target/cortexm/control.go index 92fc1c1..f854a0f 100644 --- a/target/cortexm/control.go +++ b/target/cortexm/control.go @@ -26,8 +26,8 @@ type Memory interface { WriteWord(context.Context, uint32, uint32) error } -// Target owns Cortex-M0 halting debug state through borrowed memory. Do not copy -// it. Calls and all access to the underlying memory must be serialized. Keep +// Target owns Cortex-M0 or Cortex-M33 halting debug through borrowed memory. +// Do not copy it. Calls and all access to the underlying memory must be serialized. Keep // exclusive control of the processor's debug registers until Release succeeds, // then release the memory owner. The caller controls operation cancellation // and deadlines. The zero value is inactive. @@ -42,13 +42,17 @@ type Target struct { resumeUncertain bool registerPending bool registerLost bool + snapStalled bool step stepPhase } -// Acquire enables Cortex-M0 halting debug without requesting a halt. It reads -// CPUID and DHCSR, rejecting other cores, active stepping or interrupt masking, -// and an unfinished halt transition before writing. DHCSR reads consume its -// sticky reset and instruction-retirement indicators. +// Acquire enables Cortex-M0 or Cortex-M33 halting debug without requesting a +// halt. It reads CPUID and DHCSR, rejecting other cores, active stepping or +// interrupt masking, and an unfinished halt transition before writing. +// 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. +// Register access and stepping currently require Cortex-M0. // // The caller controls cancellation and deadlines. Failed setup attempts // restoration with an independent five-second context. A non-nil target @@ -65,8 +69,8 @@ func Acquire(ctx context.Context, memory Memory) (*Target, error) { if err != nil { return nil, err } - if identity.Part != 0xc20 || identity.Architecture != 0xc { - return nil, fmt.Errorf("cortexm: control requires Cortex-M0, got CPUID %#08x", identity.Raw) + if err := controlIdentity(identity); err != nil { + return nil, err } saved, err := memory.ReadWord(ctx, dhcsrAddress) if err != nil { @@ -76,6 +80,9 @@ func Acquire(ctx context.Context, memory Memory) (*Target, error) { return nil, err } t := &Target{memory: memory, identity: identity, saved: saved & (cDebugEnable | cHalt)} + if err := t.validateArchitectureControl(saved); err != nil { + return nil, err + } if saved&cDebugEnable != 0 { return t, nil } @@ -123,6 +130,8 @@ func (t *Target) Identity() Identity { // 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. +// Observed Cortex-M33 snap-stall state permanently prevents automatic resume; +// clearing its control bit does not make the memory system safe to resume. func (t *Target) Release(ctx context.Context) error { if t == nil || t.memory == nil { return nil @@ -158,13 +167,18 @@ func (t *Target) writeControl(ctx context.Context, control uint32) error { if err := t.memory.WriteWord(ctx, dhcsrAddress, debugKey|control); err != nil { return err } - value, err := t.memory.ReadWord(ctx, dhcsrAddress) + value, err := t.readDHCSR(ctx) if err != nil { return err } if value&cDebugEnable == 0 && t.saved == 0 { t.changed = false } + if control&cDebugEnable != 0 { + if err := t.validateArchitectureControl(value); err != nil { + return err + } + } mask := cDebugEnable if control&cDebugEnable != 0 { mask |= cHalt | cStep | cMaskInts diff --git a/target/cortexm/control_integration_test.go b/target/cortexm/control_integration_test.go index 8976935..868b37f 100644 --- a/target/cortexm/control_integration_test.go +++ b/target/cortexm/control_integration_test.go @@ -28,23 +28,27 @@ func TestHILCortexM0Control(t *testing.T) { t.Fatal("require a known bench PROGRAM and aligned RAM COUNTER address") } for range 2 { - if !t.Run("session", func(t *testing.T) { controlHIL(t, uint32(counter), program) }) { + if !t.Run("session", func(t *testing.T) { controlHIL(t, uint32(counter), program, microbitControlBench()) }) { return } } } -func controlHIL(t *testing.T, counter uint32, program string) { +func controlHIL(t *testing.T, counter uint32, program string, bench controlBench) { t.Helper() ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) defer cancel() - c := openControlBench(t, ctx) + c := bench.open(t, ctx) var core *cortexm.Target t.Cleanup(func() { releaseControlBench(t, core, c) }) - memory, err := c.OpenMemAP(ctx, dap.NewAPSel(0)) + memory, err := c.OpenMemAP(ctx, bench.ap) if err != nil { t.Fatal(err) } + identity, err := cortexm.Identify(ctx, memory) + if err != nil || identity.Raw != bench.cpuid { + t.Fatalf("bench CPUID=%#x, want %#x: %v", identity.Raw, bench.cpuid, err) + } before, err := memory.ReadWord(ctx, dhcsr) if err != nil { t.Fatal(err) @@ -80,8 +84,8 @@ func controlHIL(t *testing.T, counter uint32, program string) { t.Fatalf("DHCSR before=%#x after=%#x", before, after) } checkCounterHIL(t, ctx, memory, counter, "released", false) - t.Logf("micro:bit CMSIS-DAP 1 MHz AP0 CPUID=%#x program=%q counter=%#x DHCSR before=%#x after=%#x", - core.Identity().Raw, program, counter, before, after) + t.Logf("%s CPUID=%#x program=%q counter=%#x DHCSR before=%#x after=%#x", + bench.name, core.Identity().Raw, program, counter, before, after) } func checkCounterHIL(t *testing.T, ctx context.Context, memory *dap.MemAP, addr uint32, phase string, stopped bool) { @@ -111,14 +115,35 @@ func checkCounterHIL(t *testing.T, ctx context.Context, memory *dap.MemAP, addr } } +type controlBench struct { + name string + provider discover.ProviderID + serial string + ap dap.APSel + cpuid uint32 +} + +func microbitControlBench() controlBench { + return controlBench{ + name: "micro:bit CMSIS-DAP 1 MHz AP0", + provider: "cmsisdap", serial: "9900360140124e4500279015000000360000000097969901", + ap: dap.NewAPSel(0), cpuid: 0x410cc200, + } +} + func openControlBench(t *testing.T, ctx context.Context) *armdebug.Conn { + t.Helper() + return microbitControlBench().open(t, ctx) +} + +func (bench controlBench) open(t *testing.T, ctx context.Context) *armdebug.Conn { t.Helper() inventory, err := discover.Probes(ctx) if err != nil { t.Fatal(err) } candidate, err := inventory.Select(discover.Selection{ - Provider: "cmsisdap", Serial: "9900360140124e4500279015000000360000000097969901", + Provider: bench.provider, Serial: bench.serial, }) if errors.Is(err, discover.ErrCandidateNotFound) || errors.Is(err, discover.ErrCandidateAmbiguous) { t.Skip(err) diff --git a/target/cortexm/control_test.go b/target/cortexm/control_test.go index d7bd0da..f179642 100644 --- a/target/cortexm/control_test.go +++ b/target/cortexm/control_test.go @@ -20,6 +20,7 @@ var errMemory = errors.New("memory failure") type controlMemory struct { cpuid uint32 control uint32 + status uint32 halted bool reads, writes int failRead, failWrite int @@ -56,7 +57,8 @@ func (m *controlMemory) ReadWord(ctx context.Context, addr uint32) (uint32, erro m.halted = true } } - value := m.control + value := m.control | m.status + m.status &^= 1 << 26 if m.halted { value |= haltStatus } diff --git a/target/cortexm/identity.go b/target/cortexm/identity.go index 0b3ed20..c6f9250 100644 --- a/target/cortexm/identity.go +++ b/target/cortexm/identity.go @@ -1,5 +1,6 @@ -// Package cortexm identifies Cortex-M processors and provides Cortex-M0 -// halting debug, stepping, and register access through target memory. +// Package cortexm identifies Cortex-M processors and provides Cortex-M0 and +// Cortex-M33 halting debug through target memory. Register access and stepping +// currently require Cortex-M0. package cortexm import ( diff --git a/target/cortexm/m33_integration_test.go b/target/cortexm/m33_integration_test.go new file mode 100644 index 0000000..f6ce8fe --- /dev/null +++ b/target/cortexm/m33_integration_test.go @@ -0,0 +1,35 @@ +//go:build integration + +package cortexm_test + +import ( + "os" + "strconv" + "testing" + + "github.com/jon/ostiole/dap" +) + +func TestHILRP2350Control(t *testing.T) { + if os.Getenv("OSTIOLE_RP2350_HIL_CONTROL") != "1" { + t.Skip("OSTIOLE_RP2350_HIL_CONTROL is not 1") + } + program := os.Getenv("OSTIOLE_RP2350_HIL_PROGRAM") + counter, err := strconv.ParseUint(os.Getenv("OSTIOLE_RP2350_HIL_COUNTER"), 0, 32) + if err != nil || counter%4 != 0 || program == "" { + t.Fatal("require a known bench PROGRAM and aligned RAM COUNTER address") + } + ap, err := dap.APAt(0x2000) + if err != nil { + t.Fatal(err) + } + bench := controlBench{ + name: "RP2350 core 0 J-Link 1 MHz AP 0x2000", + provider: "jlink", serial: "000802011345", ap: ap, cpuid: 0x411fd210, + } + for range 2 { + if !t.Run("session", func(t *testing.T) { controlHIL(t, uint32(counter), program, bench) }) { + return + } + } +} diff --git a/target/cortexm/m33_test.go b/target/cortexm/m33_test.go new file mode 100644 index 0000000..163e5f8 --- /dev/null +++ b/target/cortexm/m33_test.go @@ -0,0 +1,278 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +const secureDebug = uint32(1 << 20) + +func newM33Memory() *controlMemory { + m := newControlMemory() + m.cpuid, m.status = 0x411fd210, secureDebug + return m +} + +func TestM33ControlRestoresInheritedState(t *testing.T) { + for _, initial := range []uint32{0, debugEnable, debugEnable | haltRequest} { + m := newM33Memory() + m.control, m.halted = initial, initial&haltRequest != 0 + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + if core.Identity().Raw != m.cpuid || m.halted != (initial&haltRequest != 0) { + t.Fatal("acquisition changed identity or halt state") + } + if err := core.Halt(t.Context()); err != nil { + t.Fatal(err) + } + halted, err := core.Halted(t.Context()) + if err != nil || !halted { + t.Fatalf("Halted = %v, %v", halted, err) + } + err = core.Resume(t.Context()) + if initial&haltRequest != 0 { + if err == nil || !m.halted { + t.Fatal("resumed inherited halt") + } + } else { + if err != nil || m.halted { + t.Fatalf("Resume = %v, halted=%v", err, m.halted) + } + if err := core.Halt(t.Context()); err != nil { + t.Fatal(err) + } + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.control != initial || m.halted != (initial&haltRequest != 0) { + t.Fatalf("restored %#x halted=%v", m.control, m.halted) + } + } +} + +func TestM33AcquireRejectsUnsupportedStateBeforeWrites(t *testing.T) { + for _, test := range []struct { + name string + status, control uint32 + }{ + {"secure debug denied", 0, 0}, + {"denied while enabled", 0, debugEnable}, + {"snap stall", secureDebug, debugEnable | haltRequest | 32}, + {"step", secureDebug, debugEnable | 4}, + {"mask interrupts", secureDebug, debugEnable | 8}, + } { + t.Run(test.name, func(t *testing.T) { + m := newM33Memory() + m.status, m.control = test.status, test.control + m.halted = test.control&haltRequest != 0 + core, err := cortexm.Acquire(t.Context(), m) + if err == nil || core != nil || m.writes != 0 { + t.Fatalf("core=%v err=%v writes=%d", core, err, m.writes) + } + }) + } +} + +func TestM33AcquireFailureRestoresDebug(t *testing.T) { + for _, after := range []bool{false, true} { + m := newM33Memory() + m.failWrite, m.afterWrite = 1, after + core, err := cortexm.Acquire(t.Context(), m) + if core != nil || !errors.Is(err, errMemory) || m.control != 0 { + t.Fatalf("core=%v err=%v control=%#x", core, err, m.control) + } + } +} + +func TestM33RejectsControlChangesBeforeResume(t *testing.T) { + for _, snap := range []bool{false, true} { + 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) + } + if snap { + m.control |= 32 + } else { + m.status = 0 + } + writes := m.writes + if err := core.Resume(t.Context()); err == nil || m.writes != writes { + t.Fatalf("Resume = %v, writes=%d", err, m.writes-writes) + } + if err := core.Release(t.Context()); err == nil || m.writes != writes { + t.Fatalf("Release = %v, writes=%d", err, m.writes-writes) + } + if snap { + m.control &^= 32 + if err := core.Release(t.Context()); err == nil || m.writes != writes { + t.Fatal("clearing snap-stall allowed unsafe restoration") + } + } + if !snap { + m.status = secureDebug + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.control != 0 || m.halted { + t.Fatal("restoration failed after permission returned") + } + } + } +} + +func TestM33DefersRegistersAndSteppingWithoutTraffic(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.ReadRegister(t.Context(), cortexm.PC); err == nil { + t.Fatal("register read accepted") + } + if err := core.WriteRegister(t.Context(), cortexm.R0, 1); err == nil { + t.Fatal("register write accepted") + } + 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) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(t.Context()) + m.stall = true + m.onWrite = cancel + if err := core.Halt(ctx); !errors.Is(err, context.Canceled) { + t.Fatalf("Halt = %v", err) + } + m.stall = false + m.onWrite = nil + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.control != 0 || m.halted { + t.Fatal("canceled halt cleanup failed") + } +} + +func TestM33DeniedReadbackRetainsCleanup(t *testing.T) { + m := newM33Memory() + m.onWrite = func() { m.status = 0 } + core, err := cortexm.Acquire(t.Context(), m) + if err == nil || core == nil || m.writes != 1 { + t.Fatalf("core=%v err=%v writes=%d", core, err, m.writes) + } + reads := m.reads + if _, err := core.Halted(t.Context()); err == nil || m.reads != reads { + t.Fatal("ordinary call during failed acquisition cleanup") + } + m.onWrite = nil + m.status = secureDebug + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.control != 0 { + t.Fatal("did not restore disabled debug") + } +} + +func TestM33SnapStallRemainsLatchedWithOtherControlErrors(t *testing.T) { + for _, other := range []uint32{4, 8} { + 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) + } + m.control |= 32 | other + if _, err := core.Halted(t.Context()); err == nil { + t.Fatal("accepted changed control") + } + m.control &^= 32 | other + writes := m.writes + if err := core.Release(t.Context()); err == nil || m.writes != writes || !m.halted { + t.Fatalf("Release = %v, writes=%d halted=%v", err, m.writes-writes, m.halted) + } + } +} + +func TestM33RestartRelinquishesHaltOwnership(t *testing.T) { + for _, initial := range []uint32{0, debugEnable} { + for _, other := range []uint32{0, 4, 8} { + m := newM33Memory() + m.control = initial + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + if err := core.Halt(t.Context()); err != nil { + t.Fatal(err) + } + m.status |= 1 << 26 + m.control |= other + _, err = core.Halted(t.Context()) + if (err != nil) != (other != 0) { + t.Fatalf("Halted with control %#x: %v", other, err) + } + m.control &^= other + writes := m.writes + err = core.Release(t.Context()) + if m.writes != writes || !m.halted { + t.Fatal("resumed a new halt after restart") + } + if (err != nil) != (initial == 0) { + t.Fatalf("Release initial=%#x: %v", initial, err) + } + } + } +} + +func TestM33RestoreChecksFinalReadPermission(t *testing.T) { + m := newM33Memory() + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + m.onRead = func() { m.status = 0 } + writes := m.writes + if err := core.Release(t.Context()); err == nil || m.writes != writes { + t.Fatalf("Release = %v, writes=%d", err, m.writes-writes) + } + m.onRead = nil + m.status = secureDebug + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.control != 0 { + t.Fatal("did not restore debug after permission returned") + } +} diff --git a/target/cortexm/register.go b/target/cortexm/register.go index 3ae20be..8681675 100644 --- a/target/cortexm/register.go +++ b/target/cortexm/register.go @@ -42,8 +42,8 @@ const ( sReset = uint32(1 << 25) ) -// ReadRegister reads a register while halted, without acquiring halt ownership. -// It writes debug transfer registers and consumes DHCSR's sticky status. The +// ReadRegister reads a Cortex-M0 register while halted, without acquiring halt +// ownership. It writes debug transfer registers and consumes DHCSR's sticky status. The // caller controls cancellation and deadlines. An uncertain transfer blocks // ordinary calls; Release must settle it before changing debug control. Loss // of Debug state or reset during a pending transfer prevents automatic cleanup. @@ -52,7 +52,7 @@ func (t *Target) ReadRegister(ctx context.Context, reg Register) (uint32, error) if reg < R0 || reg > PSP { return 0, errors.New("cortexm: invalid register") } - if err := t.active(ctx); err != nil { + if err := t.activeM0(ctx); err != nil { return 0, err } if err := t.waitRegister(ctx); err != nil { diff --git a/target/cortexm/register_write.go b/target/cortexm/register_write.go index 583bd53..6cdb93a 100644 --- a/target/cortexm/register_write.go +++ b/target/cortexm/register_write.go @@ -5,7 +5,7 @@ import ( "errors" ) -// WriteRegister changes a register while halted, without acquiring halt +// WriteRegister changes a Cortex-M0 register while halted, without acquiring halt // ownership. XPSR is read-only. SP, MSP, and PSP require word alignment; PC // requires bit zero clear and does not change Thumb state. Invalid identifiers // and values are rejected before traffic. Release does not undo register writes. @@ -18,7 +18,7 @@ func (t *Target) WriteRegister(ctx context.Context, reg Register, value uint32) if err := validateRegisterWrite(reg, value); err != nil { return err } - if err := t.active(ctx); err != nil { + if err := t.activeM0(ctx); err != nil { return err } if err := t.waitRegister(ctx); err != nil { diff --git a/target/cortexm/restore.go b/target/cortexm/restore.go index d9cf3c9..b7df125 100644 --- a/target/cortexm/restore.go +++ b/target/cortexm/restore.go @@ -6,7 +6,7 @@ import ( ) func (t *Target) restore(ctx context.Context) error { - value, err := t.memory.ReadWord(ctx, dhcsrAddress) + value, err := t.readDHCSR(ctx) if err != nil { return err } @@ -24,10 +24,13 @@ func (t *Target) restore(ctx context.Context) error { if !t.changed { return nil } - value, err = t.memory.ReadWord(ctx, dhcsrAddress) + value, err = t.readDHCSR(ctx) if err != nil { return err } + if err := t.validateArchitectureControl(value); err != nil { + return err + } if value&(cHalt|sHalt) != 0 && value&cDebugEnable != 0 { return errors.New("cortexm: new halt prevents restoring disabled debug") } @@ -35,6 +38,14 @@ func (t *Target) restore(ctx context.Context) error { } func (t *Target) resume(ctx context.Context) error { + if t.identity.Part == 0xd21 { + if _, err := t.readControl(ctx); err != nil { + return err + } + if !t.haltOwned { + return errors.New("cortexm: halt ownership was lost") + } + } if err := ctx.Err(); err != nil { return err } @@ -52,6 +63,9 @@ func (t *Target) resume(ctx context.Context) error { } func (t *Target) checkRestoreState(value uint32) error { + if err := t.validateArchitectureControl(value); err != nil { + return err + } if value&cDebugEnable != 0 && value&(cStep|cMaskInts) != 0 { return errors.New("cortexm: cannot restore externally changed stepping or interrupt masking") } diff --git a/target/cortexm/run.go b/target/cortexm/run.go index 2bb62fb..9602f79 100644 --- a/target/cortexm/run.go +++ b/target/cortexm/run.go @@ -41,7 +41,9 @@ func (t *Target) Resume(ctx context.Context) error { } // Halted reads the current Debug state. It consumes DHCSR's sticky reset and -// instruction-retirement indicators, and does not establish halt ownership. +// instruction-retirement indicators and Cortex-M33 restart status. It does not +// establish halt ownership. The target relinquishes its halt claim when it +// observes Cortex-M33 restart status, even if the processor is halted again. func (t *Target) Halted(ctx context.Context) (bool, error) { if err := t.active(ctx); err != nil { return false, err @@ -58,10 +60,13 @@ func (t *Target) active(ctx context.Context) error { } func (t *Target) readControl(ctx context.Context) (uint32, error) { - value, err := t.memory.ReadWord(ctx, dhcsrAddress) + value, err := t.readDHCSR(ctx) if err == nil && (value&cDebugEnable == 0 || value&(cStep|cMaskInts) != 0) { err = errors.New("cortexm: halting debug control changed outside the target") } + if err == nil { + err = t.validateArchitectureControl(value) + } if err != nil { t.closing = true } else { diff --git a/target/cortexm/step.go b/target/cortexm/step.go index ebfdb13..8688db4 100644 --- a/target/cortexm/step.go +++ b/target/cortexm/step.go @@ -18,8 +18,8 @@ const ( 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 +// 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. // Interrupt masking is unchanged. Existing competing DFSR event flags prevent // stepping, and none of its flags are cleared. @@ -30,7 +30,7 @@ const ( // 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 { + if err := t.activeM0(ctx); err != nil { return err } if !t.haltOwned { diff --git a/target/cortexm/testdata/rp2350-counter/README.md b/target/cortexm/testdata/rp2350-counter/README.md new file mode 100644 index 0000000..faf85b2 --- /dev/null +++ b/target/cortexm/testdata/rp2350-counter/README.md @@ -0,0 +1,39 @@ +# RP2350 core-0 RAM counter + +This Cortex-M33 program disables configurable interrupts and increments the +32-bit word at `0x20040000` in a CPU loop. The code starts at `0x20040020` and +uses no stack, peripherals, or DMA. It runs in the core's existing Secure state. + +Build from the repository root with Clang's Arm assembler, LLD, and GNU Arm +objcopy: + +```sh +clang --target=arm-none-eabi -mcpu=cortex-m33 -mthumb -c \ + target/cortexm/testdata/rp2350-counter/counter.S -o /tmp/ostiole-rp2350-counter.o +ld.lld -T target/cortexm/testdata/rp2350-counter/counter.ld \ + /tmp/ostiole-rp2350-counter.o -o /tmp/ostiole-rp2350-counter.elf +arm-none-eabi-objcopy -O binary /tmp/ostiole-rp2350-counter.elf \ + /tmp/ostiole-rp2350-counter.bin +shasum -a 256 /tmp/ostiole-rp2350-counter.bin +``` + +The preparation script selects J-Link `000802011345` at 1 MHz and core 0's ADIv6 +MEM-AP at `0x2000`. It halts core 0, loads and verifies the ELF, sets PC and +XPSR, resumes the counter, and disables halting debug for the control test. +OpenOCD uses `0x20041000`–`0x20041fff` as a backed-up working area. + +```sh +openocd -f target/cortexm/testdata/rp2350-counter/prepare.cfg +``` + +This replaces core 0's running program state and writes the indicated RAM. Flash +is untouched, but the old PC, register values, interrupt-mask state, and +replaced RAM contents are not restored. No reset or core-1 halt is requested. +Use only on a bench where these changes are acceptable, with Secure invasive +debug allowed and no other processor or DMA writer using this RAM. Stop other +debuggers before running Ostiole. + +The [RP2350 control test](../../../../docs/cortexm.md#rp2350-hardware-procedure) +observes the counter before acquisition, while halted, after resume, and after +release from another halt. Programming and verification are separate bench +preparation; the test does not load code or change core registers. diff --git a/target/cortexm/testdata/rp2350-counter/counter.S b/target/cortexm/testdata/rp2350-counter/counter.S new file mode 100644 index 0000000..2336e24 --- /dev/null +++ b/target/cortexm/testdata/rp2350-counter/counter.S @@ -0,0 +1,15 @@ +.syntax unified +.cpu cortex-m33 +.thumb +.section .text, "ax", %progbits +.thumb_func +.global counter_start +counter_start: + cpsid i + ldr r1, =0x20040000 + movs r0, #0 +counter_loop: + adds r0, #1 + str r0, [r1] + b counter_loop +.ltorg diff --git a/target/cortexm/testdata/rp2350-counter/counter.ld b/target/cortexm/testdata/rp2350-counter/counter.ld new file mode 100644 index 0000000..7bdb7d7 --- /dev/null +++ b/target/cortexm/testdata/rp2350-counter/counter.ld @@ -0,0 +1,5 @@ +ENTRY(counter_start) +SECTIONS { + . = 0x20040020; + .text : { *(.text) } +} diff --git a/target/cortexm/testdata/rp2350-counter/prepare.cfg b/target/cortexm/testdata/rp2350-counter/prepare.cfg new file mode 100644 index 0000000..7d9401e --- /dev/null +++ b/target/cortexm/testdata/rp2350-counter/prepare.cfg @@ -0,0 +1,23 @@ +source [find interface/jlink.cfg] +transport select swd +adapter serial 000802011345 +adapter speed 1000 +gdb_port disabled +tcl_port disabled +telnet_port disabled +swd newdap rp2350 cpu -expected-id 0x4c013477 +dap create rp2350.dap -chain-position rp2350.cpu -adiv6 +target create rp2350.core0 cortex_m -dap rp2350.dap -ap-num 0x2000 -work-area-phys 0x20041000 -work-area-size 0x1000 -work-area-backup 1 +init +halt +load_image /tmp/ostiole-rp2350-counter.elf +verify_image /tmp/ostiole-rp2350-counter.elf +echo "RAM counter loaded and verified" +mdw 0x20040020 4 +reg pc 0x20040020 +reg xpsr 0x01000000 +resume +mww 0xe000edf0 0xa05f0000 +mdw 0xe000edf0 +echo "Counter running with halting debug disabled" +shutdown