fix(panda): wait for the late replayed PULSE train after a parameter change

The PULSE blocks replay a full train ~0.3-0.6 s after a parameter write, without a
trigger. configure_line returned after 50 ms, so the train ran into the first line and
blocked its trigger (line counter 0). Now both PULSE outputs must stay low for
PARAM_QUIET_TIME (1 s); only on a parameter change, i.e. once per scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M8zoDyJxbBLmjZCEPUo5FM
This commit is contained in:
Mirko HollerandClaude Opus 5.5 committed 2026-10-29 11:20:16 +01:00
1 parent 1da6330d3c
commit b85f8f9c38
3 files changed
+38 -14

No files matched your search

+2 -1
View File
@@ -165,5 +165,6 @@ edges.
PandA FPGA 4.1 PULSE block: an ENABLE edge, a parameter write or a dropped trigger often
makes it replay the previous pulse train without a trigger. The device keeps the PULSE
blocks enabled, writes their parameters only when they change (once per scan) and then waits
until such a train is over (pulses can appear on TTLOUT2/3 meanwhile), and the trigger path
until such a train is over (pulses can appear on TTLOUT2/3 meanwhile; the train starts only
~0.3-0.6 s after the write, so both outputs must stay low for 1 s), and the trigger path
filters out edges during a line, so no trigger is ever dropped by a PULSE block.
@@ -22,7 +22,9 @@ any trigger. With unchanged parameters and ENABLE kept high, hundreds of trigger
a row were correct. So the PULSE blocks stay enabled, their parameters are only written
when they change (once per scan), followed by a wait until a replayed train is over, and
edges during a line are filtered out before the PULSE blocks. Replayed trains during a
parameter change do appear on TTLOUT2/3.
parameter change do appear on TTLOUT2/3. The replayed train starts only ~0.3-0.6 s after
the write (seen 2026-10-29), so configure_line waits until both PULSE outputs stayed low
for PARAM_QUIET_TIME.
Only the control port (8888) is used: the fields are written by setup_layout() at connect,
the PCAP data capture is not touched. Timing resolution 8 ns (125 MHz FPGA clock).
@@ -129,6 +131,8 @@ class PandaLineTrigger(Device):
TRIGGER_COUNTER = "COUNTER6"
LINE_COUNTER = "COUNTER7"
PULSE_COUNTER = "COUNTER8"
# a replayed train starts up to ~0.6 s after a parameter write
PARAM_QUIET_TIME = 1.0
SHUTTER_OUTPUT = "TTLOUT2"
DETECTOR_OUTPUT = "TTLOUT3"
@@ -282,25 +286,22 @@ class PandaLineTrigger(Device):
self._pulse_params = params
def _wait_pulses_idle(self, timeout: float) -> None:
"""Wait until a replayed train is over: stable counter and both PULSE outputs low."""
"""Wait until a replayed train is over: both PULSE outputs low for PARAM_QUIET_TIME."""
sp, dp = self.SHUTTER_PULSE, self.DETECTOR_PULSE
deadline = time.time() + timeout
last = None
deadline = time.time() + timeout + self.PARAM_QUIET_TIME
last_active = time.time()
while True:
time.sleep(0.02)
state = (
self.panda.get(f"{self.PULSE_COUNTER}.OUT"),
self.panda.get(f"{sp}.OUT"),
self.panda.get(f"{dp}.OUT"),
)
if state == last and state[1:] == ("0", "0"):
time.sleep(0.005)
now = time.time()
if self.panda.get(f"{sp}.OUT") != "0" or self.panda.get(f"{dp}.OUT") != "0":
last_active = now
elif now - last_active >= self.PARAM_QUIET_TIME:
return
if time.time() > deadline:
if now > deadline:
raise PandaLineTriggerError(
f"{self.name}: PULSE blocks still active {timeout:.1f} s after a parameter"
" change."
)
last = state
def arm(self, one_shot: bool = False) -> None:
"""
@@ -52,6 +52,7 @@ class FakePanda(PandaControl):
@pytest.fixture
def trigger():
device = PandaLineTrigger(name="pandatrig", host="fake-panda")
device.PARAM_QUIET_TIME = 0.01
device.panda = FakePanda()
device.wait_for_connection()
return device
@@ -219,3 +220,24 @@ def test_falling_edge_inverts_the_input():
def test_invalid_trigger_edge():
with pytest.raises(ValueError, match="trigger_edge"):
PandaLineTrigger(name="pandatrig", host="fake-panda", trigger_edge="both")
def test_configure_line_waits_for_a_late_replayed_train(trigger):
"""The replayed train starts only after the write; configure_line must outlast it."""
import threading
import time
trigger.PARAM_QUIET_TIME = 0.2
fields = trigger.panda.fields
def replay():
time.sleep(0.05)
fields["PULSE4.OUT"] = "1"
time.sleep(0.05)
fields["PULSE4.OUT"] = "0"
threading.Thread(target=replay, daemon=True).start()
t0 = time.time()
trigger.configure_line(5, 0.01)
# the train ended at ~0.1 s, then 0.2 s quiet
assert time.time() - t0 >= 0.28