diff --git a/README.md b/README.md index ac668b2..135e285 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 over word memory, plus Cortex-M0 stepping and -halted register access; see [Cortex-M control](docs/cortexm.md). +Cortex-M33 halt/resume control and halted register access over word memory, plus +Cortex-M0 stepping; 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 6cae771..9ccea61 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,8 +18,9 @@ and how to assemble them without duplicating lower-level behavior. posted AP access, power handshakes, and MEM-AP details worth testing. - [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 acquisition, halt/resume, - restoration, and the explicitly gated control example. +- [Cortex-M control](cortexm.md) describes Cortex-M0/M33 acquisition, + halt/resume, register access, 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 8626516..7c15219 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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/M33 halting debug, with Cortex-M0 register access, over borrowed word memory. | +| `target/cortexm` | Identify Cortex-M processors and own Cortex-M0/M33 halting debug, with halted 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. | @@ -334,7 +334,8 @@ 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. +debug permission. Its stack register selectors use the halted security state; +register access does not change security state. Stepping remains Cortex-M0-only. ## Host implementations diff --git a/docs/capabilities.md b/docs/capabilities.md index d83ef25..68d9114 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -291,23 +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-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. | +| 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; stepping remains 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/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. | +| 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. +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. ## Executable surfaces diff --git a/docs/composition.md b/docs/composition.md index d8bc4d6..4cc0c49 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -38,7 +38,7 @@ data-register write can write target memory. | Identify a Cortex-M through any compatible word reader | `cortexm.Identify` | `examples/simple/cortexm-info` | | Acquire, halt, inspect registers, and resume a Cortex-M0 | `cortexm.Acquire`, `Target.Halt`, `Target.ReadRegister`, `Target.Resume`, `Target.Release` | `examples/simple/cortexm-control` | | Step a Cortex-M0 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) | +| Read or write a halted Cortex-M0/M33 register | `Target.ReadRegister`, `Target.WriteRegister` | [Register reads](cortexm.md#register-reads), [writes](cortexm.md#register-writes) | | Test SWD and DAP behavior without hardware | `swd/sim`, `dap/sim` | Package tests | The examples are intentionally small, executable compositions of public @@ -738,10 +738,10 @@ CSW, then release and reconnect the debug port. Use `target/cortexm` when the desired result is processor identity. It accepts the word-reader behavior supplied by `dap.MemAP`, so target code remains independent of the host, adapter, and wire protocol. `cortexm.Acquire` also uses -`WriteWord` to enable Cortex-M0 halting debug. Use `ReadRegister` for halted -core registers and `WriteRegister` for intentional changes. The target tracks -transfer completion but does not roll back writes. Release it before its memory -owner and retain both after failed target restoration. See +`WriteWord` to enable Cortex-M0 or Cortex-M33 halting debug. Use `ReadRegister` +for halted core registers and `WriteRegister` for intentional changes. The +target tracks transfer completion but does not roll back writes. Release it +before its memory owner and retain both after failed target restoration. See [Cortex-M control](cortexm.md) for the full composition and effects. ## Release in reverse order diff --git a/docs/cortexm.md b/docs/cortexm.md index 5ce315f..120fb55 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -86,10 +86,9 @@ 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. +`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. 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: @@ -105,10 +104,12 @@ if err != nil { } ``` -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]. +This controls one processor and does not configure cross-core stopping or +coordinate shared memory. Inherited cross-trigger routing can still couple the +cores. Halting a processor does not stop DMA and peripherals. The M33 debug +semantics follow DHCSR, DCRSR, and DCRDR in Arm DDI 0553B.y, sections D1.2.33, +D1.2.34, and D1.2.39 of the [Armv8-M Architecture Reference Manual][armv8m], and +the [RP2350 datasheet][rp2350]. ## Stepping @@ -152,10 +153,14 @@ flags, cancellation, ignored writes, partial failures, and cleanup retries. The ## Register reads `ReadRegister` reads R0–R12, SP, LR, PC, XPSR, MSP, or PSP from a halted -processor. SP selects the current stack pointer; MSP and PSP select its banks. -PC is the debug return address. An inherited halt permits inspection without -acquiring permission to resume. Invalid `Register` identifiers, including zero, -are rejected before memory traffic. +Cortex-M0 or Cortex-M33 processor. SP selects the current stack pointer; MSP and +PSP select the main and process stacks. On M33, all three use the halted +security state. The API does not change that state or DSCSR's memory-mapped bank +selection, and does not expose explicit Secure/Non-secure register selectors, +stack limits, or floating-point registers. PC is the debug return address. An +inherited halt permits inspection without acquiring permission to resume. +Invalid `Register` identifiers, including zero, are rejected before memory +traffic. ```go pc, err := core.ReadRegister(ctx, cortexm.PC) @@ -172,11 +177,11 @@ disabling debug. It never replays a selector write whose completion is uncertain. A failed precondition or cancellation before selection leaves the target usable when no transfer is pending. An error returns no register value. -Reset or loss of Debug state during a pending transfer prevents automatic -cleanup, even if a later status read would show ready. The target cannot prove -that the original transfer completed. Retain both owners; there is no forced -cleanup operation for this state. These failures have behavioral test coverage, -not physical failure-injection evidence. +Reset, loss of Debug state, or observed M33 restart during a pending transfer +prevents automatic cleanup, even if a later status read would show ready. The +target cannot prove that the original transfer completed. Retain both owners; +there is no forced cleanup operation for this state. These failures have +behavioral test coverage, not physical failure-injection evidence. ## Register writes @@ -394,3 +399,39 @@ 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. + +## RP2350 register bench + +After preparing the +[core-0 RAM counter](../target/cortexm/testdata/rp2350-counter/README.md), run +the separately gated register test: + +```sh +OSTIOLE_RP2350_HIL_CONTROL=1 \ +OSTIOLE_RP2350_HIL_REGISTERS=1 \ +OSTIOLE_RP2350_HIL_PROGRAM=c20737e61153b272322548e8e6db5c420f0c148d6707ca4412c309f70415065a \ +go test -tags integration ./target/cortexm -run '^TestHILRP2350Registers$' -count=1 -v +``` + +The test checks CPUID and the counter instructions before acquisition. It +requires a running bench and, after halting, verifies Secure state, main-stack +selection, and a PC inside the loop. It reads all 19 exposed registers, writes +and restores R4, SP, MSP, PSP, and PC, then verifies the whole snapshot and +unchanged DSCSR before resuming. An unconfirmed register restoration retains the +owners without requesting resume. + +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`. R4 retained +both `0x55aa55aa` and `0xaa55aa55`; stack and PC writes read back and were +restored before execution. All 19 registers matched their saved values and DSCSR +remained `0x00030000`. The counter stayed unchanged while halted and advanced +after resume and release. DHCSR's debug-enable and halt status matched initially +disabled debug and running state; both target release and Arm owner close +succeeded. + +This exercises Secure state on core 0. Non-secure stack selection, transfer +failures, restart during transfer, and cleanup failures have behavioral +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. diff --git a/target/cortexm/architecture.go b/target/cortexm/architecture.go index e8a963b..fb9fec8 100644 --- a/target/cortexm/architecture.go +++ b/target/cortexm/architecture.go @@ -53,7 +53,7 @@ func (t *Target) activeM0(ctx context.Context) error { return err } if t.identity.Part != 0xc20 { - return errors.New("cortexm: register access and stepping require Cortex-M0") + return errors.New("cortexm: stepping requires Cortex-M0") } return nil } diff --git a/target/cortexm/control.go b/target/cortexm/control.go index f854a0f..c916230 100644 --- a/target/cortexm/control.go +++ b/target/cortexm/control.go @@ -26,7 +26,8 @@ type Memory interface { WriteWord(context.Context, uint32, uint32) error } -// Target owns Cortex-M0 or Cortex-M33 halting debug through borrowed memory. +// Target owns one Cortex-M0 or Cortex-M33 processor's 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 @@ -52,7 +53,7 @@ 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. -// Register access and stepping currently require Cortex-M0. +// 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 @@ -127,8 +128,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. An accepted step must return -// halted before stepping can be disabled; an unconfirmed step launch prevents +// or Cortex-M33 restart 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. diff --git a/target/cortexm/identity.go b/target/cortexm/identity.go index c6f9250..d171be0 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 through target memory. Register access and stepping -// currently require Cortex-M0. +// Cortex-M33 halting debug and register access through target memory. +// Stepping currently requires Cortex-M0. package cortexm import ( diff --git a/target/cortexm/m33_register_integration_test.go b/target/cortexm/m33_register_integration_test.go new file mode 100644 index 0000000..72b737b --- /dev/null +++ b/target/cortexm/m33_register_integration_test.go @@ -0,0 +1,125 @@ +//go:build integration + +package cortexm_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/jon/ostiole/dap" + "github.com/jon/ostiole/target/cortexm" +) + +func TestHILRP2350Registers(t *testing.T) { + if os.Getenv("OSTIOLE_RP2350_HIL_REGISTERS") != "1" || os.Getenv("OSTIOLE_RP2350_HIL_CONTROL") != "1" { + t.Skip("require OSTIOLE_RP2350_HIL_REGISTERS=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", m33RegisterHIL) { + return + } + } +} + +func m33RegisterHIL(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 + dirty := false + t.Cleanup(func() { + if dirty { + t.Error("register restoration unconfirmed; retaining owners without requesting resume") + return + } + 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) + } + exerciseM33RegistersHIL(t, ctx, core, memory, &dirty) + checkCounterHIL(t, ctx, memory, 0x20040000, "registers restored, halted", true) + if err := core.Resume(ctx); err != nil { + t.Fatal(err) + } + checkCounterHIL(t, ctx, memory, 0x20040000, "resumed", false) + if err := core.Release(ctx); err != nil { + t.Fatal(err) + } + after, err := memory.ReadWord(ctx, dhcsr) + if err != nil || after&(debugEnable|haltStatus) != before&(debugEnable|haltStatus) { + t.Fatalf("DHCSR before=%#x after=%#x: %v", before, after, err) + } + checkCounterHIL(t, ctx, memory, 0x20040000, "released", 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 checkM33RegisterBench(t *testing.T, ctx context.Context, memory *dap.MemAP) uint32 { + t.Helper() + identity, err := cortexm.Identify(ctx, memory) + if err != nil || identity.Raw != 0x411fd210 { + t.Fatalf("CPUID=%#x: %v", identity.Raw, err) + } + for i, want := range []uint32{0x4902b672, 0x30012000, 0xe7fc6008, 0x20040000} { + addr := uint32(0x20040020 + i*4) + got, err := memory.ReadWord(ctx, addr) + if err != nil || got != want { + t.Fatalf("counter code %#x=%#x, want %#x: %v", addr, got, want, err) + } + } + before, err := memory.ReadWord(ctx, dhcsr) + if err != nil || before&haltStatus != 0 { + t.Fatalf("bench must be running: DHCSR=%#x: %v", before, err) + } + return before +} + +func exerciseM33RegistersHIL(t *testing.T, ctx context.Context, core *cortexm.Target, memory *dap.MemAP, dirty *bool) { + 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) + } + saved := readRegistersHIL(t, ctx, core) + pc := saved[cortexm.PC] + if (pc != 0x20040026 && pc != 0x20040028 && pc != 0x2004002a) || saved[cortexm.XPSR]&0x010001ff != 0x01000000 { + t.Fatal("unexpected counter execution state") + } + if saved[cortexm.SP] != saved[cortexm.MSP] { + t.Fatal("counter must use main stack") + } + exerciseRegistersHIL(t, ctx, core, saved, dirty) + for reg, want := range saved { + if got := readRegisterHIL(t, ctx, core, reg); got != want { + t.Fatalf("register %d=%#x, want %#x", reg, got, want) + } + } + after, err := memory.ReadWord(ctx, dscsr) + if err != nil || after != domain { + t.Fatalf("DSCSR before=%#x after=%#x: %v", domain, after, err) + } + *dirty = false + t.Logf("DSCSR preserved at %#x; all 19 registers restored", domain) +} diff --git a/target/cortexm/m33_register_test.go b/target/cortexm/m33_register_test.go new file mode 100644 index 0000000..8a47876 --- /dev/null +++ b/target/cortexm/m33_register_test.go @@ -0,0 +1,193 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func newM33RegisterMemory() *registerMemory { + m := newRegisterMemory() + m.cpuid, m.status = 0x411fd210, secureDebug + return m +} + +func TestM33Registers(t *testing.T) { + for _, inherited := range []bool{false, true} { + for _, process := range []bool{false, true} { + checkM33Registers(t, inherited, process) + } + } +} + +func checkM33Registers(t *testing.T, inherited, process bool) { + t.Helper() + m := newM33RegisterMemory() + m.processStack, m.delay = process, 2 + core := acquireRegisters(t, m, inherited) + for reg := cortexm.R0; reg <= cortexm.PSP; reg++ { + index := int(reg) - 1 + if reg == cortexm.SP && process { + index = 18 + } + want := m.registers[index] + got, err := core.ReadRegister(t.Context(), reg) + if err != nil || got != want { + t.Fatalf("read %d = %#x, %v; want %#x", reg, got, err, want) + } + if reg == cortexm.XPSR { + continue + } + value := uint32(0x20001000) + if err := core.WriteRegister(t.Context(), reg, value); err != nil { + t.Fatal(err) + } + if m.registers[index] != value { + t.Fatalf("write %d changed wrong register", reg) + } + if err := core.WriteRegister(t.Context(), reg, want); err != nil { + t.Fatal(err) + } + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.halted != inherited { + t.Fatal("register access changed halt ownership") + } +} + +func TestM33RegisterStatusPreservesOwnership(t *testing.T) { + for _, change := range []string{"restart", "snap", "snap and step", "permission"} { + t.Run(change, func(t *testing.T) { + m := newM33RegisterMemory() + core := acquireRegisters(t, m, false) + switch change { + case "restart": + m.status |= 1 << 26 + case "snap": + m.control |= 32 + case "snap and step": + m.control |= 32 | 4 + case "permission": + m.status = 0 + } + _, err := core.ReadRegister(t.Context(), cortexm.R4) + if change == "restart" && err != nil { + t.Fatal(err) + } + if change != "restart" && err == nil { + t.Fatal("unsafe state accepted") + } + writes := m.writes + m.control &^= 32 | 4 + m.status = secureDebug + if err := core.Resume(t.Context()); err == nil || m.writes != writes { + t.Fatal("resumed unowned or unsafe halt") + } + 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("unsafe cleanup", err) + } + }) + } +} + +func TestM33RegisterRestartDuringTransfer(t *testing.T) { + for _, write := range []bool{false, true} { + m := newM33RegisterMemory() + core := acquireRegisters(t, m, false) + m.afterSelector = func() { m.status |= 1 << 26 } + var err error + if write { + err = core.WriteRegister(t.Context(), cortexm.R4, 42) + } else { + _, err = core.ReadRegister(t.Context(), cortexm.R4) + } + if err == nil { + t.Fatal("restart and re-halt accepted as completed transfer") + } + if m.transfers != 1 { + t.Fatal("transfer did not start") + } + writes := m.writes + if err := core.Release(t.Context()); err == nil || m.writes != writes { + t.Fatal("lost transfer was released") + } + } +} + +func TestM33RegisterCancellationCleanup(t *testing.T) { + for _, write := range []bool{false, true} { + m := newM33RegisterMemory() + core := acquireRegisters(t, m, false) + ctx, cancel := context.WithCancel(t.Context()) + m.afterSelector = cancel + var err error + if write { + err = core.WriteRegister(ctx, cortexm.R4, 42) + } else { + _, err = core.ReadRegister(ctx, cortexm.R4) + } + cancel() + if !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.transfers != 1 || m.halted || m.control != 0 { + t.Fatal("cleanup replayed transfer or did not restore control") + } + } +} + +func TestM33CurrentStateStacks(t *testing.T) { + for _, nonsecure := range []bool{false, true} { + for _, process := range []bool{false, true} { + checkM33Stacks(t, nonsecure, process) + } + } +} + +func checkM33Stacks(t *testing.T, nonsecure, process bool) { + t.Helper() + m := newM33RegisterMemory() + m.nonsecure, m.processStack = nonsecure, process + m.nonsecureStacks = [2]uint32{0x20002000, 0x20003000} + core := acquireRegisters(t, m, true) + for _, reg := range []cortexm.Register{cortexm.SP, cortexm.MSP, cortexm.PSP} { + bank := 0 + if reg == cortexm.PSP || reg == cortexm.SP && process { + bank = 1 + } + want := m.registers[17+bank] + if nonsecure { + want = m.nonsecureStacks[bank] + } + got, err := core.ReadRegister(t.Context(), reg) + if err != nil || got != want { + t.Fatalf("read %d = %#x, %v; want %#x", reg, got, err, want) + } + secureBefore, nonsecureBefore := m.registers, m.nonsecureStacks + if err := core.WriteRegister(t.Context(), reg, want+4); err != nil { + t.Fatal(err) + } + if nonsecure { + if m.registers[17] != secureBefore[17] || m.registers[18] != secureBefore[18] || m.nonsecureStacks[bank] != want+4 { + t.Fatal("write did not preserve Secure stacks") + } + } else if m.nonsecureStacks != nonsecureBefore || m.registers[17+bank] != want+4 { + t.Fatal("write did not preserve Non-secure stacks") + } + } + 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 163e5f8..8ef5387 100644 --- a/target/cortexm/m33_test.go +++ b/target/cortexm/m33_test.go @@ -130,7 +130,7 @@ func TestM33RejectsControlChangesBeforeResume(t *testing.T) { } } -func TestM33DefersRegistersAndSteppingWithoutTraffic(t *testing.T) { +func TestM33DefersSteppingWithoutTraffic(t *testing.T) { m := newM33Memory() core, err := cortexm.Acquire(t.Context(), m) if err != nil { @@ -140,12 +140,6 @@ func TestM33DefersRegistersAndSteppingWithoutTraffic(t *testing.T) { 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") } diff --git a/target/cortexm/register.go b/target/cortexm/register.go index 8681675..0f4c07b 100644 --- a/target/cortexm/register.go +++ b/target/cortexm/register.go @@ -6,13 +6,15 @@ import ( "time" ) -// Register identifies a Cortex-M0 core register. Zero and unnamed values are -// invalid. The numeric values are not hardware register selectors. +// Register identifies a Cortex-M0 or Cortex-M33 core register. Zero and +// unnamed values are invalid. Numeric values are not hardware selectors. type Register uint8 // Core registers accessible through an acquired, halted target. SP is the -// current stack pointer; MSP and PSP select its banks explicitly. PC is the -// debug return address, not a Thumb function pointer. XPSR includes status. +// current stack pointer; MSP and PSP select the main and process stacks in the +// halted security state. These selectors do not change or select a security +// state. PC is the debug return address, not a Thumb function pointer. +// XPSR includes status. const ( R0 Register = iota + 1 R1 @@ -42,17 +44,19 @@ const ( sReset = uint32(1 << 25) ) -// 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 +// ReadRegister reads a Cortex-M0 or Cortex-M33 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. +// of Debug state, reset, or M33 restart during a pending transfer prevents +// automatic cleanup. // An error returns no valid register value. 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.activeM0(ctx); err != nil { + if err := t.active(ctx); err != nil { return 0, err } if err := t.waitRegister(ctx); err != nil { @@ -111,13 +115,14 @@ func (t *Target) registerStatus(ctx context.Context) error { if t.registerLost { return errors.New("cortexm: register transfer lost its debug state; cleanup cannot continue") } - value, err := t.memory.ReadWord(ctx, dhcsrAddress) + value, err := t.readDHCSR(ctx) if err != nil { t.closing = true return err } halted := value&(cDebugEnable|sHalt) == cDebugEnable|sHalt - if t.registerPending && (!halted || value&sReset != 0) { + restarted := t.identity.Part == 0xd21 && value&sRestart != 0 + if t.registerPending && (!halted || value&sReset != 0 || restarted) { t.registerLost = true return errors.New("cortexm: debug state changed during register transfer") } @@ -125,6 +130,10 @@ func (t *Target) registerStatus(ctx context.Context) error { t.closing = true return errors.New("cortexm: debug mode changed during register access") } + if err := t.validateArchitectureControl(value); err != nil { + t.closing = true + return err + } t.observeHaltRequest(value) if !halted { return errors.New("cortexm: register access requires a halted processor") diff --git a/target/cortexm/register_memory_test.go b/target/cortexm/register_memory_test.go index 912aad9..ef29954 100644 --- a/target/cortexm/register_memory_test.go +++ b/target/cortexm/register_memory_test.go @@ -24,6 +24,8 @@ type registerMemory struct { beforeStatus func() afterSelector func() processStack bool + nonsecure bool + nonsecureStacks [2]uint32 } func newRegisterMemory() *registerMemory { @@ -118,10 +120,14 @@ func (m *registerMemory) complete() { if selector == 13 { selector = bank } + register := &m.registers[selector] + if m.nonsecure && (selector == 17 || selector == 18) { + register = &m.nonsecureStacks[selector-17] + } if m.selector&(1<<16) != 0 { - m.registers[selector] = m.data + *register = m.data } else { - m.data = m.registers[selector] + m.data = *register } m.registers[13] = m.registers[bank] m.pending = false diff --git a/target/cortexm/register_write.go b/target/cortexm/register_write.go index 6cdb93a..f955802 100644 --- a/target/cortexm/register_write.go +++ b/target/cortexm/register_write.go @@ -5,8 +5,8 @@ import ( "errors" ) -// WriteRegister changes a Cortex-M0 register while halted, without acquiring halt -// ownership. XPSR is read-only. SP, MSP, and PSP require word alignment; PC +// WriteRegister changes a Cortex-M0 or Cortex-M33 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.activeM0(ctx); err != nil { + if err := t.active(ctx); err != nil { return err } if err := t.waitRegister(ctx); err != nil {