Skip to main content

Timing measurements

There are two ways to get numbers out of a capture, and they suit different jobs:

  • Measurements: ask the server for the duration, the frequency, the pulse widths, the duty cycle or the edge count between two points. Few calls, computed on the server, and saved into the .scana.
  • Raw transitions: read the edges and do the arithmetic yourself. This is what you want for a distribution: every pulse width, not one summary.

Measurements between two markers​

from ikalogic_scanastudio import ScanaStudio

with ScanaStudio.connect() as server:
workspace = server.create("se254")
last = workspace.capture.run(samples=500_000, sample_rate=25_000_000)

tx = workspace.channel("UART TX")

# Over the whole capture, without placing markers by hand.
# No `kinds`: the standard set.
workspace.measures.between(0, last, channel=tx)

# And separately, the edge count and the duty cycle.
workspace.measures.between(0, last, channel=tx, kinds=["edges", "duty_avg"])

for measure in workspace.measures.wait(timeout=60.0):
for result in measure.results:
print(f"{result.kind:12} {result.value}")
# time_total 0.02
# freq_avg 51976.60
# freq_min 14637.00
# freq_max 120192.31
# edges 106.0
# duty_avg 0.52

workspace.close()

measures.between() places the markers for you. If you want to place them yourself, for instance so that they are visible when someone opens the .scana in ScanaStudio, use measures.add:

start = workspace.markers.add(sample=0)
stop = workspace.markers.add(sample=last)
workspace.measures.add(start, stop, channel=tx, kinds=["high_shortest", "low_shortest"])

The measurement kinds​

kinds is a list of names from a closed set of 18. Leave it out, or pass an empty list, and you get the standard set: time_total, freq_avg, freq_min and freq_max.

KindsWhat they report
time_total, time_high, time_lowTime between the markers, and the total time spent high and low, in seconds
freq_avg, freq_min, freq_maxAverage frequency, and the lowest and highest frequency of a single period, in hertz
high_avg, high_shortest, high_longestAverage, shortest and longest high pulse, in seconds
low_avg, low_shortest, low_longestAverage, shortest and longest low pulse, in seconds
duty_avg, duty_min, duty_maxAverage, lowest and highest duty cycle, as a ratio from 0 to 1
edges, rising_edges, falling_edgesEdge counts

Each result carries the name you asked for in result.kind. Any other name is refused before the request reaches the server: Python raises ValueError, NodeJS raises RangeError, Rust only accepts MeasureKind values, and C's ss_measure_add returns 0. The lists are exported as MEASURE_KINDS and STANDARD_MEASURE_KINDS (Rust: MeasureKind::ALL and MeasureKind::STANDARD; C: ss_measure_kinds()). See Markers and measurements.

Example output. The two measurements of the first example, over a 500 000-sample window on the se254 demo:

time_total 0.02
freq_avg 51976.60058470689
freq_min 14637.002341920375
freq_max 120192.30769230769
edges 106.0
duty_avg 0.5194570135746607

Every pulse, from the raw transitions​

When you need the distribution rather than a summary, read the edges and compute it yourself.

import statistics

from ikalogic_scanastudio import ScanaStudio


def main(device: str = "se254", channel: int = 0) -> int:
with ScanaStudio.connect() as server:
workspace = server.create(device)
workspace.capture.run(samples=500_000, sample_rate=25_000_000)

# Divide by the rate the DEVICE settled on, not the one we asked for.
rate = workspace.capture.sample_rate
edges = list(workspace.data.transitions(channel=int(channel)))

# A high pulse is the span from a rising edge to the next edge.
widths = [
(b.sample - a.sample) / rate
for a, b in zip(edges, edges[1:], strict=False)
if a.level == 1
]
if not widths:
print("no high pulses on that channel")
return 1

print(f"{len(widths)} high pulses on channel {channel}")
print(f" shortest {min(widths) * 1e9:.1f} ns")
print(f" longest {max(widths) * 1e9:.1f} ns")
print(f" median {statistics.median(widths) * 1e9:.1f} ns")

workspace.close()
return 0

Example output. The pulse-width distribution on a demo channel:

53 high pulses on channel 0
shortest 4200.0 ns
longest 57000.0 ns
median 11000.0 ns

The spread is wide because this is noise. On a real clock these three numbers sit close together, and the outliers are what you are looking for.

Notes for your own bench​

Divide by the rate you got back. capture.sample_rate is what the device settled on, which is not always what you asked for. Using the requested rate gives results that are consistently off by a few percent.

Skip the first edge in a width calculation if it predates your window. The first record from transitions() is the level in force where you started reading, so its sample index can be earlier than your start. Over a whole capture starting at 0 this does not matter; over a window it does.

Pick the approach by what you need. A pass/fail on "the clock is 8 MHz ± 1 %" is a measurement: read freq_avg. The same goes for limits on pulse widths or duty cycle: high_shortest, low_longest, duty_min and duty_max give the extremes directly. "Show me every pulse that was short", or any distribution, needs the raw transitions.

Measurements are annotations. They are saved into the .scana and reappear when someone opens it in ScanaStudio, which makes them a good way to leave a note about what your script found.

Anchored measurements follow their markers

Move a marker and the measurement is recomputed. Remove a marker and the measurement anchored to it goes too. To move a whole measurement, both markers at once or onto another channel, call measures.move_to(measure_id, sample_a, sample_b, channel=...).