From 8241404a3ca9cdb458479571b0698a1b60c228b9 Mon Sep 17 00:00:00 2001 From: Georgii Surkov Date: Fri, 9 Feb 2024 19:53:52 +0300 Subject: [PATCH 1/3] Add video game module tool --- video_game_module_tool/.catalog/CHANGELOG.md | 2 + video_game_module_tool/.catalog/README.md | 17 + .../.catalog/screenshots/1.png | Bin 0 -> 1579 bytes .../.catalog/screenshots/2.png | Bin 0 -> 1865 bytes .../.catalog/screenshots/3.png | Bin 0 -> 2185 bytes video_game_module_tool/app.c | 115 +++++ video_game_module_tool/app_i.h | 58 +++ video_game_module_tool/application.fam | 17 + video_game_module_tool/custom_event.h | 11 + video_game_module_tool/files/vgm-fw-0.1.0.uf2 | Bin 0 -> 303616 bytes video_game_module_tool/flasher/board.c | 23 + video_game_module_tool/flasher/board.h | 23 + video_game_module_tool/flasher/flasher.c | 292 ++++++++++++ video_game_module_tool/flasher/flasher.h | 84 ++++ video_game_module_tool/flasher/rp2040.c | 433 ++++++++++++++++++ video_game_module_tool/flasher/rp2040.h | 53 +++ video_game_module_tool/flasher/swd.c | 281 ++++++++++++ video_game_module_tool/flasher/swd.h | 122 +++++ video_game_module_tool/flasher/target.c | 231 ++++++++++ video_game_module_tool/flasher/target.h | 45 ++ video_game_module_tool/flasher/uf2.c | 167 +++++++ video_game_module_tool/flasher/uf2.h | 60 +++ .../icons/Checkmark_44x40.png | Bin 0 -> 384 bytes .../icons/Flashing_module_70x30.png | Bin 0 -> 3821 bytes video_game_module_tool/icons/Module_60x26.png | Bin 0 -> 4494 bytes .../icons/Update_module_56x52.png | Bin 0 -> 3929 bytes .../icons/WarningDolphinFlip_45x42.png | Bin 0 -> 1437 bytes video_game_module_tool/scenes/scene.c | 30 ++ video_game_module_tool/scenes/scene.h | 28 ++ video_game_module_tool/scenes/scene_config.h | 7 + video_game_module_tool/scenes/scene_confirm.c | 66 +++ video_game_module_tool/scenes/scene_error.c | 68 +++ .../scenes/scene_file_select.c | 36 ++ video_game_module_tool/scenes/scene_install.c | 39 ++ video_game_module_tool/scenes/scene_probe.c | 43 ++ video_game_module_tool/scenes/scene_start.c | 60 +++ video_game_module_tool/scenes/scene_success.c | 54 +++ video_game_module_tool/vgm_tool.png | Bin 0 -> 972 bytes video_game_module_tool/views/progress.c | 76 +++ video_game_module_tool/views/progress.h | 21 + 40 files changed, 2562 insertions(+) create mode 100644 video_game_module_tool/.catalog/CHANGELOG.md create mode 100644 video_game_module_tool/.catalog/README.md create mode 100644 video_game_module_tool/.catalog/screenshots/1.png create mode 100644 video_game_module_tool/.catalog/screenshots/2.png create mode 100644 video_game_module_tool/.catalog/screenshots/3.png create mode 100644 video_game_module_tool/app.c create mode 100644 video_game_module_tool/app_i.h create mode 100644 video_game_module_tool/application.fam create mode 100644 video_game_module_tool/custom_event.h create mode 100644 video_game_module_tool/files/vgm-fw-0.1.0.uf2 create mode 100644 video_game_module_tool/flasher/board.c create mode 100644 video_game_module_tool/flasher/board.h create mode 100644 video_game_module_tool/flasher/flasher.c create mode 100644 video_game_module_tool/flasher/flasher.h create mode 100644 video_game_module_tool/flasher/rp2040.c create mode 100644 video_game_module_tool/flasher/rp2040.h create mode 100644 video_game_module_tool/flasher/swd.c create mode 100644 video_game_module_tool/flasher/swd.h create mode 100644 video_game_module_tool/flasher/target.c create mode 100644 video_game_module_tool/flasher/target.h create mode 100644 video_game_module_tool/flasher/uf2.c create mode 100644 video_game_module_tool/flasher/uf2.h create mode 100644 video_game_module_tool/icons/Checkmark_44x40.png create mode 100644 video_game_module_tool/icons/Flashing_module_70x30.png create mode 100644 video_game_module_tool/icons/Module_60x26.png create mode 100644 video_game_module_tool/icons/Update_module_56x52.png create mode 100644 video_game_module_tool/icons/WarningDolphinFlip_45x42.png create mode 100644 video_game_module_tool/scenes/scene.c create mode 100644 video_game_module_tool/scenes/scene.h create mode 100644 video_game_module_tool/scenes/scene_config.h create mode 100644 video_game_module_tool/scenes/scene_confirm.c create mode 100644 video_game_module_tool/scenes/scene_error.c create mode 100644 video_game_module_tool/scenes/scene_file_select.c create mode 100644 video_game_module_tool/scenes/scene_install.c create mode 100644 video_game_module_tool/scenes/scene_probe.c create mode 100644 video_game_module_tool/scenes/scene_start.c create mode 100644 video_game_module_tool/scenes/scene_success.c create mode 100644 video_game_module_tool/vgm_tool.png create mode 100644 video_game_module_tool/views/progress.c create mode 100644 video_game_module_tool/views/progress.h 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 0000000000000000000000000000000000000000..636386de2923b711cba55bb483ceaf7de7802e96 GIT binary patch literal 1579 zcmds1ZA?>F7=AB|Rd9B7i5pBPM4Z9k1{qQ8!iR2^f<`8r+F?OZ26bz2)jGkHmWCg$ zE@TY~`x5D*%h;9<6ryEXY7Hhbskb9@cB7@`qqIy`FVfMw_Lj2KwMp-698aUNip{gfEe_P!HN*826dB9KBzqFMJ8`cAq#Qt^w2jFX z9Dx0i#eV`>klJ9$JE%H*+P)@HmM2m*J5FgUrllh_M>(fg-)VL<78N?%=DJmK*~C5# zz{MvZZ6|_KOY8)Phu0GMxq~xX(s7aLWKUrDvC*qFtll5a@dX4!BosAiWp2Gl`>Rym zw0kl@w3#0c+lFrFf&y>#gVuqi@?KvDG2TY{hY9;aZYC%)Fi<`Nim!LMps4DTy4&QG zs^T^0KHP5+N{#)kw)cLK+DBcpyn*9YLZ^9hygVv@x9r6<^Py(kwsbaA-S8xFyfffV zAsuyfali_tgPhyE$Tp-?n8%~f{@nEQ_cTdg*+ zq2gT17efhQCnJXbcX;U0F#O(W^YZc%=0Ws|+Us=!@djnk`1slrb z^{F(=E?i?6j`8pvADe1yXCyy0dx*oyv?5GOL7+ED9n^xz?ykH+j7cuM-`U$YL*b$# zt9)dxX{P=`vd^N;GtVZqZx*JwW=%s!c;3Brl%VKx)T`?!nAL>OI_M(2Hd5mm7A6fo zeTCFtGL5RlGjh|c`Ot{0HxK8_*gaPDd0&5i3(c_m=9q6QZ&E$Y{9DSwb?OhYa*Ql% z2YrJSRPR!l?m6`kwNckiE61Az)~aWdHqkAK)h4<_EIn1>>%yB-0#|E3^=%dIk$y*O zfPzOS3RWtz1YQGe@!!`azy`y;{NNVDRD-JOST^Xo&_IS}FCX9I|mf!a~NP~iJV8Ac0L0=EV)7^DB{9%65?ev$xeWycrw7sNGl?Kk*3Fp zfwdDo4$nq}A14?UvgCcHP5pGz-m3`4VzRUoz1Vwy{6`oVXS+&vOs%6526Ggn7 p79KXl)9yz590@ht`(FxQWL!&I%j7aj`RMhBk{zYoo>!||{sLq;D=+{6 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..8e8ce7bf19948f9b8fe17da83036e0903c27ac11 GIT binary patch literal 1865 zcmb7FYfMvT82$?46>Xz&4p|C`adRN?(%{7QP*kwOn0T*%Tm+>iFa!%5wDgkw0G4ew zieg67BT;-weZI9m1RxA~24aH^KGB@qn8@ecg`d-mKb+)cB4%tAcb2tnVGCOyb z$}?-Sk{+A zMmUBb)zei0_9GY&zYK6UVF2#T;u%~z)GvZekRnY7OGX>$ZzD7p%Wna2Z4rp80ib$& zV0Ld$bVZ(V3xOvVJn7sSF<-jTdqfTJMs-Id6LAn4CtD15JS(LxjAgj{`!H!ZdhL9@ z4@({>NV`i`8t4-g4$lxt;w=Z^NnjoYfm=mnMA-zfut3Oa5ZnF=cAC|t2t~WbzDS)I z1aQ7VUMzi-x^pW!si^4-02j#q)6dJwS3$ypodsQ2-2EG6Lew;EAlUI_W-iksdUyye z14!@bo~_(oF$BHZob^g+kddtw%m_c- zu*Iz~oHlMguCJmYFf9)cPdJHMe+%zyWqDlp82V9G%b5pC@{kCffb4BltBYoOe!6F}%yPus!G4UEWqHddJHvg(JTcP9QLA)2#qP2bqR(v|R;q^qlYSJ1dwzy8M^{YPd zA(qQ;Vo!T6UI>!hMu+BdUoA9p%oLcNK`}rnwb&>7Q2ayUYOJFwgIfBDM$UdHL&mAl zZ#fn?$}gFBxFP*j!(xI?$}@IriwdV^gVW(+g~x@mh^2n|)jM$$+gSZJn!&2;5tg?? z(4@ZKGL&nHl}mMGGh4t%*>A^eext!7e7Cjw+d_VmK~^tuH4B`w#_cPeyM((@w4LB{ zZ|8h|%78Y8L`JD>3M>oPm3T^8mnd?yNQTZSY2lQ8<6qg9LuTYr44>I%%QeHZD6P+j zfn|)RGnY^_381nusB2E0a1fnwTNN0fNWcsw@d?g*$`}G9zD4ZX^rysq< zx58jIkP^+wN8cz&@H_uiqWS~YMp*7=Ip^97vRF6JVShtL-1L{d?Su5GeFX+%41J6? zD`;XB;%6@UsdJOwN>$cti8(~(JqfH;`+>eA?@^SpID#ZJMR0^Dt<+YxWm#P1t5MG7 zf#m6iXhg!u>+BiI-_hycYUacS@1;fDV;Hqx{6TL_N2T^yJ|?3yN~I)=Q8rnNpYIw^ zxkl}dm~$z2jPP+O*ZdfAE-&fqnBy?f=?L*|S`@#94fs)IeM^5~q1Jep`~UFdKb6S3 Z%>1dMd%|a9-hU%VOgIqVyeIpIzX3>d$h-gm literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..be7912ba92b67d6145077436c65527382c6d95a1 GIT binary patch literal 2185 zcmZ9Oe^66b7RT=sjT)mUhAvykN|&D*2oOTR z+8IF?NeJlWAV^n0sQ2+>Hvm>c205;uSdXO`OP1^ui z7aSGw38|pLFixu+`)Ko;q0`m60xmV;Vcm<>uYRpMU5(ItkV4e4CKRpA$ z=MT?9D}9;dWZCVHU&`fnncA!<`BEcjb8yt{Y(7QQnXpD>L9sr|VQXD9QZaFn68y|b zt?6F`>v2wqbWyG`L~9D`_GRpIU)FFKfD=Kvn4w&}aa>ObP7R@n6(2RWVeNCZ8F`IZ zJNw<0hTmQoB|eF^h8F%@W1q|1{^&zJ$$s8)6ago;0C3R-rnxGN8|@YP0vM?A5L^EJ@hw_IO{ru|}cC$H%tDO3;#o!tw839N6&JK*;l6H<0-E z%V<|o+9e2W8t`Qs${3vQy{21~LBg9>yszK2F1x}@sF|le_)LoOepq2mY)QrY?+r-& z?yd zo^C3O1iLJ3Nhco(`;(V4@(X(Sg!Ay4Wo=7f5BOM{Mns+4gXR}BXLaJlsop?@AlNuG zuhr_zy91(=R+AGz(W+z?=vD7iby1y#a&Z|kU({%Cc%LCn_0s$)+LY0dH z*+i}r{p`2Gy7GP;XDVE4kPvw~Cfo3~;!3AX&ynYG7~aJ_ z!-iS=PS3ZYZ1NTY%4kJDHVK2rWK~HDv?5i|sN4UsgPjw^buycwGgPfc%ofM6t%z{z z*fuS0o0>=2UqT+p^%=JE1W;HU3s;HmkK8W@7ps48a2D6gFh|T|$Sdrsb+q)jKylnaOWg zaK8E5kGD(2pF)271_9Z65-qBhEtWKeEB{Q@N$<@TcB^{-(Y=Q*gwi}ykBIXp^T3(& zB}V77d>p08Q&TCr26YWmxkqkMw>nO4RZHuf1jV71R%9BNcahpZrs~X~I?v_> zJI0)y_ZzoNAvfS?RanQM%JERk8e#jjw^i!_InhykQmYK&re#kOAKx|5{|V_kNMCz> z^3he5V6ALa%yc6SwROcjFR#kxhV6hhS;aZTRikK%k!_-)j>_gcCHx~$Zk0mhbVK_X z$7t%+Q+pKkqR~L)CwmHfqALpUVef|+r@cMz$fl62ae+IuX-IeTfJpgO>(Hg=hEIee z?F_EMIj)a=bRa9GJ`_epU;AsgoV7_)Ods8I%l3WgiHyWDNDPjL1Hxy*+z)yTCTR2f zWMble@n|B(@(Nqil^drx@LUSUU(a}@T0C$>f=W{Cm&ZlZ?^y0JpuL}viSZXvMAQAM z3VwxYf|nEBErmR^j0KckD^f>K2pL{I1L7x^XkwMk7ha!&$qvlL=Sa}oR8J0zU;Us7 zYE!y20En>0*H_k3uUzUUeIZ-VS?3x?@>~0ViGsYk`h)L9H1vK}jS5Pp@2%2fX)heK z%Ac^pJZ@IM&8%27%KcA4kfiTz_I^IwM6vr8d3TQz3a?_`nLd42V){y!@oHGKV8jsvj6}9 literal 0 HcmV?d00001 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..b035e656 --- /dev/null +++ b/video_game_module_tool/application.fam @@ -0,0 +1,17 @@ +App( + appid="vgm_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="0.1", + 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 0000000000000000000000000000000000000000..a9e8fa3985ea8c4ebaad4d8b979a3d85727e0327 GIT binary patch literal 303616 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 ~_n@u+mMx|pn5 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..92853e92ade35f369e4bdefd7b1beba81e926bba GIT binary patch literal 384 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..77496c468eb3b997477ba0c82e0c6d46168bf615 GIT binary patch literal 3929 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 Uzg^}hzX270p00i_>zopr0LnWHWB>pF literal 0 HcmV?d00001 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..a6d52564 --- /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 "vgm_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..e9eea3d8 --- /dev/null +++ b/video_game_module_tool/scenes/scene_probe.c @@ -0,0 +1,43 @@ +#include "app_i.h" + +#include + +#include "vgm_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..3d3db2b6 --- /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 "vgm_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 0000000000000000000000000000000000000000..e02d1657f972f1aa8bf3e0c29b6485d87ae663c2 GIT binary patch literal 972 zcmaJ=J#W)M7&cTORH(!Tl;w0Cpi<-WNBnh)Y125Rk?JZ+g9uw4`_fpcea5~Lw*#tx zrBWq!CUztwRv3_AVrJt9Fd^|1$~jF^27(9Qd#~Q-dB2`JX>LDUm|vV11Yx1E>9qJ+ z*z?!s`2XXt)z|#E!|ENjLwjuKr9`L(v`auE^7crJc){V*Z=@m!bIW17!#eI3_Gu(~ zMU0q72}cV;Wi?Mce?S=MlD#mt;qOnMAqWE-KGs~xO=_ecZXTs%=V-g_9}RpffU6Hc zCC8i~BFqDMv>#_Ux8aBvbGvXO2u2}nV8ipGI&KrxXi9)3$|YZtWMFWstShEv8HTX} z6iHE$q$5QuDJs?sESJIfgFIsz^l;1B80X_R8}=DXFhaxOP#mfvP4|#&Sr(EMq$njG zQOXWu=H;b0TbeL9B=gfSVIhq{!RU4A0ka{m_PmWKnbOADcvO67DEAU1i&D|nB+zyL z9~wo|=!~_n#FcswubYNbGs~K8Xp*j~vN^#z z)IW$w%qH020&C2~7K(@xp4lO3_>2S_DUHCWYaGt5r96{wj16YjqRqr2UJNRB``4}~ zE-s+LoT`^!ruru_7k$drZPE|N{AFBiZnf)YXJ_1e^yT|=e%xy~)pq`)ySsMf^i*2B td~*fdJ$+ktO1I$&d`Dirc^Tlf<4eMg>jM7z +#include + +#include "vgm_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); From 35c1fde6e02e97c0dc960b0450432b627645adf1 Mon Sep 17 00:00:00 2001 From: Georgii Surkov Date: Fri, 9 Feb 2024 20:12:53 +0300 Subject: [PATCH 2/3] Correct App ID --- video_game_module_tool/application.fam | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/video_game_module_tool/application.fam b/video_game_module_tool/application.fam index b035e656..b7c06cf2 100644 --- a/video_game_module_tool/application.fam +++ b/video_game_module_tool/application.fam @@ -1,5 +1,5 @@ App( - appid="vgm_tool", + appid="video_game_module_tool", name="Video Game Module Tool", apptype=FlipperAppType.EXTERNAL, entry_point="vgm_tool_app", @@ -9,7 +9,7 @@ App( ], stack_size=2048, fap_description="Update Video Game Module's firmware directly from Flipper", - fap_version="0.1", + fap_version="1.0", fap_icon="vgm_tool.png", fap_category="Tools", fap_icon_assets="icons", From f0ce0b19ab3c796fa2310f80ab9b885a2f1fec4f Mon Sep 17 00:00:00 2001 From: Georgii Surkov Date: Fri, 9 Feb 2024 20:16:21 +0300 Subject: [PATCH 3/3] Fix icon include file names --- video_game_module_tool/scenes/scene_error.c | 2 +- video_game_module_tool/scenes/scene_probe.c | 2 +- video_game_module_tool/scenes/scene_success.c | 2 +- video_game_module_tool/views/progress.c | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/video_game_module_tool/scenes/scene_error.c b/video_game_module_tool/scenes/scene_error.c index a6d52564..52651272 100644 --- a/video_game_module_tool/scenes/scene_error.c +++ b/video_game_module_tool/scenes/scene_error.c @@ -4,7 +4,7 @@ #include #include "custom_event.h" -#include "vgm_tool_icons.h" +#include "video_game_module_tool_icons.h" static void scene_error_button_callback(GuiButtonType button_type, InputType input_type, void* context) { diff --git a/video_game_module_tool/scenes/scene_probe.c b/video_game_module_tool/scenes/scene_probe.c index e9eea3d8..ab369dc4 100644 --- a/video_game_module_tool/scenes/scene_probe.c +++ b/video_game_module_tool/scenes/scene_probe.c @@ -2,7 +2,7 @@ #include -#include "vgm_tool_icons.h" +#include "video_game_module_tool_icons.h" void scene_probe_on_enter(void* context) { App* app = context; diff --git a/video_game_module_tool/scenes/scene_success.c b/video_game_module_tool/scenes/scene_success.c index 3d3db2b6..98898976 100644 --- a/video_game_module_tool/scenes/scene_success.c +++ b/video_game_module_tool/scenes/scene_success.c @@ -4,7 +4,7 @@ #include #include "custom_event.h" -#include "vgm_tool_icons.h" +#include "video_game_module_tool_icons.h" static void scene_success_button_callback(GuiButtonType button_type, InputType input_type, void* context) { diff --git a/video_game_module_tool/views/progress.c b/video_game_module_tool/views/progress.c index e5cde21a..838dad9a 100644 --- a/video_game_module_tool/views/progress.c +++ b/video_game_module_tool/views/progress.c @@ -3,7 +3,7 @@ #include #include -#include "vgm_tool_icons.h" +#include "video_game_module_tool_icons.h" struct Progress { View* view;