oep-client-python 0.0.26__py3-none-any.whl → 0.0.27__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 +1 -1
- oep_client/__main__.py +90 -10
- oep_client/arm.py +7 -6
- oep_client/ch32_flash.py +6 -7
- oep_client/cobs.py +5 -0
- oep_client/endpoint.py +41 -20
- oep_client/fake.py +14 -4
- oep_client/link.py +508 -226
- oep_client/linktest.py +184 -0
- oep_client/registry.py +1 -1
- oep_client/riscv.py +45 -2
- oep_client/speed_record.py +104 -0
- {oep_client_python-0.0.26.dist-info → oep_client_python-0.0.27.dist-info}/METADATA +34 -24
- {oep_client_python-0.0.26.dist-info → oep_client_python-0.0.27.dist-info}/RECORD +17 -15
- {oep_client_python-0.0.26.dist-info → oep_client_python-0.0.27.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.26.dist-info → oep_client_python-0.0.27.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.26.dist-info → oep_client_python-0.0.27.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
oep_client/__main__.py
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
oep config uart <probe> oep.fixture.uart#1 115200 --format 8N1
|
|
10
10
|
oep config disable <probe> 3 4 (channels the probe never uses or touches; remove disable CH re-enables)
|
|
11
11
|
oep config remove <probe> bind 1 oep config save <probe> oep config erase <probe>
|
|
12
|
-
oep speed <probe>
|
|
12
|
+
oep speed <probe> [--candidates 921600,500000] [--verify [--flows in:2,out:2]] (port_speed, core §3.5 and the host
|
|
13
|
+
guide §7: try the candidates in order on a UART bridge, report)
|
|
14
|
+
oep linktest <probe> --rates now,921600 --patterns in,out,duplex --inflight 1,2 --sizes 128,496 --frames 300
|
|
13
15
|
|
|
14
16
|
<probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
|
|
15
17
|
ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
|
|
@@ -30,14 +32,37 @@ def main(argv=None) -> int:
|
|
|
30
32
|
parser = argparse.ArgumentParser(prog="oep", description="Open Embedded Probe: what a probe offers, its settings")
|
|
31
33
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
32
34
|
_config_parser(sub)
|
|
33
|
-
sp = sub.add_parser("speed", help="port_speed (core §3.5): try faster rates on a UART bridge,
|
|
35
|
+
sp = sub.add_parser("speed", help="port_speed (core §3.5, host guide §7): try faster rates on a UART bridge, "
|
|
36
|
+
"print the report")
|
|
34
37
|
sp.add_argument("probe", help="the probe's serial port (a UART bridge this host opens)")
|
|
35
|
-
sp.add_argument("rates", help="comma-separated, in order of preference
|
|
38
|
+
sp.add_argument("rates", nargs="?", default="", help="the candidates, comma-separated, in order of preference "
|
|
39
|
+
"(the first that passes is kept; default 500000). Same as --candidates")
|
|
40
|
+
sp.add_argument("--candidates", default="", help="the candidates, comma-separated (default 500000)")
|
|
41
|
+
sp.add_argument("--flows", default="", help="the flows to verify, comma-separated FLOW[:N] (in, out, duplex; N in "
|
|
42
|
+
"flight, 0 = the most the link keeps; default: all three at that N). Selects --verify")
|
|
43
|
+
form = sp.add_mutually_exclusive_group()
|
|
44
|
+
form.add_argument("--verify", action="store_true", help="the full form (guide §7.3): baseline, the flows measured "
|
|
45
|
+
"at every candidate, a failed flow run again one at a time")
|
|
46
|
+
form.add_argument("--minimal", action="store_true", help="the minimal form (guide §7.2, the default): try, confirm, "
|
|
47
|
+
"commit, no measurement")
|
|
48
|
+
sp.add_argument("--frames", type=int, default=link.FLOW_FRAMES, help="frames per flow when verifying (default 16)")
|
|
49
|
+
sp.add_argument("--no-record", action="store_true", help="do not read or write the record of passed / failed rates "
|
|
50
|
+
"(~/.cache/oep-client/link-speed.json, 30 days; on by default here, off in the library)")
|
|
36
51
|
sp.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed (default 115200)")
|
|
37
|
-
sp.add_argument("--verify-bytes", type=int, default=32768)
|
|
38
|
-
sp.add_argument("--verify-s", type=float, default=1.0)
|
|
39
|
-
sp.add_argument("--duplex-s", type=float, default=1.0, help="both ways at once for this long (default 1)")
|
|
40
52
|
sp.add_argument("--json", action="store_true")
|
|
53
|
+
lt = sub.add_parser("linktest", help="measure the link: traffic patterns at rates the host asks for, what breaks")
|
|
54
|
+
lt.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID]")
|
|
55
|
+
lt.add_argument("--rates", default="", help="comma-separated rates to switch to with port_speed and measure at "
|
|
56
|
+
"(each on its own; 0 or 'now' = the speed in force). Default: only the speed in force")
|
|
57
|
+
lt.add_argument("--patterns", default="in,out,duplex", help="in (probe->host), out (host->probe), duplex")
|
|
58
|
+
lt.add_argument("--inflight", default="1", help="comma-separated in-flight counts (above the probe's max: skipped)")
|
|
59
|
+
lt.add_argument("--sizes", default="", help="comma-separated frame payload sizes (default: a whole frame)")
|
|
60
|
+
lt.add_argument("--frames", type=int, default=300, help="frames per cell (default 300)")
|
|
61
|
+
lt.add_argument("--seconds", type=float, default=None, help="per cell for this long instead of --frames")
|
|
62
|
+
lt.add_argument("--timeout", type=float, default=0.3, help="seconds to wait for one answer (default 0.3)")
|
|
63
|
+
lt.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed to open at (default 115200)")
|
|
64
|
+
lt.add_argument("--low-latency", choices=("on", "off"), default="on", help="the serial driver's low-latency mode")
|
|
65
|
+
lt.add_argument("--json", action="store_true", help="one JSON object per cell")
|
|
41
66
|
d = sub.add_parser("dump", help="list and describe every interface a probe offers")
|
|
42
67
|
src = d.add_mutually_exclusive_group(required=True)
|
|
43
68
|
src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
|
|
@@ -50,6 +75,8 @@ def main(argv=None) -> int:
|
|
|
50
75
|
return _config(args)
|
|
51
76
|
if args.command == "speed":
|
|
52
77
|
return _speed(args)
|
|
78
|
+
if args.command == "linktest":
|
|
79
|
+
return _linktest(args)
|
|
53
80
|
|
|
54
81
|
if args.fake:
|
|
55
82
|
call = fake.PROFILES[args.fake]().call
|
|
@@ -61,17 +88,70 @@ def main(argv=None) -> int:
|
|
|
61
88
|
return 0
|
|
62
89
|
|
|
63
90
|
|
|
91
|
+
# ---- oep linktest -------------------------------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def _linktest(args) -> int:
|
|
94
|
+
"""Every parameter on the command line: no fixed numbers in the measurement."""
|
|
95
|
+
import dataclasses
|
|
96
|
+
import json
|
|
97
|
+
from . import linktest
|
|
98
|
+
ints = lambda text: [int(v) for v in text.split(",") if v.strip()] # noqa: E731
|
|
99
|
+
rates = [None if v.strip() in ("0", "now") else int(v) for v in args.rates.split(",") if v.strip()] or [None]
|
|
100
|
+
hst = link.open_host(args.probe, baud=args.baud) if args.probe.startswith("/") or ":" not in args.probe \
|
|
101
|
+
else link.open_host(args.probe)
|
|
102
|
+
stream = getattr(hst.link, "stream", None)
|
|
103
|
+
if hasattr(stream, "set_low_latency_mode"):
|
|
104
|
+
try:
|
|
105
|
+
stream.set_low_latency_mode(args.low_latency == "on")
|
|
106
|
+
except (OSError, ValueError, NotImplementedError):
|
|
107
|
+
pass
|
|
108
|
+
try:
|
|
109
|
+
core.take(hst, 30000, owner="oep linktest")
|
|
110
|
+
for result in linktest.matrix(hst, rates=rates, patterns=[p for p in args.patterns.split(",") if p],
|
|
111
|
+
inflight=ints(args.inflight), sizes=ints(args.sizes) or None,
|
|
112
|
+
frames=args.frames, seconds=args.seconds, timeout=args.timeout):
|
|
113
|
+
if args.json:
|
|
114
|
+
for c in result.cells:
|
|
115
|
+
print(json.dumps({**dataclasses.asdict(c), "actual": result.actual, "low_latency": args.low_latency}), flush=True)
|
|
116
|
+
if result.why:
|
|
117
|
+
print(json.dumps({"rate": result.rate, "why": result.why}), flush=True)
|
|
118
|
+
else:
|
|
119
|
+
print(result.text(), flush=True)
|
|
120
|
+
try:
|
|
121
|
+
hst.end()
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
finally:
|
|
125
|
+
hst.link.close()
|
|
126
|
+
return 0
|
|
127
|
+
|
|
128
|
+
|
|
64
129
|
# ---- oep speed ----------------------------------------------------------------------------------------------------
|
|
65
130
|
|
|
131
|
+
def _flows(text: str) -> list[tuple[str, int]] | None:
|
|
132
|
+
"""--flows "in:2,out,duplex:1" -> [("in", 2), ("out", 0), ("duplex", 1)]; "" -> None (the default flows)."""
|
|
133
|
+
out = []
|
|
134
|
+
for item in text.replace(" ", "").split(","):
|
|
135
|
+
if not item:
|
|
136
|
+
continue
|
|
137
|
+
name, _, n = item.partition(":")
|
|
138
|
+
out.append((name, int(n) if n else 0))
|
|
139
|
+
return out or None
|
|
140
|
+
|
|
141
|
+
|
|
66
142
|
def _speed(args) -> int:
|
|
67
|
-
"""Take the lock, raise_speed, print the report; the session's end puts the port
|
|
143
|
+
"""Take the lock, raise_speed by the host guide's procedure, print the report; the session's end puts the port
|
|
144
|
+
back at its boot speed. The record of passed / failed rates is on unless --no-record."""
|
|
68
145
|
import dataclasses
|
|
69
|
-
|
|
146
|
+
text = args.candidates or args.rates
|
|
147
|
+
candidates = [int(r) for r in text.replace(" ", "").split(",") if r] or list(link.DEFAULT_CANDIDATES)
|
|
148
|
+
flows = _flows(args.flows)
|
|
149
|
+
verify = True if args.verify or flows else (False if args.minimal else None)
|
|
70
150
|
hst = link.open_host(args.probe, baud=args.baud)
|
|
71
151
|
try:
|
|
72
152
|
core.take(hst, 5000, owner="oep speed")
|
|
73
|
-
report = link.raise_speed(hst,
|
|
74
|
-
|
|
153
|
+
report = link.raise_speed(hst, candidates, flows=flows, verify=verify, frames=args.frames,
|
|
154
|
+
record=not args.no_record)
|
|
75
155
|
if args.json:
|
|
76
156
|
sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
|
|
77
157
|
else:
|
oep_client/arm.py
CHANGED
|
@@ -10,7 +10,7 @@ import struct
|
|
|
10
10
|
|
|
11
11
|
from . import host as h, message as m, registry as reg
|
|
12
12
|
from .core import Interface
|
|
13
|
-
from .riscv import OK, TargetError, WireBase, check, ran, status_name # noqa: F401
|
|
13
|
+
from .riscv import OK, BlockLength, TargetError, WireBase, check, ran, status_name # noqa: F401
|
|
14
14
|
|
|
15
15
|
DP_DPIDR = DP_ABORT = 0x0
|
|
16
16
|
DP_CTRL_STAT = 0x4
|
|
@@ -60,7 +60,9 @@ def transfer_reads(steps: bytes) -> list[bool]:
|
|
|
60
60
|
return out
|
|
61
61
|
|
|
62
62
|
|
|
63
|
-
class ArmAdi(Interface):
|
|
63
|
+
class ArmAdi(Interface, BlockLength):
|
|
64
|
+
"""oep.target.arm-adi on one connection. `max_length` / `max_words` bound read_block / write_block (from the
|
|
65
|
+
probe's describe, oep-if-debug §6: mandatory there)."""
|
|
64
66
|
NAME = "oep.target.arm-adi"
|
|
65
67
|
REVISION = 1
|
|
66
68
|
TRANSFER, READ_BLOCK, WRITE_BLOCK = _ADI.op["transfer"], _ADI.op["read_block"], _ADI.op["write_block"]
|
|
@@ -70,6 +72,7 @@ class ArmAdi(Interface):
|
|
|
70
72
|
self.conn = conn
|
|
71
73
|
self.adiv6 = adiv6
|
|
72
74
|
self._select: int | None = None
|
|
75
|
+
self._max_length = None
|
|
73
76
|
|
|
74
77
|
# ---- raw transfers ----
|
|
75
78
|
@staticmethod
|
|
@@ -140,10 +143,8 @@ class MemAp:
|
|
|
140
143
|
csw = adi.ap_read(ap, self.base)
|
|
141
144
|
adi.ap_write(ap, self.base, (((csw & ~0x37) | 0x12) | csw_set) & ~csw_clear) # 32 bits, AddrInc single
|
|
142
145
|
adi._ap_select(ap, self.base) # the bank the block operations assume
|
|
143
|
-
# Words per block operation
|
|
144
|
-
|
|
145
|
-
from .core import confirm
|
|
146
|
-
self.chunk = max(1, (confirm(adi.host)["max_frame"] - 18) // 4)
|
|
146
|
+
# Words per block operation: the probe's declared max_length (oep-if-debug §6), never computed from max_frame
|
|
147
|
+
self.chunk = adi.max_words
|
|
147
148
|
|
|
148
149
|
def write_many(self, pairs: list[tuple[int, int]]) -> None:
|
|
149
150
|
"""Scattered single-word writes in one transfer list (TAR, DRW per word, RDBUFF at the end so the last one
|
oep_client/ch32_flash.py
CHANGED
|
@@ -17,7 +17,7 @@ import struct
|
|
|
17
17
|
import time
|
|
18
18
|
from dataclasses import dataclass, field
|
|
19
19
|
|
|
20
|
-
from . import
|
|
20
|
+
from . import host as h, riscv
|
|
21
21
|
|
|
22
22
|
# oep-spec experiments/flash-primitives/x035_loader.S, assembled at 0x20000000 (riscv32-esp-elf binutils)
|
|
23
23
|
FAST_PAGE_LOADER = bytes.fromhex(
|
|
@@ -91,11 +91,10 @@ LOADER, FAST_BUFFER, FAST_DONE = 0x20000000, 0x20000400, 0x200000B0
|
|
|
91
91
|
V003_INPUT, V003_STACK, V003_EBREAK, V003_RUN = 0x20000200, 0x20000800, 0x2000015C, 1024
|
|
92
92
|
|
|
93
93
|
|
|
94
|
-
def _block_size(
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return (info["max_frame"] - 5 - 6 - 4 - 1 - 4 - 2) // 4 * 4
|
|
94
|
+
def _block_size(dm: riscv.RiscvDm) -> int:
|
|
95
|
+
"""Bytes per read_block / write_block: the probe's declared max_length (oep-if-debug §4.5), never a value computed
|
|
96
|
+
from max_frame (riscv.NoMaxLength when the probe declares none)."""
|
|
97
|
+
return dm.max_words * 4
|
|
99
98
|
|
|
100
99
|
|
|
101
100
|
def _write_requests(dm: riscv.RiscvDm, address: int, data: bytes, block: int) -> list[tuple[int, int, bytes]]:
|
|
@@ -142,7 +141,7 @@ def program(hst: h.Host, dm: riscv.RiscvDm, image: bytes, profile: FlashProfile)
|
|
|
142
141
|
image = image + b"\xff" * (-len(image) % profile.page)
|
|
143
142
|
if profile.size and len(image) > profile.size:
|
|
144
143
|
raise ValueError(f"image of {len(image)} bytes exceeds {profile.size}")
|
|
145
|
-
block = _block_size(
|
|
144
|
+
block = _block_size(dm)
|
|
146
145
|
t = {}
|
|
147
146
|
t0 = time.perf_counter()
|
|
148
147
|
if profile.method == "fast-page":
|
oep_client/cobs.py
CHANGED
|
@@ -71,3 +71,8 @@ def unframe(raw: bytes) -> bytes:
|
|
|
71
71
|
if crc16(body) != got:
|
|
72
72
|
raise CorruptFrame("CRC mismatch")
|
|
73
73
|
return body
|
|
74
|
+
|
|
75
|
+
def frame_max(message_len: int) -> int:
|
|
76
|
+
"""The longest frame a message of this length makes: CRC16, a COBS code per 254 bytes and one more, the delimiter."""
|
|
77
|
+
n = message_len + 2
|
|
78
|
+
return n + (n + 253) // 254 + 1 + 1
|
oep_client/endpoint.py
CHANGED
|
@@ -35,10 +35,12 @@ define:
|
|
|
35
35
|
session ends, then resumes from the session's last host reset. The byte framing itself is `fake_serial`.
|
|
36
36
|
- port_speed (core §3.5, optional): on when the profile's describe declares it (`port_speed_base`, the boot speed;
|
|
37
37
|
None = off: the op is unknown_operation). try / commit / revert on the UART bridge the request came in on (else
|
|
38
|
-
unavailable cause 6), a
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
unavailable cause 6), a step that does not fit the port's state (commit at the boot speed or when committed,
|
|
39
|
+
revert at the boot speed, try while trying or committed) unavailable cause 6, a step above 2 malformed, a rate the
|
|
40
|
+
fake UART cannot make (outside 300..5000000) unsupported; a try reverts after verify_ms without a commit or at a
|
|
41
|
+
broken candidate once a good frame came at the new speed, a commit after idle_ms (at most port_speed_idle_max_ms,
|
|
42
|
+
3000; 0 and anything longer count as that maximum) with no good frame or at 3 broken candidates in a row with no
|
|
43
|
+
good frame between, the session's end reverts after its answer. The line itself is modelled by `broken_rates`
|
|
42
44
|
(rate -> BrokenRate): frames at such a rate break, from a size and in the directions given, only both ways at once
|
|
43
45
|
(`duplex`), only every Nth (`every`) (`fake_serial` applies it).
|
|
44
46
|
|
|
@@ -334,7 +336,7 @@ PORT_SPEED_TAG = reg.CORE.tlv["describe"]["port_speed"]
|
|
|
334
336
|
OP_PORT_SPEED = reg.CORE.op["port_speed"]
|
|
335
337
|
SPEED_STEP = reg.CORE.enum["port_speed_step"]
|
|
336
338
|
SPEED_IDLE_MAX_MS = reg.TIMING["port_speed_idle_max_ms"] # committed: idle_ms at most this (0 and longer: this)
|
|
337
|
-
SPEED_BAD_MAX
|
|
339
|
+
SPEED_BAD_MAX = 3 # committed: this many broken candidates in a row (no good frame between) revert
|
|
338
340
|
SPEED_RATES = (300, 5_000_000) # what the fake's UART makes (anything between, exactly)
|
|
339
341
|
|
|
340
342
|
|
|
@@ -487,7 +489,8 @@ class Endpoint:
|
|
|
487
489
|
self.speed_until_ms = 0 # try: the commit's deadline (verify_ms)
|
|
488
490
|
self.speed_idle_ms = 0 # committed: revert after this long with no good frame
|
|
489
491
|
self.speed_good_ms = 0 # the last good frame on that port
|
|
490
|
-
self.
|
|
492
|
+
self.speed_heard = False # try: a good frame came at the new speed (before it, no broken counts)
|
|
493
|
+
self.speed_bad = 0 # committed: broken candidates in a row since the last good frame
|
|
491
494
|
self.speed_pending: tuple | None = None # ("switch", port, baud, verify_ms) / ("revert",): after the answer
|
|
492
495
|
self.speed_log: list[tuple[int, int]] = [] # (port, rate) every switch, reverts included
|
|
493
496
|
if self.saved is not None:
|
|
@@ -1203,23 +1206,28 @@ class Endpoint:
|
|
|
1203
1206
|
port, baud, step, verify_ms, idle_ms = t.take("BIBHI")
|
|
1204
1207
|
_, ignored = t.tail()
|
|
1205
1208
|
if step not in SPEED_STEP.values():
|
|
1206
|
-
raise
|
|
1209
|
+
raise Reject(m.MALFORMED) # not a defined step (core §4.3 order 5)
|
|
1207
1210
|
if port != self._transport or port >= len(self.transports) or \
|
|
1208
1211
|
self.transports[port] != fake.TRANSPORT["uart_bridge"]:
|
|
1209
1212
|
raise unavailable("wrong_state") # only the UART bridge the request came in on
|
|
1213
|
+
# the port's state (boot speed / trying / committed) decides which step fits (core §3.5): any other is cause 6
|
|
1210
1214
|
if step == SPEED_STEP["try"]:
|
|
1215
|
+
if self.speed_state != "base":
|
|
1216
|
+
raise unavailable("wrong_state") # already trying or committed
|
|
1211
1217
|
if not SPEED_RATES[0] <= baud <= SPEED_RATES[1]:
|
|
1212
1218
|
raise unsupported_fixed() # a baud this UART cannot make
|
|
1213
1219
|
self.speed_pending = ("switch", port, baud, verify_ms)
|
|
1214
1220
|
return self._answer(struct.pack("<I", baud), ignored)
|
|
1215
1221
|
if step == SPEED_STEP["commit"]:
|
|
1216
|
-
if self.speed_state
|
|
1217
|
-
raise unavailable("wrong_state") # nothing tried, or another baud
|
|
1222
|
+
if self.speed_state != "try" or port != self.speed_port or baud != self.speed_asked:
|
|
1223
|
+
raise unavailable("wrong_state") # nothing tried, committed already, or another baud
|
|
1218
1224
|
self.speed_state = "committed"
|
|
1219
1225
|
self.speed_idle_ms = idle_ms if 0 < idle_ms <= SPEED_IDLE_MAX_MS else SPEED_IDLE_MAX_MS
|
|
1220
1226
|
self.speed_good_ms = self.now()
|
|
1221
|
-
self.speed_bad
|
|
1227
|
+
self.speed_bad = 0
|
|
1222
1228
|
return self._answer(struct.pack("<I", self.speed_rate), ignored)
|
|
1229
|
+
if self.speed_state == "base":
|
|
1230
|
+
raise unavailable("wrong_state") # a revert at the boot speed: nothing to go back from
|
|
1223
1231
|
self.speed_pending = ("revert",)
|
|
1224
1232
|
return self._answer(struct.pack("<I", self.port_speed_base), ignored)
|
|
1225
1233
|
|
|
@@ -1255,7 +1263,8 @@ class Endpoint:
|
|
|
1255
1263
|
self.speed_state, self.speed_port, self.speed_rate, self.speed_asked = "try", port, baud, baud
|
|
1256
1264
|
self.speed_until_ms = self.now() + verify_ms
|
|
1257
1265
|
self.speed_good_ms = self.now()
|
|
1258
|
-
self.
|
|
1266
|
+
self.speed_heard = False
|
|
1267
|
+
self.speed_bad = 0
|
|
1259
1268
|
self.speed_log.append((port, baud))
|
|
1260
1269
|
|
|
1261
1270
|
def _speed_revert(self) -> None:
|
|
@@ -1263,24 +1272,32 @@ class Endpoint:
|
|
|
1263
1272
|
return
|
|
1264
1273
|
self.speed_log.append((self.speed_port, self.port_speed_base))
|
|
1265
1274
|
self.speed_state, self.speed_port, self.speed_rate, self.speed_asked = "base", None, 0, 0
|
|
1266
|
-
self.speed_bad
|
|
1275
|
+
self.speed_heard, self.speed_bad = False, 0
|
|
1267
1276
|
|
|
1268
1277
|
def speed_frame(self, port: int, good: bool) -> None:
|
|
1269
|
-
"""A candidate closed on serial port `port`: a frame (good) or not (a broken candidate).
|
|
1278
|
+
"""A candidate closed on serial port `port`: a frame (good) or not (a broken candidate). core §3.5's conditions
|
|
1279
|
+
2 (trying: one broken candidate after the first good frame at the new speed; the ones before it are the
|
|
1280
|
+
switch-over's leftovers and do not count) and 4 (committed: SPEED_BAD_MAX broken candidates in a row with no
|
|
1281
|
+
good frame between)."""
|
|
1270
1282
|
if self.speed_state == "base" or port != self.speed_port:
|
|
1271
1283
|
return
|
|
1272
1284
|
if good:
|
|
1273
1285
|
self.speed_good_ms = self.now()
|
|
1286
|
+
self.speed_heard = True
|
|
1287
|
+
self.speed_bad = 0
|
|
1274
1288
|
return
|
|
1275
1289
|
if self.speed_state == "try":
|
|
1276
|
-
self.
|
|
1290
|
+
if self.speed_heard:
|
|
1291
|
+
self._speed_revert() # the new speed breaks frames: not this one
|
|
1277
1292
|
return
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
if len(self.speed_bad) >= SPEED_BAD_MAX:
|
|
1293
|
+
self.speed_bad += 1
|
|
1294
|
+
if self.speed_bad >= SPEED_BAD_MAX:
|
|
1281
1295
|
self._speed_revert()
|
|
1282
1296
|
|
|
1283
1297
|
def _speed_tick(self) -> None:
|
|
1298
|
+
# Condition 3 (committed, idle_ms with no good frame) is not counted while a request executes (core §3.5, as
|
|
1299
|
+
# the lease, §6.1). The fake handles every request within one call and its clock does not move meanwhile, so
|
|
1300
|
+
# there is nothing to pause here: a long-running request would need `speed_good_ms` set when its answer goes out.
|
|
1284
1301
|
if self.speed_pending and self.speed_pending[0] == "revert":
|
|
1285
1302
|
self.speed_after_answer()
|
|
1286
1303
|
now = self.now()
|
|
@@ -1573,8 +1590,10 @@ class Endpoint:
|
|
|
1573
1590
|
if op == _RV.op["read_block"]:
|
|
1574
1591
|
address, count = t.take("IH")
|
|
1575
1592
|
t.tail()
|
|
1576
|
-
if
|
|
1577
|
-
raise Reject(m.MALFORMED) #
|
|
1593
|
+
if address % 4:
|
|
1594
|
+
raise Reject(m.MALFORMED) # not a word address (core §4.3 order 5)
|
|
1595
|
+
if 4 * count > self.block_max.get(fn, 1 << 16):
|
|
1596
|
+
raise unsupported_fixed() # past the declared max_length (bytes; debug §4.5)
|
|
1578
1597
|
if not tg.halted:
|
|
1579
1598
|
return m.COMPLETED, m.FAILED, struct.pack("<HB", 0, STATE) # a running hart (debug §4.5)
|
|
1580
1599
|
words, status = [], OK
|
|
@@ -1587,8 +1606,10 @@ class Endpoint:
|
|
|
1587
1606
|
struct.pack(f"<HB{len(words)}I", len(words), status, *words))
|
|
1588
1607
|
if op == _RV.op["write_block"]:
|
|
1589
1608
|
address, count = t.take("IH")
|
|
1590
|
-
if
|
|
1609
|
+
if address % 4:
|
|
1591
1610
|
raise Reject(m.MALFORMED)
|
|
1611
|
+
if 4 * count > self.block_max.get(fn, 1 << 16):
|
|
1612
|
+
raise unsupported_fixed() # past the declared max_length (debug §4.5)
|
|
1592
1613
|
words = struct.unpack(f"<{count}I", t.bytes(4 * count))
|
|
1593
1614
|
t.tail()
|
|
1594
1615
|
if not tg.halted:
|
oep_client/fake.py
CHANGED
|
@@ -157,6 +157,16 @@ MAX_CONNECTIONS = reg.WIRE_RVSWD.tlv["describe"]["max_connections"]
|
|
|
157
157
|
_CFG = reg.PROBE_CONFIG.tlv["describe"]
|
|
158
158
|
|
|
159
159
|
|
|
160
|
+
BLOCK_HEADERS = 24 # read_block's answer (5 + done 2 + status 1) and write_block's request (10 + connection 2 + address 4 + count 2) both fit
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def block_max_length(max_frame: int) -> int:
|
|
164
|
+
"""The max_length (bytes, a multiple of 4) a probe with read_block / write_block declares for its max_frame
|
|
165
|
+
(oep-if-debug §4.5): max_frame - 24 rounded down to a word, so a read's answer and a write's request both fit one
|
|
166
|
+
frame. The host takes this value; it never computes a block size from max_frame."""
|
|
167
|
+
return max(0, (max_frame - BLOCK_HEADERS) // 4 * 4)
|
|
168
|
+
|
|
169
|
+
|
|
160
170
|
def _transports(kinds: list[tuple[int, int]]) -> tuple[bytes, ...]:
|
|
161
171
|
"""(kind, USB interface or 0xFF) per transport, index = position (core §7.5)."""
|
|
162
172
|
return tuple(catalog.tlv(CORE_TRANSPORT, bytes([i, k, itf])) for i, (k, itf) in enumerate(kinds))
|
|
@@ -219,7 +229,7 @@ def p4_x035() -> FakeProbe:
|
|
|
219
229
|
catalog.channel_group(1, [(1, 2), (2, 54)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1),
|
|
220
230
|
catalog.u8(MAX_CONNECTIONS, 1))),
|
|
221
231
|
Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
|
|
222
|
-
catalog.u16(MAX_LENGTH,
|
|
232
|
+
catalog.u16(MAX_LENGTH, block_max_length(1024)))),
|
|
223
233
|
Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
|
|
224
234
|
_gpio(4, pins),
|
|
225
235
|
_uart(5, 0, pins, 3_000_000),
|
|
@@ -256,7 +266,7 @@ def esp32_v003() -> FakeProbe:
|
|
|
256
266
|
Offered(1, 0, "oep.wire.swio", (catalog.channel_group(1, [(1, 16)]), catalog.role_channels(3, [23]),
|
|
257
267
|
catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
|
|
258
268
|
Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b0111), catalog.u8(IMPLEMENTATION, 1),
|
|
259
|
-
catalog.u16(MAX_LENGTH,
|
|
269
|
+
catalog.u16(MAX_LENGTH, block_max_length(64)))),
|
|
260
270
|
Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
|
|
261
271
|
_gpio(4, wired + [23], modes=0x7F), # no mode 7 (both pulls): unsupported there
|
|
262
272
|
_uart(5, 0, wired, 115_200),
|
|
@@ -290,7 +300,7 @@ def p4_bench() -> FakeProbe:
|
|
|
290
300
|
catalog.channel_group(3, [(1, 6), (2, 7)]), catalog.u32(MAX_CLOCK_HZ, 5_000_000),
|
|
291
301
|
catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 2))),
|
|
292
302
|
Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
|
|
293
|
-
catalog.u16(MAX_LENGTH,
|
|
303
|
+
catalog.u16(MAX_LENGTH, block_max_length(1024)))),
|
|
294
304
|
Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
|
|
295
305
|
_gpio(4, pins),
|
|
296
306
|
_uart(5, 0, pins, 3_000_000),
|
|
@@ -310,7 +320,7 @@ def rp2350_pins() -> FakeProbe:
|
|
|
310
320
|
Offered(1, 0, "oep.wire.rvswd", _roles({1: pins, 2: pins, 3: pins}) + (
|
|
311
321
|
catalog.u32(MAX_CLOCK_HZ, 5_000_000), catalog.u8(IMPLEMENTATION, 1), catalog.u8(MAX_CONNECTIONS, 1))),
|
|
312
322
|
Offered(2, 0, "oep.target.riscv-dm", (catalog.u32(FEATURES, 0b1111), catalog.u8(IMPLEMENTATION, 1),
|
|
313
|
-
catalog.u16(MAX_LENGTH,
|
|
323
|
+
catalog.u16(MAX_LENGTH, block_max_length(1024)))),
|
|
314
324
|
Offered(3, 0, "oep.target.console", (catalog.tlv(MECHANISMS, bytes([0, 1, 2])),)),
|
|
315
325
|
_gpio(4, pins),
|
|
316
326
|
_uart(5, 0, pins, 3_000_000),
|