Fix waterfall designer crash and also some linker script fixes (#3281)

* init

* _
This commit is contained in:
未来方舟
2026-08-01 18:57:45 +08:00
committed by GitHub
parent c4f32ac436
commit a4d91f12ef
8 changed files with 864 additions and 238 deletions
+4 -1
View File
@@ -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})
+92 -92
View File
@@ -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
@@ -28,11 +28,47 @@
#include "ui_fileman.hpp"
#include "file_reader.hpp"
#include "ui_textentry.hpp"
#include "convert.hpp"
#include <algorithm>
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<uint8_t>(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<FileLoadView>(".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<uint8_t>(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<uint8_t>(std::stoi(line.substr(pos, next_pos - pos)));
pos = next_pos + 1;
// b
uint8_t b = static_cast<uint8_t>(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<size_t>(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<WaterfallDesignerColorPickerView>(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<WaterfallDesignerColorPickerView>(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<uint8_t>(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<uint8_t>(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<uint8_t>(std::stoi(color_str.substr(pos, next_pos - pos)));
pos = next_pos + 1;
}
// b
blue_ = static_cast<uint8_t>(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<uint8_t>(field_green.value());
blue_ = static_cast<uint8_t>(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 */
@@ -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
+4 -2
View File
@@ -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;
+139
View File
@@ -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/<app>/*(*ui*external_app*<app>*);")
return 1
if __name__ == "__main__":
sys.exit(main())
+187
View File
@@ -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("<I", f.read(4))
loads[name[:-5]] = mem
except (OSError, struct.error):
pass
return loads
def describe(tc, elf, addr, label=""):
"""Print addr2line (with inline frames) plus a short disassembly window."""
a = "0x%08x" % addr
src = run(tc + "addr2line", "-f", "-C", "-i", "-e", elf, a).strip()
print(" %s%s" % (label, a))
for i, line in enumerate(src.splitlines()):
print(" %s %s" % ("in" if i % 2 == 0 else " at", line))
dis = run(tc + "objdump", "-d", "--start-address=0x%x" % (addr - 8),
"--stop-address=0x%x" % (addr + 8), elf)
body = [l for l in dis.splitlines() if re.match(r'\s*[0-9a-f]+:\t', l)]
if body:
print(" --")
for l in body:
here = re.match(r'\s*0*%x:' % addr, l)
print(" %s%s" % (" >> " 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_<name>.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 <image>.")
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 <name>)" % hits)
if __name__ == "__main__":
main()
+214
View File
@@ -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 <file> --min 512 # everything >= 512
stack_usage.py <file> --grep copy_file # filter by name
stack_usage.py <file> --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()