Merge branch 'dev/design' into integration_new_controller

This commit is contained in:
otroubi
2026-07-29 17:26:41 +03:00
7 changed files with 1490 additions and 57 deletions
+1
View File
@@ -16,6 +16,7 @@
.Xil
xvlog.pb
*vivado_pid*
**/work/*
# some generated files (they annoy me)
update_config.tcl
+14 -7
View File
@@ -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
+136 -48
View File
@@ -11,60 +11,71 @@ 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
//------------------------------------------------------------
// Тактовые сигналы и сброс
//------------------------------------------------------------
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);
// 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]);
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
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
+96
View File
@@ -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/<version>."; \
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)
+736
View File
@@ -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.<name>.
"""
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.<name>.
Например:
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()
+505
View File
@@ -0,0 +1,505 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1023</width>
<height>708</height>
</rect>
</property>
<property name="windowTitle">
<string>Reflectometer PREMIUM</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QHBoxLayout" name="horizontalLayout" stretch="4,2">
<item>
<layout class="QVBoxLayout" name="graph_layout"/>
</item>
<item>
<layout class="QVBoxLayout" name="settings_layout">
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>1</number>
</property>
<widget class="QWidget" name="tab">
<attribute name="title">
<string>Настройки</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>294</width>
<height>621</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Аппаратные параметры</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_dac_dw">
<property name="suffix">
<string> bits</string>
</property>
<property name="prefix">
<string>DAC data width: </string>
</property>
<property name="minimum">
<number>8</number>
</property>
<property name="maximum">
<number>32</number>
</property>
<property name="value">
<number>14</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_adc_dw">
<property name="suffix">
<string> bits</string>
</property>
<property name="prefix">
<string>ADC data width: </string>
</property>
<property name="minimum">
<number>8</number>
</property>
<property name="maximum">
<number>32</number>
</property>
<property name="value">
<number>12</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_accum_width">
<property name="suffix">
<string> bits</string>
</property>
<property name="prefix">
<string>Accum width: </string>
</property>
<property name="minimum">
<number>16</number>
</property>
<property name="maximum">
<number>64</number>
</property>
<property name="singleStep">
<number>8</number>
</property>
<property name="value">
<number>32</number>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="box_adc_dac_ratio">
<property name="prefix">
<string>ADC:DAC clk ratio: </string>
</property>
<property name="minimum">
<double>0.200000000000000</double>
</property>
<property name="maximum">
<double>3.000000000000000</double>
</property>
<property name="singleStep">
<double>0.010000000000000</double>
</property>
<property name="value">
<double>0.520000000000000</double>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_nmax">
<property name="prefix">
<string>N Max: </string>
</property>
<property name="minimum">
<number>512</number>
</property>
<property name="maximum">
<number>65536</number>
</property>
<property name="value">
<number>4096</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_window_size">
<property name="prefix">
<string>Window size: </string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1024</number>
</property>
<property name="value">
<number>65</number>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_packet_size">
<property name="suffix">
<string> bytes</string>
</property>
<property name="prefix">
<string>Packet size: </string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>1572</number>
</property>
<property name="value">
<number>1024</number>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Подключение</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>IP устройства:</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="line_ip">
<property name="inputMask">
<string>999.999.999.999</string>
</property>
<property name="text">
<string>192.168.0.2</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Порт отправки:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_send_port">
<property name="minimum">
<number>80</number>
</property>
<property name="maximum">
<number>65536</number>
</property>
<property name="value">
<number>8080</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Порт приёма:</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_recv_port">
<property name="minimum">
<number>80</number>
</property>
<property name="maximum">
<number>65536</number>
</property>
<property name="value">
<number>8080</number>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Тест</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_ping">
<property name="text">
<string>алё</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_ping_status">
<property name="text">
<string>...</string>
</property>
<property name="alignment">
<set>Qt::AlignmentFlag::AlignCenter</set>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_2">
<attribute name="title">
<string>Управление</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="label_7">
<property name="font">
<font>
<pointsize>12</pointsize>
</font>
</property>
<property name="text">
<string>Импульс</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="1,1,2">
<item>
<widget class="QLabel" name="label_8">
<property name="text">
<string>Период</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_pulse_period">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="slider_pulse_period">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3" stretch="1,1,2">
<item>
<widget class="QLabel" name="label_9">
<property name="text">
<string>Ширина</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_pulse_width"/>
</item>
<item>
<widget class="QSlider" name="slider_pulse_width">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_4" stretch="1,1,2">
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>Высота</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_pulse_height"/>
</item>
<item>
<widget class="QSlider" name="slider_pulse_height">
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,1,2">
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>Количество</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="box_pulse_num">
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="slider_pulse_num">
<property name="minimum">
<number>1</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QPushButton" name="button_start">
<property name="text">
<string>start!</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6" stretch="1,3">
<item>
<widget class="QLabel" name="label_13">
<property name="font">
<font>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Статус:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_status">
<property name="text">
<string>-</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="checkbox_draw_reference">
<property name="text">
<string>Отрисовка эталона</string>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="button_graph_autoscale">
<property name="text">
<string>Сброс масштаба</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1023</width>
<height>30</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>