From 3aca5030a2f36aa70bca1b7632a4b92a63db202d Mon Sep 17 00:00:00 2001 From: Mirko Holler Date: Thu, 29 Oct 2026 12:08:34 +0100 Subject: [PATCH] fix(aerotech): position capture latches the gate start (DATAACQ trigger 3) Measured with TTL gates on J12 at 2 mm/s: trigger 0 ("UINT1 rising" in the A3200 help) latched at the gate end (+17.2 um for a 10 ms gate, expected +20), trigger 3 at the gate start (-3.0 um). The Npaq UINT input is inverted compared with the help. Default trigger_input is now 3. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM --- csaxs_bec/devices/aerotech/README.md | 6 ++++-- csaxs_bec/devices/aerotech/a3200.py | 19 +++++++++++++------ csaxs_bec/devices/sim/sim_a3200.py | 2 +- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/csaxs_bec/devices/aerotech/README.md b/csaxs_bec/devices/aerotech/README.md index d04ec221..9d8282c2 100644 --- a/csaxs_bec/devices/aerotech/README.md +++ b/csaxs_bec/devices/aerotech/README.md @@ -94,7 +94,9 @@ scans.aerotech_cont_grid(dev.aerox, 0, 1, 0.01, dev.aeroy, 0, 1, 0.1, exp_time=0 the scan); X reaches 1 um within ~60 ms but 10 nm only after 0.5-2 s - `capture_positions=True` (default False until commissioned): device `aerocapture` latches the encoder counters on each detector trigger (DATAACQ on UINT1, up to 16 kHz, 4 kHz with - MXR sources); after each line they are read back (~2600 values/s over the ASCII interface) + MXR sources). The Npaq input is inverted compared with the A3200 help (measured + 2026-10-29): DATAACQ trigger 3 latches at the rising TTL edge at J12 (gate start, the + default), trigger 0 at the falling one (gate end); after each line they are read back (~2600 values/s over the ASCII interface) and published as async data `aerocapture_positions_enc`. The counter offset is calibrated in pre_scan with one software PSO fire, before any trigger receiver is armed. @@ -150,7 +152,7 @@ lines run back to back without re-arming. |-------|-----|--------| | TTLIN3 (50 Ω) | line trigger | Npaq J7 opto output: J7-25 (+5 V) -> J7-6, J7-7 -> TTLIN3, J7-24 -> TTLIN3 ground | | TTLOUT2 | "shutter": trigger edge to end of the last gate | scope | -| TTLOUT3 | "detector" gates (high during exposure): n pulses, width exp_time - readout_time, period exp_time, first one shutter_delay after the edge | scope, later Npaq J12 pin 1 (+) / pin 2 (-) for position sampling (rising edge = exposure start) | +| TTLOUT3 | "detector" gates (high during exposure): n pulses, width exp_time - readout_time, period exp_time, first one shutter_delay after the edge | scope and Npaq J12 pin 1 (+) / pin 2 (-) for position sampling (rising edge = exposure start, DATAACQ trigger 3) | Blocks: LUT8 = TTLIN3 | BITS.A (software trigger); SRGATE1 arm gate; LUT7 = rising edge of LUT8 & SRGATE1 & no line running (PULSE3/PULSE4 low) -> PULSE3 (shutter), PULSE4 (gates). diff --git a/csaxs_bec/devices/aerotech/a3200.py b/csaxs_bec/devices/aerotech/a3200.py index a4f35d3b..b4838345 100644 --- a/csaxs_bec/devices/aerotech/a3200.py +++ b/csaxs_bec/devices/aerotech/a3200.py @@ -394,7 +394,8 @@ class A3200Controller(Controller): def dataacq_arm(self, axis: str, trigger: int, channels: list[int], n_triggers: int) -> None: """ Capture the source signals `channels` on each trigger into the drive array, - starting at index 0. Npaq trigger 0 = Input 12 (UINT1) rising edge, 6 = PSO firing. + starting at index 0. Npaq trigger 0 / 3 = Input 12 (UINT1) "rising" / "falling" edge + (measured: 3 latches when the TTL level at J12 rises), 6 = PSO firing. Npaq sources: 0-5 local encoder counter, 6-11 encoder multiplier (MXR) channel 0-5. """ self.command(f"DATAACQ {axis} 1 OFF") @@ -774,7 +775,12 @@ class A3200Motor(Device, PositionerBase): # Npaq DATAACQ position sources: 0-5 local encoder counter, 6-11 encoder multiplier (MXR) # channel 0-5 (max. 4 kHz trigger rate); 12-15 are analog inputs, not used here. DATAACQ_POSITION_SOURCES = 12 -DATAACQ_TRIGGER_UINT1_RISING = 0 +# Npaq DATAACQ triggers on UINT1 (J12 pin 1+/2-). Measured 2026-10-29 with a TTL gate on +# J12 (PandA TTLOUT3, X at 2 mm/s): trigger 3 ("falling edge" in the A3200 help) latches at +# the rising TTL edge (start of the exposure gate), trigger 0 ("rising edge") at the falling +# TTL edge (end of the gate). The opto input is inverted compared with the help. +DATAACQ_TRIGGER_UINT1_TTL_RISING = 3 +DATAACQ_TRIGGER_UINT1_TTL_FALLING = 0 DATAACQ_TRIGGER_PSO_FIRING_EVENT = 6 @@ -782,8 +788,8 @@ class A3200PositionCapture(PSIDeviceBase): """ Latch axis positions on an external trigger (Npaq DATAACQ, drive array). - Each rising edge on the trigger input (default: Input 12 / UINT1, the detector - exposure trigger) stores the encoder counters of the configured channels in the + Each trigger edge (default: rising TTL edge on Input 12 / UINT1, i.e. the start of the + detector exposure gate) stores the encoder counters of the configured channels in the drive array. After a scan line, read_line() reads them back, converts them to user units (CountsPerUnit of the axis) and publishes them as async data (signals enc). @@ -794,7 +800,8 @@ class A3200PositionCapture(PSIDeviceBase): its PositionFeedbackChannel, by default its axis index in the Npaq. Values are in controller coordinates (the BEC motor sign is not applied). drive_axis: any axis of the Npaq, used to address the drive (shared array). - trigger_input: DATAACQ trigger, 0 = Input 12 (UINT1) rising edge. + trigger_input: DATAACQ trigger, 3 = rising TTL edge at UINT1 (gate start, default), + 0 = falling TTL edge (gate end); see DATAACQ_TRIGGER_UINT1_TTL_RISING. """ USER_ACCESS = ["controller", "arm", "read_line", "calibrate", "disarm"] @@ -819,7 +826,7 @@ class A3200PositionCapture(PSIDeviceBase): port=8000, channels=None, drive_axis="X", - trigger_input=DATAACQ_TRIGGER_UINT1_RISING, + trigger_input=DATAACQ_TRIGGER_UINT1_TTL_RISING, socket_cls=SocketIO, device_manager=None, scan_info=None, diff --git a/csaxs_bec/devices/sim/sim_a3200.py b/csaxs_bec/devices/sim/sim_a3200.py index acd03e82..c6ad6ab5 100644 --- a/csaxs_bec/devices/sim/sim_a3200.py +++ b/csaxs_bec/devices/sim/sim_a3200.py @@ -166,7 +166,7 @@ class SimA3200State: if not (min(start, target) <= low and high <= max(start, target)): return self.pso_window_entries += 1 - if acq["armed"] and acq["trigger"] == 0 and acq["inputs"]: + if acq["armed"] and acq["trigger"] in (0, 3) and acq["inputs"]: n_triggers = acq["n_elements"] // len(acq["inputs"]) step = (high - low) / max(n_triggers, 1) for k in range(n_triggers):