From 62e043066890dc7372cc274d8df4e930cced0f77 Mon Sep 17 00:00:00 2001 From: Ayzen Date: Tue, 1 Sep 2026 18:09:03 +0300 Subject: [PATCH] pulse modulation added --- laser_control/constants.py | 2 ++ laser_control/controller.py | 24 ++++++++++++++++++++---- laser_control/gui/sections.py | 20 ++++++++++++++++++++ laser_control/gui/window.py | 29 ++++++++++++++++++++++++++--- laser_control/gui/worker.py | 16 ++++++++++------ laser_control/protocol.py | 10 +++++++--- waveforms/pulse_1us.txt | 11 +++++++++++ waveforms/pulse_2us.txt | 21 +++++++++++++++++++++ waveforms/pulse_500ns.txt | 6 ++++++ 9 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 waveforms/pulse_1us.txt create mode 100644 waveforms/pulse_2us.txt create mode 100644 waveforms/pulse_500ns.txt diff --git a/laser_control/constants.py b/laser_control/constants.py index f316049..c2f057c 100644 --- a/laser_control/constants.py +++ b/laser_control/constants.py @@ -172,6 +172,8 @@ AD9102_WAVE_SAMPLE_MIN = -8192 AD9102_WAVE_SAMPLE_MAX = 8191 AD9102_WAVE_MAX_CHUNK_SAMPLES = 12 AD9102_CLOCK_HZ = 150_000_000 +# Longest custom-pattern repetition period: 65535 ticks x base 15 at 150 MHz. +AD9102_WAVE_PERIOD_US_MAX = 6553 AD9833_FREQ_WORD_MIN = 0 AD9833_FREQ_WORD_MAX = 0x0FFFFFFF diff --git a/laser_control/controller.py b/laser_control/controller.py index 4b92bb7..8d93214 100644 --- a/laser_control/controller.py +++ b/laser_control/controller.py @@ -22,6 +22,7 @@ from .constants import ( AD9102_SRAM_SAMPLE_MAX, AD9102_SRAM_SAMPLE_MIN, AD9102_WAVE_MAX_CHUNK_SAMPLES, + AD9102_WAVE_PERIOD_US_MAX, AD9102_WAVE_SAMPLE_MAX, AD9102_WAVE_SAMPLE_MIN, AD9833_FREQ_WORD_MAX, @@ -533,8 +534,12 @@ class LaserController: len(waveform_bytes), ) - def upload_ad9102_waveform(self, samples: Sequence[int]) -> None: - """Upload and commit a custom AD9102 waveform from signed 14-bit samples.""" + def upload_ad9102_waveform(self, samples: Sequence[int], pat_period_us: int = 0) -> None: + """Upload and commit a custom AD9102 waveform from signed 14-bit samples. + + ``pat_period_us`` sets the pattern repetition period in microseconds; + 0 keeps the legacy back-to-back playback. + """ if not samples: raise InvalidParameterError("samples", "At least two samples are required") sample_list = [self._validate_wave_sample(sample, index) for index, sample in enumerate(samples)] @@ -544,13 +549,24 @@ class LaserController: "samples", f"Sample count must be in range [{AD9102_SRAM_SAMPLE_MIN}, {AD9102_SRAM_SAMPLE_MAX}]", ) + if not 0 <= int(pat_period_us) <= AD9102_WAVE_PERIOD_US_MAX: + raise InvalidParameterError( + "pat_period_us", + f"Repetition period must be in range [0, {AD9102_WAVE_PERIOD_US_MAX}] us", + ) - self._send_and_expect_ok(Protocol.encode_ad9102_wave_begin(sample_count)) + self._send_and_expect_ok( + Protocol.encode_ad9102_wave_begin(sample_count, int(pat_period_us)) + ) for start in range(0, sample_count, AD9102_WAVE_MAX_CHUNK_SAMPLES): chunk = sample_list[start:start + AD9102_WAVE_MAX_CHUNK_SAMPLES] self._send_and_expect_ok(Protocol.encode_ad9102_wave_data(chunk)) self._send_and_expect_ok(Protocol.encode_ad9102_wave_commit()) - logger.info("Uploaded AD9102 waveform with %d samples", sample_count) + logger.info( + "Uploaded AD9102 waveform with %d samples, period %d us", + sample_count, + int(pat_period_us), + ) def cancel_ad9102_waveform_upload(self) -> None: """Cancel an in-progress AD9102 custom waveform upload.""" diff --git a/laser_control/gui/sections.py b/laser_control/gui/sections.py index 180ad75..749f147 100644 --- a/laser_control/gui/sections.py +++ b/laser_control/gui/sections.py @@ -28,6 +28,7 @@ from laser_control.constants import ( AD9102_PAT_PERIOD_MIN, AD9102_SAW_STEP_MAX, AD9102_SAW_STEP_MIN, + AD9102_WAVE_PERIOD_US_MAX, AD9102_SRAM_AMPLITUDE_MAX, AD9102_SRAM_AMPLITUDE_MIN, AD9102_SRAM_HOLD_MAX, @@ -526,6 +527,25 @@ def _build_wave_tab(owner) -> QWidget: owner._wave_samples_box.textChanged.connect(owner._on_wave_text_changed) layout.addWidget(owner._wave_samples_box) + period_row = QWidget() + period_layout = QHBoxLayout(period_row) + period_layout.setContentsMargins(0, 0, 0, 0) + period_layout.setSpacing(8) + period_label = QLabel("Период повторения") + owner._wave_period_us = _int_spinbox(0, AD9102_WAVE_PERIOD_US_MAX, 0, suffix=" мкс") + owner._wave_period_us.setToolTip( + "Период запуска формы. Между повторами выход держит последний отсчёт формы.\n" + "0 - повтор вплотную (как раньше). Для импульса 1 мкс с частотой 1 кГц:\n" + "форма = 150 отсчётов вершины + несколько отсчётов базы, период = 1000 мкс." + ) + owner._wave_period_us.valueChanged.connect(owner._on_wave_period_changed) + owner._wave_period_info = QLabel("0 = повтор вплотную, без паузы") + owner._wave_period_info.setObjectName("captionLabel") + period_layout.addWidget(period_label) + period_layout.addWidget(owner._wave_period_us) + period_layout.addWidget(owner._wave_period_info, stretch=1) + layout.addWidget(period_row) + buttons = QWidget() buttons_layout = QHBoxLayout(buttons) buttons_layout.setContentsMargins(0, 0, 0, 0) diff --git a/laser_control/gui/window.py b/laser_control/gui/window.py index 31aa42d..3126e12 100644 --- a/laser_control/gui/window.py +++ b/laser_control/gui/window.py @@ -22,6 +22,7 @@ from PyQt6.QtWidgets import ( import pyqtgraph as pg from laser_control.constants import ( + AD9102_CLOCK_HZ, AD9833_MCLK_HZ, DEFAULT_AD9102_AMPLITUDE, DEFAULT_AD9102_HOLD_CYCLES, @@ -67,7 +68,7 @@ class MainWindow(QMainWindow): request_pulse_ds1809 = pyqtSignal(bool, int, int) request_set_stm32_dac = pyqtSignal(bool, int) request_apply_tec_modulation = pyqtSignal(bool, int, int, int) - request_upload_wave = pyqtSignal(object) + request_upload_wave = pyqtSignal(object, int) request_cancel_wave = pyqtSignal() request_save_profile = pyqtSignal(object) request_poll = pyqtSignal() @@ -359,7 +360,15 @@ class MainWindow(QMainWindow): except Exception as exc: # noqa: BLE001 self._append_log("ERROR", str(exc)) return - self._dispatch_command(lambda: self.request_upload_wave.emit(samples)) + pat_period_us = self._wave_period_us.value() + pattern_us = len(samples) * 1e6 / AD9102_CLOCK_HZ + if pat_period_us and pat_period_us <= pattern_us: + self._append_log( + "WARN", + f"Период {pat_period_us} мкс короче самой формы ({pattern_us:.2f} мкс) - " + "форма будет проигрываться вплотную", + ) + self._dispatch_command(lambda: self.request_upload_wave.emit(samples, pat_period_us)) def _on_cancel_waveform(self) -> None: self._dispatch_command(self.request_cancel_wave.emit) @@ -387,10 +396,23 @@ class MainWindow(QMainWindow): return try: count = len(self._parse_wave_samples(text)) - self._wave_info_label.setText(f"Отсчётов: {count}") + duration_us = count * 1e6 / AD9102_CLOCK_HZ + self._wave_info_label.setText( + f"Отсчётов: {count} (длительность формы {duration_us:.2f} мкс " + f"при клоке {AD9102_CLOCK_HZ / 1e6:.0f} МГц)" + ) except Exception: self._wave_info_label.setText("Отсчётов: ошибка формата") + def _on_wave_period_changed(self) -> None: + period_us = self._wave_period_us.value() + if period_us == 0: + self._wave_period_info.setText("0 = повтор вплотную, без паузы") + return + self._wave_period_info.setText( + f"= {1e6 / period_us:.1f} Гц частота повторения" + ) + def _on_reconnect(self) -> None: self._append_log("INFO", "Reconnect requested from UI") self._emit_connect_request() @@ -728,6 +750,7 @@ class MainWindow(QMainWindow): f"waveform_saw_step={self._ad9102_saw_step.value()}", f"waveform_pat_base={self._ad9102_pat_base.value()}", f"waveform_pat_period={self._ad9102_pat_period.value()}", + f"waveform_custom_period_us={self._wave_period_us.value() if custom_wave_samples else 0}", f"waveform_sample_count={waveform_sample_count}", f"waveform_hold_cycles={waveform_hold_cycles}", f"waveform_amplitude={self._ad9102_amplitude.value()}", diff --git a/laser_control/gui/worker.py b/laser_control/gui/worker.py index c6bcb0f..8e4126b 100644 --- a/laser_control/gui/worker.py +++ b/laser_control/gui/worker.py @@ -127,13 +127,13 @@ class ControllerWorker(QObject): ) ) - @pyqtSlot(object) - def upload_ad9102_waveform(self, samples: object) -> None: + @pyqtSlot(object, int) + def upload_ad9102_waveform(self, samples: object, pat_period_us: int) -> None: """Upload a custom waveform to AD9102 SRAM.""" self._run_command( lambda: ( self._ensure_connected(), - self._upload_ad9102_waveform_impl(samples), + self._upload_ad9102_waveform_impl(samples, pat_period_us), ) ) @@ -298,10 +298,14 @@ class ControllerWorker(QObject): self.log_message.emit("INFO", f"Profile saved to SD: {profile_name}") self._emit_status() - def _upload_ad9102_waveform_impl(self, samples: object) -> None: + def _upload_ad9102_waveform_impl(self, samples: object, pat_period_us: int) -> None: sample_list = list(samples) - self._controller.upload_ad9102_waveform(sample_list) - self.log_message.emit("INFO", f"AD9102 waveform uploaded ({len(sample_list)} samples)") + self._controller.upload_ad9102_waveform(sample_list, pat_period_us) + period_note = f", период {pat_period_us} мкс" if pat_period_us else "" + self.log_message.emit( + "INFO", + f"AD9102 waveform uploaded ({len(sample_list)} samples{period_note})", + ) self._emit_status() def _cancel_ad9102_waveform_upload_impl(self) -> None: diff --git a/laser_control/protocol.py b/laser_control/protocol.py index 40e2b8a..1211177 100644 --- a/laser_control/protocol.py +++ b/laser_control/protocol.py @@ -294,13 +294,17 @@ class Protocol: ) @staticmethod - def encode_ad9102_wave_begin(sample_count: int) -> bytes: - """Build an AD9102 custom-wave upload BEGIN packet.""" + def encode_ad9102_wave_begin(sample_count: int, pat_period_us: int = 0) -> bytes: + """Build an AD9102 custom-wave upload BEGIN packet. + + ``pat_period_us`` is the pattern repetition period in microseconds; + 0 keeps the legacy back-to-back playback. + """ return Protocol._encode_short_control( CMD_AD9102_WAVE_CONTROL, AD9102_WAVE_OPCODE_BEGIN, _ensure_uint(sample_count, "sample_count", 0, 0xFFFF), - 0, + _ensure_uint(pat_period_us, "pat_period_us", 0, 0xFFFF), ) @staticmethod diff --git a/waveforms/pulse_1us.txt b/waveforms/pulse_1us.txt new file mode 100644 index 0000000..8c4a6c1 --- /dev/null +++ b/waveforms/pulse_1us.txt @@ -0,0 +1,11 @@ +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +-8192 -8192 -8192 -8192 -8192 -8192 -8192 -8192 diff --git a/waveforms/pulse_2us.txt b/waveforms/pulse_2us.txt new file mode 100644 index 0000000..1c7c3ed --- /dev/null +++ b/waveforms/pulse_2us.txt @@ -0,0 +1,21 @@ +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +-8192 -8192 -8192 -8192 -8192 -8192 -8192 -8192 diff --git a/waveforms/pulse_500ns.txt b/waveforms/pulse_500ns.txt new file mode 100644 index 0000000..f59fef2 --- /dev/null +++ b/waveforms/pulse_500ns.txt @@ -0,0 +1,6 @@ +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 8191 +-8192 -8192 -8192 -8192 -8192 -8192 -8192 -8192