diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a3dfe02..4b65ac279 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ## [Unreleased] ### Added +- Enabled `ujson` module. - Added ability to use more than one `DriveBase` in the same script. - Added `Motor.busy()` and `Motor.stalled()` methods, which (in case of `Motor`) are shorthand for `not Motor.control.done()` and diff --git a/bricks/stm32/configport.h b/bricks/stm32/configport.h index daff20774..06ee30512 100644 --- a/bricks/stm32/configport.h +++ b/bricks/stm32/configport.h @@ -61,6 +61,7 @@ #define MICROPY_PY_ALL_SPECIAL_METHODS (PYBRICKS_STM32_OPT_EXTRA_MOD) #define MICROPY_PY_REVERSE_SPECIAL_METHODS (PYBRICKS_STM32_OPT_EXTRA_MOD) #define MICROPY_PY_IO (PYBRICKS_STM32_OPT_EXTRA_MOD) +#define MICROPY_PY_UJSON (PYBRICKS_STM32_OPT_EXTRA_MOD) #define MICROPY_PY_STRUCT (PYBRICKS_STM32_OPT_EXTRA_MOD) #define MICROPY_PY_SYS (PYBRICKS_STM32_OPT_EXTRA_MOD) #define MICROPY_PY_SYS_EXIT (0) diff --git a/bricks/stm32/install_pybricks.py b/bricks/stm32/install_pybricks.py index a5eea53f3..76222f7ca 100644 --- a/bricks/stm32/install_pybricks.py +++ b/bricks/stm32/install_pybricks.py @@ -40,17 +40,32 @@ ERROR_BAD_FIRMWARE = """Invalid firmware file.""" ERROR_BAD_HUB = """Incorrect hub type.""" ERROR_DUAL_BOOT = """You are running dual-boot. Please re-install the official firmware first.""" +ERROR_INCOMPATIBLE_FIRMWARE = """The detected firmware is not compatible with this installer.""" ERROR_EXTERNAL_FLASH = """Unable to create space for Pybricks firmware.""" ERROR_FIRMWARE_COPY_FAILED = """Unable to copy the Pybricks firmware.""" -# Constants. +# Flash constants. FLASH_FIRMWARE_START = 0x8008000 FLASH_READ_SIZE = 32 FLASH_WRITE_SIZE = FLASH_READ_SIZE * 4 + +# Hub IDs. +HUB_ID_SPIKE_PRIME = 0x81 +HUB_ID_SPIKE_ESSENTIAL = 0x83 HUB_IDS = { - "LEGO Technic Large Hub(0x0009)": 0x81, - "LEGO Technic Large Hub(0x0010)": 0x81, - "LEGO Technic Small Hub(0x000D)": 0x83, + "LEGO Technic Large Hub(0x0009)": HUB_ID_SPIKE_PRIME, + "LEGO Technic Large Hub(0x0010)": HUB_ID_SPIKE_PRIME, + "LEGO Technic Small Hub(0x000D)": HUB_ID_SPIKE_ESSENTIAL, +} + +# LEGO firmware versions on which Pybricks installation was successfully tested. +KNOWN_SPIKE_PRIME_FW_VERSIONS = { + "v1.3.00.0000-e8c274a": "SPIKE App v2.0.4", + "v1.4.01.0000-594ce3d": "MINDSTORMS Robot Inventor App v10.3.0", +} +KNOWN_SPIKE_ESSENTIAL_FW_VERSIONS = { + "v1.0.00.0000-d94a557": "SPIKE App v2.0.0", + "v1.0.00.0070-51a2ff4": "SPIKE App v2.0.4", } @@ -75,8 +90,8 @@ def get_lego_firmware_info(): """Gets information about the running firmware.""" # Get firmware/device ID - hub_id = firmware.id_string() - if hub_id not in HUB_IDS: + id_string = firmware.id_string() + if id_string not in HUB_IDS: stop_installation(ERROR_BAD_HUB) # Get firmware reset vector @@ -90,7 +105,7 @@ def get_lego_firmware_info(): firmware_version_position = read_internal_flash_int(FLASH_FIRMWARE_START + 0x200) firmware_version = read_internal_flash(firmware_version_position, 20).decode() - return hub_id, firmware_version, firmware_size, firmware_reset_vector + return id_string, firmware_version, firmware_size, firmware_reset_vector def get_lego_firmware(size): @@ -134,7 +149,7 @@ def get_file_hash(path): def install(auto_reboot=True): # Get information about the running original LEGO firmware. - hub_id, lego_version, lego_size, lego_reset_vector = get_lego_firmware_info() + id_string, lego_version, lego_size, lego_reset_vector = get_lego_firmware_info() # Exit if user is running (outdated) Pybricks dual-boot firmware. if lego_reset_vector >= 0x80C0000: @@ -145,6 +160,15 @@ def install(auto_reboot=True): print(" Version:", lego_version) print(" Size:", lego_size / 1024, "KB") + if ( + HUB_IDS[id_string] == HUB_ID_SPIKE_PRIME + and lego_version not in KNOWN_SPIKE_PRIME_FW_VERSIONS + ) or ( + HUB_IDS[id_string] == HUB_ID_SPIKE_ESSENTIAL + and lego_version not in KNOWN_SPIKE_ESSENTIAL_FW_VERSIONS + ): + stop_installation(ERROR_INCOMPATIBLE_FIRMWARE) + # Back up the LEGO firmware so we can restore it from Pybricks later. print("Creating backup at:", LEGO_FIRMWARE_PATH) with open(LEGO_FIRMWARE_PATH, "wb") as backup_file: @@ -156,7 +180,7 @@ def install(auto_reboot=True): with open(LEGO_METADATA_PATH, "w") as lego_meta_file: lego_backup_size, lego_backup_hash = get_file_hash(LEGO_FIRMWARE_PATH) lego_info = { - "device-id": HUB_IDS[hub_id], + "device-id": HUB_IDS[id_string], "firmware-sha256": lego_backup_hash, "firmware-size": lego_size, "firmware-version": lego_version, @@ -175,7 +199,7 @@ def install(auto_reboot=True): stop_installation(ERROR_BAD_FIRMWARE) # Check that this firmware is made for this hub. - if HUB_IDS[hub_id] != pybricks_info["device-id"]: + if HUB_IDS[id_string] != pybricks_info["device-id"]: stop_installation(ERROR_BAD_HUB) # Display Pybricks firmware information. diff --git a/bricks/stm32/main.c b/bricks/stm32/main.c index 4ff654c80..06c14b838 100644 --- a/bricks/stm32/main.c +++ b/bricks/stm32/main.c @@ -14,6 +14,7 @@ #include #include +#include #include "shared/readline/readline.h" #include "shared/runtime/gchelper.h" @@ -185,8 +186,32 @@ static uint32_t get_user_program(uint8_t **buf, uint32_t *free_len) { // If button was pressed, return code to run script in flash if (err == PBIO_ERROR_CANCELED) { + #if (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) + // Open existing main.mpy file from flash + uint32_t size = 0; + if (pb_flash_file_open_get_size("/_pybricks/main.mpy", &size) != PBIO_SUCCESS) { + return 0; + } + // Check size and allocate buffer + if (size > MPY_MAX_BYTES) { + return 0; + } + *buf = m_malloc(size); + if (*buf == NULL) { + return 0; + } + // Read the file contents + if (pb_flash_file_read(*buf, size) != PBIO_SUCCESS) { + m_free(*buf); + return 0; + } + *free_len = size; + return size; + #else + // Load main program embedded in firmware *buf = &_pb_user_mpy_data; return _pb_user_mpy_size; + #endif } // Handle other errors @@ -220,6 +245,12 @@ static uint32_t get_user_program(uint8_t **buf, uint32_t *free_len) { } *free_len = len; + + #if (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) + // Save program as file + pb_flash_file_write("/_pybricks/main.mpy", *buf, len); + #endif // (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) + return len; } @@ -328,6 +359,12 @@ static void run_user_program(uint32_t len, uint8_t *buf, uint32_t free_len) { } static void stm32_main(void) { + + #if (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) + mp_hal_delay_ms(500); + pb_flash_init(); + #endif + soft_reset: // Stack limit should be less than real stack size, so we have a chance // to recover from limit hit. (Limit is measured in bytes.) diff --git a/bricks/stm32/stm32.mk b/bricks/stm32/stm32.mk index 8e35b2763..2d002a597 100644 --- a/bricks/stm32/stm32.mk +++ b/bricks/stm32/stm32.mk @@ -264,6 +264,7 @@ PYBRICKS_PYBRICKS_SRC_C = $(addprefix pybricks/,\ util_pb/pb_conversions.c \ util_pb/pb_device_stm32.c \ util_pb/pb_error.c \ + util_pb/pb_flash.c \ util_pb/pb_imu.c \ util_pb/pb_task.c \ ) @@ -565,7 +566,7 @@ ifeq ($(PB_LIB_BTSTACK),1) OBJ += $(addprefix $(BUILD)/, $(BTSTACK_SRC_C:.c=.o)) endif ifeq ($(PB_LIB_LITTLEFS),1) -CFLAGS+= -DLFS_NO_ASSERT -DLFS_NO_MALLOC -DLFS_NO_DEBUG -DLFS_NO_WARN -DLFS_NO_ERROR -DLFS_READONLY +CFLAGS+= -DLFS_NO_ASSERT -DLFS_NO_MALLOC -DLFS_NO_DEBUG -DLFS_NO_WARN -DLFS_NO_ERROR OBJ += $(addprefix $(BUILD)/, $(LITTLEFS_SRC_C:.c=.o)) endif ifeq ($(PB_USE_HAL),1) diff --git a/lib/pbio/include/pbio/util.h b/lib/pbio/include/pbio/util.h index b22f4fb3e..236570804 100644 --- a/lib/pbio/include/pbio/util.h +++ b/lib/pbio/include/pbio/util.h @@ -102,6 +102,22 @@ void pbio_set_uint32_le(uint8_t *buf, uint32_t value) { buf[3] = value >> 24; } +#ifndef DOXYGEN +static inline +#endif +/** + * Packs 32-bit big endian value into buffer. + * + * @param [in] buf The buffer. + * @param [in] value The value. + */ +void pbio_set_uint32_be(uint8_t *buf, uint32_t value) { + buf[0] = value >> 24; + buf[1] = value >> 16; + buf[2] = value >> 8; + buf[3] = value; +} + bool pbio_uuid128_reverse_compare(const uint8_t *uuid1, const uint8_t *uuid2); void pbio_uuid128_reverse_copy(uint8_t *dst, const uint8_t *src); diff --git a/lib/pbio/platform/essential_hub/platform.c b/lib/pbio/platform/essential_hub/platform.c index d0c76c91c..7629fe96a 100644 --- a/lib/pbio/platform/essential_hub/platform.c +++ b/lib/pbio/platform/essential_hub/platform.c @@ -434,6 +434,33 @@ void DMA2_Stream0_IRQHandler(void) { pbdrv_adc_stm32_hal_handle_irq(); } +void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi) { + if (hspi->Instance == SPI2) { + // External flash + GPIO_InitTypeDef gpio_init; + + // /CS, active low + gpio_init.Pin = GPIO_PIN_12; + gpio_init.Mode = GPIO_MODE_OUTPUT_PP; + gpio_init.Pull = GPIO_NOPULL; + gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + HAL_GPIO_Init(GPIOB, &gpio_init); + HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); + + // SPI2_SCK + gpio_init.Pin = GPIO_PIN_10; + gpio_init.Mode = GPIO_MODE_AF_PP; + gpio_init.Pull = GPIO_NOPULL; + gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + gpio_init.Alternate = GPIO_AF5_SPI2; + HAL_GPIO_Init(GPIOB, &gpio_init); + + // SPI2_MISO | SPI2_MOSI + gpio_init.Pin = GPIO_PIN_2 | GPIO_PIN_3; + HAL_GPIO_Init(GPIOC, &gpio_init); + } +} + // USB void HAL_PCD_MspInit(PCD_HandleTypeDef *hpcd) { @@ -567,7 +594,7 @@ void SystemInit(void) { RCC_AHB1ENR_GPIODEN | RCC_AHB1ENR_DMA1EN | RCC_AHB1ENR_DMA2EN; RCC->APB1ENR |= RCC_APB1ENR_USART2EN | RCC_APB1ENR_USART3EN | RCC_APB1ENR_UART5EN | RCC_APB1ENR_TIM2EN | RCC_APB1ENR_TIM3EN | RCC_APB1ENR_TIM4EN | - RCC_APB1ENR_I2C3EN | RCC_APB1ENR_FMPI2C1EN; + RCC_APB1ENR_I2C3EN | RCC_APB1ENR_FMPI2C1EN | RCC_APB1ENR_SPI2EN; RCC->APB2ENR |= RCC_APB2ENR_TIM8EN | RCC_APB2ENR_ADC1EN | RCC_APB2ENR_SYSCFGEN; RCC->AHB2ENR |= RCC_AHB2ENR_OTGFSEN; diff --git a/lib/pbio/platform/prime_hub/platform.c b/lib/pbio/platform/prime_hub/platform.c index 90df9bdbf..cdbe973ff 100644 --- a/lib/pbio/platform/prime_hub/platform.c +++ b/lib/pbio/platform/prime_hub/platform.c @@ -671,6 +671,30 @@ void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi) { gpio_init.Alternate = GPIO_AF5_SPI1; HAL_GPIO_Init(GPIOA, &gpio_init); } + if (hspi->Instance == SPI2) { + // External flash + GPIO_InitTypeDef gpio_init; + + // /CS, active low + gpio_init.Pin = GPIO_PIN_12; + gpio_init.Mode = GPIO_MODE_OUTPUT_PP; + gpio_init.Pull = GPIO_NOPULL; + gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + HAL_GPIO_Init(GPIOB, &gpio_init); + HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); + + // SPI2_SCK + gpio_init.Pin = GPIO_PIN_13; + gpio_init.Mode = GPIO_MODE_AF_PP; + gpio_init.Pull = GPIO_NOPULL; + gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; + gpio_init.Alternate = GPIO_AF5_SPI2; + HAL_GPIO_Init(GPIOB, &gpio_init); + + // SPI2_MISO | SPI2_MOSI + gpio_init.Pin = GPIO_PIN_2 | GPIO_PIN_3; + HAL_GPIO_Init(GPIOC, &gpio_init); + } } void SPI1_IRQHandler(void) { @@ -808,7 +832,7 @@ void SystemInit(void) { RCC->APB1ENR |= RCC_APB1ENR_USART2EN | RCC_APB1ENR_UART4EN | RCC_APB1ENR_UART5EN | RCC_APB1ENR_UART7EN | RCC_APB1ENR_UART8EN | RCC_APB1ENR_TIM2EN | RCC_APB1ENR_TIM3EN | RCC_APB1ENR_TIM4EN | RCC_APB1ENR_TIM5EN | RCC_APB1ENR_TIM6EN | RCC_APB1ENR_TIM12EN | - RCC_APB1ENR_I2C2EN | RCC_APB1ENR_DACEN; + RCC_APB1ENR_I2C2EN | RCC_APB1ENR_DACEN | RCC_APB1ENR_SPI2EN; RCC->APB2ENR |= RCC_APB2ENR_TIM1EN | RCC_APB2ENR_TIM8EN | RCC_APB2ENR_UART9EN | RCC_APB2ENR_UART10EN | RCC_APB2ENR_ADC1EN | RCC_APB2ENR_SPI1EN | RCC_APB2ENR_SYSCFGEN; RCC->AHB2ENR |= RCC_AHB2ENR_OTGFSEN; diff --git a/pybricks/experimental/pb_module_experimental.c b/pybricks/experimental/pb_module_experimental.c index 919076ab4..7adf12b1f 100644 --- a/pybricks/experimental/pb_module_experimental.c +++ b/pybricks/experimental/pb_module_experimental.c @@ -7,11 +7,17 @@ #include "py/mphal.h" #include "py/obj.h" +#include "py/objstr.h" #include "py/runtime.h" +#include "py/mperrno.h" + +#include #include #include +#include + #include #include @@ -49,6 +55,73 @@ STATIC mp_obj_t mod_experimental_pthread_raise(mp_obj_t thread_id_in, mp_obj_t e STATIC MP_DEFINE_CONST_FUN_OBJ_2(mod_experimental_pthread_raise_obj, mod_experimental_pthread_raise); #endif // PYBRICKS_HUB_EV3BRICK +#if (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) + +#include + +STATIC mp_obj_t experimental_flash_read_raw(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args, + PB_ARG_REQUIRED(address), + PB_ARG_REQUIRED(len)); + + uint32_t read_address = mp_obj_get_int(address_in); + uint32_t read_len = mp_obj_get_int(len_in); + + // Allocate read data + uint8_t *read_data = m_malloc(read_len); + + // Read flash + pb_assert(pb_flash_raw_read(read_address, read_data, read_len)); + + // Return bytes read + return mp_obj_new_bytes(read_data, read_len); +} +MP_DEFINE_CONST_FUN_OBJ_KW(experimental_flash_read_raw_obj, 0, experimental_flash_read_raw); + +STATIC mp_obj_t experimental_flash_read_file(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args, + PB_ARG_DEFAULT_NONE(path)); + + // Get file path + GET_STR_DATA_LEN(path_in, path, path_len); + + // Mount the file system and open the file + uint32_t size; + pb_assert(pb_flash_file_open_get_size((const char *)path, &size)); + + // Read file contents + uint8_t *file_buf = m_new(uint8_t, size); + pb_assert(pb_flash_file_read(file_buf, size)); + + // Return data + return mp_obj_new_bytes(file_buf, size); +} +MP_DEFINE_CONST_FUN_OBJ_KW(experimental_flash_read_file_obj, 0, experimental_flash_read_file); + +STATIC mp_obj_t experimental_flash_write_file(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { + PB_PARSE_ARGS_FUNCTION(n_args, pos_args, kw_args, + PB_ARG_REQUIRED(path), + PB_ARG_REQUIRED(data)); + + // Get file path + GET_STR_DATA_LEN(path_in, path, path_len); + GET_STR_DATA_LEN(data_in, data, data_len); + + pb_assert(pb_flash_file_write((const char *)path, (const uint8_t *)data, data_len)); + + return mp_const_none; +} +// See also experimental_globals_table below. This function object is added there to make it importable. +MP_DEFINE_CONST_FUN_OBJ_KW(experimental_flash_write_file_obj, 0, experimental_flash_write_file); + +// pybricks.experimental.restore_firmware +STATIC mp_obj_t experimental_restore_firmware(void) { + pb_assert(pb_flash_restore_firmware()); + return mp_const_none; +} +MP_DEFINE_CONST_FUN_OBJ_0(experimental_restore_firmware_obj, experimental_restore_firmware); + +#endif // (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) // pybricks.experimental.hello_world STATIC mp_obj_t experimental_hello_world(size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args) { @@ -97,6 +170,9 @@ STATIC const mp_rom_map_elem_t experimental_globals_table[] = { #else { MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_experimental) }, #endif // PYBRICKS_HUB_EV3BRICK + #if (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) + { MP_ROM_QSTR(MP_QSTR_restore_firmware), MP_ROM_PTR(&experimental_restore_firmware_obj) }, + #endif // (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) { MP_ROM_QSTR(MP_QSTR_hello_world), MP_ROM_PTR(&experimental_hello_world_obj) }, }; STATIC MP_DEFINE_CONST_DICT(pb_module_experimental_globals, experimental_globals_table); diff --git a/pybricks/util_pb/pb_flash.c b/pybricks/util_pb/pb_flash.c new file mode 100644 index 000000000..52b006d25 --- /dev/null +++ b/pybricks/util_pb/pb_flash.c @@ -0,0 +1,682 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + + + +#include "py/mpconfig.h" + +#if (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) + +#include + +#include +#include + +#include +#include + +#include +#include + +#include "py/mphal.h" +#include "py/runtime.h" + +#include "lfs.h" + +#include + +#if PYBRICKS_HUB_PRIMEHUB +#define FLASH_SIZE_TOTAL (32 * 0x100000) +#else +#define FLASH_SIZE_TOTAL (4 * 0x100000) +#endif + +#define FLASH_SIZE_BOOT (0x100000) +#define FLASH_SIZE_USER (FLASH_SIZE_TOTAL - FLASH_SIZE_BOOT) +#define FLASH_SIZE_ERASE (0x1000) +#define FLASH_SIZE_SIZE (4) +#define FLASH_SIZE_WRITE (256) + +#define FLASH_TIMEOUT (100) + +enum { + FLASH_CMD_GET_STATUS = 0x05, + FLASH_CMD_WRITE_ENABLE = 0x06, + FLASH_CMD_GET_ID = 0x9F, + #if PYBRICKS_HUB_PRIMEHUB + FLASH_CMD_READ_DATA = 0x13, + FLASH_CMD_ERASE_BLOCK = 0x21, + FLASH_CMD_WRITE_DATA = 0x12, + #else + FLASH_CMD_READ_DATA = 0x03, + FLASH_CMD_ERASE_BLOCK = 0x20, + FLASH_CMD_WRITE_DATA = 0x02, + #endif +}; + +enum { + FLASH_STATUS_BUSY = 0x01, + FLASH_STATUS_WRITE_ENABLED = 0x02, +}; + +SPI_HandleTypeDef hspi2; + +// Whether the SPI and filesystem have been initialized once after boot. +static bool flash_initialized = false; + +// True sets the notCS pin, False resets it +static void flash_enable(bool enable) { + if (enable) { + HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_RESET); + } else { + HAL_GPIO_WritePin(GPIOB, GPIO_PIN_12, GPIO_PIN_SET); + } +} + +// Configure SPI +static pbio_error_t pb_flash_spi_init(void) { + + // SPI2 parameter configuration + hspi2.Instance = SPI2; + hspi2.Init.Mode = SPI_MODE_MASTER; + hspi2.Init.Direction = SPI_DIRECTION_2LINES; + hspi2.Init.DataSize = SPI_DATASIZE_8BIT; + hspi2.Init.CLKPolarity = SPI_POLARITY_LOW; + hspi2.Init.CLKPhase = SPI_PHASE_1EDGE; + hspi2.Init.NSS = SPI_NSS_SOFT; + hspi2.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_2; + hspi2.Init.FirstBit = SPI_FIRSTBIT_MSB; + + // Save settings + if (HAL_SPI_Init(&hspi2) != HAL_OK) { + return PBIO_ERROR_IO; + } + + // Enable flash + flash_enable(true); + + // Command parameters + uint8_t command = FLASH_CMD_GET_ID; + uint8_t id_data[3]; + HAL_StatusTypeDef err; + + // Send ID command + err = HAL_SPI_Transmit(&hspi2, &command, sizeof(command), FLASH_TIMEOUT); + if (err != 0) { + return PBIO_ERROR_IO; + } + + // Get ID command reply + err = HAL_SPI_Receive(&hspi2, id_data, sizeof(id_data), FLASH_TIMEOUT); + if (err != 0) { + return PBIO_ERROR_IO; + } + flash_enable(false); + + #if PYBRICKS_HUB_PRIMEHUB + const uint8_t valid_id[] = {0xEF, 0x40, 0x19}; + #else + const uint8_t valid_id[] = {0xEF, 0x40, 0x16}; + #endif + + // Verify flash device ID + if (memcmp(valid_id, id_data, sizeof(valid_id))) { + return PBIO_ERROR_NO_DEV; + } + + return PBIO_SUCCESS; +} + +static HAL_StatusTypeDef flash_status_read(uint8_t *status) { + + // Read status command + uint8_t command = FLASH_CMD_GET_STATUS; + + // Write the read status command + flash_enable(true); + HAL_StatusTypeDef err = HAL_SPI_Transmit(&hspi2, &command, sizeof(command), FLASH_TIMEOUT); + if (err != HAL_OK) { + flash_enable(false); + return err; + } + + // Receive data + err = HAL_SPI_Receive(&hspi2, status, 1, FLASH_TIMEOUT); + flash_enable(false); + return err; +} + +static HAL_StatusTypeDef flash_wait_ready(void) { + uint8_t status = FLASH_STATUS_BUSY | FLASH_STATUS_WRITE_ENABLED; + HAL_StatusTypeDef err; + uint32_t start_time = mp_hal_ticks_ms(); + + // While write enabled and / or busy, wait + while (status & (FLASH_STATUS_BUSY | FLASH_STATUS_WRITE_ENABLED)) { + err = flash_status_read(&status); + if (err != HAL_OK) { + return err; + } + if (mp_hal_ticks_ms() - start_time > FLASH_TIMEOUT) { + return HAL_TIMEOUT; + } + } + return HAL_OK; +} + +static HAL_StatusTypeDef flash_write_enable(void) { + uint8_t command = FLASH_CMD_WRITE_ENABLE; + flash_enable(true); + HAL_StatusTypeDef err = HAL_SPI_Transmit(&hspi2, &command, sizeof(command), FLASH_TIMEOUT); + flash_enable(false); + return err; +} + +static HAL_StatusTypeDef flash_send_address_command(uint8_t command, uint32_t address, bool keep_flash_enabled) { + + // Can only read/write user partition + if (address > FLASH_SIZE_USER) { + return HAL_ERROR; + } + + // Pack command and address as big endian. This is the absolute address, + // starting in the boot partition. + #if PYBRICKS_HUB_PRIMEHUB + uint8_t data[5] = {command}; + pbio_set_uint32_be(&data[1], address); + #else + uint8_t data[4]; + pbio_set_uint32_be(&data[0], address); + data[0] = command; + #endif + + // Enable flash + flash_enable(true); + + // Send address command + HAL_StatusTypeDef err = HAL_SPI_Transmit(&hspi2, data, sizeof(data), FLASH_TIMEOUT); + if (err != HAL_OK) { + flash_enable(false); + return err; + } + + // On success, keep flash enabled if we are asked to + if (!keep_flash_enabled) { + flash_enable(false); + } + + return HAL_OK; +} + +static HAL_StatusTypeDef flash_raw_read(uint32_t address, uint8_t *buffer, uint32_t size) { + + // Enable flash and send the read command with address, offset by boot partition + HAL_StatusTypeDef err = flash_send_address_command(FLASH_CMD_READ_DATA, address, true); + if (err != HAL_OK) { + flash_enable(false); + return err; + } + + // Receive data + err = HAL_SPI_Receive(&hspi2, buffer, size, FLASH_TIMEOUT); + flash_enable(false); + return err; +} + + +static HAL_StatusTypeDef flash_raw_write(uint32_t address, const uint8_t *buffer, uint32_t size) { + + // Enable write mode + HAL_StatusTypeDef err = flash_write_enable(); + if (err != HAL_OK) { + return err; + } + + // Enable flash and send the write command with address, offset by boot partition + err = flash_send_address_command(FLASH_CMD_WRITE_DATA, address, true); + if (err != HAL_OK) { + flash_enable(false); + return err; + } + + // Write the data + err = HAL_SPI_Transmit(&hspi2, (uint8_t *)buffer, size, FLASH_TIMEOUT); + flash_enable(false); + if (err != HAL_OK) { + return err; + } + return flash_wait_ready(); +} + +static HAL_StatusTypeDef flash_raw_block_erase(uint32_t address) { + + // Enable write mode + HAL_StatusTypeDef err = flash_write_enable(); + if (err != HAL_OK) { + return err; + } + + // Enable flash and send the erase block command with address, offset by boot partition + err = flash_send_address_command(FLASH_CMD_ERASE_BLOCK, address, false); + if (err != HAL_OK) { + return err; + } + return flash_wait_ready(); +} + + +static HAL_StatusTypeDef flash_user_read(uint32_t address, uint8_t *buffer, uint32_t size) { + if (address + size > FLASH_SIZE_USER) { + return HAL_ERROR; + } + return flash_raw_read(address + FLASH_SIZE_BOOT, buffer, size); +} + +static HAL_StatusTypeDef flash_user_write(uint32_t address, const uint8_t *buffer, uint32_t size) { + if (address + size > FLASH_SIZE_USER) { + return HAL_ERROR; + } + return flash_raw_write(address + FLASH_SIZE_BOOT, buffer, size); +} + +static HAL_StatusTypeDef flash_user_block_erase(uint32_t address) { + if (address > FLASH_SIZE_USER) { + return HAL_ERROR; + } + return flash_raw_block_erase(address + FLASH_SIZE_BOOT); +} + +static int block_device_read(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, void *buffer, lfs_size_t size) { + + lfs_size_t done = 0; + + while (done < size) { + + // How many bytes to read in one go. + lfs_size_t read_now = size - done; + if (read_now > c->read_size) { + read_now = c->read_size; + } + + // Read chunk of flash. + if (flash_user_read(block * c->block_size + off + done, buffer + done, read_now) != HAL_OK) { + return LFS_ERR_IO; + } + done += read_now; + + // Give MicroPython and PBIO some time. + MICROPY_EVENT_POLL_HOOK; + } + return LFS_ERR_OK; +} + +static int block_device_prog(const struct lfs_config *c, lfs_block_t block, lfs_off_t off, const void *buffer, lfs_size_t size) { + lfs_size_t done = 0; + + while (done < size) { + + // How many bytes to write in one go. + lfs_size_t write_now = size - done; + + // Must not be larger than write size + if (write_now > c->prog_size) { + write_now = c->prog_size; + } + + // Must not wrap around the page + lfs_size_t write_max = 256 - ((off + done) & 0xFF); + if (write_now > write_max) { + write_now = write_max; + } + + // Write chunk of flash. + if (flash_user_write(block * c->block_size + off + done, buffer + done, write_now) != HAL_OK) { + return LFS_ERR_IO; + } + done += write_now; + + // Give MicroPython and PBIO some time. + MICROPY_EVENT_POLL_HOOK; + } + + return LFS_ERR_OK; +} + +static int block_device_erase(const struct lfs_config *c, lfs_block_t block) { + // Erase block of flash. Block size assumed to match erase size. + + if (flash_user_block_erase(block * c->block_size) != HAL_OK) { + return LFS_ERR_IO; + } + + // Give MicroPython and PBIO some time. + MICROPY_EVENT_POLL_HOOK; + + return LFS_ERR_OK; +} + +static int block_device_sync(const struct lfs_config *c) { + return 0; +} + +static lfs_t lfs; +static lfs_file_t file; + +static uint8_t lfs_read_buf[256]; +static uint8_t lfs_prog_buf[256]; +static uint8_t lfs_lookahead_buf[256]; +static uint8_t lfs_file_buf[256]; + +static const struct lfs_config cfg = { + .read = block_device_read, + .prog = block_device_prog, + .erase = block_device_erase, + .sync = block_device_sync, + + .read_size = 256, + .prog_size = 256, + .block_size = FLASH_SIZE_ERASE, + .block_count = FLASH_SIZE_USER / FLASH_SIZE_ERASE, + .lookahead = sizeof(lfs_lookahead_buf) * 8, + + .read_buffer = lfs_read_buf, + .prog_buffer = lfs_prog_buf, + .lookahead_buffer = lfs_lookahead_buf, + .file_buffer = lfs_file_buf, +}; + +pbio_error_t pb_flash_init(void) { + + // Initialize only once + if (flash_initialized) { + return PBIO_SUCCESS; + } + + // Init SPI + pbio_error_t err = pb_flash_spi_init(); + if (err != PBIO_SUCCESS) { + return err; + } + + uint8_t lfs_data[58]; + + // Read littlefs data + if (flash_user_read(0, lfs_data, sizeof(lfs_data)) != HAL_OK) { + return PBIO_ERROR_IO; + } + + // Verify magic string for littlefs V1. Anything else is not supported. + const char *magic = "littlefs"; + if (memcmp(&lfs_data[40], magic, sizeof(magic) - 1) != 0) { + return PBIO_ERROR_NOT_SUPPORTED; + } + + // Verify block parameters and on-flash version of littlefs + uint32_t block_size = pbio_get_uint32_le(&lfs_data[28]); + uint32_t block_count = pbio_get_uint32_le(&lfs_data[32]); + uint32_t version = pbio_get_uint32_le(&lfs_data[36]); + + if (cfg.block_size != block_size || cfg.block_count != block_count || version != 0x00010001) { + return PBIO_ERROR_NOT_SUPPORTED; + } + + // Mount file system + if (lfs_mount(&lfs, &cfg) != LFS_ERR_OK) { + return PBIO_ERROR_FAILED; + } + + // Ensure there is a _pybricks system folder + int lfs_err = lfs_mkdir(&lfs, "_pybricks"); + if (!(lfs_err == LFS_ERR_OK || lfs_err == LFS_ERR_EXIST)) { + return PBIO_ERROR_FAILED; + } + + // We're ready to read and write now. + flash_initialized = true; + + // The first write takes a bit longer, so do it now. + const uint8_t *pybricks_magic = (const uint8_t *)"pybricks"; + return pb_flash_file_write("_pybricks/boot.txt", pybricks_magic, sizeof(pybricks_magic) - 1); +} + +pbio_error_t pb_flash_raw_read(uint32_t address, uint8_t *buf, uint32_t size) { + + if (flash_raw_read(address, buf, size) != HAL_OK) { + return PBIO_ERROR_FAILED; + } + return PBIO_SUCCESS; +} + +int lfs_file_open_retry(const char *path, int flags) { + // Try to open the file + int err = lfs_file_open(&lfs, &file, path, flags); + + // If file is already open, close it and retry + if (err == LFS_ERR_NOMEM) { + err = lfs_file_close(&lfs, &file); + if (err != LFS_ERR_OK) { + return err; + } + + err = lfs_file_open(&lfs, &file, path, flags); + } + return err; +} + +pbio_error_t pb_flash_file_open_get_size(const char *path, uint32_t *size) { + + // Check that flash was initialized + if (!flash_initialized) { + return PBIO_ERROR_INVALID_OP; + } + + // Open file + if (lfs_file_open_retry(path, LFS_O_RDONLY) != LFS_ERR_OK) { + return PBIO_ERROR_FAILED; + } + *size = file.size; + return PBIO_SUCCESS; +} + +pbio_error_t pb_flash_file_read(uint8_t *buf, uint32_t size) { + + // Check that flash was initialized + if (!flash_initialized) { + return PBIO_ERROR_INVALID_OP; + } + + // Allow only read in one go for now + if (size != file.size) { + return PBIO_ERROR_INVALID_ARG; + } + + // Read whole file to buffer + if (lfs_file_read(&lfs, &file, buf, size) != size) { + return PBIO_ERROR_FAILED; + } + + // Close the file + if (lfs_file_close(&lfs, &file) != LFS_ERR_OK) { + return PBIO_ERROR_FAILED; + } + return PBIO_SUCCESS; +} + +pbio_error_t pb_flash_file_write(const char *path, const uint8_t *buf, uint32_t size) { + + // Check that flash was initialized + if (!flash_initialized) { + return PBIO_ERROR_INVALID_OP; + } + + // Open file + if (lfs_file_open_retry(path, LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) != LFS_ERR_OK) { + return PBIO_ERROR_FAILED; + } + + // write file contents + if (lfs_file_write(&lfs, &file, buf, size) != size) { + return PBIO_ERROR_FAILED; + } + + // Close the file + if (lfs_file_close(&lfs, &file) != LFS_ERR_OK) { + return PBIO_ERROR_FAILED; + } + return PBIO_SUCCESS; +} + +extern const mp_obj_module_t mp_module_ujson; + +pbio_error_t pb_flash_restore_firmware(void) { + // Check that flash was initialized + if (!flash_initialized) { + return PBIO_ERROR_INVALID_OP; + } + + // Check that the hub is plugged in and charging. + if (pbdrv_charger_get_status() != PBDRV_CHARGER_STATUS_CHARGE) { + mp_printf(&mp_plat_print, "Please connect the hub via USB.\n"); + // TODO: Stop here once charging is fully implemented. + // return PBIO_ERROR_FAILED; + } + + mp_printf(&mp_plat_print, "Checking firmware backup files.\n"); + + // Open meta file + if (lfs_file_open_retry("_firmware/lego-firmware.metadata.json", LFS_O_RDONLY) != LFS_ERR_OK) { + mp_printf(&mp_plat_print, "Unable to open backup meta data file.\n"); + return PBIO_ERROR_FAILED; + } + // Read meta file + size_t meta_size = file.size; + char *meta_data = m_new(char, meta_size); + if (lfs_file_read(&lfs, &file, meta_data, file.size) != file.size) { + mp_printf(&mp_plat_print, "Unable to read backup meta data file.\n"); + return PBIO_ERROR_FAILED; + } + + // Close meta file + if (lfs_file_close(&lfs, &file) != LFS_ERR_OK) { + mp_printf(&mp_plat_print, "Unable to close backup meta data file.\n"); + return PBIO_ERROR_FAILED; + } + + // Read meta data into MicroPython dictionary. + mp_obj_t loads_func = mp_obj_dict_get(mp_module_ujson.globals, MP_ROM_QSTR(MP_QSTR_loads)); + mp_obj_t meta_dict = mp_call_function_1(loads_func, mp_obj_new_str(meta_data, meta_size)); + + // Get relevant meta data + mp_int_t meta_firmware_size = mp_obj_get_int(mp_obj_dict_get(meta_dict, MP_OBJ_NEW_QSTR(qstr_from_str("firmware-size")))); + mp_int_t meta_device_id = mp_obj_get_int(mp_obj_dict_get(meta_dict, MP_OBJ_NEW_QSTR(qstr_from_str("device-id")))); + mp_obj_t meta_firmware_version = mp_obj_dict_get(meta_dict, MP_OBJ_NEW_QSTR(qstr_from_str("firmware-version"))); + + mp_printf(&mp_plat_print, "Detected firmware backup: "); + mp_obj_print(meta_firmware_version, PRINT_STR); + mp_printf(&mp_plat_print, " (%d) bytes.\n", meta_firmware_size); + + // TODO: Get from platform data or hub type + #if PYBRICKS_HUB_PRIMEHUB + mp_int_t valid_device_id = 0x81; + #else + mp_int_t valid_device_id = 0x83; + #endif + + // Verify meta data + if (meta_device_id != valid_device_id) { + mp_printf(&mp_plat_print, "The firmware backup is not valid for this hub.\n"); + return PBIO_ERROR_FAILED; + } + + mp_printf(&mp_plat_print, "Preparing storage for firmware installation...\n"); + + HAL_StatusTypeDef err; + + // Erase boot partition + for (uint32_t address = 0; address < FLASH_SIZE_BOOT; address += FLASH_SIZE_ERASE) { + err = flash_raw_block_erase(address); + if (err != HAL_OK) { + mp_printf(&mp_plat_print, "Unable to erase storage.\n"); + return PBIO_ERROR_IO; + } + MICROPY_EVENT_POLL_HOOK; + } + + mp_printf(&mp_plat_print, "Restoring firmware...\n"); + + // Open firmware file + if (lfs_file_open_retry("_firmware/lego-firmware.bin", LFS_O_RDONLY) != LFS_ERR_OK) { + mp_printf(&mp_plat_print, "Unable to open backup firmware file.\n"); + return PBIO_ERROR_FAILED; + } + + // All known back up firmwares are much larger than this. + if (file.size < 128 * 1024 || file.size != meta_firmware_size) { + // TODO: Check that sha256 matches metadata + mp_printf(&mp_plat_print, "Invalid backup firmware file.\n"); + return PBIO_ERROR_FAILED; + } + + // Initialize buffers + lfs_size_t done = 0; + lfs_size_t read_size, write_size; + uint8_t buf[FLASH_SIZE_WRITE]; + uint8_t *buf_start; + + // Read/write the firmware page by page + while (done < file.size) { + + // The first page is a special case + if (done == 0) { + // It starts of with the firmware size + pbio_set_uint32_le(buf, file.size); + + // We read just enough to fill up the page + buf_start = buf + FLASH_SIZE_SIZE; + read_size = FLASH_SIZE_WRITE - FLASH_SIZE_SIZE; + + // We still write a full page + write_size = FLASH_SIZE_WRITE; + } + // Otherwise, we just look at how much is remaining + else { + // Take the remaining size, or at most one page + read_size = file.size - done; + if (read_size > sizeof(buf)) { + read_size = sizeof(buf); + } + // We write as much as we read + write_size = read_size; + buf_start = buf; + } + + // Read data from the firmware backup file + if (lfs_file_read(&lfs, &file, buf_start, read_size) != read_size) { + mp_printf(&mp_plat_print, "Unable to read backup firmware file.\n"); + return PBIO_ERROR_FAILED; + } + + // Write the data + err = flash_raw_write(done == 0 ? 0 : done + 4, buf, write_size); + if (err != HAL_OK) { + mp_printf(&mp_plat_print, "Unable to copy the backup firmware file.\n"); + return PBIO_ERROR_IO; + } + + done += read_size; + } + + // Close firmware file + if (lfs_file_close(&lfs, &file) != LFS_ERR_OK) { + mp_printf(&mp_plat_print, "Unable to close the backup firmware file.\n"); + return PBIO_ERROR_FAILED; + } + + mp_printf(&mp_plat_print, "Done. The hub will now reboot. Please keep USB attached.\n"); + mp_hal_delay_ms(500); + pbdrv_reset(PBDRV_RESET_ACTION_RESET); + + return PBIO_SUCCESS; +} + +#endif // (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) diff --git a/pybricks/util_pb/pb_flash.h b/pybricks/util_pb/pb_flash.h new file mode 100644 index 000000000..e83ad656c --- /dev/null +++ b/pybricks/util_pb/pb_flash.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2021 The Pybricks Authors + +#ifndef _PB_FLASH_H_ +#define _PB_FLASH_H_ + +#if (PYBRICKS_HUB_PRIMEHUB || PYBRICKS_HUB_ESSENTIALHUB) + +#include + +#include + +// Initialize and mount the file system +pbio_error_t pb_flash_init(void); + +// Read raw bytes from flash, ignoring file system +pbio_error_t pb_flash_raw_read(uint32_t address, uint8_t *buf, uint32_t size); + +// Open one file and get its size. +pbio_error_t pb_flash_file_open_get_size(const char *path, uint32_t *size); + +// Read the file that has been opened, all at once, then close. +pbio_error_t pb_flash_file_read(uint8_t *buf, uint32_t size); + +// Open a file for writing, write data, and close. +pbio_error_t pb_flash_file_write(const char *path, const uint8_t *buf, uint32_t size); + +// Restore a previously backed up firmware file. +pbio_error_t pb_flash_restore_firmware(void); + +#endif + +#endif // _PB_FLASH_H_