implement FPGA-based CDS to avoid BRAM overflow

Move CDS phase computation from per-point config storage to FPGA logic.
The FPGA now computes the 180° phase shift internally (M/2) and loops
twice per point when CDS is enabled. This keeps config memory at 96 bits
instead of 112, avoiding BRAM overflow on the Spartan 6.

FPGA changes:
- Add CDS_ENABLED input to Sweep module (controlled via SPI register 6)
- Compute source_phase = M/2 for 180° shift when cds_phase=1
- State machine loops through all stages twice per point when CDS enabled
- RESULT_INDEX now includes cds_phase bit: stage[2:0] & point[11:0] & cds_phase

Firmware changes:
- Add FPGA::SetCDSEnabled() to control CDS via register 6 bit 11
- Update SamplingResult to include cdsPhase field (1 bit)
- Simplify VNA.cpp: FPGA handles phase switching, MCU combines results

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Roger Henderson 2026-02-01 21:19:57 +13:00
parent 0b571688a9
commit 1ea012aa57
8 changed files with 260 additions and 241 deletions

10
FPGA/VNA/SPIConfig.vhd Normal file → Executable file
View file

@ -45,7 +45,7 @@ entity SPICommands is
MAX2871_DEF_3 : out STD_LOGIC_VECTOR (31 downto 0);
MAX2871_DEF_1 : out STD_LOGIC_VECTOR (31 downto 0);
MAX2871_DEF_0 : out STD_LOGIC_VECTOR (31 downto 0);
SWEEP_DATA : out STD_LOGIC_VECTOR (111 downto 0);
SWEEP_DATA : out STD_LOGIC_VECTOR (95 downto 0);
SWEEP_ADDRESS : out STD_LOGIC_VECTOR (12 downto 0);
SWEEP_WRITE : out STD_LOGIC_VECTOR (0 downto 0);
SWEEP_POINTS : out STD_LOGIC_VECTOR (12 downto 0);
@ -54,6 +54,7 @@ entity SPICommands is
SETTLING_TIME : out STD_LOGIC_VECTOR (19 downto 0);
SYNC_ENABLED : out STD_LOGIC;
SYNC_MASTER : out STD_LOGIC;
CDS_ENABLED : out STD_LOGIC;
PORT1_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
PORT2_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
PORT1_EN : out STD_LOGIC;
@ -288,6 +289,7 @@ begin
when 5 => ADC_PHASEINC <= spi_buf_out(11 downto 0);
when 6 => STAGES <= spi_buf_out(15 downto 13);
SYNC_ENABLED <= spi_buf_out(12);
CDS_ENABLED <= spi_buf_out(11);
PORT1_STAGE <= spi_buf_out(5 downto 3);
PORT2_STAGE <= spi_buf_out(2 downto 0);
when 7 => SPI_OVERWRITE_ENABLED <= spi_buf_out(15);
@ -308,9 +310,9 @@ begin
end case;
selected_register <= selected_register + 1;
when WriteSweepConfig =>
if word_cnt = 7 then
-- Sweep config data is complete pass on (112 bits = 96 + 16)
SWEEP_DATA <= sweepconfig_buffer & spi_buf_out;
if word_cnt = 5 then
-- Sweep config data is complete (96 bits = 6 x 16-bit words)
SWEEP_DATA <= sweepconfig_buffer(79 downto 0) & spi_buf_out;
sweep_config_write <= '1';
else
-- shift next word into buffer

53
FPGA/VNA/Sweep.vhd Normal file → Executable file
View file

@ -34,7 +34,7 @@ entity Sweep is
RESET : in STD_LOGIC;
NPOINTS : in STD_LOGIC_VECTOR (12 downto 0);
CONFIG_ADDRESS : out STD_LOGIC_VECTOR (12 downto 0);
CONFIG_DATA : in STD_LOGIC_VECTOR (111 downto 0);
CONFIG_DATA : in STD_LOGIC_VECTOR (95 downto 0);
USER_NSAMPLES : in STD_LOGIC_VECTOR (12 downto 0);
NSAMPLES : out STD_LOGIC_VECTOR (12 downto 0);
SETTLING_TIME : in STD_LOGIC_VECTOR (19 downto 0);
@ -86,6 +86,9 @@ entity Sweep is
SOURCE_CE : out STD_LOGIC;
-- Correlated Double Sampling
CDS_ENABLED : in STD_LOGIC;
-- Debug signals
DEBUG_STATUS : out STD_LOGIC_VECTOR (10 downto 0);
RESULT_INDEX : out STD_LOGIC_VECTOR (15 downto 0)
@ -98,19 +101,31 @@ architecture Behavioral of Sweep is
signal state : Point_states;
signal settling_cnt : unsigned(19 downto 0);
signal stage_cnt : unsigned (2 downto 0);
signal config_reg : std_logic_vector(111 downto 0);
signal config_reg : std_logic_vector(95 downto 0);
signal source_active : std_logic;
-- Source phase value extracted from config (bits 111:100)
-- CDS phase tracking: 0 = first measurement (phase 0), 1 = second measurement (phase 180)
signal cds_phase : std_logic;
-- Source phase value: computed from CDS state
-- When CDS disabled or cds_phase=0: phase = 0
-- When CDS enabled and cds_phase=1: phase = M/2 (180 degrees)
signal source_phase : std_logic_vector(11 downto 0);
signal source_M : std_logic_vector(11 downto 0);
begin
CONFIG_ADDRESS <= std_logic_vector(point_cnt);
-- Extract source phase from config (new bits 111:100)
source_phase <= config_reg(111 downto 100);
-- Extract Source M from config (bits 38:27)
source_M <= config_reg(38 downto 27);
-- Phase adjustment is enabled when source_phase is non-zero
-- Compute source phase for CDS
-- Phase 0: source_phase = 0
-- Phase 180: source_phase = M/2 (right shift by 1)
source_phase <= (others => '0') when (CDS_ENABLED = '0' or cds_phase = '0')
else '0' & source_M(11 downto 1); -- M/2 for 180 degree shift
-- Phase adjustment is enabled when source_phase is non-zero (i.e., CDS phase 1)
SOURCE_PHASE_ADJUST <= '0' when source_phase = x"000" else '1';
-- assemble registers
@ -164,7 +179,7 @@ begin
DEBUG_STATUS(2) <= source_active;
DEBUG_STATUS(1 downto 0) <= (others => '1');
config_reg <= CONFIG_DATA;
config_reg <= CONFIG_DATA(95 downto 0);
process(CLK, RESET)
begin
@ -172,6 +187,7 @@ begin
if RESET = '1' then
point_cnt <= (others => '0');
stage_cnt <= (others => '0');
cds_phase <= '0';
state <= WaitInitialLow;
START_SAMPLING <= '0';
RELOAD_PLL_REGS <= '0';
@ -243,7 +259,9 @@ begin
-- wait for sampling to finish
START_SAMPLING <= '0';
if SAMPLING_BUSY = '0' then
RESULT_INDEX <= std_logic_vector(stage_cnt) & std_logic_vector(point_cnt);
-- RESULT_INDEX format: stage(2:0) & point(11:0) & cds_phase when CDS enabled
-- When CDS disabled, cds_phase is always 0
RESULT_INDEX <= std_logic_vector(stage_cnt) & std_logic_vector(point_cnt(11 downto 0)) & cds_phase;
state <= WaitTriggerLow;
end if;
when WaitTriggerLow =>
@ -259,19 +277,32 @@ begin
NEW_DATA <= '1';
if stage_cnt < unsigned(STAGES) then
stage_cnt <= stage_cnt + 1;
-- can go directly to preperation for next stage
-- can go directly to preparation for next stage
state <= Settling;
else
state <= NextPoint;
-- All stages done for this measurement
-- Check if we need another CDS phase
if CDS_ENABLED = '1' and cds_phase = '0' then
-- First CDS phase done, do second phase at 180 degrees
cds_phase <= '1';
stage_cnt <= (others => '0');
-- Go back to reload PLL with new phase
state <= TriggerSetup;
else
-- CDS complete or disabled, move to next point
state <= NextPoint;
end if;
end if;
settling_cnt <= unsigned(SETTLING_TIME);
when NextPoint =>
NEW_DATA <= '0';
-- Reset CDS phase for next point
cds_phase <= '0';
if point_cnt < unsigned(NPOINTS) then
point_cnt <= point_cnt + 1;
stage_cnt <= (others => '0');
state <= TriggerSetup;
else
else
point_cnt <= (others => '0');
state <= Done;
TRIGGER_OUT <= '0';

216
FPGA/VNA/ipcore_dir/SweepConfigMem.xco Normal file → Executable file
View file

@ -1,108 +1,108 @@
##############################################################
#
# Xilinx Core Generator version 14.6
# Date: Mon Sep 14 08:50:12 2020
#
##############################################################
#
# This file contains the customisation parameters for a
# Xilinx CORE Generator IP GUI. It is strongly recommended
# that you do not manually alter this file as it may cause
# unexpected and unsupported behavior.
#
##############################################################
#
# Generated from component: xilinx.com:ip:blk_mem_gen:7.3
#
##############################################################
#
# BEGIN Project Options
SET addpads = false
SET asysymbol = true
SET busformat = BusFormatAngleBracketNotRipped
SET createndf = false
SET designentry = VHDL
SET device = xc6slx9
SET devicefamily = spartan6
SET flowvendor = Other
SET formalverification = false
SET foundationsym = false
SET implementationfiletype = Ngc
SET package = tqg144
SET removerpms = false
SET simulationfiles = Behavioral
SET speedgrade = -2
SET verilogsim = false
SET vhdlsim = true
# END Project Options
# BEGIN Select
SELECT Block_Memory_Generator xilinx.com:ip:blk_mem_gen:7.3
# END Select
# BEGIN Parameters
CSET additional_inputs_for_power_estimation=false
CSET algorithm=Minimum_Area
CSET assume_synchronous_clk=false
CSET axi_id_width=4
CSET axi_slave_type=Memory_Slave
CSET axi_type=AXI4_Full
CSET byte_size=9
CSET coe_file=no_coe_file_loaded
CSET collision_warnings=ALL
CSET component_name=SweepConfigMem
CSET disable_collision_warnings=false
CSET disable_out_of_range_warnings=false
CSET ecc=false
CSET ecctype=No_ECC
CSET enable_32bit_address=false
CSET enable_a=Use_ENA_Pin
CSET enable_b=Always_Enabled
CSET error_injection_type=Single_Bit_Error_Injection
CSET fill_remaining_memory_locations=false
CSET interface_type=Native
CSET load_init_file=false
CSET mem_file=no_Mem_file_loaded
CSET memory_type=Simple_Dual_Port_RAM
CSET operating_mode_a=WRITE_FIRST
CSET operating_mode_b=WRITE_FIRST
CSET output_reset_value_a=0
CSET output_reset_value_b=0
CSET pipeline_stages=0
CSET port_a_clock=100
CSET port_a_enable_rate=100
CSET port_a_write_rate=50
CSET port_b_clock=100
CSET port_b_enable_rate=100
CSET port_b_write_rate=0
CSET primitive=8kx2
CSET read_width_a=112
CSET read_width_b=112
CSET register_porta_input_of_softecc=false
CSET register_porta_output_of_memory_core=false
CSET register_porta_output_of_memory_primitives=false
CSET register_portb_output_of_memory_core=false
CSET register_portb_output_of_memory_primitives=false
CSET register_portb_output_of_softecc=false
CSET remaining_memory_locations=0
CSET reset_memory_latch_a=false
CSET reset_memory_latch_b=false
CSET reset_priority_a=CE
CSET reset_priority_b=CE
CSET reset_type=SYNC
CSET softecc=false
CSET use_axi_id=false
CSET use_bram_block=Stand_Alone
CSET use_byte_write_enable=false
CSET use_error_injection_pins=false
CSET use_regcea_pin=false
CSET use_regceb_pin=false
CSET use_rsta_pin=false
CSET use_rstb_pin=false
CSET write_depth_a=4501
CSET write_width_a=112
CSET write_width_b=112
# END Parameters
# BEGIN Extra information
MISC pkg_timestamp=2012-11-19T16:22:25Z
# END Extra information
GENERATE
# CRC: e7b4a756
##############################################################
#
# Xilinx Core Generator version 14.7
# Date: Sun Feb 01 07:48:09 2026
#
##############################################################
#
# This file contains the customisation parameters for a
# Xilinx CORE Generator IP GUI. It is strongly recommended
# that you do not manually alter this file as it may cause
# unexpected and unsupported behavior.
#
##############################################################
#
# Generated from component: xilinx.com:ip:blk_mem_gen:7.3
#
##############################################################
#
# BEGIN Project Options
SET addpads = false
SET asysymbol = true
SET busformat = BusFormatAngleBracketNotRipped
SET createndf = false
SET designentry = VHDL
SET device = xc6slx9
SET devicefamily = spartan6
SET flowvendor = Other
SET formalverification = false
SET foundationsym = false
SET implementationfiletype = Ngc
SET package = tqg144
SET removerpms = false
SET simulationfiles = Behavioral
SET speedgrade = -2
SET verilogsim = false
SET vhdlsim = true
# END Project Options
# BEGIN Select
SELECT Block_Memory_Generator xilinx.com:ip:blk_mem_gen:7.3
# END Select
# BEGIN Parameters
CSET additional_inputs_for_power_estimation=false
CSET algorithm=Minimum_Area
CSET assume_synchronous_clk=false
CSET axi_id_width=4
CSET axi_slave_type=Memory_Slave
CSET axi_type=AXI4_Full
CSET byte_size=9
CSET coe_file=no_coe_file_loaded
CSET collision_warnings=ALL
CSET component_name=SweepConfigMem
CSET disable_collision_warnings=false
CSET disable_out_of_range_warnings=false
CSET ecc=false
CSET ecctype=No_ECC
CSET enable_32bit_address=false
CSET enable_a=Use_ENA_Pin
CSET enable_b=Always_Enabled
CSET error_injection_type=Single_Bit_Error_Injection
CSET fill_remaining_memory_locations=false
CSET interface_type=Native
CSET load_init_file=false
CSET mem_file=no_Mem_file_loaded
CSET memory_type=Simple_Dual_Port_RAM
CSET operating_mode_a=WRITE_FIRST
CSET operating_mode_b=WRITE_FIRST
CSET output_reset_value_a=0
CSET output_reset_value_b=0
CSET pipeline_stages=0
CSET port_a_clock=100
CSET port_a_enable_rate=100
CSET port_a_write_rate=50
CSET port_b_clock=100
CSET port_b_enable_rate=100
CSET port_b_write_rate=0
CSET primitive=8kx2
CSET read_width_a=96
CSET read_width_b=96
CSET register_porta_input_of_softecc=false
CSET register_porta_output_of_memory_core=false
CSET register_porta_output_of_memory_primitives=false
CSET register_portb_output_of_memory_core=false
CSET register_portb_output_of_memory_primitives=false
CSET register_portb_output_of_softecc=false
CSET remaining_memory_locations=0
CSET reset_memory_latch_a=false
CSET reset_memory_latch_b=false
CSET reset_priority_a=CE
CSET reset_priority_b=CE
CSET reset_type=SYNC
CSET softecc=false
CSET use_axi_id=false
CSET use_bram_block=Stand_Alone
CSET use_byte_write_enable=false
CSET use_error_injection_pins=false
CSET use_regcea_pin=false
CSET use_regceb_pin=false
CSET use_rsta_pin=false
CSET use_rstb_pin=false
CSET write_depth_a=4501
CSET write_width_a=96
CSET write_width_b=96
# END Parameters
# BEGIN Extra information
MISC pkg_timestamp=2012-11-19T16:22:25Z
# END Extra information
GENERATE
# CRC: e7b4a756

BIN
FPGA/VNA/top.bin Normal file → Executable file

Binary file not shown.

19
FPGA/VNA/top.vhd Normal file → Executable file
View file

@ -113,7 +113,7 @@ architecture Behavioral of top is
CLK : IN std_logic;
RESET : IN std_logic;
NPOINTS : IN std_logic_vector(12 downto 0);
CONFIG_DATA : IN std_logic_vector(111 downto 0);
CONFIG_DATA : IN std_logic_vector(95 downto 0);
USER_NSAMPLES : in STD_LOGIC_VECTOR (12 downto 0);
NSAMPLES : out STD_LOGIC_VECTOR (12 downto 0);
SETTLING_TIME : in STD_LOGIC_VECTOR (19 downto 0);
@ -154,11 +154,12 @@ architecture Behavioral of top is
PORT1_ACTIVE : out STD_LOGIC;
PORT2_ACTIVE : out STD_LOGIC;
SOURCE_CE : out STD_LOGIC;
CDS_ENABLED : in STD_LOGIC;
RESULT_INDEX : out STD_LOGIC_VECTOR (15 downto 0);
DEBUG_STATUS : out STD_LOGIC_VECTOR (10 downto 0)
);
END COMPONENT;
COMPONENT Windowing
PORT(
CLK : IN std_logic;
@ -252,7 +253,7 @@ architecture Behavioral of top is
MAX2871_DEF_3 : OUT std_logic_vector(31 downto 0);
MAX2871_DEF_1 : OUT std_logic_vector(31 downto 0);
MAX2871_DEF_0 : OUT std_logic_vector(31 downto 0);
SWEEP_DATA : OUT std_logic_vector(111 downto 0);
SWEEP_DATA : OUT std_logic_vector(95 downto 0);
SWEEP_ADDRESS : OUT std_logic_vector(12 downto 0);
SWEEP_WRITE : OUT std_logic_vector(0 to 0);
SWEEP_POINTS : OUT std_logic_vector(12 downto 0);
@ -261,6 +262,7 @@ architecture Behavioral of top is
SETTLING_TIME : out STD_LOGIC_VECTOR (19 downto 0);
SYNC_ENABLED : out STD_LOGIC;
SYNC_MASTER : out STD_LOGIC;
CDS_ENABLED : out STD_LOGIC;
PORT1_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
PORT2_STAGE : out STD_LOGIC_VECTOR (2 downto 0);
PORT1_EN : out STD_LOGIC;
@ -315,10 +317,10 @@ architecture Behavioral of top is
ena : IN STD_LOGIC;
wea : IN STD_LOGIC_VECTOR(0 DOWNTO 0);
addra : IN STD_LOGIC_VECTOR(12 DOWNTO 0);
dina : IN STD_LOGIC_VECTOR(111 DOWNTO 0);
dina : IN STD_LOGIC_VECTOR(95 DOWNTO 0);
clkb : IN STD_LOGIC;
addrb : IN STD_LOGIC_VECTOR(12 DOWNTO 0);
doutb : OUT STD_LOGIC_VECTOR(111 DOWNTO 0)
doutb : OUT STD_LOGIC_VECTOR(95 DOWNTO 0)
);
END COMPONENT;
@ -387,14 +389,15 @@ architecture Behavioral of top is
signal sweep_sync_master : STD_LOGIC;
signal sweep_port1_stage : STD_LOGIC_VECTOR (2 downto 0);
signal sweep_port2_stage : STD_LOGIC_VECTOR (2 downto 0);
signal sweep_config_data : std_logic_vector(111 downto 0);
signal sweep_config_data : std_logic_vector(95 downto 0);
signal cds_enabled : std_logic;
signal sweep_config_address : std_logic_vector(12 downto 0);
signal sweep_source_filter : std_logic_vector(1 downto 0);
signal sweep_band : std_logic;
signal sweep_attenuator : std_logic_vector(6 downto 0);
signal sweep_config_write_address : std_logic_vector(12 downto 0);
signal sweep_config_write_data : std_logic_vector(111 downto 0);
signal sweep_config_write_data : std_logic_vector(95 downto 0);
signal sweep_config_write : std_logic_vector(0 downto 0);
-- Phase adjustment signal from Sweep to Source PLL
@ -724,6 +727,7 @@ begin
SWEEP_HALTED => sweep_halted,
SWEEP_RESUME => sweep_resume,
SOURCE_PHASE_ADJUST => source_phase_adjust,
CDS_ENABLED => cds_enabled,
SYNC_ENABLED => sweep_sync_enabled,
SYNC_MASTER => sweep_sync_master,
TRIGGER_IN => sweep_trigger_in,
@ -815,6 +819,7 @@ begin
SETTLING_TIME => settling_time,
SYNC_ENABLED => sweep_sync_enabled,
SYNC_MASTER => sweep_sync_master,
CDS_ENABLED => cds_enabled,
PORT1_STAGE => sweep_port1_stage,
PORT2_STAGE => sweep_port2_stage,
SPI_OVERWRITE_ENABLED => HW_overwrite_enabled,

View file

@ -13,6 +13,7 @@
static FPGA::HaltedCallback halted_cb;
static uint16_t SysCtrlReg = 0x0000;
static uint16_t ISRMaskReg = 0x0000;
static uint16_t SweepSetupReg = 0x0000;
static uint32_t ADC_samplerate;
static volatile bool busy_reading = false;
@ -144,17 +145,27 @@ void FPGA::SetSettlingTime(uint16_t us) {
}
void FPGA::SetupSweep(uint8_t stages, uint8_t port1_stage, uint8_t port2_stage, bool synchronize, bool syncMaster) {
uint16_t value = 0x0000;
value |= (uint16_t) (stages & 0x07) << 13;
// Preserve CDS bit when updating sweep setup
SweepSetupReg = SweepSetupReg & 0x0800; // Keep bit 11 (CDS_ENABLED)
SweepSetupReg |= (uint16_t) (stages & 0x07) << 13;
if(synchronize) {
value |= 0x1000;
SweepSetupReg |= 0x1000;
}
value |= (port1_stage & 0x07) << 3;
value |= (port2_stage & 0x07) << 0;
WriteRegister(Reg::SweepSetup, value);
SweepSetupReg |= (port1_stage & 0x07) << 3;
SweepSetupReg |= (port2_stage & 0x07) << 0;
WriteRegister(Reg::SweepSetup, SweepSetupReg);
Enable(Periphery::SyncMaster, syncMaster);
}
void FPGA::SetCDSEnabled(bool enabled) {
if(enabled) {
SweepSetupReg |= 0x0800; // Set bit 11
} else {
SweepSetupReg &= ~0x0800; // Clear bit 11
}
WriteRegister(Reg::SweepSetup, SweepSetupReg);
}
void FPGA::Enable(Periphery p, bool enable) {
if (enable) {
SysCtrlReg |= (uint16_t) p;
@ -210,9 +221,8 @@ void FPGA::WriteMAX2871Default(uint32_t *DefaultRegs) {
}
void FPGA::WriteSweepConfig(uint16_t pointnum, bool lowband, uint32_t *SourceRegs, uint32_t *LORegs,
uint8_t attenuation, uint64_t frequency, Samples samples, bool halt, LowpassFilter filter,
uint16_t sourcePhase) {
uint16_t send[8];
uint8_t attenuation, uint64_t frequency, Samples samples, bool halt, LowpassFilter filter) {
uint16_t send[7];
// select which point this sweep config is for
send[0] = pointnum & 0x1FFF;
// assemble sweep config from required fields of PLL registers
@ -230,48 +240,48 @@ void FPGA::WriteSweepConfig(uint16_t pointnum, bool lowband, uint32_t *SourceReg
uint16_t Source_Power = (SourceRegs[4] & 0x00000018) >> 3;
// Config memory layout (112 bits):
// bits 111:100 - Source Phase P[11:0]
// bits 99:96 - Reserved
// bits 95:0 - Original 96-bit config
// Config memory layout (96 bits):
// bits 95:80 - LO_M[11:4], halt, LO_N[6], Source_N[6], samples[2:0], filter[1:0]
// bits 79:64 - LO_M[3:0], LO_FRAC[11:0]
// bits 63:48 - LO_DIV_A[2:0], LO_VCO[5:0], LO_N[5:0], lowband
// bits 47:32 - Source_Power[1:0], attenuation[6:0], Source_M[11:5]
// bits 31:16 - Source_M[4:0], Source_FRAC[11:1]
// bits 15:0 - Source_FRAC[0], Source_DIV_A[2:0], Source_VCO[5:0], Source_N[5:0]
// send[1]: bits 111:96 - Source Phase (12 bits) + Reserved (4 bits)
send[1] = (sourcePhase & 0x0FFF) << 4; // Phase in bits 15:4, reserved bits 3:0
// send[2]: bits 95:80 - LO_M[11:4], halt, LO_N[6], Source_N[6], samples[2:0], filter[1:0]
send[2] = LO_M >> 4;
// send[1]: bits 95:80
send[1] = LO_M >> 4;
if (halt) {
send[2] |= 0x8000;
send[1] |= 0x8000;
}
if(LO_N & 0x40) {
send[2] |= 0x4000;
send[1] |= 0x4000;
}
if(Source_N & 0x40) {
send[2] |= 0x2000;
send[1] |= 0x2000;
}
send[2] |= (int) samples << 10;
send[1] |= (int) samples << 10;
if(filter == LowpassFilter::Auto) {
// Select source LP filter
if (frequency >= 3500000000) {
send[2] |= (int) LowpassFilter::None << 8;
send[1] |= (int) LowpassFilter::None << 8;
} else if (frequency >= 1800000000) {
send[2] |= (int) LowpassFilter::M3500 << 8;
send[1] |= (int) LowpassFilter::M3500 << 8;
} else if (frequency >= 900000000) {
send[2] |= (int) LowpassFilter::M1880 << 8;
send[1] |= (int) LowpassFilter::M1880 << 8;
} else {
send[2] |= (int) LowpassFilter::M947 << 8;
send[1] |= (int) LowpassFilter::M947 << 8;
}
} else {
send[2] |= (int) filter << 8;
send[1] |= (int) filter << 8;
}
send[3] = (LO_M & 0x000F) << 12 | LO_FRAC;
send[4] = LO_DIV_A << 13 | LO_VCO << 7 | (LO_N & 0x3F) << 1;
send[2] = (LO_M & 0x000F) << 12 | LO_FRAC;
send[3] = LO_DIV_A << 13 | LO_VCO << 7 | (LO_N & 0x3F) << 1;
if (lowband) {
send[4] |= 0x0001;
send[3] |= 0x0001;
}
send[5] = Source_Power << 14 | (uint16_t) attenuation << 7 | Source_M >> 5;
send[6] = (Source_M & 0x001F) << 11 | Source_FRAC >> 1;
send[7] = (Source_FRAC & 0x0001) << 15 | Source_DIV_A << 12 | Source_VCO << 6 | (Source_N & 0x3F);
send[4] = Source_Power << 14 | (uint16_t) attenuation << 7 | Source_M >> 5;
send[5] = (Source_M & 0x001F) << 11 | Source_FRAC >> 1;
send[6] = (Source_FRAC & 0x0001) << 15 | Source_DIV_A << 12 | Source_VCO << 6 | (Source_N & 0x3F);
SwitchBytes(send[0]);
SwitchBytes(send[1]);
SwitchBytes(send[2]);
@ -279,9 +289,8 @@ void FPGA::WriteSweepConfig(uint16_t pointnum, bool lowband, uint32_t *SourceReg
SwitchBytes(send[4]);
SwitchBytes(send[5]);
SwitchBytes(send[6]);
SwitchBytes(send[7]);
Low(CS);
HAL_SPI_Transmit(&FPGA_SPI, (uint8_t*) send, 16, 100);
HAL_SPI_Transmit(&FPGA_SPI, (uint8_t*) send, 14, 100);
High(CS);
}
@ -333,8 +342,10 @@ void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
result.P2Q = assembleSampleResultValue(&raw[14]);
result.RefI = assembleSampleResultValue(&raw[8]);
result.RefQ = assembleSampleResultValue(&raw[2]);
result.pointNum = (uint16_t)(raw[38]&0x1F) << 8 | raw[39];
// RESULT_INDEX format: stage[2:0] & point[11:0] & cds_phase
result.stageNum = (raw[38] & 0xE0) >> 5;
result.pointNum = ((uint16_t)(raw[38] & 0x1F) << 7) | (raw[39] >> 1);
result.cdsPhase = raw[39] & 0x01;
High(CS);
busy_reading = false;
if ((status & 0x0004) && callback) {

View file

@ -37,8 +37,9 @@ using SamplingResult = struct _samplingresult {
int64_t P1I, P1Q;
int64_t P2I, P2Q;
int64_t RefI, RefQ;
uint16_t pointNum :13;
uint16_t pointNum :12;
uint16_t stageNum :3;
uint16_t cdsPhase :1; // 0 = first measurement (phase 0), 1 = second measurement (phase 180)
};
using DFTResult = struct _dftresult {
@ -120,8 +121,8 @@ void DisableInterrupt(Interrupt i);
void DisableAllInterrupts();
void WriteMAX2871Default(uint32_t *DefaultRegs);
void WriteSweepConfig(uint16_t pointnum, bool lowband, uint32_t *SourceRegs, uint32_t *LORegs,
uint8_t attenuation, uint64_t frequency, Samples samples, bool halt = false, LowpassFilter filter = LowpassFilter::Auto,
uint16_t sourcePhase = 0);
uint8_t attenuation, uint64_t frequency, Samples samples, bool halt = false, LowpassFilter filter = LowpassFilter::Auto);
void SetCDSEnabled(bool enabled);
using ReadCallback = void(*)(const SamplingResult &result);
bool InitiateSampleRead(ReadCallback cb);
void SetupDFT(uint32_t f_firstBin, uint32_t f_binSpacing);

View file

@ -55,14 +55,13 @@ static constexpr uint8_t PLLRefFreqsNum = sizeof(PLLRefFreqs)/sizeof(PLLRefFreqs
static uint8_t sourceRefIndex, LO1RefIndex;
// Correlated Double Sampling (CDS) state
static uint8_t cdsPhaseCount; // Number of phase samples (0 or 2-7)
// CDS is now handled by FPGA which takes 2 measurements at 0° and 180° phase
static bool cdsEnabled;
// 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;
@ -177,34 +176,20 @@ bool VNA::Setup(Protocol::SweepSettings s) {
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;
// CDS is now handled by FPGA with 180° phase shift (2 measurements per point)
cdsEnabled = settings.cdsPhases >= 2;
// 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 CDS in FPGA (FPGA handles the phase switching internally)
FPGA::SetCDSEnabled(cdsEnabled);
// Configure sweep
FPGA::SetNumberOfPoints(internalPoints);
FPGA::SetNumberOfPoints(settings.points);
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) {
@ -324,33 +309,11 @@ bool VNA::Setup(Protocol::SweepSettings s) {
needs_halt = true;
}
// 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);
}
// Write sweep config for this point
// CDS is handled by FPGA internally - it takes 2 measurements per point when enabled
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
@ -428,19 +391,18 @@ bool VNA::MeasurementDone(const FPGA::SamplingResult &result) {
if(!active) {
return false;
}
if(result.pointNum != pointCnt || result.stageNum != stageCnt) {
LOG_WARN("Indicated point does not match (%u != %u, %d != %d)", result.pointNum, pointCnt, result.stageNum, stageCnt);
FPGA::AbortSweep();
return false;
}
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];
if(cdsEnabled) {
// CDS mode: FPGA returns 2 measurements per point (cdsPhase 0 and 1)
// Verify point and stage numbers match expected values
if(result.pointNum != pointCnt || result.stageNum != stageCnt) {
LOG_WARN("Indicated point does not match (%u != %u, %d != %d)", result.pointNum, pointCnt, result.stageNum, stageCnt);
FPGA::AbortSweep();
return false;
}
// Weight: +1.0 for phase 0 (0°), -1.0 for phase 1 (180°)
float weight = result.cdsPhase == 0 ? 1.0f : -1.0f;
// Accumulate weighted values into per-stage accumulators
cdsAccumP1I[stageCnt] += result.P1I * weight;
@ -450,24 +412,24 @@ bool VNA::MeasurementDone(const FPGA::SamplingResult &result) {
cdsAccumRefI[stageCnt] += result.RefI * weight;
cdsAccumRefQ[stageCnt] += result.RefQ * weight;
// Check if all stages for this internal point are complete
// Check if all stages for this CDS phase 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
// Check if this was the second CDS phase (phase 1 = 180°)
if(result.cdsPhase == 1) {
// Both 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
// Reset accumulators for next point
cdsAccumP1I[stg] = cdsAccumP1Q[stg] = 0;
cdsAccumP2I[stg] = cdsAccumP2Q[stg] = 0;
cdsAccumRefI[stg] = cdsAccumRefQ[stg] = 0;
}
data.pointNum = userPoint;
data.pointNum = pointCnt;
if(zerospan) {
uint64_t timestamp = HW::getLastISRTimestamp();
@ -479,24 +441,31 @@ bool VNA::MeasurementDone(const FPGA::SamplingResult &result) {
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);
data.frequency = getPointFrequency(pointCnt);
data.cdBm = settings.cdbm_excitation_start + (settings.cdbm_excitation_stop - settings.cdbm_excitation_start) * pointCnt / (settings.points - 1);
}
// Send data for this user point
// Send data for this point
STM::DispatchToInterrupt(PassOnData);
// Check if sweep is complete
if(userPoint >= settings.points - 1) {
if(pointCnt >= settings.points - 1) {
pointCnt = 0;
return true; // End of sweep
}
pointCnt++;
}
pointCnt++;
}
return false;
}
// Non-CDS mode: verify point matches
if(result.pointNum != pointCnt || result.stageNum != stageCnt) {
LOG_WARN("Indicated point does not match (%u != %u, %d != %d)", result.pointNum, pointCnt, result.stageNum, stageCnt);
FPGA::AbortSweep();
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);