12 Commits

11 changed files with 1209 additions and 1507 deletions

View File

@ -17,6 +17,7 @@ RTL_DIR = ../../rtl
include ../../scripts/vivado.mk
SYN_FILES += reflectometer.sv
SYN_FILES += tb_reflectometer.sv
SYN_FILES += $(sort $(shell find ../../rtl -type f \( -name '*.v' -o -name '*.sv' \)))
XCI_FILES = $(sort $(shell find ../../rtl/ethernet-udp/src -type f -name '*.xci'))

View File

@ -0,0 +1,145 @@
# Рефлектометр
Модуль представляет собой законченную встраиваемую систему рефлектометра, объединяющую:
- контроллер управления
- генератор импульсов (DAC path)
- сэмплер данных (ADC path)
- аккумулятор и обработчик данных
Система предназначена для формирования импульсов, синхронного сбора отраженного сигнала, накопления результатов и передачи обработанных данных во внешнюю систему.
Данный модуль является полноценным интегрируемым блоком, который может использоваться как самостоятельная аппаратная подсистема внутри более крупного проекта.
---
## Назначение системы
Основная задача системы:
1. Получить параметры измерения через AXI Stream
2. Сформировать последовательность импульсов на DAC
3. Выполнить синходную выборку данных с ADC
4. Накопить и обработать результаты
5. Передать итоговые данные обратно через AXI Stream
Таким образом реализуется полный цикл измерения без необходимости внешнего управления отдельными блоками.
---
## Состав системы
### Controller
Принимает входные команды по AXI Stream (Ethernet RX), декодирует параметры измерения и управляет всеми внутренними модулями системы.
Формирует:
- запуск генератора (`dac_start`)
- запуск аккумулятора (`adc_start`)
- параметры импульсов DAC
- параметры выборки ADC
- локальные reset-сигналы
---
### Generator
Формирует последовательность импульсов на DAC с заданными:
- амплитудой
- длительностью
- периодом
- количеством повторений
Для каждого импульса инициирует запуск выборки в сэмплере.
---
### Sampler
Выполняет синхронный сбор данных с ADC по запросу генератора.
Поддерживает:
- фильтрацию `out_of_range`
- упаковку данных
- преобразование типа кода ( прямой или дополнительный)
---
### Accumulator
Получает поток данных от сэмплера, выполняет накопление, усреднение и оконную обработку, после чего формирует пакеты для передачи результата.
---
## Управление системой
Пользователь взаимодействует только с контроллером через AXI Stream-интерфейс.
Прямое управление генератором, сэмплером и аккумулятором не требуется.
---
## Clock Domain Crossing (CDC)
Система работает в нескольких тактовых доменах:
- Ethernet RX (`gmii_rx_clk`)
- Ethernet TX (`gmii_tx_clk`)
- DAC (`dac_clk`)
- ADC (`adc_clk`)
Для корректной синхронизации между DAC и ADC используются специальные CDC-регистры для сигналов:
- `sample_req`
- `sample_done`
Это обеспечивает безопасную передачу handshake-сигналов между тактовыми доменами.
---
## Список параметров
### DAC_DATA_WIDTH
Ширина выходных данных отправляемых на ЦАП.
### ZERO_LEVEL
Уровень сигнала в состоянии отсутствия импульса (базовый уровень сигнала).
Типовые значения:
- `8192` — середина диапазона ЦАП
- `0` — нулевой уровень
### ADC_DATA_WIDTH
Ширина входных данных, получаемых с АЦП.
### PACK_FACTOR
Количество отсчетов, собираемых в один выходной пакет.
### PROCESS_MODE
Режим интерпретации входного кода:
- `0` — прямой код
- `1` — дополнительный код
### ACCUM_WIDTH
Размер данных для аккумуляции, должен быть степенью числа 2. По умолчанию - 32
### N_MAX
Максимальное число окон в последовательности. Должно быть степенью числа 2. Влияет на размер используемой памяти.
### WINDOW_SIZE
Размер окна усреднения
### PACKET_SIZE
Размер выходного пакета
---
## Сборка
```make all``` - собрать все до битстрима
```make vivado``` - открыть проект в Vivado

View File

@ -0,0 +1,267 @@
`timescale 1ns / 1ps
module tb_reflectometer;
// parameters
localparam int unsigned DAC_DATA_WIDTH = 14;
localparam int unsigned ADC_DATA_WIDTH = 12;
localparam PACK_FACTOR = 1; // not used in TB
localparam PROCESS_MODE = 0; // 0 - uint, 1 - int
localparam ZERO_LEVEL = 8192; // DAC zero voltage representation (2^14 / 2)
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 unsigned ADC_CLK_MHZ = 65;
localparam int unsigned DAC_CLK_MHZ = 125;
// may be changed for test purposes
localparam int unsigned PULSE_WIDTH = 2**6;
localparam int unsigned PULSE_PERIOD = 2**8;
localparam int unsigned PULSE_NUM = 10;
localparam int unsigned PULSE_HEIGHT = 2**12;
localparam int unsigned PULSE_PERIOD_ADC = (int'(real'(ADC_CLK_MHZ) / real'(DAC_CLK_MHZ) * real'(PULSE_PERIOD)) / int'(WINDOW_SIZE)) * int'(WINDOW_SIZE);
initial begin
if (PULSE_WIDTH <= 0)
$fatal(1, "PULSE_WIDTH should be positive");
if (PULSE_PERIOD <= 0)
$fatal(1, "PULSE_PERIOD should be positive");
if (PULSE_NUM <= 0)
$fatal(1, "PULSE_NUM should be positive");
if (PULSE_HEIGHT <= 0)
$fatal(1, "PULSE_HEIGHT should be positive");
if (PULSE_WIDTH >= 2**32-1)
$fatal(1, "PULSE_WIDTH too high");
if (PULSE_PERIOD >= 2**32-1)
$fatal(1, "PULSE_PERIOD too high");
if (PULSE_NUM >= 2**16-1)
$fatal(1, "PULSE_NUM too high");
if (PULSE_HEIGHT >= 2**DAC_DATA_WIDTH-1)
$fatal(1, "PULSE_HEIGHT too high");
if (PULSE_PERIOD_ADC % WINDOW_SIZE == 0)
$fatal(1, "PULSE_PERIOD_ADC isn't multiple of WINDOW_SIZE");
end
// DUT signals
logic clk200, clk_eth_phy_tx, clk_eth_phy_rx; // GMII clocks
logic rst_n;
wire [3:0] status_leds; // [ None, dac_start, m_axis_valid, clk_wiz_locked ]
wire dac_clk, dac_en;
wire [DAC_DATA_WIDTH-1:0] dac_data;
wire adc_clk;
logic adc_otr;
logic [ADC_DATA_WIDTH-1:0] adc_data;
wire [7:0] s_axis_tx_tdata;
wire s_axis_tx_tvalid;
logic s_axis_tx_tready;
wire s_axis_tx_tlast;
logic phy_ready;
wire accum_tx_start;
logic [7:0] m_axis_rx_tdata;
logic m_axis_rx_tvalid;
logic m_axis_rx_tlast;
logic m_axis_rx_tready;
logic [127:0] dut_config = 0;
// DUT
reflectometer_top #(
.DAC_DATA_WIDTH(DAC_DATA_WIDTH),
.ADC_DATA_WIDTH(ADC_DATA_WIDTH),
.PACK_FACTOR(PACK_FACTOR),
.PROCESS_MODE(PROCESS_MODE),
.ZERO_LEVEL(ZERO_LEVEL),
.ACCUM_WIDTH(ACCUM_WIDTH),
.N_MAX(N_MAX),
.WINDOW_SIZE(WINDOW_SIZE),
.PACKET_SIZE(PACKET_SIZE)
) DUT (
.sys_clk(clk200), // main clk 200 mhz
.rst_n(rst_n), // rst_n
.led(status_leds), // indication [3:0]
.gmii_rx_clk(clk_eth_phy_rx), // ext. clk from PHY
.gmii_tx_clk(clk_eth_phy_tx), // ext. clk from PHY
// accumulated data stream
.s_axis_tx_tdata(s_axis_tx_tdata),
.s_axis_tx_tvalid(s_axis_tx_tvalid),
.s_axis_tx_tready(s_axis_tx_tready),
.s_axis_tx_tlast(s_axis_tx_tlast),
// controller data stream
.m_axis_rx_tdata(m_axis_rx_tdata),
.m_axis_rx_tvalid(m_axis_rx_tvalid),
.m_axis_rx_tlast(m_axis_rx_tlast),
.m_axis_rx_tready(m_axis_rx_tready),
.req_ready(phy_ready), // AXI-stream requester ready
.send_req(accum_tx_start), // AXI-stream start transmit
.p2_clk(dac_clk), // DAC clk
.p2_data(dac_data), // DAC [DAC_DATA_WIDTH-1:0] data
.p2_wrt(dac_en), // DAC write enable
.ch2_clk(adc_clk), // ADC clk
.ch2_data(adc_data), // ADC [ADC_DATA_WIDTH-1:0] data
.ch2_otr(adc_otr) // ADC signal out-of-range
);
// clocks
initial begin
// 200 MHz
clk200 = 1'b0;
forever #2.5 clk200 = ~clk200;
end
initial begin
// 125 MHz
clk_eth_phy_tx = 1'b0;
forever #4 clk_eth_phy_tx = ~clk_eth_phy_tx;
end
initial begin
// 125 MHz
clk_eth_phy_rx = 1'b0;
forever #4 clk_eth_phy_rx = ~clk_eth_phy_rx;
end
// ADC input noise simulation
always @(posedge adc_clk or negedge rst_n) begin
if (!rst_n) begin
adc_data <= '0;
end else begin
adc_data <= $urandom() & ((1 << ADC_DATA_WIDTH) - 1);
end
end
assign adc_otr = 1'b0;
// AXIS tasks
task automatic axis_send_byte(
ref logic clk,
input logic [7:0] data,
input logic last,
ref logic tvalid,
ref logic [7:0] tdata,
ref logic tlast,
input logic tready
);
@(posedge clk);
tdata <= data;
tlast <= last;
tvalid <= 1'b1;
// Ждем готовности приемника
wait(tready === 1'b1);
@(posedge clk);
tvalid <= 1'b0;
tlast <= 1'b0;
endtask
task automatic dut_soft_reset();
axis_send_byte(
.clk(clk_eth_phy_rx),
.data(8'b00001111),
.last(1'b1),
.tvalid(m_axis_rx_tvalid),
.tdata(m_axis_rx_tdata),
.tlast(m_axis_rx_tlast),
.tready(m_axis_rx_tready)
);
endtask
task automatic dut_start();
axis_send_byte(
.clk(clk_eth_phy_rx),
.data(8'b11110000),
.last(1'b1),
.tvalid(m_axis_rx_tvalid),
.tdata(m_axis_rx_tdata),
.tlast(m_axis_rx_tlast),
.tready(m_axis_rx_tready)
);
endtask
// task automatic dut_send_config(
// input logic [127:0] ctrl_config
// );
// // команда set_data
// axis_send_byte(
// .clk(clk_eth_phy_rx),
// .data(8'b10001000),
// .last(1'b0),
// .tvalid(m_axis_rx_tvalid),
// .tdata(m_axis_rx_tdata),
// .tlast(m_axis_rx_tlast),
// .tready(m_axis_rx_tready)
// );
// // config burst
// for (int i = 0; i < 16; i++) begin
// logic [7:0] byte_to_send;
// logic is_last;
// // get byte
// byte_to_send = ctrl_config[i*8 +: 8];
// // tlast for last byte
// is_last = (i == 15);
// axis_send_byte(
// .clk(clk_eth_phy_rx),
// .data(byte_to_send),
// .last(is_last),
// .tvalid(m_axis_rx_tvalid),
// .tdata(m_axis_rx_tdata),
// .tlast(m_axis_rx_tlast),
// .tready(m_axis_rx_tready)
// );
// end
// endtask
// some helpers for controller axis
// GAME PLAN
// 1. setup reflectometer
// 2. create some reference signal with noise + virtual ADC
// 3. setup m_axis endpoint for controller to start reflectometer (create multiple tasks)
// 4. setup s_axis endpoint for data gathering and plotting
// 5. check standalone reflectometer
// 6. add reference signal averaging loop throw generator pulse posedge detection
// 7. visual comparision of reference VS reflectometer
// 8. add statistics for signal comparision (MSE/RMSE)
// main TB
initial begin
// setup
rst_n = 1'b0;
s_axis_tx_tready = 1'b0;
m_axis_rx_tdata = 1'b0;
m_axis_rx_tvalid = 1'b0;
m_axis_rx_tlast = 1'b0;
phy_ready = 1'b0;
// startup
#100;
rst_n = 1'b1;
wait(DUT.clk_wiz_ctrl_inst.locked == 1'b1);
#20;
$display("=== clocks ready / wiz. locked ===");
#40;
// ready to work
dut_config[31:0] = PULSE_WIDTH;
dut_config[63:32] = PULSE_PERIOD;
dut_config[79:64] = PULSE_NUM;
dut_config[79+DAC_DATA_WIDTH:80] = PULSE_HEIGHT;
dut_config[127:96] = PULSE_PERIOD_ADC;
// dut_send_config(dut_config);
dut_start();
// dut_start();
#1000;
// dut_soft_reset();
$display("=== ALL BASIC TESTS PASSED ===");
$finish;
end
endmodule

91
rtl/generator/README.md Normal file
View File

@ -0,0 +1,91 @@
# Генератор
Модуль выполняет задачу формирования последовательности импульсов заданной амплитуды, длительности и периода.
Дополнительно реализован механизм синхронизации с модулем сэмплера через сигналы `request` и `done`, позволяющий запускать сбор данных для каждого импульса и ожидать подтверждения завершения выборки перед переходом к следующему импульсу.
---
## Список параметров
### DATA_WIDTH
Ширина выходных данных генератора.
### ZERO_LEVEL
Уровень сигнала в состоянии отсутствия импульса (базовый уровень сигнала).
Типовые значения:
- `8192` — середина диапазона ЦАП
- `0` — нулевой уровень
---
## Список входных портов
### clk_dac
Сигнал тактирования модуля.
### rst
Сброс модуля и остановка генерации.
### start
Сигнал запуска последовательности импульсов.
При его активации модуль фиксирует все входные параметры и начинает генерацию.
Повторный запуск во время активной генерации блокируется с помощью внутреннего сигнала `enable`.
### [31:0] pulse_width
Длительность активной части импульса (в тактах).
### [31:0] pulse_period
Полный период импульса (в тактах).
### [DATA_WIDTH-1:0] pulse_height
Амплитуда импульса.
### [15:0] pulse_num
Количество импульсов, которое необходимо сгенерировать.
### request
Сигнал запроса на синхронизацию от сэмплера для текущего импульса.
---
## Список выходных портов
### dac_wrt
Выходной сигнал разрешения записи сигнала
### [DATA_WIDTH-1:0] dac_out
Выходное значение амплитуды сигнала.
Во время активной части импульса равно `pulse_height`, вне импульса — `ZERO_LEVEL`.
### done
Сигнал запроса на запуск синхронизации с сэмплером для текущего импульса.
Поднимается в начале каждого нового импульса и снимается после получения `request`.
---
## Логика работы
После прихода сигнала `start` модуль:
- фиксирует входные параметры генерации
- поднимает `enable = 1`
- выполняет `pulse_num` циклов работы
- - типичный цикл состоит в ожидании синхронизации (`synced`), после чего запуск генерации импульса
Синхронизация представляет из себя простое рукопожатие с внешним модулем, имеющим сигналы `request`/`done` работающими в соответствии с этими сигналами генератора. Один из модулей, входит в ожидание и ставит на свой done активный уровень, после чего ждет, пока второй, запаздывающий модуль не войдет в свой режим ожидания, и не выставит для своего done активный уровень. Для каждого из модулей, на следующий такт после выставления активного уровня, производится проверка своего request. Так, при получении активного request (иными словами активного done от внешнего модуля), модуль незамедлительно опускает уровень своего done и начинает работать. Done подымается до активного уровня хотя-бы на один такт работы соответствующего модуля.
---
## Симуляция
Тесты запускаются автоматически через make.
```
cd tests
make sim
```
При успешном завершении теста высвечивается "ALL PASSED".

View File

@ -1,105 +1,95 @@
`timescale 1ns / 1ps
module generator
#(
parameter DATA_WIDTH = 14,
parameter ZERO_LEVEL = 8192 // 8192 or 0
)
)
(
input clk_in,
input clk_dac,
input rst,
input start,
input [31:0] pulse_width,
input [31:0] pulse_period,
input [DATA_WIDTH-1:0] pulse_height,
input [15:0] pulse_num,
input sample_done,
input request,
output pulse,
output[DATA_WIDTH-1:0] pulse_height_out,
output logic sample_req
output dac_wrt,
output logic [DATA_WIDTH-1:0] dac_out,
output logic done
);
logic [DATA_WIDTH-1:0] pulse_height_reg;
logic [31:0] pulse_width_reg, pulse_period_reg;
logic [15:0] pulse_num_reg;
);
logic [15:0] cnt_pulse_num;
logic [31:0] cnt_pulse_period;
(* MARK_DEBUG="true" *) logic [DATA_WIDTH-1:0] pulse_height_reg, pulse_height_out_reg;
logic enable, synced;
(* MARK_DEBUG="true" *) logic [31:0] pulse_width_reg, pulse_period_reg;
(* MARK_DEBUG="true" *) logic [15:0] pulse_num_reg;
(* MARK_DEBUG="true" *) logic enable;
(* MARK_DEBUG="true" *) logic [15:0] cnt_pulse_num;
(* MARK_DEBUG="true" *) logic [31:0] cnt_period;
always @(posedge clk_in) begin
always @(posedge clk_dac) begin
if (rst) begin
pulse_height_reg <= ZERO_LEVEL;
pulse_height_out_reg <= ZERO_LEVEL;
pulse_width_reg <= '0;
pulse_period_reg <= '0;
pulse_num_reg <= '0;
enable <= 0;
cnt_pulse_num <= '0;
cnt_period <= '0;
sample_req <= 0;
end else begin
pulse_height_reg <= ZERO_LEVEL;
pulse_width_reg <= 0;
pulse_period_reg <= 0;
pulse_num_reg <= 0;
cnt_pulse_num <= 0;
cnt_pulse_period <= 0;
dac_out <= ZERO_LEVEL;
done <= 0;
enable <= 0;
synced <= 0;
end
else begin
// wait start for updating registers
if (start & !enable) begin
enable <= 1'b1;
cnt_pulse_num <= '0;
cnt_period <= '0;
sample_req <= 1;
pulse_width_reg <= pulse_width;
pulse_period_reg <= pulse_period;
pulse_num_reg <= pulse_num;
pulse_height_reg <= pulse_height;
enable <= 1;
pulse_width_reg <= pulse_width;
pulse_period_reg <= pulse_period;
pulse_num_reg <= pulse_num;
pulse_height_reg <= pulse_height;
end
// main work cycle
if (enable) begin
if (!sample_req && (cnt_period == 0)) begin
pulse_height_out_reg <= ZERO_LEVEL;
if (sample_done) begin
sample_req <= 1'b0;
end
if (!sample_done) begin
if (cnt_pulse_num == pulse_num_reg - 1) begin
enable <= 1'b0;
if (cnt_pulse_num != pulse_num_reg) begin
// wait for synchronization with sampler
if (!synced) begin
if (request & done) begin
synced <= 1;
done <= 0;
end
else begin
cnt_pulse_num <= cnt_pulse_num + 1;
sample_req <= 1'b1;
cnt_period <= 1;
else
done <= 1;
end
else begin
if (cnt_pulse_period != pulse_period_reg) begin
if (cnt_pulse_period < pulse_width_reg)
dac_out <= pulse_height_reg;
else
dac_out <= ZERO_LEVEL;
cnt_pulse_period++;
end
else if (cnt_pulse_period == pulse_period_reg) begin
cnt_pulse_num++;
cnt_pulse_period <= 0;
synced <= 0;
dac_out <= ZERO_LEVEL;
end
end
end
else begin
if (cnt_period <= pulse_width_reg) begin
pulse_height_out_reg <= pulse_height_reg;
end else begin
pulse_height_out_reg <= ZERO_LEVEL;
end
if (cnt_period == pulse_period_reg) begin
cnt_period <= 0;
end else begin
cnt_period <= cnt_period + 1;
end
if (sample_req && sample_done) begin
sample_req <= 0;
end
else if (cnt_pulse_num == pulse_num_reg) begin
cnt_pulse_num <= 0;
enable <= 0;
end
end
end
end
OBUF OBUF_pulse_clk (
.I(clk_in),
.O(pulse)
// Gated DAC write signal from DAC clock. Needed for posedge
OBUF OBUF_pulse_clk (
.I(clk_dac & enable),
.O(dac_wrt)
);
assign pulse_height_out = pulse_height_out_reg;
endmodule

View File

@ -1,106 +1,363 @@
`timescale 1ns / 1ps
module generator_tb;
// === Параметры ===
localparam DATA_WIDTH = 14;
localparam LOGIC_ZERO_LEVEL = 0; // DAC -5V for logic zero
localparam VOLTAGE_ZERO_LEVEL = 2**(DATA_WIDTH-1); // DAC 0V for logic zero
localparam CLK_PERIOD = 8;
parameter string ZERO_LEVEL = "logic"; // "logic" VS "true"
parameter DATA_WIDTH = 14;
parameter ZERO_LEVEL = 8192;
parameter CLK_PERIOD = 16;
// === Сигналы ===
// Системные сигналы
logic clk;
logic rst;
logic start;
// Входные сигналы
logic [31:0] pulse_width; // config reg
logic [31:0] pulse_period; // config reg
logic [DATA_WIDTH-1:0] pulse_height; // config reg
logic [15:0] pulse_num; // config reg
logic sampler_done; // sampler request for synchronization
// Выходные сигналы
wire dac_wrt; // DAC wrt singnal
wire [DATA_WIDTH-1:0] dac_out; // DAC input logic signal
wire generator_done; // generator request for synchronization
logic [31:0] pulse_width;
logic [31:0] pulse_period;
logic [DATA_WIDTH-1:0] pulse_height;
logic [15:0] pulse_num;
logic pulse;
logic [DATA_WIDTH-1:0] pulse_height_out;
// === Переменные ===
int current_zero_level;
initial begin
if (ZERO_LEVEL == "true")
current_zero_level = VOLTAGE_ZERO_LEVEL;
else
current_zero_level = LOGIC_ZERO_LEVEL;
end
// DUT
generator #(
.DATA_WIDTH(DATA_WIDTH)
) dut (
.clk_in(clk),
.rst(rst),
.start(start),
.pulse_width(pulse_width),
.pulse_period(pulse_period),
.pulse_height(pulse_height),
.pulse_num(pulse_num),
.pulse(pulse),
.pulse_height_out(pulse_height_out)
);
generate
if (ZERO_LEVEL == "true") begin : gen_dut_true
generator #(
.DATA_WIDTH(DATA_WIDTH),
.ZERO_LEVEL(VOLTAGE_ZERO_LEVEL)
) dut (
.clk_dac(clk),
.rst(rst),
.start(start),
.pulse_width(pulse_width),
.pulse_period(pulse_period),
.pulse_height(pulse_height),
.pulse_num(pulse_num),
.dac_wrt(dac_wrt),
.dac_out(dac_out),
.done(generator_done),
.request(sampler_done)
);
initial $display("[TB] Generator compiled. ZERO_LEVEL: TRUE");
end
else if (ZERO_LEVEL == "logic") begin : gen_dut_logic
generator #(
.DATA_WIDTH(DATA_WIDTH),
.ZERO_LEVEL(LOGIC_ZERO_LEVEL)
) dut (
.clk_dac(clk),
.rst(rst),
.start(start),
.pulse_width(pulse_width),
.pulse_period(pulse_period),
.pulse_height(pulse_height),
.pulse_num(pulse_num),
.dac_wrt(dac_wrt),
.dac_out(dac_out),
.done(generator_done),
.request(sampler_done)
);
initial $display("[TB] Generator compiled. ZERO_LEVEL: LOGIC");
end
else begin : gen_dut_error
// защита от дурака
initial begin
$display("[ERROR] Unknown value ZERO_LEVEL: %s", ZERO_LEVEL);
$finish;
end
end
endgenerate
// Clock
// Тактовые сигналы
initial begin
clk = 0;
forever #(CLK_PERIOD/2) clk = ~clk;
end
initial begin
$display("\n=== GENERATOR TEST ===\n");
// === Таски для тестипрования ===
// Таска синхронизации, одно рукопожатие
task automatic synchronize(
input bit sampler_first, // 1 - выставить sampler_done ДО генератора, 0 - ПОСЛЕ
input int delay_before_ack, // Если sampler_first=0: задержка ПОСЛЕ gen_done. Если 1: задержка от НАЧАЛА цикла.
input int ack_duration // сколько тактов удерживать sampler_done после встречи сигналов
);
if (sampler_first) begin
// --- сэмплер готов до генератора ---
repeat(delay_before_ack) @(posedge clk);
sampler_done <= 1;
wait(generator_done == 1);
repeat(ack_duration) @(posedge clk);
sampler_done <= 0;
end
else begin
// --- генератора готов до сэмплер ---
wait(generator_done == 1);
repeat(delay_before_ack) @(posedge clk);
sampler_done <= 1;
repeat(ack_duration) @(posedge clk);
sampler_done <= 0;
end
endtask
// Таска сброса DUT
task automatic reset_dut(
input int rst_duration // сколько тактов держать сброс
);
rst <= 1;
repeat(rst_duration) @(posedge clk);
rst <= 0;
endtask
// Таска запуска DUT
task automatic start_dut(
input int start_duration // сколько тактов держать импульс
);
start <= 1;
repeat(start_duration) @(posedge clk);
start <= 0;
endtask
// Таска конфигурации DUT
task automatic set_config(
input logic [31:0] w, // ширина импульса
input logic [31:0] p, // период импульса
input logic [15:0] n, // количество импульсов
input logic [DATA_WIDTH-1:0] h // высота импульса
);
// Задаем конфигурационные регистры
@(posedge clk);
pulse_width <= w;
pulse_period <= p;
pulse_num <= n;
pulse_height <= h;
endtask
// Таска проверки устойчивости к долгим управляющим импульсам
task automatic check_impulses;
// Локальные переменные для хранения случайных параметров
int rand_start_duration;
int rand_delay;
int rand_ack;
bit rand_first;
int total_impulse_cycles = 0;
int pulse_w = 11;
int pulse_p = 31;
int pulse_n = 5;
int pulse_h = 1024;
$display("[TB] -check_impulses- Check system stability under random latencies");
// Установка конфигурации
set_config(
.w(pulse_w),
.p(pulse_p),
.n(pulse_n),
.h(pulse_h)
);
reset_dut(5);
repeat(2) @(posedge clk);
// Старт норме 1 такт. Сделаем случайным от 5 до 25 тактов.
rand_start_duration = $urandom_range(5, 25);
$display("[TB] Long start: %0d clocks", rand_start_duration);
// Фоновый процесс подсчета тактов импульса
fork
begin : counter_proc
forever begin
@(posedge dac_wrt);
if (dac_out == pulse_h) begin
total_impulse_cycles++;
end
end
end
join_none
// Параллельный запуск длинного старта и обработки синхронизации
fork
// Поток 1: Удерживаем старт аномально долго
begin
start_dut(rand_start_duration);
end
// Поток 2: Обслуживаем n=4 циклов синхронизации со случайными задержками
begin
repeat(pulse_n) begin
// Рандомизируем параметры для каждого из 4-х рукопожатий
rand_first = $urandom; // Случайно: Самплер первый (1) или Генератор первый (0)
rand_delay = $urandom_range(1, 8); // Случайная задержка ожидания (1..8 тактов)
rand_ack = $urandom_range(5, 10); // Аномально долгий удерживаемый импульс sampler_done (10..30 тактов)
synchronize(
.sampler_first(rand_first),
.delay_before_ack(rand_delay),
.ack_duration(rand_ack)
);
end
end
join
repeat(pulse_p+5) @(posedge clk);
disable counter_proc;
// Ожидание завершения переходных процессов
repeat(10) @(posedge clk);
if (total_impulse_cycles == pulse_w*pulse_n)
$display("[TB] -check_impulses- Pulse generation CORRECT");
else begin
$display("[ERROR] -check_impulses- Pulse generation INCORRECT. Total number of pulses: %d, must be: %d", total_impulse_cycles, pulse_w*pulse_n);
$finish;
end
$display("[TB] -check_impulses- Done");
endtask
task automatic run_test_case(
input int pulse_w,
input int pulse_p,
input int pulse_n,
input int pulse_h,
input bit skip_reset, // skip reset sequence on demand
input bit count_level // count ticks of amplitude == pulse_h or amplitude != pulse_h
);
int total_impulse_cycles = 0;
if (!skip_reset) begin
reset_dut(1);
@(posedge clk);
end
set_config(
.w(pulse_w),
.p(pulse_p),
.n(pulse_n),
.h(pulse_h)
);
@(posedge clk);
start_dut(1);
// Фоновый процесс подсчета тактов импульса
fork
begin : counter_proc
forever begin
@(posedge dac_wrt);
if (count_level) begin
if (dac_out == pulse_h) begin
total_impulse_cycles++;
end
end
else begin
if (dac_out != current_zero_level) begin
total_impulse_cycles++;
end
end
end
end
join_none
repeat(pulse_n) begin
synchronize(
.sampler_first(0),
.delay_before_ack(1),
.ack_duration(2)
);
end
repeat(pulse_p+5) @(posedge clk);
disable counter_proc;
repeat(10) @(posedge clk);
if (count_level) begin
if (total_impulse_cycles == pulse_w*pulse_n)
$display("[TB] -run_test_case- Pulse generation CORRECT");
else begin
$display("[ERROR] -run_test_case- Pulse generation INCORRECT. Total number of pulses: %d, must be: %d", total_impulse_cycles, pulse_w*pulse_n);
$finish;
end
end
else begin
if (total_impulse_cycles == 0)
$display("[TB] -run_test_case- Pulse generation CORRECT");
else begin
$display("[ERROR] -run_test_case- Pulse generation INCORRECT. Total number of pulses: %d, must be: %d", total_impulse_cycles, 0);
$finish;
end
end
endtask
// --- ОСНОВНОЙ ПРОЦЕСС ТЕСТИРОВАНИЯ ---
initial begin
$display("[TB] Tests start");
// Инициализация
rst = 1;
start = 0;
pulse_width = 0;
pulse_period = 0;
pulse_height = 0;
pulse_num = 0;
sampler_done = 0;
repeat(5) @(posedge clk);
rst = 0;
$display("[TB] Test 1. Random latency for control signals");
check_impulses();
$display("[TB] Test 1 complete");
// --- Test 1 ---
// 3 clk 1, 5 clk 0, 4 pulses
repeat(2) @(posedge clk);
pulse_width = 3;
pulse_period = 8;
pulse_num = 4;
pulse_height = 14'h3FF;
start = 1;
$display("[TB] Test 2. Random configs");
for (int i = 0; i < 25; i++) begin
int r_w, r_p, r_n, r_h;
bit r_skip;
repeat(1) @(posedge clk);
start = 0;
// Генерируем параметры
r_p = $urandom_range(5, 50); // Период от 5 до 50
r_w = $urandom_range(0, r_p); // Ширина не больше периода
r_n = $urandom_range(1, 10); // Количество импульсов
r_h = $urandom_range(1, 2**DATA_WIDTH-1); // Высота (для 14 бит)
r_skip = $urandom_range(0, 1); // Случайный сброс (0 - сброс, 1 - пропуск)
repeat(50) @(posedge clk);
// Защита от "нулевого" импульса. Невозможно проверить длительность.
if (r_h == current_zero_level) begin
r_h += $urandom_range(1, 10);
end
// --- Test 2 ---
$display("\n--- SECOND RUN ---\n");
$display("[TB] --- Test #%0d (Config: W=%0d, P=%0d, N=%0d, H=%0d, SkipReset=%0b) ---",
i+1, r_w, r_p, r_n, r_h, r_skip);
@(posedge clk);
pulse_width = 2;
pulse_period = 5;
pulse_num = 3;
pulse_height = 14'h155;
start = 1;
run_test_case(
.pulse_w(r_w),
.pulse_p(r_p),
.pulse_n(r_n),
.pulse_h(r_h),
.skip_reset(r_skip),
.count_level(1)
);
end
$display("[TB] Test 2 complete");
@(posedge clk);
start = 0;
$display("[TB] Test 3. Zero level of pulse height");
run_test_case(
.pulse_w(77),
.pulse_p(131),
.pulse_n(13),
.pulse_h(current_zero_level),
.skip_reset(0),
.count_level(0)
);
$display("[TB] Test 3 complete");
repeat(40) @(posedge clk);
pulse_width = 3;
pulse_period = 8;
pulse_num = 4;
pulse_height = 14'h3FF;
start = 1;
repeat(1) @(posedge clk);
start = 0;
repeat(50) @(posedge clk);
$display("\n=== TEST FINISHED ===");
$display("[TB] ALL PASSED");
$finish;
end
// Display
always @(posedge clk) begin
$display("t=%0t | pulse=%0b | height=%h",
$time, pulse, pulse_height_out);
end
endmodule

View File

@ -1,23 +1,139 @@
# Сэмплер
Модуль выполняет задачу сбора данных с выхода АЦП, их обработку, упаковку, и передачу дальше с помощью AXI Stream интерфейса.
## Cписок параметров
DATA_WIDTH - ширина входных данных, получаемых с АЦП.
PACK_FACTOR - количество отсчетов, собираемых в один выходной пакет.
PROCESS_MODE - режим интерпретации входного кода. 0 - прямой код, 1 - дополнительный код.
Модуль выполняет задачу сбора данных с выхода АЦП, их обработки, упаковки и передачи дальше с помощью AXI Stream интерфейса.
Дополнительно реализован механизм синхронизации с внешним генератором через сигналы `sample_req` и `sample_done`, позволяющий запускать сбор строго по запросу и подтверждать завершение выборки.
---
## Список параметров
DATA_WIDTH
Ширина входных данных, получаемых с АЦП.
PACK_FACTOR
Количество отсчетов, собираемых в один выходной пакет.
PROCESS_MODE
Режим интерпретации входного кода:
- `0` — прямой код
- `1` — дополнительный код
---
## Список входных портов
clk_in - сигнал тактирования выходного интерфейса.
rst - сброс модуля и остановка подачи импульсов.
[DATA_WIDTH-1:0] data_in - входной сигнал с АЦП.
out_of_range - флаг выхода значений данных за допустимый диапазон. 0 - валидны, 1 - не валидны.
clk_in
Сигнал тактирования выходного интерфейса.
rst
Сброс модуля и остановка работы.
[DATA_WIDTH-1:0] data_in
Входной сигнал с АЦП.
out_of_range
Флаг выхода значений данных за допустимый диапазон:
- `0` — данные валидны
- `1` — данные невалидны и игнорируются
[31:0] smp_num
Количество валидных отсчетов, которое необходимо собрать после получения запроса на выборку.
sample_req
Сигнал запроса на запуск выборки.
При его активации модуль начинает сбор данных и переходит в активное состояние (`enable = 1`).
---
## Список выходных портов
[DATA_WIDTH*PACK_FACTOR-1:0] m_axis_tdata - урезанный axis формат, выходные данные. Ширина шины считается исходя из битности данных и фактора упаковки.
m_axis_tvalid - урезанный axis формат, валидность выходных данных.
[DATA_WIDTH*PACK_FACTOR-1:0] m_axis_tdata
Урезанный AXI Stream формат, выходные данные.
Ширина шины определяется как произведение битности данных и фактора упаковки.
m_axis_tvalid
Урезанный AXI Stream формат, сигнал валидности выходных данных.
Формируется при готовности очередного пакета.
sample_done
Сигнал завершения выборки.
Поднимается после того, как модуль собрал количество валидных отсчетов, равное `smp_num`.
---
## Логика работы
На каждом такте принимаются data_in (значение АЦП) и out_of_range (флаг выхода значений данных за допустимый диапазон). Если out_of_range = 1, то данные игнорируются и не попадают во внутренний буффер. В противном случае, модуль накапливает данные во внутреннем буффере, идет его заполнение до количества данных, равное PACK_FACTOR. Когда буффер оказывается заполненным, он выдает пакет упакованных данных, сопровождая их импульсом m_axis_tvalid (готовность пакета). Если PROCESS_MODE = 1, данные выдаются в дополнительном коде, если PROCESS_MODE = 0 - в прямом.
На каждом такте принимаются:
- `data_in` — значение АЦП
- `out_of_range` — флаг допустимости значения
Если `out_of_range = 1`, данные считаются невалидными, игнорируются и не попадают во внутренний буфер.
Если `out_of_range = 0`, данные считаются корректными и используются для дальнейшей обработки.
---
### Преобразование данных
Если `PROCESS_MODE = 1`, входные данные интерпретируются как дополнительный код и преобразуются перед упаковкой.
Если `PROCESS_MODE = 0`, данные передаются без преобразования (прямой код).
---
### Запуск выборки
Сбор данных начинается только после прихода сигнала `sample_req`.
При этом:
- фиксируется значение `smp_num`
- внутренний счетчик собранных отсчетов обнуляется
- модуль переходит в активное состояние (`enable = 1`)
Пока `enable = 1`, модуль принимает только валидные отсчеты и считает их.
---
### Упаковка данных
Внутренний буфер заполняется до количества данных, равного `PACK_FACTOR`.
#### Если `PACK_FACTOR = 1`
Каждый валидный отсчет сразу формирует выходной пакет:
- данные передаются в `m_axis_tdata`
- формируется импульс `m_axis_tvalid`
#### Если `PACK_FACTOR > 1`
Данные последовательно накапливаются во внутреннем сдвиговом буфере.
Когда буфер полностью заполнен:
- формируется пакет упакованных данных
- поднимается `m_axis_tvalid`
После этого начинается сбор следующего пакета.
---
### Завершение выборки
Когда количество собранных валидных отсчетов достигает значения `smp_num`:
- поднимается сигнал `sample_done`
- внутренние счетчики сбрасываются
- буфер очищается
- `enable` сбрасывается в `0`
Это означает полное завершение текущего цикла выборки.
---
## Симуляция
Тесты запускаются автоматически через make.

View File

@ -1,7 +1,5 @@
`timescale 1ns / 1ps
module sampler
#(
parameter DATA_WIDTH = 12,
@ -14,16 +12,16 @@ module sampler
input [DATA_WIDTH-1:0] data_in,
input out_of_range,
input [31:0] smp_num,
input sample_req,
input done,
output logic [DATA_WIDTH*PACK_FACTOR-1:0] m_axis_tdata,
output logic m_axis_tvalid,
output logic sample_done
output logic request
);
(* MARK_DEBUG="true" *) logic [DATA_WIDTH-1:0] data_converted;
(* MARK_DEBUG="true" *) logic out_of_range_reg;
(* MARK_DEBUG="true" *) logic [31:0] smp_num_reg, cnt_smp_num;
(* MARK_DEBUG="true" *) logic enable;
(* MARK_DEBUG="true" *) logic enable, enable_d;
generate
if (PROCESS_MODE) begin
@ -68,20 +66,22 @@ module sampler
buffer_ready <= 0;
cnt_smp_num <= '0;
smp_num_reg <= '0;
enable <= '0;
sample_done <= 0;
enable <= 0;
request <= 0;
end
else begin
buffer_ready <= 0;
if (sample_done && !sample_req) begin
sample_done <= 1'b0;
end
if (!enable && sample_req && !sample_done) begin
enable <= 1;
cnt_smp_num <= 0;
smp_num_reg <= smp_num;
end
if (enable) begin
enable_d <= enable;
if (!enable) begin
if (request && done) begin
enable <= 1;
request <= 0;
cnt_smp_num <= 0;
smp_num_reg <= smp_num;
end else begin
request <= 1;
end
end else begin
if (!out_of_range_reg) begin
if (cnt_smp_num != smp_num_reg) begin
buffer <= data_converted;
@ -90,8 +90,6 @@ module sampler
end
else begin
cnt_smp_num <= '0;
sample_done <= 1'b1;
buffer_ready <= 0;
buffer <= '0;
enable <= 0;
end
@ -108,21 +106,21 @@ module sampler
cnt_smp_num <= '0;
smp_num_reg <= '0;
enable <= 0;
sample_done <= 0;
request <= 0;
end
else begin
buffer_ready <= 0;
if (sample_done && !sample_req) begin
sample_done <= 1'b0;
end
if (!enable && sample_req && !sample_done) begin
enable <= 1;
cnt_smp_num <= 0;
smp_num_reg <= smp_num;
end
if (enable) begin
if (!enable) begin
if (!request) request <= 1;
if (request && done) begin
enable <= 1;
request <= 0;
cnt_smp_num <= 0;
smp_num_reg <= smp_num;
end
end else begin
if (!out_of_range_reg) begin
if (cnt_smp_num != smp_num_reg) begin
if (cnt_smp_num < smp_num_reg) begin
cnt_smp_num <= cnt_smp_num +1;
buffer <= {buffer[DATA_WIDTH*(PACK_FACTOR-1)-1:0], data_converted};
if (cnt == PACK_FACTOR-1) begin
@ -135,7 +133,6 @@ module sampler
end
end
else begin
sample_done <= 1'b1;
cnt_smp_num <= '0;
buffer_ready <= 0;
buffer <= '0;

View File

@ -2,130 +2,209 @@
module sampler_tb;
parameter DATA_WIDTH = 12;
parameter PROCESS_MODE = 0;
parameter CLK_PERIOD = 15.3846;
parameter TEST_NUM = 1000;
// =========================================================
// PARAMETERS
// =========================================================
localparam DATA_WIDTH = 12;
localparam PACK_FACTOR = 1;
localparam PROCESS_MODE = 0;
localparam CLK_PERIOD = 15.3846;
// =========================================================
// SIGNALS
// =========================================================
logic clk;
logic rst;
logic [DATA_WIDTH-1:0] data_in;
logic out_of_range;
logic [DATA_WIDTH-1:0] m_axis_tdata;
logic [31:0] smp_num;
logic done;
logic request;
logic [DATA_WIDTH*PACK_FACTOR-1:0] m_axis_tdata;
logic m_axis_tvalid;
integer errors = 0;
// =========================================================
// DUT
// =========================================================
sampler #(
.DATA_WIDTH(DATA_WIDTH),
.PACK_FACTOR(PACK_FACTOR),
.PROCESS_MODE(PROCESS_MODE)
) dut (
.clk_in(clk),
.rst(rst),
.data_in(data_in),
.out_of_range(out_of_range),
.smp_num(smp_num),
.done(done),
.m_axis_tdata(m_axis_tdata),
.m_axis_tvalid(m_axis_tvalid)
.m_axis_tvalid(m_axis_tvalid),
.request(request)
);
// =========================================================
// CLOCK
// =========================================================
initial begin
clk = 0;
forever #(CLK_PERIOD/2) clk = ~clk;
end
function automatic [DATA_WIDTH-1:0] ref_convert(input [DATA_WIDTH-1:0] din);
if (PROCESS_MODE == 0)
return din;
else if (din == {1'b1, {(DATA_WIDTH-1){1'b0}}})
return din;
else
return din[DATA_WIDTH-1] ?
{1'b1, (~din[DATA_WIDTH-2:0] + 1'b1)} :
din;
endfunction
task send(input [DATA_WIDTH-1:0] word, input bit oor);
@(posedge clk);
data_in <= word;
out_of_range <= oor;
endtask
logic [DATA_WIDTH-1:0] exp_d0, exp_d1, exp_d2;
logic oor_d0, oor_d1, oor_d2;
// =========================================================
// RESET (ONLY ONCE)
// =========================================================
initial begin
$display("\n=== RANDOM SAMPLER TEST===\n");
rst = 1;
done = 0;
data_in = 0;
out_of_range = 0;
exp_d0 = 0;
exp_d1 = 0;
exp_d2 = 0;
oor_d0 = 1;
oor_d1 = 1;
oor_d2 = 1;
smp_num = 0;
repeat(5) @(posedge clk);
rst = 0;
repeat(2) @(posedge clk);
end
repeat (TEST_NUM) begin
logic [DATA_WIDTH-1:0] rand_data;
bit rand_oor;
// =========================================================
// CONFIG
// =========================================================
task automatic set_config(
input int n,
input int init_delay
);
smp_num = n;
repeat(init_delay) @(posedge clk);
endtask
rand_data = $urandom_range(0, (1 << DATA_WIDTH) - 1);
rand_oor = ($urandom_range(0, 99) < 20);
// =========================================================
// HANDSHAKE (DONE/REQUEST)
// =========================================================
task automatic synchronize_sampler(
input bit sampler_first,
input int delay_before_ack,
input int ack_duration
);
if (sampler_first) begin
repeat(delay_before_ack) @(posedge clk);
@(negedge clk);
done <= 1'b1;
wait(request == 1'b1);
if (!oor_d2) begin
if (m_axis_tvalid !== 1) begin
$display("ERROR: valid=0");
errors++;
end
repeat(ack_duration) @(posedge clk);
if (m_axis_tdata !== exp_d2) begin
$display("ERROR: data mismatch");
$display(" expected = %h", exp_d2);
$display(" got = %h", m_axis_tdata);
errors++;
end
end
send(rand_data, rand_oor);
exp_d2 = exp_d1;
exp_d1 = exp_d0;
exp_d0 = ref_convert(rand_data);
oor_d2 = oor_d1;
oor_d1 = oor_d0;
oor_d0 = rand_oor;
done <= 1'b0;
end
else begin
wait(request == 1'b1);
@(posedge clk);
repeat(delay_before_ack) @(posedge clk);
done <= 1'b1;
if (!oor_d2) begin
repeat(ack_duration) @(posedge clk);
if (m_axis_tdata !== exp_d2) begin
$display("ERROR: final mismatch");
$display(" expected = %h", exp_d2);
$display(" got = %h", m_axis_tdata);
errors++;
end
done <= 1'b0;
end
endtask
if (errors == 0)
$display("\n========== ALL PASSED ==========\n");
// =========================================================
// DATA STREAM (STARTS AFTER SYNC)
// =========================================================
task automatic feed_data_stream(
input int num_words,
input bit random_mode
);
logic [DATA_WIDTH-1:0] val;
val = 1;
for (int i = 0; i < num_words; i++) begin
@(posedge clk);
if (random_mode)
val = $urandom_range(1, 2**DATA_WIDTH-1);
else
val = val + 1;
data_in <= val;
out_of_range <= 0;
end
endtask
// =========================================================
// COUNTER
// =========================================================
int received_count;
always @(posedge clk) begin
if (m_axis_tvalid)
received_count++;
end
// =========================================================
// TEST CASE
// =========================================================
task automatic run_test_case(
input int n,
input int delay,
input int ack,
input bit sampler_first,
input bit random_stream
);
received_count = 0;
set_config(n, 2);
// 1) сначала синхронизация
synchronize_sampler(sampler_first, delay, ack);
// 2) СРАЗУ после sync - поток данных
feed_data_stream(n, random_stream);
repeat(20) @(posedge clk);
if (received_count == n)
$display("[OK] received %0d / %0d", received_count, n);
else
$display("\n========== FAILED: %0d errors ==========\n", errors);
$display("[ERROR] received %0d / %0d", received_count, n);
endtask
// =========================================================
// RANDOM STRESS TEST
// =========================================================
task automatic check_random;
int n, d, a;
bit sf;
for (int i = 0; i < 20; i++) begin
n = $urandom_range(3, 10);
d = $urandom_range(0, 5);
a = $urandom_range(1, 5);
sf = $urandom_range(0, 1);
$display("\n--- TEST %0d --- n=%0d delay=%0d ack=%0d sf=%0b",
i, n, d, a, sf);
run_test_case(n, d, a, sf, 1);
end
endtask
// =========================================================
// MAIN
// =========================================================
initial begin
$display("\n=== SAMPLER TEST (FIXED FLOW: NO MULTI RESET) ===\n");
check_random();
$display("\n=== TEST FINISHED ===");
$finish;
end

View File

@ -1,736 +0,0 @@
# 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()

View File

@ -1,505 +0,0 @@
<?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>