From cfa7da2151e6e843874c5a0f2861efe7753a6b00 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Wed, 30 Sep 2026 18:58:40 -0700 Subject: [PATCH] Validate independent RP2350 cores through one Arm owner. Physical M33 evidence covered core 0, leaving core-1 access and independent halt behavior unverified. Add an opt-in two-counter bench over the two MEM-APs borrowed from one Arm owner, using existing target APIs and serialized calls. Require inactive, unrouted CTIs before acquisition. Check counter progress on the peer during each halt and step, then restore both targets before closing their shared memory owner. Retain that owner if target cleanup fails. Sequential stops do not imply an atomic snapshot or coordinated control. Include explicit RAM preparation and its effects: reset core 1, disable its Secure MPU for RAM entry, and replace volatile execution state. Image verification reads through both APs without running a target algorithm. Flash and inherited CTI routing are untouched. --- README.md | 5 +- docs/architecture.md | 7 + docs/capabilities.md | 7 +- docs/composition.md | 6 + docs/cortexm.md | 104 +++++++ target/cortexm/multicore_integration_test.go | 261 ++++++++++++++++++ target/cortexm/multicore_prepare_test.go | 17 ++ .../testdata/rp2350-dual-counter/README.md | 81 ++++++ .../testdata/rp2350-dual-counter/counter.S | 24 ++ .../testdata/rp2350-dual-counter/counter.ld | 6 + .../testdata/rp2350-dual-counter/prepare.cfg | 102 +++++++ .../rp2350-dual-counter/prepare_test.tcl | 61 ++++ 12 files changed, 677 insertions(+), 4 deletions(-) create mode 100644 target/cortexm/multicore_integration_test.go create mode 100644 target/cortexm/multicore_prepare_test.go create mode 100644 target/cortexm/testdata/rp2350-dual-counter/README.md create mode 100644 target/cortexm/testdata/rp2350-dual-counter/counter.S create mode 100644 target/cortexm/testdata/rp2350-dual-counter/counter.ld create mode 100644 target/cortexm/testdata/rp2350-dual-counter/prepare.cfg create mode 100644 target/cortexm/testdata/rp2350-dual-counter/prepare_test.tcl diff --git a/README.md b/README.md index 257b772..e83a9f3 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,10 @@ 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, halted register access, and architectural -stepping over word memory; see [Cortex-M control](docs/cortexm.md). +stepping over word memory; see [Cortex-M control](docs/cortexm.md). Independent +RP2350 targets can share one Arm owner with serialized calls; the +[two-core bench](docs/cortexm.md#rp2350-independent-core-bench) records halt, +step, peer progress, and cleanup observations. 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 f022fb7..64dbbc8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -61,6 +61,11 @@ banking, AP identification, raw AP addresses, power ownership, and MEM-AP state stay in `dap`. Higher layers should call these packages rather than reproduce their framing. +A target owns one processor. Several targets may borrow distinct MEM-APs from +one `armdebug.Conn`; all calls over that shared owner remain serialized. +Independent RP2350 core control has [physical evidence][dual-core]. It does not +provide group ownership, CTI routing, or a simultaneous memory snapshot. + ## Discovery and opening `discover.Registry` stores immutable transport providers. Registration is @@ -409,3 +414,5 @@ The layers are not entirely passive: Callers should always complete the documented release sequence, including when the primary operation fails. + +[dual-core]: cortexm.md#rp2350-independent-core-bench diff --git a/docs/capabilities.md b/docs/capabilities.md index 62b1cc0..ed3f9b5 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -301,6 +301,7 @@ skips have hardware-independent test coverage. | Cortex-M33 step | HIL | Two fresh RP2350 core-0/J-Link sessions at 1 MHz checked PC/R0/RAM across 13 steps each, resume, and release with disabled debug restored. Secure counter state and DSCSR were preserved. Permission, snap-stall, restart after completion, and failure cleanup have behavioral coverage; see the [RP2350 step bench](cortexm.md#rp2350-step-bench). | | Register reads | Yes | Halted Cortex-M0/M33 R0–R12, SP, LR, PC, XPSR, MSP, and PSP through `ReadRegister`. Two fresh sessions each on CMSIS-DAP/micro:bit and J-Link/RP2350 core 0 read all 19 registers; transfer failures and cleanup have behavioral coverage. | | Register writes | Yes | Halted Cortex-M0/M33 writes except XPSR; aligned SP/MSP/PSP and even PC values. Writes persist after release. Behavioral tests cover staging, uncertain selection, and pending cleanup. Two sessions each on micro:bit and RP2350 core 0 wrote and restored R4, SP, MSP, PSP, and PC before resuming; see the [micro:bit](cortexm.md#register-bench) and [RP2350](cortexm.md#rp2350-register-bench) register benches. | +| Independent RP2350 cores | HIL | Two fresh J-Link sessions borrowed core-0/core-1 MEM-APs under one Arm owner. Each core halted and stepped while its peer advanced; both restored disabled debug and running state with inactive CTIs unchanged. See the [independent-core bench](cortexm.md#rp2350-independent-core-bench). This provides serialized per-core control, without group or CTI ownership. | | 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. | @@ -345,9 +346,9 @@ SWD, and use the volatile DAP and MEM-AP state described above. ## Not currently provided There is no CMSIS-DAP HID/v1 transport, automatic probe discovery policy, -multi-core or SoC attachment, general target control, semihosting, trace, -debugger protocol server, firmware flashing, FPGA programming, or Windows host -implementation. +automatic SoC attachment, group or CTI control, general target control, +semihosting, trace, debugger protocol server, firmware flashing, FPGA +programming, or Windows host implementation. Treat an absent capability as an explicit boundary. Do not infer it from the project description or recreate its lower-level protocol inside an application. diff --git a/docs/composition.md b/docs/composition.md index 2a8139d..b47a859 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -744,6 +744,12 @@ 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. +For independent RP2350 processors, borrow both MEM-APs from one Arm owner and +acquire a separate target for each. Serialize calls over that connection and +release both targets before closing their memory owner. The +[two-core composition and bench](cortexm.md#independent-rp2350-cores) show the +per-core ownership boundary and physical observations. + ## Release in reverse order A complete Cortex-M identity composition acquires and releases state in one of diff --git a/docs/cortexm.md b/docs/cortexm.md index 907ca42..491fbee 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -246,6 +246,68 @@ Hardware-independent tests model DHCSR control and execution state, including partial writes, canceled operations, ignored writes, failed cleanup, and retry. They do not establish physical halt/resume behavior on a bench program. +## Independent RP2350 cores + +One `armdebug.Conn` can lend core 0's MEM-AP at `0x2000` and core 1's MEM-AP at +`0x4000`. Acquire a separate `cortexm.Target` for each. Serialize all calls over +the shared connection, including memory reads, target operations, and cleanup. +Each target owns only its processor's debug state and halt requests. Halting one +target does not claim ownership of a stop on the other. + +For an already-open Arm owner `c`, the caller supplies operation `ctx` and a +live `cleanupCtx`, including after cancellation: + +```go +var cores [2]*cortexm.Target +var err error +for i, base := range []uint64{0x2000, 0x4000} { + ap, e := dap.APAt(base) + if e != nil { + err = e + break + } + memory, e := c.OpenMemAP(ctx, ap) + if e != nil { + err = e + break + } + cores[i], err = cortexm.Acquire(ctx, memory) + if err != nil { + break + } +} +if err == nil { + err = cores[0].Halt(ctx) +} +if err == nil { + var halted bool + halted, err = cores[1].Halted(ctx) + if err == nil { + fmt.Printf("core 1 halted=%v\n", halted) + } +} +if err == nil { + err = cores[0].Resume(ctx) +} +var releaseErr error +for i := len(cores) - 1; i >= 0; i-- { + releaseErr = errors.Join(releaseErr, cores[i].Release(cleanupCtx)) +} +err = errors.Join(err, releaseErr) +if releaseErr == nil { + err = errors.Join(err, c.Close()) +} +// Retain targets and c if target cleanup fails; retain c if Close fails. +``` + +A failed acquisition can return a non-nil target, so store it before handling +the error. Release every retained target before closing its memory owner; a +failed release leaves that owner live for retry. A target acquired while its +processor is halted cannot resume that inherited halt. Existing cross-trigger +routing can couple stops: inspect the bench's routing before expecting +independent progress. This composition provides per-core control, without group +ownership, coordinated stopping, or a simultaneous snapshot. + ## Hardware procedure The opt-in integration test selects the CMSIS-DAP micro:bit with serial @@ -475,3 +537,45 @@ after a completed halt, and failure cleanup have behavioral coverage. These sessions do not establish sleeping-instruction behavior, Non-secure execution, core-1 control, or cross-core coordination. State after Arm owner close was not independently measured. Flash was untouched and the counter remains running. + +## RP2350 independent-core bench + +`TestHILRP2350IndependentCores` selects J-Link EDU Mini V2 `000802011345` at 1 +MHz, opens one Arm owner, and borrows AP `0x2000` and AP `0x4000`. Prepare the +[separate RAM counters][dual-counter] first. That procedure replaces both cores' +volatile execution state, resets core 1, and disables its Secure MPU for RAM +entry; it does not change flash or CTI routing. + +```sh +OSTIOLE_RP2350_HIL_DUAL_CORE=1 \ +OSTIOLE_RP2350_HIL_PROGRAM=bf878b47815bc5eaf6afb5279efaa6ff163832bd178c5b7b1604f80e6ad6cde9 \ +go test -tags integration ./target/cortexm -run '^TestHILRP2350IndependentCores$' -count=1 -v +``` + +The test requires the known instruction words, running Secure counters, and +inactive CTIs before acquiring either target. It checks CTI architecture and +geometry, disabled control and integration mode, zero application triggers, +output and input-channel status, and all eight input/output routes. It reads and +preserves CTIGATE. It never writes routing or acknowledgements. + +On Nostalgia, two fresh sessions read all 19 registers on each core and checked +one architectural step per core against PC, R0, and its counter word. Core 0's +counter stayed unchanged during its halt and after its step while core 1 +advanced; the reverse held when core 1 was halted and stepped. Both counters +stopped after sequential halt requests. Resuming only core 0 left core 1 +stopped; release from an owned halt restored progress on each core. + +Both sessions restored initially disabled halting debug and running state on +both cores before Arm owner close, with DHCSR `0x01100000` before/after and +DSCSR `0x00030000` unchanged. Both CTIs remained disabled and unrouted, with +zero pending output/input-channel status and CTIGATE `0x0f` unchanged. Both +targets released and the shared owner closed successfully. + +This covers the prepared Secure counter programs and serialized per-core +operations. It does not establish simultaneous stopping, CTI propagation, +Non-secure execution, sleeping instructions, or cross-core failure recovery. +Per-target cleanup failures have behavioral coverage; this bench does not inject +failures. State after Arm owner close was not independently measured. Both loops +remain running; preparation and instruction effects are not undone. + +[dual-counter]: ../target/cortexm/testdata/rp2350-dual-counter/README.md diff --git a/target/cortexm/multicore_integration_test.go b/target/cortexm/multicore_integration_test.go new file mode 100644 index 0000000..e2f0213 --- /dev/null +++ b/target/cortexm/multicore_integration_test.go @@ -0,0 +1,261 @@ +//go:build integration + +package cortexm_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/jon/ostiole/armdebug" + "github.com/jon/ostiole/dap" + "github.com/jon/ostiole/target/cortexm" +) + +const dualCounterProgram = "bf878b47815bc5eaf6afb5279efaa6ff163832bd178c5b7b1604f80e6ad6cde9" + +func TestHILRP2350IndependentCores(t *testing.T) { + if os.Getenv("OSTIOLE_RP2350_HIL_DUAL_CORE") != "1" { + t.Skip("require OSTIOLE_RP2350_HIL_DUAL_CORE=1") + } + if os.Getenv("OSTIOLE_RP2350_HIL_PROGRAM") != dualCounterProgram { + t.Fatal("require the documented two-core counter binary identity") + } + for range 2 { + if !t.Run("session", dualCoreHIL) { + return + } + } +} + +type dualCoreBench struct { + memory *dap.MemAP + core *cortexm.Target + before uint32 + domain uint32 + cti []uint32 +} + +func dualCoreHIL(t *testing.T) { + t.Helper() + ctx, cancel := context.WithTimeout(t.Context(), 90*time.Second) + defer cancel() + bench := controlBench{name: "RP2350 independent cores", provider: "jlink", serial: "000802011345"} + c := bench.open(t, ctx) + var cores [2]dualCoreBench + t.Cleanup(func() { releaseDualCoreBench(t, &cores, c) }) + for i, base := range []uint64{0x2000, 0x4000} { + ap, err := dap.APAt(base) + if err != nil { + t.Fatal(err) + } + cores[i].memory, err = c.OpenMemAP(ctx, ap) + if err != nil { + t.Fatal(err) + } + checkDualCoreBench(t, ctx, &cores[i], i) + } + checkDualCounters(t, ctx, &cores, "before acquisition", [2]bool{}) + for i := range cores { + var err error + cores[i].core, err = cortexm.Acquire(ctx, cores[i].memory) + if err != nil { + t.Fatal(err) + } + } + exerciseDualCores(t, ctx, &cores) + for i := range cores { + checkDualCoreRestored(t, ctx, &cores[i], i) + } +} + +func checkDualCoreBench(t *testing.T, ctx context.Context, b *dualCoreBench, index int) { + t.Helper() + identity, err := cortexm.Identify(ctx, b.memory) + if err != nil || identity.Raw != 0x411fd210 { + t.Fatalf("core %d CPUID=%#x: %v", index, identity.Raw, err) + } + b.cti = readInactiveCTI(t, ctx, b.memory) + for i, want := range []uint32{0x4902b672, 0x30012000, 0xe7fc6008, 0x20040000 + uint32(index)*4} { + addr := uint32(0x20040020 + index*0x40 + i*4) + got, err := b.memory.ReadWord(ctx, addr) + if err != nil || got != want { + t.Fatalf("core %d counter code %#x=%#x, want %#x: %v", index, addr, got, want, err) + } + } + b.before, err = b.memory.ReadWord(ctx, dhcsr) + if err != nil || b.before&haltStatus != 0 { + t.Fatalf("core %d must be running: DHCSR=%#x: %v", index, b.before, err) + } + b.domain, err = b.memory.ReadWord(ctx, 0xe000ee08) + if err != nil || b.domain&(1<<16) == 0 { + t.Fatalf("core %d requires Secure counter state: DSCSR=%#x: %v", index, b.domain, err) + } +} + +func readInactiveCTI(t *testing.T, ctx context.Context, memory *dap.MemAP) []uint32 { + t.Helper() + var values []uint32 + for _, item := range []struct{ offset, want uint32 }{ + {0xfbc, 0x47701a14}, {0xfc8, 0x00040800}, + {0, 0}, {0x14, 0}, {0x134, 0}, {0x138, 0}, {0xf00, 0}, + } { + got, err := memory.ReadWord(ctx, 0xe0042000+item.offset) + if err != nil || got != item.want { + t.Fatalf("require inactive RP2350 CTI: offset=%#x value=%#x want=%#x: %v", item.offset, got, item.want, err) + } + values = append(values, got) + } + gate, err := memory.ReadWord(ctx, 0xe0042140) + if err != nil { + t.Fatal(err) + } + values = append(values, gate) + for i := uint32(0); i < 8; i++ { + for _, offset := range []uint32{0x20, 0xa0} { + got, err := memory.ReadWord(ctx, 0xe0042000+offset+i*4) + if err != nil || got != 0 { + t.Fatalf("require unrouted CTI: offset=%#x value=%#x: %v", offset+i*4, got, err) + } + values = append(values, got) + } + } + return values +} + +func exerciseDualCores(t *testing.T, ctx context.Context, cores *[2]dualCoreBench) { + t.Helper() + for i := range cores { + if err := cores[i].core.Halt(ctx); err != nil { + t.Fatal(err) + } + stopped := [2]bool{} + stopped[i] = true + checkDualCounters(t, ctx, cores, "one core halted", stopped) + checkDualCoreRegisters(t, ctx, &cores[i], i) + checkCounterStepAtHIL(t, ctx, cores[i].core, cores[i].memory, i, uint32(0x20040026+i*0x40), uint32(0x20040000+i*4)) + checkDualCounters(t, ctx, cores, "one core stepped", stopped) + if err := cores[i].core.Resume(ctx); err != nil { + t.Fatal(err) + } + checkDualCounters(t, ctx, cores, "both resumed", [2]bool{}) + } + for i := range cores { + if err := cores[i].core.Halt(ctx); err != nil { + t.Fatal(err) + } + } + checkDualCounters(t, ctx, cores, "both halted sequentially", [2]bool{true, true}) + if err := cores[0].core.Resume(ctx); err != nil { + t.Fatal(err) + } + checkDualCounters(t, ctx, cores, "only core 0 resumed", [2]bool{false, true}) + if err := cores[1].core.Release(ctx); err != nil { + t.Fatal(err) + } + checkDualCounters(t, ctx, cores, "core 1 released from halt", [2]bool{}) + if err := cores[0].core.Halt(ctx); err != nil { + t.Fatal(err) + } + if err := cores[0].core.Release(ctx); err != nil { + t.Fatal(err) + } + checkDualCounters(t, ctx, cores, "both released", [2]bool{}) +} + +func checkDualCoreRegisters(t *testing.T, ctx context.Context, b *dualCoreBench, index int) { + t.Helper() + saved := readRegistersHIL(t, ctx, b.core) + pc := saved[cortexm.PC] + start := uint32(0x20040026 + index*0x40) + if (pc != start && pc != start+2 && pc != start+4) || saved[cortexm.R1] != uint32(0x20040000+index*4) || saved[cortexm.XPSR]&0x010001ff != 0x01000000 { + t.Fatalf("core %d unexpected counter registers: PC=%#x R1=%#x XPSR=%#x", index, pc, saved[cortexm.R1], saved[cortexm.XPSR]) + } + t.Logf("core %d: all 19 registers read, PC=%#x R1=%#x", index, pc, saved[cortexm.R1]) +} + +func checkDualCounters(t *testing.T, ctx context.Context, cores *[2]dualCoreBench, phase string, stopped [2]bool) { + t.Helper() + var first, last [2]uint32 + var changed [2]bool + for n := range 11 { + if n != 0 { + select { + case <-ctx.Done(): + t.Fatal(ctx.Err()) + case <-time.After(20 * time.Millisecond): + } + } + for i := range cores { + value, err := cores[i].memory.ReadWord(ctx, uint32(0x20040000+i*4)) + if err != nil { + t.Fatal(err) + } + if n == 0 { + first[i] = value + } + last[i] = value + changed[i] = changed[i] || value != first[i] + } + } + t.Logf("%s: first=%#x last=%#x changed=%v stopped=%v", phase, first, last, changed, stopped) + for i := range cores { + if changed[i] == stopped[i] { + t.Fatalf("core %d changed=%v stopped=%v", i, changed[i], stopped[i]) + } + } +} + +func checkDualCoreRestored(t *testing.T, ctx context.Context, b *dualCoreBench, index int) { + t.Helper() + after, err := b.memory.ReadWord(ctx, dhcsr) + mask := debugEnable | haltStatus + if b.before&debugEnable != 0 { + mask |= 12 + } + if err != nil || after&mask != b.before&mask { + t.Fatalf("core %d DHCSR before=%#x after=%#x: %v", index, b.before, after, err) + } + domain, err := b.memory.ReadWord(ctx, 0xe000ee08) + if err != nil || domain != b.domain { + t.Fatalf("core %d DSCSR before=%#x after=%#x: %v", index, b.domain, domain, err) + } + for i, value := range readInactiveCTI(t, ctx, b.memory) { + if value != b.cti[i] { + t.Fatalf("core %d CTI snapshot changed", index) + } + } + t.Logf("core %d AP=%#x CPUID=0x411fd210 DHCSR before=%#x after=%#x DSCSR=%#x CTI unchanged (gate=%#x)", index, 0x2000+index*0x2000, b.before, after, domain, b.cti[7]) +} + +func releaseDualCoreBench(t *testing.T, cores *[2]dualCoreBench, c *armdebug.Conn) { + t.Helper() + pending := false + for i := len(cores) - 1; i >= 0; i-- { + var err error + for range 3 { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + err = cores[i].core.Release(ctx) + cancel() + if err == nil { + break + } + } + if err != nil { + t.Errorf("core %d cleanup pending; retaining Arm owner: %v", i, err) + pending = true + } + } + if pending { + return + } + var err error + for range 3 { + if err = c.Close(); err == nil { + t.Log("both targets released and shared Arm debug owner closed") + return + } + } + t.Errorf("shared Arm debug owner cleanup remains pending: %v", err) +} diff --git a/target/cortexm/multicore_prepare_test.go b/target/cortexm/multicore_prepare_test.go new file mode 100644 index 0000000..f32596b --- /dev/null +++ b/target/cortexm/multicore_prepare_test.go @@ -0,0 +1,17 @@ +package cortexm_test + +import ( + "os/exec" + "testing" +) + +func TestRP2350PreparationGuards(t *testing.T) { + path, err := exec.LookPath("tclsh") + if err != nil { + t.Skip("tclsh is not installed") + } + cmd := exec.CommandContext(t.Context(), path, "testdata/rp2350-dual-counter/prepare_test.tcl") + if output, err := cmd.CombinedOutput(); err != nil { + t.Fatalf("preparation guard regression: %v\n%s", err, output) + } +} diff --git a/target/cortexm/testdata/rp2350-dual-counter/README.md b/target/cortexm/testdata/rp2350-dual-counter/README.md new file mode 100644 index 0000000..c92bd79 --- /dev/null +++ b/target/cortexm/testdata/rp2350-dual-counter/README.md @@ -0,0 +1,81 @@ +# RP2350 independent RAM counters + +Two Cortex-M33 loops increment separate words in shared SRAM. Core 0 uses +`0x20040000` with code at `0x20040020`; core 1 uses `0x20040004` with code at +`0x20040060`. Each loop adds one to R0, stores through R1, and branches back. +Neither loop uses a stack, peripherals, or DMA. Both run in Secure Thread mode +with configurable interrupts disabled. + +Build from the repository root: + +```sh +clang --target=arm-none-eabi -mcpu=cortex-m33 -mthumb -c \ + target/cortexm/testdata/rp2350-dual-counter/counter.S \ + -o /tmp/ostiole-rp2350-dual-counter.o +ld.lld -T target/cortexm/testdata/rp2350-dual-counter/counter.ld \ + /tmp/ostiole-rp2350-dual-counter.o -o /tmp/ostiole-rp2350-dual-counter.elf +arm-none-eabi-objcopy -O binary /tmp/ostiole-rp2350-dual-counter.elf \ + /tmp/ostiole-rp2350-dual-counter.bin +shasum -a 256 /tmp/ostiole-rp2350-dual-counter.bin +``` + +The binary SHA-256 is +`bf878b47815bc5eaf6afb5279efaa6ff163832bd178c5b7b1604f80e6ad6cde9`. + +## Preparation + +Stop other debuggers. The preparation script selects J-Link EDU Mini V2 +`000802011345` at 1 MHz and creates independent OpenOCD targets at ADIv6 AP +`0x2000` and `0x4000`. It requires both inherited CTIs to be disabled and +unrouted, with no pending output or input channel, integration mode disabled, +and the default channel gate `0x0f`. It rejects core 1 held in inherited reset. +Secure execution, Secure debugger register-bank selection, and Secure invasive +debug permission are required before preparation and again immediately before +the core-1 MPU write. A conflicting override is rejected without changing DSCSR. +No CTI configuration or acknowledgement is written. + +```sh +openocd -f target/cortexm/testdata/rp2350-dual-counter/prepare.cfg +``` + +Preparation resets core 1 through PSM FRCE_OFF.PROC1, using the atomic set/clear +aliases and preserving other reset bits. It then halts both cores, writes +`0x20040000`–`0x2004006f`, and compares all 28 words through both MEM-APs. This +verification reads memory on the host; it runs no checksum algorithm on either +processor. Core 1's boot-ROM MPU configuration prevents direct entry into this +RAM loop, so preparation disables its Secure MPU and sets its Secure MSP to +`0x20043000`. Both PCs, XPSRs, and interrupt masks are set before resume, then +halting debug is disabled. The loops overwrite R0/R1 and counter values. + +Flash, OTP, core-0 reset, and CTI routing are untouched. The previous programs, +RAM contents, core-1 reset effects and MPU control, PCs, XPSRs, registers, stack +pointer, and interrupt masks are not restored. Use a bench where replacing both +cores' volatile execution state is acceptable, with Secure invasive debug +permitted and no other processor or DMA writer using this SRAM. This prepares a +dedicated bench; Ostiole's HIL test does not perform these setup effects. + +## Validation + +Run the [independent-core test][dual-bench] after preparation. It requires the +program identity and explicit effect gate, verifies counter code and inactive +CTIs before acquiring target control, and opens one shared Arm debug owner per +session. Calls remain serialized. Each core can halt and step while its peer +advances. Both targets are released before the memory owner closes. + +The test restores inherited halting debug state. It leaves both RAM loops +running and does not undo their instruction effects or bench preparation. +Sequential stopping does not establish simultaneous halt or an atomic memory +snapshot. CTI propagation and acknowledgement require separate ownership. + +[dual-bench]: ../../../../docs/cortexm.md#rp2350-independent-core-bench + +Preparation guards can also be tested without a probe: + +```sh +tclsh target/cortexm/testdata/rp2350-dual-counter/prepare_test.tcl +``` + +The mocked OpenOCD commands exercise the actual script, including a Non-secure +bank override inherited before preparation or observed after core-1 reset, +permission loss, Non-secure execution, and inherited CTI routing. The Go test +runs this check when `tclsh` is installed; absence skips that fixture check. diff --git a/target/cortexm/testdata/rp2350-dual-counter/counter.S b/target/cortexm/testdata/rp2350-dual-counter/counter.S new file mode 100644 index 0000000..86de19f --- /dev/null +++ b/target/cortexm/testdata/rp2350-dual-counter/counter.S @@ -0,0 +1,24 @@ +.syntax unified +.cpu cortex-m33 +.thumb + +.section .counters, "aw", %progbits +.word 0, 0 + +.macro counter core, address +.section .text.core\core, "ax", %progbits +.thumb_func +.global counter_start\core +counter_start\core: + cpsid i + ldr r1, =\address + movs r0, #0 +counter_loop\core: + adds r0, #1 + str r0, [r1] + b counter_loop\core +.ltorg +.endm + +counter 0, 0x20040000 +counter 1, 0x20040004 diff --git a/target/cortexm/testdata/rp2350-dual-counter/counter.ld b/target/cortexm/testdata/rp2350-dual-counter/counter.ld new file mode 100644 index 0000000..27cab5e --- /dev/null +++ b/target/cortexm/testdata/rp2350-dual-counter/counter.ld @@ -0,0 +1,6 @@ +ENTRY(counter_start0) +SECTIONS { + .counters 0x20040000 : { *(.counters) } + .core0 0x20040020 : { *(.text.core0) } + .core1 0x20040060 : { *(.text.core1) } +} diff --git a/target/cortexm/testdata/rp2350-dual-counter/prepare.cfg b/target/cortexm/testdata/rp2350-dual-counter/prepare.cfg new file mode 100644 index 0000000..018557d --- /dev/null +++ b/target/cortexm/testdata/rp2350-dual-counter/prepare.cfg @@ -0,0 +1,102 @@ +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 +target create rp2350.core1 cortex_m -dap rp2350.dap -ap-num 0x4000 + +proc require_inactive_cti {} { + foreach address {0xe0042000 0xe0042014 0xe0042134 0xe0042138 0xe0042f00} { + if {[lindex [read_memory $address 32 1] 0] != 0} { + error "Inherited CTI is active; refusing bench preparation" + } + } + for {set i 0} {$i < 8} {incr i} { + foreach base {0xe0042020 0xe00420a0} { + if {[lindex [read_memory [expr {$base + 4*$i}] 32 1] 0] != 0} { + error "Inherited CTI routing is present; refusing bench preparation" + } + } + } + if {[lindex [read_memory 0xe0042140 32 1] 0] != 15} { + error "Require default inherited CTI gate" + } + echo [mdw 0xe0042000] + echo [mdw 0xe0042140] + echo [mdw 0xe000ee08] +} + +proc require_secure_bank {} { + set domain [lindex [read_memory 0xe000ee08 32 1] 0] + if {($domain & 0x10000) == 0 || ($domain & 3) == 1} { + error "Require Secure execution and debugger bank selection" + } + if {([lindex [read_memory 0xe000edf0 32 1] 0] & 0x00100000) == 0} { + error "Require Secure invasive debug permission" + } +} + +init +foreach core {rp2350.core0 rp2350.core1} { + targets $core + require_inactive_cti + require_secure_bank +} +targets rp2350.core0 +if {([lindex [read_memory 0x40018004 32 1] 0] & 0x01000000) != 0} { + error "Core 1 is held in reset; refusing to release inherited reset" +} +mww 0x4001a004 0x01000000 +if {([lindex [read_memory 0x40018004 32 1] 0] & 0x01000000) == 0} { + error "Core 1 reset assertion unconfirmed" +} +mww 0x4001b004 0x01000000 +sleep 20 +foreach core {rp2350.core0 rp2350.core1} { + targets $core + halt +} +targets rp2350.core0 +load_image /tmp/ostiole-rp2350-dual-counter.bin 0x20040000 bin +set expected { + 0x00000000 0x00000000 0x00000000 0x00000000 + 0x00000000 0x00000000 0x00000000 0x00000000 + 0x4902b672 0x30012000 0xe7fc6008 0x20040000 + 0x00000000 0x00000000 0x00000000 0x00000000 + 0x00000000 0x00000000 0x00000000 0x00000000 + 0x00000000 0x00000000 0x00000000 0x00000000 + 0x4902b672 0x30012000 0xe7fc6008 0x20040004 +} +foreach core {rp2350.core0 rp2350.core1} { + targets $core + foreach got [read_memory 0x20040000 32 28] want $expected { + if {$got != $want} { + error "RAM image verification failed" + } + } +} +echo "Both RAM counter images loaded and verified through both MEM-APs" +foreach {core start} {rp2350.core0 0x20040020 rp2350.core1 0x20040060} { + targets $core + if {$core eq "rp2350.core1"} { + require_secure_bank + echo "Core 1 boot MPU control before RAM entry: [mdw 0xe000ed94]" + mww 0xe000ed94 0 + reg msp_s 0x20043000 + } + reg primask 1 + echo "Configured interrupt mask: [reg primask]" + echo "Inherited security control: [reg control_s]" + reg pc $start + reg xpsr 0x01000000 + resume + mww 0xe000edf0 0xa05f0000 + echo [mdw 0xe000edf0] +} +echo "Both counters running with halting debug disabled; CTI unchanged" +shutdown diff --git a/target/cortexm/testdata/rp2350-dual-counter/prepare_test.tcl b/target/cortexm/testdata/rp2350-dual-counter/prepare_test.tcl new file mode 100644 index 0000000..3c1b05a --- /dev/null +++ b/target/cortexm/testdata/rp2350-dual-counter/prepare_test.tcl @@ -0,0 +1,61 @@ +set fixture [file join [file dirname [info script]] prepare.cfg] + +proc run_case {name before after permission route should_fail} { + set mock [interp create] + $mock eval { + rename source real_source + proc source args {} + proc find args { return interface } + foreach command {transport adapter gdb_port tcl_port telnet_port swd dap target init sleep halt echo reg resume shutdown} { + proc $command args {} + } + set selected rp2350.core0 + set reset 0 + set reset_done 0 + set writes {} + set loaded 0 + proc targets {core} { set ::selected $core } + proc mdw args { return 0 } + proc mww {address value} { + lappend ::writes $address + if {$address == 0x4001a004} { set ::reset 0x01000000 } + if {$address == 0x4001b004} { set ::reset 0; set ::reset_done 1 } + } + proc load_image args { set ::loaded 1 } + proc verify_image args { error "Target checksum algorithms must not run" } + proc read_memory {address width count} { + if {$address == 0x20040000} { return $::expected } + if {$address == 0x40018004} { return [list $::reset] } + if {$address == 0xe000ee08} { + if {$::selected eq "rp2350.core0"} { return {0x30000} } + return [list [expr {$::reset_done ? $::after : $::before}]] + } + if {$address == 0xe000edf0} { return [list $::permission] } + if {$address == 0xe0042140} { return {15} } + if {$address == 0xe0042020 && $::selected eq "rp2350.core1"} { return [list $::route] } + return {0} + } + } + $mock eval [list set before $before] + $mock eval [list set after $after] + $mock eval [list set permission $permission] + $mock eval [list set route $route] + set failed [catch {$mock eval [list real_source $::fixture]} result] + set writes [$mock eval {set writes}] + interp delete $mock + if {$failed != $should_fail} { + error "$name: expected failure=$should_fail, got failure=$failed ($result)" + } + if {$should_fail && [lsearch -exact $writes 0xe000ed94] != -1} { + error "$name: wrote MPU before rejecting the conflicting state" + } + puts "$name: passed" +} + +run_case current-secure 0x30000 0x30000 0x100000 0 0 +run_case selected-secure 0x30003 0x30003 0x100000 0 0 +run_case selected-nonsecure 0x30001 0x30001 0x100000 0 1 +run_case override-after-reset 0x30000 0x30001 0x100000 0 1 +run_case nonsecure-execution 0x20000 0x20000 0x100000 0 1 +run_case permission-lost 0x30000 0x30000 0 0 1 +run_case inherited-route 0x30000 0x30000 0x100000 1 1