oep-client-python 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oep_client/__init__.py +5 -0
- oep_client/v1/__init__.py +1 -0
- oep_client/v1/__main__.py +40 -0
- oep_client/v1/arm.py +269 -0
- oep_client/v1/capture.py +365 -0
- oep_client/v1/catalog.py +181 -0
- oep_client/v1/ch32_flash.py +254 -0
- oep_client/v1/cobs.py +73 -0
- oep_client/v1/console.py +165 -0
- oep_client/v1/core.py +172 -0
- oep_client/v1/decode.py +65 -0
- oep_client/v1/dump.py +157 -0
- oep_client/v1/endpoint.py +1479 -0
- oep_client/v1/esp32_targets.py +137 -0
- oep_client/v1/fake.py +233 -0
- oep_client/v1/fake_serial.py +105 -0
- oep_client/v1/fake_serve.py +299 -0
- oep_client/v1/fixture.py +121 -0
- oep_client/v1/frames.py +91 -0
- oep_client/v1/hid_stream.py +257 -0
- oep_client/v1/host.py +321 -0
- oep_client/v1/interfaces.py +119 -0
- oep_client/v1/link.py +414 -0
- oep_client/v1/message.py +235 -0
- oep_client/v1/names.py +86 -0
- oep_client/v1/registry.py +59 -0
- oep_client/v1/riscv.py +430 -0
- oep_client/v1/rp2350.py +85 -0
- oep_client/v1/target.py +16 -0
- oep_client/v1/uiapduino.py +121 -0
- oep_client/v1/usb_stream.py +209 -0
- oep_client_python-0.0.1.dist-info/METADATA +106 -0
- oep_client_python-0.0.1.dist-info/RECORD +36 -0
- oep_client_python-0.0.1.dist-info/WHEEL +4 -0
- oep_client_python-0.0.1.dist-info/entry_points.txt +2 -0
- oep_client_python-0.0.1.dist-info/licenses/LICENSE +21 -0
oep_client/v1/riscv.py
ADDED
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""oep.wire.rvswd / oep.wire.swio and oep.target.riscv-dm, revision 1 (oep-spec oep-if-debug §1-§4).
|
|
2
|
+
|
|
3
|
+
The host knows the target; the probe only moves wires and DMI. Everything chip-specific (flash controller, RAM loaders,
|
|
4
|
+
register meanings) stays on this side.
|
|
5
|
+
|
|
6
|
+
§5.4: wire and target results carry a status (ok, wait, line, fault, timeout, state; any other value is a failure). A
|
|
7
|
+
request the probe ran but that did not get through is completed failed (nothing done) or partial (some done) with the
|
|
8
|
+
success shape, so `done` and `status` say how far it went: this module raises TargetError with them.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import struct
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass, field
|
|
16
|
+
|
|
17
|
+
from . import host as h, message as m, registry as reg
|
|
18
|
+
from .core import Interface
|
|
19
|
+
from .fixture import Gpio
|
|
20
|
+
|
|
21
|
+
STATUS = reg.STATUS
|
|
22
|
+
OK = STATUS["ok"]
|
|
23
|
+
TIMEOUT = STATUS["timeout"]
|
|
24
|
+
STATUS_NAMES = {v: k for k, v in STATUS.items()}
|
|
25
|
+
_RV = reg.TARGET_RISCV_DM
|
|
26
|
+
STEP = _RV.enum["dmi_step"]
|
|
27
|
+
STEP_WRITE, STEP_READ, STEP_POLL_READS, STEP_WAIT_US, STEP_POLL_US = (
|
|
28
|
+
STEP["write"], STEP["read"], STEP["poll_reads"], STEP["wait_us"], STEP["poll_us"])
|
|
29
|
+
STEP_SIZES = {STEP_WRITE: 6, STEP_READ: 2, STEP_POLL_READS: 12, STEP_WAIT_US: 5, STEP_POLL_US: 14}
|
|
30
|
+
VALUE_STEPS = {STEP_READ, STEP_POLL_READS, STEP_POLL_US} # steps that add a value to the result
|
|
31
|
+
POLL_STEPS = {STEP_POLL_READS, STEP_POLL_US} # ... and add their last value when they time out
|
|
32
|
+
REG_A0, REG_A1 = 0x100A, 0x100B
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def status_name(status: int) -> str:
|
|
36
|
+
return STATUS_NAMES.get(status, f"unknown status 0x{status:02x}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TargetError(h.OepError):
|
|
40
|
+
"""A wire or target operation that did not get through: `status` (§5.4), `done` (steps / words completed, where
|
|
41
|
+
the op has it), `values` (what it did read), `result` (the probe's answer)."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, what: str, status: int, result: m.Result | None = None, done: int | None = None,
|
|
44
|
+
values: list[int] | None = None, data: bytes = b""):
|
|
45
|
+
at = f" after {done}" if done is not None else ""
|
|
46
|
+
outcome = f" ({result.describe()})" if result is not None else ""
|
|
47
|
+
super().__init__(f"{what} stopped{at}: {status_name(status)}{outcome}")
|
|
48
|
+
self.status, self.result, self.done, self.values, self.data = status, result, done, values or [], data
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def ran(result: m.Result) -> m.Reader:
|
|
52
|
+
"""The payload of a result the probe ran (success, failed or partial: all in the success shape); an unknown
|
|
53
|
+
resolution or outcome raises Failed (§0)."""
|
|
54
|
+
if not result.ran:
|
|
55
|
+
raise h.Failed(result)
|
|
56
|
+
return m.Reader(result.payload)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def check(what: str, result: m.Result, status: int, **kw) -> None:
|
|
60
|
+
"""Success needs outcome success AND status ok; anything else (an unknown status included) raises."""
|
|
61
|
+
if status != OK or not result.succeeded:
|
|
62
|
+
raise TargetError(what, status, result, **kw)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class Found:
|
|
67
|
+
kind: int
|
|
68
|
+
pins: tuple[int, int]
|
|
69
|
+
dmstatus: int
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class WireBase(Interface):
|
|
73
|
+
"""oep.wire.<link>: scan / attach / detach. The shared part; each link's attach takes its own arguments."""
|
|
74
|
+
SCAN, ATTACH, DETACH, ATTACH_UNDER_RESET = 0x01, 0x02, 0x03, 0x04
|
|
75
|
+
REVISION = 1
|
|
76
|
+
TAG_MAX_SPEED = 0x01
|
|
77
|
+
TAG_PINS = 0x03 # swdio(u16) swclk(u16, 0xFFFF on one wire), critical (oep-if-debug §1)
|
|
78
|
+
|
|
79
|
+
def scan(self, pairs: list[tuple[int, int]] | None = None) -> list[Found]:
|
|
80
|
+
"""Try `pairs` of (swdio, swclk); None = every pair the probe allows (describe's channel_group /
|
|
81
|
+
role_channels). A pair the probe does not allow refuses the whole scan (rejected unavailable). The probe stops
|
|
82
|
+
when its answer would not fit one frame and says how many pairs it tried; the rest go again (oep-if-debug §1)."""
|
|
83
|
+
pairs = list(pairs or [])
|
|
84
|
+
out = []
|
|
85
|
+
while True:
|
|
86
|
+
body = bytes([len(pairs)]) + b"".join(struct.pack("<HH", d, c) for d, c in pairs)
|
|
87
|
+
rd = m.Reader(self._call(self.SCAN, body).payload)
|
|
88
|
+
tried, count = rd.take("BB")
|
|
89
|
+
for _ in range(count):
|
|
90
|
+
kind, dio, clk, status = rd.take("BHHI")
|
|
91
|
+
out.append(Found(kind, (dio, clk), status))
|
|
92
|
+
rd.tail()
|
|
93
|
+
if not pairs or tried >= len(pairs) or tried == 0:
|
|
94
|
+
return out
|
|
95
|
+
pairs = pairs[tried:]
|
|
96
|
+
|
|
97
|
+
def detach(self, conn: int) -> None:
|
|
98
|
+
self._call(self.DETACH, struct.pack("<H", conn))
|
|
99
|
+
|
|
100
|
+
def _speed_tlv(self, max_speed: int | None) -> bytes:
|
|
101
|
+
# critical: a probe that cannot keep to a ceiling must refuse, not ignore it (§0: safety arguments)
|
|
102
|
+
return b"" if max_speed is None else m.tlv(self.TAG_MAX_SPEED, struct.pack("<I", max_speed), critical=True)
|
|
103
|
+
|
|
104
|
+
def _pins_tlv(self, pins: tuple[int, int] | None) -> bytes:
|
|
105
|
+
# the pair to attach on (a scan result's .pins); None: the probe's only pair
|
|
106
|
+
return b"" if pins is None else m.tlv(self.TAG_PINS, struct.pack("<HH", *pins), critical=True)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Wire(WireBase):
|
|
110
|
+
"""oep.wire.rvswd / oep.wire.swio (CH32 debug links to a RISC-V debug module)."""
|
|
111
|
+
NAME = "oep.wire.rvswd"
|
|
112
|
+
DEFAULT_RESET = 0xFFFF
|
|
113
|
+
RUN, HALT = reg.WIRE_RVSWD.enum["attach_method"]["run"], reg.WIRE_RVSWD.enum["attach_method"]["halt"]
|
|
114
|
+
TAG_TARGET_ID = reg.WIRE_RVSWD.tlv["attach_answer"]["target_id"]
|
|
115
|
+
SCHEME_WCH_DMI_7F = reg.WIRE_RVSWD.enum["target_id_scheme"]["wch_dmi_7f"]
|
|
116
|
+
|
|
117
|
+
def __init__(self, hst: h.Host, name: str = "oep.wire.rvswd"):
|
|
118
|
+
super().__init__(hst, name)
|
|
119
|
+
self.had_reset = self.existing = False
|
|
120
|
+
self.speed_hz = 0
|
|
121
|
+
self.ignored: list[int] = []
|
|
122
|
+
self.target_id: tuple[int, bytes] | None = None # (scheme, value) the last attach read, or None
|
|
123
|
+
|
|
124
|
+
def _take_target_id(self, tail: m.Tail) -> None:
|
|
125
|
+
v = tail.get(self.TAG_TARGET_ID)
|
|
126
|
+
self.target_id = (v[0], bytes(v[1:])) if v else None
|
|
127
|
+
|
|
128
|
+
def attach(self, halt: bool = True, max_speed: int | None = None, pins: tuple[int, int] | None = None) -> tuple[int, int]:
|
|
129
|
+
"""-> (connection, DMSTATUS). Attaching an attached wire returns its connection as it is (self.existing).
|
|
130
|
+
self.had_reset: a pending havereset was acknowledged first (a V00x's DMSTATUS halt / run bits stay frozen
|
|
131
|
+
until then); self.speed_hz: the speed the probe chose; max_speed: a ceiling the probe must keep (critical);
|
|
132
|
+
self.target_id: (scheme, value) of the target's identity when the probe could read one (oep-if-debug §1)."""
|
|
133
|
+
body = bytes([self.HALT if halt else self.RUN]) + self._speed_tlv(max_speed) + self._pins_tlv(pins)
|
|
134
|
+
rd = m.Reader(self._call(self.ATTACH, body).payload)
|
|
135
|
+
conn, status, flags, self.speed_hz = rd.take("HIBI")
|
|
136
|
+
self.had_reset, self.existing = bool(flags & 1), bool(flags & 2)
|
|
137
|
+
tail = rd.tail()
|
|
138
|
+
self.ignored = tail.ignored
|
|
139
|
+
self._take_target_id(tail)
|
|
140
|
+
return conn, status
|
|
141
|
+
|
|
142
|
+
def attach_under_reset(self, channel: int | None = None, hold_ms: int = 20,
|
|
143
|
+
max_speed: int | None = None, pins: tuple[int, int] | None = None) -> tuple[int, int]:
|
|
144
|
+
"""Hold the target in reset through `channel` (None: the probe's default reset line), attach, release and
|
|
145
|
+
halt it at once - the way back from firmware that turns the debug pins into GPIOs. -> (connection, dpc)"""
|
|
146
|
+
body = (struct.pack("<HH", self.DEFAULT_RESET if channel is None else channel, hold_ms) + self._speed_tlv(max_speed)
|
|
147
|
+
+ self._pins_tlv(pins))
|
|
148
|
+
rd = m.Reader(self._call(self.ATTACH_UNDER_RESET, body).payload)
|
|
149
|
+
conn, dpc, self.speed_hz = rd.take("HII")
|
|
150
|
+
self._take_target_id(rd.tail())
|
|
151
|
+
return conn, dpc
|
|
152
|
+
|
|
153
|
+
def find_reset_line(self, candidates: list[int], reset_vector: int = 0, hold_ms: int = 20,
|
|
154
|
+
tries: int = 3) -> list[int]:
|
|
155
|
+
"""Which of `candidates` resets the target: attach under reset through each, and see where the hart stops.
|
|
156
|
+
The real line stops it before its first instruction (dpc = reset_vector); any other channel leaves the
|
|
157
|
+
target running, so the halt lands somewhere in its code. A channel counts once any of `tries` lands on the
|
|
158
|
+
vector: the CH32L103 is caught by polling right after the release (it keeps no haltreq through NRST), which
|
|
159
|
+
misses now and then (1 in 60 after the probe fix of 2026-09-24), while landing on the vector by chance is
|
|
160
|
+
not a worry. Channels the probe does not allow (rejected) are skipped; a failed attach
|
|
161
|
+
counts as a miss and is tried again. Each try pulls one channel
|
|
162
|
+
low (open drain) for hold_ms. The target is left running (or halted, where resume is not acknowledged)."""
|
|
163
|
+
hits = []
|
|
164
|
+
self.last_search = {} # channel -> list of dpc values (None: attach failed), or the rejection
|
|
165
|
+
for channel in candidates:
|
|
166
|
+
seen = []
|
|
167
|
+
for _ in range(tries):
|
|
168
|
+
try:
|
|
169
|
+
conn, dpc = self.attach_under_reset(channel, hold_ms)
|
|
170
|
+
except h.Rejected as e: # not a channel this probe allows
|
|
171
|
+
seen = e
|
|
172
|
+
break
|
|
173
|
+
except h.Failed:
|
|
174
|
+
seen.append(None) # the attach itself failed: try again
|
|
175
|
+
continue
|
|
176
|
+
seen.append(dpc)
|
|
177
|
+
dm = RiscvDm(self.host, conn)
|
|
178
|
+
try:
|
|
179
|
+
if dpc == reset_vector:
|
|
180
|
+
# Leave the vector for real: a hart left halted there reads dpc = vector again through the
|
|
181
|
+
# next, wrong channel (2026-09-24: a CH32L103 whose resume was not acknowledged made the
|
|
182
|
+
# channel after NRST a false hit). A reset-and-run always gets it going.
|
|
183
|
+
dm.reset(confirm=True)
|
|
184
|
+
else:
|
|
185
|
+
dm.resume()
|
|
186
|
+
except h.OepError:
|
|
187
|
+
pass # a CH32L103 raises no allresumeack; a hart left halted mid-code still lands off the vector
|
|
188
|
+
finally:
|
|
189
|
+
self.detach(conn)
|
|
190
|
+
if dpc == reset_vector:
|
|
191
|
+
hits.append(channel)
|
|
192
|
+
break
|
|
193
|
+
self.last_search[channel] = seen
|
|
194
|
+
return hits
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class StepListError(TargetError):
|
|
198
|
+
"""A DMI step list that stopped early: `done` = the failed step's index, `values` = what it did read."""
|
|
199
|
+
|
|
200
|
+
def __init__(self, done: int, status: int, values: list[int], result: m.Result | None = None):
|
|
201
|
+
super().__init__("step list", status, result, done=done, values=values)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass
|
|
205
|
+
class RunResult:
|
|
206
|
+
status: int
|
|
207
|
+
stopped: bool # the hart halted on its own (ebreak) before timeout_ms
|
|
208
|
+
dpc: int
|
|
209
|
+
elapsed_us: int
|
|
210
|
+
values: list[int] = field(default_factory=list) # the registers asked for in `outs`, in order
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def count_steps(steps: bytes) -> list[int]:
|
|
214
|
+
"""The kinds of a packed step list, in order (so the count and the value rule need no bookkeeping by callers)."""
|
|
215
|
+
kinds, at = [], 0
|
|
216
|
+
while at < len(steps):
|
|
217
|
+
kind = steps[at]
|
|
218
|
+
if kind not in STEP_SIZES:
|
|
219
|
+
raise ValueError(f"DMI step kind 0x{kind:02x} at byte {at} is not one this client knows")
|
|
220
|
+
kinds.append(kind)
|
|
221
|
+
at += STEP_SIZES[kind]
|
|
222
|
+
if at != len(steps):
|
|
223
|
+
raise ValueError("the step list ends inside a step")
|
|
224
|
+
return kinds
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def dmi_value_count(kinds: list[int], done: int, status: int) -> int:
|
|
228
|
+
"""§5.5: the reads and polls among the first `done` steps, plus the failed step's last value when it is a poll
|
|
229
|
+
that timed out (a poll whose read failed on the line adds nothing)."""
|
|
230
|
+
n = sum(k in VALUE_STEPS for k in kinds[:done])
|
|
231
|
+
if status == TIMEOUT and done < len(kinds) and kinds[done] in POLL_STEPS:
|
|
232
|
+
n += 1
|
|
233
|
+
return n
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class RiscvDm(Interface):
|
|
237
|
+
"""oep.target.riscv-dm on one connection (every request starts with the connection, u16)."""
|
|
238
|
+
NAME = "oep.target.riscv-dm"
|
|
239
|
+
REVISION = 1
|
|
240
|
+
DMI, HALT, RESUME, RESET, READ_BLOCK, WRITE_BLOCK, RUN, STEP = (
|
|
241
|
+
_RV.op[k] for k in ("dmi", "halt", "resume", "reset", "read_block", "write_block", "run", "step"))
|
|
242
|
+
RESET_RUN, RESET_RUN_CONFIRM, RESET_HALT = (_RV.enum["reset_mode"][k] for k in ("run", "run_verified", "halt_at_reset"))
|
|
243
|
+
METHOD_DEFAULT, METHOD_NDMRESET, METHOD_SYSTEM = (_RV.enum["reset_method"][k]
|
|
244
|
+
for k in ("probe_default", "ndmreset", "system_reset"))
|
|
245
|
+
TAG_RESET_METHOD = _RV.tlv["reset"]["method"]
|
|
246
|
+
NO_TIMEOUT = 0xFFFFFFFF
|
|
247
|
+
|
|
248
|
+
def __init__(self, hst: h.Host, conn: int, name: str = "oep.target.riscv-dm"):
|
|
249
|
+
super().__init__(hst, name, prefix=struct.pack("<H", conn))
|
|
250
|
+
self.conn = conn
|
|
251
|
+
|
|
252
|
+
def _status_only(self, what: str, op: int) -> None:
|
|
253
|
+
r = self._request(op)
|
|
254
|
+
rd = ran(r)
|
|
255
|
+
status = rd.u8()
|
|
256
|
+
rd.tail()
|
|
257
|
+
check(what, r, status)
|
|
258
|
+
|
|
259
|
+
def halt(self) -> None:
|
|
260
|
+
"""Idempotent: an already halted hart is ok."""
|
|
261
|
+
self._status_only("halt", self.HALT)
|
|
262
|
+
|
|
263
|
+
def resume(self) -> None:
|
|
264
|
+
"""One resumereq; ok = the hart left debug mode (status state if the probe saw it not go). Parts that need more
|
|
265
|
+
(the CH32 rule) are the host's: ch32_flash.resume."""
|
|
266
|
+
self._status_only("resume", self.RESUME)
|
|
267
|
+
|
|
268
|
+
DPC = 0x07B1
|
|
269
|
+
|
|
270
|
+
def read_register(self, regno: int) -> int:
|
|
271
|
+
"""A GPR / CSR of the halted hart through an abstract command (access register, 32 bits) in plain DMI steps, so
|
|
272
|
+
any probe with dmi does it. A cmderr is cleared, then raised."""
|
|
273
|
+
_, values = self.dmi([self.step_write(0x17, 0x00220000 | regno), self.step_poll(0x16, 1 << 12, 0, 100),
|
|
274
|
+
self.step_read(0x04)])
|
|
275
|
+
cs, data0 = values[0], values[1]
|
|
276
|
+
if (cs >> 8) & 7:
|
|
277
|
+
self.dmi([self.step_write(0x16, 0x700)])
|
|
278
|
+
raise RuntimeError(f"abstract command for register {regno:#x} failed (cmderr {(cs >> 8) & 7})")
|
|
279
|
+
return data0
|
|
280
|
+
|
|
281
|
+
def _reset(self, mode: int, method: int | None) -> tuple[int, int, int]:
|
|
282
|
+
body = bytes([mode])
|
|
283
|
+
if method is not None:
|
|
284
|
+
body += m.tlv(self.TAG_RESET_METHOD, bytes([method]), critical=True)
|
|
285
|
+
r = self._request(self.RESET, body)
|
|
286
|
+
rd = ran(r)
|
|
287
|
+
status, flags, attempts, pc = rd.take("BBBI")
|
|
288
|
+
rd.tail()
|
|
289
|
+
check("reset", r, status)
|
|
290
|
+
return flags, attempts, pc
|
|
291
|
+
|
|
292
|
+
def reset(self, confirm: bool = True, method: int | None = None) -> tuple[int, int, int]:
|
|
293
|
+
"""Reset and let it run (confirm: seen running). method: METHOD_* (critical; None: the probe chooses).
|
|
294
|
+
-> (flags, attempts, pc)"""
|
|
295
|
+
return self._reset(self.RESET_RUN_CONFIRM if confirm else self.RESET_RUN, method)
|
|
296
|
+
|
|
297
|
+
def reset_halt(self, method: int | None = None) -> int:
|
|
298
|
+
"""Reset and stop before the first instruction (haltreq held through the reset). -> dpc"""
|
|
299
|
+
return self._reset(self.RESET_HALT, method)[2]
|
|
300
|
+
|
|
301
|
+
def step(self) -> tuple[bool, int, int]:
|
|
302
|
+
"""One instruction (dcsr.step, one resume, privilege kept). -> (moved, dpc before, dpc after)"""
|
|
303
|
+
r = self._request(self.STEP)
|
|
304
|
+
rd = ran(r)
|
|
305
|
+
status, moved, before, after = rd.take("BBII")
|
|
306
|
+
rd.tail()
|
|
307
|
+
check("step", r, status)
|
|
308
|
+
return bool(moved), before, after
|
|
309
|
+
|
|
310
|
+
def read_block(self, address: int, count: int) -> bytes:
|
|
311
|
+
"""`count` words from `address`. A read that stopped raises TargetError (.data = the words it did read)."""
|
|
312
|
+
r = self._request(self.READ_BLOCK, struct.pack("<IH", address, count))
|
|
313
|
+
data, done, status = self.read_block_result(r)
|
|
314
|
+
if status != OK or not r.succeeded or done != count:
|
|
315
|
+
raise TargetError("read_block", status, r, done=done, data=data)
|
|
316
|
+
return data
|
|
317
|
+
|
|
318
|
+
@staticmethod
|
|
319
|
+
def read_block_result(r: m.Result) -> tuple[bytes, int, int]:
|
|
320
|
+
"""-> (the words read as bytes, done, status)."""
|
|
321
|
+
rd = ran(r)
|
|
322
|
+
done, status = rd.take("HB")
|
|
323
|
+
data = rd.bytes(4 * done)
|
|
324
|
+
rd.tail()
|
|
325
|
+
return data, done, status
|
|
326
|
+
|
|
327
|
+
@staticmethod
|
|
328
|
+
def write_block_body(address: int, data: bytes) -> bytes:
|
|
329
|
+
if len(data) % 4:
|
|
330
|
+
raise ValueError("write_block writes whole words")
|
|
331
|
+
return struct.pack("<IH", address, len(data) // 4) + data
|
|
332
|
+
|
|
333
|
+
def write_block(self, address: int, data: bytes) -> None:
|
|
334
|
+
r = self._request(self.WRITE_BLOCK, self.write_block_body(address, data))
|
|
335
|
+
rd = ran(r)
|
|
336
|
+
done, status = rd.take("HB")
|
|
337
|
+
rd.tail()
|
|
338
|
+
check("write_block", r, status, done=done)
|
|
339
|
+
|
|
340
|
+
def write32(self, address: int, value: int) -> None:
|
|
341
|
+
self.write_block(address, struct.pack("<I", value))
|
|
342
|
+
|
|
343
|
+
def read32(self, address: int) -> int:
|
|
344
|
+
return struct.unpack("<I", self.read_block(address, 1))[0]
|
|
345
|
+
|
|
346
|
+
@classmethod
|
|
347
|
+
def run_body(cls, pc: int, regs: list[tuple[int, int]], timeout_ms: int | None = 200,
|
|
348
|
+
outs: tuple[int, ...] = (REG_A0,)) -> bytes:
|
|
349
|
+
"""pc, timeout_ms (None: no limit), the registers to set, then the registers to read back (`outs`)."""
|
|
350
|
+
t = cls.NO_TIMEOUT if timeout_ms is None else timeout_ms
|
|
351
|
+
return (struct.pack("<IIB", pc, t, len(regs)) + b"".join(struct.pack("<HI", r, v) for r, v in regs)
|
|
352
|
+
+ struct.pack("<B", len(outs)) + b"".join(struct.pack("<H", r) for r in outs))
|
|
353
|
+
|
|
354
|
+
@staticmethod
|
|
355
|
+
def run_result(result: m.Result, n_out: int = 1) -> RunResult:
|
|
356
|
+
"""Decode a run result (any known outcome) without judging it."""
|
|
357
|
+
rd = ran(result)
|
|
358
|
+
status, stopped, dpc, us = rd.take("BBII")
|
|
359
|
+
values = rd.words(n_out)
|
|
360
|
+
rd.tail()
|
|
361
|
+
return RunResult(status, bool(stopped), dpc, us, values)
|
|
362
|
+
|
|
363
|
+
def run(self, pc: int, regs: list[tuple[int, int]], timeout_ms: int | None = 200,
|
|
364
|
+
outs: tuple[int, ...] = (REG_A0,)) -> RunResult:
|
|
365
|
+
"""Set registers and dpc (dcsr.ebreakm, prv = M), resume, wait for the hart's own ebreak (forced halt at the
|
|
366
|
+
timeout: stopped False, status timeout - returned, not raised). Other statuses raise TargetError."""
|
|
367
|
+
r = self._request(self.RUN, self.run_body(pc, regs, timeout_ms, outs))
|
|
368
|
+
res = self.run_result(r, len(outs))
|
|
369
|
+
if res.status == STATUS["timeout"] and not res.stopped:
|
|
370
|
+
return res
|
|
371
|
+
check("run", r, res.status)
|
|
372
|
+
return res
|
|
373
|
+
|
|
374
|
+
def dmi(self, steps: bytes | list[bytes]) -> tuple[int, list[int]]:
|
|
375
|
+
"""Run a step list (the step_* builders, concatenated or as a list). -> (steps done, values: one per read and
|
|
376
|
+
poll step). A list that stopped early raises StepListError (with what it did read): a caller cannot mistake
|
|
377
|
+
an unfinished poll for a met one."""
|
|
378
|
+
raw = b"".join(steps) if isinstance(steps, list) else steps
|
|
379
|
+
kinds = count_steps(raw)
|
|
380
|
+
r = self._request(self.DMI, struct.pack("<H", len(kinds)) + raw)
|
|
381
|
+
rd = ran(r)
|
|
382
|
+
done, status = rd.take("HB")
|
|
383
|
+
values = rd.words(dmi_value_count(kinds, done, status))
|
|
384
|
+
rd.tail()
|
|
385
|
+
if status != OK or not r.succeeded or done != len(kinds):
|
|
386
|
+
raise StepListError(done, status, values, r)
|
|
387
|
+
return done, values
|
|
388
|
+
|
|
389
|
+
@staticmethod
|
|
390
|
+
def step_write(address: int, value: int) -> bytes:
|
|
391
|
+
return struct.pack("<BBI", STEP_WRITE, address, value)
|
|
392
|
+
|
|
393
|
+
@staticmethod
|
|
394
|
+
def step_read(address: int) -> bytes:
|
|
395
|
+
return struct.pack("<BB", STEP_READ, address)
|
|
396
|
+
|
|
397
|
+
@staticmethod
|
|
398
|
+
def step_poll(address: int, mask: int, value: int, max_reads: int) -> bytes:
|
|
399
|
+
"""Read until (value & mask) == value, at most max_reads times; adds the last value read (met or not)."""
|
|
400
|
+
return struct.pack("<BBIIH", STEP_POLL_READS, address, mask, value, max_reads)
|
|
401
|
+
|
|
402
|
+
@staticmethod
|
|
403
|
+
def step_delay(us: int) -> bytes:
|
|
404
|
+
return struct.pack("<BI", STEP_WAIT_US, us)
|
|
405
|
+
|
|
406
|
+
@staticmethod
|
|
407
|
+
def step_poll_time(address: int, mask: int, value: int, max_us: int) -> bytes:
|
|
408
|
+
"""poll bounded by time rather than reads: the same meaning on a slow bit-banged link and a fast one."""
|
|
409
|
+
return struct.pack("<BBIII", STEP_POLL_US, address, mask, value, max_us)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def attach_after_gpio_reset(hst: h.Host, wire_: Wire, gpio_fn: int, channel: int, exchange=None, *, tries: int = 10,
|
|
413
|
+
low_s: float = 0.02) -> tuple[int, int]:
|
|
414
|
+
"""For a probe without attach_under_reset: pull `channel` low through oep.fixture.gpio, then send its release
|
|
415
|
+
and an attach (halt) in one exchange so the probe starts the attach right after the release, and retry - a race
|
|
416
|
+
at the edge of the target's reset window (2026-09-24, CH32V003 with SWIO turned off: 2 of 5 pipelined, 0 of 5
|
|
417
|
+
one request at a time). `exchange` is the link's pipelining exchange (default: the host's). -> (connection, DMSTATUS)"""
|
|
418
|
+
gpio = Gpio(hst, gpio_fn)
|
|
419
|
+
last = None
|
|
420
|
+
for _ in range(tries):
|
|
421
|
+
gpio.pull_low(channel)
|
|
422
|
+
time.sleep(low_s)
|
|
423
|
+
release, last = hst.pipeline([gpio.request_release(channel), wire_.request(Wire.ATTACH, bytes([Wire.HALT]))],
|
|
424
|
+
exchange=exchange)
|
|
425
|
+
if not release.succeeded:
|
|
426
|
+
raise h.Failed(release) # never leave the reset line held
|
|
427
|
+
if last.succeeded:
|
|
428
|
+
conn, status = m.Reader(last.payload).take("HI")
|
|
429
|
+
return conn, status
|
|
430
|
+
raise h.Failed(last) if last is not None else ValueError("tries must be at least 1")
|
oep_client/v1/rp2350.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""RP2350 target knowledge for the host: its boot ROM's function table and the ROM-driven flash sequence (the
|
|
2
|
+
pico-sdk's hardware_flash order), run on the halted core through CortexM.call. The probe knows none of this.
|
|
3
|
+
|
|
4
|
+
Measured 2026-09-24 through an RP2040-Zero OEP probe (bit-bang SWD, 500 ns half period) on a Pro Micro RP2350:
|
|
5
|
+
98 KiB erased in 0.3 s, programmed in 3.0 s, verified (fast-XIP read) in 2.0 s; ROM reboot re-enumerates the USB.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import struct
|
|
11
|
+
import time
|
|
12
|
+
|
|
13
|
+
from .arm import CortexM
|
|
14
|
+
|
|
15
|
+
FLASH_XIP = 0x10000000
|
|
16
|
+
SECTOR, BLOCK, BLOCK_ERASE_CMD, PAGE = 4096, 65536, 0xD8, 256
|
|
17
|
+
RT_FLAG_FUNC_ARM_SEC = 0x0004
|
|
18
|
+
TABLE_LOOKUP_PTR = 0x16 # halfword in the ROM: rom_table_lookup(code, mask) (Arm, RP2350)
|
|
19
|
+
# RAM the target can spare while the host drives it (main SRAM ends at 0x20082000): the return breakpoint, the data
|
|
20
|
+
# buffer for programming, and the stack the ROM functions run on. Whatever ran before is restarted afterwards.
|
|
21
|
+
BKPT_AT, BUF, BUF_BYTES, STACK_TOP = 0x20040000, 0x20041000, 16 * 1024, 0x20080000
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def code(a: str, b: str) -> int:
|
|
25
|
+
return ord(a) | ord(b) << 8
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Rom:
|
|
29
|
+
def __init__(self, core: CortexM):
|
|
30
|
+
self.core = core
|
|
31
|
+
self._fn: dict[str, int] = {}
|
|
32
|
+
|
|
33
|
+
def lookup(self, c: str) -> int:
|
|
34
|
+
if c not in self._fn:
|
|
35
|
+
word = self.core.mem.read32(TABLE_LOOKUP_PTR & ~3)
|
|
36
|
+
lookup = (word >> (8 * (TABLE_LOOKUP_PTR & 3))) & 0xFFFF
|
|
37
|
+
addr = self.core.call(lookup, (code(*c), RT_FLAG_FUNC_ARM_SEC))
|
|
38
|
+
if not 0 < addr < 0x8000:
|
|
39
|
+
raise LookupError(f"ROM has no function {c!r} (lookup returned {addr:#x})")
|
|
40
|
+
self._fn[c] = addr
|
|
41
|
+
return self._fn[c]
|
|
42
|
+
|
|
43
|
+
def reboot(self, delay_ms: int = 10) -> None:
|
|
44
|
+
"""reboot(flags=0: normal boot, delay_ms, 0, 0): a whole-chip reboot, so the USB device re-enumerates. The core
|
|
45
|
+
is let go into it with interrupts enabled (its handlers are in flash, readable again by now)."""
|
|
46
|
+
self.core.prepare_call(self.lookup("RB"), (0, delay_ms, 0, 0))
|
|
47
|
+
self.core.release()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class Flash:
|
|
51
|
+
def __init__(self, core: CortexM):
|
|
52
|
+
self.core, self.rom = core, Rom(core)
|
|
53
|
+
|
|
54
|
+
def program(self, offset: int, data: bytes, log=lambda s: None) -> bytes:
|
|
55
|
+
"""Erase and program at flash `offset` (sector aligned). Leaves the flash in command-XIP mode: reads through
|
|
56
|
+
0x10000000+ work (slowly) for verification; a reboot restores the fast mode. Returns the page-padded image."""
|
|
57
|
+
if offset % SECTOR:
|
|
58
|
+
raise ValueError("offset must be a multiple of the 4 KiB sector")
|
|
59
|
+
data = data + b"\xff" * (-len(data) % PAGE)
|
|
60
|
+
erase = len(data) + (-len(data) % SECTOR)
|
|
61
|
+
fn = {c: self.rom.lookup(c) for c in ("IF", "EX", "RE", "RP", "FC", "CX")}
|
|
62
|
+
t0 = time.monotonic()
|
|
63
|
+
self.core.call(fn["IF"]) # connect_internal_flash
|
|
64
|
+
self.core.call(fn["EX"]) # flash_exit_xip
|
|
65
|
+
self.core.call(fn["RE"], (offset, erase, BLOCK, BLOCK_ERASE_CMD), timeout=120) # flash_range_erase
|
|
66
|
+
t1 = time.monotonic()
|
|
67
|
+
for at in range(0, len(data), BUF_BYTES):
|
|
68
|
+
chunk = data[at:at + BUF_BYTES]
|
|
69
|
+
self.core.mem.write_block(BUF, list(struct.unpack(f"<{len(chunk) // 4}I", chunk)))
|
|
70
|
+
self.core.call(fn["RP"], (offset + at, BUF, len(chunk)), timeout=30) # flash_range_program
|
|
71
|
+
t2 = time.monotonic()
|
|
72
|
+
self.core.call(fn["FC"]) # flash_flush_cache
|
|
73
|
+
self.core.call(fn["CX"]) # flash_enter_cmd_xip
|
|
74
|
+
log(f"erased {erase} B in {t1 - t0:.2f} s, programmed {len(data)} B in {t2 - t1:.2f} s")
|
|
75
|
+
return data
|
|
76
|
+
|
|
77
|
+
def read(self, offset: int, length: int) -> bytes:
|
|
78
|
+
words = self.core.mem.read_block(FLASH_XIP + offset, (length + 3) // 4)
|
|
79
|
+
return struct.pack(f"<{len(words)}I", *words)[:length]
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def program_and_verify(core: CortexM, image: bytes, offset: int = 0, log=lambda s: None) -> bool:
|
|
83
|
+
flash = Flash(core)
|
|
84
|
+
padded = flash.program(offset, image, log)
|
|
85
|
+
return flash.read(offset, len(padded)) == padded
|
oep_client/v1/target.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""The v1 client API in one place, for callers written against the first draft (oep_smoke, the experiments).
|
|
2
|
+
|
|
3
|
+
New code can import the modules directly: core (finding interfaces, the plan), riscv (oep.wire.rvswd / swio,
|
|
4
|
+
oep.target.riscv-dm), console (oep.target.console), fixture (gpio / uart), capture (oep.fixture.capture), arm
|
|
5
|
+
(oep.wire.swd, oep.target.arm-adi).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .capture import LogicCapture # noqa: F401
|
|
9
|
+
from .console import MARK_NAMES, Console, ConsoleIO, Mark, StreamIO # noqa: F401
|
|
10
|
+
from .core import (OP_PLAN_APPLY, OP_PLAN_RELEASE, Interface, UnsupportedRevision, confirm, find, # noqa: F401
|
|
11
|
+
find_all, plan_apply, plan_release, probe_labels)
|
|
12
|
+
from .fixture import FixtureUart, FixtureUartIO, Gpio # noqa: F401
|
|
13
|
+
from .host import Failed, NoConnection, NotV1, OepError, Rejected, Unsupported # noqa: F401
|
|
14
|
+
from .riscv import (Found, RiscvDm, RunResult, StepListError, TargetError, Wire, WireBase, # noqa: F401
|
|
15
|
+
attach_after_gpio_reset, ran)
|
|
16
|
+
from . import host as h # noqa: F401 (callers use target.h.Rejected)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""UIAPduino (CH32V003) bootloader entry and return to user mode, from the host with v1 draft parts only.
|
|
2
|
+
|
|
3
|
+
The knowledge that used to sit in the v0 probe (target.control reset modes 1 and 2) lives here now: two RAM
|
|
4
|
+
payloads (wch-protocols E129 / E130, assembled for the V003) that the V003's own CPU runs, because the same
|
|
5
|
+
registers written from a halted debug session do not take (E127). The probe only moves a gpio line, writes RAM and
|
|
6
|
+
walks DMI steps (oep-spec capability-name-hierarchy.ja.md: NRST is a labelled gpio channel, not a capability).
|
|
7
|
+
|
|
8
|
+
enter_bootloader: the bootloader stays only when RCC_RSTSCKR.PINRSTF is set (E161). Read it first: if a pin
|
|
9
|
+
reset's flag is still there (nobody wrote RMVF since), go in over SWIO alone; otherwise pulse
|
|
10
|
+
NRST when the jig has it wired, or say what is needed. Then attach halted, place PREPARE_BOOT and
|
|
11
|
+
resume into it with mstatus = 0 -> HID 1209:b803 in about a second.
|
|
12
|
+
Only an external NRST pulse sets PINRSTF on the V003: not the IWDG, the WWDG, a software reset,
|
|
13
|
+
nor the target driving its own PD7 low while the reset function owns the pad (measured
|
|
14
|
+
2026-09-24, oep-spec experiments/v003-reset-flags); power-on leaves it 0 per the RM.
|
|
15
|
+
normalize_user: the same with NORMALIZE_USER (clears BOOT_MODE) and no pin pulse
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import struct
|
|
21
|
+
import time
|
|
22
|
+
|
|
23
|
+
from . import host as h, target
|
|
24
|
+
from .fixture import Gpio
|
|
25
|
+
|
|
26
|
+
GPIO_OPEN_DRAIN_LOW, GPIO_OPEN_DRAIN_RELEASE = Gpio.OPEN_DRAIN_LOW, Gpio.OPEN_DRAIN_RELEASE # for older scripts
|
|
27
|
+
|
|
28
|
+
PAYLOAD_BASE = 0x20000000
|
|
29
|
+
RSTSCKR, PINRSTF = 0x40021024, 1 << 26
|
|
30
|
+
DMCONTROL, ABSTRACTCS, COMMAND, DATA0 = 0x10, 0x16, 0x17, 0x04
|
|
31
|
+
MSTATUS, DPC = 0x0300, 0x07B1
|
|
32
|
+
|
|
33
|
+
# kNormalizeUserReset: unlock FLASH, clear BOOT_MODE, PFIC SYSRST
|
|
34
|
+
NORMALIZE_USER = [
|
|
35
|
+
0x400222b7, 0x00428293, 0x45670337, 0x12330313, 0x0062a023, 0xcdef9337,
|
|
36
|
+
0x9ab30313, 0x0062a023, 0x400222b7, 0x02428293, 0x45670337, 0x12330313,
|
|
37
|
+
0x0062a023, 0xcdef9337, 0x9ab30313, 0x0062a023, 0x400222b7, 0x02828293,
|
|
38
|
+
0x45670337, 0x12330313, 0x0062a023, 0xcdef9337, 0x9ab30313, 0x0062a023,
|
|
39
|
+
0x400222b7, 0x00c28293, 0x0002a303, 0xffffc3b7, 0xfff38393, 0x00737333,
|
|
40
|
+
0x0062a023, 0xe000e2b7, 0x04828293, 0xbeef0337, 0x08030313, 0x0062a023,
|
|
41
|
+
0x0000006f,
|
|
42
|
+
]
|
|
43
|
+
# kPrepareBootAndReset: unlock, set BOOT_MODE, PD4 (software USB D-) low for a detach window, PFIC SYSRST
|
|
44
|
+
PREPARE_BOOT = [
|
|
45
|
+
0x400222b7, 0x00428293, 0x45670337, 0x12330313, 0x0062a023, 0xcdef9337,
|
|
46
|
+
0x9ab30313, 0x0062a023, 0x400222b7, 0x02428293, 0x45670337, 0x12330313,
|
|
47
|
+
0x0062a023, 0xcdef9337, 0x9ab30313, 0x0062a023, 0x400222b7, 0x02828293,
|
|
48
|
+
0x45670337, 0x12330313, 0x0062a023, 0xcdef9337, 0x9ab30313, 0x0062a023,
|
|
49
|
+
0x400222b7, 0x00c28293, 0x0002a303, 0xffffc3b7, 0xfff38393, 0x00737333,
|
|
50
|
+
0x000043b7, 0x00736333, 0x0062a023, 0x400212b7, 0x01828293, 0x0002a303,
|
|
51
|
+
0x02036313, 0x0062a023, 0x400112b7, 0x40028293, 0x0002a303, 0xfff103b7,
|
|
52
|
+
0xfff38393, 0x00737333, 0x000303b7, 0x00736333, 0x0062a023, 0x400112b7,
|
|
53
|
+
0x41428293, 0x01000313, 0x0062a023, 0x004c52b7, 0xb4028293, 0xfff28293,
|
|
54
|
+
0xfe029ee3, 0xe000e2b7, 0x04828293, 0xbeef0337, 0x08030313, 0x0062a023,
|
|
55
|
+
0x0000006f,
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _write_register(dm: target.RiscvDm, regno: int, value: int) -> bytes:
|
|
60
|
+
"""DMI steps of one abstract-command register write (aarsize 32, transfer, write), then wait for it."""
|
|
61
|
+
return (dm.step_write(DATA0, value) + dm.step_write(COMMAND, 0x00230000 | regno)
|
|
62
|
+
+ dm.step_poll(ABSTRACTCS, 1 << 12, 0, 100))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_payload(hst: h.Host, wire: target.Wire, payload: list[int]) -> None:
|
|
66
|
+
"""Attach halted, place the payload, resume into it with interrupts off, and let go of the target."""
|
|
67
|
+
conn, _ = wire.attach(halt=True)
|
|
68
|
+
try:
|
|
69
|
+
dm = target.RiscvDm(hst, conn)
|
|
70
|
+
data = struct.pack(f"<{len(payload)}I", *payload)
|
|
71
|
+
dm.write_block(PAYLOAD_BASE, data)
|
|
72
|
+
if dm.read_block(PAYLOAD_BASE, len(payload)) != data:
|
|
73
|
+
raise RuntimeError("payload did not read back")
|
|
74
|
+
# mstatus = 0 first: with MIE set the halted application's SysTick ran over the payload (2026-09-22).
|
|
75
|
+
# resumereq twice, then drop haltreq so the payload's own system reset is not halted again (E129).
|
|
76
|
+
# dmi() raises if an abstract-command poll gave up, so a register write that did not land stops here.
|
|
77
|
+
steps = (_write_register(dm, MSTATUS, 0) + _write_register(dm, DPC, PAYLOAD_BASE)
|
|
78
|
+
+ dm.step_write(DMCONTROL, 0x40000001) + dm.step_write(DMCONTROL, 0x40000001)
|
|
79
|
+
+ dm.step_write(DMCONTROL, 0x00000001))
|
|
80
|
+
dm.dmi(steps)
|
|
81
|
+
time.sleep(0.02)
|
|
82
|
+
finally:
|
|
83
|
+
wire.detach(conn)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def pulse_nrst(hst: h.Host, gpio_fn: int, channel: int, low_s: float = 0.02) -> None:
|
|
87
|
+
"""Open-drain low, then released to Hi-Z (never driven high). This drops any debug connection."""
|
|
88
|
+
Gpio(hst, gpio_fn).pulse_low(channel, low_s)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class NeedsPinReset(RuntimeError):
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def pin_reset_flag(hst: h.Host, wire: target.Wire) -> bool:
|
|
96
|
+
conn, _ = wire.attach(halt=True)
|
|
97
|
+
dm = target.RiscvDm(hst, conn)
|
|
98
|
+
try:
|
|
99
|
+
return bool(dm.read32(RSTSCKR) & PINRSTF)
|
|
100
|
+
finally:
|
|
101
|
+
dm.resume()
|
|
102
|
+
wire.detach(conn)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def enter_bootloader(hst: h.Host, wire: target.Wire, gpio_fn: int | None = None,
|
|
106
|
+
nrst_channel: int | None = None) -> str:
|
|
107
|
+
"""-> "swio" (a pin reset's flag was still set) or "nrst" (pulsed). Raises NeedsPinReset otherwise."""
|
|
108
|
+
how = "swio"
|
|
109
|
+
if not pin_reset_flag(hst, wire):
|
|
110
|
+
if gpio_fn is None or nrst_channel is None:
|
|
111
|
+
raise NeedsPinReset("PINRSTF is clear and no NRST line was given: press the board's reset or "
|
|
112
|
+
"power-cycle it (without the sketch clearing the flags), then try again")
|
|
113
|
+
pulse_nrst(hst, gpio_fn, nrst_channel)
|
|
114
|
+
time.sleep(0.3)
|
|
115
|
+
how = "nrst"
|
|
116
|
+
run_payload(hst, wire, PREPARE_BOOT)
|
|
117
|
+
return how
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def normalize_user(hst: h.Host, wire: target.Wire) -> None:
|
|
121
|
+
run_payload(hst, wire, NORMALIZE_USER)
|