mirror of
https://github.com/jankae/LibreVNA.git
synced 2026-03-01 10:54:15 +01:00
add Correlated Double Sampling (CDS) support
Implements CDS to reduce noise by taking multiple measurements at different source PLL phase offsets and combining with cosine weighting. Firmware changes (VNA.cpp, Protocol.hpp): - Add cdsPhases field to SweepSettings (0=disabled, 2-7=phase count) - Configure N internal sweep points per user point with phase offsets - Accumulate weighted samples: result = Σ(sample[k] × cos(2π×k/N)) - Per-stage accumulators for multi-stage measurements PC application changes: - Add "CDS" checkbox to VNA acquisition toolbar - When enabled, sets cdsPhases=2 for 180° differential measurement - Tooltip explains the feature With 180° CDS (2 samples): - Sample at 0°: weight = cos(0°) = 1 - Sample at 180°: weight = cos(180°) = -1 - Combined result = Sample₀ - Sample₁₈₀ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
6c06293179
commit
0b571688a9
|
|
@ -512,6 +512,7 @@ bool LibreVNADriver::setVNA(const DeviceDriver::VNASettings &s, std::function<vo
|
|||
p.settings.suppressPeaks = VNASuppressInvalidPeaks ? 1 : 0;
|
||||
p.settings.fixedPowerSetting = VNAAdjustPowerLevel || s.dBmStart != s.dBmStop ? 0 : 1;
|
||||
p.settings.logSweep = s.logSweep ? 1 : 0;
|
||||
p.settings.cdsPhases = s.cds ? 2 : 0; // CDS with 180° phase offset (2 samples)
|
||||
|
||||
zerospan = (s.freqStart == s.freqStop) && (s.dBmStart == s.dBmStop);
|
||||
p.settings.port1Stage = find(s.excitedPorts.begin(), s.excitedPorts.end(), 1) - s.excitedPorts.begin();
|
||||
|
|
|
|||
|
|
@ -269,6 +269,8 @@ public:
|
|||
std::vector<int> excitedPorts;
|
||||
// amount of time the source stays at each point before taking measurements. Ignore if not supported
|
||||
double dwellTime;
|
||||
// Correlated Double Sampling: if true, take 2 samples at 0° and 180° phase and combine
|
||||
bool cds;
|
||||
};
|
||||
|
||||
class VNAMeasurement {
|
||||
|
|
|
|||
|
|
@ -482,6 +482,14 @@ VNA::VNA(AppWindow *window, QString name)
|
|||
connect(this, &VNA::dwellTimeChanged, acquisitionDwellTime, &SIUnitEdit::setValueQuiet);
|
||||
tb_acq->addWidget(acquisitionDwellTime);
|
||||
|
||||
auto cbCDS = new QCheckBox("CDS");
|
||||
cbCDS->setToolTip("Correlated Double Sampling: Take 2 measurements at 180° phase offset to reduce noise");
|
||||
connect(cbCDS, &QCheckBox::toggled, this, [=](bool checked){
|
||||
settings.cds = checked;
|
||||
SettingsChanged();
|
||||
});
|
||||
tb_acq->addWidget(cbCDS);
|
||||
|
||||
tb_acq->addWidget(new QLabel("Averaging:"));
|
||||
lAverages = new QLabel("0/");
|
||||
tb_acq->addWidget(lAverages);
|
||||
|
|
@ -2032,6 +2040,7 @@ void VNA::ConfigureDevice(bool resetTraces, std::function<void(bool)> cb)
|
|||
s.logSweep = false;
|
||||
}
|
||||
s.dwellTime = settings.dwellTime;
|
||||
s.cds = settings.cds;
|
||||
if(window->getDevice() && isActive) {
|
||||
window->getDevice()->setVNA(s, [=](bool res){
|
||||
// device received command, reset traces now
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ public:
|
|||
int activeSegment;
|
||||
bool zerospan;
|
||||
double firstPointTime; // timestamp of the first point in the sweep, only use when zerospan is used
|
||||
bool cds; // Correlated Double Sampling (180° phase shift)
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -181,6 +181,7 @@ using SweepSettings = struct _sweepSettings {
|
|||
|
||||
int16_t cdbm_excitation_stop; // in 1/100 dbm
|
||||
uint16_t dwell_time; // in us
|
||||
uint8_t cdsPhases; // Correlated Double Sampling: 0=disabled, 2-7=number of phase samples
|
||||
};
|
||||
|
||||
using ReferenceSettings = struct _referenceSettings {
|
||||
|
|
|
|||
|
|
@ -54,6 +54,16 @@ static uint32_t PLLRefFreqs[] = {HW::PLLRef, HW::PLLRef - 1000000};
|
|||
static constexpr uint8_t PLLRefFreqsNum = sizeof(PLLRefFreqs)/sizeof(PLLRefFreqs[0]);
|
||||
static uint8_t sourceRefIndex, LO1RefIndex;
|
||||
|
||||
// Correlated Double Sampling (CDS) state
|
||||
static uint8_t cdsPhaseCount; // Number of phase samples (0 or 2-7)
|
||||
// CDS accumulators for weighted samples, per stage (max 8 stages)
|
||||
// I and Q for each receiver: Port1, Port2, Reference
|
||||
static double cdsAccumP1I[8], cdsAccumP1Q[8];
|
||||
static double cdsAccumP2I[8], cdsAccumP2Q[8];
|
||||
static double cdsAccumRefI[8], cdsAccumRefQ[8];
|
||||
// Precomputed cosine weights for CDS
|
||||
static float cdsWeights[7]; // Max 7 phases
|
||||
|
||||
using namespace HWHAL;
|
||||
|
||||
static uint64_t getPointFrequency(uint16_t pointNum) {
|
||||
|
|
@ -165,8 +175,36 @@ bool VNA::Setup(Protocol::SweepSettings s) {
|
|||
settings = s;
|
||||
// calculate factor between adjacent points for log sweep for faster calculation when sweeping
|
||||
logMultiplier = pow((double) settings.f_stop / settings.f_start, 1.0 / (settings.points-1));
|
||||
|
||||
// Initialize Correlated Double Sampling (CDS)
|
||||
cdsPhaseCount = settings.cdsPhases >= 2 ? settings.cdsPhases : 0;
|
||||
// Clear per-stage accumulators
|
||||
for(uint8_t stg = 0; stg < 8; stg++) {
|
||||
cdsAccumP1I[stg] = cdsAccumP1Q[stg] = 0;
|
||||
cdsAccumP2I[stg] = cdsAccumP2Q[stg] = 0;
|
||||
cdsAccumRefI[stg] = cdsAccumRefQ[stg] = 0;
|
||||
}
|
||||
// Precompute cosine weights: cos(2*pi*k/N)
|
||||
if(cdsPhaseCount >= 2) {
|
||||
for(uint8_t k = 0; k < cdsPhaseCount; k++) {
|
||||
cdsWeights[k] = cosf(2.0f * M_PI * k / cdsPhaseCount);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate internal point count (multiply by CDS phases if enabled)
|
||||
uint16_t internalPoints = settings.points;
|
||||
if(cdsPhaseCount >= 2) {
|
||||
internalPoints = settings.points * cdsPhaseCount;
|
||||
if(internalPoints > FPGA::MaxPoints) {
|
||||
// Reduce user points to fit
|
||||
settings.points = FPGA::MaxPoints / cdsPhaseCount;
|
||||
internalPoints = settings.points * cdsPhaseCount;
|
||||
LOG_WARN("CDS: reduced points to %u to fit FPGA limit", settings.points);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure sweep
|
||||
FPGA::SetNumberOfPoints(settings.points);
|
||||
FPGA::SetNumberOfPoints(internalPoints);
|
||||
uint32_t samplesPerPoint = (HW::getADCRate() / s.if_bandwidth);
|
||||
// round up to next multiple of 16 (16 samples are spread across 5 IF2 periods)
|
||||
if(samplesPerPoint%16) {
|
||||
|
|
@ -286,9 +324,33 @@ bool VNA::Setup(Protocol::SweepSettings s) {
|
|||
needs_halt = true;
|
||||
}
|
||||
|
||||
FPGA::WriteSweepConfig(i, lowband, Source.GetRegisters(),
|
||||
LO1.GetRegisters(), attenuator, freq,
|
||||
FPGA::Samples::SPPRegister, needs_halt);
|
||||
// Write sweep config(s) for this user point
|
||||
if(cdsPhaseCount >= 2) {
|
||||
// CDS enabled: write N configs with different phases
|
||||
// Extract Source M from PLL registers for phase calculation
|
||||
uint32_t* sourceRegs = Source.GetRegisters();
|
||||
uint16_t Source_M = (sourceRegs[1] & 0x00007FF8) >> 3;
|
||||
|
||||
for(uint8_t k = 0; k < cdsPhaseCount; k++) {
|
||||
uint16_t internalPointNum = i * cdsPhaseCount + k;
|
||||
// Calculate phase: sourcePhase = M * k / N
|
||||
// This gives phase = k * 360 / N degrees
|
||||
uint16_t sourcePhase = (uint16_t)((uint32_t)Source_M * k / cdsPhaseCount);
|
||||
|
||||
// Only halt on first CDS phase of each point (if needed)
|
||||
bool pointHalt = (k == 0) ? needs_halt : false;
|
||||
|
||||
FPGA::WriteSweepConfig(internalPointNum, lowband, sourceRegs,
|
||||
LO1.GetRegisters(), attenuator, freq,
|
||||
FPGA::Samples::SPPRegister, pointHalt, FPGA::LowpassFilter::Auto,
|
||||
sourcePhase);
|
||||
}
|
||||
} else {
|
||||
// No CDS: single config per point
|
||||
FPGA::WriteSweepConfig(i, lowband, Source.GetRegisters(),
|
||||
LO1.GetRegisters(), attenuator, freq,
|
||||
FPGA::Samples::SPPRegister, needs_halt);
|
||||
}
|
||||
last_lowband = lowband;
|
||||
}
|
||||
// reset a possibly changed PLL reference index
|
||||
|
|
@ -371,7 +433,71 @@ bool VNA::MeasurementDone(const FPGA::SamplingResult &result) {
|
|||
FPGA::AbortSweep();
|
||||
return false;
|
||||
}
|
||||
// normal sweep mode
|
||||
|
||||
if(cdsPhaseCount >= 2) {
|
||||
// CDS mode: accumulate weighted samples per stage
|
||||
// Internal point mapping: user_point = pointCnt / cdsPhaseCount
|
||||
// cds_phase = pointCnt % cdsPhaseCount
|
||||
uint16_t userPoint = pointCnt / cdsPhaseCount;
|
||||
uint8_t cdsPhase = pointCnt % cdsPhaseCount;
|
||||
float weight = cdsWeights[cdsPhase];
|
||||
|
||||
// Accumulate weighted values into per-stage accumulators
|
||||
cdsAccumP1I[stageCnt] += result.P1I * weight;
|
||||
cdsAccumP1Q[stageCnt] += result.P1Q * weight;
|
||||
cdsAccumP2I[stageCnt] += result.P2I * weight;
|
||||
cdsAccumP2Q[stageCnt] += result.P2Q * weight;
|
||||
cdsAccumRefI[stageCnt] += result.RefI * weight;
|
||||
cdsAccumRefQ[stageCnt] += result.RefQ * weight;
|
||||
|
||||
// Check if all stages for this internal point are complete
|
||||
stageCnt++;
|
||||
if(stageCnt > settings.stages) {
|
||||
stageCnt = 0;
|
||||
|
||||
// Check if this was the last CDS phase for this user point
|
||||
if(cdsPhase == cdsPhaseCount - 1) {
|
||||
// All CDS phases complete - add combined values to data
|
||||
for(uint8_t stg = 0; stg <= settings.stages; stg++) {
|
||||
data.addValue((int64_t)cdsAccumP1I[stg], (int64_t)cdsAccumP1Q[stg], stg, (int) Protocol::Source::Port1);
|
||||
data.addValue((int64_t)cdsAccumP2I[stg], (int64_t)cdsAccumP2Q[stg], stg, (int) Protocol::Source::Port2);
|
||||
data.addValue((int64_t)cdsAccumRefI[stg], (int64_t)cdsAccumRefQ[stg], stg, (int) Protocol::Source::Port1 | (int) Protocol::Source::Port2 | (int) Protocol::Source::Reference);
|
||||
// Reset accumulators for next user point
|
||||
cdsAccumP1I[stg] = cdsAccumP1Q[stg] = 0;
|
||||
cdsAccumP2I[stg] = cdsAccumP2Q[stg] = 0;
|
||||
cdsAccumRefI[stg] = cdsAccumRefQ[stg] = 0;
|
||||
}
|
||||
data.pointNum = userPoint;
|
||||
|
||||
if(zerospan) {
|
||||
uint64_t timestamp = HW::getLastISRTimestamp();
|
||||
if(firstPoint) {
|
||||
data.us = 0;
|
||||
firstPointTime = timestamp;
|
||||
firstPoint = false;
|
||||
} else {
|
||||
data.us = timestamp - firstPointTime;
|
||||
}
|
||||
} else {
|
||||
data.frequency = getPointFrequency(userPoint);
|
||||
data.cdBm = settings.cdbm_excitation_start + (settings.cdbm_excitation_stop - settings.cdbm_excitation_start) * userPoint / (settings.points - 1);
|
||||
}
|
||||
|
||||
// Send data for this user point
|
||||
STM::DispatchToInterrupt(PassOnData);
|
||||
|
||||
// Check if sweep is complete
|
||||
if(userPoint >= settings.points - 1) {
|
||||
pointCnt = 0;
|
||||
return true; // End of sweep
|
||||
}
|
||||
}
|
||||
pointCnt++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-CDS mode: original behavior
|
||||
data.addValue(result.P1I, result.P1Q, stageCnt, (int) Protocol::Source::Port1);
|
||||
data.addValue(result.P2I, result.P2Q, stageCnt, (int) Protocol::Source::Port2);
|
||||
data.addValue(result.RefI, result.RefQ, stageCnt, (int) Protocol::Source::Port1 | (int) Protocol::Source::Port2 | (int) Protocol::Source::Reference);
|
||||
|
|
|
|||
Loading…
Reference in a new issue