diff --git a/firmware/application/CMakeLists.txt b/firmware/application/CMakeLists.txt index dc642cfbb..2372f1dbd 100644 --- a/firmware/application/CMakeLists.txt +++ b/firmware/application/CMakeLists.txt @@ -493,7 +493,10 @@ add_executable(${PROJECT_NAME}.elf ${CSRC} ${CPPSRC} ${ASMSRC}) target_link_options(${PROJECT_NAME}.elf PRIVATE "LINKER:--defsym,LD_FLASH_SIZE=${FLASH_BYTES_LIMIT_SIZE}" ) -set_target_properties(${PROJECT_NAME}.elf PROPERTIES LINK_DEPENDS ${LDSCRIPT}) +# NB: LPC43xx_M0.ld does INCLUDE external.ld, so that one has to be listed too - +# otherwise editing the external app section rules silently does not relink. +set_target_properties(${PROJECT_NAME}.elf PROPERTIES + LINK_DEPENDS "${LDSCRIPT};${CMAKE_CURRENT_SOURCE_DIR}/external/external.ld") add_definitions(${DEFS}) include_directories(. ${INCDIR}) link_directories(${LLIBDIR}) diff --git a/firmware/application/external/external.ld b/firmware/application/external/external.ld index 6fc27a33a..d745d1da6 100644 --- a/firmware/application/external/external.ld +++ b/firmware/application/external/external.ld @@ -122,553 +122,553 @@ SECTIONS .external_app_afsk_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_afsk_rx.application_information)); - *(*ui*external_app*afsk_rx*); + */external/afsk_rx/*(*ui*external_app*afsk_rx*); } > ram_external_app_afsk_rx .external_app_calculator : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_calculator.application_information)); - *(*ui*external_app*calculator*); + */external/calculator/*(*ui*external_app*calculator*); } > ram_external_app_calculator .external_app_font_viewer : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_font_viewer.application_information)); - *(*ui*external_app*font_viewer*); + */external/font_viewer/*(*ui*external_app*font_viewer*); } > ram_external_app_font_viewer .external_app_blespam : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_blespam.application_information)); - *(*ui*external_app*blespam*); + */external/blespam/*(*ui*external_app*blespam*); } > ram_external_app_blespam .external_app_analogtv : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_analogtv.application_information)); - *(*ui*external_app*analogtv*); + */external/analogtv/*(*ui*external_app*analogtv*); } > ram_external_app_analogtv .external_app_nrf_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_nrf_rx.application_information)); - *(*ui*external_app*nrf_rx*); + */external/nrf_rx/*(*ui*external_app*nrf_rx*); } > ram_external_app_nrf_rx .external_app_coasterp : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_coasterp.application_information)); - *(*ui*external_app*coasterp*); + */external/coasterp/*(*ui*external_app*coasterp*); } > ram_external_app_coasterp .external_app_lge : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_lge.application_information)); - *(*ui*external_app*lge*); + */external/lge/*(*ui*external_app*lge*); } > ram_external_app_lge .external_app_lcr : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_lcr.application_information)); - *(*ui*external_app*lcr*); + */external/lcr/*(*ui*external_app*lcr*); } > ram_external_app_lcr .external_app_jammer : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_jammer.application_information)); - *(*ui*external_app*jammer*); + */external/jammer/*(*ui*external_app*jammer*); } > ram_external_app_jammer .external_app_gpssim : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_gpssim.application_information)); - *(*ui*external_app*gpssim*); + */external/gpssim/*(*ui*external_app*gpssim*); } > ram_external_app_gpssim .external_app_spainter : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_spainter.application_information)); - *(*ui*external_app*spainter*); + */external/spainter/*(*ui*external_app*spainter*); } > ram_external_app_spainter .external_app_keyfob : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_keyfob.application_information)); - *(*ui*external_app*keyfob*); + */external/keyfob/*(*ui*external_app*keyfob*); } > ram_external_app_keyfob .external_app_tetris : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_tetris.application_information)); - *(*ui*external_app*tetris*); + */external/tetris/*(*ui*external_app*tetris*); } > ram_external_app_tetris .external_app_extsensors : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_extsensors.application_information)); - *(*ui*external_app*extsensors*); + */external/extsensors/*(*ui*external_app*extsensors*); } > ram_external_app_extsensors .external_app_foxhunt_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_foxhunt_rx.application_information)); - *(*ui*external_app*foxhunt_rx*); + */external/foxhunt/*(*ui*external_app*foxhunt_rx*); } > ram_external_app_foxhunt_rx .external_app_audio_test : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_audio_test.application_information)); - *(*ui*external_app*audio_test*); + */external/audio_test/*(*ui*external_app*audio_test*); } > ram_external_app_audio_test .external_app_wardrivemap : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_wardrivemap.application_information)); - *(*ui*external_app*wardrivemap*); + */external/wardrivemap/*(*ui*external_app*wardrivemap*); } > ram_external_app_wardrivemap .external_app_tpmsrx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_tpmsrx.application_information)); - *(*ui*external_app*tpmsrx*); + */external/tpmsrx/*(*ui*external_app*tpmsrx*); } > ram_external_app_tpmsrx .external_app_tpmstx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_tpmstx.application_information)); - *(*ui*external_app*tpmstx*); + */external/tpmstx/*(*ui*external_app*tpmstx*); } > ram_external_app_tpmstx .external_app_protoview : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_protoview.application_information)); - *(*ui*external_app*protoview*); + */external/protoview/*(*ui*external_app*protoview*); } > ram_external_app_protoview .external_app_adsbtx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_adsbtx.application_information)); - *(*ui*external_app*adsbtx*); + */external/adsbtx/*(*ui*external_app*adsbtx*); } > ram_external_app_adsbtx .external_app_morse_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_morse_tx.application_information)); - *(*ui*external_app*morse_tx*); + */external/morse_tx/*(*ui*external_app*morse_tx*); } > ram_external_app_morse_tx .external_app_sstvtx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_sstvtx.application_information)); - *(*ui*external_app*sstvtx*); + */external/sstvtx/*(*ui*external_app*sstvtx*); } > ram_external_app_sstvtx .external_app_random_password : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_random_password.application_information)); - *(*ui*external_app*random_password*); + */external/random_password/*(*ui*external_app*random_password*); } > ram_external_app_random_password .external_app_acars_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_acars_rx.application_information)); - *(*ui*external_app*acars_rx*); + */external/acars_rx/*(*ui*external_app*acars_rx*); } > ram_external_app_acars_rx .external_app_shoppingcart_lock : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_shoppingcart_lock.application_information)); - *(*ui*external_app*shoppingcart_lock*); + */external/shoppingcart_lock/*(*ui*external_app*shoppingcart_lock*); } > ram_external_app_shoppingcart_lock .external_app_cvs_spam : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_cvs_spam.application_information)); - *(*ui*external_app*cvs_spam*); + */external/cvs_spam/*(*ui*external_app*cvs_spam*); } > ram_external_app_cvs_spam .external_app_ookbrute : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_ookbrute.application_information)); - *(*ui*external_app*ookbrute*); + */external/ookbrute/*(*ui*external_app*ookbrute*); } > ram_external_app_ookbrute .external_app_ook_editor : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_ook_editor.application_information)); - *(*ui*external_app*ook_editor*); + */external/ook_editor/*(*ui*external_app*ook_editor*); } > ram_external_app_ook_editor .external_app_flippertx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_flippertx.application_information)); - *(*ui*external_app*flippertx*); + */external/flippertx/*(*ui*external_app*flippertx*); } > ram_external_app_flippertx .external_app_remote : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_remote.application_information)); - *(*ui*external_app*remote*); + */external/remote/*(*ui*external_app*remote*); } > ram_external_app_remote .external_app_mcu_temperature : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_mcu_temperature.application_information)); - *(*ui*external_app*mcu_temperature*); + */external/mcu_temperature/*(*ui*external_app*mcu_temperature*); } > ram_external_app_mcu_temperature .external_app_fmradio : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_fmradio.application_information)); - *(*ui*external_app*fmradio*); + */external/fmradio/*(*ui*external_app*fmradio*); } > ram_external_app_fmradio .external_app_tuner : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_tuner.application_information)); - *(*ui*external_app*tuner*); + */external/tuner/*(*ui*external_app*tuner*); } > ram_external_app_tuner .external_app_metronome : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_metronome.application_information)); - *(*ui*external_app*metronome*); + */external/metronome/*(*ui*external_app*metronome*); } > ram_external_app_metronome .external_app_app_manager : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_app_manager.application_information)); - *(*ui*external_app*app_manager*); + */external/app_manager/*(*ui*external_app*app_manager*); } > ram_external_app_app_manager .external_app_hopper : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_hopper.application_information)); - *(*ui*external_app*hopper*); + */external/hopper/*(*ui*external_app*hopper*); } > ram_external_app_hopper .external_app_antenna_length : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_antenna_length.application_information)); - *(*ui*external_app*antenna_length*); + */external/antenna_length/*(*ui*external_app*antenna_length*); } > ram_external_app_antenna_length .external_app_view_wav : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_view_wav.application_information)); - *(*ui*external_app*view_wav*); + */external/wav_view/*(*ui*external_app*view_wav*); } > ram_external_app_view_wav .external_app_sd_wipe : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_sd_wipe.application_information)); - *(*ui*external_app*sd_wipe*); + */external/sd_wipe/*(*ui*external_app*sd_wipe*); } > ram_external_app_sd_wipe .external_app_playlist_editor : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_playlist_editor.application_information)); - *(*ui*external_app*playlist_editor*); + */external/playlist_editor/*(*ui*external_app*playlist_editor*); } > ram_external_app_playlist_editor .external_app_snake : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_snake.application_information)); - *(*ui*external_app*snake*); + */external/snake/*(*ui*external_app*snake*); } > ram_external_app_snake .external_app_stopwatch : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_stopwatch.application_information)); - *(*ui*external_app*stopwatch*); + */external/stopwatch/*(*ui*external_app*stopwatch*); } > ram_external_app_stopwatch .external_app_wefax_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_wefax_rx.application_information)); - *(*ui*external_app*wefax_rx*); + */external/wefax_rx/*(*ui*external_app*wefax_rx*); } > ram_external_app_wefax_rx .external_app_noaaapt_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_noaaapt_rx.application_information)); - *(*ui*external_app*noaaapt_rx*); + */external/noaaapt_rx/*(*ui*external_app*noaaapt_rx*); } > ram_external_app_noaaapt_rx .external_app_vor_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_vor_rx.application_information)); - *(*ui*external_app*vor_rx*); + */external/vor_rx/*(*ui*external_app*vor_rx*); } > ram_external_app_vor_rx .external_app_vor_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_vor_tx.application_information)); - *(*ui*external_app*vor_tx*); + */external/vor_tx/*(*ui*external_app*vor_tx*); } > ram_external_app_vor_tx .external_app_breakout : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_breakout.application_information)); - *(*ui*external_app*breakout*); + */external/breakout/*(*ui*external_app*breakout*); } > ram_external_app_breakout .external_app_doom : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_doom.application_information)); - *(*ui*external_app*doom*); + */external/doom/*(*ui*external_app*doom*); } > ram_external_app_doom .external_app_debug_pmem : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_debug_pmem.application_information)); - *(*ui*external_app*debug_pmem*); + */external/debug_pmem/*(*ui*external_app*debug_pmem*); } > ram_external_app_debug_pmem .external_app_scanner : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_scanner.application_information)); - *(*ui*external_app*scanner*); + */external/scanner/*(*ui*external_app*scanner*); } > ram_external_app_scanner .external_app_level : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_level.application_information)); - *(*ui*external_app*level*); + */external/level/*(*ui*external_app*level*); } > ram_external_app_level .external_app_gfxeq : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_gfxeq.application_information)); - *(*ui*external_app*gfxeq*); + */external/gfxeq/*(*ui*external_app*gfxeq*); } > ram_external_app_gfxeq .external_app_waterfall_designer : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_waterfall_designer.application_information)); - *(*ui*external_app*waterfall_designer*); + */external/waterfall_designer/*(*ui*external_app*waterfall_designer*); } > ram_external_app_waterfall_designer .external_app_detector_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_detector_rx.application_information)); - *(*ui*external_app*detector_rx*); + */external/detector_rx/*(*ui*external_app*detector_rx*); } > ram_external_app_detector_rx .external_app_dinogame : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_dinogame.application_information)); - *(*ui*external_app*dinogame*); + */external/dinogame/*(*ui*external_app*dinogame*); } > ram_external_app_dinogame .external_app_spaceinv : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_spaceinv.application_information)); - *(*ui*external_app*spaceinv*); + */external/spaceinv/*(*ui*external_app*spaceinv*); } > ram_external_app_spaceinv .external_app_blackjack : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_blackjack.application_information)); - *(*ui*external_app*blackjack*); + */external/blackjack/*(*ui*external_app*blackjack*); } > ram_external_app_blackjack .external_app_battleship : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_battleship.application_information)); - *(*ui*external_app*battleship*); + */external/battleship/*(*ui*external_app*battleship*); } > ram_external_app_battleship .external_app_ert : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_ert.application_information)); - *(*ui*external_app*ert*); + */external/ert/*(*ui*external_app*ert*); } > ram_external_app_ert .external_app_epirb_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_epirb_rx.application_information)); - *(*ui*external_app*epirb_rx*); + */external/epirb_rx/*(*ui*external_app*epirb_rx*); } > ram_external_app_epirb_rx .external_app_soundboard : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_soundboard.application_information)); - *(*ui*external_app*soundboard*); + */external/soundboard/*(*ui*external_app*soundboard*); } > ram_external_app_soundboard .external_app_game2048 : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_game2048.application_information)); - *(*ui*external_app*game2048*); + */external/game2048/*(*ui*external_app*game2048*); } > ram_external_app_game2048 .external_app_bht_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_bht_tx.application_information)); - *(*ui*external_app*bht_tx*); + */external/bht_tx/*(*ui*external_app*bht_tx*); } > ram_external_app_bht_tx .external_app_morse_practice : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_morse_practice.application_information)); - *(*ui*external_app*morse_practice*); + */external/morse_practice/*(*ui*external_app*morse_practice*); } > ram_external_app_morse_practice .external_app_adult_toys_controller : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_adult_toys_controller.application_information)); - *(*ui*external_app*adult_toys_controller*); + */external/adult_toys_controller/*(*ui*external_app*adult_toys_controller*); } > ram_external_app_adult_toys_controller .external_app_flex_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_flex_rx.application_information)); - *(*ui*external_app*flex_rx*); + */external/flex_rx/*(*ui*external_app*flex_rx*); } > ram_external_app_flex_rx .external_app_sstvrx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_sstvrx.application_information)); - *(*ui*external_app*sstvrx*); + */external/sstvrx/*(*ui*external_app*sstvrx*); } > ram_external_app_sstvrx .external_app_subcarrx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_subcarrx.application_information)); - *(*ui*external_app*subcarrx*); + */external/subcarrx/*(*ui*external_app*subcarrx*); } > ram_external_app_subcarrx .external_app_siggen : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_siggen.application_information)); - *(*ui*external_app*siggen*); + */external/siggen/*(*ui*external_app*siggen*); } > ram_external_app_siggen .external_app_sdusb : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_sdusb.application_information)); - *(*ui*external_app*sdusb*); + */external/sdusb/*(*ui*external_app*sdusb*); } > ram_external_app_sdusb .external_app_morse_radio : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_morse_radio.application_information)); - *(*ui*external_app*morse_radio*); + */external/morse_radio/*(*ui*external_app*morse_radio*); } > ram_external_app_morse_radio .external_app_morseradiotx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_morseradiotx.application_information)); - *(*ui*external_app*morseradiotx*); + */external/morseradiotx/*(*ui*external_app*morseradiotx*); } > ram_external_app_morseradiotx .external_app_keeloqtx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_keeloqtx.application_information)); - *(*ui*external_app*keeloqtx*); + */external/keeloqtx/*(*ui*external_app*keeloqtx*); } > ram_external_app_keeloqtx .external_app_rtty_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_rtty_rx.application_information)); - *(*ui*external_app*rtty_rx*); + */external/rtty_rx/*(*ui*external_app*rtty_rx*); } > ram_external_app_rtty_rx .external_app_rtty_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_rtty_tx.application_information)); - *(*ui*external_app*rtty_tx*); + */external/rtty_tx/*(*ui*external_app*rtty_tx*); } > ram_external_app_rtty_tx .external_app_pocsag_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_pocsag_tx.application_information)); - *(*ui*external_app*pocsag_tx*); + */external/pocsag_tx/*(*ui*external_app*pocsag_tx*); } > ram_external_app_pocsag_tx .external_app_time_sink : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_time_sink.application_information)); - *(*ui*external_app*time_sink*); + */external/time_sink/*(*ui*external_app*time_sink*); } > ram_external_app_time_sink .external_app_same_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_same_tx.application_information)); - *(*ui*external_app*same_tx*); + */external/same_tx/*(*ui*external_app*same_tx*); } > ram_external_app_same_tx .external_app_kiss_tnc : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_kiss_tnc.application_information)); - *(*ui*external_app*kiss_tnc*); + */external/kiss_tnc/*(*ui*external_app*kiss_tnc*); } > ram_external_app_kiss_tnc .external_app_mdc_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_mdc_tx.application_information)); - *(*ui*external_app*mdc_tx*); + */external/mdc_tx/*(*ui*external_app*mdc_tx*); } > ram_external_app_mdc_tx .external_app_epirb_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_epirb_tx.application_information)); - *(*ui*external_app*epirb_tx*); + */external/epirb_tx/*(*ui*external_app*epirb_tx*); } > ram_external_app_epirb_tx .external_app_fpv_detect : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_fpv_detect.application_information)); - *(*ui*external_app*fpv_detect*); + */external/fpv_detect/*(*ui*external_app*fpv_detect*); } > ram_external_app_fpv_detect .external_app_p25_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_p25_tx.application_information)); - *(*ui*external_app*p25_tx*); + */external/p25_tx/*(*ui*external_app*p25_tx*); } > ram_external_app_p25_tx .external_app_two_tone_pager : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_two_tone_pager.application_information)); - *(*ui*external_app*two_tone_pager*); + */external/two_tone_pager/*(*ui*external_app*two_tone_pager*); } > ram_external_app_two_tone_pager .external_app_two_tone_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_two_tone_rx.application_information)); - *(*ui*external_app*two_tone_rx*); + */external/two_tone_rx/*(*ui*external_app*two_tone_rx*); } > ram_external_app_two_tone_rx .external_app_flex_tx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_flex_tx.application_information)); - *(*ui*external_app*flex_tx*); + */external/flex_tx/*(*ui*external_app*flex_tx*); } > ram_external_app_flex_tx .external_app_hard_reset : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_hard_reset.application_information)); - *(*ui*external_app*hard_reset*); + */external/hard_reset/*(*ui*external_app*hard_reset*); } > ram_external_app_hard_reset .external_app_secplustx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_secplustx.application_information)); - *(*ui*external_app*secplustx*); + */external/secplustx/*(*ui*external_app*secplustx*); } > ram_external_app_secplustx .external_app_signal_hunter : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_signal_hunter.application_information)); - *(*ui*external_app*signal_hunter*); + */external/signal_hunter/*(*ui*external_app*signal_hunter*); } > ram_external_app_signal_hunter .external_app_tetra_rx : ALIGN(4) SUBALIGN(4) { KEEP(*(.external_app.app_tetra_rx.application_information)); - *(*ui*external_app*tetra_rx*); + */external/tetra_rx/*(*ui*external_app*tetra_rx*); } > ram_external_app_tetra_rx diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp index f07272cce..34916ac44 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.cpp @@ -28,11 +28,47 @@ #include "ui_fileman.hpp" #include "file_reader.hpp" #include "ui_textentry.hpp" +#include "convert.hpp" + +#include using namespace portapack; namespace ui::external_app::waterfall_designer { +namespace { + +/* Parses an "index,R,G,B" profile line. + * NB: std::stoi() must not be used here - the firmware is built with + * -fno-exceptions, so any malformed field in a hand-edited profile makes it + * call std::terminate() instead of throwing. parse_int() is what gradient.cpp + * uses to read the very same file format. */ +bool parse_level(std::string_view line, uint8_t& index, uint8_t& r, uint8_t& g, uint8_t& b) { + // Comments are skipped by Gradient::load_file() too; parse_int() ignores + // trailing junk, so "#0,0,0,0" would otherwise read back as a real level. + if (line.empty() || line.front() == '#') return false; + + auto cols = split_string(line, ','); + if (cols.size() != 4) return false; + + uint8_t* const out[4] = {&index, &r, &g, &b}; + + for (size_t i = 0; i < 4; i++) { + int32_t value; + if (!parse_int(cols[i], value) || value < 0 || value > 255) return false; + *out[i] = static_cast(value); + } + + return true; +} + +bool is_color_level(const std::string& line) { + uint8_t index, r, g, b; + return parse_level(line, index, r, g, b); +} + +} // namespace + WaterfallDesignerView::WaterfallDesignerView(NavigationView& nav) : nav_{nav} { baseband::run_prepared_image(portapack::memory::map::m4_code.base()); @@ -152,7 +188,11 @@ WaterfallDesignerView::WaterfallDesignerView( } WaterfallDesignerView::~WaterfallDesignerView() { - if (!if_apply_setting) restore_current_profile(); + if (!if_apply_setting) + restore_current_profile(); + else if (profile_backed_up_) + delete_file(waterfalls_dir / u"wtf_des_bk.bk"); // kept the edit, drop the backup + receiver_model.disable(); baseband::shutdown(); } @@ -176,35 +216,68 @@ void WaterfallDesignerView::on_open_profile() { auto open_view = nav_.push(".txt"); open_view->push_dir(waterfalls_dir); open_view->on_changed = [this](std::filesystem::path new_file_path) { - on_profile_changed(new_file_path); + // NB: FileLoadView calls this and then pops, same as text_prompt does. + // Defer for the same two reasons - see on_create_new_profile(). + pending_profile_path = std::move(new_file_path); + nav_.set_on_pop([this]() { + on_profile_changed(pending_profile_path); + }); }; } -void WaterfallDesignerView::on_profile_changed(std::filesystem::path new_profile_path) { - current_profile_path = new_profile_path; +bool WaterfallDesignerView::read_profile_file(const std::filesystem::path& path) { + File profile_file; + + if (profile_file.open(path)) return false; + profile_levels.clear(); - - File playlist_file; - auto error = playlist_file.open(new_profile_path.string()); - - if (error) return; - - menu_view.clear(); - auto reader = FileLineReader(playlist_file); + auto reader = FileLineReader(profile_file); for (const auto& line : reader) { - // remove empty lines - if (line == "\n" || line == "\r\n" || line == "\r") continue; - profile_levels.push_back(line); + auto entry = line; + + // Strip the whole line terminator; the old code only dropped one char, + // so CRLF files kept a stray '\r' in every entry. + while (!entry.empty() && (entry.back() == '\n' || entry.back() == '\r')) + entry.pop_back(); + + if (entry.empty()) continue; + profile_levels.push_back(std::move(entry)); } - for (auto& line : profile_levels) { - // remove line end \n etc - if (line.length() > 0 && (line[line.length() - 1] == '\n' || line[line.length() - 1] == '\r')) { - line = line.substr(0, line.length() - 1); - } + return true; +} + +bool WaterfallDesignerView::write_profile_file(const std::filesystem::path& path) { + File profile_file; + + if (profile_file.open(path, false, true)) return false; + + // FA_OPEN_ALWAYS leaves the position at end-of-file, so rewind before + // truncating or the existing contents are kept and appended to. + if (profile_file.seek(0).is_error()) return false; + if (profile_file.truncate().is_error()) return false; + + for (const auto& entry : profile_levels) + if (profile_file.write_line(entry)) return false; + + /* ~File() calls f_close(), which flushes, but its result is unreachable. + * Sync explicitly so a full or yanked card is reported to the caller + * instead of being announced as a successful save. */ + return !profile_file.sync(); +} + +void WaterfallDesignerView::on_profile_changed(const std::filesystem::path& new_profile_path) { + // read_profile_file() only touches profile_levels once the open succeeded, + // so a failure here leaves the previously loaded profile intact. + if (!read_profile_file(new_profile_path)) { + nav_.display_modal("Err", "open err"); + return; } + current_profile_path = new_profile_path; + highlighted_index_ = 0; + button_save.hidden(false); button_add_level.hidden(false); button_remove_level.hidden(false); @@ -219,53 +292,52 @@ void WaterfallDesignerView::refresh_menu_view() { menu_view.clear(); for (const auto& line : profile_levels) { - if (line.length() == 0 || line[0] == '#') { + uint8_t index, r, g, b; + + // index,R,G,B - anything else (comment, header, garbage) is shown greyed. + if (parse_level(line, index, r, g, b)) { + menu_view.add_item({line, + ui::Color(r, g, b), + &bitmap_icon_cwgen, + [this](KeyEvent) { + button_remove_level.focus(); + }}); + } else { menu_view.add_item({line, ui::Color::grey(), &bitmap_icon_notepad, [this](KeyEvent) { button_add_level.focus(); }}); - } else { - // index,R,G,B - size_t pos = 0; - size_t next_pos = 0; - - // pass index - next_pos = line.find(',', pos); - if (next_pos == std::string::npos) continue; - pos = next_pos + 1; - - // r - next_pos = line.find(',', pos); - if (next_pos == std::string::npos) continue; - uint8_t r = static_cast(std::stoi(line.substr(pos, next_pos - pos))); - pos = next_pos + 1; - - // g - next_pos = line.find(',', pos); - if (next_pos == std::string::npos) continue; - uint8_t g = static_cast(std::stoi(line.substr(pos, next_pos - pos))); - pos = next_pos + 1; - - // b - uint8_t b = static_cast(std::stoi(line.substr(pos))); - - ui::Color color = ui::Color(r, g, b); - menu_view.add_item({line, - color, - &bitmap_icon_cwgen, - [this](KeyEvent) { - button_remove_level.focus(); - }}); } } + + // menu_view.clear() resets its own highlight to 0 but does not fire + // on_highlight, so highlighted_index_ has to be re-synced by hand or the + // edit/remove buttons act on a different row than the one the user sees. + if (highlighted_index_ >= profile_levels.size()) + highlighted_index_ = profile_levels.empty() ? 0 : profile_levels.size() - 1; + + if (!profile_levels.empty()) + menu_view.set_highlighted(highlighted_index_); + set_dirty(); } void WaterfallDesignerView::on_apply_current_to_wtf() { - std::filesystem::path system_read_path = "waterfall.txt"; - copy_file(current_profile_path, system_read_path); + /* Take the backup lazily, right before the first overwrite of the live + * gradient. Doing it in the constructor meant copy_file() (~1.8kB of stack) + * ran underneath run_external_app(), which holds a 776 byte frame with a + * File of its own - about 3.6kB of the 4kB M0 process stack. It also did SD + * I/O even when the user just opened the app and backed straight out. */ + backup_current_profile(); + + /* NB: this used to copy_file(current_profile_path, "waterfall.txt"), which + * costs ~1.8kB of stack (two FILs + a 512 byte block buffer) out of the 4kB + * M0 process stack, on top of whatever UI callback we are nested under. + * profile_levels is the authoritative copy of the profile anyway, so write + * it straight out through a single File instead. */ + if (!write_profile_file(u"waterfall.txt")) return; waterfall.load_gradient(); @@ -281,71 +353,82 @@ void WaterfallDesignerView::on_save_profile() { return; } - File profile_file; - auto error = profile_file.open(current_profile_path.string(), false, false); - - if (error) { - nav_.display_modal("Err", "open err"); + // NB: not just "open err" any more - this now also covers a failed + // truncate/write/flush, i.e. the file may be partially written. + if (!write_profile_file(current_profile_path)) { + nav_.display_modal("Err", "write failed"); return; } - // clear file - profile_file.seek(0); - profile_file.truncate(); - - // write new data - for (const auto& entry : profile_levels) { - profile_file.write_line(entry); - } - nav_.display_modal("Save", "Saved profile\n" + current_profile_path.string()); } void WaterfallDesignerView::on_add_level() { - if (highlighted_index_ >= profile_levels.size()) return; - if (profile_levels[highlighted_index_].empty()) return; - if (profile_levels[highlighted_index_][0] == '#') return; - if (profile_levels[highlighted_index_].find(',') == std::string::npos) return; - size_t insert_pos = highlighted_index_; - std::string new_entry = "0,128,128,128"; - profile_levels.insert(profile_levels.begin() + insert_pos, new_entry); + if (current_profile_path.empty()) return; + + /* A freshly created profile has no rows at all, and the old guard + * (highlighted_index_ >= size()) then refused with no feedback - so the + * button looked dead on exactly the file you just made. Seed instead. */ + size_t insert_pos = std::min(highlighted_index_, profile_levels.size()); + + // Don't push a level above a comment/header row; drop it after instead. + if (insert_pos < profile_levels.size() && !is_color_level(profile_levels[insert_pos])) + insert_pos++; + + profile_levels.insert(profile_levels.begin() + insert_pos, "0,128,128,128"); + highlighted_index_ = insert_pos; + refresh_menu_view(); on_edit_color(); } void WaterfallDesignerView::on_remove_level() { if (highlighted_index_ >= profile_levels.size()) return; - if (profile_levels[highlighted_index_].empty()) return; - if (profile_levels[highlighted_index_][0] == '#') return; - if (profile_levels[highlighted_index_].find(',') == std::string::npos) return; + if (!is_color_level(profile_levels[highlighted_index_])) return; + profile_levels.erase(profile_levels.begin() + highlighted_index_); refresh_menu_view(); } void WaterfallDesignerView::on_edit_color() { if (highlighted_index_ >= profile_levels.size()) return; - if (profile_levels[highlighted_index_].empty()) return; - if (profile_levels[highlighted_index_][0] == '#') return; - if (profile_levels[highlighted_index_].find(',') == std::string::npos) return; + if (!is_color_level(profile_levels[highlighted_index_])) return; - auto color_picker_view = nav_.push(profile_levels[highlighted_index_]); - color_picker_view->on_save = [this](std::string new_color) { - profile_levels[highlighted_index_] = new_color; + // Capture the row by value: highlighted_index_ can move (or the list can + // shrink) while the picker is up, and the old code wrote to whatever + // highlighted_index_ happened to be at save time with no bounds check. + const size_t index = highlighted_index_; + + auto color_picker_view = nav_.push(profile_levels[index]); + color_picker_view->on_save = [this, index](std::string new_color) { + if (index >= profile_levels.size()) return; + + profile_levels[index] = std::move(new_color); refresh_menu_view(); on_apply_current_to_wtf(); }; } void WaterfallDesignerView::backup_current_profile() { - std::filesystem::path curren_wtf_path = "waterfall.txt"; - std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk"; - copy_file(curren_wtf_path, backup_path); + if (backup_attempted_) return; + backup_attempted_ = true; + + std::filesystem::path current_wtf_path = u"waterfall.txt"; + std::filesystem::path backup_path = waterfalls_dir / u"wtf_des_bk.bk"; + + if (!file_exists(current_wtf_path)) return; + + profile_backed_up_ = copy_file(current_wtf_path, backup_path).ok(); } void WaterfallDesignerView::restore_current_profile() { - std::filesystem::path backup_path = waterfalls_dir / "wtf_des_bk.bk"; - std::filesystem::path put_back_path = "waterfall.txt"; - copy_file(backup_path, put_back_path); + // Only touch waterfall.txt if we actually took a backup on entry; otherwise + // this would silently do nothing and leave the edited gradient installed. + if (!profile_backed_up_) return; + + std::filesystem::path backup_path = waterfalls_dir / u"wtf_des_bk.bk"; + + copy_file(backup_path, u"waterfall.txt"); delete_file(backup_path); } @@ -365,16 +448,35 @@ void WaterfallDesignerView::on_create_new_profile() { buffer += ".txt"; } - File new_file; - auto error = new_file.create(waterfalls_dir / buffer); - if (error) { - nav_.display_modal("Err", "create file err"); + auto new_path = waterfalls_dir / buffer; + bool created; + + { + // Scoped so the FIL is closed (and its 512 byte sector cache is + // off the stack) before anything re-opens the same file - _FS_LOCK + // is 0, so FatFs will not stop us from double-opening it. + File new_file; + created = !new_file.create(new_path); + } + + /* NB: everything below has to run *after* the keyboard view is gone. + * text_prompt()'s caller invokes this handler and then pops, so: + * - pushing a modal from here would pop the modal, not the keyboard; + * - and running on_profile_changed() inline stacks its File plus the + * gradient write under the alphanum/button/dispatch frames, which + * is what blows the 4kB M0 process stack ("Stack Overflow" guru). + * set_on_pop() defers it to just after the pop, at a shallow depth. */ + if (!created) { + nav_.set_on_pop([this]() { + nav_.display_modal("Err", "create file err"); + }); return; } - profile_levels.clear(); - current_profile_path = waterfalls_dir / buffer; - on_profile_changed(current_profile_path); + pending_profile_path = new_path; + nav_.set_on_pop([this]() { + on_profile_changed(pending_profile_path); + }); }); } @@ -392,32 +494,7 @@ WaterfallDesignerColorPickerView::WaterfallDesignerColorPickerView(NavigationVie progressbar.set_max(UINT8_MAX); - size_t pos = 0; - size_t next_pos = 0; - - // index - next_pos = color_str.find(',', pos); - if (next_pos != std::string::npos) { - index_ = static_cast(std::stoi(color_str.substr(pos, next_pos - pos))); - pos = next_pos + 1; - } - - // r - next_pos = color_str.find(',', pos); - if (next_pos != std::string::npos) { - red_ = static_cast(std::stoi(color_str.substr(pos, next_pos - pos))); - pos = next_pos + 1; - } - - // g - next_pos = color_str.find(',', pos); - if (next_pos != std::string::npos) { - green_ = static_cast(std::stoi(color_str.substr(pos, next_pos - pos))); - pos = next_pos + 1; - } - - // b - blue_ = static_cast(std::stoi(color_str.substr(pos))); + parse_level(color_str_, index_, red_, green_, blue_); field_red.set_value(red_); field_green.set_value(green_); @@ -466,19 +543,12 @@ void WaterfallDesignerColorPickerView::update_color_index() { green_ = static_cast(field_green.value()); blue_ = static_cast(field_blue.value()); - const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40}; - - Painter painter_instance_2; - painter_instance_2.fill_rectangle( - {preview_rect.left(), preview_rect.top(), preview_rect.width(), preview_rect.height()}, - ui::Color(red_, green_, blue_)); + // Repaint through the normal dirty pass instead of driving the display from + // inside an on_change handler; paint() below already draws the swatch. + set_dirty(); } void WaterfallDesignerColorPickerView::paint(Painter& painter) { - // this is not duplicated code. - // because need to display color when enter, - // but it is too early to call update_color() in the constructor. - const Rect preview_rect{screen_width - 48, 1 * 16, 40, 40}; painter.fill_rectangle( @@ -487,14 +557,12 @@ void WaterfallDesignerColorPickerView::paint(Painter& painter) { } std::string WaterfallDesignerColorPickerView::build_color_str() { - size_t index_pos = color_str_.find(','); - if (index_pos != std::string::npos) { - return color_str_.substr(0, index_pos + 1) + - to_string_dec_uint(red_) + "," + - to_string_dec_uint(green_) + "," + - to_string_dec_uint(blue_); - } - return to_string_dec_uint(index_) + "," + to_string_dec_uint(red_) + "," + to_string_dec_uint(green_) + "," + to_string_dec_uint(blue_); + // Always emit index_ - the old version kept the index substring from the + // input line, so edits made with field_index were silently discarded. + return to_string_dec_uint(index_) + "," + + to_string_dec_uint(red_) + "," + + to_string_dec_uint(green_) + "," + + to_string_dec_uint(blue_); } } /* namespace ui::external_app::waterfall_designer */ diff --git a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp index e1fadf5fe..36cf2ebf4 100644 --- a/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp +++ b/firmware/application/external/waterfall_designer/ui_waterfall_designer.hpp @@ -217,18 +217,31 @@ class WaterfallDesignerView : public View { void restore_current_profile(); void on_create_new_profile(); void on_open_profile(); - void on_profile_changed(std::filesystem::path new_profile_path); + void on_profile_changed(const std::filesystem::path& new_profile_path); void on_save_profile(); void on_add_level(); void on_remove_level(); void on_edit_color(); + /* Each of these owns the only File in its call chain. Keeping them as + * separate frames matters: a File carries a 512 byte FIL sector cache + * (_FS_TINY is 0) and the M0 process stack is 4kB, so if read_profile_file + * merged into on_profile_changed its 784 byte frame would stay live across + * the copy_file() in on_apply_current_to_wtf() - 3.6kB of 4kB instead of + * 2.8kB. GCC does not inline them at -O2 today; noinline just pins that so + * a heuristic change can't quietly eat the headroom. */ + __attribute__((noinline)) bool read_profile_file(const std::filesystem::path& path); + __attribute__((noinline)) bool write_profile_file(const std::filesystem::path& path); + void refresh_menu_view(); void on_apply_current_to_wtf(); // will restore if didn't apple, when distruct void on_apply_setting(); // apply set bool if_apply_setting{false}; + bool backup_attempted_{false}; + bool profile_backed_up_{false}; + std::filesystem::path pending_profile_path{}; // set by the New dialog, consumed on_pop /*NB: this works as: each time you change color, it apply as file realtime diff --git a/firmware/application/ui/ui_menu.cpp b/firmware/application/ui/ui_menu.cpp index c775ee46a..d267e7265 100644 --- a/firmware/application/ui/ui_menu.cpp +++ b/firmware/application/ui/ui_menu.cpp @@ -344,7 +344,9 @@ bool MenuView::on_key(const KeyEvent key) { } [[fallthrough]]; case KeyEvent::Select: - if (menu_items[highlighted_item].on_select) { + // NB: a MenuView can legitimately be empty (e.g. after clear()) while + // still holding focus; indexing menu_items unchecked faults there. + if (highlighted_item < menu_items.size() && menu_items[highlighted_item].on_select) { menu_items[highlighted_item].on_select(key); } return true; @@ -364,7 +366,7 @@ bool MenuView::on_keyboard(const KeyboardEvent key) { if (key == '-') return set_highlighted(highlighted_item - 1); if (key == '+') return set_highlighted(highlighted_item + 1); if (key == 10) { - if (menu_items[highlighted_item].on_select) { + if (highlighted_item < menu_items.size() && menu_items[highlighted_item].on_select) { menu_items[highlighted_item].on_select(KeyEvent::Right); } return true; diff --git a/firmware/tools/check_external_symbol_placement.py b/firmware/tools/check_external_symbol_placement.py new file mode 100755 index 000000000..86a58f680 --- /dev/null +++ b/firmware/tools/check_external_symbol_placement.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 + +# +# copyleft 2026 zxkmm co author with AI +# +# This file is part of PortaPack. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +"""Check that every external app symbol landed in its own app's memory region. + +The rules in external.ld match input *section names*, so a glob like +`*(*ui*external_app*level*)` also matches + + .text._ZN2ui12external_app18waterfall_designer...12on_add_levelEv + ^^^^^ contains "level" + +Section assignment is first-match-wins, so a symbol whose mangled name merely +contains another app's name gets linked into that app's region. Only one +external app is resident at a time, so calling it jumps into unmapped memory +and hard faults. + +export_external_apps.py already warns about *data* words that point at another +app, but a `bl` is PC-relative: the target never appears as a literal, so that +check cannot see it. This one works on symbol addresses instead and does. + +Usage: + check_external_symbol_placement.py [build/firmware/application/application.elf] +""" + +import os +import re +import subprocess +import sys + +REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +DEFAULT_ELF = os.path.join(REPO, "build", "firmware", "application", "application.elf") +DEFAULT_LD = os.path.join(REPO, "firmware", "application", "external", "external.ld") + + +def find_toolchain(): + for prefix in (os.environ.get("ARM_TOOLCHAIN"), + os.path.join(REPO, "armbin", "bin", "arm-none-eabi-"), + "arm-none-eabi-"): + if not prefix: + continue + try: + subprocess.run([prefix + "nm", "--version"], capture_output=True, check=True) + return prefix + except (OSError, subprocess.CalledProcessError): + continue + sys.exit("error: no arm-none-eabi toolchain found (set $ARM_TOOLCHAIN)") + + +def parse_regions(ld_path): + with open(ld_path) as f: + ld = f.read() + regions = {} + for m in re.finditer(r'ram_external_app_(\w+)\s+\(rwx\)\s*:\s*org\s*=\s*' + r'(0x[0-9A-Fa-f]+),\s*len\s*=\s*(\d+)k', ld): + regions[m.group(1)] = (int(m.group(2), 16), int(m.group(3)) * 1024) + return regions + + +def main(): + elf = sys.argv[1] if len(sys.argv) > 1 else DEFAULT_ELF + if not os.path.exists(elf): + print("skipping symbol placement check, no ELF at %s" % elf) + return 0 + + tc = find_toolchain() + regions = parse_regions(DEFAULT_LD) + if not regions: + print("skipping symbol placement check, could not parse external.ld") + return 0 + + def owner(addr): + for name, (base, size) in regions.items(): + if base <= addr < base + size: + return name + return None + + nm = subprocess.run([tc + "nm", "-C", "--defined-only", elf], + capture_output=True, text=True).stdout + + misplaced = [] + total = 0 + for line in nm.splitlines(): + parts = line.split(" ", 2) + if len(parts) < 3 or not re.fullmatch(r'[0-9a-f]{8}', parts[0]): + continue + addr, sym = int(parts[0], 16), parts[2] + if "_veneer" in sym: + continue + m = re.search(r'external_app::(\w+)::', sym) + if not m: + continue + ns, host = m.group(1), owner(addr) + total += 1 + if host is None: + continue # inlined into main firmware, harmless + # Namespace and region name need not be identical: ert/ert_app, + # keeloqtx/ui_keeloqtx, secplustx/ui_secplustx. Substring either way is fine. + if ns not in host and host not in ns: + misplaced.append((ns, host, sym)) + + print("\nchecking placement of %d external app symbols across %d regions" + % (total, len(regions))) + + if not misplaced: + print("all external app symbols are in their own app's region") + return 0 + + print("\nERROR: %d symbol(s) linked into the wrong app's region." % len(misplaced)) + print("These will hard fault when called - the owning app is not resident.\n") + for ns, host, sym in misplaced: + print(" %s -> landed in '%s' region" % (ns, host)) + print(" %s" % sym) + print("\nFix: make the rule in external.ld specific to the app's own sources, e.g.") + print(" */external//*(*ui*external_app**);") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/firmware/tools/guru_lookup.py b/firmware/tools/guru_lookup.py new file mode 100755 index 000000000..a5517a6cf --- /dev/null +++ b/firmware/tools/guru_lookup.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 + +# +# copyleft 2026 zxkmm co author with AI +# +# This file is part of PortaPack. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +"""Translate a guru meditation pc/lr into a symbol, source line and disassembly. + +Plain `arm-none-eabi-gdb -ex "x/3i 0xADDR" application.elf` only works for +addresses in the main M0 firmware. External apps are linked at a placeholder +0xADxxxxxx address but *run* from ~0x1008xxxx, so the guru shows a runtime +address that does not exist in the ELF. This script does the translation: + + link_addr = section_vma + (runtime_addr - memory_location) + +where memory_location is the first word of the app's .ppma header. + +Usage: + guru_lookup.py 0x10085CF9 0x0FF36284 + guru_lookup.py --app waterfall_designer 0x10085CF9 + guru_lookup.py --baseband adsbrx 0x10081234 +""" + +import argparse +import os +import re +import struct +import subprocess +import sys + +REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + + +def find_toolchain(explicit): + """Locate the arm-none-eabi- prefix: --toolchain, $ARM_TOOLCHAIN, ./armbin/bin, PATH.""" + candidates = [] + if explicit: + candidates.append(explicit) + if os.environ.get("ARM_TOOLCHAIN"): + candidates.append(os.environ["ARM_TOOLCHAIN"]) + candidates.append(os.path.join(REPO, "armbin", "bin", "arm-none-eabi-")) + candidates.append("arm-none-eabi-") + + for prefix in candidates: + try: + subprocess.run([prefix + "addr2line", "--version"], + capture_output=True, check=True) + return prefix + except (OSError, subprocess.CalledProcessError): + continue + sys.exit("error: no arm-none-eabi toolchain found (try --toolchain /path/to/arm-none-eabi-)") + + +def run(*cmd): + return subprocess.run(cmd, capture_output=True, text=True).stdout + + +def section_vmas(tc, elf): + """section name -> (vma, size) for every .external_app_* section.""" + out = run(tc + "objdump", "-h", elf) + vmas = {} + for m in re.finditer(r'\.external_app_(\S+)\s+([0-9a-f]{8})\s+([0-9a-f]{8})', out): + vmas[m.group(1)] = (int(m.group(3), 16), int(m.group(2), 16)) + return vmas + + +def app_load_addresses(build_dir): + """app name -> memory_location, read from the first word of each .ppma.""" + app_dir = os.path.join(build_dir, "firmware", "application") + loads = {} + if not os.path.isdir(app_dir): + return loads + for name in os.listdir(app_dir): + if not name.endswith(".ppma"): + continue + path = os.path.join(app_dir, name) + try: + with open(path, "rb") as f: + (mem,) = struct.unpack("> " if here else " ", l.strip())) + + +def main(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("addresses", nargs="+", help="pc / lr values from the guru screen") + p.add_argument("--build", default=os.path.join(REPO, "build"), help="build directory") + p.add_argument("--app", help="external app name, if auto-detection is ambiguous") + p.add_argument("--baseband", help="resolve against firmware/baseband/baseband_.elf") + p.add_argument("--toolchain", help="arm-none-eabi- prefix") + args = p.parse_args() + + tc = find_toolchain(args.toolchain) + app_elf = os.path.join(args.build, "firmware", "application", "application.elf") + + if args.baseband: + elf = os.path.join(args.build, "firmware", "baseband", + "baseband_%s.elf" % args.baseband) + if not os.path.exists(elf): + sys.exit("error: no such baseband image: " + elf) + for a in args.addresses: + addr = int(a, 16) & ~1 + print("\n%s (M4 baseband: %s)" % (a, args.baseband)) + describe(tc, elf, addr) + return + + if not os.path.exists(app_elf): + sys.exit("error: %s not found (pass --build)" % app_elf) + + vmas = section_vmas(tc, app_elf) + loads = app_load_addresses(args.build) + + for a in args.addresses: + addr = int(a, 16) & ~1 # drop the Thumb bit + print("\n=== %s ===" % a) + + # Main M0 firmware is linked at 0 (SPIFI shadow), so it resolves directly. + if addr < 0x00100000: + print(" main M0 firmware") + describe(tc, app_elf, addr) + continue + + # Otherwise it is an external app running from local SRAM. Every app has + # its own memory_location, so several can plausibly contain the address; + # list each candidate and let the caller pick the app they actually ran. + names = [args.app] if args.app else sorted(loads) + hits = 0 + for name in names: + if name not in loads or name not in vmas: + continue + off = addr - loads[name] + vma, size = vmas[name] + if not (0 <= off < size): + continue + hits += 1 + print(" candidate: %s load=0x%08X offset=0x%X" % (name, loads[name], off)) + describe(tc, app_elf, vma + off, "link ") + + if hits == 0: + print(" no external app contains this address.") + print(" If the guru header said M4, re-run with --baseband .") + print(" A wild pc with a sane lr usually means an indirect call through") + print(" a bad pointer - look up the lr instead, that is the caller.") + elif hits > 1 and not args.app: + print(" (%d candidates - narrow it with --app )" % hits) + + +if __name__ == "__main__": + main() diff --git a/firmware/tools/stack_usage.py b/firmware/tools/stack_usage.py new file mode 100755 index 000000000..8583b8871 --- /dev/null +++ b/firmware/tools/stack_usage.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 + +# +# copyleft 2026 zxkmm co author with AI +# +# This file is part of PortaPack. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; see the file COPYING. If not, write to +# the Free Software Foundation, Inc., 51 Franklin Street, +# Boston, MA 02110-1301, USA. +# + +"""Report per-function stack frame sizes for an ELF or object file. + +The M0 process stack is 4 kB total (__process_stack_size__ in LPC43xx_M0.ld), +shared by the whole UI thread including external apps. A guru that says +"Stack Overflow" is a hard fault where get_free_stack_space() < 16, i.e. the +4 kB really was consumed - it is not a heap problem. + +Watch out for anything holding a File: _FS_TINY is 0 and _MAX_SS is 512 in +ffconf.h, so every FIL carries a 512 byte sector cache and a File local costs +~560 bytes of stack. copy_file() holds two of them plus a 512 byte block +buffer, ~1.8 kB in one frame. + +Thumb-1 cannot encode `sub sp, #imm` above 508 bytes, so large frames appear as +a negative constant loaded from the literal pool: + + ldr r4, [pc, #728] ; (2e0 <...>) + add sp, r4 ; r4 = 0xfffffcfc = -772 + +Naive greps for `sub sp` miss exactly the frames that matter. This resolves the +literal. + +Usage: + stack_usage.py build/firmware/application/application.elf # top 40 + stack_usage.py --min 512 # everything >= 512 + stack_usage.py --grep copy_file # filter by name + stack_usage.py --chain main run_external_app copy_file # sum a call chain +""" + +import argparse +import os +import re +import subprocess +import sys + +REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +M0_STACK_BYTES = 4096 + +REG_ORDER = ['r0', 'r1', 'r2', 'r3', 'r4', 'r5', 'r6', 'r7', + 'r8', 'r9', 'sl', 'fp', 'ip', 'sp', 'lr', 'pc'] + + +def find_toolchain(explicit): + for prefix in (explicit, os.environ.get("ARM_TOOLCHAIN"), + os.path.join(REPO, "armbin", "bin", "arm-none-eabi-"), + "arm-none-eabi-"): + if not prefix: + continue + try: + subprocess.run([prefix + "objdump", "--version"], + capture_output=True, check=True) + return prefix + except (OSError, subprocess.CalledProcessError): + continue + sys.exit("error: no arm-none-eabi toolchain found (set $ARM_TOOLCHAIN)") + + +def push_bytes(reglist): + n = 0 + for part in reglist.split(','): + part = part.strip() + if '-' in part: + a, b = part.split('-') + if a in REG_ORDER and b in REG_ORDER: + n += REG_ORDER.index(b) - REG_ORDER.index(a) + 1 + else: + n += 2 + else: + n += 1 + return n * 4 + + +def frame_sizes(tc, path): + """function name (mangled) -> stack bytes reserved in its prologue.""" + out = subprocess.run([tc + "objdump", "-d", path], + capture_output=True, text=True).stdout + + bodies, cur = {}, None + for line in out.splitlines(): + m = re.match(r'^[0-9a-f]+ <(.+)>:', line) + if m: + cur = m.group(1) + bodies[cur] = [] + elif cur is not None: + bodies[cur].append(line) + + sizes = {} + for func, body in bodies.items(): + # Literal pool words, so `add sp, rN` can be resolved. + literals = {} + for line in body: + m = re.match(r'\s*([0-9a-f]+):\s+([0-9a-f]{8})\s+\.word\s+0x([0-9a-f]+)', line) + if m: + literals[int(m.group(1), 16)] = int(m.group(3), 16) + + total, regval = 0, {} + for line in body: + m = re.search(r'ldr\s+(\w+), \[pc, #\d+\].*;\s*\(([0-9a-f]+)', line) + if m: + regval[m.group(1)] = literals.get(int(m.group(2), 16)) + + m = re.search(r'\bsub(?:\.w)?\s+sp, (?:sp, )?#(\d+)', line) + if m: + total += int(m.group(1)) + + # Thumb-1 large frame: add sp, rN where rN holds a negative literal. + # Bound it: the same register may instead hold a data address, and + # external app addresses (0xADxxxxxx) also look "negative" here. + m = re.search(r'\badd\s+sp, (r\d+|sl|fp|ip)\b', line) + if m: + v = regval.get(m.group(1)) + if v and v > 0x80000000: + adjust = 0x100000000 - v + if adjust <= 2 * M0_STACK_BYTES: + total += adjust + + m = re.search(r'\bpush(?:\.w)?\s+\{(.+)\}', line) + if m: + total += push_bytes(m.group(1)) + + sizes[func] = total + return sizes + + +def demangle(tc, names): + if not names: + return [] + out = subprocess.run([tc + "c++filt"], input="\n".join(names), + capture_output=True, text=True).stdout + return out.splitlines() + + +def main(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("path", help="ELF or .obj to analyse") + p.add_argument("--min", type=int, default=0, help="only show frames >= this many bytes") + p.add_argument("--top", type=int, default=40, help="max rows to print (0 = all)") + p.add_argument("--grep", help="only functions whose demangled name matches this regex") + p.add_argument("--chain", nargs="+", + help="sum the frames of these functions (substring match) as a call chain") + p.add_argument("--toolchain", help="arm-none-eabi- prefix") + args = p.parse_args() + + if not os.path.exists(args.path): + sys.exit("error: no such file: " + args.path) + + tc = find_toolchain(args.toolchain) + sizes = frame_sizes(tc, args.path) + names = list(sizes) + pretty = dict(zip(names, demangle(tc, names))) + + if args.chain: + print("\ncall chain, M0 process stack is %d bytes\n" % M0_STACK_BYTES) + total = 0 + for want in args.chain: + hit = None + for n in names: + if want in pretty[n] or want in n: + if hit is None or sizes[n] > sizes[hit]: + hit = n + if hit is None: + print(" %6s %s (not found)" % ("?", want)) + continue + total += sizes[hit] + print(" %6d %s" % (sizes[hit], pretty[hit][:96])) + pct = 100.0 * total / M0_STACK_BYTES + print(" " + "-" * 60) + print(" %6d total (%.0f%% of the 4 kB stack, %d bytes free)" + % (total, pct, M0_STACK_BYTES - total)) + if total > M0_STACK_BYTES: + print("\n OVERFLOWS - this chain cannot fit.") + elif pct > 75: + print("\n Tight. Anything the callees add on top may fault.") + return + + rows = [(v, pretty[k]) for k, v in sizes.items() if v >= args.min] + if args.grep: + rx = re.compile(args.grep) + rows = [r for r in rows if rx.search(r[1])] + rows.sort(key=lambda r: -r[0]) + if args.top: + rows = rows[:args.top] + + print("\n%6s %s" % ("bytes", "function")) + for v, n in rows: + flag = " <-- over half the stack" if v > M0_STACK_BYTES // 2 else "" + print("%6d %s%s" % (v, n[:100], flag)) + + +if __name__ == "__main__": + main()