From 5deeaed5f0b3afc91ae974085ab8f9f40432e300 Mon Sep 17 00:00:00 2001 From: Pezsma <159525557+Pezsma@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:42:21 +0200 Subject: [PATCH] Praline rssi (#3127) --- firmware/application/hw/max2831.cpp | 38 ++++ firmware/application/hw/max2831.hpp | 3 + firmware/application/ui/ui_rssi.cpp | 49 +++--- firmware/application/ui/ui_rssi.hpp | 9 +- firmware/baseband/baseband_thread.cpp | 25 --- firmware/baseband/rssi_thread.cpp | 184 -------------------- firmware/common/portapack_shared_memory.hpp | 13 -- 7 files changed, 69 insertions(+), 252 deletions(-) diff --git a/firmware/application/hw/max2831.cpp b/firmware/application/hw/max2831.cpp index 4d5073d0b..5c48818a7 100644 --- a/firmware/application/hw/max2831.cpp +++ b/firmware/application/hw/max2831.cpp @@ -182,20 +182,24 @@ void MAX2831::set_mode(const Mode mode) { case Mode::Shutdown: gpio_max2831_rx_enable.write(0); /* RXTX=0 */ gpio_max283x_enable.write(0); /* ENABLE=0 */ + set_rssi_mux(0); break; case Mode::Standby: gpio_max2831_rx_enable.write(1); /* RXTX=1 */ gpio_max283x_enable.write(0); /* ENABLE=0 */ + set_rssi_mux(0); break; case Mode::Transmit: case Mode::Tx_Calibration: gpio_max2831_rx_enable.write(1); /* RXTX=1 for TX */ gpio_max283x_enable.write(1); /* ENABLE=1 */ + set_rssi_mux(2); // transmit power break; case Mode::Receive: case Mode::Rx_Calibration: gpio_max2831_rx_enable.write(0); /* RXTX=0 for RX */ gpio_max283x_enable.write(1); /* ENABLE=1 */ + set_rssi_mux(1); // RSSI break; } @@ -475,5 +479,39 @@ void MAX2831::write(const address_t reg_num, const reg_t value) { } } +void MAX2831::set_rssi_mux(const uint8_t mode) { + /* RSSI MUX allows switching the RSSI output between different internal signals. + * 0 = disable mux + * 1 = RSSI + * 2 = TX_POWER + * 3 = TEMP + */ + uint16_t mux_val = 0; + + // Select the appropriate constant based on the input mode. + if (mode == 0) { + mux_val = 0; + } else { + // Select the appropriate constant based on the input mode. + switch (mode) { + case 3: + mux_val = REG8_RSSI_MUX_TEMP; + break; + case 2: + mux_val = REG8_RSSI_MUX_TX_POWER; + break; + case 1: + default: + mux_val = REG8_RSSI_MUX_RSSI; + break; + } + mux_val |= REG8_RSSI_EN; + } + + set_reg_field(8, REG8_RSSI_MUX_MASK | REG8_RSSI_EN, mux_val); + flush_reg(8); +} +} + } // namespace max2831 #endif diff --git a/firmware/application/hw/max2831.hpp b/firmware/application/hw/max2831.hpp index 7db101825..29b2a37c3 100644 --- a/firmware/application/hw/max2831.hpp +++ b/firmware/application/hw/max2831.hpp @@ -151,6 +151,7 @@ constexpr uint16_t REG8_RSSI_MUX_MASK = 0x0300; /* D9:D8 */ constexpr uint16_t REG8_RSSI_MUX_RSSI = (0 << REG8_RSSI_MUX_SHIFT); constexpr uint16_t REG8_RSSI_MUX_TEMP = (1 << REG8_RSSI_MUX_SHIFT); constexpr uint16_t REG8_RSSI_MUX_TX_POWER = (2 << REG8_RSSI_MUX_SHIFT); +constexpr uint16_t REG8_RSSI_EN = (1 << 10); constexpr uint16_t REG8_RXVGA_GAIN_SPI_EN_SHIFT = 12; constexpr uint16_t REG8_RXVGA_GAIN_SPI_EN = (1 << REG8_RXVGA_GAIN_SPI_EN_SHIFT); @@ -213,6 +214,8 @@ class MAX2831 : public MAX283x { reg_t read(const address_t reg_num) override; void write(const address_t reg_num, const reg_t value) override; + void set_rssi_mux(const uint8_t mode); + private: spi::arbiter::Target& _target; Mode _mode{Mode::Standby}; diff --git a/firmware/application/ui/ui_rssi.cpp b/firmware/application/ui/ui_rssi.cpp index 1d3b8b359..f5bc0cb86 100644 --- a/firmware/application/ui/ui_rssi.cpp +++ b/firmware/application/ui/ui_rssi.cpp @@ -46,17 +46,17 @@ 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_min = 0.4; +#ifdef PRALINE + constexpr float rssi_voltage_max = 2.4; +#else constexpr float rssi_voltage_max = 2.2; - // constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max; - constexpr int raw_min = 0; +#endif + constexpr float adc_voltage_max = 3.3; - constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max; + constexpr int raw_min = (rssi_sample_range * rssi_voltage_min) / adc_voltage_max; + constexpr int raw_max = (int)(((rssi_sample_range * rssi_voltage_max) / adc_voltage_max) + 0.5f); constexpr int raw_delta = raw_max - raw_min; if (!vertical_rssi_enabled) { @@ -117,7 +117,6 @@ 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}; @@ -226,7 +225,6 @@ 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_) { @@ -434,25 +432,38 @@ void RSSIGraph::paint(Painter& painter) { void RSSIGraph::add_values(int16_t rssi_min, int16_t rssi_avg, int16_t rssi_max, int16_t db) { 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.4V to 2.4V + */ + constexpr int rssi_sample_range = 256; - // constexpr float rssi_voltage_min = 0.4; + constexpr float rssi_voltage_min = 0.4; +#ifdef PRALINE + constexpr float rssi_voltage_max = 2.4; +#else constexpr float rssi_voltage_max = 2.2; - // constexpr int raw_min = rssi_sample_range * rssi_voltage_min / adc_voltage_max; - constexpr int raw_min = 0; +#endif + constexpr float adc_voltage_max = 3.3; - constexpr int raw_max = rssi_sample_range * rssi_voltage_max / adc_voltage_max; + constexpr int raw_min = (rssi_sample_range * rssi_voltage_min) / adc_voltage_max; + constexpr int raw_max = (int)(((rssi_sample_range * rssi_voltage_max) / adc_voltage_max) + 0.5f); constexpr int raw_delta = raw_max - raw_min; // vertical bottom to top level meters + // y_avg const range_t y_avg_range{0, r.height() - 1}; const int16_t y_avg = y_avg_range.clip((rssi_avg - raw_min) * r.height() / raw_delta); + // y_min const range_t y_min_range{0, y_avg}; const int16_t y_min = y_min_range.clip((rssi_min - raw_min) * r.height() / raw_delta); - const range_t y_max_range{y_avg + 1, r.height() - 1}; + // y_max + const range_t y_max_range{y_avg, r.height() - 1}; const int16_t y_max = y_max_range.clip((rssi_max - raw_min) * r.height() / raw_delta); + // range const range_t db_range{-80, 10}; int16_t db_ = db_range.clip(db); - db_ = db_ - 10; + db_ -= 10; db_ = db_ * r.height() / 90; db_ = r.height() + db_; @@ -529,12 +540,6 @@ 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 65fac2312..cc1d2b37a 100644 --- a/firmware/application/ui/ui_rssi.hpp +++ b/firmware/application/ui/ui_rssi.hpp @@ -64,18 +64,11 @@ 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 c5f2859f7..289e214c7 100644 --- a/firmware/baseband/baseband_thread.cpp +++ b/firmware/baseband/baseband_thread.cpp @@ -102,31 +102,6 @@ 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 9f02d8b67..812f8c1a7 100644 --- a/firmware/baseband/rssi_thread.cpp +++ b/firmware/baseband/rssi_thread.cpp @@ -28,110 +28,6 @@ #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; @@ -158,85 +54,6 @@ 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); @@ -260,5 +77,4 @@ void RSSIThread::run() { rf::rssi::stop(); rf::rssi::dma::free(); -#endif } diff --git a/firmware/common/portapack_shared_memory.hpp b/firmware/common/portapack_shared_memory.hpp index 464ceb0de..5a3752213 100644 --- a/firmware/common/portapack_shared_memory.hpp +++ b/firmware/common/portapack_shared_memory.hpp @@ -55,19 +55,6 @@ 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;