diff --git a/.gitignore b/.gitignore index b17acc9..882fcee 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ .Xil xvlog.pb *vivado_pid* +**/work/* # some generated files (they annoy me) update_config.tcl diff --git a/designs/reflectometer_base/reflectometer.sv b/designs/reflectometer_base/reflectometer.sv index 1e1bd0a..4ae5dc4 100644 --- a/designs/reflectometer_base/reflectometer.sv +++ b/designs/reflectometer_base/reflectometer.sv @@ -10,7 +10,6 @@ module reflectometer_top #( parameter int unsigned ZERO_LEVEL = 8192, parameter int unsigned ACCUM_WIDTH = 32, parameter int unsigned N_MAX = 4096, - parameter int unsigned WINDOW_SIZE = 65, parameter int unsigned PACKET_SIZE = 1024 )( input wire clk_in, @@ -18,12 +17,17 @@ module reflectometer_top #( output wire locked, // Accumulator AXI-S bus - input wire clk_axis_accumulator, // GMII PHY RX clock - axis_if.master axis_accumulator, + input wire clk_axis_accumulator, // GMII PHY RX clock + axis_if.master axis_accumulator, // Control AXI-S bus - input wire clk_axis_control, // GMII PHY TX clock - axis_if.slave axis_control, + input wire clk_axis_control, // GMII PHY TX clock + axis_if.slave axis_control, + input wire [31:0] window_size, // New accum & old controller crutch + + // Status signals + output wire workflow_done, + output wire processing_done, // RTL-MAC handshake input wire request_ready, @@ -216,11 +220,12 @@ module reflectometer_top #( // ------------------------------------------------------------------------- // Accumulator // ------------------------------------------------------------------------- + assign workflow_done = finish; + accumulator_top #( .DATA_WIDTH(ADC_DATA_WIDTH), .ACCUM_WIDTH(ACCUM_WIDTH), .N_MAX(N_MAX), - .WINDOW_SIZE(WINDOW_SIZE), .PACKET_SIZE(PACKET_SIZE) ) accumulator_top_dut ( .clk_in(clk_sampler), @@ -230,6 +235,7 @@ module reflectometer_top #( .start(adc_start), .smp_num(adc_pulse_period), .seq_num(adc_pulse_num), + .window_size(window_size), .req_ready(request_ready), .send_req(send_request), @@ -239,7 +245,8 @@ module reflectometer_top #( .m_axis_tready(axis_accumulator.tready), .m_axis_tlast(axis_accumulator.tlast), - .finish(finish) + .finish(finish), // full reflectometer workflow complete (with transaction) + .accum_done(processing_done) // signal generation, sampling and processing complete ); endmodule diff --git a/designs/reflectometer_base/reflectometer_tb.sv b/designs/reflectometer_base/reflectometer_tb.sv index 1ffdfba..1d1cbd9 100644 --- a/designs/reflectometer_base/reflectometer_tb.sv +++ b/designs/reflectometer_base/reflectometer_tb.sv @@ -11,16 +11,22 @@ module reflectometer_tb; localparam LOGIC_ZERO_LEVEL = 0; // DAC -5V for logic zero localparam VOLTAGE_ZERO_LEVEL = 2**(DAC_DATA_WIDTH-1); // DAC 0V for logic zero localparam PACK_FACTOR = 1; // not used in TB - localparam PROCESS_MODE = 1; // 0 - uint, 1 - int + localparam PROCESS_MODE = 0; // 0 - uint, 1 - int. Current accumulator don't support signed sum localparam ACCUM_WIDTH = 32; // accumulator number bit witdth localparam N_MAX = 4096; // max value of windows to average by experiments - localparam WINDOW_SIZE = 65; // fixed subwindow size to average by time localparam PACKET_SIZE = 1024; // bytes per UDP packet + + localparam int REQUEST_TIMEOUT = 3 * PACKET_SIZE; // timeout for packet receiving from accumulator localparam ZERO_LEVEL = LOGIC_ZERO_LEVEL; // "logic" VS "voltage" localparam CLK_ETH_PHY_PERIOD = 8.000; // 125 MHz localparam CLK_REF_PERIOD = 5.000; // 200 MHz + + //------------------------------------------------------------ + // Глобальные перменные + //------------------------------------------------------------ + int unsigned WINDOW_SIZE = 65; // fixed subwindow size to average by time //------------------------------------------------------------ // Тактовые сигналы и сброс @@ -28,43 +34,48 @@ module reflectometer_tb; logic clk_ref = 1'b0; // 200 MHz logic clk_eth_phy = 1'b0; // common for RX & TX logic rst_n = 1'b0; - //------------------------------------------------------------ - // Управление и конфиг - //------------------------------------------------------------ //------------------------------------------------------------ - // Входы - //------------------------------------------------------------ - - //------------------------------------------------------------ - // Выходы - //------------------------------------------------------------ - wire mmcm_locked; - //------------------------------------------------------------ - // Внутренние сигналы тестбенча + // Управление и конфиг DUT //------------------------------------------------------------ + logic [31:0] window_size; // AXI-S интерфейс для управления axis_if axis_control_if ( .clk(clk_eth_phy), .rst_n(rst_n) ); + + //------------------------------------------------------------ + // Входы DUT + //------------------------------------------------------------ + // ADC интерфейс + wire clk_adc; + wire adc_otr; + wire [ADC_DATA_WIDTH-1:0] adc_data; + + //------------------------------------------------------------ + // Выходы + //------------------------------------------------------------ + // Статусы + wire mmcm_locked; + wire workflow_done; + wire processing_done; + // DAC интерфейс + wire clk_dac; + wire dac_wrt; + wire [DAC_DATA_WIDTH-1:0] dac_data; // AXI-S интерфейс для данных axis_if axis_accumulator_if ( .clk(clk_eth_phy), .rst_n(rst_n) ); - // DAC интерфейс - wire clk_dac; - wire dac_wrt; - wire [DAC_DATA_WIDTH-1:0] dac_data; - // ADC интерфейс - wire clk_adc; - wire adc_otr; - wire [ADC_DATA_WIDTH-1:0] adc_data; + //------------------------------------------------------------ + // Внутренние сигналы тестбенча + //------------------------------------------------------------ // Интерфейс хендшейка с MAC-PHY wire send_request; logic request_ready; - // Сигналы ЦАП и АЦП + // Сигнал между ЦАП и АЦП real signal_voltage; //------------------------------------------------------------ @@ -79,6 +90,7 @@ module reflectometer_tb; .data_i(dac_data), .voltage_o(signal_voltage) ); + //------------------------------------------------------------ // Virtual ADC //------------------------------------------------------------ @@ -90,8 +102,9 @@ module reflectometer_tb; .otr_o(adc_otr), .data_o(adc_data) ); + //------------------------------------------------------------ - // Statistics monitor + // Statistics processing //------------------------------------------------------------ //------------------------------------------------------------ @@ -109,12 +122,15 @@ module reflectometer_tb; .ZERO_LEVEL(ZERO_LEVEL), .ACCUM_WIDTH(ACCUM_WIDTH), .N_MAX(N_MAX), - .WINDOW_SIZE(WINDOW_SIZE), .PACKET_SIZE(PACKET_SIZE) ) DUT ( .clk_in(clk_ref), .rst_n(rst_n), + + // Status .locked(mmcm_locked), + .workflow_done(workflow_done), + .processing_done(processing_done), // Accumulator AXI-S bus .clk_axis_accumulator(clk_eth_phy), // GMII PHY RX clock @@ -123,6 +139,7 @@ module reflectometer_tb; // Control AXI-S bus .clk_axis_control(clk_eth_phy), // GMII PHY TX clock .axis_control(axis_control_if.slave), + .window_size(window_size), // direct signal crutch (old controller) // RTL-MAC handshake .request_ready(request_ready), @@ -138,6 +155,8 @@ module reflectometer_tb; .adc_data(adc_data), .adc_otr(adc_otr) ); + assign window_size = WINDOW_SIZE; + //------------------------------------------------------------ // Тактовые сигналы //------------------------------------------------------------ @@ -147,6 +166,7 @@ module reflectometer_tb; initial begin forever #(CLK_ETH_PHY_PERIOD/2) clk_eth_phy = ~clk_eth_phy; end + //------------------------------------------------------------ // Таски для тестирования //------------------------------------------------------------ @@ -169,11 +189,16 @@ module reflectometer_tb; input logic [31:0] pulse_period, input logic [15:0] pulse_num, input logic [13:0] pulse_height, // achtung! p_height strictly must have 14 bits of width - input logic [31:0] pulse_period_adc + input logic [31:0] pulse_period_adc, + input logic [31:0] window_size ); // Создаем временный фиксированный массив и упаковываем всё одной строкой logic [7:0] tx_packet[]; - + + // Ахтунг, 14-битный ЦАП захардкожен + if (DAC_DATA_WIDTH != 14) + $display("[WARNING] -dut_send_system_config- Default pulse height (DAC bitwidth) is equal to 14. Be aware, controller packet structure is coded for 14 bits"); + tx_packet = '{ 8'h88, // Команда pulse_width[7:0], pulse_width[15:8], pulse_width[23:16], pulse_width[31:24], @@ -183,9 +208,12 @@ module reflectometer_tb; }; vif.master_send(tx_packet); + + // TODO remove for new controller + WINDOW_SIZE = window_size; endtask - // Таски сбора и обработки статистики + // Таски сбора статистики task automatic dut_read_output( virtual axis_if#(8).tb vif, input int sample_num, @@ -194,43 +222,101 @@ module reflectometer_tb; ); logic [7:0] rx_packet[]; logic [ACCUM_WIDTH-1:0] data_packet[]; - int packet_num = $ceil(real'(sample_num / WINDOW_SIZE) / real'(PACKET_SIZE)); int numbers_per_packet = PACKET_SIZE/(ACCUM_WIDTH/8); - int idx = 0; + int packet_num = $ceil(real'(sample_num / WINDOW_SIZE) / real'(numbers_per_packet)); + int timeout_flag = 0; + int packet_counter = 0; if (sample_num % WINDOW_SIZE) begin - $display("[TB] -dut_read_output- Error, sample_num must be multiple of WINDOW_SIZE: %0d %% %0d = %0d", sample_num, WINDOW_SIZE, sample_num % WINDOW_SIZE); + $display("[ERROR] -dut_read_output- Sample_num must be multiple of WINDOW_SIZE: %0d %% %0d = %0d", sample_num, WINDOW_SIZE, sample_num % WINDOW_SIZE); $finish; end data_packet = new[numbers_per_packet]; output_data = new[numbers_per_packet * packet_num]; - // count send_request posedge todo - // timeout todo + + // count send_request pulses (equal to number of packets) + fork + begin : packet_counter_proc + forever begin + @(posedge clk_eth_phy); + if(send_request === 1) + packet_counter++; + end + end + join_none + + // Wait until reflectometer done sampling and averaging + wait(processing_done == 1); // recv loop - for (int i = 0; i < packet_num; i++) begin - // randomize_recv_delays todo - request_ready = 1; - vif.slave_recv(rx_packet); - request_ready = 0; + // если число пакетов превышает заложенное предрассчитанное значение -- ошибка + fork : recv_loop_proc + begin + // packet recv loop + forever begin + if (packet_counter > packet_num) begin + $display("[ERROR] -dut_read_output- Packet overflow detected. Number of data packets exceeds expected amount of packets"); + $finish; + end - // unpack values - data_packet = {<< byte {rx_packet}}; - data_packet = {<< ACCUM_WIDTH {data_packet}}; + if (randomize_recv_delays) + repeat($urandom_range(0, 500)) @(posedge clk_eth_phy); + + timeout_flag = 0; + fork : receive_packet_timeout + begin + request_ready = 1; + vif.slave_recv(rx_packet); + request_ready = 0; + end + begin + repeat(REQUEST_TIMEOUT) @(posedge clk_eth_phy); + timeout_flag = 1; + end + join_any - // copy and convert values - for (int j = 0; j < data_packet.size(); j++) begin - output_data[i * data_packet.size() + j] = int'(data_packet[j]); + disable receive_packet_timeout; + if (timeout_flag) begin + $display("[ERROR] -dut_read_output- Timeout detected when receiving packet"); + $finish; + end + + if (rx_packet.size() != PACKET_SIZE) begin + $display("[ERROR] -dut_read_output- Wrong packet size received: %0d bytes received, %0d bytes expected", rx_packet.size(), PACKET_SIZE); + $finish; + end + + // unpack values + data_packet = {<< byte {rx_packet}}; + data_packet = {<< ACCUM_WIDTH {data_packet}}; + + // copy and convert values + for (int j = 0; j < data_packet.size(); j++) begin + output_data[(packet_counter-1) * data_packet.size() + j] = int'(data_packet[j]); + end + + end end + begin + // IP workflow completion event + wait(workflow_done == 1); + end + join_any + + disable recv_loop_proc; + disable packet_counter_proc; + + if (packet_counter != packet_num) begin + $display("[ERROR] -dut_read_output- Wrong number of packets received: %0d received, %0d expected", packet_counter, packet_num); + $finish; end - // wait for dut.finish posedge todo - // timeout todo - - // error handling todo endtask + // Основная таска типового теста + // todo + //------------------------------------------------------------ // ОСНОВНОЙ ПРОЦЕСС ТЕСТИРОВАНИЯ //------------------------------------------------------------ @@ -261,7 +347,8 @@ module reflectometer_tb; .pulse_period(32'd5000), .pulse_num(16'd1), .pulse_height(14'd15000), // 0V - .pulse_period_adc(32'd2600) + .pulse_period_adc(32'd2600), + .window_size(1) ); #100; dut_start(control_vif); @@ -272,6 +359,7 @@ module reflectometer_tb; .randomize_recv_delays(0), .output_data(output_data) ); + #1000; $display("Received %0d numbers", output_data.size()); for (int i = 0; i < output_data.size(); i++) begin diff --git a/rtl/accum/src/accum_top.sv b/rtl/accum/src/accum_top.sv index 66a1424..8bf5a6a 100644 --- a/rtl/accum/src/accum_top.sv +++ b/rtl/accum/src/accum_top.sv @@ -121,4 +121,4 @@ module accumulator_top .batch_req (batch_req), .finish (finish_int) ); -endmodule \ No newline at end of file +endmodule diff --git a/scripts/questa.mk b/scripts/questa.mk new file mode 100644 index 0000000..59a0a1e --- /dev/null +++ b/scripts/questa.mk @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: MIT +################################################################### +# Questa/ModelSim simulation helper +# +# Expected variables from the project Makefile: +# SYN_FILES - RTL sources +# TB_FILES - testbench sources +# INC_FILES - include files, optional +# SIM_TOP - simulation top module +# SIM_DEFS - defines, optional +# SIM_RUNTIME - run time, for example "10000 us" or "-all" +# +# Useful overrides: +# make sim-questa XILINX_VIVADO=/opt/Xilinx/Vivado/2024.2 +# make sim-questa-gui +################################################################### + +.PHONY: sim-questa sim-questa-gui questa-clean questa-xpm-clean + +VLIB ?= vlib +VMAP ?= vmap +VLOG ?= vlog +VSIM ?= vsim + +QUESTA_WORK_LIB ?= work +QUESTA_XPM_LIB ?= xpm +QUESTA_DIR ?= questa_build +QUESTA_TRANSCRIPT ?= transcript +XILINX_VIVADO=/tools/Xilinx/2025.1/Vivado +SIM_RUNTIME ?= 10000 us +QUESTA_RUN ?= run $(SIM_RUNTIME) + +# XPM is needed by this design because accum.sv uses xpm_memory_sdpram and +# out_axis_fifo.sv uses xpm_fifo_async. Point XILINX_VIVADO to your Vivado +# install if it is not already exported by settings64.sh. +QUESTA_USE_XPM ?= 1 + +QUESTA_XPM_SRC = \ + $(XILINX_VIVADO)/data/ip/xpm/xpm_cdc/hdl/xpm_cdc.sv \ + $(XILINX_VIVADO)/data/ip/xpm/xpm_memory/hdl/xpm_memory.sv \ + $(XILINX_VIVADO)/data/ip/xpm/xpm_fifo/hdl/xpm_fifo.sv + +QUESTA_GLBL_SRC = $(XILINX_VIVADO)/data/verilog/src/glbl.v + +QUESTA_DEFS = $(foreach d,$(SIM_DEFS),+define+$(d)) +QUESTA_INC_DIRS = $(sort $(dir $(SYN_FILES) $(TB_FILES) $(INC_FILES))) +QUESTA_INCS = $(foreach d,$(QUESTA_INC_DIRS),+incdir+$(d)) +QUESTA_LIBS = $(if $(filter 1,$(QUESTA_USE_XPM)),-L $(QUESTA_XPM_LIB),) +QUESTA_SOURCES = $(SYN_FILES) $(TB_FILES) $(if $(wildcard $(QUESTA_GLBL_SRC)),$(QUESTA_GLBL_SRC),) + +QUESTA_GLBL_TOP = $(if $(wildcard $(QUESTA_GLBL_SRC)),$(QUESTA_WORK_LIB).glbl,) + + +$(QUESTA_DIR): + mkdir -p $@ + +$(QUESTA_DIR)/sources.f: Makefile | $(QUESTA_DIR) + @rm -f $@ + @for inc in $(QUESTA_INCS); do echo $$inc >> $@; done + @for src in $(QUESTA_SOURCES); do echo $$src >> $@; done + +$(QUESTA_DIR)/xpm.stamp: | $(QUESTA_DIR) + @if [ "$(QUESTA_USE_XPM)" = "1" ]; then \ + if [ -z "$(XILINX_VIVADO)" ]; then \ + echo "ERROR: XILINX_VIVADO is not set. Source Vivado settings64.sh or pass XILINX_VIVADO=/path/to/Vivado/."; \ + exit 1; \ + fi; \ + for src in $(QUESTA_XPM_SRC); do \ + if [ ! -f $$src ]; then \ + echo "ERROR: XPM source not found: $$src"; \ + exit 1; \ + fi; \ + done; \ + $(VLIB) $(QUESTA_XPM_LIB); \ + $(VMAP) $(QUESTA_XPM_LIB) $(QUESTA_XPM_LIB); \ + $(VLOG) -sv -work $(QUESTA_XPM_LIB) $(QUESTA_XPM_SRC); \ + fi + @touch $@ + +$(QUESTA_DIR)/compile.stamp: $(QUESTA_DIR)/sources.f $(SYN_FILES) $(TB_FILES) $(INC_FILES) $(QUESTA_DIR)/xpm.stamp + $(VLIB) $(QUESTA_WORK_LIB) + $(VMAP) $(QUESTA_WORK_LIB) $(QUESTA_WORK_LIB) + $(VLOG) -sv -work $(QUESTA_WORK_LIB) $(QUESTA_DEFS) -timescale 1ns/1ps $(QUESTA_INCS) -f $(QUESTA_DIR)/sources.f + @touch $@ + +sim-questa: $(QUESTA_DIR)/compile.stamp + $(VSIM) -c $(QUESTA_LIBS) $(QUESTA_WORK_LIB).$(SIM_TOP) $(QUESTA_GLBL_TOP) -do "$(QUESTA_RUN); quit -f" + +sim-questa-gui: $(QUESTA_DIR)/compile.stamp + $(VSIM) $(QUESTA_LIBS) -voptargs="+acc" $(QUESTA_WORK_LIB).$(SIM_TOP) $(QUESTA_GLBL_TOP) + +questa-clean: + -rm -rf $(QUESTA_DIR) $(QUESTA_WORK_LIB) $(QUESTA_TRANSCRIPT) vsim.wlf *.wlf + +questa-xpm-clean: questa-clean + -rm -rf $(QUESTA_XPM_LIB) diff --git a/software/gui.py b/software/gui.py new file mode 100644 index 0000000..1226f34 --- /dev/null +++ b/software/gui.py @@ -0,0 +1,736 @@ +# shitpost + +import sys +import math +import socket +import platform + +from PyQt6 import uic +from dataclasses import dataclass +from PyQt6.QtCore import QProcess, QTimer +from PyQt6.QtCore import QObject, QThread, pyqtSignal + +from PyQt6.QtCore import Qt +import pyqtgraph as pg +from PyQt6.QtWidgets import QApplication, QMainWindow + + +@dataclass +class ReflectometerConfig: + ip: str + send_port: int + recv_port: int + + dac_bits: int + data_width: int + window_size: int + packet_size: int + + pulse_width: int + pulse_period: int + pulse_height: int + pulse_num: int + + adc_dac_ratio: float = 0.52 + socket_timeout_sec: float = 2.0 + + +class ReflectometerWorker(QObject): + data_ready = pyqtSignal(list) + status = pyqtSignal(str) + error = pyqtSignal(str) + finished = pyqtSignal() + + def __init__(self, config: ReflectometerConfig): + super().__init__() + self.config = config + self._stop_requested = False + self._sock = None + + def stop(self): + self._stop_requested = True + + if self._sock is not None: + try: + self._sock.close() + except OSError: + pass + + def run(self): + try: + self._validate_config() + + self.status.emit("Открытие UDP-сокета...") + + self._sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + self._sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self._sock.settimeout(self.config.socket_timeout_sec) + self._sock.bind(("0.0.0.0", self.config.recv_port)) + + dest = (self.config.ip, self.config.send_port) + + self.status.emit("Отправка soft reset...") + self._sock.sendto((0x0F00).to_bytes(2, "big"), dest) + + self.status.emit("Отправка параметров...") + ctrl_data = self._format_ctrl_data() + self._sock.sendto(ctrl_data, dest) + + self.status.emit("Отправка start...") + self._sock.sendto((0xF000).to_bytes(2, "big"), dest) + + self.status.emit("Приём данных...") + data = self._recv_data() + + if self._stop_requested: + self.status.emit("Операция остановлена") + return + + self.data_ready.emit(data) + self.status.emit(f"Получено samples: {len(data)}") + + except Exception as e: + if not self._stop_requested: + self.error.emit(str(e)) + + finally: + if self._sock is not None: + try: + self._sock.close() + except OSError: + pass + + self.finished.emit() + + def _format_ctrl_data(self) -> bytes: + output = bytearray() + + output += 0b10001000.to_bytes(1, "little") + + pulse_period_adc = ( + int(self.config.pulse_period * self.config.adc_dac_ratio) + // self.config.window_size + ) * self.config.window_size + + output += self.config.pulse_width.to_bytes(4, "little") + output += self.config.pulse_period.to_bytes(4, "little") + output += self.config.pulse_num.to_bytes(2, "little") + output += self.config.pulse_height.to_bytes(2, "little") + output += pulse_period_adc.to_bytes(4, "little") + + if len(output) != 17: + raise ValueError("Config data should be 128 bits + 8 bit header") + + return bytes(output) + + def _recv_data(self) -> list[int]: + packet_count = math.ceil( + ( + self.config.adc_dac_ratio + * self.config.pulse_period + / self.config.window_size + * self.config.data_width + ) + / self.config.packet_size + ) + + expected_length = math.ceil( + self.config.adc_dac_ratio + * self.config.pulse_period + / self.config.window_size + ) + + recv_buf = [] + + for pkt_cnt in range(packet_count): + if self._stop_requested: + break + + try: + packet, _ = self._sock.recvfrom(65536) + except socket.timeout: + raise TimeoutError(f"Таймаут приёма UDP-пакета #{pkt_cnt + 1}") + + if len(packet) % self.config.data_width != 0: + raise ValueError( + f"Некорректный размер UDP-пакета: {len(packet)} байт" + ) + + for i in range(0, len(packet), self.config.data_width): + sample = int.from_bytes( + packet[i:i + self.config.data_width], + "little", + ) + recv_buf.append(sample) + + if len(recv_buf) < expected_length: + raise ValueError( + f"Data underflow: получено {len(recv_buf)}, ожидалось {expected_length}" + ) + + return recv_buf[:expected_length - 1] + + def _validate_config(self): + if self.config.pulse_period <= 0: + raise ValueError("pulse_period должен быть больше 0") + + if self.config.pulse_num <= 0: + raise ValueError("pulse_num должен быть больше 0") + + if self.config.window_size <= 0: + raise ValueError("window_size должен быть больше 0") + + if self.config.packet_size <= 0: + raise ValueError("packet_size должен быть больше 0") + + if self.config.data_width <= 0: + raise ValueError("data_width должен быть больше 0") + + if self.config.pulse_period % self.config.window_size != 0: + raise ValueError("pulse_period должен быть кратен window_size") + + if self.config.pulse_width >= 2**32 - 1: + raise ValueError("pulse_width слишком большой") + + if self.config.pulse_period >= 2**32 - 1: + raise ValueError("pulse_period слишком большой") + + if self.config.pulse_num >= 2**16 - 1: + raise ValueError("pulse_num слишком большой") + + if self.config.pulse_height > 2**self.config.dac_bits - 1: + raise ValueError("pulse_height слишком большой") + + +class MainWindow(QMainWindow): + def __init__(self): + super().__init__() + + uic.loadUi("reflectometer.ui", self) + + self.ping_process = None + + self.ping_timeout_timer = QTimer(self) + self.ping_timeout_timer.setSingleShot(True) + self.ping_timeout_timer.timeout.connect(self.on_ping_timeout) + + self.button_ping.clicked.connect(self.check_ping) + + # settings + self.pulse_period = 0 + self.pulse_height = 0 + self.pulse_width = 0 + self.pulse_num = 0 + + self.dac_dw = 14 + self.adc_dw = 12 + self.nmax = 4096 + self.packet_size = 1024 + self.window_size = 65 + self.adc_dac_ration = 0.52 + self.accum_width = 32 + + # setup + + self.setup_pulse_controls() + self.setup_global_settings() + + self.update_pulse_limits() + + self.data = [] + + self.adc_dac_ratio = 0.52 + + self.measurement_thread = None + self.measurement_worker = None + + self.setup_graph() + self.setup_network_settings() + + self.button_start.clicked.connect(self.run_measurement) + self.button_graph_autoscale.clicked.connect(self.reset_graph_autoscale) + + # ping utils + + def check_ping(self): + ip = self.line_ip.text().strip() + + if not ip: + self.label_ping_status.setText("set ip!!") + return + + if "_" in self.line_ip.displayText(): + self.label_ping_status.setText("IP invalid") + return + + if self.ping_process is not None: + if self.ping_process.state() != QProcess.ProcessState.NotRunning: + self.label_ping_status.setText("Ping inflight") + return + + self.label_ping_status.setText("ping...") + self.button_ping.setEnabled(False) + + self.ping_process = QProcess(self) + + self.ping_process.finished.connect(self.on_ping_finished) + self.ping_process.errorOccurred.connect(self.on_ping_error) + + system_name = platform.system().lower() + + if system_name == "windows": + program = "ping" + arguments = ["-n", "1", "-w", "2000", ip] + else: + program = "ping" + arguments = ["-c", "1", "-W", "2", ip] + + self.ping_process.start(program, arguments) + # fallback + self.ping_timeout_timer.start(2000) + + def on_ping_finished(self, exit_code, exit_status): + self.ping_timeout_timer.stop() + self.button_ping.setEnabled(True) + + if exit_code == 0: + self.label_ping_status.setText("алё✅") + else: + self.label_ping_status.setText("не алё❌") + + def on_ping_error(self): + self.ping_timeout_timer.stop() + self.button_ping.setEnabled(True) + self.label_ping_status.setText("ping unavail") + + def on_ping_timeout(self): + if self.ping_process is not None: + if self.ping_process.state() != QProcess.ProcessState.NotRunning: + self.ping_process.kill() + + self.button_ping.setEnabled(True) + + # pulse controls + def setup_pulse_controls(self): + self._bind_slider_and_spinbox( + name="pulse_period", + slider=self.slider_pulse_period, + box=self.box_pulse_period, + normalize_value=self.normalize_pulse_period, + ) + + self._bind_slider_and_spinbox( + name="pulse_height", + slider=self.slider_pulse_height, + box=self.box_pulse_height, + ) + + self._bind_slider_and_spinbox( + name="pulse_width", + slider=self.slider_pulse_width, + box=self.box_pulse_width, + ) + + self._bind_slider_and_spinbox( + name="pulse_num", + slider=self.slider_pulse_num, + box=self.box_pulse_num, + ) + + def _bind_slider_and_spinbox(self, name, slider, box, normalize_value=None): + """ + Связывает QSlider и QSpinBox по значению. + Значение автоматически записывается в self.. + """ + + minimum = min(slider.minimum(), box.minimum()) + maximum = max(slider.maximum(), box.maximum()) + + slider.setRange(minimum, maximum) + box.setRange(minimum, maximum) + + def normalize(value): + if normalize_value is None: + return value + + return normalize_value(value) + + value = normalize(box.value()) + + slider.setValue(value) + box.setValue(value) + setattr(self, name, value) + + def update_value(new_value): + new_value = normalize(new_value) + + if slider.value() != new_value: + slider.setValue(new_value) + + if box.value() != new_value: + box.setValue(new_value) + + setattr(self, name, new_value) + + slider.valueChanged.connect(update_value) + box.valueChanged.connect(update_value) + + def normalize_pulse_period(self, value): + step = max(1, getattr(self, "window_size", + self.box_window_size.value())) + + snapped_value = round(value / step) * step + + minimum = self.box_pulse_period.minimum() + maximum = self.box_pulse_period.maximum() + + return max(minimum, min(snapped_value, maximum)) + + def _set_max_for_pair(self, slider, box, maximum): + slider.setMaximum(maximum) + box.setMaximum(maximum) + + value = min(box.value(), maximum) + box.setValue(value) + slider.setValue(value) + + def set_max_pulse_period(self, maximum): + self._set_max_for_pair( + slider=self.slider_pulse_period, + box=self.box_pulse_period, + maximum=maximum, + ) + self.pulse_period = self.box_pulse_period.value() + + def set_max_pulse_height(self, maximum): + self._set_max_for_pair( + slider=self.slider_pulse_height, + box=self.box_pulse_height, + maximum=maximum, + ) + self.pulse_height = self.box_pulse_height.value() + + def set_max_pulse_width(self, maximum): + self._set_max_for_pair( + slider=self.slider_pulse_width, + box=self.box_pulse_width, + maximum=maximum, + ) + self.pulse_width = self.box_pulse_width.value() + + def set_max_pulse_num(self, maximum): + self._set_max_for_pair( + slider=self.slider_pulse_num, + box=self.box_pulse_num, + maximum=maximum, + ) + self.pulse_num = self.box_pulse_num.value() + + # settings + + def setup_global_settings(self): + self._bind_spinbox_setting( + name="dac_dw", + box=self.box_dac_dw, + ) + + self._bind_spinbox_setting( + name="adc_dw", + box=self.box_adc_dw, + ) + + self._bind_spinbox_setting( + name="nmax", + box=self.box_nmax, + ) + + self._bind_spinbox_setting( + name="window_size", + box=self.box_window_size, + after_change=self.on_window_size_changed, + ) + + self._bind_spinbox_setting( + name="packet_size", + box=self.box_packet_size, + ) + + self._bind_spinbox_setting( + name="adc_dac_ratio", + box=self.box_adc_dac_ratio, + ) + + self._bind_spinbox_setting( + name="accum_width", + box=self.box_accum_width, + ) + + self._bind_spinbox_setting( + name="recv_port", + box=self.box_recv_port, + ) + + self._bind_spinbox_setting( + name="send_port", + box=self.box_send_port, + ) + + # применяем шаг для pulse_period сразу при старте + self.update_pulse_period_step() + + def _bind_spinbox_setting(self, name, box, after_change=None): + """ + Связывает QSpinBox с полем self.. + Например: + box_dac_dw -> self.dac_dw + box_window_size -> self.window_size + """ + + value = box.value() + setattr(self, name, value) + + def on_value_changed(new_value): + setattr(self, name, new_value) + + self.update_pulse_limits() + + if after_change is not None: + after_change(new_value) + + box.valueChanged.connect(on_value_changed) + + def update_pulse_limits(self): + # re-calc limits + + # nmax -> pulse_period limit + self.set_max_pulse_period(self.nmax * self.window_size) + self.set_max_pulse_width(self.nmax * self.window_size) + # accum_width + adc_width -> max pulse num + + self.set_max_pulse_num( + 2 ** (self.accum_width - self.adc_dw - math.ceil(math.log2(self.window_size))) - 1) + # dac_width -> max pulse height + self.set_max_pulse_height(2 ** self.dac_dw - 1) + + self.slider_pulse_period.setMinimum(self.window_size) + self.box_pulse_period.setMinimum(self.window_size) + + def on_window_size_changed(self, new_value): + self.update_pulse_period_step() + + def update_pulse_period_step(self): + # set window_size step + + step = max(1, self.window_size) + + self.box_pulse_period.setSingleStep(step) + self.slider_pulse_period.setSingleStep(step) + self.slider_pulse_period.setPageStep(step) + + self.snap_pulse_period_to_step(step) + + def snap_pulse_period_to_step(self, step): + """ + Подгоняет текущее значение pulse_period к ближайшему кратному window_size. + + Это нужно потому, что QSlider при перетаскивании мышкой + всё равно может дать любое промежуточное значение. + """ + + current_value = self.box_pulse_period.value() + + snapped_value = round(current_value / step) * step + + minimum = self.box_pulse_period.minimum() + maximum = self.box_pulse_period.maximum() + + snapped_value = max(minimum, min(snapped_value, maximum)) + + self.box_pulse_period.setValue(snapped_value) + self.slider_pulse_period.setValue(snapped_value) + self.pulse_period = snapped_value + + # graph + def setup_graph(self): + self.graph_widget = pg.PlotWidget() + self.graph_widget.setLabel("left", "ADC value") + self.graph_widget.setLabel("bottom", "Sample") + self.graph_widget.showGrid(x=True, y=True) + + self.graph_curve = self.graph_widget.plot( + [], + name="Data", + ) + + self.reference_curve = self.graph_widget.plot( + [], + name="Reference", + ) + + self.graph_layout.addWidget(self.graph_widget) + self.graph_curve = self.graph_widget.plot( + [], pen=pg.mkPen(width=2, color="b")) + self.reference_curve = self.graph_widget.plot( + [], pen=pg.mkPen(style=Qt.PenStyle.DashLine, color="g")) + + self.checkbox_draw_reference.stateChanged.connect( + self.update_reference_graph) + + def setup_network_settings(self): + self._bind_spinbox_setting( + name="recv_port", + box=self.box_recv_port, + ) + + self._bind_spinbox_setting( + name="send_port", + box=self.box_send_port, + ) + + def run_measurement(self): + if self.measurement_thread is not None: + if self.measurement_thread.isRunning(): + self.set_measurement_status("Измерение выполняется") + return + + config = self.build_reflectometer_config() + + self.data = [] + self.graph_curve.setData([]) + + self.measurement_thread = QThread(self) + self.measurement_worker = ReflectometerWorker(config) + + self.measurement_worker.moveToThread(self.measurement_thread) + + self.measurement_thread.started.connect(self.measurement_worker.run) + + self.measurement_worker.status.connect(self.set_measurement_status) + self.measurement_worker.error.connect(self.on_measurement_error) + self.measurement_worker.data_ready.connect(self.on_data_received) + + self.measurement_worker.finished.connect(self.measurement_thread.quit) + self.measurement_worker.finished.connect( + self.measurement_worker.deleteLater) + + self.measurement_thread.finished.connect( + self.measurement_thread.deleteLater) + self.measurement_thread.finished.connect(self.on_measurement_finished) + + self.measurement_thread.start() + + def build_reflectometer_config(self) -> ReflectometerConfig: + ip = self.line_ip.text().strip() + + if not ip: + raise ValueError("IP адрес не задан") + + data_width = self.accum_width // 8 + + return ReflectometerConfig( + ip=ip, + send_port=self.send_port, + recv_port=self.recv_port, + + dac_bits=self.dac_dw, + data_width=data_width, + window_size=self.window_size, + packet_size=self.packet_size, + + pulse_width=self.pulse_width, + pulse_period=self.pulse_period, + pulse_height=self.pulse_height, + pulse_num=self.pulse_num, + + adc_dac_ratio=self.adc_dac_ratio, + ) + + def on_data_received(self, data: list[int]): + self.data = data + # normalize + for i in range(len(data)): + self.data[i] /= (self.window_size * self.pulse_num) + self.data[i] -= 2 ** (self.adc_dw - 1) + 1 + + self.draw_main_graph() + self.update_reference_graph() + + if data: + self.set_measurement_status( + f"Готово. smp: {len(data)}, min: {min(data)}, max: {max(data)}" + ) + else: + self.set_measurement_status("Данные пустые") + + def on_measurement_error(self, message: str): + self.set_measurement_status(f"Ошибка: {message}") + + def on_measurement_finished(self): + self.measurement_worker = None + self.measurement_thread = None + + def stop_measurement(self): + if self.measurement_worker is not None: + self.measurement_worker.stop() + + def set_measurement_status(self, text: str): + self.label_status.setText(text) + + def draw_main_graph(self): + if not self.data: + self.graph_curve.setData([]) + return + + x = list(range(len(self.data))) + self.graph_curve.setData(x, self.data) + + def update_reference_graph(self): + """ + Рисует или очищает эталонный график. + Вызывается после получения данных и при переключении checkbox_draw_reference. + """ + + if not self.checkbox_draw_reference.isChecked(): + self.reference_curve.setData([]) + return + + if not self.data: + self.reference_curve.setData([]) + return + + reference_data = self.build_reference_data(len(self.data)) + + if not reference_data: + self.reference_curve.setData([]) + return + + x = list(range(len(reference_data))) + self.reference_curve.setData(x, reference_data) + + def build_reference_data(self, length: int) -> list[int]: + reference = [0] * length + + actual_pulse_width = round( + (self.pulse_width * self.adc_dac_ratio) / self.window_size) + + reference[0:actual_pulse_width] = [ + (self.pulse_height / 2 ** (self.dac_dw - self.adc_dw)) - 2 ** (self.adc_dw - 1), ] * (actual_pulse_width - 1) + + return reference + + def reset_graph_autoscale(self): + self.graph_widget.enableAutoRange(axis="xy", enable=True) + self.graph_widget.autoRange() + + +def main(): + app = QApplication(sys.argv) + + window = MainWindow() + window.show() + + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/software/reflectometer.ui b/software/reflectometer.ui new file mode 100644 index 0000000..7822d14 --- /dev/null +++ b/software/reflectometer.ui @@ -0,0 +1,505 @@ + + + MainWindow + + + + 0 + 0 + 1023 + 708 + + + + Reflectometer PREMIUM + + + + + + + + + + + + 1 + + + + Настройки + + + + + + true + + + + + 0 + 0 + 294 + 621 + + + + + + + + 12 + + + + Аппаратные параметры + + + + + + + bits + + + DAC data width: + + + 8 + + + 32 + + + 14 + + + + + + + bits + + + ADC data width: + + + 8 + + + 32 + + + 12 + + + + + + + bits + + + Accum width: + + + 16 + + + 64 + + + 8 + + + 32 + + + + + + + ADC:DAC clk ratio: + + + 0.200000000000000 + + + 3.000000000000000 + + + 0.010000000000000 + + + 0.520000000000000 + + + + + + + N Max: + + + 512 + + + 65536 + + + 4096 + + + + + + + Window size: + + + 1 + + + 1024 + + + 65 + + + + + + + bytes + + + Packet size: + + + 1 + + + 1572 + + + 1024 + + + + + + + Qt::Orientation::Horizontal + + + + + + + + 12 + + + + Подключение + + + + + + + IP устройства: + + + + + + + 999.999.999.999 + + + 192.168.0.2 + + + + + + + Порт отправки: + + + + + + + 80 + + + 65536 + + + 8080 + + + + + + + Порт приёма: + + + + + + + 80 + + + 65536 + + + 8080 + + + + + + + + 12 + + + + Тест + + + + + + + алё + + + + + + + ... + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Управление + + + + + + + 12 + + + + Импульс + + + + + + + + + Период + + + + + + + 1 + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + Ширина + + + + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + Высота + + + + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + Количество + + + + + + + 1 + + + + + + + 1 + + + Qt::Orientation::Horizontal + + + + + + + + + start! + + + + + + + + + + true + + + + Статус: + + + + + + + - + + + + + + + + + Отрисовка эталона + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + Сброс масштаба + + + + + + + + + + + + + + + 0 + 0 + 1023 + 30 + + + + + + + +