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:
1 parent
1da6330d3c
commit
b85f8f9c38
3 files changed
+38
-14
No files matched your search
@@ -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
|
||||
Reference in new issue
Block a user