mirror of
https://github.com/portapack-mayhem/mayhem-firmware.git
synced 2026-09-13 18:19:33 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 367eaf54c0 | |||
| de30cbfe1b | |||
| a4d91f12ef | |||
| c4f32ac436 |
@@ -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})
|
||||
|
||||
@@ -532,6 +532,18 @@ ADSBRxView::ADSBRxView(NavigationView& nav) {
|
||||
logger = std::make_unique<ADSBLogger>();
|
||||
logger->append(logs_dir / u"ADSB.TXT");
|
||||
|
||||
/* First run only: start from the configuration that is known to receive
|
||||
* ADS-B on this hardware -- LNA 32, VGA 32, RF amp ON. The first two are
|
||||
* already ReceiverModel's defaults; the amp is not, and running without it
|
||||
* costs about 14 dB, which is the difference between a busy list and an
|
||||
* empty one. Once the user has saved settings for this app their choice
|
||||
* wins, so this only sets the starting point.
|
||||
* Going through the field rather than receiver_model keeps the displayed
|
||||
* value in step: RFAmpField snapshots rf_amp() in its own constructor,
|
||||
* which has already run by the time we get here. */
|
||||
if (!settings_.loaded())
|
||||
field_rf_amp.set_value(1);
|
||||
|
||||
receiver_model.enable();
|
||||
baseband::set_adsb();
|
||||
|
||||
|
||||
@@ -907,12 +907,24 @@ void ClockManager::set_sampling_frequency(const uint32_t frequency) {
|
||||
// Set FPGA RX decimation register
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, n);
|
||||
|
||||
/* RX Mode: Register 3 is FPGA_REG_RX_DIGITAL_GAIN.
|
||||
* We shift up by (3 * n) to compensate for CIC bit-growth.
|
||||
/* No RX digital-gain register is written here.
|
||||
*
|
||||
* Register 0x03 used to be programmed with (3 * n + 2) as a "CIC
|
||||
* bit-growth" renormalisation. The gateware has no such register: the
|
||||
* RX decimator is a chain of unity-gain half-band FIRs selected by
|
||||
* rx_decim (fpga/top/standard.py), and 0x03 is rx_pstep, whose top two
|
||||
* bits are the quarter-rate shift. Writing a gain here silently
|
||||
* cancelled the shift that set_tuning_frequency() had programmed, which
|
||||
* left the analogue passband offset with no matching rotation.
|
||||
*
|
||||
* The shift depends on the AFE rate we just chose, so re-apply it after
|
||||
* the rate change. ReceiverModel::update_sampling_rate() calls
|
||||
* update_tuning_frequency() straight after this, which does exactly
|
||||
* that; the write below only keeps the register consistent in between.
|
||||
*/
|
||||
uint8_t ds = (3 * n);
|
||||
ds += 2;
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, ds);
|
||||
fpga_debug_register_write(
|
||||
FPGA_REG_RX_PSTEP,
|
||||
(radio::debug::get_cached_quarter_shift() & 0b11) << FPGA_RX_QUARTER_SHIFT_SHIFT);
|
||||
|
||||
// Re-enable FPGA processing with clean state ===
|
||||
fpga_debug_register_write(1, 0x01);
|
||||
|
||||
+92
-92
@@ -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
|
||||
|
||||
|
||||
|
||||
+210
-142
@@ -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 */
|
||||
|
||||
+14
-1
@@ -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
|
||||
|
||||
@@ -144,6 +144,10 @@ static rf::Direction cached_direction = rf::Direction::Receive;
|
||||
static bool cached_rf_amp = false;
|
||||
static int_fast8_t cached_lna_gain = 0;
|
||||
static int_fast8_t cached_vga_gain = 0;
|
||||
/* FPGA quarter-rate shift mode currently programmed, in gateware encoding
|
||||
* (0b00 none / 0b11 up / 0b01 down). The baseband filter width depends on it,
|
||||
* so ReceiverModel reads it back through get_quarter_shift(). */
|
||||
static uint8_t cached_quarter_shift = 0;
|
||||
#endif
|
||||
|
||||
void init() {
|
||||
@@ -182,18 +186,15 @@ void init() {
|
||||
|
||||
fpga_set_mode(FPGA_MODE_RX);
|
||||
|
||||
// These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation.
|
||||
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim
|
||||
|
||||
// RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift).
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
|
||||
|
||||
/* RX Mode: Initialize DC Block parameters to standard Praline values.
|
||||
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
|
||||
*/
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
|
||||
/* Boot register state, matching fpga_init() in hackrf/firmware/common/fpga.c:
|
||||
* DC block on, no PRBS, no external trigger, no quarter shift, TX NCO off.
|
||||
* The decimation ratio and the quarter shift are programmed later by
|
||||
* ClockManager::set_sampling_frequency() and set_tuning_frequency(). */
|
||||
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM = no decimation
|
||||
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); // quarter shift off
|
||||
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
|
||||
cached_quarter_shift = 0;
|
||||
|
||||
ssp1_arbiter.invalidate();
|
||||
chThdSleepMilliseconds(10); // Let FPGA registers settle
|
||||
@@ -224,13 +225,13 @@ void set_direction(const rf::Direction new_direction) {
|
||||
fpga_debug_register_write(FPGA_REG_TX_PHASE_STEP, 0x00);
|
||||
} else {
|
||||
fpga_set_mode(FPGA_MODE_RX);
|
||||
// RX Mode: Ensure NCO is disabled and reset digital gain
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
|
||||
/* RX Mode: Initialize DC Block parameters to standard Praline values.
|
||||
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
|
||||
*/
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
|
||||
/* RX Mode: DC block on, TX NCO off. The quarter shift is re-applied by
|
||||
* set_tuning_frequency(); clear it here so a stale TX/RX transition
|
||||
* cannot leave a rotation programmed with no matching LO offset. */
|
||||
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
|
||||
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
|
||||
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00);
|
||||
cached_quarter_shift = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -309,7 +310,19 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
final_frequency = final_frequency + portapack::persistent_memory::config_freq_rx_correction();
|
||||
}
|
||||
|
||||
#ifdef PRALINE
|
||||
/* The PRALINE tuning tables offset the analogue passband by a quarter of
|
||||
* the ADC sample rate and have the FPGA rotate it back to DC, so the
|
||||
* planner needs to know the AFE rate. See tuning.cpp. */
|
||||
const uint32_t afe_rate = portapack::clock_manager.get_sampling_frequency()
|
||||
<< portapack::clock_manager.get_resampling_n();
|
||||
const auto tuning_config = tuning::config::create(
|
||||
final_frequency,
|
||||
afe_rate,
|
||||
direction == rf::Direction::Transmit);
|
||||
#else
|
||||
const auto tuning_config = tuning::config::create(final_frequency);
|
||||
#endif
|
||||
if (tuning_config.is_valid()) {
|
||||
first_if.disable();
|
||||
|
||||
@@ -339,6 +352,22 @@ bool set_tuning_frequency(const rf::Frequency frequency) {
|
||||
LPC_GPIO->CLR[0] = (1 << 13); // SGPIO12 = 0 (Q normal)
|
||||
}
|
||||
|
||||
/* Program the FPGA's quarter-rate shift to match the offset the tuning
|
||||
* table just applied to the analogue centre frequency. The gateware
|
||||
* (hackrf/firmware/fpga/top/standard.py) takes both bits from the top
|
||||
* of register 0x03 (rx_pstep):
|
||||
* rx_pstep[6] -> quarter_shift.enable
|
||||
* rx_pstep[7] -> quarter_shift.up
|
||||
* which is exactly fpga_set_rx_quarter_shift_mode() in
|
||||
* hackrf/firmware/common/fpga.c: write (mode & 0b11) << 6.
|
||||
*
|
||||
* These two settings MUST be programmed together. Tuning off-centre
|
||||
* without the rotation puts the signal outside the decimation filter's
|
||||
* passband and it disappears entirely; rotating without the offset
|
||||
* moves the wanted signal off DC by the same amount. */
|
||||
cached_quarter_shift = tuning_config.quarter_shift;
|
||||
fpga_debug_register_write(FPGA_REG_RX_PSTEP, (cached_quarter_shift & 0b11) << 6);
|
||||
|
||||
ssp1_arbiter.invalidate();
|
||||
#else
|
||||
baseband_cpld.set_invert(mixer_invert ^ baseband_invert);
|
||||
@@ -469,6 +498,10 @@ int_fast8_t get_cached_lna_gain() {
|
||||
int_fast8_t get_cached_vga_gain() {
|
||||
return cached_vga_gain;
|
||||
}
|
||||
|
||||
uint8_t get_cached_quarter_shift() {
|
||||
return cached_quarter_shift;
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace first_if {
|
||||
@@ -570,18 +603,11 @@ void register_write(const size_t register_number, uint32_t value) {
|
||||
void init() {
|
||||
fpga_set_mode(FPGA_MODE_RX);
|
||||
|
||||
// These FPGA registers control DC_BLOCK, Q-Inv, QUARTER SHIFT, and Decimation.
|
||||
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN); // DC_BLOCK=1, QUARTER_SHIFT=0, Q_INVERT=0
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM=No Decim
|
||||
|
||||
// RX Mode: Register 3 is RX Digital Gain. Start with 0dB (no shift).
|
||||
fpga_debug_register_write(FPGA_REG_RX_DIGITAL_GAIN, FPGA_RX_DEFAULT_DIGITAL_GAIN);
|
||||
|
||||
/* RX Mode: Initialize DC Block parameters to standard Praline values.
|
||||
* 0x04 Width and 0x08 Adapt Rate are typical for 40MHz stability.
|
||||
*/
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_BLOCK_WIDTH, FPGA_RX_DEFAULT_DC_WIDTH);
|
||||
fpga_debug_register_write(FPGA_REG_RX_DC_ADAPT_RATE, FPGA_RX_DEFAULT_ADAPT_RATE);
|
||||
/* Same boot state as fpga_init() in hackrf/firmware/common/fpga.c. */
|
||||
fpga_debug_register_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
|
||||
fpga_debug_register_write(FPGA_REG_DECIM, 0x00); // RX_DECIM = no decimation
|
||||
fpga_debug_register_write(FPGA_REG_RX_PSTEP, 0x00); // quarter shift off
|
||||
fpga_debug_register_write(FPGA_REG_TX_CONTROL, 0x00);
|
||||
|
||||
ssp1_arbiter.invalidate(); // Force arbiter to reconfigure on next transfer
|
||||
}
|
||||
|
||||
@@ -126,6 +126,10 @@ rf::Direction get_cached_direction();
|
||||
bool get_cached_rf_amp();
|
||||
int_fast8_t get_cached_lna_gain();
|
||||
int_fast8_t get_cached_vga_gain();
|
||||
|
||||
/* FPGA RX quarter-rate shift currently programmed, in gateware encoding:
|
||||
* 0b00 none, 0b11 up, 0b01 down. */
|
||||
uint8_t get_cached_quarter_shift();
|
||||
#endif
|
||||
|
||||
namespace sgpio {
|
||||
|
||||
@@ -244,10 +244,24 @@ void ReceiverModel::enable() {
|
||||
radio::set_direction(rf::Direction::Receive);
|
||||
|
||||
#ifdef PRALINE
|
||||
/* Anchor the Common Mode Voltage (VCM) to 1.2V.
|
||||
* This stabilizes the electrical floor of the I/Q signals.
|
||||
*/
|
||||
radio::set_rx_buff_vcm(1);
|
||||
/* MAX2831 RX IQ common-mode voltage (register 15).
|
||||
*
|
||||
* 0 = 1.1 V, 1 = 1.2 V, 2 = 1.3 V, 3 = 1.45 V.
|
||||
*
|
||||
* The reference firmware leaves this alone: max2831.c's default register
|
||||
* table has reg 15 = 0x0145 (1.1 V) and the line that would raise it is
|
||||
* commented out ("maximum rx output common-mode voltage"). Mayhem used to
|
||||
* force 1.2 V here on the theory that it "stabilises the electrical floor
|
||||
* of the I/Q signals" -- plausible, but never measured, and it was the
|
||||
* last remaining RF-path setting where this branch disagreed with the
|
||||
* configuration that is proven to receive ADS-B on this board.
|
||||
*
|
||||
* Set to 0 to match the reference (writing 1.1 V is a no-op against the
|
||||
* power-on default), or back to 1 to restore the old Mayhem behaviour.
|
||||
* If reception measurably worsens, put it back to 1 and say so -- neither
|
||||
* value has been verified on hardware. */
|
||||
#define PRALINE_RX_IQ_VCM 0
|
||||
radio::set_rx_buff_vcm(PRALINE_RX_IQ_VCM);
|
||||
#endif
|
||||
|
||||
update_tuning_frequency();
|
||||
@@ -326,47 +340,39 @@ void ReceiverModel::update_baseband_bandwidth() {
|
||||
if (enabled_) {
|
||||
#ifdef PRALINE
|
||||
/*
|
||||
* PRALINE LPF bandwidth calculation from GSG hackrf_usb radio.c
|
||||
* PRALINE LPF bandwidth, ported from auto_bandwidth() in
|
||||
* hackrf/firmware/common/radio.c:
|
||||
*
|
||||
* The LPF should be set to capture the desired signal bandwidth
|
||||
* while the FPGA decimation filter handles anti-aliasing.
|
||||
* bb_bandwidth = sample_rate * 3 / 4
|
||||
* lpf_bandwidth = bb_bandwidth + offset_hz * 2
|
||||
*
|
||||
* For most modes: LPF = (output_sample_rate * 3) / 8
|
||||
* For quarter-shift: add offset for shifted spectrum
|
||||
* where offset_hz is the quarter-rate shift, i.e. afe_rate / 4 when a
|
||||
* shift is in use. The doubling is because the wanted signal sits
|
||||
* offset from the analogue centre, so the analogue filter has to stay
|
||||
* open out to that offset on the far side too.
|
||||
*
|
||||
* Note: MAX2831 minimum LPF is 11.6 MHz, so for narrow sample rates
|
||||
* the hardware limit applies and FPGA filter does the real work.
|
||||
* The previous version used /8 in both places and read the shift from
|
||||
* bits 2-3 of FPGA register 1, which do not exist in the gateware, so
|
||||
* it always took the no-shift branch. At the ADS-B rate that asked for
|
||||
* 750 kHz, which is below the MAX2831's 1.75 MHz floor and therefore
|
||||
* also switched in the external narrowband AA filter
|
||||
* (MAX2831::set_lpf_rf_bandwidth_rx), squeezing the RX path shut. The
|
||||
* reference asks for 17.5 MHz at the same rate and the AA filter stays
|
||||
* out of circuit.
|
||||
*/
|
||||
|
||||
uint32_t sample_rate = sampling_rate();
|
||||
uint8_t resampling_n = portapack::clock_manager.get_resampling_n();
|
||||
uint32_t afe_rate = sample_rate << resampling_n;
|
||||
|
||||
// Base LPF: enough to capture desired bandwidth
|
||||
uint32_t lpf_bandwidth = (sample_rate * 3) / 8;
|
||||
|
||||
// Check if quarter-shift is enabled (FPGA register 1, bits 2-3)
|
||||
uint32_t fpga_ctrl = radio::debug::fpga::register_read(1);
|
||||
uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03;
|
||||
uint32_t lpf_bandwidth = (sample_rate * 3) / 4;
|
||||
|
||||
const uint8_t quarter_shift = radio::debug::get_cached_quarter_shift();
|
||||
if (quarter_shift != 0) {
|
||||
// Quarter-shift moves spectrum by AFE_rate/4, need wider LPF
|
||||
uint32_t offset = afe_rate / 8;
|
||||
const uint32_t offset = afe_rate / 4;
|
||||
lpf_bandwidth += offset * 2;
|
||||
}
|
||||
|
||||
// For best anti-alias performance, also consider AFE Nyquist
|
||||
// If our calculated LPF is below MAX2831 minimum, it doesn't matter
|
||||
// But if we can set LPF to just below AFE Nyquist, that's optimal
|
||||
uint32_t afe_nyquist = afe_rate / 2;
|
||||
|
||||
// Use the larger of: signal bandwidth requirement OR Nyquist protection
|
||||
// (but MAX2831 driver will clamp to its available settings anyway)
|
||||
if (lpf_bandwidth < afe_nyquist) {
|
||||
// Set LPF close to Nyquist for maximum alias rejection
|
||||
lpf_bandwidth = (afe_nyquist * 9) / 10; // 90% of Nyquist
|
||||
}
|
||||
|
||||
radio::set_baseband_filter_bandwidth_rx(lpf_bandwidth);
|
||||
#else
|
||||
radio::set_baseband_filter_bandwidth_rx(baseband_bandwidth());
|
||||
|
||||
+332
-99
@@ -27,8 +27,8 @@ namespace tuning {
|
||||
namespace config {
|
||||
|
||||
// Forward declarations
|
||||
Config low_band(const rf::Frequency target_frequency);
|
||||
Config mid_band(const rf::Frequency target_frequency);
|
||||
Config low_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit);
|
||||
Config mid_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit);
|
||||
Config high_band(const rf::Frequency target_frequency);
|
||||
|
||||
#ifdef PRALINE
|
||||
@@ -36,133 +36,366 @@ Config high_band(const rf::Frequency target_frequency);
|
||||
* PRALINE Tuning Configuration
|
||||
* ============================
|
||||
*
|
||||
* Reference: hackrf_usb/common/tune_config.h praline_tune_config_rx[]
|
||||
* These tables are copied verbatim from the reference firmware,
|
||||
* hackrf/firmware/common/tune_config.h (praline_tune_config_rx /
|
||||
* praline_tune_config_tx), and the selection and offset maths below reproduce
|
||||
* hackrf/firmware/common/radio.c radio_update_frequency() /
|
||||
* analog_from_digital_rf() / compute_offset().
|
||||
*
|
||||
* The hackrf_usb firmware uses a table-driven approach where each entry
|
||||
* specifies:
|
||||
* - rf_range_end_mhz: Upper frequency limit for this config
|
||||
* - if_mhz: IF frequency (what MAX2831 tunes to)
|
||||
* - high_lo: true = high-side injection, false = low-side
|
||||
* - shift: FPGA quarter-shift mode (not implemented in Mayhem yet)
|
||||
* Each entry gives, for target frequencies up to rf_range_end_mhz:
|
||||
* if_mhz the IF the MAX2831 tunes to (0 = mixer bypassed, IF = RF)
|
||||
* high_lo true -> LO = IF + analogue RF (mixer inverts the spectrum)
|
||||
* false -> LO = IF - analogue RF (no inversion)
|
||||
* shift the FPGA quarter-rate shift mode used for this entry
|
||||
*
|
||||
* Key insight: The IF frequency varies to keep the RFFC5072 VCO in a
|
||||
* safe operating range (ideally 3500-5000 MHz, avoiding extremes).
|
||||
* The quarter-rate shift is the part Mayhem was previously missing. RX entries
|
||||
* deliberately place the analogue passband a quarter of the ADC rate away from
|
||||
* the requested frequency (+8 MHz at the usual 32 Msps AFE rate) so that the
|
||||
* wanted signal never sits on the DC offset / LO leakage, and then ask the FPGA
|
||||
* to rotate it back down to DC. Both halves have to be programmed together:
|
||||
* - tuning to target + offset without asking the FPGA to rotate leaves the
|
||||
* signal 8 MHz out and the decimation filter deletes it;
|
||||
* - tuning to target with no offset (what Mayhem did) parks the signal on DC,
|
||||
* under the LO leakage and the gateware's adaptive DC block.
|
||||
*
|
||||
* RFFC5072 VCO calculation:
|
||||
* High-side injection: LO = IF + RF, VCO = LO × lodiv
|
||||
* Low-side injection: LO = IF - RF, VCO = LO × lodiv
|
||||
* Where lodiv = 2 for frequencies where VCO > 2700 MHz
|
||||
*
|
||||
* From hackrf_usb tune_config_rx (simplified):
|
||||
* 0-2100 MHz: IF=2375, high_lo=true → VCO = (2375+RF)×2
|
||||
* 2105-2115: IF=2375, high_lo=false → VCO = (2375-RF)×2
|
||||
* 2115-2130: IF=2425, high_lo=false → VCO = (2425-RF)×2
|
||||
* ... (more entries for fine-grained control)
|
||||
* 2320-2580: IF=0 (bypass mode, no mixer)
|
||||
* 2580+: High-pass mode
|
||||
* "up" and "down" are named for the direction the FPGA rotates, so
|
||||
* FPGA_QUARTER_SHIFT_MODE_UP means the analogue centre is placed ABOVE the
|
||||
* requested frequency, and DOWN below it (radio.c analog_from_digital_rf()).
|
||||
*/
|
||||
|
||||
// Simplified tune_config lookup for Mayhem
|
||||
// Returns the IF frequency in Hz for a given target frequency
|
||||
constexpr rf::Frequency praline_get_if_frequency(const rf::Frequency target_frequency) {
|
||||
const uint32_t freq_mhz = target_frequency / 1'000'000;
|
||||
namespace {
|
||||
|
||||
// Based on hackrf_usb tune_config_rx table
|
||||
if (freq_mhz < 2100) {
|
||||
// Most low-band frequencies: use 2375 MHz IF
|
||||
// This keeps VCO around 4750-4950 MHz for FM band
|
||||
return 2375'000'000;
|
||||
} else if (freq_mhz < 2320) {
|
||||
// Transition zone: use varying IF to avoid VCO edges
|
||||
// These frequencies are tricky - near MAX2831 minimum
|
||||
// Use 2425 MHz to give some margin
|
||||
return 2425'000'000;
|
||||
} else {
|
||||
// Bypass mode or high-band - IF not used for mixer
|
||||
return 0;
|
||||
struct PralineTuneConfig {
|
||||
uint16_t rf_range_end_mhz;
|
||||
uint16_t if_mhz;
|
||||
bool high_lo;
|
||||
uint8_t shift; /* 0b00 none, 0b11 up, 0b01 down */
|
||||
};
|
||||
|
||||
/* The tables below are kept column-aligned to match the reference source, so
|
||||
* they are exempt from reformatting. */
|
||||
// clang-format off
|
||||
|
||||
/* tuning table optimized for RX */
|
||||
constexpr PralineTuneConfig praline_tune_config_rx[] = {
|
||||
{ 0, 2360, true, 0b00},
|
||||
{ 50, 2320, true, 0b11},
|
||||
{ 100, 2320, true, 0b01},
|
||||
{ 140, 2320, true, 0b11},
|
||||
{ 406, 2560, true, 0b11},
|
||||
{ 511, 2380, true, 0b11},
|
||||
{ 578, 2560, true, 0b01},
|
||||
{ 741, 2340, true, 0b11},
|
||||
{ 861, 2560, true, 0b01},
|
||||
{ 921, 2560, true, 0b11},
|
||||
{ 1049, 2340, true, 0b01},
|
||||
{ 1169, 2380, true, 0b11},
|
||||
{ 1360, 2340, true, 0b11},
|
||||
{ 1544, 2560, true, 0b01},
|
||||
{ 1675, 2560, true, 0b11},
|
||||
{ 1992, 2380, true, 0b01},
|
||||
{ 2070, 2340, true, 0b01},
|
||||
{ 2150, 2360, true, 0b01},
|
||||
{ 2168, 2560, false, 0b11},
|
||||
{ 2185, 2580, false, 0b11},
|
||||
{ 2202, 2580, false, 0b01},
|
||||
{ 2205, 2520, false, 0b11},
|
||||
{ 2216, 2560, false, 0b11},
|
||||
{ 2223, 2540, false, 0b11},
|
||||
{ 2234, 2580, false, 0b11},
|
||||
{ 2240, 2560, false, 0b11},
|
||||
{ 2251, 2580, false, 0b01},
|
||||
{ 2258, 2580, false, 0b11},
|
||||
{ 2265, 2540, false, 0b01},
|
||||
{ 2271, 2580, false, 0b11},
|
||||
{ 2273, 2560, false, 0b11},
|
||||
{ 2275, 2580, false, 0b01},
|
||||
{ 2280, 2500, false, 0b01},
|
||||
{ 2284, 2540, false, 0b11},
|
||||
{ 2289, 2580, false, 0b01},
|
||||
{ 2293, 2540, false, 0b01},
|
||||
{ 2298, 2520, false, 0b01},
|
||||
{ 2300, 2580, false, 0b11},
|
||||
{ 2302, 2540, false, 0b01},
|
||||
{ 2309, 2560, false, 0b01},
|
||||
{ 2311, 2580, false, 0b01},
|
||||
{ 2314, 2540, false, 0b11},
|
||||
{ 2315, 2540, false, 0b01},
|
||||
{ 2320, 2580, false, 0b11},
|
||||
{ 2380, 0, false, 0b11},
|
||||
{ 2440, 0, false, 0b01},
|
||||
{ 2500, 0, false, 0b11},
|
||||
{ 2580, 0, false, 0b01},
|
||||
{ 2583, 2360, false, 0b11},
|
||||
{ 2584, 2380, false, 0b11},
|
||||
{ 2587, 2340, false, 0b11},
|
||||
{ 2593, 2340, false, 0b01},
|
||||
{ 2607, 2340, false, 0b11},
|
||||
{ 2609, 2360, false, 0b11},
|
||||
{ 2615, 2360, false, 0b01},
|
||||
{ 2627, 2340, false, 0b01},
|
||||
{ 2629, 2360, false, 0b01},
|
||||
{ 2631, 2380, false, 0b11},
|
||||
{ 2644, 2340, false, 0b11},
|
||||
{ 2649, 2380, false, 0b11},
|
||||
{ 2651, 2380, false, 0b01},
|
||||
{ 2654, 2500, false, 0b11},
|
||||
{ 2665, 2360, false, 0b11},
|
||||
{ 2669, 2380, false, 0b01},
|
||||
{ 2672, 2360, false, 0b01},
|
||||
{ 2682, 2340, false, 0b11},
|
||||
{ 2687, 2380, false, 0b11},
|
||||
{ 2692, 2340, false, 0b11},
|
||||
{ 2695, 2500, false, 0b11},
|
||||
{ 2705, 2360, false, 0b11},
|
||||
{ 2707, 2380, false, 0b01},
|
||||
{ 2712, 2340, false, 0b01},
|
||||
{ 2717, 2520, false, 0b11},
|
||||
{ 2728, 2380, false, 0b11},
|
||||
{ 2730, 2560, false, 0b11},
|
||||
{ 2734, 2500, false, 0b11},
|
||||
{ 2758, 2340, false, 0b11},
|
||||
{ 2780, 2360, false, 0b11},
|
||||
{ 2787, 2520, false, 0b11},
|
||||
{ 2802, 2380, false, 0b11},
|
||||
{ 2809, 2540, false, 0b11},
|
||||
{ 2822, 2380, false, 0b01},
|
||||
{ 2831, 2560, false, 0b11},
|
||||
{ 2854, 2340, false, 0b11},
|
||||
{ 2875, 2360, false, 0b11},
|
||||
{ 2898, 2380, false, 0b11},
|
||||
{ 2918, 2380, false, 0b01},
|
||||
{ 2936, 2520, false, 0b01},
|
||||
{ 2944, 2380, false, 0b01},
|
||||
{ 2959, 2560, false, 0b11},
|
||||
{ 2976, 2340, false, 0b11},
|
||||
{ 2985, 2500, false, 0b01},
|
||||
{ 3003, 2340, false, 0b11},
|
||||
{ 3009, 2540, false, 0b11},
|
||||
{ 3027, 2380, false, 0b11},
|
||||
{ 3034, 2560, false, 0b11},
|
||||
{ 3050, 2380, false, 0b01},
|
||||
{ 3069, 2500, false, 0b11},
|
||||
{ 3094, 2520, false, 0b11},
|
||||
{ 3119, 2540, false, 0b11},
|
||||
{ 3144, 2560, false, 0b11},
|
||||
{ 3169, 2560, false, 0b01},
|
||||
{ 3180, 2500, false, 0b11},
|
||||
{ 3204, 2340, false, 0b11},
|
||||
{ 3232, 2360, false, 0b11},
|
||||
{ 3292, 2340, false, 0b01},
|
||||
{ 3340, 2380, false, 0b01},
|
||||
{ 3369, 2340, false, 0b11},
|
||||
{ 3399, 2360, false, 0b11},
|
||||
{ 3429, 2380, false, 0b11},
|
||||
{ 3464, 2500, false, 0b11},
|
||||
{ 3489, 2520, false, 0b11},
|
||||
{ 3512, 2540, false, 0b11},
|
||||
{ 3551, 2500, false, 0b01},
|
||||
{ 3582, 2540, false, 0b11},
|
||||
{ 3611, 2560, false, 0b11},
|
||||
{ 3639, 2520, false, 0b11},
|
||||
{ 3729, 2340, false, 0b11},
|
||||
{ 3817, 2380, false, 0b01},
|
||||
{ 3942, 2360, false, 0b01},
|
||||
{ 4049, 2540, false, 0b11},
|
||||
{ 4134, 2500, false, 0b01},
|
||||
{ 4194, 2560, false, 0b11},
|
||||
{ 4353, 2520, false, 0b11},
|
||||
{ 4449, 2360, false, 0b01},
|
||||
{ 4562, 2500, false, 0b11},
|
||||
{ 4672, 2560, false, 0b11},
|
||||
{ 4769, 2540, false, 0b11},
|
||||
{ 4849, 2560, false, 0b01},
|
||||
{ 4889, 2560, false, 0b11},
|
||||
{ 4929, 2560, false, 0b11},
|
||||
{ 4969, 2560, false, 0b11},
|
||||
{ 5009, 2560, false, 0b11},
|
||||
{ 5049, 2560, false, 0b11},
|
||||
{ 5092, 2360, false, 0b11},
|
||||
{ 5209, 2340, false, 0b01},
|
||||
{ 5298, 2380, false, 0b01},
|
||||
{ 5468, 2340, false, 0b01},
|
||||
{ 5582, 2520, false, 0b11},
|
||||
{ 5702, 2340, false, 0b11},
|
||||
{ 5888, 2520, false, 0b01},
|
||||
{ 6092, 2340, false, 0b01},
|
||||
{ 6240, 2560, false, 0b11},
|
||||
{ 6609, 2340, false, 0b11},
|
||||
{ 6752, 2380, false, 0b01},
|
||||
{ 6930, 2520, false, 0b01},
|
||||
{ 7000, 2560, false, 0b11},
|
||||
{ 7070, 2560, false, 0b01},
|
||||
{ 7251, 2580, false, 0b01},
|
||||
{ 0, 0, false, 0b00},
|
||||
};
|
||||
|
||||
/* tuning table optimized for TX */
|
||||
constexpr PralineTuneConfig praline_tune_config_tx[] = {
|
||||
{ 2100, 2375, true, 0b00},
|
||||
{ 2105, 2375, false, 0b00},
|
||||
{ 2115, 2425, false, 0b00},
|
||||
{ 2130, 2375, false, 0b00},
|
||||
{ 2150, 2425, false, 0b00},
|
||||
{ 2160, 2475, false, 0b00},
|
||||
{ 2175, 2425, false, 0b00},
|
||||
{ 2190, 2475, false, 0b00},
|
||||
{ 2195, 2425, false, 0b00},
|
||||
{ 2210, 2375, false, 0b00},
|
||||
{ 2248, 2425, false, 0b00},
|
||||
{ 2265, 2525, false, 0b00},
|
||||
{ 2300, 2425, false, 0b00},
|
||||
{ 2320, 2525, false, 0b00},
|
||||
{ 2580, 0, false, 0b00},
|
||||
{ 3000, 2325, false, 0b00},
|
||||
{ 3140, 2375, false, 0b00},
|
||||
{ 3200, 2425, false, 0b00},
|
||||
{ 3280, 2375, false, 0b00},
|
||||
{ 3340, 2425, false, 0b00},
|
||||
{ 3420, 2475, false, 0b00},
|
||||
{ 3480, 2525, false, 0b00},
|
||||
{ 3500, 2475, false, 0b00},
|
||||
{ 3595, 2425, false, 0b00},
|
||||
{ 3625, 2375, false, 0b00},
|
||||
{ 3670, 2475, false, 0b00},
|
||||
{ 3710, 2425, false, 0b00},
|
||||
{ 3760, 2525, false, 0b00},
|
||||
{ 3790, 2475, false, 0b00},
|
||||
{ 3860, 2425, false, 0b00},
|
||||
{ 3915, 2375, false, 0b00},
|
||||
{ 4000, 2425, false, 0b00},
|
||||
{ 4055, 2375, false, 0b00},
|
||||
{ 4125, 2425, false, 0b00},
|
||||
{ 4700, 2375, false, 0b00},
|
||||
{ 4800, 2425, false, 0b00},
|
||||
{ 5000, 2375, false, 0b00},
|
||||
{ 5260, 2475, false, 0b00},
|
||||
{ 5465, 2525, false, 0b00},
|
||||
{ 5560, 2375, false, 0b00},
|
||||
{ 5720, 2425, false, 0b00},
|
||||
{ 5860, 2475, false, 0b00},
|
||||
{ 5970, 2575, false, 0b00},
|
||||
{ 6000, 2375, false, 0b00},
|
||||
{ 6500, 2325, false, 0b00},
|
||||
{ 6750, 2375, false, 0b00},
|
||||
{ 6850, 2425, false, 0b00},
|
||||
{ 6950, 2475, false, 0b00},
|
||||
{ 7000, 2525, false, 0b00},
|
||||
{ 7251, 2575, false, 0b00},
|
||||
{ 0, 0, false, 0b00},
|
||||
};
|
||||
|
||||
// clang-format on
|
||||
|
||||
/* radio.c select_tune_config(): first entry whose range end is above the
|
||||
* requested frequency. The list is terminated by an all-zero entry, which is
|
||||
* also what a frequency past the end of the table lands on. */
|
||||
const PralineTuneConfig* select_tune_config(const rf::Frequency target_frequency, const bool transmit) {
|
||||
const PralineTuneConfig* entry = transmit ? praline_tune_config_tx : praline_tune_config_rx;
|
||||
const uint32_t freq_mhz = static_cast<uint32_t>(target_frequency / 1'000'000);
|
||||
|
||||
while ((entry->rf_range_end_mhz != 0) || (entry->if_mhz != 0)) {
|
||||
if ((target_frequency == 0) || (entry->rf_range_end_mhz > freq_mhz))
|
||||
break;
|
||||
entry++;
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
// Returns true for high-side injection, false for low-side
|
||||
constexpr bool praline_use_high_side_injection(const rf::Frequency target_frequency) {
|
||||
const uint32_t freq_mhz = target_frequency / 1'000'000;
|
||||
|
||||
// Based on hackrf_usb tune_config_rx table
|
||||
if (freq_mhz < 2100) {
|
||||
// Standard low-band: high-side injection
|
||||
// LO = IF + RF, mixer inverts spectrum
|
||||
return true;
|
||||
} else if (freq_mhz < 2105) {
|
||||
// Narrow transition: still high-side
|
||||
return true;
|
||||
} else if (freq_mhz < 2320) {
|
||||
// Near MAX2831 minimum: use low-side injection
|
||||
// LO = IF - RF, no spectrum inversion
|
||||
return false;
|
||||
} else {
|
||||
// Bypass/high-band - doesn't matter, mixer bypassed
|
||||
return false;
|
||||
}
|
||||
/* radio.c compute_offset(): a quarter of the AFE (ADC) sample rate, or zero if
|
||||
* no shift is in use or the AFE rate isn't known yet. */
|
||||
constexpr uint32_t quarter_shift_offset(const uint8_t shift, const uint32_t afe_rate) {
|
||||
return (shift == 0) ? 0 : (afe_rate / 4);
|
||||
}
|
||||
|
||||
/* radio.c analog_from_digital_rf(). */
|
||||
rf::Frequency analog_from_digital_rf(const rf::Frequency target_frequency, const uint8_t shift, const uint32_t afe_rate) {
|
||||
const rf::Frequency offset = quarter_shift_offset(shift, afe_rate);
|
||||
|
||||
if (shift == 0b11)
|
||||
return target_frequency + offset;
|
||||
|
||||
if (shift == 0b01)
|
||||
return (offset > target_frequency) ? (offset - target_frequency)
|
||||
: (target_frequency - offset);
|
||||
|
||||
return target_frequency;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
#endif // PRALINE
|
||||
|
||||
// Low band <2170 Mhz (HackRF One) or <2320 MHz (PRALINE):
|
||||
constexpr rf::Frequency low_band_second_lo_frequency(const rf::Frequency target_frequency) {
|
||||
Config low_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
|
||||
#ifdef PRALINE
|
||||
// Use the tune_config lookup for PRALINE
|
||||
return praline_get_if_frequency(target_frequency);
|
||||
#else
|
||||
return 2650'000'000 - (target_frequency / 7);
|
||||
#endif
|
||||
}
|
||||
const PralineTuneConfig* entry = select_tune_config(target_frequency, transmit);
|
||||
|
||||
Config low_band(const rf::Frequency target_frequency) {
|
||||
const rf::Frequency second_lo_frequency = low_band_second_lo_frequency(target_frequency);
|
||||
/* Past the end of the table: no usable configuration. */
|
||||
if ((entry->rf_range_end_mhz == 0) && (entry->if_mhz == 0))
|
||||
return {};
|
||||
|
||||
#ifdef PRALINE
|
||||
/* afe_rate == 0 means the caller doesn't know the ADC rate, so no LO offset
|
||||
* is applied. The FPGA rotation has to be dropped with it: rotating without
|
||||
* the matching offset moves the wanted signal off DC by afe_rate / 4. */
|
||||
const uint8_t shift = (afe_rate == 0) ? 0 : entry->shift;
|
||||
const rf::Frequency analog_rf = analog_from_digital_rf(target_frequency, shift, afe_rate);
|
||||
|
||||
/* if_mhz == 0 means the mixer is bypassed and the transceiver tunes the RF
|
||||
* directly; there is no first LO in that case. */
|
||||
const rf::Frequency second_lo_frequency =
|
||||
(entry->if_mhz == 0) ? analog_rf : (static_cast<rf::Frequency>(entry->if_mhz) * 1'000'000);
|
||||
|
||||
if (entry->if_mhz == 0)
|
||||
return {0, second_lo_frequency, rf::path::Band::Low, false, shift};
|
||||
|
||||
/* The low band always runs through the low-pass image-reject filter, so
|
||||
* the spectrum is inverted exactly when the first LO ends up above the IF,
|
||||
* i.e. for high-side injection. This is hackrf_usb.c radio_changed():
|
||||
* invert = (img_reject == RF_PATH_FILTER_LOW_PASS) && (freq_lo > freq_if)
|
||||
*/
|
||||
rf::Frequency first_lo_frequency;
|
||||
bool mixer_invert;
|
||||
|
||||
if (praline_use_high_side_injection(target_frequency)) {
|
||||
// High-side injection: LO = IF + RF
|
||||
first_lo_frequency = second_lo_frequency + target_frequency;
|
||||
if (entry->high_lo) {
|
||||
first_lo_frequency = second_lo_frequency + analog_rf;
|
||||
mixer_invert = true;
|
||||
} else {
|
||||
// Low-side injection: LO = IF - RF
|
||||
first_lo_frequency = second_lo_frequency - target_frequency;
|
||||
first_lo_frequency = second_lo_frequency - analog_rf;
|
||||
mixer_invert = false;
|
||||
}
|
||||
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert, shift};
|
||||
#else
|
||||
(void)afe_rate;
|
||||
(void)transmit;
|
||||
const rf::Frequency second_lo_frequency = 2650'000'000 - (target_frequency / 7);
|
||||
const rf::Frequency first_lo_frequency = target_frequency + second_lo_frequency;
|
||||
const bool mixer_invert = true;
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Low, mixer_invert};
|
||||
#endif
|
||||
}
|
||||
|
||||
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2580 MHz (PRALINE):
|
||||
Config mid_band(const rf::Frequency target_frequency) {
|
||||
// Mid band 2170-2740 Mhz (HackRF One) or 2320-2740 MHz (PRALINE):
|
||||
Config mid_band(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
|
||||
#ifdef PRALINE
|
||||
// For Praline with MAX2831 (2.3-2.6 GHz range)
|
||||
// Frequencies 2170-2300 MHz need upconversion since they're below MAX2831 minimum
|
||||
if (target_frequency < 2300'000'000) {
|
||||
// Treat as low band - need mixer
|
||||
return low_band(target_frequency);
|
||||
}
|
||||
// Frequencies 2300-2600 MHz can go direct (no RFFC5072)
|
||||
else if (target_frequency <= 2600'000'000) {
|
||||
const rf::Frequency second_lo_frequency = target_frequency;
|
||||
const rf::Frequency first_lo_frequency = 0;
|
||||
const bool mixer_invert = false;
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::Mid, mixer_invert};
|
||||
}
|
||||
// Frequencies 2600-2740 MHz need downconversion since they're above MAX2831 maximum
|
||||
else {
|
||||
// Treat as high band
|
||||
return high_band(target_frequency);
|
||||
/* radio.c select_img_reject() / tuning.c: on PRALINE the MAX2831 tunes
|
||||
* direct (mixer bypassed) from 2320 to 2580 MHz. band_mid starts at
|
||||
* TRANSITION = 2320 MHz, so everything below that already went to
|
||||
* low_band(). */
|
||||
if (target_frequency <= 2580'000'000) {
|
||||
const PralineTuneConfig* entry = select_tune_config(target_frequency, transmit);
|
||||
const uint8_t shift = (afe_rate == 0) ? 0 : entry->shift;
|
||||
const rf::Frequency analog_rf = analog_from_digital_rf(target_frequency, shift, afe_rate);
|
||||
|
||||
/* Mixer bypassed: no first LO, the MAX2831 tunes the (offset)
|
||||
* analogue RF directly and the FPGA rotates it back. */
|
||||
return {0, analog_rf, rf::path::Band::Mid, false, shift};
|
||||
}
|
||||
|
||||
/* 2580-2740 MHz: above the bypass window, downconvert. */
|
||||
return high_band(target_frequency);
|
||||
#else
|
||||
(void)afe_rate;
|
||||
(void)transmit;
|
||||
const rf::Frequency second_lo_frequency = target_frequency;
|
||||
const rf::Frequency first_lo_frequency = 0;
|
||||
const bool mixer_invert = false;
|
||||
@@ -203,12 +436,12 @@ Config high_band(const rf::Frequency target_frequency) {
|
||||
return {first_lo_frequency, second_lo_frequency, rf::path::Band::High, mixer_invert};
|
||||
}
|
||||
|
||||
Config create(const rf::Frequency target_frequency) {
|
||||
Config create(const rf::Frequency target_frequency, const uint32_t afe_rate, const bool transmit) {
|
||||
/* TODO: This is some lame code. */
|
||||
if (rf::path::band_low.contains(target_frequency)) {
|
||||
return low_band(target_frequency);
|
||||
return low_band(target_frequency, afe_rate, transmit);
|
||||
} else if (rf::path::band_mid.contains(target_frequency)) {
|
||||
return mid_band(target_frequency);
|
||||
return mid_band(target_frequency, afe_rate, transmit);
|
||||
} else if (rf::path::band_high.contains(target_frequency)) {
|
||||
return high_band(target_frequency);
|
||||
} else {
|
||||
|
||||
@@ -33,18 +33,21 @@ struct Config {
|
||||
: first_lo_frequency(0),
|
||||
second_lo_frequency(0),
|
||||
rf_path_band(rf::path::Band::Mid),
|
||||
mixer_invert(false) {
|
||||
mixer_invert(false),
|
||||
quarter_shift(0) {
|
||||
}
|
||||
|
||||
constexpr Config(
|
||||
rf::Frequency first_lo_frequency,
|
||||
rf::Frequency second_lo_frequency,
|
||||
rf::path::Band rf_path_band,
|
||||
bool mixer_invert)
|
||||
bool mixer_invert,
|
||||
uint8_t quarter_shift = 0)
|
||||
: first_lo_frequency(first_lo_frequency),
|
||||
second_lo_frequency(second_lo_frequency),
|
||||
rf_path_band(rf_path_band),
|
||||
mixer_invert(mixer_invert) {
|
||||
mixer_invert(mixer_invert),
|
||||
quarter_shift(quarter_shift) {
|
||||
}
|
||||
|
||||
bool is_valid() const {
|
||||
@@ -55,9 +58,23 @@ struct Config {
|
||||
const rf::Frequency second_lo_frequency;
|
||||
const rf::path::Band rf_path_band;
|
||||
const bool mixer_invert;
|
||||
|
||||
/* PRALINE only: FPGA RX quarter-rate shift mode, in the encoding the
|
||||
* gateware expects in the top two bits of register 0x03 (rx_pstep):
|
||||
* 0b00 = none, 0b11 = up, 0b01 = down.
|
||||
* Matches fpga_quarter_shift_mode_t in hackrf/firmware/common/fpga.h.
|
||||
* Always 0 on HackRF One (no FPGA). */
|
||||
const uint8_t quarter_shift;
|
||||
};
|
||||
|
||||
Config create(const rf::Frequency target_frequency);
|
||||
/* afe_rate is the ADC sample rate in Hz (output rate << decimation), needed on
|
||||
* PRALINE to work out how far off centre the quarter-rate shift places the
|
||||
* analogue passband. Pass 0 (or leave defaulted) to disable the shift.
|
||||
* transmit selects the TX tuning table. Both are ignored on HackRF One. */
|
||||
Config create(
|
||||
const rf::Frequency target_frequency,
|
||||
const uint32_t afe_rate = 0,
|
||||
const bool transmit = false);
|
||||
|
||||
} /* namespace config */
|
||||
} /* namespace tuning */
|
||||
|
||||
@@ -83,19 +83,35 @@ GeoPos::GeoPos(
|
||||
field_lon_seconds.on_change = changed_fn;
|
||||
|
||||
const auto wrapped_lat_seconds = [this](int32_t v) {
|
||||
const auto old_minutes = field_lat_minutes.value();
|
||||
field_lat_minutes.on_encoder(v);
|
||||
if (field_lat_minutes.value() == old_minutes) {
|
||||
field_lat_seconds.set_value((v > 0) ? 59 : 0);
|
||||
}
|
||||
};
|
||||
|
||||
const auto wrapped_lat_minutes = [this](int32_t v) {
|
||||
field_lat_degrees.on_encoder((field_lat_degrees.value() >= 0) ? v : -v);
|
||||
const auto old_degrees = field_lat_degrees.value();
|
||||
field_lat_degrees.on_encoder((old_degrees >= 0) ? v : -v);
|
||||
if (field_lat_degrees.value() == old_degrees) {
|
||||
field_lat_minutes.set_value((v > 0) ? 59 : 0);
|
||||
}
|
||||
};
|
||||
|
||||
const auto wrapped_lon_seconds = [this](int32_t v) {
|
||||
const auto old_minutes = field_lon_minutes.value();
|
||||
field_lon_minutes.on_encoder(v);
|
||||
if (field_lon_minutes.value() == old_minutes) {
|
||||
field_lon_seconds.set_value((v > 0) ? 59 : 0);
|
||||
}
|
||||
};
|
||||
|
||||
const auto wrapped_lon_minutes = [this](int32_t v) {
|
||||
field_lon_degrees.on_encoder((field_lon_degrees.value() >= 0) ? v : -v);
|
||||
const auto old_degrees = field_lon_degrees.value();
|
||||
field_lon_degrees.on_encoder((old_degrees >= 0) ? v : -v);
|
||||
if (field_lon_degrees.value() == old_degrees) {
|
||||
field_lon_minutes.set_value((v > 0) ? 59 : 0);
|
||||
}
|
||||
};
|
||||
|
||||
field_lat_seconds.on_wrap = wrapped_lat_seconds;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -125,10 +125,11 @@
|
||||
// ============================================================================
|
||||
// Canonical Default Values - SINGLE SOURCE OF TRUTH
|
||||
// ============================================================================
|
||||
/* Define the canonical RX defaults in ONE place */
|
||||
#define FPGA_RX_DEFAULT_DC_WIDTH 0x04 /* Typical for 40MHz stability */
|
||||
#define FPGA_RX_DEFAULT_ADAPT_RATE 0x08 /* Typical for 40MHz stability */
|
||||
#define FPGA_RX_DEFAULT_DIGITAL_GAIN 0x00 /* No shift initially */
|
||||
/* Define the canonical RX defaults in ONE place.
|
||||
* Matches fpga_init() in hackrf/firmware/common/fpga.c: the gateware only has
|
||||
* rx_decim and rx_pstep on the RX side, both starting at zero. */
|
||||
#define FPGA_RX_DEFAULT_DECIM 0x00 /* No decimation initially */
|
||||
#define FPGA_RX_DEFAULT_PSTEP 0x00 /* No quarter-rate shift initially */
|
||||
|
||||
/* Define TX defaults */
|
||||
#define FPGA_TX_DEFAULT_NCO_CTRL 0x00 /* NCO disabled */
|
||||
@@ -221,12 +222,13 @@ static bool fpga_cdone_read(void) {
|
||||
// These functions allow reading/writing FPGA internal registers via SPI.
|
||||
// The FPGA bitstream implements a simple SPI register interface.
|
||||
//
|
||||
// FPGA Register Map:
|
||||
// Reg 1 (CTRL): DC_BLOCK(b0), QUARTER_SHIFT_EN(b1), QUARTER_SHIFT_UP(b2), PRBS(b6), TRIGGER_EN(b7)
|
||||
// Reg 2 (RX_DECIM): Decimation ratio [2:0]
|
||||
// Reg 3 (RX/TX): RX Digital Shift OR TX NCO Control
|
||||
// Reg 4 (RX_DC_BLOCK_WIDTH/TX_INTERP) [2:0]
|
||||
// Reg 5 (RX_DC_ADAPT_RATE/TX_PSTEP) [7:0]
|
||||
// FPGA Register Map (hackrf/firmware/fpga/top/standard.py):
|
||||
// Reg 1 (CTRL): DC_BLOCK(b0), PRBS(b6), TRIGGER_EN(b7)
|
||||
// Reg 2 (RX_DECIM): Decimation ratio, log2 [2:0]
|
||||
// Reg 3 (RX_PSTEP): QUARTER_SHIFT_EN(b6), QUARTER_SHIFT_UP(b7)
|
||||
// Reg 4 (TX_CTRL): NCO enable (b0)
|
||||
// Reg 5 (TX_INTRP): Interpolation ratio [2:0]
|
||||
// Reg 6 (TX_PSTEP): NCO phase step [7:0]
|
||||
//
|
||||
// SPI Protocol:
|
||||
// Read: Send [reg & 0x7F, 0x00, 0x00] -> value in byte 3
|
||||
@@ -369,22 +371,16 @@ void fpga_rx_enable_dc_block(bool enable) {
|
||||
}
|
||||
|
||||
/* RX Functions with mode assertion */
|
||||
void fpga_rx_set_digital_gain(uint8_t shift) {
|
||||
|
||||
/* Quarter-rate shift, register 0x03 bits [7:6]. Equivalent to
|
||||
* fpga_set_rx_quarter_shift_mode() in hackrf/firmware/common/fpga.c.
|
||||
* mode: 0b00 none, 0b11 up, 0b01 down. */
|
||||
void fpga_rx_set_quarter_shift_mode(uint8_t mode) {
|
||||
if (current_mode != FPGA_MODE_RX) {
|
||||
/* Log error or assert - wrong mode! */
|
||||
return;
|
||||
}
|
||||
fpga_register_write(FPGA_REG_SHARED_3, shift & FPGA_RX_GAIN_SHIFT_MASK);
|
||||
}
|
||||
|
||||
void fpga_rx_set_dc_block_width(uint8_t width) {
|
||||
if (current_mode != FPGA_MODE_RX) return;
|
||||
fpga_register_write(FPGA_REG_SHARED_4, width & FPGA_RX_DC_WIDTH_MASK);
|
||||
}
|
||||
|
||||
void fpga_rx_set_dc_adapt_rate(uint8_t rate) {
|
||||
if (current_mode != FPGA_MODE_RX) return;
|
||||
fpga_register_write(FPGA_REG_SHARED_5, rate);
|
||||
fpga_register_write(FPGA_REG_RX_PSTEP, (uint8_t)((mode & 0x03) << FPGA_RX_QUARTER_SHIFT_SHIFT));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -394,12 +390,12 @@ void fpga_rx_set_dc_adapt_rate(uint8_t rate) {
|
||||
/* TX Functions with mode assertion */
|
||||
void fpga_tx_set_nco_enable(bool enable) {
|
||||
if (current_mode != FPGA_MODE_TX) return;
|
||||
uint8_t val = fpga_register_read(FPGA_REG3_TX_NCO_CTRL);
|
||||
uint8_t val = fpga_register_read(FPGA_REG_TX_CONTROL);
|
||||
if (enable)
|
||||
val |= FPGA_TX_NCO_EN;
|
||||
else
|
||||
val &= ~FPGA_TX_NCO_EN;
|
||||
fpga_register_write(FPGA_REG3_TX_NCO_CTRL, val);
|
||||
fpga_register_write(FPGA_REG_TX_CONTROL, val);
|
||||
}
|
||||
|
||||
void fpga_tx_set_interpolation(uint8_t ratio) {
|
||||
@@ -424,17 +420,17 @@ static void fpga_register_init(void) {
|
||||
current_mode = FPGA_MODE_RX;
|
||||
|
||||
fpga_spi_write(FPGA_REG_CTRL, FPGA_CTRL_DC_BLOCK_EN);
|
||||
fpga_spi_write(FPGA_REG_DECIM, 0x00);
|
||||
fpga_spi_write(FPGA_REG_SHARED_3, FPGA_RX_DEFAULT_DIGITAL_GAIN);
|
||||
fpga_spi_write(FPGA_REG_SHARED_4, FPGA_RX_DEFAULT_DC_WIDTH);
|
||||
fpga_spi_write(FPGA_REG_SHARED_5, FPGA_RX_DEFAULT_ADAPT_RATE);
|
||||
fpga_spi_write(FPGA_REG_DECIM, FPGA_RX_DEFAULT_DECIM);
|
||||
fpga_spi_write(FPGA_REG_RX_PSTEP, FPGA_RX_DEFAULT_PSTEP);
|
||||
fpga_spi_write(FPGA_REG_TX_CONTROL, FPGA_TX_DEFAULT_NCO_CTRL);
|
||||
fpga_spi_write(FPGA_REG_TX_INTERP, FPGA_TX_DEFAULT_INTERP);
|
||||
|
||||
/* Update cache */
|
||||
fpga_reg_cache[1] = FPGA_CTRL_DC_BLOCK_EN;
|
||||
fpga_reg_cache[2] = 0x00;
|
||||
fpga_reg_cache[3] = FPGA_RX_DEFAULT_DIGITAL_GAIN;
|
||||
fpga_reg_cache[4] = FPGA_RX_DEFAULT_DC_WIDTH;
|
||||
fpga_reg_cache[5] = FPGA_RX_DEFAULT_ADAPT_RATE;
|
||||
fpga_reg_cache[2] = FPGA_RX_DEFAULT_DECIM;
|
||||
fpga_reg_cache[3] = FPGA_RX_DEFAULT_PSTEP;
|
||||
fpga_reg_cache[4] = FPGA_TX_DEFAULT_NCO_CTRL;
|
||||
fpga_reg_cache[5] = FPGA_TX_DEFAULT_INTERP;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -16,10 +16,23 @@ extern "C" {
|
||||
|
||||
#ifdef PRALINE
|
||||
|
||||
/* RX path (legacy PRALINE software map kept for compatibility) */
|
||||
#define FPGA_REG_RX_DIGITAL_GAIN 0x03 /* Digital Shift / scaling (RX Mode) */
|
||||
#define FPGA_REG_RX_DC_BLOCK_WIDTH 0x04 /* Notch filter cutoff (RX Mode) */
|
||||
#define FPGA_REG_RX_DC_ADAPT_RATE 0x05 /* Settle time/Integration (RX Mode) */
|
||||
/*
|
||||
* RX path.
|
||||
*
|
||||
* NOTE: the register map below is the one the loaded bitstream actually
|
||||
* implements. Ground truth is hackrf/firmware/fpga/top/standard.py plus
|
||||
* hackrf/firmware/common/fpga_regs.def; Mayhem, hackrf_usb and debug_for_adsb
|
||||
* all load a byte-identical praline_fpga.bin, so that map applies here too.
|
||||
*
|
||||
* An earlier "legacy PRALINE software map" claimed register 0x03 was an RX
|
||||
* digital gain and 0x04/0x05 were DC-block width / adaptation rate. The
|
||||
* gateware has none of those: 0x03 is rx_pstep (whose top two bits are the
|
||||
* quarter-rate shift) and 0x04/0x05 are TX registers. Writing the old "RX DC
|
||||
* width" value of 0x01 to 0x04 actually set tx_ctrl[0] and switched the TX NCO
|
||||
* on, and every write of the fictional digital gain to 0x03 cleared the
|
||||
* quarter shift.
|
||||
*/
|
||||
#define FPGA_REG_RX_PSTEP 0x03 /* RX phase step; bits [7:6] = quarter shift */
|
||||
|
||||
/*
|
||||
* TX path must match the currently built PRALINE standard gateware in
|
||||
@@ -43,70 +56,55 @@ typedef enum {
|
||||
|
||||
/*
|
||||
* FPGA Register Addresses
|
||||
* NOTE:
|
||||
* The currently loaded PRALINE standard gateware uses:
|
||||
* 0x01 CTRL
|
||||
* 0x02 RX_DECIM
|
||||
* 0x03 RX_DIGITAL_GAIN (RX) / TX_NCO_CTRL (TX)
|
||||
* 0x04 TX_CTRL
|
||||
* 0x05 TX_INTRP
|
||||
* 0x06 TX_PSTEP
|
||||
*
|
||||
* From hackrf/firmware/fpga/top/standard.py (spi_regs.add_register):
|
||||
* 0x01 ctrl 8 bits
|
||||
* 0x02 rx_decim 3 bits
|
||||
* 0x03 rx_pstep 8 bits
|
||||
* 0x04 tx_ctrl 1 bit
|
||||
* 0x05 tx_intrp 3 bits
|
||||
* 0x06 tx_pstep 8 bits
|
||||
*
|
||||
* There is no RX gain register: rx_decim selects half-band FIR stages
|
||||
* (hbfir1..hbfir5), which are unity-gain, so there is no CIC bit growth to
|
||||
* renormalise.
|
||||
*/
|
||||
#define FPGA_REG_CTRL 0x01 /* Control register */
|
||||
#define FPGA_REG_DECIM 0x02 /* RX decimation */
|
||||
#define FPGA_REG_SHARED_3 0x03 /* Legacy shared register */
|
||||
#define FPGA_REG_SHARED_4 0x04 /* Legacy shared register */
|
||||
#define FPGA_REG_SHARED_5 0x05 /* Legacy shared register */
|
||||
#define FPGA_REG_SHARED_6 0x06 /* TX phase step */
|
||||
#define FPGA_REG_CTRL 0x01 /* Control register */
|
||||
#define FPGA_REG_DECIM 0x02 /* RX decimation (log2, bits [2:0]) */
|
||||
/* 0x03..0x06 are FPGA_REG_RX_PSTEP / FPGA_REG_TX_CONTROL /
|
||||
* FPGA_REG_TX_INTERP / FPGA_REG_TX_PHASE_STEP, defined above. */
|
||||
|
||||
/*
|
||||
* Register 1 (CTRL) Bit Definitions
|
||||
* standard.py: ctrl[0] -> dc_block.enable, ctrl[6] -> prbs, ctrl[7] -> trigger_en.
|
||||
* Nothing else in this register is decoded.
|
||||
*/
|
||||
#define FPGA_CTRL_DC_BLOCK_EN (1 << 0) /* DC block enable */
|
||||
#define FPGA_CTRL_QUARTER_SHIFT_EN (1 << 1) /* Quarter-rate shift enable */
|
||||
#define FPGA_CTRL_QUARTER_SHIFT_UP (1 << 2) /* Shift direction: 1=up, 0=down */
|
||||
#define FPGA_CTRL_TX_MODE (1 << 5) /* TX mode indicator (if applicable) */
|
||||
#define FPGA_CTRL_PRBS_EN (1 << 6) /* PRBS test mode */
|
||||
#define FPGA_CTRL_TRIGGER_EN (1 << 7) /* External trigger enable */
|
||||
#define FPGA_CTRL_DC_BLOCK_EN (1 << 0) /* DC block enable */
|
||||
#define FPGA_CTRL_PRBS_EN (1 << 6) /* PRBS test mode */
|
||||
#define FPGA_CTRL_TRIGGER_EN (1 << 7) /* External trigger enable */
|
||||
|
||||
/*
|
||||
* Register 3 Dual-Purpose Definitions
|
||||
* Register 3 (RX_PSTEP) Bit Definitions
|
||||
* standard.py: rx_pstep[6] -> quarter_shift.enable, rx_pstep[7] -> quarter_shift.up.
|
||||
* Same encoding as fpga_quarter_shift_mode_t << 6 in hackrf/firmware/common/fpga.c.
|
||||
*/
|
||||
/* RX Mode: Digital gain/shift */
|
||||
#define FPGA_REG3_RX_DIGITAL_GAIN 0x03
|
||||
#define FPGA_RX_GAIN_SHIFT_MASK 0x0F /* Bits [3:0] - shift amount */
|
||||
/* Position of that 2-bit field inside rx_pstep, for direct register writes. */
|
||||
#define FPGA_RX_QUARTER_SHIFT_SHIFT 6
|
||||
#define FPGA_RX_QUARTER_SHIFT_MASK 0xC0
|
||||
|
||||
/* TX Mode: NCO control */
|
||||
#define FPGA_REG3_TX_NCO_CTRL 0x03
|
||||
#define FPGA_TX_NCO_EN (1 << 0) /* NCO enable */
|
||||
#define FPGA_TX_NCO_INVERT (1 << 1) /* Invert spectrum */
|
||||
/* Mode values, matching fpga_quarter_shift_mode_t in
|
||||
* hackrf/firmware/common/fpga.h. Pass these to
|
||||
* fpga_rx_set_quarter_shift_mode(), which shifts them into place. */
|
||||
#define FPGA_QUARTER_SHIFT_MODE_NONE 0b00
|
||||
#define FPGA_QUARTER_SHIFT_MODE_UP 0b11
|
||||
#define FPGA_QUARTER_SHIFT_MODE_DOWN 0b01
|
||||
|
||||
/*
|
||||
* Register 4 Dual-Purpose Definitions
|
||||
* Register 4 (TX_CTRL) / 5 (TX_INTRP) / 6 (TX_PSTEP) Bit Definitions
|
||||
*/
|
||||
/* RX Mode: DC block notch width */
|
||||
#define FPGA_REG4_RX_DC_WIDTH 0x04
|
||||
#define FPGA_RX_DC_WIDTH_MASK 0x07 /* Bits [2:0] */
|
||||
|
||||
/* TX Mode: Interpolation ratio */
|
||||
#define FPGA_REG4_TX_INTERP 0x05
|
||||
#define FPGA_TX_INTERP_MASK 0x07 /* Bits [2:0] */
|
||||
|
||||
/*
|
||||
* Register 5 Dual-Purpose Definitions
|
||||
*/
|
||||
/* RX Mode: DC block adaptation rate */
|
||||
#define FPGA_REG5_RX_DC_RATE 0x05
|
||||
#define FPGA_RX_DC_RATE_MASK 0xFF /* Bits [7:0] */
|
||||
|
||||
/* TX Mode: NCO phase step (frequency) */
|
||||
#define FPGA_REG5_TX_PHASE_STEP 0x06
|
||||
#define FPGA_TX_PHASE_STEP_MASK 0xFF /* Bits [7:0] */
|
||||
|
||||
/* Export default values so other methods can use them */
|
||||
#define FPGA_RX_DEFAULT_DIGITAL_GAIN 0x00
|
||||
#define FPGA_RX_DEFAULT_DC_WIDTH 0x04
|
||||
#define FPGA_RX_DEFAULT_ADAPT_RATE 0x08
|
||||
#define FPGA_TX_NCO_EN (1 << 0) /* tx_ctrl[0]: NCO enable */
|
||||
#define FPGA_TX_INTERP_MASK 0x07 /* tx_intrp bits [2:0] */
|
||||
#define FPGA_TX_PHASE_STEP_MASK 0xFF /* tx_pstep bits [7:0] */
|
||||
|
||||
/*
|
||||
* Core Functions
|
||||
@@ -133,9 +131,8 @@ void fpga_register_write(uint8_t reg, uint8_t value);
|
||||
* RX Mode Functions (only valid when mode == FPGA_MODE_RX)
|
||||
*/
|
||||
void fpga_rx_set_decimation(uint8_t ratio);
|
||||
void fpga_rx_set_digital_gain(uint8_t shift);
|
||||
void fpga_rx_set_dc_block_width(uint8_t width);
|
||||
void fpga_rx_set_dc_adapt_rate(uint8_t rate);
|
||||
/* mode is the gateware encoding: 0b00 none, 0b11 up, 0b01 down. */
|
||||
void fpga_rx_set_quarter_shift_mode(uint8_t mode);
|
||||
void fpga_rx_enable_dc_block(bool enable);
|
||||
|
||||
/*
|
||||
@@ -154,12 +151,13 @@ void fpga_tx_set_phase_step(uint8_t step);
|
||||
* reg: Register number (1-5)
|
||||
* Returns: Register value, or 0xFF if invalid register
|
||||
*
|
||||
* FPGA Register Map:
|
||||
* Reg 1 (CTRL): DC_BLOCK(b0), QUARTER_SHIFT_EN(b1), QUARTER_SHIFT_UP(b2), PRBS(b6), TRIGGER_EN(b7)
|
||||
* Reg 2 (RX_DECIM): Decimation ratio [2:0]
|
||||
* Reg 3 (RX/TX): RX Digital Shift OR TX NCO Control
|
||||
* Reg 4 (RX_DC_BLOCK_WIDTH/TX_INTERP) [2:0]
|
||||
* Reg 5 (RX_DC_ADAPT_RATE/TX_PSTEP) [7:0]
|
||||
* FPGA Register Map (hackrf/firmware/fpga/top/standard.py):
|
||||
* Reg 1 (CTRL): DC_BLOCK(b0), PRBS(b6), TRIGGER_EN(b7)
|
||||
* Reg 2 (RX_DECIM): Decimation ratio, log2 [2:0]
|
||||
* Reg 3 (RX_PSTEP): QUARTER_SHIFT_EN(b6), QUARTER_SHIFT_UP(b7)
|
||||
* Reg 4 (TX_CTRL): NCO_EN(b0)
|
||||
* Reg 5 (TX_INTRP): Interpolation ratio, log2 [2:0]
|
||||
* Reg 6 (TX_PSTEP): NCO phase step [7:0]
|
||||
*/
|
||||
uint8_t fpga_debug_register_read(uint8_t reg);
|
||||
|
||||
|
||||
+139
@@ -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())
|
||||
Executable
+187
@@ -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()
|
||||
Executable
+214
@@ -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()
|
||||
+1
-1
Submodule hackrf updated: 365dfbc8c4...749bca5f10
Reference in New Issue
Block a user