From 8acdce99ea5622bb17baed1fd42bed991eec7ff9 Mon Sep 17 00:00:00 2001 From: "Mark A. Tsuchida" Date: Fri, 3 Apr 2026 15:14:52 -0500 Subject: [PATCH 1/3] Separate undershoot and scan phase adjustment These are two separate adjustments (time for scanner to settle into linear motion vs compensation for command-scanner lag), and it works better to have individual adjustments. Also decouple the scan waveform sample frequency from the pixel rate, and make the retrace duration scale with amplitude. These refactorings were easier to do in one step. Clock output, for now, uses aoRateHz. In the future it might make sense for it to follow detector acquisition instead. The default values of the new parameters preserve behavior for 256x256, 200kHz, zoom=1. Remove DumpWaveform rather than maintain it, because we now have WaveformTool and waveform_viewer.py (which are updated here). (Assisted by Claude Code; any errors are mine.) --- DumpWaveform/.gitignore | 2 - DumpWaveform/AnimateWaveform.ipynb | 166 ------------------------ DumpWaveform/DumpWaveform.c | 108 ---------------- DumpWaveform/meson.build | 15 --- WaveformTool/WaveformTool.c | 88 +++++++++---- WaveformTool/waveform_viewer.py | 100 ++++++++++++--- meson.build | 1 - src/Acquisition.c | 5 +- src/Clock.c | 38 +++--- src/DAQConfig.c | 7 +- src/DeviceImplData.c | 5 +- src/DeviceImplData.h | 8 +- src/OpenScanSettings.c | 142 ++++++++++++++++++--- src/ParkUnpark.c | 21 ++-- src/Scanner.c | 4 +- src/Waveform.c | 194 ++++++++++++++++------------- src/Waveform.h | 16 ++- 17 files changed, 443 insertions(+), 477 deletions(-) delete mode 100644 DumpWaveform/.gitignore delete mode 100644 DumpWaveform/AnimateWaveform.ipynb delete mode 100644 DumpWaveform/DumpWaveform.c delete mode 100644 DumpWaveform/meson.build diff --git a/DumpWaveform/.gitignore b/DumpWaveform/.gitignore deleted file mode 100644 index 172c0d3..0000000 --- a/DumpWaveform/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -*.raw -.ipynb_checkpoints/ diff --git a/DumpWaveform/AnimateWaveform.ipynb b/DumpWaveform/AnimateWaveform.ipynb deleted file mode 100644 index 8b0dffc..0000000 --- a/DumpWaveform/AnimateWaveform.ipynb +++ /dev/null @@ -1,166 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": null, - "id": "eaa22829", - "metadata": {}, - "outputs": [], - "source": [ - "# Animation method from\n", - "# http://louistiao.me/posts/notebooks/embedding-matplotlib-animations-in-jupyter-as-interactive-javascript-widgets/" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6d0360a7", - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "from matplotlib import animation, rc\n", - "from IPython.display import HTML" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1bc60c12", - "metadata": {}, - "outputs": [], - "source": [ - "rc(\"animation\", html=\"jshtml\")\n", - "rc(\"figure\", figsize=(8, 8))" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ed4fdb04", - "metadata": {}, - "outputs": [], - "source": [ - "wave = np.fromfile(\"WaveformTest.raw\")\n", - "wave = wave.reshape(2, -1)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "16fa531e", - "metadata": {}, - "outputs": [], - "source": [ - "plot_range = 1.0" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "04653002", - "metadata": {}, - "outputs": [], - "source": [ - "fig, ax = plt.subplots()\n", - "ax.set_xlim(-plot_range, plot_range)\n", - "ax.set_ylim(plot_range, -plot_range)\n", - "ax.set_aspect('equal', adjustable='box')\n", - "# static_line = ax.plot(wave[0], wave[1])\n", - "line, = ax.plot([], [])" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "1a0617bb", - "metadata": {}, - "outputs": [], - "source": [ - "tstep = 10\n", - "frames = wave.shape[1] // tstep\n", - "frames" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5acd8868", - "metadata": {}, - "outputs": [], - "source": [ - "def init():\n", - " line.set_data([], [])\n", - " return (line,)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "19b3d5fa", - "metadata": {}, - "outputs": [], - "source": [ - "def animate(i):\n", - " tstart = 0\n", - " tstop = min(wave.shape[1], i * tstep)\n", - " t = range(tstart, tstop)\n", - " x = wave[0, t]\n", - " y = wave[1, t]\n", - " line.set_data(x, y)\n", - " return (line,)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "6643efc0", - "metadata": {}, - "outputs": [], - "source": [ - "anim = animation.FuncAnimation(fig, animate, init_func=init,\n", - " frames=frames, interval=20, blit=True)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "3c5af8ca", - "metadata": {}, - "outputs": [], - "source": [ - "anim # This will take a while to display" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "2369d7aa", - "metadata": {}, - "outputs": [], - "source": [] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/DumpWaveform/DumpWaveform.c b/DumpWaveform/DumpWaveform.c deleted file mode 100644 index 2ab6d54..0000000 --- a/DumpWaveform/DumpWaveform.c +++ /dev/null @@ -1,108 +0,0 @@ -#include "../src/Waveform.h" - -#include -#include -#include -#include -#include - -// write results to binary file -static void DumpXYWaveform(uint32_t resolution, uint32_t undershoot) { - - struct WaveformParams params; - params.width = resolution; - params.height = resolution; - params.resolution = resolution; - params.zoom = 1; - params.undershoot = undershoot; - params.xOffset = 0; - params.yOffset = 0; - params.xformMatrix[0] = 1; - params.xformMatrix[1] = 0; - params.xformMatrix[2] = 0; - params.xformMatrix[3] = 1; - params.xformOffsetX = 0; - params.xformOffsetY = 0; - - uint32_t totalElementsPerFrame = GetScannerWaveformSize(¶ms); - - uint32_t bufferSize = totalElementsPerFrame * 2; - double *xyWaveform = malloc(sizeof(double) * bufferSize); - if (xyWaveform == NULL) { - fprintf(stderr, "Out of memory\n"); - exit(EXIT_FAILURE); - } - - GenerateGalvoWaveformFrame(¶ms, xyWaveform); - - FILE *testFile = fopen("WaveformTest.raw", "wb"); - fwrite(xyWaveform, sizeof(double), bufferSize, testFile); - fclose(testFile); - - printf("total sample count = %lu\n", (unsigned long)bufferSize); - free(xyWaveform); -} - -static void DumpClockWaveform(uint32_t resolution, uint32_t lineDelay) { - struct WaveformParams WaveformParameters; - WaveformParameters.width = resolution; - WaveformParameters.height = resolution; - WaveformParameters.undershoot = lineDelay; - WaveformParameters.xOffset = 0; - WaveformParameters.yOffset = 0; - - uint32_t elementsPerFramePerChan = - GetClockWaveformSize(&WaveformParameters); - uint32_t bufferSize = elementsPerFramePerChan; - - uint8_t *lineClockPattern = malloc(elementsPerFramePerChan); - uint8_t *lineClockFLIM = malloc(elementsPerFramePerChan); - uint8_t *frameClockFLIM = malloc(elementsPerFramePerChan); - - if (lineClockPattern == NULL || lineClockFLIM == NULL || - frameClockFLIM == NULL) { - fprintf(stderr, "Out of memory\n"); - exit(EXIT_FAILURE); - } - - GenerateLineClock(&WaveformParameters, lineClockPattern); - GenerateFLIMLineClock(&WaveformParameters, lineClockFLIM); - GenerateFLIMFrameClock(&WaveformParameters, frameClockFLIM); - - FILE *clockFile = fopen("clock_uint8_numofwaveforms_3.raw", "wb"); - fwrite(lineClockPattern, sizeof(uint8_t), bufferSize, clockFile); - fwrite(lineClockFLIM, sizeof(uint8_t), bufferSize, clockFile); - fwrite(frameClockFLIM, sizeof(uint8_t), bufferSize, clockFile); - fclose(clockFile); - - printf("total sample count = %lu\n", (unsigned long)bufferSize * 3); - free(lineClockPattern); - free(lineClockFLIM); - free(frameClockFLIM); -} - -int main(int argc, char *argv[]) { - if (argc != 4) { - fprintf( - stderr, - "3 arguments required: waveformtype, resolution, undershoot/linedelay"); - } - - else { - if (strcmp(argv[1], "XYWaveform") == 0) { - uint32_t resolution = atoi(argv[2]); - uint32_t undershoot = atoi(argv[3]); - DumpXYWaveform(resolution, undershoot); - } - - else if (strcmp(argv[1], "ClockWaveform") == 0) { - uint32_t resolution = atoi(argv[2]); - uint32_t lineDelay = atoi(argv[3]); - DumpClockWaveform(resolution, lineDelay); - } - - else { - fprintf(stderr, "Invalid Waveform Type"); - } - } -} diff --git a/DumpWaveform/meson.build b/DumpWaveform/meson.build deleted file mode 100644 index e8f7f9b..0000000 --- a/DumpWaveform/meson.build +++ /dev/null @@ -1,15 +0,0 @@ -dumpwaveform_src = [ - 'DumpWaveform.c', - '../src/Waveform.c', -] - -executable( - 'DumpWaveform', - dumpwaveform_src, - c_args: [ - '-D_CRT_SECURE_NO_WARNINGS', - ], - dependencies: [ - openscandevicelib_dep, - ], -) diff --git a/WaveformTool/WaveformTool.c b/WaveformTool/WaveformTool.c index 48a2b4c..1b4fde6 100644 --- a/WaveformTool/WaveformTool.c +++ b/WaveformTool/WaveformTool.c @@ -29,7 +29,11 @@ struct Args { uint32_t xOffset; uint32_t yOffset; double zoom; - uint32_t undershoot; + double pixelRateHz; + double aoRateHz; + double undershootUs; + double scanPhaseUs; + double retraceScaleUsPerVolt; double xformMatrix[4]; double xformOffsetX; double xformOffsetY; @@ -41,6 +45,8 @@ struct Args { int hasResolution; int hasWidth; int hasHeight; + int hasPixelRate; + int hasAORate; }; static int ParseUint32(const char *str, const char *name, uint32_t *out) { @@ -84,32 +90,39 @@ static void PrintUsage(void) { "Types: raster, clock, park, unpark\n" "\n" "Options:\n" - " -o Output file (required)\n" - " --format csv|raw Override format (auto from extension)\n" - " --resolution Scanner resolution\n" - " --width ROI width (default: resolution)\n" - " --height ROI height (default: resolution)\n" - " --xoffset ROI X offset (default: 0)\n" - " --yoffset ROI Y offset (default: 0)\n" - " --zoom Zoom factor (default: 1.0)\n" - " --undershoot Undershoot / line delay (default: 0)\n" - " --tform Affine 2x2 matrix, row-major\n" - " --tform-offset Affine translation in volts\n" - " --xpark X park position (default: 0)\n" - " --ypark Y park position (default: 0)\n" - " --prev-xpark-voltage Previous X park voltage (default: 0)\n" - " --prev-ypark-voltage Previous Y park voltage (default: 0)\n" + " -o Output file (required)\n" + " --format csv|raw Override format (auto from extension)\n" + " --resolution Scanner resolution\n" + " --width ROI width (default: resolution)\n" + " --height ROI height (default: resolution)\n" + " --xoffset ROI X offset (default: 0)\n" + " --yoffset ROI Y offset (default: 0)\n" + " --zoom Zoom factor (default: 1.0)\n" + " --pixel-rate Pixel rate in Hz (required)\n" + " --ao-rate AO sample rate in Hz (default: pixel rate)\n" + " --undershoot-us Undershoot in microseconds (default: 0)\n" + " --scan-phase-us Scan phase in microseconds (default: 0)\n" + " --retrace-scale Retrace scale in us/V (default: 640)\n" + " --tform Affine 2x2 matrix, row-major\n" + " --tform-offset Affine translation in volts\n" + " --xpark X park position (default: 0)\n" + " --ypark Y park position (default: 0)\n" + " --prev-xpark-voltage Previous X park voltage (default: 0)\n" + " --prev-ypark-voltage Previous Y park voltage (default: 0)\n" "\n" "Required parameters:\n" + " All types: --pixel-rate\n" " raster: --resolution\n" " clock: --width and --height (or --resolution)\n" " park, unpark: --resolution\n" "\n" "Examples:\n" - " WaveformTool raster -o scan.csv --resolution 256\n" - " WaveformTool raster -o scan.raw --resolution 256 --undershoot 10\n" - " WaveformTool clock -o clock.csv --resolution 64 --undershoot 5\n" - " WaveformTool park -o park.csv --resolution 256\n"); + " WaveformTool raster -o scan.csv --resolution 256 --pixel-rate 200000\n" + " WaveformTool raster -o scan.csv --resolution 256 --pixel-rate 200000" + " --ao-rate 500000 --undershoot-us 250\n" + " WaveformTool clock -o clock.csv --resolution 64 --pixel-rate 200000" + " --undershoot-us 250\n" + " WaveformTool park -o park.csv --resolution 256 --pixel-rate 200000\n"); } static int ParseTform(const char *str, double m[4]) { @@ -189,6 +202,7 @@ static int ParseTformOffset(const char *str, double *tx, double *ty) { static int ParseArgs(int argc, char *argv[], struct Args *args) { memset(args, 0, sizeof(*args)); args->zoom = 1.0; + args->retraceScaleUsPerVolt = 640.0; args->xformMatrix[0] = 1.0; args->xformMatrix[1] = 0.0; args->xformMatrix[2] = 0.0; @@ -252,8 +266,24 @@ static int ParseArgs(int argc, char *argv[], struct Args *args) { } else if (strcmp(argv[i], "--zoom") == 0 && i + 1 < argc) { if (!ParseDouble(argv[++i], "--zoom", &args->zoom)) return 0; - } else if (strcmp(argv[i], "--undershoot") == 0 && i + 1 < argc) { - if (!ParseUint32(argv[++i], "--undershoot", &args->undershoot)) + } else if (strcmp(argv[i], "--pixel-rate") == 0 && i + 1 < argc) { + if (!ParseDouble(argv[++i], "--pixel-rate", &args->pixelRateHz)) + return 0; + args->hasPixelRate = 1; + } else if (strcmp(argv[i], "--ao-rate") == 0 && i + 1 < argc) { + if (!ParseDouble(argv[++i], "--ao-rate", &args->aoRateHz)) + return 0; + args->hasAORate = 1; + } else if (strcmp(argv[i], "--undershoot-us") == 0 && i + 1 < argc) { + if (!ParseDouble(argv[++i], "--undershoot-us", + &args->undershootUs)) + return 0; + } else if (strcmp(argv[i], "--scan-phase-us") == 0 && i + 1 < argc) { + if (!ParseDouble(argv[++i], "--scan-phase-us", &args->scanPhaseUs)) + return 0; + } else if (strcmp(argv[i], "--retrace-scale") == 0 && i + 1 < argc) { + if (!ParseDouble(argv[++i], "--retrace-scale", + &args->retraceScaleUsPerVolt)) return 0; } else if (strcmp(argv[i], "--tform") == 0 && i + 1 < argc) { if (!ParseTform(argv[++i], args->xformMatrix)) @@ -289,6 +319,14 @@ static int ParseArgs(int argc, char *argv[], struct Args *args) { return 0; } + if (!args->hasPixelRate) { + fprintf(stderr, "Error: --pixel-rate is required\n"); + return 0; + } + + if (!args->hasAORate) + args->aoRateHz = args->pixelRateHz; + switch (args->type) { case WAVEFORM_RASTER: case WAVEFORM_PARK: @@ -351,7 +389,11 @@ static void PopulateParams(const struct Args *args, params->height = args->height; params->resolution = args->resolution; params->zoom = args->zoom; - params->undershoot = args->undershoot; + params->pixelRateHz = args->pixelRateHz; + params->aoRateHz = args->aoRateHz; + params->undershootUs = args->undershootUs; + params->scanPhaseUs = args->scanPhaseUs; + params->retraceScaleUsPerVolt = args->retraceScaleUsPerVolt; params->xOffset = args->xOffset; params->yOffset = args->yOffset; memcpy(params->xformMatrix, args->xformMatrix, diff --git a/WaveformTool/waveform_viewer.py b/WaveformTool/waveform_viewer.py index f1d147a..ee45ea2 100644 --- a/WaveformTool/waveform_viewer.py +++ b/WaveformTool/waveform_viewer.py @@ -40,6 +40,22 @@ def run_tool(args: list[str], tmp: Path) -> subprocess.CompletedProcess: return subprocess.run(cmd, capture_output=True, text=True) +def timing_args( + pixel_rate: int, + ao_rate: int, + undershoot_us: float, + scan_phase_us: float, + retrace_scale: float, +) -> list[str]: + return [ + "--pixel-rate", str(pixel_rate), + "--ao-rate", str(ao_rate), + "--undershoot-us", str(undershoot_us), + "--scan-phase-us", str(scan_phase_us), + "--retrace-scale", str(retrace_scale), + ] + + def generate_raster( resolution: int, width: int, @@ -47,7 +63,11 @@ def generate_raster( x_offset: int, y_offset: int, zoom: float, - undershoot: int, + pixel_rate: int, + ao_rate: int, + undershoot_us: float, + scan_phase_us: float, + retrace_scale: float, tform: tuple[float, float, float, float], tform_offset: tuple[float, float], ) -> tuple[np.ndarray, np.ndarray] | str: @@ -62,7 +82,7 @@ def generate_raster( "--xoffset", str(x_offset), "--yoffset", str(y_offset), "--zoom", str(zoom), - "--undershoot", str(undershoot), + *timing_args(pixel_rate, ao_rate, undershoot_us, scan_phase_us, retrace_scale), "--tform", f"{tform[0]},{tform[1]},{tform[2]},{tform[3]}", "--tform-offset", f"{tform_offset[0]},{tform_offset[1]}", ], tmp) @@ -80,16 +100,24 @@ def generate_raster( def generate_clock( width: int, height: int, - undershoot: int, + resolution: int, + zoom: float, + pixel_rate: int, + ao_rate: int, + undershoot_us: float, + scan_phase_us: float, + retrace_scale: float, ) -> tuple[np.ndarray, np.ndarray, np.ndarray] | str: with tempfile.NamedTemporaryFile(suffix=".raw", delete=False) as f: tmp = Path(f.name) try: result = run_tool([ "clock", + "--resolution", str(resolution), "--width", str(width), "--height", str(height), - "--undershoot", str(undershoot), + "--zoom", str(zoom), + *timing_args(pixel_rate, ao_rate, undershoot_us, scan_phase_us, retrace_scale), ], tmp) if result.returncode != 0: return result.stderr.strip() or f"clock exited with code {result.returncode}" @@ -105,7 +133,11 @@ def generate_clock( def generate_park( resolution: int, zoom: float, - undershoot: int, + pixel_rate: int, + ao_rate: int, + undershoot_us: float, + scan_phase_us: float, + retrace_scale: float, x_offset: int, y_offset: int, xpark: int, @@ -120,7 +152,7 @@ def generate_park( "park", "--resolution", str(resolution), "--zoom", str(zoom), - "--undershoot", str(undershoot), + *timing_args(pixel_rate, ao_rate, undershoot_us, scan_phase_us, retrace_scale), "--xoffset", str(x_offset), "--yoffset", str(y_offset), "--xpark", str(xpark), @@ -142,7 +174,11 @@ def generate_park( def generate_unpark( resolution: int, zoom: float, - undershoot: int, + pixel_rate: int, + ao_rate: int, + undershoot_us: float, + scan_phase_us: float, + retrace_scale: float, x_offset: int, y_offset: int, xpark: int, @@ -159,7 +195,7 @@ def generate_unpark( "unpark", "--resolution", str(resolution), "--zoom", str(zoom), - "--undershoot", str(undershoot), + *timing_args(pixel_rate, ao_rate, undershoot_us, scan_phase_us, retrace_scale), "--xoffset", str(x_offset), "--yoffset", str(y_offset), "--xpark", str(xpark), @@ -205,7 +241,11 @@ def on_generate(_sender=None, _data=None): x_offset = dpg.get_value("x_offset") y_offset = dpg.get_value("y_offset") zoom = dpg.get_value("zoom") - undershoot = dpg.get_value("undershoot") + pixel_rate = dpg.get_value("pixel_rate") + ao_rate = dpg.get_value("ao_rate") + undershoot_us = dpg.get_value("undershoot_us") + scan_phase_us = dpg.get_value("scan_phase_us") + retrace_scale = dpg.get_value("retrace_scale") tform = ( dpg.get_value("tform_a"), dpg.get_value("tform_b"), @@ -221,27 +261,33 @@ def on_generate(_sender=None, _data=None): errors = [] unpark_result = generate_unpark( - resolution, zoom, undershoot, x_offset, y_offset, + resolution, zoom, pixel_rate, ao_rate, undershoot_us, + scan_phase_us, retrace_scale, x_offset, y_offset, xpark, ypark, prev_xpv, prev_ypv, tform, tform_offset, ) if isinstance(unpark_result, str): errors.append(unpark_result) raster_result = generate_raster( - resolution, width, height, x_offset, y_offset, zoom, undershoot, + resolution, width, height, x_offset, y_offset, zoom, + pixel_rate, ao_rate, undershoot_us, scan_phase_us, retrace_scale, tform, tform_offset, ) if isinstance(raster_result, str): errors.append(raster_result) park_result = generate_park( - resolution, zoom, undershoot, x_offset, y_offset, + resolution, zoom, pixel_rate, ao_rate, undershoot_us, + scan_phase_us, retrace_scale, x_offset, y_offset, xpark, ypark, tform, tform_offset, ) if isinstance(park_result, str): errors.append(park_result) - clock_result = generate_clock(width, height, undershoot) + clock_result = generate_clock( + width, height, resolution, zoom, + pixel_rate, ao_rate, undershoot_us, scan_phase_us, retrace_scale, + ) if isinstance(clock_result, str): errors.append(clock_result) @@ -408,11 +454,35 @@ def main(): default_value=1.0, min_value=0.01, min_clamped=True, format="%.3f", callback=on_generate, ) + + dpg.add_separator() + dpg.add_text("Timing") + dpg.add_input_int( - label="Undershoot", tag="undershoot", - default_value=0, min_value=0, min_clamped=True, + label="Pixel Rate (Hz)", tag="pixel_rate", + default_value=200000, min_value=1, min_clamped=True, callback=on_generate, ) + dpg.add_input_int( + label="AO Rate (Hz)", tag="ao_rate", + default_value=200000, min_value=1, min_clamped=True, + callback=on_generate, + ) + dpg.add_input_float( + label="Undershoot (us)", tag="undershoot_us", + default_value=250.0, min_value=0.0, min_clamped=True, + format="%.1f", callback=on_generate, + ) + dpg.add_input_float( + label="Scan Phase (us)", tag="scan_phase_us", + default_value=0.0, min_value=0.0, min_clamped=True, + format="%.1f", callback=on_generate, + ) + dpg.add_input_float( + label="Retrace Scale (us/V)", tag="retrace_scale", + default_value=640.0, min_value=1.0, min_clamped=True, + format="%.1f", callback=on_generate, + ) dpg.add_separator() dpg.add_text("Park / Unpark") diff --git a/meson.build b/meson.build index 2803375..158729b 100644 --- a/meson.build +++ b/meson.build @@ -70,5 +70,4 @@ shared_module( ], ) -subdir('DumpWaveform') subdir('WaveformTool') diff --git a/src/Acquisition.c b/src/Acquisition.c index 720cd09..e56bc9b 100644 --- a/src/Acquisition.c +++ b/src/Acquisition.c @@ -33,6 +33,8 @@ static OScDev_RichError *SetUpDAQ(OScDev_Device *device) { GetImplData(device)->clockConfig.mustReconfigureTiming = true; GetImplData(device)->scannerConfig.mustReconfigureTiming = true; GetImplData(device)->detectorConfig.mustReconfigureTiming = true; + GetImplData(device)->clockConfig.mustRewriteOutput = true; + GetImplData(device)->scannerConfig.mustRewriteOutput = true; } if (resolution != GetImplData(device)->configuredResolution) { GetImplData(device)->scannerConfig.mustReconfigureTiming = true; @@ -184,12 +186,11 @@ static DWORD WINAPI AcquisitionLoop(void *param) { GetImplData(device)->rawDataSize = 0; GetImplData(device)->activeWriteBuffer = 0; - double pixelRateHz = OScDev_Acquisition_GetPixelRate(acq); struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); uint32_t totalElementsPerFramePerChan = GetScannerWaveformSize(¶ms); uint32_t estFrameTimeMs = - (uint32_t)(1e3 * totalElementsPerFramePerChan / pixelRateHz); + (uint32_t)(1e3 * totalElementsPerFramePerChan / params.aoRateHz); err = StartScan(device); if (err) { diff --git a/src/Clock.c b/src/Clock.c index 48bac07..848771e 100644 --- a/src/Clock.c +++ b/src/Clock.c @@ -52,22 +52,21 @@ static OScDev_RichError *CreateClockTasks(OScDev_Device *device, return err; } - double pixelRateHz = OScDev_Acquisition_GetPixelRate(acq); - uint32_t xOffset, yOffset, width, height; - OScDev_Acquisition_GetROI(acq, &xOffset, &yOffset, &width, &height); struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); - uint32_t elementsPerLine = GetLineWaveformSize(¶ms); - double effectiveScanPortion = (double)width / elementsPerLine; - double lineFreqHz = pixelRateHz / elementsPerLine; - double scanPhase = 1.0 / pixelRateHz * GetImplData(device)->lineDelay; + uint32_t elementsPerLine = (uint32_t)GetLineWaveformSize(¶ms); + double initialDelay = + (double)(UndershootSamples(¶ms) + ScanPhaseSamples(¶ms)) / + params.aoRateHz; + double lineFreqHz = params.aoRateHz / elementsPerLine; + double dutyCycle = (double)ScanSamples(¶ms) / elementsPerLine; ss8str ctrTerms; ss8_init_copy(&ctrTerms, &GetImplData(device)->deviceName); ss8_cat_cstr(&ctrTerms, "/ctr0"); err = CreateDAQmxError(DAQmxCreateCOPulseChanFreq( config->lineCtrTask, ss8_cstr(&ctrTerms), "ClockLineCTR", DAQmx_Val_Hz, - DAQmx_Val_Low, scanPhase, lineFreqHz, effectiveScanPortion)); + DAQmx_Val_Low, initialDelay, lineFreqHz, dutyCycle)); ss8_destroy(&ctrTerms); if (err) { err = @@ -83,13 +82,12 @@ static OScDev_RichError *ConfigureClockTiming(OScDev_Device *device, OScDev_Acquisition *acq) { OScDev_RichError *err; - double pixelRateHz = OScDev_Acquisition_GetPixelRate(acq); struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); uint32_t xOffset, yOffset, width, height; OScDev_Acquisition_GetROI(acq, &xOffset, &yOffset, &width, &height); - uint32_t elementsPerLine = GetLineWaveformSize(¶ms); + uint32_t elementsPerLine = (uint32_t)GetLineWaveformSize(¶ms); int32 elementsPerFramePerChan = GetClockWaveformSize(¶ms); uint32_t totalFrames = OScDev_Acquisition_GetNumberOfFrames(acq); @@ -111,7 +109,7 @@ static OScDev_RichError *ConfigureClockTiming(OScDev_Device *device, doSamplesPerChan = totalDOSamples; } err = CreateDAQmxError( - DAQmxCfgSampClkTiming(config->doTask, "", pixelRateHz, + DAQmxCfgSampClkTiming(config->doTask, "", params.aoRateHz, DAQmx_Val_Rising, sampleMode, doSamplesPerChan)); if (err) { err = OScDev_Error_Wrap( @@ -127,9 +125,11 @@ static OScDev_RichError *ConfigureClockTiming(OScDev_Device *device, return err; } - double effectiveScanPortion = (double)width / elementsPerLine; - double lineFreqHz = pixelRateHz / elementsPerLine; - double scanPhase = 1.0 / pixelRateHz * GetImplData(device)->lineDelay; + double dutyCycle = (double)ScanSamples(¶ms) / elementsPerLine; + double lineFreqHz = params.aoRateHz / elementsPerLine; + double initialDelay = + (double)(UndershootSamples(¶ms) + ScanPhaseSamples(¶ms)) / + params.aoRateHz; err = CreateDAQmxError(DAQmxSetChanAttribute( config->lineCtrTask, "", DAQmx_CO_Pulse_Freq, lineFreqHz)); @@ -138,17 +138,17 @@ static OScDev_RichError *ConfigureClockTiming(OScDev_Device *device, return err; } - err = CreateDAQmxError(DAQmxSetChanAttribute( - config->lineCtrTask, "", DAQmx_CO_Pulse_Freq_InitialDelay, scanPhase)); + err = CreateDAQmxError( + DAQmxSetChanAttribute(config->lineCtrTask, "", + DAQmx_CO_Pulse_Freq_InitialDelay, initialDelay)); if (err) { err = OScDev_Error_Wrap(err, "Failed to set clock lineCtr initial delay"); return err; } - err = CreateDAQmxError(DAQmxSetChanAttribute(config->lineCtrTask, "", - DAQmx_CO_Pulse_DutyCyc, - effectiveScanPortion)); + err = CreateDAQmxError(DAQmxSetChanAttribute( + config->lineCtrTask, "", DAQmx_CO_Pulse_DutyCyc, dutyCycle)); if (err) { err = OScDev_Error_Wrap(err, "Failed to set clock lineCtr duty cycle"); return err; diff --git a/src/DAQConfig.c b/src/DAQConfig.c index c73ed5e..4771813 100644 --- a/src/DAQConfig.c +++ b/src/DAQConfig.c @@ -58,7 +58,12 @@ void SetWaveformParamsFromDevice(OScDev_Device *device, parameters->zoom = OScDev_Acquisition_GetZoomFactor(acq); OScDev_Acquisition_GetROI(acq, ¶meters->xOffset, ¶meters->yOffset, ¶meters->width, ¶meters->height); - parameters->undershoot = GetImplData(device)->lineDelay; + parameters->pixelRateHz = OScDev_Acquisition_GetPixelRate(acq); + parameters->aoRateHz = GetImplData(device)->aoRateHz; + parameters->undershootUs = GetImplData(device)->undershootUs; + parameters->scanPhaseUs = GetImplData(device)->scanPhaseUs; + parameters->retraceScaleUsPerVolt = + GetImplData(device)->retraceScaleUsPerVolt; for (int i = 0; i < 4; ++i) parameters->xformMatrix[i] = GetImplData(device)->xformMatrix[i]; parameters->xformOffsetX = GetImplData(device)->xformOffsetX; diff --git a/src/DeviceImplData.c b/src/DeviceImplData.c index d8b2085..07fdd8b 100644 --- a/src/DeviceImplData.c +++ b/src/DeviceImplData.c @@ -10,7 +10,10 @@ void InitializeImplData(struct DeviceImplData *data) { memset(data, 0, sizeof(*data)); ss8_init(&data->deviceName); - data->lineDelay = 50; + data->aoRateHz = 200000.0; + data->undershootUs = 250.0; + data->scanPhaseUs = 0.0; + data->retraceScaleUsPerVolt = 640.0; data->xformMatrix[0] = 1.0; data->xformMatrix[1] = 0.0; data->xformMatrix[2] = 0.0; diff --git a/src/DeviceImplData.h b/src/DeviceImplData.h index 833a489..e60e2bb 100644 --- a/src/DeviceImplData.h +++ b/src/DeviceImplData.h @@ -36,10 +36,10 @@ struct DeviceImplData { bool scannerOnly; - // counted as number of pixels. - // to adjust for the lag between the mirror control signal and the actual - // position of the mirror scan phase (uSec) = line delay / scan rate - uint32_t lineDelay; + double aoRateHz; + double undershootUs; + double scanPhaseUs; + double retraceScaleUsPerVolt; int32_t xPark; int32_t yPark; diff --git a/src/OpenScanSettings.c b/src/OpenScanSettings.c index 378763f..4115b32 100644 --- a/src/OpenScanSettings.c +++ b/src/OpenScanSettings.c @@ -33,36 +33,120 @@ GetNumericConstraintTypeImpl_Range(OScDev_Setting *setting, return OScDev_OK; } -static OScDev_Error GetLineDelay(OScDev_Setting *setting, int32_t *value) { - *value = GetSettingDeviceData(setting)->lineDelay; +static OScDev_Error GetAOSampleRate(OScDev_Setting *setting, double *value) { + *value = GetSettingDeviceData(setting)->aoRateHz; + return OScDev_OK; +} +static OScDev_Error SetAOSampleRate(OScDev_Setting *setting, double value) { + GetSettingDeviceData(setting)->aoRateHz = value; + GetSettingDeviceData(setting)->clockConfig.mustReconfigureTiming = true; + GetSettingDeviceData(setting)->scannerConfig.mustReconfigureTiming = true; + GetSettingDeviceData(setting)->clockConfig.mustRewriteOutput = true; + GetSettingDeviceData(setting)->scannerConfig.mustRewriteOutput = true; return OScDev_OK; } -static OScDev_Error SetLineDelay(OScDev_Setting *setting, int32_t value) { - GetSettingDeviceData(setting)->lineDelay = value; +static OScDev_Error GetAOSampleRateRange(OScDev_Setting *setting, double *min, + double *max) { + (void)setting; + *min = 100000.0; + *max = 1000000.0; + return OScDev_OK; +} +static OScDev_SettingImpl SettingImpl_AOSampleRate = { + .GetFloat64 = GetAOSampleRate, + .SetFloat64 = SetAOSampleRate, + .GetNumericConstraintType = GetNumericConstraintTypeImpl_Range, + .GetFloat64Range = GetAOSampleRateRange, +}; + +static OScDev_Error GetUndershoot(OScDev_Setting *setting, double *value) { + *value = GetSettingDeviceData(setting)->undershootUs; + return OScDev_OK; +} + +static OScDev_Error SetUndershoot(OScDev_Setting *setting, double value) { + GetSettingDeviceData(setting)->undershootUs = value; GetSettingDeviceData(setting)->clockConfig.mustReconfigureTiming = true; GetSettingDeviceData(setting)->scannerConfig.mustReconfigureTiming = true; GetSettingDeviceData(setting)->clockConfig.mustRewriteOutput = true; GetSettingDeviceData(setting)->scannerConfig.mustRewriteOutput = true; + return OScDev_OK; +} +static OScDev_Error GetUndershootRange(OScDev_Setting *setting, double *min, + double *max) { + (void)setting; + *min = 0.0; + *max = 5000.0; return OScDev_OK; } -static OScDev_Error GetLineDelayRange(OScDev_Setting *setting, int32_t *min, - int32_t *max) { - (void)setting; // Unused - *min = 1; - *max = 200; +static OScDev_SettingImpl SettingImpl_Undershoot = { + .GetFloat64 = GetUndershoot, + .SetFloat64 = SetUndershoot, + .GetNumericConstraintType = GetNumericConstraintTypeImpl_Range, + .GetFloat64Range = GetUndershootRange, +}; + +static OScDev_Error GetScanPhase(OScDev_Setting *setting, double *value) { + *value = GetSettingDeviceData(setting)->scanPhaseUs; return OScDev_OK; } -static OScDev_SettingImpl SettingImpl_LineDelay = { - .GetInt32 = GetLineDelay, - .SetInt32 = SetLineDelay, +static OScDev_Error SetScanPhase(OScDev_Setting *setting, double value) { + GetSettingDeviceData(setting)->scanPhaseUs = value; + GetSettingDeviceData(setting)->clockConfig.mustReconfigureTiming = true; + GetSettingDeviceData(setting)->scannerConfig.mustReconfigureTiming = true; + GetSettingDeviceData(setting)->clockConfig.mustRewriteOutput = true; + GetSettingDeviceData(setting)->scannerConfig.mustRewriteOutput = true; + return OScDev_OK; +} + +static OScDev_Error GetScanPhaseRange(OScDev_Setting *setting, double *min, + double *max) { + (void)setting; + *min = 0.0; + *max = 1000.0; + return OScDev_OK; +} + +static OScDev_SettingImpl SettingImpl_ScanPhase = { + .GetFloat64 = GetScanPhase, + .SetFloat64 = SetScanPhase, .GetNumericConstraintType = GetNumericConstraintTypeImpl_Range, - .GetInt32Range = GetLineDelayRange, + .GetFloat64Range = GetScanPhaseRange, +}; + +static OScDev_Error GetRetraceScale(OScDev_Setting *setting, double *value) { + *value = GetSettingDeviceData(setting)->retraceScaleUsPerVolt; + return OScDev_OK; +} + +static OScDev_Error SetRetraceScale(OScDev_Setting *setting, double value) { + GetSettingDeviceData(setting)->retraceScaleUsPerVolt = value; + GetSettingDeviceData(setting)->clockConfig.mustReconfigureTiming = true; + GetSettingDeviceData(setting)->scannerConfig.mustReconfigureTiming = true; + GetSettingDeviceData(setting)->clockConfig.mustRewriteOutput = true; + GetSettingDeviceData(setting)->scannerConfig.mustRewriteOutput = true; + return OScDev_OK; +} + +static OScDev_Error GetRetraceScaleRange(OScDev_Setting *setting, double *min, + double *max) { + (void)setting; + *min = 100.0; + *max = 10000.0; + return OScDev_OK; +} + +static OScDev_SettingImpl SettingImpl_RetraceScale = { + .GetFloat64 = GetRetraceScale, + .SetFloat64 = SetRetraceScale, + .GetNumericConstraintType = GetNumericConstraintTypeImpl_Range, + .GetFloat64Range = GetRetraceScaleRange, }; static OScDev_Error GetParkingPositionX(OScDev_Setting *setting, @@ -272,13 +356,37 @@ OScDev_Error NIDAQMakeSettings(OScDev_Device *device, *settings = OScDev_PtrArray_Create(); - OScDev_Setting *lineDelay; + OScDev_Setting *aoSampleRate; + err = OScDev_Error_AsRichError(OScDev_Setting_Create( + &aoSampleRate, "AO Sample Rate (Hz)", OScDev_ValueType_Float64, + &SettingImpl_AOSampleRate, device)); + if (err) + goto error; + OScDev_PtrArray_Append(*settings, aoSampleRate); + + OScDev_Setting *undershoot; + err = OScDev_Error_AsRichError(OScDev_Setting_Create( + &undershoot, "Undershoot (us)", OScDev_ValueType_Float64, + &SettingImpl_Undershoot, device)); + if (err) + goto error; + OScDev_PtrArray_Append(*settings, undershoot); + + OScDev_Setting *scanPhase; + err = OScDev_Error_AsRichError(OScDev_Setting_Create( + &scanPhase, "Scan Phase (us)", OScDev_ValueType_Float64, + &SettingImpl_ScanPhase, device)); + if (err) + goto error; + OScDev_PtrArray_Append(*settings, scanPhase); + + OScDev_Setting *retraceScale; err = OScDev_Error_AsRichError(OScDev_Setting_Create( - &lineDelay, "Line Delay (pixels)", OScDev_ValueType_Int32, - &SettingImpl_LineDelay, device)); + &retraceScale, "Retrace Scale (us/V)", OScDev_ValueType_Float64, + &SettingImpl_RetraceScale, device)); if (err) goto error; - OScDev_PtrArray_Append(*settings, lineDelay); + OScDev_PtrArray_Append(*settings, retraceScale); OScDev_Setting *parkingPositionX; err = OScDev_Error_AsRichError(OScDev_Setting_Create( diff --git a/src/ParkUnpark.c b/src/ParkUnpark.c index cf0b3d4..116f782 100644 --- a/src/ParkUnpark.c +++ b/src/ParkUnpark.c @@ -17,15 +17,15 @@ OScDev_RichError *ConfigureUnparkTiming(OScDev_Device *device, struct ScannerConfig *config, OScDev_Acquisition *acq) { OScDev_RichError *err; - double pixelRateHz = OScDev_Acquisition_GetPixelRate(acq); + double aoRateHz = GetImplData(device)->aoRateHz; struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); int32 totalElementsPerFramePerChan = GetParkWaveformSize(¶ms); err = CreateDAQmxError(DAQmxCfgSampClkTiming( - config->aoTask, "", pixelRateHz, DAQmx_Val_Rising, - DAQmx_Val_FiniteSamps, totalElementsPerFramePerChan)); + config->aoTask, "", aoRateHz, DAQmx_Val_Rising, DAQmx_Val_FiniteSamps, + totalElementsPerFramePerChan)); if (err) { err = OScDev_Error_Wrap(err, "Failed to configure timing for unpark"); return err; @@ -38,15 +38,15 @@ OScDev_RichError *ConfigureParkTiming(OScDev_Device *device, struct ScannerConfig *config, OScDev_Acquisition *acq) { OScDev_RichError *err; - double pixelRateHz = OScDev_Acquisition_GetPixelRate(acq); + double aoRateHz = GetImplData(device)->aoRateHz; struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); int32 totalElementsPerFramePerChan = GetParkWaveformSize(¶ms); err = CreateDAQmxError(DAQmxCfgSampClkTiming( - config->aoTask, "", pixelRateHz, DAQmx_Val_Rising, - DAQmx_Val_FiniteSamps, totalElementsPerFramePerChan)); + config->aoTask, "", aoRateHz, DAQmx_Val_Rising, DAQmx_Val_FiniteSamps, + totalElementsPerFramePerChan)); if (err) { err = OScDev_Error_Wrap(err, "Failed to configure timing for park"); return err; @@ -123,13 +123,12 @@ OScDev_RichError *GenerateUnparkOutput(OScDev_Device *device, struct ScannerConfig *config, OScDev_Acquisition *acq) { OScDev_RichError *err; - double pixelRateHz = OScDev_Acquisition_GetPixelRate(acq); + double aoRateHz = GetImplData(device)->aoRateHz; struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); uint32_t totalElementsPerFramePerChan = GetParkWaveformSize(¶ms); - // changed from 1e3 to 1e4 - works but does not reconfigure timing uint32_t estFrameTimeMs = - (uint32_t)(1e3 * totalElementsPerFramePerChan / pixelRateHz); + (uint32_t)(1e3 * totalElementsPerFramePerChan / aoRateHz); uint32_t maxWaitTimeMs = 2 * estFrameTimeMs; if (maxWaitTimeMs < 1000) { maxWaitTimeMs = 1000; @@ -168,12 +167,12 @@ OScDev_RichError *GenerateParkOutput(OScDev_Device *device, struct ScannerConfig *config, OScDev_Acquisition *acq) { OScDev_RichError *err; - double pixelRateHz = OScDev_Acquisition_GetPixelRate(acq); + double aoRateHz = GetImplData(device)->aoRateHz; struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); uint32_t totalElementsPerFramePerChan = GetParkWaveformSize(¶ms); uint32_t estFrameTimeMs = - (uint32_t)(1e3 * totalElementsPerFramePerChan / pixelRateHz); + (uint32_t)(1e3 * totalElementsPerFramePerChan / aoRateHz); uint32_t maxWaitTimeMs = 2 * estFrameTimeMs; if (maxWaitTimeMs < 1000) { maxWaitTimeMs = 1000; diff --git a/src/Scanner.c b/src/Scanner.c index 907554f..9d7bbe4 100644 --- a/src/Scanner.c +++ b/src/Scanner.c @@ -17,7 +17,7 @@ static OScDev_RichError *ConfigureScannerTiming(OScDev_Device *device, struct ScannerConfig *config, OScDev_Acquisition *acq) { OScDev_RichError *err; - double pixelRateHz = OScDev_Acquisition_GetPixelRate(acq); + double aoRateHz = GetImplData(device)->aoRateHz; struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); @@ -40,7 +40,7 @@ static OScDev_RichError *ConfigureScannerTiming(OScDev_Device *device, } err = CreateDAQmxError(DAQmxCfgSampClkTiming(config->aoTask, "", - pixelRateHz, DAQmx_Val_Rising, + aoRateHz, DAQmx_Val_Rising, sampleMode, samplesPerChan)); if (err) { err = OScDev_Error_Wrap(err, "Failed to configure timing for scanner"); diff --git a/src/Waveform.c b/src/Waveform.c index 3f90c01..7cc52ed 100644 --- a/src/Waveform.c +++ b/src/Waveform.c @@ -1,11 +1,40 @@ #include "Waveform.h" +#include #include #include -// TODO We should probably scale the retrace length according to -// zoomFactor * width_or_height -static const uint32_t X_RETRACE_LEN = 128; +static const double MIN_RETRACE_US = 50.0; +static const double PARK_UNPARK_DURATION_US = 640.0; + +static double ScanAmplitude(const struct WaveformParams *params) { + return (double)params->width / (params->zoom * params->resolution); +} + +uint32_t UndershootSamples(const struct WaveformParams *params) { + return (uint32_t)round(params->undershootUs * 1e-6 * params->aoRateHz); +} + +uint32_t ScanSamples(const struct WaveformParams *params) { + return (uint32_t)round((double)params->width / params->pixelRateHz * + params->aoRateHz); +} + +uint32_t ScanPhaseSamples(const struct WaveformParams *params) { + return (uint32_t)round(params->scanPhaseUs * 1e-6 * params->aoRateHz); +} + +uint32_t RetraceSamples(const struct WaveformParams *params) { + double amplitude = ScanAmplitude(params); + double retraceUs = params->retraceScaleUsPerVolt * amplitude; + if (retraceUs < MIN_RETRACE_US) + retraceUs = MIN_RETRACE_US; + return (uint32_t)round(retraceUs * 1e-6 * params->aoRateHz); +} + +uint32_t ParkSamples(const struct WaveformParams *params) { + return (uint32_t)round(PARK_UNPARK_DURATION_US * 1e-6 * params->aoRateHz); +} // n = number of elements // slope in units of per element @@ -29,27 +58,26 @@ static void SplineInterpolate(int32_t n, double yFirst, double yLast, } } -// Generate 1D (undershoot + trace + retrace). -// The trace part spans voltage scanStart to scanEnd. +// Generate 1D (undershoot + scan + overshoot + retrace). +// The scan part spans voltage scanStart to scanEnd. +// overshootLen extends the linear ramp past scanEnd. static void GenerateXGalvoWaveform(int32_t effectiveScanLen, int32_t retraceLen, int32_t undershootLen, - double scanStart, double scanEnd, - double *waveform) { + int32_t overshootLen, double scanStart, + double scanEnd, double *waveform) { double scanAmplitude = scanEnd - scanStart; double step = scanAmplitude / effectiveScanLen; - int32_t linearLen = undershootLen + effectiveScanLen; + int32_t linearLen = undershootLen + effectiveScanLen + overshootLen; - // Generate the linear scan curve double undershootStart = scanStart - undershootLen * step; for (int i = 0; i < linearLen; ++i) { waveform[i] = undershootStart + step * i; } - // Generate the rescan curve - // Slope at start end end are both equal to the linear scan + double overshootEnd = scanEnd + overshootLen * step; if (retraceLen > 0) { - SplineInterpolate(retraceLen, scanEnd, undershootStart, step, step, - waveform + linearLen); + SplineInterpolate(retraceLen, overshootEnd, undershootStart, step, + step, waveform + linearLen); } } @@ -57,17 +85,14 @@ static void GenerateXGalvoWaveform(int32_t effectiveScanLen, static void GenerateYGalvoWaveform(int32_t linesPerFrame, int32_t retraceLen, size_t xLength, double scanStart, double scanEnd, double *waveform) { - (void)retraceLen; // Unused - double scanAmplitude = scanEnd - scanStart; double step = scanAmplitude / linesPerFrame; - // Generate staircase for one frame for (int j = 0; j < linesPerFrame; ++j) { for (unsigned i = 0; i < xLength; ++i) { waveform[i + j * xLength] = scanStart + step * j; - // stop at last x retrace - if ((j >= linesPerFrame - 1) && (i >= xLength - X_RETRACE_LEN)) { + if ((j >= linesPerFrame - 1) && + (i >= xLength - (unsigned)retraceLen)) { break; } } @@ -77,128 +102,123 @@ static void GenerateYGalvoWaveform(int32_t linesPerFrame, int32_t retraceLen, for (int j = 0; j < linesPerFrame - 1; ++j) { double yThis = scanStart + step * j; double yNext = scanStart + step * (j + 1); - SplineInterpolate(X_RETRACE_LEN, yThis, yNext, 0, 0, - waveform + (j + 1) * xLength - X_RETRACE_LEN); + SplineInterpolate(retraceLen, yThis, yNext, 0, 0, + waveform + (j + 1) * xLength - retraceLen); } - // Generate the rescan curve at end of frame - if (X_RETRACE_LEN > 0) { + // Frame retrace at end + if (retraceLen > 0) { double lastLineY = scanStart + step * (linesPerFrame - 1); - SplineInterpolate(X_RETRACE_LEN, lastLineY, scanStart, 0, 0, - waveform + (linesPerFrame * xLength) - - X_RETRACE_LEN); + SplineInterpolate(retraceLen, lastLineY, scanStart, 0, 0, + waveform + (linesPerFrame * xLength) - retraceLen); } } -/* Line clock pattern for NI DAQ to output from one of its digital IOs */ void GenerateLineClock(const struct WaveformParams *parameters, uint8_t *lineClock) { - uint32_t lineDelay = parameters->undershoot; - uint32_t width = parameters->width; + uint32_t undershootSmp = UndershootSamples(parameters); + uint32_t scanPhaseSmp = ScanPhaseSamples(parameters); + uint32_t scanSmp = ScanSamples(parameters); uint32_t height = parameters->height; - uint32_t x_length = lineDelay + width + X_RETRACE_LEN; + uint32_t highStart = undershootSmp + scanPhaseSmp; + uint32_t x_length = (uint32_t)GetLineWaveformSize(parameters); for (uint32_t j = 0; j < height; j++) for (uint32_t i = 0; i < x_length; i++) lineClock[i + j * x_length] = - ((i >= lineDelay) && (i < lineDelay + width)) ? 1 : 0; + ((i >= highStart) && (i < highStart + scanSmp)) ? 1 : 0; } -// High voltage right after a line acquisition is done -// like a line clock of reversed polarity -// specially for B&H FLIM application void GenerateFLIMLineClock(const struct WaveformParams *parameters, uint8_t *lineClockFLIM) { - uint32_t lineDelay = parameters->undershoot; - uint32_t width = parameters->width; + uint32_t undershootSmp = UndershootSamples(parameters); + uint32_t scanPhaseSmp = ScanPhaseSamples(parameters); + uint32_t scanSmp = ScanSamples(parameters); uint32_t height = parameters->height; - uint32_t x_length = lineDelay + width + X_RETRACE_LEN; + uint32_t highStart = undershootSmp + scanPhaseSmp + scanSmp; + uint32_t x_length = (uint32_t)GetLineWaveformSize(parameters); for (uint32_t j = 0; j < height; j++) for (uint32_t i = 0; i < x_length; i++) - lineClockFLIM[i + j * x_length] = (i >= lineDelay + width) ? 1 : 0; + lineClockFLIM[i + j * x_length] = (i >= highStart) ? 1 : 0; } -// Frame clock for B&H FLIM -// High voltage at the end of the frame void GenerateFLIMFrameClock(const struct WaveformParams *parameters, uint8_t *frameClockFLIM) { - uint32_t lineDelay = parameters->undershoot; - uint32_t width = parameters->width; + uint32_t undershootSmp = UndershootSamples(parameters); + uint32_t scanPhaseSmp = ScanPhaseSamples(parameters); + uint32_t scanSmp = ScanSamples(parameters); uint32_t height = parameters->height; - uint32_t x_length = lineDelay + width + X_RETRACE_LEN; + uint32_t highStart = undershootSmp + scanPhaseSmp + scanSmp; + uint32_t x_length = (uint32_t)GetLineWaveformSize(parameters); for (uint32_t j = 0; j < height; ++j) for (uint32_t i = 0; i < x_length; ++i) frameClockFLIM[i + j * x_length] = - ((j == height - 1) && (i > lineDelay + width)) ? 1 : 0; + ((j == height - 1) && (i > highStart)) ? 1 : 0; } int32_t GetLineWaveformSize(const struct WaveformParams *parameters) { - return parameters->undershoot + parameters->width + X_RETRACE_LEN; + return (int32_t)(UndershootSamples(parameters) + ScanSamples(parameters) + + ScanPhaseSamples(parameters) + + RetraceSamples(parameters)); } int32_t GetClockWaveformSize(const struct WaveformParams *parameters) { - uint32_t elementsPerLine = GetLineWaveformSize(parameters); + uint32_t elementsPerLine = (uint32_t)GetLineWaveformSize(parameters); uint32_t height = parameters->height; - return elementsPerLine * height; + return (int32_t)(elementsPerLine * height); } int32_t GetScannerWaveformSize(const struct WaveformParams *parameters) { - uint32_t elementsPerLine = GetLineWaveformSize(parameters); + uint32_t elementsPerLine = (uint32_t)GetLineWaveformSize(parameters); uint32_t height = parameters->height; - uint32_t yLen = height; - return elementsPerLine * yLen; // including y retrace portion + return (int32_t)(elementsPerLine * height); } int32_t GetScannerWaveformSizeAfterLastPixel(const struct WaveformParams *parameters) { - (void)parameters; // Unused - return X_RETRACE_LEN; + return (int32_t)(ScanPhaseSamples(parameters) + + RetraceSamples(parameters)); } int32_t GetParkWaveformSize(const struct WaveformParams *parameters) { - (void)parameters; // Unused - uint32_t elementsPerLine = X_RETRACE_LEN; - return elementsPerLine; + return (int32_t)ParkSamples(parameters); } -/* -Generate X and Y waveforms in analog format (voltage) for a whole frame scan -Format: X|Y in a 1D array for NI DAQ to simultaneously output in two channels -Analog voltage range (-0.5V, 0.5V) at zoom 1 -Including Y retrace waveform that moves the slow galvo back to its starting -position -*/ void GenerateGalvoWaveformFrame(const struct WaveformParams *parameters, double *xyWaveformFrame) { - uint32_t pixelsPerLine = parameters->width; // ROI size + uint32_t pixelsPerLine = parameters->width; uint32_t linesPerFrame = parameters->height; uint32_t resolution = parameters->resolution; double zoom = parameters->zoom; - uint32_t undershoot = parameters->undershoot; - uint32_t xOffset = parameters->xOffset; // ROI offset + uint32_t xOffset = parameters->xOffset; uint32_t yOffset = parameters->yOffset; const double *m = parameters->xformMatrix; double tx = parameters->xformOffsetX; double ty = parameters->xformOffsetY; - // Voltage ranges of the ROI + uint32_t undershootSmp = UndershootSamples(parameters); + uint32_t scanSmp = ScanSamples(parameters); + uint32_t scanPhaseSmp = ScanPhaseSamples(parameters); + uint32_t retraceSmp = RetraceSamples(parameters); + double xStart = (-0.5 * resolution + xOffset) / (zoom * resolution); double yStart = (-0.5 * resolution + yOffset) / (zoom * resolution); - double xEnd = xStart + pixelsPerLine / (zoom * resolution); - double yEnd = yStart + linesPerFrame / (zoom * resolution); + double xEnd = xStart + (double)pixelsPerLine / (zoom * resolution); + double yEnd = yStart + (double)linesPerFrame / (zoom * resolution); - size_t xLength = undershoot + pixelsPerLine + X_RETRACE_LEN; + size_t xLength = undershootSmp + scanSmp + scanPhaseSmp + retraceSmp; size_t yLength = linesPerFrame; double *xWaveform = (double *)malloc(sizeof(double) * xLength); double *yWaveform = (double *)malloc(sizeof(double) * (yLength * xLength)); - GenerateXGalvoWaveform(pixelsPerLine, X_RETRACE_LEN, undershoot, xStart, - xEnd, xWaveform); - GenerateYGalvoWaveform(linesPerFrame, X_RETRACE_LEN, xLength, yStart, yEnd, - yWaveform); + GenerateXGalvoWaveform((int32_t)scanSmp, (int32_t)retraceSmp, + (int32_t)undershootSmp, (int32_t)scanPhaseSmp, + xStart, xEnd, xWaveform); + GenerateYGalvoWaveform((int32_t)linesPerFrame, (int32_t)retraceSmp, + xLength, yStart, yEnd, yWaveform); for (unsigned j = 0; j < yLength; ++j) { for (unsigned i = 0; i < xLength; ++i) { @@ -210,16 +230,10 @@ void GenerateGalvoWaveformFrame(const struct WaveformParams *parameters, } } - // TODO When we are scanning multiple frames, the Y retrace can be - // simultaneous with the last line's X retrace. (Spline interpolate - // with zero slope at each end of retrace.) - // TODO Simpler to use interleaved x,y format? - free(xWaveform); free(yWaveform); } -// Generate waveform from parking to start before one frame static void InverseTransform2x2(const double *m, double tx, double ty, double ox, double oy, double *lx, double *ly) { double det = m[0] * m[3] - m[1] * m[2]; @@ -235,21 +249,23 @@ void GenerateGalvoUnparkWaveform(const struct WaveformParams *parameters, double zoom = parameters->zoom; uint32_t xOffset = parameters->xOffset; uint32_t yOffset = parameters->yOffset; - int32_t undershoot = parameters->undershoot; const double *m = parameters->xformMatrix; double tx = parameters->xformOffsetX; double ty = parameters->xformOffsetY; - // Inverse-transform previous park voltage to logical space + uint32_t undershootSmp = UndershootSamples(parameters); + uint32_t scanSmp = ScanSamples(parameters); + double step = ScanAmplitude(parameters) / scanSmp; + double xStart, yStart; InverseTransform2x2(m, tx, ty, parameters->prevXParkVoltage, parameters->prevYParkVoltage, &xStart, &yStart); - double xEnd = - (-0.5 * resolution + xOffset - undershoot) / (zoom * resolution); + double xEnd = (-0.5 * resolution + xOffset) / (zoom * resolution) - + undershootSmp * step; double yEnd = (-0.5 * resolution + yOffset) / (zoom * resolution); - size_t length = X_RETRACE_LEN; + size_t length = ParkSamples(parameters); double *xWaveform = (double *)malloc(sizeof(double) * length); double *yWaveform = (double *)malloc(sizeof(double) * length); @@ -267,27 +283,29 @@ void GenerateGalvoUnparkWaveform(const struct WaveformParams *parameters, free(yWaveform); } -// Generate waveform from start to parking after one frame void GenerateGalvoParkWaveform(const struct WaveformParams *parameters, double *xyWaveformFrame) { uint32_t resolution = parameters->resolution; double zoom = parameters->zoom; uint32_t xOffset = parameters->xOffset; uint32_t yOffset = parameters->yOffset; - int32_t undershoot = parameters->undershoot; int32_t xPark = parameters->xPark; int32_t yPark = parameters->yPark; const double *m = parameters->xformMatrix; double tx = parameters->xformOffsetX; double ty = parameters->xformOffsetY; - double xStart = - (-0.5 * resolution + xOffset - undershoot) / (zoom * resolution); + uint32_t undershootSmp = UndershootSamples(parameters); + uint32_t scanSmp = ScanSamples(parameters); + double step = ScanAmplitude(parameters) / scanSmp; + + double xStart = (-0.5 * resolution + xOffset) / (zoom * resolution) - + undershootSmp * step; double yStart = (-0.5 * resolution + yOffset) / (zoom * resolution); double xEnd = (-0.5 * resolution + xPark) / (zoom * resolution); double yEnd = (-0.5 * resolution + yPark) / (zoom * resolution); - size_t length = X_RETRACE_LEN; + size_t length = ParkSamples(parameters); double *xWaveform = (double *)malloc(sizeof(double) * length); double *yWaveform = (double *)malloc(sizeof(double) * length); diff --git a/src/Waveform.h b/src/Waveform.h index f2d961c..d8f143a 100644 --- a/src/Waveform.h +++ b/src/Waveform.h @@ -3,13 +3,19 @@ #include struct WaveformParams { - uint32_t width; // PixelsPerLine + uint32_t width; // PixelsPerLine (ROI) uint32_t height; // numScanLines uint32_t resolution; double zoom; - uint32_t undershoot; // also LineDelay for clock waveforms uint32_t xOffset; uint32_t yOffset; + + double aoRateHz; + double pixelRateHz; + double undershootUs; + double scanPhaseUs; + double retraceScaleUsPerVolt; + double xformMatrix[4]; // {a, b, c, d} — row-major 2x2 double xformOffsetX; // tx (volts) double xformOffsetY; // ty (volts) @@ -19,6 +25,12 @@ struct WaveformParams { double prevYParkVoltage; }; +uint32_t UndershootSamples(const struct WaveformParams *params); +uint32_t ScanSamples(const struct WaveformParams *params); +uint32_t ScanPhaseSamples(const struct WaveformParams *params); +uint32_t RetraceSamples(const struct WaveformParams *params); +uint32_t ParkSamples(const struct WaveformParams *params); + void GenerateLineClock(const struct WaveformParams *parameters, uint8_t *lineClock); void GenerateFLIMLineClock(const struct WaveformParams *parameters, From 3715f6cc7e6d303771cf68a751f8f66f0ad95f5e Mon Sep 17 00:00:00 2001 From: "Mark A. Tsuchida" Date: Thu, 21 May 2026 20:18:46 +0900 Subject: [PATCH 2/3] Derive AO rate from pixel rate; remove setting The AO scan waveform and the line/frame clocks are produced on the AO sample grid, but the detector acquires pixels on the pixel grid. With the AO rate an independent setting, the per-line scan window didn't exactly equal the acquisition window: the line clock edge could be a fraction of a pixel off at line end. Remove the user setting for AO rate; constrain aoRate = K * pixelRate for integer K dividing the pixel period in sample clock timebase (usually 100 MHz) ticks. This makes the AO rate an exact timebase divisor (keeping the timebase-derived line counter drift-free) and the scan window an exact integer number of pixels. Among valid K with the AO rate in [100 kHz, device max], pick the rate closest to 200 kHz (galvo bandwidth is ~kHz; higher only wastes memory/CPU), preferring the higher on a tie. (Assisted by Claude Code; any errors are mine.) --- src/Acquisition.c | 13 +++++- src/DAQConfig.c | 96 ++++++++++++++++++++++++++++++++++++++++++ src/DAQConfig.h | 5 +++ src/DeviceImplData.h | 6 ++- src/OpenScanDevice.c | 11 ++++- src/OpenScanSettings.c | 37 ---------------- 6 files changed, 126 insertions(+), 42 deletions(-) diff --git a/src/Acquisition.c b/src/Acquisition.c index e56bc9b..12ff4ba 100644 --- a/src/Acquisition.c +++ b/src/Acquisition.c @@ -20,6 +20,17 @@ static OScDev_RichError *SetUpDAQ(OScDev_Device *device) { OScDev_Acquisition *acq = GetImplData(device)->acquisition.acquisition; double pixelRateHz = OScDev_Acquisition_GetPixelRate(acq); + + OScDev_RichError *err = EnsureTimingCapsQueried(device); + if (err) + return err; + double aoRateHz; + if (!ComputeAORateHz(pixelRateHz, GetImplData(device)->sampClkTimebaseHz, + GetImplData(device)->aoMaxRateHz, &aoRateHz)) + return OScDev_Error_Create( + "No valid AO sample rate for the selected pixel rate"); + GetImplData(device)->aoRateHz = aoRateHz; + uint32_t resolution = OScDev_Acquisition_GetResolution(acq); double zoomFactor = OScDev_Acquisition_GetZoomFactor(acq); uint32_t xOffset, yOffset, width, height; @@ -61,8 +72,6 @@ static OScDev_RichError *SetUpDAQ(OScDev_Device *device) { // Note that additional setting of 'mustReconfigure' flags occurs in // settings - OScDev_RichError *err; - err = SetUpClock(device, &GetImplData(device)->clockConfig, acq); if (err) return err; diff --git a/src/DAQConfig.c b/src/DAQConfig.c index 4771813..ff5f260 100644 --- a/src/DAQConfig.c +++ b/src/DAQConfig.c @@ -12,9 +12,105 @@ #include #include +#include +#include #include #include +static const double MIN_AO_RATE_HZ = 100.0e3; // keep galvo waveform smooth +static const double TARGET_AO_RATE_HZ = 200.0e3; // galvo BW ~kHz; higher + // wastes memory/CPU + +// Choose aoRate = timebaseHz / t, where t (AO period in ticks) divides the +// pixel period in ticks. Among candidates within [MIN_AO_RATE_HZ, aoMaxHz], +// pick the frequency closest to TARGET_AO_RATE_HZ; tie -> higher frequency. +// Returns false when no valid AO rate exists for this pixel rate. +bool ComputeAORateHz(double pixelRateHz, double timebaseHz, double aoMaxHz, + double *aoRateHz) { + long pixelPeriodTicks = lround(timebaseHz / pixelRateHz); + bool found = false; + double bestFreq = 0.0, bestDist = 0.0; + for (long t = 1; t <= pixelPeriodTicks; ++t) { + if (pixelPeriodTicks % t != 0) + continue; // K = pixelPeriodTicks / t must be integer + double f = timebaseHz / (double)t; + if (f < MIN_AO_RATE_HZ || f > aoMaxHz) + continue; + double d = fabs(f - TARGET_AO_RATE_HZ); + if (!found || d < bestDist || (d == bestDist && f > bestFreq)) { + found = true; + bestDist = d; + bestFreq = f; + } + } + if (found) + *aoRateHz = bestFreq; + return found; +} + +// Populate GetImplData(device)->sampClkTimebaseHz and ->aoMaxRateHz on first +// call; no-op afterwards (guarded by ->timingCapsQueried). +OScDev_RichError *EnsureTimingCapsQueried(OScDev_Device *device) { + if (GetImplData(device)->timingCapsQueried) + return OScDev_RichError_OK; + + const char *dev = ss8_cstr(&GetImplData(device)->deviceName); + OScDev_RichError *err; + + // AO max rate: plain device attribute, no task needed. + float64 aoMax; + err = CreateDAQmxError(DAQmxGetDevAOMaxRate(dev, &aoMax)); + if (err) + return OScDev_Error_Wrap(err, + "Failed to query AO maximum sample rate"); + + // Timebase: throwaway committed AO task. + ss8str aoChan; + ss8_init_copy(&aoChan, &GetImplData(device)->deviceName); + ss8_cat_cstr(&aoChan, "/ao0"); + + TaskHandle task = 0; + float64 timebase = 0.0; + err = CreateDAQmxError(DAQmxCreateTask("", &task)); + if (err) { + err = OScDev_Error_Wrap(err, "Failed to create timing-query task"); + goto cleanup; + } + err = CreateDAQmxError(DAQmxCreateAOVoltageChan( + task, ss8_cstr(&aoChan), "", -10.0, 10.0, DAQmx_Val_Volts, NULL)); + if (err) { + err = OScDev_Error_Wrap( + err, "Failed to create ao channel for timing query"); + goto cleanup; + } + err = CreateDAQmxError(DAQmxCfgSampClkTiming( + task, "", 100000.0, DAQmx_Val_Rising, DAQmx_Val_ContSamps, 1000)); + if (err) { + err = OScDev_Error_Wrap(err, "Failed to configure timing-query task"); + goto cleanup; + } + err = CreateDAQmxError(DAQmxTaskControl(task, DAQmx_Val_Task_Commit)); + if (err) { + err = OScDev_Error_Wrap(err, "Failed to commit timing-query task"); + goto cleanup; + } + err = CreateDAQmxError(DAQmxGetSampClkTimebaseRate(task, &timebase)); + if (err) { + err = OScDev_Error_Wrap(err, "Failed to query samp clk timebase rate"); + goto cleanup; + } + + GetImplData(device)->aoMaxRateHz = aoMax; + GetImplData(device)->sampClkTimebaseHz = timebase; + GetImplData(device)->timingCapsQueried = true; + +cleanup: + if (task) + DAQmxClearTask(task); + ss8_destroy(&aoChan); + return err; +} + // Return the index-th physical channel, or empty string if no such channel static bool GetAIPhysChan(OScDev_Device *device, int index, ss8str *chan) { if (index < 0) { diff --git a/src/DAQConfig.h b/src/DAQConfig.h index 0760a15..3c2dacb 100644 --- a/src/DAQConfig.h +++ b/src/DAQConfig.h @@ -6,6 +6,11 @@ #include #include +#include + +bool ComputeAORateHz(double pixelRateHz, double timebaseHz, double aoMaxHz, + double *aoRateHz); +OScDev_RichError *EnsureTimingCapsQueried(OScDev_Device *device); void SetWaveformParamsFromDevice(OScDev_Device *device, struct WaveformParams *parameters, OScDev_Acquisition *acq); diff --git a/src/DeviceImplData.h b/src/DeviceImplData.h index e60e2bb..f7ead30 100644 --- a/src/DeviceImplData.h +++ b/src/DeviceImplData.h @@ -36,7 +36,11 @@ struct DeviceImplData { bool scannerOnly; - double aoRateHz; + bool timingCapsQueried; // guards the one-time query + double sampClkTimebaseHz; // from DAQmxGetSampClkTimebaseRate + double aoMaxRateHz; // from DAQmxGetDevAOMaxRate + + double aoRateHz; // Derived from pixel rate; overwritten per acquisition double undershootUs; double scanPhaseUs; double retraceScaleUsPerVolt; diff --git a/src/OpenScanDevice.c b/src/OpenScanDevice.c index bd96983..dbe05d1 100644 --- a/src/OpenScanDevice.c +++ b/src/OpenScanDevice.c @@ -133,15 +133,22 @@ static OScDev_Error NIDAQHasDetector(OScDev_Device *device, static OScDev_Error NIDAQGetPixelRates(OScDev_Device *device, OScDev_NumRange **pixelRatesHz) { - (void)device; // Unused static const double ratesMHz[] = { 0.0500, 0.1000, 0.1250, 0.2000, 0.2500, 0.4000, 0.5000, 0.6250, 1.0000, 1.2500, 0.0 // End mark }; + OScDev_RichError *err = EnsureTimingCapsQueried(device); + if (err) + return OScDev_Error_ReturnAsCode(err); + double timebaseHz = GetImplData(device)->sampClkTimebaseHz; + double aoMaxHz = GetImplData(device)->aoMaxRateHz; *pixelRatesHz = OScDev_NumRange_CreateDiscrete(); for (size_t i = 0; ratesMHz[i] != 0.0; ++i) { - OScDev_NumRange_AppendDiscrete(*pixelRatesHz, 1e6 * ratesMHz[i]); + double rate = 1e6 * ratesMHz[i]; + double aoRate; + if (ComputeAORateHz(rate, timebaseHz, aoMaxHz, &aoRate)) + OScDev_NumRange_AppendDiscrete(*pixelRatesHz, rate); } return OScDev_OK; } diff --git a/src/OpenScanSettings.c b/src/OpenScanSettings.c index 4115b32..038ce30 100644 --- a/src/OpenScanSettings.c +++ b/src/OpenScanSettings.c @@ -33,35 +33,6 @@ GetNumericConstraintTypeImpl_Range(OScDev_Setting *setting, return OScDev_OK; } -static OScDev_Error GetAOSampleRate(OScDev_Setting *setting, double *value) { - *value = GetSettingDeviceData(setting)->aoRateHz; - return OScDev_OK; -} - -static OScDev_Error SetAOSampleRate(OScDev_Setting *setting, double value) { - GetSettingDeviceData(setting)->aoRateHz = value; - GetSettingDeviceData(setting)->clockConfig.mustReconfigureTiming = true; - GetSettingDeviceData(setting)->scannerConfig.mustReconfigureTiming = true; - GetSettingDeviceData(setting)->clockConfig.mustRewriteOutput = true; - GetSettingDeviceData(setting)->scannerConfig.mustRewriteOutput = true; - return OScDev_OK; -} - -static OScDev_Error GetAOSampleRateRange(OScDev_Setting *setting, double *min, - double *max) { - (void)setting; - *min = 100000.0; - *max = 1000000.0; - return OScDev_OK; -} - -static OScDev_SettingImpl SettingImpl_AOSampleRate = { - .GetFloat64 = GetAOSampleRate, - .SetFloat64 = SetAOSampleRate, - .GetNumericConstraintType = GetNumericConstraintTypeImpl_Range, - .GetFloat64Range = GetAOSampleRateRange, -}; - static OScDev_Error GetUndershoot(OScDev_Setting *setting, double *value) { *value = GetSettingDeviceData(setting)->undershootUs; return OScDev_OK; @@ -356,14 +327,6 @@ OScDev_Error NIDAQMakeSettings(OScDev_Device *device, *settings = OScDev_PtrArray_Create(); - OScDev_Setting *aoSampleRate; - err = OScDev_Error_AsRichError(OScDev_Setting_Create( - &aoSampleRate, "AO Sample Rate (Hz)", OScDev_ValueType_Float64, - &SettingImpl_AOSampleRate, device)); - if (err) - goto error; - OScDev_PtrArray_Append(*settings, aoSampleRate); - OScDev_Setting *undershoot; err = OScDev_Error_AsRichError(OScDev_Setting_Create( &undershoot, "Undershoot (us)", OScDev_ValueType_Float64, From 6133def5c741db0fa0eb567dd4de87c77057215d Mon Sep 17 00:00:00 2001 From: "Mark A. Tsuchida" Date: Thu, 21 May 2026 21:27:47 +0900 Subject: [PATCH 3/3] Use 3rd AO channel (if present) for laser blanking Adjustable on/off voltages and lead/lag time. Allow to disable blanking (= turn on laser) for alignment, etc. (Assisted by Claude Code; any errors are mine.) --- WaveformTool/WaveformTool.c | 6 ++ src/DAQConfig.c | 41 ++++++++++ src/DAQConfig.h | 5 ++ src/DeviceImplData.c | 7 ++ src/DeviceImplData.h | 7 ++ src/OpenScanSettings.c | 154 ++++++++++++++++++++++++++++++++++++ src/ParkUnpark.c | 14 +++- src/Scanner.c | 84 ++++++++++++++++++-- src/Scanner.h | 5 ++ src/Waveform.c | 45 +++++++++++ src/Waveform.h | 15 ++++ 11 files changed, 373 insertions(+), 10 deletions(-) diff --git a/WaveformTool/WaveformTool.c b/WaveformTool/WaveformTool.c index 1b4fde6..a7c66f3 100644 --- a/WaveformTool/WaveformTool.c +++ b/WaveformTool/WaveformTool.c @@ -404,6 +404,12 @@ static void PopulateParams(const struct Args *args, params->yPark = args->yPark; params->prevXParkVoltage = args->prevXParkVoltage; params->prevYParkVoltage = args->prevYParkVoltage; + params->laserBlankingSupported = false; + params->laserManualOn = false; + params->laserOnVoltage = 1.0; + params->laserOffVoltage = 0.0; + params->laserOnLeadUs = 0.0; + params->laserOnLagUs = 0.0; } static int WriteXYCsv(FILE *f, const double *xy, uint32_t n) { diff --git a/src/DAQConfig.c b/src/DAQConfig.c index ff5f260..ed22219 100644 --- a/src/DAQConfig.c +++ b/src/DAQConfig.c @@ -168,6 +168,12 @@ void SetWaveformParamsFromDevice(OScDev_Device *device, parameters->yPark = GetImplData(device)->yPark; parameters->prevXParkVoltage = GetImplData(device)->prevXParkVoltage; parameters->prevYParkVoltage = GetImplData(device)->prevYParkVoltage; + parameters->laserBlankingSupported = LaserBlankingSupported(device); + parameters->laserManualOn = GetImplData(device)->laserManualOn; + parameters->laserOnVoltage = GetImplData(device)->laserOnVoltage; + parameters->laserOffVoltage = GetImplData(device)->laserOffVoltage; + parameters->laserOnLeadUs = GetImplData(device)->laserOnLeadUs; + parameters->laserOnLagUs = GetImplData(device)->laserOnLagUs; } OScDev_RichError *EnumerateAIPhysChans(OScDev_Device *device) { @@ -219,3 +225,38 @@ int GetNumberOfAIPhysChans(OScDev_Device *device) { } return MAX_PHYSICAL_CHANS; } + +OScDev_RichError *EnumerateAOPhysChans(OScDev_Device *device) { + ss8str *dest = &GetImplData(device)->aoPhysChans; + ss8_set_len(dest, 1024); + ss8_set_front(dest, '\0'); + int32 nierr = DAQmxGetDevAOPhysicalChans( + ss8_cstr(&GetImplData(device)->deviceName), ss8_mutable_cstr(dest), + (uInt32)ss8_len(dest)); + ss8_set_len_to_cstrlen(dest); + ss8_shrink_to_fit(dest); + if (nierr < 0) + return CreateDAQmxError(nierr); + if (ss8_is_empty(dest)) + return OScDev_Error_Create("Device has no AO physical channels"); + return OScDev_RichError_OK; +} + +int GetNumberOfAOPhysChans(OScDev_Device *device) { + const ss8str *chans = &GetImplData(device)->aoPhysChans; + if (ss8_is_empty(chans)) + return 0; + int count = 1; + for (size_t p = ss8_find_ch(chans, 0, ','); p != SIZE_MAX; + p = ss8_find_ch(chans, p + 1, ',')) + ++count; + return count; +} + +bool LaserBlankingSupported(OScDev_Device *device) { + return GetNumberOfAOPhysChans(device) >= 3; +} + +int GetNumberOfScannerAOChannels(OScDev_Device *device) { + return LaserBlankingSupported(device) ? 3 : 2; +} diff --git a/src/DAQConfig.h b/src/DAQConfig.h index 3c2dacb..0d060e4 100644 --- a/src/DAQConfig.h +++ b/src/DAQConfig.h @@ -18,3 +18,8 @@ OScDev_RichError *EnumerateAIPhysChans(OScDev_Device *device); void GetEnabledChannels(OScDev_Device *device, ss8str *chans); int GetNumberOfEnabledChannels(OScDev_Device *device); int GetNumberOfAIPhysChans(OScDev_Device *device); + +OScDev_RichError *EnumerateAOPhysChans(OScDev_Device *device); +int GetNumberOfAOPhysChans(OScDev_Device *device); +bool LaserBlankingSupported(OScDev_Device *device); +int GetNumberOfScannerAOChannels(OScDev_Device *device); diff --git a/src/DeviceImplData.c b/src/DeviceImplData.c index 07fdd8b..4c14bf2 100644 --- a/src/DeviceImplData.c +++ b/src/DeviceImplData.c @@ -27,6 +27,13 @@ void InitializeImplData(struct DeviceImplData *data) { ss8_init(&data->aiPhysChans); data->channelEnabled[0] = true; + ss8_init(&data->aoPhysChans); + data->laserManualOn = false; + data->laserOnVoltage = 0.1; // Avoid high default to reduce danger + data->laserOffVoltage = 0.0; + data->laserOnLeadUs = 5.0; + data->laserOnLagUs = 5.0; + InitializeCriticalSection(&data->frameMutex); InitializeConditionVariable(&data->frameReady); diff --git a/src/DeviceImplData.h b/src/DeviceImplData.h index f7ead30..84a0678 100644 --- a/src/DeviceImplData.h +++ b/src/DeviceImplData.h @@ -64,6 +64,13 @@ struct DeviceImplData { ss8str aiPhysChans; // at least numAIPhysChans elements separated by ", " bool channelEnabled[MAX_PHYSICAL_CHANS]; + ss8str aoPhysChans; // analogous to aiPhysChans + bool laserManualOn; // "Laser On (Disable Blanking)" + double laserOnVoltage; + double laserOffVoltage; + double laserOnLeadUs; + double laserOnLagUs; + // Read, but unprocessed, raw samples; channels interleaved // Leftover data from the previous read, if any, is at the start of the // buffer and consists of rawDataSize samples. diff --git a/src/OpenScanSettings.c b/src/OpenScanSettings.c index 038ce30..2280f8f 100644 --- a/src/OpenScanSettings.c +++ b/src/OpenScanSettings.c @@ -120,6 +120,114 @@ static OScDev_SettingImpl SettingImpl_RetraceScale = { .GetFloat64Range = GetRetraceScaleRange, }; +static OScDev_Error GetLaserOnVoltage(OScDev_Setting *setting, double *value) { + *value = GetSettingDeviceData(setting)->laserOnVoltage; + return OScDev_OK; +} + +static OScDev_Error SetLaserOnVoltage(OScDev_Setting *setting, double value) { + GetSettingDeviceData(setting)->laserOnVoltage = value; + GetSettingDeviceData(setting)->scannerConfig.mustRewriteOutput = true; + OScDev_Device *device = OScDev_Setting_GetImplData(setting); + return OScDev_Error_ReturnAsCode(ApplyIdleLaserOutput(device)); +} + +static OScDev_Error GetLaserVoltageRange(OScDev_Setting *setting, double *min, + double *max) { + (void)setting; + *min = -1.0; + *max = 1.0; + return OScDev_OK; +} + +static OScDev_SettingImpl SettingImpl_LaserOnVoltage = { + .GetFloat64 = GetLaserOnVoltage, + .SetFloat64 = SetLaserOnVoltage, + .GetNumericConstraintType = GetNumericConstraintTypeImpl_Range, + .GetFloat64Range = GetLaserVoltageRange, +}; + +static OScDev_Error GetLaserOffVoltage(OScDev_Setting *setting, + double *value) { + *value = GetSettingDeviceData(setting)->laserOffVoltage; + return OScDev_OK; +} + +static OScDev_Error SetLaserOffVoltage(OScDev_Setting *setting, double value) { + GetSettingDeviceData(setting)->laserOffVoltage = value; + GetSettingDeviceData(setting)->scannerConfig.mustRewriteOutput = true; + OScDev_Device *device = OScDev_Setting_GetImplData(setting); + return OScDev_Error_ReturnAsCode(ApplyIdleLaserOutput(device)); +} + +static OScDev_SettingImpl SettingImpl_LaserOffVoltage = { + .GetFloat64 = GetLaserOffVoltage, + .SetFloat64 = SetLaserOffVoltage, + .GetNumericConstraintType = GetNumericConstraintTypeImpl_Range, + .GetFloat64Range = GetLaserVoltageRange, +}; + +static OScDev_Error GetLaserManualOn(OScDev_Setting *setting, bool *value) { + *value = GetSettingDeviceData(setting)->laserManualOn; + return OScDev_OK; +} + +static OScDev_Error SetLaserManualOn(OScDev_Setting *setting, bool value) { + GetSettingDeviceData(setting)->laserManualOn = value; + GetSettingDeviceData(setting)->scannerConfig.mustRewriteOutput = true; + OScDev_Device *device = OScDev_Setting_GetImplData(setting); + return OScDev_Error_ReturnAsCode(ApplyIdleLaserOutput(device)); +} + +static OScDev_SettingImpl SettingImpl_LaserManualOn = { + .GetBool = GetLaserManualOn, + .SetBool = SetLaserManualOn, +}; + +static OScDev_Error GetLaserOnLead(OScDev_Setting *setting, double *value) { + *value = GetSettingDeviceData(setting)->laserOnLeadUs; + return OScDev_OK; +} + +static OScDev_Error SetLaserOnLead(OScDev_Setting *setting, double value) { + GetSettingDeviceData(setting)->laserOnLeadUs = value; + GetSettingDeviceData(setting)->scannerConfig.mustRewriteOutput = true; + return OScDev_OK; +} + +static OScDev_Error GetLaserLeadLagRange(OScDev_Setting *setting, double *min, + double *max) { + (void)setting; + *min = 0.0; + *max = 100.0; + return OScDev_OK; +} + +static OScDev_SettingImpl SettingImpl_LaserOnLead = { + .GetFloat64 = GetLaserOnLead, + .SetFloat64 = SetLaserOnLead, + .GetNumericConstraintType = GetNumericConstraintTypeImpl_Range, + .GetFloat64Range = GetLaserLeadLagRange, +}; + +static OScDev_Error GetLaserOnLag(OScDev_Setting *setting, double *value) { + *value = GetSettingDeviceData(setting)->laserOnLagUs; + return OScDev_OK; +} + +static OScDev_Error SetLaserOnLag(OScDev_Setting *setting, double value) { + GetSettingDeviceData(setting)->laserOnLagUs = value; + GetSettingDeviceData(setting)->scannerConfig.mustRewriteOutput = true; + return OScDev_OK; +} + +static OScDev_SettingImpl SettingImpl_LaserOnLag = { + .GetFloat64 = GetLaserOnLag, + .SetFloat64 = SetLaserOnLag, + .GetNumericConstraintType = GetNumericConstraintTypeImpl_Range, + .GetFloat64Range = GetLaserLeadLagRange, +}; + static OScDev_Error GetParkingPositionX(OScDev_Setting *setting, int32_t *xPark) { *xPark = GetSettingDeviceData(setting)->xPark; @@ -325,6 +433,10 @@ OScDev_Error NIDAQMakeSettings(OScDev_Device *device, if (err) return OScDev_Error_ReturnAsCode(err); + err = EnumerateAOPhysChans(device); + if (err) + return OScDev_Error_ReturnAsCode(err); + *settings = OScDev_PtrArray_Create(); OScDev_Setting *undershoot; @@ -424,6 +536,48 @@ OScDev_Error NIDAQMakeSettings(OScDev_Device *device, goto error; OScDev_PtrArray_Append(*settings, inputVoltageRange); + if (LaserBlankingSupported(device)) { + OScDev_Setting *laserOnVoltage; + err = OScDev_Error_AsRichError(OScDev_Setting_Create( + &laserOnVoltage, "Laser On Voltage (V)", OScDev_ValueType_Float64, + &SettingImpl_LaserOnVoltage, device)); + if (err) + goto error; + OScDev_PtrArray_Append(*settings, laserOnVoltage); + + OScDev_Setting *laserOffVoltage; + err = OScDev_Error_AsRichError(OScDev_Setting_Create( + &laserOffVoltage, "Laser Off Voltage (V)", + OScDev_ValueType_Float64, &SettingImpl_LaserOffVoltage, device)); + if (err) + goto error; + OScDev_PtrArray_Append(*settings, laserOffVoltage); + + OScDev_Setting *laserManualOn; + err = OScDev_Error_AsRichError(OScDev_Setting_Create( + &laserManualOn, "Laser On (Disable Blanking)", + OScDev_ValueType_Bool, &SettingImpl_LaserManualOn, device)); + if (err) + goto error; + OScDev_PtrArray_Append(*settings, laserManualOn); + + OScDev_Setting *laserOnLead; + err = OScDev_Error_AsRichError(OScDev_Setting_Create( + &laserOnLead, "Laser On Lead (us)", OScDev_ValueType_Float64, + &SettingImpl_LaserOnLead, device)); + if (err) + goto error; + OScDev_PtrArray_Append(*settings, laserOnLead); + + OScDev_Setting *laserOnLag; + err = OScDev_Error_AsRichError(OScDev_Setting_Create( + &laserOnLag, "Laser On Lag (us)", OScDev_ValueType_Float64, + &SettingImpl_LaserOnLag, device)); + if (err) + goto error; + OScDev_PtrArray_Append(*settings, laserOnLag); + } + return OScDev_OK; error: diff --git a/src/ParkUnpark.c b/src/ParkUnpark.c index 116f782..078ece4 100644 --- a/src/ParkUnpark.c +++ b/src/ParkUnpark.c @@ -61,11 +61,16 @@ OScDev_RichError *WriteUnparkOutput(OScDev_Device *device, struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); + int nch = GetNumberOfScannerAOChannels(device); int32 totalElementsPerFramePerChan = GetParkWaveformSize(¶ms); double *xyWaveformFrame = - (double *)malloc(sizeof(double) * totalElementsPerFramePerChan * 2); + (double *)malloc(sizeof(double) * totalElementsPerFramePerChan * nch); GenerateGalvoUnparkWaveform(¶ms, xyWaveformFrame); + if (nch == 3) + GenerateLaserBlankingConstant(¶ms, totalElementsPerFramePerChan, + xyWaveformFrame + + 2 * totalElementsPerFramePerChan); int32 numWritten = 0; OScDev_RichError *err = CreateDAQmxError(DAQmxWriteAnalogF64( @@ -91,11 +96,16 @@ OScDev_RichError *WriteParkOutput(OScDev_Device *device, struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); + int nch = GetNumberOfScannerAOChannels(device); int32 totalElementsPerFramePerChan = GetParkWaveformSize(¶ms); double *xyWaveformFrame = - (double *)malloc(sizeof(double) * totalElementsPerFramePerChan * 2); + (double *)malloc(sizeof(double) * totalElementsPerFramePerChan * nch); GenerateGalvoParkWaveform(¶ms, xyWaveformFrame); + if (nch == 3) + GenerateLaserBlankingConstant(¶ms, totalElementsPerFramePerChan, + xyWaveformFrame + + 2 * totalElementsPerFramePerChan); GetImplData(device)->prevXParkVoltage = xyWaveformFrame[totalElementsPerFramePerChan - 1]; GetImplData(device)->prevYParkVoltage = diff --git a/src/Scanner.c b/src/Scanner.c index 9d7bbe4..9799aa6 100644 --- a/src/Scanner.c +++ b/src/Scanner.c @@ -13,6 +13,14 @@ #include #include +// Append the scanner AO channel range ("/ao0:1" or "/ao0:2") to +// terms +static void AppendScannerAOTerms(OScDev_Device *device, ss8str *terms) { + ss8_cat(terms, &GetImplData(device)->deviceName); + int nch = GetNumberOfScannerAOChannels(device); + ss8_cat_cstr(terms, nch >= 3 ? "/ao0:2" : "/ao0:1"); +} + static OScDev_RichError *ConfigureScannerTiming(OScDev_Device *device, struct ScannerConfig *config, OScDev_Acquisition *acq) { @@ -39,9 +47,9 @@ static OScDev_RichError *ConfigureScannerTiming(OScDev_Device *device, samplesPerChan = totalSamples; } - err = CreateDAQmxError(DAQmxCfgSampClkTiming(config->aoTask, "", - aoRateHz, DAQmx_Val_Rising, - sampleMode, samplesPerChan)); + err = CreateDAQmxError(DAQmxCfgSampClkTiming(config->aoTask, "", aoRateHz, + DAQmx_Val_Rising, sampleMode, + samplesPerChan)); if (err) { err = OScDev_Error_Wrap(err, "Failed to configure timing for scanner"); return err; @@ -63,11 +71,15 @@ static OScDev_RichError *WriteScannerOutput(OScDev_Device *device, struct WaveformParams params; SetWaveformParamsFromDevice(device, ¶ms, acq); + int nch = GetNumberOfScannerAOChannels(device); int32 totalElementsPerFramePerChan = GetScannerWaveformSize(¶ms); double *xyWaveformFrame = - (double *)malloc(sizeof(double) * totalElementsPerFramePerChan * 2); + (double *)malloc(sizeof(double) * totalElementsPerFramePerChan * nch); GenerateGalvoWaveformFrame(¶ms, xyWaveformFrame); + if (nch == 3) + GenerateLaserBlankingWaveform( + ¶ms, xyWaveformFrame + 2 * totalElementsPerFramePerChan); int32 numWritten = 0; OScDev_RichError *err = CreateDAQmxError(DAQmxWriteAnalogF64( @@ -102,8 +114,8 @@ OScDev_RichError *SetUpScanner(OScDev_Device *device, } ss8str aoTerms; - ss8_init_copy(&aoTerms, &GetImplData(device)->deviceName); - ss8_cat_cstr(&aoTerms, "/ao0:1"); + ss8_init(&aoTerms); + AppendScannerAOTerms(device, &aoTerms); err = CreateDAQmxError(DAQmxCreateAOVoltageChan( config->aoTask, ss8_cstr(&aoTerms), "Galvos", -10.0, 10.0, DAQmx_Val_Volts, NULL)); @@ -196,8 +208,8 @@ OScDev_RichError *CreateScannerTask(OScDev_Device *device, } ss8str aoTerms; - ss8_init_copy(&aoTerms, &GetImplData(device)->deviceName); - ss8_cat_cstr(&aoTerms, "/ao0:1"); + ss8_init(&aoTerms); + AppendScannerAOTerms(device, &aoTerms); err = CreateDAQmxError(DAQmxCreateAOVoltageChan( config->aoTask, ss8_cstr(&aoTerms), "Galvos", -10.0, 10.0, DAQmx_Val_Volts, NULL)); @@ -213,3 +225,59 @@ OScDev_RichError *CreateScannerTask(OScDev_Device *device, } return OScDev_RichError_OK; } + +OScDev_RichError *ApplyIdleLaserOutput(OScDev_Device *device) { + if (!LaserBlankingSupported(device)) + return OScDev_RichError_OK; + + CRITICAL_SECTION *mutex = &GetImplData(device)->acquisition.mutex; + EnterCriticalSection(mutex); + if (GetImplData(device)->acquisition.running) { + LeaveCriticalSection(mutex); + return OScDev_RichError_OK; + } + + struct ScannerConfig *config = &GetImplData(device)->scannerConfig; + OScDev_RichError *err; + + // Recreate the task to drop any leftover sample-clock timing from a prior + // scan; the on-demand task has no timing configured. + err = ShutdownScanner(config); + if (err) { + err = OScDev_Error_Wrap(err, "Failed to reset scanner task for idle " + "laser output"); + goto cleanup; + } + err = CreateScannerTask(device, config); + if (err) + goto cleanup; + + float64 buf[3] = { + GetImplData(device)->prevXParkVoltage, + GetImplData(device)->prevYParkVoltage, + GetImplData(device)->laserManualOn + ? GetImplData(device)->laserOnVoltage + : GetImplData(device)->laserOffVoltage, + }; + + int32 numWritten = 0; + err = CreateDAQmxError(DAQmxWriteAnalogF64(config->aoTask, 1, TRUE, 10.0, + DAQmx_Val_GroupByChannel, buf, + &numWritten, NULL)); + if (err) { + err = OScDev_Error_Wrap(err, "Failed to write idle laser output"); + ShutdownScanner(config); + goto cleanup; + } + + err = CreateDAQmxError(DAQmxStopTask(config->aoTask)); + if (err) { + err = OScDev_Error_Wrap(err, "Failed to stop idle laser output task"); + ShutdownScanner(config); + goto cleanup; + } + +cleanup: + LeaveCriticalSection(mutex); + return err; +} diff --git a/src/Scanner.h b/src/Scanner.h index 2c5dbab..17893da 100644 --- a/src/Scanner.h +++ b/src/Scanner.h @@ -20,3 +20,8 @@ OScDev_RichError *StopScanner(struct ScannerConfig *config); OScDev_RichError *CreateScannerTask(OScDev_Device *device, struct ScannerConfig *config); + +// Drive a single static sample on ao0:2 (galvos held at last park voltages, +// EOM at its current manual/blank level) when no acquisition is running. No-op +// if blanking is unsupported or an acquisition is in progress. +OScDev_RichError *ApplyIdleLaserOutput(OScDev_Device *device); diff --git a/src/Waveform.c b/src/Waveform.c index 7cc52ed..b8123af 100644 --- a/src/Waveform.c +++ b/src/Waveform.c @@ -24,6 +24,14 @@ uint32_t ScanPhaseSamples(const struct WaveformParams *params) { return (uint32_t)round(params->scanPhaseUs * 1e-6 * params->aoRateHz); } +uint32_t LaserOnLeadSamples(const struct WaveformParams *params) { + return (uint32_t)round(params->laserOnLeadUs * 1e-6 * params->aoRateHz); +} + +uint32_t LaserOnLagSamples(const struct WaveformParams *params) { + return (uint32_t)round(params->laserOnLagUs * 1e-6 * params->aoRateHz); +} + uint32_t RetraceSamples(const struct WaveformParams *params) { double amplitude = ScanAmplitude(params); double retraceUs = params->retraceScaleUsPerVolt * amplitude; @@ -234,6 +242,43 @@ void GenerateGalvoWaveformFrame(const struct WaveformParams *parameters, free(yWaveform); } +void GenerateLaserBlankingWaveform(const struct WaveformParams *params, + double *blanking) { + uint32_t lineLength = (uint32_t)GetLineWaveformSize(params); + uint32_t height = params->height; + double onV = params->laserOnVoltage; + + if (params->laserManualOn) { + for (uint32_t i = 0; i < lineLength * height; ++i) + blanking[i] = onV; + return; + } + + double offV = params->laserOffVoltage; + uint32_t highStart = UndershootSamples(params) + ScanPhaseSamples(params); + uint32_t scanSmp = ScanSamples(params); + uint32_t leadSmp = LaserOnLeadSamples(params); + uint32_t lagSmp = LaserOnLagSamples(params); + + uint32_t onStart = highStart > leadSmp ? highStart - leadSmp : 0; + uint32_t onEnd = highStart + scanSmp + lagSmp; + if (onEnd > lineLength) + onEnd = lineLength; + + for (uint32_t j = 0; j < height; ++j) + for (uint32_t i = 0; i < lineLength; ++i) + blanking[i + j * lineLength] = + (i >= onStart && i < onEnd) ? onV : offV; +} + +void GenerateLaserBlankingConstant(const struct WaveformParams *params, + uint32_t length, double *out) { + double v = params->laserManualOn ? params->laserOnVoltage + : params->laserOffVoltage; + for (uint32_t i = 0; i < length; ++i) + out[i] = v; +} + static void InverseTransform2x2(const double *m, double tx, double ty, double ox, double oy, double *lx, double *ly) { double det = m[0] * m[3] - m[1] * m[2]; diff --git a/src/Waveform.h b/src/Waveform.h index d8f143a..4fe54e2 100644 --- a/src/Waveform.h +++ b/src/Waveform.h @@ -1,5 +1,6 @@ #pragma once +#include #include struct WaveformParams { @@ -23,6 +24,13 @@ struct WaveformParams { int32_t yPark; double prevXParkVoltage; double prevYParkVoltage; + + bool laserBlankingSupported; // device has >=3 AO + bool laserManualOn; // laser always on (blanking disabled) + double laserOnVoltage; + double laserOffVoltage; + double laserOnLeadUs; + double laserOnLagUs; }; uint32_t UndershootSamples(const struct WaveformParams *params); @@ -49,3 +57,10 @@ void GenerateGalvoUnparkWaveform(const struct WaveformParams *parameters, double *xyWaveformFrame); void GenerateGalvoParkWaveform(const struct WaveformParams *parameters, double *xyWaveformFrame); + +uint32_t LaserOnLeadSamples(const struct WaveformParams *params); +uint32_t LaserOnLagSamples(const struct WaveformParams *params); +void GenerateLaserBlankingWaveform(const struct WaveformParams *params, + double *blanking); +void GenerateLaserBlankingConstant(const struct WaveformParams *params, + uint32_t length, double *out);