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
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ 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 over word memory; see [Cortex-M control](docs/cortexm.md).
halt/resume control 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.
Descriptor-driven FTDI port binding is not implemented yet. J-Link instead
Expand Down Expand Up @@ -138,7 +139,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 and halts and resumes a Cortex-M0. The `dap.MemAP` API
enables halting debug, halts a Cortex-M0, reads PC, SP, R0, and R4, then resumes
it. 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
11 changes: 7 additions & 4 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 own Cortex-M0 halting debug over borrowed word memory. |
| `target/cortexm` | Identify Cortex-M processors and own Cortex-M0 halting debug and register access over borrowed word memory. |
| `examples/...` | Demonstrate public package compositions as executable programs. |
| `cmd/ost` | Provide a small command hierarchy over the same public packages. |

Expand Down Expand Up @@ -336,8 +336,10 @@ 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 and its halt requests, and must be released
before the memory owner.
The target owns DHCSR control, its halt requests, and pending register
transfers. Release settles a pending transfer before restoring debug control;
the target must be released before the memory owner. Register writes persist
after release.
It does not know about USB, adapters, or wire protocols. See
[Cortex-M control](cortexm.md) for restoration and failure boundaries.

Expand Down Expand Up @@ -382,7 +384,8 @@ 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 and halts and resumes Cortex-M0.
`cortexm-control` example enables debug, halts Cortex-M0, reads PC, SP, R0,
and R4, then resumes it.
The `dap.MemAP` API does expose scalar and block target-memory writes;
applications choose the affected addresses and own the consequences.

Expand Down
5 changes: 3 additions & 2 deletions docs/capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ layouts and power-domain skips have hardware-independent test coverage.
| 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. |
| Register access | No | CPUID decoding is not a general core-register interface. |
| Register reads | Yes | Halted Cortex-M0 R0–R12, SP, LR, PC, XPSR, MSP, and PSP through `ReadRegister`. Two fresh CMSIS-DAP micro:bit sessions read all 19 registers; transfer failures and cleanup have behavioral coverage. |
| Register writes | Yes | Halted Cortex-M0 writes except XPSR; aligned SP/MSP/PSP and even PC values. Writes persist after release. Behavioral tests cover staging, uncertain selection, and pending cleanup. Two micro:bit sessions wrote and restored R4, SP, MSP, PSP, and PC before resuming; see the [register bench](cortexm.md#register-bench). |
| Reset | No | No architectural or pin-reset operation exists. |
| Breakpoints or watchpoints | No | No target instrumentation API exists. |
| Firmware or runtime loading | No | No ELF loader, image-placement policy, or flash driver exists. |
Expand All @@ -327,7 +328,7 @@ Available examples:
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`.
halt/resume with PC, SP, R0, and R4 reads, and requires `-allow-control`.

Available `ost` commands:

Expand Down
7 changes: 5 additions & 2 deletions docs/composition.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ 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` | `examples/simple/cortexm-control` |
| Acquire, halt, inspect registers, and resume a Cortex-M0 | `cortexm.Acquire`, `Target.Halt`, `Target.ReadRegister`, `Target.Resume`, `Target.Release` | `examples/simple/cortexm-control` |
| 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 |

The examples are intentionally small, executable compositions of public
Expand Down Expand Up @@ -745,7 +746,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 that target before
uses `WriteWord` to enable Cortex-M0 halting debug. Use `ReadRegister` for
halted core registers and `WriteRegister` for intentional changes. The target
tracks transfer completion but does not roll back writes. Release it before
its memory owner and retain both after failed target restoration. See
[Cortex-M control](cortexm.md) for the full composition and effects.

Expand Down
92 changes: 88 additions & 4 deletions docs/cortexm.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,60 @@ 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
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, general register access, breakpoints,
or watchpoints.
It does not implement reset, single-step, breakpoints, or
watchpoints.

## Register reads

`ReadRegister` reads R0–R12, SP, LR, PC, XPSR, MSP, or PSP from a halted
processor. SP selects the current stack pointer; MSP and PSP select its banks.
PC is the debug return address. An inherited halt permits inspection without
acquiring permission to resume. Invalid `Register` identifiers, including
zero, are rejected before memory traffic.

```go
pc, err := core.ReadRegister(ctx, cortexm.PC)
```

Reads write DCRSR and replace DCRDR; these transfer registers are not restored.
The target waits for S_REGRDY before and after selecting a register, with the
same five-second bound as control operations. It does not require observing
S_REGRDY clear, since a transfer may finish before the first status read.

A failed transfer leaves only `Release` available. Release waits for any
pending transfer, including one found busy before selection, before resuming
or disabling debug. It never replays a selector write whose completion is
uncertain. A failed precondition or cancellation before selection leaves the
target usable when no transfer is pending. An error returns no register value.

Reset or loss of Debug state during a pending transfer prevents automatic
cleanup, even if a later status read would show ready. The target cannot prove
that the original transfer completed. Retain both owners; there is no forced
cleanup operation for this state. These failures have behavioral test coverage,
not physical failure-injection evidence.

## Register writes

`WriteRegister` writes the same register set except XPSR, which is read-only.
SP, MSP, and PSP require word-aligned values; PC requires bit zero clear. PC
writes change the debug return address without changing Thumb state. Writing
SP changes whichever stack bank is active. The API rejects invalid identifiers
and values before traffic; it does not check whether an address is mapped or
suitable for the program.

```go
err := core.WriteRegister(ctx, cortexm.R4, 42)
```

A write stages DCRDR, selects the register, and waits for transfer completion.
An error after attempting to stage data leaves only `Release` available. If
selection was attempted, the register may have changed even when the call
returns an error. Release settles a pending transfer without replaying it.
Successful writes are intentional changes to processor state: release does
not roll them back, and resumed execution uses the changed values. An inherited
halt permits writes but still does not grant permission to resume.

## Composition

Expand All @@ -90,7 +140,8 @@ err = errors.Join(err, cleanupErr)
```

The [control example](../examples/simple/cortexm-control/main.go) selects one
probe and AP, halts, resumes, then releases the target before closing the
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:

```sh
Expand Down Expand Up @@ -156,3 +207,36 @@ 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.

### Register bench

`TestHILCortexM0Registers` uses the same micro:bit, AP0, and 1 MHz clock. It
requires the exact counter image above and checks its vectors and instruction
words before acquiring the processor. A second gate authorizes register writes:

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

On September 26, 2026, both fresh sessions passed on Nostalgia with CPUID
`0x410cc200`. Each read R0–R12, SP, LR, PC, XPSR, MSP, and PSP while halted.
R4 accepted `0x55aa55aa` and `0xaa55aa55`; SP, MSP, PSP, and PC accepted
temporary aligned values. SP and MSP aliased as expected for this firmware.
The test restored each written value and compared all 19 registers with the
saved snapshot before resuming.

The CPU counter remained unchanged across ten samples 20 milliseconds apart
after register restoration, then advanced after resume and release. DHCSR was
`0x01000000` before acquisition and after release in both sessions, with debug
disabled and the processor running. Both target releases and Arm owner closes
completed. If register restoration cannot be confirmed, the test retains both owners
without requesting resume.

These sessions exercised register transfers while halted, not execution using
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.
10 changes: 10 additions & 0 deletions examples/simple/cortexm-control/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,16 @@ func control(ctx context.Context, core *cortexm.Target) error {
return err
}
fmt.Printf("CPUID=%#08x halted\n", core.Identity().Raw)
for _, reg := range []struct {
name string
id cortexm.Register
}{{"PC", cortexm.PC}, {"SP", cortexm.SP}, {"R0", cortexm.R0}, {"R4", cortexm.R4}} {
value, err := core.ReadRegister(ctx, reg.id)
if err != nil {
return err
}
fmt.Printf("%s=%#08x\n", reg.name, value)
}
if err := core.Resume(ctx); err != nil {
return err
}
Expand Down
9 changes: 9 additions & 0 deletions target/cortexm/control.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type Target struct {
haltOwned bool
haltUncertain bool
resumeUncertain bool
registerPending bool
registerLost bool
}

// Acquire enables Cortex-M0 halting debug without requesting a halt. It reads
Expand Down Expand Up @@ -117,6 +119,8 @@ func (t *Target) Identity() Identity {
// memory and cannot repair a disconnected or invalidated memory client. It
// never repeats 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.
func (t *Target) Release(ctx context.Context) error {
if t == nil || t.memory == nil {
return nil
Expand All @@ -127,6 +131,11 @@ func (t *Target) Release(ctx context.Context) error {
}
ctx, cancel := context.WithTimeout(ctx, controlTimeout)
defer cancel()
if t.registerPending {
if err := t.waitRegister(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
4 changes: 2 additions & 2 deletions 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 controls Cortex-M0
// halting debug through target memory.
// Package cortexm identifies Cortex-M processors and provides Cortex-M0
// halting debug and register access through target memory.
package cortexm

import (
Expand Down
136 changes: 136 additions & 0 deletions target/cortexm/register.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package cortexm

import (
"context"
"errors"
"time"
)

// Register identifies a Cortex-M0 core register. Zero and unnamed values are
// invalid. The numeric values are not hardware register selectors.
type Register uint8

// Core registers accessible through an acquired, halted target. SP is the
// current stack pointer; MSP and PSP select its banks explicitly. PC is the
// debug return address, not a Thumb function pointer. XPSR includes status.
const (
R0 Register = iota + 1
R1
R2
R3
R4
R5
R6
R7
R8
R9
R10
R11
R12
SP
LR
PC
XPSR
MSP
PSP
)

const (
dcrsrAddress = uint32(0xe000edf4)
dcrdrAddress = uint32(0xe000edf8)
sRegReady = uint32(1 << 16)
sReset = uint32(1 << 25)
)

// ReadRegister reads a register while halted, without acquiring halt ownership.
// It writes debug transfer registers and consumes DHCSR's sticky status. Calls
// are bounded to five seconds or the caller's earlier deadline. An uncertain
// transfer blocks ordinary calls; Release must settle it before changing debug
// control. Loss of Debug state or reset during a pending transfer prevents
// automatic cleanup. An error returns no valid register value.
func (t *Target) ReadRegister(ctx context.Context, reg Register) (uint32, error) {
if reg < R0 || reg > PSP {
return 0, errors.New("cortexm: invalid register")
}
if err := t.active(ctx); err != nil {
return 0, err
}
ctx, cancel := context.WithTimeout(ctx, controlTimeout)
defer cancel()
if err := t.waitRegister(ctx); err != nil {
return 0, err
}
if err := t.selectRegister(ctx, uint32(reg-1)); err != nil {
return 0, err
}
value, err := t.memory.ReadWord(ctx, dcrdrAddress)
if err != nil {
t.closing = true
return 0, err
}
return value, nil
}

func (t *Target) selectRegister(ctx context.Context, selector uint32) error {
if err := ctx.Err(); err != nil {
return err
}
t.registerPending = true
if err := t.memory.WriteWord(ctx, dcrsrAddress, selector); err != nil {
t.closing = true
return err
}
return t.waitRegister(ctx)
}

func (t *Target) waitRegister(ctx context.Context) (err error) {
defer func() {
if err != nil && t.registerPending {
t.closing = true
}
}()
for {
if err := ctx.Err(); err != nil {
return err
}
if err := t.registerStatus(ctx); err != nil {
return err
}
if !t.registerPending {
return nil
}
timer := time.NewTimer(time.Millisecond)
select {
case <-ctx.Done():
timer.Stop()
return ctx.Err()
case <-timer.C:
}
}
}

func (t *Target) registerStatus(ctx context.Context) error {
if t.registerLost {
return errors.New("cortexm: register transfer lost its debug state; cleanup cannot continue")
}
value, err := t.memory.ReadWord(ctx, dhcsrAddress)
if err != nil {
t.closing = true
return err
}
halted := value&(cDebugEnable|sHalt) == cDebugEnable|sHalt
if t.registerPending && (!halted || value&sReset != 0) {
t.registerLost = true
return errors.New("cortexm: debug state changed during register transfer")
}
if value&cDebugEnable == 0 || value&(cStep|cMaskInts) != 0 {
t.closing = true
return errors.New("cortexm: debug mode changed during register access")
}
t.observeHaltRequest(value)
if !halted {
return errors.New("cortexm: register access requires a halted processor")
}
t.registerPending = value&sRegReady == 0
return nil
}
Loading
Loading