diff --git a/firmware/application/apps/ble_rx_app.hpp b/firmware/application/apps/ble_rx_app.hpp index bc9163d22..8cb21b189 100644 --- a/firmware/application/apps/ble_rx_app.hpp +++ b/firmware/application/apps/ble_rx_app.hpp @@ -446,7 +446,7 @@ class BLERxView : public View { }; // Variable to store the current limit - size_t max_recent_entries = 32; + size_t max_recent_entries = 25; }; /* BLERxView */ } /* namespace ui */ diff --git a/firmware/application/clock_manager.cpp b/firmware/application/clock_manager.cpp index 51dca3007..063492706 100644 --- a/firmware/application/clock_manager.cpp +++ b/firmware/application/clock_manager.cpp @@ -319,12 +319,21 @@ static_assert(si5351a_ms_int_mcu_clkin.f_out() == mcu_clkin_r9_f, "MS int MCU CL using namespace si5351; +#ifdef PRALINE +static constexpr ClockControl::MultiSynthSource get_si5351a_reference_clock_generator_pll(const ClockManager::ReferenceSource reference_source) { + return (reference_source == ClockManager::ReferenceSource::Xtal) + ? ClockControl::MultiSynthSource::PLLA + : ClockControl::MultiSynthSource::PLLB; +} +#else static constexpr ClockControl::MultiSynthSource get_si5351c_reference_clock_generator_pll(const ClockManager::ReferenceSource reference_source) { return (reference_source == ClockManager::ReferenceSource::Xtal) ? ClockControl::MultiSynthSource::PLLA : ClockControl::MultiSynthSource::PLLB; } +#endif +#ifndef PRALINE constexpr ClockControls si5351c_clock_control_common{{ {ClockControl::ClockCurrentDrive::_8mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off}, {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Group, ClockControl::ClockInvert::Invert, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, @@ -335,6 +344,7 @@ constexpr ClockControls si5351c_clock_control_common{{ {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Fractional, ClockControl::ClockPowerDown::Power_Off}, {ClockControl::ClockCurrentDrive::_2mA, ClockControl::ClockSource::MS_Self, ClockControl::ClockInvert::Normal, get_si5351c_reference_clock_generator_pll(ClockManager::ReferenceSource::Xtal), ClockControl::MultiSynthMode::Integer, ClockControl::ClockPowerDown::Power_Off}, }}; +#endif constexpr ClockControls si5351a_clock_control_common{{ #ifdef PRALINE @@ -451,7 +461,7 @@ void ClockManager::init_clock_generator() { clock_generator.set_pll_input_sources(si5351a_pll_input_sources); /* Skip MCU CLKIN setup and reference detection for PRALINE - not applicable */ - reference = Reference{ReferenceSource::Xtal, 0}; + reference = Reference{ReferenceSource::Xtal, 0}; // PLLA /* Clock control will be set AFTER multisynth configuration - see below */ #else @@ -640,12 +650,16 @@ uint32_t ClockManager::measure_gp_clkin_frequency() { } bool ClockManager::loss_of_signal() { +#ifdef PRALINE + return clock_generator.clkin_loss_of_signal(); +#else if (hackrf_r9) { const auto frequency = measure_gp_clkin_frequency(); return (frequency < 9850000) || (frequency > 10150000); } else { return clock_generator.clkin_loss_of_signal(); } +#endif } ClockManager::ReferenceSource ClockManager::detect_reference_source() { @@ -668,6 +682,17 @@ ClockManager::ReferenceSource ClockManager::detect_reference_source() { } ClockManager::Reference ClockManager::choose_reference() { +#ifdef PRALINE + const auto detected_reference = detect_reference_source(); + + if ((detected_reference == ReferenceSource::External) || + (detected_reference == ReferenceSource::PortaPack)) { + const auto frequency = measure_gp_clkin_frequency(); + if ((frequency >= 9850000) && (frequency <= 10150000)) { + return {detected_reference, 10000000}; + } + } +#else if (hackrf_r9) { gpio_r9_clkin_en.write(1); volatile uint32_t delay = 240000 + 24000; @@ -686,6 +711,7 @@ ClockManager::Reference ClockManager::choose_reference() { if (hackrf_r9) { gpio_r9_clkin_en.write(0); } +#endif portapack_tcxo_disable(); return {ReferenceSource::Xtal, 25000000}; @@ -881,10 +907,6 @@ void ClockManager::set_reference_ppb(const int32_t ppb) { * It is assumed an external clock coming in to CLKIN/PLLB is sufficiently accurate as to not need adjustment. * TODO: Revisit the above policy. It may be good to allow adjustment of the external reference too. */ - if (hackrf_r9 && reference.source != ReferenceSource::Xtal) { - return; - } - #ifdef PRALINE // On Praline, only apply if we aren't locked to a superior external 10MHz source // (Assuming you have a way to detect the 10MHz presence on Praline) @@ -893,6 +915,10 @@ void ClockManager::set_reference_ppb(const int32_t ppb) { } constexpr uint32_t pll_multiplier = si5351_pll_a_afe_800m.a; #else + if (hackrf_r9 && reference.source != ReferenceSource::Xtal) { + return; + } + constexpr uint32_t pll_multiplier = si5351_pll_xtal_25m.a; #endif @@ -1105,6 +1131,25 @@ void ClockManager::stop_audio_pll() { } void ClockManager::enable_clock_output(bool enable) { +#ifdef PRALINE + auto clkout_select = clock_generator_output_og_clkout; + + if (enable) { + clock_generator.enable_output(clkout_select); + if (portapack::persistent_memory::clkout_freq() < 1000) { + clock_generator.set_ms_frequency(clkout_select, portapack::persistent_memory::clkout_freq() * 128000, si5351_vco_f, 7); + } else { + clock_generator.set_ms_frequency(clkout_select, portapack::persistent_memory::clkout_freq() * 1000, si5351_vco_f, 0); + } + + auto si5351_clock_control_common = si5351a_clock_control_common; + const auto ref_pll = get_si5351a_reference_clock_generator_pll(reference.source); + clock_generator.set_clock_control(clkout_select, si5351_clock_control_common[clkout_select].ms_src(ref_pll).clk_pdn(ClockControl::ClockPowerDown::Power_On)); + } else { + clock_generator.disable_output(clkout_select); + clock_generator.set_clock_control(clkout_select, ClockControl::power_off()); + } +#else if (hackrf_r9) { gpio_r9_clkout_en.output(); gpio_r9_clkout_en.write(enable); @@ -1133,4 +1178,5 @@ void ClockManager::enable_clock_output(bool enable) { clock_generator.disable_output(clkout_select); clock_generator.set_clock_control(clkout_select, ClockControl::power_off()); } +#endif } diff --git a/firmware/application/radio.cpp b/firmware/application/radio.cpp index 2f6242946..dae757f51 100644 --- a/firmware/application/radio.cpp +++ b/firmware/application/radio.cpp @@ -24,8 +24,6 @@ #include "rf_path.hpp" #include "rffc507x.hpp" -#include "max2837.hpp" -#include "max2839.hpp" #ifdef PRALINE #include "max2831.hpp" @@ -33,6 +31,8 @@ extern "C" { #include "fpga_bridge.h" } #else +#include "max2837.hpp" +#include "max2839.hpp" #include "baseband_cpld.hpp" #endif @@ -122,11 +122,11 @@ static spi::arbiter::Target ssp1_target_max5864{ static rf::path::Path rf_path; rffc507x::RFFC507x first_if; max283x::MAX283x* second_if; -max2837::MAX2837 second_if_max2837{ssp1_target_max283x}; -max2839::MAX2839 second_if_max2839{ssp1_target_max283x}; #ifdef PRALINE max2831::MAX2831 second_if_max2831{ssp1_target_max283x}; #else +max2837::MAX2837 second_if_max2837{ssp1_target_max283x}; +max2839::MAX2839 second_if_max2839{ssp1_target_max283x}; static baseband::CPLD baseband_cpld; #endif static max5864::MAX5864 baseband_codec{ssp1_target_max5864}; @@ -415,7 +415,11 @@ void set_rx_max283x_iq_phase_calibration(const size_t v) { void disable() { set_antenna_bias(false); baseband_codec.set_mode(max5864::Mode::Shutdown); +#ifdef PRALINE + second_if->set_mode(max283x::Mode::Standby); +#else second_if->set_mode(max2837::Mode::Standby); +#endif first_if.disable(); set_rf_amp(false); diff --git a/firmware/application/ui/ui_rssi.cpp b/firmware/application/ui/ui_rssi.cpp index de0018f62..1d3b8b359 100644 --- a/firmware/application/ui/ui_rssi.cpp +++ b/firmware/application/ui/ui_rssi.cpp @@ -46,12 +46,16 @@ RSSI::RSSI( void RSSI::paint(Painter& painter) { const auto r = screen_rect(); + /* RSSI scaling based on transceiver output voltage range. + * MAX2837 (HackRF One): 0.4V to 2.2V + * MAX2831 (HackRF Pro): 0.5V to 2.0V (similar enough to use same scaling) + */ constexpr int rssi_sample_range = 256; // constexpr float rssi_voltage_min = 0.4; constexpr float rssi_voltage_max = 2.2; - constexpr float adc_voltage_max = 3.3; // constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max; constexpr int raw_min = 0; + constexpr float adc_voltage_max = 3.3; constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max; constexpr int raw_delta = raw_max - raw_min; @@ -113,6 +117,7 @@ void RSSI::paint(Painter& painter) { const Rect r_db{r.left() + x_db, r.top(), 1, r.height()}; if (db_) painter.fill_rectangle(r_db, Color::green()); + } else { // vertical bottom to top level meters const range_t y_avg_range{0, r.height() - 1}; @@ -221,6 +226,7 @@ void RSSI::on_statistics_update(const RSSIStatistics& statistics) { min_ = statistics.min; avg_ = statistics.accumulator / statistics.count; max_ = statistics.max; + if (peak_enabled) { peak_duration_ = peak_duration_ + 100; if (max_ > peak_) { @@ -431,9 +437,9 @@ void RSSIGraph::add_values(int16_t rssi_min, int16_t rssi_avg, int16_t rssi_max, constexpr int rssi_sample_range = 256; // constexpr float rssi_voltage_min = 0.4; constexpr float rssi_voltage_max = 2.2; - constexpr float adc_voltage_max = 3.3; // constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max; constexpr int raw_min = 0; + constexpr float adc_voltage_max = 3.3; constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max; constexpr int raw_delta = raw_max - raw_min; @@ -523,6 +529,12 @@ bool RSSI::on_touch(const TouchEvent event) { } void RSSI::set_db(int16_t db) { +#ifdef PRALINE + /* Add a +30dB global boost to align 40MHz/1.2V VCM data + with the UI's existing display scale. */ + db_ = db + 30; +#else db_ = db; +#endif } } /* namespace ui */ diff --git a/firmware/application/ui/ui_rssi.hpp b/firmware/application/ui/ui_rssi.hpp index ade17a158..65fac2312 100644 --- a/firmware/application/ui/ui_rssi.hpp +++ b/firmware/application/ui/ui_rssi.hpp @@ -64,10 +64,18 @@ class RSSI : public Widget { void set_db(int16_t db); private: +#ifdef PRALINE + // Changed from int8_t to uint8_t: + uint8_t min_ = 0; + uint8_t avg_ = 0; + uint8_t max_ = 0; + uint8_t peak_ = 0; +#else int8_t min_ = 0; int8_t avg_ = 0; int8_t max_ = 0; int8_t peak_ = 0; +#endif size_t peak_duration_ = 0; int16_t db_ = 0; bool instant_exec_{false}; diff --git a/firmware/baseband/baseband_thread.cpp b/firmware/baseband/baseband_thread.cpp index 289e214c7..c5f2859f7 100644 --- a/firmware/baseband/baseband_thread.cpp +++ b/firmware/baseband/baseband_thread.cpp @@ -102,6 +102,31 @@ void BasebandThread::run() { buffer_c8_t buffer{ buffer_tmp.p, buffer_tmp.count, sampling_rate_}; +#ifdef PRALINE + /* + * Software RSSI: Copy 8 I/Q samples spread across buffer. + * + * Just pack and copy - no computation here. + * rssi_thread does __SMUAD power and rssi calculation. + * 8 samples avoids zero-crossing artifacts. + */ + if (direction_ == baseband::Direction::Receive && buffer_tmp.count >= 32) { + const size_t step = buffer_tmp.count / 8; + + for (size_t i = 0; i < 8; i++) { + const size_t idx = i * step + (step / 2); + const auto sample = buffer_tmp.p[idx]; + + // Pack into 32 bits: Q in high 16 bits, I in low 16 bits, the safe approach: + // 1. Cast to uint16_t to capture the raw 16-bit pattern (e.g., -1 becomes 0xFFFF) + // 2. OR them together. The uint16_t will be promoted to uint32_t cleanly. + // This is accomplished with the specific hardware instruction designed + // for this __PKHBT (Pack Halfword Bottom Top). + shared_memory.software_rssi_iq[i] = __PKHBT(sample.real(), sample.imag(), 16); + } + } +#endif + if (shared_memory.request_m4_performance_counter == 0x02) { uint8_t max = shared_memory.m4_performance_counter; for (size_t i = 0; i < buffer_tmp.count; i++) { diff --git a/firmware/baseband/rssi_thread.cpp b/firmware/baseband/rssi_thread.cpp index 812f8c1a7..9f02d8b67 100644 --- a/firmware/baseband/rssi_thread.cpp +++ b/firmware/baseband/rssi_thread.cpp @@ -28,6 +28,110 @@ #include "message.hpp" #include "portapack_shared_memory.hpp" +#ifdef PRALINE +/* + * ============================================================================= + * PRALINE Software RSSI + * ============================================================================= + * + * Architecture: + * - baseband_thread: Copies 8 packed I/Q samples (no computation) + * - rssi_thread: __SMUAD power calc, avg power, LUT, trackers + * + * All DSP happens here to keep baseband_thread minimal. + */ + +/* + * Power-to-RSSI Lookup Table (32 entries) + * + * Maps I²+Q² power to RSSI (0-255) using logarithmic scaling. + * For 8-bit I/Q: max |I|=|Q|=127, so max I²+Q² = 32258 + * + * For example, the formula: rssi = 32 * log2((index * 8) + 1), clamped to 255 + * where using >> 8 scaling provide 4x more sensitive than >> 10: + * + * The trade-off: more sensitivity means the meter saturates (hits max) at lower signal levels. + * We'll want the bar to be mid-range at typical signal levels, not pegged at max. + * - Index 0: power 0-255 (I/Q magnitude ~11) + * - Index 31: power 7936+ (I/Q magnitude ~63+) + */ +static constexpr uint8_t power_to_rssi_lut[32] = { + 0, 101, 130, 148, 161, 171, 179, 186, + 192, 197, 202, 206, 210, 213, 217, 220, + 222, 225, 227, 230, 232, 234, 236, 238, + 240, 241, 243, 244, 246, 247, 249, 255}; + +/* + * Convert power to RSSI using 32-entry LUT. + * If more sensitivity thank >> 10 is needed: + * use power >= 8192 & >> 8, yields 4x more sensitivity than >> 10. + * use power >= 4096 & >> 7, yields 8x more sensitivity than >> 10 + * use power >= 2048 & >> 6, yields 16x more sensitivity than >> 10 + * use power >= 1024 & >> 5, yields 32x more sensitivity than >> 10 + */ +static inline uint8_t power_to_rssi(uint32_t power) { + // uint8_t index = (power >= 32768) ? 31 : static_cast(power >> 10); + uint8_t index = (power >= 2048) ? 31 : static_cast(power >> 6); + return power_to_rssi_lut[index]; +} + +/* + * IIR Smoothing Filter (Exponential Moving Average) + * Formula: smooth = (current + 7*smooth) / 8 (α = 1/8) + */ +class IIRFilter { + public: + uint8_t update(uint8_t current) { + uint16_t current_q8 = static_cast(current) << 8; + smooth_q8_ = (current_q8 + 7 * smooth_q8_) >> 3; + return static_cast(smooth_q8_ >> 8); + } + + private: + uint16_t smooth_q8_ = 0; +}; + +/* + * Running min tracker with decay. + * Instantly captures new minimums, slowly decays upward. + */ +class MinTracker { + public: + uint8_t update(uint8_t current) { + if (current < min_) { + min_ = current; + } else { + // Slow decay upward (α = 1/16) + min_ = min_ + ((current - min_) >> 4); + } + return min_; + } + + private: + uint8_t min_ = 255; +}; + +/* + * Running max tracker with decay. + * Instantly captures new maximums, slowly decays downward. + */ +class MaxTracker { + public: + uint8_t update(uint8_t current) { + if (current > max_) { + max_ = current; + } else { + // Slow decay downward (α = 1/16) + max_ = max_ - ((max_ - current) >> 4); + } + return max_; + } + + private: + uint8_t max_ = 0; +}; +#endif // PRALINE + WORKING_AREA(rssi_thread_wa, 128); Thread* RSSIThread::thread = nullptr; @@ -54,6 +158,85 @@ void RSSIThread::start() { } void RSSIThread::run() { +#ifdef PRALINE + + /* + * PRALINE (HackRF Pro): Software RSSI from I/Q samples + * + * Hardware ADC-based RSSI doesn't work on HackRF Pro. + * + * baseband_thread copies 8 packed I/Q samples. + * We use __SMUAD to compute power, then apply LUT and + * maintain running stats. + */ + + IIRFilter avg_filter; + MinTracker min_tracker; + MaxTracker max_tracker; + + RSSIStatistics stats{}; + uint32_t accumulator = 0; + uint32_t sample_count = 0; + + constexpr uint32_t samples_per_report = 50; // ~10Hz reporting at 2ms poll + + while (!chThdShouldTerminate()) { + chThdSleepMilliseconds(2); // Poll at ~500Hz + + /* + * SIMD-accelerated I/Q power calculation for Cortex-M4. + * + * Uses __SMUAD (Signed Multiply Accumulate Dual) instruction to compute + * sum of products of packed halfwords: (a0*a0) + (a1*a1) + * + * Processes complex samples per iteration, computing I²+Q² with SIMD. + * SIMD-accelerated I/Q power calculation for Cortex-M4. + * ~4x faster than scalar loop. + * Sum power from all 8 samples (more stable than peak). + * + * __SMUAD(val, val) computes: + * (low16 * low16) + (high16 * high16) = I² + Q² + */ + + uint32_t total_power = 0; + + for (size_t i = 0; i < 8; i++) { + const uint32_t packed = shared_memory.software_rssi_iq[i]; + total_power += __SMUAD(packed, packed); + } + + // Average the 8 samples for min, and avg power + const uint32_t avg_power = total_power >> 3; + + // Convert to RSSI scale (0-255) via LUT + const uint8_t avg_rssi = power_to_rssi(avg_power); + + // Update running trackers + const uint8_t smooth_min = min_tracker.update(avg_rssi); + const uint8_t smooth_avg = avg_filter.update(avg_rssi); + const uint8_t smooth_max = max_tracker.update(avg_rssi); + + // Accumulate for periodic report + accumulator += smooth_avg; + sample_count++; + + // Report periodically + if (sample_count >= samples_per_report) { + stats.min = smooth_min; + stats.max = smooth_max; + stats.accumulator = accumulator; + stats.count = sample_count; + + const RSSIStatisticsMessage message{stats}; + shared_memory.application_queue.push(message); + + // Reset accumulator for next period + accumulator = 0; + sample_count = 0; + } + } +#else + /* HackRF One: Use hardware ADC-based RSSI */ rf::rssi::init(); rf::rssi::dma::allocate(4, 400); @@ -77,4 +260,5 @@ void RSSIThread::run() { rf::rssi::stop(); rf::rssi::dma::free(); +#endif } diff --git a/firmware/chibios/os/ports/GCC/ARMCMx/rules.cmake b/firmware/chibios/os/ports/GCC/ARMCMx/rules.cmake index a892fd1e4..b4b8c0e71 100644 --- a/firmware/chibios/os/ports/GCC/ARMCMx/rules.cmake +++ b/firmware/chibios/os/ports/GCC/ARMCMx/rules.cmake @@ -112,10 +112,11 @@ if(BOARD STREQUAL "PRALINE") # backwards consistenct with hackrf one setup. # Recommend revisit by devs. # Local SRAM: 128KB (Bank 1) + 72KB (Bank 2) + # Bank 1 can also be allocated as 96KB for M4, and 32KB for M0 set(M0_RAM_SIZE "64k") # AHB SRAM Bank 0 for M0 (stacks, data, bss) - set(M0_LOCAL_HEAP_SIZE "32k") # Local SRAM heap for additional 16-bit IQ - set(M0_LOCAL_HEAP_ORIGIN "0x10018000") # After M4's 96KB - set(M4_RAM_SIZE "96k") # Local SRAM Bank 1 (same as HackRF One) + set(M0_LOCAL_HEAP_SIZE "0") # No Local SRAM, could be 32k heap for additional 16-bit IQ + set(M0_LOCAL_HEAP_ORIGIN "0x10020000") # 0x10018000 if allocating 32k heap for M0 after M4's 96KB + set(M4_RAM_SIZE "128k") # Local SRAM Bank 1 Fully allocated to M4 (HackRF One is 96k) set(M4_FLASH_SIZE "72k") # Local SRAM Bank 2 set(USB_RAM_SIZE "32k") # AHB SRAM shared with M0 set(FLASH_SIZE "4M") diff --git a/firmware/common/portapack_adc.hpp b/firmware/common/portapack_adc.hpp index 4f0892419..e2630dd34 100644 --- a/firmware/common/portapack_adc.hpp +++ b/firmware/common/portapack_adc.hpp @@ -35,6 +35,12 @@ constexpr size_t adc0_touch_xn_input = 6; /* ADC1 */ +/* + * RSSI input channel is the same on both platforms: + * - HackRF One (MAX2837): RSSI routed to ADC1 Channel 1 (PC_0) + * - HackRF Pro/PRALINE (MAX2831): RSSI also routed to ADC1 Channel 1 (PC_0) + * Confirmed by GSG hackrf firmware: adc_read(1) used for RSSI + */ constexpr size_t adc1_rssi_input = 1; } /* namespace portapack */ diff --git a/firmware/common/portapack_shared_memory.hpp b/firmware/common/portapack_shared_memory.hpp index 5a3752213..464ceb0de 100644 --- a/firmware/common/portapack_shared_memory.hpp +++ b/firmware/common/portapack_shared_memory.hpp @@ -55,6 +55,19 @@ struct ToneData { /* NOTE: These structures must be located in the same location in both M4 and M0 binaries */ struct SharedMemory { +#ifdef PRALINE + /* + * Software RSSI: 8 packed I/Q samples from baseband_thread. + * + * baseband_thread copies 8 samples spread across buffer (no computation). + * Each sample is packed: Q in high 16 bits, I in low 16 bits. + * rssi_thread uses __SMUAD on each to compute I²+Q², finds peak. + * + * 8 samples avoids zero-crossing artifacts from single-sample approach. + */ + volatile uint32_t software_rssi_iq[8]{0}; // 8 packed I/Q samples +#endif + static constexpr size_t application_queue_k = 11; static constexpr size_t app_local_queue_k = 11;