oep-client-python 0.0.27__py3-none-any.whl → 0.0.28__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 +44 -0
- oep_client/config.py +57 -1
- oep_client/endpoint.py +314 -13
- oep_client/fake.py +41 -10
- oep_client/fixture.py +54 -5
- oep_client/pins.py +614 -0
- oep_client/registry.py +5 -5
- oep_client/riscv.py +32 -8
- oep_client/targets.py +97 -0
- {oep_client_python-0.0.27.dist-info → oep_client_python-0.0.28.dist-info}/METADATA +64 -2
- {oep_client_python-0.0.27.dist-info → oep_client_python-0.0.28.dist-info}/RECORD +15 -13
- {oep_client_python-0.0.27.dist-info → oep_client_python-0.0.28.dist-info}/WHEEL +0 -0
- {oep_client_python-0.0.27.dist-info → oep_client_python-0.0.28.dist-info}/entry_points.txt +0 -0
- {oep_client_python-0.0.27.dist-info → oep_client_python-0.0.28.dist-info}/licenses/LICENSE +0 -0
oep_client/__init__.py
CHANGED
oep_client/__main__.py
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
oep speed <probe> [--candidates 921600,500000] [--verify [--flows in:2,out:2]] (port_speed, core §3.5 and the host
|
|
13
13
|
guide §7: try the candidates in order on a UART bridge, report)
|
|
14
14
|
oep linktest <probe> --rates now,921600 --patterns in,out,duplex --inflight 1,2 --sizes 128,496 --frames 300
|
|
15
|
+
oep pins <probe> --power 5 --wire swio (find the target's debug pins and reset line: classify the channels, scan,
|
|
16
|
+
identify, hold-low + attach under reset; prints a slot, --save writes it)
|
|
15
17
|
|
|
16
18
|
<probe>: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]. A change takes the lock (owner "oep config") and
|
|
17
19
|
ends the session after it; it takes effect at once, and stays over a restart only after `save` (or --save).
|
|
@@ -63,6 +65,19 @@ def main(argv=None) -> int:
|
|
|
63
65
|
lt.add_argument("--baud", type=int, default=link.BASE_BAUD, help="the boot speed to open at (default 115200)")
|
|
64
66
|
lt.add_argument("--low-latency", choices=("on", "off"), default="on", help="the serial driver's low-latency mode")
|
|
65
67
|
lt.add_argument("--json", action="store_true", help="one JSON object per cell")
|
|
68
|
+
pn = sub.add_parser("pins", help="find where a target is wired: classify the channels, scan the wire, find the "
|
|
69
|
+
"reset line, suggest a slot (writes nothing without --save)")
|
|
70
|
+
pn.add_argument("probe", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID[:SERIAL]]")
|
|
71
|
+
pn.add_argument("--power", type=int, help="the gpio channel that powers the target: switched off, then on, to "
|
|
72
|
+
"see which channels follow it (never touched without this)")
|
|
73
|
+
pn.add_argument("--exclude", default="", help="channels never to read, scan or hold, comma-separated")
|
|
74
|
+
pn.add_argument("--wire", choices=("swio", "rvswd", "swd"), default="swio")
|
|
75
|
+
pn.add_argument("--steps", default="classify,hold,scan,identify,reset,slot",
|
|
76
|
+
help="the steps to run, comma-separated (default all: classify, hold, scan, identify, reset, slot)")
|
|
77
|
+
pn.add_argument("--save", action="store_true", help="write the suggested slot (config set, then save)")
|
|
78
|
+
pn.add_argument("--slot", type=int, default=0, help="--save: the slot number (default 0)")
|
|
79
|
+
pn.add_argument("--name", help="--save: the slot's name (default: the target family, else 'target')")
|
|
80
|
+
pn.add_argument("--json", action="store_true", help="the report as JSON (the steps' lines go to stderr)")
|
|
66
81
|
d = sub.add_parser("dump", help="list and describe every interface a probe offers")
|
|
67
82
|
src = d.add_mutually_exclusive_group(required=True)
|
|
68
83
|
src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
|
|
@@ -77,6 +92,8 @@ def main(argv=None) -> int:
|
|
|
77
92
|
return _speed(args)
|
|
78
93
|
if args.command == "linktest":
|
|
79
94
|
return _linktest(args)
|
|
95
|
+
if args.command == "pins":
|
|
96
|
+
return _pins_cmd(args)
|
|
80
97
|
|
|
81
98
|
if args.fake:
|
|
82
99
|
call = fake.PROFILES[args.fake]().call
|
|
@@ -88,6 +105,33 @@ def main(argv=None) -> int:
|
|
|
88
105
|
return 0
|
|
89
106
|
|
|
90
107
|
|
|
108
|
+
# ---- oep pins -----------------------------------------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
def _pins_cmd(args) -> int:
|
|
111
|
+
from . import pins
|
|
112
|
+
exclude = [int(x, 0) for x in args.exclude.replace(" ", "").split(",") if x]
|
|
113
|
+
say = (lambda t: print(t, file=sys.stderr, flush=True)) if args.json else (lambda t: print(t, flush=True)) # noqa: E731
|
|
114
|
+
hst = link.open_host(args.probe)
|
|
115
|
+
try:
|
|
116
|
+
core.take(hst, 10000, owner="oep pins")
|
|
117
|
+
finder = pins.PinFinder(hst, wire=args.wire, power=args.power, exclude=exclude, say=say, probe=args.probe,
|
|
118
|
+
save=args.save, slot=args.slot, name=args.name)
|
|
119
|
+
report = finder.run(steps=tuple(s for s in args.steps.split(",") if s))
|
|
120
|
+
say(f"done in {report.elapsed_s:.1f} s")
|
|
121
|
+
if args.json:
|
|
122
|
+
print(json.dumps(report.as_dict(), indent=2))
|
|
123
|
+
try:
|
|
124
|
+
hst.end()
|
|
125
|
+
except host.OepError:
|
|
126
|
+
pass
|
|
127
|
+
except host.Rejected as e:
|
|
128
|
+
print(f"refused: {e}", file=sys.stderr)
|
|
129
|
+
return 2
|
|
130
|
+
finally:
|
|
131
|
+
hst.link.close()
|
|
132
|
+
return 0 if report.found else 1
|
|
133
|
+
|
|
134
|
+
|
|
91
135
|
# ---- oep linktest -------------------------------------------------------------------------------------------------
|
|
92
136
|
|
|
93
137
|
def _linktest(args) -> int:
|
oep_client/config.py
CHANGED
|
@@ -73,14 +73,23 @@ class Label:
|
|
|
73
73
|
|
|
74
74
|
@dataclass(kw_only=True)
|
|
75
75
|
class Idle:
|
|
76
|
+
"""The state of a channel no plan or connection uses (probe.config §1): at boot and after every release. mode:
|
|
77
|
+
hi-z, pull-up, pull-down, output-low, output-high (output_low / output_high too). An output mode keeps driving that
|
|
78
|
+
level while the channel is free - a target's power switch kept on - and a gpio plan that takes the channel keeps
|
|
79
|
+
it until its first set (fixture §1); a probe that cannot drive the channel refuses it unsupported."""
|
|
76
80
|
channel: int
|
|
77
|
-
mode: str = "pull-up" # hi-z, pull-up, pull-down
|
|
81
|
+
mode: str = "pull-up" # hi-z, pull-up, pull-down, output-low, output-high
|
|
78
82
|
TAG = ITEM["idle"]
|
|
79
83
|
|
|
84
|
+
def __post_init__(self):
|
|
85
|
+
self.mode = self.mode.replace("_", "-")
|
|
86
|
+
|
|
80
87
|
def key(self) -> tuple:
|
|
81
88
|
return (self.channel,)
|
|
82
89
|
|
|
83
90
|
def value(self) -> bytes:
|
|
91
|
+
if self.mode not in IDLE:
|
|
92
|
+
raise ValueError(f"idle mode {self.mode!r}: one of {', '.join(IDLE)}")
|
|
84
93
|
return struct.pack("<HB", self.channel, IDLE[self.mode])
|
|
85
94
|
|
|
86
95
|
|
|
@@ -395,3 +404,50 @@ class ProbeConfig(Interface):
|
|
|
395
404
|
return st
|
|
396
405
|
first_slot += n_slots
|
|
397
406
|
first_bind += n_binds
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# The label names of a target's power and reset lines (host-development-guide §8.1): `nrst` its reset, `power_hi` high
|
|
410
|
+
# powers it, `power_lo` low powers it. On a probe with several slots they are `<slot name>.<name>`.
|
|
411
|
+
LINE_NAMES = ("nrst", "power_hi", "power_lo")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class AmbiguousLine(LookupError):
|
|
415
|
+
"""find_line could not tell which channel is meant; `candidates` holds (label text, channel)."""
|
|
416
|
+
|
|
417
|
+
def __init__(self, message: str, candidates: list[tuple[str, int]]):
|
|
418
|
+
super().__init__(message)
|
|
419
|
+
self.candidates = candidates
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def find_line(hst_or_items, name: str, slot: int | str | None = None) -> int | None:
|
|
423
|
+
"""The channel labelled for `name` (nrst, power_hi, power_lo: host-development-guide §8.1), or None when there is
|
|
424
|
+
none. hst_or_items: a Host (its settings are read: `ProbeConfig.items()`, no lock) or the decoded items. slot: the
|
|
425
|
+
slot's name or number; None picks the probe's one slot.
|
|
426
|
+
|
|
427
|
+
`<slot>.<name>` first, then the bare `name` - the bare name only when the settings hold at most one slot (a bare
|
|
428
|
+
name is for a one-slot probe). With several slots and no `slot`, or two channels with the same label, it raises
|
|
429
|
+
AmbiguousLine listing the candidates."""
|
|
430
|
+
items = hst_or_items if isinstance(hst_or_items, (list, tuple)) else ProbeConfig(hst_or_items).items()
|
|
431
|
+
labels = [(it.text, it.channel) for it in items if isinstance(it, Label)]
|
|
432
|
+
slots = [it for it in items if isinstance(it, Slot)]
|
|
433
|
+
if isinstance(slot, int):
|
|
434
|
+
named = [s.name for s in slots if s.slot == slot]
|
|
435
|
+
if not named:
|
|
436
|
+
raise LookupError(f"no slot {slot} in the probe's settings")
|
|
437
|
+
slot = named[0]
|
|
438
|
+
if slot is None and len(slots) > 1:
|
|
439
|
+
candidates = sorted((t, c) for t, c in labels if t == name or t.endswith("." + name))
|
|
440
|
+
if candidates:
|
|
441
|
+
listed = ", ".join(f"{t} (channel {c})" for t, c in candidates)
|
|
442
|
+
raise AmbiguousLine(f"{name}: {len(slots)} slots and no slot given; candidates: {listed}", candidates)
|
|
443
|
+
return None
|
|
444
|
+
if slot is None and slots:
|
|
445
|
+
slot = slots[0].name
|
|
446
|
+
texts = ([f"{slot}.{name}"] if slot is not None else []) + ([name] if len(slots) <= 1 else [])
|
|
447
|
+
for text in texts:
|
|
448
|
+
found = sorted(c for t, c in labels if t == text)
|
|
449
|
+
if len(found) > 1:
|
|
450
|
+
raise AmbiguousLine(f"{text}: on {len(found)} channels", [(text, c) for c in found])
|
|
451
|
+
if found:
|
|
452
|
+
return found[0]
|
|
453
|
+
return None
|
oep_client/endpoint.py
CHANGED
|
@@ -26,10 +26,16 @@ define:
|
|
|
26
26
|
interfaces the profiles offer: oep.wire.rvswd / swio (scan with its TLVs, attach on declared pin pairs with the
|
|
27
27
|
reset TLV, several connections up to max_connections, the seat rule, connections), oep.target.riscv-dm on one
|
|
28
28
|
`FakeTarget` per pin pair (dmi / run answers count their values; run's stopped 2), oep.target.console streams (one
|
|
29
|
-
live stream per connection, lifetime by its users, the streams list), oep.fixture.gpio
|
|
30
|
-
|
|
29
|
+
live stream per connection, lifetime by its users, the streams list), oep.fixture.gpio (the lines outside through
|
|
30
|
+
the test hook `gpio_world`; a target's `reset_line` makes the other reset channels reset nothing), oep.fixture.uart (its stream
|
|
31
|
+
made by the plan, status, the settings' uart item), oep.fixture.i2c-target / spi-target (fixture §3 / §4: the plan's
|
|
32
|
+
SDA / SCL and SCK / MOSI / MISO / CS, role_channels and an exact channel_group; configure, arm, preload, read_rx,
|
|
33
|
+
status, reset, stretch when declared; no bus controller of its own - the test hooks `i2c_write` / `i2c_read` /
|
|
34
|
+
`spi_transfer` are one transaction on the bus), and oep.probe.config (plan / label / idle / slot / bind / uart
|
|
31
35
|
/ disable items, get / set / unset / save / erase, the state op - describe is declarations only; a disabled channel
|
|
32
|
-
is refused everywhere with unavailable cause 5 and never parked: `parked` records the free pins' states it set
|
|
36
|
+
is refused everywhere with unavailable cause 5 and never parked: `parked` records the free pins' states it set -
|
|
37
|
+
idle modes 3 / 4 drive their level, refused unsupported on `input_only` channels; every release goes there, and a
|
|
38
|
+
gpio line taken keeps that state until its first set)
|
|
33
39
|
- the serial ports' raw side (core §3.4, probe.config §1.2): `port_input` / `port_output` carry the bytes outside
|
|
34
40
|
the frames for each serial port by its bind; a port the lock holder's requests came in on is held until the
|
|
35
41
|
session ends, then resumes from the session's last host reset. The byte framing itself is `fake_serial`.
|
|
@@ -74,6 +80,7 @@ STREAM_STATE, STREAM_USERS = _CON.enum["stream_state"], _CON.enum["stream_users"
|
|
|
74
80
|
ATTACH_FLAGS = reg.WIRE_RVSWD.enum["attach_flags"]
|
|
75
81
|
RUN_STOPPED = _RV.enum["run_stopped"]
|
|
76
82
|
ITEM = _CFG.tlv["item"]
|
|
83
|
+
IDLE_MODE = _CFG.enum["idle_mode"]
|
|
77
84
|
CFG_DESCRIBE = _CFG.tlv["describe"]
|
|
78
85
|
SLOT_ATTACH = _CFG.enum["slot_attach"]
|
|
79
86
|
SLOT_STATE = _CFG.enum["slot_state"]
|
|
@@ -87,6 +94,10 @@ NO_SLOT, NEVER_NS = 0xFF, 0xFFFFFFFFFFFFFFFF
|
|
|
87
94
|
PIN_ROLE_RESET = reg.WIRE_RVSWD.enum["pin_role"]["reset"] # the channels an attach's reset TLV may take
|
|
88
95
|
TARGET_ID_LEN = reg.WIRE_RVSWD.enum["target_id_len"]["wch_dmi_7f"]
|
|
89
96
|
TARGET_ID_SCHEMES = set(reg.WIRE_RVSWD.enum["target_id_scheme"].values()) | set(reg.WIRE_SWD.enum["target_id_scheme"].values())
|
|
97
|
+
_I2C, _SPI = reg.FIXTURE_I2C_TARGET, reg.FIXTURE_SPI_TARGET
|
|
98
|
+
I2C_MODE, I2C_FEATURES, SPI_FEATURES = _I2C.enum["mode"], _I2C.enum["features"], _SPI.enum["features"]
|
|
99
|
+
TARGET_ROLES = {"oep.fixture.gpio": {1}, "oep.fixture.uart": {1, 2}, _I2C.name: set(_I2C.enum["role"].values()),
|
|
100
|
+
_SPI.name: set(_SPI.enum["role"].values())} # the plan roles each fixture takes
|
|
90
101
|
UART_FORMAT_MASK = 0x1F # the defined format bits (fixture §2)
|
|
91
102
|
UART_CONFIGURED = _UART.enum["uart_configured"] # status's configured byte: default / session / item / item_fallback
|
|
92
103
|
_T_SCAN, _T_ATTACH, _T_DETACH, _T_ATTACH_ANSWER = (reg.WIRE_RVSWD.tlv[k] for k in ("scan", "attach", "detach", "attach_answer"))
|
|
@@ -212,6 +223,7 @@ class FakeTarget:
|
|
|
212
223
|
# run(pc, regs) -> (stopped, dpc, elapsed_us); default: halts 0x10 past the start
|
|
213
224
|
run_hook: Callable | None = None
|
|
214
225
|
unstoppable: bool = False # run: the limit passes and the hart cannot be halted (stopped 2)
|
|
226
|
+
reset_line: int | None = None # the channel wired to its reset (None: any reset channel resets it)
|
|
215
227
|
|
|
216
228
|
def dmstatus(self) -> int:
|
|
217
229
|
return 0x82 | ((0x300 if self.halted else 0xC00)) | (0xC0000 if self.havereset else 0)
|
|
@@ -223,6 +235,33 @@ class FakeTarget:
|
|
|
223
235
|
return self.dmi.get(address, 0)
|
|
224
236
|
|
|
225
237
|
|
|
238
|
+
@dataclass
|
|
239
|
+
class I2cState:
|
|
240
|
+
"""One oep.fixture.i2c-target (fixture §3): what configure made, the frames waiting for read_rx, the tx slots."""
|
|
241
|
+
state: int = 0 # 0 not configured, 1 running
|
|
242
|
+
address: int = 0
|
|
243
|
+
mode: int = 0
|
|
244
|
+
armed: int = 0 # mode 1: the length arm_rx waits for (0 = not armed)
|
|
245
|
+
queue: list = field(default_factory=list) # (frame, ns)
|
|
246
|
+
rx_frames: int = 0
|
|
247
|
+
errors: int = 0
|
|
248
|
+
slots: int = 0 # preload_tx's running count (u8, wraps)
|
|
249
|
+
tx: list = field(default_factory=list) # preloaded, unread
|
|
250
|
+
stretch_us: int = 0
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@dataclass
|
|
254
|
+
class SpiState:
|
|
255
|
+
"""One oep.fixture.spi-target (fixture §4): what configure made, the armed transaction, the finished ones."""
|
|
256
|
+
state: int = 0
|
|
257
|
+
mode: int = 0
|
|
258
|
+
bit_order: int = 0
|
|
259
|
+
armed: tuple[int, bytes] | None = None # (length, MISO bytes) of the one transaction it waits for
|
|
260
|
+
queue: list = field(default_factory=list) # (bits, MOSI bytes, ns)
|
|
261
|
+
transactions: int = 0
|
|
262
|
+
errors: int = 0
|
|
263
|
+
|
|
264
|
+
|
|
226
265
|
@dataclass
|
|
227
266
|
class Stream:
|
|
228
267
|
"""A position stream (common §1): bytes from position `base`, marks with serials; a console stream's users
|
|
@@ -362,6 +401,8 @@ class Endpoint:
|
|
|
362
401
|
self.fns = {name: fn for fn, name in sorted(self.names.items(), reverse=True)} # first fn of each name
|
|
363
402
|
self.static = {o.fn: o.tlvs for o in probe.offered}
|
|
364
403
|
self.static_labels: dict[int, str] = {}
|
|
404
|
+
# channels the probe can only read (a test sets them): an idle of mode 3 / 4 there is unsupported (probe.config §1)
|
|
405
|
+
self.input_only: set[int] = set()
|
|
365
406
|
self.transports: list[int] = []
|
|
366
407
|
self.max_op_ms = reg.LIMITS["max_op_ms_reference"]
|
|
367
408
|
self.plan_roles: int | None = None
|
|
@@ -418,6 +459,14 @@ class Endpoint:
|
|
|
418
459
|
for fn, name in self.names.items() if name == "oep.fixture.uart"}
|
|
419
460
|
self.uart_max_hz = {fn: next((struct.unpack_from("<I", t, 2)[0] for t in self.static[fn] if t[0] == catalog.MAX_CLOCK_HZ),
|
|
420
461
|
3_000_000) for fn, name in self.names.items() if name == "oep.fixture.uart"}
|
|
462
|
+
def own(fn: int, tag: int, fmt: str, default: int) -> int:
|
|
463
|
+
return next((struct.unpack_from("<" + fmt, t, 2)[0] for t in self.static[fn] if t[0] == tag), default)
|
|
464
|
+
# the fixture targets' declarations (fixture §3 / §4): max_length, features, queue_depth, max_stretch_us
|
|
465
|
+
# (i2c-target only; 0 when not declared)
|
|
466
|
+
self.target_decl = {fn: (own(fn, catalog.MAX_LENGTH, "H", 1), own(fn, catalog.FEATURES, "I", 0),
|
|
467
|
+
own(fn, _I2C.tlv["describe"]["queue_depth"], "B", 1),
|
|
468
|
+
own(fn, _I2C.tlv["describe"]["max_stretch_us"], "I", 0) if name == _I2C.name else 0)
|
|
469
|
+
for fn, name in self.names.items() if name in (_I2C.name, _SPI.name)}
|
|
421
470
|
cfg_fn = self.fns.get("oep.probe.config")
|
|
422
471
|
cfg = {t[0]: t[2:2 + t[1]] for t in self.static.get(cfg_fn, ())}
|
|
423
472
|
self.slots_max = cfg[CFG_DESCRIBE["slots_max"]][0] if CFG_DESCRIBE["slots_max"] in cfg else 0
|
|
@@ -430,6 +479,9 @@ class Endpoint:
|
|
|
430
479
|
self.port_speed_base: int | None = 115200 if any(t[0] == PORT_SPEED_TAG for t in self.static.get(0, ())) else None
|
|
431
480
|
self.broken_rates: dict[int, BrokenRate] = {} # the line: rates that break frames (fake_serial applies it)
|
|
432
481
|
self._transport = 0 # the transport the request being handled came in on
|
|
482
|
+
# the lines outside (a test hook): gpio_world(channel, mode) -> the level an input mode reads (None: the
|
|
483
|
+
# default - gpio_inputs, a pull-up 1); reads see the world as the modes set it (gpio_modes)
|
|
484
|
+
self.gpio_world: Callable[[int, int], int | None] | None = None
|
|
433
485
|
self._boot()
|
|
434
486
|
|
|
435
487
|
def _boot(self) -> None:
|
|
@@ -469,6 +521,8 @@ class Endpoint:
|
|
|
469
521
|
self.uart_clock_hz = 80_000_000 # the UARTs' divider clock (a test lowers it: the item's fallback)
|
|
470
522
|
self.uart_session_cfg: set[int] = set() # fns a session's configure set (it beats the uart item)
|
|
471
523
|
self.uart_tx: dict[int, bytearray] = {} # what a serial port's raw bytes sent out on a fixture UART
|
|
524
|
+
self.i2c: dict[int, I2cState] = {fn: I2cState() for fn, n in self.names.items() if n == _I2C.name}
|
|
525
|
+
self.spi: dict[int, SpiState] = {fn: SpiState() for fn, n in self.names.items() if n == _SPI.name}
|
|
472
526
|
self.config: dict[tuple[int, int], bytes | list[bytes]] = {} # (item tag, key) -> value (plan: list)
|
|
473
527
|
self.saved: dict | None = getattr(self, "saved", None)
|
|
474
528
|
self.saved_ids: dict[int, tuple] = getattr(self, "saved_ids", {}) # saved fn -> (name, instance, revision)
|
|
@@ -822,6 +876,8 @@ class Endpoint:
|
|
|
822
876
|
return m.REJECTED, m.UNKNOWN_FUNCTION, b""
|
|
823
877
|
if req.fn == m.CORE_FN and req.op == OP_PORT_SPEED and self.port_speed_base is None:
|
|
824
878
|
return m.REJECTED, m.UNKNOWN_OPERATION, b"" # the optional feature off (core §3.5)
|
|
879
|
+
if req.fn in self.i2c and req.op == _I2C.op["stretch"] and not self.target_decl[req.fn][1] & I2C_FEATURES["stretch"]:
|
|
880
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b"" # stretch is features bit1's (fixture §3)
|
|
825
881
|
if not self._lock_free(req.fn, req.op):
|
|
826
882
|
refused = self._check(req.session)
|
|
827
883
|
if refused:
|
|
@@ -1047,6 +1103,7 @@ class Endpoint:
|
|
|
1047
1103
|
named = {fn for fn, _, _ in got}
|
|
1048
1104
|
if any(fn not in self.names for fn in named):
|
|
1049
1105
|
raise Reject(m.UNKNOWN_FUNCTION)
|
|
1106
|
+
self._check_target_roles(got) # malformed before held_by_settings (core §4.3)
|
|
1050
1107
|
if named & self.plan_from_config: # the settings' plan is the settings' (core §8)
|
|
1051
1108
|
raise unavailable("held_by_settings", holder_fn=min(named & self.plan_from_config),
|
|
1052
1109
|
holder_kind="settings_plan")
|
|
@@ -1054,7 +1111,7 @@ class Endpoint:
|
|
|
1054
1111
|
self._refuse_disabled(ch for _, _, ch in got) # a disabled channel (probe.config §1): cause 5
|
|
1055
1112
|
if self.plan_roles is not None and len([a for a in self.plan if a[0] not in named]) + len(got) > self.plan_roles:
|
|
1056
1113
|
raise unavailable("limit") # plan_roles (core §8)
|
|
1057
|
-
self.
|
|
1114
|
+
self._replace_plans(named, got)
|
|
1058
1115
|
for fn in named:
|
|
1059
1116
|
self._uart_plan_changed(fn)
|
|
1060
1117
|
return m.COMPLETED, m.SUCCESS, b""
|
|
@@ -1072,8 +1129,19 @@ class Endpoint:
|
|
|
1072
1129
|
their analog function, cutting their digital input and output (oep-if-capture §1.2), as on this library's ESP32s."""
|
|
1073
1130
|
return fn in self.captures and not self.captures[fn].analog
|
|
1074
1131
|
|
|
1132
|
+
def _check_target_roles(self, got: list[tuple[int, int, int]]) -> None:
|
|
1133
|
+
"""An i2c-target / spi-target plan holds each of its roles exactly once, on distinct channels (fixture §3 /
|
|
1134
|
+
§4): a missing role, a role twice or two roles on one channel -> malformed. A role the fn does not define is
|
|
1135
|
+
left to the declaration check (unsupported)."""
|
|
1136
|
+
for fn in {f for f, _, _ in got if f in self.i2c or f in self.spi}:
|
|
1137
|
+
roles = TARGET_ROLES[self.names[fn]]
|
|
1138
|
+
mine = [(role, ch) for f, role, ch in got if f == fn and role in roles]
|
|
1139
|
+
if sorted(r for r, _ in mine) != sorted(roles) or len({ch for _, ch in mine}) != len(mine):
|
|
1140
|
+
raise Reject(m.MALFORMED)
|
|
1141
|
+
|
|
1075
1142
|
def _check_plan(self, got: list[tuple[int, int, int]]) -> None:
|
|
1076
1143
|
"""plan_apply's all-or-nothing check: the roles each fn has, and no pin another fn (or a slot) holds."""
|
|
1144
|
+
self._check_target_roles(got)
|
|
1077
1145
|
named = {fn for fn, _, _ in got}
|
|
1078
1146
|
others = {a for a in self.plan if a[0] not in named}
|
|
1079
1147
|
kept = {a for a in others if not self._listens(a[0])}
|
|
@@ -1105,17 +1173,31 @@ class Endpoint:
|
|
|
1105
1173
|
if role not in self._declared_roles(fn, ch):
|
|
1106
1174
|
raise not_declared(ch)
|
|
1107
1175
|
continue
|
|
1108
|
-
roles =
|
|
1176
|
+
roles = TARGET_ROLES.get(self.names.get(fn, ""), set())
|
|
1109
1177
|
if role not in roles or ch not in self._declared_channels(fn):
|
|
1110
1178
|
raise not_declared(ch)
|
|
1179
|
+
if fn in self.i2c or fn in self.spi: # role_channels binds the roles it lists (core §7.4)
|
|
1180
|
+
listed = self._listed_roles(fn)
|
|
1181
|
+
if role in listed and role not in self._declared_roles(fn, ch):
|
|
1182
|
+
raise not_declared(ch)
|
|
1111
1183
|
if any(k[2] == ch for k in kept) or ch in slot_pins:
|
|
1112
1184
|
raise held(ch, next((k[0] for k in kept if k[2] == ch), None))
|
|
1185
|
+
for fn in {f for f, _, _ in got if f in self.i2c or f in self.spi}:
|
|
1186
|
+
groups = [set(catalog.unpack_channel_group(t[2:2 + t[1]])[1]) for t in self.static[fn]
|
|
1187
|
+
if t[0] == catalog.CHANNEL_GROUP]
|
|
1188
|
+
mine = {(role, ch) for f, role, ch in got if f == fn}
|
|
1189
|
+
if groups and mine not in groups: # one channel_group exactly (core §7.4)
|
|
1190
|
+
raise not_declared(min(ch for _, ch in mine))
|
|
1113
1191
|
|
|
1114
1192
|
def _declared_roles(self, fn: int, channel: int) -> set[int]:
|
|
1115
1193
|
"""The roles fn's describe offers on `channel` (role_channels)."""
|
|
1116
1194
|
return {t[2] for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS
|
|
1117
1195
|
and channel in catalog.bitmap_to_channels(struct.unpack_from("<H", t, 3)[0], t[5:2 + t[1]])}
|
|
1118
1196
|
|
|
1197
|
+
def _listed_roles(self, fn: int) -> set[int]:
|
|
1198
|
+
"""The roles fn's role_channels name (the only ones it binds, core §7.4)."""
|
|
1199
|
+
return {t[2] for t in self.static[fn] if t[0] == catalog.ROLE_CHANNELS}
|
|
1200
|
+
|
|
1119
1201
|
def _declared_channels(self, fn: int) -> set[int]:
|
|
1120
1202
|
"""Every channel fn's describe offers in any role (role_channels and channel_group)."""
|
|
1121
1203
|
out = set()
|
|
@@ -1126,6 +1208,24 @@ class Endpoint:
|
|
|
1126
1208
|
out.update(c for _, c in catalog.unpack_channel_group(t[2:2 + t[1]])[1])
|
|
1127
1209
|
return out
|
|
1128
1210
|
|
|
1211
|
+
def _replace_plans(self, fns: set[int], got) -> None:
|
|
1212
|
+
"""The plans of `fns` become `got` as one change (core §8): a channel leaving goes to its idle state, one in
|
|
1213
|
+
both the old and the new plan of a gpio keeps its state and drive, and a new gpio line keeps the state it was
|
|
1214
|
+
in - an output idle keeps driving - until the first set (fixture §1); the idle modes 0-4 are the gpio modes of
|
|
1215
|
+
the same numbers."""
|
|
1216
|
+
old = {a for a in self.plan if a[0] in fns}
|
|
1217
|
+
kept = {ch for f, _, ch in old if self.names.get(f) == "oep.fixture.gpio"} & \
|
|
1218
|
+
{ch for f, _, ch in got if self.names.get(f) == "oep.fixture.gpio"}
|
|
1219
|
+
released = {a[2] for a in old} - {a[2] for a in got}
|
|
1220
|
+
for ch in {a[2] for a in old} - kept:
|
|
1221
|
+
self.gpio_modes.pop(ch, None)
|
|
1222
|
+
self.plan = {a for a in self.plan if a[0] not in fns}
|
|
1223
|
+
self._park(released)
|
|
1224
|
+
self.plan |= set(got)
|
|
1225
|
+
for fn, _, ch in got:
|
|
1226
|
+
if ch not in kept and self.names.get(fn) == "oep.fixture.gpio" and self.parked.get(ch, 0):
|
|
1227
|
+
self.gpio_modes[ch] = self.parked[ch]
|
|
1228
|
+
|
|
1129
1229
|
def _drop_plan(self, fn: int) -> None:
|
|
1130
1230
|
released = set()
|
|
1131
1231
|
for a in [a for a in self.plan if a[0] == fn]:
|
|
@@ -1139,7 +1239,12 @@ class Endpoint:
|
|
|
1139
1239
|
# ---- oep.fixture.uart's stream: made by the plan, gone with it (fixture §2) ---------------------
|
|
1140
1240
|
def _uart_plan_changed(self, fn: int) -> None:
|
|
1141
1241
|
"""fn's plan moved: a fixture UART with RX or TX gets its stream (carrying on from where the last one ended,
|
|
1142
|
-
common §1.1) and the settings' uart item unless a session's configure holds; one without loses it.
|
|
1242
|
+
common §1.1) and the settings' uart item unless a session's configure holds; one without loses it. A fixture
|
|
1243
|
+
I2C / SPI target goes back to not configured (state 0): its configure is made on the plan's pins."""
|
|
1244
|
+
if fn in self.i2c:
|
|
1245
|
+
self.i2c[fn] = I2cState()
|
|
1246
|
+
if fn in self.spi:
|
|
1247
|
+
self.spi[fn] = SpiState()
|
|
1143
1248
|
if self.names.get(fn) != "oep.fixture.uart":
|
|
1144
1249
|
return
|
|
1145
1250
|
planned = any(a[0] == fn for a in self.plan)
|
|
@@ -1395,7 +1500,10 @@ class Endpoint:
|
|
|
1395
1500
|
for n, s in self.slots.items(): # a new connection for an evicted slot: a new cue
|
|
1396
1501
|
if s.wire_fn == fn and s.pair == pair:
|
|
1397
1502
|
self.slot_rt[n].evicted = False
|
|
1398
|
-
if reset is not None
|
|
1503
|
+
if reset is not None and tg.reset_line not in (None, channel):
|
|
1504
|
+
if method == 1: # a line that resets nothing: halted where it ran
|
|
1505
|
+
tg.halted, tg.dpc = True, tg.reset_vector + 0x2f8
|
|
1506
|
+
elif reset is not None: # the reset line held, then let go: a host reset
|
|
1399
1507
|
tg.halted = method == 1
|
|
1400
1508
|
tg.dpc = tg.reset_vector if method == 1 else tg.reset_vector + 0x200
|
|
1401
1509
|
self._host_reset(cid, MARK_RESET["attach_reset"])
|
|
@@ -1848,7 +1956,9 @@ class Endpoint:
|
|
|
1848
1956
|
levels = []
|
|
1849
1957
|
for ch in chans:
|
|
1850
1958
|
mode = self.gpio_modes.get(ch, 0)
|
|
1851
|
-
|
|
1959
|
+
outside = self.gpio_world(ch, mode) if self.gpio_world and mode in (0, 1, 2, 6, 7) else None
|
|
1960
|
+
levels.append(outside if outside is not None else
|
|
1961
|
+
{1: 1, 3: 0, 4: 1, 5: 0, 6: 1}.get(mode, self.gpio_inputs.get(ch, 0)))
|
|
1852
1962
|
return self._answer(bytes([len(levels)]) + bytes(levels), ignored) # n(u8) n x level (fixture §1)
|
|
1853
1963
|
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
1854
1964
|
|
|
@@ -1889,6 +1999,194 @@ class Endpoint:
|
|
|
1889
1999
|
"""Bytes arrive on fixture UART `fn`'s RX."""
|
|
1890
2000
|
self.uarts[fn].data += data
|
|
1891
2001
|
|
|
2002
|
+
# ---- oep.fixture.i2c-target (fixture §3) ------------------------------------------------------
|
|
2003
|
+
def _i2c_op(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
2004
|
+
st, (max_length, features, depth, max_stretch) = self.i2c[fn], self.target_decl[fn]
|
|
2005
|
+
ops = _I2C.op
|
|
2006
|
+
planned = any(a[0] == fn for a in self.plan)
|
|
2007
|
+
if op == ops["configure"]: # core §4.3's order: malformed, unsupported, unavailable
|
|
2008
|
+
address, mode = t.take("BB")
|
|
2009
|
+
_, ignored = t.tail()
|
|
2010
|
+
if address > 0x7F or mode not in I2C_MODE.values():
|
|
2011
|
+
raise Reject(m.MALFORMED)
|
|
2012
|
+
if mode == I2C_MODE["preloaded_tx"] and not features & I2C_FEATURES["preloaded_tx"]:
|
|
2013
|
+
raise unsupported_fixed() # mode 3 without features bit0
|
|
2014
|
+
if not planned:
|
|
2015
|
+
raise unavailable("wrong_state") # before the plan (cause 6)
|
|
2016
|
+
self.i2c[fn] = I2cState(state=1, address=address, mode=mode, stretch_us=st.stretch_us) # made anew
|
|
2017
|
+
return self._answer(b"", ignored)
|
|
2018
|
+
if op == ops["arm_rx"]:
|
|
2019
|
+
length = t.take("H")
|
|
2020
|
+
_, ignored = t.tail()
|
|
2021
|
+
if length == 0:
|
|
2022
|
+
raise Reject(m.MALFORMED)
|
|
2023
|
+
if length > max_length:
|
|
2024
|
+
raise unsupported_fixed()
|
|
2025
|
+
if st.state == 0 or st.mode != I2C_MODE["fixed_rx"]:
|
|
2026
|
+
raise unavailable("wrong_state") # mode 1 only
|
|
2027
|
+
st.armed = length # an earlier wait is dropped for this one
|
|
2028
|
+
return self._answer(b"", ignored)
|
|
2029
|
+
if op == ops["read_rx"]:
|
|
2030
|
+
_, ignored = t.tail()
|
|
2031
|
+
if st.state == 0:
|
|
2032
|
+
raise unavailable("wrong_state") # cause 6
|
|
2033
|
+
if not st.queue:
|
|
2034
|
+
return self._answer(struct.pack("<BH", 0, 0), ignored)
|
|
2035
|
+
frame, ns = st.queue.pop(0)
|
|
2036
|
+
return self._answer(struct.pack("<BH", min(len(st.queue), 255), len(frame)) + frame
|
|
2037
|
+
+ m.tlv(_I2C.tlv["read_rx_answer"]["ns"], struct.pack("<Q", ns)), ignored)
|
|
2038
|
+
if op == ops["preload_tx"]:
|
|
2039
|
+
count = t.take("H")
|
|
2040
|
+
data = t.bytes(count)
|
|
2041
|
+
_, ignored = t.tail()
|
|
2042
|
+
if count == 0:
|
|
2043
|
+
raise Reject(m.MALFORMED)
|
|
2044
|
+
if count > max_length:
|
|
2045
|
+
raise unsupported_fixed()
|
|
2046
|
+
if st.state == 0 or st.mode != I2C_MODE["preloaded_tx"]:
|
|
2047
|
+
raise unavailable("wrong_state") # mode 3 only
|
|
2048
|
+
if len(st.tx) >= depth:
|
|
2049
|
+
raise unavailable("limit") # every slot holds an unread preload
|
|
2050
|
+
st.tx.append(data)
|
|
2051
|
+
st.slots = (st.slots + 1) & 0xFF
|
|
2052
|
+
return self._answer(bytes([st.slots]), ignored)
|
|
2053
|
+
if op == ops["status"]: # lock-free
|
|
2054
|
+
_, ignored = t.tail()
|
|
2055
|
+
return self._answer(struct.pack("<BBBBIBI", st.state, st.mode, int(st.armed > 0), min(len(st.queue), 255),
|
|
2056
|
+
st.rx_frames, len(st.tx) if st.mode == I2C_MODE["preloaded_tx"] else 0,
|
|
2057
|
+
st.errors), ignored)
|
|
2058
|
+
if op == ops["reset"]:
|
|
2059
|
+
_, ignored = t.tail()
|
|
2060
|
+
if st.state == 0:
|
|
2061
|
+
raise unavailable("wrong_state")
|
|
2062
|
+
self.i2c[fn] = I2cState(state=1, address=st.address, mode=st.mode, stretch_us=st.stretch_us)
|
|
2063
|
+
return self._answer(b"", ignored)
|
|
2064
|
+
if op == ops["stretch"] and features & I2C_FEATURES["stretch"]:
|
|
2065
|
+
us = t.take("I")
|
|
2066
|
+
_, ignored = t.tail()
|
|
2067
|
+
if us > max_stretch:
|
|
2068
|
+
raise unsupported_fixed() # past describe's max_stretch_us
|
|
2069
|
+
st.stretch_us = us # any state; configure / reset keep it
|
|
2070
|
+
return self._answer(b"", ignored)
|
|
2071
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
2072
|
+
|
|
2073
|
+
def _i2c_frame(self, st: I2cState, frame: bytes, depth: int) -> None:
|
|
2074
|
+
if len(st.queue) >= depth:
|
|
2075
|
+
st.errors += 1 # the queue overflows: the new frame goes
|
|
2076
|
+
return
|
|
2077
|
+
st.queue.append((bytes(frame), self.now_ns()))
|
|
2078
|
+
st.rx_frames += 1
|
|
2079
|
+
|
|
2080
|
+
def i2c_write(self, fn: int, data: bytes, address: int | None = None) -> bool:
|
|
2081
|
+
"""TEST HOOK: a bus controller writes `data` to i2c-target `fn` in one transaction (START, address + W, data,
|
|
2082
|
+
STOP). -> whether the target ACKed its address (configured, and `address` None or its own). An address-only
|
|
2083
|
+
write (empty data) counts nothing in any mode. Mode 1: the armed length exactly is a frame and the wait goes on
|
|
2084
|
+
(armed until the next arm_rx / reset / configure / plan release); another length is a receive error; no wait
|
|
2085
|
+
-> ACKed, dropped, an error. Mode 2: the first byte is the length of the rest, the rest a frame (a length that
|
|
2086
|
+
does not match is a receive error). Mode 3 takes no writes: ACKed, dropped, an error."""
|
|
2087
|
+
st, (max_length, _, depth, _) = self.i2c[fn], self.target_decl[fn]
|
|
2088
|
+
if st.state == 0 or (address is not None and address != st.address):
|
|
2089
|
+
return False
|
|
2090
|
+
if not data:
|
|
2091
|
+
pass # address only: nothing counts
|
|
2092
|
+
elif st.mode == I2C_MODE["fixed_rx"] and st.armed:
|
|
2093
|
+
if len(data) == st.armed:
|
|
2094
|
+
self._i2c_frame(st, data, depth)
|
|
2095
|
+
else:
|
|
2096
|
+
st.errors += 1
|
|
2097
|
+
elif st.mode == I2C_MODE["framed_rx"] and 0 < data[0] == len(data) - 1 <= max_length:
|
|
2098
|
+
self._i2c_frame(st, data[1:], depth)
|
|
2099
|
+
else:
|
|
2100
|
+
st.errors += 1
|
|
2101
|
+
return True
|
|
2102
|
+
|
|
2103
|
+
def i2c_read(self, fn: int, n: int, address: int | None = None) -> bytes | None:
|
|
2104
|
+
"""TEST HOOK: a bus controller reads n bytes from i2c-target `fn` in one transaction. -> the bytes, or None
|
|
2105
|
+
when the address is not ACKed. Mode 3 answers from the oldest preloaded slot (cut or padded with 0xFF to n:
|
|
2106
|
+
the next read starts at the next slot); an empty slot list, mode 1 and mode 2 answer 0xFF."""
|
|
2107
|
+
st = self.i2c[fn]
|
|
2108
|
+
if st.state == 0 or (address is not None and address != st.address):
|
|
2109
|
+
return None
|
|
2110
|
+
if st.mode == I2C_MODE["preloaded_tx"] and st.tx:
|
|
2111
|
+
slot = st.tx.pop(0)
|
|
2112
|
+
return (slot + b"\xff" * n)[:n]
|
|
2113
|
+
return b"\xff" * n
|
|
2114
|
+
|
|
2115
|
+
# ---- oep.fixture.spi-target (fixture §4) ------------------------------------------------------
|
|
2116
|
+
def _spi_op(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
2117
|
+
st, (max_length, features, _, _) = self.spi[fn], self.target_decl[fn]
|
|
2118
|
+
ops = _SPI.op
|
|
2119
|
+
if op == ops["configure"]:
|
|
2120
|
+
mode, order = t.take("BB")
|
|
2121
|
+
_, ignored = t.tail()
|
|
2122
|
+
if mode > 3 or order > 1:
|
|
2123
|
+
raise Reject(m.MALFORMED)
|
|
2124
|
+
if order == 1 and not features & SPI_FEATURES["lsb_first"]:
|
|
2125
|
+
raise unsupported_fixed()
|
|
2126
|
+
if not any(a[0] == fn for a in self.plan):
|
|
2127
|
+
raise unavailable("wrong_state") # before the plan (cause 6)
|
|
2128
|
+
self.spi[fn] = SpiState(state=1, mode=mode, bit_order=order)
|
|
2129
|
+
return self._answer(b"", ignored)
|
|
2130
|
+
if op == ops["arm"]:
|
|
2131
|
+
length, count = t.take("HH")
|
|
2132
|
+
tx = t.bytes(count)
|
|
2133
|
+
_, ignored = t.tail()
|
|
2134
|
+
if length == 0 or count > length:
|
|
2135
|
+
raise Reject(m.MALFORMED)
|
|
2136
|
+
if length > max_length:
|
|
2137
|
+
raise unsupported_fixed()
|
|
2138
|
+
if st.state == 0 or st.armed is not None:
|
|
2139
|
+
raise unavailable("wrong_state") # not configured, or one is armed already
|
|
2140
|
+
st.armed = (length, tx)
|
|
2141
|
+
return self._answer(b"", ignored)
|
|
2142
|
+
if op == ops["read_rx"]:
|
|
2143
|
+
_, ignored = t.tail()
|
|
2144
|
+
if st.state == 0:
|
|
2145
|
+
raise unavailable("wrong_state") # cause 6
|
|
2146
|
+
if not st.queue:
|
|
2147
|
+
return self._answer(struct.pack("<BIH", 0, 0, 0), ignored)
|
|
2148
|
+
bits, data, ns = st.queue.pop(0)
|
|
2149
|
+
return self._answer(struct.pack("<BIH", min(len(st.queue), 255), bits, len(data)) + data
|
|
2150
|
+
+ m.tlv(_SPI.tlv["read_rx_answer"]["ns"], struct.pack("<Q", ns)), ignored)
|
|
2151
|
+
if op == ops["status"]: # lock-free
|
|
2152
|
+
_, ignored = t.tail()
|
|
2153
|
+
return self._answer(struct.pack("<BBBBBII", st.state, st.mode, st.bit_order, int(st.armed is not None),
|
|
2154
|
+
min(len(st.queue), 255), st.transactions, st.errors), ignored)
|
|
2155
|
+
if op == ops["reset"]:
|
|
2156
|
+
_, ignored = t.tail()
|
|
2157
|
+
if st.state == 0:
|
|
2158
|
+
raise unavailable("wrong_state")
|
|
2159
|
+
self.spi[fn] = SpiState(state=1, mode=st.mode, bit_order=st.bit_order)
|
|
2160
|
+
return self._answer(b"", ignored)
|
|
2161
|
+
return m.REJECTED, m.UNKNOWN_OPERATION, b""
|
|
2162
|
+
|
|
2163
|
+
def spi_transfer(self, fn: int, mosi: bytes, bits: int | None = None) -> bytes:
|
|
2164
|
+
"""TEST HOOK: a bus controller runs one transaction on spi-target `fn` (CS low, `bits` clocks - 8 a MOSI byte
|
|
2165
|
+
by default - CS high). -> the MISO bytes: the armed tx, 0 past it and when not armed. Armed: the MOSI bytes
|
|
2166
|
+
(bits / 8 rounded up) up to the armed length (more is an error) and the bits are queued, the wait ends; a full
|
|
2167
|
+
queue drops them (an error). Not armed: MOSI dropped, an error. Every transaction of a configured target
|
|
2168
|
+
counts; a target not configured sees nothing. 0 bits (CS edges without SCK) is no transaction: nothing counts,
|
|
2169
|
+
an arm keeps waiting."""
|
|
2170
|
+
st, (_, _, depth, _) = self.spi[fn], self.target_decl[fn]
|
|
2171
|
+
n = len(mosi)
|
|
2172
|
+
bits = 8 * n if bits is None else bits
|
|
2173
|
+
if st.state == 0 or bits == 0:
|
|
2174
|
+
return bytes(n)
|
|
2175
|
+
st.transactions += 1
|
|
2176
|
+
if st.armed is None:
|
|
2177
|
+
st.errors += 1
|
|
2178
|
+
return bytes(n)
|
|
2179
|
+
length, tx = st.armed
|
|
2180
|
+
st.armed = None
|
|
2181
|
+
got = (bits + 7) // 8
|
|
2182
|
+
if got > length:
|
|
2183
|
+
st.errors += 1 # past the armed length: dropped
|
|
2184
|
+
if len(st.queue) >= depth:
|
|
2185
|
+
st.errors += 1
|
|
2186
|
+
else:
|
|
2187
|
+
st.queue.append((bits, bytes(mosi[:min(got, length)]), self.now_ns()))
|
|
2188
|
+
return (tx + bytes(n))[:n]
|
|
2189
|
+
|
|
1892
2190
|
# ---- oep.probe.config -----------------------------------------------------------------------
|
|
1893
2191
|
def _config_op(self, fn: int, op: int, t: Take) -> tuple[int, int, bytes]:
|
|
1894
2192
|
if op == _CFG.op["get"]:
|
|
@@ -2113,8 +2411,10 @@ class Endpoint:
|
|
|
2113
2411
|
if key not in self.names or key == m.CORE_FN:
|
|
2114
2412
|
raise Reject(m.UNKNOWN_FUNCTION if key else m.MALFORMED)
|
|
2115
2413
|
elif tag == ITEM["idle"]:
|
|
2116
|
-
if len(value) != 3 or value[2] >
|
|
2414
|
+
if len(value) != 3 or value[2] > IDLE_MODE["output_high"]:
|
|
2117
2415
|
raise Reject(m.MALFORMED)
|
|
2416
|
+
if value[2] in (IDLE_MODE["output_low"], IDLE_MODE["output_high"]) and key in self.input_only:
|
|
2417
|
+
raise unsupported_fixed(m.tlv(_UNA["channel"], struct.pack("<H", key))) # cannot drive it (§1)
|
|
2118
2418
|
elif tag == ITEM["disable"] and key not in self._all_channels():
|
|
2119
2419
|
# a channel the firmware does not declare: unsupported, as idle (probe.config §1)
|
|
2120
2420
|
raise unsupported_fixed(m.tlv(_UNA["channel"], struct.pack("<H", key)))
|
|
@@ -2146,10 +2446,13 @@ class Endpoint:
|
|
|
2146
2446
|
old_idle = {key: v for (tag, key), v in self.config.items() if tag == ITEM["idle"]}
|
|
2147
2447
|
repark = (self.disabled - disabled) | {key for key in idles | set(old_idle) if new.get((ITEM["idle"], key)) != old_idle.get(key)}
|
|
2148
2448
|
self.config = new # the disable items first: a dropped plan's pins
|
|
2449
|
+
# idle before the plans (probe.config §2: idle, plan, uart, the at-boot attach): at boot every free channel,
|
|
2450
|
+
# later the ones whose idle changed (or enabled again); a gpio plan then takes a line in that state
|
|
2451
|
+
self._park(self._all_channels() if boot else repark)
|
|
2149
2452
|
for fn in old_plan_fns - set(plans): # are not parked when the same set disables them
|
|
2150
2453
|
self._drop_plan(fn)
|
|
2151
2454
|
for fn, assigned in plans.items():
|
|
2152
|
-
self.
|
|
2455
|
+
self._replace_plans({fn}, assigned)
|
|
2153
2456
|
self.plan_from_config.add(fn)
|
|
2154
2457
|
self._uart_plan_changed(fn)
|
|
2155
2458
|
for fn in self.uarts: # the uart item (or its going) on the planned UARTs no session set
|
|
@@ -2167,8 +2470,6 @@ class Endpoint:
|
|
|
2167
2470
|
self.flows.pop((port, key), None)
|
|
2168
2471
|
self.mixed_out.pop(port, None)
|
|
2169
2472
|
self.binds = binds
|
|
2170
|
-
if not boot:
|
|
2171
|
-
self._park(repark) # an idle item set, or a channel enabled again: now
|
|
2172
2473
|
for n, s in slots.items():
|
|
2173
2474
|
if s.attach == SLOT_ATTACH["at_boot"] and (boot or changed_slots is None or n in changed_slots):
|
|
2174
2475
|
self.slot_rt[n].evicted = False
|
|
@@ -2509,4 +2810,4 @@ class Endpoint:
|
|
|
2509
2810
|
SIMS = {"oep.wire.rvswd": "wire", "oep.wire.swio": "wire", "oep.target.riscv-dm": "dm",
|
|
2510
2811
|
"oep.target.console": "console", "oep.fixture.gpio": "gpio", "oep.fixture.uart": "uart",
|
|
2511
2812
|
"oep.probe.config": "config_op", "oep.fixture.logic": "capture", "oep.fixture.analog": "capture",
|
|
2512
|
-
"oep.fixture.capture-group": "group"}
|
|
2813
|
+
"oep.fixture.capture-group": "group", "oep.fixture.i2c-target": "i2c_op", "oep.fixture.spi-target": "spi_op"}
|