From 3cd47f85afef632454b0a87a31b23e34ed1fe77f Mon Sep 17 00:00:00 2001 From: tychovrahe Date: Wed, 23 Nov 2022 16:02:05 +0100 Subject: [PATCH] feat(legacy): qa build for upgrade testing --- core/Makefile | 4 +- core/SConscript.firmware | 17 +- .../firmware/bootloaders/bootloader_1.bin | 1 + .../bootloader_T.bin} | Bin legacy/Makefile.include | 12 + legacy/bootloader/bootloader.c | 10 +- legacy/bootloader/firmware_sign_dev.py | 333 ++++++++++++++++++ legacy/firmware/Makefile | 10 +- legacy/firmware/bl_check.c | 79 ++++- legacy/firmware/bl_check_qa.txt | 1 + legacy/firmware/bl_data.py | 36 +- legacy/firmware/bootloader_qa.dat | Bin 0 -> 52962 bytes legacy/fw_signatures.c | 28 +- legacy/intermediate_fw/Makefile | 11 +- legacy/intermediate_fw/bootloader_qa.dat | 1 + legacy/memory.c | 10 +- legacy/script/update_bootloader.py | 54 ++- legacy/setup.c | 2 +- 18 files changed, 565 insertions(+), 44 deletions(-) create mode 120000 core/embed/firmware/bootloaders/bootloader_1.bin rename core/embed/firmware/{bootloader.bin => bootloaders/bootloader_T.bin} (100%) create mode 100644 legacy/bootloader/firmware_sign_dev.py create mode 100644 legacy/firmware/bl_check_qa.txt create mode 100644 legacy/firmware/bootloader_qa.dat create mode 120000 legacy/intermediate_fw/bootloader_qa.dat diff --git a/core/Makefile b/core/Makefile index 78ed915e3..9861c6c8e 100644 --- a/core/Makefile +++ b/core/Makefile @@ -21,9 +21,11 @@ CROSS_PORT_OPTS ?= PRODUCTION ?= 0 PYOPT ?= 1 BITCOIN_ONLY ?= 0 +BOOTLOADER_QA ?= 0 TREZOR_MODEL ?= T TREZOR_MEMPERF ?= 0 ADDRESS_SANITIZER ?= 0 +UI2 ?= 0 # OpenOCD interface default. Alternative: ftdi/olimex-arm-usb-tiny-h OPENOCD_INTERFACE ?= stlink @@ -164,7 +166,7 @@ build_reflash: ## build reflash firmware + reflash image dd if=build/bootloader/bootloader.bin of=$(REFLASH_BUILD_DIR)/sdimage.bin bs=1 seek=49152 build_firmware: templates build_cross ## build firmware with frozen modules - $(SCONS) CFLAGS="$(CFLAGS)" PRODUCTION="$(PRODUCTION)" TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" $(FIRMWARE_BUILD_DIR)/firmware.bin + $(SCONS) CFLAGS="$(CFLAGS)" PRODUCTION="$(PRODUCTION)" TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="$(PYOPT)" BITCOIN_ONLY="$(BITCOIN_ONLY)" BOOTLOADER_QA="$(BOOTLOADER_QA)" $(FIRMWARE_BUILD_DIR)/firmware.bin build_unix: templates ## build unix port $(SCONS) CFLAGS="$(CFLAGS)" $(UNIX_BUILD_DIR)/trezor-emu-core $(UNIX_PORT_OPTS) TREZOR_MODEL="$(TREZOR_MODEL)" PYOPT="0" BITCOIN_ONLY="$(BITCOIN_ONLY)" TREZOR_EMULATOR_ASAN="$(ADDRESS_SANITIZER)" diff --git a/core/SConscript.firmware b/core/SConscript.firmware index 911f74365..023a53965 100644 --- a/core/SConscript.firmware +++ b/core/SConscript.firmware @@ -5,10 +5,15 @@ import os import tools BITCOIN_ONLY = ARGUMENTS.get('BITCOIN_ONLY', '0') +PRODUCTION = ARGUMENTS.get('PRODUCTION', '0') +BOOTLOADER_QA = ARGUMENTS.get('BOOTLOADER_QA', '0') == '1' EVERYTHING = BITCOIN_ONLY != '1' TREZOR_MODEL = ARGUMENTS.get('TREZOR_MODEL', 'T') DMA2D = TREZOR_MODEL in ('T', ) +if PRODUCTION != '1' and BOOTLOADER_QA: + raise ValueError('Firmware variant for bootloader upgrade testing must be done with PRODUCTION=1') + FEATURE_FLAGS = { "RDI": True, "SECP256K1_ZKP": True, # required for trezor.crypto.curve.bip340 (BIP340/Taproot) @@ -431,7 +436,7 @@ tools.add_font('MONO', FONT_MONO, CPPDEFINES_MOD, SOURCE_MOD) SOURCE_QSTR = SOURCE_MOD + SOURCE_MICROPYTHON + SOURCE_MICROPYTHON_SPEED -env = Environment(ENV=os.environ, CFLAGS='%s -DPRODUCTION=%s -DPYOPT=%s -DBITCOIN_ONLY=%s' % (ARGUMENTS.get('CFLAGS', ''), ARGUMENTS.get('PRODUCTION', '0'), PYOPT, BITCOIN_ONLY)) +env = Environment(ENV=os.environ, CFLAGS='%s -DPRODUCTION=%s -DPYOPT=%s -DBITCOIN_ONLY=%s' % (ARGUMENTS.get('CFLAGS', ''), PRODUCTION, PYOPT, BITCOIN_ONLY)) tools.configure_board(TREZOR_MODEL, env, CPPDEFINES_MOD, SOURCE_TREZORHAL) @@ -769,12 +774,18 @@ obj_program.extend( ' --rename-section .data=.vendorheader,alloc,load,readonly,contents' ' $SOURCE $TARGET', )) + +BOOTLOADER_SUFFIX = TREZOR_MODEL + ('_QA' if BOOTLOADER_QA else '') + obj_program.extend( env.Command( - target='embed/firmware/bootloader.o', - source='embed/firmware/bootloader.bin', + target='embed/firmware/bootloaders/bootloader.o', + source=f'embed/firmware/bootloaders/bootloader_{BOOTLOADER_SUFFIX}.bin', action='$OBJCOPY -I binary -O elf32-littlearm -B arm' ' --rename-section .data=.bootloader' + f' --redefine-sym _binary_embed_firmware_bootloaders_bootloader_{BOOTLOADER_SUFFIX}_bin_start=_binary_embed_firmware_bootloader_bin_start' + f' --redefine-sym _binary_embed_firmware_bootloaders_bootloader_{BOOTLOADER_SUFFIX}_bin_end=_binary_embed_firmware_bootloader_bin_end' + f' --redefine-sym _binary_embed_firmware_bootloaders_bootloader_{BOOTLOADER_SUFFIX}_bin_size=_binary_embed_firmware_bootloader_bin_size' ' $SOURCE $TARGET', )) env.Depends(obj_program, qstr_generated) diff --git a/core/embed/firmware/bootloaders/bootloader_1.bin b/core/embed/firmware/bootloaders/bootloader_1.bin new file mode 120000 index 000000000..6a019c96b --- /dev/null +++ b/core/embed/firmware/bootloaders/bootloader_1.bin @@ -0,0 +1 @@ +../../../../legacy/firmware/bootloader.dat \ No newline at end of file diff --git a/core/embed/firmware/bootloader.bin b/core/embed/firmware/bootloaders/bootloader_T.bin similarity index 100% rename from core/embed/firmware/bootloader.bin rename to core/embed/firmware/bootloaders/bootloader_T.bin diff --git a/legacy/Makefile.include b/legacy/Makefile.include index f7cbd6ec2..20fd6ae9c 100644 --- a/legacy/Makefile.include +++ b/legacy/Makefile.include @@ -156,6 +156,18 @@ CPUFLAGS += -DPRODUCTION=1 $(info PRODUCTION=1) endif +BOOTLOADER_QA ?= 0 +ifeq ($(BOOTLOADER_QA), 0) +CFLAGS += -DBOOTLOADER_QA=0 +CPUFLAGS += -DBOOTLOADER_QA=0 +$(info BOOTLOADER_QA=0) +else +CFLAGS += -DBOOTLOADER_QA=1 +CPUFLAGS += -DBOOTLOADER_QA=1 +$(info BOOTLOADER_QA=1) +endif + + ifeq ($(DEBUG_RNG), 1) CFLAGS += -DDEBUG_RNG=1 else diff --git a/legacy/bootloader/bootloader.c b/legacy/bootloader/bootloader.c index 0dfa42c72..66c1d46bb 100644 --- a/legacy/bootloader/bootloader.c +++ b/legacy/bootloader/bootloader.c @@ -64,6 +64,7 @@ void show_unplug(const char *line1, const char *line2) { "You may now", "unplug your Trezor.", NULL); } +#if !BOOTLOADER_QA static void show_unofficial_warning(const uint8_t *hash) { // On production bootloader, show warning and wait for user // to accept or reject it @@ -93,6 +94,7 @@ static void show_unofficial_warning(const uint8_t *hash) { delay(100000000); #endif } +#endif static void __attribute__((noreturn)) load_app(int signed_firmware) { // zero out SRAM @@ -159,12 +161,16 @@ int main(void) { uint8_t fingerprint[32] = {0}; int signed_firmware = signatures_match(hdr, fingerprint); if (SIG_OK != signed_firmware) { +#if BOOTLOADER_QA + show_halt("Unsigned firmware", "Won't run on QA device"); +#else show_unofficial_warning(fingerprint); +#endif } -#if !PRODUCTION +#if !PRODUCTION && !BOOTLOADER_QA && !DEBUG_T1_SIGNATURES // try to avoid bricking board SWD debug by accident else { - show_halt("Official firmware", "Won't flash on debug device"); + show_halt("Official firmware", "Won't run on debug device"); } #endif diff --git a/legacy/bootloader/firmware_sign_dev.py b/legacy/bootloader/firmware_sign_dev.py new file mode 100644 index 000000000..af66ce1ee --- /dev/null +++ b/legacy/bootloader/firmware_sign_dev.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python3 +import argparse +import hashlib +import struct + +import ecdsa +from ecdsa import BadSignatureError + +SLOTS = 3 + +pubkeys_dev = { + 1: "042c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991ae31a9c671a36543f46cea8fce6984608aa316aa0472a7eed08847440218cb2f", + 2: "04edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f2211452c88a66eb8ac3c19a1cc3a3fc6d72506f6fce2025f738d8b55f29f22125eb0a4", + 3: "04665f660a5052be7a95546a02179058d93d3e08a779734914594346075bb0afd45113948d72cf3dc8f2b70ee02dc1695d051bb0c6da2a914a69045e3277682d3b", +} + +privkeys_dev = { + 1: "0x4444444444444444444444444444444444444444444444444444444444444444", + 2: "0x4545454545454545454545454545454545454545454545454545454545454545", + 3: "0xbfc4bca9c9c228a16639d3503d999a733a439210b64cebe757a4fd03ca46a5c8", +} + +FWHEADER_SIZE = 1024 +SIGNATURES_START = 6 * 4 + 8 + 512 +INDEXES_START = SIGNATURES_START + 3 * 64 + +INDEXES_START_OLD = len("TRZR") + struct.calcsize(" size: + raise ValueError("Chunk too big already") + if len(data) == size: + return data + return data + b"\xFF" * (size - len(data)) + + +# see memory.h for details + + +def prepare_hashes(data): + # process chunks + start = 0 + end = (64 - 1) * 1024 + hashes = [] + for i in range(16): + sector = data[start:end] + if len(sector) > 0: + chunk = pad_to_size(sector, end - start) + hashes.append(hashlib.sha256(chunk).digest()) + else: + hashes.append(b"\x00" * 32) + start = end + end += 64 * 1024 + return hashes + + +def check_hashes(data): + expected_hashes = data[0x20 : 0x20 + 16 * 32] + hashes = b"" + for h in prepare_hashes(data[FWHEADER_SIZE:]): + hashes += h + + if expected_hashes == hashes: + print("HASHES OK") + else: + print("HASHES NOT OK") + + +def update_hashes_in_header(data): + # Store hashes in the firmware header + data = bytearray(data) + o = 0 + for h in prepare_hashes(data[FWHEADER_SIZE:]): + data[0x20 + o : 0x20 + o + 32] = h + o += 32 + return bytes(data) + + +def get_header(data, zero_signatures=False): + if not zero_signatures: + return data[:FWHEADER_SIZE] + else: + data = bytearray(data[:FWHEADER_SIZE]) + data[SIGNATURES_START : SIGNATURES_START + 3 * 64 + 3] = b"\x00" * (3 * 64 + 3) + return bytes(data) + + +def check_size(data): + size = struct.unpack(" 32768: @@ -18,18 +21,19 @@ with open("bl_data.h", "wt") as f: f.write(f"static const uint8_t bl_hash[32] = {{{bl_hash}}};\n") f.write(f"static const uint8_t bl_data[32768] = {{{bl_data}}};\n") -# make sure the last item listed in known_bootloader function -# is our bootloader -with open("bl_check.c", "rt") as f: - hashes = [] - for l in f.readlines(): - if not len(l) >= 78 or not l.startswith(' "\\x'): - continue - l = l[14:78] - h = "" - for i in range(0, len(l), 4): - h += l[i + 2 : i + 4] - hashes.append(h) - check = hashes[-2] + hashes[-1] - if check != bh.hex(): - raise Exception("bootloader hash not listed in bl_check.c") +if fn != "bootloader_qa.dat": + # make sure the last item listed in known_bootloader function + # is our bootloader + with open("bl_check.c", "rt") as f: + hashes = [] + for l in f.readlines(): + if not len(l) >= 78 or not l.startswith(' "\\x'): + continue + l = l[14:78] + h = "" + for i in range(0, len(l), 4): + h += l[i + 2 : i + 4] + hashes.append(h) + check = hashes[-2] + hashes[-1] + if check != bh.hex(): + raise Exception("bootloader hash not listed in bl_check.c") diff --git a/legacy/firmware/bootloader_qa.dat b/legacy/firmware/bootloader_qa.dat new file mode 100644 index 0000000000000000000000000000000000000000..2ddcc7534cb059685967182069d6d501da6dcc5a GIT binary patch literal 52962 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 BzNG*F literal 0 HcmV?d00001 diff --git a/legacy/fw_signatures.c b/legacy/fw_signatures.c index 4317f8b9b..c887b1e40 100644 --- a/legacy/fw_signatures.c +++ b/legacy/fw_signatures.c @@ -64,6 +64,30 @@ static const uint8_t * const pubkey_v3[PUBKEYS] = { // the "new", or second signing scheme keys +#if BOOTLOADER_QA +/* + Previously used for QA bootloader upgrade tests + + Debug private keys for v2 (previously called "new") scheme + corresponding to pubkeys below as python hexstring array: + + + ['4444444444444444444444444444444444444444444444444444444444444444', + '4545454545454545454545454545454545454545454545454545454545454545', + 'bfc4bca9c9c228a16639d3503d999a733a439210b64cebe757a4fd03ca46a5c8', + '5518381d95e93e8eb68a294354989906e3828f36b4556a2ad85d8333294eb1b7', + '1d1d34168760dec092c9ff89377d8659076d2dfd95e0281719c15f90d067e211'] + */ + +static const uint8_t * const pubkey_v2[PUBKEYS] = { + (const uint8_t *)"\x03\x2c\x0b\x7c\xf9\x53\x24\xa0\x7d\x05\x39\x8b\x24\x01\x74\xdc\x0c\x2b\xe4\x44\xd9\x6b\x15\x9a\xa6\xc7\xf7\xb1\xe6\x68\x68\x09\x91", + (const uint8_t *)"\x02\xed\xab\xbd\x16\xb4\x1c\x83\x71\xb9\x2e\xf2\xf0\x4c\x11\x85\xb4\xf0\x3b\x6d\xcd\x52\xba\x9b\x78\xd9\xd7\xc8\x9c\x8f\x22\x11\x45", + (const uint8_t *)"\x03\x66\x5f\x66\x0a\x50\x52\xbe\x7a\x95\x54\x6a\x02\x17\x90\x58\xd9\x3d\x3e\x08\xa7\x79\x73\x49\x14\x59\x43\x46\x07\x5b\xb0\xaf\xd4", + (const uint8_t *)"\x03\x66\x63\x5d\x99\x94\x17\xb6\x55\x66\x86\x6c\x65\x63\x0d\x97\x7a\x7a\xe7\x23\xfe\x5f\x6c\x4c\xd1\x7f\xa0\x0f\x08\x8b\xa1\x84\xc1", + (const uint8_t *)"\x03\xf3\x6c\x7d\x0f\xb6\x15\xad\xa4\x3d\x71\x88\x58\x0f\x15\xeb\xda\x22\xd6\xf6\xb9\xb1\xa9\x2b\xff\x16\xc6\x93\x77\x99\xdc\xbc\x66" + }; +#else + /* Debug private keys for v2 (previously called "new") scheme corresponding to pubkeys below as python hexstring array: @@ -81,8 +105,10 @@ static const uint8_t * const pubkey_v2[PUBKEYS] = { (const uint8_t *)"\x03\x66\x63\x5d\x99\x94\x17\xb6\x55\x66\x86\x6c\x65\x63\x0d\x97\x7a\x7a\xe7\x23\xfe\x5f\x6c\x4c\xd1\x7f\xa0\x0f\x08\x8b\xa1\x84\xc1", (const uint8_t *)"\x03\xf3\x6c\x7d\x0f\xb6\x15\xad\xa4\x3d\x71\x88\x58\x0f\x15\xeb\xda\x22\xd6\xf6\xb9\xb1\xa9\x2b\xff\x16\xc6\x93\x77\x99\xdc\xbc\x66" }; -#else +#endif +#else +#error NOT IMPLEMENTED // These public keys are production keys // - used in production devices diff --git a/legacy/intermediate_fw/Makefile b/legacy/intermediate_fw/Makefile index 35255a2f9..166ddbfb9 100644 --- a/legacy/intermediate_fw/Makefile +++ b/legacy/intermediate_fw/Makefile @@ -44,10 +44,15 @@ endif @printf " MAKO $@\n" $(Q)$(PYTHON) ../vendor/trezor-common/tools/cointool.py render $(MAKO_RENDER_FLAG) $@.mako +ifeq ($(BOOTLOADER_QA), 0) bl_data.h: bl_data.py bootloader.dat - @printf " PYTHON bl_data.py\n" - $(Q)$(PYTHON) bl_data.py - + @printf " PYTHON bl_data.py bootloader.dat\n" + $(Q)$(PYTHON) bl_data.py bootloader.dat +else +bl_data.h: bl_data.py bootloader_qa.dat + @printf " PYTHON bl_data.py bootloader_qa.dat\n" + $(Q)$(PYTHON) bl_data.py bootloader_qa.dat +endif clean:: rm -f bl_data.h find -maxdepth 1 -name "*.mako" | sed 's/.mako$$//' | xargs rm -f diff --git a/legacy/intermediate_fw/bootloader_qa.dat b/legacy/intermediate_fw/bootloader_qa.dat new file mode 120000 index 000000000..b50597d6f --- /dev/null +++ b/legacy/intermediate_fw/bootloader_qa.dat @@ -0,0 +1 @@ +../firmware/bootloader_qa.dat \ No newline at end of file diff --git a/legacy/memory.c b/legacy/memory.c index 39cebb3f9..9f66b94ca 100644 --- a/legacy/memory.c +++ b/legacy/memory.c @@ -30,6 +30,9 @@ void memory_protect(void) { #if PRODUCTION +#if BOOTLOADER_QA +#error BOOTLOADER_QA must be built with PRODUCTION=0 +#endif // Reference STM32F205 Flash programming manual revision 5 // http://www.st.com/resource/en/programming_manual/cd00233952.pdf Section 2.6 // Option bytes @@ -73,11 +76,16 @@ void memory_protect(void) { // example in STM32F427, where the protection bits are read correctly // from OPTION_BYTES and not form FLASH_OPCTR register. // -// Read protection is unaffected and always stays locked to the desired value. +// Read protection is set to level 2. void memory_write_unlock(void) { +#if PRODUCTION +#if BOOTLOADER_QA +#error BOOTLOADER_QA must be built with PRODUCTION=0 +#endif flash_unlock_option_bytes(); flash_program_option_bytes(0x0FFFCCEC); flash_lock_option_bytes(); +#endif } int memory_bootloader_hash(uint8_t *hash) { diff --git a/legacy/script/update_bootloader.py b/legacy/script/update_bootloader.py index 7e32f58ea..14b281430 100755 --- a/legacy/script/update_bootloader.py +++ b/legacy/script/update_bootloader.py @@ -11,12 +11,14 @@ LEGACY_ROOT = Path(__file__).parent.parent.resolve() BOOTLOADER_BUILT = LEGACY_ROOT / "bootloader" / "bootloader.bin" BOOTLOADER_IMAGE = LEGACY_ROOT / "firmware" / "bootloader.dat" +BOOTLOADER_QA_IMAGE = LEGACY_ROOT / "firmware" / "bootloader_qa.dat" BOOTLOADER_VERSION = LEGACY_ROOT / "bootloader" / "version.h" FIRMWARE_VERSION = LEGACY_ROOT / "firmware" / "version.h" BL_CHECK_C = LEGACY_ROOT / "firmware" / "bl_check.c" BL_CHECK_TXT = LEGACY_ROOT / "firmware" / "bl_check.txt" +BL_CHECK_QA_TXT = LEGACY_ROOT / "firmware" / "bl_check_qa.txt" BL_CHECK_PATTERN = """\ if (0 == @@ -30,6 +32,13 @@ BL_CHECK_PATTERN = """\ BL_CHECK_AUTO_BEGIN = " // BEGIN AUTO-GENERATED BOOTLOADER ENTRIES (bl_check.txt)\n" BL_CHECK_AUTO_END = " // END AUTO-GENERATED BOOTLOADER ENTRIES (bl_check.txt)\n" +BL_CHECK_AUTO_QA_BEGIN = ( + " // BEGIN AUTO-GENERATED QA BOOTLOADER ENTRIES (bl_check_qa.txt)\n" +) +BL_CHECK_AUTO_QA_END = ( + " // END AUTO-GENERATED QA BOOTLOADER ENTRIES (bl_check_qa.txt)\n" +) + def cstrify(data: bytes) -> str: """Convert bytes to C string literal. @@ -53,25 +62,28 @@ def load_version(filename: Path) -> str: return "{major}.{minor}.{patch}".format(**vdict) -def load_hash_entries() -> dict[bytes, str]: +def load_hash_entries(txt_file) -> dict[bytes, str]: """Load hash entries from bl_check.txt""" return { bytes.fromhex(digest): comment for digest, comment in ( - line.split(" ", maxsplit=1) - for line in BL_CHECK_TXT.read_text().splitlines() + line.split(" ", maxsplit=1) for line in txt_file.read_text().splitlines() ) } -def regenerate_bl_check(hash_entries: t.Iterable[tuple[bytes, str]]) -> None: +def regenerate_bl_check( + hash_entries: t.Iterable[tuple[bytes, str]], + begin, + end, +) -> None: """Regenerate bl_check.c with given hash entries.""" bl_check_new = [] with open(BL_CHECK_C) as f: # read up to AUTO-BEGIN for line in f: bl_check_new.append(line) - if line == BL_CHECK_AUTO_BEGIN: + if line == begin: break # generate new sections @@ -86,7 +98,7 @@ def regenerate_bl_check(hash_entries: t.Iterable[tuple[bytes, str]]) -> None: # consume up to AUTO-END for line in f: - if line == BL_CHECK_AUTO_END: + if line == end: bl_check_new.append(line) break @@ -98,7 +110,14 @@ def regenerate_bl_check(hash_entries: t.Iterable[tuple[bytes, str]]) -> None: @click.command() @click.option("-c", "--comment", help="Comment for the hash entry.") -def main(comment: str | None) -> None: +@click.option( + "--qa", + is_flag=True, + show_default=True, + default=False, + help="Install as QA bootloader.", +) +def main(comment: str | None, qa: bool) -> None: """Insert a new bootloader image. Takes bootloader/boootloader.dat, copies over firmware/bootloader.dat, and adds @@ -108,7 +127,12 @@ def main(comment: str | None) -> None: digest = sha256(sha256(bl_bytes).digest()).digest() click.echo("Bootloader digest: " + digest.hex()) - entries = load_hash_entries() + txt_file = BL_CHECK_QA_TXT if qa else BL_CHECK_TXT + begin = BL_CHECK_AUTO_QA_BEGIN if qa else BL_CHECK_AUTO_BEGIN + end = BL_CHECK_AUTO_QA_END if qa else BL_CHECK_AUTO_END + image = BOOTLOADER_QA_IMAGE if qa else BOOTLOADER_IMAGE + + entries = load_hash_entries(txt_file) if digest in entries: click.echo("Bootloader already in bl_check.txt: " + entries[digest]) @@ -119,19 +143,23 @@ def main(comment: str | None) -> None: comment = f"{bl_version} shipped with fw {fw_version}" # insert new bootloader - with open(BL_CHECK_TXT, "a") as f: + with open(txt_file, "a") as f: f.write(f"{digest.hex()} {comment}\n") entries[digest] = comment click.echo("Inserted new entry: " + comment) - + # rewrite bl_check.c - regenerate_bl_check(entries.items()) + regenerate_bl_check(entries.items(), begin, end) click.echo("Regenerated bl_check.c") # overwrite bootloader.dat - BOOTLOADER_IMAGE.write_bytes(bl_bytes) - click.echo("Installed bootloader.dat into firmware") + image.write_bytes(bl_bytes) + + if qa: + click.echo("Installed bootloader_qa.dat into firmware") + else: + click.echo("Installed bootloader.dat into firmware") if __name__ == "__main__": diff --git a/legacy/setup.c b/legacy/setup.c index 21ad3c37d..1537c3fb1 100644 --- a/legacy/setup.c +++ b/legacy/setup.c @@ -221,7 +221,7 @@ void mpu_config_bootloader(void) { // Never use in bootloader! Disables access to PPB (including MPU, NVIC, SCB) void mpu_config_firmware(void) { -#if PRODUCTION +#if PRODUCTION || BOOTLOADER_QA // Disable MPU MPU_CTRL = 0;