Compare commits
2 Commits
cbb3c58300
...
37f9747e26
| Author | SHA1 | Date | |
|---|---|---|---|
| 37f9747e26 | |||
| 6ff407e7bb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ Debug
|
||||
|
||||
# CMake / clangd
|
||||
/build/
|
||||
/local/
|
||||
compile_commands.json
|
||||
.cache/
|
||||
|
||||
|
||||
@@ -289,6 +289,21 @@ if(PROJECT_IS_TOP_LEVEL)
|
||||
set_tests_properties(pureboot.dirty PROPERTIES TIMEOUT 180)
|
||||
endif()
|
||||
|
||||
# The reset walk region: an application that grew into the span reset
|
||||
# crosses to reach the loader, refused by the ordinary --flash. One
|
||||
# boot-sectioned mega carries it - the per-chip BOOTSZ ladder the
|
||||
# refusal decodes is a planner test (test_planner.py).
|
||||
if(LIBAVR_MCU STREQUAL "atmega328p")
|
||||
add_test(NAME pureboot.walk
|
||||
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test/pbwalk.py
|
||||
${PB_DEVICE} $<TARGET_FILE:pureboot> ${PUREBOOT_SIM_MCU} ${_pb_stock_hz}
|
||||
${PUREBOOT_BASE_HEX} ${PUREBOOT_PAGE} ${_pb_stock_baud}
|
||||
$<TARGET_FILE:pbapp>.bin
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/pureboot/pureboot.py
|
||||
${CMAKE_BINARY_DIR}/pbwalk-work)
|
||||
set_tests_properties(pureboot.walk PROPERTIES TIMEOUT 300)
|
||||
endif()
|
||||
|
||||
# Re-homing: a loader mistakenly programmed at address 0 (a raw .bin
|
||||
# handed to a programmer) or sitting in the staging slot must heal
|
||||
# into the canonical slot through the ordinary --update-loader flow.
|
||||
|
||||
@@ -587,8 +587,7 @@ one; a re-run on the resident's link finds nothing at all. The state file carrie
|
||||
the only bytes not recoverable from the device; losing it mid-update still
|
||||
completes the update, and the staging region comes back by reflashing the
|
||||
application. A boot-sectioned mega needs its fuses for the preflight — read
|
||||
from the device, or supplied with `--assume-fuses` where reading is impossible
|
||||
(simulators).
|
||||
from the device, or supplied with `--assume-fuses` where reading is impossible.
|
||||
|
||||
## Host tool
|
||||
|
||||
@@ -611,7 +610,11 @@ verify by read-back unless `--no-verify`, and a flash page that reads back
|
||||
wrong is rewritten up to three times before the run stops (see the fill above).
|
||||
`--verify-flash` only reports. Images are raw binary, or Intel HEX by
|
||||
extension. `--force` overrides the refusable safety checks — today, flashing
|
||||
application data into a mega's reset walk region.
|
||||
application data into a mega's reset walk region. That check reads the boot
|
||||
fuses itself rather than waiting to be handed them, because a plain `--flash`
|
||||
asks for none and is exactly the operation that must not skip it; fuses it
|
||||
cannot read leave the region unknown, and unknown is refused rather than
|
||||
assumed empty (`--assume-fuses` states them).
|
||||
|
||||
`--autobaud` opens with the calibration pulse instead of the plain knock, for a
|
||||
loader built `SERIAL autobaud`; the rest of the session is identical, at
|
||||
@@ -750,6 +753,13 @@ Per chip preset, `ctest` runs:
|
||||
a bare verify must see the corruption and the repairing verify must fix it in
|
||||
one rewrite. Hardware forbids the state here, but simavr dispatches SPM from
|
||||
anywhere, which is what makes the path constructible;
|
||||
- `pureboot.walk` (328P) — an application whose tail lands in the span reset
|
||||
crosses to reach the loader, refused by a plain `--flash` and written only
|
||||
under `--force`, with a lower image and an unprogrammed BOOTRST left alone.
|
||||
The refusal is decided from a modeled fuse read: the diversion BLBSET arms
|
||||
answers an LPM, which simavr executes straight out of flash with no hook, so
|
||||
the runner lends those bytes the fuses for the cycles SELFPRGEN holds (`-f`
|
||||
states them, unprogrammed elsewhere);
|
||||
- `pureboot.update` — the full `--update-loader` flow, then every power-fail
|
||||
phase: the device is killed mid-write, restarted from its flash dump, and a
|
||||
re-run must complete the update with the application intact;
|
||||
|
||||
@@ -1476,11 +1476,36 @@ def op_update_loader(loader, wait, path, state_path, fuse_bytes, staged_link=Non
|
||||
print(f"loader updated: pureboot {update.version}, {len(image)} B at {info.base:#06x}, staging region restored")
|
||||
|
||||
|
||||
def walk_check_fuses(loader):
|
||||
"""The fuses the walk check needs, fetched where the check is rather than
|
||||
by whichever operation happened to want them printed. An ordinary --flash
|
||||
asks for no fuses of its own and is exactly the caller that must not skip
|
||||
the check, so the dependency is fetched here and cannot be forgotten.
|
||||
None where they cannot be had, which the check answers with its refusal."""
|
||||
if loader.info.patch_vector:
|
||||
return None
|
||||
try:
|
||||
return read_fuse_bytes(loader)
|
||||
except Error:
|
||||
return None
|
||||
|
||||
|
||||
def check_walk_region(pages, info, fuse_bytes, force):
|
||||
"""BOOTRST programmed below the loader means reset reaches it only by
|
||||
walking across erased flash; application data in that span would divert
|
||||
reset into itself. Needs the fuses (--fuses or --assume-fuses)."""
|
||||
if info.patch_vector or fuse_bytes is None:
|
||||
reset into itself. The fuses are what name that span, so arriving without
|
||||
them is a refusal and not a pass: the check that did not run answers
|
||||
unknown, and unknown is not empty."""
|
||||
if info.patch_vector:
|
||||
return
|
||||
if fuse_bytes is None:
|
||||
if not force:
|
||||
raise Error(
|
||||
f"the fuses could not be read, so the reset walk region below {info.base:#06x} "
|
||||
"is unknown - an image writing into it while BOOTRST is programmed leaves "
|
||||
"reset unable to reach the loader - --assume-fuses to state them, --force "
|
||||
"to flash without the check"
|
||||
)
|
||||
return
|
||||
bootrst, bls_start = mega_boot(info, fuse_bytes)
|
||||
if not bootrst or bls_start >= info.base:
|
||||
@@ -1520,7 +1545,7 @@ def op_flash(loader, path, erase, verify, fuse_bytes=None, force=False):
|
||||
image = load_image(path)
|
||||
verbose(f"{path}: {len(image)} B image")
|
||||
pages = plan_flash(image, loader.info)
|
||||
check_walk_region(pages, loader.info, fuse_bytes, force)
|
||||
check_walk_region(pages, loader.info, fuse_bytes or walk_check_fuses(loader), force)
|
||||
if erase:
|
||||
op_erase_flash(loader)
|
||||
order = covered(pages, loader.info, skip_blank=erase)
|
||||
@@ -1648,14 +1673,21 @@ def op_poke(loader, spec):
|
||||
print(f"poke: {len(data)} B at {int(address, 0):#06x}")
|
||||
|
||||
|
||||
def op_fuses(loader):
|
||||
def read_fuse_bytes(loader):
|
||||
"""The device's fuses in the order this tool indexes them everywhere:
|
||||
low, lock, extended, high - BOOT_FUSE's order and --assume-fuses'."""
|
||||
low, lock, extended, high = loader.read_fuses()
|
||||
return bytes((low, lock, extended, high))
|
||||
|
||||
|
||||
def op_fuses(loader):
|
||||
fuse_bytes = read_fuse_bytes(loader)
|
||||
low, lock, extended, high = fuse_bytes
|
||||
print("fuses:")
|
||||
print(f" low 0x{low:02x}")
|
||||
print(f" high 0x{high:02x}")
|
||||
print(f" extended 0x{extended:02x}")
|
||||
print(f" lock 0x{lock:02x}")
|
||||
fuse_bytes = bytes((low, lock, extended, high))
|
||||
# On a boot-sectioned mega the BOOTSZ/BOOTRST decode is the fuse fact the
|
||||
# loader's whole deployment hangs on - say it in words.
|
||||
if not loader.info.patch_vector:
|
||||
|
||||
@@ -9,12 +9,16 @@ import subprocess
|
||||
|
||||
class Device:
|
||||
def __init__(self, binary, elf, mcu, hz, base_hex, page, baud, dump, reset_hex=None, resume=None, link=None,
|
||||
window=False):
|
||||
window=False, fuses=None):
|
||||
cmd = [binary]
|
||||
if link:
|
||||
cmd += ["-l", link]
|
||||
if window:
|
||||
cmd.append("-w") # report the first-transmit cycle, free-run idle
|
||||
if fuses:
|
||||
# What a fuse read answers, low,lock,extended,high. Unprogrammed
|
||||
# otherwise, which is the profile every other test here runs.
|
||||
cmd += ["-f", fuses]
|
||||
cmd += [elf, mcu, hz, base_hex, str(page), str(baud), dump]
|
||||
if reset_hex is not None or resume is not None:
|
||||
# Chips without a hardware boot section - the tinies and the
|
||||
|
||||
119
test/pbwalk.py
Normal file
119
test/pbwalk.py
Normal file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The reset walk region, end to end - the brick this refusal exists for.
|
||||
|
||||
BOOTRST programmed on a boot-sectioned mega enters reset at the boot section's
|
||||
base, and the loader is reached only by walking up across erased flash. An
|
||||
application that grew into that span is executed by the reset instead: it
|
||||
happened to an ssd1306 board on a 328P with hfuse 0xdc, and with no reset edge
|
||||
brought out, an ICE was the only way back.
|
||||
|
||||
The plain --flash is the path under test. It asks for no fuses of its own,
|
||||
which is exactly why it once skipped the check and wrote the image anyway.
|
||||
|
||||
The runner still resets into the loader - the walk itself is not modeled. What
|
||||
is modeled is the fuse read, so the refusal is decided from the bytes silicon
|
||||
would have answered (-f, test/pureboot_device.cpp).
|
||||
|
||||
Usage: pbwalk.py <device_bin> <pureboot_elf> <mcu> <hz> <base_hex> <page>
|
||||
<baud> <app_bin> <tool_py> <workdir>
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def fail(message):
|
||||
print(f"FAIL: {message}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def intel_hex(chunks):
|
||||
"""Intel HEX for {address: bytes}. An application that reaches the walk
|
||||
region is sparse there - a .bin would have to carry every byte between."""
|
||||
lines = []
|
||||
for address, data in sorted(chunks.items()):
|
||||
for at in range(0, len(data), 16):
|
||||
row = data[at : at + 16]
|
||||
here = address + at
|
||||
record = bytes((len(row), here >> 8, here & 0xFF, 0)) + row
|
||||
lines.append(":" + (record + bytes((-sum(record) & 0xFF,))).hex())
|
||||
return "\n".join(lines + [":00000001FF"]) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
device_bin, elf, mcu, hz, base_hex, page, baud, app_bin, tool, workdir = sys.argv[1:]
|
||||
page, baud = int(page), int(baud)
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(tool)))
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import pbsim
|
||||
import pureboot as pb
|
||||
|
||||
os.makedirs(workdir, exist_ok=True)
|
||||
dump = os.path.join(workdir, "dump.bin")
|
||||
# The ssd1306 board's own fuses, and the same boot section with reset left
|
||||
# on the application - the one difference that decides the refusal.
|
||||
bricking, harmless = "ffffffdc", "ffffffdd"
|
||||
|
||||
def session(fuses):
|
||||
return pbsim.Device(device_bin, elf, mcu, hz, base_hex, page, baud, dump, fuses=fuses)
|
||||
|
||||
def connected(device):
|
||||
port = pb.Port(device.pty, baud)
|
||||
loader = pb.Loader(port)
|
||||
loader.connect(25)
|
||||
return port, loader
|
||||
|
||||
device = session(bricking)
|
||||
try:
|
||||
port, loader = connected(device)
|
||||
info = loader.info
|
||||
bootrst, bls_start = pb.mega_boot(info, bytes.fromhex(bricking))
|
||||
if not bootrst or bls_start >= info.base:
|
||||
fail(f"this profile cannot brick: BOOTRST {bootrst}, section at {bls_start:#06x}")
|
||||
port.close()
|
||||
|
||||
# An application whose tail lands in the walk region, which is what the
|
||||
# bricked board's dump showed: live bytes at the section base.
|
||||
overgrown = os.path.join(workdir, "overgrown.hex")
|
||||
with open(overgrown, "w") as image:
|
||||
image.write(intel_hex({0: open(app_bin, "rb").read(), bls_start: bytes(page)}))
|
||||
|
||||
out = pbsim.run_tool(tool, device.pty, baud, "--fuses", "--stay")
|
||||
if f"0x{bricking[6:]}" not in out:
|
||||
fail(f"the device did not answer the fuses it was given ({bricking}):\n{out}")
|
||||
|
||||
try:
|
||||
pbsim.run_tool(tool, device.pty, baud, "--flash", overgrown, "--stay")
|
||||
except RuntimeError as refused:
|
||||
if "walk region" not in str(refused):
|
||||
fail(f"--flash failed, but not on the walk region: {refused}")
|
||||
else:
|
||||
fail("a plain --flash wrote an application into the reset walk region")
|
||||
|
||||
port, loader = connected(device)
|
||||
if loader.read_flash(bls_start, page) != bytes((0xFF,)) * page:
|
||||
fail(f"the refused image was written to {bls_start:#06x} anyway")
|
||||
port.close()
|
||||
|
||||
# The override is the escape hatch, not the default.
|
||||
pbsim.run_tool(tool, device.pty, baud, "--flash", overgrown, "--erase-flash", "--force", "--stay")
|
||||
port, loader = connected(device)
|
||||
if loader.read_flash(bls_start, page) != bytes(page):
|
||||
fail(f"--force did not write {bls_start:#06x}")
|
||||
port.close()
|
||||
finally:
|
||||
device.stop()
|
||||
|
||||
# An image that stays below the region flashes with no override at all,
|
||||
# and so does the same overgrown image once reset boots the application.
|
||||
for fuses, image in ((bricking, app_bin), (harmless, overgrown)):
|
||||
device = session(fuses)
|
||||
try:
|
||||
pbsim.run_tool(tool, device.pty, baud, "--flash", image, "--erase-flash", "--stay")
|
||||
finally:
|
||||
device.stop()
|
||||
|
||||
print("pbwalk: the walk region is refused by default, overridden by --force, and left alone otherwise")
|
||||
|
||||
|
||||
main()
|
||||
@@ -18,12 +18,13 @@
|
||||
// is a silent no-op (the mega's boot section has one, avr_flash). The
|
||||
// missing module is supplied here: the SPM ioctl reads SPMCSR/Z/r1:r0 and
|
||||
// implements buffer fill, page erase, page write, and CTPB, completing
|
||||
// instantly. RFLB's LPM diversion (fuse readout) stays unmodeled, so the
|
||||
// 'F' command answers with flash bytes - the tests assert transport only.
|
||||
// instantly. The fuse readout's LPM diversion is missing from every core and
|
||||
// is supplied too, so a fuse read answers fuses (-f) and not flash bytes.
|
||||
//
|
||||
// On exit (or SIGTERM) the flash and EEPROM are dumped to files for a
|
||||
// ground-truth cross-check against what the host read back.
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <csignal>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -177,6 +178,58 @@ void fix_mega_flash_erase()
|
||||
std::println(stderr, "device: no flash module to fix - SPM page erases may misalign");
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------- fuses ---
|
||||
|
||||
// The fuse and lock bytes answer an LPM, not an SPM: BLBSET|SELFPRGEN in
|
||||
// SPMCSR diverts the next LPM to them, selected by Z (Atmel-8271 section
|
||||
// 26.8.9). simavr executes LPM straight out of avr->flash and offers no hook
|
||||
// on it, so the diversion is modeled where there is one - the SPMCSR write -
|
||||
// by lending the four flash bytes Z can select to the fuses for as long as the
|
||||
// hardware holds SELFPRGEN. Unprogrammed until -f says otherwise, as a part
|
||||
// ships and as the erased flash above is.
|
||||
auto fuses = std::to_array<std::uint8_t>({0xFF, 0xFF, 0xFF, 0xFF}); // Z order: low, lock, extended, high
|
||||
decltype(fuses) lent;
|
||||
|
||||
int parse_fuses(std::string_view spec)
|
||||
{
|
||||
if (spec.size() != 2 * fuses.size()) {
|
||||
return -1;
|
||||
}
|
||||
for (std::size_t at = 0; at < fuses.size(); at++) {
|
||||
const auto digits = spec.substr(2 * at, 2);
|
||||
if (std::from_chars(digits.data(), digits.data() + digits.size(), fuses[at], 16).ec != std::errc{}) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
avr_cycle_count_t end_fuse_read(avr_t *mcu, avr_cycle_count_t, void *)
|
||||
{
|
||||
std::memcpy(mcu->flash, lent.data(), lent.size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
void spmcsr_written(avr_t *mcu, avr_io_addr_t at, std::uint8_t value, void *)
|
||||
{
|
||||
// A registered write handler *replaces* the store simavr would have done
|
||||
// (sim_core.c), so performing it is this handler's job - on the tinies
|
||||
// nothing else is watching the register, and swallowing the store would
|
||||
// leave every SPM command unseen by the NVM model above.
|
||||
avr_core_watch_write(mcu, at, value);
|
||||
constexpr std::uint8_t read_fuse_command = 0x09; // BLBSET|SELFPRGEN
|
||||
// The LPM must follow within three cycles of the arming store (Atmel-8271
|
||||
// section 26.8.9), and simavr runs its cycle timers between instructions -
|
||||
// so the loan is returned after the LPM that took it, never during.
|
||||
constexpr avr_cycle_count_t selfprgen_window = 3;
|
||||
if ((value & 0x1F) != read_fuse_command) {
|
||||
return;
|
||||
}
|
||||
std::memcpy(lent.data(), mcu->flash, lent.size());
|
||||
std::memcpy(mcu->flash, fuses.data(), fuses.size());
|
||||
avr_cycle_timer_register(mcu, selfprgen_window, end_fuse_read, nullptr);
|
||||
}
|
||||
|
||||
void request_reset(int)
|
||||
{
|
||||
reset_requested = 1;
|
||||
@@ -184,6 +237,9 @@ void request_reset(int)
|
||||
|
||||
// ------------------------------------------------------------- tiny NVM ---
|
||||
|
||||
// Where SPMCSR sits on the cores that carry no flash module to name it.
|
||||
constexpr avr_io_addr_t tiny_spmcsr = 0x57;
|
||||
|
||||
struct tiny_nvm_t {
|
||||
avr_io_t io;
|
||||
std::array<std::uint8_t, 128> buffer;
|
||||
@@ -200,7 +256,7 @@ int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
|
||||
}
|
||||
auto *n = reinterpret_cast<tiny_nvm_t *>(io);
|
||||
avr_t *mcu = io->avr;
|
||||
std::uint8_t command = mcu->data[0x57] & 0x1f; // SPMCSR, both tinies
|
||||
std::uint8_t command = mcu->data[tiny_spmcsr] & 0x1f;
|
||||
auto z = static_cast<std::uint16_t>(mcu->data[30] | (mcu->data[31] << 8));
|
||||
std::uint32_t page_base = static_cast<std::uint32_t>(z & ~(n->page - 1)) % (mcu->flashend + 1);
|
||||
if (command == 0x01) { // SPMEN alone: buffer fill from r1:r0
|
||||
@@ -222,7 +278,7 @@ int nvm_ioctl(avr_io_t *io, std::uint32_t ctl, void *)
|
||||
std::memset(n->buffer.data(), 0xff, n->page);
|
||||
std::memset(n->used.data(), 0, n->page);
|
||||
}
|
||||
mcu->data[0x57] &= static_cast<std::uint8_t>(~0x1f); // the operation completes instantly
|
||||
mcu->data[tiny_spmcsr] &= static_cast<std::uint8_t>(~0x1f); // the operation completes instantly
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -494,11 +550,18 @@ void poll_pty()
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
bool link_given = false;
|
||||
for (int opt; (opt = getopt(argc, argv, "l:w")) != -1;) {
|
||||
for (int opt; (opt = getopt(argc, argv, "l:wf:")) != -1;) {
|
||||
if (opt == 'w') {
|
||||
window_report = true;
|
||||
continue;
|
||||
}
|
||||
if (opt == 'f') {
|
||||
if (parse_fuses(optarg) != 0) {
|
||||
std::println(stderr, "device: -f takes 8 hex digits: low,lock,extended,high");
|
||||
return 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (opt != 'l' || parse_link(optarg) != 0) {
|
||||
std::println(stderr, "device: bad link spec (usart0, usart1, sw, or sw:B0,B1 as RX,TX)");
|
||||
return 2;
|
||||
@@ -508,10 +571,12 @@ int main(int argc, char *argv[])
|
||||
int args = argc - optind;
|
||||
if (args < 7 || args > 9) {
|
||||
std::print(stderr,
|
||||
"usage: {} [-l link] [-w] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud> <flash_dump>"
|
||||
" [reset_hex] [resume_flash]\n"
|
||||
"usage: {} [-l link] [-w] [-f fuses] <pureboot.elf> <mcu> <hz> <base_hex> <page> <baud>"
|
||||
" <flash_dump> [reset_hex] [resume_flash]\n"
|
||||
" -l link: usart0 | usart1 | sw[:B0,B1[@0]] (RX,TX, then the USART owning\n"
|
||||
" them); default: the chip's own\n"
|
||||
" -f fuses: 8 hex digits, low,lock,extended,high - what a fuse read answers;\n"
|
||||
" default: unprogrammed. reset_hex stays the reset target\n"
|
||||
" -w: print PB_WINDOW_TX <cycle> at the first transmit activity and\n"
|
||||
" free-run idle time (window measurement mode)\n"
|
||||
" reset_hex: reset vector (default: base with a boot section, else 0)\n"
|
||||
@@ -599,6 +664,9 @@ int main(int argc, char *argv[])
|
||||
nvm.io.ioctl = nvm_ioctl;
|
||||
avr_register_io(avr, &nvm.io);
|
||||
}
|
||||
// Where the fuse read is armed: the mega's flash module names its SPMCSR,
|
||||
// and the tinies keep theirs at the address both those cores share.
|
||||
avr_register_io_write(avr, mega_flash ? mega_flash->r_spm : tiny_spmcsr, spmcsr_written, nullptr);
|
||||
|
||||
if (!link_software) {
|
||||
// POLL_SLEEP paces an idle-polling loader in host real time (a
|
||||
|
||||
@@ -243,7 +243,14 @@ def main():
|
||||
pb.check_walk_region(deep, mega, fuses(0xFA), True)
|
||||
pb.check_walk_region(deep, mega, fuses(0xFB), False) # BOOTRST unprogrammed
|
||||
pb.check_walk_region({0x7800: bytes((0xFF,)) * 128}, mega, fuses(0xFA), False)
|
||||
pb.check_walk_region(deep, mega, None, False) # fuses unknown: no check
|
||||
|
||||
# Fuses that could not be read leave the span unknown, and unknown is not
|
||||
# empty: the check that did not run must refuse rather than pass, since the
|
||||
# span it would have named is the one that costs the part.
|
||||
expect_error("walk region without fuses",
|
||||
lambda: pb.check_walk_region(deep, mega, None, False), "--assume-fuses", "--force")
|
||||
pb.check_walk_region(deep, mega, None, True)
|
||||
pb.check_walk_region(deep, tiny, None, False) # patched reset vector: no walk to guard
|
||||
|
||||
# The repairing verify: a mismatched page is rewritten rather than raised,
|
||||
# bounded so a fault that is not self-clearing cannot spin.
|
||||
|
||||
Reference in New Issue
Block a user