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​
- Python
- NodeJS
- Rust
- C
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()
import { ScanaStudio } from '@ikalogic/scanastudio';
const server = await ScanaStudio.connect();
try {
const workspace = await server.create('se254');
const last = await workspace.capture.run({ samples: 500_000, sample_rate: 25_000_000 });
const tx = workspace.channel('UART TX');
// No `kinds`: the standard set.
await workspace.measures.between(0, last, { channel: tx });
// And separately, the edge count and the duty cycle.
await workspace.measures.between(0, last, { channel: tx, kinds: ['edges', 'duty_avg'] });
for (const measure of await workspace.measures.wait(60_000)) {
for (const result of measure.results) {
console.log(result.kind.padEnd(12), result.value);
}
}
// time_total 0.02
// freq_avg 51976.60
// freq_min 14637.00
// freq_max 120192.31
// edges 106
// duty_avg 0.52
workspace.close();
} finally {
server.close();
}
use std::time::Duration;
use scanastudio_client::{
CaptureRequest, MeasureKind, Precision, ScanaStudio, DEFAULT_URL, Result,
};
#[tokio::main]
async fn main() -> Result<()> {
let server = ScanaStudio::connect(DEFAULT_URL).await?;
let workspace = server.create("se254").await?;
let last = workspace.capture()
.run(&CaptureRequest::new(500_000, 25_000_000), Duration::from_secs(300))
.await?;
let tx = workspace.channel("UART TX")?;
// `false`: measure the capture, not the channel's generation row.
// No kinds: the standard set.
workspace.measures()
.between(0, last, tx, false, &[], Precision::default())
.await?;
// And separately, the edge count and the duty cycle.
workspace.measures()
.between(0, last, tx, false,
&[MeasureKind::Edges, MeasureKind::DutyAvg], Precision::default())
.await?;
for measure in workspace.measures().wait(Duration::from_secs(60)).await? {
for result in &measure.results {
println!("{:12} {}", result.kind.as_str(), result.value);
}
}
// time_total 0.02
// freq_avg 51976.60
// freq_min 14637.00
// freq_max 120192.31
// edges 106
// duty_avg 0.52
workspace.close()?;
Ok(())
}
#include <stdio.h>
#include <stdlib.h>
#include "scanastudio.h"
int main(void) {
ss_server *server = ss_server_connect(NULL);
ss_workspace *workspace = ss_server_create(server, "se254");
ss_capture_run(workspace, "{\"samples\": 500000, \"sample_rate\": 25000000}", 300.0);
int tx = ss_workspace_channel(workspace, "UART TX");
int64_t last = ss_capture_last_sample(workspace);
/* Over the whole capture, without placing markers by hand.
NULL kinds: the standard set. The two 3s are the precisions. */
ss_measure_between(workspace, 0, last, (uint16_t)tx, NULL, 3, 3);
/* And separately, the edge count and the duty cycle. */
ss_measure_between(workspace, 0, last, (uint16_t)tx,
"[\"edges\", \"duty_avg\"]", 3, 3);
/* Blocks until nothing is still computing, then reads the results. */
ss_measures *batch = ss_measures_wait(workspace, 60.0);
for (size_t i = 0; i < ss_measures_count(batch); i += 1) {
ss_measure row;
if (ss_measures_copy(batch, i, &row, 1) != 1) break;
ss_measure_value *values = malloc(sizeof *values * row.value_count);
ptrdiff_t got = ss_measure_values_copy(batch, i, 0, values, row.value_count);
for (ptrdiff_t v = 0; v < got; v += 1) {
printf("%-12s %g\n", values[v].kind, values[v].value);
}
free(values);
}
/* Every `kind` printed above belonged to the batch, so it is freed last. */
ss_measures_free(batch);
ss_workspace_close(workspace);
ss_workspace_free(workspace);
ss_server_disconnect(server);
return 0;
}
The values are unrounded SI units (seconds, hertz, counts), and duty cycles are ratios from 0 to 1. The precisions say what was asked for; formatting is the caller's job.
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:
- Python
- NodeJS
- Rust
- C
start = workspace.markers.add(sample=0)
stop = workspace.markers.add(sample=last)
workspace.measures.add(start, stop, channel=tx, kinds=["high_shortest", "low_shortest"])
const start = await workspace.markers.add(0);
const stop = await workspace.markers.add(last);
await workspace.measures.add(start, stop, {
channel: tx,
kinds: ['high_shortest', 'low_shortest'],
});
let start = workspace.markers().add(0).await?;
let stop = workspace.markers().add(last).await?;
workspace.measures()
.add(start, stop, tx, false,
&[MeasureKind::HighShortest, MeasureKind::LowShortest], Precision::default())
.await?;
uint32_t start = ss_marker_add(workspace, 0);
uint32_t stop = ss_marker_add(workspace, last);
ss_measure_add(workspace, start, stop, (uint16_t)tx,
"[\"high_shortest\", \"low_shortest\"]", 3, 3);
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.
| Kinds | What they report |
|---|---|
time_total, time_high, time_low | Time between the markers, and the total time spent high and low, in seconds |
freq_avg, freq_min, freq_max | Average frequency, and the lowest and highest frequency of a single period, in hertz |
high_avg, high_shortest, high_longest | Average, shortest and longest high pulse, in seconds |
low_avg, low_shortest, low_longest | Average, shortest and longest low pulse, in seconds |
duty_avg, duty_min, duty_max | Average, lowest and highest duty cycle, as a ratio from 0 to 1 |
edges, rising_edges, falling_edges | Edge 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.
- Python
- NodeJS
- Rust
- C
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
import { ScanaStudio } from '@ikalogic/scanastudio';
const server = await ScanaStudio.connect();
try {
const workspace = await server.create('se254');
await 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.
const rate = workspace.capture.sample_rate;
const edges = [];
for await (const edge of workspace.data.transitions(0)) edges.push(edge);
// A high pulse is the span from a rising edge to the next edge.
const widths = [];
for (let i = 0; i + 1 < edges.length; i += 1) {
if (edges[i].level === 1) {
widths.push((edges[i + 1].sample - edges[i].sample) / rate);
}
}
if (widths.length === 0) {
console.log('no high pulses on that channel');
} else {
widths.sort((a, b) => a - b);
console.log(`${widths.length} high pulses`);
console.log(` shortest ${(widths[0] * 1e9).toFixed(1)} ns`);
console.log(` longest ${(widths.at(-1) * 1e9).toFixed(1)} ns`);
console.log(` median ${(widths[widths.length >> 1] * 1e9).toFixed(1)} ns`);
}
workspace.close();
} finally {
server.close();
}
use std::time::Duration;
use scanastudio_client::{CaptureRequest, ScanaStudio, DEFAULT_URL, Result};
#[tokio::main]
async fn main() -> Result<()> {
let server = ScanaStudio::connect(DEFAULT_URL).await?;
let workspace = server.create("se254").await?;
workspace.capture()
.run(&CaptureRequest::new(500_000, 25_000_000), Duration::from_secs(300))
.await?;
// Divide by the rate the DEVICE settled on, not the one we asked for.
let rate = workspace.capture().sample_rate() as f64;
let edges = workspace.data().transitions(0, 0, None).await?;
// A high pulse is the span from a rising edge to the next edge.
let mut widths: Vec<f64> = edges.windows(2)
.filter(|pair| pair[0].level == 1)
.map(|pair| (pair[1].sample - pair[0].sample) as f64 / rate)
.collect();
if widths.is_empty() {
println!("no high pulses on that channel");
} else {
widths.sort_by(f64::total_cmp);
println!("{} high pulses", widths.len());
println!(" shortest {:.1} ns", widths[0] * 1e9);
println!(" longest {:.1} ns", widths[widths.len() - 1] * 1e9);
println!(" median {:.1} ns", widths[widths.len() / 2] * 1e9);
}
workspace.close()?;
Ok(())
}
#include <stdio.h>
#include <stdlib.h>
#include "scanastudio.h"
int main(void) {
ss_server *server = ss_server_connect(NULL);
ss_workspace *workspace = ss_server_create(server, "se254");
ss_capture_run(workspace, "{\"samples\": 500000, \"sample_rate\": 25000000}", 300.0);
/* Divide by the rate the DEVICE settled on, not the one we asked for. */
double rate = (double)ss_capture_sample_rate(workspace);
enum { ROOM = 65536 };
ss_edge *edges = malloc(sizeof(ss_edge) * ROOM);
ptrdiff_t n = ss_data_transitions(workspace, 0, 0, -1, edges, ROOM);
if (n < 0) { fprintf(stderr, "%s\n", ss_last_error()); return 1; }
/* A high pulse is the span from a rising edge to the next edge. */
double shortest = 1e9, longest = 0.0;
long pulses = 0;
for (ptrdiff_t i = 0; i + 1 < n; i += 1) {
if (edges[i].level != 1) continue;
double width = (double)(edges[i + 1].sample - edges[i].sample) / rate;
if (width < shortest) shortest = width;
if (width > longest) longest = width;
pulses += 1;
}
if (pulses == 0) printf("no high pulses on that channel\n");
else printf("%ld high pulses: %.1f ns to %.1f ns\n",
pulses, shortest * 1e9, longest * 1e9);
free(edges);
ss_workspace_close(workspace);
ss_workspace_free(workspace);
ss_server_disconnect(server);
return 0;
}
ss_data_transitions truncates rather than failing when the window holds more
edges than capacity. For a full capture, loop: read a window, then ask again
from the last sample you got.
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.
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=...).