diff --git a/video_game_module_tool/.catalog/CHANGELOG.md b/video_game_module_tool/.catalog/CHANGELOG.md new file mode 100644 index 00000000..b6c1b566 --- /dev/null +++ b/video_game_module_tool/.catalog/CHANGELOG.md @@ -0,0 +1,2 @@ +## 1.0 + - Initial release diff --git a/video_game_module_tool/.catalog/README.md b/video_game_module_tool/.catalog/README.md new file mode 100644 index 00000000..9a1d1cf4 --- /dev/null +++ b/video_game_module_tool/.catalog/README.md @@ -0,0 +1,17 @@ +# Video Game Module Tool + +Standalone firmware updater/installer for the Video Game Module. + +## Features + +- Install the official VGM firmware directly from Flipper Zero (firmware comes bundled with the application) +- Install custom VGM firmware files in UF2 format from SD card (see limitations) + +## Limitations + +When creating a custom UF2 firmware image, some limitations are to keep in mind: + +- Non-flash blocks are NOT supported +- Block payloads MUST be exactly 256 bytes +- Payload target addresses MUST be 256 byte-aligned with no gaps +- Features such as file containers and extension tags are NOT supported diff --git a/video_game_module_tool/.catalog/screenshots/1.png b/video_game_module_tool/.catalog/screenshots/1.png new file mode 100644 index 00000000..636386de Binary files /dev/null and b/video_game_module_tool/.catalog/screenshots/1.png differ diff --git a/video_game_module_tool/.catalog/screenshots/2.png b/video_game_module_tool/.catalog/screenshots/2.png new file mode 100644 index 00000000..8e8ce7bf Binary files /dev/null and b/video_game_module_tool/.catalog/screenshots/2.png differ diff --git a/video_game_module_tool/.catalog/screenshots/3.png b/video_game_module_tool/.catalog/screenshots/3.png new file mode 100644 index 00000000..be7912ba Binary files /dev/null and b/video_game_module_tool/.catalog/screenshots/3.png differ diff --git a/video_game_module_tool/app.c b/video_game_module_tool/app.c new file mode 100644 index 00000000..6c3442c4 --- /dev/null +++ b/video_game_module_tool/app.c @@ -0,0 +1,115 @@ +#include + +#include +#include + +#include +#include + +#include "app_i.h" + +static bool custom_event_callback(void* context, uint32_t event) { + furi_assert(context); + App* app = context; + return scene_manager_handle_custom_event(app->scene_manager, event); +} + +static bool back_event_callback(void* context) { + furi_assert(context); + App* app = context; + return scene_manager_handle_back_event(app->scene_manager); +} + +static void tick_event_callback(void* context) { + furi_assert(context); + App* app = context; + scene_manager_handle_tick_event(app->scene_manager); +} + +static App* app_alloc() { + App* app = malloc(sizeof(App)); + + app->file_path = furi_string_alloc(); + + app->view_dispatcher = view_dispatcher_alloc(); + app->scene_manager = scene_manager_alloc(&scene_handlers, app); + + app->widget = widget_alloc(); + app->submenu = submenu_alloc(); + app->progress = progress_alloc(); + + view_dispatcher_add_view(app->view_dispatcher, ViewIdWidget, widget_get_view(app->widget)); + view_dispatcher_add_view(app->view_dispatcher, ViewIdSubmenu, submenu_get_view(app->submenu)); + view_dispatcher_add_view( + app->view_dispatcher, ViewIdProgress, progress_get_view(app->progress)); + + view_dispatcher_enable_queue(app->view_dispatcher); + view_dispatcher_set_event_callback_context(app->view_dispatcher, app); + view_dispatcher_set_custom_event_callback(app->view_dispatcher, custom_event_callback); + view_dispatcher_set_navigation_event_callback(app->view_dispatcher, back_event_callback); + view_dispatcher_set_tick_event_callback(app->view_dispatcher, tick_event_callback, 500); + + app->notification = furi_record_open(RECORD_NOTIFICATION); + + return app; +} + +static void app_free(App* app) { + furi_record_close(RECORD_NOTIFICATION); + + for(uint32_t i = 0; i < ViewIdMax; ++i) { + view_dispatcher_remove_view(app->view_dispatcher, i); + } + + progress_free(app->progress); + submenu_free(app->submenu); + widget_free(app->widget); + + scene_manager_free(app->scene_manager); + view_dispatcher_free(app->view_dispatcher); + + furi_string_free(app->file_path); + + free(app); +} + +void submenu_item_common_callback(void* context, uint32_t index) { + furi_assert(context); + + App* app = context; + view_dispatcher_send_custom_event(app->view_dispatcher, index); +} + +int32_t vgm_tool_app(void* arg) { + UNUSED(arg); + + Expansion* expansion = furi_record_open(RECORD_EXPANSION); + expansion_disable(expansion); + + const bool is_debug_enabled = furi_hal_rtc_is_flag_set(FuriHalRtcFlagDebug); + if(is_debug_enabled) { + furi_hal_debug_disable(); + } + + App* app = app_alloc(); + Gui* gui = furi_record_open(RECORD_GUI); + + view_dispatcher_attach_to_gui(app->view_dispatcher, gui, ViewDispatcherTypeFullscreen); + scene_manager_next_scene(app->scene_manager, SceneProbe); + + view_dispatcher_run(app->view_dispatcher); + + flasher_deinit(); + app_free(app); + + furi_record_close(RECORD_GUI); + + if(is_debug_enabled) { + furi_hal_debug_enable(); + } + + expansion_enable(expansion); + furi_record_close(RECORD_EXPANSION); + + return 0; +} diff --git a/video_game_module_tool/app_i.h b/video_game_module_tool/app_i.h new file mode 100644 index 00000000..11d5e732 --- /dev/null +++ b/video_game_module_tool/app_i.h @@ -0,0 +1,58 @@ +/** + * @file app_i.h + * @brief Main application header file. + * + * Contains defines, structure definitions and function prototypes + * used throughout the whole application. + */ +#pragma once + +#include +#include + +#include +#include + +#include + +#include + +#include "scenes/scene.h" +#include "views/progress.h" +#include "flasher/flasher.h" + +#define VGM_TOOL_TAG "VgmTool" + +// This can be set by the build system to avoid manual code editing +#ifndef VGM_FW_VERSION +#define VGM_FW_VERSION "0.1.0" +#endif +#define VGM_FW_FILE_EXTENSION ".uf2" +#define VGM_FW_FILE_NAME "vgm-fw-" VGM_FW_VERSION VGM_FW_FILE_EXTENSION + +#define VGM_DEFAULT_FW_FILE APP_ASSETS_PATH(VGM_FW_FILE_NAME) +#define VGM_FW_DEFAULT_PATH EXT_PATH("") + +typedef struct { + SceneManager* scene_manager; + ViewDispatcher* view_dispatcher; + + Widget* widget; + Submenu* submenu; + Progress* progress; + + NotificationApp* notification; + + FuriString* file_path; + FlasherError flasher_error; +} App; + +typedef enum { + ViewIdWidget, + ViewIdSubmenu, + ViewIdProgress, + + ViewIdMax, +} ViewId; + +void submenu_item_common_callback(void* context, uint32_t index); diff --git a/video_game_module_tool/application.fam b/video_game_module_tool/application.fam new file mode 100644 index 00000000..b7c06cf2 --- /dev/null +++ b/video_game_module_tool/application.fam @@ -0,0 +1,17 @@ +App( + appid="video_game_module_tool", + name="Video Game Module Tool", + apptype=FlipperAppType.EXTERNAL, + entry_point="vgm_tool_app", + requires=[ + "gui", + "dialogs", + ], + stack_size=2048, + fap_description="Update Video Game Module's firmware directly from Flipper", + fap_version="1.0", + fap_icon="vgm_tool.png", + fap_category="Tools", + fap_icon_assets="icons", + fap_file_assets="files", +) diff --git a/video_game_module_tool/custom_event.h b/video_game_module_tool/custom_event.h new file mode 100644 index 00000000..1aa476c5 --- /dev/null +++ b/video_game_module_tool/custom_event.h @@ -0,0 +1,11 @@ +#pragma once + +typedef enum { + // Reserve first 100 events for submenu indexes, starting from 0 + CustomEventReserved = 100, + + CustomEventFileConfirmed, + CustomEventFileRejected, + CustomEventSuccessDismissed, + CustomEventRetryRequested, +} CustomEvent; diff --git a/video_game_module_tool/files/vgm-fw-0.1.0.uf2 b/video_game_module_tool/files/vgm-fw-0.1.0.uf2 new file mode 100644 index 00000000..a9e8fa39 Binary files /dev/null and b/video_game_module_tool/files/vgm-fw-0.1.0.uf2 differ diff --git a/video_game_module_tool/flasher/board.c b/video_game_module_tool/flasher/board.c new file mode 100644 index 00000000..49046448 --- /dev/null +++ b/video_game_module_tool/flasher/board.c @@ -0,0 +1,23 @@ +#include "board.h" + +#include +#include + +#define BOARD_RESET_PIN (gpio_ext_pc1) + +void board_init(void) { + furi_hal_gpio_write(&BOARD_RESET_PIN, false); + furi_hal_gpio_init(&BOARD_RESET_PIN, GpioModeOutputPushPull, GpioPullNo, GpioSpeedLow); +} + +void board_deinit(void) { + furi_hal_gpio_write(&BOARD_RESET_PIN, false); + furi_hal_gpio_init_simple(&BOARD_RESET_PIN, GpioModeAnalog); +} + +void board_reset(void) { + furi_hal_gpio_write(&BOARD_RESET_PIN, true); + furi_delay_ms(5); + furi_hal_gpio_write(&BOARD_RESET_PIN, false); + furi_delay_ms(5); +} diff --git a/video_game_module_tool/flasher/board.h b/video_game_module_tool/flasher/board.h new file mode 100644 index 00000000..02bf28b8 --- /dev/null +++ b/video_game_module_tool/flasher/board.h @@ -0,0 +1,23 @@ +/** + * @file board.h + * @brief Video Game Module-specific functions. + */ +#pragma once + +/** + * @brief Initialise the module-specific hardware. + */ +void board_init(void); + +/** + * @brief Disable the module-specific hardware. + */ +void board_deinit(void); + +/** + * @brief Reset the module. + * + * Resets the Video Game Module through the dedicated + * reset pin (Pin 15) + */ +void board_reset(void); diff --git a/video_game_module_tool/flasher/flasher.c b/video_game_module_tool/flasher/flasher.c new file mode 100644 index 00000000..45abe063 --- /dev/null +++ b/video_game_module_tool/flasher/flasher.c @@ -0,0 +1,292 @@ +#include "flasher.h" + +#include +#include + +#include "uf2.h" +#include "swd.h" +#include "board.h" +#include "target.h" +#include "rp2040.h" + +#define TAG "VgmFlasher" + +#define W25Q128_CAPACITY (0x1000000UL) +#define W25Q128_PAGE_SIZE (0x100UL) +#define W25Q128_SECTOR_SIZE (0x1000UL) + +#define PROGRESS_VERIFY_WEIGHT (4U) +#define PROGRESS_ERASE_WEIGHT (6U) +#define PROGRESS_PROGRAM_WEIGHT (90U) + +#define FLASHER_ATTEMPT_COUNT (10UL) + +typedef struct { + FlasherCallback callback; + void* context; +} Flasher; + +static Flasher flasher; + +bool flasher_init(void) { + FURI_LOG_D(TAG, "Attaching the target"); + + board_init(); + + bool success = false; + FURI_CRITICAL_ENTER(); + do { + swd_init(); + if(!target_attach(RP2040_CORE0_ADDR)) { + FURI_LOG_E(TAG, "Failed to attach target"); + break; + } + success = true; + } while(false); + FURI_CRITICAL_EXIT(); + + if(!success) { + flasher_deinit(); + } + + return success; +} + +void flasher_deinit(void) { + FURI_LOG_D(TAG, "Detaching target and restoring pins"); + + FURI_CRITICAL_ENTER(); + target_detach(); + swd_deinit(); + FURI_CRITICAL_EXIT(); + + board_reset(); + board_deinit(); +} + +void flasher_set_callback(FlasherCallback callback, void* context) { + flasher.callback = callback; + flasher.context = context; +} + +static inline bool flasher_init_chip(void) { + FURI_CRITICAL_ENTER(); + const bool success = rp2040_init(); + FURI_CRITICAL_EXIT(); + return success; +} + +static inline bool flasher_erase_sector(uint32_t address) { + FURI_CRITICAL_ENTER(); + const bool success = rp2040_flash_erase_sector(address); + FURI_CRITICAL_EXIT(); + return success; +} + +static inline bool flasher_program_page(uint32_t address, const void* data, size_t data_size) { + FURI_CRITICAL_ENTER(); + const bool success = rp2040_flash_program_page(address, data, data_size); + FURI_CRITICAL_EXIT(); + return success; +} + +static void flasher_emit_progress(uint8_t start, uint8_t weight, uint8_t progress) { + furi_assert(flasher.callback); + + FlasherEvent event = { + .type = FlasherEventTypeProgress, + .progress = start + ((uint32_t)weight * progress) / 100U, + }; + + flasher.callback(event, flasher.context); +} + +static void flasher_emit_error(FlasherError error) { + furi_assert(flasher.callback); + + FlasherEvent event = { + .type = FlasherEventTypeError, + .error = error, + }; + + flasher.callback(event, flasher.context); +} + +static void flasher_emit_success(void) { + furi_assert(flasher.callback); + + FlasherEvent event = { + .type = FlasherEventTypeSuccess, + }; + + flasher.callback(event, flasher.context); +} + +static bool flasher_prepare_target(void) { + bool success = false; + + for(uint32_t i = 0; i < FLASHER_ATTEMPT_COUNT; ++i) { + if(flasher_init()) { + success = true; + break; + } + furi_delay_ms(10); + } + + if(!success) { + flasher_emit_error(FlasherErrorDisconnect); + } + + return success; +} + +static bool flasher_prepare_file(File* file, const char* file_path) { + bool success = false; + + do { + if(!flasher_init_chip()) { + FURI_LOG_E(TAG, "Failed to initialise chip"); + flasher_emit_error(FlasherErrorDisconnect); + break; + } + if(!storage_file_open(file, file_path, FSAM_READ, FSOM_OPEN_EXISTING)) { + FURI_LOG_E(TAG, "Failed to open firmware file: %s", file_path); + flasher_emit_error(FlasherErrorBadFile); + break; + } + success = true; + } while(false); + + return success; +} + +static bool flasher_verify_file(File* file, size_t* data_size) { + bool success = false; + + do { + uint32_t block_count; + if(!uf2_get_block_count(file, &block_count)) { + FURI_LOG_E(TAG, "Failed to get block count"); + flasher_emit_error(FlasherErrorBadFile); + break; + } + + uint32_t blocks_verified; + uint8_t prev_progress = UINT8_MAX; + + for(blocks_verified = 0; blocks_verified < block_count; ++blocks_verified) { + if(!uf2_verify_block(file, RP2040_FAMILY_ID, W25Q128_PAGE_SIZE)) break; + + const uint8_t verify_progress = (blocks_verified * 100UL) / block_count; + if(verify_progress != prev_progress) { + prev_progress = verify_progress; + flasher_emit_progress(0, PROGRESS_VERIFY_WEIGHT, verify_progress); + FURI_LOG_D(TAG, "Verifying file: %u%%", verify_progress); + } + } + + if(blocks_verified < block_count) { + FURI_LOG_E(TAG, "Failed to verify all blocks"); + flasher_emit_error(FlasherErrorBadFile); + break; + } + + const size_t size_total = block_count * W25Q128_PAGE_SIZE; + + if(size_total > W25Q128_CAPACITY) { + FURI_LOG_E(TAG, "File is too large to fit on the flash"); + flasher_emit_error(FlasherErrorBadFile); + break; + } + + if(!storage_file_seek(file, 0, true)) { + FURI_LOG_E(TAG, "Failed to rewind the file"); + flasher_emit_error(FlasherErrorBadFile); + break; + } + + *data_size = size_total; + success = true; + } while(false); + + return success; +} + +static bool flasher_erase_flash(size_t erase_size) { + uint8_t prev_progress = UINT8_MAX; + + size_t size_erased; + for(size_erased = 0; size_erased < erase_size;) { + if(!flasher_erase_sector(size_erased)) { + FURI_LOG_E(TAG, "Failed to erase flash sector at address 0x%zX", size_erased); + flasher_emit_error(FlasherErrorDisconnect); + break; + } + + size_erased += MIN(erase_size - size_erased, W25Q128_SECTOR_SIZE); + + const uint8_t erase_progress = (size_erased * 100UL) / erase_size; + if(erase_progress != prev_progress) { + prev_progress = erase_progress; + flasher_emit_progress(PROGRESS_VERIFY_WEIGHT, PROGRESS_ERASE_WEIGHT, erase_progress); + FURI_LOG_D(TAG, "Erasing flash: %u%%", erase_progress); + } + } + + return size_erased == erase_size; +} + +static bool flasher_program_flash(File* file, size_t data_size) { + uint8_t prev_progress = UINT8_MAX; + + size_t size_programmed; + for(size_programmed = 0; size_programmed < data_size;) { + uint8_t buf[W25Q128_PAGE_SIZE]; + + if(!uf2_read_block(file, buf, W25Q128_PAGE_SIZE)) { + FURI_LOG_E(TAG, "Failed to read UF2 block"); + flasher_emit_error(FlasherErrorBadFile); + break; + } + + if(!flasher_program_page(size_programmed, buf, W25Q128_PAGE_SIZE)) { + FURI_LOG_E(TAG, "Failed to program flash page at address 0x%zX", size_programmed); + flasher_emit_error(FlasherErrorDisconnect); + break; + } + + size_programmed += W25Q128_PAGE_SIZE; + + const uint8_t program_progress = (size_programmed * 100UL) / data_size; + if(program_progress != prev_progress) { + prev_progress = program_progress; + flasher_emit_progress( + PROGRESS_VERIFY_WEIGHT + PROGRESS_ERASE_WEIGHT, + PROGRESS_PROGRAM_WEIGHT, + program_progress); + FURI_LOG_D(TAG, "Programming flash: %u%%", program_progress); + } + } + + return size_programmed == data_size; +} + +void flasher_start(const char* file_path) { + FURI_LOG_D(TAG, "Flashing firmware from file: %s", file_path); + + Storage* storage = furi_record_open(RECORD_STORAGE); + File* file = storage_file_alloc(storage); + size_t data_size; + + do { + if(!flasher_prepare_target()) break; + if(!flasher_prepare_file(file, file_path)) break; + if(!flasher_verify_file(file, &data_size)) break; + if(!flasher_erase_flash(data_size)) break; + if(!flasher_program_flash(file, data_size)) break; + flasher_emit_success(); + } while(false); + + storage_file_free(file); + furi_record_close(RECORD_STORAGE); +} diff --git a/video_game_module_tool/flasher/flasher.h b/video_game_module_tool/flasher/flasher.h new file mode 100644 index 00000000..fac327d9 --- /dev/null +++ b/video_game_module_tool/flasher/flasher.h @@ -0,0 +1,84 @@ +/** + * @file flasher.h + * @brief High-level functions for flashing the VGM firmware. + */ +#pragma once + +#include +#include + +/** + * @brief Enumeration of possible flasher event types. + */ +typedef enum { + FlasherEventTypeProgress, /**< Operation progress has been reported. */ + FlasherEventTypeSuccess, /**< Operation has finished successfully. */ + FlasherEventTypeError, /**< Operation has finished with an error. */ +} FlasherEventType; + +/** + * @brief Enumeration of possible flasher errors. + */ +typedef enum { + FlasherErrorBadFile, /**< File error: wrong format, I/O problem, etc.*/ + FlasherErrorDisconnect, /**< Connection error: Module disconnected, frozen, etc. */ + FlasherErrorUnknown, /**< An error that does not fit to any of the above categories. */ +} FlasherError; + +/** + * @brief Flasher event structure. + * + * Events of FlasherEventTypeSuccess type do not carry additional data. + */ +typedef struct { + FlasherEventType type; /**< Type of the event that has occurred. */ + union { + uint8_t progress; /**< Progress value (0-100). */ + FlasherError error; /**< Error value. */ + }; +} FlasherEvent; + +/** + * @brief Flasher event callback type. + * + * @param[in] event Description of the event that has occurred. + * @param[in,out] context Pointer to a user-specified object. + */ +typedef void (*FlasherCallback)(FlasherEvent event, void* context); + +/** + * @brief Initialise the flasher. + * + * Calling this function will initialise the GPIO, set up the debug + * connection, halt the module's CPU, etc. + * + * @returns true on success, false on failure. + */ +bool flasher_init(void); + +/** + * @brief Disable the flasher. + * + * Calling this function will disable all activated hardware and + * reset the module. + */ +void flasher_deinit(void); + +/** + * @brief Set callback for flasher events. + * + * The callback MUST be set before calling flasher_start(). + * + * @param[in] callback pointer to the function used to receive events. + * @param[in] context pointer to a user-specified object (will be passed to the callback function). + */ +void flasher_set_callback(FlasherCallback callback, void* context); + +/** + * @brief Start the flashing process. + * + * The only way to get the return value is via the event callback. + * + * @param[in] file_path pointer to a zero-terminated string containing the full firmware file path. + */ +void flasher_start(const char* file_path); diff --git a/video_game_module_tool/flasher/rp2040.c b/video_game_module_tool/flasher/rp2040.c new file mode 100644 index 00000000..7afbc4ce --- /dev/null +++ b/video_game_module_tool/flasher/rp2040.c @@ -0,0 +1,433 @@ +#include "rp2040.h" + +#include + +#include "target.h" + +// Most of the below code is heavily inspired by or taken directly from: +// Blackmagic: https://github.com/blackmagic-debug/blackmagic +// Pico-bootrom: https://github.com/raspberrypi/pico-bootrom + +#define RP_REG_ACCESS_NORMAL 0x0000U +#define RP_REG_ACCESS_WRITE_XOR 0x1000U +#define RP_REG_ACCESS_WRITE_ATOMIC_BITSET 0x2000U +#define RP_REG_ACCESS_WRITE_ATOMIC_BITCLR 0x3000U + +#define RP_CLOCKS_BASE_ADDR 0x40008000U +#define RP_CLOCKS_WAKE_EN0 (RP_CLOCKS_BASE_ADDR + 0xa0U) +#define RP_CLOCKS_WAKE_EN1 (RP_CLOCKS_BASE_ADDR + 0xa4U) +#define RP_CLOCKS_WAKE_EN0_MASK 0xff0c0f19U +#define RP_CLOCKS_WAKE_EN1_MASK 0x00002007U + +#define RP_GPIO_QSPI_BASE_ADDR 0x40018000U +#define RP_GPIO_QSPI_SCLK_CTRL (RP_GPIO_QSPI_BASE_ADDR + 0x04U) +#define RP_GPIO_QSPI_CS_CTRL (RP_GPIO_QSPI_BASE_ADDR + 0x0cU) +#define RP_GPIO_QSPI_SD0_CTRL (RP_GPIO_QSPI_BASE_ADDR + 0x14U) +#define RP_GPIO_QSPI_SD1_CTRL (RP_GPIO_QSPI_BASE_ADDR + 0x1cU) +#define RP_GPIO_QSPI_SD2_CTRL (RP_GPIO_QSPI_BASE_ADDR + 0x24U) +#define RP_GPIO_QSPI_SD3_CTRL (RP_GPIO_QSPI_BASE_ADDR + 0x2cU) +#define RP_GPIO_QSPI_CS_DRIVE_NORMAL (0U << 8U) +#define RP_GPIO_QSPI_CS_DRIVE_INVERT (1U << 8U) +#define RP_GPIO_QSPI_CS_DRIVE_LOW (2U << 8U) +#define RP_GPIO_QSPI_CS_DRIVE_HIGH (3U << 8U) +#define RP_GPIO_QSPI_CS_DRIVE_MASK 0x00000300U +#define RP_GPIO_QSPI_SD1_CTRL_INOVER_BITS 0x00030000U +#define RP_GPIO_QSPI_SCLK_POR 0x0000001fU + +#define RP_SSI_BASE_ADDR 0x18000000U +#define RP_SSI_CTRL0 (RP_SSI_BASE_ADDR + 0x00U) +#define RP_SSI_CTRL1 (RP_SSI_BASE_ADDR + 0x04U) +#define RP_SSI_ENABLE (RP_SSI_BASE_ADDR + 0x08U) +#define RP_SSI_SER (RP_SSI_BASE_ADDR + 0x10U) +#define RP_SSI_BAUD (RP_SSI_BASE_ADDR + 0x14U) +#define RP_SSI_TXFLR (RP_SSI_BASE_ADDR + 0x20U) +#define RP_SSI_RXFLR (RP_SSI_BASE_ADDR + 0x24U) +#define RP_SSI_SR (RP_SSI_BASE_ADDR + 0x28U) +#define RP_SSI_ICR (RP_SSI_BASE_ADDR + 0x48U) +#define RP_SSI_DR0 (RP_SSI_BASE_ADDR + 0x60U) +#define RP_SSI_XIP_SPI_CTRL0 (RP_SSI_BASE_ADDR + 0xf4U) +#define RP_SSI_CTRL0_FRF_MASK 0x00600000U +#define RP_SSI_CTRL0_FRF_SERIAL (0U << 21U) +#define RP_SSI_CTRL0_FRF_DUAL (1U << 21U) +#define RP_SSI_CTRL0_FRF_QUAD (2U << 21U) +#define RP_SSI_CTRL0_TMOD_MASK 0x00000300U +#define RP_SSI_CTRL0_TMOD_BIDI (0U << 8U) +#define RP_SSI_CTRL0_TMOD_TX_ONLY (1U << 8U) +#define RP_SSI_CTRL0_TMOD_RX_ONLY (2U << 8U) +#define RP_SSI_CTRL0_TMOD_EEPROM (3U << 8U) +#define RP_SSI_CTRL0_DATA_BIT_MASK 0x001f0000U +#define RP_SSI_CTRL0_DATA_BIT_SHIFT 16U +#define RP_SSI_CTRL0_DATA_BITS(x) (((x)-1U) << RP_SSI_CTRL0_DATA_BIT_SHIFT) +#define RP_SSI_CTRL0_MASK \ + (RP_SSI_CTRL0_FRF_MASK | RP_SSI_CTRL0_TMOD_MASK | RP_SSI_CTRL0_DATA_BIT_MASK) +#define RP_SSI_ENABLE_SSI (1U << 0U) +#define RP_SSI_XIP_SPI_CTRL0_FORMAT_STD_SPI (0U << 0U) +#define RP_SSI_XIP_SPI_CTRL0_FORMAT_SPLIT (1U << 0U) +#define RP_SSI_XIP_SPI_CTRL0_FORMAT_FRF (2U << 0U) +#define RP_SSI_XIP_SPI_CTRL0_ADDRESS_LENGTH(x) (((x)*2U) << 2U) +#define RP_SSI_XIP_SPI_CTRL0_INSTR_LENGTH_8b (2U << 8U) +#define RP_SSI_XIP_SPI_CTRL0_WAIT_CYCLES(x) (((x)*8U) << 11U) +#define RP_SSI_XIP_SPI_CTRL0_XIP_CMD_SHIFT 24U +#define RP_SSI_XIP_SPI_CTRL0_XIP_CMD(x) ((x) << RP_SSI_XIP_SPI_CTRL0_XIP_CMD_SHIFT) +#define RP_SSI_XIP_SPI_CTRL0_TRANS_1C1A (0U << 0U) +#define RP_SSI_XIP_SPI_CTRL0_TRANS_1C2A (1U << 0U) +#define RP_SSI_XIP_SPI_CTRL0_TRANS_2C2A (2U << 0U) + +#define RP_PADS_QSPI_BASE_ADDR 0x40020000U +#define RP_PADS_QSPI_GPIO_SCLK (RP_PADS_QSPI_BASE_ADDR + 0x04U) +#define RP_PADS_QSPI_GPIO_SD0 (RP_PADS_QSPI_BASE_ADDR + 0x08U) +#define RP_PADS_QSPI_GPIO_SD1 (RP_PADS_QSPI_BASE_ADDR + 0x0cU) +#define RP_PADS_QSPI_GPIO_SD2 (RP_PADS_QSPI_BASE_ADDR + 0x10U) +#define RP_PADS_QSPI_GPIO_SD3 (RP_PADS_QSPI_BASE_ADDR + 0x14U) +#define RP_PADS_QSPI_GPIO_SCLK_FAST_SLEW 0x00000001U +#define RP_PADS_QSPI_GPIO_SCLK_8mA_DRIVE 0x00000020U +#define RP_PADS_QSPI_GPIO_SCLK_IE 0x00000040U +#define RP_PADS_QSPI_GPIO_SD0_OD_BITS 0x00000080U +#define RP_PADS_QSPI_GPIO_SD0_PUE_BITS 0x00000008U +#define RP_PADS_QSPI_GPIO_SD0_PDE_BITS 0x00000004U + +#define RP_RESETS_BASE_ADDR 0x4000c000U +#define RP_RESETS_RESET (RP_RESETS_BASE_ADDR + 0x00U) +#define RP_RESETS_RESET_DONE (RP_RESETS_BASE_ADDR + 0x08U) +#define RP_RESETS_RESET_IO_QSPI_BITS 0x00000040U +#define RP_RESETS_RESET_PADS_QSPI_BITS 0x00000200U + +// SPI Flash defines +#define SPI_FLASH_OPCODE_MASK 0x00ffU +#define SPI_FLASH_OPCODE(x) ((x)&SPI_FLASH_OPCODE_MASK) +#define SPI_FLASH_DUMMY_MASK 0x0700U +#define SPI_FLASH_DUMMY_SHIFT 8U +#define SPI_FLASH_DUMMY_LEN(x) (((x) << SPI_FLASH_DUMMY_SHIFT) & SPI_FLASH_DUMMY_MASK) +#define SPI_FLASH_OPCODE_MODE_MASK 0x0800U +#define SPI_FLASH_OPCODE_ONLY (0U << 11U) +#define SPI_FLASH_OPCODE_3B_ADDR (1U << 11U) +#define SPI_FLASH_DATA_MASK 0x1000U +#define SPI_FLASH_DATA_SHIFT 12U +#define SPI_FLASH_DATA_IN (0U << SPI_FLASH_DATA_SHIFT) +#define SPI_FLASH_DATA_OUT (1U << SPI_FLASH_DATA_SHIFT) + +#define SPI_FLASH_CMD_WRITE_ENABLE \ + (SPI_FLASH_OPCODE_ONLY | SPI_FLASH_DUMMY_LEN(0) | SPI_FLASH_OPCODE(0x06U)) +#define SPI_FLASH_CMD_PAGE_PROGRAM \ + (SPI_FLASH_OPCODE_3B_ADDR | SPI_FLASH_DATA_OUT | SPI_FLASH_DUMMY_LEN(0) | \ + SPI_FLASH_OPCODE(0x02)) +#define SPI_FLASH_CMD_SECTOR_ERASE \ + (SPI_FLASH_OPCODE_3B_ADDR | SPI_FLASH_DUMMY_LEN(0) | SPI_FLASH_OPCODE(0x20U)) +#define SPI_FLASH_CMD_CHIP_ERASE \ + (SPI_FLASH_OPCODE_ONLY | SPI_FLASH_DUMMY_LEN(0) | SPI_FLASH_OPCODE(0x60U)) +#define SPI_FLASH_CMD_READ_STATUS \ + (SPI_FLASH_OPCODE_ONLY | SPI_FLASH_DATA_IN | SPI_FLASH_DUMMY_LEN(0) | SPI_FLASH_OPCODE(0x05U)) +#define SPI_FLASH_CMD_READ_JEDEC_ID \ + (SPI_FLASH_OPCODE_ONLY | SPI_FLASH_DATA_IN | SPI_FLASH_DUMMY_LEN(0) | SPI_FLASH_OPCODE(0x9FU)) +#define SPI_FLASH_CMD_READ_SFDP \ + (SPI_FLASH_OPCODE_3B_ADDR | SPI_FLASH_DATA_IN | SPI_FLASH_DUMMY_LEN(1) | \ + SPI_FLASH_OPCODE(0x5AU)) +#define SPI_FLASH_CMD_WAKE_UP \ + (SPI_FLASH_OPCODE_ONLY | SPI_FLASH_DUMMY_LEN(0) | SPI_FLASH_OPCODE(0xABU)) +#define SPI_FLASH_CMD_READ_DATA \ + (SPI_FLASH_OPCODE_3B_ADDR | SPI_FLASH_DATA_IN | SPI_FLASH_DUMMY_LEN(0) | \ + SPI_FLASH_OPCODE(0x03U)) + +#define SPI_FLASH_STATUS_BUSY 0x01U +#define SPI_FLASH_STATUS_WRITE_ENABLED 0x02U + +#define RP2040_IO_PADS_BITS (RP_RESETS_RESET_IO_QSPI_BITS | RP_RESETS_RESET_PADS_QSPI_BITS) + +#define W25X_CMD_RESET_ENABLE (0x66U) +#define W25X_CMD_RESET (0x99U) + +#define TAG "VgmRp2040" + +static bool rp2040_spi_gpio_init(void) { + bool success = false; + + do { + if(!target_write_memory_32( + RP_RESETS_RESET | RP_REG_ACCESS_WRITE_ATOMIC_BITSET, RP2040_IO_PADS_BITS)) + break; + if(!target_write_memory_32( + RP_RESETS_RESET | RP_REG_ACCESS_WRITE_ATOMIC_BITCLR, RP2040_IO_PADS_BITS)) + break; + + uint32_t reset_done = 0; + while((reset_done & RP2040_IO_PADS_BITS) != RP2040_IO_PADS_BITS) { + if(!target_read_memory_32(RP_RESETS_RESET_DONE, &reset_done)) break; + } + + if(reset_done == 0) break; + + if(!target_write_memory_32(RP_GPIO_QSPI_SCLK_CTRL, 0)) break; + if(!target_write_memory_32(RP_GPIO_QSPI_CS_CTRL, 0)) break; + if(!target_write_memory_32(RP_GPIO_QSPI_SD0_CTRL, 0)) break; + if(!target_write_memory_32(RP_GPIO_QSPI_SD1_CTRL, 0)) break; + if(!target_write_memory_32(RP_GPIO_QSPI_SD2_CTRL, 0)) break; + if(!target_write_memory_32(RP_GPIO_QSPI_SD3_CTRL, 0)) break; + + success = true; + } while(false); + + return success; +} + +// Configure SSI in regular SPI mode +static bool rp2040_spi_init(void) { + bool success = false; + + do { + // Disable SSI + if(!target_write_memory_32(RP_SSI_ENABLE, 0)) break; + // Clear error all flags + if(!target_read_memory_32(RP_SSI_SR, NULL)) break; + // Clear all pending interrupts + if(!target_read_memory_32(RP_SSI_ICR, NULL)) break; + // Set SPI clock divisor (Fclk_out = Fssi_clk / RP_SSI_BAUD) + if(!target_write_memory_32(RP_SSI_BAUD, 6UL)) break; + // Set SPI configuration: + // - Regular 1-bit SPI frame format, + // - Frame size = 8 bit, + // - Both transmit and receive + if(!target_write_memory_32( + RP_SSI_CTRL0, + RP_SSI_CTRL0_FRF_SERIAL | RP_SSI_CTRL0_DATA_BITS(8) | RP_SSI_CTRL0_TMOD_BIDI)) + break; + if(!target_write_memory_32(RP_SSI_SER, 1)) break; + // Enable SSI + if(!target_write_memory_32(RP_SSI_ENABLE, 1)) break; + success = true; + } while(false); + + return success; +} + +// Force CS pin to a chosen state +static bool rp2040_spi_chip_select(uint32_t state) { + bool success = false; + + do { + uint32_t cs_value; + // Read GPIO control register + if(!target_read_memory_32(RP_GPIO_QSPI_CS_CTRL, &cs_value)) break; + // Modify GPIO control register + if(!target_write_memory_32( + RP_GPIO_QSPI_CS_CTRL, (cs_value & (~RP_GPIO_QSPI_CS_DRIVE_MASK)) | state)) + break; + success = true; + } while(false); + + return success; +} + +// Perform an SPI transaction (transmit one byte, receive one byte at the same time) +static bool rp2040_spi_txrx(uint8_t tx_data, uint8_t* rx_data) { + bool success = false; + + do { + // Write to SSI data register 0 + if(!target_write_memory_32(RP_SSI_DR0, tx_data)) break; + uint32_t value; + // Read from SSI data register 0 + if(!target_read_memory_32(RP_SSI_DR0, &value)) break; + if(rx_data) { + *rx_data = value; + } + success = true; + } while(false); + + return success; +} + +// Prepare SPI flash operation +static bool rp2040_spi_setup_txrx(uint16_t command, uint32_t address, size_t data_size) { + bool success = false; + + do { + // Number of data frames = data_size + if(!target_write_memory_32(RP_SSI_CTRL1, data_size)) break; + // Select flash chip + if(!rp2040_spi_chip_select(RP_GPIO_QSPI_CS_DRIVE_LOW)) break; + // Transmit command + const uint8_t opcode = command & SPI_FLASH_OPCODE_MASK; + if(!rp2040_spi_txrx(opcode, NULL)) break; + + // Transmit 24-bit address for commands that require it + if((command & SPI_FLASH_OPCODE_MODE_MASK) == SPI_FLASH_OPCODE_3B_ADDR) { + if(!rp2040_spi_txrx((address >> 16U) & 0xFFUL, NULL)) break; + if(!rp2040_spi_txrx((address >> 8U) & 0xFFUL, NULL)) break; + if(!rp2040_spi_txrx(address & 0xFFUL, NULL)) break; + } + + const size_t inter_length = (command & SPI_FLASH_DUMMY_MASK) >> SPI_FLASH_DUMMY_SHIFT; + + size_t i; + for(i = 0; i < inter_length; ++i) { + if(!rp2040_spi_txrx(0, NULL)) break; + } + if(i < inter_length) break; + + success = true; + } while(false); + + return success; +} + +static bool rp2040_spi_read(uint16_t command, uint32_t address, void* data, size_t data_size) { + bool success = false; + + do { + if(!rp2040_spi_setup_txrx(command, address, data_size)) break; + uint8_t* rx_data = data; + size_t rx_data_size; + for(rx_data_size = 0; rx_data_size < data_size; ++rx_data_size) { + if(!rp2040_spi_txrx(0, &rx_data[rx_data_size])) break; + } + if(rx_data_size < data_size) break; + rp2040_spi_chip_select(RP_GPIO_QSPI_CS_DRIVE_HIGH); + success = true; + } while(false); + + return success; +} + +static bool + rp2040_spi_write(uint16_t command, uint32_t address, const void* data, const size_t data_size) { + bool success = false; + + do { + if(!rp2040_spi_setup_txrx(command, address, data_size)) break; + const uint8_t* tx_data = data; + size_t tx_data_size; + for(tx_data_size = 0; tx_data_size < data_size; ++tx_data_size) { + if(!rp2040_spi_txrx(tx_data[tx_data_size], NULL)) break; + } + if(tx_data_size < data_size) break; + if(!rp2040_spi_chip_select(RP_GPIO_QSPI_CS_DRIVE_HIGH)) break; + success = true; + } while(false); + + return success; +} + +static bool rp2040_spi_run_command(uint16_t command, uint32_t address) { + return rp2040_spi_write(command, address, NULL, 0); +} + +// Custom procedure to reset the W25X SPI flash +static bool rp2040_w25xx_flash_reset(void) { + bool success = false; + do { + if(!rp2040_spi_txrx(W25X_CMD_RESET_ENABLE, NULL)) break; + if(!rp2040_spi_txrx(W25X_CMD_RESET, NULL)) break; + furi_delay_us(50); + success = true; + } while(false); + + return success; +} + +bool rp2040_init(void) { + bool success = false; + + do { + if(!rp2040_spi_gpio_init()) { + FURI_LOG_E(TAG, "Failed to initialize SPI pins"); + break; + } + if(!rp2040_spi_init()) { + FURI_LOG_E(TAG, "Failed to configure SPI hardware"); + break; + } + if(!rp2040_w25xx_flash_reset()) { + FURI_LOG_E(TAG, "Failed to reset SPI flash"); + break; + } + success = true; + } while(false); + + return success; +} + +bool rp2040_flash_read_data(uint32_t address, void* data, size_t data_size) { + bool success = false; + + do { + if(!rp2040_spi_read(SPI_FLASH_CMD_READ_DATA, address, data, data_size)) { + FURI_LOG_E(TAG, "Failed to read data"); + break; + } + success = true; + } while(false); + + return success; +} + +bool rp2040_flash_erase_sector(uint32_t address) { + bool success = false; + + do { + if(!rp2040_spi_run_command(SPI_FLASH_CMD_WRITE_ENABLE, 0)) { + FURI_LOG_E(TAG, "Failed to issue WRITE_ENABLE command"); + break; + } + uint8_t status; + if(!rp2040_spi_read(SPI_FLASH_CMD_READ_STATUS, 0U, &status, sizeof(status))) { + FURI_LOG_E(TAG, "Failed to issue READ_STATUS command"); + break; + } + if((status & SPI_FLASH_STATUS_WRITE_ENABLED) == 0) { + FURI_LOG_E(TAG, "Failed to enable write mode, status byte: 0x%02X", status); + break; + } + if(!rp2040_spi_run_command(SPI_FLASH_CMD_SECTOR_ERASE, address)) { + FURI_LOG_E(TAG, "Failed to issue SECTOR_ERASE command"); + break; + } + do { + if(!rp2040_spi_read(SPI_FLASH_CMD_READ_STATUS, 0U, &status, sizeof(status))) { + FURI_LOG_E(TAG, "Failed to issue READ_STATUS command"); + break; + } + } while(status & SPI_FLASH_STATUS_BUSY); + + if(status & SPI_FLASH_STATUS_BUSY) break; + + success = true; + } while(false); + + return success; +} + +bool rp2040_flash_program_page(uint32_t address, const void* data, size_t data_size) { + bool success = false; + + do { + if(!rp2040_spi_run_command(SPI_FLASH_CMD_WRITE_ENABLE, 0)) { + FURI_LOG_E(TAG, "Failed to issue WRITE_ENABLE command"); + break; + } + uint8_t status; + if(!rp2040_spi_read(SPI_FLASH_CMD_READ_STATUS, 0U, &status, sizeof(status))) { + FURI_LOG_E(TAG, "Failed to issue READ_STATUS command"); + break; + } + if((status & SPI_FLASH_STATUS_WRITE_ENABLED) == 0) { + FURI_LOG_E(TAG, "Failed to enable write mode, status byte: 0x%02X", status); + break; + } + if(!rp2040_spi_write(SPI_FLASH_CMD_PAGE_PROGRAM, address, data, data_size)) { + FURI_LOG_E(TAG, "Failed to issue PAGE_PROGRAM command"); + break; + } + do { + if(!rp2040_spi_read(SPI_FLASH_CMD_READ_STATUS, 0U, &status, sizeof(status))) { + FURI_LOG_E(TAG, "Failed to issue READ_STATUS command"); + break; + } + } while(status & SPI_FLASH_STATUS_BUSY); + + if(status & SPI_FLASH_STATUS_BUSY) break; + + success = true; + } while(false); + + return success; +} diff --git a/video_game_module_tool/flasher/rp2040.h b/video_game_module_tool/flasher/rp2040.h new file mode 100644 index 00000000..83482e71 --- /dev/null +++ b/video_game_module_tool/flasher/rp2040.h @@ -0,0 +1,53 @@ +/** + * @file rp2040.h + * @brief RP2040-specific functions. + * + * This file is responsible for initialising and accessing + * the SPI flash chip via RP2040 hardware. + */ +#pragma once + +#include +#include +#include + +#define RP2040_CORE0_ADDR (0x01002927UL) +#define RP2040_CORE1_ADDR (0x11002927UL) +#define RP2040_RESCUE_ADDR (0xF1002927UL) + +#define RP2040_FAMILY_ID (0xE48BFF56UL) + +/** + * @brief Initialise RP2040-specific hardware. + * + * @returns true on success, false otherwise. + */ +bool rp2040_init(void); + +/** + * @brief Read data from the SPI flash chip. + * + * @param[in] address target address within the flash address space. + * @param[out] data pointer to the buffer to contain the data to be read. + * @param[in] data_size size of the data to be read. + * @returns true on success, false otherwise. + */ +bool rp2040_flash_read_data(uint32_t address, void* data, size_t data_size); + +/** + * @brief Erase one sector (4K) of the SPI flash chip. + * + * @param[in] address target address within the flash address space (must be sector-aligned). + * @returns true on success, false otherwise. + */ +bool rp2040_flash_erase_sector(uint32_t address); + +/** + * @brief Program one page (256B) of the SPI flash chip. + * + * @param[in] address target address within the flash address space. + * @param[in] data pointer to the buffer containing the data to be written. + * @param[in] data_size size of the data to be written. + * @returns true on success, false otherwise. + */ +bool rp2040_flash_program_page(uint32_t address, const void* data, size_t data_size); diff --git a/video_game_module_tool/flasher/swd.c b/video_game_module_tool/flasher/swd.c new file mode 100644 index 00000000..244ee886 --- /dev/null +++ b/video_game_module_tool/flasher/swd.c @@ -0,0 +1,281 @@ +#include "swd.h" + +#include +#include + +#define TAG "VgmSwd" + +#define SWD_REQUEST_LEN (8U) +#define SWD_RESPONSE_LEN (3U) +#define SWD_DATA_LEN (32U) + +#define SWD_ALERT_SEQUENCE_0 (0x6209F392UL) +#define SWD_ALERT_SEQUENCE_1 (0x86852D95UL) +#define SWD_ALERT_SEQUENCE_2 (0xE3DDAFE9UL) +#define SWD_ALERT_SEQUENCE_3 (0x19BC0EA2UL) + +#define SWD_ACTIVATION_CODE (0x1AU) + +#define SWD_SLEEP_SEQUENCE (0xE3BCU) + +#define SWD_READ_REQUEST_INIT (0x85U) +#define SWD_WRITE_REQUEST_INIT (0x81U) +#define SWD_REQUEST_INIT (0x81U) + +typedef enum { + SwdioDirectionIn, + SwdioDirectionOut, +} SwdioDirection; + +typedef enum { + SwdResponseOk = 1U, + SwdResponseWait = 2U, + SwdResponseFault = 4U, + SwdResponseNone = 7U, +} SwdResponse; + +typedef enum { + SwdAccessTypeDp = 0U << 1, + SwdAccessTypeAp = 1U << 1, +} SwdAccessType; + +typedef enum { + SwdAccessDirectionWrite = 0U << 2, + SwdAccessDirectionRead = 1U << 2, +} SwdAccessDirection; + +#ifdef SWD_ENABLE_CYCLE_DELAY +// Slows SWCLK down, useful for debugging via logic analyzer +__attribute__((always_inline)) static inline void swd_delay_half_cycle(void) { + asm volatile("nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n" + "nop \n"); +} +#else +#define swd_delay_half_cycle() +#endif + +static void __attribute__((optimize("-O3"))) swd_turnaround(SwdioDirection mode) { + static SwdioDirection prev_dir = SwdioDirectionIn; + + if(prev_dir == mode) { + return; + } else { + prev_dir = mode; + } + + if(mode == SwdioDirectionIn) { + // Using LL functions for performance reasons + LL_GPIO_SetPinMode(gpio_swdio.port, gpio_swdio.pin, LL_GPIO_MODE_INPUT); + } else { + furi_hal_gpio_write(&gpio_swclk, false); + } + swd_delay_half_cycle(); + + furi_hal_gpio_write(&gpio_swclk, true); + swd_delay_half_cycle(); + + if(mode == SwdioDirectionOut) { + furi_hal_gpio_write(&gpio_swclk, false); + // Using LL functions for performance reasons + LL_GPIO_SetPinMode(gpio_swdio.port, gpio_swdio.pin, LL_GPIO_MODE_OUTPUT); + } +} + +static void __attribute__((optimize("-O3"))) swd_tx(uint32_t data, uint32_t n_cycles) { + swd_turnaround(SwdioDirectionOut); + + for(uint32_t i = 0; i < n_cycles; ++i) { + furi_hal_gpio_write(&gpio_swclk, false); + furi_hal_gpio_write(&gpio_swdio, data & (1UL << i)); + swd_delay_half_cycle(); + + furi_hal_gpio_write(&gpio_swclk, true); + swd_delay_half_cycle(); + } + + furi_hal_gpio_write(&gpio_swclk, false); +} + +static void __attribute__((optimize("-O3"))) swd_tx_parity(uint32_t data, uint32_t n_cycles) { + const int parity = __builtin_parity(data); + swd_tx(data, n_cycles); + furi_hal_gpio_write(&gpio_swdio, parity); + swd_delay_half_cycle(); + furi_hal_gpio_write(&gpio_swclk, true); + swd_delay_half_cycle(); + furi_hal_gpio_write(&gpio_swclk, false); +} + +static uint32_t __attribute__((optimize("-O3"))) swd_rx(uint32_t n_cycles) { + uint32_t ret = 0; + swd_turnaround(SwdioDirectionIn); + + for(uint32_t i = 0; i < n_cycles; ++i) { + furi_hal_gpio_write(&gpio_swclk, false); + ret |= furi_hal_gpio_read(&gpio_swdio) ? (1UL << i) : 0; + swd_delay_half_cycle(); + + furi_hal_gpio_write(&gpio_swclk, true); + swd_delay_half_cycle(); + } + + furi_hal_gpio_write(&gpio_swclk, false); + return ret; +} + +static bool __attribute__((optimize("-O3"))) swd_rx_parity(uint32_t* data, uint32_t n_cycles) { + furi_assert(data); + + const uint32_t rx_value = swd_rx(n_cycles); + swd_delay_half_cycle(); + + const bool parity_calc = __builtin_parity(rx_value); + const bool parity_rx = furi_hal_gpio_read(&gpio_swdio); + + furi_hal_gpio_write(&gpio_swclk, true); + swd_delay_half_cycle(); + furi_hal_gpio_write(&gpio_swclk, false); + + if(data) { + *data = rx_value; + } + + return parity_calc == parity_rx; +} + +static void swd_line_reset(bool idle_cycles) { + swd_tx(0xFFFFFFFFUL, 32U); + swd_tx(0x0FFFFFFFUL, idle_cycles ? 32U : 24U); +} + +static void swd_leave_dormant_state(void) { + swd_line_reset(false); + swd_tx(SWD_ALERT_SEQUENCE_0, 32U); + swd_tx(SWD_ALERT_SEQUENCE_1, 32U); + swd_tx(SWD_ALERT_SEQUENCE_2, 32U); + swd_tx(SWD_ALERT_SEQUENCE_3, 32U); + swd_tx(SWD_ACTIVATION_CODE << 4U, 12U); +} + +static void swd_enter_dormant_state(void) { + swd_line_reset(false); + swd_tx(SWD_SLEEP_SEQUENCE, 16U); +} + +void swd_init(void) { + furi_hal_gpio_init_ex( + &gpio_swclk, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh, GpioAltFnUnused); + furi_hal_gpio_init_ex( + &gpio_swdio, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh, GpioAltFnUnused); + + swd_leave_dormant_state(); + swd_line_reset(true); +} + +void swd_deinit(void) { + swd_enter_dormant_state(); + + furi_hal_gpio_init_simple(&gpio_swclk, GpioModeAnalog); + furi_hal_gpio_init_simple(&gpio_swdio, GpioModeAnalog); +} + +static inline uint8_t swd_prepare_request( + SwdAccessDirection access_direction, + SwdAccessType access_type, + uint8_t address) { + uint8_t ret = SWD_REQUEST_INIT | access_type | access_direction | (address << 3); + ret |= __builtin_parity(ret) << 5; + return ret; +} + +static bool swd_read_request(SwdAccessType access_type, uint8_t address, uint32_t* data) { + const uint8_t request = swd_prepare_request(SwdAccessDirectionRead, access_type, address); + swd_tx(request, SWD_REQUEST_LEN); + + const uint32_t response = swd_rx(SWD_RESPONSE_LEN); + if(response == SwdResponseOk) { + return swd_rx_parity(data, SWD_DATA_LEN); + } else { + return false; + } +} + +static bool swd_write_request(SwdAccessType access_type, uint8_t address, uint32_t data) { + const uint8_t request = swd_prepare_request(SwdAccessDirectionWrite, access_type, address); + swd_tx(request, SWD_REQUEST_LEN); + + const uint32_t response = swd_rx(SWD_RESPONSE_LEN); + if(response == SwdResponseOk) { + swd_tx_parity(data, SWD_DATA_LEN); + swd_tx(0UL, 8); + return true; + } else { + return false; + } +} + +void swd_select_target(uint32_t target_id) { + swd_tx(SWD_WRITE_REQUEST_INIT | (SWD_DP_REG_WO_TASRGETSEL << 3), SWD_REQUEST_LEN); + swd_rx(SWD_RESPONSE_LEN); + swd_tx_parity(target_id, SWD_DATA_LEN); + swd_tx(0UL, 8); +} + +bool swd_dp_read(uint8_t address, uint32_t* data) { + return swd_read_request(SwdAccessTypeDp, address, data); +} + +bool swd_dp_write(uint8_t address, uint32_t data) { + return swd_write_request(SwdAccessTypeDp, address, data); +} + +bool swd_ap_read(uint8_t address, uint32_t* data) { + bool success = false; + + do { + // Using hardcoded AP 0 + const uint32_t select_val = address & 0xF0U; + if(!swd_write_request(SwdAccessTypeDp, SWD_DP_REG_WO_SELECT, select_val)) break; + if(!swd_read_request(SwdAccessTypeAp, (address & 0x0FU) >> 2, NULL)) break; + if(!swd_read_request(SwdAccessTypeDp, SWD_DP_REG_RO_RDBUFF, data)) break; + success = true; + } while(false); + + return success; +} + +bool swd_ap_write(uint8_t address, uint32_t data) { + bool success = false; + + do { + // Using hardcoded AP 0 + const uint32_t select_val = address & 0xF0U; + if(!swd_write_request(SwdAccessTypeDp, SWD_DP_REG_WO_SELECT, select_val)) break; + if(!swd_write_request(SwdAccessTypeAp, (address & 0x0FU) >> 2, data)) break; + success = true; + } while(false); + + return success; +} diff --git a/video_game_module_tool/flasher/swd.h b/video_game_module_tool/flasher/swd.h new file mode 100644 index 00000000..93d151d0 --- /dev/null +++ b/video_game_module_tool/flasher/swd.h @@ -0,0 +1,122 @@ +/** + * @file swd.h + * @brief Serial Wire Debug (SWD) bus functions. + * + * This file is responsible for: + * + * - Debug hardware initialisation + * - Target selection in a multidrop bus + * - Debug and Access port access + * + * For more information, see ARM IHI0031G + * https://documentation-service.arm.com/static/622222b2e6f58973271ebc21 + */ +#pragma once + +#include +#include +#include + +// Only bits [3:2] are used to access DP registers +#define SWD_DP_REG_ADDR_SHIFT (2U) + +// Debug port registers - write +#define SWD_DP_REG_WO_ABORT (0x0U >> SWD_DP_REG_ADDR_SHIFT) +#define SWD_DP_REG_WO_SELECT (0x8U >> SWD_DP_REG_ADDR_SHIFT) +#define SWD_DP_REG_WO_TASRGETSEL (0xCU >> SWD_DP_REG_ADDR_SHIFT) + +// Debug port registers - read +#define SWD_DP_REG_RO_DPIDR (0x0U >> SWD_DP_REG_ADDR_SHIFT) +#define SWD_DP_REG_RO_RESEND (0x8U >> SWD_DP_REG_ADDR_SHIFT) +#define SWD_DP_REG_RO_RDBUFF (0xCU >> SWD_DP_REG_ADDR_SHIFT) + +// Debug port registers - read/write +#define SWD_DP_REG_RW_BANK (0x4U >> SWD_DP_REG_ADDR_SHIFT) +#define SWD_DP_REG_RW_CTRL_STAT (SWD_DP_REG_RW_BANK) + +// Access port registers +#define SWD_AP_REG_RW_CSW (0x00U) +#define SWD_AP_REG_RW_TAR (0x04U) +#define SWD_AP_REG_RW_DRW (0x0CU) +#define SWD_AP_REG_RO_IDR (0xFCU) + +// CTRL/STAT bits +#define SWD_DP_REG_CTRL_STAT_CDBGPWRUPREQ (1UL << 28U) +#define SWD_DP_REG_CTRL_STAT_CDBGPWRUPACK (1UL << 29U) +#define SWD_DP_REG_CTRL_STAT_CSYSPWRUPREQ (1UL << 30U) +#define SWD_DP_REG_CTRL_STAT_CSYSPWRUPACK (1UL << 31U) + +// CSW bits (PROT bits are for AHB3) +#define SWD_AP_REG_CSW_SIZE_WORD (2UL << 0U) +#define SWD_AP_REG_CSW_HPROT_DATA (1UL << 24U) +#define SWD_AP_REG_CSW_HPROT_PRIVILIGED (1UL << 25U) +#define SWD_AP_REG_CSW_HPROT_BUFFERABLE (1UL << 26U) +#define SWD_AP_REG_CSW_HPROT_CACHEABLE (1UL << 27U) +#define SWD_AP_REG_CSW_HNONSEC (1UL << 30U) + +/** + * @brief Initialise SWD bus. + * + * Configures SWCLK and SWDIO pins, wakes up the target from + * dormant state and resets the SWD bus. + */ +void swd_init(void); + +/** + * @brief Disable SWD bus. + * + * Sets the target to dormant state and returns + * SWCLK and SWDIO pins to analog mode. + */ +void swd_deinit(void); + +/** + * @brief Select one target on a multidrop (SWD v2) bus. + * + * @param[in] target_id target address or id (specified in device datasheet) + */ +void swd_select_target(uint32_t target_id); + +/** + * @brief Perform a Debug Port (DP) read. + * + * Reads a 32-bit word from the designated DP register. + * + * @param[in] address DP register address. + * @param[out] data pointer to the value to contain the read data. + * @returns true on success, false otherwise. + */ +bool swd_dp_read(uint8_t address, uint32_t* data); + +/** + * @brief Perform a Debug Port (DP) write. + * + * Writes a 32-bit word to the designated DP register. + * + * @param[in] address DP register address. + * @param[in] data value to be written as data. + * @returns true on success, false otherwise. + */ +bool swd_dp_write(uint8_t address, uint32_t data); + +/** + * @brief Perform an Access Port (AP) read. + * + * Reads a 32-bit word from the designated AP register. + * + * @param[in] address AP register address. + * @param[out] data pointer to the value to contain the read data. + * @returns true on success, false otherwise. + */ +bool swd_ap_read(uint8_t address, uint32_t* data); + +/** + * @brief Perform an Access Port (AP) write. + * + * Writes a 32-bit word to the designated AP register. + * + * @param[in] address AP register address. + * @param[in] data value to be written as data. + * @returns true on success, false otherwise. + */ +bool swd_ap_write(uint8_t address, uint32_t data); diff --git a/video_game_module_tool/flasher/target.c b/video_game_module_tool/flasher/target.c new file mode 100644 index 00000000..927ae391 --- /dev/null +++ b/video_game_module_tool/flasher/target.c @@ -0,0 +1,231 @@ +#include "target.h" + +#include + +#include "swd.h" + +/* Cortex-M registers (taken from Blackmagic) */ +#define CORTEXM_PPB_BASE 0xe0000000U + +#define CORTEXM_SCS_BASE (CORTEXM_PPB_BASE + 0xe000U) + +#define CORTEXM_CPUID (CORTEXM_SCS_BASE + 0xd00U) +#define CORTEXM_AIRCR (CORTEXM_SCS_BASE + 0xd0cU) +#define CORTEXM_CFSR (CORTEXM_SCS_BASE + 0xd28U) +#define CORTEXM_HFSR (CORTEXM_SCS_BASE + 0xd2cU) +#define CORTEXM_DFSR (CORTEXM_SCS_BASE + 0xd30U) +#define CORTEXM_CPACR (CORTEXM_SCS_BASE + 0xd88U) +#define CORTEXM_DHCSR (CORTEXM_SCS_BASE + 0xdf0U) +#define CORTEXM_DCRSR (CORTEXM_SCS_BASE + 0xdf4U) +#define CORTEXM_DCRDR (CORTEXM_SCS_BASE + 0xdf8U) +#define CORTEXM_DEMCR (CORTEXM_SCS_BASE + 0xdfcU) + +/* Debug Halting Control and Status Register (DHCSR) */ +/* This key must be written to bits 31:16 for write to take effect */ +#define CORTEXM_DHCSR_DBGKEY 0xa05f0000U +/* Bits 31:26 - Reserved */ +#define CORTEXM_DHCSR_S_RESET_ST (1U << 25U) +#define CORTEXM_DHCSR_S_RETIRE_ST (1U << 24U) +/* Bits 23:20 - Reserved */ +#define CORTEXM_DHCSR_S_LOCKUP (1U << 19U) +#define CORTEXM_DHCSR_S_SLEEP (1U << 18U) +#define CORTEXM_DHCSR_S_HALT (1U << 17U) +#define CORTEXM_DHCSR_S_REGRDY (1U << 16U) +/* Bits 15:6 - Reserved */ +#define CORTEXM_DHCSR_C_SNAPSTALL (1U << 5U) /* v7m only */ +/* Bit 4 - Reserved */ +#define CORTEXM_DHCSR_C_MASKINTS (1U << 3U) +#define CORTEXM_DHCSR_C_STEP (1U << 2U) +#define CORTEXM_DHCSR_C_HALT (1U << 1U) +#define CORTEXM_DHCSR_C_DEBUGEN (1U << 0U) + +/* Debug Exception and Monitor Control Register (DEMCR) */ +/* Bits 31:25 - Reserved */ +#define CORTEXM_DEMCR_TRCENA (1U << 24U) +/* Bits 23:20 - Reserved */ +#define CORTEXM_DEMCR_MON_REQ (1U << 19U) /* v7m only */ +#define CORTEXM_DEMCR_MON_STEP (1U << 18U) /* v7m only */ +#define CORTEXM_DEMCR_VC_MON_PEND (1U << 17U) /* v7m only */ +#define CORTEXM_DEMCR_VC_MON_EN (1U << 16U) /* v7m only */ +/* Bits 15:11 - Reserved */ +#define CORTEXM_DEMCR_VC_HARDERR (1U << 10U) +#define CORTEXM_DEMCR_VC_INTERR (1U << 9U) /* v7m only */ +#define CORTEXM_DEMCR_VC_BUSERR (1U << 8U) /* v7m only */ +#define CORTEXM_DEMCR_VC_STATERR (1U << 7U) /* v7m only */ +#define CORTEXM_DEMCR_VC_CHKERR (1U << 6U) /* v7m only */ +#define CORTEXM_DEMCR_VC_NOCPERR (1U << 5U) /* v7m only */ +#define CORTEXM_DEMCR_VC_MMERR (1U << 4U) /* v7m only */ +/* Bits 3:1 - Reserved */ +#define CORTEXM_DEMCR_VC_CORERESET (1U << 0U) + +#define CORTEXM_DHCSR_DEBUG_HALT (CORTEXM_DHCSR_C_DEBUGEN | CORTEXM_DHCSR_C_HALT) + +#define TAG "VgmTarget" + +static uint32_t prev_address; + +static bool target_memory_access_setup(uint32_t address) { + bool success = false; + do { + // If the address was previously set up, do not waste time on it + if(address != prev_address) { + // Word access, no auto increment + if(!swd_ap_write( + SWD_AP_REG_RW_CSW, + SWD_AP_REG_CSW_HPROT_DATA | SWD_AP_REG_CSW_HPROT_PRIVILIGED | + SWD_AP_REG_CSW_HNONSEC | SWD_AP_REG_CSW_SIZE_WORD)) + break; + if(!swd_ap_write(SWD_AP_REG_RW_TAR, address)) break; + prev_address = address; + } + success = true; + } while(false); + + return success; +} + +static bool target_dbg_power_up(void) { + if(!swd_dp_write(SWD_DP_REG_RW_CTRL_STAT, 0)) return false; + + uint32_t status; + + do { + if(!swd_dp_read(SWD_DP_REG_RW_CTRL_STAT, &status)) return false; + } while(status & (SWD_DP_REG_CTRL_STAT_CDBGPWRUPACK | SWD_DP_REG_CTRL_STAT_CSYSPWRUPACK)); + + if(!swd_dp_write( + SWD_DP_REG_RW_CTRL_STAT, + (SWD_DP_REG_CTRL_STAT_CDBGPWRUPREQ | SWD_DP_REG_CTRL_STAT_CSYSPWRUPREQ))) + return false; + + do { + furi_delay_us(10000); + if(!swd_dp_read(SWD_DP_REG_RW_CTRL_STAT, &status)) return false; + } while((status & (SWD_DP_REG_CTRL_STAT_CDBGPWRUPACK | SWD_DP_REG_CTRL_STAT_CSYSPWRUPACK)) != + (SWD_DP_REG_CTRL_STAT_CDBGPWRUPACK | SWD_DP_REG_CTRL_STAT_CSYSPWRUPACK)); + + return true; +} + +static bool target_halt(void) { + bool success = false; + + do { + if(!target_write_memory_32(CORTEXM_DHCSR, CORTEXM_DHCSR_DBGKEY | CORTEXM_DHCSR_DEBUG_HALT)) + break; + + bool target_halted = false; + for(bool target_reset = false; !target_halted;) { + uint32_t dhcsr; + if(!target_read_memory_32(CORTEXM_DHCSR, &dhcsr)) break; + if((dhcsr & CORTEXM_DHCSR_S_RESET_ST) && !target_reset) { + target_reset = true; + continue; + } + if((dhcsr & CORTEXM_DHCSR_DEBUG_HALT) == CORTEXM_DHCSR_DEBUG_HALT) { + target_halted = true; + } + } + + if(!target_halted) break; + + if(!target_write_memory_32( + CORTEXM_DEMCR, + CORTEXM_DEMCR_TRCENA | CORTEXM_DEMCR_VC_HARDERR | CORTEXM_DEMCR_VC_CORERESET)) + break; + + bool target_local_reset = false; + for(; !target_local_reset;) { + uint32_t dhcsr; + if(!target_read_memory_32(CORTEXM_DHCSR, &dhcsr)) break; + if((dhcsr & CORTEXM_DHCSR_S_RESET_ST) == 0) { + target_local_reset = true; + } + } + + if(!target_local_reset) break; + + success = true; + } while(false); + + return success; +} + +bool target_attach(uint32_t id) { + bool success = false; + + do { + // Reset previous memory address + prev_address = UINT32_MAX; + + swd_select_target(id); + + uint32_t dpidr; + if(!swd_dp_read(SWD_DP_REG_RO_DPIDR, &dpidr)) { + FURI_LOG_E(TAG, "Failed to read DPIDR"); + break; + } + + if(dpidr == 0) { + FURI_LOG_E(TAG, "Zero DPIDR value"); + break; + } + + if(!target_dbg_power_up()) { + FURI_LOG_E(TAG, "Failed to enable debug power"); + break; + } + + if(!target_halt()) { + FURI_LOG_E(TAG, "Failed to halt target"); + break; + } + + success = true; + } while(false); + + return success; +} + +bool target_detach(void) { + bool success = false; + + do { + if(!target_write_memory_32(CORTEXM_DHCSR, CORTEXM_DHCSR_DBGKEY | CORTEXM_DHCSR_DEBUG_HALT)) + break; + if(!target_write_memory_32(CORTEXM_DHCSR, CORTEXM_DHCSR_DBGKEY | CORTEXM_DHCSR_C_DEBUGEN)) + break; + if(!target_write_memory_32(CORTEXM_DHCSR, CORTEXM_DHCSR_DBGKEY)) break; + success = true; + } while(false); + + return success; +} + +bool target_read_memory_32(uint32_t address, uint32_t* data) { + furi_assert((address & 3U) == 0); + + bool success = false; + + do { + if(!target_memory_access_setup(address)) break; + if(!swd_ap_read(SWD_AP_REG_RW_DRW, data)) break; + success = true; + } while(false); + + return success; +} + +bool target_write_memory_32(uint32_t address, uint32_t data) { + furi_assert((address & 3U) == 0); + + bool success = false; + + do { + if(!target_memory_access_setup(address)) break; + if(!swd_ap_write(SWD_AP_REG_RW_DRW, data)) break; + success = true; + } while(false); + + return success; +} diff --git a/video_game_module_tool/flasher/target.h b/video_game_module_tool/flasher/target.h new file mode 100644 index 00000000..1a8d351c --- /dev/null +++ b/video_game_module_tool/flasher/target.h @@ -0,0 +1,45 @@ +/** + * @file target.h + * @brief Debug target functions. + * + * This file is responsible for configuring the debug target + * and accessing its memory-mapped devices. + */ +#pragma once + +#include +#include +#include + +/** + * @brief Attach and halt the debug target. + * + * @param[in] target_id target address or id (specified in device datasheet) + * @returns true on success, false otherwise. + */ +bool target_attach(uint32_t id); + +/** + * @brief Detach and resume the debug target. + * + * @returns true on success, false otherwise. + */ +bool target_detach(void); + +/** + * @brief Read a 32-bit word within target address space. + * + * @param[in] address target memory address. + * @param[out] data pointer to the value to hold the read data. + * @returns true on success, false otherwise. + */ +bool target_read_memory_32(uint32_t address, uint32_t* data); + +/** + * @brief Write a 32-bit word within target address space. + * + * @param[in] address target memory address. + * @param[in] data value to be written as data. + * @returns true on success, false otherwise. + */ +bool target_write_memory_32(uint32_t address, uint32_t data); diff --git a/video_game_module_tool/flasher/uf2.c b/video_game_module_tool/flasher/uf2.c new file mode 100644 index 00000000..deacf182 --- /dev/null +++ b/video_game_module_tool/flasher/uf2.c @@ -0,0 +1,167 @@ +#include "uf2.h" + +#include + +#define UF2_BLOCK_SIZE (512UL) +#define UF2_DATA_SIZE (476UL) +#define UF2_CHECKSUM_SIZE (16UL) + +#define UF2_MAGIC_START_0 (0x0A324655UL) +#define UF2_MAGIC_START_1 (0x9E5D5157UL) +#define UF2_MAGIC_END (0x0AB16F30UL) + +#define TAG "VgmUf2" + +typedef enum { + Uf2FlagNotMainFlash = 1UL << 0, + Uf2FlagFileContainer = 1UL << 12, + Uf2FlagFamilyIdPresent = 1UL << 13, + Uf2FlagChecksumPresent = 1UL << 14, + Uf2FlagExtensionPresent = 1UL << 15, +} Uf2Flag; + +typedef struct { + uint32_t magic_start[2]; + uint32_t flags; + uint32_t target_addr; + uint32_t payload_size; + uint32_t block_no; + uint32_t num_blocks; + union { + uint32_t file_size; + uint32_t family_id; + }; +} Uf2BlockHeader; + +typedef union { + uint8_t payload[UF2_DATA_SIZE]; + struct { + uint8_t reserved[UF2_DATA_SIZE - 24]; + uint32_t start_addr; + uint32_t region_len; + uint8_t checksum[UF2_CHECKSUM_SIZE]; + }; +} Uf2BlockData; + +typedef struct { + uint32_t magic_end; +} Uf2BlockTrailer; + +static bool uf2_block_header_read(Uf2BlockHeader* header, File* file) { + const size_t size_read = storage_file_read(file, header, sizeof(Uf2BlockHeader)); + return size_read == sizeof(Uf2BlockHeader); +} + +static bool + uf2_block_header_verify(const Uf2BlockHeader* header, uint32_t family_id, size_t payload_size) { + bool success = false; + + do { + if(header->magic_start[0] != UF2_MAGIC_START_0) break; + if(header->magic_start[1] != UF2_MAGIC_START_1) break; + if(header->flags & Uf2FlagNotMainFlash) { + FURI_LOG_E(TAG, "Non-flash blocks are not supported (block #%lu)", header->block_no); + break; + } + if(header->flags & Uf2FlagFamilyIdPresent) { + if(header->family_id != family_id) { + FURI_LOG_E( + TAG, + "Family ID expected: %lX, got: %lX (block #%lu)", + family_id, + header->family_id, + header->block_no); + break; + } + } + if(header->payload_size != payload_size) { + FURI_LOG_E( + TAG, + "Only %zu-byte block payloads are supported (block #%lu)", + payload_size, + header->block_no); + break; + } + if(header->target_addr % payload_size != 0) { + FURI_LOG_E( + TAG, + "Only %zu-byte aligned are allowed (block #%lu)", + payload_size, + header->block_no); + break; + } + success = true; + } while(false); + + return success; +} + +static bool uf2_block_header_skip(File* file) { + return storage_file_seek(file, sizeof(Uf2BlockHeader), false); +} + +static bool uf2_block_payload_skip(File* file) { + return storage_file_seek(file, sizeof(Uf2BlockData), false); +} + +static bool uf2_block_trailer_skip(File* file) { + return storage_file_seek(file, sizeof(Uf2BlockTrailer), false); +} + +static bool uf2_block_payload_read(File* file, void* payload_data, size_t payload_size) { + bool success = false; + + do { + const size_t size_read = storage_file_read(file, payload_data, payload_size); + if(size_read != payload_size) break; + if(!storage_file_seek(file, UF2_DATA_SIZE - payload_size, false)) break; + success = true; + } while(false); + + return success; +} + +static bool uf2_block_trailer_read(Uf2BlockTrailer* trailer, File* file) { + const size_t size_read = storage_file_read(file, trailer, sizeof(Uf2BlockTrailer)); + return size_read == sizeof(Uf2BlockTrailer); +} + +static bool uf2_block_trailer_verify(const Uf2BlockTrailer* trailer) { + return trailer->magic_end == UF2_MAGIC_END; +} + +bool uf2_get_block_count(File* file, uint32_t* block_count) { + const size_t file_size = storage_file_size(file); + + if(file_size == 0) { + FURI_LOG_E(TAG, "File size is zero"); + return false; + } else if(file_size % UF2_BLOCK_SIZE != 0) { + FURI_LOG_E(TAG, "File size is not a multiple of %lu bytes", UF2_BLOCK_SIZE); + return false; + } + + *block_count = file_size / UF2_BLOCK_SIZE; + return true; +} + +bool uf2_verify_block(File* file, uint32_t family_id, size_t payload_size) { + Uf2BlockHeader header; + Uf2BlockTrailer trailer; + + if(!uf2_block_header_read(&header, file)) return false; + if(!uf2_block_header_verify(&header, family_id, payload_size)) return false; + if(!uf2_block_payload_skip(file)) return false; + if(!uf2_block_trailer_read(&trailer, file)) return false; + if(!uf2_block_trailer_verify(&trailer)) return false; + + return true; +} + +bool uf2_read_block(File* file, void* payload_data, size_t payload_size) { + if(!uf2_block_header_skip(file)) return false; + if(!uf2_block_payload_read(file, payload_data, payload_size)) return false; + if(!uf2_block_trailer_skip(file)) return false; + + return true; +} diff --git a/video_game_module_tool/flasher/uf2.h b/video_game_module_tool/flasher/uf2.h new file mode 100644 index 00000000..d61b9b70 --- /dev/null +++ b/video_game_module_tool/flasher/uf2.h @@ -0,0 +1,60 @@ +/** + * @file uf2.h + * @brief UF2 file support functions. + * + * This is a minimal UF2 file implementation. + * + * UNsupported features: + * - Non-flash blocks + * - File containers + * - Extended tags + * - Md5 checksum + * + * Suported features: + * - Family id (respective flag must be set) + * + * See https://github.com/Microsoft/uf2 for more information. + */ +#pragma once + +#include + +/** + * @brief Get the block count in a UF2 file. + * + * The file MUST be already open. + * + * Will fail if the file size is not evenly divisible + * by 512 bytes (UF2 block size). + * + * @param[in] file pointer to the storage file instance. + * @param[out] block_count pointer to the value to contain the block count. + * @returns true on success, false otherwise. + */ +bool uf2_get_block_count(File* file, uint32_t* block_count); + +/** + * @brief Verify a single UF2 block. + * + * The file MUST be already open. + * + * Will fail if: + * - the family id flag is set, but does not match the provided value, + * - payload size does not match the provided value. + * + * @param[in] file pointer to the storage file instance. + * @param[in] family_id family identifier to check against the respective header field. + * @param[in] payload_size payload size to check agains the respective header field, in bytes. + * @returns true on success, false otherwise. + */ +bool uf2_verify_block(File* file, uint32_t family_id, size_t payload_size); + +/** + * @brief Read the payload from a single UF2 block. + * + * @param[in] file pointer to the storage file instance. + * @param[out] payload pointer to the buffer to contain the payload data. + * @param[in] payload_size size of the payload buffer, in bytes. + * @returns true on success, false otherwise. + */ +bool uf2_read_block(File* file, void* payload, size_t payload_size); diff --git a/video_game_module_tool/icons/Checkmark_44x40.png b/video_game_module_tool/icons/Checkmark_44x40.png new file mode 100644 index 00000000..92853e92 Binary files /dev/null and b/video_game_module_tool/icons/Checkmark_44x40.png differ diff --git a/video_game_module_tool/icons/Flashing_module_70x30.png b/video_game_module_tool/icons/Flashing_module_70x30.png new file mode 100644 index 00000000..223afcbe Binary files /dev/null and b/video_game_module_tool/icons/Flashing_module_70x30.png differ diff --git a/video_game_module_tool/icons/Module_60x26.png b/video_game_module_tool/icons/Module_60x26.png new file mode 100644 index 00000000..a965fc62 Binary files /dev/null and b/video_game_module_tool/icons/Module_60x26.png differ diff --git a/video_game_module_tool/icons/Update_module_56x52.png b/video_game_module_tool/icons/Update_module_56x52.png new file mode 100644 index 00000000..77496c46 Binary files /dev/null and b/video_game_module_tool/icons/Update_module_56x52.png differ diff --git a/video_game_module_tool/icons/WarningDolphinFlip_45x42.png b/video_game_module_tool/icons/WarningDolphinFlip_45x42.png new file mode 100644 index 00000000..2ba54afc Binary files /dev/null and b/video_game_module_tool/icons/WarningDolphinFlip_45x42.png differ diff --git a/video_game_module_tool/scenes/scene.c b/video_game_module_tool/scenes/scene.c new file mode 100644 index 00000000..ceca5bed --- /dev/null +++ b/video_game_module_tool/scenes/scene.c @@ -0,0 +1,30 @@ +#include "scene.h" + +// Generate scene on_enter handlers array +#define ADD_SCENE(name, id) scene_##name##_on_enter, +static void (*const on_enter_handlers[])(void*) = { +#include "scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_event handlers array +#define ADD_SCENE(name, id) scene_##name##_on_event, +static bool (*const on_event_handlers[])(void* context, SceneManagerEvent event) = { +#include "scene_config.h" +}; +#undef ADD_SCENE + +// Generate scene on_exit handlers array +#define ADD_SCENE(name, id) scene_##name##_on_exit, +static void (*const on_exit_handlers[])(void* context) = { +#include "scene_config.h" +}; +#undef ADD_SCENE + +// Initialize scene handlers configuration structure +const SceneManagerHandlers scene_handlers = { + .on_enter_handlers = on_enter_handlers, + .on_event_handlers = on_event_handlers, + .on_exit_handlers = on_exit_handlers, + .scene_num = SceneNum, +}; diff --git a/video_game_module_tool/scenes/scene.h b/video_game_module_tool/scenes/scene.h new file mode 100644 index 00000000..c5c4b455 --- /dev/null +++ b/video_game_module_tool/scenes/scene.h @@ -0,0 +1,28 @@ +#pragma once + +#include + +// Generate scene id and total number +#define ADD_SCENE(name, id) Scene##id, +typedef enum { +#include "scene_config.h" + SceneNum, +} Scene; +#undef ADD_SCENE + +extern const SceneManagerHandlers scene_handlers; + +// Generate scene on_enter handlers declaration +#define ADD_SCENE(name, id) void scene_##name##_on_enter(void*); +#include "scene_config.h" +#undef ADD_SCENE + +// Generate scene on_event handlers declaration +#define ADD_SCENE(name, id) bool scene_##name##_on_event(void* context, SceneManagerEvent event); +#include "scene_config.h" +#undef ADD_SCENE + +// Generate scene on_exit handlers declaration +#define ADD_SCENE(name, id) void scene_##name##_on_exit(void* context); +#include "scene_config.h" +#undef ADD_SCENE diff --git a/video_game_module_tool/scenes/scene_config.h b/video_game_module_tool/scenes/scene_config.h new file mode 100644 index 00000000..e03f47a4 --- /dev/null +++ b/video_game_module_tool/scenes/scene_config.h @@ -0,0 +1,7 @@ +ADD_SCENE(probe, Probe) +ADD_SCENE(start, Start) +ADD_SCENE(confirm, Confirm) +ADD_SCENE(install, Install) +ADD_SCENE(file_select, FileSelect) +ADD_SCENE(success, Success) +ADD_SCENE(error, Error) diff --git a/video_game_module_tool/scenes/scene_confirm.c b/video_game_module_tool/scenes/scene_confirm.c new file mode 100644 index 00000000..4eb44d1e --- /dev/null +++ b/video_game_module_tool/scenes/scene_confirm.c @@ -0,0 +1,66 @@ +#include "app_i.h" + +#include +#include + +#include "custom_event.h" + +static void + scene_confirm_button_callback(GuiButtonType button_type, InputType input_type, void* context) { + furi_assert(context); + App* app = context; + + if(input_type == InputTypeShort) { + if(button_type == GuiButtonTypeLeft) { + view_dispatcher_send_custom_event(app->view_dispatcher, CustomEventFileRejected); + } else if(button_type == GuiButtonTypeRight) { + view_dispatcher_send_custom_event(app->view_dispatcher, CustomEventFileConfirmed); + } + } +} + +void scene_confirm_on_enter(void* context) { + App* app = context; + + FuriString* file_name = furi_string_alloc(); + path_extract_filename(app->file_path, file_name, false); + + FuriString* label = furi_string_alloc_printf("Install %s?", furi_string_get_cstr(file_name)); + widget_add_string_element( + app->widget, 64, 0, AlignCenter, AlignTop, FontPrimary, furi_string_get_cstr(label)); + + furi_string_free(label); + furi_string_free(file_name); + + widget_add_button_element( + app->widget, GuiButtonTypeLeft, "Cancel", scene_confirm_button_callback, app); + widget_add_button_element( + app->widget, GuiButtonTypeRight, "Install", scene_confirm_button_callback, app); + + view_dispatcher_switch_to_view(app->view_dispatcher, ViewIdWidget); +} + +bool scene_confirm_on_event(void* context, SceneManagerEvent event) { + App* app = context; + + bool consumed = false; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == CustomEventFileConfirmed) { + scene_manager_next_scene(app->scene_manager, SceneInstall); + } else if(event.event == CustomEventFileRejected) { + furi_string_reset(app->file_path); + scene_manager_previous_scene(app->scene_manager); + } + consumed = true; + } else if(event.type == SceneManagerEventTypeBack) { + consumed = true; + } + + return consumed; +} + +void scene_confirm_on_exit(void* context) { + App* app = context; + widget_reset(app->widget); +} diff --git a/video_game_module_tool/scenes/scene_error.c b/video_game_module_tool/scenes/scene_error.c new file mode 100644 index 00000000..52651272 --- /dev/null +++ b/video_game_module_tool/scenes/scene_error.c @@ -0,0 +1,68 @@ +#include "app_i.h" + +#include +#include + +#include "custom_event.h" +#include "video_game_module_tool_icons.h" + +static void + scene_error_button_callback(GuiButtonType button_type, InputType input_type, void* context) { + App* app = context; + if(input_type == InputTypeShort && button_type == GuiButtonTypeLeft) { + view_dispatcher_send_custom_event(app->view_dispatcher, CustomEventRetryRequested); + } +} + +void scene_error_on_enter(void* context) { + App* app = context; + + widget_add_icon_element(app->widget, 83, 22, &I_WarningDolphinFlip_45x42); + widget_add_button_element( + app->widget, GuiButtonTypeLeft, "Retry", scene_error_button_callback, app); + widget_add_string_element( + app->widget, 64, 0, AlignCenter, AlignTop, FontPrimary, "Installation Failed!"); + + const char* error_msg; + if(app->flasher_error == FlasherErrorBadFile) { + error_msg = "This file is\ncorrupted or\nunsupported"; + } else if(app->flasher_error == FlasherErrorDisconnect) { + error_msg = "The module was\ndisconnected\nduring the update"; + } else if(app->flasher_error == FlasherErrorUnknown) { + error_msg = "An unknown error\nhas occurred"; + } else { + furi_crash(); + } + + widget_add_string_multiline_element( + app->widget, 0, 28, AlignLeft, AlignCenter, FontSecondary, error_msg); + + view_dispatcher_switch_to_view(app->view_dispatcher, ViewIdWidget); + + notification_message(app->notification, &sequence_error); + notification_message(app->notification, &sequence_set_red_255); +} + +bool scene_error_on_event(void* context, SceneManagerEvent event) { + App* app = context; + + bool consumed = false; + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == CustomEventRetryRequested) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, SceneProbe); + } + consumed = true; + } else if(event.type == SceneManagerEventTypeBack) { + furi_string_reset(app->file_path); + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, SceneProbe); + consumed = true; + } + + return consumed; +} + +void scene_error_on_exit(void* context) { + App* app = context; + widget_reset(app->widget); + notification_message(app->notification, &sequence_reset_red); +} diff --git a/video_game_module_tool/scenes/scene_file_select.c b/video_game_module_tool/scenes/scene_file_select.c new file mode 100644 index 00000000..0f7c1c3e --- /dev/null +++ b/video_game_module_tool/scenes/scene_file_select.c @@ -0,0 +1,36 @@ +#include "app_i.h" + +#include + +#include +#include + +void scene_file_select_on_enter(void* context) { + App* app = context; + DialogsApp* dialogs = furi_record_open(RECORD_DIALOGS); + + DialogsFileBrowserOptions options; + dialog_file_browser_set_basic_options(&options, VGM_FW_FILE_EXTENSION, NULL); + + options.hide_dot_files = true; + options.base_path = VGM_FW_DEFAULT_PATH; + + if(dialog_file_browser_show(dialogs, app->file_path, app->file_path, &options)) { + scene_manager_next_scene(app->scene_manager, SceneConfirm); + } else { + furi_string_reset(app->file_path); + scene_manager_previous_scene(app->scene_manager); + } + + furi_record_close(RECORD_DIALOGS); +} + +bool scene_file_select_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return false; +} + +void scene_file_select_on_exit(void* context) { + UNUSED(context); +} diff --git a/video_game_module_tool/scenes/scene_install.c b/video_game_module_tool/scenes/scene_install.c new file mode 100644 index 00000000..cc8b8674 --- /dev/null +++ b/video_game_module_tool/scenes/scene_install.c @@ -0,0 +1,39 @@ +#include "app_i.h" + +#include + +#include "flasher/flasher.h" + +static void scene_install_flasher_callback(FlasherEvent event, void* context) { + furi_assert(context); + App* app = context; + + if(event.type == FlasherEventTypeProgress) { + progress_set_value(app->progress, event.progress); + } else if(event.type == FlasherEventTypeSuccess) { + scene_manager_next_scene(app->scene_manager, SceneSuccess); + } else if(event.type == FlasherEventTypeError) { + app->flasher_error = event.error; + scene_manager_next_scene(app->scene_manager, SceneError); + } +} + +void scene_install_on_enter(void* context) { + App* app = context; + + view_dispatcher_switch_to_view(app->view_dispatcher, ViewIdProgress); + + flasher_set_callback(scene_install_flasher_callback, app); + flasher_start(furi_string_get_cstr(app->file_path)); +} + +bool scene_install_on_event(void* context, SceneManagerEvent event) { + UNUSED(context); + UNUSED(event); + return true; +} + +void scene_install_on_exit(void* context) { + App* app = context; + progress_reset(app->progress); +} diff --git a/video_game_module_tool/scenes/scene_probe.c b/video_game_module_tool/scenes/scene_probe.c new file mode 100644 index 00000000..ab369dc4 --- /dev/null +++ b/video_game_module_tool/scenes/scene_probe.c @@ -0,0 +1,43 @@ +#include "app_i.h" + +#include + +#include "video_game_module_tool_icons.h" + +void scene_probe_on_enter(void* context) { + App* app = context; + + if(flasher_init()) { + scene_manager_next_scene(app->scene_manager, SceneStart); + } else { + widget_add_icon_element(app->widget, 1, 1, &I_Update_module_56x52); + widget_add_string_multiline_element( + app->widget, + 92, + 32, + AlignCenter, + AlignCenter, + FontSecondary, + "Install Video\nGame Module"); + view_dispatcher_switch_to_view(app->view_dispatcher, ViewIdWidget); + } +} + +bool scene_probe_on_event(void* context, SceneManagerEvent event) { + App* app = context; + + bool consumed = false; + if(event.type == SceneManagerEventTypeTick) { + if(flasher_init()) { + scene_manager_next_scene(app->scene_manager, SceneStart); + } + consumed = true; + } + return consumed; +} + +void scene_probe_on_exit(void* context) { + App* app = context; + widget_reset(app->widget); + flasher_deinit(); +} diff --git a/video_game_module_tool/scenes/scene_start.c b/video_game_module_tool/scenes/scene_start.c new file mode 100644 index 00000000..05c02b97 --- /dev/null +++ b/video_game_module_tool/scenes/scene_start.c @@ -0,0 +1,60 @@ +#include "app_i.h" + +#include + +typedef enum { + SceneStartIndexInstallDefault, + SceneStartIndexInstallCustom, +} SceneStartIndex; + +void scene_start_on_enter(void* context) { + App* app = context; + + if(!furi_string_empty(app->file_path)) { + // File path is set, go directly to firmware install + scene_manager_next_scene(app->scene_manager, SceneInstall); + return; + } + + submenu_add_item( + app->submenu, + "Install Official Firmware", + SceneStartIndexInstallDefault, + submenu_item_common_callback, + app); + submenu_add_item( + app->submenu, + "Install Firmware from File", + SceneStartIndexInstallCustom, + submenu_item_common_callback, + app); + + view_dispatcher_switch_to_view(app->view_dispatcher, ViewIdSubmenu); +} + +bool scene_start_on_event(void* context, SceneManagerEvent event) { + furi_assert(context); + + App* app = context; + + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == SceneStartIndexInstallDefault) { + furi_string_set(app->file_path, VGM_DEFAULT_FW_FILE); + scene_manager_next_scene(app->scene_manager, SceneConfirm); + } else if(event.event == SceneStartIndexInstallCustom) { + scene_manager_next_scene(app->scene_manager, SceneFileSelect); + } + + return true; + } else if(event.type == SceneManagerEventTypeBack) { + view_dispatcher_stop(app->view_dispatcher); + return true; + } + + return false; +} + +void scene_start_on_exit(void* context) { + App* app = context; + submenu_reset(app->submenu); +} diff --git a/video_game_module_tool/scenes/scene_success.c b/video_game_module_tool/scenes/scene_success.c new file mode 100644 index 00000000..98898976 --- /dev/null +++ b/video_game_module_tool/scenes/scene_success.c @@ -0,0 +1,54 @@ +#include "app_i.h" + +#include +#include + +#include "custom_event.h" +#include "video_game_module_tool_icons.h" + +static void + scene_success_button_callback(GuiButtonType button_type, InputType input_type, void* context) { + App* app = context; + if(input_type == InputTypeShort && button_type == GuiButtonTypeCenter) { + view_dispatcher_send_custom_event(app->view_dispatcher, CustomEventSuccessDismissed); + } +} + +void scene_success_on_enter(void* context) { + App* app = context; + + widget_add_icon_element(app->widget, 11, 24, &I_Module_60x26); + widget_add_icon_element(app->widget, 77, 10, &I_Checkmark_44x40); + widget_add_button_element( + app->widget, GuiButtonTypeCenter, "OK", scene_success_button_callback, app); + widget_add_string_multiline_element( + app->widget, 64, 0, AlignCenter, AlignTop, FontPrimary, "Video Game Module\nUpdated"); + + view_dispatcher_switch_to_view(app->view_dispatcher, ViewIdWidget); + + notification_message(app->notification, &sequence_success); + notification_message(app->notification, &sequence_set_green_255); +} + +bool scene_success_on_event(void* context, SceneManagerEvent event) { + App* app = context; + + bool consumed = false; + if(event.type == SceneManagerEventTypeCustom) { + if(event.event == CustomEventSuccessDismissed) { + scene_manager_search_and_switch_to_previous_scene(app->scene_manager, SceneProbe); + } + consumed = true; + } else if(event.type == SceneManagerEventTypeBack) { + consumed = true; + } + + return consumed; +} + +void scene_success_on_exit(void* context) { + App* app = context; + widget_reset(app->widget); + furi_string_reset(app->file_path); + notification_message(app->notification, &sequence_reset_green); +} diff --git a/video_game_module_tool/vgm_tool.png b/video_game_module_tool/vgm_tool.png new file mode 100644 index 00000000..e02d1657 Binary files /dev/null and b/video_game_module_tool/vgm_tool.png differ diff --git a/video_game_module_tool/views/progress.c b/video_game_module_tool/views/progress.c new file mode 100644 index 00000000..838dad9a --- /dev/null +++ b/video_game_module_tool/views/progress.c @@ -0,0 +1,76 @@ +#include "progress.h" + +#include +#include + +#include "video_game_module_tool_icons.h" + +struct Progress { + View* view; +}; + +typedef struct { + FuriString* text; + uint8_t progress; +} ProgressModel; + +static void progress_draw_callback(Canvas* canvas, void* _model) { + ProgressModel* model = _model; + + canvas_set_font(canvas, FontPrimary); + canvas_draw_str_aligned(canvas, 64, 0, AlignCenter, AlignTop, "INSTALLING"); + canvas_draw_icon(canvas, 34, 11, &I_Flashing_module_70x30); + + elements_progress_bar_with_text( + canvas, 22, 45, 84, model->progress / 100.f, furi_string_get_cstr(model->text)); +} + +Progress* progress_alloc() { + Progress* instance = malloc(sizeof(Progress)); + instance->view = view_alloc(); + + view_allocate_model(instance->view, ViewModelTypeLocking, sizeof(ProgressModel)); + view_set_draw_callback(instance->view, progress_draw_callback); + + with_view_model( + instance->view, + ProgressModel * model, + { + model->progress = 0; + model->text = furi_string_alloc_printf("0%%"); + }, + true); + + return instance; +} + +void progress_free(Progress* instance) { + with_view_model( + instance->view, ProgressModel * model, { furi_string_free(model->text); }, false); + + view_free(instance->view); + free(instance); +} + +View* progress_get_view(Progress* instance) { + return instance->view; +} + +void progress_set_value(Progress* instance, uint8_t value) { + bool update = false; + with_view_model( + instance->view, + ProgressModel * model, + { + update = model->progress != value; + if(update) { + furi_string_printf(model->text, "%u%%", value); + model->progress = value; + } + }, + update); +} + +void progress_reset(Progress* instance) { + progress_set_value(instance, 0); +} diff --git a/video_game_module_tool/views/progress.h b/video_game_module_tool/views/progress.h new file mode 100644 index 00000000..6628c726 --- /dev/null +++ b/video_game_module_tool/views/progress.h @@ -0,0 +1,21 @@ +/** + * @file progress.h + * @brief Gui view used to display the flashing progress. + * + * Includes a progress bar and some static graphics. + */ +#pragma once + +#include + +typedef struct Progress Progress; + +Progress* progress_alloc(); + +void progress_free(Progress* instance); + +View* progress_get_view(Progress* instance); + +void progress_set_value(Progress* instance, uint8_t value); + +void progress_reset(Progress* instance);