From 13088dc99ad6434452877c90af00cc8030119dc4 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sat, 26 Sep 2026 13:44:43 -0700 Subject: [PATCH 1/4] Acquire Cortex-M0 halting debug over word memory. Processor identity alone gives callers no owner for halting debug state. Enable Cortex-M0 halting debug through borrowed word memory, preserve its inherited control, and retain restoration state for release retries. Reject unsupported cores and debug modes before writes; failed setup uses an independent cleanup deadline. --- README.md | 4 +- dap/memap.go | 6 + dap/memap_word_test.go | 26 +++ docs/README.md | 2 + docs/architecture.md | 8 +- docs/capabilities.md | 7 +- docs/composition.md | 8 +- docs/cortexm.md | 45 +++++ target/cortexm/acquire_cancellation_test.go | 36 ++++ target/cortexm/control.go | 166 ++++++++++++++++ target/cortexm/control_test.go | 210 ++++++++++++++++++++ target/cortexm/identity.go | 3 +- target/cortexm/ignored_enable_test.go | 64 ++++++ target/cortexm/restore.go | 23 +++ 14 files changed, 598 insertions(+), 10 deletions(-) create mode 100644 dap/memap_word_test.go create mode 100644 docs/cortexm.md create mode 100644 target/cortexm/acquire_cancellation_test.go create mode 100644 target/cortexm/control.go create mode 100644 target/cortexm/control_test.go create mode 100644 target/cortexm/ignored_enable_test.go create mode 100644 target/cortexm/restore.go diff --git a/README.md b/README.md index 24bcab0..ef39c5a 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,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. +through any compatible target-word reader. `cortexm.Acquire` enables Cortex-M0 +halting debug over borrowed word memory and retains its restoration state; +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/dap/memap.go b/dap/memap.go index 0c61aac..1594607 100644 --- a/dap/memap.go +++ b/dap/memap.go @@ -128,6 +128,12 @@ func (m *MemAP) ReadWord(ctx context.Context, addr uint32) (uint32, error) { return uint32(value), err } +// WriteWord writes one aligned 32-bit target word and waits for AP completion. +// It has the same effects and failure rules as WriteScalar with Size32. +func (m *MemAP) WriteWord(ctx context.Context, addr, value uint32) error { + return m.WriteScalar(ctx, uint64(addr), Size32, uint64(value)) +} + // ReadScalar performs one aligned, sized target-memory read. The returned // value is right-justified regardless of target byte order or address lane. func (m *MemAP) ReadScalar(ctx context.Context, addr uint64, size TransferSize) (uint64, error) { diff --git a/dap/memap_word_test.go b/dap/memap_word_test.go new file mode 100644 index 0000000..1b5f2cb --- /dev/null +++ b/dap/memap_word_test.go @@ -0,0 +1,26 @@ +package dap_test + +import ( + "testing" + + "github.com/jon/ostiole/dap" + "github.com/jon/ostiole/dap/sim" +) + +func TestMEMAPWriteWord(t *testing.T) { + target := sim.New(0x2ba01477) + addMEMAP(t, target, 0, 0x00010001, nil) + mem, err := dap.OpenMemAP(t.Context(), enteredDAPClient(t, target), apSel(0)) + if err != nil { + t.Fatal(err) + } + if err := mem.WriteWord(t.Context(), 0x100, 0x12345678); err != nil { + t.Fatal(err) + } + if got, err := mem.ReadWord(t.Context(), 0x100); err != nil || got != 0x12345678 { + t.Fatalf("ReadWord = %#x, %v", got, err) + } + if err := mem.WriteWord(t.Context(), 0x101, 0); err == nil { + t.Fatal("unaligned WriteWord succeeded") + } +} diff --git a/docs/README.md b/docs/README.md index d002b6b..1fafc76 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,8 @@ today 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 halting-debug acquisition + and restoration over borrowed word memory. - [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 c5300f7..2387bc8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -51,7 +51,7 @@ debugger 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` | Read and decode the architectural Cortex-M CPUID value. | +| `target/cortexm` | Identify Cortex-M processors and acquire Cortex-M0 halting debug 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,8 +334,10 @@ children with power-domain metadata and reports an incomplete result. It uses DAP transfer sizes but owns no DAP or MEM-AP state. See [CoreSight component identity](coresight.md) for its register and failure boundaries. -`target/cortexm` depends only on a compatible word reader. It knows the CPUID -address and encoding, but it does not know about USB, FTDI, or SWD. +`target/cortexm` identifies processors through a word reader. Acquisition of +Cortex-M0 halting debug also requires completed word writes. Release the target +before the memory owner. See [Cortex-M control](cortexm.md) for effects and +restoration limits. ## Host implementations diff --git a/docs/capabilities.md b/docs/capabilities.md index 42db91f..1b28abc 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -245,7 +245,7 @@ See [JTAG](protocols/jtag.md) for effects and ownership. | MEM-AP acquisition | Yes | `OpenMemAP` performs AP traffic, rejects an absent or non-MEM AP, and snapshots the state which `Release` restores. | | MEM-AP debug entry | Yes | `ReadDebugBase` decodes ADIv5 and legacy BASE formats, distinguishes absence from address zero, and reads the upper word only for a present entry with CFG.LA. It preserves the memory client on success and does not access target memory. Behavioral tests cover formats, malformed values, cancellation, failure, retry, and shared SWD/JTAG access. | | MEM-AP configuration | Yes | `OpenMemAP` reads CFG, models BE, LA, and LD, and includes TARHI in retryable restoration when large addresses are available. | -| Scalar target-memory access | Yes | `ReadScalar` and `WriteScalar` support aligned 8-, 16-, and 32-bit values and verify the implementation-defined CSW.Size before using the byte lane selected by CFG.BE. CFG.LA permits addresses above 32 bits; CFG.LD makes 64-bit access eligible for the same CSW check. Oversized write values fail before traffic, and writes finish with an AP completion barrier. If the first DRW access of a failed Size64 transfer might have started, ordinary traffic remains blocked until cleanup. `ReadWord` provides the 32-bit convenience operation. | +| Scalar target-memory access | Yes | `ReadScalar` and `WriteScalar` support aligned 8-, 16-, and 32-bit values and verify the implementation-defined CSW.Size before using the byte lane selected by CFG.BE. CFG.LA permits addresses above 32 bits; CFG.LD makes 64-bit access eligible for the same CSW check. Oversized write values fail before traffic, and writes finish with an AP completion barrier. If the first DRW access of a failed Size64 transfer might have started, ordinary traffic remains blocked until cleanup. `ReadWord` and `WriteWord` provide 32-bit convenience operations. | | MEM-AP restoration | Yes | Saves and restores CSW, TAR, and TARHI when present; failed restoration remains retryable. MEM-AP restoration remains available while debug-port cleanup is pending. If framing is unknown, `Release` re-enters the bound protocol and verifies identity before restoration. It terminates a possibly incomplete Size64 transfer through CSW before touching TAR or TARHI. If DAPABORT interrupts cleanup, the next `Release` retries every saved value. The invalidated handle remains invalid. | | Managed target-memory writes | Yes | `WriteScalar` and `WriteBlock` are effectful. The caller selects the address; the API checks alignment and range, not whether that address is safe to modify. `WriteRawAP` remains an unmanaged escape hatch. | | Block reads | Yes | Accepts empty, unaligned, and mixed-width ranges. No auto-incrementing word run crosses a 1 KiB TAR boundary. If the MEM-AP does not accept single address increment, the reader writes TAR before each word. It uses the ordinary DAP WAIT policy. If selection, framing, or cleanup becomes uncertain, repair is required. A FAULT returns only the confirmed prefix. Cancellation and transport or protocol failures can also interrupt the read. Unread destination bytes remain untouched. | @@ -302,14 +302,15 @@ layouts and power-domain skips have hardware-independent test coverage. | --- | --- | --- | | CPUID read and decode | Yes | Accepts any aligned-word reader and validates a plausible Arm Cortex-M identity. | | Physical identity read | HIL | Opt-in FTDI/SWD/DAP/MEM-AP integration test. | +| Cortex-M0 acquisition | Yes | Enables halting debug through borrowed word memory, preserves inherited control, and retains failed restoration for retry. Other cores and active stepping or interrupt masking are rejected before writes. | | Halt, resume, or step | No | No target run-control API exists. | | Register access | No | CPUID decoding is not a general core-register interface. | | 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. | -The package identifies a processor; it is not yet a complete Cortex-M target -driver. +The package identifies Cortex-M processors and acquires Cortex-M0 halting +debug. See [Cortex-M control](cortexm.md) for effects and cleanup limits. ## Executable surfaces diff --git a/docs/composition.md b/docs/composition.md index 3fdf642..9706501 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -36,6 +36,7 @@ data-register write can write target memory. | Identify one debug component through scalar memory | `coresight.Identify` | `examples/simple/coresight-info` | | Inspect ROM entries or a bounded component hierarchy | `Component.ROMTable`, `ROMTable.ReadEntry`, `coresight.Walk` | `examples/simple/coresight-info -walk` | | Identify a Cortex-M through any compatible word reader | `cortexm.Identify` | `examples/simple/cortexm-info` | +| Acquire Cortex-M0 halting debug | `cortexm.Acquire`, `Target.Release` | [Cortex-M control](cortexm.md) | | Test SWD and DAP behavior without hardware | `swd/sim`, `dap/sim` | Package tests | The examples are intentionally small, executable compositions of public @@ -705,7 +706,8 @@ Use `dap.MemAP` for aligned 8-, 16-, or 32-bit target-memory reads and writes through an explicitly selected MEM-AP. Support for the non-word sizes is implementation-defined, so each access verifies that CSW accepted its size before touching memory. CFG.LD makes 64-bit access possible; CFG.LA permits -addresses above 32 bits. `target/cortexm` uses `ReadWord` for its 32-bit reads. +addresses above 32 bits. `ReadWord` and `WriteWord` supply aligned 32-bit +convenience calls over the scalar operations. `MemAP.ReadBlock` accepts empty, unaligned, and mixed-width ranges. It uses the same configured WAIT policy as the scalar and raw DAP operations. If selection, @@ -739,7 +741,9 @@ 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. +independent of the host, adapter, and wire protocol. `cortexm.Acquire` also +uses `WriteWord` to enable Cortex-M0 halting debug. Release the target before +its memory owner; see [Cortex-M control](cortexm.md) for the full lifecycle. ## Release in reverse order diff --git a/docs/cortexm.md b/docs/cortexm.md new file mode 100644 index 0000000..82b7b5f --- /dev/null +++ b/docs/cortexm.md @@ -0,0 +1,45 @@ +# Cortex-M control + +`target/cortexm.Identify` reads CPUID through an aligned-word reader. +`Acquire` enables Cortex-M0 halting debug through borrowed `Memory` with +`ReadWord` and `WriteWord` methods, as supplied by `dap.MemAP`. It rejects +other processor parts before writing a debug register. + +Acquisition does not request a halt. It preserves an inherited halt and +rejects active stepping, interrupt masking, and unfinished halt transitions. +When debug is disabled, other control bits are unknown; acquisition initializes +them to zero when enabling debug. Enabling debug can change how the processor +handles debug events. DHCSR reads consume its sticky reset and retirement +indicators, which cannot be restored. + +Keep exclusive control of the debug registers and serialize all access to the +memory connection. Release the target before the memory owner. Each operation +is capped at five seconds or the caller's earlier deadline. Failed acquisition +attempts cleanup with an independent five-second context. A non-nil target +returned with an error retains cleanup obligations and permits only Release. +Failed restoration retains state for retry, including when a new observed halt +prevents restoring disabled debug. Cleanup requires usable memory; the target +cannot repair an invalidated MEM-AP or a poisoned transport. + +```go +core, err := cortexm.Acquire(ctx, memory) +// Retain any non-nil core, including on error, for cleanup. +if err == nil { + identity := core.Identity() + _ = identity +} +cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) +cleanupErr := core.Release(cleanupCtx) +cancel() +err = errors.Join(err, cleanupErr) +// Keep both owners on cleanup failure. Close the memory owner only after +// target release succeeds. +``` + +`Identity` returns the cached CPUID after release. A zero target cannot access +memory. This API does not request halt, resume, step, or reset. + +Register semantics follow Arm DDI 0419E, sections C1.5 and C1.6.3 of the +[Armv6-M Architecture Reference Manual](https://documentation-service.arm.com/static/5f8ff05ef86e16515cdbf826). +Behavioral tests cover partial writes, cancellation, failed cleanup, and retry; +they do not establish physical execution-control behavior. diff --git a/target/cortexm/acquire_cancellation_test.go b/target/cortexm/acquire_cancellation_test.go new file mode 100644 index 0000000..c1ba558 --- /dev/null +++ b/target/cortexm/acquire_cancellation_test.go @@ -0,0 +1,36 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +type cancelReadMemory struct { + *controlMemory + cancel context.CancelFunc +} + +func (m cancelReadMemory) ReadWord(ctx context.Context, addr uint32) (uint32, error) { + value, err := m.controlMemory.ReadWord(ctx, addr) + if addr == dhcsr { + m.cancel() + } + return value, err +} + +func TestAcquireCancellationAfterReadNeedsNoRestoration(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + m := newControlMemory() + m.failWrite = 1 + core, err := cortexm.Acquire(ctx, cancelReadMemory{m, cancel}) + if core != nil || !errors.Is(err, context.Canceled) { + t.Fatalf("core=%v err=%v", core, err) + } + if m.reads != 2 || m.writes != 0 || m.control != 0 { + t.Fatalf("reads=%d writes=%d control=%#x", m.reads, m.writes, m.control) + } +} diff --git a/target/cortexm/control.go b/target/cortexm/control.go new file mode 100644 index 0000000..fa9f4fc --- /dev/null +++ b/target/cortexm/control.go @@ -0,0 +1,166 @@ +package cortexm + +import ( + "context" + "errors" + "fmt" + "time" +) + +const ( + dhcsrAddress = uint32(0xe000edf0) + debugKey = uint32(0xa05f0000) + cDebugEnable = uint32(1) + cHalt = uint32(2) + cStep = uint32(4) + cMaskInts = uint32(8) + sHalt = uint32(1 << 17) + controlTimeout = 5 * time.Second +) + +// Memory reads and writes aligned 32-bit target words. A successful write must +// include completion of the underlying memory access, as dap.MemAP does. +// Implementations must honor context cancellation and deadlines. +type Memory interface { + WordReader + 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 +// exclusive control of the processor's debug registers until Release succeeds, +// then release the memory owner. The zero value is inactive. +type Target struct { + memory Memory + identity Identity + saved uint32 + closing bool + changed bool +} + +// 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. +// +// Calls are bounded to five seconds or the caller's earlier deadline. Failed +// setup attempts restoration with an independent five-second context. A non-nil +// target returned with an error retains cleanup obligations; only Release is +// then available. Memory remains borrowed on every return. +func Acquire(ctx context.Context, memory Memory) (*Target, error) { + if memory == nil { + return nil, errors.New("cortexm: nil memory") + } + if err := liveContext(ctx); err != nil { + return nil, err + } + ctx, cancel := context.WithTimeout(ctx, controlTimeout) + defer cancel() + identity, err := Identify(ctx, memory) + 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) + } + saved, err := memory.ReadWord(ctx, dhcsrAddress) + if err != nil { + return nil, err + } + if err := validateControl(saved); err != nil { + return nil, err + } + t := &Target{memory: memory, identity: identity, saved: saved & (cDebugEnable | cHalt)} + if saved&cDebugEnable != 0 { + return t, nil + } + t.saved = 0 + if err := t.writeControl(ctx, cDebugEnable); err != nil { + cleanup, cancel := context.WithTimeout(context.Background(), controlTimeout) + defer cancel() + if releaseErr := t.Release(cleanup); releaseErr != nil { + return t, errors.Join(err, releaseErr) + } + return nil, err + } + return t, nil +} + +func validateControl(value uint32) error { + if value&cDebugEnable == 0 { + return nil + } + if value&(cStep|cMaskInts) != 0 { + return errors.New("cortexm: inherited stepping or interrupt masking is unsupported") + } + if value&cHalt != 0 && value&sHalt == 0 { + return errors.New("cortexm: halt transition is incomplete") + } + return nil +} + +// Identity returns the acquired CPUID, including after release. +func (t *Target) Identity() Identity { + if t == nil { + return Identity{} + } + return t.identity +} + +// Release restores the inherited debug control. A target initially halted +// remains halted. Failed restoration is retryable and blocks ordinary calls. +// Nil and released targets need no cleanup. Use a fresh context after operation +// cancellation; each attempt is capped at five seconds. Release requires usable +// memory and cannot repair a disconnected or invalidated memory client. It +// refuses to disable debug while a new halt is observed; cleanup remains +// pending until execution resumes. +func (t *Target) Release(ctx context.Context) error { + if t == nil || t.memory == nil { + return nil + } + t.closing = true + if err := liveContext(ctx); err != nil { + return err + } + ctx, cancel := context.WithTimeout(ctx, controlTimeout) + defer cancel() + if t.changed { + if err := t.restore(ctx); err != nil { + return fmt.Errorf("cortexm: restore debug control: %w", err) + } + } + t.memory = nil + return nil +} + +func (t *Target) writeControl(ctx context.Context, control uint32) error { + if err := ctx.Err(); err != nil { + return err + } + t.changed = true + if err := t.memory.WriteWord(ctx, dhcsrAddress, debugKey|control); err != nil { + return err + } + value, err := t.memory.ReadWord(ctx, dhcsrAddress) + if err != nil { + return err + } + if value&cDebugEnable == 0 && t.saved == 0 { + t.changed = false + } + mask := cDebugEnable + if control&cDebugEnable != 0 { + mask |= cHalt | cStep | cMaskInts + } + if value&mask != control&mask { + return fmt.Errorf("cortexm: DHCSR control %#x, want %#x", value&mask, control&mask) + } + return nil +} + +func liveContext(ctx context.Context) error { + if ctx == nil { + return errors.New("cortexm: nil context") + } + return ctx.Err() +} diff --git a/target/cortexm/control_test.go b/target/cortexm/control_test.go new file mode 100644 index 0000000..ffd2c80 --- /dev/null +++ b/target/cortexm/control_test.go @@ -0,0 +1,210 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +const ( + dhcsr = uint32(0xe000edf0) + debugEnable = uint32(1) + haltRequest = uint32(2) + haltStatus = uint32(1 << 17) +) + +var errMemory = errors.New("memory failure") + +type controlMemory struct { + cpuid uint32 + control uint32 + halted bool + reads, writes int + failRead, failWrite int + afterWrite bool + ignoreWrites bool + rejectWrites bool + onWrite func() +} + +func newControlMemory() *controlMemory { return &controlMemory{cpuid: 0x410cc200} } + +func (m *controlMemory) ReadWord(ctx context.Context, addr uint32) (uint32, error) { + if err := ctx.Err(); err != nil { + return 0, err + } + m.reads++ + if m.reads == m.failRead { + return 0, errMemory + } + if addr == 0xe000ed00 { + return m.cpuid, nil + } + if addr != dhcsr { + return 0, errors.New("unexpected read address") + } + value := m.control + if m.halted { + value |= haltStatus + } + return value, nil +} + +func (m *controlMemory) WriteWord(ctx context.Context, addr, value uint32) error { + if err := ctx.Err(); err != nil { + return err + } + if addr != dhcsr || value>>16 != 0xa05f { + return errors.New("invalid DHCSR write") + } + m.writes++ + fail := m.rejectWrites || m.writes == m.failWrite + if fail && !m.afterWrite { + return errMemory + } + if !m.ignoreWrites { + m.control = value & 15 + m.halted = m.control&(debugEnable|haltRequest) == debugEnable|haltRequest + } + if m.onWrite != nil { + m.onWrite() + } + if fail { + return errMemory + } + return nil +} + +func TestAcquireRestoresDebugEnable(t *testing.T) { + for _, initial := range []uint32{0, debugEnable, debugEnable | haltRequest} { + t.Run(string(rune('0'+initial)), func(t *testing.T) { + m := newControlMemory() + 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.control&debugEnable == 0 { + t.Fatalf("identity/control = %#v/%#x", core.Identity(), m.control) + } + if m.halted != (initial&haltRequest != 0) { + t.Fatal("acquisition changed halt state") + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.control != initial { + t.Fatalf("restored control = %#x, want %#x", m.control, initial) + } + writes := m.writes + if err := core.Release(t.Context()); err != nil || m.writes != writes { + t.Fatal("release was not idempotent") + } + }) + } +} + +func TestAcquirePreservesEventInducedHalt(t *testing.T) { + m := newControlMemory() + m.control, m.halted = debugEnable, true + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.control != debugEnable || !m.halted || m.writes != 0 { + t.Fatalf("control=%#x halted=%t writes=%d", m.control, m.halted, m.writes) + } +} + +func TestAcquireRejectsUnsupportedStateBeforeWrites(t *testing.T) { + for _, control := range []uint32{5, 9, 3} { + m := newControlMemory() + m.control = control + if core, err := cortexm.Acquire(t.Context(), m); err == nil || core != nil || m.writes != 0 { + t.Fatalf("control %#x: core=%v error=%v writes=%d", control, core, err, m.writes) + } + } + m := newControlMemory() + m.cpuid = 0x410fc241 + if _, err := cortexm.Acquire(t.Context(), m); err == nil || m.writes != 0 { + t.Fatal("accepted another architecture") + } +} + +func TestAcquireFailureCleanupAndRetry(t *testing.T) { + for _, after := range []bool{false, true} { + m := newControlMemory() + 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("after=%v: core=%v err=%v control=%#x", after, core, err, m.control) + } + } + m := newControlMemory() + m.failRead, m.failWrite = 3, 2 + core, err := cortexm.Acquire(t.Context(), m) + if core == nil || !errors.Is(err, errMemory) { + t.Fatalf("core=%v err=%v", core, err) + } + if err := core.Release(t.Context()); err != nil || m.control != 0 { + t.Fatalf("release = %v; control=%#x", err, m.control) + } +} + +func TestAcquireCleanupOutlivesOperationCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + m := newControlMemory() + m.onWrite = cancel + core, err := cortexm.Acquire(ctx, m) + if core != nil || !errors.Is(err, context.Canceled) || m.control != 0 { + t.Fatalf("core=%v err=%v control=%#x", core, err, m.control) + } +} + +func TestAcquireIgnoresUnknownDisabledControlBits(t *testing.T) { + m := newControlMemory() + m.control = 14 + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + if m.control != debugEnable { + t.Fatalf("control = %#x", m.control) + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestAcquireRequiresLiveContextAndMemory(t *testing.T) { + m := newControlMemory() + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if _, err := cortexm.Acquire(ctx, m); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + var nilContext context.Context + if _, err := cortexm.Acquire(nilContext, m); err == nil { + t.Fatal("nil context accepted") + } + if _, err := cortexm.Acquire(t.Context(), nil); err == nil { + t.Fatal("nil memory accepted") + } + if m.reads != 0 || m.writes != 0 { + t.Fatal("invalid input reached memory") + } +} + +func TestAcquireDetectsIgnoredWrite(t *testing.T) { + m := newControlMemory() + m.ignoreWrites = true + if core, err := cortexm.Acquire(t.Context(), m); err == nil || core != nil { + t.Fatalf("core=%v err=%v", core, err) + } +} diff --git a/target/cortexm/identity.go b/target/cortexm/identity.go index ea9f59d..db3aad9 100644 --- a/target/cortexm/identity.go +++ b/target/cortexm/identity.go @@ -1,4 +1,5 @@ -// Package cortexm identifies Cortex-M processors through target memory. +// Package cortexm identifies Cortex-M processors and controls Cortex-M0 +// halting debug through target memory. package cortexm import ( diff --git a/target/cortexm/ignored_enable_test.go b/target/cortexm/ignored_enable_test.go new file mode 100644 index 0000000..4190bd1 --- /dev/null +++ b/target/cortexm/ignored_enable_test.go @@ -0,0 +1,64 @@ +package cortexm_test + +import ( + "errors" + "fmt" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func TestAcquireIgnoredEnableNeedsNoRestoration(t *testing.T) { + for _, initial := range []uint32{0, 14} { + t.Run(fmt.Sprintf("control=%d", initial), func(t *testing.T) { + m := newControlMemory() + m.control, m.ignoreWrites, m.failWrite = initial, true, 2 + core, err := cortexm.Acquire(t.Context(), m) + if core != nil || err == nil || errors.Is(err, errMemory) { + t.Fatalf("core=%v err=%v", core, err) + } + if m.reads != 3 || m.writes != 1 || m.control != initial { + t.Fatalf("reads=%d writes=%d control=%#x", m.reads, m.writes, m.control) + } + }) + } +} + +func TestAcquireFailedEnableNeedsNoRestoration(t *testing.T) { + for _, initial := range []uint32{0, 14} { + t.Run(fmt.Sprintf("control=%d", initial), func(t *testing.T) { + m := newControlMemory() + m.control, m.rejectWrites = initial, true + core, err := cortexm.Acquire(t.Context(), m) + if core != nil || !errors.Is(err, errMemory) { + t.Fatalf("core=%v err=%v", core, err) + } + if m.reads != 3 || m.writes != 1 || m.control != initial { + t.Fatalf("reads=%d writes=%d control=%#x", m.reads, m.writes, m.control) + } + }) + } +} + +func TestReleaseRetryConfirmsDisabledDebugWithoutWrite(t *testing.T) { + m := newControlMemory() + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + m.failWrite, m.afterWrite = 2, true + if err := core.Release(t.Context()); !errors.Is(err, errMemory) { + t.Fatalf("first release = %v", err) + } + m.failRead = m.reads + 1 + if err := core.Release(t.Context()); !errors.Is(err, errMemory) || m.writes != 2 { + t.Fatalf("failed confirmation: err=%v writes=%d", err, m.writes) + } + m.failRead, m.rejectWrites = 0, true + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.writes != 2 || m.control != 0 { + t.Fatalf("writes=%d control=%#x", m.writes, m.control) + } +} diff --git a/target/cortexm/restore.go b/target/cortexm/restore.go new file mode 100644 index 0000000..af33ecc --- /dev/null +++ b/target/cortexm/restore.go @@ -0,0 +1,23 @@ +package cortexm + +import ( + "context" + "errors" +) + +func (t *Target) restore(ctx context.Context) error { + value, err := t.memory.ReadWord(ctx, dhcsrAddress) + if err != nil { + return err + } + if t.saved == 0 && value&cDebugEnable == 0 { + return nil + } + if value&cDebugEnable != 0 && value&(cStep|cMaskInts) != 0 { + return errors.New("cortexm: cannot restore externally changed stepping or interrupt masking") + } + if value&(cHalt|sHalt) != 0 && value&cDebugEnable != 0 { + return errors.New("cortexm: new halt prevents restoring disabled debug") + } + return t.writeControl(ctx, t.saved) +} From 1e27eef74008d3cb7f584364bbb7d6edcb3de712 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sat, 26 Sep 2026 13:45:47 -0700 Subject: [PATCH 2/4] Track Cortex-M0 halt ownership through resume and release. Enabling debug does not distinguish a halt requested by this owner from one it inherited. Add bounded halt, status, and resume operations with ownership retained through readback failures and release. Do not replay a completed resume when execution immediately halts again. An unconfirmed control write cannot establish the origin of an observed stop, so cleanup refuses to resume that halt. --- README.md | 18 ++-- docs/README.md | 2 +- docs/architecture.md | 23 +++-- docs/capabilities.md | 11 ++- docs/composition.md | 7 +- docs/cortexm.md | 113 ++++++++++++++++------ target/cortexm/cancellation_test.go | 45 +++++++++ target/cortexm/control.go | 17 ++-- target/cortexm/control_test.go | 31 ++++-- target/cortexm/failure_test.go | 134 ++++++++++++++++++++++++++ target/cortexm/halt_uncertain_test.go | 27 ++++++ target/cortexm/ignored_resume_test.go | 105 ++++++++++++++++++++ target/cortexm/lost_halt_test.go | 96 ++++++++++++++++++ target/cortexm/reentry_test.go | 74 ++++++++++++++ target/cortexm/restore.go | 50 +++++++++- target/cortexm/run.go | 130 +++++++++++++++++++++++++ target/cortexm/run_test.go | 132 +++++++++++++++++++++++++ 17 files changed, 937 insertions(+), 78 deletions(-) create mode 100644 target/cortexm/cancellation_test.go create mode 100644 target/cortexm/failure_test.go create mode 100644 target/cortexm/halt_uncertain_test.go create mode 100644 target/cortexm/ignored_resume_test.go create mode 100644 target/cortexm/lost_halt_test.go create mode 100644 target/cortexm/reentry_test.go create mode 100644 target/cortexm/run.go create mode 100644 target/cortexm/run_test.go diff --git a/README.md b/README.md index ef39c5a..09b531d 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,8 @@ 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. `cortexm.Acquire` enables Cortex-M0 -halting debug over borrowed word memory and retains its restoration state; -see [Cortex-M control](docs/cortexm.md). +through any compatible target-word reader. It also provides acquired Cortex-M0 +halt/resume control over word memory; see [Cortex-M control](docs/cortexm.md). The FTDI path uses the standard H-series MPSSE port and endpoint layout. Descriptor-driven FTDI port binding is not implemented yet. J-Link instead @@ -132,12 +131,13 @@ root. See [Linux USB access](docs/linux-usb.md) for udev rules and a bounded ## Safety Debug and programming interfaces can reset processors, halt execution, modify -memory, reconfigure programmable logic, and change persistent device state. -The shipped examples and `ost` commands avoid reset, halt, target-memory -writes, and persistent changes. The `dap.MemAP` API does expose effectful -scalar writes; callers choose the addresses and own the consequences. -Establishing an ADIv5 connection also changes volatile debug-port control -state; the connection releases its own power requests before return. +memory, reconfigure programmable logic, and change persistent device state. The +shipped examples and `ost` commands avoid reset, halt, target-memory writes, and +persistent changes. The Cortex-M0 target API enables halting debug and controls +execution. The `dap.MemAP` API exposes effectful scalar writes; callers choose +the addresses and own the consequences. Establishing an ADIv5 connection also +changes volatile debug-port control state; the connection releases its own power +requests before return. ## SWD DPIDR example diff --git a/docs/README.md b/docs/README.md index 1fafc76..5d04530 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,7 +18,7 @@ today 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 halting-debug acquisition +- [Cortex-M control](cortexm.md) describes Cortex-M0 acquisition, halt/resume, and restoration over borrowed word memory. - [Composition](composition.md) maps common tasks to the narrowest public package that implements them and gives coding agents a selection checklist. diff --git a/docs/architecture.md b/docs/architecture.md index 2387bc8..76b349b 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 + Cortex-M identity and Cortex-M0 control | v examples and ost @@ -51,7 +51,7 @@ debugger 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 acquire Cortex-M0 halting debug over borrowed word memory. | +| `target/cortexm` | Identify Cortex-M processors and own Cortex-M0 halting debug 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,10 +334,12 @@ children with power-domain metadata and reports an incomplete result. It uses DAP 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. Acquisition of -Cortex-M0 halting debug also requires completed word writes. Release the target -before the memory owner. See [Cortex-M control](cortexm.md) for effects and -restoration limits. +`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 and its halt requests, and must be released +before the memory owner. +It does not know about USB, adapters, or wire protocols. See +[Cortex-M control](cortexm.md) for restoration and failure boundaries. ## Host implementations @@ -378,10 +380,11 @@ replaceable while exercising the public protocol and DAP layers. ## Safety effects -The current examples and `ost` inspection commands do not reset or halt the -target, write target memory, or change persistent state. The `dap.MemAP` API -does expose scalar and block target-memory writes; applications choose the -affected addresses and own the consequences. +The inspection examples and `ost` commands do not reset or halt the target, +write target memory, or change persistent state. The Cortex-M0 target API +enables halting debug and controls execution. +The `dap.MemAP` API does expose scalar and block target-memory writes; +applications choose the affected addresses and own the consequences. The layers are not entirely passive: diff --git a/docs/capabilities.md b/docs/capabilities.md index 1b28abc..b06e1a8 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -302,15 +302,15 @@ layouts and power-domain skips have hardware-independent test coverage. | --- | --- | --- | | CPUID read and decode | Yes | Accepts any aligned-word reader and validates a plausible Arm Cortex-M identity. | | Physical identity read | HIL | Opt-in FTDI/SWD/DAP/MEM-AP integration test. | -| Cortex-M0 acquisition | Yes | Enables halting debug through borrowed word memory, preserves inherited control, and retains failed restoration for retry. Other cores and active stepping or interrupt masking are rejected before writes. | -| Halt, resume, or step | No | No target run-control API exists. | +| Cortex-M0 acquisition and halt/resume | Yes | Borrowed word memory, inherited-halt protection, bounded operations, and retryable cleanup. Behavioral failure tests pass; physical control evidence is not yet recorded. | +| Step | No | No single-step API exists. | | Register access | No | CPUID decoding is not a general core-register interface. | | 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. | -The package identifies Cortex-M processors and acquires Cortex-M0 halting -debug. See [Cortex-M control](cortexm.md) for effects and cleanup limits. +Identity covers Cortex-M; acquired control currently accepts Cortex-M0 only. +See [Cortex-M control](cortexm.md) for its effects and cleanup limits. ## Executable surfaces @@ -336,7 +336,8 @@ ost dap ap id --ap N ost target cortex-m id --ap N ``` -These hardware operations are read-only with respect to target memory and do +The inspection examples and these commands are read-only with respect to +target memory and do not halt or reset the target. They still claim the adapter, clock SWD, and use the volatile DAP and MEM-AP state described above. diff --git a/docs/composition.md b/docs/composition.md index 9706501..3f29c89 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -36,7 +36,7 @@ data-register write can write target memory. | Identify one debug component through scalar memory | `coresight.Identify` | `examples/simple/coresight-info` | | Inspect ROM entries or a bounded component hierarchy | `Component.ROMTable`, `ROMTable.ReadEntry`, `coresight.Walk` | `examples/simple/coresight-info -walk` | | Identify a Cortex-M through any compatible word reader | `cortexm.Identify` | `examples/simple/cortexm-info` | -| Acquire Cortex-M0 halting debug | `cortexm.Acquire`, `Target.Release` | [Cortex-M control](cortexm.md) | +| Acquire, halt, and resume a Cortex-M0 | `cortexm.Acquire`, `Target.Halt`, `Target.Resume`, `Target.Release` | [Cortex-M control](cortexm.md) | | Test SWD and DAP behavior without hardware | `swd/sim`, `dap/sim` | Package tests | The examples are intentionally small, executable compositions of public @@ -742,8 +742,9 @@ 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. Release the target before -its memory owner; see [Cortex-M control](cortexm.md) for the full lifecycle. +uses `WriteWord` to enable Cortex-M0 halting debug. Release that target 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 82b7b5f..fde3176 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -1,45 +1,94 @@ # Cortex-M control -`target/cortexm.Identify` reads CPUID through an aligned-word reader. -`Acquire` enables Cortex-M0 halting debug through borrowed `Memory` with -`ReadWord` and `WriteWord` methods, as supplied by `dap.MemAP`. It rejects -other processor parts before writing a debug register. - -Acquisition does not request a halt. It preserves an inherited halt and -rejects active stepping, interrupt masking, and unfinished halt transitions. -When debug is disabled, other control bits are unknown; acquisition initializes -them to zero when enabling debug. Enabling debug can change how the processor -handles debug events. DHCSR reads consume its sticky reset and retirement -indicators, which cannot be restored. - -Keep exclusive control of the debug registers and serialize all access to the -memory connection. Release the target before the memory owner. Each operation -is capped at five seconds or the caller's earlier deadline. Failed acquisition -attempts cleanup with an independent five-second context. A non-nil target -returned with an error retains cleanup obligations and permits only Release. -Failed restoration retains state for retry, including when a new observed halt -prevents restoring disabled debug. Cleanup requires usable memory; the target -cannot repair an invalidated MEM-AP or a poisoned transport. +`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. + +## 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. + +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 acquired. Serialize the target and its memory connection. `Identity` returns +the cached CPUID after release; the zero target cannot access memory. + +`Halt` waits for Debug state. `Resume` accepts only a halt requested by this +target. Observing an already-halted processor does not acquire permission to +resume it. `Halted` reads the current status without acquiring halt ownership. + +Release the target before its MEM-AP or Arm debug owner. `Release` restores +the debug control changed by the target and leaves an inherited halt alone. +Each operation is bounded to five seconds or the caller's earlier deadline. +Failed acquisition attempts cleanup with a fresh five-second context; a +non-nil target returned with an error must be retained for release retries. +Once release starts, or a control write fails, ordinary target calls stop. +Failed cleanup retains the restoration state for another `Release`. + +Cleanup needs a usable memory connection. A poisoned transport or invalidated +MEM-AP can prevent restoration; retaining the target does not repair either. +Retain both the target and its memory owner after a release failure so +restoration can be retried. + +## Effects + +Enabling halting debug changes how the processor handles debug events, even +before an explicit halt. Halting does not stop peripheral clocks. Resuming +can execute instructions before a later failure is reported, and release +cannot undo those instructions or recover elapsed time. + +A successful memory write does not prove that the halt request cleared. If +readback never shows it clear, cleanup stays pending without repeating resume: +an ignored write cannot be distinguished from an immediate new halt. + +A completed resume can immediately encounter a new debug event. The target +reports that halt and never repeats the completed resume during cleanup. If +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. +Debug events racing with restoration of disabled debug can still affect +execution. + +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. + +The implementation follows Arm DDI 0419E, sections C1.5 and C1.6.3 of the +[Armv6-M Architecture Reference Manual](https://documentation-service.arm.com/static/5f8ff05ef86e16515cdbf826). +It does not implement reset, single-step, general register access, breakpoints, +or watchpoints. + +## Composition + +For a MEM-AP borrowed from `armdebug.Conn`, acquire the target and retain any +non-nil result before checking the error: ```go core, err := cortexm.Acquire(ctx, memory) -// Retain any non-nil core, including on error, for cleanup. +// Retain core for Release even when err is non-nil. +if err == nil { + err = core.Halt(ctx) +} if err == nil { - identity := core.Identity() - _ = identity + err = core.Resume(ctx) } cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) cleanupErr := core.Release(cleanupCtx) cancel() err = errors.Join(err, cleanupErr) -// Keep both owners on cleanup failure. Close the memory owner only after -// target release succeeds. +// Close the Arm debug owner only after target release succeeds. +// On failure retain both owners for a later cleanup attempt. ``` -`Identity` returns the cached CPUID after release. A zero target cannot access -memory. This API does not request halt, resume, step, or reset. - -Register semantics follow Arm DDI 0419E, sections C1.5 and C1.6.3 of the -[Armv6-M Architecture Reference Manual](https://documentation-service.arm.com/static/5f8ff05ef86e16515cdbf826). -Behavioral tests cover partial writes, cancellation, failed cleanup, and retry; -they do not establish physical execution-control behavior. +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. diff --git a/target/cortexm/cancellation_test.go b/target/cortexm/cancellation_test.go new file mode 100644 index 0000000..702a3a2 --- /dev/null +++ b/target/cortexm/cancellation_test.go @@ -0,0 +1,45 @@ +package cortexm_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func TestHaltCancellationBeforeWritePreservesRestorationState(t *testing.T) { + for _, initial := range []uint32{0, debugEnable} { + t.Run(fmt.Sprintf("debug=%d", initial), func(t *testing.T) { + m := newControlMemory() + m.control = initial + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + m.onRead = cancel + writes := m.writes + if err := core.Halt(ctx); !errors.Is(err, context.Canceled) { + t.Fatalf("halt error = %v", err) + } + if m.writes != writes { + t.Fatal("canceled halt attempted a write") + } + m.onRead = nil + if initial == debugEnable { + m.failWrite = writes + 1 + } else { + writes++ // Acquisition still requires restoration. + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if m.writes != writes || m.control != initial { + t.Fatalf("writes=%d want=%d control=%#x", m.writes, writes, m.control) + } + }) + } +} diff --git a/target/cortexm/control.go b/target/cortexm/control.go index fa9f4fc..4256a49 100644 --- a/target/cortexm/control.go +++ b/target/cortexm/control.go @@ -31,11 +31,14 @@ type Memory interface { // exclusive control of the processor's debug registers until Release succeeds, // then release the memory owner. The zero value is inactive. type Target struct { - memory Memory - identity Identity - saved uint32 - closing bool - changed bool + memory Memory + identity Identity + saved uint32 + closing bool + changed bool + haltOwned bool + haltUncertain bool + resumeUncertain bool } // Acquire enables Cortex-M0 halting debug without requesting a halt. It reads @@ -112,8 +115,8 @@ func (t *Target) Identity() Identity { // Nil and released targets need no cleanup. Use a fresh context after operation // cancellation; each attempt is capped at five seconds. Release requires usable // memory and cannot repair a disconnected or invalidated memory client. It -// refuses to disable debug while a new halt is observed; cleanup remains -// pending until execution resumes. +// never repeats a completed resume. An unconfirmed control change, or a new halt while +// restoring disabled debug, can prevent cleanup until execution resumes. func (t *Target) Release(ctx context.Context) error { if t == nil || t.memory == nil { return nil diff --git a/target/cortexm/control_test.go b/target/cortexm/control_test.go index ffd2c80..d7bd0da 100644 --- a/target/cortexm/control_test.go +++ b/target/cortexm/control_test.go @@ -26,7 +26,11 @@ type controlMemory struct { afterWrite bool ignoreWrites bool rejectWrites bool + stall bool + rehaltAfter int + rehaltPending int onWrite func() + onRead func() } func newControlMemory() *controlMemory { return &controlMemory{cpuid: 0x410cc200} } @@ -45,10 +49,20 @@ func (m *controlMemory) ReadWord(ctx context.Context, addr uint32) (uint32, erro if addr != dhcsr { return 0, errors.New("unexpected read address") } + if m.rehaltPending > 0 { + m.rehaltPending-- + if m.rehaltPending == 0 { + m.control |= haltRequest + m.halted = true + } + } value := m.control if m.halted { value |= haltStatus } + if m.onRead != nil { + m.onRead() + } return value, nil } @@ -65,8 +79,15 @@ func (m *controlMemory) WriteWord(ctx context.Context, addr, value uint32) error return errMemory } if !m.ignoreWrites { + wasHalted := m.halted m.control = value & 15 - m.halted = m.control&(debugEnable|haltRequest) == debugEnable|haltRequest + if !m.stall { + m.halted = m.control&(debugEnable|haltRequest) == debugEnable|haltRequest + } + if wasHalted && m.control == debugEnable && m.rehaltAfter > 0 { + m.rehaltPending = m.rehaltAfter + m.halted = true + } } if m.onWrite != nil { m.onWrite() @@ -200,11 +221,3 @@ func TestAcquireRequiresLiveContextAndMemory(t *testing.T) { t.Fatal("invalid input reached memory") } } - -func TestAcquireDetectsIgnoredWrite(t *testing.T) { - m := newControlMemory() - m.ignoreWrites = true - if core, err := cortexm.Acquire(t.Context(), m); err == nil || core != nil { - t.Fatalf("core=%v err=%v", core, err) - } -} diff --git a/target/cortexm/failure_test.go b/target/cortexm/failure_test.go new file mode 100644 index 0000000..292ebcd --- /dev/null +++ b/target/cortexm/failure_test.go @@ -0,0 +1,134 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func TestControlReadFailuresRetainCleanup(t *testing.T) { + for fail := 1; fail <= 6; fail++ { + m := newControlMemory() + m.failRead = fail + core, err := cortexm.Acquire(t.Context(), m) + if err == nil { + err = core.Halt(t.Context()) + } + if err == nil { + err = core.Resume(t.Context()) + } + if !errors.Is(err, errMemory) { + t.Fatalf("read %d: %v", fail, err) + } + if core != nil { + if err := core.Release(t.Context()); err != nil { + t.Fatalf("read %d cleanup: %v", fail, err) + } + } + if m.control&debugEnable != 0 || m.halted { + t.Fatalf("read %d left control=%#x halted=%v", fail, m.control, m.halted) + } + } +} + +func TestResumeFailureRetainsCleanup(t *testing.T) { + for _, after := range []bool{false, true} { + m := newControlMemory() + 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.failWrite, m.afterWrite = m.writes+1, after + if err := core.Resume(t.Context()); !errors.Is(err, errMemory) { + t.Fatal(err) + } + if err := core.Halt(t.Context()); err == nil { + t.Fatal("halt during cleanup") + } + err = core.Release(t.Context()) + if !after { + if err == nil { + t.Fatal("replayed uncertain resume") + } + continue + } + if err != nil { + t.Fatal(err) + } + if m.halted || m.control != 0 { + t.Fatalf("restored halted=%v control=%#x", m.halted, m.control) + } + } +} + +func TestCanceledReleaseKeepsTargetUnavailable(t *testing.T) { + m := newControlMemory() + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + if err := core.Release(ctx); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + if err := core.Halt(t.Context()); err == nil { + t.Fatal("halt after release started") + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + if err := core.Resume(t.Context()); err == nil { + t.Fatal("resume after release") + } +} + +func TestAcquireDetectsIgnoredWrite(t *testing.T) { + m := newControlMemory() + m.ignoreWrites = true + if core, err := cortexm.Acquire(t.Context(), m); err == nil || core != nil { + t.Fatalf("core=%v err=%v", core, err) + } +} + +func TestReleaseRefusesUnsafeExternalModeChange(t *testing.T) { + m := newControlMemory() + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + m.control |= 8 + writes := m.writes + if err := core.Release(t.Context()); err == nil || m.writes != writes { + t.Fatal("release changed live interrupt masking") + } + m.control &^= 8 + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestReleaseAfterResumePreservesNewHalt(t *testing.T) { + m := newControlMemory() + m.control = debugEnable + 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 err := core.Resume(t.Context()); err != nil { + t.Fatal(err) + } + m.control, m.halted = debugEnable|haltRequest, true + writes := m.writes + if err := core.Release(t.Context()); err != nil || !m.halted || m.writes != writes { + t.Fatal("release resumed a new unowned halt") + } +} diff --git a/target/cortexm/halt_uncertain_test.go b/target/cortexm/halt_uncertain_test.go new file mode 100644 index 0000000..6b84b61 --- /dev/null +++ b/target/cortexm/halt_uncertain_test.go @@ -0,0 +1,27 @@ +package cortexm_test + +import ( + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func TestUncertainHaltDoesNotAcquireAnIndependentStop(t *testing.T) { + for _, initial := range []uint32{0, debugEnable} { + m := newControlMemory() + m.control = initial + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + m.failWrite = m.writes + 1 + if err := core.Halt(t.Context()); err == nil { + t.Fatal("halt succeeded") + } + m.control, m.halted = debugEnable|haltRequest, true + writes := m.writes + if err := core.Release(t.Context()); err == nil || m.writes != writes || !m.halted { + t.Fatal("cleanup resumed an unattributed halt") + } + } +} diff --git a/target/cortexm/ignored_resume_test.go b/target/cortexm/ignored_resume_test.go new file mode 100644 index 0000000..7b74f77 --- /dev/null +++ b/target/cortexm/ignored_resume_test.go @@ -0,0 +1,105 @@ +package cortexm_test + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func TestIgnoredResumeRetainsCleanup(t *testing.T) { + for _, initial := range []uint32{0, debugEnable} { + for _, cleanup := range []bool{false, true} { + for _, failure := range []string{"none", "read", "cancel"} { + t.Run(fmt.Sprintf("debug=%d/cleanup=%t/failure=%s", initial, cleanup, failure), func(t *testing.T) { + checkIgnoredResume(t, initial, cleanup, failure) + }) + } + } + } +} + +func checkIgnoredResume(t *testing.T, initial uint32, cleanup bool, failure string) { + t.Helper() + m := newControlMemory() + 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) + } + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + if failure == "read" { + m.failRead = m.reads + 1 + if cleanup { + m.failRead++ + } + } + if failure == "cancel" { + m.onWrite = cancel + } + m.ignoreWrites = true + if cleanup { + err = core.Release(ctx) + } else { + err = core.Resume(ctx) + } + if err == nil { + t.Fatal("ignored resume was not reported") + } + if failure == "read" && !errors.Is(err, errMemory) { + t.Fatalf("read failure = %v", err) + } + if failure == "cancel" && !errors.Is(err, context.Canceled) { + t.Fatalf("cancellation = %v", err) + } + m.onWrite = nil + writes := m.writes + if err := core.Release(t.Context()); err == nil || m.writes != writes || !m.halted { + t.Fatalf("release=%v writes=%d halted=%t", err, m.writes, m.halted) + } + m.ignoreWrites = false + m.control, m.halted = debugEnable, false + if err := core.Release(t.Context()); err != nil || m.control != initial { + t.Fatalf("resolved release=%v control=%#x", err, m.control) + } +} + +func TestLaterResumeConfirmationPreservesIndependentHalt(t *testing.T) { + for _, initial := range []uint32{0, debugEnable} { + m := newControlMemory() + 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.failRead = m.reads + 1 + if err := core.Resume(t.Context()); !errors.Is(err, errMemory) { + t.Fatal(err) + } + m.halted = true + writes := m.writes + err = core.Release(t.Context()) + if m.writes != writes || !m.halted { + t.Fatal("cleanup resumed an independent halt") + } + if initial == debugEnable && err != nil { + t.Fatal(err) + } + if initial == 0 && err == nil { + t.Fatal("disabled debug despite an independent halt") + } + m.halted = false + if err := core.Release(t.Context()); err != nil || m.control != initial { + t.Fatalf("release retry=%v control=%#x", err, m.control) + } + } +} diff --git a/target/cortexm/lost_halt_test.go b/target/cortexm/lost_halt_test.go new file mode 100644 index 0000000..b407014 --- /dev/null +++ b/target/cortexm/lost_halt_test.go @@ -0,0 +1,96 @@ +package cortexm_test + +import ( + "fmt" + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func TestLostHaltRequestPreservesIndependentStop(t *testing.T) { + for _, initial := range []uint32{0, debugEnable} { + for _, stopped := range []bool{false, true} { + t.Run(fmt.Sprintf("debug=%d/stopped=%t", initial, stopped), func(t *testing.T) { + checkLostHaltRequest(t, initial, stopped) + }) + } + } +} + +func checkLostHaltRequest(t *testing.T, initial uint32, stopped bool) { + t.Helper() + m := newControlMemory() + m.control = initial + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + m.onWrite = func() { + m.control, m.halted = debugEnable, stopped + } + if err := core.Halt(t.Context()); err == nil { + t.Fatal("lost halt request was not reported") + } + m.onWrite = nil + m.halted = true + writes := m.writes + err = core.Release(t.Context()) + if m.writes != writes || !m.halted { + t.Fatal("cleanup resumed an independent halt after losing its request") + } + if initial == debugEnable && err != nil { + t.Fatalf("release with no remaining control changes: %v", err) + } + if initial == 0 && err == nil { + t.Fatal("disabled debug despite an independent halt") + } + m.halted = false + if err := core.Release(t.Context()); err != nil || m.control != initial { + t.Fatalf("release retry=%v control=%#x", err, m.control) + } +} + +func TestLaterObservationRelinquishesLostHaltRequest(t *testing.T) { + for _, initial := range []uint32{0, debugEnable} { + for _, status := range []bool{false, true} { + t.Run(fmt.Sprintf("debug=%d/status=%t", initial, status), func(t *testing.T) { + checkLaterHaltLoss(t, initial, status) + }) + } + } +} + +func checkLaterHaltLoss(t *testing.T, initial uint32, status bool) { + t.Helper() + m := newControlMemory() + 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.control, m.halted = debugEnable, false + writes := m.writes + if status { + if halted, err := core.Halted(t.Context()); err != nil || halted { + t.Fatalf("halted=%t err=%v", halted, err) + } + m.control, m.halted = debugEnable|haltRequest, true + if err := core.Resume(t.Context()); err == nil || m.writes != writes { + t.Fatal("resume retained ownership after observing the lost request") + } + } + m.halted = true + err = core.Release(t.Context()) + if m.writes != writes || !m.halted { + t.Fatal("release resumed a stop after observing the lost request") + } + if initial == debugEnable && err != nil { + t.Fatal(err) + } + if initial == 0 && err == nil { + t.Fatal("disabled debug despite an independent halt") + } +} diff --git a/target/cortexm/reentry_test.go b/target/cortexm/reentry_test.go new file mode 100644 index 0000000..62404b5 --- /dev/null +++ b/target/cortexm/reentry_test.go @@ -0,0 +1,74 @@ +package cortexm_test + +import ( + "testing" + + "github.com/jon/ostiole/target/cortexm" +) + +func TestCompletedResumeNeverReleasesNewHalt(t *testing.T) { + for _, cleanup := range []bool{false, true} { + for _, delay := range []int{1, 3} { + for _, initial := range []uint32{0, debugEnable} { + checkNewHalt(t, cleanup, delay, initial) + } + } + } +} + +func checkNewHalt(t *testing.T, cleanup bool, delay int, initial uint32) { + t.Helper() + m := newControlMemory() + 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.rehaltAfter = delay + if cleanup { + err = core.Release(t.Context()) + } else { + err = core.Resume(t.Context()) + } + if err == nil { + t.Fatal("new debug event was not reported") + } + writes := m.writes + err = core.Release(t.Context()) + if initial == debugEnable && delay > 1 && err != nil { + t.Fatal(err) + } + if (initial == 0 || delay == 1) && err == nil { + t.Fatal("released despite an unresolved halt") + } + if m.writes != writes || !m.halted { + t.Fatalf("replayed resume: initial=%d cleanup=%v delay=%d", initial, cleanup, delay) + } +} + +func TestUncertainResumeDoesNotReplayWhileHalted(t *testing.T) { + m := newControlMemory() + 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.failWrite = m.writes + 1 + if err := core.Resume(t.Context()); err == nil { + t.Fatal("resume succeeded") + } + writes := m.writes + if err := core.Release(t.Context()); err == nil || m.writes != writes { + t.Fatal("uncertain resume was replayed") + } + // A later running observation resolves the uncertainty without another run request. + m.control, m.halted = debugEnable, false + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } +} diff --git a/target/cortexm/restore.go b/target/cortexm/restore.go index af33ecc..d9cf3c9 100644 --- a/target/cortexm/restore.go +++ b/target/cortexm/restore.go @@ -13,11 +13,57 @@ func (t *Target) restore(ctx context.Context) error { if t.saved == 0 && value&cDebugEnable == 0 { return nil } - if value&cDebugEnable != 0 && value&(cStep|cMaskInts) != 0 { - return errors.New("cortexm: cannot restore externally changed stepping or interrupt masking") + if err := t.checkRestoreState(value); err != nil { + return err + } + if t.haltOwned { + if err := t.resume(ctx); err != nil { + return err + } + } + if !t.changed { + return nil + } + value, err = t.memory.ReadWord(ctx, dhcsrAddress) + if err != nil { + return err } if value&(cHalt|sHalt) != 0 && value&cDebugEnable != 0 { return errors.New("cortexm: new halt prevents restoring disabled debug") } return t.writeControl(ctx, t.saved) } + +func (t *Target) resume(ctx context.Context) error { + if err := ctx.Err(); err != nil { + return err + } + t.resumeUncertain = true + if err := t.memory.WriteWord(ctx, dhcsrAddress, debugKey|cDebugEnable); err != nil { + t.closing = true + return err + } + t.haltOwned = false + if err := t.waitHalt(ctx, false); err != nil { + t.closing = true + return err + } + return nil +} + +func (t *Target) checkRestoreState(value uint32) error { + if value&cDebugEnable != 0 && value&(cStep|cMaskInts) != 0 { + return errors.New("cortexm: cannot restore externally changed stepping or interrupt masking") + } + if !t.haltUncertain { + t.observeHaltRequest(value) + } + if t.resumeUncertain || t.haltUncertain { + if value&(cHalt|sHalt) != 0 { + return errors.New("cortexm: control write completion is unknown; refusing to resume this halt") + } + t.resumeUncertain, t.haltUncertain, t.haltOwned = false, false, false + t.changed = t.saved&cDebugEnable == 0 + } + return nil +} diff --git a/target/cortexm/run.go b/target/cortexm/run.go new file mode 100644 index 0000000..4ccb495 --- /dev/null +++ b/target/cortexm/run.go @@ -0,0 +1,130 @@ +package cortexm + +import ( + "context" + "errors" + "time" +) + +// Halt requests a halt and waits for Debug state. A halt already present when +// observed remains unowned. Failure after a write requires Release; the request +// might have taken effect. An unconfirmed write cannot establish ownership of +// an observed halt, so it can prevent cleanup. Halting does not stop peripheral +// clocks or undo instructions executed before the halt. +func (t *Target) Halt(ctx context.Context) error { + if err := t.active(ctx); err != nil { + return err + } + ctx, cancel := context.WithTimeout(ctx, controlTimeout) + defer cancel() + value, err := t.readControl(ctx) + if err != nil || value&sHalt != 0 { + return err + } + return t.requestHalt(ctx) +} + +// Resume releases a halt requested by this target and waits to leave Debug +// state. It refuses inherited halts. Failure can mean execution has already +// resumed; only Release remains available. Until readback confirms that the +// halt request cleared, cleanup stays pending without repeating resume. +// No instruction effects are undone. +func (t *Target) Resume(ctx context.Context) error { + if err := t.active(ctx); err != nil { + return err + } + if !t.haltOwned { + return errors.New("cortexm: no owned halt to resume") + } + ctx, cancel := context.WithTimeout(ctx, controlTimeout) + defer cancel() + if err := t.resume(ctx); err != nil { + return err + } + return nil +} + +// Halted reads the current Debug state. It consumes DHCSR's sticky reset and +// instruction-retirement indicators, and does not establish halt ownership. +func (t *Target) Halted(ctx context.Context) (bool, error) { + if err := t.active(ctx); err != nil { + return false, err + } + ctx, cancel := context.WithTimeout(ctx, controlTimeout) + defer cancel() + value, err := t.readControl(ctx) + return value&sHalt != 0, err +} + +func (t *Target) active(ctx context.Context) error { + if t == nil || t.memory == nil || t.closing { + return errors.New("cortexm: target is unavailable; release may be pending") + } + return liveContext(ctx) +} + +func (t *Target) readControl(ctx context.Context) (uint32, error) { + value, err := t.memory.ReadWord(ctx, dhcsrAddress) + if err == nil && (value&cDebugEnable == 0 || value&(cStep|cMaskInts) != 0) { + err = errors.New("cortexm: halting debug control changed outside the target") + } + if err != nil { + t.closing = true + } else { + t.observeHaltRequest(value) + } + return value, err +} + +func (t *Target) observeHaltRequest(value uint32) { + if (t.haltOwned || t.resumeUncertain) && value&cDebugEnable != 0 && value&cHalt == 0 { + t.haltOwned, t.resumeUncertain = false, false + t.changed = t.saved&cDebugEnable == 0 + } +} + +func (t *Target) requestHalt(ctx context.Context) error { + if err := ctx.Err(); err != nil { + return err + } + t.changed = true + t.haltUncertain = true + if err := t.memory.WriteWord(ctx, dhcsrAddress, debugKey|cDebugEnable|cHalt); err != nil { + t.closing = true + return err + } + t.haltUncertain, t.haltOwned = false, true + if err := t.waitHalt(ctx, true); err != nil { + t.closing = true + return err + } + return nil +} + +func (t *Target) waitHalt(ctx context.Context, halted bool) error { + for { + if err := ctx.Err(); err != nil { + return err + } + value, err := t.readControl(ctx) + if err != nil { + return err + } + if value&cHalt == 0 && halted { + return errors.New("cortexm: halt request was not retained") + } + if value&cHalt != 0 && !halted { + return errors.New("cortexm: halt request remains set after resume") + } + if (value&sHalt != 0) == halted { + return nil + } + timer := time.NewTimer(time.Millisecond) + select { + case <-ctx.Done(): + timer.Stop() + return ctx.Err() + case <-timer.C: + } + } +} diff --git a/target/cortexm/run_test.go b/target/cortexm/run_test.go new file mode 100644 index 0000000..298d333 --- /dev/null +++ b/target/cortexm/run_test.go @@ -0,0 +1,132 @@ +package cortexm_test + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/jon/ostiole/target/cortexm" +) + +func TestHaltResumeAndRelease(t *testing.T) { + for _, initial := range []uint32{0, debugEnable} { + m := newControlMemory() + 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) + } + if halted, err := core.Halted(t.Context()); err != nil || !halted { + t.Fatalf("halted=%v err=%v", halted, err) + } + if err := core.Resume(t.Context()); 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 || m.halted || m.control != initial { + t.Fatalf("release=%v control=%#x halted=%v", err, m.control, m.halted) + } + } +} + +func TestInheritedHaltCannotBeResumed(t *testing.T) { + for _, control := range []uint32{debugEnable, debugEnable | haltRequest} { + m := newControlMemory() + m.control, m.halted = control, true + 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 halted, err := core.Halted(t.Context()); err != nil || !halted { + t.Fatalf("halted=%t err=%v", halted, err) + } + if err := core.Resume(t.Context()); err == nil { + t.Fatal("resumed inherited halt") + } + if err := core.Release(t.Context()); err != nil || !m.halted || m.control != control || m.writes != 0 { + t.Fatalf("release=%v halted=%v control=%#x writes=%d", err, m.halted, m.control, m.writes) + } + } +} + +func TestHaltReadbackFailureRequiresRelease(t *testing.T) { + m := newControlMemory() + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + m.failRead, m.afterWrite = m.reads+2, true + if err := core.Halt(t.Context()); !errors.Is(err, errMemory) { + t.Fatal(err) + } + reads, writes := m.reads, m.writes + if err := core.Resume(t.Context()); err == nil { + t.Fatal("resume after uncertain halt succeeded") + } + if _, err := core.Halted(t.Context()); err == nil { + t.Fatal("status after uncertain halt succeeded") + } + if reads != m.reads || writes != m.writes { + t.Fatal("ordinary traffic during cleanup") + } + m.failWrite = m.writes + 1 + if err := core.Release(t.Context()); !errors.Is(err, errMemory) { + t.Fatal(err) + } + if err := core.Release(t.Context()); err != nil || m.control != 0 || m.halted { + t.Fatalf("release=%v control=%#x", err, m.control) + } +} + +func TestControlCancellationBeforeTrafficAndPolling(t *testing.T) { + m := newControlMemory() + core, err := cortexm.Acquire(t.Context(), m) + if err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithCancel(t.Context()) + cancel() + reads, writes := m.reads, m.writes + if err := core.Halt(ctx); !errors.Is(err, context.Canceled) { + t.Fatal(err) + } + if reads != m.reads || writes != m.writes { + t.Fatal("canceled halt reached memory") + } + m.stall = true + ctx, cancel = context.WithTimeout(t.Context(), 10*time.Millisecond) + defer cancel() + if err := core.Halt(ctx); !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("halt = %v", err) + } + m.stall = false + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } +} + +func TestInactiveTargetRejectsControl(t *testing.T) { + for _, core := range []*cortexm.Target{nil, {}} { + if err := core.Halt(t.Context()); err == nil { + t.Fatal("inactive halt") + } + if err := core.Resume(t.Context()); err == nil { + t.Fatal("inactive resume") + } + if _, err := core.Halted(t.Context()); err == nil { + t.Fatal("inactive status") + } + if err := core.Release(t.Context()); err != nil { + t.Fatal(err) + } + } +} From 2843813ce2b5e293ebc0862a1f6a3b53cc82aebb Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sat, 26 Sep 2026 13:48:42 -0700 Subject: [PATCH 3/4] Exercise Cortex-M0 control with a counter firmware bench. Identity reads cannot establish that execution stops and restarts. Supply a Cortex-M0 counter program and an explicitly gated micro:bit test which checks progress around halt, resume, and release in fresh sessions. Keep firmware programming separate from the Ostiole control path, and provide a small executable composition with ordered target cleanup. --- README.md | 14 +- docs/README.md | 2 +- docs/architecture.md | 4 +- docs/capabilities.md | 5 +- docs/composition.md | 2 +- docs/cortexm.md | 59 ++++++++ examples/README.md | 8 +- examples/simple/cortexm-control/main.go | 89 +++++++++++ target/cortexm/control_integration_test.go | 163 +++++++++++++++++++++ target/cortexm/testdata/counter/README.md | 39 +++++ target/cortexm/testdata/counter/counter.S | 27 ++++ target/cortexm/testdata/counter/counter.ld | 9 ++ 12 files changed, 407 insertions(+), 14 deletions(-) create mode 100644 examples/simple/cortexm-control/main.go create mode 100644 target/cortexm/control_integration_test.go create mode 100644 target/cortexm/testdata/counter/README.md create mode 100644 target/cortexm/testdata/counter/counter.S create mode 100644 target/cortexm/testdata/counter/counter.ld diff --git a/README.md b/README.md index 09b531d..b370b31 100644 --- a/README.md +++ b/README.md @@ -131,13 +131,13 @@ root. See [Linux USB access](docs/linux-usb.md) for udev rules and a bounded ## Safety Debug and programming interfaces can reset processors, halt execution, modify -memory, reconfigure programmable logic, and change persistent device state. The -shipped examples and `ost` commands avoid reset, halt, target-memory writes, and -persistent changes. The Cortex-M0 target API enables halting debug and controls -execution. The `dap.MemAP` API exposes effectful scalar writes; callers choose -the addresses and own the consequences. Establishing an ADIv5 connection also -changes volatile debug-port control state; the connection releases its own power -requests before return. +memory, reconfigure programmable logic, and change persistent device state. +The inspection examples and `ost` commands avoid reset, halt, target-memory +writes, and persistent changes. The separately gated `cortexm-control` example +enables halting debug and halts and resumes a Cortex-M0. The `dap.MemAP` API +does expose effectful scalar writes; callers choose the addresses and own the consequences. +Establishing an ADIv5 connection also changes volatile debug-port control +state; the connection releases its own power requests before return. ## SWD DPIDR example diff --git a/docs/README.md b/docs/README.md index 5d04530..c317672 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,7 +19,7 @@ today and how to assemble them without duplicating lower-level behavior. - [CoreSight component inspection](coresight.md) describes identification registers, ROM entry decoding, bounded traversal, and the inspection example. - [Cortex-M control](cortexm.md) describes Cortex-M0 acquisition, halt/resume, - and restoration over borrowed word memory. + 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 76b349b..7321084 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -381,8 +381,8 @@ replaceable while exercising the public protocol and DAP layers. ## Safety effects The inspection examples and `ost` commands do not reset or halt the target, -write target memory, or change persistent state. The Cortex-M0 target API -enables halting debug and controls execution. +write target memory, or change persistent state. The explicitly gated +`cortexm-control` example enables debug and halts and resumes Cortex-M0. The `dap.MemAP` API does expose scalar and block target-memory writes; applications choose the affected addresses and own the consequences. diff --git a/docs/capabilities.md b/docs/capabilities.md index b06e1a8..123621d 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -302,7 +302,7 @@ layouts and power-domain skips have hardware-independent test coverage. | --- | --- | --- | | CPUID read and decode | Yes | Accepts any aligned-word reader and validates a plausible Arm Cortex-M identity. | | Physical identity read | HIL | Opt-in FTDI/SWD/DAP/MEM-AP integration test. | -| Cortex-M0 acquisition and halt/resume | Yes | Borrowed word memory, inherited-halt protection, bounded operations, and retryable cleanup. Behavioral failure tests pass; physical control evidence is not yet recorded. | +| Cortex-M0 acquisition and halt/resume | HIL | Two fresh CMSIS-DAP micro:bit sessions at a requested 100 kHz stopped a CPU counter during halt and observed progress after resume and release. Both preserved initially enabled debug and running state before Arm debug owner close. Initially disabled debug and cleanup failures are covered only by behavioral tests; see the [control evidence](cortexm.md#hardware-evidence). | | Step | No | No single-step API exists. | | Register access | No | CPUID decoding is not a general core-register interface. | | Reset | No | No architectural or pin-reset operation exists. | @@ -326,6 +326,9 @@ Available examples: - `examples/simple/arm-info` reports the same identities through generic probe discovery and one Arm debug owner, with explicit AP selection. +`examples/simple/cortexm-control` separately demonstrates effectful Cortex-M0 +halt/resume and requires `-allow-control`. + Available `ost` commands: ```text diff --git a/docs/composition.md b/docs/composition.md index 3f29c89..c2c34ff 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -36,7 +36,7 @@ data-register write can write target memory. | Identify one debug component through scalar memory | `coresight.Identify` | `examples/simple/coresight-info` | | Inspect ROM entries or a bounded component hierarchy | `Component.ROMTable`, `ROMTable.ReadEntry`, `coresight.Walk` | `examples/simple/coresight-info -walk` | | Identify a Cortex-M through any compatible word reader | `cortexm.Identify` | `examples/simple/cortexm-info` | -| Acquire, halt, and resume a Cortex-M0 | `cortexm.Acquire`, `Target.Halt`, `Target.Resume`, `Target.Release` | [Cortex-M control](cortexm.md) | +| Acquire, halt, and resume a Cortex-M0 | `cortexm.Acquire`, `Target.Halt`, `Target.Resume`, `Target.Release` | `examples/simple/cortexm-control` | | Test SWD and DAP behavior without hardware | `swd/sim`, `dap/sim` | Package tests | The examples are intentionally small, executable compositions of public diff --git a/docs/cortexm.md b/docs/cortexm.md index fde3176..6c8fc62 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -89,6 +89,65 @@ 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, resumes, then releases the target before closing the +connection. It requires explicit consent to control execution: + +```sh +go run ./examples/simple/cortexm-control \ + -provider cmsisdap -serial SERIAL -ap 0 -allow-control +``` + 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. + +## Hardware procedure + +The opt-in integration test selects the CMSIS-DAP micro:bit with serial +`9900360140124e4500279015000000360000000097969901`, AP0, and a requested +100 kHz clock. It requires a known firmware program with an aligned 32-bit RAM +counter incremented by the CPU at least once per 200 milliseconds. The counter +must not be updated by DMA or another processor. Loading firmware is outside +the test. + +The [counter firmware](../target/cortexm/testdata/counter/README.md) supplies +a loop that increments the counter at `0x20000000`, with build instructions +and a separate programming procedure. Loading it replaces the target program +and resets the processor. + +```sh +OSTIOLE_CORTEXM_HIL_CONTROL=1 \ +OSTIOLE_CORTEXM_HIL_PROGRAM='program name and build identity' \ +OSTIOLE_CORTEXM_HIL_COUNTER=0xRAM_ADDRESS \ +go test -tags integration ./target/cortexm -run '^TestHILCortexM0Control$' -v +``` + +Two fresh sessions check counter progress before control, no progress during +a halt, and renewed progress after resume and after release from a second +halt. The test compares inherited debug-enable and halt status before closing +the Arm debug owner. It refuses an already-halted bench. These observations +do not establish peripheral behavior, register preservation, reset, stepping, +or restoration after a physical transport failure. + +## Hardware evidence + +On September 26, 2026, Nostalgia (macOS) completed the control test in two +fresh sessions on the selected micro:bit, with Cortex-M0 CPUID `0x410cc200`. +OpenOCD 0.12.0 programmed and verified the counter image using the procedure +above. The Intel HEX image's SHA-256 was +`ee294cc06ab6e8228161b49506675b065c0148b26421cf1f83c8e45e35cd4e5d`. + +In both sessions, the CPU counter advanced before acquisition, remained +unchanged across ten samples 20 milliseconds apart while halted, and advanced +after resume and after release from a second halt. The halted values were +`0x014c4757` and `0x017ebe6c`. Both target releases and Arm debug owner closes +completed. DHCSR showed debug enabled and the processor running before +acquisition and after release in each session; the first read also consumed +the sticky reset indicator. + +This run covers inherited enabled debug on one micro:bit. It does not verify +enabling and restoring initially disabled debug, restoration after closing +the Arm debug owner, or cleanup after a physical transport failure. Earlier +attempts with Ostiole and OpenOCD could not read DPIDR; the cause of that +connection failure and its recovery remain unknown. diff --git a/examples/README.md b/examples/README.md index 7674369..50b9d9f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,8 +5,8 @@ Ostiole examples are grouped by how much of the library they compose. `trivial/` contains small protocol demonstrations. They make one narrow operation visible and are primarily useful for learning or hardware bring-up. -`simple/` is reserved for focused inspection tools that could be useful on -their own, such as processor, CoreSight, or ROM-table discovery. +`simple/` contains focused inspection and control tools, such as processor +identity, CoreSight discovery, and Cortex-M0 halt/resume. `advanced/` is reserved for composed workflows such as loading ELF payloads, programming firmware or FPGA bitstreams, and extracting data through a @@ -28,6 +28,10 @@ an example has been implemented. - [`simple/coresight-info`](simple/coresight-info) reads the advertised debug entry of a selected MEM-AP, or a known component page, through a managed SWD connection. Add `-walk` for bounded ROM traversal with partial-result reporting. +- [`simple/cortexm-control`](simple/cortexm-control) enables Cortex-M0 halting + debug, halts and resumes the processor, then restores debug control. It + requires `-allow-control`; see [Cortex-M control](../docs/cortexm.md) for + effects and cleanup limits. For ADIv6 SW-DP targets, `coresight-info -debug-space -walk` inspects the DP's advertised discovery tree. Use `-ap-base ADDRESS` instead of `-ap INDEX` to diff --git a/examples/simple/cortexm-control/main.go b/examples/simple/cortexm-control/main.go new file mode 100644 index 0000000..60ea21e --- /dev/null +++ b/examples/simple/cortexm-control/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + "time" + + "github.com/jon/ostiole/armdebug" + "github.com/jon/ostiole/dap" + "github.com/jon/ostiole/discover" + _ "github.com/jon/ostiole/discover/probes" + "github.com/jon/ostiole/probe" + "github.com/jon/ostiole/target/cortexm" +) + +func main() { + if err := run(); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run() (err error) { + provider := flag.String("provider", "", "required probe provider") + serial := flag.String("serial", "", "required probe serial") + ap := flag.Int("ap", -1, "required MEM-AP index (0..255)") + allow := flag.Bool("allow-control", false, "allow enabling debug, halting, and resuming the processor") + flag.Parse() + if !*allow || *provider == "" || *serial == "" || *ap < 0 || *ap > 255 || flag.NArg() != 0 { + return errors.New("require -allow-control, -provider, -serial, and -ap 0..255") + } + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + c, err := armdebug.Open(ctx, discover.Selection{ + Provider: discover.ProviderID(*provider), Serial: *serial, + }, armdebug.Config{Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000})}) + var core *cortexm.Target + if c != nil { + defer func() { err = errors.Join(err, release(core, c)) }() + } + if err != nil { + return err + } + memory, err := c.OpenMemAP(ctx, dap.NewAPSel(uint8(*ap))) + if err != nil { + return err + } + core, err = cortexm.Acquire(ctx, memory) + if err != nil { + return err + } + return control(ctx, core) +} + +func control(ctx context.Context, core *cortexm.Target) error { + if err := core.Halt(ctx); err != nil { + return err + } + fmt.Printf("CPUID=%#08x halted\n", core.Identity().Raw) + if err := core.Resume(ctx); err != nil { + return err + } + fmt.Println("resumed") + return nil +} + +func release(core *cortexm.Target, c *armdebug.Conn) error { + var err error + for range 3 { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + err = core.Release(ctx) + cancel() + if err == nil { + break + } + } + if err != nil { + return fmt.Errorf("target restoration remains pending; memory owner retained: %w", err) + } + for range 3 { + if err = c.Close(); err == nil { + return nil + } + } + return fmt.Errorf("connection cleanup remains pending: %w", err) +} diff --git a/target/cortexm/control_integration_test.go b/target/cortexm/control_integration_test.go new file mode 100644 index 0000000..fda0eca --- /dev/null +++ b/target/cortexm/control_integration_test.go @@ -0,0 +1,163 @@ +//go:build integration + +package cortexm_test + +import ( + "context" + "errors" + "os" + "strconv" + "testing" + "time" + + "github.com/jon/ostiole/armdebug" + "github.com/jon/ostiole/dap" + "github.com/jon/ostiole/discover" + _ "github.com/jon/ostiole/discover/probes" + "github.com/jon/ostiole/probe" + "github.com/jon/ostiole/target/cortexm" +) + +func TestHILCortexM0Control(t *testing.T) { + if os.Getenv("OSTIOLE_CORTEXM_HIL_CONTROL") != "1" { + t.Skip("OSTIOLE_CORTEXM_HIL_CONTROL is not 1") + } + program := os.Getenv("OSTIOLE_CORTEXM_HIL_PROGRAM") + counter, err := strconv.ParseUint(os.Getenv("OSTIOLE_CORTEXM_HIL_COUNTER"), 0, 32) + if err != nil || counter%4 != 0 || program == "" { + 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) }) { + return + } + } +} + +func controlHIL(t *testing.T, counter uint32, program string) { + t.Helper() + ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second) + defer cancel() + c := openControlBench(t, ctx) + var core *cortexm.Target + t.Cleanup(func() { releaseControlBench(t, core, c) }) + memory, err := c.OpenMemAP(ctx, dap.NewAPSel(0)) + if err != nil { + t.Fatal(err) + } + before, err := memory.ReadWord(ctx, dhcsr) + if err != nil { + t.Fatal(err) + } + if before&haltStatus != 0 { + t.Fatal("bench is already halted; refusing to resume it") + } + checkCounterHIL(t, ctx, memory, counter, "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) + } + checkCounterHIL(t, ctx, memory, counter, "halted", true) + if err := core.Resume(ctx); err != nil { + t.Fatal(err) + } + checkCounterHIL(t, ctx, memory, counter, "resumed", false) + if err := core.Halt(ctx); err != nil { + t.Fatal(err) + } + if err := core.Release(ctx); err != nil { + t.Fatal(err) + } + after, err := memory.ReadWord(ctx, dhcsr) + if err != nil { + t.Fatal(err) + } + mask := debugEnable | haltStatus + if after&mask != before&mask { + t.Fatalf("DHCSR before=%#x after=%#x", before, after) + } + checkCounterHIL(t, ctx, memory, counter, "released", false) + t.Logf("micro:bit CMSIS-DAP 100 kHz AP0 CPUID=%#x program=%q counter=%#x DHCSR before=%#x after=%#x", + core.Identity().Raw, program, counter, before, after) +} + +func checkCounterHIL(t *testing.T, ctx context.Context, memory *dap.MemAP, addr uint32, phase string, stopped bool) { + t.Helper() + first, err := memory.ReadWord(ctx, addr) + if err != nil { + t.Fatal(err) + } + changed := false + last := first + for range 10 { + select { + case <-ctx.Done(): + t.Fatal(ctx.Err()) + case <-time.After(20 * time.Millisecond): + } + value, err := memory.ReadWord(ctx, addr) + if err != nil { + t.Fatal(err) + } + changed = changed || value != first + last = value + } + t.Logf("%s: counter[%#x] first=%#x last=%#x changed=%v", phase, addr, first, last, changed) + if changed == stopped { + t.Fatalf("counter at %#x: changed=%v stopped=%v", addr, changed, stopped) + } +} + +func openControlBench(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", + }) + if errors.Is(err, discover.ErrCandidateNotFound) || errors.Is(err, discover.ErrCandidateAmbiguous) { + t.Skip(err) + } + if err != nil { + t.Fatal(err) + } + c, err := armdebug.Open(ctx, discover.Selection{Binding: candidate.Info().Binding}, armdebug.Config{ + Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000}), + }) + if err != nil { + if c != nil { + releaseControlBench(t, nil, c) + } + t.Fatal(err) + } + return c +} + +func releaseControlBench(t *testing.T, core *cortexm.Target, c *armdebug.Conn) { + t.Helper() + var err error + for range 3 { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + err = core.Release(ctx) + cancel() + if err == nil { + break + } + } + if err != nil { + t.Errorf("target cleanup remains pending; retaining memory owner: %v", err) + return + } + for range 3 { + if err = c.Close(); err == nil { + t.Log("target released and Arm debug owner closed") + return + } + } + t.Errorf("Arm debug cleanup remains pending: %v", err) +} diff --git a/target/cortexm/testdata/counter/README.md b/target/cortexm/testdata/counter/README.md new file mode 100644 index 0000000..f7c97d8 --- /dev/null +++ b/target/cortexm/testdata/counter/README.md @@ -0,0 +1,39 @@ +# Cortex-M0 counter firmware + +This micro:bit v1 bench program increments the 32-bit word at `0x20000000` +in a CPU loop. It disables interrupts and uses no peripheral or DMA engine. +The vector table starts at flash address zero and uses the top of the first +16 KiB of RAM for the initial stack pointer. Every unexpected exception loops +without changing the counter. + +Build from the repository root with Clang's Arm assembler, LLD, and GNU Arm +objcopy: + +```sh +clang --target=arm-none-eabi -mcpu=cortex-m0 -mthumb -c \ + target/cortexm/testdata/counter/counter.S -o /tmp/ostiole-counter.o +ld.lld -T target/cortexm/testdata/counter/counter.ld \ + /tmp/ostiole-counter.o -o /tmp/ostiole-counter.elf +arm-none-eabi-objcopy -O ihex /tmp/ostiole-counter.elf /tmp/ostiole-counter.hex +``` + +Loading this image replaces the target program and resets the processor. It +does not update the DAPLink interface firmware. Select the exact probe when +using an external programmer. Programming is bench preparation, separate from +the Ostiole [control test](../../../../docs/cortexm.md#hardware-procedure). + +For the selected micro:bit, use OpenOCD's CMSIS-DAP v2 transport and nRF51 +flash driver: + +```sh +openocd -f interface/cmsis-dap.cfg \ + -c 'cmsis_dap_backend usb_bulk' \ + -c 'adapter serial 9900360140124e4500279015000000360000000097969901' \ + -f target/nrf51.cfg -c 'adapter speed 100' \ + -c 'gdb_port disabled; tcl_port disabled; telnet_port disabled' \ + -c 'program /tmp/ostiole-counter.hex verify reset exit' +``` + +After successful verification and reset, run the control test with +`OSTIOLE_CORTEXM_HIL_COUNTER=0x20000000`. Use the image's SHA-256 as the program +identity in `OSTIOLE_CORTEXM_HIL_PROGRAM`. diff --git a/target/cortexm/testdata/counter/counter.S b/target/cortexm/testdata/counter/counter.S new file mode 100644 index 0000000..3709816 --- /dev/null +++ b/target/cortexm/testdata/counter/counter.S @@ -0,0 +1,27 @@ +.syntax unified +.cpu cortex-m0 +.thumb + +.section .vectors, "a", %progbits +.word 0x20004000 +.word reset_handler +.rept 46 +.word default_handler +.endr + +.section .text, "ax", %progbits +.thumb_func +.global reset_handler +reset_handler: + cpsid i + ldr r1, =0x20000000 + movs r0, #0 +counter_loop: + adds r0, #1 + str r0, [r1] + b counter_loop + +.thumb_func +default_handler: + b default_handler +.ltorg diff --git a/target/cortexm/testdata/counter/counter.ld b/target/cortexm/testdata/counter/counter.ld new file mode 100644 index 0000000..5e25095 --- /dev/null +++ b/target/cortexm/testdata/counter/counter.ld @@ -0,0 +1,9 @@ +ENTRY(reset_handler) +MEMORY { + FLASH (rx) : ORIGIN = 0, LENGTH = 256K +} +SECTIONS { + .vectors : { KEEP(*(.vectors)) } > FLASH + .text : { *(.text*) } > FLASH + /DISCARD/ : { *(.comment) *(.ARM.attributes) } +} From efe53fd77dcfd37aa4e6d7c1799f823d28cc1e93 Mon Sep 17 00:00:00 2001 From: Jon Olson Date: Sat, 26 Sep 2026 14:24:10 -0700 Subject: [PATCH 4/4] Default SWD tools and micro:bit benches to 1 MHz. The micro:bit tests requested 100 kHz, below the nRF51's documented 125 kHz minimum for entering debug mode after power-on. At 100 kHz, they passed after OpenOCD activated the interface but failed when Ostiole connected first after a physical replug. Default the SWD tools and micro:bit benches to 1 MHz and document the cold-start observation. Let the probe-based inspection and control examples accept a clock ceiling for targets that need another rate. Keep clock selection with the caller; the CMSIS-DAP driver is unchanged. --- README.md | 6 ++- cmd/ost/internal/app/session.go | 2 +- cmsisdap/session_integration_test.go | 4 +- coresight/component_integration_test.go | 4 +- coresight/walk_integration_test.go | 4 +- docs/capabilities.md | 6 +-- docs/composition.md | 17 ++++---- docs/coresight.md | 28 ++++++++++---- docs/cortexm.md | 45 ++++++++++++---------- docs/protocols/cmsisdap.md | 35 +++++++++++++++++ examples/README.md | 5 +++ examples/simple/ap-id/main.go | 2 +- examples/simple/arm-info/main.go | 6 ++- examples/simple/coresight-info/main.go | 6 ++- examples/simple/cortexm-control/main.go | 19 ++++++--- examples/simple/cortexm-info/main.go | 2 +- examples/trivial/swd-dpidr/main.go | 2 +- target/cortexm/control_integration_test.go | 4 +- target/cortexm/testdata/counter/README.md | 7 +++- 19 files changed, 144 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index b370b31..bd62c3f 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,10 @@ root. See [Linux USB access](docs/linux-usb.md) for udev rules and a bounded Debug and programming interfaces can reset processors, halt execution, modify memory, reconfigure programmable logic, and change persistent device state. +The SWD inspection examples and `ost` commands request a 1 MHz clock ceiling. +The `arm-info`, `coresight-info`, and `cortexm-control` examples accept +`-clock` in Hz for targets that require another rate. + The inspection examples and `ost` commands avoid reset, halt, target-memory writes, and persistent changes. The separately gated `cortexm-control` example enables halting debug and halts and resumes a Cortex-M0. The `dap.MemAP` API @@ -142,7 +146,7 @@ state; the connection releases its own power requests before return. ## SWD DPIDR example The program expects exactly one supported FTDI H-series attachment and uses -MPSSE port A at 400 kHz. Connect it to a powered SWD target as follows: +MPSSE port A at 1 MHz. Connect it to a powered SWD target as follows: | Adapter signal | Target signal | | --- | --- | diff --git a/cmd/ost/internal/app/session.go b/cmd/ost/internal/app/session.go index 5157cca..e58c718 100644 --- a/cmd/ost/internal/app/session.go +++ b/cmd/ost/internal/app/session.go @@ -48,7 +48,7 @@ func openSWDTransport(ctx context.Context) (*swdSession, error) { if err != nil { return nil, err } - channel, err := ftdi.Open(ctx, device, ftdi.Config{Port: ftdi.PortA, MaxClockHz: 400_000}) + channel, err := ftdi.Open(ctx, device, ftdi.Config{Port: ftdi.PortA, MaxClockHz: 1_000_000}) if err != nil { closeOwner := device.Close if channel != nil { diff --git a/cmsisdap/session_integration_test.go b/cmsisdap/session_integration_test.go index 2e1ebdc..24a9568 100644 --- a/cmsisdap/session_integration_test.go +++ b/cmsisdap/session_integration_test.go @@ -101,13 +101,13 @@ func observeCMSISDAPTarget(t *testing.T, ctx context.Context, readyOnOpen bool) t.Helper() var options []cmsisdap.Option if readyOnOpen { - options = append(options, cmsisdap.WithSWD(100_000)) + options = append(options, cmsisdap.WithSWD(1_000_000)) } session := openCMSISDAPSession(t, ctx, options...) cleanup := newCMSISDAPCleanup(t) cleanup.retain("CMSIS-DAP session", func(context.Context) error { return session.Close() }) if !readyOnOpen { - if err := session.ConfigureSWD(ctx, 100_000); err != nil { + if err := session.ConfigureSWD(ctx, 1_000_000); err != nil { t.Fatal(err) } } diff --git a/coresight/component_integration_test.go b/coresight/component_integration_test.go index 7162db5..45f78b1 100644 --- a/coresight/component_integration_test.go +++ b/coresight/component_integration_test.go @@ -32,7 +32,7 @@ func TestHILComponentIdentity(t *testing.T) { base uint64 class uint8 }{ - {"microbit", discover.Selection{Provider: "cmsisdap", Serial: "9900360140124e4500279015000000360000000097969901"}, armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000}), 0, 0xe00ff000, 1}, + {"microbit", discover.Selection{Provider: "cmsisdap", Serial: "9900360140124e4500279015000000360000000097969901"}, armdebug.SWDP(probe.SWDConfig{MaxClockHz: 1_000_000}), 0, 0xe00ff000, 1}, {"zcu104", discover.Selection{Provider: "ftdi", Serial: "01691", Function: "A"}, armdebug.JTAGDP(probe.JTAGConfig{MaxClockHz: 100_000}, jtag.Layout{arm, xilinx}, 0), 1, 0x80410000, 9}, } { t.Run(bench.name, func(t *testing.T) { @@ -53,7 +53,7 @@ func TestHILComponentIdentity(t *testing.T) { if got.Class() != bench.class { t.Fatalf("class=%#x, want %#x", got.Class(), bench.class) } - t.Logf("session=%d AP%d 100 kHz identity=%+v", session, bench.ap, got) + t.Logf("session=%d AP%d identity=%+v", session, bench.ap, got) }) { return } diff --git a/coresight/walk_integration_test.go b/coresight/walk_integration_test.go index ddb4161..6885b40 100644 --- a/coresight/walk_integration_test.go +++ b/coresight/walk_integration_test.go @@ -33,7 +33,7 @@ func TestHILROMWalk(t *testing.T) { arm, _ := jtag.IDCODE(4, 0x5ba00477) xilinx, _ := jtag.IDCODE(12, 0x14730093) for _, bench := range []romBench{ - {"microbit", discover.Selection{Provider: "cmsisdap", Serial: "9900360140124e4500279015000000360000000097969901"}, armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000}), 0, 6, 0}, + {"microbit", discover.Selection{Provider: "cmsisdap", Serial: "9900360140124e4500279015000000360000000097969901"}, armdebug.SWDP(probe.SWDConfig{MaxClockHz: 1_000_000}), 0, 6, 0}, {"zcu104", discover.Selection{Provider: "ftdi", Serial: "01691", Function: "A"}, armdebug.JTAGDP(probe.JTAGConfig{MaxClockHz: 100_000}, jtag.Layout{arm, xilinx}, 0), 1, 18, 0x803e0000}, } { t.Run(bench.name, func(t *testing.T) { @@ -69,7 +69,7 @@ func observeROMWalk(t *testing.T, bench romBench, session int) { t.Logf("visit=%d parent=%d entry=%d base=%#x: %v", i, v.Parent, v.Index, v.Entry.Base, v.Err) } } - t.Logf("session=%d AP%d 100 kHz root=%#x visits=%d complete=%t error=%v", session, bench.ap, base, len(visits), err == nil, err) + t.Logf("session=%d AP%d root=%#x visits=%d complete=%t error=%v", session, bench.ap, base, len(visits), err == nil, err) checkROMWalkObservation(t, bench, visits, err) } diff --git a/docs/capabilities.md b/docs/capabilities.md index 123621d..2d90cce 100644 --- a/docs/capabilities.md +++ b/docs/capabilities.md @@ -114,7 +114,7 @@ does not prove that the board connects those pins to a debug target. | FT232H | Yes | Port A; full MPSSE and SWD HIL on Linux and macOS. | | FT2232H | Yes | Ports A and B using the standard H-series interface and endpoint layout. | | FT4232H | Yes | Ports A and B using the standard H-series interface and endpoint layout. | -| Explicit clock | Yes | `MaxClockHz` is a ceiling; `Channel.ClockHz` reports the attainable configured rate. Examples request 400 kHz. | +| Explicit clock | Yes | `MaxClockHz` is a ceiling; `Channel.ClockHz` reports the attainable configured rate. SWD examples request 1 MHz. | | MPSSE lifecycle | Yes | Claim, reset bit mode, purge stale traffic, synchronize, and configure the clock with target pins as inputs. Close drains pending bulk OUT work before resetting bit mode, setting the latency timer to 16 ms, purging the receive and transmit paths, releasing, and closing. | | SWD bit streams | Yes | Direction-safe output and input runs. Enough maximum-packet-sized IN transfers remain posted to cover the worst-case response admitted by the shared 8,192-clock wire limit, including FTDI status bytes. That requires seventeen requests for a 512-byte endpoint and 133 for a 64-byte endpoint. The receive path consumes them in submission order, replenishes each before delivering its payload, and discards status-only packets independently of OUT completion. | | Ambiguous transfer handling | Yes | A USB error, including an asynchronous receive failure, invalid transfer count, malformed FTDI packet, or surplus payload poisons the channel. A call which observes the poisoned channel returns the first cause and matches `ErrChannelPoisoned`; later SWD traffic requires a fresh channel. `Close` remains available and retryable. | @@ -178,7 +178,7 @@ requires a supported v2 interface when SWD is activated. | Ownership and cleanup | Yes | Successful open owns the USB device. After failed SWD configuration, `Open` makes a bounded cleanup attempt; if a synchronized disconnect remains pending, it returns the session with the error. After a poisoned exchange, `Close` reports the abandoned port and continues USB cleanup without sending another command. Interface release remains retryable, and device close runs once. When failed open returns no session, the caller closes the device to finish or repeat cleanup. | | Passive v1 rejection | HIL | The Linux all-device inventory reported the `0d28:0204` DAPLink product and serial. HIL selected it by serial, then rejected it from the v2 path before interface claim. Its command interface is HID; no CMSIS-DAP command or target traffic was sent. | | v2 metadata reopen | HIL | The macOS all-device inventory found a `0d28:0204` micro:bit by its `BBC micro:bit CMSIS-DAP` product string. Two fresh sessions returned protocol `2.1.0`, firmware `0257`, packet size 64, packet count 5, and capabilities `0x11`. No target command was sent. | -| SWD target access | HIL | Two fresh sessions against the same micro:bit used `ConfigureSWD` and `WithSWD` at 100 kHz. Both returned DPIDR `0x0bb11477`, AP0 IDR `0x04770021`, and CPUID `0x410cc200`; `DHCSR.S_HALT` was unchanged. Each restored the saved AP0 CSW and TAR before releasing the debug port and disconnecting. OpenOCD 0.12.0 independently selected the same serial and v2 bulk interface, returned the same DPIDR and AP0 IDR, and identified the target as Cortex-M0. This is read-only evidence from one probe and target; CMSIS-DAP does not report the attained clock. | +| SWD target access | HIL | Two fresh sessions against the same micro:bit used `ConfigureSWD` and `WithSWD` at 100 kHz. Both returned DPIDR `0x0bb11477`, AP0 IDR `0x04770021`, and CPUID `0x410cc200`; `DHCSR.S_HALT` was unchanged. Each restored the saved AP0 CSW and TAR before releasing the debug port and disconnecting. OpenOCD 0.12.0 independently selected the same serial and v2 bulk interface, returned the same DPIDR and AP0 IDR, and identified the target as Cortex-M0. That 100 kHz run used an active debug interface. A later 1 MHz run connected first after physical replug and repeated both sessions; see [nRF51 startup](protocols/cmsisdap.md#nrf51-startup-clock). CMSIS-DAP does not report the attained clock. | | JTAG or SWO | No | The current session does not connect JTAG or use the optional SWO endpoint. | The [CMSIS-DAP v2 session guide](protocols/cmsisdap.md) gives the descriptor, @@ -302,7 +302,7 @@ layouts and power-domain skips have hardware-independent test coverage. | --- | --- | --- | | CPUID read and decode | Yes | Accepts any aligned-word reader and validates a plausible Arm Cortex-M identity. | | Physical identity read | HIL | Opt-in FTDI/SWD/DAP/MEM-AP integration test. | -| Cortex-M0 acquisition and halt/resume | HIL | Two fresh CMSIS-DAP micro:bit sessions at a requested 100 kHz stopped a CPU counter during halt and observed progress after resume and release. Both preserved initially enabled debug and running state before Arm debug owner close. Initially disabled debug and cleanup failures are covered only by behavioral tests; see the [control evidence](cortexm.md#hardware-evidence). | +| Cortex-M0 acquisition and halt/resume | HIL | Two CMSIS-DAP micro:bit sessions at a requested 1 MHz stopped a CPU counter during halt and observed progress after resume and release. Both restored initially disabled debug and running state before Arm debug owner close. Earlier sessions preserved initially enabled debug. Cleanup failures remain covered only by behavioral tests; see the [control evidence](cortexm.md#hardware-evidence). | | Step | No | No single-step API exists. | | Register access | No | CPUID decoding is not a general core-register interface. | | Reset | No | No architectural or pin-reset operation exists. | diff --git a/docs/composition.md b/docs/composition.md index c2c34ff..adb8c9b 100644 --- a/docs/composition.md +++ b/docs/composition.md @@ -50,7 +50,7 @@ Arm SW-DP. Pass an explicit port configuration: ```go connected, err := armdebug.Connect(ctx, opened, armdebug.Config{ - Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000}), + Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 1_000_000}), }) if connected != nil { defer func() { err = errors.Join(err, connected.Close()) }() @@ -101,7 +101,10 @@ The generic `examples/simple/arm-info` program uses this ownership path: go run ./examples/simple/arm-info -provider cmsisdap -serial SERIAL -ap 0 ``` -It requests a 100 kHz SW-DP, reads DPIDR, AP IDR, and Cortex-M identity, and +It defaults to a 1 MHz SW-DP; `-clock` selects the requested ceiling in Hz. +The default meets the micro:bit nRF51's +[startup clock requirement](protocols/cmsisdap.md#nrf51-startup-clock). +It reads DPIDR, AP IDR, and Cortex-M identity, and attempts owner cleanup up to three times. It does not halt, reset, or write target memory. Probe filters may be omitted only when selection remains unique; the AP argument is required. @@ -124,7 +127,7 @@ For an owned Arm debug connection, `armdebug.Open` combines discovery and ```go connected, err := armdebug.Open(ctx, selection, armdebug.Config{ - Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000}), + Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 1_000_000}), }) ``` @@ -234,7 +237,7 @@ An application with a `probe.SWDBackend` can transfer it to a generic owner: ```go opened := probe.New(info, backend) defer func() { err = errors.Join(err, opened.Close()) }() -wire, err := opened.SWD(ctx, probe.SWDConfig{MaxClockHz: 100_000}) +wire, err := opened.SWD(ctx, probe.SWDConfig{MaxClockHz: 1_000_000}) if err != nil { return err } @@ -323,7 +326,7 @@ return a cleanup function even when `connection.Connect` fails: ```go func connectCMSISDAPSWD(ctx context.Context, device *usb.Device) (_ uint32, cleanup func() error, err error) { - session, err := cmsisdap.Open(ctx, device, cmsisdap.WithSWD(100_000)) + session, err := cmsisdap.Open(ctx, device, cmsisdap.WithSWD(1_000_000)) if err != nil { if session != nil { return 0, session.Close, err @@ -415,7 +418,7 @@ after a complete scan error before retrying SWD cleanup. ```go func connectJLinkSWD(ctx context.Context, device *usb.Device) (_ uint32, cleanup func() error, err error) { - session, err := jlink.Open(ctx, device, jlink.WithSWD(100_000)) + session, err := jlink.Open(ctx, device, jlink.WithSWD(1_000_000)) if err != nil { closeErr := device.Close() if closeErr != nil { @@ -431,7 +434,7 @@ func connectJLinkSWD(ctx context.Context, device *usb.Device) (_ uint32, cleanup defer cancel() if connectionOwned { if session.ClockHz() == 0 { - if err := session.ConfigureSWD(cleanupCtx, 100_000); err != nil { + if err := session.ConfigureSWD(cleanupCtx, 1_000_000); err != nil { if !errors.Is(err, jlink.ErrSessionPoisoned) { return err } diff --git a/docs/coresight.md b/docs/coresight.md index 392ab72..84edb55 100644 --- a/docs/coresight.md +++ b/docs/coresight.md @@ -186,11 +186,11 @@ go run ./examples/simple/coresight-info \ -provider cmsisdap -serial SERIAL -ap 0 ``` -To inspect another known page, supply `-base ADDRESS`; this bypasses the -BASE read. The override must name an accessible, 4 KiB aligned -identification page. The example requests a 100 kHz clock and applies a -ten-second operation deadline. The library also accepts memory clients -reached through JTAG; the example configures SWD only. +To inspect another known page, supply `-base ADDRESS`; this bypasses the BASE +read. The override must name an accessible, 4 KiB aligned identification page. +The example defaults to a 1 MHz clock, accepts `-clock` in Hz, and applies a +ten-second operation deadline. The library also accepts memory clients reached +through JTAG; the example configures SWD only. Add `-walk` to follow the advertised root with depth 8, at most 256 visits, and at most 4096 entry reads across the hierarchy: @@ -253,9 +253,9 @@ OSTIOLE_ROM_HIL=1 \ ``` The test uses the same exact probe selections and externally enabled ZCU104 -chain described above. Each path opens two fresh sessions at 100 kHz, reads -its MEM-AP's advertised root, and walks with depth 8, 256 visits, 4096 entry -reads, and a 120-second operation deadline. +chain described above. In that run, each path opened two fresh sessions at +100 kHz, read its MEM-AP's advertised root, and walked with depth 8, 256 +visits, 4096 entry reads, and a 120-second operation deadline. The micro:bit SWD AP0 walk completed with six identities. Its root at `0xf0000000` led to the nested table at `0xe00ff000`, components at @@ -332,3 +332,15 @@ writes, halt, reset, component unlock, or component power requests. The walks cover advertised entries, not every component in the RP2350 debug address space. Addresses above 32 bits, memory writes, and injected failures have simulation coverage but were not exercised on this bench. + +On September 26, the micro:bit identity and ROM-walk tests repeated both +sessions at a requested 1 MHz and reproduced the identities and six visits +above. Both tests and the SWD example now use 1 MHz by default to meet the +nRF51's [startup clock requirement](protocols/cmsisdap.md#nrf51-startup-clock). +The ZCU104 test clock remains 100 kHz. Run only the micro:bit paths with: + +```sh +OSTIOLE_CORESIGHT_HIL=1 OSTIOLE_ROM_HIL=1 \ +go test -tags integration ./coresight \ + -run 'TestHIL(ComponentIdentity|ROMWalk)/microbit' -count=1 -v +``` diff --git a/docs/cortexm.md b/docs/cortexm.md index 6c8fc62..c26845d 100644 --- a/docs/cortexm.md +++ b/docs/cortexm.md @@ -95,7 +95,7 @@ connection. It requires explicit consent to control execution: ```sh go run ./examples/simple/cortexm-control \ - -provider cmsisdap -serial SERIAL -ap 0 -allow-control + -provider cmsisdap -serial SERIAL -ap 0 -clock 1000000 -allow-control ``` Hardware-independent tests model DHCSR control and execution state, including @@ -106,7 +106,9 @@ retry. They do not establish physical halt/resume behavior on a bench program. The opt-in integration test selects the CMSIS-DAP micro:bit with serial `9900360140124e4500279015000000360000000097969901`, AP0, and a requested -100 kHz clock. It requires a known firmware program with an aligned 32-bit RAM +1 MHz clock. The nRF51 needs at least 125 kHz during debug activation after +power-on; see the [startup evidence](protocols/cmsisdap.md#nrf51-startup-clock). +It requires a known firmware program with an aligned 32-bit RAM counter incremented by the CPU at least once per 200 milliseconds. The counter must not be updated by DMA or another processor. Loading firmware is outside the test. @@ -132,22 +134,25 @@ or restoration after a physical transport failure. ## Hardware evidence -On September 26, 2026, Nostalgia (macOS) completed the control test in two -fresh sessions on the selected micro:bit, with Cortex-M0 CPUID `0x410cc200`. -OpenOCD 0.12.0 programmed and verified the counter image using the procedure -above. The Intel HEX image's SHA-256 was +On September 26, 2026, Nostalgia (macOS) completed two control sessions at +1 MHz on the selected micro:bit, with Cortex-M0 CPUID `0x410cc200`. The +counter image had been programmed and verified with OpenOCD 0.12.0. Its +Intel HEX SHA-256 was `ee294cc06ab6e8228161b49506675b065c0148b26421cf1f83c8e45e35cd4e5d`. - -In both sessions, the CPU counter advanced before acquisition, remained -unchanged across ten samples 20 milliseconds apart while halted, and advanced -after resume and after release from a second halt. The halted values were -`0x014c4757` and `0x017ebe6c`. Both target releases and Arm debug owner closes -completed. DHCSR showed debug enabled and the processor running before -acquisition and after release in each session; the first read also consumed -the sticky reset indicator. - -This run covers inherited enabled debug on one micro:bit. It does not verify -enabling and restoring initially disabled debug, restoration after closing -the Arm debug owner, or cleanup after a physical transport failure. Earlier -attempts with Ostiole and OpenOCD could not read DPIDR; the cause of that -connection failure and its recovery remain unknown. +After a physical replug, Ostiole's read-only test connected first at 1 MHz, +then the control test ran without any intervening OpenOCD session. + +In both control sessions, the CPU counter advanced before acquisition, +remained unchanged across ten samples 20 milliseconds apart while halted, +and advanced after resume and after release from a second halt. The halted +values were `0x0d8abd3d` and `0x0db618ae`. DHCSR was `0x01000000` before +acquisition and after release in each session: debug was initially disabled, +acquisition enabled it, and release restored disabled debug with the processor +running. Both target releases and Arm debug owner closes completed. + +An earlier pair of sessions at 100 kHz, after OpenOCD had activated the +interface, preserved initially enabled debug. Those sessions do not establish +startup at 100 kHz. The later 1 MHz run covers initially disabled debug on the +same board. Neither run verifies state after closing the Arm debug owner or +cleanup after a physical transport failure. Peripheral behavior, register +preservation, reset, and stepping are outside this test. diff --git a/docs/protocols/cmsisdap.md b/docs/protocols/cmsisdap.md index 02ec257..a5b151e 100644 --- a/docs/protocols/cmsisdap.md +++ b/docs/protocols/cmsisdap.md @@ -134,3 +134,38 @@ the v2 opener rejected it before claiming an interface. No CMSIS-DAP command or target traffic was sent. The Linux bench still exercises only passive v1 rejection. + +## nRF51 startup clock + +The nRF51 on the micro:bit requires at least 125 kHz when entering debug +interface mode after power-on. Nordic also specifies at least 150 SWCLK cycles +with SWDIO high to guarantee that the DAP captures 50 cycles while its power +domain starts; see section 11.1.2 of the +[nRF51 reference manual](https://docs-be.nordicsemi.com/bundle/nRF51-Series/raw/resource/enus/nRF51_RM_v3.0.1.pdf). +A slower clock can work once another debugger has activated the interface. +The CMSIS-DAP driver accepts the caller's clock ceiling; it does not know the +target's startup requirements or silently raise that ceiling. + +On Nostalgia, Ostiole connected first after a physical micro:bit replug at 1 MHz +and passed both read-only sessions. At 100 kHz, both Ostiole and OpenOCD 0.12.0 +failed to read DPIDR after replug, though Ostiole passed after OpenOCD activated +the interface at 1 MHz. Only the test's requested clock changed; the CMSIS-DAP +driver was unchanged. DPIDR was `0x0bb11477`, AP0 IDR was `0x04770021`, and +CPUID was `0x410cc200`. Both sessions restored AP0 CSW and TAR and closed their +owners. + +The read-only HIL now requests 1 MHz: + +```sh +OSTIOLE_CMSISDAP_HIL=1 \ +OSTIOLE_CMSISDAP_HIL_SERIAL=9900360140124e4500279015000000360000000097969901 \ +go test -tags integration ./cmsisdap \ + -run '^TestHILCMSISDAPSWDReadOnlyStateRestoration$' -count=1 -v +``` + +For a startup check, physically unplug/replug before running the command and +leave other debuggers stopped. Reopening a session alone does not reproduce +power-on state. The earlier 100 kHz evidence above covers an active interface; +it does not establish startup from power-on. The new result covers one board +and firmware revision, with no measurement of the attained clock or guarantee +of target-specific activation timing on other devices. diff --git a/examples/README.md b/examples/README.md index 50b9d9f..5b6969b 100644 --- a/examples/README.md +++ b/examples/README.md @@ -37,3 +37,8 @@ For ADIv6 SW-DP targets, `coresight-info -debug-space -walk` inspects the DP's advertised discovery tree. Use `-ap-base ADDRESS` instead of `-ap INDEX` to inspect memory through one ADIv6 MEM-AP. The same probe selection and cleanup rules apply. See the [RP2350 procedure](../docs/coresight.md#adiv6-and-the-rp2350). + +The `arm-info`, `coresight-info`, and `cortexm-control` examples accept +`-clock` in Hz and default to 1 MHz. This also suits the micro:bit +nRF51: its debug interface needs at least 125 kHz during startup. See the +[startup evidence](../docs/protocols/cmsisdap.md#nrf51-startup-clock). diff --git a/examples/simple/ap-id/main.go b/examples/simple/ap-id/main.go index e4b83b2..0169099 100644 --- a/examples/simple/ap-id/main.go +++ b/examples/simple/ap-id/main.go @@ -57,7 +57,7 @@ func readIdentity(ctx context.Context) (_ identity, err error) { if err != nil { return identity{}, err } - ch, err := ftdi.Open(ctx, dev, ftdi.Config{Port: ftdi.PortA, MaxClockHz: 400_000}) + ch, err := ftdi.Open(ctx, dev, ftdi.Config{Port: ftdi.PortA, MaxClockHz: 1_000_000}) if err != nil { closeOwner := dev.Close if ch != nil { diff --git a/examples/simple/arm-info/main.go b/examples/simple/arm-info/main.go index d03175b..9567214 100644 --- a/examples/simple/arm-info/main.go +++ b/examples/simple/arm-info/main.go @@ -28,7 +28,11 @@ func run() (err error) { serial := flag.String("serial", "", "exact probe serial") function := flag.String("function", "", "exact probe function") ap := flag.Int("ap", -1, "required MEM-AP index (0..255)") + clock := flag.Uint64("clock", 1_000_000, "maximum SWD clock in Hz") flag.Parse() + if *clock < 1000 || *clock > 1<<32-1 { + return errors.New("require -clock 1000..4294967295 Hz") + } if *ap < 0 || *ap > 255 || flag.NArg() != 0 { return errors.New("require -ap 0..255 and no positional arguments") } @@ -36,7 +40,7 @@ func run() (err error) { defer cancel() c, err := armdebug.Open(ctx, discover.Selection{ Provider: discover.ProviderID(*provider), Serial: *serial, Function: *function, - }, armdebug.Config{Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000})}) + }, armdebug.Config{Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: uint32(*clock)})}) if c != nil { defer func() { err = errors.Join(err, closeConnection(c)) }() } diff --git a/examples/simple/coresight-info/main.go b/examples/simple/coresight-info/main.go index dc16311..e9d1a89 100644 --- a/examples/simple/coresight-info/main.go +++ b/examples/simple/coresight-info/main.go @@ -33,7 +33,11 @@ func run() (err error) { ap := flag.Int("ap", -1, "ADIv5 MEM-AP index (0..255)") address := flag.String("base", "", "override the MEM-AP debug base with a known identification page") walk := flag.Bool("walk", false, "walk ROM tables with depth 8, 256 visits, and 4096 entry reads") + clock := flag.Uint64("clock", 1_000_000, "maximum SWD clock in Hz") flag.Parse() + if *clock < 1000 || *clock > 1<<32-1 { + return errors.New("require -clock 1000..4294967295 Hz") + } base, err := parseBase(*address) if err != nil { return err @@ -49,7 +53,7 @@ func run() (err error) { defer cancel() c, err := armdebug.Open(ctx, discover.Selection{ Provider: discover.ProviderID(*provider), Serial: *serial, Function: *function, - }, armdebug.Config{Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000})}) + }, armdebug.Config{Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: uint32(*clock)})}) if c != nil { defer func() { err = errors.Join(err, closeConnection(c)) }() } diff --git a/examples/simple/cortexm-control/main.go b/examples/simple/cortexm-control/main.go index 60ea21e..00785c5 100644 --- a/examples/simple/cortexm-control/main.go +++ b/examples/simple/cortexm-control/main.go @@ -23,20 +23,29 @@ func main() { } } -func run() (err error) { +func run() error { provider := flag.String("provider", "", "required probe provider") serial := flag.String("serial", "", "required probe serial") ap := flag.Int("ap", -1, "required MEM-AP index (0..255)") allow := flag.Bool("allow-control", false, "allow enabling debug, halting, and resuming the processor") + clock := flag.Uint64("clock", 1_000_000, "maximum SWD clock in Hz") flag.Parse() + if *clock < 1000 || *clock > 1<<32-1 { + return errors.New("require -clock 1000..4294967295 Hz") + } if !*allow || *provider == "" || *serial == "" || *ap < 0 || *ap > 255 || flag.NArg() != 0 { return errors.New("require -allow-control, -provider, -serial, and -ap 0..255") } + selection := discover.Selection{Provider: discover.ProviderID(*provider), Serial: *serial} + return runControl(selection, dap.NewAPSel(uint8(*ap)), uint32(*clock)) +} + +func runControl(selection discover.Selection, ap dap.APSel, clock uint32) (err error) { ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) defer cancel() - c, err := armdebug.Open(ctx, discover.Selection{ - Provider: discover.ProviderID(*provider), Serial: *serial, - }, armdebug.Config{Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000})}) + c, err := armdebug.Open(ctx, selection, armdebug.Config{ + Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: clock}), + }) var core *cortexm.Target if c != nil { defer func() { err = errors.Join(err, release(core, c)) }() @@ -44,7 +53,7 @@ func run() (err error) { if err != nil { return err } - memory, err := c.OpenMemAP(ctx, dap.NewAPSel(uint8(*ap))) + memory, err := c.OpenMemAP(ctx, ap) if err != nil { return err } diff --git a/examples/simple/cortexm-info/main.go b/examples/simple/cortexm-info/main.go index b35ab61..cafc2de 100644 --- a/examples/simple/cortexm-info/main.go +++ b/examples/simple/cortexm-info/main.go @@ -94,7 +94,7 @@ func openChannel(ctx context.Context) (*ftdi.Channel, error) { if err != nil { return nil, err } - ch, err := ftdi.Open(ctx, dev, ftdi.Config{Port: ftdi.PortA, MaxClockHz: 400_000}) + ch, err := ftdi.Open(ctx, dev, ftdi.Config{Port: ftdi.PortA, MaxClockHz: 1_000_000}) if err != nil { closeOwner := dev.Close if ch != nil { diff --git a/examples/trivial/swd-dpidr/main.go b/examples/trivial/swd-dpidr/main.go index 3a04872..79111bc 100644 --- a/examples/trivial/swd-dpidr/main.go +++ b/examples/trivial/swd-dpidr/main.go @@ -48,7 +48,7 @@ func readDPIDR(ctx context.Context) (value uint32, err error) { if err != nil { return 0, err } - ch, err := ftdi.Open(ctx, dev, ftdi.Config{Port: ftdi.PortA, MaxClockHz: 400_000}) + ch, err := ftdi.Open(ctx, dev, ftdi.Config{Port: ftdi.PortA, MaxClockHz: 1_000_000}) if err != nil { closeOwner := dev.Close if ch != nil { diff --git a/target/cortexm/control_integration_test.go b/target/cortexm/control_integration_test.go index fda0eca..8976935 100644 --- a/target/cortexm/control_integration_test.go +++ b/target/cortexm/control_integration_test.go @@ -80,7 +80,7 @@ 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 100 kHz AP0 CPUID=%#x program=%q counter=%#x DHCSR before=%#x after=%#x", + 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) } @@ -127,7 +127,7 @@ func openControlBench(t *testing.T, ctx context.Context) *armdebug.Conn { t.Fatal(err) } c, err := armdebug.Open(ctx, discover.Selection{Binding: candidate.Info().Binding}, armdebug.Config{ - Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 100_000}), + Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 1_000_000}), }) if err != nil { if c != nil { diff --git a/target/cortexm/testdata/counter/README.md b/target/cortexm/testdata/counter/README.md index f7c97d8..54f93e2 100644 --- a/target/cortexm/testdata/counter/README.md +++ b/target/cortexm/testdata/counter/README.md @@ -23,13 +23,16 @@ using an external programmer. Programming is bench preparation, separate from the Ostiole [control test](../../../../docs/cortexm.md#hardware-procedure). For the selected micro:bit, use OpenOCD's CMSIS-DAP v2 transport and nRF51 -flash driver: +flash driver at 1 MHz. The nRF51 requires at least 125 kHz when entering +debug interface mode after power-on; 100 kHz can work after another debugger +has already activated it. See the +[startup evidence](../../../../docs/protocols/cmsisdap.md#nrf51-startup-clock). ```sh openocd -f interface/cmsis-dap.cfg \ -c 'cmsis_dap_backend usb_bulk' \ -c 'adapter serial 9900360140124e4500279015000000360000000097969901' \ - -f target/nrf51.cfg -c 'adapter speed 100' \ + -f target/nrf51.cfg -c 'adapter speed 1000' \ -c 'gdb_port disabled; tcl_port disabled; telnet_port disabled' \ -c 'program /tmp/ostiole-counter.hex verify reset exit' ```