oep-client-python 0.0.1__tar.gz
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_python-0.0.1/.gitignore +6 -0
- oep_client_python-0.0.1/CHANGELOG.md +7 -0
- oep_client_python-0.0.1/LICENSE +21 -0
- oep_client_python-0.0.1/PKG-INFO +106 -0
- oep_client_python-0.0.1/README.ja.md +84 -0
- oep_client_python-0.0.1/pyproject.toml +41 -0
- oep_client_python-0.0.1/src/oep_client/__init__.py +5 -0
- oep_client_python-0.0.1/src/oep_client/v1/__init__.py +1 -0
- oep_client_python-0.0.1/src/oep_client/v1/__main__.py +40 -0
- oep_client_python-0.0.1/src/oep_client/v1/arm.py +269 -0
- oep_client_python-0.0.1/src/oep_client/v1/capture.py +365 -0
- oep_client_python-0.0.1/src/oep_client/v1/catalog.py +181 -0
- oep_client_python-0.0.1/src/oep_client/v1/ch32_flash.py +254 -0
- oep_client_python-0.0.1/src/oep_client/v1/cobs.py +73 -0
- oep_client_python-0.0.1/src/oep_client/v1/console.py +165 -0
- oep_client_python-0.0.1/src/oep_client/v1/core.py +172 -0
- oep_client_python-0.0.1/src/oep_client/v1/decode.py +65 -0
- oep_client_python-0.0.1/src/oep_client/v1/dump.py +157 -0
- oep_client_python-0.0.1/src/oep_client/v1/endpoint.py +1479 -0
- oep_client_python-0.0.1/src/oep_client/v1/esp32_targets.py +137 -0
- oep_client_python-0.0.1/src/oep_client/v1/fake.py +233 -0
- oep_client_python-0.0.1/src/oep_client/v1/fake_serial.py +105 -0
- oep_client_python-0.0.1/src/oep_client/v1/fake_serve.py +299 -0
- oep_client_python-0.0.1/src/oep_client/v1/fixture.py +121 -0
- oep_client_python-0.0.1/src/oep_client/v1/frames.py +91 -0
- oep_client_python-0.0.1/src/oep_client/v1/hid_stream.py +257 -0
- oep_client_python-0.0.1/src/oep_client/v1/host.py +321 -0
- oep_client_python-0.0.1/src/oep_client/v1/interfaces.py +119 -0
- oep_client_python-0.0.1/src/oep_client/v1/link.py +414 -0
- oep_client_python-0.0.1/src/oep_client/v1/message.py +235 -0
- oep_client_python-0.0.1/src/oep_client/v1/names.py +86 -0
- oep_client_python-0.0.1/src/oep_client/v1/registry.py +59 -0
- oep_client_python-0.0.1/src/oep_client/v1/riscv.py +430 -0
- oep_client_python-0.0.1/src/oep_client/v1/rp2350.py +85 -0
- oep_client_python-0.0.1/src/oep_client/v1/target.py +16 -0
- oep_client_python-0.0.1/src/oep_client/v1/uiapduino.py +121 -0
- oep_client_python-0.0.1/src/oep_client/v1/usb_stream.py +209 -0
- oep_client_python-0.0.1/tests/test_v1_capabilities.py +158 -0
- oep_client_python-0.0.1/tests/test_v1_capture.py +104 -0
- oep_client_python-0.0.1/tests/test_v1_cobs.py +47 -0
- oep_client_python-0.0.1/tests/test_v1_esp32_targets.py +52 -0
- oep_client_python-0.0.1/tests/test_v1_fake_spec.py +346 -0
- oep_client_python-0.0.1/tests/test_v1_flash_console.py +163 -0
- oep_client_python-0.0.1/tests/test_v1_hid_stream.py +141 -0
- oep_client_python-0.0.1/tests/test_v1_interfaces.py +360 -0
- oep_client_python-0.0.1/tests/test_v1_link_host.py +250 -0
- oep_client_python-0.0.1/tests/test_v1_link_serial.py +127 -0
- oep_client_python-0.0.1/tests/test_v1_session.py +291 -0
- oep_client_python-0.0.1/tests/test_v1_target_parts.py +317 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Changelog / 変更履歴
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.0.1
|
|
6
|
+
- (EN) First beta on PyPI (`pip install oep-client-python`, imported as `oep_client`). OEP v1 host: serial ports always COBS with the probe's raw bytes skipped as noise and opened exclusively (TIOCEXCL), USB vendor bulk / HID and TCP (a broker) with length frames, all under one `Host` (`link.open_host(target)`); the session rules (owner, resend with the same corr, `core.take` for the lock); the standard interfaces; `Host.on_capture` for run recorders; a fake probe (`oep_client.v1.endpoint`) and `python -m oep_client.v1.fake_serve` (pty / TCP) as the working spec for other hosts' tests.
|
|
7
|
+
- (JA) PyPI での最初のβ版(`pip install oep-client-python`、import は `oep_client`)。OEP v1 の host: シリアルの口は常に COBS(probe の生のバイトは雑音として捨てる、排他で開く TIOCEXCL)、USB の vendor bulk / HID と TCP(ブローカー)は長さつきのフレームで、どれも同じ `Host`(`link.open_host(target)`)。セッションの規則(owner、同じ corr での送り直し、ロックの取り方 `core.take`)、標準インターフェース、記録の受け口 `Host.on_capture`。ほかの host の試験のための「動く spec」として、偽の probe(`oep_client.v1.endpoint`)と `python -m oep_client.v1.fake_serve`(pty / TCP)。
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Open Embedded Probe
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: oep-client-python
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe
|
|
5
|
+
Project-URL: Homepage, https://github.com/Open-Embedded-Probe/oep-client-python
|
|
6
|
+
Project-URL: Repository, https://github.com/Open-Embedded-Probe/oep-client-python
|
|
7
|
+
Author: TANAKA Masayuki
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
13
|
+
Classifier: Topic :: Software Development :: Testing
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Requires-Dist: pyserial>=3.5
|
|
16
|
+
Requires-Dist: pyusb>=1.3
|
|
17
|
+
Provides-Extra: hid
|
|
18
|
+
Requires-Dist: hidapi>=0.14; extra == 'hid'
|
|
19
|
+
Provides-Extra: usb-async
|
|
20
|
+
Requires-Dist: libusb1>=3; extra == 'usb-async'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# OEP Python client
|
|
24
|
+
|
|
25
|
+
Open Embedded Probe の host 側。v1(oep-spec の `docs/oep-core.ja.md` と `docs/oep-if-*.ja.md`、固める候補の形)を話す。番号は oep-spec の
|
|
26
|
+
`generated/oep-v1/oep_v1_registry.py` をそのまま写した `oep_client.v1.registry` から取る。破壊的変更を前提とする
|
|
27
|
+
実験段階で、互換 API は約束しない。OEP を初めて読む人は oep-spec の `docs/review-guide.ja.md`(どこに何が書いてあるか)から。
|
|
28
|
+
|
|
29
|
+
target の知識は host にある、という OEP の分担に従う。probe は線と DMI / DP・AP の転送しか知らず、CH32 の flash
|
|
30
|
+
コントローラ、RAM ローダー、RP2350 の boot ROM、Cortex-M の debug レジスタなどはここに置く。
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
|
|
34
|
+
uv run pytest # in a checkout
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`import oep_client.v1` だけで使える(`sys.path` に `src/` を足す使い方は不要になった)。番号の表は `tools/sync_registry.sh` で
|
|
38
|
+
oep-spec から写す。PyPI の `oep-client` は別のプロジェクトなので、配布名は `oep-client-python`。
|
|
39
|
+
|
|
40
|
+
リリースは GitHub Actions の Release(workflow_dispatch、version = X.Y.Z か X.Y.ZbN): `tools/prepare_release.py` が
|
|
41
|
+
pyproject.toml と `oep_client.__version__` を書き換え、CHANGELOG.md の Unreleased をその版にし、試験と build の後に commit と tag、
|
|
42
|
+
GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md の Unreleased に (EN) / (JA) で書き足しておく。
|
|
43
|
+
|
|
44
|
+
## モジュール(`oep_client.v1`)
|
|
45
|
+
|
|
46
|
+
| モジュール | 中身 |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `host` | 要求と結果、session_id とロック、`call()`(失敗なら例外)、pipeline、エラーの階層(`OepError` / `Rejected` / `Failed`) |
|
|
49
|
+
| `link` | transport: シリアルの口(常に COBS + CRC、`0x00 <COBS> 0x00`、フレームの外は雑音として捨てる、排他で開く)、USB vendor bulk / HID と TCP(長さつきフレーム、§5.1 の立て直し)、corr による照合と送り直し、`open_host(target)` |
|
|
50
|
+
| `core` | インターフェースを名前で探す(キャッシュつき)、confirm、probe の describe(ラベル、transport の一覧)、ロックの取り方(`take`)、ピンの割り当て(plan)、`Interface` の土台 |
|
|
51
|
+
| `riscv` | `oep.wire.rvswd` / `oep.wire.swio`、`oep.target.riscv-dm`、リセット線の探索、GPIO 経由の attach |
|
|
52
|
+
| `console` | `oep.target.console`(位置つきのストリーム)と、バイト列として読む `ConsoleIO` |
|
|
53
|
+
| `fixture` | `oep.fixture.gpio` / `uart`(revision 1) |
|
|
54
|
+
| `capture` | `oep.fixture.capture`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
|
|
55
|
+
| `esp32_targets` | 独自インターフェース `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target`(oep-probe-arduino の ESP32 の I2C / SPI の target) |
|
|
56
|
+
| `decode` | キャプチャのチャネルの復号(I2C) |
|
|
57
|
+
| `registry` | oep-spec の番号の表から生成したモジュール(編集しない。oep-spec から写し直す) |
|
|
58
|
+
| `arm` | `oep.wire.swd`、`oep.target.arm-adi`、MEM-AP、Cortex-M の停止と関数呼び出し |
|
|
59
|
+
| `ch32_flash` | CH32 の書き込み(RAM ローダー、ページ単位の書き直し) |
|
|
60
|
+
| `rp2350` | RP2350 の boot ROM 経由の flash と reboot |
|
|
61
|
+
| `uiapduino` | UIAPduino のブートローダへの出入り |
|
|
62
|
+
| `catalog` / `names` / `interfaces` / `dump` | 能力の一覧と describe の形、表示 |
|
|
63
|
+
| `fake` / `endpoint` / `fake_serial` / `fake_serve` | 偽の probe(下の「偽の probe」) |
|
|
64
|
+
| `target` | 上の主なものを 1 か所から import する入口(最初の版に合わせて書いた呼び出し側のため) |
|
|
65
|
+
|
|
66
|
+
## 使い方の例
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from oep_client.v1 import core, link, riscv, ch32_flash
|
|
70
|
+
|
|
71
|
+
hst = link.open_host("/run/board-identify/by-id/esp32-series-30eda0e31108") # pipelining つき
|
|
72
|
+
# a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
|
|
73
|
+
core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
|
|
74
|
+
wire = riscv.Wire(hst, "oep.wire.rvswd")
|
|
75
|
+
conn, _ = wire.attach(halt=True)
|
|
76
|
+
dm = riscv.RiscvDm(hst, conn)
|
|
77
|
+
dm.reset_halt()
|
|
78
|
+
result = ch32_flash.program(hst, dm, open("sketch.bin", "rb").read(), ch32_flash.PROFILES["x035"])
|
|
79
|
+
dm.reset(confirm=True)
|
|
80
|
+
wire.detach(conn)
|
|
81
|
+
hst.end()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
能力の一覧は `uv run python -m oep_client.v1 dump --port <probe>`(`--fake p4-x035` でハードウェアなし)。
|
|
85
|
+
実機での一通りの確認は ArduinoCore-CH32 の `tests/manual/oep_smoke/`(`oep_smoke.py`、`oep_probe_checks.py`)。
|
|
86
|
+
|
|
87
|
+
## 偽の probe(動く spec)
|
|
88
|
+
|
|
89
|
+
`endpoint.Endpoint` は oep-spec の規範どおりに答える偽の probe で、ch32rv・この client・probe の firmware を突き合わせる
|
|
90
|
+
「動く spec」として使う(spec が変わったら、probe の firmware より先にここを合わせる)。`fake` は宣言の例(profile:
|
|
91
|
+
`p4-x035`、`esp32-v003`、`p4-bench` = スロット 3 か所と席 2 つの架空の治具)、`fake_serial` はシリアルの口のバイトの側(COBS の
|
|
92
|
+
候補、生のバイトと bind、セッション中の停止と再開)。
|
|
93
|
+
|
|
94
|
+
外のプログラムの試験には `fake_serve` を子プロセスで使う:
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
uv run python -m oep_client.v1.fake_serve --pty --profile p4-bench --slot x035 --bind last-reset \
|
|
98
|
+
--console 'uptime %d\r\n' --every 100
|
|
99
|
+
# 最初の行: PTY /dev/pts/N(--tcp 0 なら PORT n)。stdin を閉じると終わる
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
pty がシリアルの口(host が TIOCEXCL を掛けて開く)、`--tcp PORT` は `--framing cobs`(シリアルの口)か `--framing length`
|
|
103
|
+
(vendor bulk / TCP の形)。故障の注入は `--drop N`(N 番目の答えを 1 回出さない。要求は実行済みなので送り直しは覚えた答えを
|
|
104
|
+
受ける)、`--noise TEXT`(答えの前に雑音)、`--corrupt N`(N 番目の答えの CRC を 1 回壊す)。ほかは `--help`。
|
|
105
|
+
|
|
106
|
+
v0 の client(`oep_client.v0`)は 2026-09-26 に消した(git の履歴に残る)。v0 を話す probe はもう無い。
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# OEP Python client
|
|
2
|
+
|
|
3
|
+
Open Embedded Probe の host 側。v1(oep-spec の `docs/oep-core.ja.md` と `docs/oep-if-*.ja.md`、固める候補の形)を話す。番号は oep-spec の
|
|
4
|
+
`generated/oep-v1/oep_v1_registry.py` をそのまま写した `oep_client.v1.registry` から取る。破壊的変更を前提とする
|
|
5
|
+
実験段階で、互換 API は約束しない。OEP を初めて読む人は oep-spec の `docs/review-guide.ja.md`(どこに何が書いてあるか)から。
|
|
6
|
+
|
|
7
|
+
target の知識は host にある、という OEP の分担に従う。probe は線と DMI / DP・AP の転送しか知らず、CH32 の flash
|
|
8
|
+
コントローラ、RAM ローダー、RP2350 の boot ROM、Cortex-M の debug レジスタなどはここに置く。
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pip install oep-client-python # PyPI (import oep_client); a checkout: pip install -e <checkout>
|
|
12
|
+
uv run pytest # in a checkout
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`import oep_client.v1` だけで使える(`sys.path` に `src/` を足す使い方は不要になった)。番号の表は `tools/sync_registry.sh` で
|
|
16
|
+
oep-spec から写す。PyPI の `oep-client` は別のプロジェクトなので、配布名は `oep-client-python`。
|
|
17
|
+
|
|
18
|
+
リリースは GitHub Actions の Release(workflow_dispatch、version = X.Y.Z か X.Y.ZbN): `tools/prepare_release.py` が
|
|
19
|
+
pyproject.toml と `oep_client.__version__` を書き換え、CHANGELOG.md の Unreleased をその版にし、試験と build の後に commit と tag、
|
|
20
|
+
GitHub Release、PyPI(Trusted Publishing)へ出す。変更は CHANGELOG.md の Unreleased に (EN) / (JA) で書き足しておく。
|
|
21
|
+
|
|
22
|
+
## モジュール(`oep_client.v1`)
|
|
23
|
+
|
|
24
|
+
| モジュール | 中身 |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `host` | 要求と結果、session_id とロック、`call()`(失敗なら例外)、pipeline、エラーの階層(`OepError` / `Rejected` / `Failed`) |
|
|
27
|
+
| `link` | transport: シリアルの口(常に COBS + CRC、`0x00 <COBS> 0x00`、フレームの外は雑音として捨てる、排他で開く)、USB vendor bulk / HID と TCP(長さつきフレーム、§5.1 の立て直し)、corr による照合と送り直し、`open_host(target)` |
|
|
28
|
+
| `core` | インターフェースを名前で探す(キャッシュつき)、confirm、probe の describe(ラベル、transport の一覧)、ロックの取り方(`take`)、ピンの割り当て(plan)、`Interface` の土台 |
|
|
29
|
+
| `riscv` | `oep.wire.rvswd` / `oep.wire.swio`、`oep.target.riscv-dm`、リセット線の探索、GPIO 経由の attach |
|
|
30
|
+
| `console` | `oep.target.console`(位置つきのストリーム)と、バイト列として読む `ConsoleIO` |
|
|
31
|
+
| `fixture` | `oep.fixture.gpio` / `uart`(revision 1) |
|
|
32
|
+
| `capture` | `oep.fixture.capture`(revision 1、oep-spec の oep-if-capture)。読んだ区画は `Host.on_capture` の callback に `CaptureRecord` で渡る(記録の受け口。wireskein には依存しない) |
|
|
33
|
+
| `esp32_targets` | 独自インターフェース `io.github.ch32-riscv-ug.esp32.i2c-target` / `spi-target`(oep-probe-arduino の ESP32 の I2C / SPI の target) |
|
|
34
|
+
| `decode` | キャプチャのチャネルの復号(I2C) |
|
|
35
|
+
| `registry` | oep-spec の番号の表から生成したモジュール(編集しない。oep-spec から写し直す) |
|
|
36
|
+
| `arm` | `oep.wire.swd`、`oep.target.arm-adi`、MEM-AP、Cortex-M の停止と関数呼び出し |
|
|
37
|
+
| `ch32_flash` | CH32 の書き込み(RAM ローダー、ページ単位の書き直し) |
|
|
38
|
+
| `rp2350` | RP2350 の boot ROM 経由の flash と reboot |
|
|
39
|
+
| `uiapduino` | UIAPduino のブートローダへの出入り |
|
|
40
|
+
| `catalog` / `names` / `interfaces` / `dump` | 能力の一覧と describe の形、表示 |
|
|
41
|
+
| `fake` / `endpoint` / `fake_serial` / `fake_serve` | 偽の probe(下の「偽の probe」) |
|
|
42
|
+
| `target` | 上の主なものを 1 か所から import する入口(最初の版に合わせて書いた呼び出し側のため) |
|
|
43
|
+
|
|
44
|
+
## 使い方の例
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from oep_client.v1 import core, link, riscv, ch32_flash
|
|
48
|
+
|
|
49
|
+
hst = link.open_host("/run/board-identify/by-id/esp32-series-30eda0e31108") # pipelining つき
|
|
50
|
+
# a serial port (always COBS), "tcp://127.0.0.1:PORT" (a broker), "usb" / "usb:303a:0002[:SERIAL]" (vendor, then HID)
|
|
51
|
+
core.take(hst, 30000, owner="flash script") # the only way in: force; else wait out the lease, name the holder
|
|
52
|
+
wire = riscv.Wire(hst, "oep.wire.rvswd")
|
|
53
|
+
conn, _ = wire.attach(halt=True)
|
|
54
|
+
dm = riscv.RiscvDm(hst, conn)
|
|
55
|
+
dm.reset_halt()
|
|
56
|
+
result = ch32_flash.program(hst, dm, open("sketch.bin", "rb").read(), ch32_flash.PROFILES["x035"])
|
|
57
|
+
dm.reset(confirm=True)
|
|
58
|
+
wire.detach(conn)
|
|
59
|
+
hst.end()
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
能力の一覧は `uv run python -m oep_client.v1 dump --port <probe>`(`--fake p4-x035` でハードウェアなし)。
|
|
63
|
+
実機での一通りの確認は ArduinoCore-CH32 の `tests/manual/oep_smoke/`(`oep_smoke.py`、`oep_probe_checks.py`)。
|
|
64
|
+
|
|
65
|
+
## 偽の probe(動く spec)
|
|
66
|
+
|
|
67
|
+
`endpoint.Endpoint` は oep-spec の規範どおりに答える偽の probe で、ch32rv・この client・probe の firmware を突き合わせる
|
|
68
|
+
「動く spec」として使う(spec が変わったら、probe の firmware より先にここを合わせる)。`fake` は宣言の例(profile:
|
|
69
|
+
`p4-x035`、`esp32-v003`、`p4-bench` = スロット 3 か所と席 2 つの架空の治具)、`fake_serial` はシリアルの口のバイトの側(COBS の
|
|
70
|
+
候補、生のバイトと bind、セッション中の停止と再開)。
|
|
71
|
+
|
|
72
|
+
外のプログラムの試験には `fake_serve` を子プロセスで使う:
|
|
73
|
+
|
|
74
|
+
```sh
|
|
75
|
+
uv run python -m oep_client.v1.fake_serve --pty --profile p4-bench --slot x035 --bind last-reset \
|
|
76
|
+
--console 'uptime %d\r\n' --every 100
|
|
77
|
+
# 最初の行: PTY /dev/pts/N(--tcp 0 なら PORT n)。stdin を閉じると終わる
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
pty がシリアルの口(host が TIOCEXCL を掛けて開く)、`--tcp PORT` は `--framing cobs`(シリアルの口)か `--framing length`
|
|
81
|
+
(vendor bulk / TCP の形)。故障の注入は `--drop N`(N 番目の答えを 1 回出さない。要求は実行済みなので送り直しは覚えた答えを
|
|
82
|
+
受ける)、`--noise TEXT`(答えの前に雑音)、`--corrupt N`(N 番目の答えの CRC を 1 回壊す)。ほかは `--help`。
|
|
83
|
+
|
|
84
|
+
v0 の client(`oep_client.v0`)は 2026-09-26 に消した(git の履歴に残る)。v0 を話す probe はもう無い。
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "oep-client-python"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "Open Embedded Probe (OEP) v1 host: serial / USB / TCP transports, the session rules, the standard interfaces, and a fake probe"
|
|
9
|
+
readme = "README.ja.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "TANAKA Masayuki" },
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Software Development :: Embedded Systems",
|
|
19
|
+
"Topic :: Software Development :: Testing",
|
|
20
|
+
]
|
|
21
|
+
dependencies = ["pyserial>=3.5", "pyusb>=1.3"]
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
usb-async = ["libusb1>=3"] # queued asynchronous IN transfers: streaming near the USB HS ceiling
|
|
25
|
+
hid = ["hidapi>=0.14"] # the vendor HID way in through the OS HID driver (Windows without WinUSB, Linux hidraw)
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
oep = "oep_client.v1.__main__:main"
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/Open-Embedded-Probe/oep-client-python"
|
|
32
|
+
Repository = "https://github.com/Open-Embedded-Probe/oep-client-python"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/oep_client"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.sdist]
|
|
38
|
+
only-include = ["src/oep_client", "tests", "README.ja.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
|
|
39
|
+
|
|
40
|
+
[dependency-groups]
|
|
41
|
+
dev = ["pytest>=8"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Draft: capability discovery by name (oep-spec capability-*.ja.md). No hardware yet."""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""Draft OEP capability discovery by name, on an in-process fake or a v1 draft probe.
|
|
2
|
+
|
|
3
|
+
uv run python -m oep_client.v1 dump --port /run/board-identify/by-id/<probe>
|
|
4
|
+
uv run python -m oep_client.v1 dump --fake p4-x035
|
|
5
|
+
uv run python -m oep_client.v1 dump --fake esp32-v003 --prefix oep.fixture
|
|
6
|
+
uv run python -m oep_client.v1 dump --fake p4-x035 --prefix oep.target --json
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from . import dump, fake, host, link
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def main(argv=None) -> int:
|
|
18
|
+
parser = argparse.ArgumentParser(description="OEP capability discovery (draft)")
|
|
19
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
20
|
+
d = sub.add_parser("dump", help="list and describe every interface a probe offers")
|
|
21
|
+
src = d.add_mutually_exclusive_group(required=True)
|
|
22
|
+
src.add_argument("--fake", choices=sorted(fake.PROFILES), help="in-process example probe")
|
|
23
|
+
src.add_argument("--port", help="a probe: a serial port, tcp://HOST:PORT or usb[:VID:PID] (lock-free reads only)")
|
|
24
|
+
d.add_argument("--prefix", default="", help="only names under this namespace (label boundaries)")
|
|
25
|
+
d.add_argument("--exact", action="store_true", help="the prefix is a whole name")
|
|
26
|
+
d.add_argument("--json", action="store_true", help="machine-readable output")
|
|
27
|
+
args = parser.parse_args(argv)
|
|
28
|
+
|
|
29
|
+
if args.fake:
|
|
30
|
+
call = fake.PROFILES[args.fake]().call
|
|
31
|
+
else:
|
|
32
|
+
hst = link.open_host(args.port)
|
|
33
|
+
call = lambda fn, op, payload: hst.request(fn, op, payload, locked=False).payload # noqa: E731
|
|
34
|
+
caps = dump.collect(call, args.prefix, args.exact)
|
|
35
|
+
sys.stdout.write(dump.to_json(caps) + "\n" if args.json else dump.to_text(caps))
|
|
36
|
+
return 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
sys.exit(main())
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""oep.wire.swd and oep.target.arm-adi, revision 1 (oep-spec oep-if-debug §1, §5-§6).
|
|
2
|
+
|
|
3
|
+
The probe moves raw DP / AP transfers and MEM-AP blocks; everything above - power-up, SELECT (ADIv5 APSEL/APBANKSEL or
|
|
4
|
+
ADIv6 AP addresses), CSW, the Cortex-M debug registers - is here, as target knowledge belongs to the host.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import struct
|
|
10
|
+
|
|
11
|
+
from . import host as h, message as m, registry as reg
|
|
12
|
+
from .core import Interface
|
|
13
|
+
from .riscv import OK, TargetError, WireBase, check, ran, status_name # noqa: F401
|
|
14
|
+
|
|
15
|
+
DP_DPIDR = DP_ABORT = 0x0
|
|
16
|
+
DP_CTRL_STAT = 0x4
|
|
17
|
+
DP_SELECT = 0x8
|
|
18
|
+
DP_RDBUFF = 0xC
|
|
19
|
+
_ADI = reg.TARGET_ARM_ADI
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SwdWire(WireBase):
|
|
23
|
+
NAME = "oep.wire.swd"
|
|
24
|
+
TAG_TARGETSEL = reg.WIRE_SWD.tlv["attach"]["targetsel"]
|
|
25
|
+
|
|
26
|
+
def __init__(self, hst: h.Host):
|
|
27
|
+
super().__init__(hst)
|
|
28
|
+
self.speed_hz = 0
|
|
29
|
+
self.existing = False
|
|
30
|
+
|
|
31
|
+
def attach(self, targetsel: int | None = None, max_speed: int | None = None,
|
|
32
|
+
pins: tuple[int, int] | None = None) -> tuple[int, int, bool]:
|
|
33
|
+
"""-> (connection, DPIDR, woke from dormant). targetsel (multidrop) and max_speed go as critical TLVs: a probe
|
|
34
|
+
that cannot honour them refuses. self.existing: the wire was attached already (its connection returned)."""
|
|
35
|
+
body = self._speed_tlv(max_speed) + self._pins_tlv(pins)
|
|
36
|
+
if targetsel is not None:
|
|
37
|
+
body += m.tlv(self.TAG_TARGETSEL, struct.pack("<I", targetsel), critical=True)
|
|
38
|
+
rd = m.Reader(self._call(self.ATTACH, body).payload)
|
|
39
|
+
conn, dpidr, flags, self.speed_hz = rd.take("HIBI")
|
|
40
|
+
self.existing = bool(flags & 2)
|
|
41
|
+
rd.tail()
|
|
42
|
+
return conn, dpidr, bool(flags & 1)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class AdiError(TargetError):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def transfer_reads(steps: bytes) -> list[bool]:
|
|
50
|
+
"""Per transfer in a packed list: True for a read (1 byte), False for a write (1 + 4 bytes)."""
|
|
51
|
+
out, at = [], 0
|
|
52
|
+
while at < len(steps):
|
|
53
|
+
read = bool(steps[at] & 2)
|
|
54
|
+
out.append(read)
|
|
55
|
+
at += 1 if read else 5
|
|
56
|
+
if at != len(steps):
|
|
57
|
+
raise ValueError("the transfer list ends inside a write")
|
|
58
|
+
return out
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ArmAdi(Interface):
|
|
62
|
+
NAME = "oep.target.arm-adi"
|
|
63
|
+
REVISION = 1
|
|
64
|
+
TRANSFER, READ_BLOCK, WRITE_BLOCK = _ADI.op["transfer"], _ADI.op["read_block"], _ADI.op["write_block"]
|
|
65
|
+
|
|
66
|
+
def __init__(self, hst: h.Host, conn: int, adiv6: bool = False):
|
|
67
|
+
super().__init__(hst, prefix=struct.pack("<H", conn))
|
|
68
|
+
self.conn = conn
|
|
69
|
+
self.adiv6 = adiv6
|
|
70
|
+
self._select: int | None = None
|
|
71
|
+
|
|
72
|
+
# ---- raw transfers ----
|
|
73
|
+
@staticmethod
|
|
74
|
+
def req(ap: bool, read: bool, addr: int, value: int = 0) -> bytes:
|
|
75
|
+
b = bytes([int(ap) | (int(read) << 1) | (((addr >> 2) & 3) << 2)])
|
|
76
|
+
return b if read else b + struct.pack("<I", value)
|
|
77
|
+
|
|
78
|
+
def transfer(self, steps: bytes) -> list[int]:
|
|
79
|
+
"""A packed transfer list (req() concatenated). -> the values read, in order (an AP read's value arrives one
|
|
80
|
+
transfer late, as on the wire). A list that stopped raises AdiError (status, done, the values it read, and
|
|
81
|
+
self.last_ack = the raw ACK of the last transfer)."""
|
|
82
|
+
reads = transfer_reads(steps)
|
|
83
|
+
r = self._request(self.TRANSFER, struct.pack("<H", len(reads)) + steps)
|
|
84
|
+
rd = ran(r)
|
|
85
|
+
done, status, self.last_ack = rd.take("HBB")
|
|
86
|
+
values = rd.words(sum(reads[:done]))
|
|
87
|
+
rd.tail()
|
|
88
|
+
if status != OK or not r.succeeded or done != len(reads):
|
|
89
|
+
raise AdiError(f"transfer (ack {self.last_ack:#x})", status, r, done=done, values=values)
|
|
90
|
+
return values
|
|
91
|
+
def dp_read(self, addr: int) -> int:
|
|
92
|
+
return self.transfer(self.req(False, True, addr))[0]
|
|
93
|
+
|
|
94
|
+
def dp_write(self, addr: int, value: int) -> None:
|
|
95
|
+
self.transfer(self.req(False, False, addr, value))
|
|
96
|
+
|
|
97
|
+
def select(self, value: int) -> None:
|
|
98
|
+
if value != self._select:
|
|
99
|
+
self.dp_write(DP_SELECT, value)
|
|
100
|
+
self._select = value
|
|
101
|
+
|
|
102
|
+
def _ap_select(self, ap: int, reg: int) -> None:
|
|
103
|
+
"""ADIv5: ap = APSEL (0..255), reg = register offset in the AP. ADIv6: ap = the AP's base address."""
|
|
104
|
+
if self.adiv6:
|
|
105
|
+
self.select((ap + reg) & ~0xF)
|
|
106
|
+
else:
|
|
107
|
+
self.select((ap << 24) | (reg & 0xF0))
|
|
108
|
+
|
|
109
|
+
def ap_read(self, ap: int, reg: int) -> int:
|
|
110
|
+
self._ap_select(ap, reg)
|
|
111
|
+
return self.transfer(self.req(True, True, reg) + self.req(False, True, DP_RDBUFF))[1] # posted
|
|
112
|
+
|
|
113
|
+
def ap_write(self, ap: int, reg: int, value: int) -> None:
|
|
114
|
+
self._ap_select(ap, reg)
|
|
115
|
+
self.transfer(self.req(True, False, reg, value))
|
|
116
|
+
|
|
117
|
+
def power_up(self) -> int:
|
|
118
|
+
"""Clear sticky errors, request debug + system power, wait for both acks. -> CTRL/STAT"""
|
|
119
|
+
self.dp_write(DP_ABORT, 0x1E)
|
|
120
|
+
self._select = None
|
|
121
|
+
self.select(0)
|
|
122
|
+
self.dp_write(DP_CTRL_STAT, 0x50000000)
|
|
123
|
+
for _ in range(100):
|
|
124
|
+
cs = self.dp_read(DP_CTRL_STAT)
|
|
125
|
+
if (cs >> 29) & 1 and (cs >> 31) & 1:
|
|
126
|
+
return cs
|
|
127
|
+
raise h.OepError(f"no power-up ack: CTRL/STAT {cs:#010x}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class MemAp:
|
|
131
|
+
"""One MEM-AP (ADIv5 APSEL or ADIv6 base address) with 32-bit, auto-incrementing access."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, adi: ArmAdi, ap: int, csw_set: int = 0, csw_clear: int = 0):
|
|
134
|
+
"""csw_set / csw_clear: target-specific CSW bits (protection, security). The RP2350's AHB-APs come up
|
|
135
|
+
non-secure (CSW bit 30), and its SRAM then faults: pass csw_clear=1 << 30 there (2026-09-24)."""
|
|
136
|
+
self.adi, self.ap = adi, ap
|
|
137
|
+
self.base = 0xD00 if adi.adiv6 else 0x00 # CSW, TAR, DRW at base + 0x0 / 0x4 / 0xC
|
|
138
|
+
csw = adi.ap_read(ap, self.base)
|
|
139
|
+
adi.ap_write(ap, self.base, (((csw & ~0x37) | 0x12) | csw_set) & ~csw_clear) # 32 bits, AddrInc single
|
|
140
|
+
adi._ap_select(ap, self.base) # the bank the block operations assume
|
|
141
|
+
# Words per block operation, from the probe's frame limit: request header 6 + session 4 + connection 2 +
|
|
142
|
+
# address 4 + count 2 on the way in (the answer's 5 + done 2 + status 1 is smaller).
|
|
143
|
+
from .core import confirm
|
|
144
|
+
self.chunk = max(1, (confirm(adi.host)["max_frame"] - 18) // 4)
|
|
145
|
+
|
|
146
|
+
def write_many(self, pairs: list[tuple[int, int]]) -> None:
|
|
147
|
+
"""Scattered single-word writes in one transfer list (TAR, DRW per word, RDBUFF at the end so the last one
|
|
148
|
+
has landed): one round trip instead of one per word - what a debug-register sequence needs."""
|
|
149
|
+
self.adi._ap_select(self.ap, self.base)
|
|
150
|
+
steps = b"".join(self.adi.req(True, False, self.base + 0x4, a) + self.adi.req(True, False, self.base + 0xC, v)
|
|
151
|
+
for a, v in pairs)
|
|
152
|
+
self.adi.transfer(steps + self.adi.req(False, True, DP_RDBUFF))
|
|
153
|
+
|
|
154
|
+
def read_block(self, address: int, words: int) -> list[int]:
|
|
155
|
+
out, chunk = [], self.chunk
|
|
156
|
+
for off in range(0, words, chunk):
|
|
157
|
+
self.adi._ap_select(self.ap, self.base)
|
|
158
|
+
n = min(chunk, words - off)
|
|
159
|
+
r = self.adi._request(ArmAdi.READ_BLOCK, struct.pack("<IH", address + off * 4, n))
|
|
160
|
+
rd = ran(r)
|
|
161
|
+
done, status = rd.take("HB")
|
|
162
|
+
got = rd.words(done)
|
|
163
|
+
rd.tail()
|
|
164
|
+
if status != OK or not r.succeeded or done != n:
|
|
165
|
+
raise AdiError("read_block", status, r, done=off + done, values=out + got)
|
|
166
|
+
out += got
|
|
167
|
+
return out
|
|
168
|
+
|
|
169
|
+
def write_block(self, address: int, values: list[int]) -> None:
|
|
170
|
+
chunk = self.chunk
|
|
171
|
+
for off in range(0, len(values), chunk):
|
|
172
|
+
self.adi._ap_select(self.ap, self.base)
|
|
173
|
+
part = values[off:off + chunk]
|
|
174
|
+
r = self.adi._request(ArmAdi.WRITE_BLOCK, struct.pack("<IH", address + off * 4, len(part))
|
|
175
|
+
+ struct.pack(f"<{len(part)}I", *part))
|
|
176
|
+
rd = ran(r)
|
|
177
|
+
done, status = rd.take("HB")
|
|
178
|
+
rd.tail()
|
|
179
|
+
if status != OK or not r.succeeded:
|
|
180
|
+
raise AdiError("write_block", status, r, done=off + done)
|
|
181
|
+
|
|
182
|
+
def read32(self, address: int) -> int:
|
|
183
|
+
return self.read_block(address, 1)[0]
|
|
184
|
+
|
|
185
|
+
def write32(self, address: int, value: int) -> None:
|
|
186
|
+
self.write_block(address, [value])
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class CortexM:
|
|
190
|
+
"""Armv7-M / Armv8-M core debug through a MEM-AP: halt, resume, core registers through DCRSR / DCRDR, and running
|
|
191
|
+
a function on the target (arguments in r0-r3, LR at a BKPT in RAM, run until the core halts on it) - the way a
|
|
192
|
+
host-side flash algorithm drives the target's own ROM or a RAM loader."""
|
|
193
|
+
|
|
194
|
+
DHCSR, DCRSR, DCRDR, AIRCR = 0xE000EDF0, 0xE000EDF4, 0xE000EDF8, 0xE000ED0C
|
|
195
|
+
KEY = 0xA05F0000
|
|
196
|
+
C_DEBUGEN, C_HALT, C_MASKINTS = 1, 2, 8
|
|
197
|
+
S_REGRDY, S_HALT = 1 << 16, 1 << 17
|
|
198
|
+
SP, LR, PC, XPSR = 13, 14, 15, 16
|
|
199
|
+
|
|
200
|
+
def __init__(self, mem: MemAp, bkpt_at: int, stack_top: int):
|
|
201
|
+
"""bkpt_at: a word of RAM the target does not need (the return breakpoint goes there); stack_top: where the
|
|
202
|
+
called function's stack starts (its RAM below is clobbered)."""
|
|
203
|
+
self.mem, self.bkpt_at, self.stack_top = mem, bkpt_at, stack_top
|
|
204
|
+
|
|
205
|
+
def _wait(self, mask: int, timeout: float):
|
|
206
|
+
import time
|
|
207
|
+
deadline = time.monotonic() + timeout
|
|
208
|
+
while True:
|
|
209
|
+
v = self.mem.read32(self.DHCSR)
|
|
210
|
+
if v & mask:
|
|
211
|
+
return v
|
|
212
|
+
if time.monotonic() > deadline:
|
|
213
|
+
raise TimeoutError(f"DHCSR {v:#010x}: waiting for {mask:#x}")
|
|
214
|
+
|
|
215
|
+
def halted(self) -> bool:
|
|
216
|
+
return bool(self.mem.read32(self.DHCSR) & self.S_HALT)
|
|
217
|
+
|
|
218
|
+
def halt(self) -> None:
|
|
219
|
+
self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
|
|
220
|
+
self._wait(self.S_HALT, 1.0)
|
|
221
|
+
|
|
222
|
+
def resume(self, mask_ints: bool = False) -> None:
|
|
223
|
+
self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | (self.C_MASKINTS if mask_ints else 0))
|
|
224
|
+
|
|
225
|
+
def release(self) -> None:
|
|
226
|
+
"""Run, debug off. C_MASKINTS is cleared first: it lives in the debug domain and survives every reset but
|
|
227
|
+
power-on, and firmware left with it set runs without SysTick / USB interrupts (RP2350, 2026-09-24)."""
|
|
228
|
+
self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
|
|
229
|
+
self.mem.write32(self.DHCSR, self.KEY)
|
|
230
|
+
|
|
231
|
+
def reg(self, n: int) -> int:
|
|
232
|
+
self.mem.write32(self.DCRSR, n)
|
|
233
|
+
self._wait(self.S_REGRDY, 1.0)
|
|
234
|
+
return self.mem.read32(self.DCRDR)
|
|
235
|
+
|
|
236
|
+
def set_reg(self, n: int, value: int) -> None:
|
|
237
|
+
self.mem.write32(self.DCRDR, value)
|
|
238
|
+
self.mem.write32(self.DCRSR, (1 << 16) | n)
|
|
239
|
+
self._wait(self.S_REGRDY, 1.0)
|
|
240
|
+
|
|
241
|
+
def prepare_call(self, fn: int, args=()) -> None:
|
|
242
|
+
"""Registers for fn(args...): r0-r3, SP, LR to the breakpoint, PC, Thumb bit, no active exception. The
|
|
243
|
+
writes go out as one transfer list; a register write takes the core a few cycles and each SWD transfer
|
|
244
|
+
takes microseconds, so S_REGRDY is checked once at the end rather than after each."""
|
|
245
|
+
xpsr = (self.reg(self.XPSR) | 1 << 24) & ~0x1FF
|
|
246
|
+
regs = [*enumerate(args), (self.SP, self.stack_top), (self.LR, self.bkpt_at | 1), (self.PC, fn & ~1),
|
|
247
|
+
(self.XPSR, xpsr)]
|
|
248
|
+
pairs = [(self.bkpt_at, 0xBE00BE00)] # bkpt #0, twice
|
|
249
|
+
for n, value in regs:
|
|
250
|
+
pairs += [(self.DCRDR, value), (self.DCRSR, (1 << 16) | n)]
|
|
251
|
+
self.mem.write_many(pairs)
|
|
252
|
+
self._wait(self.S_REGRDY, 1.0)
|
|
253
|
+
|
|
254
|
+
def call(self, fn: int, args=(), timeout: float = 10.0) -> int:
|
|
255
|
+
"""Run fn(args...) on the halted core with interrupts masked (their handlers may live in flash that the call
|
|
256
|
+
makes unreadable), wait for the breakpoint, clear the mask, return r0."""
|
|
257
|
+
self.prepare_call(fn, args)
|
|
258
|
+
self.resume(mask_ints=True)
|
|
259
|
+
self._wait(self.S_HALT, timeout)
|
|
260
|
+
self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT) # MASKINTS off while halted
|
|
261
|
+
pc = self.reg(self.PC)
|
|
262
|
+
if pc & ~3 != self.bkpt_at:
|
|
263
|
+
raise h.OepError(f"stopped at {pc:#010x}, not at the return breakpoint")
|
|
264
|
+
return self.reg(0)
|
|
265
|
+
|
|
266
|
+
def sys_reset(self) -> None:
|
|
267
|
+
"""AIRCR.SYSRESETREQ: the core restarts; debug-domain state (DHCSR) survives, so clear the mask first."""
|
|
268
|
+
self.mem.write32(self.DHCSR, self.KEY | self.C_DEBUGEN | self.C_HALT)
|
|
269
|
+
self.mem.write32(self.AIRCR, 0x05FA0004)
|