From 3a54b112ec7fddd40e8d2d4c9287247542ac3a97 Mon Sep 17 00:00:00 2001 From: stafur Date: Sat, 28 Feb 2026 17:36:01 -0500 Subject: [PATCH] Imrpoved sound quality in analog_audio_app, and added Nyquist protection option to BBW filter for praline. (#3054) --- .../application/apps/analog_audio_app.cpp | 4 + firmware/application/apps/ui_debug.cpp | 75 ++++++++++++++++- firmware/application/apps/ui_debug.hpp | 73 ++++++++++++++++ firmware/application/clock_manager.cpp | 84 ++++++++----------- firmware/application/hw/si5351.hpp | 10 +++ firmware/application/portapack.cpp | 19 ----- firmware/application/receiver_model.cpp | 34 ++++++-- 7 files changed, 225 insertions(+), 74 deletions(-) diff --git a/firmware/application/apps/analog_audio_app.cpp b/firmware/application/apps/analog_audio_app.cpp index cadbc092e..682ee06a8 100644 --- a/firmware/application/apps/analog_audio_app.cpp +++ b/firmware/application/apps/analog_audio_app.cpp @@ -574,7 +574,11 @@ void AnalogAudioView::update_modulation(ReceiverModel::Mode modulation) { const auto is_wideband_spectrum_mode = (modulation == ReceiverModel::Mode::SpectrumAnalysis); receiver_model.set_modulation(modulation); +#ifdef PRALINE + receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 3023000); +#else receiver_model.set_sampling_rate(is_wideband_spectrum_mode ? spec_bw : 3072000); +#endif receiver_model.set_baseband_bandwidth(is_wideband_spectrum_mode ? spec_bw / 2 : 1750000); receiver_model.set_hidden_offset(modulation == ReceiverModel::Mode::AMAudioFMApt ? -2200 : 0); // wefax needs to be shifted, see wefax rx app. diff --git a/firmware/application/apps/ui_debug.cpp b/firmware/application/apps/ui_debug.cpp index 2c45798d7..1751fc418 100644 --- a/firmware/application/apps/ui_debug.cpp +++ b/firmware/application/apps/ui_debug.cpp @@ -750,7 +750,7 @@ void WFMAudioDebugView::refresh() { // === Status Summary === bool sample_rate_ok = (clk0_khz >= 3000 && clk0_khz <= 3200); - bool lpf_ok = (lpf_coarse >= 0); // 7.5 MHz minimum is technically OK + bool lpf_ok = (lpf_coarse <= 0x0F); // check against the 4-bit max bool dc_ok = dc_block; if (sample_rate_ok && lpf_ok && dc_ok) { @@ -2182,6 +2182,78 @@ void SystemDiagnosticsView::refresh() { } #endif +#ifdef PRALINE +PralineClockDebugView::PralineClockDebugView(NavigationView& nav) + : View(), + rows{ + {&t0_id, &t0_ma, &t0_mode, &t0_ph, &t0_st}, + {&t1_id, &t1_ma, &t1_mode, &t1_ph, &t1_st}, + {&t2_id, &t2_ma, &t2_mode, &t2_ph, &t2_st}, + {&t3_id, &t3_ma, &t3_mode, &t3_ph, &t3_st}, + {&t4_id, &t4_ma, &t4_mode, &t4_ph, &t4_st}, + {&t5_id, &t5_ma, &t5_mode, &t5_ph, &t5_st}} { + add_children({&text_title, &text_lbl_pll, &text_pll_status, + &text_lbl_afe, &text_afe_rate, &text_lbl_n, &text_n_val, + &text_header, + &t0_id, &t0_ma, &t0_mode, &t0_ph, &t0_st, + &t1_id, &t1_ma, &t1_mode, &t1_ph, &t1_st, + &t2_id, &t2_ma, &t2_mode, &t2_ph, &t2_st, + &t3_id, &t3_ma, &t3_mode, &t3_ph, &t3_st, + &t4_id, &t4_ma, &t4_mode, &t4_ph, &t4_st, + &t5_id, &t5_ma, &t5_mode, &t5_ph, &t5_st, + &button_refresh, &button_done}); + + button_refresh.on_select = [this](Button&) { this->refresh(); }; + button_done.on_select = [&nav](Button&) { nav.pop(); }; + + refresh(); +} + +void PralineClockDebugView::focus() { + button_refresh.focus(); +} + +void PralineClockDebugView::refresh() { + // 1. System Status + uint8_t status = portapack::clock_manager.si5351_read_status(); + bool pll_a = !(status & 0x20); + bool pll_b = !(status & 0x40); + text_pll_status.set(std::string(pll_a ? "A:OK " : "A:ERR ") + (pll_b ? "B:OK" : "B:ERR")); + text_pll_status.set_style((pll_a && pll_b) ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + + // 2. AFE & Decimation Info + uint32_t base_rate = portapack::clock_manager.get_sampling_frequency(); + uint8_t n = portapack::clock_manager.get_resampling_n(); + text_afe_rate.set(to_string_dec_uint(base_rate << n) + " Hz"); + text_n_val.set(to_string_dec_uint(n)); + + // 3. Clock Table Decoding + uint8_t output_en = portapack::clock_manager.si5351_read_register(3); + const char* ma_lookup[] = {"2m", "4m", "6m", "8m"}; + + for (size_t i = 0; i < 6; i++) { + uint8_t ctrl = portapack::clock_manager.si5351_read_register(16 + i); + + // mA (Bits 1:0) + rows[i].ma->set(ma_lookup[ctrl & 0x03]); + + // Mode (Bit 6: 1=Integer, 0=Fractional) + rows[i].mode->set((ctrl & 0x40) ? "INT" : "FRAC"); + rows[i].mode->set_style((ctrl & 0x40) ? Theme::getInstance()->fg_blue : Theme::getInstance()->fg_yellow); + + // Phase (Bit 4: 1=Inverted, 0=Normal) + // Use 0x10 (Bit 4) + rows[i].phase->set((ctrl & 0x10) ? "INVRT" : "NORM "); + rows[i].phase->set_style((ctrl & 0x10) ? Theme::getInstance()->fg_orange : Theme::getInstance()->fg_light); + + // Status (Powered On and Output Enabled) + bool is_on = !(ctrl & 0x80) && !(output_en & (1 << i)); + rows[i].stat->set(is_on ? "ON" : "OFF"); + rows[i].stat->set_style(is_on ? Theme::getInstance()->fg_green : Theme::getInstance()->fg_red); + } +} +#endif + #ifdef PRALINE /* GPIODebugView *************************************************/ GPIODebugView::GPIODebugView(NavigationView& nav) { @@ -3003,6 +3075,7 @@ void DebugMenuView::on_populate() { add_items({ #ifdef PRALINE {"System Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, + {"PRO Clocks", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, {"Radio Diag", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, {"WFM Audio", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, {"ProRadio Debug", ui::Theme::getInstance()->fg_yellow->foreground, &bitmap_icon_peripherals, [this]() { nav_.push(); }}, diff --git a/firmware/application/apps/ui_debug.hpp b/firmware/application/apps/ui_debug.hpp index 9c1ed5a00..b08873641 100644 --- a/firmware/application/apps/ui_debug.hpp +++ b/firmware/application/apps/ui_debug.hpp @@ -1125,6 +1125,79 @@ class SystemDiagnosticsView : public View { }; #endif +#ifdef PRALINE +class PralineClockDebugView : public View { + public: + PralineClockDebugView(NavigationView& nav); + void focus() override; + std::string title() const override { return "Pro Clock Status"; }; + + private: + void refresh(); + + Text text_title{{0, 0, 240, 16}, "=== Pro Clock Dashboard ==="}; + + // System Status + Text text_lbl_pll{{0, 20, 80, 16}, "PLL Lock:"}; + Text text_pll_status{{88, 20, 152, 16}, "---"}; + + Text text_lbl_afe{{0, 36, 80, 16}, "AFE Rate:"}; + Text text_afe_rate{{88, 36, 152, 16}, "---"}; + + Text text_lbl_n{{0, 52, 80, 16}, "Decim (n):"}; + Text text_n_val{{88, 52, 152, 16}, "-"}; + + // Table Header + Text text_header{{0, 72, 240, 16}, "ID mA Mode Phase Stat"}; + + // Helper structure to group row widgets for CLK0-CLK5 + struct ClockRow { + Text* id; + Text* ma; + Text* mode; + Text* phase; + Text* stat; + }; + std::vector rows; + + // We define the actual widgets for 6 clocks + // Note: Layout uses 16px vertical spacing per row + Text t0_id{{0, 88, 24, 16}, "C0:"}; + Text t0_ma{{32, 88, 24, 16}, "-"}; + Text t0_mode{{64, 88, 48, 16}, "-"}; + Text t0_ph{{128, 88, 56, 16}, "-"}; + Text t0_st{{192, 88, 48, 16}, "-"}; + Text t1_id{{0, 104, 24, 16}, "C1:"}; + Text t1_ma{{32, 104, 24, 16}, "-"}; + Text t1_mode{{64, 104, 48, 16}, "-"}; + Text t1_ph{{128, 104, 56, 16}, "-"}; + Text t1_st{{192, 104, 48, 16}, "-"}; + Text t2_id{{0, 120, 24, 16}, "C2:"}; + Text t2_ma{{32, 120, 24, 16}, "-"}; + Text t2_mode{{64, 120, 48, 16}, "-"}; + Text t2_ph{{128, 120, 56, 16}, "-"}; + Text t2_st{{192, 120, 48, 16}, "-"}; + Text t3_id{{0, 136, 24, 16}, "C3:"}; + Text t3_ma{{32, 136, 24, 16}, "-"}; + Text t3_mode{{64, 136, 48, 16}, "-"}; + Text t3_ph{{128, 136, 56, 16}, "-"}; + Text t3_st{{192, 136, 48, 16}, "-"}; + Text t4_id{{0, 152, 24, 16}, "C4:"}; + Text t4_ma{{32, 152, 24, 16}, "-"}; + Text t4_mode{{64, 152, 48, 16}, "-"}; + Text t4_ph{{128, 152, 56, 16}, "-"}; + Text t4_st{{192, 152, 48, 16}, "-"}; + Text t5_id{{0, 168, 24, 16}, "C5:"}; + Text t5_ma{{32, 168, 24, 16}, "-"}; + Text t5_mode{{64, 168, 48, 16}, "-"}; + Text t5_ph{{128, 168, 56, 16}, "-"}; + Text t5_st{{192, 168, 48, 16}, "-"}; + + Button button_refresh{{8, 260, 100, 24}, "Refresh"}; + Button button_done{{132, 260, 100, 24}, "Done"}; +}; +#endif + #endif class DebugPeripheralsMenuView : public BtnGridView { diff --git a/firmware/application/clock_manager.cpp b/firmware/application/clock_manager.cpp index e44a5741f..e66daade1 100644 --- a/firmware/application/clock_manager.cpp +++ b/firmware/application/clock_manager.cpp @@ -247,21 +247,20 @@ constexpr ClockControls si5351c_clock_control_common{{ constexpr ClockControls si5351a_clock_control_common{{ #ifdef PRALINE - // CLK0: MAX5864 (ADC) - {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, - // CLK1: SCT_CLK - iCE40 FPGA timing clock - {ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, - // CLK2: LPC43xx MCU - {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, - // CLK3: CLKOUT (optional) SMA Port P1 - {ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, - // CLK4: PRALINE MAX2831 reference (40 MHz) - INVERTED per hackrf_usb, 4mA, Integer mode - {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, - // CLK5: PRALINE RFFC5072 reference (40 MHz) - INVERTED, 6mA, Integer mode - // This matches HackRF One OG configuration for RFFC5072 - {ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, - // CLK6: SMA Port P2 - {ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, + // CLK0: MAX5864 (ADC) - 4mA, Inverted (Standard for Praline sync) + {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_On}, + // CLK1: SCT_CLK (iCE40 FPGA) - 6mA, Inverted (Fixes 30-60Hz Drumming) + {ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, + // CLK2: LPC43xx MCU - 4mA, Normal (Must be Integer for MCU stability) + {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, + // CLK3: CLKOUT SMA Port P1 - 8mA, Normal + {ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, + // CLK4: MAX2831 reference (40 MHz) - Inverted (Required for mixer lock) + {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, + // CLK5: RFFC5072 reference (40 MHz) - Inverted (Required for mixer lock) + {ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Invert, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, + // CLK6: SMA Port P2 - 8mA, Normal + {ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_On}, #else {ClockControl::ClockCurrentDrive::_6mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, {ClockControl::ClockCurrentDrive::_4mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off}, @@ -274,7 +273,6 @@ constexpr ClockControls si5351a_clock_control_common{{ // CLK6: Not used {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, #endif - // CLK7: Not used {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, ClockControl::MultiSynthSource::PLLA, ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, @@ -469,7 +467,9 @@ void ClockManager::init_clock_generator() { si5351a_clock_control_common[6].ms_src(ref_pll), si5351a_clock_control_common[7].ms_src(ref_pll), }}; - clock_generator.set_clock_control(si5351_clock_control); + // clock_generator.set_clock_control(si5351_clock_control); + // Use single-byte writes instead of multi-byte + clock_generator.set_clock_control_single_byte(si5351_clock_control); #else if (hackrf_r9) { const PLLReg pll_reg = (reference.source == ReferenceSource::Xtal) @@ -686,53 +686,41 @@ void ClockManager::disable_if_clocks() { void ClockManager::set_sampling_frequency(const uint32_t frequency) { #ifdef PRALINE - /* PRALINE: CLK0=AFE_CLK runs at sample rate (VCO/divider/2) - * CLK1=SCT_CLK runs at 2x sample rate (VCO/divider/1) - * Reference: hackrf_core.c sample_rate_frac_set() lines 580-582 - */ - - /* PRALINE: Match HackRF USB sample_rate_frac_set() - * Reference: hackrf_usb radio.c lines 29-91, hackrf_core.c lines 501-685 */ - - _base_band_frequency = frequency; // Store frequency for StatusViews - /* - * PRALINE sample rate strategy from GSG hackrf_usb radio.c: - * - * 1. Run ADC at the highest rate possible (up to 40 MHz) + * PRALINE sample rate strategy: + * 1. Maximize AFE rate to push Nyquist above MAX2831's 11.6 MHz LPF minimum * 2. Use FPGA decimation to achieve desired output rate - * 3. This makes the analog LPF effective at rejecting aliases - * 4. Re-apply frequency after to reconfigure LPF bandwidth + * 3. Ensure AFE rate is achievable by Si5351 (clean division from 800 MHz VCO) */ - // 20 MHz, since GSG reference of 40MHz caused shifts at certain values. - constexpr uint32_t MAX_AFE_RATE = 20000000; - constexpr uint8_t MAX_N = 5; // Max decimation = 2^5 = 32 + constexpr uint32_t MAX_AFE_RATE = 40000000; // Use 40 MHz per GSG reference + constexpr uint8_t MAX_N = 5; - // Calculate optimal decimation factor for RX - // Start with n=1 (minimum decimation of 2) per reference - uint8_t n = 1; - uint32_t afe_rate_x2 = 2 * frequency; + _base_band_frequency = frequency; - while ((afe_rate_x2 <= MAX_AFE_RATE) && (n < MAX_N)) { - afe_rate_x2 <<= 1; + uint8_t n = 0; + uint32_t afe_rate = frequency; + + // Find the largest n where AFE rate stays within limit + // Start at n=0 and work up + while (n < MAX_N) { + uint32_t next_rate = afe_rate << 1; + if (next_rate > MAX_AFE_RATE) break; + afe_rate = next_rate; n++; } - // Store decimation factor for potential use elsewhere _resampling_n = n; - // The actual AFE rate = frequency * 2^n - uint32_t afe_rate = frequency << n; - // Set FPGA RX decimation register fpga_debug_register_write(2, n); radio::invalidate_spi_config(); // Configure Si5351 clocks - clock_generator.set_ms_frequency(0, afe_rate * 4, si5351_vco_f, 2); // CLK0: AFE_CLK - clock_generator.set_ms_frequency(1, afe_rate * 4, si5351_vco_f, 1); // CLK1: SCT_CLK - + // CLK0: AFE_CLK (with r_div=1 for ÷2) + // CLK1: SCT_CLK (with r_div=0 for ÷1, runs at 2× AFE for FPGA timing) + clock_generator.set_ms_frequency(0, afe_rate * 2, si5351_vco_f, 1); + clock_generator.set_ms_frequency(1, afe_rate * 2, si5351_vco_f, 0); #else /* Codec clock is at sampling frequency, CPLD and SGPIO clocks are at * twice the frequency, and derived from the MS0 synth. So it's only diff --git a/firmware/application/hw/si5351.hpp b/firmware/application/hw/si5351.hpp index 061e7dcec..b03a61fb0 100644 --- a/firmware/application/hw/si5351.hpp +++ b/firmware/application/hw/si5351.hpp @@ -373,6 +373,16 @@ class Si5351 { #endif } +#ifdef PRALINE + void set_clock_control_single_byte(const ClockControls& clock_control) { + _clock_control = clock_control; + // Use single-byte writes for PRALINE (multi-byte I2C fails) + for (size_t i = 0; i < 8; i++) { + write_register(Register::CLKControl_Base + i, _clock_control[i]); + } + } +#endif + bool plla_loss_of_signal() { return (device_status() >> 5) & 1; } diff --git a/firmware/application/portapack.cpp b/firmware/application/portapack.cpp index 770ad8264..a18fcbb64 100644 --- a/firmware/application/portapack.cpp +++ b/firmware/application/portapack.cpp @@ -589,25 +589,6 @@ init_status_t init() { clock_manager.init_clock_generator(); -#ifdef PRALINE - // Force CLK4/CLK5 configuration BEFORE I2C bus stops - // This ensures the inversion bits are written while I2C is still active - - // CLK4 (MAX2831): ON, Integer, PLLA, INVERTED, MS_Self, 4mA = 0x5D - clock_manager.si5351_write_register(20, 0x5D); - - // CLK5 (RFFC5072): ON, Integer, PLLA, INVERTED, MS_Self, 6mA = 0x5E - clock_manager.si5351_write_register(21, 0x5E); - - // Enable CLK4 and CLK5 outputs NOW (before I2C stops) - uint8_t reg3 = clock_manager.si5351_read_register(3); - reg3 &= ~0x30; // Clear bits 4 and 5 to enable - clock_manager.si5351_write_register(3, reg3); - - // Wait for clocks to stabilize - chThdSleepMilliseconds(10); -#endif - i2c0.stop(); chThdSleepMilliseconds(10); diff --git a/firmware/application/receiver_model.cpp b/firmware/application/receiver_model.cpp index 0a89618fa..716286c16 100644 --- a/firmware/application/receiver_model.cpp +++ b/firmware/application/receiver_model.cpp @@ -326,12 +326,23 @@ void ReceiverModel::update_baseband_bandwidth() { if (enabled_) { #ifdef PRALINE /* - * PRALINE LPF bandwidth calculation from GSG hackrf_usb radio.c: + * PRALINE LPF bandwidth calculation from GSG hackrf_usb radio.c * - * Base: (sample_rate * 3) / 8 - * If quarter-shift enabled: add (AFE_rate / 8) * 2 + * The LPF should be set to capture the desired signal bandwidth + * while the FPGA decimation filter handles anti-aliasing. + * + * For most modes: LPF = (output_sample_rate * 3) / 8 + * For quarter-shift: add offset for shifted spectrum + * + * Note: MAX2831 minimum LPF is 11.6 MHz, so for narrow sample rates + * the hardware limit applies and FPGA filter does the real work. */ + 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) @@ -339,12 +350,23 @@ void ReceiverModel::update_baseband_bandwidth() { uint8_t quarter_shift = (fpga_ctrl >> 2) & 0x03; if (quarter_shift != 0) { - // Get resampling factor from clock manager - uint8_t resampling_n = portapack::clock_manager.get_resampling_n(); - uint32_t offset = (sample_rate << resampling_n) / 8; // AFE_rate / 8 + // Quarter-shift moves spectrum by AFE_rate/4, need wider LPF + uint32_t offset = afe_rate / 8; 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());