From 7582be6d5e24148be494d38d22e6ce2ad2090506 Mon Sep 17 00:00:00 2001 From: chrysn Date: Thu, 7 May 2020 16:37:33 +0200 Subject: [PATCH] b/c/particle-mesh: Support particle bootloader Closes: https://github.com/RIOT-OS/RIOT/issues/12320 --- boards/common/particle-mesh/Makefile.include | 42 ++++++--- boards/common/particle-mesh/board.c | 25 ++++++ boards/common/particle-mesh/bootloader.c | 84 ++++++++++++++++++ boards/common/particle-mesh/doc.txt | 32 +++++++ boards/common/particle-mesh/include/board.h | 55 ++++++++++++ .../common/particle-mesh/monofirmware-tool.py | 88 +++++++++++++++++++ boards/particle-argon/Makefile.include | 1 + boards/particle-boron/Makefile.include | 1 + boards/particle-xenon/Makefile.include | 1 + 9 files changed, 317 insertions(+), 12 deletions(-) create mode 100644 boards/common/particle-mesh/bootloader.c create mode 100755 boards/common/particle-mesh/monofirmware-tool.py diff --git a/boards/common/particle-mesh/Makefile.include b/boards/common/particle-mesh/Makefile.include index 5988b063ea..6e7387215c 100644 --- a/boards/common/particle-mesh/Makefile.include +++ b/boards/common/particle-mesh/Makefile.include @@ -9,18 +9,36 @@ PORT_DARWIN ?= $(firstword $(sort $(wildcard /dev/tty.SLAB_USBtoUART*))) # add the common header files to the include path INCLUDES += -I$(RIOTBOARD)/common/particle-mesh/include -# This board uses a DAP-Link programmer -# Flashing support is provided through pyocd (default) and openocd. -# For openocd, a version built against the development branch and containing -# the support for nrf52 cpu is required. -PROGRAMMER ?= pyocd -ifeq (pyocd,$(PROGRAMMER)) - # The board is not recognized automatically by pyocd, so the CPU target - # option is passed explicitly - FLASH_TARGET_TYPE ?= -t nrf52840 - include $(RIOTMAKE)/tools/pyocd.inc.mk -else ifeq (openocd,$(PROGRAMMER)) - DEBUG_ADAPTER ?= dap +ifeq (1,$(PARTICLE_MONOFIRMWARE)) + CFLAGS += -DPARTICLE_MONOFIRMWARE + ROM_OFFSET = 0x30000 + FW_ROM_LEN = 0xc4000 + FLASHFILE = $(BINFILE)-checksummed + # Setting DFU_ARGS won't work as the implied --reset causes errors. + FFLAGS = -d 0x2B04:0xD00E -a 0 -s 0x30000:leave -D $(FLASHFILE) + PROGRAMMER = dfu-util + include $(RIOTMAKE)/tools/dfu.inc.mk +else + # This board uses a DAP-Link programmer + # Flashing support is provided through pyocd (default) and openocd. + # For openocd, a version built against the development branch and containing + # the support for nrf52 cpu is required. + PROGRAMMER ?= pyocd + ifeq (pyocd,$(PROGRAMMER)) + # The board is not recognized automatically by pyocd, so the CPU target + # option is passed explicitly + FLASH_TARGET_TYPE ?= -t nrf52840 + include $(RIOTMAKE)/tools/pyocd.inc.mk + else ifeq (openocd,$(PROGRAMMER)) + DEBUG_ADAPTER ?= dap + endif endif +MONOFIRMWARETOOL = ${RIOTBOARD}/common/particle-mesh/monofirmware-tool.py + +# Rule (variations) for building a monofirmware populated with the right +# (lengths and) checksum in it; only used iuf PARTICLE_MONOFIRMWARE=1 +%.bin-checksummed: %.bin %.elf ${MONOFIRMWARETOOL} + ${MONOFIRMWARETOOL} $*.elf $< $@ + include $(RIOTBOARD)/common/nrf52/Makefile.include diff --git a/boards/common/particle-mesh/board.c b/boards/common/particle-mesh/board.c index d25c4b8f1b..26e354d939 100644 --- a/boards/common/particle-mesh/board.c +++ b/boards/common/particle-mesh/board.c @@ -45,6 +45,31 @@ void board_nrfantenna_select(enum board_nrfantenna_selection choice) void board_init(void) { +#ifdef PARTICLE_MONOFIRMWARE + /* For comparison with MicroPython's hook into this bootloader, they'd set + * SCB->VTOR here. That is necessary because while the particle bootloader + * largely mimics the ARM bootup by requiring a VTOR at the start of the + * writable firmware, it does not set the VTOR to the loaded data. + * + * That step is not executed *right* here as cpu_init will do it a few + * lines down anyway. */ + + /* Force keeping the metadata -- the __attribute__((used)) in their macro + * expansions and the KEEP on the section in the linker only almost do + * that: at least *something* from the object file needs to be referenced + * to pull them all in. */ + extern uint32_t particle_monofirmware_padding; + uint32_t x; + x = (uint32_t)&particle_monofirmware_padding; + (void)x; + + /* Clear out POWER_CLOCK and GPIOTE interrupts set by the bootloader. (If + * actual RIOT code enables them, it'll do so after the board_init call). + * */ + NVIC_DisableIRQ(0); + NVIC_DisableIRQ(6); +#endif + /* initialize the boards LEDs */ gpio_init(LED0_PIN, GPIO_OUT); gpio_set(LED0_PIN); diff --git a/boards/common/particle-mesh/bootloader.c b/boards/common/particle-mesh/bootloader.c new file mode 100644 index 0000000000..7679db3162 --- /dev/null +++ b/boards/common/particle-mesh/bootloader.c @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2014-2016 Freie Universität Berlin + * + * This file is subject to the terms and conditions of the GNU Lesser + * General Public License v2.1. See the file LICENSE in the top level + * directory for more details. + */ + +/** + * @{ + * + * @file + * @author Christian Amsüss + */ + +#ifdef PARTICLE_MONOFIRMWARE +#include +#include "vectors_cortexm.h" + +extern const void *_isr_vectors; + +/* As described in Particle's dynalib/inc/module_info.h (there under LGPL-3 + * terms; detailed license compatibility discussion skipped for not crossing + * any threshold of originality) */ +typedef struct module_info_t { + const void* module_start_address; + const void* module_end_address; + uint8_t reserved; + uint8_t flags; + uint16_t module_version; + uint16_t platform_id; + uint8_t module_function; + uint8_t module_index; + uint32_t dependency; /* was module_dependency_t; kept in here to ensure it's blank */ + uint32_t dependency2; /* was module_dependency_t */ +} module_info_t; + +/* 64 words is the distance between the known ISR table and the fixed-position + * module_info at 0x200. If the ISR length is changed for any reason, this must + * be adapted until particle_monofirmware_module_info is precisely at 0x30200. + * (monofirmware-tool.py will complain if it isn't). + * + * This is not `static` as *something* from this module needs to be visible to + * the outside for the __attribute__((used)) and the KEEP on the section in the + * linker script to work; see also the reference to it in board.c. */ +ISR_VECTOR(50) const uint32_t particle_monofirmware_padding[64] = {0, }; + +#ifdef PARTICLE_MONOFIRMWARE_CHECKSUMLIMIT +/* Watch the ISR vector sequence here: This is placed *after* the module_info + * in the flash (52 > 51), but placed before it in the code to be referencable + * from there. + * + * The actual value is replaced by monofirmware-tool.py. + * */ +ISR_VECTOR(52) static const uint8_t particle_monofirmware_checksum[4] = {0, 0, 0, 0}; +#endif + +ISR_VECTOR(51) static const struct module_info_t particle_monofirmware_module_info = { + .module_start_address = &_isr_vectors, +#ifdef PARTICLE_MONOFIRMWARE_CHECKSUMLIMIT + .module_end_address = &particle_monofirmware_checksum, + /* else, it is set by the monofirmware-tool. + * + * For that (more common) case where we check the full firmware, we would + * want to set set + * + * .module_end_address = &_etext + (&_erelocate - &_srelocate), + * + * but that can not be determined at compile time. Instead, this will be + * populated by monofirmware-tool.py, which needs to write in here anyway + * for the checksum. */ +#endif + .module_version = 0x138, /* module version that micropython uses */ + + .platform_id = PARTICLE_PLATFORM_ID, + + .module_index = 3, /* MOD_FUNC_MONO_FIRMWARE from dynalib/inc/module_info.h */ +}; + +#else +typedef int dont_be_pedantic; +#endif + +/** @} */ diff --git a/boards/common/particle-mesh/doc.txt b/boards/common/particle-mesh/doc.txt index af0fca5ebe..5e9c617448 100644 --- a/boards/common/particle-mesh/doc.txt +++ b/boards/common/particle-mesh/doc.txt @@ -35,6 +35,38 @@ To flash the board with OpenOCD, use the `PROGRAMMER` variable: PROGRAMMER=openocd make BOARD= -C examples/hello-world flash ``` +#### Alternative flashing procedure: Particle bootloader and DFU-Util + +As an alternative to using the Particle Mesh debugger, +the USB DFU mode bootloader shipped with Particle Mesh devices can be used. +That mode can be entered by keeping the SETUP/MODE button pressed after power-up or reset +until the RGB LED **blinks yellow fast**. + +In this mode, the bootloader and Soft Device are kept in place, +reducing the available flash memory to 784kB, +but in turn, devices can easily be switched back to using Particle software. + +In order to use this mode, set `PARTICLE_MONOFIRMWARE=1` wherever you set the `BOARD`. +This will trigger the use of the appropriate linker script, +and switch in [`dfu-util`](http://dfu-util.sourceforge.net/) as the default programmer, +which needs to be installed. + + +On Linux hosts, devices for the DFU mode are frequently only accessible to root, +causing error messages like + + dfu-util: Cannot open DFU device 2b04:d00e + dfu-util: No DFU capable USB device available + +To make Particle devices writable for all users, +follow [Particle's instructions](https://docs.particle.io/support/particle-tools-faq/installing-dfu-util/) +on setting up dfu-util. + +The Particle bootloader checks the firmware's checksum at startup time. +As these checks would be frustrated by flash writes inside RIOT (typically using the @ref drivers_periph_flashpage), +the CFLAG @ref PARTICLE_MONOFIRMWARE_CHECKSUMLIMIT "-DPARTICLE_MONOFIRMWARE_CHECKSUMLIMIT" can be set. +Then, the checksum is only calculated over the memory region that contains the interrupt vector and the bootloader manifest. + ### Reset the board The on-board reset button doesn't work, so to trigger a reset of the board, use diff --git a/boards/common/particle-mesh/include/board.h b/boards/common/particle-mesh/include/board.h index 4343e528b8..be0af85380 100644 --- a/boards/common/particle-mesh/include/board.h +++ b/boards/common/particle-mesh/include/board.h @@ -26,6 +26,61 @@ extern "C" { #endif +/** + * + * @name Bootloader configuration options + * @{ + */ + +/** @brief Build a firmware suitable for the Particle bootloader + * + * If this is defined, additional metadata about the firmware is included in + * the firmware (called the module_info in Particle), and additional code is + * inserted in board setup. + * + * Do not define this manually; instead, set `PARTICLE_MONOFIRMWARE=1` as an + * variable in the build scripts like `BOARD` is defined, and the particle + * common make file defines it and configures suitable postprocessing of the + * binary. + * + * @see @ref boards_common_particle-mesh + * + */ +#ifdef DOXYGEN +#define PARTICLE_MONOFIRMWARE +#endif + +/** @brief Limit Particle bootloader checksumming to the binary start + * + * If this define is set in the Makefile, the binary size announced to the + * bootloader is limited to the reset vector and the firmware metadata, and + * only that part is checksummed. + * + * This is useful when @ref drivers_periph_flashpage is used, as otherwise the + * firmware's writes on itself would invalidate its checksum. + * + * @see @ref boards_common_particle-mesh + */ +#ifdef DOXYGEN +#define PARTICLE_MONOFIRMWARE_CHECKSUMLIMIT +#endif + +/** @brief Platform ID of the board for the Particle bootloader + * + * This is set by the individual board's build configuration, and gets used + * when building with @ref PARTICLE_MONOFIRMWARE; then, it is put into the + * module information for the board bootloader to verify that the firmware was + * built for the right device. + * + * The individual values are documented in the Particle DeviceOS source code in + * `build/platform-id.mk`. + */ +#ifdef DOXYGEN +#define PARTICLE_PLATFORM_ID +#endif + +/** @} */ + /** * @name LED pin configuration * @{ diff --git a/boards/common/particle-mesh/monofirmware-tool.py b/boards/common/particle-mesh/monofirmware-tool.py new file mode 100755 index 0000000000..5cd2807e90 --- /dev/null +++ b/boards/common/particle-mesh/monofirmware-tool.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +"""Verify that a pair of .bin/.elf has the particle_monofirmware_module_info at +the right place. If there is a particle_monofirmware_checksum symbol, verify +that the module_info says the firmware ends at it, and populate the +checksum. Otherwise, append the checksum and set the end-of-firmware field +accordingly.""" + +import os +import sys +import zlib +import subprocess +import argparse + +expected_romstart = 0x30000 +expected_moduleinfo_position = expected_romstart + 0x200 +expected_moduleinfo_size = 0x18 +# offsetof(module_end_address, module_info_t) +end_field_offset = 4 +end_field_start = expected_moduleinfo_position - expected_romstart + end_field_offset +end_field_end = end_field_start + 4 + + +def check(condition, message): + if not condition: + print(message, file=sys.stderr) + sys.exit(1) + + +p = argparse.ArgumentParser(description=__doc__) +p.add_argument("elf_in", help="Program in ELF form") +p.add_argument("bin_in", help="objdump'd ROM binary of elf_in") +p.add_argument("bin_out", help="File to write the modified bin to") +args = p.parse_args() + +symbols = subprocess.check_output([ + os.environ.get('NM', 'nm'), + "--format=posix", + "--print-size", + "--defined-only", + args.elf_in, + ]) +symbols = [line.split(' ') for line in symbols.decode('ascii').split('\n') if line] +position = {s[0]: int(s[2], 16) for s in symbols} +size = {s[0]: int(s[3], 16) for s in symbols if s[3]} +symtype = {s[0]: s[1] for s in symbols} + +romstart = min(pos for (name, pos) in position.items() if symtype[name] not in '?A') + +check(romstart == expected_romstart, "Defined symbols do not start at %#x" % expected_romstart) + +check(position.get('particle_monofirmware_module_info') == expected_moduleinfo_position, + """Struct particle_monofirmware_module_info not found at %#x but on %#x, + adjust the size of particle_monofirmware_padding to match""" % + (expected_moduleinfo_position, position.get('particle_monofirmware_module_info'))) +check(size.get('particle_monofirmware_module_info') == expected_moduleinfo_size, + "Struct particle_monofirmware_module_info not of expected size %d" % expected_moduleinfo_size) + +binary = bytearray(open(args.bin_in, 'rb').read()) + +indicated_start = int.from_bytes(binary[expected_moduleinfo_position - romstart:][:4], 'little') +check(romstart == indicated_start, + "particle_monofirmware_module_info.module_start does not point to the module start") + +if 'particle_monofirmware_checksum' in position: + print("monifirmware-tool: Checksum symbol found, populating it.") + check(size.get('particle_monofirmware_checksum') == 4, "Checksum not of expected size 4") + + checksum_until = position['particle_monofirmware_checksum'] - romstart + + indicated_end = int.from_bytes(binary[end_field_start:end_field_end], 'little') + check(indicated_end == position['particle_monofirmware_checksum'], + "particle_monofirmware_module_info.module_end does not point to particle_monofirmware_checksum") +else: + print("monifirmware-tool: No checksum symbol found, appending the checksum to the binary.") + checksum_until = len(binary) + + # Populating the length: When the whole binary (including the rom + # initialization block) is checksummed, the end address can't be populated + # from C because that would require evaluating `&_etext + (&_erelocate - + # &_srelocate)` in advance + binary[end_field_start:end_field_end] = (checksum_until + romstart).to_bytes(4, 'little') + +checksum = zlib.crc32(binary[:checksum_until]).to_bytes(4, 'big') +binary[checksum_until:checksum_until + 4] = checksum + +with open(args.bin_out, "wb") as o: + o.write(binary) diff --git a/boards/particle-argon/Makefile.include b/boards/particle-argon/Makefile.include index 9a16211c41..c0678225fd 100644 --- a/boards/particle-argon/Makefile.include +++ b/boards/particle-argon/Makefile.include @@ -1 +1,2 @@ +CFLAGS += "-DPARTICLE_PLATFORM_ID=12" include $(RIOTBOARD)/common/particle-mesh/Makefile.include diff --git a/boards/particle-boron/Makefile.include b/boards/particle-boron/Makefile.include index 9a16211c41..47a1c38eea 100644 --- a/boards/particle-boron/Makefile.include +++ b/boards/particle-boron/Makefile.include @@ -1 +1,2 @@ +CFLAGS += "-DPARTICLE_PLATFORM_ID=13" include $(RIOTBOARD)/common/particle-mesh/Makefile.include diff --git a/boards/particle-xenon/Makefile.include b/boards/particle-xenon/Makefile.include index 9a16211c41..191c5283e9 100644 --- a/boards/particle-xenon/Makefile.include +++ b/boards/particle-xenon/Makefile.include @@ -1 +1,2 @@ +CFLAGS += "-DPARTICLE_PLATFORM_ID=14" include $(RIOTBOARD)/common/particle-mesh/Makefile.include