Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ add posted access-port reads and a Cortex-M identity read through a MEM-AP.
They compose the public packages explicitly without duplicating their framing.
The `target/cortexm` package reads and decodes the architectural CPUID value
through any compatible target-word reader. It also provides acquired Cortex-M0
halt/resume control and halted register access over word memory; see
halt/resume control, stepping, and halted register access over word memory; see
[Cortex-M control](docs/cortexm.md).

The FTDI path uses the standard H-series MPSSE port and endpoint layout.
Expand Down Expand Up @@ -140,7 +140,8 @@ The `arm-info`, `coresight-info`, and `cortexm-control` examples accept
The inspection examples and `ost` commands avoid reset, halt, target-memory
writes, and persistent changes. The separately gated `cortexm-control` example
enables halting debug, halts a Cortex-M0, reads PC, SP, R0, and R4, then resumes
it. The `dap.MemAP` API
it. Add `-step` to perform one architectural step before resume. The
`dap.MemAP` API
does expose effectful scalar writes; callers choose the addresses and own the consequences.
Establishing an ADIv5 connection also changes volatile debug-port control
state; the connection releases its own power requests before return.
Expand Down
7 changes: 4 additions & 3 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,8 +337,9 @@ component identity](coresight.md) for its register and failure boundaries.

`target/cortexm` identifies processors through a word reader. Cortex-M0
control also requires a word writer that waits for each access to complete.
The target owns DHCSR control, its halt requests, and pending register
transfers. Release settles a pending transfer before restoring debug control;
The target owns DHCSR control, its halt requests, and pending register and
step operations. Stepping checks DFSR to preserve competing stops. Release
settles pending operations before restoring debug control;
the target must be released before the memory owner. Register writes persist
after release.
It does not know about USB, adapters, or wire protocols. See
Expand Down Expand Up @@ -386,7 +387,7 @@ replaceable while exercising the public protocol and DAP layers.
The inspection examples and `ost` commands do not reset or halt the target,
write target memory, or change persistent state. The explicitly gated
`cortexm-control` example enables debug, halts Cortex-M0, reads PC, SP, R0,
and R4, then resumes it.
and R4, then resumes it. Its `-step` option steps once while halted.
The `dap.MemAP` API does expose scalar and block target-memory writes;
applications choose the affected addresses and own the consequences.

Expand Down
3 changes: 2 additions & 1 deletion docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ layouts and power-domain skips have hardware-independent test coverage.
| CPUID read and decode | Yes | Accepts any aligned-word reader and validates a plausible Arm Cortex-M identity. |
| Physical identity read | HIL | Opt-in FTDI/SWD/DAP/MEM-AP integration test. |
| Cortex-M0 acquisition and halt/resume | HIL | Two CMSIS-DAP micro:bit sessions at a requested 1 MHz stopped a CPU counter during halt and observed progress after resume and release. Both restored initially disabled debug and running state before Arm debug owner close. Earlier sessions preserved initially enabled debug. Cleanup failures remain covered only by behavioral tests; see the [control evidence](cortexm.md#hardware-evidence). |
| Step | No | No single-step API exists. |
| Cortex-M0 step | HIL | `Target.Step` requires an owned halt and returns halted. Two fresh micro:bit sessions checked PC/R0/RAM across 13 steps each, resume, and release with disabled debug restored. Competing events and failure cleanup have behavioral coverage; see the [step bench](cortexm.md#step-bench). |
| Register reads | Yes | Halted Cortex-M0 R0–R12, SP, LR, PC, XPSR, MSP, and PSP through `ReadRegister`. Two fresh CMSIS-DAP micro:bit sessions read all 19 registers; transfer failures and cleanup have behavioral coverage. |
| Register writes | Yes | Halted Cortex-M0 writes except XPSR; aligned SP/MSP/PSP and even PC values. Writes persist after release. Behavioral tests cover staging, uncertain selection, and pending cleanup. Two micro:bit sessions wrote and restored R4, SP, MSP, PSP, and PC before resuming; see the [register bench](cortexm.md#register-bench). |
| Reset | No | No architectural or pin-reset operation exists. |
Expand All @@ -329,6 +329,7 @@ Available examples:

`examples/simple/cortexm-control` separately demonstrates effectful Cortex-M0
halt/resume with PC, SP, R0, and R4 reads, and requires `-allow-control`.
Its optional `-step` performs one architectural step before resume.

Available `ost` commands:

Expand Down
1 change: 1 addition & 0 deletions docs/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ data-register write can write target memory.
| Inspect ROM entries or a bounded component hierarchy | `Component.ROMTable`, `ROMTable.ReadEntry`, `coresight.Walk` | `examples/simple/coresight-info -walk` |
| Identify a Cortex-M through any compatible word reader | `cortexm.Identify` | `examples/simple/cortexm-info` |
| Acquire, halt, inspect registers, and resume a Cortex-M0 | `cortexm.Acquire`, `Target.Halt`, `Target.ReadRegister`, `Target.Resume`, `Target.Release` | `examples/simple/cortexm-control` |
| Step a Cortex-M0 from an owned halt | `Target.Step` | `examples/simple/cortexm-control -step` |
| Read or write a halted Cortex-M0 register | `Target.ReadRegister`, `Target.WriteRegister` | [Register reads](cortexm.md#register-reads), [writes](cortexm.md#register-writes) |
| Test SWD and DAP behavior without hardware | `swd/sim`, `dap/sim` | Package tests |

Expand Down
85 changes: 80 additions & 5 deletions docs/cortexm.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,46 @@ The package does not restore those indicators or clear DFSR event flags.

The implementation follows Arm DDI 0419E, sections C1.5 and C1.6.3–C1.6.5 of the
[Armv6-M Architecture Reference Manual](https://documentation-service.arm.com/static/5f8ff05ef86e16515cdbf826).
It does not implement reset, single-step, breakpoints, or
watchpoints.
It does not implement reset, breakpoints, or watchpoints.

## Stepping

`Step(ctx)` performs one architectural step from a halt owned by the target.
It returns halted with stepping disabled, retaining ownership for another
step, register access, or resume. It rejects a running processor or an inherited
halt, settles any pending register transfer before launch, and uses the
caller's context for cancellation and deadlines.

```go
if err := core.Step(ctx); err != nil {
// Retain core and its memory owner for Release.
return err
}
pc, err := core.ReadRegister(ctx, cortexm.PC)
```

Stepping does not change interrupt masking. An architectural step can enter
an exception handler instead of retiring an instruction. A breakpoint,
watchpoint, vector catch, or external halt can also interrupt it. The target
checks DFSR before launch and rejects any existing flags for those events;
it preserves all DFSR flags. After launch, it requires a fresh halt with the
HALTED reason and no competing event before claiming that stop. A competing
stop returns an error and remains unowned.

Once launch is attempted, any failure leaves only `Release` available. Release
never repeats the step. After a confirmed launch, it waits for a fresh halt
before clearing C_STEP; it does not change stepping control while running.
A failed write to clear C_STEP can be retried without restarting execution.
An unconfirmed launch, ignored step request, reset, changed debug control, or
loss of the completed halt can prevent automatic cleanup. A competing stop can
prevent restoring initially disabled debug until the processor runs again.
Retain both owners when release fails; this package provides no forced cleanup
operation.

Instructions, exception entry, elapsed time, and peripheral effects cannot be
undone. Behavioral tests cover immediate and delayed completion, competing
flags, cancellation, ignored writes, partial failures, and cleanup retries.
The [step bench](#step-bench) records physical instruction checks.

## Register reads

Expand Down Expand Up @@ -142,12 +180,12 @@ err = errors.Join(err, cleanupErr)

The [control example](../examples/simple/cortexm-control/main.go) selects one
probe and AP, halts, prints PC, SP, R0, and R4, resumes, then releases the
target before closing the
connection. It requires explicit consent to control execution:
target before closing the connection. With `-step`, it also steps once and
prints the resulting PC. It requires explicit consent to control execution:

```sh
go run ./examples/simple/cortexm-control \
-provider cmsisdap -serial SERIAL -ap 0 -clock 1000000 -allow-control
-provider cmsisdap -serial SERIAL -ap 0 -clock 1000000 -allow-control -step
```

Hardware-independent tests model DHCSR control and execution state, including
Expand Down Expand Up @@ -241,3 +279,40 @@ the temporary PC or stack values. Writes to the other general registers and LR,
process-stack selection, inherited halts, and failure cleanup have behavioral
test coverage only. XPSR writes, stepping, reset, and state after Arm owner
close were not tested.

### Step bench

`TestHILCortexM0Step` uses the same micro:bit and verified counter firmware,
with a separate gate for stepping:

```sh
OSTIOLE_CORTEXM_HIL_CONTROL=1 \
OSTIOLE_CORTEXM_HIL_STEP=1 \
OSTIOLE_CORTEXM_HIL_PROGRAM=sha256:ee294cc06ab6e8228161b49506675b065c0148b26421cf1f83c8e45e35cd4e5d \
go test -tags integration ./target/cortexm -run '^TestHILCortexM0Step$' -count=1 -v
```

On September 26, 2026, two fresh sessions passed on Nostalgia through
CMSIS-DAP, 1 MHz SWD, and AP0, with CPUID `0x410cc200`. Each checked twelve
consecutive steps through the counter loop:

- At PC `0xc6`, `adds r0, #1` advanced PC to `0xc8` and incremented R0,
leaving RAM unchanged.
- At PC `0xc8`, `str r0, [r1]` advanced PC to `0xca` and copied R0 to the
counter at `0x20000000`, leaving R0 unchanged.
- At PC `0xca`, the branch returned PC to `0xc6`, leaving R0 and RAM unchanged.

After every step, `Halted` confirmed Debug state with stepping and interrupt
masking disabled. The counter then remained unchanged across ten samples
20 milliseconds apart while halted, and advanced after resume. Each session
halted again, checked one further step, and released from that halt. The
counter advanced after release. DHCSR was `0x01000000` before acquisition and
after release in both sessions; initially disabled debug and running state
were restored. Both target releases and Arm owner closes completed. The
control example also completed a step with `-allow-control -step`.

Stepping's register and memory effects were intentional and were not rolled
back. The firmware disables configurable interrupts, so these runs do not
establish exception entry, competing debug events, sleeping instructions, or
failure cleanup on hardware. Those control failures have behavioral coverage;
state after Arm owner close was not measured.
21 changes: 16 additions & 5 deletions examples/simple/cortexm-control/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ func run() error {
provider := flag.String("provider", "", "required probe provider")
serial := flag.String("serial", "", "required probe serial")
ap := flag.Int("ap", -1, "required MEM-AP index (0..255)")
allow := flag.Bool("allow-control", false, "allow enabling debug, halting, and resuming the processor")
allow := flag.Bool("allow-control", false, "allow enabling debug, halting, stepping, and resuming the processor")
step := flag.Bool("step", false, "perform one architectural step while halted")
clock := flag.Uint64("clock", 1_000_000, "maximum SWD clock in Hz")
flag.Parse()
if *clock < 1000 || *clock > 1<<32-1 {
Expand All @@ -37,10 +38,10 @@ func run() error {
return errors.New("require -allow-control, -provider, -serial, and -ap 0..255")
}
selection := discover.Selection{Provider: discover.ProviderID(*provider), Serial: *serial}
return runControl(selection, dap.NewAPSel(uint8(*ap)), uint32(*clock))
return runControl(selection, dap.NewAPSel(uint8(*ap)), uint32(*clock), *step)
}

func runControl(selection discover.Selection, ap dap.APSel, clock uint32) (err error) {
func runControl(selection discover.Selection, ap dap.APSel, clock uint32, step bool) (err error) {
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
c, err := armdebug.Open(ctx, selection, armdebug.Config{
Expand All @@ -61,10 +62,10 @@ func runControl(selection discover.Selection, ap dap.APSel, clock uint32) (err e
if err != nil {
return err
}
return control(ctx, core)
return control(ctx, core, step)
}

func control(ctx context.Context, core *cortexm.Target) error {
func control(ctx context.Context, core *cortexm.Target, step bool) error {
if err := core.Halt(ctx); err != nil {
return err
}
Expand All @@ -79,6 +80,16 @@ func control(ctx context.Context, core *cortexm.Target) error {
}
fmt.Printf("%s=%#08x\n", reg.name, value)
}
if step {
if err := core.Step(ctx); err != nil {
return err
}
pc, err := core.ReadRegister(ctx, cortexm.PC)
if err != nil {
return err
}
fmt.Printf("stepped; PC=%#08x\n", pc)
}
if err := core.Resume(ctx); err != nil {
return err
}
Expand Down
10 changes: 9 additions & 1 deletion target/cortexm/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type Target struct {
resumeUncertain bool
registerPending bool
registerLost bool
step stepPhase
}

// Acquire enables Cortex-M0 halting debug without requesting a halt. It reads
Expand Down Expand Up @@ -119,7 +120,9 @@ func (t *Target) Identity() Identity {
// a completed resume. An unconfirmed control change, or a new halt while
// restoring disabled debug, can prevent cleanup until execution resumes.
// Pending register transfers must settle first. Reset or loss of Debug state
// during a transfer prevents automatic cleanup.
// during a transfer prevents automatic cleanup. An accepted step must return
// halted before stepping can be disabled; an unconfirmed step launch prevents
// automatic cleanup. A competing debug event leaves its halt unowned.
func (t *Target) Release(ctx context.Context) error {
if t == nil || t.memory == nil {
return nil
Expand All @@ -133,6 +136,11 @@ func (t *Target) Release(ctx context.Context) error {
return err
}
}
if t.step != stepIdle {
if err := t.settleStep(ctx); err != nil {
return err
}
}
if t.changed {
if err := t.restore(ctx); err != nil {
return fmt.Errorf("cortexm: restore debug control: %w", err)
Expand Down
2 changes: 1 addition & 1 deletion target/cortexm/identity.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Package cortexm identifies Cortex-M processors and provides Cortex-M0
// halting debug and register access through target memory.
// halting debug, stepping, and register access through target memory.
package cortexm

import (
Expand Down
Loading
Loading