oep-client-python 0.0.25__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 -8
- oep_client/arm.py +8 -7
- oep_client/capture.py +3 -2
- oep_client/ch32_flash.py +6 -7
- oep_client/cobs.py +5 -0
- oep_client/config.py +3 -1
- oep_client/core.py +15 -5
- oep_client/endpoint.py +65 -24
- oep_client/fake.py +14 -4
- oep_client/fake_serial.py +34 -5
- oep_client/fake_serve.py +7 -4
- oep_client/host.py +30 -5
- oep_client/link.py +634 -197
- oep_client/linktest.py +184 -0
- oep_client/registry.py +1 -1
- oep_client/riscv.py +65 -5
- oep_client/speed_record.py +104 -0
- {oep_client_python-0.0.25.dist-info → oep_client_python-0.0.27.dist-info}/METADATA +34 -19
- oep_client_python-0.0.27.dist-info/RECORD +37 -0
- oep_client_python-0.0.25.dist-info/RECORD +0 -35
- {oep_client_python-0.0.25.dist-info → oep_client_python-0.0.27.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.25.dist-info → oep_client_python-0.0.27.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.25.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,13 +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
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")
|
|
40
66
|
d = sub.add_parser("dump", help="list and describe every interface a probe offers")
|
|
41
67
|
src = d.add_mutually_exclusive_group(required=True)
|
|
42
68
|
src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
|
|
@@ -49,6 +75,8 @@ def main(argv=None) -> int:
|
|
|
49
75
|
return _config(args)
|
|
50
76
|
if args.command == "speed":
|
|
51
77
|
return _speed(args)
|
|
78
|
+
if args.command == "linktest":
|
|
79
|
+
return _linktest(args)
|
|
52
80
|
|
|
53
81
|
if args.fake:
|
|
54
82
|
call = fake.PROFILES[args.fake]().call
|
|
@@ -60,16 +88,70 @@ def main(argv=None) -> int:
|
|
|
60
88
|
return 0
|
|
61
89
|
|
|
62
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
|
+
|
|
63
129
|
# ---- oep speed ----------------------------------------------------------------------------------------------------
|
|
64
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
|
+
|
|
65
142
|
def _speed(args) -> int:
|
|
66
|
-
"""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."""
|
|
67
145
|
import dataclasses
|
|
68
|
-
|
|
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)
|
|
69
150
|
hst = link.open_host(args.probe, baud=args.baud)
|
|
70
151
|
try:
|
|
71
152
|
core.take(hst, 5000, owner="oep speed")
|
|
72
|
-
report = link.raise_speed(hst,
|
|
153
|
+
report = link.raise_speed(hst, candidates, flows=flows, verify=verify, frames=args.frames,
|
|
154
|
+
record=not args.no_record)
|
|
73
155
|
if args.json:
|
|
74
156
|
sys.stdout.write(json.dumps(dataclasses.asdict(report), indent=2) + "\n")
|
|
75
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
|
|
@@ -37,7 +37,7 @@ class SwdWire(WireBase):
|
|
|
37
37
|
body = bytes([0]) + self._speed_tlv(self._speed_or_default(max_speed)) + self._pins_tlv(pins) + self._reset_tlv(reset)
|
|
38
38
|
if targetsel is not None:
|
|
39
39
|
body += m.tlv(self.TAG_TARGETSEL, struct.pack("<I", targetsel), critical=True)
|
|
40
|
-
rd = m.Reader(self._call(self.ATTACH, body).payload)
|
|
40
|
+
rd = m.Reader(self._call(self.ATTACH, body, expect_ms=reset[1] if reset else 0).payload)
|
|
41
41
|
conn, dpidr, self.flags, self.speed_hz = rd.take("HIBI")
|
|
42
42
|
self.existing = bool(self.flags & self.FLAGS["existing"])
|
|
43
43
|
rd.tail()
|
|
@@ -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/capture.py
CHANGED
|
@@ -335,7 +335,7 @@ class LogicCapture(Interface):
|
|
|
335
335
|
|
|
336
336
|
def start(self) -> int:
|
|
337
337
|
"""-> blocking_ms (0: the probe keeps answering while it captures). self.generation: the new capture's."""
|
|
338
|
-
rd = m.Reader(self._call(self.START).payload)
|
|
338
|
+
rd = m.Reader(self._call(self.START, expect_ms=self.config.blocking_ms if self.config else 0).payload)
|
|
339
339
|
blocking, self.generation = rd.take("II")
|
|
340
340
|
rd.tail()
|
|
341
341
|
self.armed_s = time.monotonic()
|
|
@@ -576,7 +576,8 @@ class CaptureGroup(Interface):
|
|
|
576
576
|
def start(self, tracks: list[LogicCapture] = ()) -> tuple[int, int]:
|
|
577
577
|
"""-> (blocking_ms, the group's start_ns). `tracks`: whose armed_s and generation to set (the answer's TLV
|
|
578
578
|
generations names each track's new generation; self.generations keeps them by fn)."""
|
|
579
|
-
rd = m.Reader(self._call(self.START
|
|
579
|
+
rd = m.Reader(self._call(self.START, expect_ms=max([t.config.blocking_ms for t in tracks if t.config] or [0]))
|
|
580
|
+
.payload)
|
|
580
581
|
blocking, start_ns = rd.take("IQ")
|
|
581
582
|
gens = rd.tail().get(GROUP_GENERATIONS) or b""
|
|
582
583
|
self.generations = {fn: g for fn, g in struct.iter_unpack("<HI", gens[:len(gens) // 6 * 6])}
|
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/config.py
CHANGED
|
@@ -292,6 +292,8 @@ class State:
|
|
|
292
292
|
binds: list[BindState] = field(default_factory=list)
|
|
293
293
|
|
|
294
294
|
|
|
295
|
+
SAVE_EXPECT_MS = 2000 # a save writes the probe's flash: the link waits at least this (Host.expecting)
|
|
296
|
+
|
|
295
297
|
class ProbeConfig(Interface):
|
|
296
298
|
NAME = "oep.probe.config"
|
|
297
299
|
REVISION = 1
|
|
@@ -346,7 +348,7 @@ class ProbeConfig(Interface):
|
|
|
346
348
|
|
|
347
349
|
def save(self) -> int:
|
|
348
350
|
"""Save the current settings (a probe with storage; the whole is replaced). -> the hash saved."""
|
|
349
|
-
return self._hash_answer(self._call(self.SAVE))
|
|
351
|
+
return self._hash_answer(self._call(self.SAVE, expect_ms=SAVE_EXPECT_MS))
|
|
350
352
|
|
|
351
353
|
def erase(self) -> None:
|
|
352
354
|
self._call(self.ERASE)
|
oep_client/core.py
CHANGED
|
@@ -4,6 +4,8 @@ entry, and calls that raise unless they worked)."""
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
import contextlib
|
|
8
|
+
|
|
7
9
|
import struct
|
|
8
10
|
|
|
9
11
|
from . import catalog, host as h, message as m, registry as reg
|
|
@@ -196,12 +198,20 @@ class Interface:
|
|
|
196
198
|
raise UnsupportedRevision(f"{self.name} (fn {self.fn}) is revision {rev} on this probe; this client "
|
|
197
199
|
f"speaks revision {self.REVISION} only")
|
|
198
200
|
|
|
199
|
-
def
|
|
200
|
-
|
|
201
|
+
def _expecting(self, ms: int):
|
|
202
|
+
"""Host.expecting(ms) when the host has it (a stand-in host may not)."""
|
|
203
|
+
expecting = getattr(self.host, "expecting", None) if ms else None
|
|
204
|
+
return expecting(ms) if expecting else contextlib.nullcontext()
|
|
205
|
+
|
|
206
|
+
def _call(self, op: int, body: bytes = b"", *, locked: bool = True, expect_ms: int = 0) -> m.Result:
|
|
207
|
+
with self._expecting(expect_ms):
|
|
208
|
+
return self.host.call(self.fn, op, self.prefix + body, locked=locked)
|
|
201
209
|
|
|
202
|
-
def _request(self, op: int, body: bytes = b"", *, locked: bool = True) -> m.Result:
|
|
203
|
-
"""Rejections raise; completed results of any outcome come back for the caller to decode.
|
|
204
|
-
|
|
210
|
+
def _request(self, op: int, body: bytes = b"", *, locked: bool = True, expect_ms: int = 0) -> m.Result:
|
|
211
|
+
"""Rejections raise; completed results of any outcome come back for the caller to decode. expect_ms: how
|
|
212
|
+
long it may take on the probe (Host.expecting)."""
|
|
213
|
+
with self._expecting(expect_ms):
|
|
214
|
+
return self.host.request(self.fn, op, self.prefix + body, locked=locked)
|
|
205
215
|
|
|
206
216
|
def request(self, op: int, body: bytes = b"") -> tuple[int, int, bytes]:
|
|
207
217
|
"""The raw (fn, op, payload) of one operation, for Host.pipeline / pipeline_calls."""
|
oep_client/endpoint.py
CHANGED
|
@@ -35,11 +35,14 @@ 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
|
-
|
|
42
|
-
|
|
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`
|
|
44
|
+
(rate -> BrokenRate): frames at such a rate break, from a size and in the directions given, only both ways at once
|
|
45
|
+
(`duplex`), only every Nth (`every`) (`fake_serial` applies it).
|
|
43
46
|
|
|
44
47
|
Every other non-core fn gets two stand-in operations so the session rules can be exercised - FAKE ONLY, they mean
|
|
45
48
|
nothing on a real probe: 0x01 write(u32) changes state, 0x02 read -> u32 needs no lock.
|
|
@@ -317,13 +320,23 @@ class BrokenRate:
|
|
|
317
320
|
min_size: int = 0
|
|
318
321
|
to_host: bool = True
|
|
319
322
|
to_probe: bool = True
|
|
323
|
+
duplex: bool = False # only while both ways carry such frames at once (a request of min_size and more comes in
|
|
324
|
+
# while an answer of min_size and more is still unread): the request breaks (to_probe),
|
|
325
|
+
# the unread answer breaks (to_host)
|
|
326
|
+
every: int = 1 # of the frames that would break, only every Nth does (1: all)
|
|
327
|
+
seen: int = 0 # the frames that would have broken so far (`every` counts these)
|
|
328
|
+
|
|
329
|
+
def hit(self) -> bool:
|
|
330
|
+
"""One more frame that would break: True when this one does (`every`)."""
|
|
331
|
+
self.seen += 1
|
|
332
|
+
return self.seen % max(1, self.every) == 0
|
|
320
333
|
|
|
321
334
|
|
|
322
335
|
PORT_SPEED_TAG = reg.CORE.tlv["describe"]["port_speed"]
|
|
323
336
|
OP_PORT_SPEED = reg.CORE.op["port_speed"]
|
|
324
337
|
SPEED_STEP = reg.CORE.enum["port_speed_step"]
|
|
325
338
|
SPEED_IDLE_MAX_MS = reg.TIMING["port_speed_idle_max_ms"] # committed: idle_ms at most this (0 and longer: this)
|
|
326
|
-
SPEED_BAD_MAX
|
|
339
|
+
SPEED_BAD_MAX = 3 # committed: this many broken candidates in a row (no good frame between) revert
|
|
327
340
|
SPEED_RATES = (300, 5_000_000) # what the fake's UART makes (anything between, exactly)
|
|
328
341
|
|
|
329
342
|
|
|
@@ -476,7 +489,8 @@ class Endpoint:
|
|
|
476
489
|
self.speed_until_ms = 0 # try: the commit's deadline (verify_ms)
|
|
477
490
|
self.speed_idle_ms = 0 # committed: revert after this long with no good frame
|
|
478
491
|
self.speed_good_ms = 0 # the last good frame on that port
|
|
479
|
-
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
|
|
480
494
|
self.speed_pending: tuple | None = None # ("switch", port, baud, verify_ms) / ("revert",): after the answer
|
|
481
495
|
self.speed_log: list[tuple[int, int]] = [] # (port, rate) every switch, reverts included
|
|
482
496
|
if self.saved is not None:
|
|
@@ -1192,23 +1206,28 @@ class Endpoint:
|
|
|
1192
1206
|
port, baud, step, verify_ms, idle_ms = t.take("BIBHI")
|
|
1193
1207
|
_, ignored = t.tail()
|
|
1194
1208
|
if step not in SPEED_STEP.values():
|
|
1195
|
-
raise
|
|
1209
|
+
raise Reject(m.MALFORMED) # not a defined step (core §4.3 order 5)
|
|
1196
1210
|
if port != self._transport or port >= len(self.transports) or \
|
|
1197
1211
|
self.transports[port] != fake.TRANSPORT["uart_bridge"]:
|
|
1198
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
|
|
1199
1214
|
if step == SPEED_STEP["try"]:
|
|
1215
|
+
if self.speed_state != "base":
|
|
1216
|
+
raise unavailable("wrong_state") # already trying or committed
|
|
1200
1217
|
if not SPEED_RATES[0] <= baud <= SPEED_RATES[1]:
|
|
1201
1218
|
raise unsupported_fixed() # a baud this UART cannot make
|
|
1202
1219
|
self.speed_pending = ("switch", port, baud, verify_ms)
|
|
1203
1220
|
return self._answer(struct.pack("<I", baud), ignored)
|
|
1204
1221
|
if step == SPEED_STEP["commit"]:
|
|
1205
|
-
if self.speed_state
|
|
1206
|
-
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
|
|
1207
1224
|
self.speed_state = "committed"
|
|
1208
1225
|
self.speed_idle_ms = idle_ms if 0 < idle_ms <= SPEED_IDLE_MAX_MS else SPEED_IDLE_MAX_MS
|
|
1209
1226
|
self.speed_good_ms = self.now()
|
|
1210
|
-
self.speed_bad
|
|
1227
|
+
self.speed_bad = 0
|
|
1211
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
|
|
1212
1231
|
self.speed_pending = ("revert",)
|
|
1213
1232
|
return self._answer(struct.pack("<I", self.port_speed_base), ignored)
|
|
1214
1233
|
|
|
@@ -1218,10 +1237,19 @@ class Endpoint:
|
|
|
1218
1237
|
return self.speed_rate
|
|
1219
1238
|
return self.port_speed_base or 115200
|
|
1220
1239
|
|
|
1221
|
-
def breaks(self, port: int, size: int, to_host: bool) -> bool:
|
|
1222
|
-
"""The line model: a frame of `size` bytes on `port` at its rate now breaks (`broken_rates`).
|
|
1240
|
+
def breaks(self, port: int, size: int, to_host: bool, duplex: bool = False) -> bool:
|
|
1241
|
+
"""The line model: a frame of `size` bytes on `port` at its rate now breaks (`broken_rates`). duplex: the other
|
|
1242
|
+
way carries a frame of the rate's min_size or more at the same time (a BrokenRate with `duplex` breaks only
|
|
1243
|
+
then)."""
|
|
1244
|
+
b = self.broken_rates.get(self.port_baud(port))
|
|
1245
|
+
if b is None or size < b.min_size or not (b.to_host if to_host else b.to_probe) or (b.duplex and not duplex):
|
|
1246
|
+
return False
|
|
1247
|
+
return b.hit()
|
|
1248
|
+
|
|
1249
|
+
def duplex_rate(self, port: int) -> BrokenRate | None:
|
|
1250
|
+
"""The rate `port` runs at now when it breaks only both ways at once (fake_serial checks the unread answers)."""
|
|
1223
1251
|
b = self.broken_rates.get(self.port_baud(port))
|
|
1224
|
-
return b is not None and
|
|
1252
|
+
return b if b is not None and b.duplex else None
|
|
1225
1253
|
|
|
1226
1254
|
def speed_after_answer(self) -> None:
|
|
1227
1255
|
"""The answer that asked for a switch or a revert is out (at the old speed): now do it."""
|
|
@@ -1235,7 +1263,8 @@ class Endpoint:
|
|
|
1235
1263
|
self.speed_state, self.speed_port, self.speed_rate, self.speed_asked = "try", port, baud, baud
|
|
1236
1264
|
self.speed_until_ms = self.now() + verify_ms
|
|
1237
1265
|
self.speed_good_ms = self.now()
|
|
1238
|
-
self.
|
|
1266
|
+
self.speed_heard = False
|
|
1267
|
+
self.speed_bad = 0
|
|
1239
1268
|
self.speed_log.append((port, baud))
|
|
1240
1269
|
|
|
1241
1270
|
def _speed_revert(self) -> None:
|
|
@@ -1243,24 +1272,32 @@ class Endpoint:
|
|
|
1243
1272
|
return
|
|
1244
1273
|
self.speed_log.append((self.speed_port, self.port_speed_base))
|
|
1245
1274
|
self.speed_state, self.speed_port, self.speed_rate, self.speed_asked = "base", None, 0, 0
|
|
1246
|
-
self.speed_bad
|
|
1275
|
+
self.speed_heard, self.speed_bad = False, 0
|
|
1247
1276
|
|
|
1248
1277
|
def speed_frame(self, port: int, good: bool) -> None:
|
|
1249
|
-
"""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)."""
|
|
1250
1282
|
if self.speed_state == "base" or port != self.speed_port:
|
|
1251
1283
|
return
|
|
1252
1284
|
if good:
|
|
1253
1285
|
self.speed_good_ms = self.now()
|
|
1286
|
+
self.speed_heard = True
|
|
1287
|
+
self.speed_bad = 0
|
|
1254
1288
|
return
|
|
1255
1289
|
if self.speed_state == "try":
|
|
1256
|
-
self.
|
|
1290
|
+
if self.speed_heard:
|
|
1291
|
+
self._speed_revert() # the new speed breaks frames: not this one
|
|
1257
1292
|
return
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
if len(self.speed_bad) >= SPEED_BAD_MAX:
|
|
1293
|
+
self.speed_bad += 1
|
|
1294
|
+
if self.speed_bad >= SPEED_BAD_MAX:
|
|
1261
1295
|
self._speed_revert()
|
|
1262
1296
|
|
|
1263
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.
|
|
1264
1301
|
if self.speed_pending and self.speed_pending[0] == "revert":
|
|
1265
1302
|
self.speed_after_answer()
|
|
1266
1303
|
now = self.now()
|
|
@@ -1553,8 +1590,10 @@ class Endpoint:
|
|
|
1553
1590
|
if op == _RV.op["read_block"]:
|
|
1554
1591
|
address, count = t.take("IH")
|
|
1555
1592
|
t.tail()
|
|
1556
|
-
if
|
|
1557
|
-
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)
|
|
1558
1597
|
if not tg.halted:
|
|
1559
1598
|
return m.COMPLETED, m.FAILED, struct.pack("<HB", 0, STATE) # a running hart (debug §4.5)
|
|
1560
1599
|
words, status = [], OK
|
|
@@ -1567,8 +1606,10 @@ class Endpoint:
|
|
|
1567
1606
|
struct.pack(f"<HB{len(words)}I", len(words), status, *words))
|
|
1568
1607
|
if op == _RV.op["write_block"]:
|
|
1569
1608
|
address, count = t.take("IH")
|
|
1570
|
-
if
|
|
1609
|
+
if address % 4:
|
|
1571
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)
|
|
1572
1613
|
words = struct.unpack(f"<{count}I", t.bytes(4 * count))
|
|
1573
1614
|
t.tail()
|
|
1574
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),
|