From 07ffb3165176c288c224d4bd9870eb04c74442f8 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 26 May 2026 16:11:29 +0300 Subject: [PATCH 01/19] sw: gui version --- software/gui.py | 698 ++++++++++++++++++++++++++++++++++++++ software/reflectometer.ui | 447 ++++++++++++++++++++++++ 2 files changed, 1145 insertions(+) create mode 100644 software/gui.py create mode 100644 software/reflectometer.ui diff --git a/software/gui.py b/software/gui.py new file mode 100644 index 0000000..784794e --- /dev/null +++ b/software/gui.py @@ -0,0 +1,698 @@ +# 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 + + +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 + + dac_adc_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.dac_adc_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.dac_adc_ratio + * self.config.pulse_period + / self.config.window_size + * self.config.data_width + ) + / self.config.packet_size + ) + + expected_length = math.ceil( + self.config.dac_adc_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.dac_adc_ration = 0.52 + self.accum_width = 32 + + # setup + self.set_max_pulse_height(100) + self.set_max_pulse_width(500) + self.set_max_pulse_period(1000) + self.set_max_pulse_num(20) + + self.setup_pulse_controls() + self.setup_global_settings() + + self.update_pulse_limits() + + self.data = [] + + self.dac_adc_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) + + # ping utils + + def check_ping(self): + ip = self.line_ip.text().strip() + + if not ip: + self.label_ping_status.setText("set ip!!") + return + + if "_" in self.line_ip.displayText(): + self.label_ping_status.setText("IP invalid") + return + + if self.ping_process is not None: + if self.ping_process.state() != QProcess.ProcessState.NotRunning: + self.label_ping_status.setText("Ping inflight") + return + + self.label_ping_status.setText("ping...") + self.button_ping.setEnabled(False) + + self.ping_process = QProcess(self) + + self.ping_process.finished.connect(self.on_ping_finished) + self.ping_process.errorOccurred.connect(self.on_ping_error) + + system_name = platform.system().lower() + + if system_name == "windows": + program = "ping" + arguments = ["-n", "1", "-w", "2000", ip] + else: + program = "ping" + arguments = ["-c", "1", "-W", "2", ip] + + self.ping_process.start(program, arguments) + # fallback + self.ping_timeout_timer.start(2000) + + def on_ping_finished(self, exit_code, exit_status): + self.ping_timeout_timer.stop() + self.button_ping.setEnabled(True) + + if exit_code == 0: + self.label_ping_status.setText("алё✅") + else: + self.label_ping_status.setText("не алё❌") + + def on_ping_error(self): + self.ping_timeout_timer.stop() + self.button_ping.setEnabled(True) + self.label_ping_status.setText("ping unavail") + + def on_ping_timeout(self): + if self.ping_process is not None: + if self.ping_process.state() != QProcess.ProcessState.NotRunning: + self.ping_process.kill() + + self.button_ping.setEnabled(True) + + # pulse controls + def setup_pulse_controls(self): + self._bind_slider_and_spinbox( + name="pulse_period", + slider=self.slider_pulse_period, + box=self.box_pulse_period, + normalize_value=self.normalize_pulse_period, + ) + + self._bind_slider_and_spinbox( + name="pulse_height", + slider=self.slider_pulse_height, + box=self.box_pulse_height, + ) + + self._bind_slider_and_spinbox( + name="pulse_width", + slider=self.slider_pulse_width, + box=self.box_pulse_width, + ) + + self._bind_slider_and_spinbox( + name="pulse_num", + slider=self.slider_pulse_num, + box=self.box_pulse_num, + ) + + def _bind_slider_and_spinbox(self, name, slider, box, normalize_value=None): + """ + Связывает QSlider и QSpinBox по значению. + Значение автоматически записывается в self.. + """ + + minimum = min(slider.minimum(), box.minimum()) + maximum = max(slider.maximum(), box.maximum()) + + slider.setRange(minimum, maximum) + box.setRange(minimum, maximum) + + def normalize(value): + if normalize_value is None: + return value + + return normalize_value(value) + + value = normalize(box.value()) + + slider.setValue(value) + box.setValue(value) + setattr(self, name, value) + + def update_value(new_value): + new_value = normalize(new_value) + + if slider.value() != new_value: + slider.setValue(new_value) + + if box.value() != new_value: + box.setValue(new_value) + + setattr(self, name, new_value) + + slider.valueChanged.connect(update_value) + box.valueChanged.connect(update_value) + + def normalize_pulse_period(self, value): + step = max(1, getattr(self, "window_size", + self.box_window_size.value())) + + snapped_value = round(value / step) * step + + minimum = self.box_pulse_period.minimum() + maximum = self.box_pulse_period.maximum() + + return max(minimum, min(snapped_value, maximum)) + + def _set_max_for_pair(self, slider, box, maximum): + slider.setMaximum(maximum) + box.setMaximum(maximum) + + value = min(box.value(), maximum) + box.setValue(value) + slider.setValue(value) + + def set_max_pulse_period(self, maximum): + self._set_max_for_pair( + slider=self.slider_pulse_period, + box=self.box_pulse_period, + maximum=maximum, + ) + self.pulse_period = self.box_pulse_period.value() + + def set_max_pulse_height(self, maximum): + self._set_max_for_pair( + slider=self.slider_pulse_height, + box=self.box_pulse_height, + maximum=maximum, + ) + self.pulse_height = self.box_pulse_height.value() + + def set_max_pulse_width(self, maximum): + self._set_max_for_pair( + slider=self.slider_pulse_width, + box=self.box_pulse_width, + maximum=maximum, + ) + self.pulse_width = self.box_pulse_width.value() + + def set_max_pulse_num(self, maximum): + self._set_max_for_pair( + slider=self.slider_pulse_num, + box=self.box_pulse_num, + maximum=maximum, + ) + self.pulse_num = self.box_pulse_num.value() + + # settings + + def setup_global_settings(self): + self._bind_spinbox_setting( + name="dac_dw", + box=self.box_dac_dw, + ) + + self._bind_spinbox_setting( + name="adc_dw", + box=self.box_adc_dw, + ) + + self._bind_spinbox_setting( + name="nmax", + box=self.box_nmax, + ) + + self._bind_spinbox_setting( + name="window_size", + box=self.box_window_size, + after_change=self.on_window_size_changed, + ) + + self._bind_spinbox_setting( + name="packet_size", + box=self.box_packet_size, + ) + + self._bind_spinbox_setting( + name="dac_adc_ratio", + box=self.box_dac_adc_ratio, + ) + + self._bind_spinbox_setting( + name="accum_width", + box=self.box_accum_width, + ) + + self._bind_spinbox_setting( + name="recv_port", + box=self.box_recv_port, + ) + + self._bind_spinbox_setting( + name="send_port", + box=self.box_send_port, + ) + + # применяем шаг для pulse_period сразу при старте + self.update_pulse_period_step() + + def _bind_spinbox_setting(self, name, box, after_change=None): + """ + Связывает QSpinBox с полем self.. + Например: + box_dac_dw -> self.dac_dw + box_window_size -> self.window_size + """ + + value = box.value() + setattr(self, name, value) + + def on_value_changed(new_value): + setattr(self, name, new_value) + + self.update_pulse_limits() + + if after_change is not None: + after_change(new_value) + + box.valueChanged.connect(on_value_changed) + + def update_pulse_limits(self): + # re-calc limits + """ + Сюда потом можно дописать формулы для пересчета максимумов. + + Например: + max_pulse_height = 2 ** self.dac_dw - 1 + self.set_max_pulse_height(max_pulse_height) + + max_pulse_period = self.nmax * self.window_size + self.set_max_pulse_period(max_pulse_period) + + max_pulse_width = ... + self.set_max_pulse_width(max_pulse_width) + + max_pulse_num = ... + self.set_max_pulse_num(max_pulse_num) + """ + # 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)) + # dac_width -> max pulse height + self.set_max_pulse_height(2 ** self.dac_dw) + + 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([]) + + self.graph_layout.addWidget(self.graph_widget) + + 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 адрес не задан") + + # ВАЖНО: + # В console.py data_width был в байтах: 4 для int32. + # Если у тебя box_adc_dw хранит именно 4, оставь так. + # Если box_adc_dw хранит 32 бита, замени на: + # data_width = self.adc_dw // 8 + data_width = self.adc_dw // 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, + + dac_adc_ratio=self.dac_adc_ratio, + ) + + def on_data_received(self, data: list[int]): + self.data = data + + x = list(range(len(data))) + self.graph_curve.setData(x, data) + + if data: + self.set_measurement_status( + f"Готово. Samples: {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): + print(text) + + # Если отдельного label под статус измерения пока нет, + # можно временно использовать label_ping_status. + if hasattr(self, "label_ping_status"): + self.label_ping_status.setText(text) + + +def main(): + app = QApplication(sys.argv) + + window = MainWindow() + window.show() + + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() diff --git a/software/reflectometer.ui b/software/reflectometer.ui new file mode 100644 index 0000000..59e8de5 --- /dev/null +++ b/software/reflectometer.ui @@ -0,0 +1,447 @@ + + + MainWindow + + + + 0 + 0 + 1023 + 708 + + + + Reflectometer PREMIUM + + + + + + + + + + + + 0 + + + + Настройки + + + + + + true + + + + + 0 + 0 + 294 + 621 + + + + + + + + 12 + + + + Аппаратные параметры + + + + + + + DAC data width: + + + 8 + + + 32 + + + 14 + + + + + + + ADC data width: + + + 8 + + + 32 + + + 12 + + + + + + + Accum width: + + + 16 + + + 64 + + + 32 + + + + + + + DAC:ADC clk ratio: + + + 0.200000000000000 + + + 3.000000000000000 + + + 0.520000000000000 + + + + + + + N Max: + + + 512 + + + 65536 + + + 4096 + + + + + + + Window size: + + + 1 + + + 1024 + + + 65 + + + + + + + Packet size: + + + 1 + + + 1572 + + + 1024 + + + + + + + Qt::Orientation::Horizontal + + + + + + + + 12 + + + + Подключение + + + + + + + IP устройства: + + + + + + + 999.999.999.999 + + + 192.168.0.2 + + + + + + + Порт отправки: + + + + + + + 80 + + + 65536 + + + 8080 + + + + + + + Порт приёма: + + + + + + + 80 + + + 65536 + + + 8080 + + + + + + + + 12 + + + + Тест + + + + + + + алё + + + + + + + ... + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + Управление + + + + + + + 12 + + + + Импульс + + + + + + + + + Период + + + + + + + 1 + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + Ширина + + + + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + Высота + + + + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + + + Количество + + + + + + + 1 + + + + + + + Qt::Orientation::Horizontal + + + + + + + + + start! + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + 0 + 0 + 1023 + 30 + + + + + + + + From d925a4ffaa8450de7b65e4c0218adb14d2f41138 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 26 May 2026 16:16:15 +0300 Subject: [PATCH 02/19] sw: update reference graph options --- software/gui.py | 70 ++++++++++++++++++++++++++++++++++++--- software/reflectometer.ui | 9 ++++- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/software/gui.py b/software/gui.py index 784794e..4bbc24a 100644 --- a/software/gui.py +++ b/software/gui.py @@ -10,7 +10,7 @@ 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 @@ -574,9 +574,23 @@ class MainWindow(QMainWindow): self.graph_widget.setLabel("bottom", "Sample") self.graph_widget.showGrid(x=True, y=True) - self.graph_curve = self.graph_widget.plot([]) + 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)) + self.reference_curve = self.graph_widget.plot( + [], pen=pg.mkPen(style=Qt.PenStyle.DashLine)) + + self.checkbox_draw_reference.stateChanged.connect( + self.update_reference_graph) def setup_network_settings(self): self._bind_spinbox_setting( @@ -655,8 +669,8 @@ class MainWindow(QMainWindow): def on_data_received(self, data: list[int]): self.data = data - x = list(range(len(data))) - self.graph_curve.setData(x, data) + self.draw_main_graph() + self.update_reference_graph() if data: self.set_measurement_status( @@ -684,6 +698,54 @@ class MainWindow(QMainWindow): if hasattr(self, "label_ping_status"): self.label_ping_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 + reference[10:100] = ... + return reference + """ + + # TODO: сюда вставить свою логику эталонного графика + + reference = [0] * length + + return reference + def main(): app = QApplication(sys.argv) diff --git a/software/reflectometer.ui b/software/reflectometer.ui index 59e8de5..654da3c 100644 --- a/software/reflectometer.ui +++ b/software/reflectometer.ui @@ -23,7 +23,7 @@ - 0 + 1 @@ -409,6 +409,13 @@ + + + + Отрисовка эталона + + + From 99d4eb976f777ea08b28841f79219fdac3856b86 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 26 May 2026 16:36:03 +0300 Subject: [PATCH 03/19] sw: fix gui console data recv --- software/gui.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/software/gui.py b/software/gui.py index 4bbc24a..5d90203 100644 --- a/software/gui.py +++ b/software/gui.py @@ -231,10 +231,6 @@ class MainWindow(QMainWindow): self.accum_width = 32 # setup - self.set_max_pulse_height(100) - self.set_max_pulse_width(500) - self.set_max_pulse_period(1000) - self.set_max_pulse_num(20) self.setup_pulse_controls() self.setup_global_settings() @@ -525,9 +521,9 @@ class MainWindow(QMainWindow): 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)) + self.set_max_pulse_num(2 ** (self.accum_width - self.adc_dw) - 1) # dac_width -> max pulse height - self.set_max_pulse_height(2 ** self.dac_dw) + 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) @@ -641,12 +637,7 @@ class MainWindow(QMainWindow): if not ip: raise ValueError("IP адрес не задан") - # ВАЖНО: - # В console.py data_width был в байтах: 4 для int32. - # Если у тебя box_adc_dw хранит именно 4, оставь так. - # Если box_adc_dw хранит 32 бита, замени на: - # data_width = self.adc_dw // 8 - data_width = self.adc_dw // 8 + data_width = self.accum_width // 8 return ReflectometerConfig( ip=ip, From 9b5e39f3dfe395723a018f342e769bbab76f28c8 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 26 May 2026 16:46:51 +0300 Subject: [PATCH 04/19] sw: add normalization --- software/gui.py | 39 ++++++++++++++++++--------------------- software/reflectometer.ui | 24 +++++++++++++++++++++--- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/software/gui.py b/software/gui.py index 5d90203..9f473d3 100644 --- a/software/gui.py +++ b/software/gui.py @@ -31,7 +31,7 @@ class ReflectometerConfig: pulse_height: int pulse_num: int - dac_adc_ratio: float = 0.52 + adc_dac_ratio: float = 0.52 socket_timeout_sec: float = 2.0 @@ -108,7 +108,7 @@ class ReflectometerWorker(QObject): output += 0b10001000.to_bytes(1, "little") pulse_period_adc = ( - int(self.config.pulse_period * self.config.dac_adc_ratio) + int(self.config.pulse_period * self.config.adc_dac_ratio) // self.config.window_size ) * self.config.window_size @@ -126,7 +126,7 @@ class ReflectometerWorker(QObject): def _recv_data(self) -> list[int]: packet_count = math.ceil( ( - self.config.dac_adc_ratio + self.config.adc_dac_ratio * self.config.pulse_period / self.config.window_size * self.config.data_width @@ -135,7 +135,7 @@ class ReflectometerWorker(QObject): ) expected_length = math.ceil( - self.config.dac_adc_ratio + self.config.adc_dac_ratio * self.config.pulse_period / self.config.window_size ) @@ -227,7 +227,7 @@ class MainWindow(QMainWindow): self.nmax = 4096 self.packet_size = 1024 self.window_size = 65 - self.dac_adc_ration = 0.52 + self.adc_dac_ration = 0.52 self.accum_width = 32 # setup @@ -239,7 +239,7 @@ class MainWindow(QMainWindow): self.data = [] - self.dac_adc_ratio = 0.52 + self.adc_dac_ratio = 0.52 self.measurement_thread = None self.measurement_worker = None @@ -455,8 +455,8 @@ class MainWindow(QMainWindow): ) self._bind_spinbox_setting( - name="dac_adc_ratio", - box=self.box_dac_adc_ratio, + name="adc_dac_ratio", + box=self.box_adc_dac_ratio, ) self._bind_spinbox_setting( @@ -578,6 +578,7 @@ class MainWindow(QMainWindow): self.reference_curve = self.graph_widget.plot( [], name="Reference", + color="b" ) self.graph_layout.addWidget(self.graph_widget) @@ -654,7 +655,7 @@ class MainWindow(QMainWindow): pulse_height=self.pulse_height, pulse_num=self.pulse_num, - dac_adc_ratio=self.dac_adc_ratio, + adc_dac_ratio=self.adc_dac_ratio, ) def on_data_received(self, data: list[int]): @@ -670,6 +671,11 @@ class MainWindow(QMainWindow): else: self.set_measurement_status("Готово, но данные пустые") + # normalize + for i in range(len(data)): + self.data[i] -= 2 ** (self.adc_dw - 1) + self.data[i] /= (self.window_size * self.pulse_num) + def on_measurement_error(self, message: str): self.set_measurement_status(f"Ошибка: {message}") @@ -721,20 +727,11 @@ class MainWindow(QMainWindow): self.reference_curve.setData(x, reference_data) def build_reference_data(self, length: int) -> list[int]: - """ - Тут потом опишешь свою логику построения эталонного сигнала. - - Главное: вернуть массив такой же или нужной тебе длины. - Например: - reference = [0] * length - reference[10:100] = ... - return reference - """ - - # TODO: сюда вставить свою логику эталонного графика - reference = [0] * length + reference[0:(self.pulse_width // self.window_size + 1) + ] = [self.pulse_height, ] * (self.pulse_width // self.window_size) + return reference diff --git a/software/reflectometer.ui b/software/reflectometer.ui index 654da3c..120c512 100644 --- a/software/reflectometer.ui +++ b/software/reflectometer.ui @@ -23,7 +23,7 @@ - 1 + 0 @@ -59,6 +59,9 @@ + + bits + DAC data width: @@ -75,6 +78,9 @@ + + bits + ADC data width: @@ -91,6 +97,9 @@ + + bits + Accum width: @@ -100,15 +109,18 @@ 64 + + 8 + 32 - + - DAC:ADC clk ratio: + ADC:DAC clk ratio: 0.200000000000000 @@ -116,6 +128,9 @@ 3.000000000000000 + + 0.010000000000000 + 0.520000000000000 @@ -155,6 +170,9 @@ + + bytes + Packet size: From 4ecb3f5ea50458bb2f2b5362ce944da0ecd2c608 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 26 May 2026 17:27:39 +0300 Subject: [PATCH 05/19] sw: fix reference and some options --- software/gui.py | 55 ++++++++++++++------------------------- software/reflectometer.ui | 28 +++++++++++++++++++- 2 files changed, 46 insertions(+), 37 deletions(-) diff --git a/software/gui.py b/software/gui.py index 9f473d3..17658c9 100644 --- a/software/gui.py +++ b/software/gui.py @@ -198,7 +198,7 @@ class ReflectometerWorker(QObject): if self.config.pulse_num >= 2**16 - 1: raise ValueError("pulse_num слишком большой") - if self.config.pulse_height >= 2**self.config.dac_bits - 1: + if self.config.pulse_height > 2**self.config.dac_bits - 1: raise ValueError("pulse_height слишком большой") @@ -500,28 +500,14 @@ class MainWindow(QMainWindow): def update_pulse_limits(self): # re-calc limits - """ - Сюда потом можно дописать формулы для пересчета максимумов. - Например: - max_pulse_height = 2 ** self.dac_dw - 1 - self.set_max_pulse_height(max_pulse_height) - - max_pulse_period = self.nmax * self.window_size - self.set_max_pulse_period(max_pulse_period) - - max_pulse_width = ... - self.set_max_pulse_width(max_pulse_width) - - max_pulse_num = ... - self.set_max_pulse_num(max_pulse_num) - """ # 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) - 1) + 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) @@ -578,13 +564,13 @@ class MainWindow(QMainWindow): self.reference_curve = self.graph_widget.plot( [], name="Reference", - color="b" ) self.graph_layout.addWidget(self.graph_widget) - self.graph_curve = self.graph_widget.plot([], pen=pg.mkPen(width=2)) + 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)) + [], pen=pg.mkPen(style=Qt.PenStyle.DashLine, color="g")) self.checkbox_draw_reference.stateChanged.connect( self.update_reference_graph) @@ -603,7 +589,7 @@ class MainWindow(QMainWindow): def run_measurement(self): if self.measurement_thread is not None: if self.measurement_thread.isRunning(): - self.set_measurement_status("Измерение уже выполняется") + self.set_measurement_status("Измерение выполняется") return config = self.build_reflectometer_config() @@ -660,21 +646,20 @@ class MainWindow(QMainWindow): 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"Готово. Samples: {len(data)}, min: {min(data)}, max: {max(data)}" + f"Готово. smp: {len(data)}, min: {min(data)}, max: {max(data)}" ) else: - self.set_measurement_status("Готово, но данные пустые") - - # normalize - for i in range(len(data)): - self.data[i] -= 2 ** (self.adc_dw - 1) - self.data[i] /= (self.window_size * self.pulse_num) + self.set_measurement_status("Данные пустые") def on_measurement_error(self, message: str): self.set_measurement_status(f"Ошибка: {message}") @@ -688,12 +673,7 @@ class MainWindow(QMainWindow): self.measurement_worker.stop() def set_measurement_status(self, text: str): - print(text) - - # Если отдельного label под статус измерения пока нет, - # можно временно использовать label_ping_status. - if hasattr(self, "label_ping_status"): - self.label_ping_status.setText(text) + self.label_status.setText(text) def draw_main_graph(self): if not self.data: @@ -729,8 +709,11 @@ class MainWindow(QMainWindow): def build_reference_data(self, length: int) -> list[int]: reference = [0] * length - reference[0:(self.pulse_width // self.window_size + 1) - ] = [self.pulse_height, ] * (self.pulse_width // self.window_size) + actual_pulse_width = round( + (self.pulse_width * self.adc_dac_ratio) / self.window_size) + + reference[0:actual_pulse_width] = [ + self.pulse_height - 2 ** (self.adc_dw - 1) + 1, ] * (actual_pulse_width - 1) return reference diff --git a/software/reflectometer.ui b/software/reflectometer.ui index 120c512..96ac6f0 100644 --- a/software/reflectometer.ui +++ b/software/reflectometer.ui @@ -23,7 +23,7 @@ - 0 + 1 @@ -413,6 +413,9 @@ + + 1 + Qt::Orientation::Horizontal @@ -427,6 +430,29 @@ + + + + + + + true + + + + Статус: + + + + + + + - + + + + + From c7216e4e8ebaf4aeb0364194836d9c3ce3a469a6 Mon Sep 17 00:00:00 2001 From: "babintsev.lv" Date: Tue, 26 May 2026 18:05:24 +0300 Subject: [PATCH 06/19] fix normalization --- software/gui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/software/gui.py b/software/gui.py index 17658c9..a01a7c8 100644 --- a/software/gui.py +++ b/software/gui.py @@ -713,7 +713,7 @@ class MainWindow(QMainWindow): (self.pulse_width * self.adc_dac_ratio) / self.window_size) reference[0:actual_pulse_width] = [ - self.pulse_height - 2 ** (self.adc_dw - 1) + 1, ] * (actual_pulse_width - 1) + (self.pulse_height / 2 ** (self.dac_dw - self.adc_dw)) - 2 ** (self.adc_dw - 1), ] * (actual_pulse_width - 1) return reference From 4938b80af6f86d43c9db0751523505632287fbb2 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 26 May 2026 18:15:56 +0300 Subject: [PATCH 07/19] sw: add reset scale button --- software/gui.py | 5 +++++ software/reflectometer.ui | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/software/gui.py b/software/gui.py index a01a7c8..1226f34 100644 --- a/software/gui.py +++ b/software/gui.py @@ -248,6 +248,7 @@ class MainWindow(QMainWindow): self.setup_network_settings() self.button_start.clicked.connect(self.run_measurement) + self.button_graph_autoscale.clicked.connect(self.reset_graph_autoscale) # ping utils @@ -717,6 +718,10 @@ class MainWindow(QMainWindow): 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) diff --git a/software/reflectometer.ui b/software/reflectometer.ui index 96ac6f0..7822d14 100644 --- a/software/reflectometer.ui +++ b/software/reflectometer.ui @@ -473,6 +473,13 @@ + + + + Сброс масштаба + + + From 02416a96210207e0f8bb02caa23413e80c425385 Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 26 Jun 2026 18:07:02 +0300 Subject: [PATCH 08/19] rtl: rework memory controls in accum --- rtl/accum/src/accum.sv | 55 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/rtl/accum/src/accum.sv b/rtl/accum/src/accum.sv index 121acdc..d77ad2c 100644 --- a/rtl/accum/src/accum.sv +++ b/rtl/accum/src/accum.sv @@ -39,10 +39,6 @@ module accumulator logic out_valid_reg; logic finish_reg, finish_buf; - // registers for port b data request - reg req_data_b; - reg [15:0] req_addr_b; - typedef enum logic [3:0] { IDLE, INIT_MEM, @@ -58,6 +54,57 @@ module accumulator } wr_state_t; (* MARK_DEBUG="true" *) wr_state_t wr_state; + // One word per clock accumulation pipeline + // On every sum_valid in ACCUM we launch a BRAM read for cnt_addr + // On the next clock the saved sum_data is added to doutb and written back + logic accum_pipe_valid; + logic [15:0] accum_pipe_addr; + logic [ACCUM_WIDTH-1:0] accum_pipe_data; + + // case smp_num // WINDOW_SIZE == 1 + // Then the next sequence can read the same address it is written + logic accum_pipe_bypass_valid; + logic [ACCUM_WIDTH-1:0] accum_pipe_bypass_data; + logic accum_done; + + logic accum_accept_last; + logic accum_accept_last_all; + logic [ACCUM_WIDTH-1:0] accum_write_base; + logic [ACCUM_WIDTH-1:0] accum_write_value; + + assign accum_accept_last = (cnt_smp_num + WINDOW_SIZE >= smp_num_reg); + assign accum_accept_last_all = accum_accept_last && (cnt_seq_num == seq_num_reg - 1); + assign accum_write_base = accum_pipe_bypass_valid ? accum_pipe_bypass_data : data_bram_out; + assign accum_write_value = accum_pipe_data + accum_write_base; + + // Memory controls to XPM + // In accumulation/init states they are driven directly from the current + // state and pipeline registers. That avoids an extra register stage + logic mem_wea; + logic mem_enb; + logic [15:0] mem_addra; + logic [15:0] mem_addrb; + logic [ACCUM_WIDTH-1:0] mem_dina; + + assign mem_wea = (wr_state == INIT_MEM) ? valid_data : + (wr_state == ACCUM) ? accum_pipe_valid : + 1'b0; + + assign mem_addra = (wr_state == INIT_MEM) ? cnt_addr : + (wr_state == ACCUM) ? accum_pipe_addr : + addra; + + assign mem_dina = (wr_state == INIT_MEM) ? data : + (wr_state == ACCUM) ? accum_write_value : + data_bram_in; + + assign mem_enb = (wr_state == ACCUM) ? valid_data : enb; + assign mem_addrb = (wr_state == ACCUM) ? cnt_addr : addrb; + + // registers for port b data request + reg req_data_b; + reg [15:0] req_addr_b; + always @(posedge clk_in) begin if (rst) begin smp_num_reg <= '0; From 425c9226901053a9e199ebcddb91da5ceeb6a5f8 Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 26 Jun 2026 18:39:33 +0300 Subject: [PATCH 09/19] rtl: update accum to support window_size < 3 --- rtl/accum/src/accum.sv | 149 ++++++++++++++++++++++++++++++----------- 1 file changed, 109 insertions(+), 40 deletions(-) diff --git a/rtl/accum/src/accum.sv b/rtl/accum/src/accum.sv index d77ad2c..b7ef552 100644 --- a/rtl/accum/src/accum.sv +++ b/rtl/accum/src/accum.sv @@ -112,11 +112,23 @@ module accumulator seq_num_reg <= '0; cnt_seq_num <= '0; cnt_addr <= '0; + addra <= '0; + addrb <= '0; + data_bram_in <= '0; wea <= 0; enb <= 0; wr_state <= IDLE; finish_reg <= 0; + finish_buf <= 0; + readout_begin_reg <= 0; + out_data_reg <= '0; out_valid_reg <= 0; + accum_pipe_valid <= 0; + accum_pipe_addr <= '0; + accum_pipe_data <= '0; + accum_pipe_bypass_valid <= 0; + accum_pipe_bypass_data <= '0; + accum_done <= 0; end else begin finish_buf <= finish; @@ -130,6 +142,13 @@ module accumulator readout_begin_reg <= 0; finish_reg <= 0; out_valid_reg <= 0; + accum_pipe_valid <= 0; + accum_pipe_bypass_valid <= 0; + accum_done <= 0; + cnt_smp_num <= '0; + cnt_seq_num <= '0; + cnt_addr <= '0; + addrb <= '0; if (start) begin smp_num_reg <= smp_num; seq_num_reg <= seq_num; @@ -137,76 +156,122 @@ module accumulator end end + INIT_MEM: begin - // first run to initialize memory with first batch of values + // First sequence wea <= 0; + enb <= 0; + out_valid_reg <= 0; + accum_pipe_valid <= 0; + accum_pipe_bypass_valid <= 0; + accum_done <= 0; + if (valid_data) begin + // mem_wea/mem_addra/mem_dina do the actual write in this clock data_bram_in <= data; addra <= cnt_addr; wea <= 1; - cnt_addr <= cnt_addr + 1; - cnt_smp_num <= cnt_smp_num + WINDOW_SIZE; - end - if (cnt_smp_num >= smp_num_reg) begin - wr_state <= BEGIN_SEQ; + if (cnt_smp_num + WINDOW_SIZE >= smp_num_reg) begin + cnt_smp_num <= '0; + cnt_addr <= '0; + + if (seq_num_reg <= 16'd1) begin + cnt_seq_num <= '0; + addrb <= '0; + wr_state <= READOUT_START; + end else begin + // start further accumulation + cnt_seq_num <= 16'd1; + wr_state <= ACCUM; + end + end else begin + cnt_smp_num <= cnt_smp_num + WINDOW_SIZE; + cnt_addr <= cnt_addr + 1; + end end end + BEGIN_SEQ: begin - // start new acc seq + // FIXME: unused wea <= 0; enb <= 0; - if (cnt_seq_num == seq_num_reg - 1) begin - cnt_seq_num <= '0; - cnt_smp_num <= '0; - cnt_addr <= '0; - wr_state <= READOUT_START; - addrb <= '0; - enb <= 0; - end else begin - // beginning of new data sequence - cnt_seq_num <= cnt_seq_num + 1; - cnt_smp_num <= '0; - cnt_addr <= '0; - wea <= 0; - addrb <= 0; - wr_state <= REQ_WORD_B; - end + wr_state <= ACCUM; end REQ_WORD_B: begin - // pre-request data for port b + // FIXME: depr wea <= 0; - enb <= 1; - addrb <= cnt_addr; + enb <= 0; wr_state <= ACCUM; end ACCUM: begin - // sum mem+input + // accum pipeline + wea <= 0; enb <= 0; - if (valid_data) begin - addra <= cnt_addr; + out_valid_reg <= 0; + + if (accum_pipe_valid) begin + // mem_wea/mem_addra/mem_dina do the actual write this clock + addra <= accum_pipe_addr; + data_bram_in <= accum_write_value; wea <= 1; - data_bram_in <= data + data_bram_out; - cnt_smp_num <= cnt_smp_num + WINDOW_SIZE; - if (cnt_smp_num + WINDOW_SIZE >= smp_num_reg) begin - wr_state <= BEGIN_SEQ; + end + + if (accum_done) begin + // Last input word was accepted on the previous clk + accum_pipe_valid <= 0; + accum_pipe_bypass_valid <= 0; + cnt_smp_num <= '0; + cnt_seq_num <= '0; + cnt_addr <= '0; + addrb <= '0; + enb <= 0; + wr_state <= READOUT_START; + end else if (valid_data) begin + // mem_enb/mem_addrb launch the actual read this clock + enb <= 1; + addrb <= cnt_addr; + + accum_pipe_valid <= 1; + accum_pipe_addr <= cnt_addr; + accum_pipe_data <= data; + + // case window_size=1 && smp_num is small + accum_pipe_bypass_valid <= accum_pipe_valid && (accum_pipe_addr == cnt_addr); + accum_pipe_bypass_data <= accum_write_value; + + if (accum_accept_last) begin + cnt_smp_num <= '0; + cnt_addr <= '0; + + if (cnt_seq_num == seq_num_reg - 1) begin + accum_done <= 1; + end else begin + cnt_seq_num <= cnt_seq_num + 1; + end end else begin + cnt_smp_num <= cnt_smp_num + WINDOW_SIZE; cnt_addr <= cnt_addr + 1; - wr_state <= REQ_WORD_B; end + end else begin + accum_pipe_valid <= 0; + accum_pipe_bypass_valid <= 0; end end + READOUT_START: begin readout_begin_reg <= 1'b1; wr_state <= READOUT_AWAIT; enb <= 0; + wea <= 0; end READOUT_AWAIT: begin - // req await + delay for every-clock readout. + // req await + delay for every-clock readout + wea <= 0; if (batch_req) begin enb <= 1; wr_state <= READOUT_DELAY; @@ -220,12 +285,14 @@ module accumulator READOUT_DELAY: begin // wait for mem latency + wea <= 0; addrb <= addrb + 1; wr_state <= READOUT_PUT; end READOUT_PUT: begin // main data output + wea <= 0; if ((addrb % READ_BATCH_SIZE) == 0) begin wr_state <= READOUT_LAST; enb <= 0; @@ -237,6 +304,7 @@ module accumulator READOUT_LAST: begin // last word of packet + wea <= 0; out_valid_reg <= 0; out_data_reg <= data_bram_out; wr_state <= READOUT_START; @@ -245,6 +313,7 @@ module accumulator FINISH: begin out_valid_reg <= 0; enb <= 0; + wea <= 0; wr_state <= IDLE; end @@ -301,14 +370,14 @@ module accumulator .doutb(data_bram_out), - .addra(addra), - .addrb(addrb), + .addra(mem_addra), + .addrb(mem_addrb), .clka(clk_in), .clkb(clk_in), - .dina(data_bram_in), + .dina(mem_dina), .ena(1'b1), - .enb(enb), - .wea(wea) + .enb(mem_enb), + .wea(mem_wea) ); assign readout_begin = readout_begin_reg; From 2a3796a60f762d51e41417a6c22beba500010122 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 1 Jul 2026 14:57:23 +0300 Subject: [PATCH 10/19] rtl: update accum - change window size to signal --- rtl/accum/src/accum.sv | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/rtl/accum/src/accum.sv b/rtl/accum/src/accum.sv index b7ef552..b09b838 100644 --- a/rtl/accum/src/accum.sv +++ b/rtl/accum/src/accum.sv @@ -5,7 +5,6 @@ module accumulator parameter DATA_WIDTH = 12, parameter ACCUM_WIDTH = 32, parameter N_MAX = 4096, - parameter WINDOW_SIZE = 4, parameter PACKET_SIZE = 8, parameter READ_BATCH_SIZE =(PACKET_SIZE*8)/(ACCUM_WIDTH) ) @@ -17,6 +16,7 @@ module accumulator input start, input [31:0] smp_num, input [15:0] seq_num, + input [31:0] window_size, output [ACCUM_WIDTH-1:0] out_data, output out_valid, @@ -26,6 +26,7 @@ module accumulator ); logic [31:0] smp_num_reg, cnt_smp_num; + logic [31:0] window_size_reg; logic [15:0] seq_num_reg, cnt_seq_num; logic [15:0] cnt_addr, addra, addrb; @@ -61,7 +62,7 @@ module accumulator logic [15:0] accum_pipe_addr; logic [ACCUM_WIDTH-1:0] accum_pipe_data; - // case smp_num // WINDOW_SIZE == 1 + // case smp_num // window_size == 1 // Then the next sequence can read the same address it is written logic accum_pipe_bypass_valid; logic [ACCUM_WIDTH-1:0] accum_pipe_bypass_data; @@ -72,7 +73,10 @@ module accumulator logic [ACCUM_WIDTH-1:0] accum_write_base; logic [ACCUM_WIDTH-1:0] accum_write_value; - assign accum_accept_last = (cnt_smp_num + WINDOW_SIZE >= smp_num_reg); + wire [31:0] window_size_safe = (window_size == 32'd0) ? 32'd1 : window_size; + wire start_accept = start && (wr_state == IDLE); + + assign accum_accept_last = (cnt_smp_num + window_size_reg >= smp_num_reg); assign accum_accept_last_all = accum_accept_last && (cnt_seq_num == seq_num_reg - 1); assign accum_write_base = accum_pipe_bypass_valid ? accum_pipe_bypass_data : data_bram_out; assign accum_write_value = accum_pipe_data + accum_write_base; @@ -109,6 +113,7 @@ module accumulator if (rst) begin smp_num_reg <= '0; cnt_smp_num <= '0; + window_size_reg <= 32'd1; seq_num_reg <= '0; cnt_seq_num <= '0; cnt_addr <= '0; @@ -152,6 +157,7 @@ module accumulator if (start) begin smp_num_reg <= smp_num; seq_num_reg <= seq_num; + window_size_reg <= window_size_safe; wr_state <= INIT_MEM; end @@ -172,7 +178,7 @@ module accumulator addra <= cnt_addr; wea <= 1; - if (cnt_smp_num + WINDOW_SIZE >= smp_num_reg) begin + if (cnt_smp_num + window_size_reg >= smp_num_reg) begin cnt_smp_num <= '0; cnt_addr <= '0; @@ -186,7 +192,7 @@ module accumulator wr_state <= ACCUM; end end else begin - cnt_smp_num <= cnt_smp_num + WINDOW_SIZE; + cnt_smp_num <= cnt_smp_num + window_size_reg; cnt_addr <= cnt_addr + 1; end end @@ -253,7 +259,7 @@ module accumulator cnt_seq_num <= cnt_seq_num + 1; end end else begin - cnt_smp_num <= cnt_smp_num + WINDOW_SIZE; + cnt_smp_num <= cnt_smp_num + window_size_reg; cnt_addr <= cnt_addr + 1; end end else begin @@ -326,12 +332,13 @@ module accumulator adder #( .DATA_WIDTH(DATA_WIDTH), - .WINDOW_SIZE(WINDOW_SIZE), .ACCUM_WIDTH(ACCUM_WIDTH) ) adder_dut ( .clk_in(clk_in), .rst(rst), + .start(start_accept), + .window_size(window_size), .s_axis_tdata(s_axis_tdata), .s_axis_tvalid(s_axis_tvalid), .sum_data(data), From 46333c3601e58d0183b437db80b7e0fba60861a1 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 1 Jul 2026 18:06:56 +0300 Subject: [PATCH 11/19] rtl: rework adder for dynamic window_size --- rtl/accum/src/adder.sv | 43 +++++++++++++++++++++++++++++------------- 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/rtl/accum/src/adder.sv b/rtl/accum/src/adder.sv index dd1c3c9..f457024 100644 --- a/rtl/accum/src/adder.sv +++ b/rtl/accum/src/adder.sv @@ -4,12 +4,13 @@ module adder #( parameter DATA_WIDTH = 12, - parameter WINDOW_SIZE = 4, parameter ACCUM_WIDTH = 32 ) ( input clk_in, input rst, + input start, + input [31:0] window_size, input [DATA_WIDTH-1:0] s_axis_tdata, input s_axis_tvalid, @@ -20,7 +21,10 @@ module adder logic [ACCUM_WIDTH-1:0] accum, res; logic [DATA_WIDTH-1:0] axis_data; logic res_valid, axis_valid; - (* MARK_DEBUG = "TRUE" *) logic [15:0] cnt; + (* MARK_DEBUG = "TRUE" *) logic [31:0] cnt; + logic [31:0] window_size_reg; + + wire [31:0] window_size_safe = (window_size == 32'd0) ? 32'd1 : window_size; always @(posedge clk_in) begin if (rst) begin @@ -28,19 +32,32 @@ module adder cnt <= '0; res <= '0; res_valid <= 0; + axis_data <= '0; + axis_valid <= 0; + window_size_reg <= 32'd1; end else begin res_valid <= 0; - axis_data <= s_axis_tdata; - axis_valid <= s_axis_tvalid; - if ( axis_valid) begin - if (cnt == WINDOW_SIZE-1) begin - res <= accum + axis_data; - res_valid <= 1; - accum <= '0; - cnt <= '0; - end else begin - accum <= accum + axis_data; - cnt <= cnt + 1; + + if (start) begin + accum <= '0; + cnt <= '0; + res <= '0; + axis_data <= '0; + axis_valid <= 0; + window_size_reg <= window_size_safe; + end else begin + axis_data <= s_axis_tdata; + axis_valid <= s_axis_tvalid; + if (axis_valid) begin + if (cnt == window_size_reg - 1) begin + res <= accum + axis_data; + res_valid <= 1; + accum <= '0; + cnt <= '0; + end else begin + accum <= accum + axis_data; + cnt <= cnt + 1; + end end end end From f0542845b0759cfc780d5b7a7569c1db8823fc30 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 1 Jul 2026 18:21:11 +0300 Subject: [PATCH 12/19] rtl: rework out_axis_fifo --- rtl/accum/src/out_axis_fifo.sv | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rtl/accum/src/out_axis_fifo.sv b/rtl/accum/src/out_axis_fifo.sv index 0deb4f5..6ee3322 100644 --- a/rtl/accum/src/out_axis_fifo.sv +++ b/rtl/accum/src/out_axis_fifo.sv @@ -1,12 +1,12 @@ module out_axis_fifo #( parameter ACCUM_WIDTH = 32, - parameter WINDOW_SIZE = 65, parameter PACKET_SIZE = 1024 ) ( input logic eth_clk_in, input logic acc_clk_in, input logic rst, input logic [31:0] smp_num, + input logic [31:0] window_size, // AXI stream master for output, eth_clk_in domain output logic [7:0] m_axis_tdata, @@ -85,6 +85,8 @@ module out_axis_fifo #( reg [31:0] wr_cnt; // current BIT mem ptr reg [31:0] wr_batch_tgt; // next 'target' that should be written from batch reg [31:0] wr_total; // total BITS to be sent! + logic [31:0] window_size_reg; + wire [31:0] window_size_safe = (window_size == 32'd0) ? 32'd1 : window_size; wire empty; @@ -92,7 +94,7 @@ module out_axis_fifo #( // NOTE: // each written "acc_din" ACCUM_WIDTH word - // is counted as WINDOWS_SIZE samples actually + // is counted as window_size samples actually // because hw division for counters is painful // so we just increased the counter sizes @@ -102,6 +104,7 @@ module out_axis_fifo #( wr_cnt <= 32'b0; wr_batch_tgt <= 32'b0; wr_total <= 32'b0; + window_size_reg <= 32'd1; batch_req <= 0; finish <= 0; @@ -115,6 +118,7 @@ module out_axis_fifo #( wr_state <= WR_CHECK; wr_total <= smp_num * ACCUM_WIDTH; wr_batch_tgt <= 32'b0; + window_size_reg <= window_size_safe; batch_req <= 0; finish <= 0; end @@ -126,9 +130,9 @@ module out_axis_fifo #( if ((wr_data_count < (FIFO_WDEPTH - (PACKET_SIZE / (ACCUM_WIDTH / 8)))) && ~wr_rst_busy) begin batch_req <= 1; // should give us exactly PACKET_SIZE * 8 bits - // multiplied by WINDOW_SIZE, because we count - // each given ACCUM_WIDTH word as WINDOWS_SIZE samples !!! - wr_batch_tgt <= wr_batch_tgt + (8 * WINDOW_SIZE * PACKET_SIZE); + // multiplied by window_size, because we count + // each given ACCUM_WIDTH word as window_size samples !!! + wr_batch_tgt <= wr_batch_tgt + (8 * window_size_reg * PACKET_SIZE); wr_state <= WR_RUN; end else begin batch_req <= 0; @@ -150,8 +154,8 @@ module out_axis_fifo #( if (din_valid) begin // data supplied - // count as we got WINDOW_SIZE samples - wr_cnt <= wr_cnt + ACCUM_WIDTH * WINDOW_SIZE; + // count as we got window_size samples + wr_cnt <= wr_cnt + ACCUM_WIDTH * window_size_reg; end end From 5010c42ae3a76a0f7fdb5125f8c887c98c9912dc Mon Sep 17 00:00:00 2001 From: Phil Date: Thu, 2 Jul 2026 11:33:40 +0300 Subject: [PATCH 13/19] rtl: update accum_top to use dynamic window_size --- rtl/accum/src/accum_top.sv | 44 ++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/rtl/accum/src/accum_top.sv b/rtl/accum/src/accum_top.sv index 3ae2f62..359ce15 100644 --- a/rtl/accum/src/accum_top.sv +++ b/rtl/accum/src/accum_top.sv @@ -5,7 +5,6 @@ module accumulator_top parameter DATA_WIDTH = 12, parameter ACCUM_WIDTH = 32, parameter N_MAX = 4096, - parameter WINDOW_SIZE = 65, parameter PACKET_SIZE = 1024, parameter READ_BATCH_SIZE =(PACKET_SIZE*8)/(ACCUM_WIDTH) ) @@ -22,6 +21,7 @@ module accumulator_top input start, input [31:0] smp_num, input [15:0] seq_num, + input [31:0] window_size, // eth signals input eth_clk_in, @@ -42,36 +42,66 @@ module accumulator_top wire readout_begin; wire batch_req; + logic finish_int; + logic finish_int_d; + logic finish_pulse; + logic calc_active; + logic start_accept; + logic [31:0] window_size_reg; + + wire [31:0] window_size_safe = (window_size == 32'd0) ? 32'd1 : window_size; + + assign finish_pulse = finish_int && !finish_int_d; + assign start_accept = start && !calc_active; + assign finish = finish_int; + + // Keep the top copy stable for blocks that start later than the accum itself + always_ff @(posedge clk_in) begin + if (rst) begin + calc_active <= 1'b0; + finish_int_d <= 1'b0; + window_size_reg <= 32'd1; + end else begin + finish_int_d <= finish_int; + + if (start_accept) begin + calc_active <= 1'b1; + end else if (finish_pulse) begin + calc_active <= 1'b0; + end + end + end + accumulator #( .DATA_WIDTH(DATA_WIDTH), .ACCUM_WIDTH(ACCUM_WIDTH), .N_MAX(N_MAX), - .WINDOW_SIZE(WINDOW_SIZE), .PACKET_SIZE(PACKET_SIZE) ) accum_main ( .clk_in(clk_in), .rst(rst), .s_axis_tdata(s_axis_tdata), .s_axis_tvalid(s_axis_tvalid), - .start(start), + .start(start_accept), .smp_num(smp_num), .seq_num(seq_num), + .window_size(window_size), .out_data(out_data), .out_valid(out_valid), .readout_begin(readout_begin), .batch_req(batch_req), - .finish(finish) + .finish(finish_int) ); out_axis_fifo #( .ACCUM_WIDTH(ACCUM_WIDTH), - .WINDOW_SIZE(WINDOW_SIZE), .PACKET_SIZE(PACKET_SIZE) ) output_async_fifo ( .eth_clk_in (eth_clk_in), .acc_clk_in (clk_in), .rst (rst), .smp_num (smp_num), + .window_size (window_size_reg), .m_axis_tdata (m_axis_tdata), .m_axis_tvalid (m_axis_tvalid), @@ -87,6 +117,6 @@ module accumulator_top .send_req (send_req), .batch_req (batch_req), - .finish (finish) + .finish (finish_int) ); -endmodule \ No newline at end of file +endmodule From 77526c44c20e6bc17b77af59ee3bc51240a8ef38 Mon Sep 17 00:00:00 2001 From: Phil Date: Thu, 2 Jul 2026 11:49:31 +0300 Subject: [PATCH 14/19] tests: update accum TB --- rtl/accum/tests/accum_full_tb.sv | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/rtl/accum/tests/accum_full_tb.sv b/rtl/accum/tests/accum_full_tb.sv index f736609..f173559 100644 --- a/rtl/accum/tests/accum_full_tb.sv +++ b/rtl/accum/tests/accum_full_tb.sv @@ -20,6 +20,7 @@ module tb_accumulator_top; logic start; logic [31:0] smp_num; logic [15:0] seq_num; + logic [31:0] window_size; logic req_ready; wire send_req; @@ -60,6 +61,7 @@ module tb_accumulator_top; .start(start), .smp_num(smp_num), .seq_num(seq_num), + .window_size(window_size), .eth_clk_in(eth_clk_in), .req_ready(req_ready), .send_req(send_req), @@ -104,6 +106,7 @@ module tb_accumulator_top; s_axis_tvalid = 1'b0; smp_num = '0; seq_num = '0; + window_size = 32'd1; req_ready = 1'b0; m_axis_tready = 1'b1; clear_scoreboard(); @@ -141,13 +144,14 @@ module tb_accumulator_top; task automatic run_test( input integer test_id, + input integer window_size_i, input integer seq_num_i, input integer smp_num_i, input bit randomize_data, input integer base_value, input string test_name ); - logic [DATA_WIDTH-1:0] sample_mem [0:MAX_SEQ_NUM-1][0:(N_MAX*WINDOW_SIZE)-1]; + logic [DATA_WIDTH-1:0] sample_mem [0:MAX_SEQ_NUM-1][0:(N_MAX*MAX_WINDOW_SIZE)-1]; integer seq_idx; integer sample_idx; integer word_idx; @@ -165,22 +169,25 @@ module tb_accumulator_top; tests_total = tests_total + 1; errors_before = total_errors; - if (smp_num_i <= 0 || smp_num_i > N_MAX * WINDOW_SIZE || (smp_num_i % WINDOW_SIZE) != 0) - $fatal(1, "[%0s] invalid smp_num=%0d", test_name, smp_num_i); + if (window_size_i <= 0 || window_size_i > MAX_WINDOW_SIZE) + $fatal(1, "[%0s] invalid window_size=%0d", test_name, window_size_i); + if (smp_num_i <= 0 || smp_num_i > N_MAX * window_size_i || (smp_num_i % window_size_i) != 0) + $fatal(1, "[%0s] invalid smp_num=%0d for window_size=%0d", test_name, smp_num_i, window_size_i); if (seq_num_i <= 0 || seq_num_i > MAX_SEQ_NUM) $fatal(1, "[%0s] invalid seq_num=%0d", test_name, seq_num_i); $display("\n========================================"); $display("TEST %0d: %0s", test_id, test_name); - $display("seq_num=%0d smp_num=%0d randomize=%0d", seq_num_i, smp_num_i, randomize_data); + $display("window_size=%0d seq_num=%0d smp_num=%0d randomize=%0d", window_size_i, seq_num_i, smp_num_i, randomize_data); $display("========================================"); reset_dut(); - smp_num = smp_num_i; - seq_num = seq_num_i; - req_ready = 1'b1; // приемник готов заранее + smp_num = smp_num_i; + seq_num = seq_num_i; + window_size = window_size_i; + req_ready = 1'b1; // приемник готов заранее - exp_word_count = smp_num_i / WINDOW_SIZE; + exp_word_count = smp_num_i / window_size_i; exp_packet_count = (exp_word_count + READ_BATCH_SIZE - 1) / READ_BATCH_SIZE; for (seq_idx = 0; seq_idx < seq_num_i; seq_idx = seq_idx + 1) begin @@ -196,8 +203,8 @@ module tb_accumulator_top; for (word_idx = 0; word_idx < exp_word_count; word_idx = word_idx + 1) begin local_sum = 0; for (seq_idx = 0; seq_idx < seq_num_i; seq_idx = seq_idx + 1) begin - for (k = 0; k < WINDOW_SIZE; k = k + 1) - local_sum = local_sum + sample_mem[seq_idx][word_idx * WINDOW_SIZE + k]; + for (k = 0; k < window_size_i; k = k + 1) + local_sum = local_sum + sample_mem[seq_idx][word_idx * window_size_i + k]; end expected_words[word_idx] = local_sum[ACCUM_WIDTH-1:0]; $display(" expected[%0d] = %0d (0x%08x)", word_idx, expected_words[word_idx], expected_words[word_idx]); From df5469d538f1f13b4ce8ef4038f2268751a3341c Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 3 Jul 2026 18:03:04 +0300 Subject: [PATCH 15/19] tests: add new tests for accum --- rtl/accum/tests/accum_full_tb.sv | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/rtl/accum/tests/accum_full_tb.sv b/rtl/accum/tests/accum_full_tb.sv index f173559..cf74d81 100644 --- a/rtl/accum/tests/accum_full_tb.sv +++ b/rtl/accum/tests/accum_full_tb.sv @@ -5,7 +5,7 @@ module tb_accumulator_top; localparam DATA_WIDTH = 12; localparam ACCUM_WIDTH = 32; localparam N_MAX = 4096; - localparam WINDOW_SIZE = 65; + localparam MAX_WINDOW_SIZE = 65; localparam PACKET_SIZE = 1024; localparam READ_BATCH_SIZE = (PACKET_SIZE*8)/ACCUM_WIDTH; localparam MAX_WORDS = N_MAX; @@ -51,7 +51,6 @@ module tb_accumulator_top; .DATA_WIDTH(DATA_WIDTH), .ACCUM_WIDTH(ACCUM_WIDTH), .N_MAX(N_MAX), - .WINDOW_SIZE(WINDOW_SIZE), .PACKET_SIZE(PACKET_SIZE) ) dut ( .clk_in(clk_in), @@ -325,16 +324,17 @@ module tb_accumulator_top; reset_dut(); - run_test(1, 1, 1 * WINDOW_SIZE, 1'b0, 1, "deterministic_small"); -// $finish; - run_test(2, 2, 1 * WINDOW_SIZE, 1'b1, 0, "random_seq3_smp8"); - run_test(3, 1, 16 * WINDOW_SIZE, 1'b1, 0, "random_seq5_smp16_multi_packet"); - run_test(4, 2, 12 * WINDOW_SIZE, 1'b1, 0, "random_seq7_smp12"); - run_test(5, 4, 256 * WINDOW_SIZE, 1'b1, 0, "random_max_smpnum"); - run_test(6, 2, 1500 * WINDOW_SIZE, 1'b1, 0, "random_max_smpnum2"); - run_test(7, 20, 1 * WINDOW_SIZE, 1'b1, 0, "random_20seq"); - run_test(8, 20, 3 * WINDOW_SIZE, 1'b1, 0, "random_20seqx3"); - run_test(9, 200, 1 * WINDOW_SIZE, 1'b1, 0, "random_200seq"); + run_test(1, 1, 1, 1 * 1, 1'b0, 1, "w1_deterministic_small"); + run_test(2, 1, 2, 16 * 1, 1'b1, 0, "w1_random_seq2_smp16"); + run_test(3, 2, 2, 16 * 2, 1'b1, 0, "w2_random_seq2_smp32"); + run_test(4, 3, 1, 16 * 3, 1'b1, 0, "w3_random_seq1_smp48"); + run_test(5, 4, 2, 12 * 4, 1'b1, 0, "w4_random_seq2_smp48"); + run_test(6, 65, 4, 256 * 65, 1'b1, 0, "w65_random_seq4_smp16640"); + run_test(7, 2, 20, 3 * 2, 1'b1, 0, "w2_random_20seqx3"); + run_test(8, 65, 20, 3 * 65, 1'b1, 0, "w65_random_20seqx3"); + run_test(9, 1, 200, 1 * 1, 1'b1, 0, "w1_random_200seq"); + run_test(10, 2, 200, 2 * 1, 1'b1, 0, "w2_random_200seq"); + run_test(11, 65, 200, 65 * 1, 1'b1, 0, "w1_random_200seq"); $display("\n========================================"); $display("ALL TESTS COMPLETED"); From 8e5b3929ac5bfd22cdf1ed30f306788ba677912a Mon Sep 17 00:00:00 2001 From: Phil Date: Fri, 3 Jul 2026 18:35:58 +0300 Subject: [PATCH 16/19] fix: missing state update --- rtl/accum/src/accum_top.sv | 1 + 1 file changed, 1 insertion(+) diff --git a/rtl/accum/src/accum_top.sv b/rtl/accum/src/accum_top.sv index 359ce15..dc788a1 100644 --- a/rtl/accum/src/accum_top.sv +++ b/rtl/accum/src/accum_top.sv @@ -66,6 +66,7 @@ module accumulator_top if (start_accept) begin calc_active <= 1'b1; + window_size_reg <= window_size_safe; end else if (finish_pulse) begin calc_active <= 1'b0; end From 177c7d0bc2c8adf07abc1a002e9bf8b93b124fb9 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 7 Jul 2026 17:45:32 +0300 Subject: [PATCH 17/19] infra: update makefile --- rtl/accum/tests/Makefile | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rtl/accum/tests/Makefile b/rtl/accum/tests/Makefile index 45f5626..e6d2005 100644 --- a/rtl/accum/tests/Makefile +++ b/rtl/accum/tests/Makefile @@ -13,20 +13,24 @@ FPGA_ARCH = artix7 RTL_DIR = ../src +# Simulation settings +SIM_TOP = tb_accumulator_top +SIM_RUNTIME ?= 10000 us -include ../../../scripts/vivado.mk +# Design sources only +SYN_FILES += $(filter-out %_tb.sv,$(sort $(shell find $(RTL_DIR) -type f -name '*.v' -o -type f -name '*.sv'))) -SYN_FILES += $(sort $(shell find ../src -type f \( -name '*.v' -o -name '*.sv' \))) +# Testbench sources. Vivado puts these into "sim_1", Questa compiles them into "work". +TB_FILES += out_axis_fifo_tb.sv +TB_FILES += accum_full_tb.sv -XCI_FILES = $(sort $(shell find ../src -type f -name '*.xci')) +XCI_FILES = $(sort $(shell find $(RTL_DIR) -type f -name '*.xci')) XDC_FILES += ../../../constraints/ax7a035b.xdc XDC_FILES += test_timing.xdc -SYN_FILES += out_axis_fifo_tb.sv -SYN_FILES += accum_full_tb.sv -SIM_TOP = tb_accumulator_top - +include ../../../scripts/vivado.mk +include ../../../scripts/questa.mk program: $(PROJECT).bit echo "open_hw_manager" > program.tcl From 9c70029f3e690e727b1ca88cfd879761c681015b Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 7 Jul 2026 17:47:03 +0300 Subject: [PATCH 18/19] infra: add questa makefile --- scripts/questa.mk | 96 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 scripts/questa.mk diff --git a/scripts/questa.mk b/scripts/questa.mk new file mode 100644 index 0000000..59a0a1e --- /dev/null +++ b/scripts/questa.mk @@ -0,0 +1,96 @@ +# SPDX-License-Identifier: MIT +################################################################### +# Questa/ModelSim simulation helper +# +# Expected variables from the project Makefile: +# SYN_FILES - RTL sources +# TB_FILES - testbench sources +# INC_FILES - include files, optional +# SIM_TOP - simulation top module +# SIM_DEFS - defines, optional +# SIM_RUNTIME - run time, for example "10000 us" or "-all" +# +# Useful overrides: +# make sim-questa XILINX_VIVADO=/opt/Xilinx/Vivado/2024.2 +# make sim-questa-gui +################################################################### + +.PHONY: sim-questa sim-questa-gui questa-clean questa-xpm-clean + +VLIB ?= vlib +VMAP ?= vmap +VLOG ?= vlog +VSIM ?= vsim + +QUESTA_WORK_LIB ?= work +QUESTA_XPM_LIB ?= xpm +QUESTA_DIR ?= questa_build +QUESTA_TRANSCRIPT ?= transcript +XILINX_VIVADO=/tools/Xilinx/2025.1/Vivado +SIM_RUNTIME ?= 10000 us +QUESTA_RUN ?= run $(SIM_RUNTIME) + +# XPM is needed by this design because accum.sv uses xpm_memory_sdpram and +# out_axis_fifo.sv uses xpm_fifo_async. Point XILINX_VIVADO to your Vivado +# install if it is not already exported by settings64.sh. +QUESTA_USE_XPM ?= 1 + +QUESTA_XPM_SRC = \ + $(XILINX_VIVADO)/data/ip/xpm/xpm_cdc/hdl/xpm_cdc.sv \ + $(XILINX_VIVADO)/data/ip/xpm/xpm_memory/hdl/xpm_memory.sv \ + $(XILINX_VIVADO)/data/ip/xpm/xpm_fifo/hdl/xpm_fifo.sv + +QUESTA_GLBL_SRC = $(XILINX_VIVADO)/data/verilog/src/glbl.v + +QUESTA_DEFS = $(foreach d,$(SIM_DEFS),+define+$(d)) +QUESTA_INC_DIRS = $(sort $(dir $(SYN_FILES) $(TB_FILES) $(INC_FILES))) +QUESTA_INCS = $(foreach d,$(QUESTA_INC_DIRS),+incdir+$(d)) +QUESTA_LIBS = $(if $(filter 1,$(QUESTA_USE_XPM)),-L $(QUESTA_XPM_LIB),) +QUESTA_SOURCES = $(SYN_FILES) $(TB_FILES) $(if $(wildcard $(QUESTA_GLBL_SRC)),$(QUESTA_GLBL_SRC),) + +QUESTA_GLBL_TOP = $(if $(wildcard $(QUESTA_GLBL_SRC)),$(QUESTA_WORK_LIB).glbl,) + + +$(QUESTA_DIR): + mkdir -p $@ + +$(QUESTA_DIR)/sources.f: Makefile | $(QUESTA_DIR) + @rm -f $@ + @for inc in $(QUESTA_INCS); do echo $$inc >> $@; done + @for src in $(QUESTA_SOURCES); do echo $$src >> $@; done + +$(QUESTA_DIR)/xpm.stamp: | $(QUESTA_DIR) + @if [ "$(QUESTA_USE_XPM)" = "1" ]; then \ + if [ -z "$(XILINX_VIVADO)" ]; then \ + echo "ERROR: XILINX_VIVADO is not set. Source Vivado settings64.sh or pass XILINX_VIVADO=/path/to/Vivado/."; \ + exit 1; \ + fi; \ + for src in $(QUESTA_XPM_SRC); do \ + if [ ! -f $$src ]; then \ + echo "ERROR: XPM source not found: $$src"; \ + exit 1; \ + fi; \ + done; \ + $(VLIB) $(QUESTA_XPM_LIB); \ + $(VMAP) $(QUESTA_XPM_LIB) $(QUESTA_XPM_LIB); \ + $(VLOG) -sv -work $(QUESTA_XPM_LIB) $(QUESTA_XPM_SRC); \ + fi + @touch $@ + +$(QUESTA_DIR)/compile.stamp: $(QUESTA_DIR)/sources.f $(SYN_FILES) $(TB_FILES) $(INC_FILES) $(QUESTA_DIR)/xpm.stamp + $(VLIB) $(QUESTA_WORK_LIB) + $(VMAP) $(QUESTA_WORK_LIB) $(QUESTA_WORK_LIB) + $(VLOG) -sv -work $(QUESTA_WORK_LIB) $(QUESTA_DEFS) -timescale 1ns/1ps $(QUESTA_INCS) -f $(QUESTA_DIR)/sources.f + @touch $@ + +sim-questa: $(QUESTA_DIR)/compile.stamp + $(VSIM) -c $(QUESTA_LIBS) $(QUESTA_WORK_LIB).$(SIM_TOP) $(QUESTA_GLBL_TOP) -do "$(QUESTA_RUN); quit -f" + +sim-questa-gui: $(QUESTA_DIR)/compile.stamp + $(VSIM) $(QUESTA_LIBS) -voptargs="+acc" $(QUESTA_WORK_LIB).$(SIM_TOP) $(QUESTA_GLBL_TOP) + +questa-clean: + -rm -rf $(QUESTA_DIR) $(QUESTA_WORK_LIB) $(QUESTA_TRANSCRIPT) vsim.wlf *.wlf + +questa-xpm-clean: questa-clean + -rm -rf $(QUESTA_XPM_LIB) From 9f9902116818ade94f18228fcd928fcbd80a4144 Mon Sep 17 00:00:00 2001 From: Phil Date: Tue, 7 Jul 2026 17:47:21 +0300 Subject: [PATCH 19/19] infra: update gitignore for questa folder --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a89a8f3..fd060a8 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ .Xil xvlog.pb *vivado_pid* +**/work/* # some generated files (they annoy me) update_config.tcl