Next praline clean up (add SW: RSSI for hackrf pro praline) (#3091)

* Cleaned up #ifndef PRALINE and updated logic to being with #ifdef PRALINE entries where possible to make logic flow for PRALINE code execution pipeline clearer. Cleaned up compiletime warnings for PRALINE related codebase updates.

* Addressed comments provided by copilot during PR review. Combed through frequency definitions for consistency between PLL A and PLL B register definitions for CLKs 0-7. Ensured CLK3/LK6 <- SMA PORTs and CLK7 <- not utiliized are disabled during core development phase to support root cause analysis of any spectral artifacts. Updated MCU frequency to 40MHz to ensure audio harmonics are outside FM radio band range (< 80 MHz, >120MHz) and added comments clarifying choice of 40 over 10 MHz for potential future root cause analysis in other bands where audio may be expected as needed. Added CLK6 and CLK7 to Clocks Status View Debug display. Moved CLK defintions and PLL instantiations for components that are most RF sensitive to PLL A. Left others in PLL B. That is move FPGA CLK1 to PLL B, while moving CLK2, CLK4, and CLK5 to PLL A.

* Cleaned up PLL A and B XTAL reference checks relative to 800 MHz.

* Encapsulated HackRF Pro Praline debug and status vies into a single Pro Debug submenu as part of clean up.

* Fixed BLE RX Out of Memory error. Updated LPC43xx ld scripts to accouint for additional HackRF Pro praline memory.

* Addressed copilot comments for ble_rx_app by adding recent_entries_view.set_dirty. Updated ble_rx_app for easier use with heap limit set to one less than recent entries max limit.

* Addressed copilot comments by updating comment clarity in source files. Updated ui_debug to allow for return reference if set for PRO debug menu item.

* Improved readability of intialization parameters for the FPGA registers, and addressed 20Mhz nulls by initializing DC Notch width with standard setting, and DC Adaptiation rate with a balanced setting.

* Ran format-code.sh

* Added option to allow for user to set number if entries in recent list. Default is set to a relatively stable 32.

* Removed #ifdef PRALINE pragmas from ble_rx_app such that HackRF One can also use the updated UI widget to allow for user to set number of entries in recent list.

* Ran format-code.sh

* Improved RSSI for praline. Values are now being counted correctly by statistics methods after selecting ADC = 0 0 for RSSI. This was different than for the hackrf one which uses ADC = 1.

* Implemented SW RSSI calculation since HW RSSI not currently available in hackrf pro (praline) via fpga pass through. Calibrated RSSI power sensistivity to mid level signal intensitities.

* In preparation for WIP allocated full 128k to M4 limiting heap for M0 to 0k, and leabing it only with 64k from bank 0. That is, bank1 and bank2 are fully allocated to M4. Cleaned up #ifdef PRALINE conditionals such taht max2837/39 are in #else conditionals, as well as continuing to clean up hackrf_r9 booleans by placing them within #else conditionals.

* fix typo

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Addressed co-pilot comments. Clarified use of 25 recent samples in ble app for stability, udpated comments to reflect use of avg power over pwak pwer detection, improved safety of sample packing while relying on intrinstics for execute the implementation in a single machine instruction (PKHBT) for computational and memory efficiency.
This commit is contained in:
stafur
2026-03-17 03:13:09 -04:00
committed by GitHub
parent 9bf1f2175e
commit 395e20d17b
10 changed files with 314 additions and 15 deletions
+1 -1
View File
@@ -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 */
+51 -5
View File
@@ -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
}
+8 -4
View File
@@ -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);
+14 -2
View File
@@ -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<int> 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 */
+8
View File
@@ -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};
+25
View File
@@ -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++) {
+184
View File
@@ -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<uint8_t>(power >> 10);
uint8_t index = (power >= 2048) ? 31 : static_cast<uint8_t>(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<uint16_t>(current) << 8;
smooth_q8_ = (current_q8 + 7 * smooth_q8_) >> 3;
return static_cast<uint8_t>(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
}
@@ -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")
+6
View File
@@ -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 */
@@ -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;