pulse modulation added

This commit is contained in:
Ayzen
2026-09-01 18:09:03 +03:00
parent 4c6292d6aa
commit 62e0430668
9 changed files with 123 additions and 16 deletions
+2
View File
@@ -172,6 +172,8 @@ AD9102_WAVE_SAMPLE_MIN = -8192
AD9102_WAVE_SAMPLE_MAX = 8191 AD9102_WAVE_SAMPLE_MAX = 8191
AD9102_WAVE_MAX_CHUNK_SAMPLES = 12 AD9102_WAVE_MAX_CHUNK_SAMPLES = 12
AD9102_CLOCK_HZ = 150_000_000 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_MIN = 0
AD9833_FREQ_WORD_MAX = 0x0FFFFFFF AD9833_FREQ_WORD_MAX = 0x0FFFFFFF
+20 -4
View File
@@ -22,6 +22,7 @@ from .constants import (
AD9102_SRAM_SAMPLE_MAX, AD9102_SRAM_SAMPLE_MAX,
AD9102_SRAM_SAMPLE_MIN, AD9102_SRAM_SAMPLE_MIN,
AD9102_WAVE_MAX_CHUNK_SAMPLES, AD9102_WAVE_MAX_CHUNK_SAMPLES,
AD9102_WAVE_PERIOD_US_MAX,
AD9102_WAVE_SAMPLE_MAX, AD9102_WAVE_SAMPLE_MAX,
AD9102_WAVE_SAMPLE_MIN, AD9102_WAVE_SAMPLE_MIN,
AD9833_FREQ_WORD_MAX, AD9833_FREQ_WORD_MAX,
@@ -533,8 +534,12 @@ class LaserController:
len(waveform_bytes), len(waveform_bytes),
) )
def upload_ad9102_waveform(self, samples: Sequence[int]) -> None: 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.""" """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: if not samples:
raise InvalidParameterError("samples", "At least two samples are required") raise InvalidParameterError("samples", "At least two samples are required")
sample_list = [self._validate_wave_sample(sample, index) for index, sample in enumerate(samples)] sample_list = [self._validate_wave_sample(sample, index) for index, sample in enumerate(samples)]
@@ -544,13 +549,24 @@ class LaserController:
"samples", "samples",
f"Sample count must be in range [{AD9102_SRAM_SAMPLE_MIN}, {AD9102_SRAM_SAMPLE_MAX}]", 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): for start in range(0, sample_count, AD9102_WAVE_MAX_CHUNK_SAMPLES):
chunk = sample_list[start:start + 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_data(chunk))
self._send_and_expect_ok(Protocol.encode_ad9102_wave_commit()) 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: def cancel_ad9102_waveform_upload(self) -> None:
"""Cancel an in-progress AD9102 custom waveform upload.""" """Cancel an in-progress AD9102 custom waveform upload."""
+20
View File
@@ -28,6 +28,7 @@ from laser_control.constants import (
AD9102_PAT_PERIOD_MIN, AD9102_PAT_PERIOD_MIN,
AD9102_SAW_STEP_MAX, AD9102_SAW_STEP_MAX,
AD9102_SAW_STEP_MIN, AD9102_SAW_STEP_MIN,
AD9102_WAVE_PERIOD_US_MAX,
AD9102_SRAM_AMPLITUDE_MAX, AD9102_SRAM_AMPLITUDE_MAX,
AD9102_SRAM_AMPLITUDE_MIN, AD9102_SRAM_AMPLITUDE_MIN,
AD9102_SRAM_HOLD_MAX, 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) owner._wave_samples_box.textChanged.connect(owner._on_wave_text_changed)
layout.addWidget(owner._wave_samples_box) 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 = QWidget()
buttons_layout = QHBoxLayout(buttons) buttons_layout = QHBoxLayout(buttons)
buttons_layout.setContentsMargins(0, 0, 0, 0) buttons_layout.setContentsMargins(0, 0, 0, 0)
+26 -3
View File
@@ -22,6 +22,7 @@ from PyQt6.QtWidgets import (
import pyqtgraph as pg import pyqtgraph as pg
from laser_control.constants import ( from laser_control.constants import (
AD9102_CLOCK_HZ,
AD9833_MCLK_HZ, AD9833_MCLK_HZ,
DEFAULT_AD9102_AMPLITUDE, DEFAULT_AD9102_AMPLITUDE,
DEFAULT_AD9102_HOLD_CYCLES, DEFAULT_AD9102_HOLD_CYCLES,
@@ -67,7 +68,7 @@ class MainWindow(QMainWindow):
request_pulse_ds1809 = pyqtSignal(bool, int, int) request_pulse_ds1809 = pyqtSignal(bool, int, int)
request_set_stm32_dac = pyqtSignal(bool, int) request_set_stm32_dac = pyqtSignal(bool, int)
request_apply_tec_modulation = pyqtSignal(bool, int, int, 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_cancel_wave = pyqtSignal()
request_save_profile = pyqtSignal(object) request_save_profile = pyqtSignal(object)
request_poll = pyqtSignal() request_poll = pyqtSignal()
@@ -359,7 +360,15 @@ class MainWindow(QMainWindow):
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
self._append_log("ERROR", str(exc)) self._append_log("ERROR", str(exc))
return 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: def _on_cancel_waveform(self) -> None:
self._dispatch_command(self.request_cancel_wave.emit) self._dispatch_command(self.request_cancel_wave.emit)
@@ -387,10 +396,23 @@ class MainWindow(QMainWindow):
return return
try: try:
count = len(self._parse_wave_samples(text)) 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: except Exception:
self._wave_info_label.setText("Отсчётов: ошибка формата") 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: def _on_reconnect(self) -> None:
self._append_log("INFO", "Reconnect requested from UI") self._append_log("INFO", "Reconnect requested from UI")
self._emit_connect_request() self._emit_connect_request()
@@ -728,6 +750,7 @@ class MainWindow(QMainWindow):
f"waveform_saw_step={self._ad9102_saw_step.value()}", f"waveform_saw_step={self._ad9102_saw_step.value()}",
f"waveform_pat_base={self._ad9102_pat_base.value()}", f"waveform_pat_base={self._ad9102_pat_base.value()}",
f"waveform_pat_period={self._ad9102_pat_period.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_sample_count={waveform_sample_count}",
f"waveform_hold_cycles={waveform_hold_cycles}", f"waveform_hold_cycles={waveform_hold_cycles}",
f"waveform_amplitude={self._ad9102_amplitude.value()}", f"waveform_amplitude={self._ad9102_amplitude.value()}",
+10 -6
View File
@@ -127,13 +127,13 @@ class ControllerWorker(QObject):
) )
) )
@pyqtSlot(object) @pyqtSlot(object, int)
def upload_ad9102_waveform(self, samples: object) -> None: def upload_ad9102_waveform(self, samples: object, pat_period_us: int) -> None:
"""Upload a custom waveform to AD9102 SRAM.""" """Upload a custom waveform to AD9102 SRAM."""
self._run_command( self._run_command(
lambda: ( lambda: (
self._ensure_connected(), 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.log_message.emit("INFO", f"Profile saved to SD: {profile_name}")
self._emit_status() 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) sample_list = list(samples)
self._controller.upload_ad9102_waveform(sample_list) self._controller.upload_ad9102_waveform(sample_list, pat_period_us)
self.log_message.emit("INFO", f"AD9102 waveform uploaded ({len(sample_list)} samples)") 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() self._emit_status()
def _cancel_ad9102_waveform_upload_impl(self) -> None: def _cancel_ad9102_waveform_upload_impl(self) -> None:
+7 -3
View File
@@ -294,13 +294,17 @@ class Protocol:
) )
@staticmethod @staticmethod
def encode_ad9102_wave_begin(sample_count: int) -> bytes: def encode_ad9102_wave_begin(sample_count: int, pat_period_us: int = 0) -> bytes:
"""Build an AD9102 custom-wave upload BEGIN packet.""" """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( return Protocol._encode_short_control(
CMD_AD9102_WAVE_CONTROL, CMD_AD9102_WAVE_CONTROL,
AD9102_WAVE_OPCODE_BEGIN, AD9102_WAVE_OPCODE_BEGIN,
_ensure_uint(sample_count, "sample_count", 0, 0xFFFF), _ensure_uint(sample_count, "sample_count", 0, 0xFFFF),
0, _ensure_uint(pat_period_us, "pat_period_us", 0, 0xFFFF),
) )
@staticmethod @staticmethod
+11
View File
@@ -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
+21
View File
@@ -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
+6
View File
@@ -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