brickproof-fw 0.1.0__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.
- brickproof/__init__.py +3 -0
- brickproof/adapter.py +399 -0
- brickproof/cli.py +304 -0
- brickproof/demo/README.md +23 -0
- brickproof/demo/__init__.py +3 -0
- brickproof/demo/adapter.yaml +59 -0
- brickproof/demo/bug-consume_first.yaml +10 -0
- brickproof/demo/bug-early_unpg.yaml +11 -0
- brickproof/demo/bug-naive_floor.yaml +10 -0
- brickproof/demo/bug-no_erase.yaml +11 -0
- brickproof/demo/bug-no_unlock.yaml +11 -0
- brickproof/demo/bug-pop_after_switch.yaml +11 -0
- brickproof/demo/bug-protected_slot.yaml +9 -0
- brickproof/demo/bug-resume_copy.yaml +13 -0
- brickproof/demo/bug-retry_limit.yaml +14 -0
- brickproof/demo/bug-self_lock.yaml +14 -0
- brickproof/demo/bug-trust_header.yaml +10 -0
- brickproof/demo/build.py +101 -0
- brickproof/demo/f411-bug-consume_first.yaml +10 -0
- brickproof/demo/f411-bug-early_unpg.yaml +11 -0
- brickproof/demo/f411-bug-naive_floor.yaml +10 -0
- brickproof/demo/f411-bug-no_erase.yaml +12 -0
- brickproof/demo/f411-bug-no_unlock.yaml +11 -0
- brickproof/demo/f411-bug-pop_after_switch.yaml +11 -0
- brickproof/demo/f411-bug-protected_slot.yaml +9 -0
- brickproof/demo/f411-bug-retry_limit.yaml +14 -0
- brickproof/demo/f411-bug-self_lock.yaml +14 -0
- brickproof/demo/f411-bug-trust_header.yaml +10 -0
- brickproof/demo/f411-bug-watchdog.yaml +14 -0
- brickproof/demo/f411-hold_pg.yaml +11 -0
- brickproof/demo/f411-watchdog.yaml +7 -0
- brickproof/demo/f411.yaml +62 -0
- brickproof/demo/hold_pg.yaml +11 -0
- brickproof/demo/prebuilt/app_v1.img +0 -0
- brickproof/demo/prebuilt/app_v2.img +0 -0
- brickproof/demo/prebuilt/bootloader.elf +0 -0
- brickproof/demo/prebuilt/bootloader_bug_consume_first.elf +0 -0
- brickproof/demo/prebuilt/bootloader_bug_early_unpg.elf +0 -0
- brickproof/demo/prebuilt/bootloader_bug_naive_floor.elf +0 -0
- brickproof/demo/prebuilt/bootloader_bug_no_erase.elf +0 -0
- brickproof/demo/prebuilt/bootloader_bug_no_unlock.elf +0 -0
- brickproof/demo/prebuilt/bootloader_bug_pop_after_switch.elf +0 -0
- brickproof/demo/prebuilt/bootloader_bug_resume_copy.elf +0 -0
- brickproof/demo/prebuilt/bootloader_bug_retry_limit.elf +0 -0
- brickproof/demo/prebuilt/bootloader_bug_self_lock.elf +0 -0
- brickproof/demo/prebuilt/bootloader_bug_trust_header.elf +0 -0
- brickproof/demo/prebuilt/bootloader_hold_pg.elf +0 -0
- brickproof/demo/prebuilt/f411_app_v1.img +0 -0
- brickproof/demo/prebuilt/f411_app_v2.img +0 -0
- brickproof/demo/prebuilt/f411_bootloader.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_bug_consume_first.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_bug_early_unpg.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_bug_naive_floor.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_bug_no_erase.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_bug_no_unlock.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_bug_pop_after_switch.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_bug_retry_limit.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_bug_self_lock.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_bug_trust_header.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_bug_watchdog.elf +0 -0
- brickproof/demo/prebuilt/f411_bootloader_hold_pg.elf +0 -0
- brickproof/demo/prebuilt/meta_floor1.bin +0 -0
- brickproof/demo/watchdog.yaml +7 -0
- brickproof/drivers/__init__.py +1 -0
- brickproof/drivers/staged_slot.py +91 -0
- brickproof/engine/__init__.py +0 -0
- brickproof/engine/session.py +724 -0
- brickproof/engine/workdir.py +167 -0
- brickproof/models/mem_guard.py +21 -0
- brickproof/models/stm32f1_flash.py +435 -0
- brickproof/models/stm32f1_rcc.py +17 -0
- brickproof/models/stm32f4_flash.py +435 -0
- brickproof/oracle.py +105 -0
- brickproof/plan.py +137 -0
- brickproof/platforms/stm32f103.repl +34 -0
- brickproof/platforms/stm32f411.repl +20 -0
- brickproof/pool.py +79 -0
- brickproof/regulations.py +137 -0
- brickproof/report.py +287 -0
- brickproof/runner.py +638 -0
- brickproof/torn.py +101 -0
- brickproof/trace.py +186 -0
- brickproof_fw-0.1.0.dist-info/METADATA +249 -0
- brickproof_fw-0.1.0.dist-info/RECORD +88 -0
- brickproof_fw-0.1.0.dist-info/WHEEL +5 -0
- brickproof_fw-0.1.0.dist-info/entry_points.txt +2 -0
- brickproof_fw-0.1.0.dist-info/licenses/LICENSE +202 -0
- brickproof_fw-0.1.0.dist-info/top_level.txt +1 -0
brickproof/__init__.py
ADDED
brickproof/adapter.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Target adapters: one YAML file describing the firmware, plus a Python driver.
|
|
2
|
+
|
|
3
|
+
The YAML says what to load, the flash geometry, how to reset, and where to cut.
|
|
4
|
+
The driver knows the target's update protocol (how to stage and start an update,
|
|
5
|
+
how to tell the device is alive). See adapters/three_tier_f103/ for an example.
|
|
6
|
+
"""
|
|
7
|
+
import importlib.util
|
|
8
|
+
import os
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
|
|
13
|
+
from brickproof.engine.session import FlashGeometry
|
|
14
|
+
from brickproof.torn import KINDS as TORN_KINDS
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AdapterError(ValueError):
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class Cut:
|
|
23
|
+
"""One place to pull the plug.
|
|
24
|
+
|
|
25
|
+
on_erase cut just before the erase of this page address starts
|
|
26
|
+
at_op cut just before flash operation number N, counted from the start
|
|
27
|
+
of the update (0 = its first erase or program)
|
|
28
|
+
expect optional (kind, address) the cut must land on, as a sanity check
|
|
29
|
+
group cuts expanded from one at_ops range share a group; once one of them
|
|
30
|
+
finds the update already over, the later ones are skipped
|
|
31
|
+
"""
|
|
32
|
+
name: str
|
|
33
|
+
on_erase: int = None
|
|
34
|
+
at_op: int = None
|
|
35
|
+
expect: tuple = None
|
|
36
|
+
group: int = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Adapter:
|
|
41
|
+
path: str # the YAML file it was read from
|
|
42
|
+
name: str
|
|
43
|
+
folder: str
|
|
44
|
+
target: str
|
|
45
|
+
platform: str
|
|
46
|
+
platform_extra: str
|
|
47
|
+
flash: FlashGeometry
|
|
48
|
+
ram_base: int
|
|
49
|
+
ram_size: int
|
|
50
|
+
firmware: list
|
|
51
|
+
uart: str
|
|
52
|
+
vector_table: int
|
|
53
|
+
mips: int
|
|
54
|
+
reset_cmds: list
|
|
55
|
+
boot_settle: float
|
|
56
|
+
cut_limit: float
|
|
57
|
+
update_time: float # virtual seconds an uninterrupted update needs
|
|
58
|
+
check_states: bool # compare flash at each cut with the golden run replayed
|
|
59
|
+
oracle: dict = None # approved images and which ones a cut may leave (see oracle.py)
|
|
60
|
+
plan: dict = None # let the planner choose cuts from the golden run (see plan.py)
|
|
61
|
+
torn: list = None # torn-operation shapes to try at each cut (see torn.py)
|
|
62
|
+
torn_seed: int = 0
|
|
63
|
+
depth: dict = None # explore cuts during recovery (see runner.explore)
|
|
64
|
+
shard: tuple = None # (k, n): run only every nth cut, starting with the kth
|
|
65
|
+
driver_file: str = None # a driver.py next to the adapter, or
|
|
66
|
+
driver_builtin: str = None # one that ships with BrickProof
|
|
67
|
+
driver_options: dict = field(default_factory=dict)
|
|
68
|
+
cuts: list = field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
def load_driver(self, session):
|
|
71
|
+
"""Import the driver module and return its driver object for this session."""
|
|
72
|
+
if self.driver_builtin:
|
|
73
|
+
mod = importlib.import_module("brickproof.drivers." + self.driver_builtin)
|
|
74
|
+
else:
|
|
75
|
+
spec = importlib.util.spec_from_file_location("brickproof_driver_" + self.name, self.driver_file)
|
|
76
|
+
mod = importlib.util.module_from_spec(spec)
|
|
77
|
+
spec.loader.exec_module(mod)
|
|
78
|
+
return mod.make_driver(session, self)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _protected_ranges(flash, protected):
|
|
82
|
+
"""The write_protected entries as checked (where, start, size) tuples."""
|
|
83
|
+
out = []
|
|
84
|
+
for i, r in enumerate(protected):
|
|
85
|
+
where = "flash.write_protected[%d]" % i
|
|
86
|
+
start, size = _need(r, "from", where), _need(r, "size", where)
|
|
87
|
+
if start < flash.base or start + size > flash.base + flash.size or size <= 0:
|
|
88
|
+
raise AdapterError("%s: outside the flash" % where)
|
|
89
|
+
out.append((where, start, size))
|
|
90
|
+
return out
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def stm32f1_option_bytes(flash, protected, read_protected, hardware_watchdog=False):
|
|
94
|
+
"""The 16 option bytes of an STM32F1 as the chip holds them: RDP, USER, Data0,
|
|
95
|
+
Data1, WRP0..WRP3, each followed by its complement. `protected` is a list of
|
|
96
|
+
{from, size} ranges to write-protect; a cleared WRP bit protects its unit."""
|
|
97
|
+
unit = flash.wrp_unit
|
|
98
|
+
wrp = 0xFFFFFFFF
|
|
99
|
+
for where, start, size in _protected_ranges(flash, protected):
|
|
100
|
+
if (start - flash.base) % unit or size % unit:
|
|
101
|
+
raise AdapterError("%s: write protection goes in units of 0x%X on this chip"
|
|
102
|
+
% (where, unit))
|
|
103
|
+
for u in range((start - flash.base) // unit, (start - flash.base + size) // unit):
|
|
104
|
+
if u >= 31 and (start - flash.base + size) < flash.size:
|
|
105
|
+
raise AdapterError("%s: the last protection bit covers everything from 0x%08X "
|
|
106
|
+
"to the end of flash, all or nothing" % (where, flash.base + 31 * unit))
|
|
107
|
+
wrp &= ~(1 << min(u, 31))
|
|
108
|
+
out = bytearray()
|
|
109
|
+
user = 0xFE if hardware_watchdog else 0xFF # WDG_SW is bit 0, cleared = hardware
|
|
110
|
+
for data in (0x00 if read_protected else 0xA5, user, 0xFF, 0xFF,
|
|
111
|
+
wrp & 0xFF, (wrp >> 8) & 0xFF, (wrp >> 16) & 0xFF, (wrp >> 24) & 0xFF):
|
|
112
|
+
out += bytes([data, ~data & 0xFF])
|
|
113
|
+
return bytes(out)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def stm32f4_option_bytes(flash, protected, read_protected, hardware_watchdog=False):
|
|
117
|
+
"""The option bytes of an STM32F4 as the chip shows them: USER and RDP in one
|
|
118
|
+
halfword, the nWRP bits in another, each followed by its complement. Write
|
|
119
|
+
protection is one bit per sector, and a cleared bit protects its sector."""
|
|
120
|
+
units = flash.units()
|
|
121
|
+
nwrp = 0xFFF
|
|
122
|
+
for where, start, size in _protected_ranges(flash, protected):
|
|
123
|
+
starts = [u for u, _ in units]
|
|
124
|
+
ends = [u + n for u, n in units]
|
|
125
|
+
if start not in starts or start + size not in ends:
|
|
126
|
+
raise AdapterError("%s: write protection goes by whole sectors on this chip" % where)
|
|
127
|
+
for s in range(starts.index(start), ends.index(start + size) + 1):
|
|
128
|
+
nwrp &= ~(1 << s)
|
|
129
|
+
out = bytearray()
|
|
130
|
+
user = 0xCF if hardware_watchdog else 0xEF # WDG_SW is bit 5, cleared = hardware
|
|
131
|
+
for half in (((0x55 if read_protected else 0xAA) << 8) | user, 0xF000 | nwrp):
|
|
132
|
+
out += bytes([half & 0xFF, half >> 8, ~half & 0xFF, (~half >> 8) & 0xFF]) + bytes([0xFF]) * 4
|
|
133
|
+
return bytes(out)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# What the engine has to know about each chip family it can emulate.
|
|
137
|
+
# model the flash controller model in models/
|
|
138
|
+
# controller where that controller sits
|
|
139
|
+
# ob_base where the option bytes are
|
|
140
|
+
# option_bytes builds them from an adapter's write_protected / read_protected
|
|
141
|
+
# wrp_unit how much flash one write-protection bit covers (None: one per sector)
|
|
142
|
+
# write_unit bytes a program usually writes
|
|
143
|
+
# endurance erase cycles per page or sector the datasheet promises
|
|
144
|
+
CHIPS = {
|
|
145
|
+
"stm32f103": dict(model="stm32f1_flash.py", controller=0x40022000, ob_base=0x1FFFF800,
|
|
146
|
+
option_bytes=stm32f1_option_bytes, wrp_unit=0x1000, write_unit=2,
|
|
147
|
+
endurance=10000),
|
|
148
|
+
"stm32f411": dict(model="stm32f4_flash.py", controller=0x40023C00, ob_base=0x1FFFC000,
|
|
149
|
+
option_bytes=stm32f4_option_bytes, wrp_unit=None, write_unit=4,
|
|
150
|
+
endurance=10000),
|
|
151
|
+
}
|
|
152
|
+
PLATFORMS = tuple(CHIPS)
|
|
153
|
+
|
|
154
|
+
# flash.timing: how long erases and programs take in the emulator
|
|
155
|
+
TIMINGS = {"none": 0, "typical": 1, "max": 2}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _need(d, key, where):
|
|
159
|
+
if key not in d:
|
|
160
|
+
raise AdapterError("%s: missing '%s'" % (where, key))
|
|
161
|
+
return d[key]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _parse_cuts(i, c):
|
|
165
|
+
"""One YAML entry: a single cut, or an at_ops range that expands to many."""
|
|
166
|
+
where = "cuts[%d]" % i
|
|
167
|
+
if not isinstance(c, dict):
|
|
168
|
+
raise AdapterError("%s: expected a mapping" % where)
|
|
169
|
+
if "at_ops" in c:
|
|
170
|
+
r = c["at_ops"]
|
|
171
|
+
lo, hi, step = r.get("from", 0), r.get("to"), r.get("step", 1)
|
|
172
|
+
if hi is None or hi < lo or step < 1:
|
|
173
|
+
raise AdapterError("%s.at_ops: need from <= to and step >= 1" % where)
|
|
174
|
+
return [Cut(name="op-%d" % n, at_op=n, group=i) for n in range(lo, hi + 1, step)]
|
|
175
|
+
return [_parse_cut(where, c)]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _parse_cut(where, c):
|
|
179
|
+
on_erase, at_op = c.get("on_erase"), c.get("at_op")
|
|
180
|
+
if (on_erase is None) == (at_op is None):
|
|
181
|
+
raise AdapterError("%s: give exactly one of on_erase or at_op" % where)
|
|
182
|
+
expect = None
|
|
183
|
+
if "expect" in c:
|
|
184
|
+
e = c["expect"]
|
|
185
|
+
kind = {"erase": 1, "program": 2}.get(e.get("kind"))
|
|
186
|
+
if kind is None:
|
|
187
|
+
raise AdapterError("%s.expect.kind: use erase or program" % where)
|
|
188
|
+
expect = (kind, e.get("addr", 0))
|
|
189
|
+
default = ("erase-0x%08X" % on_erase) if on_erase is not None else "op-%d" % at_op
|
|
190
|
+
return Cut(name=c.get("name", default), on_erase=on_erase, at_op=at_op, expect=expect)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _merge(base, over):
|
|
194
|
+
"""Mappings merge key by key, anything else (lists included) is replaced."""
|
|
195
|
+
out = dict(base)
|
|
196
|
+
for k, v in over.items():
|
|
197
|
+
out[k] = _merge(out[k], v) if isinstance(v, dict) and isinstance(out.get(k), dict) else v
|
|
198
|
+
return out
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _read(path, depth=0):
|
|
202
|
+
with open(path) as f:
|
|
203
|
+
d = yaml.safe_load(f) or {}
|
|
204
|
+
parent = d.pop("extends", None)
|
|
205
|
+
if parent:
|
|
206
|
+
if depth > 4:
|
|
207
|
+
raise AdapterError("%s: 'extends' nests too deep" % path)
|
|
208
|
+
d = _merge(_read(os.path.join(os.path.dirname(path), parent), depth + 1), d)
|
|
209
|
+
return d
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def _parse_plan(scenario):
|
|
213
|
+
"""scenario.plan: a mode name, or {mode, per_phase, small, real_every}. With
|
|
214
|
+
real_every N only every Nth planned cut is made in the emulator; the others are
|
|
215
|
+
powered up from the computed flash state. None if absent."""
|
|
216
|
+
p = scenario.get("plan")
|
|
217
|
+
if p is None:
|
|
218
|
+
return None
|
|
219
|
+
if isinstance(p, str):
|
|
220
|
+
p = {"mode": p}
|
|
221
|
+
if not isinstance(p, dict):
|
|
222
|
+
raise AdapterError("scenario.plan: give a mode (phases, every) or a mapping")
|
|
223
|
+
unknown = set(p) - {"mode", "per_phase", "small", "real_every"}
|
|
224
|
+
if unknown:
|
|
225
|
+
raise AdapterError("scenario.plan: unknown key(s) %s" % ", ".join(sorted(unknown)))
|
|
226
|
+
mode = p.get("mode", "phases")
|
|
227
|
+
if mode not in ("phases", "every"):
|
|
228
|
+
raise AdapterError("scenario.plan.mode: use phases or every, not %r" % mode)
|
|
229
|
+
out = {"mode": mode}
|
|
230
|
+
for key in ("per_phase", "small", "real_every"):
|
|
231
|
+
if key in p:
|
|
232
|
+
if not isinstance(p[key], int) or p[key] < 0:
|
|
233
|
+
raise AdapterError("scenario.plan.%s: a whole number, 0 or more" % key)
|
|
234
|
+
out[key] = p[key]
|
|
235
|
+
return out
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
DEPTH_DEFAULTS = {"mode": "phases", "per_phase": 2, "small": 6, "max_states": 2000, "torn": False}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _parse_depth(scenario):
|
|
242
|
+
"""scenario.depth: how many power cuts in a row to explore. A number, or a mapping
|
|
243
|
+
{levels, mode, per_phase, small, max_states, torn}. With torn, the half-done
|
|
244
|
+
versions of each recovery operation are tried as well (it needs scenario.torn or
|
|
245
|
+
--torn for the shapes). None when it is 1 or absent."""
|
|
246
|
+
d = scenario.get("depth")
|
|
247
|
+
if d is None:
|
|
248
|
+
return None
|
|
249
|
+
if isinstance(d, int) and not isinstance(d, bool):
|
|
250
|
+
d = {"levels": d}
|
|
251
|
+
if not isinstance(d, dict):
|
|
252
|
+
raise AdapterError("scenario.depth: a number of cuts in a row, or a mapping")
|
|
253
|
+
unknown = set(d) - {"levels", *DEPTH_DEFAULTS}
|
|
254
|
+
if unknown:
|
|
255
|
+
raise AdapterError("scenario.depth: unknown key(s) %s" % ", ".join(sorted(unknown)))
|
|
256
|
+
out = dict(DEPTH_DEFAULTS, **d)
|
|
257
|
+
out.setdefault("levels", 2)
|
|
258
|
+
if out["mode"] not in ("phases", "every"):
|
|
259
|
+
raise AdapterError("scenario.depth.mode: use phases or every, not %r" % out["mode"])
|
|
260
|
+
for key in ("levels", "per_phase", "small", "max_states"):
|
|
261
|
+
if not isinstance(out[key], int) or isinstance(out[key], bool) or out[key] < 0:
|
|
262
|
+
raise AdapterError("scenario.depth.%s: a whole number, 0 or more" % key)
|
|
263
|
+
if not isinstance(out["torn"], bool):
|
|
264
|
+
raise AdapterError("scenario.depth.torn: true or false")
|
|
265
|
+
if out["levels"] < 1:
|
|
266
|
+
raise AdapterError("scenario.depth.levels: at least 1")
|
|
267
|
+
return out if out["levels"] > 1 else None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _parse_torn(scenario):
|
|
271
|
+
"""scenario.torn: true for every shape, or a list of shape names. None if off."""
|
|
272
|
+
t = scenario.get("torn")
|
|
273
|
+
if not t:
|
|
274
|
+
return None
|
|
275
|
+
if t is True:
|
|
276
|
+
return list(TORN_KINDS)
|
|
277
|
+
if not isinstance(t, list) or any(k not in TORN_KINDS for k in t):
|
|
278
|
+
raise AdapterError("scenario.torn: true, or a list from: %s" % ", ".join(TORN_KINDS))
|
|
279
|
+
return list(t)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _parse_oracle(d, target):
|
|
283
|
+
"""The `oracle:` section, with image paths made absolute. None if there is none."""
|
|
284
|
+
o = d.get("oracle")
|
|
285
|
+
if not o:
|
|
286
|
+
return None
|
|
287
|
+
images = {}
|
|
288
|
+
for name, spec in (o.get("images") or {}).items():
|
|
289
|
+
where = "oracle.images.%s" % name
|
|
290
|
+
path = os.path.normpath(os.path.join(target, _need(spec, "file", where)))
|
|
291
|
+
images[str(name)] = (path, _need(spec, "at", where))
|
|
292
|
+
accept = [str(a) for a in _need(o, "accept", "oracle")]
|
|
293
|
+
if not accept:
|
|
294
|
+
raise AdapterError("oracle.accept: name at least one image")
|
|
295
|
+
run_in = o.get("run_in")
|
|
296
|
+
if run_in is not None:
|
|
297
|
+
run_in = (_need(run_in, "start", "oracle.run_in"), _need(run_in, "end", "oracle.run_in"))
|
|
298
|
+
if run_in[0] >= run_in[1]:
|
|
299
|
+
raise AdapterError("oracle.run_in: start must be below end")
|
|
300
|
+
return {"images": images, "accept": accept, "run_in": run_in,
|
|
301
|
+
"samples": int(o.get("samples", 4)), "interval": float(o.get("interval", 0.02))}
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def load(path, target=None):
|
|
305
|
+
"""Read an adapter YAML. `target` overrides the firmware project folder it names.
|
|
306
|
+
|
|
307
|
+
`extends: other.yaml` starts from another adapter and changes only what this
|
|
308
|
+
file says. Relative paths resolve against this file's folder, so keep the two
|
|
309
|
+
side by side.
|
|
310
|
+
"""
|
|
311
|
+
path = os.path.abspath(path)
|
|
312
|
+
folder = os.path.dirname(path)
|
|
313
|
+
d = _read(path)
|
|
314
|
+
|
|
315
|
+
name = _need(d, "name", path)
|
|
316
|
+
target = os.path.abspath(target or os.path.join(folder, _need(d, "target", path)))
|
|
317
|
+
|
|
318
|
+
plat = _need(d, "platform", path)
|
|
319
|
+
base = _need(plat, "base", "platform")
|
|
320
|
+
if base not in PLATFORMS:
|
|
321
|
+
raise AdapterError("platform.base: unknown '%s' (have: %s)" % (base, ", ".join(PLATFORMS)))
|
|
322
|
+
extra = plat.get("extra")
|
|
323
|
+
|
|
324
|
+
fl = _need(d, "flash", path)
|
|
325
|
+
chip = CHIPS[base]
|
|
326
|
+
flash = FlashGeometry(base=_need(fl, "base", "flash"), size=_need(fl, "size", "flash"),
|
|
327
|
+
page=fl.get("page"), write_unit=fl.get("write_unit", chip["write_unit"]),
|
|
328
|
+
controller=chip["controller"], model=chip["model"], ob_base=chip["ob_base"],
|
|
329
|
+
wrp_unit=chip["wrp_unit"], endurance=chip["endurance"])
|
|
330
|
+
timing = fl.get("timing", "typical")
|
|
331
|
+
if timing not in TIMINGS:
|
|
332
|
+
raise AdapterError("flash.timing: unknown '%s' (have: %s)" % (timing, ", ".join(TIMINGS)))
|
|
333
|
+
flash.timing = TIMINGS[timing]
|
|
334
|
+
if (flash.page is None) == ("sectors" not in fl):
|
|
335
|
+
raise AdapterError("flash: give exactly one of page (one erase size everywhere) "
|
|
336
|
+
"or sectors (a list of sizes)")
|
|
337
|
+
if flash.page is not None:
|
|
338
|
+
if flash.page <= 0 or flash.size % flash.page:
|
|
339
|
+
raise AdapterError("flash.size must be a whole number of pages")
|
|
340
|
+
else:
|
|
341
|
+
sizes = fl["sectors"]
|
|
342
|
+
if not isinstance(sizes, list) or not sizes or any(not isinstance(n, int) or n <= 0 for n in sizes):
|
|
343
|
+
raise AdapterError("flash.sectors must be a list of sector sizes")
|
|
344
|
+
if sum(sizes) != flash.size:
|
|
345
|
+
raise AdapterError("flash.sectors add up to 0x%X, but flash.size is 0x%X"
|
|
346
|
+
% (sum(sizes), flash.size))
|
|
347
|
+
flash.sectors = [(sum(sizes[:i]), n) for i, n in enumerate(sizes)]
|
|
348
|
+
if chip["model"] == "stm32f1_flash.py" and flash.page is None:
|
|
349
|
+
raise AdapterError("flash: an %s erases by page, not by sector" % base)
|
|
350
|
+
if chip["model"] == "stm32f4_flash.py" and flash.sectors is None:
|
|
351
|
+
raise AdapterError("flash: an %s erases by sector; list them in flash.sectors" % base)
|
|
352
|
+
flash.option_bytes = chip["option_bytes"](flash, fl.get("write_protected") or [],
|
|
353
|
+
bool(fl.get("read_protected", False)),
|
|
354
|
+
bool(fl.get("hardware_watchdog", False)))
|
|
355
|
+
|
|
356
|
+
# RAM size is not optional: an emulator with more RAM than the chip hides
|
|
357
|
+
# real faults (a stack access past the end of RAM just works)
|
|
358
|
+
ram = _need(d, "ram", path)
|
|
359
|
+
ram_size = _need(ram, "size", "ram")
|
|
360
|
+
if ram_size <= 0:
|
|
361
|
+
raise AdapterError("ram.size must be positive")
|
|
362
|
+
|
|
363
|
+
firmware = []
|
|
364
|
+
for i, fw in enumerate(_need(d, "firmware", path)):
|
|
365
|
+
file = _need(fw, "file", "firmware[%d]" % i)
|
|
366
|
+
addr = fw.get("addr")
|
|
367
|
+
if not file.lower().endswith(".elf") and addr is None:
|
|
368
|
+
raise AdapterError("firmware[%d]: a raw binary needs 'addr'" % i)
|
|
369
|
+
firmware.append((os.path.normpath(os.path.join(target, file)), addr))
|
|
370
|
+
|
|
371
|
+
cpu = d.get("cpu", {})
|
|
372
|
+
drv = _need(d, "driver", path)
|
|
373
|
+
scenario = d.get("scenario") or {}
|
|
374
|
+
cuts = [cut for i, c in enumerate(scenario.get("cuts") or []) for cut in _parse_cuts(i, c)]
|
|
375
|
+
if bool(drv.get("file")) == bool(drv.get("builtin")):
|
|
376
|
+
raise AdapterError("driver: give exactly one of file or builtin")
|
|
377
|
+
|
|
378
|
+
return Adapter(
|
|
379
|
+
path=path, name=name, folder=folder, target=target,
|
|
380
|
+
platform=base, platform_extra=os.path.join(folder, extra) if extra else None,
|
|
381
|
+
flash=flash, ram_base=ram.get("base", 0x20000000), ram_size=ram_size, firmware=firmware,
|
|
382
|
+
uart=d.get("uart", "usart2"),
|
|
383
|
+
vector_table=cpu.get("vector_table", flash.base),
|
|
384
|
+
mips=cpu.get("mips", 72),
|
|
385
|
+
reset_cmds=list(d.get("reset", [])),
|
|
386
|
+
boot_settle=float(d.get("boot_settle", 1.5)),
|
|
387
|
+
cut_limit=float(scenario.get("cut_limit", 30.0)),
|
|
388
|
+
update_time=float(scenario.get("update_time", scenario.get("cut_limit", 30.0))),
|
|
389
|
+
check_states=bool(scenario.get("check_states", True)),
|
|
390
|
+
oracle=_parse_oracle(d, target),
|
|
391
|
+
plan=_parse_plan(scenario),
|
|
392
|
+
torn=_parse_torn(scenario),
|
|
393
|
+
torn_seed=int(scenario.get("torn_seed", 0)),
|
|
394
|
+
depth=_parse_depth(scenario),
|
|
395
|
+
driver_file=os.path.join(folder, drv["file"]) if drv.get("file") else None,
|
|
396
|
+
driver_builtin=drv.get("builtin"),
|
|
397
|
+
driver_options=drv.get("options", {}) or {},
|
|
398
|
+
cuts=cuts,
|
|
399
|
+
)
|