From 4ed7db0dd73dc255e245c14e83bf5cd6d773e8c0 Mon Sep 17 00:00:00 2001 From: SteamedFish's OpenCode <69167+SteamedFish@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:00:12 +0800 Subject: [PATCH 1/5] SoapySDDC: Add ADC frequency control Adds support for configuring ADC sampling frequency through SoapySDR Settings API, enabling users to adjust the ADC rate for high-ADC devices (RX-888 mkII) and standard devices. Changes: - Add adc_frequency setting in getSettingInfo() and listSettings() - Implement readSetting() and writeSetting() for ADC frequency - Add supportsHighADCFrequency() method to detect device capability - Auto-detect default: 128 MHz for high-ADC devices, 64 MHz otherwise - Frequency range: 50-140 MHz (high-ADC), 50-64 MHz (standard) - Call RadioHandler.UpdateSampleRate() to apply ADC frequency changes Tested with RX-888 mkII, confirmed ADC frequency control works correctly. --- SoapySDDC/Settings.cpp | 161 +++++++++++++++++++++++++++++++++------- SoapySDDC/SoapySDDC.hpp | 5 ++ 2 files changed, 139 insertions(+), 27 deletions(-) diff --git a/SoapySDDC/Settings.cpp b/SoapySDDC/Settings.cpp index e34c52b1..6ae870e2 100644 --- a/SoapySDDC/Settings.cpp +++ b/SoapySDDC/Settings.cpp @@ -45,6 +45,11 @@ SoapySDDC::SoapySDDC(const SoapySDR::Kwargs &args) : deviceId(-1), Fx3->Enumerate(idx, devicelist.dev[0]); Fx3->Open(); RadioHandler.Init(Fx3, _Callback, nullptr, this); + + if (supportsHighADCFrequency()) { + adcnominalfreq = 128000000; + RadioHandler.UpdateSampleRate(adcnominalfreq); + } } SoapySDDC::~SoapySDDC(void) @@ -319,30 +324,68 @@ SoapySDR::ArgInfoList SoapySDDC::getFrequencyArgsInfo(const int, const size_t) c void SoapySDDC::setSampleRate(const int, const size_t, const double rate) { DbgPrintf("SoapySDDC::setSampleRate %f\n", rate); - switch ((int)rate) + + if (adcnominalfreq > N2_BANDSWITCH) { - case 32000000: - sampleRate = 32000000; - samplerateidx = 4; - break; - case 16000000: - sampleRate = 16000000; - samplerateidx = 3; - break; - case 8000000: - sampleRate = 8000000; - samplerateidx = 2; - break; - case 4000000: - sampleRate = 4000000; - samplerateidx = 1; - break; - case 2000000: - sampleRate = 2000000; - samplerateidx = 0; - break; - default: - return; + // 128MHz ADC mode: 6 sample rates available + switch ((int)rate) + { + case 64000000: + sampleRate = 64000000; + samplerateidx = 5; + break; + case 32000000: + sampleRate = 32000000; + samplerateidx = 4; + break; + case 16000000: + sampleRate = 16000000; + samplerateidx = 3; + break; + case 8000000: + sampleRate = 8000000; + samplerateidx = 2; + break; + case 4000000: + sampleRate = 4000000; + samplerateidx = 1; + break; + case 2000000: + sampleRate = 2000000; + samplerateidx = 0; + break; + default: + return; + } + } + else + { + // 64MHz ADC mode: 5 sample rates available + switch ((int)rate) + { + case 32000000: + sampleRate = 32000000; + samplerateidx = 4; + break; + case 16000000: + sampleRate = 16000000; + samplerateidx = 3; + break; + case 8000000: + sampleRate = 8000000; + samplerateidx = 2; + break; + case 4000000: + sampleRate = 4000000; + samplerateidx = 1; + break; + case 2000000: + sampleRate = 2000000; + samplerateidx = 0; + break; + default: + return; + } } } @@ -363,29 +406,65 @@ std::vector SoapySDDC::listSampleRates(const int, const size_t) const results.push_back(16000000); results.push_back(32000000); + if (adcnominalfreq > N2_BANDSWITCH) + { + results.push_back(64000000); + } + return results; } +bool SoapySDDC::supportsHighADCFrequency() const +{ + auto model = const_cast(this)->RadioHandler.getModel(); + return model == RX888 || model == RX888r2 || model == RX888r3 || model == RX999; +} + SoapySDR::ArgInfoList SoapySDDC::getSettingInfo(void) const { SoapySDR::ArgInfoList setArgs; + // BiasT HF setting SoapySDR::ArgInfo BiasTHFArg; BiasTHFArg.key = "UpdBiasT_HF"; - BiasTHFArg.value = "false"; + BiasTHFArg.value = "false"; // Default: BiasT disabled BiasTHFArg.name = "HF Bias Tee enable"; - BiasTHFArg.description = "Enabe Bias Tee on HF antenna port"; + BiasTHFArg.description = "Enable Bias Tee on HF antenna port"; BiasTHFArg.type = SoapySDR::ArgInfo::BOOL; setArgs.push_back(BiasTHFArg); + // BiasT VHF setting SoapySDR::ArgInfo BiasTVHFArg; BiasTVHFArg.key = "UpdBiasT_VHF"; - BiasTVHFArg.value = "false"; + BiasTVHFArg.value = "false"; // Default: BiasT disabled BiasTVHFArg.name = "VHF Bias Tee enable"; - BiasTVHFArg.description = "Enabe Bias Tee on VHF antenna port"; + BiasTVHFArg.description = "Enable Bias Tee on VHF antenna port"; BiasTVHFArg.type = SoapySDR::ArgInfo::BOOL; setArgs.push_back(BiasTVHFArg); + // ADC frequency setting + SoapySDR::ArgInfo AdcFreqArg; + AdcFreqArg.key = "adc_frequency"; + // Default: 128MHz for capable devices, 64MHz for others + AdcFreqArg.value = supportsHighADCFrequency() ? "128000000" : "64000000"; + AdcFreqArg.name = "ADC Sample Rate"; + + if (supportsHighADCFrequency()) + { + AdcFreqArg.description = "ADC sample rate in Hz (50MHz-140MHz). Default 128MHz. " + "Rates above 80MHz enable extended sample rates up to 64MHz output."; + AdcFreqArg.range = SoapySDR::Range(MIN_ADC_FREQ, MAX_ADC_FREQ); + } + else + { + AdcFreqArg.description = "ADC sample rate in Hz (50MHz-64MHz). Default 64MHz. " + "This hardware does not support rates above 64MHz."; + AdcFreqArg.range = SoapySDR::Range(MIN_ADC_FREQ, 64000000); + } + + AdcFreqArg.type = SoapySDR::ArgInfo::INT; + setArgs.push_back(AdcFreqArg); + return setArgs; } @@ -402,8 +481,36 @@ void SoapySDDC::writeSetting(const std::string &key, const std::string &value) biasTee = (value == "true") ? true: false; RadioHandler.UpdBiasT_VHF(biasTee); } + else if (key == "adc_frequency") + { + uint32_t newAdcFreq = static_cast(std::stoul(value)); + if (newAdcFreq >= MIN_ADC_FREQ && newAdcFreq <= MAX_ADC_FREQ) + { + if (newAdcFreq <= 64000000 || supportsHighADCFrequency()) + { + adcnominalfreq = newAdcFreq; + RadioHandler.UpdateSampleRate(newAdcFreq); + } + } + } } +std::string SoapySDDC::readSetting(const std::string &key) const +{ + if (key == "UpdBiasT_HF") + { + return const_cast(this)->RadioHandler.GetBiasT_HF() ? "true" : "false"; + } + else if (key == "UpdBiasT_VHF") + { + return const_cast(this)->RadioHandler.GetBiasT_VHF() ? "true" : "false"; + } + else if (key == "adc_frequency") + { + return std::to_string(adcnominalfreq); + } + return ""; +} // void SoapySDDC::setMasterClockRate(const double rate) // { diff --git a/SoapySDDC/SoapySDDC.hpp b/SoapySDDC/SoapySDDC.hpp index e813421d..3353fe5f 100644 --- a/SoapySDDC/SoapySDDC.hpp +++ b/SoapySDDC/SoapySDDC.hpp @@ -111,6 +111,8 @@ class SoapySDDC : public SoapySDR::Device void writeSetting(const std::string &key, const std::string &value); + std::string readSetting(const std::string &key) const; + // void setMasterClockRate(const double rate); // double getMasterClockRate(void) const; @@ -137,6 +139,9 @@ class SoapySDDC : public SoapySDR::Device fx3class *Fx3; RadioHandlerClass RadioHandler; + // Helper to check if device supports high ADC frequencies + bool supportsHighADCFrequency() const; + public: int Callback(void *context, const float *data, uint32_t len); From 598e2e39948a56066a87b012bfcdc155755ecaa4 Mon Sep 17 00:00:00 2001 From: SteamedFish's OpenCode <69167+SteamedFish@users.noreply.github.com> Date: Wed, 11 Feb 2026 01:35:57 +0800 Subject: [PATCH 2/5] Fix frequency offset bug at 32MHz sample rate Remove hardcoded 8MHz offset in getFrequency() when sampleRate is 32MHz. This offset caused spectrum display to start at 8MHz instead of 0Hz, leaving 0-8MHz range empty. The bug existed since the initial SoapySDR implementation. --- SoapySDDC/Settings.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/SoapySDDC/Settings.cpp b/SoapySDDC/Settings.cpp index 6ae870e2..cec43ba8 100644 --- a/SoapySDDC/Settings.cpp +++ b/SoapySDDC/Settings.cpp @@ -288,10 +288,6 @@ double SoapySDDC::getFrequency(const int, const size_t) const double SoapySDDC::getFrequency(const int, const size_t, const std::string &name) const { DbgPrintf("SoapySDDC::getFrequency with name %s\n", name.c_str()); - if (sampleRate == 32000000) - { - return 8000000.000000; - } return (double)centerFrequency; } From e76a9539d8b84a76e782be41a07cac3ae924d390 Mon Sep 17 00:00:00 2001 From: SteamedFish's OpenCode <69167+SteamedFish@users.noreply.github.com> Date: Wed, 11 Feb 2026 12:58:00 +0800 Subject: [PATCH 3/5] Fix CI build: add apt-get update before package installation --- .github/workflows/cmake.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 36814ec3..d3ed168e 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -29,7 +29,7 @@ jobs: - name: Install libs # Some projects don't allow in-source building, so create a separate build directory # We'll use this as our working directory for all subsequent commands - run: sudo apt-get install -y gcc-arm-none-eabi + run: sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi - name: Build firmware shell: bash @@ -127,7 +127,7 @@ jobs: - name: Install libs # Some projects don't allow in-source building, so create a separate build directory # We'll use this as our working directory for all subsequent commands - run: sudo apt-get install -y libfftw3-dev libusb-1.0-0-dev gcc-arm-none-eabi libsoapysdr-dev + run: sudo apt-get update && sudo apt-get install -y libfftw3-dev libusb-1.0-0-dev gcc-arm-none-eabi libsoapysdr-dev - name: Create Build Environment # Some projects don't allow in-source building, so create a separate build directory From b1d14c98544a619e404e2db46e6142258a8316b4 Mon Sep 17 00:00:00 2001 From: SteamedFish's OpenCode <69167+SteamedFish@users.noreply.github.com> Date: Thu, 12 Feb 2026 01:00:59 +0800 Subject: [PATCH 4/5] Fix PR #240 AI review comments: improve sample rate handling and error safety Addresses 5 of 6 AI code review comments on PR #240: 1. **Comment #1 (Performance)**: Cache supportsHighADCFrequency() result in getSettingInfo() to avoid redundant calls. 2. **Comment #2 (CRITICAL - Uninitialized variable)**: Initialize samplerateidx in constructor to match default sampleRate=32MHz. Previously undefined behavior if activateStream() called before setSampleRate(). 3. **Comment #4 (HIGH - Architecture)**: Replace hardcoded sample rate switch statements with dynamic calculation based on current ADC frequency. Matches ExtIO reference implementation formula: rate = (adcfreq/64.0) * (2^idx) * 2.0 with Nyquist validation. 4. **Comment #5 (HIGH - API consistency)**: Make listSampleRates() dynamically generate rates from current adcnominalfreq instead of returning fixed list. 5. **Comment #6 (CRITICAL - Exception safety)**: Add try-catch exception handling for std::stoul() in writeSetting("adc_frequency"). Prevents crash on invalid input. Validates uint32_t range and hardware-specific frequency limits. Comment #3 (const_cast in supportsHighADCFrequency) deferred - requires Core API changes affecting all platforms. Changes: - Add helper methods: computeSampleRateFromIndex(), findSampleRateIndex() - Add SoapySDR/Logger.hpp include for error logging - Total: +97 insertions, -80 deletions Tested: Builds cleanly with gcc 15.2.1, SoapySDR module links successfully. --- SoapySDDC/Settings.cpp | 171 +++++++++++++++++++++------------------- SoapySDDC/SoapySDDC.hpp | 6 ++ 2 files changed, 97 insertions(+), 80 deletions(-) diff --git a/SoapySDDC/Settings.cpp b/SoapySDDC/Settings.cpp index cec43ba8..212f06cb 100644 --- a/SoapySDDC/Settings.cpp +++ b/SoapySDDC/Settings.cpp @@ -1,4 +1,5 @@ #include "SoapySDDC.hpp" +#include #include #include #include @@ -50,6 +51,10 @@ SoapySDDC::SoapySDDC(const SoapySDR::Kwargs &args) : deviceId(-1), adcnominalfreq = 128000000; RadioHandler.UpdateSampleRate(adcnominalfreq); } + + // Initialize samplerateidx to match default sampleRate = 32MHz + // In both modes (high-ADC and standard), 32MHz maps to index 4 + samplerateidx = 4; } SoapySDDC::~SoapySDDC(void) @@ -321,68 +326,18 @@ void SoapySDDC::setSampleRate(const int, const size_t, const double rate) { DbgPrintf("SoapySDDC::setSampleRate %f\n", rate); - if (adcnominalfreq > N2_BANDSWITCH) - { - // 128MHz ADC mode: 6 sample rates available - switch ((int)rate) - { - case 64000000: - sampleRate = 64000000; - samplerateidx = 5; - break; - case 32000000: - sampleRate = 32000000; - samplerateidx = 4; - break; - case 16000000: - sampleRate = 16000000; - samplerateidx = 3; - break; - case 8000000: - sampleRate = 8000000; - samplerateidx = 2; - break; - case 4000000: - sampleRate = 4000000; - samplerateidx = 1; - break; - case 2000000: - sampleRate = 2000000; - samplerateidx = 0; - break; - default: - return; - } - } - else - { - // 64MHz ADC mode: 5 sample rates available - switch ((int)rate) - { - case 32000000: - sampleRate = 32000000; - samplerateidx = 4; - break; - case 16000000: - sampleRate = 16000000; - samplerateidx = 3; - break; - case 8000000: - sampleRate = 8000000; - samplerateidx = 2; - break; - case 4000000: - sampleRate = 4000000; - samplerateidx = 1; - break; - case 2000000: - sampleRate = 2000000; - samplerateidx = 0; - break; - default: - return; - } + int idx = findSampleRateIndex(rate); + if (idx < 0) { + SoapySDR_logf(SOAPY_SDR_ERROR, "Unsupported sample rate %f Hz for ADC frequency %u Hz", + rate, adcnominalfreq); + return; } + + double computed = computeSampleRateFromIndex(idx); + sampleRate = computed; + samplerateidx = idx; + + DbgPrintf("SoapySDDC::setSampleRate: set index %d, actual rate %f\n", idx, computed); } double SoapySDDC::getSampleRate(const int, const size_t) const @@ -396,15 +351,13 @@ std::vector SoapySDDC::listSampleRates(const int, const size_t) const DbgPrintf("SoapySDDC::listSampleRates\n"); std::vector results; - results.push_back(2000000); - results.push_back(4000000); - results.push_back(8000000); - results.push_back(16000000); - results.push_back(32000000); - - if (adcnominalfreq > N2_BANDSWITCH) - { - results.push_back(64000000); + int numRates = (adcnominalfreq > N2_BANDSWITCH) ? 6 : 5; + + for (int idx = 0; idx < numRates; idx++) { + double rate = computeSampleRateFromIndex(idx); + if (rate > 0) { + results.push_back(rate); + } } return results; @@ -416,6 +369,44 @@ bool SoapySDDC::supportsHighADCFrequency() const return model == RX888 || model == RX888r2 || model == RX888r3 || model == RX999; } +double SoapySDDC::computeSampleRateFromIndex(int idx) const +{ + int numRates = (adcnominalfreq > N2_BANDSWITCH) ? 6 : 5; + if (idx < 0 || idx >= numRates) { + return -1.0; + } + + double bwmin = adcnominalfreq / 64.0; + if (adcnominalfreq > N2_BANDSWITCH) { + bwmin /= 2.0; + } + + int div = 1 << idx; + double srateM = div * 2.0; + double rate = bwmin * srateM; + + if (rate / adcnominalfreq * 2.0 > 1.1) { + return -1.0; + } + + return rate; +} + +int SoapySDDC::findSampleRateIndex(double rate) const +{ + int numRates = (adcnominalfreq > N2_BANDSWITCH) ? 6 : 5; + + for (int idx = 0; idx < numRates; idx++) { + double computed = computeSampleRateFromIndex(idx); + if (computed < 0) continue; + if (std::abs(computed - rate) < 1.0) { + return idx; + } + } + + return -1; +} + SoapySDR::ArgInfoList SoapySDDC::getSettingInfo(void) const { SoapySDR::ArgInfoList setArgs; @@ -439,13 +430,14 @@ SoapySDR::ArgInfoList SoapySDDC::getSettingInfo(void) const setArgs.push_back(BiasTVHFArg); // ADC frequency setting + bool highADCSupported = supportsHighADCFrequency(); + SoapySDR::ArgInfo AdcFreqArg; AdcFreqArg.key = "adc_frequency"; - // Default: 128MHz for capable devices, 64MHz for others - AdcFreqArg.value = supportsHighADCFrequency() ? "128000000" : "64000000"; + AdcFreqArg.value = highADCSupported ? "128000000" : "64000000"; AdcFreqArg.name = "ADC Sample Rate"; - if (supportsHighADCFrequency()) + if (highADCSupported) { AdcFreqArg.description = "ADC sample rate in Hz (50MHz-140MHz). Default 128MHz. " "Rates above 80MHz enable extended sample rates up to 64MHz output."; @@ -479,15 +471,34 @@ void SoapySDDC::writeSetting(const std::string &key, const std::string &value) } else if (key == "adc_frequency") { - uint32_t newAdcFreq = static_cast(std::stoul(value)); - if (newAdcFreq >= MIN_ADC_FREQ && newAdcFreq <= MAX_ADC_FREQ) - { - if (newAdcFreq <= 64000000 || supportsHighADCFrequency()) - { - adcnominalfreq = newAdcFreq; - RadioHandler.UpdateSampleRate(newAdcFreq); + try { + unsigned long freq_ul = std::stoul(value); + + if (freq_ul > UINT32_MAX) { + SoapySDR_logf(SOAPY_SDR_ERROR, "ADC frequency exceeds uint32_t maximum"); + return; } + + uint32_t newAdcFreq = static_cast(freq_ul); + uint32_t max_freq = supportsHighADCFrequency() ? MAX_ADC_FREQ : 64000000; + + if (newAdcFreq < MIN_ADC_FREQ || newAdcFreq > max_freq) { + SoapySDR_logf(SOAPY_SDR_ERROR, + "Invalid adc_frequency: must be %u-%u Hz", MIN_ADC_FREQ, max_freq); + return; + } + + adcnominalfreq = newAdcFreq; + RadioHandler.UpdateSampleRate(newAdcFreq); + + } catch (const std::invalid_argument& e) { + SoapySDR_logf(SOAPY_SDR_ERROR, + "Invalid adc_frequency format: '%s'", value.c_str()); + } catch (const std::out_of_range& e) { + SoapySDR_logf(SOAPY_SDR_ERROR, + "ADC frequency out of range: '%s'", value.c_str()); } + return; } } diff --git a/SoapySDDC/SoapySDDC.hpp b/SoapySDDC/SoapySDDC.hpp index 3353fe5f..e6ed7eb9 100644 --- a/SoapySDDC/SoapySDDC.hpp +++ b/SoapySDDC/SoapySDDC.hpp @@ -141,6 +141,12 @@ class SoapySDDC : public SoapySDR::Device // Helper to check if device supports high ADC frequencies bool supportsHighADCFrequency() const; + + // Compute expected sample rate for given index based on current ADC frequency + double computeSampleRateFromIndex(int idx) const; + + // Find best sample rate index for requested rate, returns -1 if invalid + int findSampleRateIndex(double rate) const; public: int Callback(void *context, const float *data, uint32_t len); From 2f8ee97c06cee6f000f95aa3a52cc0ea32311689 Mon Sep 17 00:00:00 2001 From: SteamedFish's OpenCode <69167+SteamedFish@users.noreply.github.com> Date: Thu, 12 Feb 2026 02:09:56 +0800 Subject: [PATCH 5/5] Fix ADC frequency change not updating sample rate member When ADC frequency is changed via writeSetting("adc_frequency"), the RadioHandler.UpdateSampleRate() is called correctly, but the sampleRate member variable was not recomputed to reflect the new ADC clock. This caused getSampleRate() to return stale values after ADC frequency changes until the stream was restarted. Fix: Recompute sampleRate from current samplerateidx after ADC update, following ExtIO's SetOverclock pattern (ExtIO_sddc.cpp:854-874). If the rate becomes invalid (Nyquist violation), reset to index 4 (mid-range). --- SoapySDDC/Settings.cpp | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/SoapySDDC/Settings.cpp b/SoapySDDC/Settings.cpp index 212f06cb..2cebaa27 100644 --- a/SoapySDDC/Settings.cpp +++ b/SoapySDDC/Settings.cpp @@ -385,6 +385,11 @@ double SoapySDDC::computeSampleRateFromIndex(int idx) const double srateM = div * 2.0; double rate = bwmin * srateM; + // Nyquist validation with 10% tolerance (1.1x instead of 1.0x) + // Intentional design per PR #240 GitHub Copilot review recommendation: + // Allows margin for floating-point precision, hardware ADC clock tolerances, + // and DSP pipeline headroom. ExtIO doesn't need this check because it restricts + // ADC to discrete values; SoapySDDC supports arbitrary 50-140 MHz ADC frequencies. if (rate / adcnominalfreq * 2.0 > 1.1) { return -1.0; } @@ -472,6 +477,13 @@ void SoapySDDC::writeSetting(const std::string &key, const std::string &value) else if (key == "adc_frequency") { try { + // Reject negative input before parsing (std::stoul wraps negative strings to huge unsigned values) + if (!value.empty() && value[0] == '-') { + SoapySDR_logf(SOAPY_SDR_ERROR, + "Invalid adc_frequency: cannot be negative ('%s')", value.c_str()); + return; + } + unsigned long freq_ul = std::stoul(value); if (freq_ul > UINT32_MAX) { @@ -491,6 +503,23 @@ void SoapySDDC::writeSetting(const std::string &key, const std::string &value) adcnominalfreq = newAdcFreq; RadioHandler.UpdateSampleRate(newAdcFreq); + // Recompute sampleRate member variable for current sample rate index + // Follows ExtIO's SetOverclock pattern (ExtIO_sddc.cpp lines 854-874) + double newRate = computeSampleRateFromIndex(samplerateidx); + if (newRate > 0) { + sampleRate = newRate; + SoapySDR_logf(SOAPY_SDR_INFO, + "ADC frequency changed to %u Hz, sample rate adjusted to %f Hz", + newAdcFreq, newRate); + } else { + // Current index invalid for new ADC freq, reset to safe default (index 4 = mid-range) + samplerateidx = 4; + sampleRate = computeSampleRateFromIndex(4); + SoapySDR_logf(SOAPY_SDR_WARNING, + "ADC frequency change invalidated sample rate index, reset to %f Hz", + sampleRate); + } + } catch (const std::invalid_argument& e) { SoapySDR_logf(SOAPY_SDR_ERROR, "Invalid adc_frequency format: '%s'", value.c_str());