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 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)
clock = workspace.channel("CLK")
# Over the whole capture, without placing markers by hand.
# No `kinds`: the standard timing set.
workspace.measures.between(0, last, channel=clock)
# And separately, the edge count.
workspace.measures.between(0, last, channel=clock, kinds=["edges"])
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 51480.17
# freq_min 19638.65
# freq_max 107758.62
# edges 101.0
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 clock = workspace.channel('CLK');
// No `kinds`: the standard timing set.
await workspace.measures.between(0, last, { channel: clock });
// And separately, the edge count.
await workspace.measures.between(0, last, { channel: clock, kinds: ['edges'] });
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 51480.17
// freq_min 19638.65
// freq_max 107758.62
// edges 101.0
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?;
let last = workspace.capture()
.run(&CaptureRequest::new(500_000, 25_000_000), Duration::from_secs(300))
.await?;
let clock = workspace.channel("CLK")?;
// No kinds: the standard timing set.
workspace.measures().between(0, last, clock, &[]).await?;
// And separately, the edge count.
workspace.measures().between(0, last, clock, &["edges"]).await?;
for measure in workspace.measures().wait(Duration::from_secs(60)).await? {
for result in &measure.results {
println!("{:12} {}", result.kind, result.value);
}
}
// time_total 0.02
// freq_avg 51480.17
// freq_min 19638.65
// freq_max 107758.62
// edges 101.0
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 clock = ss_workspace_channel(workspace, "CLK");
int64_t last = ss_capture_last_sample(workspace);
/* Over the whole capture, without placing markers by hand.
NULL kinds: the standard timing set. The two 3s are the precisions. */
ss_measure_between(workspace, 0, last, (uint16_t)clock, NULL, 3, 3);
/* And separately, the edge count. */
ss_measure_between(workspace, 0, last, (uint16_t)clock, "[\"edges\"]", 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). 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=clock, kinds=["time", "frequency"])
const start = await workspace.markers.add(0);
const stop = await workspace.markers.add(last);
await workspace.measures.add(start, stop, { channel: clock, kinds: ['time', 'frequency'] });
let start = workspace.markers().add(0).await?;
let stop = workspace.markers().add(last).await?;
workspace.measures().add(start, stop, clock, &["time", "frequency"]).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)clock,
"[\"time\", \"frequency\"]", 3, 3);
edges, or ask for nothingThe server currently recognises two cases. A kinds list containing edges
returns the edge count and nothing else. Any other list, or none, returns the
standard timing set: time_total, freq_avg, freq_min, freq_max. The other
kind names are accepted but do not change the result, and the kind names in a
result are not the ones you asked for. Read result.kind rather than assuming.
See
Markers and measurements.
Example output. The two measurements above, over a 500 000-sample window:
time_total 0.02
freq_avg 51480.173999
freq_min 19638.648861
freq_max 107758.62069
edges 101.0
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:
44 high pulses on channel 0
shortest 4320.0 ns
longest 63880.0 ns
median 12240.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. "Show me every pulse that was short",
or anything needing a duty cycle or a distribution, needs the raw transitions.
The server does not compute those today.
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.