Преглед изворни кода

Fix video_game_module_tool remote

Willy-JL пре 1 година
родитељ
комит
1d24d645fb
42 измењених фајлова са 2554 додато и 0 уклоњено
  1. 7 0
      video_game_module_tool/.catalog/CHANGELOG.md
  2. 16 0
      video_game_module_tool/.catalog/README.md
  3. BIN
      video_game_module_tool/.catalog/screenshots/1.png
  4. BIN
      video_game_module_tool/.catalog/screenshots/2.png
  5. BIN
      video_game_module_tool/.catalog/screenshots/3.png
  6. 2 0
      video_game_module_tool/.gitsubtree
  7. 115 0
      video_game_module_tool/app.c
  8. 58 0
      video_game_module_tool/app_i.h
  9. 17 0
      video_game_module_tool/application.fam
  10. 11 0
      video_game_module_tool/custom_event.h
  11. BIN
      video_game_module_tool/files/vgm-fw-0.1.0.uf2
  12. BIN
      video_game_module_tool/files/vgm-fw-rgb.uf2
  13. 23 0
      video_game_module_tool/flasher/board.c
  14. 23 0
      video_game_module_tool/flasher/board.h
  15. 271 0
      video_game_module_tool/flasher/flasher.c
  16. 84 0
      video_game_module_tool/flasher/flasher.h
  17. 433 0
      video_game_module_tool/flasher/rp2040.c
  18. 53 0
      video_game_module_tool/flasher/rp2040.h
  19. 278 0
      video_game_module_tool/flasher/swd.c
  20. 122 0
      video_game_module_tool/flasher/swd.h
  21. 231 0
      video_game_module_tool/flasher/target.c
  22. 45 0
      video_game_module_tool/flasher/target.h
  23. 167 0
      video_game_module_tool/flasher/uf2.c
  24. 60 0
      video_game_module_tool/flasher/uf2.h
  25. BIN
      video_game_module_tool/icons/Checkmark_44x40.png
  26. BIN
      video_game_module_tool/icons/Flashing_module_70x30.png
  27. BIN
      video_game_module_tool/icons/Module_60x26.png
  28. BIN
      video_game_module_tool/icons/Update_module_56x52.png
  29. BIN
      video_game_module_tool/icons/WarningDolphinFlip_45x42.png
  30. 30 0
      video_game_module_tool/scenes/scene.c
  31. 28 0
      video_game_module_tool/scenes/scene.h
  32. 7 0
      video_game_module_tool/scenes/scene_config.h
  33. 66 0
      video_game_module_tool/scenes/scene_confirm.c
  34. 68 0
      video_game_module_tool/scenes/scene_error.c
  35. 36 0
      video_game_module_tool/scenes/scene_file_select.c
  36. 39 0
      video_game_module_tool/scenes/scene_install.c
  37. 43 0
      video_game_module_tool/scenes/scene_probe.c
  38. 70 0
      video_game_module_tool/scenes/scene_start.c
  39. 54 0
      video_game_module_tool/scenes/scene_success.c
  40. BIN
      video_game_module_tool/vgm_tool.png
  41. 76 0
      video_game_module_tool/views/progress.c
  42. 21 0
      video_game_module_tool/views/progress.h

+ 7 - 0
video_game_module_tool/.catalog/CHANGELOG.md

@@ -0,0 +1,7 @@
+## 1.2
+ - Fix crash due to log output in critical section
+ - Fix crash when built with DEBUG=1
+## 1.1
+ - Description update
+## 1.0
+ - Initial release

+ 16 - 0
video_game_module_tool/.catalog/README.md

@@ -0,0 +1,16 @@
+# Video Game Module Tool
+
+This app is a standalone firmware updater/installer for the Video Game Module
+
+## Features
+
+- Install the official Video Game Module firmware directly from Flipper Zero (firmware comes bundled with the app).
+- Install custom Video Game Module firmware files in UF2 format from a microSD card (see limitations).
+
+## Limitations
+
+When creating a custom UF2 firmware image, keep in mind the following limitations:
+- 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.

BIN
video_game_module_tool/.catalog/screenshots/1.png


BIN
video_game_module_tool/.catalog/screenshots/2.png


BIN
video_game_module_tool/.catalog/screenshots/3.png


+ 2 - 0
video_game_module_tool/.gitsubtree

@@ -0,0 +1,2 @@
+https://github.com/xMasterX/all-the-plugins dev base_pack/video_game_module_tool
+https://github.com/flipperdevices/flipperzero-good-faps dev video_game_module_tool

+ 115 - 0
video_game_module_tool/app.c

@@ -0,0 +1,115 @@
+#include <furi.h>
+
+#include <furi_hal_rtc.h>
+#include <furi_hal_debug.h>
+
+#include <gui/gui.h>
+#include <expansion/expansion.h>
+
+#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;
+}

+ 58 - 0
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 <gui/scene_manager.h>
+#include <gui/view_dispatcher.h>
+
+#include <gui/modules/widget.h>
+#include <gui/modules/submenu.h>
+
+#include <storage/storage.h>
+
+#include <notification/notification.h>
+
+#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);

+ 17 - 0
video_game_module_tool/application.fam

@@ -0,0 +1,17 @@
+App(
+    appid="video_game_module_tool",
+    name="[VGM] Video Game Module Tool",
+    apptype=FlipperAppType.EXTERNAL,
+    entry_point="vgm_tool_app",
+    requires=[
+        "gui",
+        "dialogs",
+    ],
+    stack_size=2048,
+    fap_description="This app is a standalone firmware updater/installer for the Video Game Module",
+    fap_version="1.2",
+    fap_icon="vgm_tool.png",
+    fap_category="GPIO",
+    fap_icon_assets="icons",
+    fap_file_assets="files",
+)

+ 11 - 0
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;

BIN
video_game_module_tool/files/vgm-fw-0.1.0.uf2


BIN
video_game_module_tool/files/vgm-fw-rgb.uf2


+ 23 - 0
video_game_module_tool/flasher/board.c

@@ -0,0 +1,23 @@
+#include "board.h"
+
+#include <furi.h>
+#include <furi_hal_resources.h>
+
+#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);
+}

+ 23 - 0
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);

+ 271 - 0
video_game_module_tool/flasher/flasher.c

@@ -0,0 +1,271 @@
+#include "flasher.h"
+
+#include <furi.h>
+#include <storage/storage.h>
+
+#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();
+    swd_init();
+
+    if(!target_attach(RP2040_CORE0_ADDR)) {
+        FURI_LOG_E(TAG, "Failed to attach target");
+        flasher_deinit();
+        return false;
+    }
+
+    return true;
+}
+
+void flasher_deinit(void) {
+    FURI_LOG_D(TAG, "Detaching target and restoring pins");
+
+    target_detach();
+    swd_deinit();
+    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) {
+    return rp2040_init();
+}
+
+static inline bool flasher_erase_sector(uint32_t address) {
+    return rp2040_flash_erase_sector(address);
+}
+
+static inline bool flasher_program_page(uint32_t address, const void* data, size_t data_size) {
+    return rp2040_flash_program_page(address, data, data_size);
+}
+
+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);
+}

+ 84 - 0
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 <stdint.h>
+#include <stdbool.h>
+
+/**
+ * @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);

+ 433 - 0
video_game_module_tool/flasher/rp2040.c

@@ -0,0 +1,433 @@
+#include "rp2040.h"
+
+#include <furi.h>
+
+#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;
+}

+ 53 - 0
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 <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#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);

+ 278 - 0
video_game_module_tool/flasher/swd.c

@@ -0,0 +1,278 @@
+#include "swd.h"
+
+#include <furi.h>
+#include <furi_hal_resources.h>
+
+#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) {
+    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;
+}

+ 122 - 0
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 <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+// 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);

+ 231 - 0
video_game_module_tool/flasher/target.c

@@ -0,0 +1,231 @@
+#include "target.h"
+
+#include <furi.h>
+
+#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;
+}

+ 45 - 0
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 <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+/**
+ * @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);

+ 167 - 0
video_game_module_tool/flasher/uf2.c

@@ -0,0 +1,167 @@
+#include "uf2.h"
+
+#include <furi.h>
+
+#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;
+}

+ 60 - 0
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 <storage/storage.h>
+
+/**
+ * @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);

BIN
video_game_module_tool/icons/Checkmark_44x40.png


BIN
video_game_module_tool/icons/Flashing_module_70x30.png


BIN
video_game_module_tool/icons/Module_60x26.png


BIN
video_game_module_tool/icons/Update_module_56x52.png


BIN
video_game_module_tool/icons/WarningDolphinFlip_45x42.png


+ 30 - 0
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,
+};

+ 28 - 0
video_game_module_tool/scenes/scene.h

@@ -0,0 +1,28 @@
+#pragma once
+
+#include <gui/scene_manager.h>
+
+// 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

+ 7 - 0
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)

+ 66 - 0
video_game_module_tool/scenes/scene_confirm.c

@@ -0,0 +1,66 @@
+#include "app_i.h"
+
+#include <furi.h>
+#include <toolbox/path.h>
+
+#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);
+}

+ 68 - 0
video_game_module_tool/scenes/scene_error.c

@@ -0,0 +1,68 @@
+#include "app_i.h"
+
+#include <furi.h>
+#include <notification/notification_messages.h>
+
+#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);
+}

+ 36 - 0
video_game_module_tool/scenes/scene_file_select.c

@@ -0,0 +1,36 @@
+#include "app_i.h"
+
+#include <furi.h>
+
+#include <dialogs/dialogs.h>
+#include <storage/storage.h>
+
+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);
+}

+ 39 - 0
video_game_module_tool/scenes/scene_install.c

@@ -0,0 +1,39 @@
+#include "app_i.h"
+
+#include <furi.h>
+
+#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);
+}

+ 43 - 0
video_game_module_tool/scenes/scene_probe.c

@@ -0,0 +1,43 @@
+#include "app_i.h"
+
+#include <furi.h>
+
+#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();
+}

+ 70 - 0
video_game_module_tool/scenes/scene_start.c

@@ -0,0 +1,70 @@
+#include "app_i.h"
+
+#include <furi.h>
+
+typedef enum {
+    SceneStartIndexInstallDefault,
+    SceneStartIndexInstallRGB,
+    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 RGB Firmware",
+        SceneStartIndexInstallRGB,
+        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 == SceneStartIndexInstallRGB) {
+            furi_string_set(app->file_path, APP_ASSETS_PATH("vgm-fw-rgb.uf2"));
+            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);
+}

+ 54 - 0
video_game_module_tool/scenes/scene_success.c

@@ -0,0 +1,54 @@
+#include "app_i.h"
+
+#include <furi.h>
+#include <notification/notification_messages.h>
+
+#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);
+}

BIN
video_game_module_tool/vgm_tool.png


+ 76 - 0
video_game_module_tool/views/progress.c

@@ -0,0 +1,76 @@
+#include "progress.h"
+
+#include <gui/canvas.h>
+#include <gui/elements.h>
+
+#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);
+}

+ 21 - 0
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 <gui/view.h>
+
+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);