diff --git a/radio/src/boards/generic_stm32/linker/definitions.ld b/radio/src/boards/generic_stm32/linker/definitions.ld index 47d1ac07bce..ac91cbbb64d 100644 --- a/radio/src/boards/generic_stm32/linker/definitions.ld +++ b/radio/src/boards/generic_stm32/linker/definitions.ld @@ -11,3 +11,14 @@ _main_stack_start = _estack - MAIN_STACK_SIZE; /* Higest heap address */ _heap_end = HEAP_ADDRESS; + +/* Fastcode defaults for scripts that do not define the section + (bootloader): the startup copy loop becomes a no-op */ +PROVIDE(_sfastcode = 0); +PROVIDE(_efastcode = 0); +PROVIDE(_fastcode_load = 0); + +/* Same for scripts without a .text.shared section */ +PROVIDE(_stext_shared = 0); +PROVIDE(_etext_shared = 0); +PROVIDE(_text_shared_load = 0); diff --git a/radio/src/boards/generic_stm32/linker/firmware.ld b/radio/src/boards/generic_stm32/linker/firmware.ld index d20169298bd..71309eea5ce 100644 --- a/radio/src/boards/generic_stm32/linker/firmware.ld +++ b/radio/src/boards/generic_stm32/linker/firmware.ld @@ -33,6 +33,59 @@ SECTIONS _isr_load = LOADADDR(.isr_vector); + /* Hot interrupt-path code (USB device stack, MSC storage glue, SDIO + diskio) executed from RAM: on some TX16S units, instruction + fetches from the upper flash bank get corrupted under heavy + IRQ/DMA load (USB crash, EdgeTX#5899); claimed before .text so + these input sections land here. Requires non-LTO objects. */ + .fastcode : + { + . = ALIGN(4); + _sfastcode = .; + *ll_usb*(.text .text.*) + *hal_pcd*(.text .text.*) + *usb_driver*(.text .text.*) + *usbd_conf*(.text .text.*) + *usbd_core*(.text .text.*) + *usbd_ioreq*(.text .text.*) + *usbd_ctlreq*(.text .text.*) + *usbd_msc*(.text .text.*) + *usbd_storage_msd*(.text .text.*) + *diskio_sdio*(.text .text.*) + . = ALIGN(4); + _efastcode = .; + } > REGION_BSS AT> REGION_TEXT_STORAGE + + _fastcode_load = LOADADDR(.fastcode); + + /* C++ comdat sections (templates, inline functions, operator + new/delete) from the std:: namespace are instantiated by both UI + and core objects, and the linker keeps a single copy whose home + object depends on link order. That copy must stay callable at + any time, so claim it here, before any target-specific pre-text + section can grab it through file patterns (see + pre_text_sections.ld). */ + .text.shared (READONLY) : + { + . = ALIGN(4); + _stext_shared = .; + *(.text._ZSt* .text._ZNSt* .text._ZNKSt* .text._ZN9__gnu_cxx*) + *(.text._Znwj .text._Znwm .text._Znaj .text._Znam) + *(.text._ZdlPv* .text._ZdaPv*) + *(.rodata._ZSt* .rodata._ZNSt* .rodata._ZNKSt*) + *(.rodata._ZTVSt* .rodata._ZTVNSt*) + . = ALIGN(4); + _etext_shared = .; + } > REGION_TEXT AT> REGION_TEXT_STORAGE + + _text_shared_load = LOADADDR(.text.shared); + + /* Target-specific output sections that must claim their input + sections before the .text catch-all (e.g. TX16S UI code + relegated to flash bank 2, see + stm32f429_sdram/pre_text_sections.ld) */ + INCLUDE pre_text_sections.ld + /* Main code section */ .text (READONLY) : { @@ -52,7 +105,8 @@ SECTIONS INCLUDE common_sections.ld - /* Assets in FLASH only */ + /* Assets in FLASH only (REGION_ASSETS: same as REGION_TEXT_STORAGE + on most targets, flash bank 2 on TX16S/F429) */ .flash (READONLY) : { . = ALIGN(4); @@ -61,9 +115,10 @@ SECTIONS . = ALIGN(4); _eflash = .; - } > REGION_TEXT_STORAGE + } > REGION_ASSETS .text_end_section : {} > REGION_TEXT AT > REGION_TEXT_STORAGE - _firmware_length = LOADADDR(.text_end_section) - LOADADDR(.firmware_header); + /* .flash is the highest LMA of the image on every target */ + _firmware_length = (LOADADDR(.flash) + SIZEOF(.flash)) - LOADADDR(.firmware_header); _firmware_version = _text_load; } diff --git a/radio/src/boards/generic_stm32/linker/stm32f20x/layout.ld b/radio/src/boards/generic_stm32/linker/stm32f20x/layout.ld index 8190ec13c30..ded0dca4c02 100644 --- a/radio/src/boards/generic_stm32/linker/stm32f20x/layout.ld +++ b/radio/src/boards/generic_stm32/linker/stm32f20x/layout.ld @@ -25,6 +25,7 @@ REGION_ALIAS("REGION_BOOTLOADER", FLASH); REGION_ALIAS("REGION_ISR_VECT", FLASH); REGION_ALIAS("REGION_TEXT", FLASH); REGION_ALIAS("REGION_TEXT_STORAGE", FLASH); +REGION_ALIAS("REGION_ASSETS", FLASH); REGION_ALIAS("REGION_DATA", RAM); REGION_ALIAS("REGION_BSS", RAM); diff --git a/radio/src/boards/generic_stm32/linker/stm32f20x/pre_text_sections.ld b/radio/src/boards/generic_stm32/linker/stm32f20x/pre_text_sections.ld new file mode 100644 index 00000000000..42c12af0136 --- /dev/null +++ b/radio/src/boards/generic_stm32/linker/stm32f20x/pre_text_sections.ld @@ -0,0 +1 @@ +/* No target-specific pre-text sections (see firmware.ld) */ diff --git a/radio/src/boards/generic_stm32/linker/stm32f40x/layout.ld b/radio/src/boards/generic_stm32/linker/stm32f40x/layout.ld index 94b26b49380..34f35ebadab 100644 --- a/radio/src/boards/generic_stm32/linker/stm32f40x/layout.ld +++ b/radio/src/boards/generic_stm32/linker/stm32f40x/layout.ld @@ -29,6 +29,7 @@ REGION_ALIAS("REGION_BOOTLOADER", FLASH); REGION_ALIAS("REGION_ISR_VECT", FLASH); REGION_ALIAS("REGION_TEXT", FLASH); REGION_ALIAS("REGION_TEXT_STORAGE", FLASH); +REGION_ALIAS("REGION_ASSETS", FLASH); REGION_ALIAS("REGION_DATA", CCM); REGION_ALIAS("REGION_BSS", CCM); diff --git a/radio/src/boards/generic_stm32/linker/stm32f40x/pre_text_sections.ld b/radio/src/boards/generic_stm32/linker/stm32f40x/pre_text_sections.ld new file mode 100644 index 00000000000..42c12af0136 --- /dev/null +++ b/radio/src/boards/generic_stm32/linker/stm32f40x/pre_text_sections.ld @@ -0,0 +1 @@ +/* No target-specific pre-text sections (see firmware.ld) */ diff --git a/radio/src/boards/generic_stm32/linker/stm32f413/layout.ld b/radio/src/boards/generic_stm32/linker/stm32f413/layout.ld index c660b556256..3d67f953a22 100644 --- a/radio/src/boards/generic_stm32/linker/stm32f413/layout.ld +++ b/radio/src/boards/generic_stm32/linker/stm32f413/layout.ld @@ -25,6 +25,7 @@ REGION_ALIAS("REGION_BOOTLOADER", FLASH); REGION_ALIAS("REGION_ISR_VECT", FLASH); REGION_ALIAS("REGION_TEXT", FLASH); REGION_ALIAS("REGION_TEXT_STORAGE", FLASH); +REGION_ALIAS("REGION_ASSETS", FLASH); REGION_ALIAS("REGION_DATA", RAM); REGION_ALIAS("REGION_BSS", RAM); diff --git a/radio/src/boards/generic_stm32/linker/stm32f413/pre_text_sections.ld b/radio/src/boards/generic_stm32/linker/stm32f413/pre_text_sections.ld new file mode 100644 index 00000000000..42c12af0136 --- /dev/null +++ b/radio/src/boards/generic_stm32/linker/stm32f413/pre_text_sections.ld @@ -0,0 +1 @@ +/* No target-specific pre-text sections (see firmware.ld) */ diff --git a/radio/src/boards/generic_stm32/linker/stm32f429_sdram/layout.ld b/radio/src/boards/generic_stm32/linker/stm32f429_sdram/layout.ld index 66924e688d1..fb14cef8983 100644 --- a/radio/src/boards/generic_stm32/linker/stm32f429_sdram/layout.ld +++ b/radio/src/boards/generic_stm32/linker/stm32f429_sdram/layout.ld @@ -18,19 +18,29 @@ EXTRAM_SIZE = DEFINED(__EXTRAM_SIZE__) ? __EXTRAM_SIZE__ : 8192K; /* Highest heap address */ HEAP_ADDRESS = EXTRAM_START + EXTRAM_SIZE; -/* Specify the memory areas */ +/* Specify the memory areas. + + The 2MB flash is split at the hardware bank boundary: on some + STM32F42x units (flash rev < 3), USB D+ (PA12) switching noise + corrupts reads from bank 2 (ST errata, EdgeTX#5899). Restricting + FLASH to bank 1 turns any overflow of code into a link error; + only cold UI code/rodata and assets are placed in bank 2 (see + pre_text_sections.ld) and must not be accessed while a USB + session is active. */ MEMORY { - FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 2048K - RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K - CCM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K - SDRAM(xrw) : ORIGIN = EXTRAM_START, LENGTH = EXTRAM_SIZE + FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 1024K /* bank 1 */ + FLASH_BANK2 (rx) : ORIGIN = 0x08100000, LENGTH = 1024K /* bank 2 */ + RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 192K + CCM (xrw) : ORIGIN = 0x10000000, LENGTH = 64K + SDRAM(xrw) : ORIGIN = EXTRAM_START, LENGTH = EXTRAM_SIZE } REGION_ALIAS("REGION_BOOTLOADER", FLASH); REGION_ALIAS("REGION_ISR_VECT", FLASH); REGION_ALIAS("REGION_TEXT", FLASH); REGION_ALIAS("REGION_TEXT_STORAGE", FLASH); +REGION_ALIAS("REGION_ASSETS", FLASH_BANK2); REGION_ALIAS("REGION_DATA", CCM); REGION_ALIAS("REGION_BSS", RAM); diff --git a/radio/src/boards/generic_stm32/linker/stm32f429_sdram/pre_text_sections.ld b/radio/src/boards/generic_stm32/linker/stm32f429_sdram/pre_text_sections.ld new file mode 100644 index 00000000000..e9d9a100177 --- /dev/null +++ b/radio/src/boards/generic_stm32/linker/stm32f429_sdram/pre_text_sections.ld @@ -0,0 +1,45 @@ +/* TX16S / STM32F429 (EdgeTX#5899): on units with flash rev < 3, + USB D+ (PA12) switching noise corrupts reads from flash bank 2 + (0x08100000 and above). All code and rodata that may be used + while a USB session is active must live in bank 1; this is + guaranteed by the FLASH region being restricted to bank 1 + (overflow = link error). + + UI code and its rodata are relegated to bank 2 here: they only + run while USB is inactive (the UI is frozen in USB modes). + Claimed before the .text catch-all so these input sections land + here; file-pattern matching requires non-LTO objects. Shared + C++ comdats are already pinned to bank 1 by .text.shared. */ +/* Bank 1 pins: symbols living in UI objects but called from non-UI + tasks (mixer pulses, telemetry/logs timers, audio), found by the + bank1->bank2 call scan (bench/check_bank1.sh). Section names + follow the mangled symbol: a signature change silently un-pins, + the scan is the gatekeeper. Claimed before .uicode. */ +.text.pinned : +{ + . = ALIGN(4); + /* tick LVGL incremente par le hook SysTick (ISR, meme pendant l'USB) */ + *(.text.lv_tick_inc) + /* helpers R9M inline (pulses, chaque cycle mixer) */ + *(.text._Z17isModuleR9MAccessh .text._Z20isModuleR9MNonAccessh) + /* mailbox popup differee (timers telemetrie/logs) */ + *(.text._Z24POPUP_WARNING_ON_UI_TASKPKcS0_) + /* popups bloquants, gardes par _usb_ui_frozen() (audio, PXX2) */ + *(.text._Z13POPUP_WARNINGPKcS0_ .text._Z17POPUP_INFORMATIONPKc) + /* fonction speciale SET_SCREEN (mixer) */ + *(.text._Z20setRequestedMainViewh) + /* appelee par pwrCheck (menusTask, toutes les 50 ms, gel compris) */ + *(.text._Z23cancelShutdownAnimationv) + . = ALIGN(4); +} > REGION_TEXT AT> REGION_TEXT_STORAGE + +.uicode : +{ + . = ALIGN(4); + _suicode = .; + *colorlcd*(.text .text.* .rodata .rodata.*) + *lvgl*(.text .text.* .rodata .rodata.*) + *libopenui*(.text .text.* .rodata .rodata.*) + . = ALIGN(4); + _euicode = .; +} > REGION_ASSETS diff --git a/radio/src/boards/generic_stm32/linker/stm32h750_sdram/layout.ld b/radio/src/boards/generic_stm32/linker/stm32h750_sdram/layout.ld index 1ce2dbb7456..a64bce94ee7 100644 --- a/radio/src/boards/generic_stm32/linker/stm32h750_sdram/layout.ld +++ b/radio/src/boards/generic_stm32/linker/stm32h750_sdram/layout.ld @@ -35,6 +35,7 @@ MEMORY REGION_ALIAS("REGION_BOOTLOADER", FLASH); REGION_ALIAS("REGION_TEXT_STORAGE", NORFLASH); +REGION_ALIAS("REGION_ASSETS", NORFLASH); REGION_ALIAS("REGION_TEXT", SDRAM); REGION_ALIAS("REGION_ISR_VECT", DTCMRAM); REGION_ALIAS("REGION_DATA", DTCMRAM); diff --git a/radio/src/boards/generic_stm32/linker/stm32h750_sdram/pre_text_sections.ld b/radio/src/boards/generic_stm32/linker/stm32h750_sdram/pre_text_sections.ld new file mode 100644 index 00000000000..42c12af0136 --- /dev/null +++ b/radio/src/boards/generic_stm32/linker/stm32h750_sdram/pre_text_sections.ld @@ -0,0 +1 @@ +/* No target-specific pre-text sections (see firmware.ld) */ diff --git a/radio/src/boards/generic_stm32/linker/stm32h7rs_sdram/layout.ld b/radio/src/boards/generic_stm32/linker/stm32h7rs_sdram/layout.ld index cd0582a2dea..1f17268b919 100644 --- a/radio/src/boards/generic_stm32/linker/stm32h7rs_sdram/layout.ld +++ b/radio/src/boards/generic_stm32/linker/stm32h7rs_sdram/layout.ld @@ -34,6 +34,7 @@ MEMORY REGION_ALIAS("REGION_BOOTLOADER", FLASH); REGION_ALIAS("REGION_TEXT_STORAGE", NORFLASH); +REGION_ALIAS("REGION_ASSETS", NORFLASH); REGION_ALIAS("REGION_TEXT", PSRAM); REGION_ALIAS("REGION_ISR_VECT", DTCMRAM); REGION_ALIAS("REGION_DATA", RAM); diff --git a/radio/src/boards/generic_stm32/linker/stm32h7rs_sdram/pre_text_sections.ld b/radio/src/boards/generic_stm32/linker/stm32h7rs_sdram/pre_text_sections.ld new file mode 100644 index 00000000000..42c12af0136 --- /dev/null +++ b/radio/src/boards/generic_stm32/linker/stm32h7rs_sdram/pre_text_sections.ld @@ -0,0 +1 @@ +/* No target-specific pre-text sections (see firmware.ld) */ diff --git a/radio/src/bootloader/CMakeLists.txt b/radio/src/bootloader/CMakeLists.txt index 25d5aa39c36..6d4c25bbf98 100644 --- a/radio/src/bootloader/CMakeLists.txt +++ b/radio/src/bootloader/CMakeLists.txt @@ -41,12 +41,12 @@ remove_definitions(-DUSB_SERIAL) if(NOT NO_LTO) message("-- Use LTO to compile bootloader") - target_compile_options(stm32cube_ll PRIVATE -flto) - target_compile_options(stm32usb PRIVATE -flto) + # stm32cube_ll, stm32usb, stm32usb_device_fw and + # stm32_drivers_w_dbg_fw are kept out of LTO: their objects must + # stay pattern-matchable by the .fastcode output section (hot + # USB/SD interrupt code executed from RAM, see firmware.ld) target_compile_options(stm32usb_device_bl PRIVATE -flto) - target_compile_options(stm32usb_device_fw PRIVATE -flto) target_compile_options(stm32_drivers PRIVATE -flto) - target_compile_options(stm32_drivers_w_dbg_fw PRIVATE -flto) target_compile_options(stm32_drivers_w_dbg_bl PRIVATE -flto) target_compile_options(board_bl PRIVATE -flto) endif() diff --git a/radio/src/edgetx.cpp b/radio/src/edgetx.cpp index e856137cacc..4188ecc1698 100644 --- a/radio/src/edgetx.cpp +++ b/radio/src/edgetx.cpp @@ -36,6 +36,16 @@ #include "hal/watchdog_driver.h" #include "hal/abnormal_reboot.h" #include "hal/usb_driver.h" + +#if defined(DIAG_BANK2_FENCE) +extern "C" void bank2FenceDumpPostMortem(void); +#endif + +#if defined(STM32) && !defined(SIMU) +void usbSessionForceStop(); +#else +inline void usbSessionForceStop() {} +#endif #include "hal/audio_driver.h" #include "hal/rgbleds.h" @@ -1503,6 +1513,11 @@ void edgeTxInit() if (!sdMounted()) sdInit(); +#if defined(DIAG_BANK2_FENCE) + // report a bank 2 fence trap from the previous session, if any + bank2FenceDumpPostMortem(); +#endif + #if !defined(COLORLCD) if (!sdMounted()) { g_eeGeneral.pwrOffSpeed = 2; @@ -1805,6 +1820,10 @@ uint32_t pwrCheck() } if (get_tmr10ms() - pwr_press_time > PWR_PRESS_SHUTDOWN_DELAY()) { #if defined(COLORLCD) + // End the USB session before any shutdown UI runs: the UI + // lives in flash bank 2, unreadable during USB traffic + // (PA12 errata, EdgeTX#5899). Same cleanup as an unplug. + usbSessionForceStop(); bool usbConfirmed = !usbPlugged() || getSelectedUsbMode() == USB_UNSELECTED_MODE; bool modelConnectedConfirmed = !TELEMETRY_STREAMING() || g_eeGeneral.disableRssiPoweroffAlarm; bool trainerConfirmed = !isTrainerConnected(); @@ -1914,14 +1933,21 @@ uint32_t pwrCheck() return e_power_off; } else { - drawShutdownAnimation(pwrPressedDuration(), PWR_PRESS_SHUTDOWN_DELAY(), message); +#if defined(COLORLCD) + // no drawing while the USB session is active (UI in bank 2) + if (!usbPlugged() || getSelectedUsbMode() == USB_UNSELECTED_MODE) +#endif + drawShutdownAnimation(pwrPressedDuration(), PWR_PRESS_SHUTDOWN_DELAY(), message); return e_power_press; } } } else { #if defined(COLORLCD) - cancelShutdownAnimation(); + // pinned to bank 1 on TX16S, and skipped while the USB session + // is active: menusTask polls this every 50ms (PA12 errata) + if (!usbPlugged() || getSelectedUsbMode() == USB_UNSELECTED_MODE) + cancelShutdownAnimation(); #endif pwr_check_state = PWR_CHECK_ON; pwr_press_time = 0; diff --git a/radio/src/gui/colorlcd/libui/popups.cpp b/radio/src/gui/colorlcd/libui/popups.cpp index 5f70b2212ef..0e12c7ee79e 100644 --- a/radio/src/gui/colorlcd/libui/popups.cpp +++ b/radio/src/gui/colorlcd/libui/popups.cpp @@ -24,6 +24,7 @@ #include "dialog.h" #include "edgetx.h" #include "etx_lv_theme.h" +#include "hal/usb_driver.h" #include "hal/watchdog_driver.h" #include "lvgl/src/hal/lv_hal_tick.h" #include "mainwindow.h" @@ -40,13 +41,29 @@ static void _run_popup_dialog(const char* title, const char* msg, }); } +// While a USB session is active the UI is frozen (PA12/bank 2 +// errata, EdgeTX#5899): a nested popup dialog must never run then. +// These two functions are pinned to flash bank 1 on TX16S (see +// stm32f429_sdram/pre_text_sections.ld) so callers from non-UI +// tasks (audio, telemetry) never fetch bank 2 code. +static inline __attribute__((always_inline)) bool _usb_ui_frozen() +{ +#if defined(STM32) && !defined(SIMU) + return usbPlugged() && getSelectedUsbMode() != USB_UNSELECTED_MODE; +#else + return false; +#endif +} + void POPUP_INFORMATION(const char* message) { + if (_usb_ui_frozen()) return; _run_popup_dialog("Message", message); } void POPUP_WARNING(const char* message, const char* info) { + if (_usb_ui_frozen()) return; _run_popup_dialog("Warning", message, info); } diff --git a/radio/src/main.cpp b/radio/src/main.cpp index 0906531ad90..16df162129b 100644 --- a/radio/src/main.cpp +++ b/radio/src/main.cpp @@ -35,6 +35,7 @@ #include "etx_lv_theme.h" #include "menu.h" #include "mainwindow.h" +#include "static.h" #endif #if defined(CLI) @@ -161,6 +162,8 @@ class UsbSDConnected : public Window static UsbSDConnected* usbConnectedWindow = nullptr; #endif +void usbSessionCleanup(bool wait_sd = true); + void handleUsbConnection() { #if defined(STM32) && !defined(SIMU) @@ -191,9 +194,6 @@ void handleUsbConnection() if (getSelectedUsbMode() != USB_UNSELECTED_MODE) { if (getSelectedUsbMode() == USB_MASS_STORAGE_MODE) { edgeTxClose(false); -#if defined(COLORLCD) - usbConnectedWindow = new UsbSDConnected(); -#endif } #if defined(USB_SERIAL) else if (getSelectedUsbMode() == USB_SERIAL_MODE) { @@ -201,41 +201,87 @@ void handleUsbConnection() } #endif +#if defined(COLORLCD) + // Static USB screen for every mode, rendered now: the UI + // freezes as soon as the USB session starts (PA12/bank 2 + // errata, EdgeTX#5899) and will not draw anything anymore + usbConnectedWindow = new UsbSDConnected(); + { + // name the active mode: the same frozen screen serves all + // three modes and the icon alone reads as "mass storage" + const char* mode_str = STR_USB_MASS_STORAGE; + if (getSelectedUsbMode() == USB_JOYSTICK_MODE) + mode_str = STR_USB_JOYSTICK; +#if defined(USB_SERIAL) + else if (getSelectedUsbMode() == USB_SERIAL_MODE) + mode_str = STR_USB_SERIAL; +#endif + auto label = new StaticText( + usbConnectedWindow, {0, LCD_H - 60, LCD_W, LV_SIZE_CONTENT}, + mode_str, COLOR_THEME_PRIMARY2_INDEX, FONT(L) | CENTERED); + (void)label; + } + lv_refr_now(nullptr); +#endif + usbStart(); TRACE("USB started"); } } if (usbStarted() && !usbPlugged()) { - usbStop(); - TRACE("USB stopped"); - if (getSelectedUsbMode() == USB_MASS_STORAGE_MODE) { + usbSessionCleanup(); + } +#endif // defined(STM32) && !defined(SIMU) +} + +#if defined(STM32) && !defined(SIMU) +// Tear a running USB session down (same cleanup as a physical +// unplug). wait_sd selects the blocking "no SD card" screen +// (unplug path only: forced stops must not block on it). +void usbSessionCleanup(bool wait_sd) +{ + usbStop(); + TRACE("USB stopped"); #if defined(COLORLCD) - usbConnectedWindow->deleteLater(); - usbConnectedWindow = nullptr; - // In case the SD card is removed during the session - if (!SD_CARD_PRESENT()) { - // Blocks until shutdown or SD card inserted - auto w = drawFatalErrorScreen(STR_NO_SDCARD); - MainWindow::instance()->blockUntilClose(true, []() { - return SD_CARD_PRESENT(); - }, true); - w->deleteLater(); - } - edgeTxResume(); -#else - edgeTxResume(); - pushEvent(EVT_ENTRY); + if (usbConnectedWindow) { + usbConnectedWindow->deleteLater(); + usbConnectedWindow = nullptr; + } #endif - } else if (getSelectedUsbMode() == USB_SERIAL_MODE) { - serialStop(SP_VCP); + if (getSelectedUsbMode() == USB_MASS_STORAGE_MODE) { +#if defined(COLORLCD) + // In case the SD card is removed during the session + if (wait_sd && !SD_CARD_PRESENT()) { + // Blocks until shutdown or SD card inserted + auto w = drawFatalErrorScreen(STR_NO_SDCARD); + MainWindow::instance()->blockUntilClose(true, []() { + return SD_CARD_PRESENT(); + }, true); + w->deleteLater(); } - TRACE("reset selected USB mode"); - setSelectedUsbMode(USB_UNSELECTED_MODE); +#endif + edgeTxResume(); +#if !defined(COLORLCD) + pushEvent(EVT_ENTRY); +#endif + } else if (getSelectedUsbMode() == USB_SERIAL_MODE) { + serialStop(SP_VCP); } -#endif // defined(STM32) && !defined(SIMU) + TRACE("reset selected USB mode"); + setSelectedUsbMode(USB_UNSELECTED_MODE); } +// Shutdown path (PA12 errata, EdgeTX#5899): the shutdown UI lives in +// flash bank 2 and may only run once USB traffic has stopped. +void usbSessionForceStop() +{ + if (usbStarted() && getSelectedUsbMode() != USB_UNSELECTED_MODE) { + usbSessionCleanup(false); + } +} +#endif // defined(STM32) && !defined(SIMU) + void checkSpeakerVolume() { #if defined(AUDIO_HP_DETECT_PIN) @@ -543,6 +589,16 @@ void perMain() checkBacklight(); +#if defined(COLORLCD) + // Freeze the whole UI while a USB session is active: on TX16S all + // UI code/rodata lives in flash bank 2, which must not be accessed + // during USB traffic (PA12 errata, EdgeTX#5899). The static USB + // screen was rendered before usbStart(). + if (usbPlugged() && getSelectedUsbMode() != USB_UNSELECTED_MODE) { + return; + } +#endif + #if defined(USE_HATS_AS_KEYS) checkHatsAsKeys(); #endif @@ -574,15 +630,15 @@ void perMain() } } - if (usbPlugged() && getSelectedUsbMode() == USB_MASS_STORAGE_MODE) { #if !defined(COLORLCD) + if (usbPlugged() && getSelectedUsbMode() == USB_MASS_STORAGE_MODE) { // disable access to menus lcdClear(); menuMainView(0); lcdRefresh(); -#endif return; } +#endif #if defined(MULTIMODULE) checkFailsafeMulti(); diff --git a/radio/src/targets/common/arm/stm32/bank2_fence.c b/radio/src/targets/common/arm/stm32/bank2_fence.c new file mode 100644 index 00000000000..9f57d9b3317 --- /dev/null +++ b/radio/src/targets/common/arm/stm32/bank2_fence.c @@ -0,0 +1,132 @@ +/* + * Copyright (C) EdgeTX + * + * License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 as + * published by the Free Software Foundation. + */ + +/* DIAG_BANK2_FENCE (validation builds, TX16S/STM32F429): + * + * While a USB session is active, flash bank 2 (0x08100000, 1MB) must + * never be read: USB D+ (PA12) switching noise corrupts the flash + * read path on some units (ST errata ES0166 §2.4.6, EdgeTX#5899). + * The firmware layout keeps everything hot in bank 1 and freezes the + * UI (whose code lives in bank 2) during USB. + * + * This fence arms an MPU no-access region over bank 2 for the whole + * USB session, so any stray access — including through function + * pointers that the static scan (bench/check_bank1.sh) cannot see — + * traps deterministically (MemManage) instead of corrupting reads + * probabilistically. The fault handler stores a magic and the + * faulting address in RTC backup registers (they survive the reset) + * and reboots. + * + * Post-mortem readout: BKP4R = 0xB2FE0000 | (CFSR & 0xFFFF), + * BKP5R = faulting address (MMFAR if valid, else stacked PC). + */ + +#if defined(DIAG_BANK2_FENCE) && !defined(BOOT) + +#include "stm32_hal.h" + +#define FENCE_REGION_NUMBER 7 +#define FENCE_BASE 0x08100000UL +#define FENCE_SIZE_1MB (19UL << MPU_RASR_SIZE_Pos) +#define FENCE_MAGIC 0xB2FE0000UL + +void bank2FenceEnable(void) +{ + __disable_irq(); + MPU->RNR = FENCE_REGION_NUMBER; + MPU->RBAR = FENCE_BASE; + MPU->RASR = MPU_RASR_ENABLE_Msk | FENCE_SIZE_1MB | MPU_RASR_XN_Msk; + /* AP = 0b000: no access, privileged included */ + MPU->CTRL |= MPU_CTRL_PRIVDEFENA_Msk | MPU_CTRL_ENABLE_Msk; + SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk; + __DSB(); + __ISB(); + __enable_irq(); +} + +void bank2FenceDisable(void) +{ + __disable_irq(); + MPU->RNR = FENCE_REGION_NUMBER; + MPU->RASR = 0; + MPU->RBAR = 0; + /* MPU left enabled with PRIVDEFENA: background map = default */ + __DSB(); + __ISB(); + __enable_irq(); +} + +void MemManage_C(uint32_t *frame) +{ + uint32_t cfsr = SCB->CFSR; + uint32_t addr = (cfsr & SCB_CFSR_MMARVALID_Msk) ? SCB->MMFAR : frame[6]; + + /* Backup domain write access (registers survive the reset) */ + __HAL_RCC_PWR_CLK_ENABLE(); + SET_BIT(PWR->CR, PWR_CR_DBP); + RTC->BKP4R = FENCE_MAGIC | (cfsr & 0xFFFF); + RTC->BKP5R = addr; + + NVIC_SystemReset(); +} + +__attribute__((naked)) void MemManage_Handler(void) +{ + __asm volatile( + "tst lr, #4 \n" + "ite eq \n" + "mrseq r0, msp \n" + "mrsne r0, psp \n" + "b MemManage_C \n"); +} + +/* A MemManage fault raised while the core is already at or above the + * MemManage priority escalates to HardFault: record it from the + * existing HardFault handler (cortex_m_isr.c) with a distinct magic + * (0xB2FA). */ +void bank2FenceRecordHardFault(uint32_t return_address) +{ + uint32_t cfsr = SCB->CFSR; + uint32_t addr = (cfsr & SCB_CFSR_MMARVALID_Msk) ? SCB->MMFAR + : (cfsr & SCB_CFSR_BFARVALID_Msk) ? SCB->BFAR + : return_address; + __HAL_RCC_PWR_CLK_ENABLE(); + SET_BIT(PWR->CR, PWR_CR_DBP); + RTC->BKP4R = 0xB2FA0000UL | (cfsr & 0xFFFF); + RTC->BKP5R = addr; + + NVIC_SystemReset(); +} + +/* Called once at boot when storage is up: if the previous reset was + * a fence trap, append it to /bank2fence.log and clear the marker. */ +#include "ff.h" + +void bank2FenceDumpPostMortem(void) +{ + uint32_t r4 = RTC->BKP4R; + uint32_t hi = r4 & 0xFFFF0000UL; + if (hi != 0xB2FE0000UL && hi != 0xB2FA0000UL) return; + + FIL f; + if (f_open(&f, "/bank2fence.log", FA_WRITE | FA_OPEN_APPEND) == FR_OK) { + f_printf(&f, "%s cfsr=%04X addr=%08lX\n", + (hi == 0xB2FE0000UL) ? "MEMMANAGE" : "HARDFAULT", + (unsigned)(r4 & 0xFFFF), (unsigned long)RTC->BKP5R); + f_close(&f); + } + + __HAL_RCC_PWR_CLK_ENABLE(); + SET_BIT(PWR->CR, PWR_CR_DBP); + RTC->BKP4R = 0; + RTC->BKP5R = 0; +} + +#endif /* DIAG_BANK2_FENCE && !BOOT */ diff --git a/radio/src/targets/common/arm/stm32/cortex_m_isr.c b/radio/src/targets/common/arm/stm32/cortex_m_isr.c index 667c2eba682..52bbf0c8cf8 100644 --- a/radio/src/targets/common/arm/stm32/cortex_m_isr.c +++ b/radio/src/targets/common/arm/stm32/cortex_m_isr.c @@ -73,6 +73,12 @@ typedef struct __attribute__((packed)) ContextStateFrame { __attribute__((optimize("O0"))) void hard_fault_handler_c(sContextStateFrame *frame) { HALT_IF_DEBUGGING(); +#if defined(DIAG_BANK2_FENCE) && !defined(BOOT) + // record escalated fence traps (bank 2 access from a high-priority + // context) and reboot instead of returning into the fault + extern void bank2FenceRecordHardFault(uint32_t return_address); + bank2FenceRecordHardFault(frame->return_address); +#endif } void HardFault_Handler(void) { diff --git a/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt b/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt index 72477d740bd..bfafcaa054d 100644 --- a/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt +++ b/radio/src/targets/common/arm/stm32/f4/CMakeLists.txt @@ -85,6 +85,7 @@ add_library(cmsis INTERFACE) target_sources(cmsis INTERFACE targets/common/arm/stm32/system_init.c + targets/common/arm/stm32/bank2_fence.c targets/common/arm/stm32/cortex_m_isr.c targets/common/arm/stm32/f4/system_clock.c targets/common/arm/stm32/f4/system_stm32f4xx.c diff --git a/radio/src/targets/common/arm/stm32/system_init.c b/radio/src/targets/common/arm/stm32/system_init.c index 72e530e69c9..f291b0161aa 100644 --- a/radio/src/targets/common/arm/stm32/system_init.c +++ b/radio/src/targets/common/arm/stm32/system_init.c @@ -71,6 +71,24 @@ NAKED BOOTSTRAP void Reset_Handler() "bl naked_copy \n" ); + // Copy hot interrupt-path code into RAM (no-op when the linker + // script does not define a .fastcode section) + asm inline ( + "ldr r0, =_sfastcode \n" + "ldr r1, =_efastcode \n" + "ldr r2, =_fastcode_load \n" + "bl naked_copy \n" + ); + + // Copy shared C++ comdat code (.text.shared); no-op on XIP + // targets where VMA == LMA + asm inline ( + "ldr r0, =_stext_shared \n" + "ldr r1, =_etext_shared \n" + "ldr r2, =_text_shared_load \n" + "bl naked_copy \n" + ); + #if defined(BOOT) && defined(REQUIRE_MPU_CONFIG) asm inline ( "bl MPU_Config \n" diff --git a/radio/src/targets/common/arm/stm32/usb_driver.cpp b/radio/src/targets/common/arm/stm32/usb_driver.cpp index 291f94c3dcf..c1d6ef31982 100644 --- a/radio/src/targets/common/arm/stm32/usb_driver.cpp +++ b/radio/src/targets/common/arm/stm32/usb_driver.cpp @@ -157,6 +157,11 @@ void usbInit() extern void usbInitLUNs(); extern USBD_HandleTypeDef hUsbDevice; extern "C" USBD_StorageTypeDef USBD_Storage_Interface_fops; +#if defined(DIAG_BANK2_FENCE) && !defined(BOOT) +extern "C" void bank2FenceEnable(void); +extern "C" void bank2FenceDisable(void); +#endif + extern USBD_CDC_ItfTypeDef USBD_Interface_fops; extern USBD_DFU_MediaTypeDef USBD_DFU_MEDIA_fops; @@ -206,11 +211,19 @@ void usbStart() if (USBD_Start(&hUsbDevice) == USBD_OK) { usbDriverStarted = true; +#if defined(DIAG_BANK2_FENCE) && !defined(BOOT) + // Validation builds: trap any flash bank 2 access for the whole + // USB session (PA12 errata, EdgeTX#5899) + bank2FenceEnable(); +#endif } } void usbStop() { +#if defined(DIAG_BANK2_FENCE) && !defined(BOOT) + bank2FenceDisable(); +#endif usbDriverStarted = false; USBD_DeInit(&hUsbDevice); } @@ -260,6 +273,12 @@ void usbJoystickRestart() */ void usbJoystickUpdate() { + // Do not touch the report buffer while the previous transfer is + // still in flight (DMA reads it), and do not hammer the endpoint + // at mixer rate when the host has not polled yet (EdgeTX#5899) + if (!USBD_HID_IsReady(&hUsbDevice)) + return; + #if !defined(USBJ_EX) static uint8_t HID_Buffer[HID_IN_PACKET]; diff --git a/radio/src/targets/common/arm/stm32/usbd_hid_joystick.c b/radio/src/targets/common/arm/stm32/usbd_hid_joystick.c index 2ef49a40f61..80b427227be 100644 --- a/radio/src/targets/common/arm/stm32/usbd_hid_joystick.c +++ b/radio/src/targets/common/arm/stm32/usbd_hid_joystick.c @@ -520,6 +520,22 @@ uint8_t USBD_HID_SendReport(USBD_HandleTypeDef *pdev, uint8_t *report, uint16_t return (uint8_t)USBD_OK; } +/** + * @brief USBD_HID_IsReady + * Check whether the HID IN endpoint is idle, i.e. the + * previous report has been fully transmitted and the report + * buffer may be safely rewritten + * @param pdev: device instance + * @retval 1 if ready, 0 if busy or not initialized + */ +uint8_t USBD_HID_IsReady(USBD_HandleTypeDef *pdev) +{ + USBD_HID_HandleTypeDef *hhid = + (USBD_HID_HandleTypeDef *)pdev->pClassDataCmsit[pdev->classId]; + if (hhid == NULL) return 0; + return (hhid->state == USBD_HID_IDLE) ? 1 : 0; +} + /** * @brief USBD_HID_GetPollingInterval * return polling interval from endpoint descriptor diff --git a/radio/src/targets/horus/CMakeLists.txt b/radio/src/targets/horus/CMakeLists.txt index 80c04f93a8f..8082b921179 100644 --- a/radio/src/targets/horus/CMakeLists.txt +++ b/radio/src/targets/horus/CMakeLists.txt @@ -1,4 +1,5 @@ option(DISK_CACHE "Enable SD card disk cache" ON) +option(DIAG_BANK2_FENCE "MPU fence trapping any flash bank 2 access during USB (validation builds, EdgeTX#5899)" OFF) option(UNEXPECTED_SHUTDOWN "Enable the Unexpected Shutdown screen" ON) option(IMU_LSM6DS33 "Enable I2C2 and LSM6DS33 IMU" OFF) option(PXX1 "PXX1 protocol support" ON) @@ -221,6 +222,10 @@ if(DISK_CACHE) set(SRC ${SRC} disk_cache.cpp) endif() +if(DIAG_BANK2_FENCE) + add_definitions(-DDIAG_BANK2_FENCE) +endif() + if(INTERNAL_GPS) message("-- Internal GPS enabled") add_definitions(-DINTERNAL_GPS) diff --git a/radio/src/thirdparty/STM32_USB_Device_Library/Class/HID/Inc/usbd_hid.h b/radio/src/thirdparty/STM32_USB_Device_Library/Class/HID/Inc/usbd_hid.h index 8b22a8df9f3..758ec5fea05 100644 --- a/radio/src/thirdparty/STM32_USB_Device_Library/Class/HID/Inc/usbd_hid.h +++ b/radio/src/thirdparty/STM32_USB_Device_Library/Class/HID/Inc/usbd_hid.h @@ -140,6 +140,7 @@ uint8_t USBD_HID_SendReport(USBD_HandleTypeDef *pdev, uint8_t *report, uint16_t uint8_t USBD_HID_SendReport(USBD_HandleTypeDef *pdev, uint8_t *report, uint16_t len); #endif /* USE_USBD_COMPOSITE */ uint32_t USBD_HID_GetPollingInterval(USBD_HandleTypeDef *pdev); +uint8_t USBD_HID_IsReady(USBD_HandleTypeDef *pdev); /** * @} diff --git a/tools/check_bank1.sh b/tools/check_bank1.sh new file mode 100755 index 00000000000..0f5ca0c02be --- /dev/null +++ b/tools/check_bank1.sh @@ -0,0 +1,166 @@ +#!/bin/bash +# Verification post-link du layout "code en bank 1" (TX16S, EdgeTX#5899, +# errata ST ES0166 §2.4.6 : bruit PA12/USB-D+ -> lectures corrompues en +# flash bank 2 pendant une session USB). +# +# Garanties verifiees : +# 1. aucune section executable au-dessus de 0x08100000, sauf .uicode +# (UI gelee pendant l'USB) ; le depassement de la bank 1 est deja +# une erreur de link (region FLASH = 1024K) +# 2. le code chaud USB est bien en RAM (.fastcode) +# 3. aucun chemin d'appel DIRECT depuis un contexte actif pendant +# l'USB (ISR, mixer, audio, timers telemetrie/logs, tete de +# perMain) vers la bank 2 — hors aretes gardees a l'execution +# (liste blanche ci-dessous) +# +# Limites connues : les appels indirects (pointeurs de fonction) ne +# sont pas suivis ; la couverture est completee a l'execution par la +# barriere MPU du build de validation (DIAG_BANK2_FENCE). +# +# Usage: tools/check_bank1.sh [chemin/firmware.elf] + +set -e +ELF=${1:-"$(dirname "$0")/../build-bank1/arm-none-eabi/firmware.elf"} +[ -f "$ELF" ] || { echo "ELF introuvable: $ELF"; exit 1; } + +echo "== 1. sections executables ==" +BAD_SECTIONS=$(arm-none-eabi-objdump -h "$ELF" | awk ' + /CODE/ { print prev } + { if ($2 ~ /^\./) prev = $2" "$4 }' | while read name vma; do + case "$name" in .uicode) continue ;; esac + v=$((16#$vma)) + if [ $v -ge $((0x08100000)) ] && [ $v -lt $((0x08200000)) ]; then + echo "$name@0x$vma" + fi + done) +if [ -n "$BAD_SECTIONS" ]; then + echo "ERREUR: sections code en bank 2: $BAD_SECTIONS"; exit 1 +fi +echo "OK (seule .uicode est en bank 2)" + +echo "== 2. code USB en RAM ==" +ADDR=$(arm-none-eabi-nm "$ELF" | awk '/ T OTG_FS_IRQHandler/ {print $1}') +case "$ADDR" in + 2000*) echo "OK: OTG_FS_IRQHandler @ 0x$ADDR" ;; + *) echo "ERREUR: OTG_FS_IRQHandler @ 0x$ADDR (attendu 0x2000xxxx)"; exit 1 ;; +esac + +echo "== 3. joignabilite bank 2 depuis les contextes actifs pendant l'USB ==" +DISAS=$(mktemp) +trap 'rm -f "$DISAS"' EXIT +arm-none-eabi-objdump -d "$ELF" > "$DISAS" +python3 - "$DISAS" <<'PYEOF' +import sys, re, collections +sys.stdin = open(sys.argv[1]) + +BANK2 = range(0x08100000, 0x08200000) + +# Racines : tout ce qui peut s'executer pendant une session USB active. +# - toutes les ISR (suffixe _IRQHandler / Handler du vecteur) +# - taches et timers qui ne sont pas geles par le gel UI +# - la tete de perMain (avant le retour anticipe du gel) +ROOT_PATTERNS = [ + r'.*_IRQHandler$', r'^HardFault_Handler$', r'^SVC_Handler$', + r'^PendSV_Handler$', r'^SysTick_Handler$', + r'.*mixerTask.*', r'.*audioTask.*', r'^telemetryTimerCb$', + r'.*logsTimerCb.*', r'^_Z9logsWritev$', r'^_Z15telemetryWakeupv$', + r'^_Z13evalFunctions.*', + # menusTask tourne pendant le gel (seul perMain est tronque) et + # appelle pwrCheck a chaque cycle de 50 ms + r'.*menusTask.*', r'^pwrCheck$', + # timers FreeRTOS (indirects via la tache timer, non vus par le + # graphe d'appels directs) : chaque callback est une racine + r'.*_timer_10ms_cb.*', r'.*per10msv?$', r'.*loggingTimerCb.*', + r'.*spacemouseTimerCb.*', r'.*MultiRfProtocols7timerCb.*', + r'^_ZL11_refresh_cb.*', # rgb_leds uniquement + # tete de perMain : ce qui s'execute AVANT le retour anticipe du + # gel USB (perMain lui-meme est tronque a l'execution) + r'^_Z17checkSpeakerVolumev$', r'^_Z16initLoggingTimerv$', + r'^_Z20checkTrainerSettingsv$', r'^_Z12periodicTickv$', + r'^_Z14checkBacklightv$', r'^_Z11flightResetv$', +] + +# Fonctions gardees a l'execution : ne s'executent jamais pendant une +# session USB active (etat USB teste en entree, ou garde +# _usb_ui_frozen) -> sous-arbre entier elague +PRUNED = { + '_Z19handleUsbConnectionv', # demarre/arrete l'USB : UI appelee + # uniquement avant usbStart/apres usbStop + '_Z11closeUsbMenuv', # uniquement sur debranchement + '_Z13POPUP_WARNINGPKcS0_', # garde _usb_ui_frozen() + '_Z17POPUP_INFORMATIONPKc', # garde _usb_ui_frozen() + '_Z18checkStorageUpdatev', # garde !usbPlugged dans perMain + '_Z17usbSessionCleanupv', # commence par usbStop() : la bank 2 + '_Z19usbSessionForceStopv', # redevient sure (PA12 au repos) + '_Z10edgeTxInitv', # boot uniquement, avant toute session USB + '_Z7perMainv', # gel : retour anticipe avant toute UI ; + # sa tete est auditee par racines dediees + '_Z11edgeTxCloseh', # extinction (toujours apres + # usbSessionForceStop) ou pre-usbStart (MSC) +} + +# Aretes tolerees individuellement : gardees a l'execution, mais dont +# l'appelant doit rester audite pour le reste de son sous-arbre +WHITELIST_EDGES = { + # pwrCheck coupe la session USB (usbSessionForceStop) avant tout + # flux d'extinction, et saute l'animation pendant le gel + ('pwrCheck', '_Z21drawShutdownAnimationmmPKc'), + ('pwrCheck', '_Z18confirmationDialogPKcS0_bRKSt8functionIFbvEE'), + # sortie de menusTask = power off, toujours apres usbSessionForceStop + ('_ZL9menusTaskv', '_Z15drawSleepBitmapv'), + # initialisation LVGL au demarrage de la tache, avant tout USB + ('_ZL9menusTaskv', '_ZN11LvglWrapper8instanceEv'), + # ecran d'urgence au boot (UNEXPECTED_SHUTDOWN), avant tout USB + ('_ZL9menusTaskv', '_Z20drawFatalErrorScreenPKc'), +} + +funcs = {} # nom -> addr +calls = collections.defaultdict(set) # caller -> {(callee, addr)} +cur = None +for line in sys.stdin: + m = re.match(r'^([0-9a-f]+) <(.+)>:', line) + if m: + cur = m.group(2); funcs[cur] = int(m.group(1), 16); continue + if cur is None: continue + m = re.search(r'\t(?:bl|b\.w|b\.n|b|beq\.w|bne\.w|bcs\.w|bcc\.w|bmi\.w|bpl\.w|bvs\.w|bvc\.w|bhi\.w|bls\.w|bge\.w|blt\.w|bgt\.w|ble\.w|cbz|cbnz)\s+([0-9a-f]+) <([^>+]+)(?:\+0x[0-9a-f]+)?>', line) + if m: + calls[cur].add((m.group(2), int(m.group(1), 16))) + +# une racine doit vivre en bank 1 : un symbole apparie en bank 2 est +# du code UI (gele pendant l'USB), pas un contexte chaud +roots = [f for f in funcs + if any(re.match(p, f) for p in ROOT_PATTERNS) + and funcs[f] not in BANK2] + +# BFS sur les appels directs, en restant en bank 1 ; une arete vers la +# bank 2 depuis un appelant non liste = violation +violations = [] +seen = set(roots) +parent = {r: None for r in roots} +queue = list(roots) +while queue: + f = queue.pop() + if f in PRUNED: continue + for callee, addr in calls.get(f, ()): + if addr in BANK2: + if (f, callee) in WHITELIST_EDGES: continue + chain = [] + n = f + while n is not None: + chain.append(n); n = parent.get(n) + violations.append((' -> '.join(reversed(chain)), callee)) + continue + if callee not in seen and callee in funcs: + seen.add(callee); parent[callee] = f + queue.append(callee) + +if violations: + print(f"ERREUR: {len(violations)} chemin(s) direct(s) vers la bank 2 :") + for chain, callee in sorted(set(violations))[:40]: + print(f" {chain} ==> {callee}") + sys.exit(1) +print(f"OK ({len(roots)} racines, {len(seen)} fonctions parcourues, " + f"aucun chemin direct vers la bank 2)") +PYEOF + +echo "== verification bank 1 : OK ==" diff --git a/tools/test_invariance.sh b/tools/test_invariance.sh new file mode 100644 index 00000000000..848dac05e9c --- /dev/null +++ b/tools/test_invariance.sh @@ -0,0 +1,77 @@ +#!/bin/bash +# Preuve d'invariance du layout "code en bank 1" (TX16S, EdgeTX#5899) : +# la garantie doit venir du linker script, pas d'un placement chanceux. +# +# Deux builds volontairement perturbes : +# 1. +8K de rodata et une fonction dans un fichier CORE (decale tout +# le contenu de la bank 1) +# 2. +8K de rodata et une fonction dans un fichier UI, plus +# inversion de l'ordre de link de deux sources UI (decale la +# bank 2 et change la survie des comdats) +# +# Chaque build doit passer tools/check_bank1.sh. Les perturbations ne +# sont jamais commitees (restauration git a la fin, arbre propre exige). +# +# Usage: bench/test_invariance.sh + +set -e +cd "$(dirname "$0")/.." + +if ! git diff --quiet radio/src; then + echo "ERREUR: modifications non commitees dans radio/src, abandon"; exit 1 +fi + +cleanup() { + git checkout -- radio/src/gps.cpp radio/src/gui/colorlcd/mainview/view_main.cpp \ + radio/src/gui/colorlcd/CMakeLists.txt 2>/dev/null || true +} +trap cleanup EXIT + +build_and_check() { + local label=$1 + echo "=== build perturbe: $label ===" + docker run --rm -u $(id -u):$(id -g) -v "$(pwd)":/src -w /src \ + ghcr.io/edgetx/edgetx-dev:latest bash -c " +set -e +rm -rf build-invar && mkdir build-invar && cd build-invar +cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/toolchain/arm-none-eabi.cmake \ + -DPCB=X10 -DPCBREV=TX16S -DCMAKE_BUILD_TYPE=Release .. > cmake.log 2>&1 +make firmware -j\$(nproc) > make.log 2>&1 || { tail -20 make.log; exit 1; } +" + tools/check_bank1.sh build-invar/arm-none-eabi/firmware.elf + echo "=== $label: OK ===" +} + +# --- perturbation 1: core --- +cat >> radio/src/gps.cpp <<'PAD' + +// (test d'invariance bank 1 -- jamais commite) +const volatile char _invar_pad_core[8192] = {1}; +void _invar_dummy_core() { (void)_invar_pad_core[0]; } +PAD +build_and_check "padding core (+8K bank 1)" +cleanup + +# --- perturbation 2: UI + ordre de link --- +cat >> radio/src/gui/colorlcd/mainview/view_main.cpp <<'PAD' + +// (test d'invariance bank 1 -- jamais commite) +const volatile char _invar_pad_ui[8192] = {2}; +void _invar_dummy_ui() { (void)_invar_pad_ui[0]; } +PAD +python3 - <<'PYEOF' +import re +p = 'radio/src/gui/colorlcd/CMakeLists.txt' +lines = open(p).readlines() +# inverse les deux premieres sources .cpp adjacentes trouvees +for i in range(len(lines) - 1): + if lines[i].strip().endswith('.cpp') and lines[i+1].strip().endswith('.cpp'): + lines[i], lines[i+1] = lines[i+1], lines[i] + print(f"swap: {lines[i].strip()} <-> {lines[i+1].strip()}") + break +open(p, 'w').writelines(lines) +PYEOF +build_and_check "padding UI (+8K bank 2) + ordre de link inverse" + +echo +echo "== invariance: les 2 builds perturbes passent la verification =="