@@ -102,12 +102,21 @@ def expected_ela_version_reg() -> int:
102102_ADDR_SQ_MASK = 0x0038
103103_ADDR_DATA_BASE = 0x0100
104104
105+ _STATUS_ARMED = 1 << 0
106+ _STATUS_TRIGGERED = 1 << 1
105107_STATUS_DONE = 1 << 2
106108_STATUS_OVERFLOW = 1 << 3
109+ # Capture FSM is "busy" (i.e. not idle) if any of armed/triggered/done is set.
110+ _STATUS_BUSY = _STATUS_ARMED | _STATUS_TRIGGERED | _STATUS_DONE
107111
108112_CTRL_ARM = 1 << 0
109113_CTRL_RESET = 1 << 1
110114
115+ # force_idle(): per-reset settle window. A re-armed core (startup-arm CDC race)
116+ # reveals itself within a couple of sample clocks of reset deassert, so a short
117+ # bounded wait suffices before spending another reset.
118+ _FORCE_IDLE_SETTLE_S = 0.05
119+
111120_TRIG_VALUE_MATCH = 1 << 0
112121_TRIG_EDGE_DETECT = 1 << 1
113122
@@ -197,7 +206,10 @@ class CaptureConfig:
197206 stor_qual_mode : int = 0 # 0=disabled; 1=store when match, 2=store when no match
198207 stor_qual_value : int = 0 # storage qualification comparison value
199208 stor_qual_mask : int = 0 # storage qualification mask
200- startup_arm : bool = False # if True, reset leaves the core armed
209+ startup_arm : bool = False # runtime STARTUP_ARM register (0xD8): if True a
210+ # soft reset re-arms the core. Distinct from the
211+ # compile-time STARTUP_ARM RTL parameter, which
212+ # sets the power-up (boot-armed) value.
201213 trigger_holdoff : int = 0 # sample-clock cycles to ignore triggers after arm/re-arm
202214 trigger_delay : int = 0 # post-trigger delay in sample-clock cycles
203215 # (0..65535) — shifts the committed trigger
@@ -329,6 +341,101 @@ def reset(self) -> None:
329341 self ._select_instance ()
330342 self .transport .write_reg (_ADDR_CTRL , _CTRL_RESET )
331343
344+ def force_idle (
345+ self ,
346+ timeout : float = 1.0 ,
347+ poll_interval : float = 0.01 ,
348+ max_resets : int = 3 ,
349+ ) -> None :
350+ """Drive the core to a *verified* idle state before arming.
351+
352+ A bitstream built with ``STARTUP_ARM=1`` (e.g. the Arty example) comes
353+ up already armed -- it may even be triggered/done -- before the host
354+ issues a single command. Arming and capturing on top of that yields a
355+ valid but *unexpected* window. ``force_idle`` issues a soft reset and
356+ confirms the core actually settled to idle (armed=0, triggered=0,
357+ done=0) so the next capture is deterministic.
358+
359+ This is a *soft* reset only: it clears the sample-domain capture FSM
360+ (armed/triggered/done and the buffer pointers). It does **not** touch
361+ configuration registers, so the canonical sequence is::
362+
363+ a.configure(cfg) # cfg.startup_arm defaults False, clearing the
364+ # runtime STARTUP_ARM register (addr 0xD8)
365+ a.force_idle()
366+ a.arm()
367+ result = a.capture()
368+
369+ Note the boot-armed behavior comes from the *compile-time* ``STARTUP_ARM``
370+ RTL parameter, which seeds the auto-arm flag at configuration time and
371+ cannot be changed from the host. :meth:`configure` instead clears the
372+ *runtime* STARTUP_ARM register (addr 0xD8), which overrides that flag for
373+ subsequent resets.
374+
375+ A startup-armed core re-loads its auto-arm flag from that register on
376+ every soft reset, so a single reset can race the configuration
377+ clock-domain crossing and come back still armed. ``force_idle`` polls
378+ STATUS after the reset and, if the core re-armed, resets again -- by which
379+ point the runtime STARTUP_ARM register cleared by :meth:`configure` has
380+ crossed into the sample domain -- up to ``max_resets`` times.
381+
382+ Timing assumption: a STATUS read round-trips over JTAG far slower than
383+ the core's reset-to-re-arm latency (a couple of sample clocks), so the
384+ first post-reset STATUS read reliably observes the settled state. This
385+ holds for any practical sample clock; only a sample clock so slow that a
386+ full JTAG access spans fewer than ~2 sample cycles could read a
387+ transient idle before a pending re-arm propagates.
388+
389+ Note this is intentionally lossy: it discards any in-flight or
390+ startup-armed capture. Workflows that *want* the from-boot capture
391+ (including tests that validate ``STARTUP_ARM`` behavior) should not call
392+ it and should use :meth:`reset`/:meth:`arm` directly.
393+
394+ Args:
395+ timeout: overall budget, in seconds, to reach idle across all resets.
396+ poll_interval: STATUS poll spacing in seconds.
397+ max_resets: maximum soft resets to attempt.
398+
399+ Raises:
400+ RuntimeError: if the core is still busy after ``max_resets`` resets
401+ (e.g. ``STARTUP_ARM`` is still enabled because :meth:`configure`
402+ was not called first).
403+ """
404+ read_status = getattr (
405+ self .transport , "read_reg_verified" , self .transport .read_reg
406+ )
407+ deadline = time .monotonic () + timeout
408+ last_status = - 1
409+ attempt = 0
410+ for attempt in range (max (1 , max_resets )):
411+ with self .transport .transaction_lock ():
412+ self ._select_instance ()
413+ self .transport .write_reg (_ADDR_CTRL , _CTRL_RESET )
414+ # A re-armed core (startup-arm CDC race) will not clear on its own,
415+ # so cap each attempt's wait and fall through to another reset.
416+ attempt_deadline = time .monotonic () + _FORCE_IDLE_SETTLE_S
417+ while True :
418+ with self .transport .transaction_lock ():
419+ self ._select_instance ()
420+ last_status = read_status (_ADDR_STATUS )
421+ if (last_status & _STATUS_BUSY ) == 0 :
422+ return
423+ now = time .monotonic ()
424+ if now >= attempt_deadline or now >= deadline :
425+ break
426+ time .sleep (poll_interval )
427+ if time .monotonic () >= deadline :
428+ break
429+ raise RuntimeError (
430+ f"force_idle: core still busy after { attempt + 1 } reset(s); "
431+ f"last STATUS=0x{ last_status & 0xFFFFFFFF :08x} "
432+ f"(armed={ bool (last_status & _STATUS_ARMED )} , "
433+ f"triggered={ bool (last_status & _STATUS_TRIGGERED )} , "
434+ f"done={ bool (last_status & _STATUS_DONE )} ). "
435+ f"If the core keeps re-arming, the runtime STARTUP_ARM register is "
436+ f"still set -- call configure() (startup_arm=False) before force_idle()."
437+ )
438+
332439 @staticmethod
333440 def _validate_probes (probes : List [ProbeSpec ], sample_width : int ) -> None :
334441 occupied : set [int ] = set ()
@@ -758,6 +865,7 @@ def capture_continuous(
758865 self ,
759866 count : int = 0 ,
760867 timeout_per : float = 10.0 ,
868+ force_idle : bool = False ,
761869 ):
762870 """Generator that yields successive ``CaptureResult`` objects.
763871
@@ -766,9 +874,18 @@ def capture_continuous(
766874 iteration calls :meth:`arm`, then :meth:`capture` to wait for the next
767875 trigger and read back. Yields *count* results, or runs indefinitely if
768876 *count* is 0.
877+
878+ On a bitstream that boots with ``STARTUP_ARM=1`` the first iteration
879+ would otherwise inherit the power-up capture window. Pass
880+ ``force_idle=True`` to call :meth:`force_idle` once before the first
881+ arm so the stream starts from a known-idle state. Defaults to ``False``
882+ to preserve the original behavior (and leave the from-boot window
883+ observable for callers that want it).
769884 """
770885 if self ._config is None :
771886 raise RuntimeError ("call configure() before capture_continuous()" )
887+ if force_idle :
888+ self .force_idle ()
772889 yielded = 0
773890 while count == 0 or yielded < count :
774891 self .arm ()
0 commit comments