Choose the highest-level package that already owns the behavior an application needs. Drop to a lower layer only when the lower-level operation is itself the goal.
For example, a program identifying a Cortex-M should call cortexm.Identify
rather than read and decode CPUID itself. A program inspecting an access port
should call dap.DebugPort.ReadAPIDR rather than manually implement the posted
read pipeline with SWD transactions. Use ReadRawAP and WriteRawAP only when
the caller understands the selected AP class and will restore any state the
access changes. Derive the complete address with APSel.Address. A raw MEM-AP
data-register write can write target memory.
| Task | Public API | Executable reference |
|---|---|---|
| Discover and select among registered probe drivers | discover.Probes, ProbeInventory.Select, Candidate.Open, or discover.OpenProbe |
discover/probes integration tests |
| List every host USB attachment | usb.New, usb.AllDevices, Enumerator.List |
Package tests |
| List USB attachments understood by the FTDI driver | usb.New, ftdi.SupportedDevices, Enumerator.List |
ost ftdi list |
| Read metadata from one J-Link | usb.New, jlink.SupportedDevices, Enumerator.Open, jlink.Open, Session.Info |
Package tests |
| Read metadata from one CMSIS-DAP v2 probe | usb.New, usb.AllDevices, cmsisdap.Candidates, Enumerator.Open, cmsisdap.Open, Session.Info |
Package tests |
| Open one FTDI MPSSE SWD port | Enumerator.Open, ftdi.Open |
examples/trivial/swd-dpidr |
| Use an FTDI JTAG chain | ftdi.Open or Probe.JTAG, then jtag.New and jtag.NewChain |
FTDI integration tests |
| Open one J-Link SWD session | Enumerator.Open, jlink.Open, jlink.WithSWD |
Package tests |
| Use a J-Link JTAG chain | jlink.Open with WithJTAG or Probe.JTAG, then jtag.New and jtag.NewChain |
J-Link integration tests |
| Open one CMSIS-DAP SWD session | Enumerator.Open, cmsisdap.Open, cmsisdap.WithSWD |
Package tests |
| Connect SWD or transfer DP/AP registers | swd.New, Conn.Connect, Conn.ReadDP, Conn.WriteDP, Conn.ReadAP, Conn.WriteAP, Conn.NewBatch, Conn.Release |
examples/trivial/swd-dpidr |
| Enter SWD, decode a DPIDR, and manage SW-DP power | dap.NewDebugPort, DebugPort.Connect, DebugPort.Release |
ost dap dp id |
| Identify one explicitly selected AP | DebugPort.ReadAPIDR, DecodeAPIDR |
examples/simple/ap-id |
| Access another AP register by its full register address | DebugPort.ReadRawAP, DebugPort.WriteRawAP |
Package tests |
| Read or write one aligned target scalar through a MEM-AP | dap.OpenMemAP, MemAP.ReadScalar, MemAP.WriteScalar, MemAP.Release |
examples/simple/cortexm-info uses ReadWord. |
| Read or write arbitrary target bytes through a MEM-AP | dap.OpenMemAP, MemAP.ReadBlock, MemAP.WriteBlock, MemAP.Release |
Package tests |
| Obtain a MEM-AP's advertised debug entry | MemAP.ReadDebugBase |
examples/simple/coresight-info |
| 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, 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/M33 from an owned halt | Target.Step |
examples/simple/cortexm-control -step |
| Own an explicit Cortex-M group and control selected cores | cortexm.AcquireGroup, Group.Halt, Group.Resume, Group.Status, Group.Release |
Group ownership |
| Access a group member's registers or step it | Group.ReadRegister, Group.WriteRegister, Group.Step |
Member access |
| Read or write a halted Cortex-M0/M33 register | Target.ReadRegister, Target.WriteRegister |
Register reads, writes |
| Test SWD and DAP behavior without hardware | swd/sim, dap/sim |
Package tests |
The examples are intentionally small, executable compositions of public
packages. ost adds command parsing and output policy, but its internal
packages are not a reusable library surface.
armdebug.Connect owns the connection from an already-open probe through an Arm
SW-DP. Pass an explicit port configuration:
connected, err := armdebug.Connect(ctx, opened, armdebug.Config{
Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 1_000_000}),
})
if connected != nil {
defer func() { err = errors.Join(err, connected.Close()) }()
}
if err != nil {
return err
}
port := connected.Port()For SWD, armdebug checks opened.SupportsRegisterSWD() before activation. An
advertised register backend uses opened.RegisterSWD and dap.RegisterDP;
otherwise it uses the existing borrowed wire and dap.SWDP. An activation
error, including probe.ErrUnsupportedRegisterSWD returned after activation
began, ends setup without falling back. Existing armdebug.Config, MEM-AP,
Target, and Group calls are unchanged.
The call takes responsibility for opened even on invalid input. Stop using the
supplied probe directly. Keep any non-nil returned owner, including on an error,
until Close succeeds; the single deferred attempt above reports a failure but
does not replace an application's bounded retry policy. The borrowed port is for
serialized operations, not independent Connect or Release calls. Stop using
it when owner cleanup begins. A manually acquired MEM-AP must be released before
closing the owner.
Close releases DAP, which releases SWD or the JTAG chain, before closing the
probe. A live release failure retains the probe for another attempt. Cleanup
uses fresh bounded contexts, not the operation's possibly canceled context. If
the backend reports restoration impossible after device loss or a poisoned
stream, Close stops target traffic, reports
armdebug.ErrRestorationAbandoned, and continues host cleanup. It preserves any
restoration and host-close errors from that attempt. Retrying Close then
retries only pending host cleanup; a fully released owner returns nil. Freeing
host resources does not resume a halted target or restore its debug state.
Backends without the optional restoration contract retain conservative retry
behavior. Port() returns nil once cleanup starts; previously returned pointers
are governed by the borrowing contract, not forcibly revoked.
connected.RestorationPossible() reports the retained probe's restoration
availability without sending traffic. Use it after a borrowed target fails to
release, before deciding whether to close its memory owner:
cleanupErr := core.Release(cleanupCtx)
if cleanupErr != nil && connected.RestorationPossible() {
return errors.Join(err, cleanupErr) // Retain core and connected for retry.
}
return errors.Join(err, cleanupErr, connected.Close())A false result permits terminal host cleanup through Close; it does not prove
that target state was restored. A true result does not validate an invalidated
MEM-AP or make a stale client usable. Nil and released connections report false.
Use the owner's method to acquire a MEM-AP whose cleanup it will track:
memory, err := connected.OpenMemAP(ctx, dap.NewAPSel(0))
if err != nil {
return err
}
processor, err := cortexm.Identify(ctx, memory)Distinct APs may be acquired and used serially; acquiring the same AP twice
fails before traffic. Do not call Release on these borrowed clients. Close
releases them in reverse acquisition order before releasing DAP/SWD. If one
release fails, the owner retains that client and all lower dependencies for
retry. An acquisition error preserves existing clients and ownership; the DAP
client's state determines which subsequent operations remain possible. Clients
acquired directly with dap.OpenMemAP, rather than this method, are not tracked
and remain the caller's cleanup responsibility.
The generic examples/simple/arm-info program uses this ownership path:
go run ./examples/simple/arm-info -provider jlink -serial SERIAL -ap 0It defaults to a 1 MHz SW-DP; -clock selects the requested ceiling in Hz. 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.
A generic tool enables the bundled providers with these imports:
import (
"github.com/jon/ostiole/discover"
_ "github.com/jon/ostiole/discover/probes"
)For a smaller binary, blank-import just jlink/discovery, ftdi/discovery, or
cmsisdap/discovery. The core discover and probe packages import no USB
implementation or concrete driver. Registration performs no hardware I/O.
A probe-owned J-Link wire retains its configuration after a complete scan-status error and reconfigures before the next valid nonempty cleanup request. The failed scan is not replayed. Keep the protocol owner for its normal restoration retry; a scan-status error alone does not establish terminal loss. Direct J-Link sessions still require explicit reconfiguration. See the J-Link session boundary.
For an owned Arm debug connection, armdebug.Open combines discovery and
Connect, using the same selection and configuration:
connected, err := armdebug.Open(ctx, selection, armdebug.Config{
Port: armdebug.SWDP(probe.SWDConfig{MaxClockHz: 1_000_000}),
})Handle the returned owner and error as in the Connect example above. Invalid
configuration or incomplete discovery prevents opening. For an explicit
registry, use registry.OpenProbe and then armdebug.Connect; if opening fails
with a probe, close that probe rather than trying to activate it.
With providers registered, discover.OpenProbe(ctx, selection) combines
enumeration, classification, unique selection, and opening. It stops on any
discovery error. discover.Probes(ctx) returns a partial inventory alongside
errors so an application can inspect those errors before deliberately opening a
surviving candidate. An explicit discover.Registry offers the same calls. All
paths leave the same cleanup obligations on the returned owner, including when
opening returns both an owner and an error.
A discover.ProbeInventory supports direct iteration and exact selection:
for candidate := range inventory {
fmt.Println(candidate.Info())
}
selected, err := inventory.Select(discover.Selection{Serial: serial})
if err != nil {
return err
}
opened, err := selected.Open(ctx)
if opened != nil {
defer func() { err = errors.Join(err, opened.Close()) }()
}
if err != nil {
return err
}inventory.Open(ctx, selector) combines the last two operations. If an opener
returns an owner with its error, preserve that owner for cleanup. No open path
tries another candidate after failure. Selection errors return copied matching
metadata in inventory order.
Transport discovery uses registered providers:
transports, err := discover.Transports(ctx)
if err != nil {
return err
}
for transport := range transports {
fmt.Println(transport.Info())
}
inventory, err := transports.Probes(ctx)
if err != nil {
return err
}
selected, err := inventory.Select(discover.Selection{Provider: provider, Serial: serial})
if err != nil {
return err
}
opened, err := selected.Open(ctx)
// Retain opened even when err != nil; its Close may still be needed.For a helper which leaves ownership with its caller, return cleanup explicitly:
func openSelected(ctx context.Context, selection discover.Selection) (_ *probe.Probe, cleanup func() error, err error) {
opened, err := discover.OpenProbe(ctx, selection)
if opened == nil {
return nil, nil, err
}
return opened, opened.Close, err
}Retain a non-nil cleanup function even after an error, and retry failed cleanup. Release higher-level SWD, DAP, and MEM-AP state before calling it. Closing the probe invalidates every borrowed wire, including when transport cleanup fails.
Providers can instead register on a caller-owned discover.Registry with
RegisterTransport. EnsureTransport shares an identical provider dependency
without accepting a different provider under the same ID. Iteration repeats the
detached snapshot without enumerating again. To use slices.Collect, convert
the named sequence to iter.Seq[discover.Transport] explicitly.
Probe order is provider ID, serial, function, location, then product, with binding identity breaking final ties. Transport order is provider ID, serial, location, product, then attachment identity. Strings compare lexicographically and case-sensitively; empty strings sort first. USB uses numeric bus/address and VID/PID for its final identity key. Missing or duplicate serials cannot keep physical devices in the same order when replugging changes their addresses.
candidate.Info().Binding is an opaque token for the complete provider,
transport, and binding identity. Display it when ordinary metadata cannot
distinguish candidates, and pass it back as an exact filter:
selected, err := inventory.Select(discover.Selection{Binding: binding})The binding filter combines with every other nonempty filter. An unknown binding
returns not-found; it never falls back to another candidate. Do not parse the
token or assume it survives replugging. An empty Selection{} deliberately
selects the sole candidate and reports ambiguity for several.
An application with a probe.SWDBackend can transfer it to a generic owner:
opened := probe.New(info, backend)
defer func() { err = errors.Join(err, opened.Close()) }()
wire, err := opened.SWD(ctx, probe.SWDConfig{MaxClockHz: 1_000_000})
if err != nil {
return err
}
connection := swd.New(wire)Use a named error result for this cleanup pattern. Release any SWD or DAP state before closing the probe. The caller must stop using the transferred backend directly; the borrowed wire has no independent close operation.
A probe.RegisterSWDBackend lends completed register operations instead of SWD
bits. Query opened.SupportsRegisterSWD() before choosing activation, then call
opened.RegisterSWD(ctx, config). The returned probe.RegisterSWD implements
dap.RegisterTransport; bind it with dap.RegisterDP(registers). Activation
configures the adapter but performs no protocol entry. The backend retains all
resources after a failure, and the probe attempts host cleanup; keep it for
retry while cleanup remains pending. Nil and typed-nil successful surfaces are
rejected through the same cleanup path.
Only one SWD wire, register transport, or JTAG surface may be activated from one
probe. Release DAP before closing the probe. The borrowed register surface has
no independent host close; closing the probe invalidates every borrowed
operation even when host cleanup must be retried. Inactive register reads and
writes return dap.RegisterUnsent with an error.
After releasing borrowed protocol and target owners, retain a probe according to its cleanup state while preserving the error from the close attempt:
func closeProbe(p *probe.Probe) (*probe.Probe, error) {
err := p.Close()
if p.CleanupPending() {
return p, err
}
return nil, err
}Backends can implement probe.CleanupState to distinguish retained resources
from a final close error; those without it retain conservative retry behavior.
For direct USB ownership, use device.CleanupPending() or
claim.CleanupPending() after the corresponding close attempt. USB continues
returning its cached final error even after releasing its host resources.
opened.RestorationPossible() independently reports whether restoration
communication remains possible, with conservative behavior for a backend which
lacks probe.RestorationState. It performs no restoration and does not confirm
that a halted target was restored.
FTDI and J-Link owners can instead lend JTAG with
opened.JTAG(ctx, probe.JTAGConfig{MaxClockHz: 100_000}). Pass the wire to
jtag.New, supply an explicit layout, and release the chain before closing the
probe. Retain both after a failed release so cleanup can be retried. The
JTAG guide covers pin wiring and chain ownership.
Concrete drivers can open that owner from an exact USB identity:
opened, err := jlink.OpenProbe(ctx, identity)
// Or: ftdi.OpenProbe(ctx, identity, ftdi.PortA)
if err != nil {
return err
}
defer func() { err = errors.Join(err, opened.Close()) }()Start with usb.New and list only the identities accepted by the intended
driver. Treat the result as a snapshot rather than a live handle.
When a device family has no numeric identity catalog, usb.AllDevices makes
that broader inventory request explicit. The caller can inspect
DeviceInfo.Product, but a product string alone does not establish driver or
protocol support.
The current examples require exactly one supported FTDI attachment. A larger
application can present the returned identities to a user or match a known
nonempty DeviceInfo.Serial, but it should still make one explicit selection
before calling Open. The selected candidate retains its enumerated bus and
address; pass the complete value to Open so it can reject a replugged or
replaced attachment. Do not silently pick the first result from an ambiguous
inventory.
A CMSIS-DAP v2 metadata session begins with the explicit broad inventory:
func openCMSISDAP(ctx context.Context) (_ *cmsisdap.Session, cleanup func() error, err error) {
enumerator := usb.New()
devices, err := enumerator.List(ctx, []usb.DeviceFilter{usb.AllDevices()})
if err != nil {
return nil, nil, err
}
candidates := cmsisdap.Candidates(devices)
if len(candidates) != 1 {
return nil, nil, fmt.Errorf("found %d CMSIS-DAP candidates, want one", len(candidates))
}
device, err := enumerator.Open(ctx, candidates[0])
if err != nil {
return nil, nil, err
}
session, err := cmsisdap.Open(ctx, device)
if err != nil {
if session != nil && session.CleanupPending() {
return nil, session.Close, err
}
if device.CleanupPending() {
return nil, device.Close, err
}
return nil, nil, err
}
return session, session.Close, nil
}On success the session owns the device, and cleanup calls session.Close.
After failed open, a retained session owns its command port, claim, and device;
otherwise the caller retains any pending device cleanup. Keep a non-nil cleanup
function and report its errors. Final cleanup errors are already part of the
open error and do not require another close after resources are released.
Product matching is case-sensitive and only shortlists candidates; Open still
requires the exact v2 bulk interface. An application which knows a composite
probe by serial or another explicit policy may select it from devices even
when its device product string is absent from Candidates.
A configured CMSIS-DAP session supports safe explicit SWDIO sequences, with
whole-call preflight and direction restrictions. It cannot serve as the wire for
swd.New: firmware enables output at command end, while a general SWD wire must
also preserve target ownership across calls. Use completed register operations
for target compositions:
registers := cmsisdap.NewRegisterSWD(session) // session configured with WithSWD
dp := dap.NewDebugPort(dap.RegisterDP(registers))Connect the debug port explicitly and release it before closing the session.
cmsisdap.OpenProbe advertises RegisterSWD, which armdebug prefers before
activation; the examples use this path without assembling a raw SWD wire.
Activation errors do not fall back to sequences. See the
register ownership contract
and sequence contract.
A metadata-only J-Link session follows the same explicit inventory rule:
func readJLinkInfo(ctx context.Context) (_ jlink.Info, cleanup func() error, err error) {
enumerator := usb.New()
candidates, err := enumerator.List(ctx, jlink.SupportedDevices())
if err != nil {
return jlink.Info{}, nil, err
}
if len(candidates) != 1 {
return jlink.Info{}, nil, fmt.Errorf("found %d supported J-Links, want one", len(candidates))
}
device, err := enumerator.Open(ctx, candidates[0])
if err != nil {
return jlink.Info{}, nil, err
}
session, err := jlink.Open(ctx, device)
if err != nil {
closeErr := device.Close()
if closeErr != nil {
return jlink.Info{}, device.Close, errors.Join(err, closeErr)
}
return jlink.Info{}, nil, err
}
info := session.Info()
if err := session.Close(); err != nil {
return jlink.Info{}, session.Close, err
}
return info, nil, nil
}Inventory policy still belongs to the application. jlink.Open takes ownership
on success. It claims only the J-Link application interface, resolves the active
endpoints after selecting its alternate, and drains stale IN data before
metadata. It requires a 100 ms quiet interval within a one-second
synchronization budget, with separate abort and cleanup bounds. Replies delayed
beyond the quiet window remain outside that guarantee. With no options it does
not select or configure a target interface. If a close fails, the returned
cleanup function keeps the affected device or session reachable. Calling it
again either retries a retained interface claim or returns the cached
device-close result.
To use the same selected probe as an SWD wire, configure it while opening and
pass the session to swd.New. Keep the cleanup closure when the operation
fails: it retains the highest owner until release succeeds, and reconfigures
after a complete scan error before retrying SWD cleanup.
func connectJLinkSWD(ctx context.Context, device *usb.Device) (_ uint32, cleanup func() error, err error) {
session, err := jlink.Open(ctx, device, jlink.WithSWD(1_000_000))
if err != nil {
closeErr := device.Close()
if closeErr != nil {
return 0, device.Close, errors.Join(err, closeErr)
}
return 0, nil, err
}
connection := swd.New(session)
connectionOwned := true
var abandoned error
cleanup = func() error {
cleanupCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if connectionOwned {
if session.ClockHz() == 0 {
if err := session.ConfigureSWD(cleanupCtx, 1_000_000); err != nil {
if !errors.Is(err, jlink.ErrSessionPoisoned) {
return err
}
abandoned = errors.Join(abandoned, fmt.Errorf("reconfigure SWD for release: %w", err))
connectionOwned = false
}
}
if connectionOwned {
if err := connection.Release(cleanupCtx); err != nil {
if !errors.Is(err, jlink.ErrSessionPoisoned) {
return err
}
abandoned = errors.Join(abandoned, fmt.Errorf("release SWD connection: %w", err))
}
connectionOwned = false
}
}
return errors.Join(abandoned, session.Close())
}
raw, err := connection.Connect(ctx)
return raw, cleanup, err
}WithSWD selects the advertised SWD interface and requests a whole-kHz clock no
greater than its argument. A metadata-only session can instead call
ConfigureSWD later. Both forms change volatile probe interface and clock
state, which Close does not restore. A complete nonzero scan status requires
another ConfigureSWD; an ambiguous transfer requires close and reopen. The
returned cleanup closure stops at a transient higher-level release failure and
can be retried. After a complete nonzero scan status, ClockHz() returns zero,
so the closure reconfigures SWD before retrying swd.Conn.Release; it does not
close the session while the connection still owns restorable target state.
jlink.ErrSessionPoisoned is different because restoration is no longer
possible. The closure records that restoration error, drops the higher-level
owner, continues with retryable session cleanup, and keeps returning the error
after the session closes.
Pass the opened device to ftdi.Open with the MPSSE port and maximum requested
clock. The driver reads and validates the product from the device identity;
discovery does not choose the port or clock.
ftdi.Open checks MPSSE synchronization both before and after applying the
H-series clock configuration. FT2232 generations share product identifier
0x6010; a matching identifier alone does not establish support. A rejected
configuration command leaves a bad-command echo ahead of the second
synchronization response, so opening fails rather than treating that echo as
protocol data. This check does not select a port or identify an exact chip
revision.
ftdi.Open initializes MPSSE and the clock with target pins as inputs. The
returned channel supplies both SWDIO and JTAGIO; each call establishes its
own directions. A non-nil channel owns the USB device even on error; close that
channel and retain it if cleanup fails. Only a nil result leaves Device.Close
with the caller. Release higher-level protocol state before closing the channel,
and do not mix raw operations underneath a live protocol connection.
Adapter drivers submit USB transfers through the claimed interface and keep their scheduling policy themselves:
claim, err := device.ClaimInterface(0)
if err != nil {
return err
}
endpoint, err := claim.Endpoint(ctx, 0x81)
if err != nil {
return errors.Join(err, claim.Close())
}
if endpoint.TransferType != usb.TransferBulk || endpoint.MaxPacketSize == 0 {
return errors.Join(errors.New("invalid bulk IN endpoint"), claim.Close())
}
buffer := make([]byte, endpoint.MaxPacketSize)
input, err := claim.SubmitBulk(ctx, endpoint.Address, buffer)
if err != nil {
return errors.Join(err, claim.Close())
}
output, err := claim.SubmitBulk(ctx, 0x02, command)
if err != nil {
return errors.Join(err, claim.AbortBulk(endpoint.Address), claim.Close())
}
if _, err := output.Wait(ctx); err != nil {
return errors.Join(err, claim.AbortBulk(0x02), claim.AbortBulk(endpoint.Address), claim.Close())
}
received, err := input.Wait(ctx)
if err != nil {
return errors.Join(err, claim.Close())
}
completion := buffer[:received]
if err := consume(completion); err != nil {
return errors.Join(err, claim.Close())
}
return claim.Close()This example posts one IN request before its OUT request. A protocol which needs
a receive window submits several maximum-packet-sized buffers instead; one which
does not tolerate read-ahead submits its IN request only when the response is
due. Each handle reports its own completion, including a short or zero-length
transfer. Ending a Wait context does not cancel the request. If the host
transfer engine fails, Wait returns that error while Done can remain open;
the caller must not reuse the buffer until Done closes. AbortBulk is
endpoint-wide and performs a bounded drain of every pending request on that
endpoint. A drain timeout matches context.DeadlineExceeded. If native
cancellation or the drain fails, those requests and the claim remain owned so
cleanup can be retried. Closing the claim applies the same bound across its
endpoints before release. The first endpoint lookup reads the interface's
current alternate setting; a new claim does not imply alternate zero. If
alternate selection fails, the next endpoint lookup reads the host state again
instead of retaining descriptors for the previous alternate.
A successful AbortBulk does not prove that an IN request received no packet:
completion can race with the abort. Inspect the stable result with a fresh
bounded observation context after the drain:
observeCtx, cancel := context.WithTimeout(context.Background(), time.Second)
received, completionErr := input.Wait(observeCtx)
cancel()
aborted := input.Aborted()Aborted reports a completed native abort with no transferred data and no other
transfer or native cleanup failure. It sends no traffic; nil, zero, and pending
transfers report false. Wait retains the original native error. A successful
completion can contain a packet even when the earlier wait timed out. The
adapter decides whether an abort was expected and what the received data means;
the query does not release the claim or establish protocol restoration.
Select JTAG-DP through the same Open or Connect calls. The configuration
contains a complete expected layout, a zero-based TDO-first TAP index, and the
probe's clock ceiling:
arm, err := jtag.IDCODE(4, 0x5ba00477)
if err != nil {
return err
}
xilinx, err := jtag.IDCODE(12, 0x14730093)
if err != nil {
return err
}
config := armdebug.Config{
Port: armdebug.JTAGDP(
probe.JTAGConfig{MaxClockHz: 100_000},
jtag.Layout{arm, xilinx}, 0,
),
CleanupTimeout: 30 * time.Second,
DAPOptions: []dap.Option{dap.WithMaxWaits(100)},
}
connected, err := armdebug.Open(ctx, selection, config)Store any non-nil connected owner before handling err, including when setup
failed. For an already-open probe, call armdebug.Connect(ctx, opened, config)
instead; ownership of opened transfers even on error. Close any returned owner
and retain it until cleanup succeeds. These are the same ownership rules as the
SW-DP path.
The configuration copies the layout without traffic. Static layout, selected TAP, clock, and cleanup-timeout validation precede discovery or activation. The selected TAP must have an IDCODE and a four- or eight-bit IR. DAP then validates the exact physical chain and enters baseline ADIv5 JTAG-DP. Board-specific routing must already be enabled; the owner does not infer a layout or activate a hidden DAP. DAP options are checked after probe activation.
The borrowed connected.Port() supplies IDCODE through its cached Identity,
and the existing AP and transaction APIs. Acquire memory separately with
connected.OpenMemAP(ctx, dap.NewAPSel(1)); the owner tracks that MEM-AP's
restoration. Do not release or reconnect borrowed clients yourself. Close
restores owned MEM-APs in reverse acquisition order, releases DAP and the chain
to BYPASS/Idle, then closes the probe. A failure retains the current owner and
its dependencies, and completed releases are not repeated.
CleanupTimeout bounds each owned release attempt. Zero chooses one second for
SWD or thirty seconds for JTAG; a negative value is invalid. Each attempt uses a
fresh context, independent of the operation context. DAP's own recovery
attempts, including chain revalidation, have separate bounds configured with
dap.WithCleanupTimeout in DAPOptions; host cleanup also keeps its own
limits. CleanupTimeout is not a total deadline for Close.
The opt-in managed JTAG bench test is:
OSTIOLE_ARMDEBUG_JTAG_HIL=1 go test -tags=integration ./armdebug -run '^TestHILArmJTAGOwnership$' -count=1 -vOn Nostalgia, FT4232H 01691/A at 100 kHz completed two fresh Connect
sessions and two fresh Open sessions against the externally activated Arm
0x5ba00477/IR4 and Xilinx 0x14730093/IR12 chain. AP1 IDR was 0x44770002;
component words at 0x80410ff0 through 0x80410ffc were 0x0d, 0x90,
0x05, and 0xb1. Each owner closed successfully. Fresh sessions found the
same initial AP1 CSW/TAR (0x80000042/0) and connected CTRL/STAT
(0xf0000000). This exercises managed cleanup and repeated-session AP
restoration; it does not independently measure power after closing the probe. No
halt, target reset, or target-memory write was performed.
Use dap.NewDebugPort(dap.RegisterDP(transport)) when a caller-supplied
dap.RegisterTransport completes each physical DP or AP read or write
internally. DAP retains identity, bank selection, power, WAIT policy,
transactions, and restoration. The transport owns protocol entry and framing.
Give the DP owner exclusive use, retain both values after a cleanup failure, and
release the DP before closing its transport. Use dap.OpenMemAP and release
each acquired MEM-AP before releasing the DP, as with the SWD-wire and JTAG
bindings. See the
completed register helper for
entry, error handling, and bounded cleanup.
Use swd.Conn when the application needs one explicit wire-protocol transaction
or is bringing up an SWD path. Call Connect before register access and
Release before closing the wire. Connect returns DPIDR, keeps inherited
ORUNDETECT or tries to enable it, and records whether the setting was inherited.
Release restores only a change made by that connection and can be retried. A
register operation returns WAIT, FAULT, parity, and protocol errors without
replaying the requested transaction. When a fixed response returns WAIT, the
connection clears STICKYORUN before returning it. Use Conn.NewBatch for an
ordered group of raw register operations. Queue each operation with the
direction-specific DP or AP method, call Commit, then read each
direction-specific result. The batch uses the connection's established response
grammar. In simple mode it sends one request at a time; in overrun mode it packs
complete fixed frames when the wire reports room for more than one. A transport
failure makes the operations in that physical chunk indeterminate and leaves
later chunks unsent. WAIT and FAULT still stop the batch, and the connection
never replays a requested operation. The SWD protocol guide
describes the wire transaction and the specification details which are easiest
to misread.
Use dap.DebugPort when the application needs debug-port identity, power
ownership, bank selection, or AP access. Call Connect before AP operations and
Release afterward. DebugPort.Connect also connects its underlying SWD
stream, and DebugPort.Release releases it after restoring DAP state; do not
connect or release that stream separately. Give the debug port exclusive,
serialized use of its swd.Conn; direct transfers on that connection can
invalidate cached DAP state. DP, AP, transaction, and MEM-AP operations require
an active connection. ReadDP and WriteDP take logical ADIv5 register names
and manage DPBANKSEL without exposing a current-bank API. NewAPSel constructs
an ADIv5 index; APAt constructs an ADIv6 base-address selector. Both return
APSel values; the zero APSel remains invalid. APSel.Address combines a
selector with an eight-bit ADIv5 or twelve-bit ADIv6 register offset; the
resulting APAddress also has an invalid zero value. ReadAPIDR reads and
decodes the common read-only AP identity. EnumerateAPs scans every ADIv5 AP
selector without reading class-specific registers. Raw AP access rejects an
invalid or unaligned address before traffic. Use it only when the caller
understands the selected AP class and will restore any state the access changes.
A raw MEM-AP data-register write can write target memory. This layer owns AP
read and write completion. Construct an SWD binding with
dap.NewDebugPort(dap.SWDP(conn)); the operation context bounds WAIT retry.
Adding dap.WithMaxWaits(1) stops at the first clean WAIT, reporting both
dap.ErrWait and its underlying swd.ErrWait. SetMaxWaits changes the limit
before Connect or after a successful Release; it rejects the change while
the port is connected or cleanup is pending. The count is per physical request
and does not bound host I/O. A raw AP read or write which completes, or might
have completed, invalidates existing MemAP values. If the limit or context
ends after an AP WAIT, dap.DebugPort issues DAPABORT; existing dap.MemAP
values reject further reads, though dap.MemAP.Release still attempts to
restore their saved state.
For an explicit JTAG composition, pass dap.JTAGDP(chain, tapIndex) instead.
The chain supplies the complete expected layout; the index is zero-based and
TDO-first. Connect validates it and enters baseline ADIv5 JTAG-DP. The same
AP, Txn, and OpenMemAP APIs then apply. JTAG polls accepted requests without
replaying them and checks CTRL/STAT after each AP operation. It temporarily
disables inherited ORUNDETECT and restores it during release. Release MEM-APs,
then DAP and its chain, before closing the probe. Independent recovery defaults
to thirty seconds for JTAG, versus one second for SWD; use
dap.WithCleanupTimeout for slower clocks. The DAP guide shows
the binding and the read-only FTDI bench procedure. armdebug.JTAGDP composes
these same owners through the managed path above.
The SWD connection reads DPIDR, clears supported sticky conditions with ABORT,
establishes bank zero through RDBUFF, and establishes its response grammar
before DAP requests power. Debug-port CTRL/STAT writes must preserve ORUNDETECT.
DAP settles a new SELECT through RDBUFF before sending AP traffic. If WAIT
cleanup or a later retry leaves framing unknown, dap.DebugPort invalidates
those values and later DP and AP calls stop before sending traffic. Connect
performs bounded cleanup after failed setup. When cleanup succeeds, the debug
port can connect again immediately. If cleanup also fails, or if Release
fails, ordinary DP, AP, and MEM-AP operations remain blocked. Call
MemAP.Release before retrying DebugPort.Release; cleanup re-enters SWD when
necessary and verifies that DPIDR still identifies the connection being cleaned
up before restoring state. DebugPort.Release settles its final bank-zero
SELECT through RDBUFF, releases power, and restores connection-owned ORUNDETECT
before returning success. The DAP guide describes the ADIv5
register protocol behind that lifecycle.
Use DebugPort.NewTxn when several DP or AP accesses need ordered results.
Commit validates the complete queue, settles an earlier immediate DP write if
necessary, then lets the SWD connection pack fixed frames within its wire limit.
Sticky-exempt DPIDR, CTRL/STAT, and ABORT operations remain separate so an
earlier WAIT or FAULT cannot hide behind one of them. DP writes and AP
operations settle through RDBUFF before reporting success. Queued reads return a
ReadResult, whose Value method returns the data. Queued writes return a
WriteResult, whose Err method reports completion without a placeholder
value. If the earlier write cannot be settled, none of the queued operations
runs. Otherwise, confirmed results remain available after a failure; a clocked
operation whose completion is uncertain reports ErrIndeterminate, and later
operations report ErrNotExecuted. A transaction acquires no additional state
and does not change the release order.
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. 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,
framing, or cleanup becomes uncertain, repair is required. A FAULT returns the
contiguous prefix read before the fault; a configured WAIT limit, cancellation,
and transport or protocol failures can also interrupt the read. The rest of the
destination remains unchanged. No auto-incrementing word run crosses a 1 KiB TAR
boundary; unaligned edges still require the MEM-AP to accept byte or halfword
CSW sizes. If the MEM-AP does not accept single address increment, ReadBlock
and WriteBlock write TAR before each word.
MemAP.WriteBlock accepts the same ranges and uses the same geometry. Its
returned prefix includes only chunks whose RDBUFF completion requests were
accepted. If a failed chunk might already have changed memory, the error
includes dap.ErrIndeterminate; the method does not retry that chunk
automatically. An indeterminate chunk invalidates the MemAP, but Release
remains available to restore its saved state. An accepted write is not replayed;
if its RDBUFF completion request returns WAIT, only that request is retried.
MemAP.WriteScalar and MemAP.WriteBlock change target memory at the selected
addresses. The library rejects a scalar value which does not fit the selected
size and validates alignment, range, and advertised extensions, but deciding
whether an address is safe to write belongs to the application. Keep the debug
port connected until MemAP.Release restores CSW, TAR, and TARHI when present.
Calls sharing a MEM-AP, its debug port, or their SWD connection must be
serialized; these values do not add locking. If a Size64 transfer fails after
its first DRW access might have started, ordinary debug-port traffic remains
blocked. Release the MEM-AP so it can terminate the incomplete transfer through
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 or Cortex-M33 halting debug. Use ReadRegister
for halted core registers and WriteRegister for intentional changes. The
target tracks transfer completion but does not roll back writes. Release it
before its memory owner and retain both after failed target restoration. See
Cortex-M control for the full composition and effects.
For independent RP2350 processors, borrow both MEM-APs from one Arm owner and acquire a separate target for each. Serialize calls over that connection and release both targets before closing their memory owner. The two-core composition and bench show the per-core ownership boundary and physical observations.
Use cortexm.AcquireGroup when one owner should acquire those targets and
retain per-member results after partial failure. Supply explicit IDs and
borrowed memory clients; access the targets through group methods. Serialize
group and other connection calls. Release the group before closing the Arm
owner, and retain both when target restoration remains pending. See
group ownership and control for the composition
and failure rules.
A complete Cortex-M identity composition acquires and releases state in one of these orders:
acquire: USB device → FTDI channel → debug port (enters SWD) → MEM-AP
release: MEM-AP → debug port → FTDI channel
acquire: USB device → J-Link session (configures SWD) → debug port → MEM-AP
release: MEM-AP → debug port → J-Link session
Use a fresh, bounded cleanup context if the operation context may already be canceled. Join cleanup errors with the operation error so a restoration or close failure is not lost.
Enumerator.Open cleans up native resources before returning an error.
ftdi.Open attempts cleanup, then leaves the original device with the caller
for the Device.Close described above. A successfully returned value belongs to
the caller until its documented release or close method succeeds.
After acquiring a MEM-AP through the owner above, obtain its advertised identification page and pass it to the component reader:
base, present, err := memory.ReadDebugBase(ctx)
if err != nil {
return err
}
if !present {
return errors.New("MEM-AP advertises no debug entry")
}
component, err := coresight.Identify(ctx, memory, base)
if err != nil {
return err
}
fmt.Printf("class=%#x part=%#x\n", component.Class(), component.Part())This borrows the same memory client and adds no cleanup owner. An advertised
address still requires component power and access permissions. A caller that
already knows another accessible identification page can pass that address
directly to coresight.Identify. Use coresight.Walk with explicit
WalkLimits to follow ROM entries. Retain partial visits when it returns an
error, and leave power-domain children skipped until access has been established
separately. The same memory owner retains cleanup responsibility. The
component guide describes entry decoding, traversal bounds,
power metadata, and incomplete results.
Applications own choices that depend on their users:
- which enumerated attachment to open;
- which explicit FTDI port, J-Link session, or CMSIS-DAP session and clock to request;
- which access port to inspect;
- operation and cleanup deadlines;
- how errors and identities are presented.
Libraries own reusable hardware and protocol behavior. USB requests, MPSSE commands, SWD frames, AP posted reads, MEM-AP register restoration, and CPUID decoding should not be recreated in an application.
If a needed capability is absent, add it at the layer that can express and test
it as a reusable mechanism. Do not hide a second hardware stack in an example,
an ost subcommand, or another application's command package.
Before writing a hardware composition:
- Read the capability boundaries and confirm the behavior exists.
- Select the highest package in the task table that owns the desired result.
- Follow the matching executable example for acquisition and cleanup.
- Import public packages only; do not import
cmd/ost/internal/.... - Preserve explicit selection, deadlines, cleanup, and safety effects.
- If the capability is missing, make that gap explicit instead of reimplementing lower-level framing in application code.
The architecture guide is the authority for current package ownership. The examples are the authority for compact, executable composition.