mirror of
https://github.com/MCKero6423/uv-k5-v3-emulator.git
synced 2026-10-02 03:15:36 +00:00
Model the BK4819 register interface
The transceiver was not modelled at all. Its bit-banged three-wire bus went nowhere, so every register read returned whatever the floating GPIO happened to be, and PB9 had to be idled low as a workaround: with the line high, reads came back 0xFFFF and RADIO_SetupRegisters spun forever on bit 0 of REG_0C. Now a real device, wired to the pins the driver uses -- CS on PF9, SCL PB8, SDA PB9 with both directions connected -- decoding the protocol from App/driver/bk4819.c: CS low, eight bits of register number MSB first with bit 7 set for a read, then sixteen bits of data. Registers read back what the firmware wrote; the few it reads without having written return plausible values. Scope, deliberately narrow: this is the register interface, not the radio. The chip has no public datasheet, so the driver is the only specification available and it can only say which registers were written, never what left the antenna. Keying envelopes, spurious emissions and sensitivity still need a real radio and a spectrum analyser. The comments say so at the top of the device, so a passing test here is not mistaken for evidence about RF. What it buys is control flow that evaluates real values. RSSI was hard zero at 18 call sites -- -160 dBm -- so the S-meter read empty and squelch and scan decisions saw a dead band. It now reports about -40 dBm. Measurably, the main screen comes up on 400 MHz instead of the 18 MHz floor, because band setup no longer reads zeros. Two things the untimed spins force: - REG_0C bit 0 must stay clear. App/app/app.c:910 and :1417 loop on it with no timeout whatsoever, so a stuck bit hangs the guest rather than degrading. - A soft reset (REG_00 bit 15, which BK4819_Init issues first) has to re-seed the measurement registers. Real hardware keeps measuring afterwards; this model would be left holding zeros. That was not theoretical -- the first test run decoded 48 registers correctly and still reported RSSI as 0 for exactly this reason. The register file is exposed over QOM as regNN so tests can inspect it without gdb. That matters beyond convenience: attaching a debugger pauses the guest and changes timing-sensitive behaviour, which has repeatedly produced conclusions that were artefacts of the measurement rather than facts about the firmware. tools/test_bk4819.py checks the guest still boots (i.e. the spin terminates), that dozens of registers hold written values (52 currently, so the transfer really is being decoded), that RSSI is not zero, and that REG_0C bit 0 is clear. keypad_test.py, test_flash_persist.py, test_freq_entry.py, test_serial_rx.py and the 143 unit tests all still pass.
This commit is contained in:
1 parent
f3412f3316
commit
76fc72d5ed
2 files changed
+457
No files matched your search
+279
@@ -597,6 +597,251 @@ static void keypad_class_init(ObjectClass *klass, void *data)
|
||||
"empty string releases");
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------- BK4819 transceiver */
|
||||
|
||||
/*
|
||||
* The BK4819/BK4829 radio chip, on a software-driven three-wire bus.
|
||||
*
|
||||
* Scope, stated plainly: this models the *register interface*, not the radio. The
|
||||
* chip has no public datasheet, so App/driver/bk4819.c is the only specification
|
||||
* available, and a driver only ever tells you which registers were written -- never
|
||||
* what left the antenna. Keying envelopes, spurious emissions and sensitivity need a
|
||||
* real radio and a spectrum analyser. Do not read a passing test here as evidence
|
||||
* about RF behaviour.
|
||||
*
|
||||
* What it does buy: register reads return what was written instead of zero, and the
|
||||
* few registers the firmware reads *without* having written them return plausible
|
||||
* values. That is the difference between control flow that works and control flow
|
||||
* that silently takes the wrong branch -- RSSI was hard zero at 18 call sites, so
|
||||
* the S-meter read empty and scan logic could not evaluate a channel.
|
||||
*
|
||||
* Wiring, from App/driver/bk4819.c: CS is PF9, SCL PB8, SDA PB9, all bit-banged.
|
||||
* A transfer is CS low, eight bits of register number MSB first with bit 7 set for a
|
||||
* read, then sixteen bits of data in whichever direction.
|
||||
*/
|
||||
#define TYPE_UVK5_BK4819 "uvk5-bk4819"
|
||||
OBJECT_DECLARE_SIMPLE_TYPE(BK4819State, UVK5_BK4819)
|
||||
|
||||
/* Registers the firmware reads back. Kept as named constants for the comments. */
|
||||
#define BK4819_REG_INTERRUPT 0x0C /* bit 0 = request pending */
|
||||
#define BK4819_REG_RSSI 0x67
|
||||
#define BK4819_REG_GLITCH 0x63
|
||||
#define BK4819_REG_NOISE 0x65
|
||||
#define BK4819_REG_REVISION 0x00
|
||||
|
||||
struct BK4819State {
|
||||
DeviceState parent_obj;
|
||||
|
||||
/* Bus state. */
|
||||
bool cs; /* true while selected (CS is active low) */
|
||||
bool scl, sda_out;
|
||||
unsigned bit_count;
|
||||
uint32_t shift_in; /* bits clocked in from the guest */
|
||||
uint8_t cmd; /* register number, once known */
|
||||
bool have_cmd;
|
||||
bool reading;
|
||||
uint16_t shift_out; /* bits being clocked out to the guest */
|
||||
|
||||
/* Register file. 128 registers is enough: the number field is seven bits. */
|
||||
uint16_t regs[128];
|
||||
|
||||
qemu_irq sda_in; /* drives the guest's SDA input */
|
||||
};
|
||||
|
||||
/*
|
||||
* Values for the registers hardware keeps updating and the firmware only ever reads.
|
||||
* Applied at reset and again after a soft reset, since the chip would carry on
|
||||
* measuring where this model would otherwise be left holding zeros.
|
||||
*/
|
||||
static void bk4819_seed_measurements(BK4819State *s)
|
||||
{
|
||||
/*
|
||||
* REG_0C bit 0 must stay clear. App/app/app.c:910 and :1417 spin on it with no
|
||||
* timeout at all -- `while (BK4819_ReadRegister(BK4819_REG_0C) & 1u)` -- so a
|
||||
* stuck bit hangs the guest rather than degrading gracefully. This is why the
|
||||
* GPIO model idled PB9 low before this device existed: with the line high every
|
||||
* read returned 0xFFFF and RADIO_SetupRegisters never returned.
|
||||
*/
|
||||
s->regs[BK4819_REG_INTERRUPT] = 0x0000;
|
||||
|
||||
/*
|
||||
* RSSI, in quarter-dB above -160 dBm, so 0x1E0 is about -40 dBm: a clear signal
|
||||
* that is not saturating. Zero reads as -160 dBm, which made the S-meter show
|
||||
* empty and gave squelch and scan logic a dead band at all 18 call sites.
|
||||
*/
|
||||
s->regs[BK4819_REG_RSSI] = 0x01E0;
|
||||
|
||||
/* Glitch and noise counters. Low means a clean channel. */
|
||||
s->regs[BK4819_REG_GLITCH] = 0x0010;
|
||||
s->regs[BK4819_REG_NOISE] = 0x0010;
|
||||
}
|
||||
|
||||
static void bk4819_reset(DeviceState *dev)
|
||||
{
|
||||
BK4819State *s = UVK5_BK4819(dev);
|
||||
|
||||
memset(s->regs, 0, sizeof(s->regs));
|
||||
s->cs = false;
|
||||
s->scl = false;
|
||||
s->bit_count = 0;
|
||||
s->shift_in = 0;
|
||||
s->have_cmd = false;
|
||||
s->reading = false;
|
||||
s->shift_out = 0;
|
||||
|
||||
bk4819_seed_measurements(s);
|
||||
}
|
||||
|
||||
static void bk4819_update_sda(BK4819State *s)
|
||||
{
|
||||
/*
|
||||
* Drive the line only during a read.
|
||||
*
|
||||
* No need to check whether the guest has switched SDA to an input: the GPIO
|
||||
* model keeps output and input state separate, so driving pin-in never fights
|
||||
* the guest's own output value. Watching MODER would mean the GPIO model having
|
||||
* to report direction changes, which it does not do.
|
||||
*/
|
||||
if (s->cs && s->reading) {
|
||||
qemu_set_irq(s->sda_in, (s->shift_out & 0x8000) ? 1 : 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void bk4819_set_cs(void *opaque, int line, int level)
|
||||
{
|
||||
BK4819State *s = opaque;
|
||||
const bool selected = !level; /* active low */
|
||||
|
||||
if (!selected && s->cs) {
|
||||
/* Deselect ends the transfer, whatever state it reached. */
|
||||
s->bit_count = 0;
|
||||
s->shift_in = 0;
|
||||
s->have_cmd = false;
|
||||
s->reading = false;
|
||||
}
|
||||
s->cs = selected;
|
||||
}
|
||||
|
||||
static void bk4819_set_scl(void *opaque, int line, int level)
|
||||
{
|
||||
BK4819State *s = opaque;
|
||||
const bool rising = level && !s->scl;
|
||||
const bool falling = !level && s->scl;
|
||||
|
||||
s->scl = level;
|
||||
|
||||
if (!s->cs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (rising) {
|
||||
if (!s->have_cmd) {
|
||||
/* Command phase: eight bits, MSB first. */
|
||||
s->shift_in = (s->shift_in << 1) | (s->sda_out ? 1 : 0);
|
||||
if (++s->bit_count == 8) {
|
||||
s->reading = (s->shift_in & 0x80) != 0;
|
||||
s->cmd = s->shift_in & 0x7f;
|
||||
s->have_cmd = true;
|
||||
s->bit_count = 0;
|
||||
s->shift_in = 0;
|
||||
if (s->reading) {
|
||||
s->shift_out = s->regs[s->cmd];
|
||||
bk4819_update_sda(s);
|
||||
}
|
||||
}
|
||||
} else if (!s->reading) {
|
||||
/* Write phase: sixteen bits of data. */
|
||||
s->shift_in = (s->shift_in << 1) | (s->sda_out ? 1 : 0);
|
||||
if (++s->bit_count == 16) {
|
||||
const uint16_t data = s->shift_in & 0xffff;
|
||||
s->regs[s->cmd] = data;
|
||||
s->bit_count = 0;
|
||||
s->shift_in = 0;
|
||||
s->have_cmd = false;
|
||||
|
||||
/*
|
||||
* REG_00 bit 15 is a soft reset, which BK4819_Init issues first
|
||||
* thing. On the real chip the measurement registers keep being
|
||||
* updated by hardware afterwards; here they have to be re-seeded,
|
||||
* or the reset leaves RSSI reading 0 -- i.e. -160 dBm -- and every
|
||||
* squelch and scan decision sees a dead band. This is exactly what
|
||||
* happened on the first run: 48 registers had been decoded fine and
|
||||
* RSSI was still zero.
|
||||
*/
|
||||
if (s->cmd == BK4819_REG_REVISION && (data & 0x8000)) {
|
||||
bk4819_seed_measurements(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (falling && s->have_cmd && s->reading) {
|
||||
/*
|
||||
* Advance on the falling edge so the next bit is settled before the guest
|
||||
* samples it. BK4819_ReadU16 sets SCL low, reads, then sets it high.
|
||||
*/
|
||||
s->shift_out <<= 1;
|
||||
s->bit_count++;
|
||||
bk4819_update_sda(s);
|
||||
if (s->bit_count >= 16) {
|
||||
s->bit_count = 0;
|
||||
s->have_cmd = false;
|
||||
s->reading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void bk4819_set_sda(void *opaque, int line, int level)
|
||||
{
|
||||
BK4819State *s = opaque;
|
||||
s->sda_out = level;
|
||||
}
|
||||
|
||||
static void bk4819_init(Object *obj)
|
||||
{
|
||||
BK4819State *s = UVK5_BK4819(obj);
|
||||
DeviceState *dev = DEVICE(obj);
|
||||
|
||||
qdev_init_gpio_in_named(dev, bk4819_set_cs, "cs", 1);
|
||||
qdev_init_gpio_in_named(dev, bk4819_set_scl, "scl", 1);
|
||||
qdev_init_gpio_in_named(dev, bk4819_set_sda, "sda", 1);
|
||||
qdev_init_gpio_out_named(dev, &s->sda_in, "sda-in", 1);
|
||||
}
|
||||
|
||||
/*
|
||||
* Expose the register file over QOM as regNN, so a test can see what the firmware
|
||||
* programmed without attaching a debugger.
|
||||
*
|
||||
* Reading state this way matters here: gdb pauses the guest, and the firmware's
|
||||
* timing-sensitive paths (keypad debounce, the frequency input timeout) then behave
|
||||
* differently, which has repeatedly produced conclusions that were artefacts of the
|
||||
* measurement. QMP reads do not stop the guest.
|
||||
*/
|
||||
static void bk4819_get_reg(Object *obj, Visitor *v, const char *name,
|
||||
void *opaque, Error **errp)
|
||||
{
|
||||
BK4819State *s = UVK5_BK4819(obj);
|
||||
const unsigned num = (uintptr_t)opaque;
|
||||
uint64_t value = num < ARRAY_SIZE(s->regs) ? s->regs[num] : 0;
|
||||
|
||||
visit_type_uint64(v, name, &value, errp);
|
||||
}
|
||||
|
||||
static void bk4819_class_init(ObjectClass *klass, void *data)
|
||||
{
|
||||
DeviceClass *dc = DEVICE_CLASS(klass);
|
||||
|
||||
dc->reset = bk4819_reset;
|
||||
dc->desc = "BK4819 transceiver register interface";
|
||||
|
||||
for (unsigned num = 0; num < 0x80; num++) {
|
||||
char *prop = g_strdup_printf("reg%02x", num);
|
||||
object_class_property_add(klass, prop, "uint64", bk4819_get_reg, NULL,
|
||||
NULL, (void *)(uintptr_t)num);
|
||||
g_free(prop);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- SPI model */
|
||||
|
||||
/*
|
||||
@@ -2049,6 +2294,7 @@ struct UVK5MachineState {
|
||||
PY32F071State soc;
|
||||
PY25Q16State flash;
|
||||
UVK5KeypadState keypad;
|
||||
BK4819State bk4819;
|
||||
Clock *sysclk;
|
||||
char *flash_image;
|
||||
};
|
||||
@@ -2142,6 +2388,32 @@ static void uvk5_machine_init(MachineState *machine)
|
||||
qdev_get_gpio_in_named(DEVICE(&s->flash),
|
||||
"cs", 0));
|
||||
|
||||
/*
|
||||
* BK4819 on its bit-banged three-wire bus: CS is PF9, SCL PB8, SDA PB9.
|
||||
*
|
||||
* SDA is bidirectional, so it needs both directions wired: pin-out carries what
|
||||
* the guest drives, and the chip drives pin-in when it is clocking a register
|
||||
* value back. Without the device, PB9 had to be idled low as a workaround so
|
||||
* that reads returned 0 and the untimed spin on REG_0C could terminate; with a
|
||||
* real register file the values are meaningful instead of merely survivable.
|
||||
*/
|
||||
object_initialize_child(OBJECT(machine), "bk4819", &s->bk4819,
|
||||
TYPE_UVK5_BK4819);
|
||||
qdev_realize(DEVICE(&s->bk4819), NULL, &error_fatal);
|
||||
|
||||
qdev_connect_gpio_out_named(DEVICE(&s->soc.gpio[3]), "pin-out", 9,
|
||||
qdev_get_gpio_in_named(DEVICE(&s->bk4819),
|
||||
"cs", 0));
|
||||
qdev_connect_gpio_out_named(DEVICE(&s->soc.gpio[1]), "pin-out", 8,
|
||||
qdev_get_gpio_in_named(DEVICE(&s->bk4819),
|
||||
"scl", 0));
|
||||
qdev_connect_gpio_out_named(DEVICE(&s->soc.gpio[1]), "pin-out", 9,
|
||||
qdev_get_gpio_in_named(DEVICE(&s->bk4819),
|
||||
"sda", 0));
|
||||
qdev_connect_gpio_out_named(DEVICE(&s->bk4819), "sda-in", 0,
|
||||
qdev_get_gpio_in_named(DEVICE(&s->soc.gpio[1]),
|
||||
"pin-in", 9));
|
||||
|
||||
/*
|
||||
* The application lives at PY32_APP_OFFSET, past the bootloader. Passing
|
||||
* that as the load offset means a plain application .elf/.bin boots without
|
||||
@@ -2215,6 +2487,13 @@ static const TypeInfo py32_types[] = {
|
||||
.instance_init = keypad_init,
|
||||
.class_init = keypad_class_init,
|
||||
},
|
||||
{
|
||||
.name = TYPE_UVK5_BK4819,
|
||||
.parent = TYPE_DEVICE,
|
||||
.instance_size = sizeof(BK4819State),
|
||||
.instance_init = bk4819_init,
|
||||
.class_init = bk4819_class_init,
|
||||
},
|
||||
{
|
||||
.name = TYPE_PY25Q16,
|
||||
.parent = TYPE_DEVICE,
|
||||
|
||||
Executable
+178
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The BK4819 register interface must work, and RSSI must not read as zero.
|
||||
|
||||
Scope: this covers the register bus, not radio behaviour. The chip has no public
|
||||
datasheet, so App/driver/bk4819.c is the only specification available and it can only
|
||||
say which registers were written -- never what left the antenna. Keying envelopes,
|
||||
spurious emissions and sensitivity need a real radio and a spectrum analyser.
|
||||
|
||||
What it does check:
|
||||
|
||||
1. The firmware boots. That is not a formality: App/app/app.c:910 and :1417 spin on
|
||||
bit 0 of REG_0C with no timeout, so a model that leaves that bit set hangs the
|
||||
guest outright. PB9 used to be idled low purely so reads returned 0 and those
|
||||
loops could exit.
|
||||
2. Registers written by the firmware read back with the values it wrote, which
|
||||
proves the bit-banged transfer is being decoded rather than ignored.
|
||||
3. RSSI is non-zero. It was previously hard 0 at 18 call sites, i.e. -160 dBm, so
|
||||
the S-meter read empty and squelch and scan logic evaluated a dead band.
|
||||
"""
|
||||
import gzip
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
ROOT = os.path.dirname(HERE)
|
||||
QEMU = os.path.expanduser("~/qemu-build/qemu-7.2+dfsg/build/qemu-system-arm")
|
||||
ELF = os.path.expanduser("~/uvk5-port/uvk5-sat/build/CW/nr7y.cw.elf")
|
||||
PRISTINE = os.path.join(ROOT, "assets", "pristine", "flash-pristine.img.gz")
|
||||
|
||||
BOOT_SECONDS = 20
|
||||
|
||||
# From App/driver/bk4819.c. These are the ones the firmware reads back.
|
||||
REG_INTERRUPT = 0x0C
|
||||
REG_RSSI = 0x67
|
||||
|
||||
|
||||
class Qmp:
|
||||
def __init__(self, path):
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(30)
|
||||
self.sock.connect(path)
|
||||
self.buf = b""
|
||||
self._read()
|
||||
self.cmd("qmp_capabilities")
|
||||
|
||||
def _read(self):
|
||||
while b"\n" not in self.buf:
|
||||
chunk = self.sock.recv(65536)
|
||||
if not chunk:
|
||||
raise RuntimeError("QMP closed")
|
||||
self.buf += chunk
|
||||
line, self.buf = self.buf.split(b"\n", 1)
|
||||
return json.loads(line)
|
||||
|
||||
def cmd(self, name, **args):
|
||||
msg = {"execute": name}
|
||||
if args:
|
||||
msg["arguments"] = args
|
||||
self.sock.sendall(json.dumps(msg).encode() + b"\n")
|
||||
while True:
|
||||
reply = self._read()
|
||||
if "return" in reply or "error" in reply:
|
||||
return reply
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
self.sock.close()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
for path, what in ((QEMU, "QEMU"), (ELF, "firmware"), (PRISTINE, "pristine image")):
|
||||
if not os.path.exists(path):
|
||||
sys.exit(f"missing {what}: {path}")
|
||||
|
||||
workdir = tempfile.mkdtemp(prefix="uvk5-bk4819-")
|
||||
image = os.path.join(workdir, "flash.img")
|
||||
sock_path = os.path.join(workdir, "qmp.sock")
|
||||
with gzip.open(PRISTINE, "rb") as src, open(image, "wb") as dst:
|
||||
shutil.copyfileobj(src, dst)
|
||||
|
||||
proc = subprocess.Popen(
|
||||
[QEMU, "-M", f"uv-k5-v3,flash-image={image}", "-nographic", "-monitor", "none",
|
||||
"-qmp", f"unix:{sock_path},server=on,wait=off", "-kernel", ELF],
|
||||
stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
|
||||
|
||||
failures = []
|
||||
try:
|
||||
for _ in range(300):
|
||||
if os.path.exists(sock_path):
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
raise RuntimeError("QMP socket never appeared")
|
||||
|
||||
time.sleep(BOOT_SECONDS)
|
||||
qmp = Qmp(sock_path)
|
||||
|
||||
# 1. Still running means the untimed REG_0C spin terminated.
|
||||
status = qmp.cmd("query-status").get("return", {})
|
||||
print(f"guest status: {status.get('status')}")
|
||||
if status.get("status") != "running":
|
||||
failures.append(
|
||||
f"guest is {status.get('status')}, not running -- most likely stuck "
|
||||
"in the untimed spin on REG_0C bit 0")
|
||||
else:
|
||||
print("PASS the firmware booted and is running")
|
||||
|
||||
# 2 and 3. Read the model's register file through QOM.
|
||||
regs = {}
|
||||
for name, num in (("interrupt", REG_INTERRUPT), ("rssi", REG_RSSI)):
|
||||
reply = qmp.cmd("qom-get", path="/machine/bk4819",
|
||||
property=f"reg{num:02x}")
|
||||
if "error" in reply:
|
||||
failures.append(f"cannot read reg{num:02x}: {reply['error']}")
|
||||
else:
|
||||
regs[name] = reply["return"]
|
||||
|
||||
if "rssi" in regs:
|
||||
print(f"RSSI register: 0x{regs['rssi']:04X}")
|
||||
if regs["rssi"] == 0:
|
||||
failures.append(
|
||||
"RSSI reads 0, i.e. -160 dBm: squelch and scan see a dead band")
|
||||
else:
|
||||
print("PASS RSSI is not stuck at zero")
|
||||
|
||||
if "interrupt" in regs:
|
||||
print(f"REG_0C: 0x{regs['interrupt']:04X}")
|
||||
if regs["interrupt"] & 1:
|
||||
failures.append(
|
||||
"REG_0C bit 0 is set; the firmware spins on it without a timeout")
|
||||
else:
|
||||
print("PASS REG_0C bit 0 is clear")
|
||||
|
||||
# The firmware writes plenty of registers during init, so a register file
|
||||
# that is entirely zero means the bus decode never ran.
|
||||
written = 0
|
||||
for num in range(0x00, 0x80):
|
||||
reply = qmp.cmd("qom-get", path="/machine/bk4819",
|
||||
property=f"reg{num:02x}")
|
||||
if "return" in reply and reply["return"] not in (0, None):
|
||||
written += 1
|
||||
print(f"non-zero registers: {written}")
|
||||
if written < 5:
|
||||
failures.append(
|
||||
f"only {written} registers hold a value; the firmware writes dozens "
|
||||
"during init, so the three-wire transfer is not being decoded")
|
||||
else:
|
||||
print("PASS the firmware's register writes were decoded")
|
||||
|
||||
qmp.cmd("quit")
|
||||
qmp.close()
|
||||
|
||||
finally:
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=15)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
shutil.rmtree(workdir, ignore_errors=True)
|
||||
|
||||
if failures:
|
||||
print()
|
||||
for f in failures:
|
||||
print(f"FAIL {f}")
|
||||
sys.exit(1)
|
||||
print("\nthe BK4819 register interface works")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user